From d4d2b2c8851898ab3d42a822a89473de5d879f44 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 07:49:00 +0000 Subject: [PATCH 01/11] Answer page tidy-up: warning line under notice, shorter notice, square chips, rows Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N75FN1tsEXMbK7qAxe5L9p --- .../2026-10-17-answer-page-tidy-up.md | 8 +++ src/components/ClinicalDashboard.tsx | 12 +++- .../answer-provider-notice.tsx | 62 +++++++++++++++-- .../answer-result-surface.tsx | 18 +++-- .../clinical-dashboard/answer-status.tsx | 67 ++++++++++++------- .../clinical-dashboard/cross-mode-links.tsx | 39 ++++------- src/components/ui/answer-card.tsx | 8 +-- tests/answer-progress-ui-smoke.spec.ts | 8 ++- tests/answer-provider-notice.dom.test.tsx | 4 +- tests/ui-smoke.spec.ts | 7 +- 10 files changed, 160 insertions(+), 73 deletions(-) create mode 100644 docs/decisions/2026-10-17-answer-page-tidy-up.md diff --git a/docs/decisions/2026-10-17-answer-page-tidy-up.md b/docs/decisions/2026-10-17-answer-page-tidy-up.md new file mode 100644 index 0000000000..0e4f85f25b --- /dev/null +++ b/docs/decisions/2026-10-17-answer-page-tidy-up.md @@ -0,0 +1,8 @@ +# Decision: Answer page tidy-up and a shorter overseas notice + +- **Status:** decided 2026-10-17 +- **Source:** `src/components/clinical-dashboard/answer-provider-notice.tsx`, find "ANSWER_PROVIDER_NOTICE_TEXT" and "AnswerSummaryUnavailableNotice" + +The owner approved a mockup of the Answer page and asked for it to be built. The overseas notice under an answer now reads "Sent to OpenAI overseas. Don't include patient details." and is centred. When the model could not write the answer, an amber "AI summary unavailable" line sits directly under that notice instead of a sentence above the question. Other degraded reasons, which say something about the evidence, keep their sentence above the answer. The limitations chip drops its count when it already says "Source-only" or "Review due", and the support and limitations chips share one square shape and size. "How this answer was built" moved under the answer's actions, and "Also in your library" became a plain row with its count shown at every width. + +If this summary and the source ever differ, the source wins. diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index d3912045dc..443a4d14d1 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -71,9 +71,13 @@ import { SystemNotice, DegradedNoticeFrame } from "@/components/clinical-dashboa import { resolveModeHomeCanvasClass } from "@/components/clinical-dashboard/mode-home-canvas"; import { sanitizeAnswerDisplayText } from "@/components/clinical-dashboard/display-text"; import { AnswerCancelledNotice } from "@/components/clinical-dashboard/answer-cancelled-notice"; -import { AnswerProviderNotice } from "@/components/clinical-dashboard/answer-provider-notice"; +import { + AnswerProviderNotice, + AnswerSummaryUnavailableNotice, +} from "@/components/clinical-dashboard/answer-provider-notice"; import { isPreformattedGroundedAnswer } from "@/components/clinical-dashboard/answer-content"; import { + AnswerBuildDetails, AnswerProgress, AnswerSkeleton, SearchProgressBanner, @@ -2988,12 +2992,16 @@ function ClinicalDashboardContent({ restoredSavedAt={restoredAnswerSavedAt} onRecheckAnswer={handleRecheckRestoredAnswer} /> + {/* Under the answer, not above the question (owner decision, 17 Oct 2026). */} + {loading ? null : } {/* Once per page, under the latest answer: where a question goes (audit S3). */} {explicitDemoMode || answer.demoMode === true || answer.fallbackMode === "non_production_demo" ? null : ( - + )} + {/* Directly under it: the model could not write this answer (owner decision, 17 Oct 2026). */} + ) : null ) : null} diff --git a/src/components/clinical-dashboard/answer-provider-notice.tsx b/src/components/clinical-dashboard/answer-provider-notice.tsx index 6e0c273f0b..91cc7783d9 100644 --- a/src/components/clinical-dashboard/answer-provider-notice.tsx +++ b/src/components/clinical-dashboard/answer-provider-notice.tsx @@ -1,10 +1,18 @@ -import { Globe } from "lucide-react"; +import { Globe, TriangleAlert } from "lucide-react"; import { cn, textMuted } from "@/components/ui-primitives"; +import { answerUsesDegradedMode, type AnswerStateInput } from "@/components/ui/answer-state"; +import { isProviderGenerationFallbackCode, publicFallbackReason } from "@/lib/rag/rag-fallback-reason"; -/** The overseas-processing line under an answer (audit S3, 2026-10-13). Draft wording: owner approval pending. */ -export const ANSWER_PROVIDER_NOTICE_TEXT = - "Questions are sent to an overseas AI service (OpenAI) to search and write answers. Don't include patient details."; +/** + * The overseas-processing line under an answer (audit S3, 2026-10-13). Shortened to one phone line by the + * owner, 17 Oct 2026 (docs/decisions/2026-10-17-answer-page-tidy-up.md). It still names the service, says + * it is overseas and asks for no patient details. + */ +export const ANSWER_PROVIDER_NOTICE_TEXT = "Sent to OpenAI overseas. Don't include patient details."; + +/** The visible words when the model could not write the answer and the source text is shown instead. */ +export const ANSWER_SUMMARY_UNAVAILABLE_TEXT = "AI summary unavailable"; /** * One quiet line under the latest answer, and on the Answer home before the first question is sent @@ -36,3 +44,49 @@ export function AnswerProviderNotice({

); } + +/** + * The public reason when this answer is degraded only because the model could not write it, else null. + * One test for both places, so the sentence is never shown above the answer and below it at once. + */ +export function answerSummaryUnavailableReason(answer: AnswerStateInput): string | null { + const code = answer.fallbackReasonCode; + if (!answerUsesDegradedMode(answer) || !code || !isProviderGenerationFallbackCode(code)) return null; + return publicFallbackReason(code); +} + +/** + * Amber line directly under the overseas notice when the model could not write the answer + * (owner decision, 17 Oct 2026). It replaced the grey sentence above the question, which pushed the + * question and the answer down. Only the provider failures land here: every other degraded reason + * says something about the evidence and keeps its full sentence above the answer. + * + * The visible words are short. The full public reason stays as screen-reader text, so a screen + * reader still hears why and that the verified source-backed portion is what is shown. + */ +export function AnswerSummaryUnavailableNotice({ + answer, + className, +}: { + answer: AnswerStateInput; + className?: string; +}) { + const reason = answerSummaryUnavailableReason(answer); + if (!reason) return null; + return ( +

+

+ ); +} diff --git a/src/components/clinical-dashboard/answer-result-surface.tsx b/src/components/clinical-dashboard/answer-result-surface.tsx index 9d26b1977c..203a3386d2 100644 --- a/src/components/clinical-dashboard/answer-result-surface.tsx +++ b/src/components/clinical-dashboard/answer-result-surface.tsx @@ -44,6 +44,7 @@ import { isCurrencyReviewWarning, type AnswerRenderModel } from "@/lib/answer-re import { demoAnswerDisclosure } from "@/lib/answer-client-payload"; import { formatRestoredAnswerAge } from "@/lib/answer-thread-storage"; import { publicFallbackReason } from "@/lib/rag/rag-fallback-reason"; +import { answerSummaryUnavailableReason } from "@/components/clinical-dashboard/answer-provider-notice"; import type { ClientBestSourceRecommendation, ClientQuoteCard, @@ -78,7 +79,7 @@ import { compactVerificationWordingFor, VerificationNotice } from "@/components/ * is the micro floor for count bubbles and legends. * * Nothing about what these state has changed: same words, same amber tokens, same icons. */ -const chipShape = "inline-flex min-h-6 items-center gap-1 rounded-full border px-2.5 text-xs font-semibold"; +const chipShape = "inline-flex min-h-6 items-center gap-1 rounded-md border px-2 text-xs font-semibold"; /** * An interactive chip is a small pill inside a full-size button, not a small * button. `before:-inset-y-*` hit expansion draws the same 48px region and is @@ -390,7 +391,7 @@ function StagedAnswerResultSurfaceImpl({ > 0 || answerReviewDue || sourceOnlyAnswer ? ( <> @@ -480,7 +487,7 @@ function StagedAnswerResultSurfaceImpl({ )} >