diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 67758856f..6783d5184 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -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 {
@@ -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,
@@ -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(
@@ -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
@@ -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);
@@ -2830,7 +2872,7 @@ export function ClinicalDashboard({
href: "#search",
count:
activeModeResultKind === "tools"
- ? applicationsLauncherItemCount
+ ? toolCatalogRecords.length
: activeModeResultKind === "favourites"
? null
: activeModeResultKind === "documents"
@@ -3762,16 +3804,18 @@ export function ClinicalDashboard({
onNavigate={navigateMobileSection}
/>
-
-
+ {settingsDialogMounted ? (
+
+ ) : null}
+ {accountSetupDialogMounted ? : null}
app.id === id) ?? launcherApps[0];
}
diff --git a/src/components/clinical-dashboard-client.tsx b/src/components/clinical-dashboard-client.tsx
deleted file mode 100644
index d9a3bee82..000000000
--- a/src/components/clinical-dashboard-client.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-"use client";
-
-import dynamic from "next/dynamic";
-import type { AppModeId } from "@/lib/app-modes";
-
-const ClinicalDashboard = dynamic(() => import("@/components/clinical-dashboard").then((m) => m.ClinicalDashboard), {
- ssr: false,
-});
-
-type ClinicalDashboardClientProps = {
- initialSearchMode?: AppModeId;
- initialQuery?: string;
- focusSearch?: boolean;
- autoRunSearch?: boolean;
-};
-
-export function ClinicalDashboardClient(props: ClinicalDashboardClientProps) {
- return ;
-}
diff --git a/src/components/clinical-dashboard-lazy.tsx b/src/components/clinical-dashboard-lazy.tsx
deleted file mode 100644
index 70bd7aafc..000000000
--- a/src/components/clinical-dashboard-lazy.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-
-import dynamic from "next/dynamic";
-
-// `ssr: false` requires a Client Component in the App Router; this wrapper
-// keeps the heavy dashboard bundle browser-only for the server-rendered
-// home page.
-export const ClinicalDashboardLazy = dynamic(
- () => import("@/components/clinical-dashboard").then((m) => m.ClinicalDashboard),
- { ssr: false },
-);
diff --git a/src/components/clinical-dashboard/ClinicalDashboard.tsx b/src/components/clinical-dashboard/ClinicalDashboard.tsx
deleted file mode 100644
index 4973c2930..000000000
--- a/src/components/clinical-dashboard/ClinicalDashboard.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { ClinicalDashboard } from "@/components/ClinicalDashboard";
diff --git a/src/components/clinical-dashboard/DocumentManagerPanel.tsx b/src/components/clinical-dashboard/DocumentManagerPanel.tsx
index 74ddae38d..57a810b94 100644
--- a/src/components/clinical-dashboard/DocumentManagerPanel.tsx
+++ b/src/components/clinical-dashboard/DocumentManagerPanel.tsx
@@ -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;
- 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(["env", "project", "schema", "search", "openai"]);
-const requiredPublicSearchConfigCheckIds = new Set(["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;
diff --git a/src/components/clinical-dashboard/dashboard-shell.tsx b/src/components/clinical-dashboard/dashboard-shell.tsx
index 8842291d3..54d95e9d2 100644
--- a/src/components/clinical-dashboard/dashboard-shell.tsx
+++ b/src/components/clinical-dashboard/dashboard-shell.tsx
@@ -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 (
+ {title}
+ );
+}
+
export function GuideDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
return (
{title}
- );
-}
diff --git a/src/components/clinical-dashboard/document-manager-model.ts b/src/components/clinical-dashboard/document-manager-model.ts
new file mode 100644
index 000000000..1050ec077
--- /dev/null
+++ b/src/components/clinical-dashboard/document-manager-model.ts
@@ -0,0 +1,89 @@
+// Data-only setup/quality model shared by ClinicalDashboard and the
+// DocumentManagerPanel components. Kept free of React/JSX/icon imports so the
+// dashboard can consume these types and helpers without statically pulling the
+// heavy panel module, which loads via next/dynamic when an admin drawer opens.
+
+export type SetupCheckStatus = "ready" | "needs_setup" | "unknown";
+export type SetupCheck = {
+ id: "env" | "project" | "schema" | "search" | "openai" | "worker";
+ label: string;
+ status: SetupCheckStatus;
+ detail: string;
+};
+
+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;
+ 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(["env", "project", "schema", "search", "openai"]);
+const requiredPublicSearchConfigCheckIds = new Set(["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",
+ );
+}
diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx
index 7b4f5c5fb..9f1f237d7 100644
--- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx
+++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx
@@ -12,11 +12,14 @@ import {
useState,
} from "react";
-import { ClinicalDashboard } from "@/components/clinical-dashboard";
-import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
-import { recentQueryStorageKey } from "@/components/ClinicalDashboard";
+import dynamic from "next/dynamic";
+
+// Import the dashboard module directly: the clinical-dashboard barrel re-exported
+// heavy result surfaces whose value exports were pulled eagerly through it,
+// defeating their dynamic() splits.
+import { ClinicalDashboard, recentQueryStorageKey } from "@/components/ClinicalDashboard";
import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context";
-import { SettingsDialog } from "@/components/clinical-dashboard/settings-dialog";
+import { useHasEverBeenTrue } from "@/components/clinical-dashboard/use-has-ever-been-true";
import {
ClinicalDesktopSidebar,
ClinicalMobileSidebar,
@@ -43,6 +46,17 @@ import type { SearchScopeFilters } from "@/lib/search-scope";
import { useAuthSession } from "@/lib/supabase/client";
import type { ClinicalQueryMode } from "@/lib/types";
+// Lazy dialogs, mounted on first open (see ClinicalDashboard for the same
+// pattern): their chunks never download for users who never open them.
+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 },
+);
+
const mockupQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }> = [
{ value: "auto", label: "Auto" },
{ value: "monitoring_schedule", label: "Monitoring" },
@@ -148,6 +162,8 @@ function GlobalMockupSearchShellClient({
const [guideOpen, setGuideOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [accountSetupOpen, setAccountSetupOpen] = useState(false);
+ const settingsDialogMounted = useHasEverBeenTrue(settingsOpen);
+ const accountSetupDialogMounted = useHasEverBeenTrue(accountSetupOpen);
const [recentQueries, setRecentQueries] = useState([]);
const [commandScopes, setCommandScopes] = useState([]);
const [bottomSearchScrollHidden, setBottomSearchScrollHidden] = useState(false);
@@ -507,16 +523,20 @@ function GlobalMockupSearchShellClient({
setGuideOpen(false)} />
- setSettingsOpen(false)}
- identity={sidebarIdentity}
- theme={theme}
- onToggleTheme={toggleTheme}
- onSignOut={auth.signOut}
- onOpenGuide={openGuide}
- />
- setAccountSetupOpen(false)} />
+ {settingsDialogMounted ? (
+ setSettingsOpen(false)}
+ identity={sidebarIdentity}
+ theme={theme}
+ onToggleTheme={toggleTheme}
+ onSignOut={auth.signOut}
+ onOpenGuide={openGuide}
+ />
+ ) : null}
+ {accountSetupDialogMounted ? (
+ setAccountSetupOpen(false)} />
+ ) : null}