harryagasi
fix: resizable report sidebar, stable chat layout, guided empty state
15d8afd
Raw
History Blame Contribute Delete
10.1 kB
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>
);
}