| 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> |
| ); |
| } |
|
|