// Source: index.html script block (extracted). Loaded via <script type="text/babel"> in index.html.
function Simulator({ task, setRoute }) {
  const { t: tr } = useLang();
  // Live sims start empty — tau2 flow is: patient opens, clinician responds.
  // Seed data is only shown if the user hasn't kicked off a real sim yet.
  const [messages, setMessages] = React.useState(() => {
    // If the task has a reasonForCall, open with the patient line; else fall back to seed.
    if (task && task.reasonForCall) {
      return [{ idx: 0, role: "user", text: task.reasonForCall, t: nowHMS() }];
    }
    return SEED_SIM;
  });
  const [selected, setSelected] = React.useState(0);
  const [scores, setScores] = React.useState({});
  const [running, setRunning] = React.useState(false);
  const [input, setInput] = React.useState("");
  // Which role the annotator is typing as. "user" = patient (default — matches
  // the stock sim flow). "assistant" = clinician (annotator shows what the
  // clinician *should* have said, useful for RL preference-pair authoring).
  const [composerRole, setComposerRole] = React.useState("user");
  const [cfg, setCfg] = React.useState(() => getLlmConfig());
  React.useEffect(() => {
    const onChange = (e) => setCfg(e.detail || getLlmConfig());
    window.addEventListener("caa-llm-cfg-changed", onChange);
    return () => window.removeEventListener("caa-llm-cfg-changed", onChange);
  }, []);
  const [temp, setTemp] = React.useState(0.3);
  const clampSteps = (n) => Math.max(5, Math.min(200, Math.round(Number(n) || 0)));
  const [maxSteps, setMaxStepsRaw] = React.useState(() => {
    const fromLS = task && task.id ? Number(localStorage.getItem(`caa_sim_max_steps_${task.id}`)) : NaN;
    if (Number.isFinite(fromLS) && fromLS > 0) return clampSteps(fromLS);
    const fromTask = task && typeof task.maxSteps === "number" ? task.maxSteps : null;
    if (fromTask) return clampSteps(fromTask);
    return clampSteps(task && task.maxTurns ? task.maxTurns * 3 : 40);
  });
  const setMaxSteps = (n) => {
    const v = clampSteps(n);
    setMaxStepsRaw(v);
    if (task) {
      try { task.maxSteps = v; } catch {}
      if (task.id) {
        try { localStorage.setItem(`caa_sim_max_steps_${task.id}`, String(v)); } catch {}
      }
    }
  };
  // When the active task changes (component is reused across tasks), re-read
  // the per-task ceiling rather than keeping the prior task's value.
  React.useEffect(() => {
    if (!task) return;
    const fromLS = task.id ? Number(localStorage.getItem(`caa_sim_max_steps_${task.id}`)) : NaN;
    if (Number.isFinite(fromLS) && fromLS > 0) { setMaxStepsRaw(clampSteps(fromLS)); return; }
    if (typeof task.maxSteps === "number") { setMaxStepsRaw(clampSteps(task.maxSteps)); return; }
    setMaxStepsRaw(clampSteps(task.maxTurns ? task.maxTurns * 3 : 40));
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [task?.id]);
  const [showSettings, setShowSettings] = React.useState(false);
  const [error, setError] = React.useState(null);
  const scrollRef = React.useRef(null);
  const abortRef = React.useRef(null);

  const hasKey = !!cfg.apiKey;
  const model = cfg.model;

  const [envState, setEnvState] = React.useState({});
  const [completed, setCompleted] = React.useState(false);
  const [submitted, setSubmitted] = React.useState(false);
  const [toast, setToast] = React.useState(null);
  const [showSubmissions, setShowSubmissions] = React.useState(false);
  const [rollup, setRollup] = React.useState({ clinical_accuracy: null, dialogue_fluency: null, safety_empathy: null });
  const [notes, setNotes] = React.useState("");
  // When the annotator loads a prior submission, we remember its id so subsequent
  // Submit calls upsert (same id) and don't create a duplicate record.
  const [loadedSubmissionId, setLoadedSubmissionId] = React.useState(null);
  const [showLoad, setShowLoad] = React.useState(false);

  // ----- Autosave + restore -----
  // Drafts live in localStorage keyed by task id so reloading/navigating away
  // doesn't lose an in-progress trajectory. Cleared on Reset or Submit.
  const draftKey = task && task.id ? `caa_draft_${task.id}` : null;
  const draftLoadedRef = React.useRef(false);

  // Load draft on mount (once per task id).
  React.useEffect(() => {
    if (!draftKey || draftLoadedRef.current) return;
    draftLoadedRef.current = true;
    try {
      const raw = localStorage.getItem(draftKey);
      if (!raw) return;
      const d = JSON.parse(raw);
      if (d && Array.isArray(d.messages) && d.messages.length > 0) {
        setMessages(d.messages);
        if (d.scores) setScores(d.scores);
        if (d.envState) setEnvState(d.envState);
        if (d.rollup) setRollup(d.rollup);
        if (typeof d.notes === "string") setNotes(d.notes);
        if (d.completed) setCompleted(true);
      }
    } catch {}
  }, [draftKey]);

  // Persist draft whenever the trajectory changes. Debounced via a small
  // timeout so we don't hit localStorage on every token.
  React.useEffect(() => {
    if (!draftKey) return;
    const id = setTimeout(() => {
      try {
        localStorage.setItem(draftKey, JSON.stringify({
          messages, scores, envState, rollup, notes, completed,
          updated_at: new Date().toISOString(),
        }));
      } catch {}
    }, 250);
    return () => clearTimeout(id);
  }, [draftKey, messages, scores, envState, rollup, notes, completed]);

  const setScore = (idx, v) => setScores(s => ({...s, [idx]: s[idx]===v ? undefined : v}));

  const appendMessages = (msgs) => {
    setMessages(prev => {
      const next = [...prev];
      for (const m of msgs) {
        next.push({ ...m, idx: next.length });
      }
      return next;
    });
  };

  // One "agent step" = clinician LLM speaks, possibly calls tools, until it yields.
  const runClinicianNow = async (startMsgs) => {
    if (running || !hasKey) return;
    setError(null);
    setRunning(true);
    const ac = new AbortController();
    abortRef.current = ac;
    try {
      const base = startMsgs || messages;
      const { newMessages, state: nextState, completed: done } = await runOneAgentStep({
        messages: base, task, state: envState, cfg, temperature: temp, signal: ac.signal,
        tools: buildToolsForTask(task),
      });
      appendMessages(newMessages);
      setEnvState(nextState);
      if (done) setCompleted(true);
    } catch (e) {
      if (e.name !== "AbortError") setError(e.message);
    } finally {
      setRunning(false);
    }
  };

  const runPatientNow = async (startMsgs) => {
    if (running || !hasKey) return;
    setError(null);
    setRunning(true);
    const ac = new AbortController();
    abortRef.current = ac;
    try {
      const base = startMsgs || messages;
      const turn = await runOnePatientStep({ messages: base, task, cfg, signal: ac.signal });
      appendMessages([turn]);
    } catch (e) {
      if (e.name !== "AbortError") setError(e.message);
    } finally {
      setRunning(false);
    }
  };

  // Full auto-loop: agent ↔ patient ↔ agent … until record_diagnosis or max_turns.
  const runFullSim = async () => {
    if (running || !hasKey) return;
    setError(null);
    setRunning(true);
    const ac = new AbortController();
    abortRef.current = ac;
    const maxTurns = task.maxTurns || 12;
    try {
      let convo = messages.slice();
      let state = { ...envState };
      let done = false;
      while (!done) {
        if (ac.signal.aborted) break;
        if (convo.length >= maxSteps) {
          setError(`Reached max steps (${maxSteps}) — stopping sim.`);
          break;
        }
        const last = convo[convo.length - 1];
        const lastRole = last?.role;
        if (lastRole === "user" || lastRole === "tool") {
          if (convo.length >= maxSteps) {
            setError(`Reached max steps (${maxSteps}) — stopping sim.`);
            break;
          }
          const res = await runOneAgentStep({
            messages: convo, task, state, cfg, temperature: temp, signal: ac.signal,
            tools: buildToolsForTask(task),
            onProgress: (partial) => {
              // Live-stream the emerging turns into the UI.
              setMessages(prev => {
                const baseLen = convo.length;
                const merged = prev.slice(0, baseLen);
                for (const m of partial) merged.push({ ...m, idx: merged.length });
                return merged;
              });
            },
          });
          convo = [...convo, ...res.newMessages];
          state = res.state;
          done = res.completed;
          if (done) break;
        } else {
          // Assistant just spoke (no tool calls, or finished tool round) — patient turn.
          const patientOnlyAssistant = convo.filter(m => m.role === "assistant" && !m.toolCalls?.length);
          if (patientOnlyAssistant.length >= maxTurns) {
            setError(`Reached max turns (${maxTurns}) without a recorded diagnosis.`);
            break;
          }
          if (convo.length >= maxSteps) {
            setError(`Reached max steps (${maxSteps}) — stopping sim.`);
            break;
          }
          const turn = await runOnePatientStep({ messages: convo, task, cfg, signal: ac.signal });
          convo = [...convo, turn];
          appendMessages([turn]);
        }
      }
      setEnvState(state);
      if (done) setCompleted(true);
    } catch (e) {
      if (e.name !== "AbortError") setError(e.message);
    } finally {
      setRunning(false);
    }
  };

  // Human-typed reply. Annotator picks the role via the composer toggle:
  //   "user"      → add as patient turn, then ask clinician LLM to respond.
  //   "assistant" → add as clinician turn (author an ideal response), then
  //                 ask the patient LLM (if a key is set) to respond.
  const sendTurn = async () => {
    if (!input.trim() || running) return;
    setError(null);
    const role = composerRole;
    const human = { idx: messages.length, role, text: input, t: nowHMS() };
    const after = [...messages, human];
    setMessages(after);
    setInput("");
    if (!hasKey) {
      // Still allow typing offline — just no auto-advance.
      return;
    }
    if (role === "user") {
      await runClinicianNow(after);
    } else {
      await runPatientNow(after);
    }
  };

  const resetSim = () => {
    if (abortRef.current) abortRef.current.abort();
    setRunning(false);
    setError(null);
    setScores({});
    setSelected(0);
    setEnvState({});
    setCompleted(false);
    setSubmitted(false);
    setRollup({ clinical_accuracy: null, dialogue_fluency: null, safety_empathy: null });
    setNotes("");
    setLoadedSubmissionId(null);
    if (draftKey) { try { localStorage.removeItem(draftKey); } catch {} }
    if (task && task.reasonForCall) {
      setMessages([{ idx: 0, role: "user", text: task.reasonForCall, t: nowHMS() }]);
    } else {
      setMessages(SEED_SIM);
    }
  };

  // Apply a submitted trajectory into simulator state. Pass a full record
  // (messages + scores + rollup + ...). Called by the load modal and by
  // the Review panel's "sim" button.
  const loadSubmission = (s) => {
    if (!s) return;
    if (abortRef.current) abortRef.current.abort();
    setRunning(false);
    setError(null);
    // Drop the autosave draft for this task — the loaded submission supersedes it.
    if (draftKey) { try { localStorage.removeItem(draftKey); } catch {} }
    setMessages(Array.isArray(s.messages) ? s.messages : []);
    setScores(s.scores || {});
    setRollup(s.rollup || { clinical_accuracy: null, dialogue_fluency: null, safety_empathy: null });
    setNotes(typeof s.notes === "string" ? s.notes : "");
    setEnvState(s.env_state || {});
    setCompleted(!!s.completed);
    setLoadedSubmissionId(s.id || null);
    setSubmitted(false);
    setShowLoad(false);
    setToast({
      kind: "ok",
      title: "Submission loaded",
      lines: [
        `${s.id ? `id ${s.id}` : "(no id)"} · submitted ${(s.submitted_at || "").slice(0,19).replace("T"," ")}`,
        `Editing annotations here will upsert the same id on next Submit.`,
      ],
    });
    setTimeout(() => setToast(null), 6000);
  };

  // Fetch a full trajectory by id from the server (used when the Review panel
  // only knows the id — e.g. via window.__caaLoadTrajectory).
  const fetchAndLoad = React.useCallback(async (id) => {
    // First try localStorage.
    try {
      const arr = JSON.parse(localStorage.getItem("caa_submitted_trajectories") || "[]");
      const hit = arr.find(x => x.id === id);
      if (hit) { loadSubmission(hit); return; }
    } catch {}
    // Fall back to server.
    try {
      const res = await fetch(`/api/trajectories/${id}`);
      if (res.ok) {
        const full = await res.json();
        loadSubmission(full);
      } else {
        setError(`Could not fetch submission ${id} (HTTP ${res.status}).`);
      }
    } catch (e) {
      setError(`Could not fetch submission ${id}: ${e.message}`);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [draftKey]);

  // Review panel hands us a trajectory id via window.__caaLoadTrajectory.
  // Consume it once on mount, then clear so we don't re-apply on next route.
  React.useEffect(() => {
    const hint = window.__caaLoadTrajectory;
    if (hint && hint.task_id && task && hint.task_id === task.id) {
      if (hint.trajectory) loadSubmission(hint.trajectory);
      else if (hint.id) fetchAndLoad(hint.id);
    }
    window.__caaLoadTrajectory = undefined;
  // Mount only — the Review panel re-sets the hint before navigating.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [task?.id]);

  const submitTrajectory = async () => {
    setError(null);
    setToast(null);

    // Attach annotator identity from login state so the server can index by author.
    let user = null;
    try { user = JSON.parse(localStorage.getItem("caa_user") || "null"); } catch {}

    // If we loaded an existing submission, reuse its id so server upserts in
    // place and localStorage dedupes by id. Otherwise mint a new uuid.
    const trajId = loadedSubmissionId
      || ((typeof crypto !== "undefined" && crypto.randomUUID)
            ? crypto.randomUUID()
            : `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`);

    // Aggregate per-turn scores into a simple reward summary for the trainer.
    const scoreVals = Object.values(scores).filter(v => typeof v === "number");
    const reward_summary = {
      per_turn: scores,
      turns_scored: scoreVals.length,
      reward_sum: scoreVals.reduce((a, b) => a + b, 0),
      reward_mean: scoreVals.length ? scoreVals.reduce((a, b) => a + b, 0) / scoreVals.length : null,
      reward_scale: "0=wrong · 1=partial · 2=correct",
    };

    const final_reward = computeFinalReward({ messages, task });

    const payload = {
      id: trajId,
      task_id: task.id,
      submitted_at: new Date().toISOString(),
      annotator_email: user?.email || null,
      annotator_name: user?.name || null,
      model: cfg.model,
      patient_model: cfg.patientModel || cfg.model,
      temperature: temp,
      completed,
      recorded_diagnosis: envState.recordedDiagnosis || null,
      reference_diagnosis: task.diagnosis || null,
      env_state: envState,
      scores,
      rollup,
      notes,
      final_reward,
      reward_summary,
      messages,
    };

    let jsonText;
    try {
      jsonText = JSON.stringify(payload, null, 2);
    } catch (e) {
      setError(`Could not serialize trajectory: ${e.message}`);
      return;
    }

    const key = "caa_submitted_trajectories";
    let storedCount = 0;
    let storageWarning = null;
    try {
      const arr = JSON.parse(localStorage.getItem(key) || "[]");
      const existingIdx = arr.findIndex(s => s && s.id === trajId);
      if (existingIdx >= 0) arr[existingIdx] = payload;      // upsert by id
      else arr.push(payload);
      const trimmed = arr.slice(-200);
      localStorage.setItem(key, JSON.stringify(trimmed));
      storedCount = trimmed.length;
    } catch (e) {
      storageWarning = `localStorage save failed (${e.name || "quota?"}).`;
    }

    // Server-side persistence is the source of truth. Local cache above is a
    // retry-buffer / offline queue — the Review panel reads from it too.
    // Common "backend not deployed" signals we treat as a failure:
    //   - 404: path genuinely absent on the server
    //   - 501: we're served by python -m http.server (dev), which only supports GET
    //   - non-JSON HTML response: similar — we hit a static host, not CF Pages
    let serverOk = false;
    let serverLine = null;
    let serverId = null;
    try {
      const res = await fetch("/api/trajectories", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: jsonText,
      });
      const ct = res.headers.get("content-type") || "";
      if (res.ok && ct.includes("application/json")) {
        const data = await res.json().catch(() => ({}));
        serverOk = true;
        serverId = data.id || trajId;
        serverLine = `id ${serverId} · annotator ${data.annotator || user?.email || "anonymous"}`;
      } else if (res.status === 404 || res.status === 501 || !ct.includes("application/json")) {
        serverLine = `Server not available (HTTP ${res.status}).`;
      } else {
        const txt = await res.text().catch(() => "");
        serverLine = `Server save failed (HTTP ${res.status}) — ${txt.slice(0, 120)}`;
      }
    } catch (e) {
      serverLine = `Server unreachable (${e.message}).`;
    }

    setSubmitted(true);
    if (serverOk && draftKey) { try { localStorage.removeItem(draftKey); } catch {} }
    if (serverOk) {
      setToast({
        kind: "ok",
        title: "Trajectory saved",
        lines: [
          serverLine,
          `Local cache: ${storedCount} submission${storedCount === 1 ? "" : "s"} under "${key}".`,
          storageWarning,
          `Reward sum ${scoreVals.reduce((a,b)=>a+b,0)} across ${scoreVals.length} turn${scoreVals.length===1?"":"s"}.`,
        ].filter(Boolean),
      });
      setTimeout(() => setToast(null), 9000);
    } else {
      setToast({
        kind: "err",
        title: "Server save failed — trajectory kept locally, retry when online",
        lines: [
          serverLine,
          `Local cache: ${storedCount} submission${storedCount === 1 ? "" : "s"} under "${key}".`,
          storageWarning,
        ].filter(Boolean),
      });
      setTimeout(() => setToast(null), 12000);
    }
  };

  const viewSubmissions = () => {
    try {
      const arr = JSON.parse(localStorage.getItem("caa_submitted_trajectories") || "[]");
      setShowSubmissions(arr);
    } catch (e) {
      setError(`Could not read submissions: ${e.message}`);
    }
  };

  const clearSubmissions = () => {
    if (!confirm("Delete all locally stored trajectory submissions?")) return;
    localStorage.removeItem("caa_submitted_trajectories");
    setShowSubmissions([]);
    setToast({ kind: "ok", title: "Cleared", lines: ["All local submissions removed."] });
    setTimeout(() => setToast(null), 4000);
  };

  const saveSettings = (patch) => {
    const next = setLlmConfig(patch);
    setCfg(next);
    setShowSettings(false);
    setError(null);
  };

  const totalScored = Object.values(scores).filter(v=>v!==undefined).length;
  const sum = Object.values(scores).reduce((a,b)=>a+(b||0),0);
  const avg = totalScored ? (sum/totalScored).toFixed(2) : "—";

  const finalReward = React.useMemo(
    () => computeFinalReward({ messages, task }),
    [messages, envState, task]
  );

  const [ctx, setCtx] = React.useState(false);
  const [ann, setAnn] = React.useState(false);

  return (
    <div className="workspace">
      <div className="ws-head" style={{paddingBottom:0}}>
        <div className="ws-crumbs"><span>{tr("nav_simulate").toLowerCase()}</span> / <b>{task.id}</b> / <span>{tr("sim_crumb")}</span></div>
        <div className="ws-status">
          <button className="btn ghost" onClick={()=>setRoute("editor")}>{tr("sim_back_editor")}</button>
          <button className="btn" onClick={()=>setShowLoad(true)} title="Load a previously-submitted trajectory for this task">
            {Ico.inbox ? Ico.inbox() : "📂"} Load submission
          </button>
          <button className="btn" onClick={resetSim}>{Ico.reset()} {tr("sim_reset")}</button>
          <button className="btn primary" onClick={runFullSim} disabled={running || !hasKey || completed}
            title={!hasKey ? "Set an API key first" : completed ? "Diagnosis already recorded — reset to re-run" : "Run the full agent↔patient↔tools loop until record_diagnosis or max_turns"}>
            {Ico.play()} Run full sim
          </button>
        </div>
      </div>
      <div className="sim-toolbar">
        <div className="model-pill" title={hasKey ? "API key set" : "No API key — click ⚙ Settings"}>
          <span className="live-dot" style={{background: hasKey ? "var(--green, #2e8a53)" : "var(--ink-mute)"}}/>
          <b>clinician:</b> {model}
        </div>
        <div className="model-pill"><b>patient:</b> {cfg.patientModel || cfg.model}</div>
        <span style={{color:"var(--ink-mute)", fontSize:11.5, fontFamily:"var(--font-mono)", display:"inline-flex", alignItems:"center", gap:6}}>
          <span>temp <b style={{color:"var(--ink)"}}>{temp}</b></span>
          <span>·</span>
          <span>turn <b style={{color:"var(--ink)"}}>{messages.filter(m=>m.role!=="tool").length}</b>/{task.maxTurns || 12}</span>
          <span>·</span>
          <span>step <b style={{color:"var(--ink)"}}>{messages.length}</b>/{maxSteps}</span>
          <span>·</span>
          <label style={{display:"inline-flex", alignItems:"center", gap:4}} title="Hard ceiling on total messages (including tool calls) during Run full sim. Clamped to [5, 200].">
            <span>max steps</span>
            <MaxStepsInput value={maxSteps} onCommit={setMaxSteps}/>
          </label>
          {completed && <b style={{color:"var(--green, #2e8a53)", marginLeft:4}}>· completed</b>}
        </span>
        <div style={{marginLeft:"auto", display:"flex", gap:8, alignItems:"center"}}>
          <button className="btn ghost" onClick={()=>setShowSettings(true)} title="LLM endpoint & key">
            ⚙ {hasKey ? new URL(cfg.baseUrl).host : "Settings"}
          </button>
          <button className="btn" onClick={()=>runClinicianNow()} disabled={running || !hasKey || completed} title={completed ? "Consultation already complete" : "One clinician step (may include tool calls)"}>
            {Ico.play()} Clinician step
          </button>
          <button className="btn" onClick={()=>runPatientNow()} disabled={running || !hasKey || completed} title="Generate one patient reply from the persona">
            {Ico.play()} Patient step
          </button>
          {running && (
            <button className="btn" onClick={()=>{ abortRef.current?.abort(); setRunning(false); }}>
              {Ico.pause()} Stop
            </button>
          )}
        </div>
      </div>
      {error && (
        <div style={{padding:"8px 14px", background:"var(--red-bg, #fef2f2)", color:"var(--red, #b91c1c)", fontSize:12.5, fontFamily:"var(--font-mono)", borderBottom:"1px solid var(--line)"}}>
          ⚠ {error}
        </div>
      )}
      {toast && (
        <div style={{padding:"10px 14px", background: toast.kind==="ok" ? "#ecfdf5" : "#fef2f2", color: toast.kind==="ok" ? "#065f46" : "#b91c1c", fontSize:12.5, borderBottom:"1px solid var(--line)"}}>
          <div style={{fontWeight:600, marginBottom:2}}>{toast.kind==="ok" ? "✓" : "⚠"} {toast.title}</div>
          {toast.lines.map((ln, i) => (
            <div key={i} style={{fontFamily:"var(--font-mono)", fontSize:11.5}}>{ln}</div>
          ))}
        </div>
      )}
      {loadedSubmissionId && (
        <div style={{padding:"6px 14px", background:"#eef6ff", color:"#1e40af", fontSize:12, borderBottom:"1px solid var(--line)", display:"flex", alignItems:"center", gap:10}}>
          <span>Reviewing submission <code style={{fontFamily:"var(--font-mono)"}}>{loadedSubmissionId.slice(0,8)}…</code> — edits upsert the same record on Submit.</span>
          <button onClick={() => setLoadedSubmissionId(null)}
            style={{marginLeft:"auto", background:"none", border:0, color:"inherit", cursor:"pointer", textDecoration:"underline", fontSize:11.5}}>
            detach (save as new on next submit)
          </button>
        </div>
      )}
      {showSettings && <SettingsModal cfg={cfg} onSave={saveSettings} onClose={()=>setShowSettings(false)}/>}
      {showSubmissions !== false && <SubmissionsModal list={showSubmissions} onClose={()=>setShowSubmissions(false)} onClear={clearSubmissions}/>}
      {showLoad && <LoadSubmissionModal taskId={task?.id} onPick={loadSubmission} onClose={()=>setShowLoad(false)}/>}

      <div className="sim" data-ctx={ctx?"open":"closed"} data-ann={ann?"open":"closed"}>
        <button className="sim-panel-toggle left" onClick={()=>setCtx(v=>!v)} title="Case context">{ctx?"×":"Ctx"}</button>
        <button className="sim-panel-toggle right" onClick={()=>setAnn(v=>!v)} title="Annotation">{ann?"×":"Ann"}</button>
        <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:17, lineHeight:1.3, marginBottom:10}}>
            {task.age}-year-old {task.gender} with <b>{task.chief.toLowerCase()}</b> × {task.duration}
          </div>
          <div style={{fontSize:12, color:"var(--ink-dim)", marginBottom:14}}>
            Suspected dx: <i>{task.diagnosis}</i>
          </div>

          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"14px 0 6px"}}>Required actions</div>
          {(task.requiredTools.length?task.requiredTools:["get_patient_by_mrn"]).map((a,i)=>{
            const used = messages.some(m => m.role==='tool' && m.name===a);
            return (
              <div key={i} style={{display:"flex", alignItems:"center", gap:6, padding:"4px 0", fontSize:12.5}}>
                <span style={{color: used?"var(--green)":"var(--ink-mute)"}}>{used? Ico.check(): "○"}</span>
                <code style={{fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--ink-dim)"}}>{a}</code>
              </div>
            );
          })}

          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"18px 0 6px"}}>Red-flag checks</div>
          <div style={{display:"flex", alignItems:"center", gap:6, fontSize:12.5, padding:"3px 0"}}>
            <span style={{color:"var(--green)"}}>{Ico.check()}</span><span>No definitive dx given</span>
          </div>
          <div style={{display:"flex", alignItems:"center", gap:6, fontSize:12.5, padding:"3px 0"}}>
            <span style={{color:"var(--green)"}}>{Ico.check()}</span><span>No medication stopped</span>
          </div>
          <div style={{display:"flex", alignItems:"center", gap:6, fontSize:12.5, padding:"3px 0"}}>
            <span style={{color:"var(--amber)"}}>{Ico.warn()}</span><span>Allergies not verified</span>
          </div>
        </div>

        <div className="sim-center">
          <div className="transcript" ref={scrollRef}>
            {messages.map((m,i)=>(
              <div key={i} className="turn" data-selected={selected===m.idx} onClick={()=>setSelected(m.idx)}>
                <div className="turn-role">
                  <b>{m.role}</b>
                  <div><span className="turn-idx">#{String(m.idx).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}}>returned in 284ms</span>
                    </div>
                    <div className="bubble-tool-body">
                      <div><span className="key">args</span> {JSON.stringify(m.args)}</div>
                      <div><span className="key">ret </span><span className="ret">{JSON.stringify(m.returns)}</span></div>
                    </div>
                    <TurnScorer idx={m.idx} v={scores[m.idx]} set={setScore}/>
                  </div>
                ) : (
                  <div className="bubble" data-role={m.role}>
                    <span style={{fontSize:13.5, lineHeight:1.5}}>{m.text}</span>
                    <TurnScorer idx={m.idx} v={scores[m.idx]} set={setScore}/>
                  </div>
                )}
              </div>
            ))}
            {running && (
              <div className="turn">
                <div className="turn-role"><b>assistant</b><div><span className="turn-idx">#{String(messages.length).padStart(2,'0')}</span>···</div></div>
                <div className="bubble" style={{display:"flex",gap:6, alignItems:"center", color:"var(--ink-mute)", fontStyle:"italic"}}>
                  <Dots/> generating response
                </div>
              </div>
            )}
          </div>

          <div className="composer">
            <div className="composer-persona">
              <span>Reply as</span>
              <span
                role="button"
                tabIndex={0}
                className="persona-chip"
                onClick={() => setComposerRole(r => r === "user" ? "assistant" : "user")}
                onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setComposerRole(r => r === "user" ? "assistant" : "user"); } }}
                style={{cursor:"pointer", userSelect:"none"}}
                title="Click to switch role"
              >
                {composerRole === "user" ? "patient persona" : "clinician"} ⇄
              </span>
              <span style={{color:"var(--ink-mute)"}}>
                {composerRole === "user"
                  ? "· clinician LLM will respond"
                  : "· patient LLM will respond (auto)"}
              </span>
            </div>
            <div className="composer-row">
              <textarea className="composer-input"
                placeholder={
                  !hasKey
                    ? "Set an API key in ⚙ Settings to run real simulations."
                    : composerRole === "user"
                      ? "Type a patient reply — clinician LLM will respond."
                      : "Author an ideal clinician reply — patient LLM will respond."
                }
                value={input} onChange={e=>setInput(e.target.value)}
                onKeyDown={e => { if (e.key==='Enter' && !e.shiftKey) { e.preventDefault(); sendTurn(); } }}
              />
              <button className="btn primary" onClick={sendTurn} disabled={running || !input.trim()}>
                {Ico.send()} Send as {composerRole === "user" ? "patient" : "clinician"} <span className="kbd">⏎</span>
              </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}}>Trajectory annotation</div>

          <FinalRewardCard
            finalReward={finalReward}
            perTurnAvg={avg}
            turnsScored={totalScored}
            turnsTotal={messages.length}
          />

          {selected != null && messages[selected] && (
            <AnnotGroup title={`Turn #${String(selected).padStart(2,'0')} · ${messages[selected].role}`}>
              <div style={{fontSize:12, color:"var(--ink-dim)", marginBottom:8, fontFamily:"var(--font-mono)"}}>
                {messages[selected].role === 'tool' ? messages[selected].name+"(…)" : (messages[selected].text||"").slice(0,80)+"…"}
              </div>
              <div style={{display:"flex", gap:6, marginBottom:10}}>
                {[{v:2,l:"Correct"},{v:1,l:"Partial"},{v:0,l:"Wrong"}].map(o=>(
                  <button key={o.v} className="score-btn" data-v={o.v} data-active={scores[selected]===o.v} onClick={()=>setScore(selected,o.v)} style={{flex:1, height:30}}>
                    {o.l}
                  </button>
                ))}
              </div>
              <div style={{display:"flex", flexWrap:"wrap", gap:5, marginBottom:10}}>
                {["premature-dx","missed-red-flag","wrong-tool","good-empathy","thorough-hpi","safety-check"].map(t=>(
                  <button key={t} className="filter-pill">{t}</button>
                ))}
              </div>
              <textarea className="ta" rows={3}
                placeholder="Note for the RL team…"
                value={notes}
                onChange={e=>setNotes(e.target.value)} />
            </AnnotGroup>
          )}

          <AnnotGroup title="Rollup (full trajectory)">
            <Pips label="Clinical accuracy" val={rollup.clinical_accuracy}
              onChange={v=>setRollup(r=>({...r, clinical_accuracy:v}))}/>
            <Pips label="Dialogue fluency" val={rollup.dialogue_fluency}
              onChange={v=>setRollup(r=>({...r, dialogue_fluency:v}))}/>
            <Pips label="Safety & empathy" val={rollup.safety_empathy}
              onChange={v=>setRollup(r=>({...r, safety_empathy:v}))}/>
          </AnnotGroup>

          <button className="btn accent"
            style={{justifyContent:"center", width:"100%"}}
            onClick={submitTrajectory}
            disabled={messages.length < 2}
            title={messages.length < 2 ? "Run at least one turn before submitting" : "Save trajectory to the backend"}
          >
            {Ico.check()} {submitted ? "Submit again" : "Submit trajectory to RL dataset"}
          </button>
          <div style={{fontSize:11, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textAlign:"center", marginTop:6, display:"flex", justifyContent:"center", gap:8}}>
            <button onClick={viewSubmissions}
              style={{background:"none", border:0, padding:0, color:"var(--ink-dim)", cursor:"pointer", textDecoration:"underline"}}>
              view submissions
            </button>
            <span>·</span>
            <span>{completed ? "consultation complete" : "saves to backend"}</span>
          </div>
        </div>
      </div>
    </div>
  );
}

function FinalRewardCard({ finalReward, perTurnAvg, turnsScored, turnsTotal }) {
  const pass = !!(finalReward && finalReward.binary_pass);
  const cl = (finalReward && finalReward.checklist) || {};
  const tip = (finalReward && finalReward.comments) || "";
  const passColor = "var(--green, #2e8a53)";
  const failColor = "var(--red, #b91c1c)";
  const glyph = (ok) => (
    <span style={{color: ok ? passColor : failColor, fontFamily: "var(--font-mono)"}}>
      {ok ? (Ico.check ? Ico.check() : "yes") : (Ico.x ? Ico.x() : "no")}
    </span>
  );
  const fmt = (v) => (typeof v === "number" ? v.toFixed(2) : "—");
  return (
    <div className="overall" style={{flexDirection:"column", alignItems:"stretch"}} title={tip}>
      <div style={{display:"flex", alignItems:"baseline", gap:10}}>
        <span
          className="overall-score"
          style={{color: pass ? passColor : failColor, letterSpacing: "0.05em"}}
        >
          {pass ? "PASS" : "FAIL"}
        </span>
        <span className="overall-label">
          Final reward<br/>
          <span style={{color:"var(--ink-mute)"}}>rule-based · tau2</span>
        </span>
      </div>
      <div className="final-reward-breakdown">
        <div className="final-reward-row" title="record_diagnosis or record_differential contains the expected disease">
          <span className="frr-label">outcome_correct</span>
          <span className="frr-score">{glyph(!!cl.outcome_correct)}</span>
        </div>
        <div className="final-reward-row" title="No red-flag text, and any prescribe_medication was preceded by a safety tool">
          <span className="frr-label">safety_passed</span>
          <span className="frr-score">{glyph(!!cl.safety_passed)}</span>
        </div>
        <div
          className="final-reward-row"
          title={
            cl.forbidden_tools_respected === false
              ? ("Called: " + (((finalReward && finalReward.info && finalReward.info.forbidden && finalReward.info.forbidden.violations) || []).join(", ")))
              : "No forbidden tool was invoked"
          }
        >
          <span className="frr-label">forbidden_tools</span>
          <span className="frr-score">{glyph(cl.forbidden_tools_respected !== false)}</span>
        </div>
        <div className="final-reward-row" title="READ tools called before the first WRITE tool">
          <span className="frr-label">info_gathering</span>
          <span className="frr-score">{fmt(cl.info_gathering)}</span>
        </div>
        <div className="final-reward-row" title="Fraction of communicate_info keywords present in assistant text">
          <span className="frr-label">communication</span>
          <span className="frr-score">{fmt(cl.communication)}</span>
        </div>
        <div className="final-reward-secondary">
          <span>human per-action avg {perTurnAvg}/2.00</span>
          <span>{turnsScored}/{turnsTotal} turns scored</span>
        </div>
      </div>
    </div>
  );
}

function MaxStepsInput({ value, onCommit }) {
  const [buf, setBuf] = React.useState(String(value));
  React.useEffect(() => { setBuf(String(value)); }, [value]);
  return (
    <input
      type="number"
      min={5}
      max={200}
      value={buf}
      onChange={e => setBuf(e.target.value)}
      onBlur={() => onCommit(buf)}
      onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); onCommit(buf); e.currentTarget.blur(); } }}
      style={{width:56, padding:"2px 4px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:11.5, color:"var(--ink)"}}
    />
  );
}

function TurnScorer({ idx, v, set }) {
  return (
    <div className="turn-score" onClick={e=>e.stopPropagation()}>
      {[2,1,0].map(s=>(
        <button key={s} className="score-btn" data-v={s} data-active={v===s} onClick={()=>set(idx,s)} title={s===2?"Correct":s===1?"Partial":"Wrong"}>
          {s}
        </button>
      ))}
    </div>
  );
}

function Dots() {
  return (<span style={{display:"inline-flex",gap:3}}>
    <span className="dot-a"/><span className="dot-a" style={{animationDelay:".15s"}}/><span className="dot-a" style={{animationDelay:".3s"}}/>
    <style>{`.dot-a{width:5px;height:5px;border-radius:50%;background:var(--ink-mute);animation:bop 1s infinite}@keyframes bop{0%,80%,100%{opacity:.3;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}`}</style>
  </span>);
}

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

function SettingsModal({ cfg, onSave, onClose }) {
  const [baseUrl, setBaseUrl] = React.useState(cfg.baseUrl);
  const [apiKey, setApiKey] = React.useState(cfg.apiKey);
  const [model, setModel] = React.useState(cfg.model);
  const [patientModel, setPatientModel] = React.useState(cfg.patientModel);
  const [autoPatient, setAutoPatient] = React.useState(!!cfg.autoPatient);
  const [azureApiVersion, setAzureApiVersion] = React.useState(cfg.azureApiVersion || "2024-10-21");
  const [showKey, setShowKey] = React.useState(false);
  const isAzure = isAzureOpenAI(baseUrl);
  const save = () => onSave({
    baseUrl: baseUrl.trim(),
    apiKey: apiKey.trim(),
    model: model.trim(),
    patientModel: patientModel.trim(),
    autoPatient,
    azureApiVersion: azureApiVersion.trim() || "2024-10-21",
  });
  return (
    <div onClick={onClose} style={{position:"fixed", inset:0, background:"rgba(0,0,0,.4)", zIndex:50, display:"flex", alignItems:"center", justifyContent:"center"}}>
      <div onClick={e=>e.stopPropagation()} style={{background:"var(--bg, #fff)", border:"1px solid var(--line)", borderRadius:6, width:520, maxWidth:"92vw", padding:20, fontSize:13}}>
        <div style={{display:"flex", alignItems:"center", marginBottom:14}}>
          <h3 style={{margin:0, flex:1, fontSize:15}}>LLM settings</h3>
          <button className="btn ghost" onClick={onClose} style={{padding:"2px 8px"}}>×</button>
        </div>
        <div style={{fontSize:12, color:"var(--ink-dim)", marginBottom:14}}>
          OpenAI-compatible endpoint. Key is stored in browser localStorage on this device only — never sent anywhere except the endpoint you set.
        </div>
        <label style={{display:"block", marginBottom:10}}>
          <div style={{fontSize:11, color:"var(--ink-mute)", marginBottom:3, fontFamily:"var(--font-mono)"}}>BASE URL</div>
          <input value={baseUrl} onChange={e=>setBaseUrl(e.target.value)} placeholder="https://api.openai.com/v1"
            style={{width:"100%", padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:12}}/>
          <div style={{fontSize:10.5, color:"var(--ink-mute)", marginTop:3}}>
            Examples: <code>https://api.openai.com/v1</code> · <code>https://openrouter.ai/api/v1</code> · <code>https://api.groq.com/openai/v1</code>
            <br/>Azure: <code>https://&lt;resource&gt;.openai.azure.com</code> (no <code>/v1</code>; set deployment name as the model)
          </div>
        </label>
        {isAzure && (
          <label style={{display:"block", marginBottom:10}}>
            <div style={{fontSize:11, color:"var(--ink-mute)", marginBottom:3, fontFamily:"var(--font-mono)"}}>AZURE API VERSION</div>
            <input value={azureApiVersion} onChange={e=>setAzureApiVersion(e.target.value)} placeholder="2024-10-21"
              style={{width:"100%", padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:12}}/>
            <div style={{fontSize:10.5, color:"var(--ink-mute)", marginTop:3}}>
              Must match an API version supported by your Azure resource. <code>2024-10-21</code> supports chat + tool calling.
            </div>
          </label>
        )}
        <label style={{display:"block", marginBottom:10}}>
          <div style={{fontSize:11, color:"var(--ink-mute)", marginBottom:3, fontFamily:"var(--font-mono)"}}>API KEY</div>
          <div style={{display:"flex", gap:6}}>
            <input type={showKey?"text":"password"} value={apiKey} onChange={e=>setApiKey(e.target.value)} placeholder="sk-..."
              style={{flex:1, padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:12}}/>
            <button type="button" className="btn ghost" onClick={()=>setShowKey(v=>!v)} style={{padding:"2px 10px", fontSize:11}}>
              {showKey?"hide":"show"}
            </button>
          </div>
        </label>
        <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginBottom:10}}>
          <label>
            <div style={{fontSize:11, color:"var(--ink-mute)", marginBottom:3, fontFamily:"var(--font-mono)"}}>{isAzure ? "CLINICIAN DEPLOYMENT" : "CLINICIAN MODEL"}</div>
            <input value={model} onChange={e=>setModel(e.target.value)}
              placeholder={isAzure ? "e.g. gpt-4o (your deployment name)" : "gpt-4o-mini"}
              style={{width:"100%", padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:12}}/>
          </label>
          <label>
            <div style={{fontSize:11, color:"var(--ink-mute)", marginBottom:3, fontFamily:"var(--font-mono)"}}>{isAzure ? "PATIENT DEPLOYMENT" : "PATIENT MODEL"}</div>
            <input value={patientModel} onChange={e=>setPatientModel(e.target.value)}
              placeholder={isAzure ? "e.g. gpt-4o-mini (your deployment name)" : "gpt-4o-mini"}
              style={{width:"100%", padding:"6px 8px", border:"1px solid var(--line)", borderRadius:4, fontFamily:"var(--font-mono)", fontSize:12}}/>
          </label>
        </div>
        <label style={{display:"flex", alignItems:"center", gap:8, marginBottom:16, fontSize:12.5}}>
          <input type="checkbox" checked={autoPatient} onChange={e=>setAutoPatient(e.target.checked)}/>
          <span>Auto-generate patient replies with LLM (using task persona)</span>
        </label>
        <div style={{display:"flex", justifyContent:"flex-end", gap:8}}>
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={save}>Save</button>
        </div>
      </div>
    </div>
  );
}

function SubmissionsModal({ list, onClose, onClear }) {
  const items = Array.isArray(list) ? list : [];
  return (
    <div onClick={onClose} style={{position:"fixed", inset:0, background:"rgba(0,0,0,.4)", zIndex:50, display:"flex", alignItems:"center", justifyContent:"center"}}>
      <div onClick={e=>e.stopPropagation()} style={{background:"var(--bg, #fff)", border:"1px solid var(--line)", borderRadius:6, width:720, maxWidth:"92vw", maxHeight:"80vh", padding:20, fontSize:13, display:"flex", flexDirection:"column"}}>
        <div style={{display:"flex", alignItems:"center", marginBottom:10}}>
          <h3 style={{margin:0, flex:1, fontSize:15}}>Submitted trajectories</h3>
          <span style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginRight:12}}>
            localStorage["caa_submitted_trajectories"] — {items.length} item{items.length===1?"":"s"}
          </span>
          <button className="btn ghost" onClick={onClose} style={{padding:"2px 8px"}}>×</button>
        </div>
        <div style={{flex:1, overflowY:"auto", border:"1px solid var(--line)", borderRadius:4}}>
          {items.length === 0 ? (
            <div style={{padding:20, color:"var(--ink-mute)", fontSize:12.5, textAlign:"center"}}>No trajectories saved yet.</div>
          ) : items.slice().reverse().map((s, i) => (
            <div key={i} style={{padding:"8px 12px", borderBottom:"1px solid var(--line)", fontFamily:"var(--font-mono)", fontSize:11.5, display:"grid", gridTemplateColumns:"160px 1fr auto", gap:10, alignItems:"center"}}>
              <span style={{color:"var(--ink-mute)"}}>{(s.submitted_at||"").slice(0,19).replace("T"," ")}</span>
              <span>
                {s.task_id}
                {s.completed && <b style={{color:"#065f46", marginLeft:8}}>✓ dx</b>}
                <span style={{color:"var(--ink-mute)", marginLeft:8}}>· {s.messages?.length||0} msgs</span>
              </span>
              <button className="btn ghost" style={{padding:"2px 8px", fontSize:11}}
                onClick={()=>downloadBlob(`trajectory_${s.task_id}_${new Date(s.submitted_at).getTime()}.json`, JSON.stringify(s, null, 2))}>
                ⇓ re-download
              </button>
            </div>
          ))}
        </div>
        <div style={{display:"flex", justifyContent:"space-between", marginTop:12}}>
          <button className="btn ghost" onClick={onClear} style={{color:"var(--red, #b91c1c)"}} disabled={items.length===0}>
            Clear all
          </button>
          <button className="btn" onClick={()=>{
            if (!items.length) return;
            const lines = items.map(x => JSON.stringify(x)).join("\n");
            downloadBlob(`trajectories_bulk_${Date.now()}.jsonl`, lines, "application/x-ndjson");
          }} disabled={items.length===0}>
            Download all as JSONL
          </button>
        </div>
      </div>
    </div>
  );
}

function LoadSubmissionModal({ taskId, onPick, onClose }) {
  const [local, setLocal] = React.useState([]);
  const [server, setServer] = React.useState([]);
  const [serverStatus, setServerStatus] = React.useState("checking");
  const [loadingId, setLoadingId] = React.useState(null);

  React.useEffect(() => {
    try {
      const all = JSON.parse(localStorage.getItem("caa_submitted_trajectories") || "[]");
      setLocal(Array.isArray(all) ? all.filter(s => s.task_id === taskId) : []);
    } catch { setLocal([]); }

    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(`/api/trajectories?task_id=${encodeURIComponent(taskId)}&limit=100`);
        const ct = res.headers.get("content-type") || "";
        if (!res.ok || !ct.includes("application/json")) {
          if (!cancelled) setServerStatus("unavailable");
          return;
        }
        const data = await res.json();
        if (!cancelled) {
          setServer(Array.isArray(data.items) ? data.items : []);
          setServerStatus("ok");
        }
      } catch {
        if (!cancelled) setServerStatus("unavailable");
      }
    })();
    return () => { cancelled = true; };
  }, [taskId]);

  // Merge by id, local preferred (has full messages inline).
  const merged = React.useMemo(() => {
    const byId = new Map();
    for (const s of server) byId.set(s.id, { ...s, _source: "server" });
    for (const l of local) {
      const prior = byId.get(l.id);
      byId.set(l.id, { ...(prior || {}), ...l, _source: prior ? "both" : "local" });
    }
    return Array.from(byId.values()).sort((a, b) => (b.submitted_at || "").localeCompare(a.submitted_at || ""));
  }, [local, server]);

  const pick = async (s) => {
    // If we only have the server summary, fetch the full record.
    if (!Array.isArray(s.messages) && s.id && (s._source === "server")) {
      setLoadingId(s.id);
      try {
        const res = await fetch(`/api/trajectories/${s.id}`);
        if (res.ok) {
          const full = await res.json();
          onPick(full);
          return;
        }
      } catch {}
      setLoadingId(null);
    }
    onPick(s);
  };

  return (
    <div onClick={onClose} style={{position:"fixed", inset:0, background:"rgba(0,0,0,.4)", zIndex:50, display:"flex", alignItems:"center", justifyContent:"center"}}>
      <div onClick={e=>e.stopPropagation()} style={{background:"var(--bg, #fff)", border:"1px solid var(--line)", borderRadius:6, width:680, maxWidth:"92vw", maxHeight:"80vh", padding:20, fontSize:13, display:"flex", flexDirection:"column"}}>
        <div style={{display:"flex", alignItems:"center", marginBottom:10}}>
          <div style={{flex:1}}>
            <h3 style={{margin:"0 0 2px", fontSize:15}}>Load a submission</h3>
            <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)"}}>
              task <code>{taskId}</code> · local {local.length} · server {serverStatus === "ok" ? server.length : serverStatus}
            </div>
          </div>
          <button className="btn ghost" onClick={onClose} style={{padding:"2px 8px"}}>×</button>
        </div>
        <div style={{flex:1, overflowY:"auto", border:"1px solid var(--line)", borderRadius:4}}>
          {merged.length === 0 ? (
            <div style={{padding:20, color:"var(--ink-mute)", fontSize:12.5, textAlign:"center"}}>
              No submissions found for this task.<br/>
              {serverStatus === "unavailable" && <span style={{fontSize:11}}>(Server backend not available — only local submissions shown.)</span>}
            </div>
          ) : merged.map(s => (
            <div key={s.id || s.submitted_at} style={{padding:"8px 12px", borderBottom:"1px solid var(--line)", display:"grid", gridTemplateColumns:"150px 1fr auto auto", gap:10, alignItems:"center", fontSize:12}}>
              <span style={{fontFamily:"var(--font-mono)", fontSize:11, color:"var(--ink-mute)"}}>
                {(s.submitted_at || "").slice(0,19).replace("T"," ")}
              </span>
              <span style={{minWidth:0, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>
                <span>{s.annotator_name || s.annotator_email || "anonymous"}</span>
                {s.completed && <b style={{color:"#065f46", marginLeft:8}}>✓ dx</b>}
                {s.reward_summary?.reward_sum != null && (
                  <span style={{color:"var(--ink-mute)", marginLeft:8}}>
                    · reward {s.reward_summary.reward_sum}/{s.reward_summary.turns_scored}
                  </span>
                )}
                <span style={{color:"var(--ink-mute)", marginLeft:8, fontFamily:"var(--font-mono)", fontSize:10.5}}>
                  [{s._source}]
                </span>
              </span>
              <span style={{fontFamily:"var(--font-mono)", fontSize:11, color:"var(--ink-dim)"}}>
                {s.model || "—"}
              </span>
              <button className="btn primary" onClick={() => pick(s)}
                disabled={loadingId === s.id}
                style={{padding:"3px 10px", fontSize:11}}>
                {loadingId === s.id ? "loading…" : "Load →"}
              </button>
            </div>
          ))}
        </div>
        <div style={{fontSize:11, color:"var(--ink-mute)", marginTop:10, fontFamily:"var(--font-mono)"}}>
          Loading overwrites the current transcript + annotations for this task.
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { Simulator, SettingsModal, SubmissionsModal, LoadSubmissionModal });