From 3449cf10edf45e3bad29d24a9604424bcc5a1f3d Mon Sep 17 00:00:00 2001
From: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Date: Sat, 18 Jul 2026 03:26:51 +0800
Subject: [PATCH] fix: unify answer recovery states
---
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 931335a1a..73c4056ca 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";
@@ -3506,100 +3506,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" ? (
-
-
-
-
-
- {answerRecovery.noResults.heading}
-
-
{answerRecovery.noResults.body}
-
-
-
+ focusComposerInput()}
className={cn(primaryControl, "text-xs")}
+ onClick={() => void ask(answerLifecycle.query ?? query)}
>
- {answerRecovery.rephrase}
+
+ Run again
-
-
-
- ) : error ? (
-
-
-
- {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");
+ });
+});