/* jedai-decodable.jsx — Decodable Passages workspace.
   Wires controls → server-side agent pipeline → validator report → preview. */

(function () {
  const {
    SCOPE_AND_SEQUENCE, PASSAGE_TEMPLATES, GRADE_LEVELS, DEFAULT_SPEC,
    graphemesThroughWeek, newGraphemesAtWeek, normalizeWordList,
    newJob, appendTrace, makeId
  } = window.JEDAI_DATA;
  const { Icon, Mascot } = window.JEDAI_ICONS;

  /* Icon picker for trace steps. The single-loop agent emits dynamic stage
     names (`agent`, `turn-1`, `turn-2`, …); pick an icon based on what the
     stage actually did. */
  function iconForStage(stage, info) {
    if (stage === "agent")                          return "agent_done";
    if (typeof info === "string" && info.includes("validate_passage"))  return "agent_validator";
    if (typeof info === "string" && info.includes("finalize"))          return "agent_editor";
    if (typeof info === "string" && info.includes("get_scope"))         return "agent_planner";
    if (stage.startsWith("turn-"))                   return "agent_generator";
    return "cog";
  }

  function prettyStageName(stage) {
    if (stage === "agent") return "Agent loop";
    if (stage.startsWith("turn-")) return "Turn " + stage.slice(5);
    return stage;
  }

  /* ----- Subcomponents ----- */

  const GEN_MESSAGES = [
    "Crafting your story...",
    "Picking the perfect words...",
    "Building sentences with care...",
    "Making reading fun...",
    "Checking every phonics rule...",
    "Polishing the passage...",
    "Adding a dash of magic...",
    "Almost there...",
  ];

  function GeneratingAnimation({ title }) {
    const [msgIdx, setMsgIdx] = React.useState(0);
    React.useEffect(() => {
      const id = setInterval(() => setMsgIdx((i) => (i + 1) % GEN_MESSAGES.length), 3000);
      return () => clearInterval(id);
    }, []);

    const letters = (title || "Reading is fun").split("");

    return (
      <div className="gen-anim">
        <div className="gen-anim-rainbow"/>
        <div className="gen-anim-content">
          <div className="gen-anim-pencil">
            <svg viewBox="0 0 64 64" width="72" height="72">
              <g className="gen-pencil-bob">
                <rect x="22" y="8" width="20" height="46" rx="3" fill="#FFD54F" stroke="#333" strokeWidth="2"/>
                <polygon points="22,54 42,54 32,64" fill="#FFB74D" stroke="#333" strokeWidth="2"/>
                <rect x="22" y="8" width="20" height="8" rx="2" fill="#EF5350" stroke="#333" strokeWidth="2"/>
                <line x1="32" y1="54" x2="32" y2="62" stroke="#333" strokeWidth="1.5"/>
              </g>
            </svg>
          </div>
          <div className="gen-anim-letters" aria-hidden="true">
            {letters.map((ch, i) => (
              <span key={i} className="gen-letter" style={{ animationDelay: `${i * 0.12}s` }}>
                {ch === " " ? " " : ch}
              </span>
            ))}
          </div>
          <div className="gen-anim-msg" key={msgIdx}>{GEN_MESSAGES[msgIdx]}</div>
          <div className="gen-anim-dots">
            <span className="gen-dot"/>
            <span className="gen-dot"/>
            <span className="gen-dot"/>
          </div>
        </div>
      </div>
    );
  }

  function ScopeChips({ week, grade }) {
    const scopeGrade = window.JEDAI_DATA.scopeGradeForLevel(grade || "1");
    const all = Array.from(graphemesThroughWeek(week, scopeGrade)).sort();
    const isNew = new Set(newGraphemesAtWeek(week, scopeGrade).map((u) => u.g.toLowerCase()));
    return (
      <div className="scope-chips">
        {all.map((g) => (
          <span key={g} className={"scope-chip" + (isNew.has(g) ? " new" : "")}>
            {g}
          </span>
        ))}
      </div>
    );
  }

  function AgentTrace({ trace, showAgentTrace }) {
    if (!showAgentTrace) return null;
    const steps = Array.isArray(trace) ? trace : [];
    if (!steps.length) {
      return (
        <div className="agent-trace">
          <h3>{Icon.cog()}Agent trace</h3>
          <div style={{ fontSize: 13, color: "var(--ink-soft)", fontWeight: 700 }}>
            No turns recorded yet.
          </div>
        </div>
      );
    }
    return (
      <div className="agent-trace">
        <h3>{Icon.cog()}Agent trace · single-loop</h3>
        <div className="trace-steps">
          {steps.map((step, idx) => {
            const status = step.status === "active" ? "active" :
              step.status === "done"   ? "done"   :
              step.status === "failed" ? "failed" : step.status;
            const klass = "trace-step " + (status || "");
            const took = step.finishedAt ? `${Math.round((step.finishedAt - step.startedAt)/100)/10}s` : "";
            const iconName = iconForStage(step.stage, step.info);
            return (
              <div key={idx + "-" + step.stage} className={klass}>
                <div className="ts-icon">{Icon[iconName] ? Icon[iconName]() : Icon.cog()}</div>
                <div className="ts-body">
                  <div className="ts-name">{prettyStageName(step.stage)}</div>
                  <div className="ts-sub">{step.info || ""}</div>
                </div>
                <div className="ts-time">{took || (status === "active" ? "…" : "")}</div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  function ValidatorReport({ report }) {
    if (!report) return null;
    const cls = report.passed ? "pass" : "fail";
    return (
      <div className="validator-report">
        <h3>
          {Icon.agent_validator()}Validator
          <span className={"badge-pill " + cls}>{report.passed ? "Pass" : "Fail"}</span>
        </h3>
        <ul className="validator-list">
          {report.checks.map((c) => (
            <li key={c.id} className={c.pass ? "ok" : "fail"}>
              <span className="v-mark">{c.pass ? "✓" : "!"}</span>
              <div>
                <div>{c.label}</div>
                {c.detail && <div className="v-detail">{c.detail}</div>}
              </div>
            </li>
          ))}
        </ul>
        {report.stats && (
          <div style={{ marginTop: 10, fontSize: 12, color: "var(--ink-soft)", fontWeight: 700 }}>
            {report.stats.totalWords} words · {report.stats.distinctTokens} distinct · sight words {report.stats.sightCount} · decodability {report.stats.decodabilityPct}%
          </div>
        )}
      </div>
    );
  }

  function buildProvenanceText(job) {
    const p = job.provenance || {};
    const stages = Array.isArray(p.stages) ? p.stages : [];
    const lines = [];
    lines.push("=== JEDAI Tools — Provenance Record ===");
    lines.push(`Title: ${job.title || "(untitled)"}`);
    lines.push(`Job ID: ${job.id}`);
    lines.push(`Generated: ${p.startedAt || job.createdAt || "(unknown)"}`);
    if (p.finishedAt) lines.push(`Finished: ${p.finishedAt}`);
    lines.push(`Spec: week ${job.spec?.week}, grade ${job.spec?.grade}, ~${job.spec?.wordCount} words, sentences ≤ ${job.spec?.maxSentenceLen}`);
    if (Array.isArray(job.spec?.targetWords) && job.spec.targetWords.length) {
      lines.push(`Target words: ${job.spec.targetWords.join(", ")}`);
    }
    lines.push("");
    lines.push("Pipeline stages:");
    for (const s of stages) {
      lines.push(`  • ${s.stage} → ${s.model} [${s.license}${s.sellable ? "" : " — TOS-restricted"}]`);
    }
    if (p.summary) {
      lines.push("");
      lines.push(p.summary);
    }
    if (job.validator) {
      lines.push("");
      lines.push(`Validator: ${job.validator.passed ? "PASS" : "FAIL"} — ${job.validator.checks.filter((c) => c.pass).length}/${job.validator.checks.length} checks pass, ${job.validator.stats?.decodabilityPct}% decodable`);
    }
    lines.push("");
    lines.push("Generated by JEDAI Tools (Just Educational AI Tools). Validator-checked.");
    lines.push("Outputs are owned by the operator per the listed model licenses.");
    return lines.join("\n");
  }

  function PassageCard({ job, spec, tweaks }) {
    const text = job.finalArtifact || job.draft || "";
    const scopeGrade = window.JEDAI_DATA.scopeGradeForLevel(spec.grade || "1");
    const cumulativeScope = window.JEDAI_DATA.getCumulativeScope(scopeGrade, spec.week);
    const annotated = (text && window.JEDAI_VALIDATOR)
      ? window.JEDAI_VALIDATOR.annotatePassage(text, { ...spec, targetWords: normalizeWordList(spec.targetWords) }, cumulativeScope)
      : null;
    const chunks = annotated?.chunks;
    const validator = job.validator || annotated?.result;
    const cls = validator ? (validator.passed ? "ok" : "fail") : "";
    const provenance = job.provenance || {};
    const sellable = provenance.sellable !== false && (provenance.stages || []).length > 0;

    function exportText() {
      const provText = buildProvenanceText(job);
      const body = [
        job.title ? job.title : "",
        "",
        text,
        "",
        "----",
        provText
      ].filter(Boolean).join("\n");
      const blob = new Blob([body], { type: "text/plain" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = (job.title || "passage").replace(/[^a-z0-9-_]+/gi, "-").toLowerCase() + ".txt";
      a.click();
      URL.revokeObjectURL(url);
    }

    function exportProvenance() {
      const blob = new Blob([JSON.stringify({
        app: "JEDAI Tools",
        version: provenance.appVersion || "0.1.0",
        jobId: job.id,
        title: job.title,
        spec: job.spec,
        validator: job.validator,
        provenance,
        artifact: text
      }, null, 2)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = (job.title || "passage").replace(/[^a-z0-9-_]+/gi, "-").toLowerCase() + ".provenance.json";
      a.click();
      URL.revokeObjectURL(url);
    }

    async function copyText() {
      try { await navigator.clipboard.writeText(text); } catch (e) {}
    }

    return (
      <div className="passage-card">
        <div className="pc-title">{job.title || (spec.title || "Untitled passage")}</div>
        <div className="pc-meta">
          <span className="meta-pill">Week {spec.week}</span>
          <span className="meta-pill">Grade {spec.grade}</span>
          <span className="meta-pill">{PASSAGE_TEMPLATES.find((p) => p.id === spec.template)?.label || spec.template}</span>
          {validator && <span className={"meta-pill " + cls}>{validator.passed ? "Validated" : "Failed checks"}</span>}
          {validator?.stats && <span className="meta-pill">{validator.stats.decodabilityPct}% decodable</span>}
          {provenance.stages?.length > 0 && (
            <span className={"meta-pill " + (sellable ? "ok" : "warn")}>
              {sellable ? "Sellable" : "TOS-restricted"}
            </span>
          )}
          {Array.isArray(provenance.ccssTags) && provenance.ccssTags.map((tag) => (
            <span key={tag} className="meta-pill ccss" title={`CCSS standard ${tag}`}>{tag}</span>
          ))}
        </div>
        <div className="passage-body">
          {chunks
            ? chunks.map((chunk, i) => {
                if (chunk.kind === "plain") return chunk.text;
                return <span key={i} className={chunk.kind}>{chunk.text}</span>;
              })
            : text}
        </div>
        <div className="passage-actions">
          <button className="tool-btn" onClick={copyText}>{Icon.copy()}Copy</button>
          <button className="tool-btn" onClick={exportText}>{Icon.download()}Export .txt</button>
          <button className="tool-btn" onClick={exportProvenance}>{Icon.download()}Provenance .json</button>
          <button className="tool-btn" onClick={() => window.print()}>{Icon.print()}Print</button>
        </div>
      </div>
    );
  }

  function QuestionsCard({ questions }) {
    if (!Array.isArray(questions) || questions.length === 0) return null;
    return (
      <div className="validator-report">
        <h3>
          {Icon.copy()}Comprehension questions
          <span className="badge-pill pass">{questions.length}</span>
        </h3>
        <ol className="questions-list">
          {questions.map((q, i) => (
            <li key={i}>
              <div className="q-text">{q.question}</div>
              <div className="q-answer"><strong>Answer:</strong> {q.answer}</div>
              <div className="q-meta">
                {q.type}{q.ccss ? ` · ${q.ccss}` : ""}{q.difficulty ? ` · ${q.difficulty}` : ""}
              </div>
            </li>
          ))}
        </ol>
      </div>
    );
  }

  function ProvenanceCard({ provenance }) {
    if (!provenance || !provenance.stages || !provenance.stages.length) return null;
    const sellable = provenance.sellable !== false;
    return (
      <div className="validator-report">
        <h3>
          {Icon.copy()}Provenance
          <span className={"badge-pill " + (sellable ? "pass" : "fail")}>
            {sellable ? "Sellable" : "TOS-restricted"}
          </span>
        </h3>
        <ul className="validator-list">
          {provenance.stages.map((s, i) => (
            <li key={i} className={s.sellable ? "ok" : "fail"}>
              <span className="v-mark">{s.sellable ? "✓" : "!"}</span>
              <div>
                <div><strong>{s.stage}</strong> · {s.model}</div>
                <div className="v-detail">{s.license}{s.tokensOut ? ` · ${s.tokensOut} tokens out` : ""}</div>
              </div>
            </li>
          ))}
        </ul>
        {provenance.summary && (
          <div style={{ marginTop: 10, fontSize: 12, color: "var(--ink-soft)", fontWeight: 700 }}>
            {provenance.summary}
          </div>
        )}
      </div>
    );
  }

  /* ----- Main workspace ----- */

  function JedaiDecodable({ apiFetch, teacher, tweaks, showToast, jobs, setJobs }) {
    const [spec, setSpec] = React.useState(() => ({ ...DEFAULT_SPEC }));
    const [currentJob, setCurrentJob] = React.useState(null);
    const [busy, setBusy] = React.useState(false);
    const [serverErr, setServerErr] = React.useState("");
    const [modelChains, setModelChains] = React.useState(null);
    const [lastFailedSpec, setLastFailedSpec] = React.useState(null);
    const [showScope, setShowScope] = React.useState(false);
    const [showAdvanced, setShowAdvanced] = React.useState(false);
    const [showRecent, setShowRecent] = React.useState(false);

    /* Pull the live model stack so teachers can see what will generate. */
    React.useEffect(() => {
      let cancelled = false;
      (async () => {
        try {
          const result = await apiFetch("/api/config/models");
          if (!cancelled) setModelChains(result.chains);
        } catch (_) { /* non-fatal */ }
      })();
      return () => { cancelled = true; };
    }, []);

    const decodableJobs = React.useMemo(
      () => (jobs || []).filter((j) => j.toolId === "decodable").slice(0, 25),
      [jobs]
    );

    React.useEffect(() => {
      if (currentJob || decodableJobs.length === 0) return;
      setCurrentJob(decodableJobs[0]);
      // also load its spec so controls reflect the historical job
      if (decodableJobs[0].spec) setSpec({ ...DEFAULT_SPEC, ...decodableJobs[0].spec });
    }, [decodableJobs, currentJob]);

    function bind(field, parse) {
      return (event) => {
        const raw = event.target ? event.target.value : event;
        const v = parse ? parse(raw) : raw;
        setSpec((prev) => {
          const next = { ...prev, [field]: v };
          // When grade changes, clamp week to the new grade's max week
          // and snap curriculum-aligned defaults (word count, sentence cap,
          // decodability floor, sight-word cap) for the new scope grade.
          if (field === "grade") {
            const sg = window.JEDAI_DATA.scopeGradeForLevel(v);
            const maxW = window.JEDAI_DATA.maxWeekForGrade(sg);
            if (next.week > maxW) next.week = maxW;
            const defaults = window.JEDAI_DATA.SPEC_DEFAULTS_BY_GRADE[sg];
            if (defaults) Object.assign(next, defaults);
          }
          return next;
        });
      };
    }

    async function generate(retrySpec) {
      const effectiveSpec = retrySpec || spec;
      const targets = normalizeWordList(effectiveSpec.targetWords);
      const payload = {
        toolId: "decodable",
        spec: { ...effectiveSpec, targetWords: targets }
      };
      const optimistic = newJob("decodable", payload.spec);
      optimistic.title = effectiveSpec.title || "Generating…";
      optimistic.status = "running";
      appendTrace(optimistic, "planning", "active", "Picking phonics targets…");
      setCurrentJob(optimistic);
      setBusy(true);
      setServerErr("");
      setLastFailedSpec(null);

      const finalize = (job) => {
        const sg = window.JEDAI_DATA.scopeGradeForLevel(effectiveSpec.grade || "1");
        const cumScope = window.JEDAI_DATA.getCumulativeScope(sg, effectiveSpec.week);
        const liveValidator = window.JEDAI_VALIDATOR.validatePassage(
          job.finalArtifact || job.draft,
          { ...effectiveSpec, targetWords: targets },
          cumScope
        );
        job.validator = liveValidator;
        setCurrentJob(job);
        setJobs((prev) => [job, ...((prev || []).filter((j) => j.id !== job.id))].slice(0, 100));
        showToast(liveValidator.passed ? "Validated" : "Generated — review failures");
      };

      try {
        const result = await apiFetch("/api/jobs/run", {
          method: "POST",
          body: JSON.stringify(payload)
        });
        const initialJob = result.job;

        if (initialJob.status !== "running") {
          /* Legacy synchronous path — server already finished. */
          finalize(initialJob);
          return;
        }

        /* Async path — server returned {status: 'running'}. Poll until done.
           Show incremental trace updates so the UI feels alive. */
        const jobId = initialJob.id;
        setCurrentJob(initialJob);

        const POLL_MS = 2500;
        const MAX_POLLS = 80; // 80 * 2.5s = 200s max
        for (let i = 0; i < MAX_POLLS; i += 1) {
          await new Promise((r) => setTimeout(r, POLL_MS));
          let polled;
          try {
            polled = await apiFetch(`/api/jobs/${jobId}`, { method: "GET" });
          } catch (err) {
            // transient network error — keep polling
            continue;
          }
          const job = polled.job;
          setCurrentJob(job);
          if (job.status === "done" || job.status === "failed") {
            finalize(job);
            if (job.status === "failed") {
              const lastTrace = (job.trace || []).filter((t) => t.status === "failed").slice(-1)[0];
              setServerErr(lastTrace ? `Agent failed: ${lastTrace.info || "unknown"}` : "Generation failed.");
              setLastFailedSpec(effectiveSpec);
            }
            return;
          }
        }

        /* Polling timed out client-side. The server sweeper will eventually
           mark the row failed; show that state. */
        setServerErr("Generation took longer than 3 minutes. Check history shortly.");
        setLastFailedSpec(effectiveSpec);
        showToast("Timed out — try again");
      } catch (err) {
        setServerErr(err.message || "Generation failed.");
        setLastFailedSpec(effectiveSpec);
        showToast("Generation failed");
      } finally {
        setBusy(false);
      }
    }

    function pickHistory(job) {
      setCurrentJob(job);
      if (job.spec) setSpec({ ...DEFAULT_SPEC, ...job.spec });
    }

    const scopeGrade = window.JEDAI_DATA.scopeGradeForLevel(spec.grade || "1");
    const gradeScope = window.JEDAI_DATA.SCOPE_BY_GRADE[scopeGrade] || [];
    const weekRow = gradeScope.find((r) => r.week === spec.week) || gradeScope[0] || { week: 1, phase: "", new: [] };
    const taughtCount = Array.from(window.JEDAI_DATA.graphemesThroughWeek(spec.week, scopeGrade)).length;

    return (
      <div className="workspace">
        <aside className="ws-controls">
          {/* ── Week hero ── */}
          <div className="ws-week-hero">
            <div className="ws-week-badge">{scopeGrade}·W{spec.week}</div>
            <div className="ws-week-info">
              <select className="ws-week-select" value={spec.week} onChange={bind("week", Number)}>
                {gradeScope.map((row) => (
                  <option key={row.week} value={row.week}>
                    Week {row.week} — {(row.phase || "").replace(/^(KG|G1|G2) · /, "")}
                  </option>
                ))}
              </select>
              <div className="ws-week-meta">
                <span>{taughtCount} graphemes</span>
                <button type="button" className="ws-link" onClick={() => setShowScope((s) => !s)}>
                  {showScope ? "Hide" : "Show"}
                </button>
              </div>
            </div>
          </div>
          {showScope && <ScopeChips week={spec.week} grade={spec.grade}/>}

          {/* ── Content section ── */}
          <div className="ws-section">
            <div className="ws-section-bar" style={{"--accent": "var(--c-reading)"}}/>
            <div className="ws-section-body">
              <div className="ws-row">
                <div className="ws-field">
                  <label>Grade</label>
                  <select value={spec.grade} onChange={bind("grade")}>
                    {GRADE_LEVELS.map((g) => (
                      <option key={g.id} value={g.id}>{g.label}</option>
                    ))}
                  </select>
                </div>
                <div className="ws-field">
                  <label>Style</label>
                  <select value={spec.template} onChange={bind("template")}>
                    {PASSAGE_TEMPLATES.map((t) => (
                      <option key={t.id} value={t.id}>{t.label}</option>
                    ))}
                  </select>
                </div>
              </div>
              <div className="ws-field">
                <label>Title <span className="ws-hint">(optional)</span></label>
                <input
                  type="text"
                  value={spec.title}
                  onChange={bind("title")}
                  placeholder="e.g. Sam and the Map"
                />
              </div>
              <div className="ws-field">
                <label>Target words <span className="ws-hint">(comma-separated)</span></label>
                <textarea
                  value={spec.targetWords}
                  onChange={bind("targetWords")}
                  placeholder="stamp, rust, lamp, hand, sand"
                />
              </div>
            </div>
          </div>

          {/* ── Advanced (collapsible) ── */}
          <div className="ws-section">
            <div className="ws-section-bar" style={{"--accent": "var(--c-assess)"}}/>
            <div className="ws-section-body">
              <button type="button" className="ws-group-toggle" onClick={() => setShowAdvanced((a) => !a)}>
                <span>Advanced rules</span>
                <span className="ws-chev">{showAdvanced ? "−" : "+"}</span>
              </button>
              {showAdvanced && (
                <div className="ws-adv-fields">
                  <div className="ws-row">
                    <div className="ws-field">
                      <label>Words</label>
                      <input type="number" min="20" max="500" value={spec.wordCount}
                        onChange={bind("wordCount", (v) => Math.max(20, Math.min(500, Number(v) || 80)))}/>
                    </div>
                    <div className="ws-field">
                      <label>Max sentence</label>
                      <input type="number" min="6" max="40" value={spec.maxSentenceLen}
                        onChange={bind("maxSentenceLen", (v) => Math.max(6, Math.min(40, Number(v) || 12)))}/>
                    </div>
                  </div>
                  <div className="ws-row">
                    <div className="ws-field">
                      <label>Decode %</label>
                      <input type="number" min="50" max="100" value={spec.decodabilityFloor}
                        onChange={bind("decodabilityFloor", (v) => Math.max(50, Math.min(100, Number(v) || 95)))}/>
                    </div>
                    <div className="ws-field">
                      <label>Sight cap</label>
                      <input type="number" min="0" max="100" value={spec.sightWordCap}
                        onChange={bind("sightWordCap", (v) => Math.max(0, Math.min(100, Number(v) || 12)))}/>
                    </div>
                  </div>
                </div>
              )}
            </div>
          </div>

          {/* ── Mode toggle ── */}
          <div className="ws-mode" role="radiogroup" aria-label="Generation mode">
            <button
              type="button"
              role="radio"
              aria-checked={spec.mode !== "pro"}
              className={"ws-mode-opt" + (spec.mode !== "pro" ? " active" : "")}
              onClick={() => setSpec((s) => ({ ...s, mode: "regular" }))}
              disabled={busy}
              title="DeepSeek via OpenRouter · faster, pay-as-you-go"
            >
              <span className="ws-mode-label">Regular</span>
              <span className="ws-mode-sub">DeepSeek · fast</span>
            </button>
            <button
              type="button"
              role="radio"
              aria-checked={spec.mode === "pro"}
              className={"ws-mode-opt" + (spec.mode === "pro" ? " active" : "")}
              onClick={() => setSpec((s) => ({ ...s, mode: "pro" }))}
              disabled={busy}
              title="Claude Opus 4.7 via Agent SDK · higher quality, uses your Claude subscription"
            >
              <span className="ws-mode-label">Pro</span>
              <span className="ws-mode-sub">Claude · best quality</span>
            </button>
          </div>

          {/* ── Generate CTA ── */}
          <div className="ws-cta">
            <button
              className="tool-btn ws-generate"
              onClick={() => generate()}
              disabled={busy || !teacher}
            >
              {busy ? <span className="spinner"/> : Icon.wand()}
              {busy ? "Generating…" : (spec.mode === "pro" ? "Generate with Claude" : "Generate passage")}
            </button>
          </div>

          {serverErr && (
            <div className="auth-error" style={{ marginTop: 8 }}>
              {serverErr}
              {lastFailedSpec && (
                <button className="tool-btn" style={{ marginTop: 8, background: "var(--c-reading)" }}
                  onClick={() => generate(lastFailedSpec)} disabled={busy}>
                  {Icon.refresh()}Retry
                </button>
              )}
            </div>
          )}

          {/* ── Recent (collapsible) ── */}
          {decodableJobs.length > 0 && (
            <div className="ws-section ws-section-recent">
              <div className="ws-section-bar" style={{"--accent": "var(--c-phonics)"}}/>
              <div className="ws-section-body">
                <button type="button" className="ws-group-toggle" onClick={() => setShowRecent((r) => !r)}>
                  <span>Recent <span className="ws-hint">· {decodableJobs.length}</span></span>
                  <span className="ws-chev">{showRecent ? "−" : "+"}</span>
                </button>
                {showRecent && (
                  <div className="history-list">
                    {decodableJobs.map((j) => {
                      const pass = j.validator?.passed;
                      const badge = pass ? "pass" : (j.status === "failed" ? "fail" : "draft");
                      const badgeLabel = pass ? "Pass" : (j.status === "failed" ? "Fail" : "Draft");
                      return (
                        <div key={j.id}
                          className={"history-item" + (currentJob?.id === j.id ? " selected" : "")}
                          onClick={() => pickHistory(j)}>
                          <div>
                            <div className="history-title">{j.title || "(untitled)"}</div>
                            <div className="history-meta">Week {j.spec.week} · {(j.spec.targetWords?.length || 0)} targets</div>
                          </div>
                          <span className={"history-badge " + badge}>{badgeLabel}</span>
                        </div>
                      );
                    })}
                  </div>
                )}
              </div>
            </div>
          )}
        </aside>

        <main className="ws-output">
          {busy ? (
            <GeneratingAnimation title={spec.title}/>
          ) : currentJob ? (
            <>
              <PassageCard job={currentJob} spec={currentJob.spec || spec} tweaks={tweaks}/>
              <ValidatorReport report={currentJob.validator}/>
              <QuestionsCard questions={currentJob.provenance?.comprehensionQuestions}/>
              <ProvenanceCard provenance={currentJob.provenance}/>
              <AgentTrace trace={currentJob.trace} showAgentTrace={tweaks.showAgentTrace}/>
            </>
          ) : (
            <div className="ws-empty">
              <div className="we-mark">{Mascot.book()}</div>
              <h3>Generate your first passage</h3>
              <p>
                Pick a scope-and-sequence week, drop in 3-6 target words, and hit
                <strong> Generate passage</strong>. The agent pipeline will draft,
                validate, revise if needed, and polish — typically in 10–20 seconds.
              </p>
            </div>
          )}
        </main>
      </div>
    );
  }

  window.JedaiDecodable = JedaiDecodable;
})();
