diff --git a/frontend/src/components/ai/SessionList.tsx b/frontend/src/components/ai/SessionList.tsx
index abc8652..63aa25b 100644
--- a/frontend/src/components/ai/SessionList.tsx
+++ b/frontend/src/components/ai/SessionList.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef, useCallback } from 'react';
import clsx from 'clsx';
import {
fetchSessions, createSession, deleteSession, updateSession,
@@ -19,6 +19,13 @@ interface TreeNode {
children: TreeNode[];
}
+interface ContextMenuState {
+ x: number;
+ y: number;
+ type: 'session' | 'folder' | 'root';
+ id: string | null;
+}
+
function buildTree(folders: ChatFolder[], sessions: ChatSession[]): TreeNode[] {
const rootSessions = sessions.filter((s) => !s.folder_id);
const root: TreeNode = { folder: null, sessions: rootSessions, children: [] };
@@ -36,14 +43,13 @@ function buildTree(folders: ChatFolder[], sessions: ChatSession[]): TreeNode[] {
}
function TreeItem({
- node, activeSessionId, onSelectSession, depth, onDeleteFolder, onAddFolder, onDragSession, onDropToFolder,
+ node, activeSessionId, onSelectSession, depth, onContextMenu, onDragSession, onDropToFolder,
}: {
node: TreeNode;
activeSessionId: string | null;
onSelectSession: (id: string) => void;
depth: number;
- onDeleteFolder: (folderId: string) => void;
- onAddFolder: (parentId: string | null) => void;
+ onContextMenu: (e: React.MouseEvent, type: 'session' | 'folder' | 'root', id: string | null) => void;
onDragSession: (sessionId: string) => void;
onDropToFolder: (folderId: string | null) => void;
}) {
@@ -52,7 +58,6 @@ function TreeItem({
const isRoot = node.folder === null;
const folderId = node.folder?.id || null;
- // Drop handler for this specific folder - stopPropagation so parent doesn't also fire
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -63,23 +68,18 @@ function TreeItem({
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
- setIsDragOver(true);
- };
-
- const handleDragLeave = (e: React.DragEvent) => {
- e.stopPropagation();
- // Only clear if leaving to a non-child element
- setIsDragOver(false);
+ if (!isRoot) setIsDragOver(true);
};
return (
- {/* Folder header - this is the drop target */}
+ {/* Folder header - drop target */}
{!isRoot && (
setIsDragOver(false)}
onDrop={handleDrop}
+ onContextMenu={(e) => onContextMenu(e, 'folder', node.folder!.id)}
className={clsx(
'group flex items-center gap-1 px-2 py-1.5 text-sm font-medium rounded-md cursor-pointer',
isDragOver ? 'bg-primary-100 ring-2 ring-primary-400' : 'text-secondary-700 hover:bg-secondary-100'
@@ -94,46 +94,51 @@ function TreeItem({
{node.folder!.name}
-
-
)}
- {/* Sessions in this folder */}
+ {/* Sessions */}
{(isRoot || expanded) && (
{node.sessions.map((session) => (
{ e.stopPropagation(); onDragSession(session.id); }}
+ onDragStart={(e) => {
+ e.dataTransfer.setData('text/plain', session.id);
+ e.dataTransfer.effectAllowed = 'move';
+ onDragSession(session.id);
+ }}
+ onContextMenu={(e) => onContextMenu(e, 'session', 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',
- activeSessionId === session.id && 'bg-primary-100 text-primary-700'
+ activeSessionId === session.id && 'bg-primary-50'
)}
style={{ paddingLeft: depth * 12 + 24 }}
>
-
{session.title}
-
+
+ {session.title}
+
))}
)}
- {/* Root drop zone - only for root level, separate from sessions */}
+ {/* Root drop zone */}
{isRoot && (
setIsDragOver(false)}
onDrop={handleDrop}
+ onContextMenu={(e) => onContextMenu(e, 'root', null)}
className={clsx(
- 'min-h-[8px] rounded-md transition-colors',
- isDragOver && 'bg-primary-50 ring-1 ring-primary-300'
+ 'min-h-[12px] mt-1 rounded-md transition-colors',
+ isDragOver ? 'bg-primary-50 ring-1 ring-primary-300' : 'hover:bg-secondary-50'
)}
/>
)}
@@ -146,8 +151,7 @@ function TreeItem({
activeSessionId={activeSessionId}
onSelectSession={onSelectSession}
depth={depth + 1}
- onDeleteFolder={onDeleteFolder}
- onAddFolder={onAddFolder}
+ onContextMenu={onContextMenu}
onDragSession={onDragSession}
onDropToFolder={onDropToFolder}
/>
@@ -156,12 +160,69 @@ function TreeItem({
);
}
+function ContextMenu({
+ state, onClose, onRename, onDelete, onNewFolder, onNewChat,
+}: {
+ state: ContextMenuState;
+ onClose: () => void;
+ onRename: (id: string, type: 'session' | 'folder') => void;
+ onDelete: (id: string, type: 'session' | 'folder') => void;
+ onNewFolder: () => void;
+ onNewChat: () => void;
+}) {
+ const ref = useRef
(null);
+
+ useEffect(() => {
+ const handler = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, [onClose]);
+
+ const items: { label: string; action: () => void; danger?: boolean }[] = [];
+
+ if (state.type === 'session') {
+ items.push({ label: 'Umbenennen', action: () => { onRename(state.id!, 'session'); onClose(); } });
+ items.push({ label: 'Löschen', action: () => { onDelete(state.id!, 'session'); onClose(); }, danger: true });
+ } else if (state.type === 'folder') {
+ items.push({ label: 'Umbenennen', action: () => { onRename(state.id!, 'folder'); onClose(); } });
+ items.push({ label: 'Neuer Unterordner', action: () => { onClose(); /* TODO */ } });
+ items.push({ label: 'Löschen', action: () => { onDelete(state.id!, 'folder'); onClose(); }, danger: true });
+ } else {
+ items.push({ label: 'Neuer Chat', action: () => { onNewChat(); onClose(); } });
+ items.push({ label: 'Neuer Ordner', action: () => { onNewFolder(); onClose(); } });
+ }
+
+ return (
+
+ {items.map((item, i) => (
+
+ ))}
+
+ );
+}
+
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 [dragSessionId, setDragSessionId] = useState(null);
+ const [contextMenu, setContextMenu] = useState(null);
const loadData = async () => {
try {
@@ -174,15 +235,50 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class
useEffect(() => { loadData(); }, [isSidebar]);
- 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 handleContextMenu = useCallback((e: React.MouseEvent, type: 'session' | 'folder' | 'root', id: string | null) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setContextMenu({ x: e.clientX, y: e.clientY, type, id });
+ }, []);
+
+ const handleRename = async (id: string, type: 'session' | 'folder') => {
+ const newName = prompt('Neuer Name:', type === 'session'
+ ? sessions.find(s => s.id === id)?.title || ''
+ : folders.find(f => f.id === id)?.name || ''
+ );
+ if (!newName) return;
+ try {
+ if (type === 'session') await updateSession(id, { title: newName });
+ else await updateFolder(id, { name: newName });
+ loadData();
+ } catch (e) { setError(e instanceof Error ? e.message : 'Rename failed'); }
};
- const handleDeleteFolder = (folderId: string) => {
- if (!confirm('Ordner löschen? Chats bleiben erhalten.')) return;
- deleteFolder(folderId).then(() => loadData()).catch((e) => setError(e?.message));
+ const handleDelete = async (id: string, type: 'session' | 'folder') => {
+ if (!confirm(type === 'session' ? 'Chat löschen?' : 'Ordner löschen? Chats bleiben erhalten.')) return;
+ try {
+ if (type === 'session') {
+ await deleteSession(id);
+ if (activeSessionId === id) onSelectSession('');
+ } else {
+ await deleteFolder(id);
+ }
+ loadData();
+ } catch (e) { setError(e instanceof Error ? e.message : 'Delete failed'); }
+ };
+
+ const handleNewFolder = () => {
+ const name = prompt('Ordnername:');
+ if (!name) return;
+ createFolder({ name }).then(() => loadData()).catch((e) => setError(e?.message));
+ };
+
+ const handleNewChat = async () => {
+ try {
+ const session = await createSession({ is_sidebar: isSidebar || false });
+ onSelectSession(session.id);
+ loadData();
+ } catch (e) { setError(e instanceof Error ? e.message : 'Failed'); }
};
const handleDropToFolder = async (folderId: string | null) => {
@@ -201,7 +297,12 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class
return (
{error &&
{error}
}
-
+
{
+ if (e.target === e.currentTarget) handleContextMenu(e, 'root', null);
+ }}
+ >
{tree.map((node) => (
))}
+ {contextMenu && (
+
setContextMenu(null)}
+ onRename={handleRename}
+ onDelete={handleDelete}
+ onNewFolder={handleNewFolder}
+ onNewChat={handleNewChat}
+ />
+ )}
);
}