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 } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { useAuthStore } from "@/store/authStore"; import { api } from "@/lib/api"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; interface SessionInfo { key: string; created_at: string; updated_at: string; alias?: string | null; pinned?: boolean; archived?: boolean; metadata?: { title?: string; }; } function Section({ title, count, items, onSelect, onDelete, onRename, onTogglePinned, onToggleArchived, activeKey }: { title: string; count: number; items: SessionInfo[]; onSelect: (key: string) => void; onDelete: (key: string) => void; onRename: (key: string, currentTitle: string) => void; onTogglePinned: (key: string, pinned: boolean) => void; onToggleArchived: (key: string, archived: boolean) => void; activeKey: string | null; }) { return (
{title} ({count})
{items.map((item) => { const displayTitle = item.metadata?.title || item.key.replace("api:", ""); const isActive = activeKey === item.key; return (
onSelect(item.key)} >
{item.pinned && } {displayTitle}
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"> { e.preventDefault(); e.stopPropagation(); onRename(item.key, displayTitle); }} onSelect={(e) => { e.preventDefault(); e.stopPropagation(); onRename(item.key, displayTitle); }} > 重命名 { e.preventDefault(); e.stopPropagation(); onTogglePinned(item.key, !!item.pinned); }} onSelect={(e) => { e.preventDefault(); e.stopPropagation(); onTogglePinned(item.key, !!item.pinned); }} > {item.pinned ? "取消置顶" : "置顶"} { e.preventDefault(); e.stopPropagation(); onToggleArchived(item.key, !!item.archived); }} onSelect={(e) => { e.preventDefault(); e.stopPropagation(); onToggleArchived(item.key, !!item.archived); }} > {item.archived ? "取消归档" : "归档"} { e.preventDefault(); e.stopPropagation(); onDelete(item.key); }} onSelect={(e) => { e.preventDefault(); e.stopPropagation(); onDelete(item.key); }} className="text-red-600 focus:text-red-600 focus:bg-red-50" > 删除会话
); })}
); } function SidebarBody() { const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useAuthStore(); const [showUserMenu, setShowUserMenu] = useState(false); const menuRef = useRef(null); // Session management state const [sessions, setSessions] = useState([]); const [sessionFilter, setSessionFilter] = useState(""); const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [sessionToRename, setSessionToRename] = useState<{key: string, title: string} | null>(null); const [newTitle, setNewTitle] = useState(""); // Try to parse active session from URL query const queryParams = new URLSearchParams(location.search); const activeSessionKey = queryParams.get("session") || "api:default"; const fetchSessions = async () => { try { const data = await api.get("/nanobot/sessions"); setSessions(data); } catch (e) { console.error("Failed to fetch sessions", e); } }; useEffect(() => { fetchSessions(); }, [location.pathname, location.search]); useEffect(() => { const onFocus = () => fetchSessions(); const onSessionsChanged = () => fetchSessions(); window.addEventListener("focus", onFocus); window.addEventListener("nanobot:sessions-changed", onSessionsChanged); return () => { window.removeEventListener("focus", onFocus); window.removeEventListener("nanobot:sessions-changed", onSessionsChanged); }; }, []); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setShowUserMenu(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleLogout = () => { logout(); navigate("/login"); }; const handleSelectSession = (key: string) => { navigate(`/?session=${encodeURIComponent(key)}`); }; const handleNewThread = async () => { const newSessionId = `api:${Date.now()}`; try { await api.post(`/nanobot/sessions/${encodeURIComponent(newSessionId)}/ensure`, {}); await fetchSessions(); window.dispatchEvent(new Event("nanobot:sessions-changed")); } catch (e) { console.error("Failed to create session", e); } navigate(`/?session=${encodeURIComponent(newSessionId)}`); }; const handleDeleteSession = async (key: string) => { if (!window.confirm("确定要删除这个会话吗?")) return; try { await api.delete(`/nanobot/sessions/${encodeURIComponent(key)}`); if (activeSessionKey === key) { navigate("/"); } fetchSessions(); window.dispatchEvent(new Event("nanobot:sessions-changed")); } catch (e) { console.error("Failed to delete session", e); } }; const openRenameDialog = (key: string, currentTitle: string) => { setSessionToRename({ key, title: currentTitle }); setNewTitle(currentTitle); setRenameDialogOpen(true); }; const handleRename = async () => { if (!sessionToRename || !newTitle.trim()) return; try { const nextTitle = newTitle.trim(); await api.put(`/nanobot/sessions/${encodeURIComponent(sessionToRename.key)}`, { title: nextTitle }); setSessions((prev) => prev.map((item) => item.key === sessionToRename.key ? { ...item, alias: nextTitle, metadata: { ...(item.metadata || {}), title: nextTitle } } : item ) ); setRenameDialogOpen(false); fetchSessions(); window.dispatchEvent(new Event("nanobot:sessions-changed")); } catch (e) { console.error("Failed to rename session", e); } }; const handleTogglePinned = async (key: string, pinned: boolean) => { const nextPinned = !pinned; try { await api.put(`/nanobot/sessions/${encodeURIComponent(key)}`, { pinned: nextPinned }); setSessions((prev) => prev .map((item) => (item.key === key ? { ...item, pinned: nextPinned } : item)) .sort((a, b) => { const ap = a.pinned ? 1 : 0; const bp = b.pinned ? 1 : 0; if (bp !== ap) return bp - ap; const aa = a.archived ? 1 : 0; const ba = b.archived ? 1 : 0; if (aa !== ba) return aa - ba; return (b.updated_at || "").localeCompare(a.updated_at || ""); }) ); window.dispatchEvent(new Event("nanobot:sessions-changed")); } catch (e) { console.error("Failed to toggle pinned", e); } }; const handleToggleArchived = async (key: string, archived: boolean) => { const nextArchived = !archived; try { await api.put(`/nanobot/sessions/${encodeURIComponent(key)}`, { archived: nextArchived }); setSessions((prev) => prev .map((item) => (item.key === key ? { ...item, archived: nextArchived } : item)) .sort((a, b) => { const ap = a.pinned ? 1 : 0; const bp = b.pinned ? 1 : 0; if (bp !== ap) return bp - ap; const aa = a.archived ? 1 : 0; const ba = b.archived ? 1 : 0; if (aa !== ba) return aa - ba; return (b.updated_at || "").localeCompare(a.updated_at || ""); }) ); window.dispatchEvent(new Event("nanobot:sessions-changed")); } catch (e) { console.error("Failed to toggle archived", e); } }; const normalizedFilter = sessionFilter.trim().toLowerCase(); const activeSessions = sessions.filter((item) => { if (item.archived) return false; if (!normalizedFilter) return true; const title = (item.metadata?.title || item.key.replace("api:", "")).toLowerCase(); return title.includes(normalizedFilter); }); const archivedSessions = sessions.filter((item) => { if (!item.archived) return false; if (!normalizedFilter) return true; const title = (item.metadata?.title || item.key.replace("api:", "")).toLowerCase(); return title.includes(normalizedFilter); }); return (
{/* Header */}
🦞 龙虾问数
setSessionFilter(e.target.value)} placeholder="过滤会话名称" className="pl-9 h-9 border-zinc-200 bg-white" />
重命名会话
setNewTitle(e.target.value)} placeholder="输入新的会话标题" autoFocus onKeyDown={(e) => { if (e.key === 'Enter') { handleRename(); } }} />
{/* User Settings Popover Menu */} {showUserMenu && (

{user?.username}

{user?.email}

{user?.is_admin && ( <> )}
)}
); } export function Sidebar() { const [isOpen, setIsOpen] = useState(false); return ( <> } />
); }