"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { Home, Search, Sparkles, FileText, Files, Database, FolderKanban, FlaskConical, Share2, Link2, ChevronRight, Plus, Settings, Trash2, Star, MoreHorizontal, Moon, Sun, Laptop, LogOut, BarChart3, Bot, Inbox, LayoutDashboard } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import type { TreeNode, ShellUser } from "./types"; import { cn } from "@/lib/utils"; import { useUI } from "@/lib/client/store"; import { api } from "@/lib/client/api"; import { Menu, MenuTrigger, MenuContent, MenuItem, MenuSeparator } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; const SECTIONS: { id: string; label: string; icon: React.ReactNode; type?: string }[] = [ { id: "notes", label: "My Notes", icon: }, { id: "documents", label: "Documents", icon: }, { id: "databases", label: "Databases", icon: , type: "database" }, { id: "projects", label: "Projects", icon: }, { id: "research", label: "Research", icon: , type: "research" }, ]; export function Sidebar({ tree, user, workspace, onNavigate }: { tree: TreeNode[]; user: ShellUser; workspace: { name: string }; onNavigate?: () => void }) { const pathname = usePathname(); const r = useRouter(); const qc = useQueryClient(); const ui = useUI(); const byParent = useMemo(() => { const m = new Map(); for (const n of tree) { const k = n.parentId; if (!m.has(k)) m.set(k, []); m.get(k)!.push(n); } return m; }, [tree]); const [open, setOpen] = useState>(() => { try { return JSON.parse(localStorage.getItem("note-tree-open") ?? "{}"); } catch { return {}; } }); const toggle = (id: string) => setOpen((o) => { const n = { ...o, [id]: !o[id] }; localStorage.setItem("note-tree-open", JSON.stringify(n)); return n; }); async function create(section: string, parentId: string | null = null, type?: string) { const p = await api("/api/pages", { method: "POST", json: { title: "", section, parentId, type: type ?? "page" } }); qc.invalidateQueries({ queryKey: ["tree"] }); r.push(`/p/${p.id}`); onNavigate?.(); } async function trash(n: TreeNode) { await api(`/api/pages/${n.id}`, { method: "DELETE" }); qc.invalidateQueries({ queryKey: ["tree"] }); toast("Moved to trash", { description: n.title }); if (pathname === `/p/${n.id}`) r.push("/"); } async function pin(n: TreeNode) { await api(`/api/pages/${n.id}`, { method: "PATCH", json: { pinned: !n.pinned } }); qc.invalidateQueries({ queryKey: ["tree"] }); } const Item = ({ n, depth }: { n: TreeNode; depth: number }) => { const kids = byParent.get(n.id) ?? []; const active = pathname === `/p/${n.id}`; const isOpen = open[n.id]; return (
{n.icon ?? (n.type === "database" ? "๐Ÿ—‚" : n.type === "research" ? "๐Ÿ”ฌ" : "๐Ÿ“„")}{n.title || "Untitled"} create(n.section, n.id)}> Add sub-page pin(n)}> {n.pinned ? "Unpin" : "Pin"} { const p = await api(`/api/pages/${n.id}?action=duplicate`, { method: "POST" }); qc.invalidateQueries({ queryKey: ["tree"] }); r.push(`/p/${p.id}`); }}> Duplicate trash(n)}> Move to trash
{isOpen && kids.map((k) => )}
); }; const NavLink = ({ href, icon, label, onClick }: { href?: string; icon: React.ReactNode; label: string; onClick?: () => void }) => { const active = href && (pathname === href); const cls = cn("flex items-center gap-2 rounded-md px-2 py-1.5 text-[13px] text-fg-muted hover:bg-bg-sunk hover:text-fg [&_svg]:size-4", active && "bg-bg-sunk text-fg font-medium"); return href ? {icon}{label} : ; }; const pinned = tree.filter((n) => n.pinned); return (
{user.displayName} ยท @{user.username}
ui.setTheme("light")}> Light {ui.theme === "light" && "โœ“"} ui.setTheme("dark")}> Dark {ui.theme === "dark" && "โœ“"} ui.setTheme("system")}> System {ui.theme === "system" && "โœ“"} r.push("/settings")}> Settings { await api("/api/auth/logout", { method: "POST" }); window.location.href = "/login"; }}> Sign out
} label="Home" /> } label="Search" /> } label="AI Assistant" onClick={() => ui.openAI()} /> } label="Tasks" />
{pinned.length > 0 && (
Pinned
{pinned.map((n) => {n.title || "Untitled"})}
)}
Private
{SECTIONS.map((s) => { const roots = (byParent.get(null) ?? []).filter((n) => n.section === s.id); const isOpen = open[`sec:${s.id}`] ?? true; return (
{isOpen && (roots.length ? roots.map((n) => ) : )}
); })}
Knowledge
} label="Graph" />} label="Backlinks" />} label="AI models" />} label="Compare models" />} label="Agents" />
} label="Trash" />} label="Settings" />
); }