Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 70 additions & 26 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ import {
toneInfo,
} from "@/components/ui-primitives";
import { useAuthSession } from "@/lib/supabase/client";
import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
import { CrossModeLinksSection } from "@/components/clinical-dashboard/cross-mode-links";
import { useEventCallback } from "@/components/clinical-dashboard/use-event-callback";
import { useHasEverBeenTrue } from "@/components/clinical-dashboard/use-has-ever-been-true";
import { AuthPanel } from "@/components/clinical-dashboard/auth-panel";
import { buildMobileSectionFabState, MobileSectionFab, ToolsHub } from "@/components/clinical-dashboard/dashboard-nav";
import { SettingsDialog } from "@/components/clinical-dashboard/settings-dialog";
import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed";
import { useTheme } from "@/components/clinical-dashboard/use-theme";
import {
Expand All @@ -54,18 +53,18 @@ import {
ClinicalMobileSidebar,
} from "@/components/clinical-dashboard/ClinicalSidebar";
import {
SetupChecklist,
UploadPanel,
IndexingMonitor,
IngestionQualityConsole,
LibraryHealthStrip,
fallbackSetupChecks,
hasReadyRequiredPublicSearchConfig,
hasReadyPublicSearchSetup,
type SetupCheck,
type IngestionQualityReviewItem,
} from "@/components/clinical-dashboard/DocumentManagerPanel";
import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell";
} from "@/components/clinical-dashboard/document-manager-model";
import {
DrawerGroupLabel,
GuideDialog,
GuideTrigger,
UtilityDrawer,
} from "@/components/clinical-dashboard/dashboard-shell";
import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text";
import {
NaturalLanguageAnswer,
Expand All @@ -78,12 +77,17 @@ import { MasterSearchHeader } from "@/components/clinical-dashboard/master-searc
import { useScrollHideReporter } from "@/components/clinical-dashboard/use-hide-on-scroll";
import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context";
import { answerRecovery, errorCopy } from "@/lib/ui-copy";
import { applicationsLauncherItemCount } from "@/components/applications-launcher-page";
import {
DrawerGroupLabel,
type DocumentDrawerMode,
type DocumentDrawerStatusFilter,
type LabelReviewMutationBody,
// The tools count comes from the shared data-only catalog, not the launcher page
// component: a static value import from applications-launcher-page would pull the
// whole 800-line module (icons and all) into this chunk and defeat the dynamic()
// split below.
import { toolCatalogRecords } from "@/lib/tools-catalog";
// Type-only: erased at compile time, so this does not pull the admin drawer
// module into the eager bundle (its components load via dynamic() above).
import type {
DocumentDrawerMode,
DocumentDrawerStatusFilter,
LabelReviewMutationBody,
} from "@/components/clinical-dashboard/document-admin";

const DifferentialsHome = dynamic(
Expand All @@ -109,6 +113,42 @@ const DocumentDrawer = dynamic(
() => import("@/components/clinical-dashboard/document-admin").then((m) => m.DocumentDrawer),
{ ssr: false },
);
// The document-manager admin surfaces render only inside drawers that start
// closed, so their chunk downloads on first drawer open instead of shipping
// with the dashboard. Data-only helpers stay in document-manager-model.ts.
const SetupChecklist = dynamic(
() => import("@/components/clinical-dashboard/DocumentManagerPanel").then((m) => m.SetupChecklist),
{ ssr: false },
);
const UploadPanel = dynamic(
() => import("@/components/clinical-dashboard/DocumentManagerPanel").then((m) => m.UploadPanel),
{ ssr: false },
);
const IndexingMonitor = dynamic(
() => import("@/components/clinical-dashboard/DocumentManagerPanel").then((m) => m.IndexingMonitor),
{ ssr: false },
);
const IngestionQualityConsole = dynamic(
() => import("@/components/clinical-dashboard/DocumentManagerPanel").then((m) => m.IngestionQualityConsole),
{ ssr: false },
);
const LibraryHealthStrip = dynamic(
() => import("@/components/clinical-dashboard/DocumentManagerPanel").then((m) => m.LibraryHealthStrip),
{ ssr: false },
);
// Settings/account dialogs live in their own modules and render nothing until
// opened (Sheet returns null when closed), so they mount lazily on first open —
// gated with useHasEverBeenTrue so they stay mounted afterwards. GuideDialog
// stays a static import: it shares dashboard-shell.tsx with the eagerly-used
// UtilityDrawer/GuideTrigger, so splitting it would not remove any code.
const SettingsDialog = dynamic(
() => import("@/components/clinical-dashboard/settings-dialog").then((m) => m.SettingsDialog),
{ ssr: false },
);
const AccountSetupDialog = dynamic(
() => import("@/components/clinical-dashboard/account-setup-dialog").then((m) => m.AccountSetupDialog),
{ ssr: false },
);

// Results surfaces load lazily: they only render after a submitted search/answer, so their chunk
// downloads behind the (multi-second) retrieval/answer request rather than bloating the initial
Expand Down Expand Up @@ -875,6 +915,8 @@ export function ClinicalDashboard({
const [guideOpen, setGuideOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [accountSetupOpen, setAccountSetupOpen] = useState(false);
const settingsDialogMounted = useHasEverBeenTrue(settingsOpen);
const accountSetupDialogMounted = useHasEverBeenTrue(accountSetupOpen);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed();
const [documentsDrawerOpen, setDocumentsDrawerOpen] = useState(false);
Expand Down Expand Up @@ -2830,7 +2872,7 @@ export function ClinicalDashboard({
href: "#search",
count:
activeModeResultKind === "tools"
? applicationsLauncherItemCount
? toolCatalogRecords.length
: activeModeResultKind === "favourites"
? null
: activeModeResultKind === "documents"
Expand Down Expand Up @@ -3762,16 +3804,18 @@ export function ClinicalDashboard({
onNavigate={navigateMobileSection}
/>
<GuideDialog open={guideOpen} onClose={closeGuide} />
<SettingsDialog
open={settingsOpen}
onClose={closeSettings}
identity={sidebarIdentity}
theme={theme}
onToggleTheme={toggleTheme}
onSignOut={auth.signOut}
onOpenGuide={openGuide}
/>
<AccountSetupDialog open={accountSetupOpen} onClose={closeAccountSetup} />
{settingsDialogMounted ? (
<SettingsDialog
open={settingsOpen}
onClose={closeSettings}
identity={sidebarIdentity}
theme={theme}
onToggleTheme={toggleTheme}
onSignOut={auth.signOut}
onOpenGuide={openGuide}
/>
) : null}
{accountSetupDialogMounted ? <AccountSetupDialog open={accountSetupOpen} onClose={closeAccountSetup} /> : null}
<ClinicalMobileSidebar
open={mobileSidebarOpen}
recentQueries={recentQueries}
Expand Down
2 changes: 0 additions & 2 deletions src/components/applications-launcher-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ const mobileFilters: Array<{ id: LauncherFilter; label: string; hasMenu?: boolea
{ id: "more", label: "More", hasMenu: true },
];

export const applicationsLauncherItemCount = launcherApps.length;

function appById(id: string) {
return launcherApps.find((app) => app.id === id) ?? launcherApps[0];
}
Expand Down
19 changes: 0 additions & 19 deletions src/components/clinical-dashboard-client.tsx

This file was deleted.

11 changes: 0 additions & 11 deletions src/components/clinical-dashboard-lazy.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/components/clinical-dashboard/ClinicalDashboard.tsx

This file was deleted.

110 changes: 25 additions & 85 deletions src/components/clinical-dashboard/DocumentManagerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,96 +21,36 @@ import { cleanDisplayTitle } from "@/components/clinical-dashboard/display-text"
import { emptyStates, errorCopy } from "@/lib/ui-copy";
import { StatusBadge } from "@/components/clinical-dashboard/badges";
import type { ClinicalDocument, IngestionJob, ImportBatch } from "@/lib/types";

// Setup and quality types
export type SetupCheckStatus = "ready" | "needs_setup" | "unknown";
export type SetupCheck = {
id: "env" | "project" | "schema" | "search" | "openai" | "worker";
label: string;
status: SetupCheckStatus;
detail: string;
import {
fallbackSetupChecks,
hasReadyPublicSearchSetup,
hasReadyRequiredPublicSearchConfig,
type IndexingMonitorFilter,
type IngestionQualityReviewItem,
type IngestionQualityReviewType,
type LibraryHealthTarget,
type SetupCheck,
type SetupCheckStatus,
} from "@/components/clinical-dashboard/document-manager-model";

// Setup/quality types and helpers live in document-manager-model.ts (data-only)
// so consumers that only need them never pull this component module; re-exported
// here to keep the panel the canonical import for panel users.
export {
fallbackSetupChecks,
hasReadyPublicSearchSetup,
hasReadyRequiredPublicSearchConfig,
type IndexingMonitorFilter,
type IngestionQualityReviewItem,
type IngestionQualityReviewType,
type LibraryHealthTarget,
type SetupCheck,
type SetupCheckStatus,
};

const demoUploadReadOnlyMessage =
"Demo mode is read-only. Configure Supabase, OpenAI, and the local worker before uploading private guideline files.";

export type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures";
export type IndexingMonitorFilter = "all" | "active" | "failed";

export type IngestionQualityReviewType =
"failed_ocr" | "low_extraction_confidence" | "missing_tables" | "image_only_pages" | "failed_job" | "manual_review";

export type IngestionQualityReviewItem = {
id: string;
type: IngestionQualityReviewType;
severity: "danger" | "warning" | "info";
title: string;
detail: string;
documentId: string;
documentTitle: string;
fileName: string;
jobId: string | null;
qualityScore: number | null;
extractionQuality: string | null;
reasons: string[];
metrics: Record<string, unknown>;
updatedAt: string | null;
};

export const fallbackSetupChecks: SetupCheck[] = [
{
id: "env",
label: ".env.local configured",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
{
id: "project",
label: "Clinical KB Database target",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
{
id: "schema",
label: "supabase/schema.sql applied",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
{
id: "search",
label: "Search RPC and vector indexes",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
{
id: "openai",
label: "OpenAI API key available",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
{
id: "worker",
label: "npm run worker running",
status: "unknown",
detail: "Setup status has not loaded yet.",
},
];

const publicSearchSetupCheckIds = new Set<SetupCheck["id"]>(["env", "project", "schema", "search", "openai"]);
const requiredPublicSearchConfigCheckIds = new Set<SetupCheck["id"]>(["env", "project", "schema", "openai"]);

export function hasReadyPublicSearchSetup(checks: SetupCheck[]) {
return Array.from(publicSearchSetupCheckIds).every(
(id) => checks.find((check) => check.id === id)?.status === "ready",
);
}

export function hasReadyRequiredPublicSearchConfig(checks: SetupCheck[]) {
return Array.from(requiredPublicSearchConfigCheckIds).every(
(id) => checks.find((check) => check.id === id)?.status === "ready",
);
}

function setupBadgeClasses(status: SetupCheckStatus) {
if (status === "ready") {
return toneSuccess;
Expand Down
9 changes: 9 additions & 0 deletions src/components/clinical-dashboard/dashboard-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,15 @@ const guideSections = [
},
] as const;

// Drawer-chrome primitive kept here (already in the eager shell chunk) rather
// than in document-admin: a value import from that module would pull the whole
// admin drawer into the dashboard bundle and defeat its dynamic() split.
export function DrawerGroupLabel({ title }: { title: string }) {
return (
<p className="px-1 pt-1 text-2xs font-bold uppercase tracking-[0.1em] text-[color:var(--text-muted)]">{title}</p>
);
}

export function GuideDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
return (
<Sheet
Expand Down
6 changes: 0 additions & 6 deletions src/components/clinical-dashboard/document-admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1110,9 +1110,3 @@ function statusFilterLabel(filter: DocumentDrawerStatusFilter) {
if (filter === "failed") return "Failed documents";
return "All documents";
}

export function DrawerGroupLabel({ title }: { title: string }) {
return (
<p className="px-1 pt-1 text-2xs font-bold uppercase tracking-[0.1em] text-[color:var(--text-muted)]">{title}</p>
);
}
Loading