From f2d624720fab52d2e2e68c2937791fe7fb200001 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 17 Jul 2026 17:09:26 +0200 Subject: [PATCH] feat: drag&drop treeview, flat chat UI, activity indicator, fixed toolbar registration --- frontend/src/components/ai/ChatWindow.tsx | 52 +++++--- frontend/src/components/ai/SessionList.tsx | 133 +++++++-------------- frontend/src/pages/AIAssistant.tsx | 26 +++- 3 files changed, 105 insertions(+), 106 deletions(-) diff --git a/frontend/src/components/ai/ChatWindow.tsx b/frontend/src/components/ai/ChatWindow.tsx index e7516b3..ae822bd 100644 --- a/frontend/src/components/ai/ChatWindow.tsx +++ b/frontend/src/components/ai/ChatWindow.tsx @@ -25,19 +25,33 @@ function MessageContent({ content, role }: { content: string; role: string }) { return
{content}
; } return ( -
+
{content}
); } +function ActivityIndicator({ status }: { status: string | null }) { + if (!status) return null; + return ( +
+
+ + + +
+ {status} +
+ ); +} + export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindowProps) { const [messages, setMessages] = useState([]); const [attachments, setAttachments] = useState([]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [streamingContent, setStreamingContent] = useState(''); - const [toolStatus, setToolStatus] = useState(null); + const [activity, setActivity] = useState(null); const [error, setError] = useState(null); const [uploadingFile, setUploadingFile] = useState(false); const scrollRef = useRef(null); @@ -55,7 +69,7 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - }, [messages, streamingContent]); + }, [messages, streamingContent, activity]); const handleFileSelect = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); @@ -80,7 +94,7 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo setInput(''); setIsStreaming(true); setStreamingContent(''); - setToolStatus(null); + setActivity('Denke nach...'); setError(null); const userMsg: ChatMessage = { id: 'temp-' + Date.now(), session_id: sessionId, role: 'user', content, tokens: 0, model_used: '' }; @@ -93,17 +107,19 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo if (event.type === 'token' && event.content) { accumulated += event.content; setStreamingContent(accumulated); + setActivity(null); } else if (event.type === 'tool_calls' && event.tools) { - setToolStatus(`Tools: ${event.tools.join(', ')}`); + setActivity(`Rufe Tool auf: ${event.tools.join(', ')}`); } else if (event.type === 'tool_result' && event.tool) { - setToolStatus(`Tool '${event.tool}' ausgefuehrt`); + setActivity(`Tool '${event.tool}' ausgeführt`); } else if (event.type === 'done') { - setToolStatus(null); + setActivity(null); const msgs = await fetchMessages(sessionId); setMessages(msgs); setStreamingContent(''); } else if (event.type === 'error') { setError(event.content || 'Unknown error'); + setActivity(null); } } } catch (e) { @@ -111,7 +127,7 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo } finally { setIsStreaming(false); setStreamingContent(''); - setToolStatus(null); + setActivity(null); inputRef.current?.focus(); } }; @@ -140,24 +156,30 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo
Starte eine Konversation...
)} {messages.map((msg) => ( -
-
+
+
+ {msg.role === 'user' ? 'Du' : 'KI'} +
+
{msg.tool_calls && msg.tool_calls.length > 0 && ( -
Tools: {msg.tool_calls.map((tc: any) => tc.function?.name).join(', ')}
+
+ ⚙️ Tools: {msg.tool_calls.map((tc: any) => tc.function?.name).join(', ')} +
)}
))} {streamingContent && ( -
-
+
+
KI
+
)} - {toolStatus &&
⚙️ {toolStatus}
} - {error &&
{error}
} + + {error &&
{error}
}
diff --git a/frontend/src/components/ai/SessionList.tsx b/frontend/src/components/ai/SessionList.tsx index 25de807..211cab4 100644 --- a/frontend/src/components/ai/SessionList.tsx +++ b/frontend/src/components/ai/SessionList.tsx @@ -22,32 +22,21 @@ interface TreeNode { function buildTree(folders: ChatFolder[], sessions: ChatSession[]): TreeNode[] { const rootSessions = sessions.filter((s) => !s.folder_id); const root: TreeNode = { folder: null, sessions: rootSessions, children: [] }; - const folderMap = new Map(); - for (const f of folders) { - folderMap.set(f.id, { folder: f, sessions: [], children: [] }); - } - + for (const f of folders) folderMap.set(f.id, { folder: f, sessions: [], children: [] }); for (const f of folders) { const node = folderMap.get(f.id)!; - if (f.parent_id && folderMap.has(f.parent_id)) { - folderMap.get(f.parent_id)!.children.push(node); - } else { - root.children.push(node); - } + if (f.parent_id && folderMap.has(f.parent_id)) folderMap.get(f.parent_id)!.children.push(node); + else root.children.push(node); } - for (const s of sessions) { - if (s.folder_id && folderMap.has(s.folder_id)) { - folderMap.get(s.folder_id)!.sessions.push(s); - } + if (s.folder_id && folderMap.has(s.folder_id)) folderMap.get(s.folder_id)!.sessions.push(s); } - return [root]; } function TreeItem({ - node, activeSessionId, onSelectSession, depth, onMoveSession, onDeleteFolder, onAddFolder, + node, activeSessionId, onSelectSession, depth, onMoveSession, onDeleteFolder, onAddFolder, onDragSession, onDropToFolder, }: { node: TreeNode; activeSessionId: string | null; @@ -56,16 +45,25 @@ function TreeItem({ onMoveSession: (sessionId: string, folderId: string | null) => void; onDeleteFolder: (folderId: string) => void; onAddFolder: (parentId: string | null) => void; + onDragSession: (sessionId: string) => void; + onDropToFolder: (folderId: string | null) => void; }) { const [expanded, setExpanded] = useState(true); + const [isDragOver, setIsDragOver] = useState(false); const isRoot = node.folder === null; return ( -
- {/* Folder header */} +
{ e.preventDefault(); setIsDragOver(true); }} + onDragLeave={() => setIsDragOver(false)} + onDrop={(e) => { e.preventDefault(); setIsDragOver(false); onDropToFolder(node.folder?.id || null); }} + > {!isRoot && (
setExpanded(!expanded)} > @@ -76,29 +74,21 @@ function TreeItem({ {node.folder!.name} - - + +
)} - - {/* Sessions in this folder */} {(isRoot || expanded) && (
{node.sessions.map((session) => (
onDragSession(session.id)} onClick={() => onSelectSession(session.id)} className={clsx( 'group flex items-center gap-2 px-3 py-1.5 text-sm cursor-pointer rounded-md', - 'hover:bg-secondary-100', + 'hover:bg-secondary-100 active:cursor-grabbing', activeSessionId === session.id && 'bg-primary-100 text-primary-700' )} style={{ paddingLeft: depth * 12 + 24 }} @@ -107,26 +97,11 @@ function TreeItem({ {session.title} - - +
))}
)} - - {/* Child folders */} {(isRoot || expanded) && node.children.map((child) => ( ))}
@@ -148,22 +125,15 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [dragSessionId, setDragSessionId] = useState(null); const loadData = async () => { try { setLoading(true); - const [s, f] = await Promise.all([ - fetchSessions(isSidebar ? true : undefined), - fetchFolders(), - ]); - setSessions(s); - setFolders(f); - setError(null); - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to load'); - } finally { - setLoading(false); - } + const [s, f] = await Promise.all([fetchSessions(isSidebar ? true : undefined), fetchFolders()]); + setSessions(s); setFolders(f); setError(null); + } catch (e) { setError(e instanceof Error ? e.message : 'Failed to load'); } + finally { setLoading(false); } }; useEffect(() => { loadData(); }, [isSidebar]); @@ -171,19 +141,14 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class const handleNewChat = async () => { try { const session = await createSession({ is_sidebar: isSidebar || false }); - setSessions((prev) => [session, ...prev]); - onSelectSession(session.id); - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to create session'); - } + setSessions((prev) => [session, ...prev]); onSelectSession(session.id); + } catch (e) { setError(e instanceof Error ? e.message : 'Failed'); } }; const handleAddFolder = (parentId: string | null) => { - const name = prompt('Ordnername:'); + const name = prompt('Ordnername:');; if (!name) return; - createFolder({ name, parent_id: parentId || undefined }) - .then(() => loadData()) - .catch((e) => setError(e?.message)); + createFolder({ name, parent_id: parentId || undefined }).then(() => loadData()).catch((e) => setError(e?.message)); }; const handleDeleteFolder = (folderId: string) => { @@ -191,14 +156,13 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class deleteFolder(folderId).then(() => loadData()).catch((e) => setError(e?.message)); }; - const handleMoveSession = async (sessionId: string, target: string) => { - const folderId = target === 'root' ? null : target; + const handleDropToFolder = async (folderId: string | null) => { + if (!dragSessionId) return; try { - await updateSession(sessionId, { folder_id: folderId }); + await updateSession(dragSessionId, { folder_id: folderId }); + setDragSessionId(null); loadData(); - } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to move'); - } + } catch (e) { setError(e instanceof Error ? e.message : 'Failed to move'); } }; if (loading) return
Laden...
; @@ -208,19 +172,8 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class return (
- - + +
{error &&
{error}
}
@@ -231,9 +184,11 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class activeSessionId={activeSessionId} onSelectSession={onSelectSession} depth={0} - onMoveSession={handleMoveSession} + onMoveSession={() => {}} onDeleteFolder={handleDeleteFolder} onAddFolder={handleAddFolder} + onDragSession={setDragSessionId} + onDropToFolder={handleDropToFolder} /> ))}
diff --git a/frontend/src/pages/AIAssistant.tsx b/frontend/src/pages/AIAssistant.tsx index 890d92e..7b21105 100644 --- a/frontend/src/pages/AIAssistant.tsx +++ b/frontend/src/pages/AIAssistant.tsx @@ -15,8 +15,30 @@ export function AIAssistantPage() { useEffect(() => { registerItems('ai_assistant', [ - { id: 'agent-select', label: 'Agent', type: 'select', options: agents.map((a) => ({ value: a.id, label: a.name })), value: selectedAgentId || '', onChange: (val: string) => setSelectedAgentId(val || null) }, - { id: 'new-chat', label: 'Neuer Chat', type: 'button', onClick: () => { setActiveSessionId(null); setMobileView('list'); } }, + { + id: 'agent-select', + plugin: 'ai_assistant', + label: 'Agent', + type: 'select', + group: 'agent', + selectOptions: agents.map((a) => ({ value: a.id, label: a.name })), + selectValue: selectedAgentId || '', + onSelect: (val: string) => setSelectedAgentId(val || null), + onClick: () => {}, + }, + { + id: 'new-chat', + plugin: 'ai_assistant', + label: 'Neuer Chat', + type: 'button', + group: 'actions', + icon: ( + + + + ), + onClick: () => { setActiveSessionId(null); setMobileView('list'); }, + }, ]); return () => unregisterPlugin('ai_assistant'); }, [agents, selectedAgentId, registerItems, unregisterPlugin]);