Spaces:
Sleeping
Sleeping
| import { BarChart3, BookOpen, ChevronLeft, ChevronRight, Home, LogOut, Moon, Plus, Sun, Trash2 } from "lucide-react"; | |
| import { useTheme } from "next-themes"; | |
| import type { Analysis } from "@/services/orchestrationApi"; | |
| import { cx, formatDateTime } from "./utils"; | |
| export type AppMenuKey = "home" | "knowledge" | "analysis-agent"; | |
| interface AppNavigationProps { | |
| active: AppMenuKey; | |
| userName?: string; | |
| collapsed?: boolean; | |
| analyses?: Analysis[]; | |
| activeAnalysisId?: string; | |
| loadingAnalyses?: boolean; | |
| onChange: (menu: AppMenuKey) => void; | |
| onLogout: () => void; | |
| onToggleCollapsed?: () => void; | |
| onNewAnalysis?: () => void; | |
| onSelectAnalysis?: (analysis: Analysis) => void; | |
| onDeleteAnalysis?: (analysis: Analysis) => void; | |
| } | |
| const navItems: Array<{ | |
| key: AppMenuKey; | |
| label: string; | |
| description: string; | |
| icon: typeof Home; | |
| badge?: string; | |
| }> = [ | |
| { | |
| key: "home", | |
| label: "Home", | |
| description: "Overview cara pakai Data Eyond dari knowledge sampai report.", | |
| icon: Home, | |
| }, | |
| { | |
| key: "knowledge", | |
| label: "Knowledge", | |
| description: "Langkah pertama: upload dokumen, proses knowledge, atau connect database.", | |
| icon: BookOpen, | |
| badge: "Start here", | |
| }, | |
| { | |
| key: "analysis-agent", | |
| label: "Analysis Agent", | |
| description: "Buat analysis, chat dengan AI agent, gunakan Help, dan generate report.", | |
| icon: BarChart3, | |
| }, | |
| ]; | |
| export function AppNavigation({ | |
| active, | |
| userName, | |
| collapsed = false, | |
| analyses = [], | |
| activeAnalysisId, | |
| loadingAnalyses = false, | |
| onChange, | |
| onLogout, | |
| onToggleCollapsed, | |
| onNewAnalysis, | |
| onSelectAnalysis, | |
| onDeleteAnalysis, | |
| }: AppNavigationProps) { | |
| const { resolvedTheme, setTheme } = useTheme(); | |
| const isDark = resolvedTheme === "dark"; | |
| const showAnalysisList = active === "analysis-agent" && !collapsed; | |
| return ( | |
| <aside | |
| className={cx( | |
| "flex h-full min-h-0 w-full flex-col border-r border-slate-200 bg-white py-4 text-slate-900 transition-[padding]", | |
| collapsed ? "px-2" : "px-3" | |
| )} | |
| > | |
| <div className={cx("flex items-center pb-5", collapsed ? "flex-col justify-center gap-2 px-0" : "gap-3 px-2")}> | |
| <div className="brand-logo-surface flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md border p-2 shadow-sm" style={{ backgroundColor: "#ffffff", borderColor: "#e2e8f0" }}> | |
| <img src="/logo.png" alt="Data Eyond" className="h-full w-full object-contain" /> | |
| </div> | |
| {!collapsed && ( | |
| <div className="min-w-0 flex-1"> | |
| <p className="truncate text-sm font-semibold leading-none">Data Eyond</p> | |
| <p className="mt-1 truncate text-xs text-slate-500">Analysis workspace</p> | |
| </div> | |
| )} | |
| {onToggleCollapsed && ( | |
| <button | |
| type="button" | |
| title={collapsed ? "Expand sidebar" : "Collapse sidebar"} | |
| aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"} | |
| onClick={onToggleCollapsed} | |
| className="hidden h-8 w-8 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 hover:text-slate-950 lg:flex" | |
| > | |
| {collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />} | |
| </button> | |
| )} | |
| </div> | |
| <nav className="min-h-0 flex-1 space-y-1 overflow-y-auto" aria-label="Primary workspace navigation"> | |
| {navItems.map((item) => { | |
| const Icon = item.icon; | |
| const selected = active === item.key; | |
| return ( | |
| <div key={item.key}> | |
| <button | |
| type="button" | |
| title={item.description} | |
| aria-label={`${item.label}: ${item.description}`} | |
| onClick={() => onChange(item.key)} | |
| className={cx( | |
| "group flex w-full items-center rounded-lg py-2.5 text-left text-sm transition", | |
| collapsed ? "justify-center px-2" : "gap-3 px-3", | |
| selected | |
| ? "bg-slate-950 text-white shadow-sm" | |
| : "text-slate-600 hover:bg-slate-100 hover:text-slate-950" | |
| )} | |
| > | |
| <Icon className="h-4 w-4 flex-shrink-0" /> | |
| {!collapsed && <span className="min-w-0 flex-1 truncate font-medium">{item.label}</span>} | |
| {!collapsed && item.badge && ( | |
| <span | |
| className={cx( | |
| "hidden rounded-md px-1.5 py-0.5 text-[10px] font-semibold lg:inline-flex", | |
| selected ? "bg-white/15 text-white" : "bg-emerald-50 text-emerald-700" | |
| )} | |
| > | |
| {item.badge} | |
| </span> | |
| )} | |
| </button> | |
| {item.key === "analysis-agent" && showAnalysisList && ( | |
| <div className="mt-2 space-y-2 border-l border-slate-200 pl-3 pr-1"> | |
| <button | |
| type="button" | |
| title="Create new analysis" | |
| aria-label="Create new analysis" | |
| onClick={onNewAnalysis} | |
| className="flex w-full items-center gap-2 rounded-md border border-slate-200 bg-white px-2.5 py-2 text-left text-xs font-medium text-slate-600 transition hover:border-emerald-200 hover:bg-emerald-50 hover:text-emerald-700 disabled:cursor-not-allowed disabled:opacity-60" | |
| disabled={!onNewAnalysis} | |
| > | |
| <Plus className="h-3.5 w-3.5" /> | |
| <span className="truncate">New analysis</span> | |
| </button> | |
| <div className="space-y-2 pb-2"> | |
| <p className="px-1 text-[11px] font-semibold uppercase text-slate-400">Saved analyses</p> | |
| {loadingAnalyses ? ( | |
| <div className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-500">Loading analyses</div> | |
| ) : analyses.length === 0 ? ( | |
| <div className="rounded-md border border-dashed border-slate-200 bg-slate-50 px-3 py-3 text-xs leading-5 text-slate-500"> | |
| No analyses yet. Create one after knowledge is ready. | |
| </div> | |
| ) : ( | |
| analyses.map((analysis) => { | |
| const selectedAnalysis = activeAnalysisId === analysis.id; | |
| return ( | |
| <div | |
| key={analysis.id} | |
| className={cx( | |
| "group flex items-start gap-1 rounded-md border transition", | |
| selectedAnalysis ? "border-emerald-200 bg-emerald-50" : "border-slate-200 bg-white hover:bg-slate-50" | |
| )} | |
| > | |
| <button | |
| type="button" | |
| title={analysis.analysis_title} | |
| onClick={() => onSelectAnalysis?.(analysis)} | |
| className="min-w-0 flex-1 rounded-md p-2.5 text-left" | |
| > | |
| <span className="block truncate text-sm font-medium text-slate-900">{analysis.analysis_title}</span> | |
| <span className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">{analysis.objective}</span> | |
| <span className="mt-2 block text-[11px] text-slate-400">{formatDateTime(analysis.updated_at || analysis.created_at)}</span> | |
| </button> | |
| {onDeleteAnalysis && ( | |
| <button | |
| type="button" | |
| title={`Delete ${analysis.analysis_title}`} | |
| aria-label={`Delete ${analysis.analysis_title}`} | |
| onClick={() => onDeleteAnalysis(analysis)} | |
| className="mr-1 mt-2 rounded p-1 text-slate-400 opacity-0 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 focus:opacity-100" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </nav> | |
| <div className="mt-3 border-t border-slate-100 pt-3"> | |
| {!collapsed && ( | |
| <div className="mb-2 px-2 text-xs text-slate-500"> | |
| <p className="truncate font-medium text-slate-700">{userName ?? "Signed in"}</p> | |
| <p>Secure session</p> | |
| </div> | |
| )} | |
| <button | |
| type="button" | |
| title={isDark ? "Switch to light mode" : "Switch to dark mode"} | |
| aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} | |
| onClick={() => setTheme(isDark ? "light" : "dark")} | |
| className={cx( | |
| "mb-1 flex w-full items-center rounded-lg py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-950", | |
| collapsed ? "justify-center px-2" : "gap-3 px-3" | |
| )} | |
| > | |
| {isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} | |
| {!collapsed && (isDark ? "Light mode" : "Dark mode")} | |
| </button> | |
| <button | |
| type="button" | |
| title="Logout dari workspace" | |
| aria-label="Logout from Data Eyond workspace" | |
| onClick={onLogout} | |
| className={cx( | |
| "flex w-full items-center rounded-lg py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-950", | |
| collapsed ? "justify-center px-2" : "gap-3 px-3" | |
| )} | |
| > | |
| <LogOut className="h-4 w-4" /> | |
| {!collapsed && "Logout"} | |
| </button> | |
| </div> | |
| </aside> | |
| ); | |
| } | |