Spaces:
Sleeping
Sleeping
feat: add confluence/slack search tools, chat history, cloud Qdrant support, sync trigger fixes
68af3c5 | import { useNavigate, useLocation, Link } from '@tanstack/react-router' | |
| import { useQuery } from '@tanstack/react-query' | |
| import { | |
| Home, Search, Clock, BarChart2, Settings2, LogOut, | |
| Sun, Moon, ChevronLeft, ChevronRight, Zap, Bell, | |
| } from 'lucide-react' | |
| import { useUIStore } from '@/stores/uiStore' | |
| import { useAuth } from '@/hooks/useAuth' | |
| import { useNotifications } from '@/hooks/useNotifications' | |
| import { NotificationCenter } from './NotificationCenter' | |
| import { apiFetch } from '@/lib/http' | |
| import { cn } from '@/lib/utils' | |
| import { useState } from 'react' | |
| // βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| interface RecentQuery { | |
| id: string | |
| query: string | |
| created_at: string | |
| success: boolean | |
| } | |
| // βββ Data fetching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function fetchRecentQueries(): Promise<RecentQuery[]> { | |
| const res = await apiFetch('/api/workspace/history?page=1&limit=6') | |
| const data = await res.json() | |
| return (data.items ?? []) as RecentQuery[] | |
| } | |
| // βββ Nav definition βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const BASE_NAV = [ | |
| { to: '/', Icon: Home, label: 'Home' }, | |
| { to: '/query', Icon: Search, label: 'Ask' }, | |
| { to: '/workspace', Icon: Clock, label: 'History' }, | |
| { to: '/analytics', Icon: BarChart2, label: 'Analytics' }, | |
| ] | |
| const ADMIN_NAV = { to: '/admin', Icon: Settings2, label: 'Admin' } | |
| // βββ Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function Sidebar() { | |
| const { sidebarOpen, toggleSidebar, theme, toggleTheme } = useUIStore() | |
| const { user, signOut } = useAuth() | |
| const navigate = useNavigate() | |
| const { pathname } = useLocation() | |
| const { unreadCount } = useNotifications() | |
| const [notifOpen, setNotifOpen] = useState(false) | |
| const { data: recent = [] } = useQuery({ | |
| queryKey: ['sidebar-recent'], | |
| queryFn: fetchRecentQueries, | |
| staleTime: 30_000, | |
| refetchInterval: 60_000, | |
| enabled: !!user, | |
| }) | |
| const handleSignOut = async () => { | |
| await signOut() | |
| navigate({ to: '/login' }) | |
| } | |
| const navItems = (user?.role === 'admin' || user?.role === 'org_admin') | |
| ? [...BASE_NAV, ADMIN_NAV] | |
| : BASE_NAV | |
| return ( | |
| <> | |
| <aside | |
| className={cn( | |
| 'fixed left-0 top-0 z-20 flex h-screen flex-col border-r border-surface-subtle bg-white transition-[width] duration-200 ease-in-out dark:bg-stone-950', | |
| sidebarOpen ? 'w-60' : 'w-14', | |
| )} | |
| > | |
| {/* ββ Header: logo + collapse toggle βββββββββββββββββββββββββββββββ */} | |
| <div className="flex h-12 shrink-0 items-center gap-2 border-b border-surface-subtle px-3"> | |
| {sidebarOpen && ( | |
| <Link | |
| to="/" | |
| className="flex min-w-0 flex-1 items-center gap-2 text-sm font-semibold tracking-tight text-stone-900 dark:text-stone-100" | |
| > | |
| <Zap className="h-4 w-4 shrink-0 text-brand" /> | |
| Godspeed | |
| </Link> | |
| )} | |
| {!sidebarOpen && <Zap className="mx-auto h-4 w-4 text-brand" />} | |
| <button | |
| onClick={toggleSidebar} | |
| className={cn( | |
| 'shrink-0 rounded-md p-1.5 text-stone-400 hover:bg-stone-100 hover:text-stone-600 dark:hover:bg-stone-800 dark:hover:text-stone-300', | |
| !sidebarOpen && 'mx-auto', | |
| )} | |
| aria-label={sidebarOpen ? 'Collapse sidebar' : 'Expand sidebar'} | |
| > | |
| {sidebarOpen | |
| ? <ChevronLeft className="h-4 w-4" /> | |
| : <ChevronRight className="h-4 w-4" />} | |
| </button> | |
| </div> | |
| {/* ββ Navigation links ββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| <nav className="flex flex-col gap-0.5 p-2 pt-3"> | |
| {navItems.map(({ to, Icon, label }) => { | |
| const active = to === '/' ? pathname === '/' : pathname.startsWith(to) | |
| return ( | |
| <Link | |
| key={to} | |
| to={to} | |
| title={!sidebarOpen ? label : undefined} | |
| className={cn( | |
| 'flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition-colors', | |
| active | |
| ? 'bg-stone-100 font-medium text-stone-900 dark:bg-stone-800 dark:text-stone-100' | |
| : 'text-stone-500 hover:bg-stone-50 hover:text-stone-800 dark:hover:bg-stone-900 dark:hover:text-stone-200', | |
| !sidebarOpen && 'justify-center px-2', | |
| )} | |
| > | |
| <Icon className="h-4 w-4 shrink-0" /> | |
| {sidebarOpen && ( | |
| <> | |
| <span className="flex-1">{label}</span> | |
| {label === 'Ask' && ( | |
| <kbd className="rounded border border-stone-200 bg-stone-50 px-1 py-0.5 text-[9px] font-medium text-stone-400 dark:border-stone-700 dark:bg-stone-800"> | |
| βK | |
| </kbd> | |
| )} | |
| </> | |
| )} | |
| </Link> | |
| ) | |
| })} | |
| {/* Notifications */} | |
| <button | |
| onClick={() => setNotifOpen(true)} | |
| title={!sidebarOpen ? 'Notifications' : undefined} | |
| className={cn( | |
| 'relative flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm text-stone-500 transition-colors hover:bg-stone-50 hover:text-stone-800 dark:hover:bg-stone-900 dark:hover:text-stone-200', | |
| !sidebarOpen && 'justify-center px-2', | |
| )} | |
| > | |
| <Bell className="h-4 w-4 shrink-0" /> | |
| {sidebarOpen && <span className="flex-1 text-left">Notifications</span>} | |
| {unreadCount > 0 && ( | |
| <span className={cn( | |
| 'flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-brand px-1 text-[10px] font-bold text-white', | |
| !sidebarOpen && 'absolute right-1.5 top-1.5', | |
| )}> | |
| {unreadCount > 9 ? '9+' : unreadCount} | |
| </span> | |
| )} | |
| </button> | |
| </nav> | |
| {/* ββ Recent queries βββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| {sidebarOpen && recent.length > 0 && ( | |
| <div className="mt-1 px-2"> | |
| <p className="px-2 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-stone-400"> | |
| Recent | |
| </p> | |
| <div className="flex flex-col gap-0.5"> | |
| {recent.map((item) => ( | |
| <button | |
| key={item.id} | |
| onClick={() => navigate({ to: '/query', search: { q: item.query, qid: undefined, fresh: false } })} | |
| title={item.query} | |
| className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors hover:bg-stone-50 dark:hover:bg-stone-900" | |
| > | |
| <span | |
| className={cn( | |
| 'h-1.5 w-1.5 shrink-0 rounded-full', | |
| item.success ? 'bg-green-400' : 'bg-stone-300', | |
| )} | |
| /> | |
| <span className="truncate text-xs text-stone-500 dark:text-stone-400" title={item.query}> | |
| {item.query.length > 34 ? item.query.slice(0, 34) + 'β¦' : item.query} | |
| </span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* ββ Spacer βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */} | |
| <div className="flex-1" /> | |
| {/* ββ Bottom: theme + account βββββββββββββββββββββββββββββββββββββββ */} | |
| <div className="border-t border-surface-subtle p-2"> | |
| {/* Theme toggle */} | |
| <button | |
| onClick={toggleTheme} | |
| title={!sidebarOpen ? (theme === 'light' ? 'Dark mode' : 'Light mode') : undefined} | |
| className={cn( | |
| 'flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-sm text-stone-500 transition-colors hover:bg-stone-50 hover:text-stone-800 dark:hover:bg-stone-900 dark:hover:text-stone-200', | |
| !sidebarOpen && 'justify-center px-2', | |
| )} | |
| > | |
| {theme === 'light' | |
| ? <Moon className="h-4 w-4 shrink-0" /> | |
| : <Sun className="h-4 w-4 shrink-0" />} | |
| {sidebarOpen && <span>{theme === 'light' ? 'Dark mode' : 'Light mode'}</span>} | |
| </button> | |
| {/* User section */} | |
| {user && ( | |
| <div className={cn( | |
| 'mt-1 flex items-center gap-2 rounded-lg px-2 py-2', | |
| !sidebarOpen && 'justify-center', | |
| )}> | |
| {/* Avatar */} | |
| <span | |
| title={!sidebarOpen ? user.name : undefined} | |
| className="inline-flex h-7 w-7 shrink-0 cursor-default items-center justify-center rounded-full bg-brand text-[11px] font-semibold text-white" | |
| > | |
| {user.name.charAt(0).toUpperCase()} | |
| </span> | |
| {sidebarOpen && ( | |
| <> | |
| <div className="min-w-0 flex-1"> | |
| <p className="truncate text-xs font-medium text-stone-700 dark:text-stone-300"> | |
| {user.name} | |
| </p> | |
| <p className="truncate text-[10px] text-stone-400">{user.email}</p> | |
| </div> | |
| <button | |
| onClick={handleSignOut} | |
| title="Sign out" | |
| className="shrink-0 rounded-md p-1.5 text-stone-400 hover:bg-stone-100 hover:text-stone-700 dark:hover:bg-stone-800 dark:hover:text-stone-300" | |
| aria-label="Sign out" | |
| > | |
| <LogOut className="h-3.5 w-3.5" /> | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </aside> | |
| {/* Notification center β portal-rendered */} | |
| <NotificationCenter open={notifOpen} onClose={() => setNotifOpen(false)} /> | |
| </> | |
| ) | |
| } | |