import { useState } from "react"; import { applyTheme, readTheme, type ThemeMode } from "../lib/theme"; import { createSubSession, getAuthToken } from "../lib/api"; import { useAppStore } from "../store/useAppStore"; import { Icon, type IconName } from "./Icon"; import { Tag } from "./primitives"; /** * Left rail — NOT a behavior-mode list (design/01 §3: mode switching is driven * at runtime by the agent / user, and rendered per message). This is a small * navigation rail: brand + connection status + theme + admin/settings entry. * * Two surface variants share the same NavContent: * - Sidebar (desktop): fixed rail, `hidden md:flex`. * - MobileNav(mobile): drawer overlay opened from TopBar's hamburger button * (`md:hidden`), with a scrim + a spring slide-in panel. Content is shared * so desktop and mobile never diverge. */ function NavContent({ onNavigate }: { onNavigate: () => void }): React.ReactNode { const wsStatus = useAppStore((s) => s.wsStatus); const mood = useAppStore((s) => s.mood); const sessions = useAppStore((s) => s.sessions); const activeSessionId = useAppStore((s) => s.activeSessionId); const setSessions = useAppStore((s) => s.setSessions); const setActiveSession = useAppStore((s) => s.setActiveSession); const [theme, setTheme] = useState(readTheme); const [creating, setCreating] = useState(false); const connected = wsStatus === "connected"; const wsLabel = connected ? "实时连接" : wsStatus === "expired" ? "登录过期" : "未连接"; const hasToken = !!getAuthToken(); const toggleTheme = () => { const next: ThemeMode = theme === "dark" ? "light" : "dark"; applyTheme(next); setTheme(next); }; // Main session pinned first; secondary sessions follow by creation time. const sessionList = Object.values(sessions).sort((a, b) => { if (a.type !== b.type) return a.type === "main" ? -1 : 1; return a.createdAt - b.createdAt; }); const selectSession = (id: string) => { setActiveSession(id); onNavigate(); }; const createSession = async () => { if (creating) return; setCreating(true); try { const meta = await createSubSession(); // Optimistically append so the new session shows immediately; the backend // will return it on the next GET /sessions too. const next = Object.values(useAppStore.getState().sessions); setSessions([...next, ...(next.some((s) => s.id === meta.id) ? [] : [meta])]); setActiveSession(meta.id); onNavigate(); } catch { // Backend not wired yet — leave the list unchanged. } finally { setCreating(false); } }; return ( <>
Cyrene
Cyrene
昔涟
{/* connection status */} {mood ? (
心情 · {mood}
) : null} {/* conversation sessions (design/16 §2) — main fixed, secondary created */} {hasToken ? (
会话
{sessionList.map((s) => ( selectSession(s.id)} /> ))}
) : null}
{ window.location.hash = "#/admin/system"; onNavigate(); }} /> { window.location.hash = "#/admin"; onNavigate(); }} /> {!hasToken ? 未登录 : null}
); } /** Desktop left rail. */ export function Sidebar(): React.ReactNode { return ( ); } /** Mobile drawer — overlay + scrim + spring slide-in panel. */ export function MobileNav({ open, onClose }: { open: boolean; onClose: () => void }): React.ReactNode { if (!open) return null; return (
{/* scrim */}
{/* panel */}
); } function NavItem({ icon, label, onClick, active = false, hint, }: { icon: IconName; label: string; onClick: () => void; active?: boolean; hint?: string; }): React.ReactNode { return ( ); }