import { useState } from 'react'; import { Link, useRouterState } from '@tanstack/react-router'; import { LayoutGroup } from 'framer-motion'; import { House, ChartGantt, ClipboardCheck, FolderKanban, PanelLeft, Settings, Sparkles, Check, Sun, Moon, Monitor, Palette } from 'lucide-react'; import { trpc } from '@/lib/trpc'; import { useDoubleClickAI } from '@/hooks/useDoubleClickAI'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, useSidebar, } from '@/components/ui/sidebar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuPortal, DropdownMenuSub, DropdownMenuTrigger, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { AIChatPanel } from '@/components/ai/AIChatPanel'; import { FloatingChatPortal } from '@/components/ai/FloatingChat'; import { useTheme } from '@/components/theme-provider'; import { FloatingChatProvider } from '@/context/FloatingChatContext'; const NAV_ITEMS = [ { to: '/', icon: House, label: 'Home' }, { to: '/timeline', icon: ChartGantt, label: 'Timeline' }, { to: '/tasks', icon: ClipboardCheck, label: 'Tasks' }, { to: '/projects', icon: FolderKanban, label: 'Projects' }, ] as const; interface AppShellProps { children: React.ReactNode; } export function AppShell({ children }: AppShellProps) { return ( {children} ); } function AppShellInner({ children }: AppShellProps) { useDoubleClickAI(); const collapsedQuery = trpc.settings.getSidebarCollapsed.useQuery(undefined, { staleTime: Infinity, }); const setSidebarCollapsedMutation = trpc.settings.setSidebarCollapsed.useMutation(); const routerState = useRouterState(); const currentPath = routerState.location.pathname; // Controlled open state (spec: "Controlled Sidebar" pattern) // Default to collapsed (false) until the persisted preference loads const [open, setOpen] = useState(() => collapsedQuery.data === undefined ? false : !collapsedQuery.data ); const handleOpenChange = (value: boolean) => { setOpen(value); setSidebarCollapsedMutation.mutate({ collapsed: !value }); }; // AI token dialog state (shared between sidebar gear menu and AIChatPanel prompt) const [tokenDialogOpen, setTokenDialogOpen] = useState(false); const [tokenInput, setTokenInput] = useState(''); const [saved, setSaved] = useState(false); const hasTokenQuery = trpc.ai.hasToken.useQuery(); const utils = trpc.useUtils(); const setTokenMutation = trpc.ai.setToken.useMutation({ onSuccess: () => { setSaved(true); setTokenInput(''); void utils.ai.hasToken.invalidate(); setTimeout(() => setSaved(false), 2000); }, }); const isHomePage = currentPath === '/'; return ( {isHomePage ? ( setTokenDialogOpen(true)} isHomePage /> ) : ( {children} )} {/* Floating AI Chat — portal to document.body */} {/* AI Token Dialog — rendered outside Sidebar to avoid layout conflicts */} { setTokenDialogOpen(open); if (!open) { setTokenInput(''); setSaved(false); } }}> AI Provider Configure your AI provider credentials for chat, summaries, and suggestions. GitHub Copilot Token setTokenInput(e.target.value)} /> Your token is stored securely in the OS keychain. {hasTokenQuery.data === true && ( A token is currently stored. )} {saved && ( Saved )} setTokenMutation.mutate({ token: tokenInput.trim() })} > {setTokenMutation.isPending ? 'Saving...' : 'Save Token'} ); } interface AppSidebarProps { currentPath: string; setTokenDialogOpen: (open: boolean) => void; } function AppSidebar({ currentPath, setTokenDialogOpen }: AppSidebarProps) { const { toggleSidebar } = useSidebar(); const { theme, setTheme } = useTheme(); return ( {/* Logo */} Adiuva {/* Nav */} {NAV_ITEMS.map(({ to, icon: Icon, label }) => { const isActive = to === '/' ? currentPath === '/' : currentPath.startsWith(to); return ( {label} ); })} {/* Settings gear + Collapse toggle */} Settings setTokenDialogOpen(true)}> AI Provider Theme setTheme('light')}> Light {theme === 'light' && } setTheme('dark')}> Dark {theme === 'dark' && } setTheme('system')}> System {theme === 'system' && } Collapse ); }
Your token is stored securely in the OS keychain. {hasTokenQuery.data === true && ( A token is currently stored. )}