From 09916566d887df6e3fd3adb29e32a4814df474ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Jul 2026 20:12:16 +0000 Subject: [PATCH] fix: unify answer recovery states Co-authored-by: BigSimmo --- src/components/ClinicalDashboard.tsx | 146 +++++++++++++-------------- src/components/ui-primitives.tsx | 39 ++++++- tests/ui-primitives.dom.test.tsx | 30 ++++++ 3 files changed, 134 insertions(+), 81 deletions(-) create mode 100644 tests/ui-primitives.dom.test.tsx diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index a4a636c71..d5de7857a 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -33,11 +33,11 @@ import { appBackdrop, answerSurface, cn, + EmptyState, floatingControl, InlineNotice, primaryControl, textMuted, - toneInfo, } from "@/components/ui-primitives"; import { useAuthSession } from "@/lib/supabase/client"; import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog"; @@ -3598,100 +3598,90 @@ export function ClinicalDashboard({ {activeModeSearch.resultHeading} {answerLifecycle.status === "cancelled" && activeModeResultKind === "answer" ? ( -
-
-

Generation stopped

-

No partial clinical answer was kept.

-
- -
- ) : error && errorKind === "no-results" && activeModeResultKind === "answer" ? ( -
-
-
-
+ focusComposerInput()} className={cn(primaryControl, "text-xs")} + onClick={() => void ask(answerLifecycle.query ?? query)} > - {answerRecovery.rephrase} +
-
- ) : error ? ( -
-
-
- {activeModeResultKind === "answer" && lastFailedQuery && ( -
+ } + /> + ) : error && errorKind === "no-results" && activeModeResultKind === "answer" ? ( + -
- )} -
+ + } + /> + ) : error ? ( + + + + + ) : undefined + } + /> ) : null} {searchMode !== "prescribing" && diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index 7c736d6bb..79effd296 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -426,18 +426,51 @@ export function LoadingPanel({ ); } -export function EmptyState({ icon: Icon, title, body }: { icon?: IconComponent; title: string; body: string }) { +export function EmptyState({ + icon: Icon, + title, + body, + actions, + live, + tone = "neutral", + testId, +}: { + icon?: IconComponent; + title: string; + body: string; + /** Optional controls stay within the shared state surface rather than becoming a second panel. */ + actions?: ReactNode; + /** Announce a state transition only when the state is introduced dynamically. */ + live?: "polite" | "assertive"; + tone?: "neutral" | "info" | "danger"; + testId?: string; +}) { return ( -
+
{Icon && ( - + )}

{title}

{body}

+ {actions ?
{actions}
: null}
diff --git a/tests/ui-primitives.dom.test.tsx b/tests/ui-primitives.dom.test.tsx new file mode 100644 index 000000000..4a380b2f8 --- /dev/null +++ b/tests/ui-primitives.dom.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from "@testing-library/react"; +import { Search } from "lucide-react"; +import { describe, expect, it } from "vitest"; + +import { EmptyState } from "@/components/ui-primitives"; + +describe("EmptyState", () => { + it("keeps recovery actions inside an announced state surface", () => { + render( + Rephrase question} + />, + ); + + const state = screen.getByTestId("recovery-state"); + expect(state).toHaveAttribute("role", "status"); + expect(screen.getByRole("button", { name: "Rephrase question" })).toBeVisible(); + }); + + it("uses an assertive announcement for a dynamic failure", () => { + render(); + + expect(screen.getByRole("alert")).toHaveTextContent("Answer unavailable"); + }); +});