marmar-context / web / context / src / demo / DemoApp.tsx
DemoApp.tsx
Raw
import { validCoverageResolution, type CoverageReview } from "../coverage";
import { factDetail } from "../logic/format";
import React, { useEffect, useRef, useState } from "react";
import {
  ArrowLeft,
  ArrowRight,
  ArrowSquareOut,
  ArrowsClockwise,
  Baby,
  BookOpenText,
  CaretRight,
  Check,
  CheckCircle,
  CircleNotch,
  DotsThree,
  DownloadSimple,
  FileText,
  Flask,
  Heartbeat,
  Info,
  List,
  Pill,
  Plus,
  Scales,
  ShieldCheck,
  ShieldWarning,
  Stack,
  Stethoscope,
  TestTube,
  Trash,
  UploadSimple,
  User,
  UsersThree,
  WarningCircle,
  X,
} from "@phosphor-icons/react";
import type {
  Assessment,
  Draft,
  Evaluation,
  Fact,
  Review,
  Scenario,
  Source,
  Status,
} from "../types";
import { conflicts } from "../review";
import {
  checkBlocker,
  doseLabels,
  findingEvidence,
  formatDate,
  initials,
  medicinePlan,
  patientName,
  patientProfile,
  plural,
  recordsFromText,
  resultSummary,
} from "../flow";
import {
  readWorkspace,
  parseWorkspace,
  WORKSPACE_KEY,
  recoverReview,
  linkedClarification,
  clarificationReviewed,
  actionableWarnings,
  clarificationError,
  type Workspace,
} from "../workspace";

import {
  WorkspaceStore,
  WorkspaceConflict,
  workspaceContent,
} from "../workspace-store";

function downloadJson(value: unknown, filename: string) {
  const url = URL.createObjectURL(
    new Blob([JSON.stringify(value, null, 2)], { type: "application/json" })
  );
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

type Page = "workspace" | "cases" | "evidence" | "evaluation";
type Step = "sources" | "review" | "insights";
const pageNames: Record<Page, string> = {
  workspace: "Check",
  cases: "Patients",
  evidence: "How it works",
  evaluation: "Tests",
};
const pageIcons: Record<Page, typeof Flask> = {
  workspace: Stethoscope,
  cases: UsersThree,
  evidence: BookOpenText,
  evaluation: Flask,
};
const doseTone: Record<string, string> = {
  within_usual_range: "green",
  above_limit: "red",
  check_with_pharmacist: "amber",
  not_enough_info: "",
};
const tierNames: Record<string, string> = {
  today: "Review today",
  this_week: "Review this week",
  good_to_know: "Good to know",
  get_help_now: "Urgent review",
};
async function api<T>(
  path: string,
  body?: unknown,
  signal?: AbortSignal
): Promise<T> {
  try {
    const response = await fetch(`/api/${path}`, {
      signal: AbortSignal.any([
        AbortSignal.timeout(45000),
        ...(signal ? [signal] : []),
      ]),
      ...(body === undefined
        ? {}
        : {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
          }),
    });
    const data = await response.json();
    if (data.signInRequired)
      window.dispatchEvent(new Event("marmar-sign-in-required"));
    if (!response.ok)
      throw new Error(data.error || "The request could not be completed.");
    return data as T;
  } catch (error) {
    if (signal?.aborted)
      throw new Error(
        "Extraction cancelled. Your source notes are unchanged; no new result was used."
      );
    if (error instanceof Error && error.name === "TimeoutError")
      throw new Error(
        "The request timed out. Your source notes are unchanged. Retry extraction when the service is available."
      );
    if (error instanceof TypeError || error instanceof SyntaxError)
      throw new Error(
        "The local service is unavailable or returned an invalid response. Your source notes are unchanged. Check the service and retry."
      );
    throw error;
  }
}

/**
 * "Try a fictional patient" (ACC-2): no uploads, no pasted text, local reader only, nothing saved.
 * The full reviewer preview (readOnly false: the shared password and the model connection form)
 * runs only on the Vite dev server (ACC-11); a build always gets the read-only demo.
 */
export default function DemoApp({ readOnly: asked = false }: { readOnly?: boolean }) {
  const readOnly = asked || !import.meta.env.DEV;
  const [signInNeeded, setSignInNeeded] = useState(false);
  const [signInError, setSignInError] = useState("");
  const [signingIn, setSigningIn] = useState(false);
  useEffect(() => {
    const requireSignIn = () => setSignInNeeded(true);
    window.addEventListener("marmar-sign-in-required", requireSignIn);
    return () =>
      window.removeEventListener("marmar-sign-in-required", requireSignIn);
  }, []);
  const workspaceStore = useRef(new WorkspaceStore());
  const savedRevision = useRef<string | null>(null);
  const savedContent = useRef("");
  const saveQueue = useRef(Promise.resolve());
  const saveEpoch = useRef(0);
  const savePaused = useRef(false);
  const [saving, setSaving] = useState(false);
  const [storageConflict, setStorageConflict] = useState(false);
  const [saveAttempt, setSaveAttempt] = useState(0);
  const [ready, setReady] = useState(false);
  const [factFilter, setFactFilter] = useState<"all" | "pending" | "excluded">(
    "all"
  );
  const [saveError, setSaveError] = useState("");
  const [storageBlocked, setStorageBlocked] = useState(false);
  const [recoveryNeeded, setRecoveryNeeded] = useState(false);
  const [restored, setRestored] = useState(false);
  const [savedReport, setSavedReport] = useState<Record<
    string,
    unknown
  > | null>(null);
  const [clarificationFormError, setClarificationFormError] = useState("");
  const [correcting, setCorrecting] = useState<Source | null>(null);
  const [page, setPage] = useState<Page>("workspace");
  const [menuOpen, setMenuOpen] = useState(false);
  const [step, setStep] = useState<Step>("sources");
  const [cases, setCases] = useState<Scenario[]>([]);
  const [scenario, setScenario] = useState<Scenario | null>(null);
  const [sources, setSources] = useState<Source[]>([]);
  const [sourceId, setSourceId] = useState("");
  const [status, setStatus] = useState<Status | null>(null);
  const [draft, setDraft] = useState<Draft | null>(null);
  const [review, setReview] = useState<Review>({});
  const [coverageReview, setCoverageReview] = useState<CoverageReview>({});
  const [assessment, setAssessment] = useState<Assessment | null>(null);
  const [evaluation, setEvaluation] = useState<Evaluation | null>(null);
  const [busy, setBusy] = useState("");
  const extractionController = useRef<AbortController | null>(null);
  const [elapsed, setElapsed] = useState(0);
  const [error, setError] = useState("");
  const [toast, setToast] = useState("");
  const [highlight, setHighlight] = useState<number | null>(null);
  const [editing, setEditing] = useState(false);
  const [editText, setEditText] = useState("");
  const [useModel, setUseModel] = useState(false);
  const [freshModel, setFreshModel] = useState(false);
  const [acknowledge, setAcknowledge] = useState(false);
  const sourcePanel = useRef<HTMLDivElement>(null);
  const connectionDialog = useRef<HTMLDialogElement>(null);
  const caseDialog = useRef<HTMLDialogElement>(null);
  const recordDialog = useRef<HTMLDialogElement>(null);
  const clarificationDialog = useRef<HTMLDialogElement>(null);
  const [clarificationTarget, setClarificationTarget] = useState<{
    sourceId: string;
    line: number;
    reason: string;
  } | null>(null);
  const [recordText, setRecordText] = useState("");
  const [newCaseFiles, setNewCaseFiles] = useState<
    { name: string; text: string }[]
  >([]);
  const [newCaseError, setNewCaseError] = useState("");
  const [recordError, setRecordError] = useState("");
  const connectionForm = useRef<HTMLFormElement>(null);
  const [connectionError, setConnectionError] = useState("");
  const current = sources.find((s) => s.id === sourceId) ?? sources[0];
  const reviewed = draft?.facts.filter((f) => review[f.id]).length ?? 0;
  const selected = draft?.facts.filter((f) => review[f.id] === "include") ?? [];
  const conflictKeys = conflicts(selected);
  const conflictLabels = [
    ...new Set(
      selected.filter((f) => conflictKeys.includes(f.key)).map((f) => f.label)
    ),
  ];
  const issues = selected.filter((f) => f.problem);
  const pending = (draft?.facts.length ?? 0) - reviewed;
  const clarificationOriginal = sources.find(
    (s) => s.id === clarificationTarget?.sourceId
  );
  const clarify = (sourceId: string, line: number, reason: string) => {
    setCorrecting(null);
    setClarificationFormError("");
    setClarificationTarget({ sourceId, line, reason });
    clarificationDialog.current?.showModal();
  };
  const clarificationLink = (originalId: string, line: number) => {
    const linked = linkedClarification(sources, originalId, line);
    const resolved = clarificationReviewed(
      sources,
      draft,
      review,
      originalId,
      line
    );
    return linked ? (
      <button
        className="link"
        onClick={() => {
          setSourceId(linked.id);
          setHighlight(null);
        }}
      >
        <FileText size={12} />
        {resolved ? "Clarification reviewed" : "Review added clarification"}
        <ArrowSquareOut size={11} />
      </button>
    ) : null;
  };

  const load = (item: Scenario) => {
    setRecoveryNeeded(false);
    setRestored(false);
    setScenario(item);
    setFactFilter("all");
    setSources(structuredClone(item.sources));
    setSourceId(item.sources[0].id);
    setDraft(null);
    setRecoveryNeeded(false);
    setReview({});
    setAssessment(null);
    setSavedReport(null);
    setStep("sources");
    setPage("workspace");
    setError("");
    setHighlight(null);
    setEditing(false);
    setAcknowledge(false);
  };
  const applySaved = (saved: Workspace) => {
    setScenario(saved.scenario);
    setSources(saved.sources);
    setSourceId(saved.sourceId);
    setDraft(saved.draft);
    setReview(saved.review);
    setCoverageReview(saved.coverageReview ?? {});
    setUseModel(saved.useModel);
    setEditing(saved.editing);
    setEditText(saved.editText);
    setSavedReport(saved.report);
    setAssessment(null);
    setAcknowledge(false);
    setStep(saved.draft ? "review" : "sources");
    setRestored(true);
    setRecoveryNeeded(false);
    setError("");
    setCases((prev) => [
      ...prev.filter((c) => c.id !== saved.scenario.id),
      saved.scenario,
    ]);
  };
  const loadLatest = async () => {
    try {
      const value = await workspaceStore.current.read();
      if (!value.workspace) throw new Error("No saved workspace is available.");
      saveEpoch.current++;
      savedRevision.current = value.revision;
      savedContent.current = workspaceContent(value.workspace);
      savePaused.current = false;
      setStorageBlocked(false);
      setStorageConflict(false);
      setSaveError("");
      setSaving(false);
      applySaved(value.workspace);
    } catch {
      setSaveError(
        "Could not load the saved workspace. This tab's work is unchanged."
      );
    }
  };
  useEffect(() => {
    let active = true;
    void (async () => {
      let saved: Workspace | null = null;
      if (!readOnly) try {
        const value = await workspaceStore.current.read();
        if (!active) return;
        savedRevision.current = value.revision;
        saved = value.workspace;
        if (saved) savedContent.current = workspaceContent(saved);
        // Migrate old per-tab saves only when no durable workspace exists.
        if (!saved) saved = readWorkspace(sessionStorage);
      } catch {
        savePaused.current = true;
        setStorageBlocked(true);
        setSaveError(
          "Browser storage could not be opened or its saved copy could not be read. It has not been overwritten. Retry saving, or keep this tab open."
        );
        try {
          saved = readWorkspace(sessionStorage);
        } catch {
          /* Preserve unreadable legacy data. */
        }
      }
      if (!active) return;
      if (saved) applySaved(saved);
      setReady(true);
      try {
        const [items, health] = await Promise.all([
          api<Scenario[]>("cases"),
          api<Status>("status"),
        ]);
        if (!active) return;
        setCases(
          saved && !items.some((i) => i.id === saved!.scenario.id)
            ? [...items, saved.scenario]
            : items
        );
        setStatus(health);
        if (!saved) {
          setUseModel(!readOnly && health.modelConfigured);
          load(items[0]);
        }
      } catch (e) {
        if (active)
          setError(
            e instanceof Error ? e.message : "The local service is unavailable."
          );
      }
    })();
    return () => {
      active = false;
    };
  }, []);
  const snapshot = (): Workspace | null =>
    scenario
      ? {
          version: 1,
          savedAt: new Date().toISOString(),
          scenario,
          sources,
          sourceId,
          draft,
          review,
          coverageReview,
          useModel,
          editing,
          editText,
          report: savedReport,
        }
      : null;
  useEffect(() => {
    if (readOnly || !ready || !scenario || storageBlocked || savePaused.current) return;
    const workspace = snapshot()!;
    const content = workspaceContent(workspace);
    if (content === savedContent.current) return;
    const epoch = saveEpoch.current;
    setSaving(true);
    saveQueue.current = saveQueue.current.then(async () => {
      if (epoch !== saveEpoch.current || savePaused.current) return;
      try {
        const revision = await workspaceStore.current.save(
          workspace,
          savedRevision.current
        );
        savedRevision.current = revision;
        savedContent.current = content;
        setSaveError("");
        try {
          sessionStorage.removeItem(WORKSPACE_KEY);
        } catch {
          /* Durable commit already succeeded. */
        }
      } catch (e) {
        savePaused.current = true;
        setStorageBlocked(true);
        setStorageConflict(e instanceof WorkspaceConflict);
        setSaveError(
          e instanceof WorkspaceConflict
            ? e.message
            : "Autosave failed. Your last saved copy is intact, but these changes are not saved yet. Check field limits or available browser storage, then retry. Keep this tab open meanwhile."
        );
      }
    });
    const queued = saveQueue.current;
    void queued.finally(() => {
      if (saveQueue.current === queued) setSaving(false);
    });
  }, [
    ready,
    scenario,
    sources,
    sourceId,
    draft,
    review,
    coverageReview,
    useModel,
    editing,
    editText,
    savedReport,
    storageBlocked,
    saveAttempt,
  ]);
  useEffect(() => {
    const protectUnsaved = (event: BeforeUnloadEvent) => {
      if (saving || storageBlocked) {
        event.preventDefault();
        event.returnValue = "";
      }
    };
    window.addEventListener("beforeunload", protectUnsaved);
    return () => window.removeEventListener("beforeunload", protectUnsaved);
  }, [saving, storageBlocked]);
  useEffect(() => {
    const checkOtherTab = async () => {
      if (!ready || saving || savePaused.current) return;
      try {
        const value = await workspaceStore.current.read();
        if (value.revision !== savedRevision.current) {
          savePaused.current = true;
          setStorageBlocked(true);
          setStorageConflict(true);
          setSaveError(
            "Newer work was saved in another tab. This tab has not overwritten it."
          );
        }
      } catch {
        /* The next save surfaces a storage failure without losing this workspace. */
      }
    };
    window.addEventListener("focus", checkOtherTab);
    return () => window.removeEventListener("focus", checkOtherTab);
  }, [ready, saving]);
  useEffect(() => {
    if (!toast) return;
    const timer = window.setTimeout(() => setToast(""), 4500);
    return () => clearTimeout(timer);
  }, [toast]);
  const run = async (label: string, action: () => Promise<void>) => {
    setBusy(label);
    setError("");
    try {
      await action();
    } catch (e) {
      const message = e instanceof Error ? e.message : "Something went wrong.";
      setError(message);
      if (/expired|server restarted|no longer current/i.test(message))
        setRecoveryNeeded(true);
    } finally {
      setBusy("");
    }
  };
  useEffect(() => {
    if (busy !== "Extracting context") {
      setElapsed(0);
      return;
    }
    const start = Date.now();
    const timer = window.setInterval(
      () => setElapsed(Math.floor((Date.now() - start) / 1000)),
      1000
    );
    return () => clearInterval(timer);
  }, [busy]);
  useEffect(() => () => extractionController.current?.abort(), []);
  const extract = (recover = false) =>
    run("Extracting context", async () => {
      const controller = new AbortController();
      extractionController.current = controller;
      try {
        const health = await api<Status>("status");
        setStatus(health);
        if (useModel && !health.modelConfigured)
          throw new Error(
            "Reconnect Azure below before restoring live-model extraction. Your notes and review are saved."
          );
        const result = await api<Draft>(
          "extract",
          { sources, syntheticOnly: true, useModel, freshModel },
          controller.signal
        );
        controller.signal.throwIfAborted();
        const recovered = recover ? recoverReview(draft, result, review) : {};
        setDraft(result);
        setReview(recovered);
        setCoverageReview({});
        setAssessment(null);
        setSavedReport(null);
        setAcknowledge(false);
        setStep("review");
        setRecoveryNeeded(false);
        setRestored(false);
        if (recover)
          setToast(
            `${
              Object.keys(recovered).length
            } unchanged review decisions restored. Review new or changed facts and acknowledge the original notes again.`
          );
      } finally {
        extractionController.current = null;
      }
    });
  const check = () =>
    run("Running medication check", async () => {
      const result = await api<Assessment>("assess", {
        sources,
        review,
        coverageReview,
        fingerprint: draft!.fingerprint,
        draftId: draft!.id,
        acknowledgeUnparsed: acknowledge,
        syntheticOnly: true,
      });
      setAssessment(result);
      setRecoveryNeeded(false);
      setRestored(false);
      setStep("insights");
      try {
        setSavedReport(
          await api<Record<string, unknown>>(`reports/${result.reportId}`)
        );
      } catch {
        setToast(
          "Assessment complete. Report could not be saved locally yet; retry Export report while the service is available."
        );
      }
    });
  const showFact = (fact: Fact) => {
    setSourceId(fact.sourceId);
    setHighlight(fact.line);
    if (step === "insights") setStep("review");
    setTimeout(
      () =>
        sourcePanel.current?.scrollIntoView({
          behavior: "smooth",
          block: "nearest",
        }),
      40
    );
  };
  const decide = (id: string, decision: "include" | "exclude") => {
    setReview((prev) => ({ ...prev, [id]: decision }));
    setAssessment(null);
    setSavedReport(null);
    setError("");
  };
  const saveSource = () => {
    if (!editText.trim()) {
      setError("A source cannot be empty.");
      return;
    }
    setSources((prev) =>
      prev.map((s) => (s.id === sourceId ? { ...s, text: editText } : s))
    );
    setDraft(null);
    setRecoveryNeeded(false);
    setReview({});
    setAssessment(null);
    setSavedReport(null);
    setStep("sources");
    setEditing(false);
    setHighlight(null);
    setError("");
    setToast("Source updated. Extract again to review the new version.");
  };
  const exportReport = () =>
    run("Preparing report", async () => {
      if (savedReport) {
        downloadJson(savedReport, "marmar-context-evidence.json");
        return;
      }
      if (!assessment?.reportId) return;
      const report = await api<Record<string, unknown>>(
        `reports/${assessment.reportId}`
      );
      setSavedReport(report);
      downloadJson(report, "marmar-context-evidence.json");
    });
  const visibleWarnings = actionableWarnings(draft);
  const reviewedWarnings = visibleWarnings.filter(
    (w) =>
      !w.blocked &&
      clarificationReviewed(sources, draft, review, w.sourceId, w.line)
  );
  const unresolvedWarnings = visibleWarnings.filter(
    (w) => !reviewedWarnings.includes(w)
  );
  const remainingLines = (draft?.unparsed ?? []).filter(
    (l) =>
      !clarificationReviewed(sources, draft, review, l.sourceId, l.line) &&
      !visibleWarnings.some(
        (w) => w.sourceId === l.sourceId && w.line === l.line
      )
  );
  const nav = (next: Page) => {
    setPage(next);
    setMenuOpen(false);
    setError("");
  };

  const sourceTitle = (id: string) => sources.find((s) => s.id === id)?.title;
  const sourceLine = (id: string, line: number) =>
    sources.find((s) => s.id === id)?.text.split("\n")[line - 1] ?? "";
  const canClarify = (id: string, line: number) =>
    !readOnly &&
    !sources.find((s) => s.id === id)?.clarification &&
    !linkedClarification(sources, id, line);
  const blocker = checkBlocker({
    coverageReview,
    draft,
    review,
    acknowledged: acknowledge,
    editing,
  });
  const summary = assessment ? resultSummary(assessment) : null;
  const notesToCheck = !!draft?.unparsed.length || !!draft?.sourceReviewRequired;
  const excludedCount =
    draft?.facts.filter((f) => review[f.id] === "exclude").length ?? 0;
  const saveLabel = saveError
    ? "Not saved"
    : saving
    ? "Saving…"
    : "Saved in this browser";
  const includeAll = () => {
    setReview(
      Object.fromEntries(
        draft!.facts.filter((f) => !f.problem).map((f) => [f.id, "include"])
      )
    );
    setAssessment(null);
    setSavedReport(null);
    setError("");
  };
  const openNewCase = () => {
    setNewCaseFiles([]);
    setNewCaseError("");
    caseDialog.current?.showModal();
  };

  const documentView = (withTabs: boolean) =>
    current && (
      <div className="doc">
        {withTabs && sources.length > 1 && (
          <div className="doc-tabs" role="tablist" aria-label="Records">
            {sources.map((s) => (
              <button
                key={s.id}
                role="tab"
                aria-selected={s.id === current.id}
                disabled={!!busy || editing}
                onClick={() => {
                  setSourceId(s.id);
                  setHighlight(null);
                }}
              >
                {s.title}
              </button>
            ))}
          </div>
        )}
        <div className="doc-head">
          <div>
            <span className="eyebrow">{current.kind}</span>
            <h3>{current.title}</h3>
          </div>
          <span className="doc-date">{formatDate(current.date)}</span>
        </div>
        {current.clarification && (
          <div className="clarification-box">
            <strong>Added clarification</strong>
            <p>
              Confirmed by {current.clarification.confirmedBy}. Supplied by the
              reviewer, not independently verified.
            </p>
            <blockquote>{current.clarification.quote}</blockquote>
            <button
              className="link"
              onClick={() => {
                setSourceId(current.clarification!.sourceId);
                setHighlight(current.clarification!.line);
              }}
            >
              View original line {current.clarification.line}
              <ArrowSquareOut size={12} />
            </button>
          </div>
        )}
        {editing ? (
          <div className="editor">
            <label htmlFor="source-editor">Edit this synthetic record</label>
            <textarea
              id="source-editor"
              maxLength={12000}
              value={editText}
              onChange={(e) => setEditText(e.target.value)}
              spellCheck={false}
            />
            <div className="row-end">
              <button
                className="button"
                onClick={() => {
                  setEditing(false);
                  setError("");
                }}
              >
                Cancel
              </button>
              <button className="button primary" onClick={saveSource}>
                Save record
              </button>
            </div>
          </div>
        ) : (
          <ol className="doc-lines">
            {current.text.split("\n").map((line, i) => (
              <li
                key={i}
                className={highlight === i + 1 ? "highlight" : undefined}
              >
                <span className="ln">{i + 1}</span>
                <span className="lt">{line || " "}</span>
              </li>
            ))}
          </ol>
        )}
        <div className="doc-foot">
          <span>
            <ShieldCheck size={14} />
            Synthetic record
          </span>
          <span className="doc-foot-actions">
            {current.clarification && (
              <button
                className="link"
                disabled={!!busy}
                onClick={() => {
                  setCorrecting(current);
                  setClarificationTarget(current.clarification!);
                  setClarificationFormError("");
                  clarificationDialog.current?.showModal();
                }}
              >
                Correct clarification
              </button>
            )}
            {!readOnly && !editing &&
              !current.clarification &&
              !sources.some((s) => s.clarification?.sourceId === current.id) && (
                <button
                  className="link"
                  disabled={!!busy}
                  onClick={() => {
                    setEditText(current.text);
                    setEditing(true);
                  }}
                >
                  Edit record
                </button>
              )}
          </span>
        </div>
      </div>
    );

  const factRow = (f: Fact) => {
    const decision = review[f.id];
    const title = sourceTitle(f.sourceId);
    return (
      <div
        key={f.id}
        className={`fact ${decision ?? "pending"} ${
          conflictKeys.includes(f.key) && decision === "include"
            ? "conflict"
            : ""
        } ${f.problem ? "problem" : ""}`}
      >
        <span className={`fact-icon ${f.kind}`} aria-hidden="true">
          <FactIcon kind={f.kind} />
        </span>
        <div className="fact-body">
          <div className="fact-title">
            <strong>{f.label}</strong>
            {f.detail && <span>{factDetail(f.detail)}</span>}
          </div>
          {f.problem && (
            <p className="fact-problem">
              <WarningCircle size={14} />
              {f.problem}
            </p>
          )}
          <div className="fact-links">
            <button className="link quiet" onClick={() => showFact(f)}>
              <FileText size={12} />
              {title} · line {f.line}
            </button>
            {f.problem && clarificationLink(f.sourceId, f.line)}
            {f.problem && canClarify(f.sourceId, f.line) && (
              <button
                className="link"
                disabled={!!busy || sources.length >= 8}
                onClick={() => clarify(f.sourceId, f.line, f.problem!)}
              >
                Add clarification
              </button>
            )}
          </div>
        </div>
        <div className="decision" role="group" aria-label={`Decision: ${f.label}`}>
          <button
            title={`Include ${f.label}`}
            aria-label={`Include ${f.label} from ${title}`}
            aria-pressed={decision === "include"}
            disabled={!!f.problem || !!busy}
            onClick={() => decide(f.id, "include")}
          >
            <Check size={15} weight="bold" />
            <span>Include</span>
          </button>
          <button
            title={`Exclude ${f.label}`}
            aria-label={`Exclude ${f.label} from ${title}`}
            aria-pressed={decision === "exclude"}
            disabled={!!busy}
            onClick={() => decide(f.id, "exclude")}
          >
            <X size={14} weight="bold" />
            <span>Exclude</span>
          </button>
        </div>
      </div>
    );
  };

  const PageIcon = pageIcons[page];
  const navButton = (id: Page, count?: number) => {
    const Icon = pageIcons[id];
    return (
      <button
        key={id}
        className="nav-item"
        aria-current={page === id ? "page" : undefined}
        onClick={() => nav(id)}
      >
        <Icon size={16} />
        <span>{pageNames[id]}</span>
        {!!count && <span className="nav-count">{count}</span>}
      </button>
    );
  };
  const sidebar = (
    <>
      <div className="sidebar-brand">
        <a
          className="brand"
          aria-label="Context home"
          href="#"
          onClick={(e) => {
            e.preventDefault();
            nav("workspace");
          }}
        >
          <img src="/assets/logo-mark.svg" alt="MARMAR" />
          <span>Context</span>
        </a>
        {menuOpen && (
          <button
            className="icon-button"
            aria-label="Close navigation"
            onClick={() => setMenuOpen(false)}
          >
            <X size={16} />
          </button>
        )}
      </div>
      <div className="sidebar-body">
        <nav className="sidebar-nav" aria-label="Main navigation">
          <div className="nav-group" role="group" aria-label="Workspace">
            <p>Workspace</p>
            {navButton("workspace")}
            {navButton("cases", cases.length)}
          </div>
          <div className="nav-group" role="group" aria-label="Guides">
            <p>Guides</p>
            {navButton("evidence")}
            {navButton("evaluation")}
          </div>
        </nav>
        <div className="sidebar-foot">
          <span className="icon-chip">
            <ShieldCheck size={15} />
          </span>
          <span>
            <strong>Information tool</strong>
            <small>A clinician decides what to do</small>
          </span>
        </div>
      </div>
    </>
  );

  return (
    <div className="app">
      {import.meta.env.DEV && signInNeeded && (
        <div className="signin-backdrop">
          <form
            className="signin"
            aria-label="Reviewer sign-in"
            onSubmit={async (e) => {
              e.preventDefault();
              const form = e.currentTarget;
              setSigningIn(true);
              setSignInError("");
              try {
                await api("auth", {
                  password: new FormData(form).get("password"),
                });
                form.reset();
                const [items, health] = await Promise.all([
                  api<Scenario[]>("cases"),
                  api<Status>("status"),
                ]);
                setCases(
                  scenario && !items.some((i) => i.id === scenario.id)
                    ? [...items, scenario]
                    : items
                );
                setStatus(health);
                if (!scenario) {
                  load(items[0]);
                  setUseModel(health.modelConfigured);
                }
                setError("");
                setSignInNeeded(false);
              } catch (e) {
                setSignInError(
                  e instanceof Error ? e.message : "Sign-in failed. Try again."
                );
              } finally {
                setSigningIn(false);
              }
            }}
          >
            <img className="signin-mark" src="/assets/logo-mark.svg" alt="MARMAR" />
            <img className="signin-yasmar" src="/assets/yasmar-wave.png" alt="" />
            <h1>Context reviewer preview</h1>
            <p>
              See how a patient’s wider records change a medication check. Use
              synthetic patients only; your work saves in this browser.
            </p>
            <label htmlFor="preview-password">Preview password</label>
            <input
              id="preview-password"
              name="password"
              type="password"
              autoComplete="current-password"
              required
              maxLength={1024}
            />
            {signInError && (
              <p className="form-error" role="alert">
                {signInError}
              </p>
            )}
            <button className="button primary large" disabled={signingIn}>
              {signingIn ? "Signing in…" : "Open workspace"}
            </button>
          </form>
        </div>
      )}
      <div className="shell">
      <aside className="sidebar" aria-label="Context navigation">
        {sidebar}
      </aside>
      {menuOpen && (
        <div
          className="sheet"
          onClick={(e) => {
            if (e.target === e.currentTarget) setMenuOpen(false);
          }}
        >
          <aside className="sidebar open" aria-label="Context navigation">
            {sidebar}
          </aside>
        </div>
      )}
      <div className="main">
      <header className="appbar">
        <button
          className="icon-button menu-toggle"
          aria-label="Open navigation"
          aria-expanded={menuOpen}
          onClick={() => setMenuOpen(true)}
        >
          <List size={18} />
        </button>
        <nav className="crumbs" aria-label="Breadcrumb">
          <span className="crumb-icon" aria-hidden="true">
            <PageIcon size={14} />
          </span>
          {page === "workspace" && scenario ? (
            <>
              <span className="crumb-parent">{pageNames.workspace}</span>
              <CaretRight size={12} className="crumb-parent" />
              <span>{scenario.name}</span>
            </>
          ) : (
            <span>{pageNames[page]}</span>
          )}
        </nav>
        <div className="appbar-end">
          {scenario && !readOnly && (
            <span
              className={`save-pill ${saveError ? "bad" : saving ? "busy" : ""}`}
              role="status"
            >
              <span className="dot" />
              {saveLabel}
            </span>
          )}
          <span className="env-chip">
            <span className="dot" />
            {readOnly ? "Fictional demo" : status?.hosted ? "Reviewer preview" : "Local preview"}
            <small>Synthetic patients</small>
          </span>
        </div>
      </header>

      <main className="page">
        {readOnly && (
          <div className="banner quiet" role="note">
            <Info size={16} />
            <span>
              <strong>Fictional patients, read-only.</strong> Uploads and pasted
              text are off and the local reader runs, so no model is called and
              nothing is saved.
            </span>
            <div className="banner-actions">
              <a className="button" href="/">Context home</a>
              <a className="button primary" href="/request-access">Request access</a>
            </div>
          </div>
        )}
        {(saveError || recoveryNeeded) && page === "workspace" && (
          <div className="banner warn" role={saveError ? "alert" : "status"}>
            <WarningCircle size={20} />
            <div>
              <strong>
                {saveError
                  ? storageConflict
                    ? "This tab is out of date"
                    : "Autosave needs attention"
                  : "Restore your review session"}
              </strong>
              <p>
                {saveError ||
                  "Your notes and decisions are safe in this browser. Restore the session to continue."}
              </p>
            </div>
            <div className="banner-actions">
              {storageConflict && (
                <button
                  className="button"
                  disabled={!!busy || saving}
                  onClick={loadLatest}
                >
                  Load latest saved work
                </button>
              )}
              {storageBlocked && !storageConflict && (
                <button
                  className="button"
                  disabled={!!busy}
                  onClick={() => {
                    savePaused.current = false;
                    setStorageBlocked(false);
                    setSaveAttempt((n) => n + 1);
                  }}
                >
                  Retry saving
                </button>
              )}
              {recoveryNeeded && draft && (
                <button
                  className="button primary"
                  disabled={!!busy || editing}
                  onClick={() => extract(true)}
                >
                  Restore review session
                </button>
              )}
              {import.meta.env.DEV && recoveryNeeded && !status?.modelConnectionManaged && (
                <button
                  className="button"
                  disabled={!!busy}
                  onClick={() => connectionDialog.current?.showModal()}
                >
                  Manage Azure connection
                </button>
              )}
            </div>
          </div>
        )}
        {restored && !recoveryNeeded && !saveError && page === "workspace" && (
          <div className="banner quiet" role="status">
            <CheckCircle size={18} />
            <p>
              Picked up where you left off. Your records and decisions were
              saved in this browser.
            </p>
            {draft && (
              <button
                className="link"
                disabled={!!busy || editing}
                onClick={() => extract(true)}
              >
                Restore review session
              </button>
            )}
            <button
              className="icon-button"
              aria-label="Dismiss"
              onClick={() => setRestored(false)}
            >
              <X size={15} />
            </button>
          </div>
        )}
        {error && (
          <div className="banner error" role="alert">
            <WarningCircle size={20} />
            <div>
              <strong>Something needs your attention</strong>
              <p>{error}</p>
            </div>
            <button
              aria-label="Dismiss error"
              className="icon-button"
              onClick={() => setError("")}
            >
              <X size={16} />
            </button>
          </div>
        )}

        {page === "workspace" && scenario && (
          <>
            <section className="patient">
              <div className="patient-meta">
                <h1>
                  {scenario.name}
                  <span className="badge">Synthetic</span>
                </h1>
                <p>{scenario.subtitle}</p>
              </div>
              <div className="patient-actions">
                <label className="sr-only" htmlFor="case-select">
                  Switch patient
                </label>
                <select
                  id="case-select"
                  disabled={!!busy}
                  value={scenario.id}
                  onChange={(e) =>
                    load(cases.find((c) => c.id === e.target.value)!)
                  }
                >
                  {cases.map((c) => (
                    <option key={c.id} value={c.id}>
                      {c.name} · {c.focus}
                    </option>
                  ))}
                </select>
                {!readOnly && <button
                  className="button"
                  disabled={!!busy}
                  onClick={openNewCase}
                >
                  <Plus size={15} weight="bold" />
                  New patient
                </button>}
                <details className="menu">
                  <summary aria-label="More options">
                    <DotsThree size={20} weight="bold" />
                  </summary>
                  <div className="menu-panel">
                    <button
                      disabled={!!busy}
                      onClick={(e) => {
                        e.currentTarget.closest("details")!.open = false;
                        load(scenario);
                      }}
                    >
                      <ArrowsClockwise size={16} />
                      Start this patient again
                    </button>
                    {savedReport && (
                      <button disabled={!!busy} onClick={exportReport}>
                        <DownloadSimple size={16} />
                        Download saved report
                      </button>
                    )}
                    {!readOnly && <><button
                      disabled={!!busy}
                      onClick={() =>
                        downloadJson(snapshot(), "marmar-context-workspace.json")
                      }
                    >
                      <DownloadSimple size={16} />
                      Download workspace backup
                    </button>
                    <label className={busy ? "disabled" : undefined}>
                      <UploadSimple size={16} />
                      Restore workspace backup
                      <input
                        type="file"
                        accept="application/json,.json"
                        aria-label="Restore workspace backup"
                        disabled={!!busy}
                        onChange={async (e) => {
                          const file = e.target.files?.[0];
                          e.target.value = "";
                          if (!file) return;
                          try {
                            if (file.size > 2000000)
                              throw new Error(
                                "Workspace backup must be smaller than 2 MB."
                              );
                            const saved = parseWorkspace(await file.text());
                            setScenario(saved.scenario);
                            setSources(saved.sources);
                            setSourceId(saved.sourceId);
                            setDraft(saved.draft);
                            setReview(saved.review);
    setCoverageReview(saved.coverageReview ?? {});
                            setUseModel(saved.useModel);
                            setEditing(saved.editing);
                            setEditText(saved.editText);
                            setSavedReport(saved.report);
                            setAssessment(null);
                            setAcknowledge(false);
                            setStep(saved.draft ? "review" : "sources");
                            setCases((prev) => [
                              ...prev.filter((c) => c.id !== saved.scenario.id),
                              saved.scenario,
                            ]);
                            setRestored(true);
                            setRecoveryNeeded(false);
                            // Import cannot bypass an unresolved cross-tab or storage failure.
                            setStorageBlocked(savePaused.current);
                            setError("");
                            setToast(
                              "Workspace backup restored. Previous results are saved copies; validate the review before running a new check."
                            );
                          } catch {
                            setError(
                              "This is not a supported workspace backup. Your current work has not changed."
                            );
                          }
                        }}
                      />
                    </label>
                    {import.meta.env.DEV && !status?.modelConnectionManaged && (
                      <button
                        disabled={!!busy}
                        onClick={() => {
                          setConnectionError("");
                          connectionDialog.current?.showModal();
                        }}
                      >
                        <Stack size={16} />
                        {status?.modelConfigured
                          ? "Manage Azure connection"
                          : "Connect Azure model"}
                      </button>
                    )}
                    <p>
                      Work saves automatically in this browser. Backups are
                      optional; credentials are never included.
                    </p></>}
                  </div>
                </details>
              </div>
            </section>

            <ol className="stepper" aria-label="Workflow steps">
              {(
                [
                  {
                    id: "sources",
                    title: "Records",
                    sub: plural(sources.length, "record"),
                    done: !!draft,
                  },
                  {
                    id: "review",
                    title: "Review",
                    sub: draft
                      ? `${reviewed} of ${draft.facts.length} decided`
                      : "Confirm each fact",
                    done: !!assessment,
                  },
                  {
                    id: "insights",
                    title: "Findings",
                    sub: assessment
                      ? plural(assessment.contextual.findings.length, "finding")
                      : "With and without context",
                    done: false,
                  },
                ] as const
              ).map((s, i) => (
                <li key={s.id}>
                  <button
                    className={`${step === s.id ? "current" : ""} ${
                      s.done ? "done" : ""
                    }`}
                    aria-current={step === s.id ? "step" : undefined}
                    disabled={
                      !!busy ||
                      editing ||
                      (s.id === "review" && !draft) ||
                      (s.id === "insights" && !assessment)
                    }
                    onClick={() => setStep(s.id)}
                  >
                    <span className="step-num">
                      {s.done && step !== s.id ? (
                        <Check size={13} weight="bold" />
                      ) : (
                        i + 1
                      )}
                    </span>
                    <span className="step-text">
                      <strong>{s.title}</strong>
                      <small>{s.sub}</small>
                    </span>
                  </button>
                </li>
              ))}
            </ol>

            {busy === "Extracting context" && (
              <div className="working" role="status">
                <img src="/assets/yasmar-search.png" alt="" />
                <div>
                  <strong>
                    Reading {plural(sources.length, "record")}
                    {elapsed >= 2 ? ` · ${elapsed}s` : "…"}
                  </strong>
                  <p>
                    {elapsed >= 5
                      ? "The model is still working. No partial result will be used, and the request has a time limit."
                      : "Finding medicines, results, allergies and history, each linked to its line."}
                  </p>
                </div>
                <button
                  className="button"
                  onClick={() => extractionController.current?.abort()}
                >
                  Cancel
                </button>
              </div>
            )}

            {step === "sources" && (
              <>
                <p className="lead">
                  Check these are the right records for {scenario.name}. MARMAR
                  reads them and links every fact to its line; you confirm
                  each one before any check runs.
                </p>
                <div className="records">
                  <aside className="frame" aria-label="Records">
                    <div className="frame-head">
                      <FileText size={16} />
                      <h2>Records</h2>
                      <span className="count">{sources.length}</span>
                    </div>
                    <div className="frame-body record-list">
                    {sources.map((s) => (
                      <button
                        key={s.id}
                        className="record"
                        aria-current={s.id === current?.id ? "true" : undefined}
                        disabled={!!busy || editing}
                        onClick={() => {
                          setSourceId(s.id);
                          setHighlight(null);
                        }}
                      >
                        <FileText size={18} />
                        <span>
                          <strong>{s.title}</strong>
                          <small>
                            {s.kind} · {formatDate(s.date)}
                          </small>
                        </span>
                      </button>
                    ))}
                    {!readOnly && <button
                      className="button dashed"
                      disabled={!!busy || editing || sources.length >= 8}
                      onClick={() => {
                        setRecordText("");
                        setRecordError("");
                        recordDialog.current?.showModal();
                      }}
                    >
                      <Plus size={15} weight="bold" />
                      Add record
                    </button>}
                    <p className="hint">
                      {readOnly ? "Fictional records, read-only." : <>{sources.length} of 8 records · .txt or pasted text. PDF
                      and images are not supported yet.</>}
                    </p>
                    </div>
                  </aside>
                  <section className="frame" aria-label="Original record">
                    <div className="frame-head">
                      <Stack size={16} />
                      <h2>Original record</h2>
                    </div>
                    <div className="frame-body flush">{documentView(false)}</div>
                  </section>
                </div>
              </>
            )}

            {step === "review" && draft && (
              <div className="review">
                <section className="review-main">
                  <div className="section-head">
                    <div>
                      <h2>Confirm the patient picture</h2>
                      <p>
                        {plural(draft.facts.length, "fact")} found in{" "}
                        {plural(sources.length, "record")}. Check each against
                        its line, then include or exclude it.
                      </p>
                    </div>
                    <button
                      className="button"
                      disabled={!!busy || !draft.facts.some((f) => !f.problem)}
                      onClick={includeAll}
                    >
                      <Check size={15} weight="bold" />
                      Include all supported
                    </button>
                  </div>

                  {!!draft.coverage?.some((c) => !c.matchedFactIds.length && !c.contextExclusion) && (
                    <section className="attention" aria-label="Medication source coverage">
                      <h3>Medication mentions needing a decision</h3>
                      <p>These source mentions have no extracted medication fact. Add a confirmed clarification, or document why this mention should be excluded. A general acknowledgement is not enough. Detection uses limited English patterns and can miss other medicines.</p>
                      {draft.coverage.filter((c) => !c.matchedFactIds.length && !c.contextExclusion).map((candidate) => {
                        const decision = coverageReview[candidate.id];
                        const update = (field: "reason" | "confirmedBy", value: string) => {
                          setCoverageReview((current) => ({...current, [candidate.id]: {
                            fingerprint: draft.fingerprint, reason: current[candidate.id]?.reason ?? "",
                            confirmedBy: current[candidate.id]?.confirmedBy ?? "", [field]: value,
                          }}));
                          setAssessment(null); setSavedReport(null); setAcknowledge(false);
                        };
                        return <div className="attention-item" key={candidate.id}>
                          <strong>{candidate.text} · {sourceTitle(candidate.sourceId)} · line {candidate.line}</strong>
                          <blockquote>{candidate.quote}</blockquote>
                          <button className="link" disabled={!!busy || sources.length >= 8} onClick={() => clarify(candidate.sourceId, candidate.line, "Medication mention was not extracted: " + candidate.text)}>Add confirmed clarification</button>
                          <label>Reason for excluding this mention (at least 10 characters)
                            <textarea maxLength={500} disabled={!!busy} value={decision?.reason ?? ""} onChange={(e) => update("reason", e.target.value)} />
                          </label>
                          <label>Reviewed by (self-reported)
                            <input maxLength={120} disabled={!!busy} value={decision?.confirmedBy ?? ""} onChange={(e) => update("confirmedBy", e.target.value)} />
                          </label>
                          <p>{validCoverageResolution(decision, draft.fingerprint) ? "Specific exclusion recorded in this source version." : "Unresolved: medication check remains blocked."}</p>
                        </div>;
                      })}
                    </section>
                  )}

                  {conflictKeys.length > 0 && (
                    <div className="callout warn" role="alert">
                      <WarningCircle size={18} />
                      <div>
                        <strong>
                          Records disagree: {conflictLabels.join(", ")}
                        </strong>
                        <p>
                          Compare the entries and exclude the one you are not
                          using. MARMAR will not pick one for you.
                        </p>
                      </div>
                    </div>
                  )}

                  {!!unresolvedWarnings.length && (
                    <section className="attention" aria-label="Needs clarification">
                      <h3>
                        <WarningCircle size={16} />
                        Held back for clarification ({unresolvedWarnings.length})
                      </h3>
                      <p className="muted">
                        These lines were not used in the check. Add a
                        clarification only for details you have confirmed.
                      </p>
                      {unresolvedWarnings.map((warning, i) => (
                        <div key={i} className="attention-item">
                          <blockquote>
                            {sourceLine(warning.sourceId, warning.line)}
                          </blockquote>
                          <p>{warning.reason}</p>
                          <div className="fact-links">
                            <button
                              className="link quiet"
                              onClick={() => {
                                setSourceId(warning.sourceId);
                                setHighlight(warning.line);
                              }}
                            >
                              <FileText size={12} />
                              {sourceTitle(warning.sourceId)} · line{" "}
                              {warning.line}
                            </button>
                            {clarificationLink(warning.sourceId, warning.line)}
                            {!warning.blocked &&
                              canClarify(warning.sourceId, warning.line) && (
                                <button
                                  className="link"
                                  disabled={!!busy || sources.length >= 8}
                                  onClick={() =>
                                    clarify(
                                      warning.sourceId,
                                      warning.line,
                                      warning.reason
                                    )
                                  }
                                >
                                  Add clarification
                                </button>
                              )}
                          </div>
                        </div>
                      ))}
                    </section>
                  )}

                  <div className="filters" aria-label="Filter context facts">
                    {(
                      [
                        ["all", "All", draft.facts.length],
                        ["pending", "Needs a decision", pending],
                        ["excluded", "Excluded", excludedCount],
                      ] as const
                    ).map(([id, label, count]) => (
                      <button
                        key={id}
                        aria-pressed={factFilter === id}
                        onClick={() => setFactFilter(id)}
                      >
                        {label}
                        <span>{count}</span>
                      </button>
                    ))}
                  </div>

                  {[
                    { title: "Medications", kinds: ["medicine"] },
                    { title: "Lab results", kinds: ["lab"] },
                    {
                      title: "Allergies and conditions",
                      kinds: ["allergy", "condition"],
                    },
                    {
                      title: "Patient details",
                      kinds: ["patient", "age", "sex", "weight", "pregnancy"],
                    },
                  ].map((group) => {
                    const facts = draft.facts.filter(
                      (f) =>
                        group.kinds.includes(f.kind) &&
                        (factFilter === "all" ||
                          (factFilter === "pending"
                            ? !review[f.id]
                            : review[f.id] === "exclude"))
                    );
                    return facts.length ? (
                      <section
                        className="frame fact-group"
                        key={group.title}
                        aria-label={group.title}
                      >
                        <div className="frame-head">
                          <FactIcon kind={group.kinds[0]} size={16} />
                          <h3>{group.title}</h3>
                          <span className="count">{facts.length}</span>
                        </div>
                        <div className="frame-body flush">
                          {facts.map(factRow)}
                        </div>
                      </section>
                    ) : null;
                  })}
                  {((factFilter === "pending" && pending === 0) ||
                    (factFilter === "excluded" && excludedCount === 0)) && (
                    <div className="empty">
                      <CheckCircle size={22} />
                      <strong>
                        {factFilter === "pending"
                          ? "Every fact has a decision"
                          : "Nothing excluded"}
                      </strong>
                      <button
                        className="link"
                        onClick={() => setFactFilter("all")}
                      >
                        Show all facts
                      </button>
                    </div>
                  )}

                  {!!reviewedWarnings.length && (
                    <details className="drawer">
                      <summary>
                        Reviewed clarifications
                        <span className="count">{reviewedWarnings.length}</span>
                      </summary>
                      <p className="muted">
                        The original text stays unchanged and outside the check;
                        only included facts are assessed.
                      </p>
                      {reviewedWarnings.map((w) => (
                        <div
                          key={`${w.sourceId}:${w.line}`}
                          className="drawer-row"
                        >
                          <span>
                            {sourceTitle(w.sourceId)}, line {w.line}
                          </span>
                          {clarificationLink(w.sourceId, w.line)}
                        </div>
                      ))}
                    </details>
                  )}

                  {notesToCheck && (
                    <section className="frame notes-check" aria-label="Original notes">
                      <div className="frame-head">
                        <FileText size={16} />
                        <h3>Lines not turned into facts</h3>
                        <span className="count">{remainingLines.length}</span>
                      </div>
                      <div className="frame-body">
                      <p className="muted">
                        Read these for anything important. They are outside
                        this check.
                      </p>
                      {remainingLines.length > 0 && (
                        <ul>
                          {remainingLines.map((line, i) => (
                            <li key={i}>
                              <button
                                className="link quiet"
                                onClick={() => {
                                  setSourceId(line.sourceId);
                                  setHighlight(line.line);
                                }}
                              >
                                {sourceTitle(line.sourceId)} · line {line.line}
                              </button>
                              <span>{line.text}</span>
                              {!draft.warnings?.some(
                                (w) =>
                                  w.sourceId === line.sourceId &&
                                  w.line === line.line
                              ) &&
                                canClarify(line.sourceId, line.line) && (
                                  <button
                                    className="link"
                                    disabled={!!busy || sources.length >= 8}
                                    onClick={() =>
                                      clarify(
                                        line.sourceId,
                                        line.line,
                                        "This source line was not mapped. Confirm the specific patient fact before adding it."
                                      )
                                    }
                                  >
                                    Add clarification
                                  </button>
                                )}
                            </li>
                          ))}
                        </ul>
                      )}
                      <label className="ack">
                        <input
                          type="checkbox"
                          disabled={!!busy}
                          checked={acknowledge}
                          onChange={(e) => {
                            setAcknowledge(e.target.checked);
                            setAssessment(null);
                          }}
                        />
                        I checked the original notes for anything missed, and
                        understand that lines not turned into facts are outside
                        this check.
                      </label>
                      </div>
                    </section>
                  )}

                  <details className="drawer">
                    <summary>How these facts were read</summary>
                    <p className="muted">
                      {draft.strategy === "cached"
                        ? "Reused a validated reading of these unchanged records; no new model call."
                        : draft.mode === "model"
                        ? `${draft.model} read the narrative text; labelled fields were parsed locally.`
                        : "Labelled fields were parsed locally; no model call was needed."}{" "}
                      {draft.durationMs} ms ·{" "}
                      {plural(draft.metrics?.providerCalls ?? 0, "provider call")}.
                      Conflicting records must be resolved before the check;
                      excluded facts stay in the evidence report.
                    </p>
                  </details>
                </section>
                <aside
                  className="review-source frame"
                  ref={sourcePanel}
                  aria-label="Original record"
                >
                  <div className="frame-head">
                    <Stack size={16} />
                    <h2>Original record</h2>
                  </div>
                  <div className="frame-body flush">{documentView(true)}</div>
                </aside>
              </div>
            )}

            {step === "insights" && assessment && summary && (
              <div className="results">
                <section className="frame" aria-label="Result">
                  <div className="frame-head">
                    <ShieldCheck size={16} />
                    <h2>Result</h2>
                  </div>
                  <div className="frame-body answer">
                  <img
                    src={
                      summary.after
                        ? "/assets/yasmar-present.png"
                        : "/assets/yasmar-doctor.png"
                    }
                    alt=""
                  />
                  <div className="answer-body">
                    <h2>{summary.headline}</h2>
                    <p>{summary.subline}</p>
                    <div className="compare" aria-label="Comparison">
                      <div>
                        <small>Generic check · medication list only</small>
                        <strong>{summary.before}</strong>
                      </div>
                      <ArrowRight size={18} />
                      <div className="with-context">
                        <small>
                          Personalized to {patientName(assessment)} ·{" "}
                          {plural(assessment.includedFacts.length, "reviewed fact")}
                        </small>
                        <strong>{summary.after}</strong>
                      </div>
                    </div>
                  </div>
                  </div>
                </section>

                <section className="frame" aria-label="Patient profile">
                  <div className="frame-head">
                    <User size={16} />
                    <h2>What we know about {patientName(assessment)}</h2>
                    <span className="frame-note">
                      {
                        patientProfile(assessment).filter((p) => p.status === "used")
                          .length
                      }{" "}
                      personal factors used
                    </span>
                  </div>
                  <div className="frame-body profile">
                    {patientProfile(assessment).map((item) => (
                      <button
                        key={item.id}
                        className={`profile-item ${item.status}`}
                        disabled={!item.fact}
                        onClick={() => item.fact && showFact(item.fact)}
                        title={item.fact ? "Show the source line" : undefined}
                      >
                        <span className="profile-label">{item.label}</span>
                        <strong>{item.value ?? "Not on file"}</strong>
                        <small>
                          <span className="dot" aria-hidden="true" />
                          {item.note}
                        </small>
                      </button>
                    ))}
                  </div>
                </section>

                {/* Clinicians see Urgent review and a call to the patient (YCI-1a), never the patient's emergency card. */}
                {assessment.contextual.findings.some((f) => f.tier === "get_help_now") && (
                  <div className="banner error">
                    <WarningCircle size={20} />
                    <div>
                      <strong>Urgent review</strong>
                      <p>Call the patient before their next dose. A clinician decides what happens next.</p>
                    </div>
                  </div>
                )}

                {assessment.contextual.findings.length > 0 && (
                  <section className="frame" aria-label="Findings">
                    <div className="frame-head">
                      <WarningCircle size={16} />
                      <h2>Findings for clinician review</h2>
                      <span className="count">
                        {assessment.contextual.findings.length}
                      </span>
                    </div>
                    <div className="findings">
                    {assessment.contextual.findings.map((f) => (
                      <article className={`finding ${f.tier}`} key={f.id}>
                        <div className="finding-tags">
                          <span className={`tier ${f.tier}`}>
                            {tierNames[f.tier]}
                          </span>
                          {assessment.addedRuleIds.includes(f.ruleId) && (
                            <span className="tag new">New with patient context</span>
                          )}
                        </div>
                        <h3>{f.title}</h3>
                        <p className="finding-why">{f.why}</p>
                        <div className="finding-todo">
                          <strong>For clinician review</strong>
                          {f.what_to_do.map((line, i) => (
                            <p key={i}>{line}</p>
                          ))}
                        </div>
                        {findingEvidence(assessment, f).length > 0 && (
                          <div className="finding-evidence">
                            <small>Based on</small>
                            {findingEvidence(assessment, f).map((fact) => (
                              <button
                                className="evidence-chip"
                                key={fact.id}
                                onClick={() => showFact(fact)}
                              >
                                {fact.label}
                                <span>
                                  {sourceTitle(fact.sourceId)} · line {fact.line}
                                </span>
                              </button>
                            ))}
                          </div>
                        )}
                        <details className="finding-more">
                          <summary>
                            Rule and references ·{" "}
                            {plural(f.source_ids.length, "reference")}
                          </summary>
                          <div>
                            <p>
                              <strong>Rule</strong> {f.ruleId}
                            </p>
                            <p>{f.clinical}</p>
                            {f.source_ids.map((id) => {
                              const ref =
                                assessment.contextual.what_we_checked.references.find(
                                  (r) => r.id === id
                                );
                              return (
                                <p key={id}>
                                  {ref?.url ? (
                                    <a href={ref.url} target="_blank" rel="noreferrer">
                                      {ref.name}
                                      <ArrowSquareOut size={12} />
                                    </a>
                                  ) : (
                                    <span>
                                      {ref?.name ?? id} · no direct link recorded
                                    </span>
                                  )}
                                </p>
                              );
                            })}
                            {f.questions_for_doctor.map((question, i) => (
                              <p key={i}>
                                <strong>Ask</strong> {question}
                              </p>
                            ))}
                            {f.watch_for.length > 0 && (
                              <p>
                                <strong>Watch for</strong> {f.watch_for.join("; ")}.
                              </p>
                            )}
                          </div>
                        </details>
                      </article>
                    ))}
                    </div>
                  </section>
                )}

                <section className="frame" aria-label="Medicine by medicine">
                  <div className="frame-head">
                    <Pill size={16} />
                    <h2>Medicine by medicine</h2>
                    <span className="count">{medicinePlan(assessment).length}</span>
                  </div>
                  <div className="frame-body flush">
                    {medicinePlan(assessment).map((m) => (
                      <div className="plan-row" key={m.fact.id}>
                        <span className="fact-icon medicine" aria-hidden="true">
                          <Pill size={17} />
                        </span>
                        <div className="plan-body">
                          <div className="fact-title">
                            <strong>{m.fact.label}</strong>
                            <span>{factDetail(m.fact.detail)}</span>
                          </div>
                          <p>{m.plain}</p>
                          {m.findings.length > 0 && (
                            <p className="plan-findings">
                              {m.findings.map((f) => f.title).join(" · ")}
                            </p>
                          )}
                        </div>
                        <div className="plan-status">
                          <span className={`status ${doseTone[m.status]}`}>
                            {doseLabels[m.status]}
                          </span>
                          <span className={`status ${m.findings.length ? "amber" : "green"}`}>
                            {m.findings.length
                              ? plural(m.findings.length, "finding")
                              : "No findings"}
                          </span>
                        </div>
                      </div>
                    ))}
                  </div>
                </section>

                {assessment.contextual.tests_to_ask_about.length > 0 && (
                  <section className="frame" aria-label="Monitoring">
                    <div className="frame-head">
                      <TestTube size={16} />
                      <h2>Monitoring for {patientName(assessment)}</h2>
                    </div>
                    <div className="frame-body flush">
                      {assessment.contextual.tests_to_ask_about.map((t) => (
                        <div className="plan-row" key={t.test}>
                          <div className="plan-body">
                            <div className="fact-title">
                              <strong>{t.test}</strong>
                            </div>
                            <p>
                              {t.why} {t.when}.
                            </p>
                          </div>
                          <div className="plan-status">
                            <span
                              className={`status ${
                                t.status === "up_to_date"
                                  ? "green"
                                  : t.status === "due"
                                  ? "amber"
                                  : ""
                              }`}
                            >
                              {t.status === "up_to_date"
                                ? `Up to date · ${formatDate(
                                    t.last_result_at?.slice(0, 10) ?? ""
                                  )}`
                                : t.status === "due"
                                ? "Due"
                                : "No result on file"}
                            </span>
                          </div>
                        </div>
                      ))}
                    </div>
                  </section>
                )}

                <details
                  className="drawer"
                  open={assessment.contextual.findings.length === 0}
                >
                  <summary>
                    What this check could not assess
                    <span className="count">
                      {assessment.contextual.what_we_couldnt_check.length + 1}
                    </span>
                  </summary>
                  {assessment.contextual.what_we_couldnt_check.map((item, i) => (
                    <div className="gap" key={i}>
                      <strong>{item.item}</strong>
                      <p>{item.plain}</p>
                    </div>
                  ))}
                  <div className="gap">
                    <strong>Clinical review pending</strong>
                    <p>
                      Later changes to the medication engine await clinical
                      sign-off. This is an information tool; it does not
                      decide treatment.
                    </p>
                  </div>
                </details>
                <details className="drawer">
                  <summary>How this result was produced</summary>
                  <dl className="trace">
                    <div>
                      <dt>Records</dt>
                      <dd>{plural(assessment.sourceCount, "synthetic record")}</dd>
                    </div>
                    <div>
                      <dt>Facts included / excluded</dt>
                      <dd>
                        {assessment.includedFacts.length} /{" "}
                        {assessment.excludedFacts.length}
                      </dd>
                    </div>
                    <div>
                      <dt>Read by</dt>
                      <dd>
                        {draft?.mode === "model"
                          ? draft.model
                          : "Local structured reader"}
                      </dd>
                    </div>
                    <div>
                      <dt>Provider calls</dt>
                      <dd>
                        {draft?.metrics?.providerCalls ?? draft?.attempts ?? 0}
                        {draft?.strategy === "cached" ? " (cached reading)" : ""}
                      </dd>
                    </div>
                    <div>
                      <dt>Engine / rules</dt>
                      <dd>
                        {assessment.contextual.engineVersion} /{" "}
                        {assessment.contextual.rulesVersion}
                      </dd>
                    </div>
                    <div>
                      <dt>Check runtime</dt>
                      <dd>{assessment.durationMs} ms</dd>
                    </div>
                    <div>
                      <dt>Scenario date</dt>
                      <dd>23 Oct 2026</dd>
                    </div>
                    <div>
                      <dt>Source fingerprint</dt>
                      <dd>
                        <code>{assessment.fingerprint.slice(0, 18)}…</code>
                      </dd>
                    </div>
                  </dl>
                </details>
              </div>
            )}

            <div className="actionbar">
              <div className="actionbar-inner">
                {step === "sources" ? (
                  <div className="actionbar-info">
                    {readOnly ? <span className="hint">Reader: local structured reader. No model is called.</span> : <><label htmlFor="reader-select">Reader</label>
                    <select
                      id="reader-select"
                      aria-label="Extraction mode"
                      disabled={!!busy}
                      value={useModel ? "model" : "local"}
                      onChange={(e) => setUseModel(e.target.value === "model")}
                    >
                      <option value="local">Local structured reader</option>
                      <option value="model" disabled={!status?.modelConfigured}>
                        {status?.modelConfigured
                          ? status.model
                          : "Model (connect first)"}
                      </option>
                    </select>
                    {useModel && (
                      <label className="check">
                        <input
                          type="checkbox"
                          checked={freshModel}
                          disabled={!!busy}
                          onChange={(e) => setFreshModel(e.target.checked)}
                        />
                        Fresh model call
                      </label>
                    )}</>}
                    {editing && (
                      <span className="hint blocked">
                        Save or cancel your record edit first.
                      </span>
                    )}
                  </div>
                ) : step === "review" ? (
                  <div className="actionbar-info">
                    <progress
                      aria-label="Facts decided"
                      value={reviewed}
                      max={Math.max(1, draft?.facts.length ?? 1)}
                    />
                    <span className={blocker ? "hint blocked" : "hint ready"}>
                      {blocker ?? "Ready. The check uses only the facts you included."}
                    </span>
                  </div>
                ) : (
                  <div className="actionbar-info">
                    <button
                      className="button"
                      disabled={!!busy}
                      onClick={() => setStep("review")}
                    >
                      <ArrowLeft size={15} />
                      Back to review
                    </button>
                    {savedReport && (
                      <span className="hint">Report saved in this browser.</span>
                    )}
                  </div>
                )}
                {step === "insights" && assessment ? (
                  <button className="button primary large" onClick={exportReport}>
                    <DownloadSimple size={17} />
                    Export evidence report
                  </button>
                ) : step === "review" ? (
                  <button
                    className="button primary large"
                    disabled={!!busy || !!blocker}
                    onClick={check}
                  >
                    {busy ? (
                      <CircleNotch className="spin" size={17} />
                    ) : (
                      <ShieldCheck size={17} />
                    )}
                    {busy || "Run medication check"}
                  </button>
                ) : (
                  <button
                    className="button primary large"
                    disabled={!!busy || !scenario || editing}
                    onClick={() => extract()}
                  >
                    {busy ? (
                      <CircleNotch className="spin" size={17} />
                    ) : (
                      <Stack size={17} />
                    )}
                    {busy || "Read records"}
                    {!busy && <ArrowRight size={16} />}
                  </button>
                )}
              </div>
            </div>
          </>
        )}

        {page === "cases" && (
          <>
            <header className="page-head">
              <h1>Patients</h1>
              <p>
                Fictional examples that each show one part of the workflow. Open
                one{readOnly ? "." : ", or add your own synthetic patient."}
              </p>
              {!readOnly && <button className="button primary" disabled={!!busy} onClick={openNewCase}>
                <Plus size={15} weight="bold" />
                New patient
              </button>}
            </header>
            <section className="frame" aria-label="Synthetic patients">
            <div className="frame-head">
              <UsersThree size={16} />
              <h2>Synthetic patients</h2>
              <span className="count">{cases.length}</span>
            </div>
            <div className="frame-body flush case-list">
              {cases.map((c) => (
                <article className="case" key={c.id}>
                  <div className="avatar">{c.initials}</div>
                  <div className="case-body">
                    <h2>
                      {c.name}
                      <span className="badge">{c.focus}</span>
                    </h2>
                    <p>{c.description}</p>
                    <small>
                      {c.subtitle} · {plural(c.sources.length, "record")}
                    </small>
                  </div>
                  <button
                    className="button"
                    disabled={!!busy}
                    onClick={() => load(c)}
                  >
                    Open <ArrowRight size={15} />
                  </button>
                </article>
              ))}
            </div>
            </section>
            <p className="footnote">
              <Info size={15} />
              Illustrative test cases, not patient outcomes or a clinical
              validation cohort.
            </p>
          </>
        )}

        {page === "evidence" && (
          <>
            <header className="page-head">
              <h1>A medication check personalized to the patient</h1>
              <p>
                Context reads a patient’s records for the factors that change
                how medicines act in that person: age, weight, kidney function,
                potassium, INR, allergies and pregnancy status. A person confirms
                every fact, then MARMAR’s existing rules run with them. It
                informs a clinician; it does not decide, diagnose or prescribe.
              </p>
            </header>
            <ol className="how">
              {[
                [
                  "Read",
                  "The live model or the local reader turns record text into medicines and personal factors, each tied to its exact line. Uncertain lines, and facts about relatives, are held back, not guessed.",
                ],
                [
                  "Review",
                  "A person includes or excludes every fact. Conflicting records block the check until resolved. Editing a record resets the review.",
                ],
                [
                  "Check",
                  "The existing medication engine runs twice: a generic check on the medication list alone, and one personalized with the reviewed factors, so the difference is visible.",
                ],
                [
                  "Trace",
                  "Each medicine gets its own dose status, findings and monitoring. Every finding keeps its source lines, decisions, rule IDs and references for the evidence report.",
                ],
              ].map(([title, text], i) => (
                <li className="frame" key={title}>
                  <div className="frame-head">
                    <h2>Step {i + 1}</h2>
                  </div>
                  <div className="frame-body">
                    <h3>{title}</h3>
                    <p>{text}</p>
                  </div>
                </li>
              ))}
            </ol>
            <section className="frame scope" aria-label="What is running today">
              <div className="frame-head">
                <Stack size={16} />
                <h2>What is running today</h2>
              </div>
              <div className="frame-body">
              {[
                [
                  "Medication rules",
                  "green",
                  "Running",
                  `Engine ${status?.engineVersion ?? "…"}, rules ${
                    status?.rulesVersion ?? "…"
                  }. No duplicate clinical rules.`,
                ],
                [
                  status?.model ?? "Meta Muse Spark",
                  status?.modelConfigured ? "green" : "grey",
                  status?.modelVerifiedAt
                    ? "Live call verified"
                    : status?.modelConfigured
                    ? "Connected"
                    : "Not connected",
                  "Server-side only. Facts must cite exact lines and pass grounding checks; a person still reviews meaning and omissions. Not a clinical accuracy measure.",
                ],
                [
                  "Personal factors",
                  "green",
                  "Running",
                  "Age, weight, kidney function, potassium, INR and allergies change findings and dose checks. Sex, pregnancy status and conditions are recorded but no current rule reads them.",
                ],
                [
                  "Genetic results",
                  "grey",
                  "Not yet",
                  "Pharmacogenomic results (for example CYP2C19 or HLA-B*58:01) are not read or checked. Adding them needs new engine rules and clinical review.",
                ],
                [
                  "EHR connection",
                  "grey",
                  "Simulated",
                  "Synthetic text records only. No hospital, EHR or patient database is connected.",
                ],
                [
                  "Clinical scope",
                  "amber",
                  "Review pending",
                  "Condition-specific checks and broad coverage are not established. A result never certifies a medicine as safe.",
                ],
                [
                  "Data handling",
                  "grey",
                  "Synthetic only",
                  "Work saves in this browser. Review sessions on the server expire after two hours. Choosing the model sends synthetic notes to it.",
                ],
              ].map(([name, tone, label, text]) => (
                <div className="scope-row" key={name}>
                  <strong>{name}</strong>
                  <span className={`status ${tone}`}>{label}</span>
                  <p>{text}</p>
                </div>
              ))}
              </div>
            </section>
          </>
        )}

        {page === "evaluation" && (
          <>
            <header className="page-head">
              <h1>Software tests</h1>
              <p>
                Run synthetic fixtures through the reader, the review gate and
                the real medication engine. These check software behaviour, not
                clinical accuracy.
              </p>
              <button
                className="button primary"
                disabled={!!busy}
                onClick={() =>
                  run("Running evaluation", async () => {
                    setEvaluation(await api<Evaluation>("evaluate", {}));
                  })
                }
              >
                {busy ? <CircleNotch className="spin" size={17} /> : <Flask size={17} />}
                {busy || (evaluation ? "Run again" : "Run tests")}
              </button>
            </header>
            {evaluation ? (
              <section className="frame tests" aria-label="Test results">
                <div className="frame-head">
                  <Flask size={16} />
                  <h2>
                    {evaluation.passed} of {evaluation.total} checks passed
                  </h2>
                  <span className="frame-note">
                    {evaluation.durationMs} ms ·{" "}
                    {new Date(evaluation.runAt).toLocaleTimeString()}
                  </span>
                </div>
                <div className="frame-body flush">
                {evaluation.tests.map((t, i) => (
                  <div className="test-row" key={i}>
                    {t.passed ? (
                      <CheckCircle className="pass" size={19} weight="fill" />
                    ) : (
                      <WarningCircle className="fail" size={19} weight="fill" />
                    )}
                    <span>{t.name}</span>
                    <small>{t.group}</small>
                    <span className={`status ${t.passed ? "green" : "red"}`}>
                      {t.passed ? "Passed" : "Failed"}
                    </span>
                  </div>
                ))}
                </div>
              </section>
            ) : (
              <div className="empty card">
                <Flask size={30} />
                <strong>No test run yet</strong>
                <p className="muted">
                  Covers provenance, context-sensitive findings, uncertainty and
                  reconciliation.
                </p>
              </div>
            )}
          </>
        )}
      </main>
      </div>
      </div>
      <dialog
        ref={clarificationDialog}
        className="dialog"
        onClose={() => {
          setClarificationTarget(null);
          setCorrecting(null);
          setClarificationFormError("");
        }}
      >
        <form
          onSubmit={(e) => {
            e.preventDefault();
            if (
              !clarificationTarget ||
              !clarificationOriginal ||
              (!correcting && sources.length >= 8)
            )
              return;
            const form = e.currentTarget;
            const data = new FormData(form);
            const text = String(data.get("statement") || "").trim();
            const confirmedBy = String(data.get("confirmedBy") || "").trim();
            const validation = clarificationError(text, confirmedBy);
            if (validation) {
              setClarificationFormError(validation);
              return;
            }
            const record: Source = {
              id: correcting?.id ?? `source-${crypto.randomUUID()}`,
              title: `Clarification: ${clarificationOriginal.title}`.slice(
                0,
                120
              ),
              kind: "Reviewer clarification",
              date: "2026-10-23",
              text,
              clarification: {
                ...clarificationTarget,
                quote: clarificationOriginal.text
                  .split("\n")
                  [clarificationTarget.line - 1].trim(),
                confirmedBy,
              },
            };
            setSources((previous) =>
              correcting
                ? previous.map((s) => (s.id === correcting.id ? record : s))
                : [...previous, record]
            );
            setSourceId(record.id);
            setDraft(null);
            setRecoveryNeeded(false);
            setReview({});
            setAssessment(null);
            setSavedReport(null);
            setAcknowledge(false);
            setStep("sources");
            setHighlight(null);
            setError("");
            clarificationDialog.current?.close();
            form.reset();
            setToast(
              correcting
                ? "Clarification corrected. Original evidence retained. Read the records again to review it."
                : "Clarification added. Original evidence retained. Read the records again to review it."
            );
          }}
        >
          <div className="dialog-head">
            <h2>
              {correcting ? "Correct a clarification" : "Add a clarification"}
            </h2>
            <button
              type="button"
              className="icon-button"
              aria-label="Close"
              onClick={() => clarificationDialog.current?.close()}
            >
              <X size={18} />
            </button>
          </div>
          <p className="muted">
            {correcting
              ? "Correcting this record clears its review and result. The original note stays unchanged."
              : "Record only what you have confirmed. The original note stays unchanged and both appear in the evidence report."}
          </p>
          <div className="clarification-box">
            <strong>
              {clarificationOriginal?.title} · line {clarificationTarget?.line}
            </strong>
            <blockquote>
              {
                clarificationOriginal?.text.split("\n")[
                  (clarificationTarget?.line ?? 1) - 1
                ]
              }
            </blockquote>
            <p>{clarificationTarget?.reason}</p>
          </div>
          <label>
            Confirmed statement
            <textarea
              key={
                (correcting?.id ?? "new") +
                ":" +
                clarificationTarget?.sourceId +
                ":" +
                clarificationTarget?.line
              }
              name="statement"
              defaultValue={correcting?.text ?? ""}
              rows={4}
              maxLength={12000}
              required
              placeholder="Identify the patient and state the confirmed detail. For labs, include the date and units. Leave unconfirmed details unknown."
            />
          </label>
          <label>
            Confirmed by
            <input
              key={correcting?.id ?? "new"}
              defaultValue={correcting?.clarification?.confirmedBy ?? ""}
              name="confirmedBy"
              required
              maxLength={120}
              placeholder="For example: synthetic pharmacist, checked the original label"
            />
          </label>
          <label className="check">
            <input type="checkbox" required />I verified this synthetic
            clarification; I am not guessing a missing value.
          </label>
          {clarificationFormError && (
            <p className="form-error" role="alert">
              {clarificationFormError}
            </p>
          )}
          {!correcting && sources.length >= 8 && (
            <p className="form-error" role="alert">
              This patient has reached the eight-record limit. A clarification
              cannot be added.
            </p>
          )}
          <div className="row-end">
            <button
              type="button"
              className="button"
              onClick={() => clarificationDialog.current?.close()}
            >
              Cancel
            </button>
            <button
              type="submit"
              className="button primary"
              disabled={!correcting && sources.length >= 8}
            >
              {correcting ? "Save correction" : "Add clarification record"}
            </button>
          </div>
        </form>
      </dialog>
      <dialog ref={caseDialog} className="dialog">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            const form = e.currentTarget;
            const data = new FormData(form);
            const name = String(data.get("name")).trim();
            const pasted = String(data.get("text") ?? "");
            if (!name) return;
            const { records, error } = recordsFromText(
              [...newCaseFiles, { name: "Pasted note", text: pasted }],
              0,
              () => crypto.randomUUID()
            );
            if (error) {
              setNewCaseError(error);
              return;
            }
            const item: Scenario = {
              id: `custom-${crypto.randomUUID()}`,
              name,
              initials: initials(name),
              subtitle: "Your synthetic patient",
              description:
                "Review the original notes, reconcile context and inspect the evidence behind every finding.",
              focus: "Custom",
              sources: records,
            };
            setCases((previous) => [...previous, item]);
            load(item);
            caseDialog.current?.close();
            form.reset();
            setNewCaseFiles([]);
          }}
        >
          <div className="dialog-head">
            <h2>New synthetic patient</h2>
            <button
              type="button"
              className="icon-button"
              aria-label="Close"
              onClick={() => caseDialog.current?.close()}
            >
              <X size={18} />
            </button>
          </div>
          <p className="muted">
            Upload fictional records as .txt files. Each file stays a separate
            record so every fact points back to where it came from.
          </p>
          <label>
            Patient name
            <input name="name" required maxLength={80} autoComplete="off" />
          </label>
          <label className="dropzone">
            <UploadSimple size={22} />
            <strong>Choose .txt files</strong>
            <small>Up to 8 records, 12,000 characters each</small>
            <input
              type="file"
              multiple
              accept=".txt,text/plain"
              aria-label="Upload record files"
              onChange={async (e) => {
                const files = [...(e.currentTarget.files ?? [])];
                e.currentTarget.value = "";
                setNewCaseError("");
                const added: { name: string; text: string }[] = [];
                for (const file of files) {
                  if (!file.name.toLowerCase().endsWith(".txt") || file.size > 48000) {
                    setNewCaseError(
                      `${file.name} is not a .txt file of up to 12,000 characters.`
                    );
                    return;
                  }
                  added.push({ name: file.name, text: await file.text() });
                }
                setNewCaseFiles((previous) => [...previous, ...added]);
              }}
            />
          </label>
          {newCaseFiles.length > 0 && (
            <ul className="file-list">
              {newCaseFiles.map((file, i) => (
                <li key={i}>
                  <FileText size={15} />
                  <span>{file.name}</span>
                  <small>{file.text.length.toLocaleString("en")} characters</small>
                  <button
                    type="button"
                    className="icon-button"
                    aria-label={`Remove ${file.name}`}
                    onClick={() =>
                      setNewCaseFiles((previous) =>
                        previous.filter((_, j) => j !== i)
                      )
                    }
                  >
                    <Trash size={15} />
                  </button>
                </li>
              ))}
            </ul>
          )}
          <details className="paste">
            <summary>Or paste a note</summary>
            <textarea
              name="text"
              maxLength={12000}
              rows={6}
              placeholder="Paste a fictional prescription or clinical note. Include lab dates and units."
            />
          </details>
          <label className="check">
            <input type="checkbox" required />
            These records are synthetic, with no real patient information.
          </label>
          {newCaseError && (
            <p className="form-error" role="alert">
              {newCaseError}
            </p>
          )}
          <div className="row-end">
            <button
              type="button"
              className="button"
              onClick={() => caseDialog.current?.close()}
            >
              Cancel
            </button>
            <button className="button primary" type="submit">
              Create patient
            </button>
          </div>
        </form>
      </dialog>
      <dialog ref={recordDialog} className="dialog">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            if (
              !recordText.trim() ||
              recordText.length > 12000 ||
              sources.length >= 8
            )
              return;
            const data = new FormData(e.currentTarget);
            const record: Source = {
              id: `source-${crypto.randomUUID()}`,
              title: String(data.get("title")).trim() || "Additional note",
              kind: "Synthetic note",
              date: "2026-10-23",
              text: recordText.trim(),
            };
            setSources((previous) => [...previous, record]);
            setSourceId(record.id);
            setDraft(null);
            setRecoveryNeeded(false);
            setReview({});
            setAssessment(null);
            setSavedReport(null);
            setStep("sources");
            setAcknowledge(false);
            setHighlight(null);
            setError("");
            recordDialog.current?.close();
            e.currentTarget.reset();
          }}
        >
          <div className="dialog-head">
            <h2>Add a record</h2>
            <button
              type="button"
              className="icon-button"
              aria-label="Close"
              onClick={() => recordDialog.current?.close()}
            >
              <X size={18} />
            </button>
          </div>
          <p className="muted">
            Keep each record separate so every fact can point back to its
            source. Synthetic data only.
          </p>
          <label>
            Record title
            <input name="title" required maxLength={120} autoComplete="off" />
          </label>
          <label className="dropzone compact">
            <UploadSimple size={20} />
            <strong>Import a .txt file</strong>
            <input
              type="file"
              accept=".txt,text/plain"
              aria-label="Import a text file"
              onChange={async (e) => {
                const input = e.currentTarget;
                const file = input.files?.[0];
                setRecordError("");
                if (!file) return;
                if (
                  !file.name.toLowerCase().endsWith(".txt") ||
                  file.size > 48000
                ) {
                  setRecordError(
                    "Choose a .txt file with no more than 12,000 characters."
                  );
                  return;
                }
                const text = await file.text();
                if (text.length > 12000 || text.includes("\u0000")) {
                  setRecordError("This file is too long or is not plain text.");
                  return;
                }
                const title = input.form?.elements.namedItem(
                  "title"
                ) as HTMLInputElement | null;
                if (title && !title.value)
                  title.value = file.name.replace(/\.txt$/i, "").slice(0, 120);
                setRecordText(text);
              }}
            />
          </label>
          <label>
            Record text
            <textarea
              required
              maxLength={12000}
              rows={7}
              value={recordText}
              onChange={(e) => setRecordText(e.target.value)}
            />
          </label>
          <label className="check">
            <input type="checkbox" required />
            This record contains only synthetic data.
          </label>
          {recordError && (
            <p className="form-error" role="alert">
              {recordError}
            </p>
          )}
          <div className="row-end">
            <button
              type="button"
              className="button"
              onClick={() => recordDialog.current?.close()}
            >
              Cancel
            </button>
            <button type="submit" className="button primary">
              Add record
            </button>
          </div>
        </form>
      </dialog>
      {import.meta.env.DEV && (
      <dialog
        ref={connectionDialog}
        className="dialog"
        onClose={() => connectionForm.current?.reset()}
      >
        <form
          ref={connectionForm}
          onSubmit={async (e) => {
            e.preventDefault();
            const form = e.currentTarget;
            const data = new FormData(form);
            setBusy("Saving connection");
            setConnectionError("");
            try {
              await api("model", {
                endpoint: data.get("endpoint"),
                model: data.get("model"),
                key: data.get("key"),
                auth: "api-key",
              });
              setStatus(await api<Status>("status"));
              setUseModel(true);
              connectionDialog.current?.close();
              setToast(
                "Azure connection configured for this server session. Read the records to test it."
              );
            } catch (error) {
              setConnectionError(
                error instanceof Error
                  ? error.message
                  : "Connection could not be configured."
              );
            } finally {
              (form.elements.namedItem("key") as HTMLInputElement).value = "";
              setBusy("");
            }
          }}
        >
          <div className="dialog-head">
            <h2>Connect your Azure model</h2>
            <button
              type="button"
              className="icon-button"
              aria-label="Close connection form"
              onClick={() => connectionDialog.current?.close()}
            >
              <X size={18} />
            </button>
          </div>
          <p className="muted">
            Use the deployment details from Azure Foundry. The key stays in
            local server memory and is cleared when the server stops.
          </p>
          <label>
            Azure endpoint
            <input
              name="endpoint"
              aria-label="Azure endpoint"
              type="url"
              required
              placeholder="https://your-resource.services.ai.azure.com/openai/v1/chat/completions"
              autoComplete="off"
            />
          </label>
          <label>
            Deployment name
            <input
              name="model"
              aria-label="Deployment name"
              required
              defaultValue={status?.model ?? "MARMAR-Llama-3.3-70B-Instruct"}
              autoComplete="off"
            />
          </label>
          <label>
            API key
            <input
              name="key"
              aria-label="API key"
              type="password"
              required
              autoComplete="off"
            />
          </label>
          {connectionError && (
            <p className="form-error" role="alert">
              {connectionError}
            </p>
          )}
          <div className="row-end">
            <button
              type="button"
              className="button"
              onClick={() => connectionDialog.current?.close()}
            >
              Cancel
            </button>
            <button type="submit" className="button primary" disabled={!!busy}>
              {busy || "Save session connection"}
            </button>
          </div>
        </form>
      </dialog>
      )}
      {toast && (
        <div className="toast" role="status">
          <CheckCircle size={18} weight="fill" />
          {toast}
        </div>
      )}
    </div>
  );
}
function FactIcon({ kind, size = 17 }: { kind: string; size?: number }) {
  const Icon =
    kind === "lab"
      ? TestTube
      : kind === "medicine"
      ? Pill
      : kind === "allergy"
      ? ShieldWarning
      : kind === "condition"
      ? Heartbeat
      : kind === "weight"
      ? Scales
      : kind === "pregnancy"
      ? Baby
      : User;
  return <Icon size={size} />;
}