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
136 changes: 22 additions & 114 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import dynamic from "next/dynamic";
import {
CircleAlert,
BookOpen,
ChevronDown,
Clock3,
ExternalLink,
FileImage,
Expand Down Expand Up @@ -42,7 +41,6 @@ import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local
import { isDeployedClinicalKb } from "@/lib/deployed-app";
import {
appBackdrop,
answerSurface,
cn,
EmptyState,
floatingControl,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<div
// Historical conversation turns grow unbounded and most are collapsed and
// scrolled off-screen; content-auto skips their layout/paint until near the
// viewport. Safe here — the surface has no overflowing popovers, and the
// expand toggle is only reachable once the turn is scrolled into view.
className="content-auto min-w-0 space-y-4 sm:space-y-5"
data-dashboard-stage="answer-thread-turn"
data-collapsed={collapsed ? "true" : "false"}
>
<div className={cn(answerSurface, "space-y-3 p-2.5 sm:p-3")}>
<UserQuestionBubble query={turn.query} />
<button
type="button"
onClick={onToggleCollapsed}
aria-expanded={!collapsed}
className="inline-flex min-h-tap items-center gap-1.5 rounded-md px-1 text-xs font-semibold text-[color:var(--text-muted)] transition hover:text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
<ChevronDown className={cn("h-4 w-4 transition-transform", !collapsed && "rotate-180")} aria-hidden="true" />
{collapsed ? "Show previous answer" : "Hide previous answer"}
</button>
{collapsed ? (
<p className={cn("line-clamp-2 text-sm leading-6", textMuted)}>{previewText}</p>
) : (
<>
<NaturalLanguageAnswer
text={turn.answer.answer}
preformatted={turnPreformatted}
sourceCount={sourceCount}
sourceOnly={turn.answer.answerQualityTier === "source_only"}
bestSource={renderModel.bestSource}
sources={renderModel.reviewSources}
sourceLinks={renderModel.primarySources}
copied={copied}
onCopy={() => onCopy(renderModel.copyText || previewText)}
/>
{needsSourceReview ? (
<div
role="note"
data-testid="prior-answer-source-review"
className="mt-2 flex items-start gap-2 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-xs text-[color:var(--text-muted)]"
>
<CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-[color:var(--warning)]" aria-hidden />
<span>
<strong className="text-[color:var(--text-heading)]">Review source match.</strong> Verify cited
passages before relying on this previous answer.
</span>
</div>
) : null}
</>
)}
</div>
</div>
);
}

type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures";
type IndexingMonitorFilter = "all" | "active" | "failed";
type UploadIndexingTab = "setup" | "upload" | "jobs" | "quality";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -2760,6 +2656,11 @@ export function ClinicalDashboard({
}

function selectSearchMode(mode: AppModeId) {
if (mode === "favourites" && !favouritesAccessible) {
closeDashboardTransientSurfaces("accountSetup");
openAccountSetup("favourites");
return;
}
Comment on lines 2658 to +2663

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i favourites-hub | xargs -I{} ast-grep outline {} --items all
rg -n "favouritesAccessible|canAccessFavouritesMode" src/components/clinical-dashboard/favourites-hub.tsx 2>/dev/null || echo "favourites-hub.tsx not found by that name; search broader"
rg -n "FavouritesHub" -g '*.tsx' -A2 -B2

Repository: BigSimmo/Database

Length of output: 1490


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files and inspect the specific sections mentioned in the comment.
ast-grep outline src/components/ClinicalDashboard.tsx --items all | sed -n '1,220p'
printf '\n--- ClinicalDashboard around mode-sync / search-mode logic ---\n'
sed -n '2500,2745p' src/components/ClinicalDashboard.tsx
printf '\n--- ClinicalDashboard FavouritesHub rendering area ---\n'
sed -n '3725,3785p' src/components/ClinicalDashboard.tsx
printf '\n--- Favourites hub component outline ---\n'
ast-grep outline src/components/clinical-dashboard/favourites-hub.tsx --items all | sed -n '1,220p'
printf '\n--- Favourites hub access-related searches ---\n'
rg -n "favouritesAccessible|sign up to save favourites|account setup|openAccountSetup|useSavedRegistryFavourites|useDemoMode|isDemoMode|accessible" src/components/clinical-dashboard/favourites-hub.tsx src/components/ClinicalDashboard.tsx tests/favourites-hub-unavailable-controls.dom.test.tsx
printf '\n--- Favourites hub relevant top/mid sections ---\n'
sed -n '1,220p' src/components/clinical-dashboard/favourites-hub.tsx
printf '\n--- Favourites hub later sections ---\n'
sed -n '220,620p' src/components/clinical-dashboard/favourites-hub.tsx

Repository: BigSimmo/Database

Length of output: 46458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the dashboard effects that react to search params / initial URL state.
rg -n "searchParams|window.location.search|setSearchMode\\(|appModeId|appModeVisible|modeSearchSubmitted|executeSearch\\(" src/components/ClinicalDashboard.tsx
printf '\n--- search-param/bootstrap effect region ---\n'
sed -n '1500,1825p' src/components/ClinicalDashboard.tsx
printf '\n--- executeSearch region around the unconditional setSearchMode ---\n'
sed -n '2560,2715p' src/components/ClinicalDashboard.tsx
printf '\n--- any favourites-specific route/guard helpers in dashboard ---\n'
rg -n "favouritesAccessible|openAccountSetup\\(\"favourites\"\\)|mode === \"favourites\"|appModeHomeHref\\(\"favourites\"|appModeVisible" src/components/ClinicalDashboard.tsx src/lib src/components/clinical-dashboard tests

Repository: BigSimmo/Database

Length of output: 25197


Guard dashboard URL-driven favourites navigation. searchParams/bootstrap handling in src/components/ClinicalDashboard.tsx still accepts mode=favourites without favouritesAccessible, so a signed-out non-demo user can land in the inline FavouritesHub even though the UI entry point is blocked. Add the same access check there and fall back to account setup or another mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ClinicalDashboard.tsx` around lines 2658 - 2663, Update the
searchParams/bootstrap handling in ClinicalDashboard so URL-driven
mode=favourites is validated with favouritesAccessible before rendering or
selecting FavouritesHub. When access is unavailable, route through the existing
account-setup fallback (or another permitted mode), matching selectSearchMode’s
behavior, while preserving accessible favourites navigation.

modeChangeFromUiRef.current = true;
if (mode === "differentials") clearDifferentialModeResultState();
setQuery("");
Expand Down Expand Up @@ -3468,6 +3369,7 @@ export function ClinicalDashboard({
recentQueries={recentQueries}
identity={sidebarIdentity}
activeMode={searchMode}
showAccountLibrary={favouritesAccessible}
onCollapsedChange={setSidebarCollapsed}
onNewChat={startNewChat}
onPickRecent={pickRecentQuery}
Expand All @@ -3493,6 +3395,11 @@ export function ClinicalDashboard({
realDataReady={canRunSearch}
onQueryChange={setQuery}
onSearchModeChange={selectSearchMode}
canAccessFavourites={favouritesAccessible}
onRequestAccountSetup={() => {
closeDashboardTransientSurfaces("accountSetup");
openAccountSetup("favourites");
}}
onAsk={ask}
onClearQuery={() => {
setQuery("");
Expand Down Expand Up @@ -4244,12 +4151,13 @@ export function ClinicalDashboard({
onSignOut={auth.signOut}
onOpenGuide={openGuide}
/>
<AccountSetupDialog open={accountSetupOpen} onClose={closeAccountSetup} />
<AccountSetupDialog open={accountSetupOpen} onClose={closeAccountSetup} intent={accountSetupIntent} />
<ClinicalMobileSidebar
open={mobileSidebarOpen}
recentQueries={recentQueries}
identity={sidebarIdentity}
activeMode={searchMode}
showAccountLibrary={favouritesAccessible}
onOpenChange={setMobileSidebarOpen}
onNewChat={startNewChat}
onPickRecent={pickRecentQuery}
Expand Down
Loading
Loading