import React, { useEffect, useState } from 'react'; import clsx from 'clsx'; import { fetchSessions, createSession, deleteSession, updateSession, fetchFolders, createFolder, deleteFolder, updateFolder, type ChatSession, type ChatFolder, } from '@/api/ai'; interface SessionListProps { activeSessionId: string | null; onSelectSession: (id: string) => void; isSidebar?: boolean; className?: string; } interface TreeNode { folder: ChatFolder | null; sessions: ChatSession[]; children: 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) { 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); } } for (const s of sessions) { 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: TreeNode; activeSessionId: string | null; onSelectSession: (id: string) => void; depth: number; onMoveSession: (sessionId: string, folderId: string | null) => void; onDeleteFolder: (folderId: string) => void; onAddFolder: (parentId: string | null) => void; }) { const [expanded, setExpanded] = useState(true); const isRoot = node.folder === null; return (
{/* Folder header */} {!isRoot && (
setExpanded(!expanded)} > {node.folder!.name}
)} {/* Sessions in this folder */} {(isRoot || expanded) && (
{node.sessions.map((session) => (
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', activeSessionId === session.id && 'bg-primary-100 text-primary-700' )} style={{ paddingLeft: depth * 12 + 24 }} > {session.title}
))}
)} {/* Child folders */} {(isRoot || expanded) && node.children.map((child) => ( ))}
); } export function SessionList({ activeSessionId, onSelectSession, isSidebar, className }: SessionListProps) { const [sessions, setSessions] = useState([]); const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = 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); } }; useEffect(() => { loadData(); }, [isSidebar]); 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'); } }; const handleAddFolder = (parentId: string | null) => { const name = prompt('Ordnername:'); if (!name) return; createFolder({ name, parent_id: parentId || undefined }) .then(() => loadData()) .catch((e) => setError(e?.message)); }; const handleDeleteFolder = (folderId: string) => { if (!confirm('Ordner löschen? Chats bleiben erhalten.')) return; deleteFolder(folderId).then(() => loadData()).catch((e) => setError(e?.message)); }; const handleMoveSession = async (sessionId: string, target: string) => { const folderId = target === 'root' ? null : target; try { await updateSession(sessionId, { folder_id: folderId }); loadData(); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to move'); } }; if (loading) return
Laden...
; const tree = buildTree(folders, sessions); return (
{error &&
{error}
}
{tree.map((node) => ( ))}
); }