"use client"; /** * v1.4.25 W19c-Frontend — Research-mode MDR acknowledgment dialog. * * Opt-in gate in front of the GLP-1 drug-level chart. The chart paints * a unit-less Bateman PK curve over the user's logged intakes (research * §2.3 / §9.6.7). Because a "drug-level" surface is the brightest EU * MDR Class IIa edge in HealthLog's surface area (research §11 + §12.4), * the chart never paints until the user has explicitly read and * acknowledged the disclaimer below. The acknowledgment record is * server-stamped with `RESEARCH_MODE_DISCLAIMER_VERSION`; if the copy * changes the user re-acknowledges before the chart unhides. * * The dialog is rendered controlled by the Settings toggle and the * "re-prompt" banner (commit 3 of this phase). It accepts: * - `open` / `onOpenChange` for full external control * - `currentDisclaimerVersion` — the server's authoritative version * string. Acknowledgment posts THIS value, not the imported * constant — server redeploys that bump the constant must be * reflected without a client reload (W19c-Backend phase report). * - `onAcknowledged()` — fires after a successful 200; the caller * re-fetches the research-mode state and renders the toggle in * the new state. * * Copy direction (Marc-Voice, English, no AI/phase/wave mentions): * - "What this is" — single-paragraph framing: the chart is an * estimate from EMA-published population PK. * - "What this isn't" — explicit boundary: not a measurement, not * personalised, not medical advice. * - "Why it's an estimate" — population PK + IIV; per-patient * variation is large. * - "MDR boundary" — cite EU 2017/745 and MDCG 2021-24 verbatim. * - "Citations" — EMA EPAR + Schneck/Urva 2024 (ASCPT psp4.13099). * * Toast surface uses sonner (the project convention) so the user gets * a transient confirmation on either success or rejection. */ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { AlertTriangle, BookOpenCheck, Loader2 } from "lucide-react"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; export interface ResearchModeAcknowledgmentDialogProps { open: boolean; onOpenChange: (next: boolean) => void; /** * The version string from `GET /api/auth/me/research-mode`. The * server compares it byte-for-byte against the current constant; a * stale value forces a 400 from the API which we surface as an * error toast and keep the dialog open. Pass `null` while the * GET is in flight; the Acknowledge CTA stays disabled. */ currentDisclaimerVersion: string | null; /** * Fires after the POST returns 200. Caller invalidates the * `research-mode` query so the toggle reflects the new state. */ onAcknowledged?: () => void; } export function ResearchModeAcknowledgmentDialog({ open, onOpenChange, currentDisclaimerVersion, onAcknowledged, }: ResearchModeAcknowledgmentDialogProps) { const { t } = useTranslations(); const queryClient = useQueryClient(); const [errorMessage, setErrorMessage] = useState(null); const mutation = useMutation({ mutationFn: async (version: string) => { const res = await fetch("/api/auth/me/research-mode", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ acknowledged: true, version }), }); const json = await res.json().catch(() => ({})); if (!res.ok) { const code = typeof json?.error === "string" ? json.error : `http-${res.status}`; throw new Error(code); } return json?.data ?? null; }, onSuccess: () => { setErrorMessage(null); toast.success(t("medications.researchMode.dialog.successToast")); queryClient.invalidateQueries({ queryKey: queryKeys.researchMode() }); onAcknowledged?.(); onOpenChange(false); }, onError: (err: unknown) => { // The 400 stale-version path is the only one we surface // explicitly — every other failure mode (401, 429, 500, network) // collapses to a generic error so the dialog stays open but the // user knows the acknowledgment didn't persist. const code = err instanceof Error ? err.message : "unknown"; const isStale = code === "research-mode.version.stale"; const message = isStale ? t("medications.researchMode.dialog.staleVersionToast") : t("medications.researchMode.dialog.errorToast"); setErrorMessage(message); toast.error(message); }, }); function handleAcknowledge() { if (!currentDisclaimerVersion) return; mutation.mutate(currentDisclaimerVersion); } function handleCancel() { setErrorMessage(null); onOpenChange(false); } return ( {t("medications.researchMode.dialog.title")} } description={t("medications.researchMode.dialog.intro")} className="sm:max-w-xl" footer={
} >

{t("medications.researchMode.dialog.whatItIsHeader")}

{t("medications.researchMode.dialog.whatItIs")}

{t("medications.researchMode.dialog.whatItIsntHeader")}

{t("medications.researchMode.dialog.whatItIsnt")}

{t("medications.researchMode.dialog.whyEstimateHeader")}

{t("medications.researchMode.dialog.whyEstimate")}

{t("medications.researchMode.dialog.mdrBoundary")}

{t("medications.researchMode.dialog.citationsHeader")}

{t("medications.researchMode.dialog.citations")}

{currentDisclaimerVersion && (

{t("medications.researchMode.dialog.versionLine", { version: currentDisclaimerVersion, })}

)} {errorMessage && (

{errorMessage}

)}
); }