diff --git a/backend/main.py b/backend/main.py index 999efd4..cf6b16f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -87,6 +87,10 @@ class SessionAliasUpdateRequest(BaseModel): archived: Optional[bool] = None +class BatchDeleteRequest(BaseModel): + session_ids: List[str] + + class SessionFileContextUpdateRequest(BaseModel): active_data_file: Optional[Dict[str, Any]] = None @@ -248,6 +252,30 @@ def delete_session(session_id: str): return {"status": "success"} raise HTTPException(status_code=404, detail="Session not found") + +@app.post("/nanobot/sessions/batch-delete") +def batch_delete_sessions(request: BatchDeleteRequest): + if not nanobot_service.agent: + raise HTTPException(status_code=400, detail="Nanobot not running") + + deleted_ids = [] + for session_id in request.session_ids: + try: + # Try to remove from cache and delete file + session = nanobot_service.agent.sessions.get_or_create(session_id) + if session: + nanobot_service.agent.sessions.invalidate(session_id) + path = nanobot_service.agent.sessions._get_session_path(session_id) + if path.exists(): + path.unlink() + session_alias_store.delete_session(session_id) + deleted_ids.append(session_id) + except Exception as e: + print(f"Failed to delete session {session_id}: {e}") + + return {"status": "success", "deleted_count": len(deleted_ids), "deleted_ids": deleted_ids} + + @app.put("/nanobot/sessions/{session_id}") def update_session(session_id: str, payload: SessionAliasUpdateRequest): updated = session_alias_store.update_alias_meta( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 9abb2c4..1e65dd6 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings, Brain, Trash2, Pencil, Pin, Archive, Database } from "lucide-react"; +import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { useAuthStore } from "@/store/authStore"; @@ -31,6 +31,7 @@ function Section({ onRename, onTogglePinned, onToggleArchived, + onBatchDelete, activeKey }: { title: string; @@ -41,37 +42,138 @@ function Section({ onRename: (key: string, currentTitle: string) => void; onTogglePinned: (key: string, pinned: boolean) => void; onToggleArchived: (key: string, archived: boolean) => void; + onBatchDelete: (keys: string[]) => void; activeKey: string | null; }) { + const [selectedKeys, setSelectedKeys] = useState([]); + const [isSelectionMode, setIsSelectionMode] = useState(false); + + const toggleSelect = (key: string, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedKeys(prev => + prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] + ); + }; + + const handleSelectAll = (e: React.MouseEvent) => { + e.stopPropagation(); + if (selectedKeys.length === items.length && items.length > 0) { + setSelectedKeys([]); + } else { + setSelectedKeys(items.map(item => item.key)); + } + }; + + const handleInvertSelection = (e: React.MouseEvent) => { + e.stopPropagation(); + const allKeys = items.map(item => item.key); + setSelectedKeys(allKeys.filter(key => !selectedKeys.includes(key))); + }; + + const handleBatchDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + if (selectedKeys.length === 0) return; + onBatchDelete(selectedKeys); + setSelectedKeys([]); + setIsSelectionMode(false); + }; + + useEffect(() => { + if (!isSelectionMode) { + setSelectedKeys([]); + } + }, [isSelectionMode]); + return (
-
+
{title} ({count})
+
+ {isSelectionMode ? ( + <> + + + + + + ) : ( + + )} +
{items.map((item) => { const displayTitle = item.metadata?.title || item.key.replace("api:", ""); const isActive = activeKey === item.key; + const isSelected = selectedKeys.includes(item.key); return (
onSelect(item.key)} + isActive && !isSelectionMode ? 'bg-zinc-100 text-zinc-900 font-medium' : 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900' + } ${isSelected ? 'bg-indigo-50/50 text-indigo-700' : ''}`} + onClick={(e) => isSelectionMode ? toggleSelect(item.key, e) : onSelect(item.key)} >
- - {item.pinned && } - + {isSelectionMode ? ( + toggleSelect(item.key, e)} + > + {isSelected ? ( + + ) : ( + + )} + + ) : ( + + {item.pinned && } + + )} {displayTitle}
- + {!isSelectionMode && ( + e.stopPropagation()} className="h-6 w-6 flex items-center justify-center rounded hover:bg-zinc-200 text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity outline-none"> @@ -139,6 +241,7 @@ function Section({ + )}
); })} @@ -234,6 +337,20 @@ function SidebarBody() { } }; + const handleBatchDelete = async (keys: string[]) => { + if (!window.confirm(`确定要删除选中的 ${keys.length} 个会话吗?`)) return; + try { + await api.post("/nanobot/sessions/batch-delete", { session_ids: keys }); + if (keys.includes(activeSessionKey)) { + navigate("/"); + } + fetchSessions(); + window.dispatchEvent(new Event("nanobot:sessions-changed")); + } catch (e) { + console.error("Failed to batch delete sessions", e); + } + }; + const openRenameDialog = (key: string, currentTitle: string) => { setSessionToRename({ key, title: currentTitle }); setNewTitle(currentTitle); @@ -371,17 +488,19 @@ function SidebarBody() { items={activeSessions} onSelect={handleSelectSession} onDelete={handleDeleteSession} + onBatchDelete={handleBatchDelete} onRename={openRenameDialog} onTogglePinned={handleTogglePinned} onToggleArchived={handleToggleArchived} activeKey={activeSessionKey} />