"use client"; import Image from "next/image"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState, type ReactNode } from "react"; import { useAppShell } from "@/context/AppShellContext"; import { BookOpen, BookText, Bot, Brain, ChevronDown, Github, HeartHandshake, House, LayoutGrid, Library, Lock, PanelLeftClose, PanelLeftOpen, PenLine, Settings, type LucideIcon, } from "lucide-react"; import { useTranslation } from "react-i18next"; import SessionList from "@/components/SessionList"; import { VersionBadge } from "@/components/sidebar/VersionBadge"; import type { SessionSummary } from "@/lib/session-api"; import { Tooltip } from "@/components/ui/Tooltip"; import { useCapabilityAccess } from "@/components/access/CapabilityAccessContext"; import type { Capability } from "@/lib/capability-routes"; interface NavEntry { href: string; label: string; icon: LucideIcon; tooltipKey?: string; /** Model capability this feature needs; locked when the user lacks it. */ requires?: Capability; } const PRIMARY_NAV: NavEntry[] = [ { href: "/home", label: "Home", icon: House, tooltipKey: "Home tooltip", requires: "llm", }, { href: "/partners", label: "Partners", icon: HeartHandshake, tooltipKey: "Partners tooltip", requires: "llm", }, { // My Agents is its own top-level feature (pulled out of the Learning // Space): connect a live local Claude Code / Codex to consult in chat, // and manage imported agent conversations. Ungated — managing connections // and imports needs no per-user model grant. href: "/agents", label: "My Agents", icon: Bot, tooltipKey: "Agents tooltip", }, { href: "/co-writer", label: "Co-Writer", icon: PenLine, tooltipKey: "Co-Writer tooltip", requires: "llm", }, { href: "/book", label: "Book", icon: Library, tooltipKey: "Book tooltip", requires: "llm", }, { href: "/space", label: "Learning Space", icon: LayoutGrid, tooltipKey: "Space tooltip", }, ]; const SECONDARY_NAV: NavEntry[] = [ { // Memory is its own top-level console (pulled out of the Learning Space): // a place to inspect and curate the tutor's long-term memory, not a daily // workspace. Never gated — memory has no per-user model requirement. href: "/memory", label: "Memory", icon: Brain, tooltipKey: "Memory tooltip", }, { // Knowledge Center sits just above Settings: it's a console for managing // KBs and retrieval engines, not a daily workspace. Never gated — embedding // / search are shared admin infrastructure, no per-user model grant needed. href: "/knowledge", label: "Knowledge Center", icon: BookOpen, tooltipKey: "Knowledge tooltip", }, { href: "/settings", label: "Settings", icon: Settings }, ]; const GITHUB_REPO_URL = "https://github.com/HKUDS/DeepTutor"; const DOCS_URL = "https://deeptutor.info/"; const RECENTS_COLLAPSED_KEY = "deeptutor.sidebar.recentsCollapsed"; interface SidebarShellProps { sessions?: SessionSummary[]; activeSessionId?: string | null; loadingSessions?: boolean; showSessions?: boolean; /** Clicking the Chat nav item resets to a fresh session via this handler. */ onNewChat?: () => void; onSelectSession?: (sessionId: string) => void | Promise; onRenameSession?: (sessionId: string, title: string) => void | Promise; onDeleteSession?: (sessionId: string) => void | Promise; /** * Footer content rendered below the nav. Pass a render function to receive * the current ``collapsed`` state so footer items (e.g. Admin / Sign out) can * switch to their icon-only variant when the rail is collapsed. */ footerSlot?: ReactNode | ((collapsed: boolean) => ReactNode); } export function SidebarShell({ sessions = [], activeSessionId = null, loadingSessions = false, showSessions = false, onNewChat, onSelectSession, onRenameSession, onDeleteSession, footerSlot, }: SidebarShellProps) { const pathname = usePathname(); const router = useRouter(); const { t } = useTranslation(); const { has } = useCapabilityAccess(); const { sidebarCollapsed: collapsed, setSidebarCollapsed: setCollapsed } = useAppShell(); const navLocked = (item: NavEntry) => item.requires ? !has(item.requires) : false; const lockedTooltip = t("Locked — contact your administrator to get access."); const renderedFooter = typeof footerSlot === "function" ? footerSlot(collapsed) : footerSlot; const [recentsCollapsed, setRecentsCollapsed] = useState(false); // Hydrate Recents collapse from localStorage after first render to stay SSR-safe. useEffect(() => { if (typeof window === "undefined") return; // eslint-disable-next-line react-hooks/set-state-in-effect setRecentsCollapsed( window.localStorage.getItem(RECENTS_COLLAPSED_KEY) === "1", ); }, []); const toggleRecents = () => { setRecentsCollapsed((prev) => { const next = !prev; if (typeof window !== "undefined") { window.localStorage.setItem(RECENTS_COLLAPSED_KEY, next ? "1" : "0"); } return next; }); }; const handleHomeClick = (event: React.MouseEvent) => { // Always reset to a fresh session (mirrors the old "New Chat" affordance); // let modifier-clicks fall through to default Link behavior so middle-click // open-in-new-tab still works. if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return; event.preventDefault(); onNewChat?.(); router.push("/home"); }; /* ---- Collapsed state ---- */ if (collapsed) { return ( ); } /* ---- Expanded state ---- */ return ( ); }