diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index f103f834e..d44906295 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -1525,7 +1525,9 @@ export function ClinicalDashboard({ const [answerThreadBootstrapped, setAnswerThreadBootstrapped] = useState(false); const [query, setQuery] = useState(initialQuery); const [searchMode, setSearchMode] = useState(initialSearchMode); - const [modeSearchSubmitted, setModeSearchSubmitted] = useState(false); + const [modeSearchSubmitted, setModeSearchSubmitted] = useState(() => + Boolean(autoRunSearch && initialQuery.trim() && initialSearchMode !== "tools"), + ); const [answer, setAnswer] = useState(null); const [sources, setSources] = useState([]); // Answer-mode conversation thread. `priorAnswerTurns` holds completed @@ -1549,6 +1551,14 @@ export function ClinicalDashboard({ const activeModeSearch = appModeSearchConfig(searchMode); const activeModeResultKind = appModeResultKind(searchMode); const requestQueryMode = appModeQueryMode(searchMode, queryMode); + const submittedUrlMode = searchParams.get("mode"); + const submittedUrlModeMatchesActive = + !submittedUrlMode || + (isAppModeId(submittedUrlMode) && isAppModeVisible(submittedUrlMode) && submittedUrlMode === searchMode); + const submittedUrlQuery = + autoRunSearch && searchParams.get("run") === "1" && submittedUrlModeMatchesActive + ? (searchParams.get("q") ?? searchParams.get("query") ?? "").trim() + : ""; // Record matches come from the owner-scoped registry API (mock fixtures in // demo mode); ranking stays client-side so live-typing behaviour is @@ -2846,26 +2856,27 @@ export function ClinicalDashboard({ if (updateUrl) router.replace(appModeHomeHref("prescribing", { query: trimmedSearchText })); } - async function ask() { - const trimmedQuery = query.trim(); + async function ask(searchText = query) { + const trimmedQuery = searchText.trim(); if (searchMode === "documents" && trimmedQuery) { rememberRecentQuery(trimmedQuery); router.push(documentsSearchHref({ query: trimmedQuery, focus: true, run: true })); return; } if (searchMode === "prescribing") { - setMedicationSearchQuery(query); + setMedicationSearchQuery(searchText); return; } - await executeSearch(query, searchMode, scopeFilters); + await executeSearch(searchText, searchMode, scopeFilters); } const askRef = useRef(ask); askRef.current = ask; useEffect(() => { const trimmedQuery = query.trim(); + const submittedSearchText = searchMode === "answer" && submittedUrlQuery ? submittedUrlQuery : trimmedQuery; const canAutoRunMode = searchMode === "documents" || searchMode === "prescribing" || canRunSearch; - if (!autoRunSearch || !trimmedQuery || !canAutoRunMode || loading) return; + if (!autoRunSearch || !submittedSearchText || !canAutoRunMode || loading) return; if (searchMode === "answer" && !answerThreadBootstrapped) return; // Once an answer is on screen, composer edits are follow-up drafts and must // only run on explicit submit — not on every query keystroke while run=1 @@ -2873,15 +2884,25 @@ export function ClinicalDashboard({ if (searchMode === "answer" && answer) return; // After reload, the URL query matches the restored latest turn — do not // archive it again into a duplicate prior turn. - if (searchMode === "answer" && latestAnswerQuery?.trim() === trimmedQuery) { - autoRunSearchSignatureRef.current = `${searchMode}:${trimmedQuery}`; + if (searchMode === "answer" && latestAnswerQuery?.trim() === submittedSearchText) { + autoRunSearchSignatureRef.current = `${searchMode}:${submittedSearchText}`; return; } - const signature = `${searchMode}:${trimmedQuery}`; + const signature = `${searchMode}:${submittedSearchText}`; if (autoRunSearchSignatureRef.current === signature) return; autoRunSearchSignatureRef.current = signature; - void askRef.current(); - }, [autoRunSearch, canRunSearch, loading, query, searchMode, answer, answerThreadBootstrapped, latestAnswerQuery]); + void askRef.current(submittedSearchText); + }, [ + autoRunSearch, + canRunSearch, + loading, + query, + submittedUrlQuery, + searchMode, + answer, + answerThreadBootstrapped, + latestAnswerQuery, + ]); function pickRecentQuery(recentQuery: string) { if (searchMode === "prescribing") { @@ -3556,12 +3577,17 @@ export function ClinicalDashboard({ const showAuthPanel = false; const showDegradedNotice = !isOnline || (apiUnavailable && !canRunSearch); const hasMobileBottomSearch = searchMode !== "answer"; + const submittedAnswerSearchActive = + activeModeResultKind === "answer" && !answer && canRunSearch && (modeSearchSubmitted || Boolean(submittedUrlQuery)); + const showAnswerHome = activeModeResultKind === "answer" && !answer && !loading && !submittedAnswerSearchActive; + const showAnswerPending = + activeModeResultKind === "answer" && !answer && (loading || (submittedAnswerSearchActive && !error)); const showDesktopHomeComposer = !error && (activeModeResultKind === "tools" || activeModeResultKind === "favourites" || (!loading && - ((activeModeResultKind === "answer" && !answer && !modeSearchSubmitted) || + (showAnswerHome || (searchMode === "documents" && activeModeResultKind === "documents" && documentMatches.length === 0 && @@ -3865,7 +3891,7 @@ export function ClinicalDashboard({
) - ) : loading && !answer ? ( + ) : showAnswerPending ? ( ) : answer && answerRenderModel ? ( stagedDashboardExtraction.answerSurface ? ( @@ -4047,14 +4073,14 @@ export function ClinicalDashboard({ /> ) : null - ) : ( + ) : showAnswerHome ? ( setSearchMode("documents")} onUploadDocument={openUploadDrawer} desktopComposerSlotId={desktopHomeComposerSlotId} /> - )} + ) : null}
{showSystemNotice && answer ? renderSystemNotice("sm:hidden") : null} diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index a7a7d0da1..98b23c05e 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -193,8 +193,13 @@ async function fulfillAnswerResponse(route: Route, payload: unknown) { } type DemoAnswerOverride = (query: string, documentId?: string, documentIds?: string[]) => ReturnType; +type MockDemoApiOptions = { + answerOverride?: DemoAnswerOverride; + answerDelayMs?: number; + onAnswerRequest?: (query: string) => void; +}; -async function mockDemoApi(page: Page, options: { answerOverride?: DemoAnswerOverride } = {}) { +async function mockDemoApi(page: Page, options: MockDemoApiOptions = {}) { await mockLocalProjectIdentity(page); await page.route("**/api/setup-status**", async (route) => { await route.fulfill({ @@ -273,9 +278,14 @@ async function mockDemoApi(page: Page, options: { answerOverride?: DemoAnswerOve documentId?: string; documentIds?: string[]; }; + const query = body.query ?? "What monitoring is required?"; + options.onAnswerRequest?.(query); + if (options.answerDelayMs) { + await new Promise((resolve) => setTimeout(resolve, options.answerDelayMs)); + } const answer = - options.answerOverride?.(body.query ?? "What monitoring is required?", body.documentId, body.documentIds) ?? - demoAnswer(body.query ?? "What monitoring is required?", body.documentId, body.documentIds); + options.answerOverride?.(query, body.documentId, body.documentIds) ?? + demoAnswer(query, body.documentId, body.documentIds); await fulfillAnswerResponse(route, { ...answer, demoMode: true, @@ -1317,6 +1327,33 @@ test.describe("Clinical KB UI smoke coverage", () => { await expectNoPageHorizontalOverflow(page); }); + test("answer search URL opens chat without the answer home copy", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 820 }); + const answerRequests: string[] = []; + const question = "What clozapine monitoring items are shown in the table image?"; + await mockDemoApi(page, { + answerDelayMs: 1500, + onAnswerRequest: (query) => answerRequests.push(query), + }); + + await page.goto(`/?mode=answer&q=${encodeURIComponent(question)}&focus=1&run=1`, { waitUntil: "domcontentloaded" }); + + await expect(page.getByTestId("answer-empty-state")).toHaveCount(0); + await expect(page.getByText("How can I help?", { exact: true })).toHaveCount(0); + await expect(page.getByLabel("Loading answer")).toBeVisible(); + await expect.poll(() => answerRequests[0]).toBe(question); + + const questionBubble = page.getByTestId("user-question-bubble"); + await expect(questionBubble).toBeVisible({ timeout: uiAssertionTimeoutMs }); + await expect(questionBubble).toContainText(question); + await expect(page.getByTestId("plain-answer-response")).toContainText("synthetic clozapine table image highlights"); + await expect(visibleQuestionInput(page)).toHaveValue(""); + await expect(page.getByTestId("answer-empty-state")).toHaveCount(0); + await expect(page.getByText("How can I help?", { exact: true })).toHaveCount(0); + expect(answerRequests).toEqual([question]); + await expectNoPageHorizontalOverflow(page); + }); + test("answer mode keeps prior turns visible for follow-up questions", async ({ page }) => { await page.setViewportSize({ width: 390, height: 820 }); await mockDemoApi(page);