// Practice blade — annotator-as-clinician mode. The LLM plays only the
// patient; the human annotator types clinician questions and invokes tools
// from the right-rail palette. At the end, they pick the disease from a
// 20-option multiple-choice list and the page reveals the criteria they were
// scored against. Manager-only.
//
// Why a separate page from Simulator: the role-swap (annotator IS clinician,
// LLM IS patient) inverts every render decision the simulator makes, and the
// MCQ commit flow is unique to this mode. Reuses CLINICAL_TOOLS, executeTool,
// runOnePatientStep, checkOutcomeCorrect, and checkMustAskAbout from data.jsx.

function PracticePage({ task, user, setRoute }) {
  if (!user || user.kind !== "manager") {
    return (
      <div className="page" style={{padding:"22px 32px", color:"var(--ink-dim)"}}>
        Managers only.
      </div>
    );
  }
  if (!task) {
    return (
      <div className="page" style={{padding:"22px 32px", color:"var(--ink-dim)"}}>
        Pick a task in the queue first — Practice runs against the active task.
      </div>
    );
  }

  const nowHMS = () => {
    const d = new Date();
    return [d.getHours(), d.getMinutes(), d.getSeconds()]
      .map(n => String(n).padStart(2, "0")).join(":");
  };

  const storageKey = `caa_practice_state_${task.id}`;

  // ----- core state -----
  const [messages, setMessages] = React.useState([]);
  const [envState, setEnvState] = React.useState({});
  const [maxSteps, setMaxSteps] = React.useState(30);
  const [completed, setCompleted] = React.useState(false);
  const [input, setInput] = React.useState("");
  const [running, setRunning] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [showGuess, setShowGuess] = React.useState(false);
  const [reveal, setReveal] = React.useState(null); // { chosen, outcome }
  const [cfg, setCfg] = React.useState(() => window.getLlmConfig());

  React.useEffect(() => {
    const onChange = (e) => setCfg(e.detail || window.getLlmConfig());
    window.addEventListener("caa-llm-cfg-changed", onChange);
    return () => window.removeEventListener("caa-llm-cfg-changed", onChange);
  }, []);

  const abortRef = React.useRef(null);
  const hydratedRef = React.useRef(false);

  // ----- persistence: hydrate on mount, then debounced save -----
  React.useEffect(() => {
    if (hydratedRef.current) return;
    hydratedRef.current = true;
    try {
      const raw = localStorage.getItem(storageKey);
      if (!raw) return;
      const d = JSON.parse(raw);
      if (d && Array.isArray(d.messages)) setMessages(d.messages);
      if (d && d.state) setEnvState(d.state);
      if (d && typeof d.maxSteps === "number") setMaxSteps(d.maxSteps);
      if (d && d.completed) setCompleted(true);
      if (d && d.reveal) setReveal(d.reveal);
    } catch {}
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [storageKey]);

  React.useEffect(() => {
    const id = setTimeout(() => {
      try {
        localStorage.setItem(storageKey, JSON.stringify({
          messages, state: envState, maxSteps, completed, reveal,
        }));
      } catch {}
    }, 250);
    return () => clearTimeout(id);
  }, [storageKey, messages, envState, maxSteps, completed, reveal]);

  const userMsgCount = messages.filter(m => m && m.role === "assistant").length;
  const stepCount = messages.length;
  const stepCapped = stepCount >= maxSteps;
  const locked = !!reveal || showGuess;

  // ----- tool palette: same surface the LLM gets, minus terminal actions.
  // A tool is exposed iff its name appears in task.requiredTools, optionalTools,
  // or forbiddenTools. Tools whose state is "off" (not in any of those three
  // arrays) are hidden — matching what the practitioner would see if they were
  // the LLM clinician on this task. record_diagnosis / record_differential are
  // always filtered because the practitioner commits via Finish & guess, not
  // by tool call.
  const palette = React.useMemo(() => {
    const exposed = new Set();
    for (const f of ["requiredTools", "optionalTools", "forbiddenTools"]) {
      const arr = Array.isArray(task && task[f]) ? task[f] : [];
      for (const n of arr) if (typeof n === "string" && n.trim()) exposed.add(n);
    }
    const out = [];
    const seen = new Set();
    const builtins = Array.isArray(window.CLINICAL_TOOLS) ? window.CLINICAL_TOOLS : [];
    for (const t of builtins) {
      const fn = t && t.function;
      if (!fn || !fn.name) continue;
      if (fn.name === "record_diagnosis" || fn.name === "record_differential") continue;
      if (!exposed.has(fn.name)) continue; // off → hide
      if (seen.has(fn.name)) continue;
      seen.add(fn.name);
      out.push({ name: fn.name, description: fn.description || "", parameters: fn.parameters || {} });
    }
    const catalog = Array.isArray(window.__CAA_TOOL_CATALOG) ? window.__CAA_TOOL_CATALOG : [];
    for (const t of catalog) {
      if (!t || !t.name || seen.has(t.name)) continue;
      if (!exposed.has(t.name)) continue;
      if (t.name === "record_diagnosis" || t.name === "record_differential") continue;
      seen.add(t.name);
      out.push({
        name: t.name,
        description: t.description || "",
        parameters: (t.openai_spec && t.openai_spec.parameters) || t.parameters || { type: "object", properties: {} },
      });
    }
    const custom = Array.isArray(task && task.customTools) ? task.customTools : [];
    for (const t of custom) {
      if (!t || !t.name || seen.has(t.name)) continue;
      if (!exposed.has(t.name)) continue;
      if (t.name === "record_diagnosis" || t.name === "record_differential") continue;
      seen.add(t.name);
      out.push({
        name: t.name,
        description: t.description || "",
        parameters: t.parameters || { type: "object", properties: {} },
      });
    }
    return out;
  }, [task]);

  // ----- send a clinician (annotator) turn, then trigger the patient LLM -----
  const sendTurn = async () => {
    if (locked || !input.trim() || running || stepCapped) return;
    setError(null);
    const human = { idx: messages.length, role: "assistant", text: input, t: nowHMS(), _practice_clinician: true };
    const after = [...messages, human];
    setMessages(after);
    setInput("");
    if (!cfg.apiKey) return; // offline — annotator can keep typing, no auto patient
    if (after.length >= maxSteps) return;
    setRunning(true);
    const ac = new AbortController();
    abortRef.current = ac;
    try {
      const turn = await window.runOnePatientStep({ messages: after, task, cfg, signal: ac.signal });
      // runOnePatientStep returns role:"user" because in the simulator's master
      // convo the patient IS the user. In Practice mode the patient sits on the
      // RIGHT, so we render with role:"user" but tag it so the bubble layout
      // knows which side. We keep role:"user" — the data shape stays identical
      // so messagesForLLM(messages,"user") on the next turn reads it correctly.
      const stamped = { ...turn, idx: after.length, t: nowHMS() };
      setMessages(prev => [...prev, stamped]);
    } catch (e) {
      if (e && e.name !== "AbortError") setError(e.message || String(e));
    } finally {
      setRunning(false);
    }
  };

  // ----- run a tool from the palette -----
  const runTool = async (tool, argValues) => {
    if (locked || running || stepCapped) return;
    setError(null);
    setRunning(true);
    try {
      const localState = envState;
      const returns = await window.executeTool({
        name: tool.name, args: argValues || {}, task, state: localState, messages,
      });
      const toolMsg = {
        idx: messages.length, role: "tool", name: tool.name,
        args: argValues || {}, returns, t: nowHMS(),
      };
      setMessages(prev => [...prev, toolMsg]);
      // executeTool may have mutated the localState reference; pull a copy.
      setEnvState({ ...localState });
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setRunning(false);
    }
  };

  const resetPractice = () => {
    if (abortRef.current) abortRef.current.abort();
    setMessages([]);
    setEnvState({});
    setCompleted(false);
    setReveal(null);
    setShowGuess(false);
    setInput("");
    setError(null);
    try { localStorage.removeItem(storageKey); } catch {}
  };

  // ----- candidate-list builder for Finish & guess modal -----
  const buildCandidateList = async () => {
    const correct = task.diagnosis || "";
    const accepted = (task.acceptableDiagnoses || []).filter(Boolean).slice(0, 4);
    const targetSet = new Set([correct, ...accepted].map(s => String(s).toLowerCase()));
    const pool = [];
    try {
      const res = await window.tasksApi.list({ cohort: task.cohortId, limit: 200 });
      for (const t of (res && res.items) || []) {
        if (t && t.diagnosis && !targetSet.has(String(t.diagnosis).toLowerCase())) {
          pool.push(t.diagnosis);
        }
      }
    } catch {}
    if (pool.length < 19 && Array.isArray(window.TASKS)) {
      for (const t of window.TASKS) {
        if (t && t.diagnosis && !targetSet.has(String(t.diagnosis).toLowerCase())) {
          pool.push(t.diagnosis);
        }
      }
    }
    const distractors = [...new Set(pool)].sort(() => Math.random() - 0.5).slice(0, 19);
    const all = [correct, ...accepted, ...distractors].filter(Boolean).slice(0, 20);
    return all.sort(() => Math.random() - 0.5);
  };

  // Save status for the trajectory: "idle" | "saving" | "saved" | "error".
  // Practitioner controls whether to save via a button in the reveal modal —
  // we don't auto-persist, since some practice runs are throwaway exploration.
  const [saveState, setSaveState] = React.useState("idle");
  const [saveErr, setSaveErr] = React.useState(null);

  const submitGuess = (chosen) => {
    const synthetic = {
      idx: messages.length,
      role: "user",
      text: `[Annotator final guess: ${chosen}]`,
      t: nowHMS(),
      _practice_guess: true,
    };
    const after = [...messages, synthetic];
    setMessages(after);

    // Build a scoring view the simulator's evaluator can read. Practice stores
    // tool calls as standalone role:"tool" turns, but extractToolCalls/checkSafety
    // only see toolCalls hanging off role:"assistant". Re-shape each tool turn
    // into an assistant turn carrying the same call, append a synthesized
    // record_diagnosis for the MCQ pick, then run the same gates the LLM is
    // judged on so a correct guess alone no longer passes when required tools
    // / required actions / must-ask / forbidden tools are unmet.
    const scoringMessages = [];
    for (const m of after) {
      if (m && m.role === "tool" && m.name) {
        scoringMessages.push({
          role: "assistant",
          text: "",
          toolCalls: [{ name: m.name, args: m.args || {} }],
        });
      } else {
        scoringMessages.push(m);
      }
    }
    scoringMessages.push({
      role: "assistant",
      text: "",
      toolCalls: [{
        name: "record_diagnosis",
        args: {
          diagnosis: chosen,
          confidence: "annotator",
          reasoning: "[annotator picked from 20-option MCQ]",
        },
      }],
    });

    const reward = window.computeFinalReward
      ? window.computeFinalReward({ messages: scoringMessages, task })
      : null;
    const outcome = reward ? (reward.info && reward.info.outcome) : window.checkOutcomeCorrect(
      [{ name: "record_diagnosis", arguments: { diagnosis: chosen } }], task
    );
    const pass = reward ? !!reward.binary_pass : !!(outcome && outcome.diagnosis_correct);
    setReveal({ chosen, outcome, reward, pass });
    setShowGuess(false);
    setCompleted(true);
    setSaveState("idle");
    setSaveErr(null);
  };

  const saveTrajectoryToD1 = React.useCallback(async () => {
    if (saveState === "saving" || saveState === "saved") return;
    if (!reveal) return;
    setSaveState("saving");
    setSaveErr(null);
    try {
      // Reuse the reward already computed at submit time. Recomputing here
      // from raw `messages` would mis-score: practice stores tool calls as
      // role:"tool" turns and never emits a real record_diagnosis call, so
      // computeFinalReward(messages, task) sees zero tool calls and returns
      // outcome_pass=false regardless of what the practitioner did.
      const finalReward = reveal.reward || (window.computeFinalReward
        ? window.computeFinalReward({ messages, task }) : null);
      const binaryPass = !!(finalReward && finalReward.binary_pass);
      const reward_summary = finalReward && {
        turns_total: messages.length,
        turns_scored: 0, // practice mode has no per-turn scores
        reward_sum: binaryPass ? 1 : 0,
        reward_mean: binaryPass ? 1 : 0,
        reward_scale: "binary · practice",
      };
      const submissionId = `practice_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
      const body = {
        id: submissionId,
        task_id: task.id,
        annotator_email: (user && user.email) || null,
        annotator_name: (user && user.name) || null,
        submitted_at: new Date().toISOString(),
        mode: "practice",
        // Top-level pass flag so trajectory list/filter queries don't have to
        // dig into rollup. Mirrors `final_reward.binary_pass`.
        binary_pass: binaryPass,
        messages,
        env_state: envState,
        scores: {},
        notes: "",
        tags: ["practice", binaryPass ? "pass" : "fail"],
        rollup: {
          mode: "practice",
          chosen_diagnosis: reveal.chosen,
          outcome: reveal.outcome,
          binary_pass: binaryPass,
          checklist: (finalReward && finalReward.checklist) || null,
        },
        recorded_diagnosis: { diagnosis: reveal.chosen, confidence: "annotator", reasoning: "[annotator picked from 20-option MCQ]" },
        final_reward: finalReward,
        reward_summary,
      };
      const res = await fetch("/api/trajectories", {
        method: "POST",
        credentials: "same-origin",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        let detail = `HTTP ${res.status}`;
        try { const j = await res.json(); if (j && j.error) detail = j.error; } catch {}
        throw new Error(detail);
      }
      setSaveState("saved");
    } catch (e) {
      setSaveErr((e && e.message) || String(e));
      setSaveState("error");
    }
  }, [saveState, reveal, messages, envState, task, user]);

  // ----- left rail: case context (no spoilers) -----
  const summary = `${task.age || "?"}-year-old ${task.gender || "patient"} with ${
    task.chief ? task.chief.toLowerCase() : "an unspecified complaint"
  }${task.duration ? ` × ${task.duration}` : ""}`;

  return (
    <div className="workspace">
      <div className="ws-head" style={{paddingBottom:0}}>
        <div className="ws-crumbs">
          <span>practice</span> / <b>{task.id}</b> / <span>annotator-as-clinician</span>
        </div>
        <div className="ws-status">
          <button className="btn ghost" onClick={()=>setRoute("queue")}>back to queue</button>
          <label style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", display:"inline-flex", alignItems:"center", gap:6}}>
            max steps
            <input type="number" min={5} max={200}
              value={maxSteps}
              onChange={e => setMaxSteps(Math.max(5, Math.min(200, Number(e.target.value) || 30)))}
              style={{width:60, padding:"3px 6px", border:"1px solid var(--line)", background:"var(--bg)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:11.5}}
              disabled={locked}
            />
          </label>
          <button className="btn" onClick={resetPractice} disabled={running}>{Ico.reset()} Reset</button>
          <button className="btn primary"
            onClick={() => setShowGuess(true)}
            disabled={locked || running}
            title="End the consultation now and pick a diagnosis from the 20-option list"
          >
            {Ico.check()} Finish &amp; guess
          </button>
        </div>
      </div>

      {error && (
        <div style={{padding:"8px 14px", background:"#fef2f2", color:"#b91c1c", fontSize:12.5, fontFamily:"var(--font-mono)", borderBottom:"1px solid var(--line)"}}>
          ⚠ {error}
        </div>
      )}
      {stepCapped && !reveal && (
        <div style={{padding:"8px 14px", background:"var(--bg-sunken)", color:"var(--ink-dim)", fontSize:12.5, borderBottom:"1px solid var(--line)"}}>
          Max steps reached — click "Finish &amp; guess" in the top-left to commit.
        </div>
      )}

      <div className="sim">
        <div className="sim-context">
          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:8}}>
            Case context
          </div>
          <div style={{fontFamily:"var(--font-serif)", fontSize:16, lineHeight:1.35, marginBottom:6}}>
            {summary}
          </div>
          {/* Patient id — same string the LLM clinician sees in its system
              prompt. Practitioners need it for tool calls (every chart-read
              tool takes patient_id). */}
          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginBottom:14}}>
            patient_id: <b style={{color:"var(--ink-dim)"}}>{
              (task && task.patientId)
              || (task && task.id ? `MRN${String(task.id).slice(-6)}` : "P001")
            }</b>
          </div>
          <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>
            Progress
          </div>
          <div style={{fontSize:12.5, color:"var(--ink-dim)", marginBottom:14}}>
            Step <b style={{color:"var(--ink)"}}>{stepCount}</b> / {maxSteps}
            <div style={{fontSize:11, color:"var(--ink-mute)", marginTop:2}}>
              {userMsgCount} clinician message{userMsgCount === 1 ? "" : "s"} sent
            </div>
          </div>
          <div style={{fontSize:11, color:"var(--ink-mute)", lineHeight:1.5, marginBottom:14}}>
            You're playing the clinician. Ask questions, order tests via the
            tool palette, and commit a final diagnosis. The evaluation criteria
            stay hidden until you submit.
          </div>

          {/* Clinician's policy + custom instructions — same content the LLM
              clinician would receive in its system prompt. Shown to the
              practitioner so they have the same playing field. Collapsed by
              default since the policy text can be long. */}
          {(() => {
            const policyId = (task && task.policyId) || "primekg";
            const builtIns = (typeof window !== "undefined" && window.CAA_POLICIES) || {};
            const customMap = (typeof window !== "undefined" && window.__CAA_POLICIES_CUSTOM) || {};
            const policyText =
              (customMap[policyId] && customMap[policyId].content) ||
              builtIns[policyId] ||
              builtIns.primekg ||
              "";
            const customInstructions = String((task && task.customPolicy) || "").trim();
            if (!policyText && !customInstructions) return null;
            return (
              <details style={{marginBottom:14}}>
                <summary style={{cursor:"pointer", fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em"}}>
                  Clinician policy &amp; instructions
                  <span style={{textTransform:"none", letterSpacing:0, fontFamily:"var(--font-mono)", fontSize:10.5, marginLeft:6, color:"var(--ink-mute)"}}>
                    (same as the LLM sees)
                  </span>
                </summary>
                <div style={{marginTop:8, display:"grid", gap:10}}>
                  {policyText && (
                    <div>
                      <div style={{fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:4}}>
                        Policy ({policyId})
                      </div>
                      <pre style={{
                        background:"var(--bg-sunken)", padding:"8px 10px",
                        borderRadius:4, fontSize:11, lineHeight:1.45,
                        whiteSpace:"pre-wrap", wordBreak:"break-word",
                        margin:0, maxHeight:300, overflow:"auto",
                        fontFamily:"var(--font-serif)", color:"var(--ink-dim)",
                      }}>{policyText}</pre>
                    </div>
                  )}
                  {customInstructions && (
                    <div>
                      <div style={{fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:4}}>
                        Task-specific instructions
                      </div>
                      <pre style={{
                        background:"var(--bg-sunken)", padding:"8px 10px",
                        borderRadius:4, fontSize:11, lineHeight:1.45,
                        whiteSpace:"pre-wrap", wordBreak:"break-word",
                        margin:0, maxHeight:240, overflow:"auto",
                        fontFamily:"var(--font-serif)", color:"var(--ink-dim)",
                      }}>{customInstructions}</pre>
                    </div>
                  )}
                </div>
              </details>
            );
          })()}
        </div>

        <div className="sim-center">
          <div className="transcript">
            {messages.length === 0 ? (
              <div style={{padding:"40px 20px", textAlign:"center", color:"var(--ink-mute)", fontSize:13}}>
                The patient is waiting. Open with a question.
              </div>
            ) : null}
            {messages.map((m, i) => {
              // role -> visual side
              //   "assistant" (clinician/annotator)  → LEFT, label "Clinician (you)"
              //   "user" (patient LLM)                → RIGHT, label "Patient"
              //   "tool"                              → tool bubble
              const isClinician = m.role === "assistant";
              const isPatient = m.role === "user";
              const label = isClinician ? "Clinician (you)" : isPatient ? "Patient" : m.role;
              return (
                <div key={i} className="turn" style={{
                  gridTemplateColumns: isPatient ? "1fr 80px" : "80px 1fr",
                }}>
                  {!isPatient && (
                    <div className="turn-role">
                      <b>{label}</b>
                      <div><span className="turn-idx">#{String(i).padStart(2,'0')}</span>{m.t}</div>
                    </div>
                  )}
                  {m.role === "tool" ? (
                    <div className="bubble" data-role="tool">
                      <div className="bubble-tool-head">
                        {Ico.tool()}
                        <span className="tname">{m.name}</span>
                        <span style={{color:"var(--ink-mute)", fontSize:11}}>tool call</span>
                      </div>
                      <details style={{marginTop:4, fontSize:11, color:"var(--ink-mute)", padding:"6px 12px"}}>
                        <summary style={{cursor:"pointer", fontFamily:"var(--font-mono)"}}>input / output</summary>
                        <div style={{marginTop:6, display:"grid", gap:6}}>
                          <div>
                            <div style={{fontFamily:"var(--font-mono)", fontSize:10, textTransform:"uppercase", letterSpacing:"0.06em"}}>input (args)</div>
                            <pre style={{background:"var(--bg-sunken)", padding:"6px 8px", borderRadius:4, fontSize:11, lineHeight:1.35, whiteSpace:"pre-wrap", wordBreak:"break-word", maxHeight:240, overflow:"auto", margin:0}}>{JSON.stringify(m.args, null, 2)}</pre>
                          </div>
                          <div>
                            <div style={{fontFamily:"var(--font-mono)", fontSize:10, textTransform:"uppercase", letterSpacing:"0.06em"}}>output (returns)</div>
                            <pre style={{background:"var(--bg-sunken)", padding:"6px 8px", borderRadius:4, fontSize:11, lineHeight:1.35, whiteSpace:"pre-wrap", wordBreak:"break-word", maxHeight:240, overflow:"auto", margin:0}}>{JSON.stringify(m.returns, null, 2)}</pre>
                          </div>
                        </div>
                      </details>
                    </div>
                  ) : (
                    <div className="bubble"
                      data-role={isPatient ? "user" : "assistant"}
                      style={{
                        cursor:"default",
                        ...(m._practice_guess ? {fontStyle:"italic", color:"var(--ink-mute)"} : {}),
                        justifySelf: isPatient ? "end" : "start",
                      }}>
                      <span style={{fontSize:13.5, lineHeight:1.5}}>{m.text}</span>
                    </div>
                  )}
                  {isPatient && (
                    <div className="turn-role" style={{textAlign:"left", paddingLeft:14}}>
                      <b>{label}</b>
                      <div><span className="turn-idx">#{String(i).padStart(2,'0')}</span>{m.t}</div>
                    </div>
                  )}
                </div>
              );
            })}
            {running && (
              <div className="turn">
                <div className="turn-role"><b>Patient</b><div>···</div></div>
                <div className="bubble" style={{display:"flex", gap:6, alignItems:"center", color:"var(--ink-mute)", fontStyle:"italic"}}>
                  thinking…
                </div>
              </div>
            )}
          </div>

          <div className="composer">
            <div className="composer-persona">
              <span className="persona-chip">Clinician (you)</span>
              <span style={{color:"var(--ink-mute)"}}>· patient LLM will reply automatically</span>
            </div>
            <div className="composer-row">
              <textarea className="composer-input"
                placeholder={
                  !cfg.apiKey
                    ? "Set an API key in Settings — the patient won't reply without one."
                    : locked
                      ? "Locked — review the result, then Run again."
                      : stepCapped
                        ? "Max steps reached — click 'Finish & guess' in the top-left to commit."
                        : "Type a clinician question for the patient…"
                }
                value={input} onChange={e=>setInput(e.target.value)}
                onKeyDown={e => { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendTurn(); } }}
                disabled={locked || stepCapped}
              />
              <button className="btn primary" onClick={sendTurn}
                disabled={running || locked || stepCapped || !input.trim()}>
                {Ico.send()} Send
              </button>
            </div>
          </div>
        </div>

        <div className="sim-right">
          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:10}}>
            Tool palette ({palette.length})
          </div>
          {palette.length === 0 ? (
            <div style={{fontSize:12, color:"var(--ink-mute)", fontStyle:"italic"}}>
              No tools available for this task.
            </div>
          ) : palette.map(tool => (
            <ToolPaletteCard key={tool.name} tool={tool}
              disabled={locked || running || stepCapped}
              onUse={(args) => runTool(tool, args)}/>
          ))}
        </div>
      </div>

      {showGuess && (
        <GuessModal
          task={task}
          buildCandidateList={buildCandidateList}
          onSubmit={submitGuess}
          onClose={() => setShowGuess(false)}
        />
      )}
      {reveal && (
        <RevealModal
          task={task}
          messages={messages}
          chosen={reveal.chosen}
          outcome={reveal.outcome}
          reward={reveal.reward}
          pass={reveal.pass}
          onRunAgain={resetPractice}
          onClose={() => setReveal(null)}
          saveState={saveState}
          saveErr={saveErr}
          onSave={saveTrajectoryToD1}
        />
      )}
    </div>
  );
}

// One tool entry: name, collapsible description, auto-built form, Use button.
function ToolPaletteCard({ tool, disabled, onUse }) {
  const [expanded, setExpanded] = React.useState(false);
  const [vals, setVals] = React.useState({});
  const props = (tool.parameters && tool.parameters.properties) || {};
  const required = new Set((tool.parameters && tool.parameters.required) || []);
  const propNames = Object.keys(props);

  const setField = (k, v) => setVals(s => ({ ...s, [k]: v }));

  const collectArgs = () => {
    const out = {};
    for (const k of propNames) {
      const spec = props[k] || {};
      const raw = vals[k];
      if (raw === undefined || raw === "") continue;
      if (spec.type === "integer") {
        const n = parseInt(raw, 10);
        if (Number.isFinite(n)) out[k] = n;
      } else if (spec.type === "number") {
        const n = Number(raw);
        if (Number.isFinite(n)) out[k] = n;
      } else if (spec.type === "boolean") {
        out[k] = !!raw;
      } else if (spec.type === "array") {
        out[k] = String(raw).split(",").map(s => s.trim()).filter(Boolean);
      } else {
        out[k] = String(raw);
      }
    }
    return out;
  };

  return (
    <div style={{border:"1px solid var(--line)", borderRadius:4, marginBottom:8, background:"var(--bg)"}}>
      <div style={{padding:"6px 10px", borderBottom: expanded ? "1px solid var(--line)" : "none"}}>
        <div style={{display:"flex", alignItems:"center", gap:6}}>
          <code style={{flex:1, fontFamily:"var(--font-mono)", fontSize:12, color:"var(--ink)", overflow:"hidden", textOverflow:"ellipsis"}}>{tool.name}</code>
          <button onClick={() => setExpanded(v => !v)}
            style={{background:"none", border:"1px solid var(--line)", borderRadius:3, padding:"2px 6px", fontSize:10.5, fontFamily:"var(--font-mono)", color:"var(--ink-dim)", cursor:"pointer"}}>
            {expanded ? "hide" : "open"}
          </button>
        </div>
        {tool.description && expanded && (
          <div style={{fontSize:11, color:"var(--ink-mute)", marginTop:6, lineHeight:1.45}}>
            {tool.description}
          </div>
        )}
      </div>
      {expanded && (
        <div style={{padding:"8px 10px", display:"grid", gap:6}}>
          {propNames.length === 0 ? (
            <div style={{fontSize:11, color:"var(--ink-mute)", fontStyle:"italic"}}>No parameters.</div>
          ) : propNames.map(k => {
            const spec = props[k] || {};
            const isReq = required.has(k);
            const labelTxt = `${k}${isReq ? " *" : ""}`;
            const common = {
              style: {width:"100%", padding:"4px 6px", border:"1px solid var(--line)", borderRadius:3, background:"var(--bg)", fontFamily:"var(--font-mono)", fontSize:11.5},
              placeholder: spec.description || "",
            };
            let input;
            if (spec.type === "boolean") {
              input = (
                <input type="checkbox" checked={!!vals[k]}
                  onChange={e => setField(k, e.target.checked)} />
              );
            } else if (spec.type === "integer" || spec.type === "number") {
              input = (
                <input type="number" {...common}
                  value={vals[k] ?? ""} onChange={e => setField(k, e.target.value)} />
              );
            } else if (spec.type === "array") {
              input = (
                <input type="text" {...common}
                  placeholder={(spec.description || "") + " (comma-separated)"}
                  value={vals[k] ?? ""} onChange={e => setField(k, e.target.value)} />
              );
            } else {
              input = (
                <input type="text" {...common}
                  value={vals[k] ?? ""} onChange={e => setField(k, e.target.value)} />
              );
            }
            return (
              <label key={k} style={{display:"grid", gap:3, fontSize:11, color:"var(--ink-dim)", fontFamily:"var(--font-mono)"}}>
                <span>{labelTxt}</span>
                {input}
              </label>
            );
          })}
          <button className="btn"
            disabled={disabled}
            onClick={() => onUse(collectArgs())}
            style={{justifySelf:"start", marginTop:2}}>
            {Ico.play()} Use
          </button>
        </div>
      )}
    </div>
  );
}

// 20-option multiple-choice modal. Builds the candidate list async on mount.
function GuessModal({ task, buildCandidateList, onSubmit, onClose }) {
  const [candidates, setCandidates] = React.useState(null);
  const [chosen, setChosen] = React.useState("");
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const list = await buildCandidateList();
        if (!cancelled) { setCandidates(list); setLoading(false); }
      } catch {
        if (!cancelled) { setCandidates([task.diagnosis || ""]); setLoading(false); }
      }
    })();
    return () => { cancelled = true; };
  }, [buildCandidateList, task]);

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(20,20,20,0.45)", display:"grid", placeItems:"center", zIndex:200}}>
      <div style={{background:"var(--bg-panel)", border:"1px solid var(--line)", borderRadius:6, width:"min(560px, 94vw)", maxHeight:"86vh", display:"flex", flexDirection:"column"}}>
        <div style={{padding:"14px 18px", borderBottom:"1px solid var(--line)"}}>
          <div style={{fontFamily:"var(--font-serif)", fontSize:16, marginBottom:2}}>Pick the diagnosis</div>
          <div style={{fontSize:11.5, color:"var(--ink-mute)"}}>Choose one disease from the list. Submitting locks the practice run.</div>
        </div>
        <div style={{padding:"10px 18px", overflowY:"auto", flex:1}}>
          {loading ? (
            <div style={{color:"var(--ink-mute)", fontSize:12, padding:"20px 0"}}>Loading candidates…</div>
          ) : (
            <div style={{display:"grid", gap:4}}>
              {(candidates || []).map((d, i) => (
                <label key={i} style={{display:"flex", alignItems:"center", gap:8, padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, cursor:"pointer", background: chosen === d ? "var(--bg-sunken)" : "transparent"}}>
                  <input type="radio" name="dx-guess" value={d} checked={chosen === d} onChange={() => setChosen(d)} />
                  <span style={{fontSize:13}}>{d}</span>
                </label>
              ))}
            </div>
          )}
        </div>
        <div style={{padding:"10px 18px", borderTop:"1px solid var(--line)", display:"flex", gap:8, justifyContent:"flex-end"}}>
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={() => onSubmit(chosen)} disabled={!chosen || loading}>
            Submit guess
          </button>
        </div>
      </div>
    </div>
  );
}

// Post-submit reveal: PASS/FAIL + previously-hidden criteria.
function RevealModal({ task, messages, chosen, outcome, reward, pass: passProp, onRunAgain, onClose, saveState, saveErr, onSave }) {
  // Practice now uses the same binary_pass gate as the LLM simulator:
  // outcome_pass && safety && forbidden_tools && must_ask_about. A correct
  // diagnosis alone is not enough.
  const pass = (typeof passProp === "boolean")
    ? passProp
    : !!(outcome && outcome.diagnosis_correct);
  const checklist = (reward && reward.checklist) || null;
  const failedGates = checklist
    ? Object.entries({
        outcome_correct: "Outcome (diagnosis + required tools + actions)",
        safety_passed: "Safety",
        forbidden_tools_respected: "Forbidden tools",
        must_ask_about_covered: "Must ask about",
      }).filter(([k]) => checklist[k] === false).map(([, label]) => label)
    : [];
  const correct = task.diagnosis || "—";
  const matchTarget = outcome && outcome.match_target;
  const matchMethod = outcome && outcome.match_method;
  const methodLabel = (m) => ({
    substring: "exact / substring",
    core_substring: "core-disease substring",
    keyword_overlap: "keyword overlap ≥50%",
    mondo_synonym: "MONDO synonym",
    empty: "empty match",
  }[m] || m || "—");

  const mustAsk = (window.checkMustAskAbout && window.checkMustAskAbout(messages, task)) || { asked: [], total: 0, missing: [] };
  const askedSet = new Set(mustAsk.asked);
  const requiredTools = Array.isArray(task.requiredTools) ? task.requiredTools.filter(Boolean) : [];
  const optionalTools = Array.isArray(task.optionalTools) ? task.optionalTools.filter(Boolean) : [];
  const forbiddenTools = Array.isArray(task.forbiddenTools) ? task.forbiddenTools.filter(Boolean) : [];
  const usedToolNames = new Set(messages.filter(m => m.role === "tool" && m.name).map(m => m.name));
  const acceptable = Array.isArray(task.acceptableDiagnoses) ? task.acceptableDiagnoses.filter(Boolean) : [];
  const reasoning = Array.isArray(task.reasoning) ? task.reasoning.filter(Boolean) : [];
  const mustAskItems = Array.isArray(task.mustAskAbout) ? task.mustAskAbout.filter(Boolean) : [];

  // Parameterized action specs — evaluate against the practitioner's actual
  // tool call trajectory using the same matchers the simulator uses.
  const requiredActionsSpecs = Array.isArray(task.requiredActions)
    ? task.requiredActions.filter(a => a && a.name) : [];
  const forbiddenActionsSpecs = Array.isArray(task.forbiddenActions)
    ? task.forbiddenActions.filter(a => a && a.name) : [];
  const toolCallTrace = (window.extractToolCalls && window.extractToolCalls(messages)) || [];
  const requiredActionsResult = (requiredActionsSpecs.length && window.evaluateRequiredActions)
    ? window.evaluateRequiredActions(toolCallTrace, requiredActionsSpecs) : [];
  const forbiddenActionsResult = (forbiddenActionsSpecs.length && window.evaluateForbiddenActions)
    ? window.evaluateForbiddenActions(toolCallTrace, forbiddenActionsSpecs) : [];

  const taskInstructions = String(task.taskInstructions || "").trim();
  const renderToolList = (names, emptyText) => names.length === 0
    ? <div style={{fontSize:12, color:"var(--ink-mute)"}}>{emptyText}</div>
    : names.map((n, i) => {
        const used = usedToolNames.has(n);
        return (
          <div key={i} style={{display:"flex", alignItems:"center", gap:6, padding:"3px 0", fontSize:12}}>
            <span style={{color: used ? "var(--green, #2e8a53)" : "var(--ink-mute)"}}>
              {used ? Ico.check() : "○"}
            </span>
            <code style={{fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--ink-dim)"}}>{n}</code>
          </div>
        );
      });

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(20,20,20,0.45)", display:"grid", placeItems:"center", zIndex:200}}>
      <div style={{background:"var(--bg-panel)", border:"1px solid var(--line)", borderRadius:6, width:"min(900px, 96vw)", maxHeight:"92vh", display:"flex", flexDirection:"column"}}>
        <div style={{padding:"14px 18px", borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:14}}>
          <div style={{
            fontFamily:"var(--font-mono)", fontSize:20, fontWeight:600,
            letterSpacing:"0.1em",
            color: pass ? "var(--green, #2e8a53)" : "var(--red, #b91c1c)",
            padding:"4px 12px", border:`1px solid ${pass ? "var(--green, #2e8a53)" : "var(--red, #b91c1c)"}`,
            borderRadius:4,
          }}>
            {pass ? "PASS" : "FAIL"}
          </div>
          <div style={{fontSize:12, color:"var(--ink-dim)"}}>
            <div>You picked: <b style={{color:"var(--ink)"}}>{chosen}</b></div>
            <div>Correct answer: <b style={{color:"var(--ink)"}}>{correct}</b></div>
            {matchTarget && (
              <div style={{color:"var(--ink-mute)", fontSize:11, fontFamily:"var(--font-mono)", marginTop:2}}>
                {outcome && outcome.diagnosis_correct ? "matched" : "no match"}{matchTarget ? ` "${matchTarget}"` : ""} {matchMethod ? `via ${methodLabel(matchMethod)}` : ""}
              </div>
            )}
            {!pass && failedGates.length > 0 && (
              <div style={{color:"var(--red, #b91c1c)", fontSize:11, fontFamily:"var(--font-mono)", marginTop:4}}>
                failed: {failedGates.join(" · ")}
              </div>
            )}
          </div>
          {checklist && (
            <div style={{marginLeft:"auto", display:"flex", gap:6, flexWrap:"wrap"}}>
              {[
                ["outcome", checklist.outcome_correct],
                ["safety", checklist.safety_passed],
                ["forbidden", checklist.forbidden_tools_respected],
                ["must ask", checklist.must_ask_about_covered],
              ].map(([label, ok]) => (
                <span key={label} style={{
                  fontFamily:"var(--font-mono)", fontSize:10.5, padding:"2px 6px",
                  border:`1px solid ${ok ? "var(--green, #2e8a53)" : "var(--red, #b91c1c)"}`,
                  color: ok ? "var(--green, #2e8a53)" : "var(--red, #b91c1c)",
                  borderRadius:3,
                }}>
                  {ok ? "✓" : "✗"} {label}
                </span>
              ))}
            </div>
          )}
        </div>
        <div style={{padding:"14px 18px", overflowY:"auto", flex:1, display:"grid", gridTemplateColumns:"1fr 1fr", gap:18}}>
          <div>
            <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>
              Must ask about ({mustAskItems.length})
            </div>
            {mustAskItems.length === 0 ? (
              <div style={{fontSize:12, color:"var(--ink-mute)"}}>—</div>
            ) : mustAskItems.map((item, i) => {
              const hit = askedSet.has(item);
              return (
                <div key={i} style={{display:"flex", alignItems:"flex-start", gap:6, padding:"3px 0", fontSize:12}}>
                  <span style={{color: hit ? "var(--green, #2e8a53)" : "var(--ink-mute)", flexShrink:0, marginTop:2}}>
                    {hit ? Ico.check() : "○"}
                  </span>
                  <span style={{color: hit ? "var(--ink-dim)" : "var(--ink-mute)"}}>{item}</span>
                </div>
              );
            })}

            <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
              Required tools ({requiredTools.length})
            </div>
            {renderToolList(requiredTools, "—")}

            {optionalTools.length > 0 && (
              <>
                <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
                  Optional tools ({optionalTools.length})
                </div>
                {renderToolList(optionalTools, "—")}
              </>
            )}

            {forbiddenTools.length > 0 && (
              <>
                <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
                  Forbidden tools ({forbiddenTools.length})
                </div>
                {forbiddenTools.map((n, i) => {
                  const violated = usedToolNames.has(n);
                  return (
                    <div key={i} style={{display:"flex", alignItems:"center", gap:6, padding:"3px 0", fontSize:12}}>
                      <span style={{color: violated ? "var(--red, #b91c1c)" : "var(--green, #2e8a53)"}}>
                        {violated ? (Ico.x ? Ico.x() : "✗") : Ico.check()}
                      </span>
                      <code style={{fontFamily:"var(--font-mono)", fontSize:11.5, color: violated ? "var(--red, #b91c1c)" : "var(--ink-dim)"}}>{n}</code>
                    </div>
                  );
                })}
              </>
            )}
          </div>
          <div>
            <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>
              Reference diagnosis
            </div>
            <div style={{fontSize:13, fontFamily:"var(--font-serif)", color:"var(--ink)"}}>{correct}</div>

            <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
              Acceptable diagnoses ({acceptable.length})
            </div>
            {acceptable.length === 0 ? (
              <div style={{fontSize:12, color:"var(--ink-mute)"}}>—</div>
            ) : acceptable.map((d, i) => (
              <div key={i} style={{fontSize:12, padding:"2px 0", color:"var(--ink-dim)"}}>{d}</div>
            ))}

            <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
              Ideal reasoning steps
            </div>
            {reasoning.length === 0 ? (
              <div style={{fontSize:12, color:"var(--ink-mute)"}}>—</div>
            ) : (
              <ol style={{margin:0, paddingLeft:18, fontSize:12, color:"var(--ink-dim)", lineHeight:1.5}}>
                {reasoning.map((r, i) => <li key={i}>{r}</li>)}
              </ol>
            )}
          </div>

          {/* Full-width row: parameterized action specs (required/forbidden) +
              patient role-brief + outcome JSON. These were hidden during the
              practice run; revealed in full after submission. */}
          {(requiredActionsSpecs.length > 0 || forbiddenActionsSpecs.length > 0) && (
            <div style={{gridColumn:"1 / -1", borderTop:"1px solid var(--line)", paddingTop:14}}>
              {requiredActionsSpecs.length > 0 && (
                <>
                  <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>
                    Required actions ({requiredActionsSpecs.length})
                  </div>
                  {requiredActionsResult.map((r, i) => {
                    const spec = r.spec || {};
                    const argHint = spec.arg_constraints && Object.keys(spec.arg_constraints).length
                      ? " (" + Object.entries(spec.arg_constraints).map(([k, c]) => `${k} ${c.mode || "equals"} ${Array.isArray(c.value) ? c.value.join("/") : c.value}`).join(", ") + ")"
                      : "";
                    return (
                      <div key={i} style={{display:"flex", alignItems:"flex-start", gap:6, padding:"3px 0", fontSize:12}}>
                        <span style={{color: r.matched ? "var(--green, #2e8a53)" : "var(--ink-mute)", flexShrink:0, marginTop:2}}>
                          {r.matched ? Ico.check() : "○"}
                        </span>
                        <span style={{color: r.matched ? "var(--ink-dim)" : "var(--ink-mute)"}}>
                          <b>{spec.label || spec.name}</b>
                          <code style={{fontFamily:"var(--font-mono)", fontSize:11, color:"var(--ink-mute)", marginLeft:6}}>{spec.name}{argHint}</code>
                        </span>
                      </div>
                    );
                  })}
                </>
              )}
              {forbiddenActionsSpecs.length > 0 && (
                <>
                  <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>
                    Forbidden actions ({forbiddenActionsSpecs.length})
                  </div>
                  {forbiddenActionsResult.map((r, i) => {
                    const spec = r.spec || {};
                    return (
                      <div key={i} style={{display:"flex", alignItems:"flex-start", gap:6, padding:"3px 0", fontSize:12}}>
                        <span style={{color: r.violated ? "var(--red, #b91c1c)" : "var(--green, #2e8a53)", flexShrink:0, marginTop:2}}>
                          {r.violated ? (Ico.x ? Ico.x() : "✗") : Ico.check()}
                        </span>
                        <span style={{color: r.violated ? "var(--red, #b91c1c)" : "var(--ink-dim)"}}>
                          <b>{spec.label || spec.name}</b>
                          <code style={{fontFamily:"var(--font-mono)", fontSize:11, color:"var(--ink-mute)", marginLeft:6}}>{spec.name}</code>
                          {r.violated ? " — VIOLATED (you called this)" : " — not called"}
                        </span>
                      </div>
                    );
                  })}
                </>
              )}
            </div>
          )}

          {/* Patient role-brief — what the LLM was acting from. Long, so
              collapse by default; reveal answers questions like "why did
              the patient never mention X?". */}
          {taskInstructions && (
            <div style={{gridColumn:"1 / -1", borderTop:"1px solid var(--line)", paddingTop:14}}>
              <details>
                <summary style={{cursor:"pointer", fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em"}}>
                  Patient role brief (what the LLM was simulating)
                </summary>
                <pre style={{
                  marginTop:8, background:"var(--bg-sunken)", padding:"10px 12px",
                  borderRadius:4, fontSize:11.5, lineHeight:1.5,
                  whiteSpace:"pre-wrap", wordBreak:"break-word",
                }}>{taskInstructions}</pre>
              </details>
            </div>
          )}

          {/* Full evaluator outcome — collapsed by default; for power users
              who want to see exactly what checkOutcomeCorrect returned
              (match_target, match_method, etc.). */}
          {outcome && (
            <div style={{gridColumn:"1 / -1", borderTop:"1px solid var(--line)", paddingTop:14}}>
              <details>
                <summary style={{cursor:"pointer", fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em"}}>
                  Full evaluator outcome (JSON)
                </summary>
                <pre style={{
                  marginTop:8, background:"var(--bg-sunken)", padding:"10px 12px",
                  borderRadius:4, fontSize:11, lineHeight:1.4,
                  whiteSpace:"pre-wrap", wordBreak:"break-word",
                }}>{JSON.stringify(outcome, null, 2)}</pre>
              </details>
            </div>
          )}
        </div>
        <div style={{padding:"10px 18px", borderTop:"1px solid var(--line)", display:"flex", gap:8, justifyContent:"flex-end", alignItems:"center", flexWrap:"wrap"}}>
          {saveState === "error" && saveErr && (
            <span style={{fontSize:11.5, fontFamily:"var(--font-mono)", color:"var(--red, #b91c1c)", marginRight:"auto"}}>
              ⚠ {saveErr}
            </span>
          )}
          {saveState === "saved" && (
            <span style={{fontSize:11.5, fontFamily:"var(--font-mono)", color:"var(--green, #2e8a53)", marginRight:"auto"}}>
              ✓ Trajectory saved
            </span>
          )}
          {onSave && saveState !== "saved" && (
            <button
              className="btn"
              onClick={onSave}
              disabled={saveState === "saving"}
              title="Persist this practice run to the trajectories table"
            >
              {saveState === "saving"
                ? "Saving…"
                : saveState === "error"
                  ? "Retry save"
                  : "Save trajectory"}
            </button>
          )}
          <button className="btn ghost" onClick={onClose}>Close</button>
          <button className="btn primary" onClick={onRunAgain}>{Ico.reset()} Run again</button>
        </div>
      </div>
    </div>
  );
}

window.PracticePage = PracticePage;
