diff --git a/frontend/app/(app)/analysis/page.tsx b/frontend/app/(app)/analysis/page.tsx new file mode 100644 index 0000000..739134d --- /dev/null +++ b/frontend/app/(app)/analysis/page.tsx @@ -0,0 +1,10 @@ +import { AnalysisView } from "@/components/analysis/analysis-view"; +import { SidebarInset } from "@/components/ui/sidebar"; + +export default function AnalysisPage() { + return ( + + + + ); +} diff --git a/frontend/app/(app)/layout.tsx b/frontend/app/(app)/layout.tsx index 22962e6..b797bd8 100644 --- a/frontend/app/(app)/layout.tsx +++ b/frontend/app/(app)/layout.tsx @@ -1,4 +1,5 @@ import { AppAuthGuard } from "@/components/auth/app-auth-guard"; +import { CommandPaletteProvider } from "@/components/command-palette"; import { DashboardSidebar } from "@/components/sidebar-02/app-sidebar"; import { SidebarProvider } from "@/components/ui/sidebar"; @@ -9,12 +10,14 @@ export default function AppLayout({ }) { return ( - -
- - {children} -
-
+ + +
+ + {children} +
+
+
); } diff --git a/frontend/app/(auth)/onboarding/page.tsx b/frontend/app/(auth)/onboarding/page.tsx index bdcea5d..36412ce 100644 --- a/frontend/app/(auth)/onboarding/page.tsx +++ b/frontend/app/(auth)/onboarding/page.tsx @@ -1,32 +1,16 @@ "use client"; import { useRouter } from "next/navigation"; -import { type FormEvent, useEffect, useState } from "react"; +import { useEffect } from "react"; -import { AuthShell, Field, FormAlert } from "@/components/auth/auth-ui"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { AuthShell } from "@/components/auth/auth-ui"; +import { CreateClinicForm } from "@/components/clinic/create-clinic-form"; import { authClient } from "@/lib/auth-client"; -import { notify } from "@/lib/toast"; - -function slugify(value: string): string { - return value - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} export default function OnboardingPage() { const router = useRouter(); const { data: session, isPending } = authClient.useSession(); - const [name, setName] = useState(""); - const [slug, setSlug] = useState(""); - const [slugEdited, setSlugEdited] = useState(false); - const [error, setError] = useState(null); - const [submitting, setSubmitting] = useState(false); - // Send unauthenticated users to login. Authenticated users (whether brand // new or creating an additional clinic) stay on this page. useEffect(() => { @@ -34,69 +18,12 @@ export default function OnboardingPage() { if (!session?.user) router.replace("/login"); }, [session, isPending, router]); - const onSubmit = async (event: FormEvent) => { - event.preventDefault(); - if (submitting) return; - setSubmitting(true); - setError(null); - - const finalSlug = (slugEdited ? slug : slugify(name)) || slugify(name); - const { data: org, error: createErr } = await authClient.organization.create( - { name: name.trim(), slug: finalSlug } - ); - - if (createErr || !org) { - const message = createErr?.message ?? "Could not create the clinic."; - setError(message); - notify.error("Couldn't create clinic", message); - setSubmitting(false); - return; - } - - await authClient.organization.setActive({ organizationId: org.id }); - notify.success("Clinic created", `${org.name} is ready.`); - router.push("/"); - }; - return ( -
- {error && {error}} - - { - setName(e.target.value); - if (!slugEdited) setSlug(slugify(e.target.value)); - }} - placeholder="North Side Family Practice" - required - value={name} - /> - - - { - setSlugEdited(true); - setSlug(slugify(e.target.value)); - }} - placeholder="north-side-family-practice" - required - value={slug} - /> - - -
+ router.push("/")} />
); } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 8a1072a..0ed646e 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -15,6 +15,7 @@ export const metadata: Metadata = { title: "temetro — AI assistant for clinicians", description: "Retrieve patient information by simply asking. The open-source AI assistant for clinicians.", + icons: { icon: "/temetro-logo.png", apple: "/temetro-logo.png" }, }; export default function RootLayout({ diff --git a/frontend/components/analysis/analysis-view.tsx b/frontend/components/analysis/analysis-view.tsx new file mode 100644 index 0000000..59fdd6e --- /dev/null +++ b/frontend/components/analysis/analysis-view.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { TrendingDown, TrendingUp } from "lucide-react"; +import type { ReactNode } from "react"; + +import { Sparkline } from "@/components/chat/sparkline"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +// All figures here are mock/placeholder data — there is no analytics backend. +// They illustrate the dashboard layout (clinic profits, patient volume, etc.). + +type Metric = { + label: string; + value: string; + // % change vs the previous period; sign drives the up/down badge. + delta?: number; + points?: number[]; + // Tailwind text-color class tinting the sparkline (via currentColor). + tone?: string; +}; + +function DeltaBadge({ delta }: { delta: number }) { + const up = delta >= 0; + return ( + + {up ? ( + + ) : ( + + )} + {up ? "+" : ""} + {delta}% + + ); +} + +function StatCard({ label, value, delta, points, tone }: Metric) { + return ( + +
+ {label} + {typeof delta === "number" && } +
+
+ {value} +
+ {points && ( +
+ +
+ )} +
+ ); +} + +function Section({ + title, + description, + children, +}: { + title: string; + description: string; + children: ReactNode; +}) { + return ( +
+
+

{title}

+

{description}

+
+
+ {children} +
+
+ ); +} + +const revenue: Metric[] = [ + { + label: "Revenue (this month)", + value: "$48.2k", + delta: 12, + points: [31, 34, 33, 38, 41, 44, 48.2], + tone: "text-emerald-500", + }, + { + label: "Profit margin", + value: "32%", + delta: 4, + points: [24, 26, 25, 28, 30, 31, 32], + tone: "text-emerald-500", + }, + { + label: "Outstanding balances", + value: "$6.4k", + delta: -8, + points: [9.1, 8.4, 8.8, 7.6, 7.0, 6.7, 6.4], + tone: "text-amber-500", + }, +]; + +const volume: Metric[] = [ + { + label: "New patients", + value: "38", + delta: 9, + points: [22, 27, 25, 30, 33, 35, 38], + tone: "text-sky-500", + }, + { + label: "Returning patients", + value: "212", + delta: 3, + points: [188, 196, 201, 199, 205, 209, 212], + tone: "text-sky-500", + }, + { + label: "Active patients", + value: "1,284", + delta: 2, + points: [1190, 1210, 1230, 1242, 1260, 1271, 1284], + tone: "text-sky-500", + }, +]; + +const appointments: Metric[] = [ + { + label: "Appointments this week", + value: "146", + delta: 6, + points: [120, 128, 131, 134, 139, 142, 146], + tone: "text-violet-500", + }, + { label: "No-show rate", value: "4.1%", delta: -2 }, + { label: "Schedule utilization", value: "87%", delta: 5 }, +]; + +const operations: Metric[] = [ + { + label: "Avg. wait time", + value: "14 min", + delta: -11, + points: [22, 21, 19, 18, 17, 15, 14], + tone: "text-amber-500", + }, + { + label: "Prescriptions issued", + value: "318", + delta: 7, + points: [270, 281, 290, 297, 305, 312, 318], + tone: "text-primary", + }, + { label: "Top diagnosis", value: "Hypertension" }, +]; + +export function AnalysisView() { + return ( +
+
+

Analysis

+

+ Clinic performance at a glance. Figures are sample data. +

+
+ +
+ {revenue.map((m) => ( + + ))} +
+ +
+ {volume.map((m) => ( + + ))} +
+ +
+ {appointments.map((m) => ( + + ))} +
+ +
+ {operations.map((m) => ( + + ))} +
+
+ ); +} diff --git a/frontend/components/chat/patient-cards.tsx b/frontend/components/chat/patient-cards.tsx index 719de94..67635d8 100644 --- a/frontend/components/chat/patient-cards.tsx +++ b/frontend/components/chat/patient-cards.tsx @@ -35,6 +35,9 @@ type PatientResultProps = { fileNumber: string; patient?: Patient; onPatientUpdated?: (patient: Patient) => void; + // "row" = horizontal scroll (chat); "column" = full-width vertical stack + // (the Patients detail Sheet). + layout?: "row" | "column"; }; const severityVariant: Record = { @@ -575,6 +578,7 @@ export function PatientResult({ fileNumber, patient, onPatientUpdated, + layout = "row", }: PatientResultProps) { const [editOpen, setEditOpen] = useState(false); // Bumped on open so the editor remounts with the latest patient data. @@ -593,7 +597,14 @@ export function PatientResult({ } return ( -
+
{status === "loading" || !patient ? ( ) : ( diff --git a/frontend/components/clinic/create-clinic-form.tsx b/frontend/components/clinic/create-clinic-form.tsx new file mode 100644 index 0000000..8f689e4 --- /dev/null +++ b/frontend/components/clinic/create-clinic-form.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { type FormEvent, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { authClient } from "@/lib/auth-client"; +import { notify } from "@/lib/toast"; + +function slugify(value: string): string { + return value + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +// Shared "create a clinic" form: name + auto-derived slug, creates the org and +// makes it active. Used by both the onboarding page and the sidebar-footer +// clinic menu's "Create clinic" dialog. +export function CreateClinicForm({ + onCreated, + submitLabel = "Create clinic", +}: { + onCreated?: (org: { id: string; name: string }) => void; + submitLabel?: string; +}) { + const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [slugEdited, setSlugEdited] = useState(false); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + if (submitting) return; + setSubmitting(true); + setError(null); + + const finalSlug = (slugEdited ? slug : slugify(name)) || slugify(name); + const { data: org, error: createErr } = + await authClient.organization.create({ name: name.trim(), slug: finalSlug }); + + if (createErr || !org) { + const message = createErr?.message ?? "Could not create the clinic."; + setError(message); + notify.error("Couldn't create clinic", message); + setSubmitting(false); + return; + } + + await authClient.organization.setActive({ organizationId: org.id }); + notify.success("Clinic created", `${org.name} is ready.`); + onCreated?.(org); + }; + + return ( +
+ {error && ( +

+ {error} +

+ )} +
+ + { + setName(e.target.value); + if (!slugEdited) setSlug(slugify(e.target.value)); + }} + placeholder="North Side Family Practice" + required + value={name} + /> +
+
+ + { + setSlugEdited(true); + setSlug(slugify(e.target.value)); + }} + placeholder="north-side-family-practice" + required + value={slug} + /> +

+ Used in links and invitations. Lowercase letters, numbers and dashes. +

+
+ +
+ ); +} diff --git a/frontend/components/command-palette.tsx b/frontend/components/command-palette.tsx new file mode 100644 index 0000000..c67009a --- /dev/null +++ b/frontend/components/command-palette.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { + ArrowDownIcon, + ArrowUpIcon, + CornerDownLeftIcon, + Search, +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; + +import { + Command, + CommandCollection, + CommandDialog, + CommandDialogPopup, + CommandEmpty, + CommandFooter, + CommandGroup, + CommandGroupLabel, + CommandInput, + CommandItem, + CommandList, + CommandPanel, +} from "@/components/ui/command"; +import { Kbd, KbdGroup } from "@/components/ui/kbd"; +import { useSidebar } from "@/components/ui/sidebar"; +import { navItems } from "@/lib/nav"; + +type CommandPaletteContextValue = { open: () => void }; + +const CommandPaletteContext = createContext( + null, +); + +export function useCommandPalette(): CommandPaletteContextValue { + const ctx = useContext(CommandPaletteContext); + if (!ctx) { + throw new Error( + "useCommandPalette must be used within a CommandPaletteProvider", + ); + } + return ctx; +} + +// Holds the ⌘K palette open state, wires the global shortcut, and renders the +// dialog. Wrap the app shell so the sidebar (and any child) can open it. +export function CommandPaletteProvider({ children }: { children: ReactNode }) { + const router = useRouter(); + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + setOpen((prev) => !prev); + } + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, []); + + // One group ("Go to") matching the COSS command-palette particle shape. + const groups = useMemo( + () => [ + { + value: "pages", + label: t("nav.commandGroup"), + items: navItems.map((item) => ({ + id: item.id, + label: t(item.labelKey), + link: item.link, + Icon: item.icon, + })), + }, + ], + [t], + ); + + type Group = (typeof groups)[number]; + type Item = Group["items"][number]; + + const value = useMemo( + () => ({ open: () => setOpen(true) }), + [], + ); + + const go = (link: string) => { + setOpen(false); + router.push(link); + }; + + return ( + + {children} + + + + + + {t("nav.commandEmpty")} + + {(group: Group) => ( + + {group.label} + + {(item: Item) => ( + go(item.link)} + value={item.label} + > + + {item.label} + + )} + + + )} + + + +
+ + + + + + + + + + {t("nav.commandNavigate")} + + + + + + {t("nav.commandOpen")} + +
+ + Esc + {t("nav.commandClose")} + +
+
+
+
+
+ ); +} + +// Sidebar-footer affordance: shows the ⌘K hint and opens the palette on click. +// Hidden when the sidebar is collapsed to its icon rail. +export function SidebarCommandButton() { + const { open } = useCommandPalette(); + const { state } = useSidebar(); + const { t } = useTranslation(); + + if (state === "collapsed") return null; + + return ( + + ); +} diff --git a/frontend/components/patients/patient-detail-sheet.tsx b/frontend/components/patients/patient-detail-sheet.tsx new file mode 100644 index 0000000..69a95d2 --- /dev/null +++ b/frontend/components/patients/patient-detail-sheet.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { PatientResult } from "@/components/chat/patient-cards"; +import { + Sheet, + SheetHeader, + SheetPanel, + SheetPopup, + SheetTitle, +} from "@/components/ui/sheet"; +import { getPatient, type Patient } from "@/lib/patients"; + +type Status = "loading" | "ready" | "not-found"; + +// Right-side Sheet showing a patient's full record. Reuses the chat's +// PatientResult cards in their vertical (column) layout. Opened from the +// Patients table instead of routing into the AI chat. +export function PatientDetailSheet({ + fileNumber, + open, + onOpenChange, +}: { + fileNumber: string | null; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const [patient, setPatient] = useState(null); + const [status, setStatus] = useState("loading"); + + useEffect(() => { + if (!open || !fileNumber) return; + let active = true; + setStatus("loading"); + setPatient(null); + getPatient(fileNumber) + .then((data) => { + if (!active) return; + setPatient(data); + setStatus(data ? "ready" : "not-found"); + }) + .catch(() => { + if (active) setStatus("not-found"); + }); + return () => { + active = false; + }; + }, [open, fileNumber]); + + const title = + status === "ready" && patient + ? patient.name + : status === "not-found" + ? "Patient not found" + : "Loading patient…"; + + return ( + + + + {title} + + + {fileNumber && ( + + )} + + + + ); +} diff --git a/frontend/components/patients/patients-view.tsx b/frontend/components/patients/patients-view.tsx index 2ea7430..a4f0527 100644 --- a/frontend/components/patients/patients-view.tsx +++ b/frontend/components/patients/patients-view.tsx @@ -1,10 +1,10 @@ "use client"; import { Plus, Search } from "lucide-react"; -import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { PatientFormDialog } from "@/components/chat/patient-form-dialog"; +import { PatientDetailSheet } from "@/components/patients/patient-detail-sheet"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -19,12 +19,15 @@ const statusVariant: Record = { }; export function PatientsView() { - const router = useRouter(); const [query, setQuery] = useState(""); const [addOpen, setAddOpen] = useState(false); // Bumped on open so the create dialog remounts with a fresh file # / form. const [addKey, setAddKey] = useState(0); + // The patient whose record is shown in the side Sheet. + const [selected, setSelected] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + const [allPatients, setAllPatients] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); @@ -57,7 +60,18 @@ export function PatientsView() { (p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q) ); - const open = (fileNumber: string) => router.push(`/?patient=${fileNumber}`); + const open = (fileNumber: string) => { + setSelected(fileNumber); + setSheetOpen(true); + }; + + const refresh = () => { + void listPatients() + .then(setAllPatients) + .catch(() => { + /* keep the current list on a refresh error */ + }); + }; return (
@@ -169,10 +183,23 @@ export function PatientsView() { open(fileNumber)} + onCreated={(fileNumber) => { + refresh(); + open(fileNumber); + }} onOpenChange={setAddOpen} open={addOpen} /> + + { + setSheetOpen(o); + // Reflect any edits made in the Sheet back into the table. + if (!o) refresh(); + }} + open={sheetOpen} + />
); } diff --git a/frontend/components/sidebar-02/app-sidebar.tsx b/frontend/components/sidebar-02/app-sidebar.tsx index 4683e42..153427b 100644 --- a/frontend/components/sidebar-02/app-sidebar.tsx +++ b/frontend/components/sidebar-02/app-sidebar.tsx @@ -9,11 +9,12 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; +import { navItems } from "@/lib/nav"; import { motion } from "framer-motion"; -import { Plus, Settings, Users } from "lucide-react"; import Image from "next/image"; import { useTranslation } from "react-i18next"; import type { Route } from "./nav-main"; +import { SidebarCommandButton } from "@/components/command-palette"; import DashboardNavigation from "@/components/sidebar-02/nav-main"; import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications"; import { NavUser } from "@/components/sidebar-02/nav-user"; @@ -48,26 +49,12 @@ export function DashboardSidebar() { const { t } = useTranslation(); const isCollapsed = state === "collapsed"; - const dashboardRoutes: Route[] = [ - { - id: "new-chat", - title: t("nav.newChat"), - icon: , - link: "/", - }, - { - id: "patients", - title: t("nav.patients"), - icon: , - link: "/patients", - }, - { - id: "settings", - title: t("nav.settings"), - icon: , - link: "/settings", - }, - ]; + const dashboardRoutes: Route[] = navItems.map((item) => ({ + id: item.id, + title: t(item.labelKey), + icon: , + link: item.link, + })); return ( @@ -110,10 +97,11 @@ export function DashboardSidebar() { - - + + + diff --git a/frontend/components/sidebar-02/team-switcher.tsx b/frontend/components/sidebar-02/team-switcher.tsx index 0810b22..201a4f4 100644 --- a/frontend/components/sidebar-02/team-switcher.tsx +++ b/frontend/components/sidebar-02/team-switcher.tsx @@ -1,8 +1,17 @@ "use client"; -import { Building2, ChevronsUpDown, Plus } from "lucide-react"; -import { useRouter } from "next/navigation"; +import { Building2, ChevronsUpDown, Info, Plus } from "lucide-react"; +import { useState } from "react"; +import { CreateClinicForm } from "@/components/clinic/create-clinic-form"; +import { + Dialog, + DialogDescription, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "@/components/ui/dialog"; import { Menu, MenuGroup, @@ -20,15 +29,27 @@ import { } from "@/components/ui/sidebar"; import { authClient } from "@/lib/auth-client"; +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} + // Switches the active clinic (organization). Scopes every subsequent patient -// API call. Replaces the old static "team switcher". +// API call. Lives in the sidebar footer; its menu also opens dialogs to view +// clinic info or create a new clinic. export function OrgSwitcher() { const { isMobile, state } = useSidebar(); const isCollapsed = state === "collapsed"; - const router = useRouter(); const { data: orgs } = authClient.useListOrganizations(); const { data: activeOrg } = authClient.useActiveOrganization(); + const [infoOpen, setInfoOpen] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const setActive = async (organizationId: string) => { if (organizationId === activeOrg?.id) return; await authClient.organization.setActive({ organizationId }); @@ -56,7 +77,7 @@ export function OrgSwitcher() { <>
{activeName} - + Clinic
@@ -67,11 +88,11 @@ export function OrgSwitcher() { - + Clinics {(orgs ?? []).map((org) => ( @@ -90,8 +111,17 @@ export function OrgSwitcher() { router.push("/onboarding")} + disabled={!activeOrg} + onClick={() => setInfoOpen(true)} > +
+ +
+
+ Clinic info +
+
+ setCreateOpen(true)}>
@@ -102,6 +132,39 @@ export function OrgSwitcher() {
+ + {/* Read-only clinic details */} + + + + {activeOrg?.name ?? "Clinic"} + Clinic information + + + + + + + + + + {/* Create a new clinic (replaces the old /onboarding redirect) */} + + + + Create clinic + + Add a new clinic and switch to it. + + + + setCreateOpen(false)} /> + + + ); } diff --git a/frontend/components/ui/kbd.tsx b/frontend/components/ui/kbd.tsx new file mode 100644 index 0000000..59c15c2 --- /dev/null +++ b/frontend/components/ui/kbd.tsx @@ -0,0 +1,31 @@ +import type * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Kbd({ + className, + ...props +}: React.ComponentProps<"kbd">): React.ReactElement { + return ( + + ); +} + +export function KbdGroup({ + className, + ...props +}: React.ComponentProps<"kbd">): React.ReactElement { + return ( + + ); +} diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index 63fbf4f..bf507df 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -47,9 +47,17 @@ "nav": { "newChat": "New chat", "patients": "Patients", + "analysis": "Analysis", "settings": "Settings", "notifications": "Notifications", - "viewAllNotifications": "View all notifications" + "viewAllNotifications": "View all notifications", + "quickNav": "Quick nav", + "commandGroup": "Go to", + "commandPlaceholder": "Search pages…", + "commandEmpty": "No results.", + "commandNavigate": "Navigate", + "commandOpen": "Open", + "commandClose": "Close" }, "settings": { "tabs": { diff --git a/frontend/lib/nav.ts b/frontend/lib/nav.ts new file mode 100644 index 0000000..e8bf238 --- /dev/null +++ b/frontend/lib/nav.ts @@ -0,0 +1,30 @@ +import { + BarChart3, + type LucideIcon, + Plus, + Settings, + Users, +} from "lucide-react"; + +export type NavItem = { + id: string; + // i18n key resolved with t() at render time. + labelKey: string; + icon: LucideIcon; + link: string; +}; + +// Single source of truth for the primary navigation. Consumed by the sidebar +// (components/sidebar-02/app-sidebar.tsx) and the command palette +// (components/command-palette.tsx) so the two never drift. +export const navItems: NavItem[] = [ + { id: "new-chat", labelKey: "nav.newChat", icon: Plus, link: "/" }, + { id: "patients", labelKey: "nav.patients", icon: Users, link: "/patients" }, + { + id: "analysis", + labelKey: "nav.analysis", + icon: BarChart3, + link: "/analysis", + }, + { id: "settings", labelKey: "nav.settings", icon: Settings, link: "/settings" }, +];