feat: add Proaktiv and KI Chat tabs to AI sidebar
This commit is contained in:
@@ -7,14 +7,6 @@ interface SuggestionSidebarProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SuggestionSidebar({ isOpen, onClose }: SuggestionSidebarProps) {
|
|
||||||
const { suggestions, connected, dismiss, act } = useSuggestions();
|
|
||||||
const [filter, setFilter] = useState<string>('all');
|
|
||||||
|
|
||||||
const filteredSuggestions = filter === 'all'
|
|
||||||
? suggestions
|
|
||||||
: suggestions.filter(s => s.suggestion_type === filter);
|
|
||||||
|
|
||||||
const filterOptions = [
|
const filterOptions = [
|
||||||
{ value: 'all', label: 'Alle' },
|
{ value: 'all', label: 'Alle' },
|
||||||
{ value: 'info', label: 'Info' },
|
{ value: 'info', label: 'Info' },
|
||||||
@@ -23,6 +15,80 @@ export function SuggestionSidebar({ isOpen, onClose }: SuggestionSidebarProps) {
|
|||||||
{ value: 'insight', label: 'Erkenntnis' },
|
{ value: 'insight', label: 'Erkenntnis' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline suggestion list — no overlay/aside wrapper.
|
||||||
|
* Used inside the AISidebar proactive tab.
|
||||||
|
*/
|
||||||
|
export function SuggestionList() {
|
||||||
|
const { suggestions, connected, dismiss, act } = useSuggestions();
|
||||||
|
const [filter, setFilter] = useState<string>('all');
|
||||||
|
|
||||||
|
const filteredSuggestions = filter === 'all'
|
||||||
|
? suggestions
|
||||||
|
: suggestions.filter(s => s.suggestion_type === filter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full" data-testid="suggestion-list">
|
||||||
|
{/* Live indicator */}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-secondary-100">
|
||||||
|
{connected && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-green-600">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-secondary-400 ml-auto">
|
||||||
|
{suggestions.length} Vorschläge
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div className="flex gap-1 px-3 py-2 border-b border-secondary-100 overflow-x-auto">
|
||||||
|
{filterOptions.map(opt => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
onClick={() => setFilter(opt.value)}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
|
filter === opt.value
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3">
|
||||||
|
{filteredSuggestions.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-secondary-400">
|
||||||
|
<div className="text-4xl mb-3">🤖</div>
|
||||||
|
<p className="text-sm">Keine Vorschläge vorhanden</p>
|
||||||
|
<p className="text-xs mt-1">Die KI analysiert deinen Kontext...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredSuggestions.map(suggestion => (
|
||||||
|
<SuggestionCard
|
||||||
|
key={suggestion.id}
|
||||||
|
suggestion={suggestion}
|
||||||
|
onDismiss={dismiss}
|
||||||
|
onAct={act}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy overlay-based SuggestionSidebar — kept for backwards compat.
|
||||||
|
* Uses SuggestionList internally.
|
||||||
|
*/
|
||||||
|
export function SuggestionSidebar({ isOpen, onClose }: SuggestionSidebarProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
@@ -43,12 +109,6 @@ export function SuggestionSidebar({ isOpen, onClose }: SuggestionSidebarProps) {
|
|||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="font-semibold text-gray-800">KI Vorschläge</h2>
|
<h2 className="font-semibold text-gray-800">KI Vorschläge</h2>
|
||||||
{connected && (
|
|
||||||
<span className="flex items-center gap-1 text-xs text-green-600">
|
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
||||||
Live
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -58,51 +118,7 @@ export function SuggestionSidebar({ isOpen, onClose }: SuggestionSidebarProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter */}
|
<SuggestionList />
|
||||||
<div className="flex gap-1 p-3 border-b border-gray-100 overflow-x-auto">
|
|
||||||
{filterOptions.map(opt => (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
onClick={() => setFilter(opt.value)}
|
|
||||||
className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-colors ${
|
|
||||||
filter === opt.value
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
|
||||||
{filteredSuggestions.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
|
||||||
<div className="text-4xl mb-3">🤖</div>
|
|
||||||
<p className="text-sm">Keine Vorschläge vorhanden</p>
|
|
||||||
<p className="text-xs mt-1">Die KI analysiert deinen Kontext...</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
{filteredSuggestions.map(suggestion => (
|
|
||||||
<SuggestionCard
|
|
||||||
key={suggestion.id}
|
|
||||||
suggestion={suggestion}
|
|
||||||
onDismiss={dismiss}
|
|
||||||
onAct={act}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="p-3 border-t border-gray-200 text-center">
|
|
||||||
<span className="text-xs text-gray-400">
|
|
||||||
{suggestions.length} Vorschläge insgesamt
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ChatWindow } from '@/components/ai/ChatWindow';
|
import { ChatWindow } from '@/components/ai/ChatWindow';
|
||||||
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||||
|
import { SuggestionList } from '@/components/ai/SuggestionSidebar';
|
||||||
import { createSession, fetchSessions } from '@/api/ai';
|
import { createSession, fetchSessions } from '@/api/ai';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
|
|
||||||
export function AISidebar() {
|
export function AISidebar() {
|
||||||
const { aiSidebarCollapsed, toggleAISidebar } = useUIStore();
|
const { aiSidebarCollapsed, toggleAISidebar, aiSidebarTab, setAISidebarTab } = useUIStore();
|
||||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -48,6 +49,65 @@ export function AISidebar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTabContent = () => {
|
||||||
|
if (aiSidebarTab === 'proactive') {
|
||||||
|
return <SuggestionList />;
|
||||||
|
}
|
||||||
|
// chat tab
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-secondary-400">Laden...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
return <ChatWindow sessionId={sessionId} compact className="h-full" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-red-500">
|
||||||
|
Session konnte nicht erstellt werden
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabBar = (
|
||||||
|
<div className="flex border-b border-secondary-200" role="tablist">
|
||||||
|
<button
|
||||||
|
onClick={() => setAISidebarTab('proactive')}
|
||||||
|
className={`px-3 py-2 text-sm font-medium flex items-center gap-1.5 border-b-2 transition-colors ${
|
||||||
|
aiSidebarTab === 'proactive'
|
||||||
|
? 'text-primary-600 border-primary-500'
|
||||||
|
: 'text-secondary-500 hover:text-secondary-700 border-transparent'
|
||||||
|
}`}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={aiSidebarTab === 'proactive'}
|
||||||
|
aria-label="Proaktiv"
|
||||||
|
data-testid="ai-sidebar-tab-proactive"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
Proaktiv
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setAISidebarTab('chat')}
|
||||||
|
className={`px-3 py-2 text-sm font-medium flex items-center gap-1.5 border-b-2 transition-colors ${
|
||||||
|
aiSidebarTab === 'chat'
|
||||||
|
? 'text-primary-600 border-primary-500'
|
||||||
|
: 'text-secondary-500 hover:text-secondary-700 border-transparent'
|
||||||
|
}`}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={aiSidebarTab === 'chat'}
|
||||||
|
aria-label="KI Chat"
|
||||||
|
data-testid="ai-sidebar-tab-chat"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||||
|
</svg>
|
||||||
|
KI Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// Expanded: Mobile = full width overlay, Desktop = resizable panel
|
// Expanded: Mobile = full width overlay, Desktop = resizable panel
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -70,16 +130,9 @@ export function AISidebar() {
|
|||||||
</button>
|
</button>
|
||||||
<span className="text-sm font-medium text-secondary-700 ml-2">KI Assistent</span>
|
<span className="text-sm font-medium text-secondary-700 ml-2">KI Assistent</span>
|
||||||
</div>
|
</div>
|
||||||
|
{tabBar}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{loading ? (
|
{renderTabContent()}
|
||||||
<div className="flex items-center justify-center h-full text-sm text-secondary-400">Laden...</div>
|
|
||||||
) : sessionId ? (
|
|
||||||
<ChatWindow sessionId={sessionId} compact className="h-full" />
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-full text-sm text-red-500">
|
|
||||||
Session konnte nicht erstellt werden
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,16 +158,9 @@ export function AISidebar() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{tabBar}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{loading ? (
|
{renderTabContent()}
|
||||||
<div className="flex items-center justify-center h-full text-sm text-secondary-400">Laden...</div>
|
|
||||||
) : sessionId ? (
|
|
||||||
<ChatWindow sessionId={sessionId} compact className="h-full" />
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-full text-sm text-red-500">
|
|
||||||
Session konnte nicht erstellt werden
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import { Sidebar } from './Sidebar';
|
|||||||
import { TopBar } from './TopBar';
|
import { TopBar } from './TopBar';
|
||||||
import { PluginToolbar } from './PluginToolbar';
|
import { PluginToolbar } from './PluginToolbar';
|
||||||
import { AISidebar } from './AISidebar';
|
import { AISidebar } from './AISidebar';
|
||||||
import { SuggestionSidebar } from '@/components/ai/SuggestionSidebar';
|
|
||||||
import { ToastContainer } from '@/components/ui/Toast';
|
import { ToastContainer } from '@/components/ui/Toast';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
|
||||||
import { useAIContext } from '@/hooks/useAIContext';
|
import { useAIContext } from '@/hooks/useAIContext';
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { suggestionSidebarOpen, toggleSuggestionSidebar } = useUIStore();
|
|
||||||
|
|
||||||
// Track context for AI Proactive suggestions
|
// Track context for AI Proactive suggestions
|
||||||
useAIContext();
|
useAIContext();
|
||||||
@@ -39,11 +36,6 @@ export function AppShell() {
|
|||||||
</div>
|
</div>
|
||||||
{/* AI Sidebar — full height, right of TopBar and Toolbar */}
|
{/* AI Sidebar — full height, right of TopBar and Toolbar */}
|
||||||
{showAISidebar && <AISidebar />}
|
{showAISidebar && <AISidebar />}
|
||||||
{/* Suggestion Sidebar — overlays from right */}
|
|
||||||
<SuggestionSidebar
|
|
||||||
isOpen={suggestionSidebarOpen}
|
|
||||||
onClose={() => toggleSuggestionSidebar()}
|
|
||||||
/>
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function TopBar() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const { toggleSidebar, toggleAISidebar, toggleSuggestionSidebar, aiSidebarCollapsed } = useUIStore();
|
const { toggleSidebar, toggleAISidebar, openAISidebarProactive, aiSidebarCollapsed } = useUIStore();
|
||||||
const { currentTenant, availableTenants, switchTenant, isSwitching } = useTenant();
|
const { currentTenant, availableTenants, switchTenant, isSwitching } = useTenant();
|
||||||
const logoutMutation = useLogout();
|
const logoutMutation = useLogout();
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ export function TopBar() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* AI Suggestions Badge */}
|
{/* AI Suggestions Badge */}
|
||||||
<SuggestionBadge onClick={toggleSuggestionSidebar} />
|
<SuggestionBadge onClick={openAISidebarProactive} />
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
<div ref={notifRef} className="relative">
|
<div ref={notifRef} className="relative">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
export type Theme = 'light' | 'dark' | 'system';
|
export type Theme = 'light' | 'dark' | 'system';
|
||||||
export type Locale = 'de' | 'en';
|
export type Locale = 'de' | 'en';
|
||||||
|
export type AISidebarTab = 'proactive' | 'chat';
|
||||||
|
|
||||||
export interface Toast {
|
export interface Toast {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -16,6 +17,7 @@ export interface UIState {
|
|||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
suggestionSidebarOpen: boolean;
|
suggestionSidebarOpen: boolean;
|
||||||
aiSidebarCollapsed: boolean;
|
aiSidebarCollapsed: boolean;
|
||||||
|
aiSidebarTab: AISidebarTab;
|
||||||
toasts: Toast[];
|
toasts: Toast[];
|
||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
setLocale: (locale: Locale) => void;
|
setLocale: (locale: Locale) => void;
|
||||||
@@ -24,6 +26,8 @@ export interface UIState {
|
|||||||
toggleSuggestionSidebar: () => void;
|
toggleSuggestionSidebar: () => void;
|
||||||
toggleAISidebar: () => void;
|
toggleAISidebar: () => void;
|
||||||
setAISidebarCollapsed: (collapsed: boolean) => void;
|
setAISidebarCollapsed: (collapsed: boolean) => void;
|
||||||
|
setAISidebarTab: (tab: AISidebarTab) => void;
|
||||||
|
openAISidebarProactive: () => void;
|
||||||
addToast: (toast: Omit<Toast, 'id'>) => void;
|
addToast: (toast: Omit<Toast, 'id'>) => void;
|
||||||
removeToast: (id: string) => void;
|
removeToast: (id: string) => void;
|
||||||
clearToasts: () => void;
|
clearToasts: () => void;
|
||||||
@@ -37,6 +41,7 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
suggestionSidebarOpen: false,
|
suggestionSidebarOpen: false,
|
||||||
aiSidebarCollapsed: true,
|
aiSidebarCollapsed: true,
|
||||||
|
aiSidebarTab: 'chat',
|
||||||
toasts: [],
|
toasts: [],
|
||||||
setTheme: (theme) => {
|
setTheme: (theme) => {
|
||||||
localStorage.setItem('leocrm_theme', theme);
|
localStorage.setItem('leocrm_theme', theme);
|
||||||
@@ -51,6 +56,8 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
toggleSuggestionSidebar: () => set((s) => ({ suggestionSidebarOpen: !s.suggestionSidebarOpen })),
|
toggleSuggestionSidebar: () => set((s) => ({ suggestionSidebarOpen: !s.suggestionSidebarOpen })),
|
||||||
toggleAISidebar: () => set((s) => ({ aiSidebarCollapsed: !s.aiSidebarCollapsed })),
|
toggleAISidebar: () => set((s) => ({ aiSidebarCollapsed: !s.aiSidebarCollapsed })),
|
||||||
setAISidebarCollapsed: (collapsed) => set({ aiSidebarCollapsed: collapsed }),
|
setAISidebarCollapsed: (collapsed) => set({ aiSidebarCollapsed: collapsed }),
|
||||||
|
setAISidebarTab: (tab) => set({ aiSidebarTab: tab }),
|
||||||
|
openAISidebarProactive: () => set({ aiSidebarCollapsed: false, aiSidebarTab: 'proactive' }),
|
||||||
addToast: (toast) => {
|
addToast: (toast) => {
|
||||||
const id = `toast-${++toastIdCounter}`;
|
const id = `toast-${++toastIdCounter}`;
|
||||||
set((s) => ({ toasts: [...s.toasts, { ...toast, id }] }));
|
set((s) => ({ toasts: [...s.toasts, { ...toast, id }] }));
|
||||||
|
|||||||
Reference in New Issue
Block a user