diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 77cd4894a..843397608 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -5,7 +5,6 @@ import dynamic from "next/dynamic"; import { CircleAlert, BookOpen, - ChevronDown, Clock3, ExternalLink, FileImage, @@ -42,7 +41,6 @@ import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local import { isDeployedClinicalKb } from "@/lib/deployed-app"; import { appBackdrop, - answerSurface, cn, EmptyState, floatingControl, @@ -54,11 +52,13 @@ 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 { useFavouritesAccess } from "@/components/clinical-dashboard/use-favourites-access"; 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 { PriorAnswerTurnSurface } from "@/components/clinical-dashboard/prior-answer-turn-surface"; import { deriveSidebarIdentity, ClinicalDesktopSidebar, @@ -78,12 +78,7 @@ import { } from "@/components/clinical-dashboard/DocumentManagerPanel"; import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text"; -import { - isPreformattedGroundedAnswer, - NaturalLanguageAnswer, - ScopeAndGovernanceNotice, - UserQuestionBubble, -} from "@/components/clinical-dashboard/answer-content"; +import { isPreformattedGroundedAnswer, ScopeAndGovernanceNotice } from "@/components/clinical-dashboard/answer-content"; import { AnswerEmptyState, AnswerProgressStepper, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status"; import { type AnswerProgressUpdate, @@ -344,105 +339,6 @@ type AnswerTurn = { const maxVisiblePriorTurns = 10; -/** - * Renders a collapsible, read-only view of a previous answer-thread turn with its question, answer, sources, and source-review notice. - * - * @param turn - The previous question and answer turn to display - * @param copied - Whether the turn's answer has been copied - * @param collapsed - Whether the answer content is collapsed - * @param onToggleCollapsed - Called when the answer visibility is toggled - * @param onCopy - Called with the answer text when copying is requested - */ -function PriorAnswerTurnSurface({ - turn, - copied, - collapsed, - onToggleCollapsed, - onCopy, -}: { - turn: AnswerTurn; - copied: boolean; - collapsed: boolean; - onToggleCollapsed: () => void; - onCopy: (text: string) => void; -}) { - const renderModel = useMemo( - () => buildAnswerRenderModel(turn.answer, { sources: turn.sources }), - [turn.answer, turn.sources], - ); - const turnPreformatted = isPreformattedGroundedAnswer(turn.answer); - const safeText = useMemo( - () => sanitizeAnswerDisplayText(turn.answer.answer, { preformatted: turnPreformatted }), - [turn.answer.answer, turnPreformatted], - ); - const sourceCount = - renderModel.primarySources.length || - turn.sources.length || - turn.answer.sources?.length || - turn.answer.citations.length; - const previewText = safeText || turn.answer.answer; - const needsSourceReview = - turn.answer.answerQualityTier === "source_only" || - turn.answer.grounded === false || - renderModel.trust === "low" || - renderModel.trust === "unsupported"; - - return ( -
-
- - - {collapsed ? ( -

{previewText}

- ) : ( - <> - onCopy(renderModel.copyText || previewText)} - /> - {needsSourceReview ? ( -
- - - Review source match. Verify cited - passages before relying on this previous answer. - -
- ) : null} - - )} -
-
- ); -} - type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures"; type IndexingMonitorFilter = "all" | "active" | "failed"; type UploadIndexingTab = "setup" | "upload" | "jobs" | "quality"; @@ -672,7 +568,6 @@ export function ClinicalDashboard({ const [activeHash, setActiveHash] = useState("#search"); const [guideOpen, setGuideOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); - const [accountSetupOpen, setAccountSetupOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed(); const [documentsDrawerOpen, setDocumentsDrawerOpen] = useState(false); @@ -811,6 +706,8 @@ export function ClinicalDashboard({ authUnavailableFallback: browserAuthUnavailableDemoFallback, localNoAuthMode, }); + const { favouritesAccessible, accountSetupOpen, accountSetupIntent, openAccountSetup, closeAccountSetup } = + useFavouritesAccess(authStatus === "authenticated", clientDemoMode); const answerThreadOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); const previousAnswerThreadOwnerIdRef = useRef(answerThreadOwnerId); useEffect(() => { @@ -889,12 +786,12 @@ export function ClinicalDashboard({ (except?: "guide" | "settings" | "accountSetup" | "mobileSidebar" | "documents" | "upload") => { if (except !== "guide") setGuideOpen(false); if (except !== "settings") setSettingsOpen(false); - if (except !== "accountSetup") setAccountSetupOpen(false); + if (except !== "accountSetup") closeAccountSetup(); if (except !== "mobileSidebar") setMobileSidebarOpen(false); if (except !== "documents") setDocumentsDrawerOpen(false); if (except !== "upload") setUploadDrawerOpen(false); }, - [], + [closeAccountSetup], ); const openGuide = useCallback(() => { closeDashboardTransientSurfaces("guide"); @@ -914,9 +811,8 @@ export function ClinicalDashboard({ return; } closeDashboardTransientSurfaces("accountSetup"); - setAccountSetupOpen(true); - }, [closeDashboardTransientSurfaces, sidebarIdentity.signedIn]); - const closeAccountSetup = useCallback(() => setAccountSetupOpen(false), []); + openAccountSetup("default"); + }, [closeDashboardTransientSurfaces, openAccountSetup, sidebarIdentity.signedIn]); const prefetchApplications = useCallback(() => { router.prefetch("/?mode=tools"); router.prefetch("/favourites"); @@ -2760,6 +2656,11 @@ export function ClinicalDashboard({ } function selectSearchMode(mode: AppModeId) { + if (mode === "favourites" && !favouritesAccessible) { + closeDashboardTransientSurfaces("accountSetup"); + openAccountSetup("favourites"); + return; + } modeChangeFromUiRef.current = true; if (mode === "differentials") clearDifferentialModeResultState(); setQuery(""); @@ -3468,6 +3369,7 @@ export function ClinicalDashboard({ recentQueries={recentQueries} identity={sidebarIdentity} activeMode={searchMode} + showAccountLibrary={favouritesAccessible} onCollapsedChange={setSidebarCollapsed} onNewChat={startNewChat} onPickRecent={pickRecentQuery} @@ -3493,6 +3395,11 @@ export function ClinicalDashboard({ realDataReady={canRunSearch} onQueryChange={setQuery} onSearchModeChange={selectSearchMode} + canAccessFavourites={favouritesAccessible} + onRequestAccountSetup={() => { + closeDashboardTransientSurfaces("accountSetup"); + openAccountSetup("favourites"); + }} onAsk={ask} onClearQuery={() => { setQuery(""); @@ -4244,12 +4151,13 @@ export function ClinicalDashboard({ onSignOut={auth.signOut} onOpenGuide={openGuide} /> - + void; onPickRecent: (query: string) => void; onOpenGuide: () => void; @@ -279,6 +285,44 @@ export function ClinicalSidebarContent({ })} + + {showAccountLibrary ? ( +
+
+

+ Your library +

+
+ +
+ ) : null}
@@ -350,6 +394,7 @@ function ClinicalCollapsedRail({ collapseLocked, identity, activeMode, + showAccountLibrary = false, onCollapsedChange, onNewChat, onOpenGuide, @@ -364,6 +409,7 @@ function ClinicalCollapsedRail({ collapseLocked: boolean; identity: SidebarIdentity; activeMode: AppModeId; + showAccountLibrary?: boolean; onCollapsedChange: (collapsed: boolean) => void; onNewChat: () => void; onOpenGuide: () => void; @@ -451,6 +497,24 @@ function ClinicalCollapsedRail({ ); })} + {showAccountLibrary + ? sidebarAccountLibraryItems.map((item) => { + const Icon = item.icon; + const active = activeMode === item.id; + return ( + + + + ); + }) + : null}
+ + setAccountSetupDismissed(true)} + intent="favourites" + /> + + ); + } + return (
- {!demoMode && auth.status !== "authenticated" && auth.status !== "loading" ? ( -

- Sign in or create an account from Account settings to save favourites and access them across devices. -

- ) : null} -
([]); const [commandScopes, setCommandScopes] = useState([]); const removeCommandScope = useCallback( @@ -345,6 +345,8 @@ function GlobalStandaloneSearchShellClient({ authUnavailableFallback: !auth.isConfigured, localNoAuthMode: isLocalNoAuthMode(), }); + const { favouritesAccessible, accountSetupOpen, accountSetupIntent, openAccountSetup, closeAccountSetup } = + useFavouritesAccess(auth.status === "authenticated", clientDemoMode); const recentQueriesOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); useEffect(() => { @@ -374,14 +376,14 @@ function GlobalStandaloneSearchShellClient({ function openGuide() { setSettingsOpen(false); - setAccountSetupOpen(false); + closeAccountSetup(); setMobileMenuOpen(false); setGuideOpen(true); } function openSettings() { setGuideOpen(false); - setAccountSetupOpen(false); + closeAccountSetup(); setMobileMenuOpen(false); setSettingsOpen(true); } @@ -390,12 +392,12 @@ function GlobalStandaloneSearchShellClient({ setGuideOpen(false); setMobileMenuOpen(false); if (sidebarIdentity.signedIn) { - setAccountSetupOpen(false); + closeAccountSetup(); setSettingsOpen(true); return; } setSettingsOpen(false); - setAccountSetupOpen(true); + openAccountSetup("default"); } function navigateToMode(mode: AppModeId, options: SearchNavigationOptions = {}) { @@ -417,6 +419,13 @@ function GlobalStandaloneSearchShellClient({ } function changeMode(mode: AppModeId) { + if (mode === "favourites" && !favouritesAccessible) { + setGuideOpen(false); + setSettingsOpen(false); + setMobileMenuOpen(false); + openAccountSetup("favourites"); + return; + } setQuery(""); setCommandScopes([]); setSearchMode(mode); @@ -527,6 +536,7 @@ function GlobalStandaloneSearchShellClient({ recentQueries={recentQueries} identity={sidebarIdentity} activeMode={searchMode} + showAccountLibrary={favouritesAccessible} onCollapsedChange={setSidebarCollapsed} onNewChat={startNewAnswerChat} onPickRecent={pickRecentQuery} @@ -556,6 +566,13 @@ function GlobalStandaloneSearchShellClient({ realDataReady onQueryChange={setQuery} onSearchModeChange={changeMode} + canAccessFavourites={favouritesAccessible} + onRequestAccountSetup={() => { + setGuideOpen(false); + setSettingsOpen(false); + setMobileMenuOpen(false); + openAccountSetup("favourites"); + }} onAsk={submitSearch} onClearQuery={() => { setQuery(""); @@ -643,7 +660,7 @@ function GlobalStandaloneSearchShellClient({ onSignOut={auth.signOut} onOpenGuide={openGuide} /> - setAccountSetupOpen(false)} /> +