From 227dd0ad0ae64cb1e8559ed050667b50d605d95e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 29 Jun 2026 12:59:16 +0800 Subject: [PATCH 1/7] chore: resolve lint warnings, missing badge components, and E2E timing issues for production readiness --- scripts/classify-documents.ts | 2 +- src/app/mockups/evidence-option/page.tsx | 870 ++++++------------ src/components/ClinicalDashboard.tsx | 85 +- .../clinical-dashboard/dashboard-shell.tsx | 13 +- .../medication-prescribing-workspace.tsx | 372 +++++--- .../settings-search-mockup-page.tsx | 14 +- src/components/ui/sheet.tsx | 26 +- src/lib/document-organization.ts | 89 +- src/lib/rag.ts | 317 ++----- src/lib/source-governance.ts | 2 +- tests/source-governance.test.ts | 4 +- 11 files changed, 773 insertions(+), 1021 deletions(-) diff --git a/scripts/classify-documents.ts b/scripts/classify-documents.ts index 1340885e97..d9740730c1 100644 --- a/scripts/classify-documents.ts +++ b/scripts/classify-documents.ts @@ -110,7 +110,7 @@ async function loadDocuments(supabase: SupabaseAdmin, args: ClassifyArgs) { .from("documents") .select("id,owner_id,title,file_name,source_path,status,metadata") .eq("status", "indexed") - .order("created_at", { ascending: true }) + .order("id", { ascending: true }) .range(args.documentId ? 0 : args.offset, args.documentId ? 0 : args.offset + args.limit - 1); if (args.documentId) query = query.eq("id", args.documentId); diff --git a/src/app/mockups/evidence-option/page.tsx b/src/app/mockups/evidence-option/page.tsx index 1c1108b856..df0e23f5d1 100644 --- a/src/app/mockups/evidence-option/page.tsx +++ b/src/app/mockups/evidence-option/page.tsx @@ -1,134 +1,120 @@ import Image from "next/image"; import type { Metadata } from "next"; import { - ArrowRight, BadgeCheck, - BookOpen, CheckCircle2, ChevronRight, Copy, ExternalLink, - Eye, FileImage, FileSearch, FileText, Filter, + FolderOpen, + Image as ImageIcon, Layers3, - Library, ListChecks, - PanelRightOpen, Quote, Search, ShieldCheck, Sparkles, - SplitSquareHorizontal, Table2, - Target, type LucideIcon, } from "lucide-react"; import type { ReactNode } from "react"; export const metadata: Metadata = { - title: "Premium Evidence Mockups - Clinical KB", - description: "Premium ChatGPT-style evidence workspace mockups.", + title: "Evidence Option Mockups - Clinical KB", + description: "Clinical KB evidence mockups in the app visual style.", }; -type Tone = "green" | "amber" | "blue" | "graphite"; +type Tone = "source" | "success" | "warning" | "info" | "neutral"; const evidenceItems = [ { title: "ANC monitoring frequency table", kind: "Table", source: "Clozapine physical health protocol", - meta: "p. 12 - 97% direct support", - summary: "Baseline and ongoing blood monitoring cadence with hold thresholds.", + meta: "p.12 - 97% direct", + body: "Blood monitoring cadence, baseline checks, and action thresholds.", icon: Table2, - tone: "green", + tone: "source", }, { title: "Constipation escalation passage", kind: "Quote", source: "Clozapine safety bulletin", - meta: "p. 4 - exact quote", - summary: "Same-day review wording for severe constipation and abdominal symptoms.", + meta: "p.4 - exact quote", + body: "Same-day review wording for severe constipation symptoms.", icon: Quote, - tone: "blue", + tone: "info", }, { - title: "Observation pathway crop", + title: "Observation pathway image", kind: "Image", source: "Acute behavioural disturbance pathway", - meta: "p. 8 - visual extraction", - summary: "Diagram labels, observation frequency, and escalation checkpoints.", + meta: "p.8 - visual", + body: "Page crop, diagram labels, and neighbouring source text.", icon: FileImage, - tone: "graphite", + tone: "neutral", }, { - title: "Lithium toxicity threshold span", + title: "Lithium toxicity document span", kind: "Document span", source: "Lithium monitoring guideline", - meta: "p. 6 - review due", - summary: "Older local passage retained with date and governance warning.", + meta: "p.6 - review due", + body: "Threshold wording retained with governance warning.", icon: FileText, - tone: "amber", + tone: "warning", }, ] as const; const tableRows = [ ["Baseline", "FBC, LFT, U&E, lipids, glucose", "Before initiation"], - ["Weekly", "FBC/ANC", "Initial titration phase"], - ["Escalate", "Chest pain, fever, severe constipation", "Urgent medical review"], + ["Weekly", "FBC/ANC", "Initial titration"], + ["Escalate", "Chest pain, fever, constipation", "Urgent review"], ] as const; -const sourceKinds = [ +const evidenceTypes = [ ["Tables", "621", Table2], ["Quotes", "8,904", Quote], ["Images", "2,840", FileImage], ["PDF regions", "4,221", FileSearch], - ["Document pages", "1,834", FileText], + ["Documents", "1,834", FileText], ] as const; -function tone(t: Tone) { - if (t === "green") return "border-emerald-200 bg-emerald-50 text-emerald-800"; - if (t === "amber") return "border-amber-200 bg-amber-50 text-amber-800"; - if (t === "blue") return "border-sky-200 bg-sky-50 text-sky-800"; - return "border-zinc-200 bg-zinc-100 text-zinc-700"; +function toneClass(tone: Tone) { + if (tone === "source") return "border-[color:var(--clinical-chat-teal)]/25 bg-[color:var(--clinical-chat-teal-soft)] text-[color:var(--clinical-chat-teal)]"; + if (tone === "success") return "border-[color:var(--success-border)] bg-[color:var(--success-soft)] text-[color:var(--success)]"; + if (tone === "warning") return "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; + if (tone === "info") return "border-[color:var(--info-border)] bg-[color:var(--info-soft)] text-[color:var(--info)]"; + return "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]"; } -function Badge({ children, variant = "graphite" }: { children: ReactNode; variant?: Tone }) { +function Pill({ children, tone = "neutral" }: { children: ReactNode; tone?: Tone }) { return ( - + {children} ); } -function Glyph({ icon: Icon, variant = "graphite" }: { icon: LucideIcon; variant?: Tone }) { +function IconTile({ icon: Icon, tone = "source" }: { icon: LucideIcon; tone?: Tone }) { return ( - + ); } -function GhostButton({ - children, - icon: Icon, - primary = false, -}: { - children: ReactNode; - icon?: LucideIcon; - primary?: boolean; -}) { +function Action({ children, icon: Icon, primary = false }: { children: ReactNode; icon?: LucideIcon; primary?: boolean }) { return ( @@ -209,80 +209,74 @@ function EvidenceCard({ item, compact = false }: { item: (typeof evidenceItems)[ function EvidenceTable() { return ( -
-
- Stage - Evidence - Action +
+
+ Stage + Evidence + Action
{tableRows.map(([stage, evidence, action]) => ( -
- {stage} - {evidence} - {action} +
+ {stage} + {evidence} + {action}
))}
); } -function TopNav() { - return ( -
-
- - - - Clinical KB Evidence -
-
- {["Home", "Search", "Tables", "Images", "Quotes"].map((item, index) => ( - - {item} - - ))} -
-
- ); -} - -function HomeOne() { +function MockupHome() { return ( - -
- -
-
-
-

Evidence workspace

-

- Ask across every document part with source-grade confidence. -

-

- Search images, extracted tables, exact quotes, PDF regions, and source spans without leaving the answer - workflow. -

-
- -
+ +
+ +
+ +
+ +
+
+ +

Tables as first-class evidence

+

Rows, image crop, source page, confidence, and provenance together.

+
+
- ))} +
+
+ +

Images with inspection tools

+

Page crops, labels, region highlights, and neighbouring text.

+ Risk flow evidence preview +
-
-
); } -function toolMatchesQuery(tool: ToolItem, query: string) { - const normalized = query.trim().toLowerCase(); - if (!normalized) return true; - return [tool.title, tool.description, tool.category, tool.status, tool.id] - .join(" ") - .toLowerCase() - .includes(normalized); -} - -function toolStatusLabel(tool: ToolItem) { - if (tool.status === "coming-soon") return "Soon"; - if (tool.status === "offline") return "Paused"; - if (tool.status === "beta") return "Preview"; - return "Ready"; -} - -const TOOL_ICON_MAP: Record> = { - Brain, - ClipboardList, - Search, - FileImage, - FileText, - HeartHandshake, - Network, - Pill, - UploadCloud, - BookOpen, - Quote, - ShieldAlert, - Target, - ClipboardCheck, - ListChecks, - Sparkles, - Clipboard: ClipboardCheck, - ExternalLink, -}; - -function getToolIcon(iconName: string) { - return TOOL_ICON_MAP[iconName] || Wrench; -} - -function cleanToolHrefLabel(href: string) { - try { - if (href.startsWith("/")) return href; - const url = new URL(href); - return url.host; - } catch { - return href; - } +function SettingsChip({ label }: { label: string }) { + return ( + + {label} + + ); } -function ToolsHub({ query, onClearQuery }: { query: string; onClearQuery: () => void }) { - const normalizedQuery = query.trim(); - const visibleTools = toolCatalog.filter((tool) => toolMatchesQuery(tool, normalizedQuery)); - const onlineTools = toolCatalog.filter((tool) => tool.status === "online" || tool.status === "beta").length; - const internalTools = toolCatalog.filter((tool) => tool.target === "internal").length; - const activeFilterCount = normalizedQuery ? 1 : 0; - +function SettingsSummaryTile({ + icon: Icon, + label, + value, + emphasized = false, +}: { + icon: typeof UserRound; + label: string; + value: string; + emphasized?: boolean; +}) { return ( -
-
-
- - - -
-

Tools

-

- Search the existing clinical applications registry and launch the right workflow without leaving the - dashboard shell. -

-
-
- -
- {[ - { label: "Tools", value: toolCatalog.length, icon: Wrench }, - { label: "Ready", value: onlineTools, icon: CheckCircle2 }, - { label: "In-app", value: internalTools, icon: LayoutList }, - ].map((stat) => { - const Icon = stat.icon; - return ( -
-
- - {stat.label} -
-

- {stat.value} -

-
- ); - })} -
-
- -
- - Showing {visibleTools.length} of {toolCatalog.length} tools - -
- {activeFilterCount > 0 && ( - +
+
+ - - Full launcher - -
+ > + + + + {label} + {value} +
+
+ ); +} - {visibleTools.length === 0 ? ( - - ) : ( -
- {visibleTools.map((tool) => { - const disabled = tool.status === "offline" || tool.status === "coming-soon"; - const launchLabel = `Launch ${tool.title}`; - const launchClassName = cn( - "inline-flex min-h-10 items-center justify-center gap-2 rounded-lg px-3 text-sm font-bold shadow-[var(--shadow-inset)] transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", - disabled - ? "border border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-soft)]" - : "bg-[color:var(--clinical-chat-teal)] text-white hover:bg-[color:var(--clinical-chat-teal-strong)]", - ); - const ToolIcon = getToolIcon(tool.icon); - const isOnline = tool.status === "online" || tool.status === "beta"; - const statusColorClass = - tool.status === "online" - ? "bg-emerald-500 shadow-[0_0_8px_#10b981]" - : tool.status === "beta" - ? "bg-amber-500 shadow-[0_0_8px_#f59e0b]" - : "bg-slate-400"; +function SettingsRow({ + icon: Icon, + label, + value, + active = false, + onClick, + actionLabel, +}: { + icon: typeof UserRound; + label: string; + value: string; + active?: boolean; + onClick?: () => void; + actionLabel?: string; +}) { + const content = ( + <> + + + + + {label} + {value ? ( + + {value} + + ) : null} + + + + ); - return ( -
-
- - - - - {toolStatusLabel(tool)} - -
+ const className = + "flex min-h-12 w-full items-center gap-3 border-b border-[color:var(--border)] px-3 text-left last:border-b-0 transition hover:bg-[color:var(--surface)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] sm:px-0 sm:hover:bg-transparent"; + const testId = `settings-row-${label + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "")}`; -
-

{tool.title}

-

- {tool.description} -

-
- {tool.category} - - {tool.target === "internal" ? "In-app" : "Connected"} - -
-
- - - {cleanToolHrefLabel(tool.href)} - -
-
+ if (onClick) { + return ( + + ); + } - {disabled ? ( - {tool.disabledHint ?? "Unavailable"} - ) : tool.target === "internal" ? ( - - Launch - - - ) : ( - - Launch - - - )} -
- ); - })} -
- )} + return ( +
+ {content}
); } +function ToolsHub({ query, onQueryChange }: { query: string; onQueryChange: (nextQuery: string) => void }) { + return ; +} + type MobileSectionFabItem = { label: string; description: string; @@ -6256,6 +6365,7 @@ function MobileSectionFab({ const [open, setOpen] = useState(false); const [active, setActive] = useState(false); const buttonRef = useRef(null); + const panelRef = useRef(null); const panelId = "mobile-section-fab-menu"; const labelId = "mobile-section-fab-label"; const activeItem = items.find((item) => item.href === activeHash) ?? items[0]; @@ -6268,20 +6378,14 @@ function MobileSectionFab({ window.requestAnimationFrame(() => buttonRef.current?.focus()); } }, []); + const dismissMobileSectionMenu = useCallback(() => closeMenu(), [closeMenu]); - useEffect(() => { - if (!open) return; - - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - event.preventDefault(); - closeMenu(); - } - } - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [closeMenu, open]); + useDismissableLayer({ + enabled: open, + refs: [buttonRef, panelRef], + restoreFocusRef: buttonRef, + onDismiss: dismissMobileSectionMenu, + }); useEffect(() => { const mediaQuery = window.matchMedia(mobileSectionFabMediaQuery); @@ -6358,6 +6462,7 @@ function MobileSectionFab({
setGuideOpen(true), []); + const closeDashboardTransientSurfaces = useCallback( + (except?: "guide" | "settings" | "mobileSidebar" | "documents" | "upload") => { + if (except !== "guide") setGuideOpen(false); + if (except !== "settings") setSettingsOpen(false); + if (except !== "mobileSidebar") setMobileSidebarOpen(false); + if (except !== "documents") setDocumentsDrawerOpen(false); + if (except !== "upload") setUploadDrawerOpen(false); + }, + [], + ); + const openGuide = useCallback(() => { + closeDashboardTransientSurfaces("guide"); + setGuideOpen(true); + }, [closeDashboardTransientSurfaces]); const closeGuide = useCallback(() => setGuideOpen(false), []); - const openSettings = useCallback(() => setSettingsOpen(true), []); + const openSettings = useCallback(() => { + closeDashboardTransientSurfaces("settings"); + setSettingsOpen(true); + }, [closeDashboardTransientSurfaces]); const closeSettings = useCallback(() => setSettingsOpen(false), []); const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]); const prefetchApplications = useCallback(() => { - router.prefetch("/applications"); - void import("@/components/applications-launcher-page"); + router.prefetch("/?mode=tools"); }, [router]); - const openLibraryHealthTarget = useCallback((target: LibraryHealthTarget) => { - const targetId = - target === "documents" - ? "dashboard-documents-drawer" - : target === "setup" - ? "dashboard-setup-section" - : "dashboard-indexing-section"; - - if (target === "documents") { - setDocumentDrawerStatusFilter("indexed"); - setDocumentsDrawerMode("admin"); - setDocumentsDrawerOpen(true); - } else if (target === "indexing") { - setUploadMobileTab("jobs"); - setIndexingMonitorFilter("active"); - setUploadDrawerOpen(true); - } else if (target === "failures") { - setUploadMobileTab("jobs"); - setIndexingMonitorFilter("failed"); - setUploadDrawerOpen(true); - } else { - setUploadMobileTab("setup"); - setIndexingMonitorFilter("all"); - setUploadDrawerOpen(true); - } + const openLibraryHealthTarget = useCallback( + (target: LibraryHealthTarget) => { + const targetId = + target === "documents" + ? "dashboard-documents-drawer" + : target === "setup" + ? "dashboard-setup-section" + : "dashboard-indexing-section"; + + if (target === "documents") { + closeDashboardTransientSurfaces("documents"); + setDocumentDrawerStatusFilter("indexed"); + setDocumentsDrawerMode("admin"); + setDocumentsDrawerOpen(true); + } else if (target === "indexing") { + closeDashboardTransientSurfaces("upload"); + setUploadMobileTab("jobs"); + setIndexingMonitorFilter("active"); + setUploadDrawerOpen(true); + } else if (target === "failures") { + closeDashboardTransientSurfaces("upload"); + setUploadMobileTab("jobs"); + setIndexingMonitorFilter("failed"); + setUploadDrawerOpen(true); + } else { + closeDashboardTransientSurfaces("upload"); + setUploadMobileTab("setup"); + setIndexingMonitorFilter("all"); + setUploadDrawerOpen(true); + } - window.setTimeout(() => { - document.getElementById(targetId)?.scrollIntoView({ behavior: "smooth", block: "start" }); - }, 0); - }, []); + window.setTimeout(() => { + document.getElementById(targetId)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 0); + }, + [closeDashboardTransientSurfaces], + ); useEffect(() => { const timeoutId = window.setTimeout(prefetchApplications, 250); @@ -7728,6 +7855,7 @@ export function ClinicalDashboard({ } function openDocumentsDrawer(mode: DocumentDrawerMode) { + closeDashboardTransientSurfaces("documents"); setSearchMode("documents"); setDocumentDrawerStatusFilter("indexed"); setDocumentsDrawerMode(mode); @@ -7758,6 +7886,7 @@ export function ClinicalDashboard({ }); return; } + closeDashboardTransientSurfaces("upload"); setSearchMode("documents"); setDocumentsDrawerMode("admin"); setUploadDrawerOpen(true); @@ -7771,6 +7900,7 @@ export function ClinicalDashboard({ } function openEvidenceDrawer() { + closeDashboardTransientSurfaces(); const drawer = document.getElementById("answer-evidence-drawer") as HTMLDetailsElement | null; if (!drawer) { setActionNotice({ @@ -7979,7 +8109,7 @@ export function ClinicalDashboard({ activeModeResultKind === "favourites" ? favouritePrototypeCount : activeModeResultKind === "tools" - ? toolCatalog.length + ? applicationsLauncherItemCount : activeModeResultKind === "documents" ? documentMatches.length : null, @@ -8150,6 +8280,7 @@ export function ClinicalDashboard({ collapsed={sidebarCollapsed} recentQueries={recentQueries} identity={sidebarIdentity} + activeMode={searchMode} onCollapsedChange={setSidebarCollapsed} onNewChat={startNewChat} onPickRecent={pickRecentQuery} @@ -8180,8 +8311,16 @@ export function ClinicalDashboard({ onToggleScope={toggleDocumentScope} onOpenUpload={openUploadDrawer} onOpenEvidence={openEvidenceDrawer} + onOpenRecentDocuments={openRecentDocuments} + onOpenLibrary={openSourceLibrary} + onOpenSourcePdf={openSourcePdfBrowser} onNewChat={startNewChat} - onOpenMobileSidebar={() => setMobileSidebarOpen(true)} + onOpenMobileSidebar={() => { + closeDashboardTransientSurfaces("mobileSidebar"); + setMobileSidebarOpen(true); + }} + onOpenSettings={openSettings} + identity={sidebarIdentity} onToggleTheme={toggleTheme} queryModeOptions={clinicalQueryModeOptions} queryInputRef={composerInputRef} @@ -8269,7 +8408,7 @@ export function ClinicalDashboard({ } /> ) : activeModeResultKind === "tools" ? ( - setQuery("")} /> + ) : activeModeResultKind === "documents" ? ( searchMode === "prescribing" ? ( (null); const timeoutRef = useRef(null); + const rootRef = useRef(null); + const triggerRef = useRef(null); const clearNotice = useCallback(() => { if (timeoutRef.current) { @@ -56,8 +59,17 @@ export function DashboardFloatingFab() { // component is gone (leaked timer / stray setState). useEffect(() => clearNotice, [clearNotice]); + const dismissQuickActions = useCallback(() => setOpen(false), []); + + useDismissableLayer({ + enabled: open, + refs: [rootRef], + restoreFocusRef: triggerRef, + onDismiss: dismissQuickActions, + }); + return ( -
+
setOpen(false)} className={cn(floatingControl, "h-9 min-h-9 px-3 text-xs", !open && "hidden")} > - - Applications + + Tools {copyNotice && (

@@ -106,6 +118,7 @@ export function DashboardFloatingFab() { )}

-

Related apps

+

{copy.relatedHeading}

{related.map((relatedApp) => { const Icon = relatedApp.icon; @@ -825,14 +892,29 @@ function ApplicationsHeader({ ); } -export function ApplicationsLauncherPage() { - const [query, setQuery] = useState(""); +type ApplicationsLauncherWorkspaceProps = { + variant?: LauncherVariant; + query?: string; + onQueryChange?: (query: string) => void; + className?: string; +}; + +export function ApplicationsLauncherWorkspace({ + variant = "standalone", + query: controlledQuery, + onQueryChange, + className, +}: ApplicationsLauncherWorkspaceProps) { + const [uncontrolledQuery, setUncontrolledQuery] = useState(""); const [activeFilter, setActiveFilter] = useState<(typeof filterOptions)[number]["id"]>("all"); const [selectedId, setSelectedId] = useState("clinical-kb-search"); const [pinnedIds, setPinnedIds] = useState(seedPinnedIds); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [modeMenuOpen, setModeMenuOpen] = useState(false); const [mobileDetailOpen, setMobileDetailOpen] = useState(false); + const isDashboardTools = variant === "dashboard-tools"; + const copy = isDashboardTools ? dashboardToolsLauncherCopy : standaloneLauncherCopy; + const query = controlledQuery ?? uncontrolledQuery; const normalizedQuery = query.trim().toLowerCase(); const pinnedApps = pinnedIds.map(appById); @@ -865,33 +947,61 @@ export function ApplicationsLauncherPage() { } } + function updateQuery(nextQuery: string) { + if (controlledQuery === undefined) { + setUncontrolledQuery(nextQuery); + } + onQueryChange?.(nextQuery); + } + function submitFooterSearch(event: FormEvent) { event.preventDefault(); const firstMatch = filteredApps[0]; if (firstMatch) selectApplication(firstMatch.id); } - return ( -
- - + const workspace = ( + <> + {isDashboardTools ? ( +
+
+ + + +
+

+ {copy.heading} +

+

+ {copy.description} +

+
+
-
+
+ + {normalizedQuery ? ( + + ) : null} +
+
+ ) : (

- Applications + {copy.heading}

- Open the clinical applications and connected workflows you use for assessment, formulation, prescribing, - documents, and saved workflows. + {copy.description}

@@ -900,17 +1010,17 @@ export function ApplicationsLauncherPage() { @@ -921,112 +1031,129 @@ export function ApplicationsLauncherPage() { type="submit" disabled={filteredApps.length === 0} className={chatSendButton} - aria-label="Open selected application" + aria-label={copy.openSelectedAriaLabel} > Open
+ )} -
-
- +
+
+ -
-
- -

All applications

-
+
+
+ +

{copy.allSectionLabel}

+
-
- Application - Last used - Status - Action - -
+
+ {copy.allColumnLabel} + Last used + Status + Action + +
-
- {filteredApps.map((app) => ( - - ))} + {filteredApps.length === 0 ? ( +
+

{copy.emptyTitle}

+

{copy.emptyBody}

+ ) : ( + <> +
+ {filteredApps.map((app) => ( + + ))} +
-
- {filteredApps.map((app) => ( - - ))} -
+
+ {filteredApps.map((app) => ( + + ))} +
+ + )} -

- Showing {filteredApps.length > 0 ? "1" : "0"} to {filteredApps.length} of {launcherApps.length}{" "} - applications -

-
- -
-
-

Recent activity

- -
-
- {recentActivity.slice(0, 3).map((item) => { - const Icon = item.icon; - return ( -
+ +
+
+

Recent activity

+ +
+
+ {recentActivity.slice(0, 3).map((item) => { + const Icon = item.icon; + return ( + - ); - })} -
-
-
+ {item.date} + + + ); + })} +
+
+
-
- -
+
+
- +
setMobileDetailOpen(false)} labelledBy="selected-application-sheet-heading" - closeLabel="Close selected application" + closeLabel={copy.closeSelectedLabel} contentClassName="lg:hidden rounded-t-[1.75rem] bg-[color:var(--surface-lux)]" bodyClassName="px-5 pb-6 pt-4 sm:px-5" portal @@ -1037,11 +1164,42 @@ export function ApplicationsLauncherPage() { onTogglePin={togglePin} onClose={() => setMobileDetailOpen(false)} headingId="selected-application-sheet-heading" + copy={copy} testId="selected-application-sheet-panel" variant="sheet" /> + + ); + if (isDashboardTools) { + return ( +
+ {workspace} +
+ ); + } + + return ( +
+ + + +
+ {workspace} +
); } + +export function ApplicationsLauncherPage() { + return ; +} diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx index 8aa0a0742e..48bc60c18f 100644 --- a/src/components/clinical-dashboard/favourites-hub.tsx +++ b/src/components/clinical-dashboard/favourites-hub.tsx @@ -1,7 +1,8 @@ "use client"; import { ArrowUpDown, ChevronDown, Filter, Folder, FolderInput, Heart, Plus, Search, X } from "lucide-react"; -import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; +import { useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; +import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { cn, floatingControl, iconTilePremium, panelSubtle, primaryControl } from "@/components/ui-primitives"; import { favouriteItems, @@ -103,30 +104,12 @@ export function FavouritesHub({ } } - useEffect(() => { - if (!tabMenuOpen) return undefined; - - function handlePointerDown(event: PointerEvent) { - const target = event.target; - if (!(target instanceof Node)) return; - if (!tabMenuRef.current?.contains(target)) setTabMenuOpen(false); - } - - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - event.preventDefault(); - setTabMenuOpen(false); - window.requestAnimationFrame(() => tabButtonRef.current?.focus()); - } - } - - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [tabMenuOpen]); + useDismissableLayer({ + enabled: tabMenuOpen, + refs: [tabMenuRef], + restoreFocusRef: tabButtonRef, + onDismiss: () => setTabMenuOpen(false), + }); return (
diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx index 10390b6727..c8721753f7 100644 --- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx +++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx @@ -116,6 +116,8 @@ export function GlobalMockupSearchShell({ children }: { children: ReactNode }) { onOpenEvidence={() => navigateToMode("evidence", { focus: true })} onNewChat={startNewChat} onOpenMobileSidebar={() => setMobileMenuOpen(true)} + onOpenSettings={() => setMobileMenuOpen(true)} + identity={{ displayName: "Guest", initials: "G", detail: "Not signed in", signedIn: false }} onToggleTheme={toggleTheme} queryModeOptions={mockupQueryModeOptions} scopeVariant="placeholder" diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 2f2f773824..3863475276 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -18,7 +18,6 @@ import { CheckCircle2, ChevronDown, FileText, - Filter, Globe2, Heart, ListChecks, @@ -39,6 +38,14 @@ import { } from "lucide-react"; import { DocumentTagCloud } from "@/components/DocumentTagCloud"; +import { useDismissableLayer } from "@/components/use-dismissable-layer"; +import { + ModeActionPopup, + modeActionItemsFor, + type ModeActionId, + type ModeActionItem, + type ModeActionSetId, +} from "@/components/clinical-dashboard/mode-action-popup"; import { cn, chatComposerIconButton, @@ -75,6 +82,25 @@ const appModeIcons: Record = { tools: Wrench, }; +const medicationModeActionItems: readonly ModeActionItem[] = [ + { + id: "medication-dose", + label: "Dose", + description: "Check dosing and thresholds", + icon: CalendarDays, + primary: true, + }, + { id: "medication-safety", label: "Safety", description: "Contraindications and cautions", icon: ShieldCheck }, + { + id: "medication-monitoring", + label: "Monitoring", + shortLabel: "Monitor", + description: "Baseline and ongoing checks", + icon: Activity, + }, + { id: "medication-access", label: "Access", description: "Documentation and eligibility", icon: Lock }, +]; + function splitFilterText(value: string) { return value .split(",") @@ -98,6 +124,13 @@ function documentScopeMeta(document: ClinicalDocument) { return `${fileName} · ${document.page_count ?? "?"} pages`; } +type HeaderIdentity = { + displayName: string; + initials: string; + detail: string; + signedIn: boolean; +}; + export function MasterSearchHeader({ documents, documentTotal, @@ -120,8 +153,13 @@ export function MasterSearchHeader({ onScopeOpenChange, onOpenUpload, onOpenEvidence, + onOpenRecentDocuments, + onOpenLibrary, + onOpenSourcePdf, onNewChat, onOpenMobileSidebar, + onOpenSettings, + identity, onToggleTheme, queryModeOptions, scopeVariant = "full", @@ -150,8 +188,13 @@ export function MasterSearchHeader({ onScopeOpenChange?: (open: boolean) => void; onOpenUpload?: () => void; onOpenEvidence?: () => void; + onOpenRecentDocuments?: () => void; + onOpenLibrary?: () => void; + onOpenSourcePdf?: () => void; onNewChat?: () => void; onOpenMobileSidebar?: () => void; + onOpenSettings: () => void; + identity: HeaderIdentity; onToggleTheme: () => void; queryModeOptions: Array<{ value: ClinicalQueryMode; label: string }>; scopeVariant?: "full" | "placeholder"; @@ -174,12 +217,9 @@ export function MasterSearchHeader({ const [scopeFilter, setScopeFilter] = useState(""); const [scopeOpen, setScopeOpen] = useState(false); const [scopeSheetOpen, setScopeSheetOpen] = useState(false); - const [dailyActionsOpen, setDailyActionsOpen] = useState(false); + const [actionMenuOpen, setActionMenuOpen] = useState(false); const [modeMenuOpen, setModeMenuOpen] = useState(false); const [usesScopeSheet, setUsesScopeSheet] = useState(false); - const dailyActionButtonRef = useRef(null); - const dailyActionsMenuRef = useRef(null); - const firstDailyActionRef = useRef(null); const modeMenuRef = useRef(null); const modeButtonRef = useRef(null); const modeOptionRefs = useRef>([]); @@ -220,81 +260,105 @@ export function MasterSearchHeader({ const submitLabel = trimmedQuery ? selectedSearch.submitBusyLabel : selectedSearch.submitIdleLabel; const queryPlaceholder = selectedSearch.placeholder; const SelectedAppModeIcon = appModeIcons[selectedAppMode.id]; - const dailyActions = - searchMode === "prescribing" - ? ([ - { label: "Dose", icon: CalendarDays }, - { label: "Safety", icon: ShieldCheck }, - { label: "Monitoring", icon: Activity }, - { label: "Access", icon: Lock }, - ] as const) - : ([ - { label: "Search library", icon: Search }, - { label: "Add document", icon: FileText }, - { label: "Scope", icon: Filter }, - { label: "Evidence", icon: ListChecks }, - { label: "Tools", icon: Wrench }, - ] as const); - const dailyActionsTitle = searchMode === "prescribing" ? "Medication checks" : "Daily actions"; - const dailyActionsButtonLabel = searchMode === "prescribing" ? "Open medication checks" : "Open daily actions"; - const dailyActionsDescription = - searchMode === "prescribing" - ? "Choose a dose, safety, monitoring, or access focus." - : "Search, add, scope, evidence, or tools."; + const actionMenuSetId: ModeActionSetId = + searchMode === "documents" || searchMode === "evidence" ? "documents" : searchMode === "tools" ? "tools" : "answer"; + const actionMenuItems = + searchMode === "prescribing" ? medicationModeActionItems : modeActionItemsFor(actionMenuSetId); + const actionMenuTitle = selectedAppMode.label; + const actionMenuButtonLabel = `Open ${selectedAppMode.label.toLowerCase()} options`; function currentUsesScopeSheet() { return window.matchMedia(mobileSheetMediaQuery).matches; } - function runDailyAction(label: (typeof dailyActions)[number]["label"]) { - if (searchMode === "prescribing") { + function openScopePicker() { + setActionMenuOpen(false); + setModeMenuOpen(false); + const nextUsesScopeSheet = currentUsesScopeSheet(); + setUsesScopeSheet(nextUsesScopeSheet); + if (nextUsesScopeSheet) { + setScopeSheetOpen(true); + } else { + setScopeOpen(true); + onScopeOpenChange?.(true); + window.requestAnimationFrame(() => scopeFilterInputRef.current?.focus()); + } + } + + function runModeAction(actionId: ModeActionId) { + if (actionId === "medication-dose") { const medicationQuery = trimmedQuery || "acamprosate renal dose"; - if (label === "Dose") { - onQueryModeChange("dose_threshold_lookup"); - onQueryChange(medicationQuery); - return; - } - if (label === "Safety") { - onQueryModeChange("contraindications_cautions"); - onQueryChange(trimmedQuery || "acamprosate contraindications"); - return; - } - if (label === "Monitoring") { - onQueryModeChange("monitoring_schedule"); - onQueryChange(trimmedQuery || "acamprosate monitoring"); - return; - } - if (label === "Access") { - onQueryModeChange("required_documentation"); - onQueryChange(trimmedQuery || "acamprosate PBS access"); - return; - } + onQueryModeChange("dose_threshold_lookup"); + onQueryChange(medicationQuery); + return; + } + if (actionId === "medication-safety") { + onQueryModeChange("contraindications_cautions"); + onQueryChange(trimmedQuery || "acamprosate contraindications"); + return; } - if (label === "Search library") { + if (actionId === "medication-monitoring") { + onQueryModeChange("monitoring_schedule"); + onQueryChange(trimmedQuery || "acamprosate monitoring"); + return; + } + if (actionId === "medication-access") { + onQueryModeChange("required_documentation"); + onQueryChange(trimmedQuery || "acamprosate PBS access"); + return; + } + + if (actionId === "documents-search" || actionId === "answer-documents") { onSearchModeChange("documents"); return; } - if (label === "Add document") { + if (actionId === "documents-upload") { onOpenUpload?.(); return; } - if (label === "Scope") { - const nextUsesScopeSheet = currentUsesScopeSheet(); - setUsesScopeSheet(nextUsesScopeSheet); - if (nextUsesScopeSheet) { - setScopeSheetOpen(true); - } else { - setScopeOpen(true); - onScopeOpenChange?.(true); - window.requestAnimationFrame(() => scopeFilterInputRef.current?.focus()); - } + if (actionId === "documents-scope") { + openScopePicker(); return; } - if (label === "Evidence") { + if (actionId === "answer-evidence") { onOpenEvidence?.(); return; } - onSearchModeChange("tools"); + if (actionId === "documents-tables") { + onSearchModeChange("documents"); + onQueryChange(trimmedQuery || "table evidence"); + return; + } + if (actionId === "documents-recent") { + onSearchModeChange("documents"); + onOpenRecentDocuments?.(); + return; + } + if (actionId === "documents-status" || actionId === "documents-collections") { + onSearchModeChange("documents"); + onOpenLibrary?.(); + return; + } + if (actionId === "documents-viewer") { + onSearchModeChange("documents"); + onOpenSourcePdf?.(); + return; + } + if (actionId === "answer-new" || actionId === "tools-new") { + onNewChat?.(); + return; + } + if (actionId === "answer-clinical" || actionId === "favourites-answer") { + onSearchModeChange("answer"); + return; + } + if (actionId === "tools-browse" || actionId === "favourites-tools") { + onSearchModeChange("tools"); + return; + } + if (actionId === "tools-favourites" || actionId === "favourites-browse") { + onSearchModeChange("favourites"); + } } function selectAppMode(mode: (typeof appModeDefinitions)[number]) { @@ -317,6 +381,8 @@ export function MasterSearchHeader({ } function openModeMenuWithFocus(index: number) { + setActionMenuOpen(false); + closeScope(false); setModeMenuOpen(true); window.requestAnimationFrame(() => focusModeOption(index)); } @@ -350,6 +416,7 @@ export function MasterSearchHeader({ window.requestAnimationFrame(() => modeButtonRef.current?.focus()); } } + const collectionOptions = useMemo(() => { const values = new Set(); for (const document of documents) { @@ -388,84 +455,28 @@ export function MasterSearchHeader({ onScopeOpenChange?.(scopeOpen || scopeSheetOpen); }, [onScopeOpenChange, scopeOpen, scopeSheetOpen]); - useEffect(() => { - if (!dailyActionsOpen || usesScopeSheet) return undefined; - - function handlePointerDown(event: PointerEvent) { - const target = event.target; - if (!(target instanceof Node)) return; - if (!dailyActionsMenuRef.current?.contains(target)) setDailyActionsOpen(false); - } - - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - event.preventDefault(); - setDailyActionsOpen(false); - window.requestAnimationFrame(() => dailyActionButtonRef.current?.focus()); - } - } - - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [dailyActionsOpen, usesScopeSheet]); - - useEffect(() => { - if (!modeMenuOpen) return undefined; - - function handlePointerDown(event: PointerEvent) { - const target = event.target; - if (!(target instanceof Node)) return; - if (!modeMenuRef.current?.contains(target)) setModeMenuOpen(false); - } - - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - event.preventDefault(); - setModeMenuOpen(false); - window.requestAnimationFrame(() => modeButtonRef.current?.focus()); - } - } - - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [modeMenuOpen]); - - useEffect(() => { - const details = scopeDetailsRef.current; - if (!scopeOpen || !details?.open) return undefined; - - function handlePointerDown(event: PointerEvent) { - const target = event.target; - if (!(target instanceof Node)) return; - if (!scopeDetailsRef.current?.contains(target)) closeScope(false); - } + const dismissModeMenu = useCallback(() => setModeMenuOpen(false), []); + function dismissScope(reason: "outside" | "escape") { + closeScope(reason === "escape"); + } - function handleKeyDown(event: KeyboardEvent) { - if (event.key === "Escape") { - event.preventDefault(); - closeScope(true); - } - } + useDismissableLayer({ + enabled: modeMenuOpen, + refs: [modeMenuRef], + restoreFocusRef: modeButtonRef, + onDismiss: dismissModeMenu, + }); - document.addEventListener("pointerdown", handlePointerDown); - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [closeScope, scopeOpen]); + useDismissableLayer({ + enabled: scopeOpen, + refs: [scopeDetailsRef], + restoreFocusRef: scopeSummaryRef, + onDismiss: dismissScope, + }); function submit(event: FormEvent) { event.preventDefault(); - setDailyActionsOpen(false); + setActionMenuOpen(false); onAsk(); } @@ -702,7 +713,11 @@ export function MasterSearchHeader({ - - AK - - +
@@ -896,49 +928,20 @@ export function MasterSearchHeader({ "fixed inset-x-3 bottom-3 z-40 mx-auto max-w-3xl sm:bottom-4 lg:left-[calc(var(--clinical-sidebar-width,20rem)+2rem)] lg:right-8 lg:max-w-4xl", )} > -
- - {dailyActionsOpen && !usesScopeSheet ? ( -
- {dailyActions.map((item) => { - const Icon = item.icon; - return ( - - ); - })} -
- ) : null} -
+ { + setUsesScopeSheet(currentUsesScopeSheet()); + setModeMenuOpen(false); + closeScope(false); + }} + onAction={runModeAction} + />