From 9400f5214aba5157f44efe87dd5ec292f830c9ad Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:31:11 +0800 Subject: [PATCH 1/7] Implement audit design fixes and fallback improvements --- .../differentials/[id]/not-found.tsx | 22 +++++++++++ .../(search-app)/documents/[id]/not-found.tsx | 22 +++++++++++ src/app/(search-app)/dsm/error.tsx | 7 ++++ src/app/(search-app)/formulation/error.tsx | 7 ++++ src/app/(search-app)/medications/error.tsx | 7 ++++ .../(search-app)/therapy-compass/error.tsx | 7 ++++ src/app/global-error.tsx | 39 ++++++++++++++++++- src/components/DocumentViewer.tsx | 3 +- .../clinical-dashboard/answer-status.tsx | 2 +- .../clinical-dashboard/image-lightbox.tsx | 17 ++++++-- .../clinical-dashboard/search-utils.ts | 31 +++++++++++++++ .../clinical-dashboard/signed-image.tsx | 9 ++++- src/components/mode-home-page-skeleton.tsx | 6 +-- src/components/route-error-boundary.tsx | 30 +++++++++++++- src/components/ui-primitives.tsx | 2 +- 15 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 src/app/(search-app)/differentials/[id]/not-found.tsx create mode 100644 src/app/(search-app)/documents/[id]/not-found.tsx create mode 100644 src/app/(search-app)/dsm/error.tsx create mode 100644 src/app/(search-app)/formulation/error.tsx create mode 100644 src/app/(search-app)/medications/error.tsx create mode 100644 src/app/(search-app)/therapy-compass/error.tsx diff --git a/src/app/(search-app)/differentials/[id]/not-found.tsx b/src/app/(search-app)/differentials/[id]/not-found.tsx new file mode 100644 index 0000000000..74109796be --- /dev/null +++ b/src/app/(search-app)/differentials/[id]/not-found.tsx @@ -0,0 +1,22 @@ +"use client"; + +import Link from "next/link"; +import { EmptyState } from "@/components/ui-primitives"; +import { FileQuestion } from "lucide-react"; + +export default function NotFound() { + return ( +
+ + Return to differentials + + } + /> +
+ ); +} diff --git a/src/app/(search-app)/documents/[id]/not-found.tsx b/src/app/(search-app)/documents/[id]/not-found.tsx new file mode 100644 index 0000000000..8b7da78cf6 --- /dev/null +++ b/src/app/(search-app)/documents/[id]/not-found.tsx @@ -0,0 +1,22 @@ +"use client"; + +import Link from "next/link"; +import { EmptyState } from "@/components/ui-primitives"; +import { FileQuestion } from "lucide-react"; + +export default function NotFound() { + return ( +
+ + Return to document library + + } + /> +
+ ); +} diff --git a/src/app/(search-app)/dsm/error.tsx b/src/app/(search-app)/dsm/error.tsx new file mode 100644 index 0000000000..547d82d6d1 --- /dev/null +++ b/src/app/(search-app)/dsm/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ; +} diff --git a/src/app/(search-app)/formulation/error.tsx b/src/app/(search-app)/formulation/error.tsx new file mode 100644 index 0000000000..547d82d6d1 --- /dev/null +++ b/src/app/(search-app)/formulation/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ; +} diff --git a/src/app/(search-app)/medications/error.tsx b/src/app/(search-app)/medications/error.tsx new file mode 100644 index 0000000000..547d82d6d1 --- /dev/null +++ b/src/app/(search-app)/medications/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ; +} diff --git a/src/app/(search-app)/therapy-compass/error.tsx b/src/app/(search-app)/therapy-compass/error.tsx new file mode 100644 index 0000000000..547d82d6d1 --- /dev/null +++ b/src/app/(search-app)/therapy-compass/error.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ; +} diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index e71ed9dd48..bc42369a68 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; /** * Last-resort boundary for the App Router. Unlike `app/error.tsx`, this replaces @@ -12,11 +12,28 @@ import { useEffect, useRef } from "react"; */ export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { const headingRef = useRef(null); + const [copied, setCopied] = useState(false); + useEffect(() => { console.error("Fatal error captured by global-error boundary:", error); headingRef.current?.focus({ preventScroll: true }); }, [error]); + const handleCopyDiagnostics = () => { + const diagnosticPayload = { + name: error.name, + message: error.message, + digest: error.digest, + url: typeof window !== "undefined" ? window.location.href : "unknown", + userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "unknown", + timestamp: new Date().toISOString(), + }; + navigator.clipboard.writeText(JSON.stringify(diagnosticPayload, null, 2)).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + return ( Reload page + diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index e6002dcfa8..2b0d89300f 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -41,6 +41,7 @@ import { clinicalDivider, cn, codeText, + EmptyState, eyebrowText, floatingControl, glassOverlaySurface, @@ -1618,7 +1619,7 @@ export function DocumentViewer({ {effectiveLoadingDocument ? ( ) : clinicalImages.length === 0 ? ( -

No indexed clinically useful tables or diagrams.

+ ) : ( clinicalImages.map((image) => ) )} diff --git a/src/components/clinical-dashboard/answer-status.tsx b/src/components/clinical-dashboard/answer-status.tsx index 2e616ac7b6..9e6db22bfb 100644 --- a/src/components/clinical-dashboard/answer-status.tsx +++ b/src/components/clinical-dashboard/answer-status.tsx @@ -216,7 +216,7 @@ export function AnswerProgressStepper({ type="button" onClick={onStop} data-testid="stop-answer" - className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-full border border-[color:var(--border-strong)] bg-[color:var(--surface-raised)] px-3 text-xs font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" + className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded-full border border-[color:var(--border-strong)] bg-[color:var(--surface-raised)] px-3 text-xs font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition motion-reduce:transition-none hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" > Stop diff --git a/src/components/clinical-dashboard/image-lightbox.tsx b/src/components/clinical-dashboard/image-lightbox.tsx index 69c3e639bf..283634ae23 100644 --- a/src/components/clinical-dashboard/image-lightbox.tsx +++ b/src/components/clinical-dashboard/image-lightbox.tsx @@ -37,6 +37,7 @@ export function ImageLightbox({ returnFocusRef?: RefObject; }) { const { url, failed, retry, markFailed } = useSignedImageUrl(endpoint, open); + const [retryDisabled, setRetryDisabled] = useState(false); const [scale, setScale] = useState(1); const [rotation, setRotation] = useState(0); const [translate, setTranslate] = useState({ x: 0, y: 0 }); @@ -47,6 +48,13 @@ export function ImageLightbox({ scaleRef.current = scale; }, [scale]); + const handleRetry = useCallback(() => { + if (retryDisabled) return; + setRetryDisabled(true); + retry(); + setTimeout(() => setRetryDisabled(false), 2000); + }, [retryDisabled, retry]); + // Reset the view on close so the next open never inherits a prior image's zoom. const handleClose = useCallback(() => { setScale(1); @@ -125,11 +133,12 @@ export function ImageLightbox({ Image could not load. ) : ( diff --git a/src/components/clinical-dashboard/search-utils.ts b/src/components/clinical-dashboard/search-utils.ts index cf7325a62c..50597aa85a 100644 --- a/src/components/clinical-dashboard/search-utils.ts +++ b/src/components/clinical-dashboard/search-utils.ts @@ -312,3 +312,34 @@ export function classifyAnswerError(error: unknown): AnswerErrorKind { } return "failure"; } + +/** + * Generate intelligent query rephrasing suggestions for zero-result states. + */ +export function generateQuerySuggestions(query: string): string[] { + if (!query || query.trim() === "") { + return ["Check for spelling errors", "Try broader search terms", "Remove strict filters"]; + } + + const suggestions: string[] = []; + const trimmed = query.trim(); + + if (trimmed.split(/\s+/).length > 3) { + suggestions.push("Use fewer words"); + } + + if (trimmed.includes('"') || trimmed.includes("'")) { + suggestions.push("Remove quotes for a broader search"); + } + + const upperQuery = trimmed.toUpperCase(); + if (upperQuery.includes(" AND ") || upperQuery.includes(" OR ") || upperQuery.includes(" NOT ")) { + suggestions.push("Check boolean operators (AND, OR, NOT)"); + } else { + suggestions.push("Try more general medical terms"); + } + + suggestions.push("Check for alternate spellings"); + + return suggestions.slice(0, 3); +} diff --git a/src/components/clinical-dashboard/signed-image.tsx b/src/components/clinical-dashboard/signed-image.tsx index 6523a59702..f65033221c 100644 --- a/src/components/clinical-dashboard/signed-image.tsx +++ b/src/components/clinical-dashboard/signed-image.tsx @@ -57,6 +57,7 @@ export const SignedImage = memo(function SignedImage({ const [lightboxOpen, setLightboxOpen] = useState(false); const frameRef = useRef(null); const triggerRef = useRef(null); + const [retryDisabled, setRetryDisabled] = useState(false); const { url, failed, retry, markFailed } = useSignedImageUrl(endpoint, shouldLoad); // Defer the request until the frame is near the viewport. A cached URL seeds @@ -84,9 +85,12 @@ export const SignedImage = memo(function SignedImage({ }, [rootMargin, shouldLoad]); function retryImage() { + if (retryDisabled) return; + setRetryDisabled(true); setLoaded(false); setShouldLoad(true); retry(); + setTimeout(() => setRetryDisabled(false), 2000); } function handleImageError() { @@ -111,9 +115,10 @@ export const SignedImage = memo(function SignedImage({ diff --git a/src/components/mode-home-page-skeleton.tsx b/src/components/mode-home-page-skeleton.tsx index 6c9eaa2955..b2811600b1 100644 --- a/src/components/mode-home-page-skeleton.tsx +++ b/src/components/mode-home-page-skeleton.tsx @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui-primitives"; export function ModeHomePageSkeleton() { return (
@@ -36,7 +36,7 @@ export function ModeHomeRouteLoading() { export function DocumentSearchPageSkeleton() { return (
@@ -55,7 +55,7 @@ export function DocumentSearchPageSkeleton() { export function DocumentViewerPageSkeleton() { return (
diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index c905644e9f..846cb70778 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useRef } from "react"; -import { TriangleAlert, RefreshCw } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { TriangleAlert, RefreshCw, ClipboardCopy, Check } from "lucide-react"; import { cn, primaryControl } from "@/components/ui-primitives"; @@ -39,11 +39,28 @@ export function RouteErrorBoundary({ minHeightClass = "min-h-[50vh]", }: RouteErrorBoundaryProps) { const headingRef = useRef(null); + const [copied, setCopied] = useState(false); + useEffect(() => { console.error(logLabel, error); headingRef.current?.focus({ preventScroll: true }); }, [error, logLabel]); + const handleCopyDiagnostics = () => { + const diagnosticPayload = { + name: error.name, + message: error.message, + digest: error.digest, + url: typeof window !== "undefined" ? window.location.href : "unknown", + userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "unknown", + timestamp: new Date().toISOString(), + }; + navigator.clipboard.writeText(JSON.stringify(diagnosticPayload, null, 2)).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + return (
)} + +
diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index fc515d7612..f746bc86a9 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -487,7 +487,7 @@ export function EmptyState({ title, body, actions, - live, + live = "polite", tone = "neutral", testId, }: { From 47f69ffebaf1e38d6e42cd37d7f6a351bbdc33e4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:54:08 +0800 Subject: [PATCH 2/7] chore: format files --- src/components/DocumentViewer.tsx | 6 +++++- src/components/route-error-boundary.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 2b0d89300f..8568bc36d3 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1619,7 +1619,11 @@ export function DocumentViewer({ {effectiveLoadingDocument ? ( ) : clinicalImages.length === 0 ? ( - + ) : ( clinicalImages.map((image) => ) )} diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index 846cb70778..76db858867 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -116,7 +116,11 @@ export function RouteErrorBoundary({ onClick={handleCopyDiagnostics} className="flex items-center justify-center gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-4 py-2 text-sm font-medium text-[color:var(--text)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" > - {copied ?
From da02ea0d6d04523fe7189eeaecb94066c7b04cf6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Jul 2026 17:33:58 +0000 Subject: [PATCH 3/7] ci: retrigger checks after npm ECONNRESET flake Safety and config checks failed on a transient npm ci network abort; Build/Unit/Static already passed on this tip. Co-authored-by: BigSimmo From 412a802fd7df0a125e6b8bb1fd24a28561b1ff84 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Jul 2026 17:56:39 +0000 Subject: [PATCH 4/7] fix: make audit fallbacks reachable and address review blockers Move differentials not-found to the real segment ancestor, call notFound() for document 404s, wire zero-result query suggestions, harden clipboard copy diagnostics, and use shared mode home hrefs for recovery links. Co-authored-by: BigSimmo --- .../differentials/{[id] => }/not-found.tsx | 3 ++- .../(search-app)/documents/[id]/not-found.tsx | 3 ++- src/app/(search-app)/documents/[id]/page.tsx | 5 ++++ src/app/global-error.tsx | 26 +++++++++++++++---- src/components/ClinicalDashboard.tsx | 3 ++- .../clinical-dashboard/search-utils.ts | 7 ++--- .../document-viewer/document-rail-panels.tsx | 2 +- src/components/route-error-boundary.tsx | 26 +++++++++++++++---- 8 files changed, 58 insertions(+), 17 deletions(-) rename src/app/(search-app)/differentials/{[id] => }/not-found.tsx (76%) diff --git a/src/app/(search-app)/differentials/[id]/not-found.tsx b/src/app/(search-app)/differentials/not-found.tsx similarity index 76% rename from src/app/(search-app)/differentials/[id]/not-found.tsx rename to src/app/(search-app)/differentials/not-found.tsx index fa396b7760..667a52c89a 100644 --- a/src/app/(search-app)/differentials/[id]/not-found.tsx +++ b/src/app/(search-app)/differentials/not-found.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { EmptyState } from "@/components/ui-primitives"; import { FileQuestion } from "lucide-react"; +import { appModeHomeHref } from "@/lib/app-modes"; export default function NotFound() { return ( @@ -12,7 +13,7 @@ export default function NotFound() { title="Differential Not Found" body="The requested differential diagnosis could not be found or has been deleted." actions={ - + Return to differentials } diff --git a/src/app/(search-app)/documents/[id]/not-found.tsx b/src/app/(search-app)/documents/[id]/not-found.tsx index 2a702cc995..b2d52ee867 100644 --- a/src/app/(search-app)/documents/[id]/not-found.tsx +++ b/src/app/(search-app)/documents/[id]/not-found.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { EmptyState } from "@/components/ui-primitives"; import { FileQuestion } from "lucide-react"; +import { appModeHomeHref } from "@/lib/app-modes"; export default function NotFound() { return ( @@ -12,7 +13,7 @@ export default function NotFound() { title="Document Not Found" body="The requested document could not be found or has been deleted." actions={ - + Return to document library } diff --git a/src/app/(search-app)/documents/[id]/page.tsx b/src/app/(search-app)/documents/[id]/page.tsx index ac08e8ffa2..2adedd964f 100644 --- a/src/app/(search-app)/documents/[id]/page.tsx +++ b/src/app/(search-app)/documents/[id]/page.tsx @@ -1,4 +1,5 @@ import { headers } from "next/headers"; +import { notFound } from "next/navigation"; import { DocumentViewerLazy as DocumentViewer } from "@/components/document-viewer-lazy"; import { documentDetailQuerySchema, @@ -6,6 +7,7 @@ import { sanitizeDocumentDetailError, } from "@/lib/document-detail"; import type { DocumentDetailPayload } from "@/lib/document-detail-contract"; +import { PublicApiError } from "@/lib/http"; export default async function DocumentPage({ params, @@ -32,6 +34,9 @@ export default async function DocumentPage({ }); initialDetail = await loadAuthorizedDocumentDetail({ request, rawId: id, query: detailQuery }); } catch (error) { + if (error instanceof PublicApiError && error.status === 404) { + notFound(); + } initialError = sanitizeDocumentDetailError(error); } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index bc42369a68..ed0dc70ee6 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import { copyTextToClipboard } from "@/lib/copy-to-clipboard"; /** * Last-resort boundary for the App Router. Unlike `app/error.tsx`, this replaces @@ -12,13 +13,21 @@ import { useEffect, useRef, useState } from "react"; */ export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { const headingRef = useRef(null); + const copiedResetTimerRef = useRef | null>(null); const [copied, setCopied] = useState(false); + const [copyFailed, setCopyFailed] = useState(false); useEffect(() => { console.error("Fatal error captured by global-error boundary:", error); headingRef.current?.focus({ preventScroll: true }); }, [error]); + useEffect(() => { + return () => { + if (copiedResetTimerRef.current) clearTimeout(copiedResetTimerRef.current); + }; + }, []); + const handleCopyDiagnostics = () => { const diagnosticPayload = { name: error.name, @@ -28,10 +37,17 @@ export default function GlobalError({ error, reset }: { error: Error & { digest? userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "unknown", timestamp: new Date().toISOString(), }; - navigator.clipboard.writeText(JSON.stringify(diagnosticPayload, null, 2)).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); + void copyTextToClipboard(JSON.stringify(diagnosticPayload, null, 2)) + .then(() => { + setCopyFailed(false); + setCopied(true); + if (copiedResetTimerRef.current) clearTimeout(copiedResetTimerRef.current); + copiedResetTimerRef.current = setTimeout(() => setCopied(false), 2000); + }) + .catch(() => { + setCopied(false); + setCopyFailed(true); + }); }; return ( @@ -151,7 +167,7 @@ export default function GlobalError({ error, reset }: { error: Error & { digest? gap: "0.5rem", }} > - {copied ? "Copied Diagnostics" : "Copy Diagnostics"} + {copied ? "Copied Diagnostics" : copyFailed ? "Copy failed — try again" : "Copy Diagnostics"} diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 98aabf4197..ece5cb7496 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -136,6 +136,7 @@ import { answerPayloadIsUsable, classifyAnswerError, createAnswerRequestWatchdog, + generateQuerySuggestions, isRetryableError, keywordQueryFromNaturalLanguage, makeSearchError, @@ -3560,7 +3561,7 @@ export function ClinicalDashboard({ ) : clinicalImages.length === 0 ? ( diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index 76db858867..4ce84b85d5 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { TriangleAlert, RefreshCw, ClipboardCopy, Check } from "lucide-react"; import { cn, primaryControl } from "@/components/ui-primitives"; +import { copyTextToClipboard } from "@/lib/copy-to-clipboard"; export type RouteErrorBoundaryProps = { /** The error thrown by the segment, forwarded by Next.js. */ @@ -39,13 +40,21 @@ export function RouteErrorBoundary({ minHeightClass = "min-h-[50vh]", }: RouteErrorBoundaryProps) { const headingRef = useRef(null); + const copiedResetTimerRef = useRef | null>(null); const [copied, setCopied] = useState(false); + const [copyFailed, setCopyFailed] = useState(false); useEffect(() => { console.error(logLabel, error); headingRef.current?.focus({ preventScroll: true }); }, [error, logLabel]); + useEffect(() => { + return () => { + if (copiedResetTimerRef.current) clearTimeout(copiedResetTimerRef.current); + }; + }, []); + const handleCopyDiagnostics = () => { const diagnosticPayload = { name: error.name, @@ -55,10 +64,17 @@ export function RouteErrorBoundary({ userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "unknown", timestamp: new Date().toISOString(), }; - navigator.clipboard.writeText(JSON.stringify(diagnosticPayload, null, 2)).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); + void copyTextToClipboard(JSON.stringify(diagnosticPayload, null, 2)) + .then(() => { + setCopyFailed(false); + setCopied(true); + if (copiedResetTimerRef.current) clearTimeout(copiedResetTimerRef.current); + copiedResetTimerRef.current = setTimeout(() => setCopied(false), 2000); + }) + .catch(() => { + setCopied(false); + setCopyFailed(true); + }); }; return ( @@ -121,7 +137,7 @@ export function RouteErrorBoundary({ ) : (