/* ============================================================
   EDUCARE — DNOI · Denominação Oral de Imagens (4 a 12 anos)
   Fluxo: Identificação → Seleção de etapas (Partes A/B/C) → parte.
   Cada parte é uma área dedicada (conteúdo definido nos próximos
   passos). O progresso das partes fica salvo por criança.
   ============================================================ */
(function () {
  const { useState, useRef, useEffect } = React;
  const Icon = window.Icon;

  const INSTRUMENT_NAME = "DNOI — Denominação Oral de Imagens (4 a 12 anos)";

  // ---------------- Partes do teste ----------------
  const PARTES = [
    { key: "a", n: 1, next: "b", titulo: "Parte A — Reconto Parcial da História", icon: "chat",
      desc: "A criança reconta a história parágrafo por parágrafo." },
    { key: "b", n: 2, next: "c", titulo: "Parte B — Reconto Integral da História", icon: "fileText",
      desc: "A criança reconta a história inteira, do início ao fim." },
    { key: "c", n: 3, next: null, titulo: "Parte C — Avaliação da Compreensão do Texto", icon: "clipboardCheck",
      desc: "Perguntas de compreensão do texto, incluindo o título da história." },
  ];
  const STATUS = {
    nao:       { l: "Não iniciada", tag: "muted" },
    andamento: { l: "Em andamento", tag: "warn" },
    concluida: { l: "Concluída",    tag: "ok" },
  };
  const EMPTY_STATUS = { a: "nao", b: "nao", c: "nao" };

  // ---------------- Persistência por INSTÂNCIA DE AVALIAÇÃO (ID único) ----------------
  // Cada avaliação pertence a um paciente e tem seu próprio ID. Nunca há estado
  // global/compartilhado: a chave é sempre a instância; o filtro por paciente é por pid.
  const AVAL_KEY = "educare-dnoi-avaliacoes";
  function loadAvals() { try { const o = JSON.parse(localStorage.getItem(AVAL_KEY) || "null"); if (o && typeof o === "object") return o; } catch (e) { /* */ } return {}; }
  function saveAvals(obj) { localStorage.setItem(AVAL_KEY, JSON.stringify(obj)); }
  function loadAval(id) { return loadAvals()[id] || null; }
  function putAval(aval) { const all = loadAvals(); all[aval.id] = aval; saveAvals(all); }
  function newAvalId() { return "av_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8); }
  function avalsDoPaciente(pid) { const all = loadAvals(); return Object.keys(all).map((k) => all[k]).filter((a) => a && a.pid === pid).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); }

  // Migração única do formato ANTIGO (chaves por paciente) para instâncias. O pid
  // vem na chave, então a avaliação é sempre vinculável ao paciente correto.
  function migrarDnoiAntigo() {
    const all = loadAvals(); let mudou = 0;
    const patients = window.PATIENTS || [];
    const keys = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k) keys.push(k); }
    keys.forEach((key) => {
      const m = key.match(/^educare-dnoi-reconto-(.+)$/); if (!m) return;
      const pid = m[1];
      let rec = {}, savedAt = null;
      try { const o = JSON.parse(localStorage.getItem(key) || "null"); if (o && o.data) { rec = o.data; savedAt = o.savedAt || null; } else if (o && typeof o === "object") rec = o; } catch (e) { /* */ }
      let status = { a: "nao", b: "nao", c: "nao" }, date = (window.todayISO ? window.todayISO() : "2026-06-04"), escolaTipo = "", idade = "";
      try { const pr = JSON.parse(localStorage.getItem("educare-dnoi-" + pid) || "null"); if (pr) { status = pr.status || status; date = pr.date || date; escolaTipo = pr.escolaTipo || ""; idade = pr.idade || ""; } } catch (e) { /* */ }
      const p = patients.find((x) => x.id === pid);
      const id = newAvalId();
      all[id] = { id, pid, patientName: (p && p.name) || pid, date, escolaTipo, idade, status, recontos: rec, finalizada: false, savedAt, createdAt: Date.now(), finishedAt: null, migrada: true };
      localStorage.removeItem(key); localStorage.removeItem("educare-dnoi-" + pid);
      mudou += 1;
    });
    if (mudou) saveAvals(all);
    return mudou;
  }

  const tipoLabel = (t) => (t === "publica" ? "Pública" : t === "privada" ? "Particular" : "");

  // Chip de ícone (equivale à .sec-head .si, escopada no CSS).
  const SI = { width: 32, height: 32, borderRadius: 10, background: "var(--primary-soft)", color: "var(--primary)", display: "grid", placeItems: "center", flex: "none" };

  // Instruções de Aplicação — Parte A (Reconto Parcial).
  const PARTE_A_INSTR = {
    orient1: "O examinador informa à criança que ela ouvirá uma história curta e que deverá, após cada pedaço (parágrafo) da história, recontar o que acabou de ouvir.",
    fala: "Vou ler uma história curta pra você. Depois de cada pedaço da história, eu quero que você me conte o que acabou de ouvir.",
    orient2: "Em seguida, o examinador lê os parágrafos, um a um, em voz alta, em uma velocidade regular e de forma dinâmica, sem, no entanto, colocar ênfase em um ou outro elemento do texto. O segundo parágrafo deverá ser lido pelo examinador duas vezes, por se tratar de um parágrafo muito longo.",
    aviso: "Atenção: o 2º parágrafo deve ser lido DUAS vezes — é um parágrafo muito longo.",
  };

  // Estímulos — Texto (Parte A): 5 parágrafos, na divisão exata do protocolo.
  // A criança reconta após cada parágrafo; o 2º é lido duas vezes.
  const PARTE_A_ESTIMULOS = [
    { texto: "Marcos é um fazendeiro. Ele passou vários dias cavando um buraco para fazer um poço na sua fazenda e o trabalho estava quase terminado." },
    { texto: "Uma manhã, quando chegou ao campo para terminar de cavar, ele notou que o poço estava quase cheio de terra. Ficou desanimado. Então, teve uma ideia. Tirou o chapéu e a camisa e os colocou na beira do poço. Em seguida, escondeu a enxada e a pá e subiu numa árvore, onde ficou escondido.", duasVezes: true },
    { texto: "Um vizinho, que atravessava a fazenda, chegou logo depois. Vendo o chapéu e a camisa, pensou que Marcos devia estar trabalhando no fundo do poço." },
    { texto: "Ao se abaixar para falar com ele, viu que o buraco estava quase cheio de terra. Gritou pedindo ajuda: 'Socorro! Socorro! Venham rápido! O Marcos ficou enterrado no poço!'." },
    { texto: "Os vizinhos foram ajudar, e começaram a esvaziar o poço para encontrar o amigo. Quando o poço já estava quase vazio, Marcos desceu devagar da árvore, se aproximou e disse: 'Muitíssimo obrigado! Vocês me fizeram um grande favor!'." },
  ];

  /* ---------------- Itens do protocolo (Anexo B) — Parte A · Reconto Parcial ----------------
     Redação EXATA do protocolo (não resumir/reagrupar). Cada item:
     id ("1.1"…) · ess=true para ideias essenciais (➔) · t = texto com os termos-chave
     entre (parênteses), renderizados em negrito. Denominadores: Essenciais /18, Presentes /29. */
  const PARTE_A_ITENS = {
    1: [
      { id: "1.1", ess: true,  t: "(Marcos) é um (fazendeiro)." },
      { id: "1.2", ess: false, t: "Ele passou vários dias" },
      { id: "1.3", ess: true,  t: "cavando um (buraco) para fazer um poço" },
      { id: "1.4", ess: false, t: "na sua fazenda" },
      { id: "1.5", ess: true,  t: "e o trabalho estava quase terminado." },
    ],
    2: [
      { id: "2.1", ess: false, t: "Uma manhã," },
      { id: "2.2", ess: false, t: "quando (chegou) no campo para (terminar de cavar)," },
      { id: "2.3", ess: true,  t: "ele (notou) que o poço estava quase cheio de terra." },
      { id: "2.4", ess: false, t: "Ficou desanimado." },
      { id: "2.5", ess: false, t: "Então, teve uma ideia." },
      { id: "2.6", ess: true,  t: "Tirou o (chapéu) e a (camisa), e colocou-os na beira do poço." },
      { id: "2.7", ess: true,  t: "Em seguida, escondeu a (enxada) e a (pá)" },
      { id: "2.8", ess: true,  t: "e (subiu) numa árvore, onde ficou (escondido)." },
    ],
    3: [
      { id: "3.1", ess: true,  t: "um vizinho," },
      { id: "3.2", ess: false, t: "que atravessava a fazenda," },
      { id: "3.3", ess: false, t: "chegou logo depois." },
      { id: "3.4", ess: true,  t: "Vendo o (chapéu) e a (camisa)," },
      { id: "3.5", ess: true,  t: "pensou que Marcos devia estar trabalhando no fundo do poço." },
    ],
    4: [
      { id: "4.1", ess: false, t: "Ao se abaixar para falar com ele," },
      { id: "4.2", ess: true,  t: "(viu) que o buraco estava (quase) cheio de terra" },
      { id: "4.3", ess: true,  t: "(Gritou), pedindo ajuda: (Socorro!) Socorro! Venham rápido!" },
      { id: "4.4", ess: true,  t: "O Marcos ficou enterrado no poço." },
    ],
    5: [
      { id: "5.1", ess: true,  t: "(Os vizinhos) (foram ajudar)," },
      { id: "5.2", ess: true,  t: "e (começaram) a esvaziar o poço" },
      { id: "5.3", ess: false, t: "para encontrar o amigo." },
      { id: "5.4", ess: true,  t: "Quando (o poço) já estava quase vazio," },
      { id: "5.5", ess: true,  t: "Marcos desceu devagar da árvore" },
      { id: "5.6", ess: false, t: "Se aproximou" },
      { id: "5.7", ess: true,  t: "e (disse): \"− Muitíssimo obrigado: vocês me fizeram um grande favor!\"" },
    ],
  };
  const ESS_TOTAL = 18, PRES_TOTAL = 29;

  // Renderiza o item: termos entre (parênteses) em negrito; resto normal.
  function renderItemText(t) {
    return t.split(/(\([^)]*\))/g).map((seg, i) =>
      (seg.charAt(0) === "(" && seg.charAt(seg.length - 1) === ")")
        ? <b key={i} style={{ fontWeight: 800 }}>{seg}</b>
        : <span key={i}>{seg}</span>
    );
  }

  // Config de IA (mesma do TFV): baseUrl + key + model no localStorage.
  function loadAiCfg() {
    try { const o = JSON.parse(localStorage.getItem("educare-ai-tfv") || "null"); if (o && o.baseUrl && o.key) return o; } catch (e) { /* */ }
    return null;
  }

  const RECONTO_PROMPT = `Você corrige o reconto de uma criança em uma tarefa de reconto de história (DNOI — Reconto Parcial). Recebe: (1) a TRANSCRIÇÃO da fala da criança sobre UM parágrafo; (2) a lista de UNIDADES de informação daquele parágrafo (id + texto). No texto, os termos entre (parênteses) são as informações-chave que a criança deve evocar e ADMITEM substituição por sinônimo/informação equivalente.

Para CADA unidade, decida:
- presente=true se a criança mencionou a informação (a ideia em negrito/parênteses) OU um sinônimo/informação semelhante aceitável (ex.: "homem da roça" = "fazendeiro"; "tampado de terra" = "cheio de terra").
- Se a criança evoca a informação COM acréscimos, omissões parciais ou modificações, mantenha presente=true e escreva a versão da criança em "transformada".
- presente=false apenas se a criança não mencionou a informação nem equivalente.

REGRAS: não exija literalidade — o que importa é a IDEIA estar representada. Baseie-se APENAS na transcrição. "trecho" = trecho literal da fala da criança que justifica (string vazia se ausente). Português do Brasil.

Responda APENAS com JSON válido: {"itens":[{"id":"1.1","presente":true,"transformada":"","trecho":"..."}]}`;

  async function analyzeReconto(cfg, transcript, itens) {
    const payload = { transcricao: transcript, unidades: itens.map((u) => ({ id: u.id, texto: u.t })) };
    const res = await fetch(cfg.baseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
      body: JSON.stringify({ model: cfg.model || "gpt-4o-mini", temperature: 0.2, messages: [{ role: "system", content: RECONTO_PROMPT }, { role: "user", content: JSON.stringify(payload) }] }),
    });
    const j = await res.json();
    let txt = (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || "";
    txt = txt.replace(/```json|```/g, "").trim();
    const parsed = JSON.parse(txt);
    return parsed.itens || [];
  }

  // Estrutura padrão de um parágrafo/parte do reconto.
  const DEF = { transcript: "", obs: "", sug: {}, conf: {} };
  // Há reconto/marcação/observação preenchidos em alguma parte?
  function parteAHasData(rs) {
    if (rs.aComport && Object.keys(rs.aComport).some((k) => rs.aComport[k] === true)) return true;
    return [1, 2, 3, 4, 5].some((n) => {
      const r = rs[n]; if (!r) return false;
      return (r.transcript && r.transcript.trim()) || (r.obs && r.obs.trim()) ||
        (r.conf && Object.keys(r.conf).some((id) => r.conf[id].checked || (r.conf[id].transformada || "").trim()));
    });
  }

  // Player de áudio: play/pause, barra arrastável, tempo atual/total e velocidade
  // (1x/0.75x/0.5x). Trata a duração "Infinity" de blobs do MediaRecorder no Chrome.
  function AudioPlayer({ src }) {
    const ref = useRef(null);
    const [playing, setPlaying] = useState(false);
    const [cur, setCur] = useState(0);
    const [dur, setDur] = useState(0);
    const [rate, setRate] = useState(1);

    useEffect(() => {
      const a = ref.current; if (!a) return;
      let fixing = false;
      const setD = () => { if (isFinite(a.duration) && a.duration > 0) setDur(a.duration); };
      const onLoaded = () => { if (!isFinite(a.duration) || a.duration === 0) { fixing = true; try { a.currentTime = 1e101; } catch (e) { /* */ } } else setD(); };
      const onTime = () => { if (!fixing) setCur(a.currentTime || 0); };
      const onDur = () => { if (fixing && isFinite(a.duration)) { fixing = false; setD(); try { a.currentTime = 0; } catch (e) { /* */ } setCur(0); } };
      const onEnd = () => setPlaying(false);
      a.addEventListener("loadedmetadata", onLoaded);
      a.addEventListener("durationchange", onDur);
      a.addEventListener("timeupdate", onTime);
      a.addEventListener("ended", onEnd);
      if (a.readyState >= 1) onLoaded();
      return () => { a.removeEventListener("loadedmetadata", onLoaded); a.removeEventListener("durationchange", onDur); a.removeEventListener("timeupdate", onTime); a.removeEventListener("ended", onEnd); };
    }, [src]);

    const toggle = () => { const a = ref.current; if (!a) return; if (a.paused) { a.play(); setPlaying(true); } else { a.pause(); setPlaying(false); } };
    const seek = (e) => { const a = ref.current; if (!a) return; const t = parseFloat(e.target.value); a.currentTime = t; setCur(t); };
    const setSpeed = (rr) => { const a = ref.current; if (a) a.playbackRate = rr; setRate(rr); };
    const fmt = (s) => { s = Math.max(0, Math.floor(s || 0)); const m = Math.floor(s / 60); const ss = s % 60; return m + ":" + (ss < 10 ? "0" : "") + ss; };

    return (
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 12px", borderRadius: 10, background: "var(--surface-2)", border: "1px solid var(--border)", marginBottom: 8, flexWrap: "wrap" }}>
        <audio ref={ref} src={src} preload="metadata" />
        <button type="button" onClick={toggle} aria-label={playing ? "Pausar" : "Reproduzir"}
          style={{ width: 34, height: 34, flex: "none", borderRadius: "50%", border: "none", cursor: "pointer", background: "var(--primary)", color: "#fff", fontSize: 14, display: "grid", placeItems: "center" }}>
          {playing ? "⏸" : "▶"}
        </button>
        <input type="range" min={0} max={dur || 0} step="0.01" value={cur > (dur || 0) ? (dur || 0) : cur} onChange={seek}
          style={{ flex: 1, minWidth: 120, accentColor: "var(--primary)", cursor: "pointer" }} />
        <span style={{ fontSize: 12, fontWeight: 700, color: "var(--text-2)", fontVariantNumeric: "tabular-nums", flex: "none" }}>{fmt(cur)} / {fmt(dur)}</span>
        <div style={{ display: "flex", gap: 4, flex: "none" }}>
          {[1, 0.75, 0.5].map((rr) => (
            <button key={rr} type="button" onClick={() => setSpeed(rr)}
              style={{ fontSize: 11.5, fontWeight: 800, padding: "3px 7px", borderRadius: 7, cursor: "pointer", border: "1px solid " + (rate === rr ? "var(--primary)" : "var(--border-2)"), background: rate === rr ? "var(--primary)" : "transparent", color: rate === rr ? "#fff" : "var(--text-2)" }}>
              {rr}x
            </button>
          ))}
        </div>
      </div>
    );
  }

  // Comportamentos comunicativos desviantes (análise qualitativa) — compartilhado
  // pelo checklist da Parte B e pelo seletor múltiplo da Parte A. Não pontuam.
  const COMPORTAMENTOS = [
    { k: "obs_pessoais", nome: "Observações pessoais abundantes", def: "O examinando personifica ou personaliza a história, incluindo fatos da sua autobiografia no reconto.", ex: ['"Meu pai também tem um sítio e lá também tem um poço"', '"Meu primo também se chama Marcos"'] },
    { k: "tangencial", nome: "Discurso tangencial", def: "Desvia do tópico e inicia outro reconto que pode ter relação com a história mas foge do tema; constrói outra história (pode ter poço ou elementos da história ouvida).", ex: [] },
    { k: "cronologia", nome: "Não respeito à cronologia dos eventos", def: "As informações são evocadas, mas recontadas em ordem alterada, sem seguir a ordem original.", ex: [] },
    { k: "marcadores", nome: "Omissão de marcadores de relação", def: "Falta de conectivos entre palavras/frases; frases desconectadas de contexto, isoladas, que não parecem formar uma história; o ouvinte precisa fazer as conexões.", ex: [] },
    { k: "lexico", nome: "Léxico impreciso", def: "Uso de termos vagos no lugar das palavras precisas.", ex: ['"a coisa"', '"aquilo"', '"o trumbico"', '"o treco"', '"o negócio"', '"sei lá..."'] },
    { k: "referencias", nome: "Referências imprecisas", def: 'Trocas do pronome "ele" para "ela", trocas de nome, sujeito não reconhecido ou especificado (não fica explícito de quem está falando).', ex: [] },
    { k: "intrusao_nao", nome: "Intrusões não relacionadas", def: "Acréscimo de informações tangenciais ou nada relacionadas ao texto, não narradas no original, com a criança retornando ao enredo depois.", ex: ['"e daí o Marcos chegou de cavalo em sua fazenda e amarrou ele ao lado da árvore..."'], obs: "Nem toda intrusão é discurso tangencial: no DT a criança segue com outra história e não retorna; na intrusão não relacionada ela retorna ao tema. Se muitas intrusões fizerem a criança se perder e produzir um DT, classificar como as duas coisas." },
    { k: "intrusao_rel", nome: "Intrusões relacionadas", def: "Informações com relação à história, mas não exatamente narradas no original; detalhes alterados (trocar os objetos escondidos, cair água em vez de terra, Marcos pedir socorro em vez do vizinho) sem alterar enredo e conteúdo gerais.", ex: [] },
    { k: "fluencia", nome: "Falta de fluência", def: "O examinador precisa intervir/indagar/estimular para a narrativa continuar; muitas pausas, silêncios, períodos sem evocar informações; evocação lentificada, sem conexão entre ideias; pouca iniciativa e produção oral. Difere da omissão de marcadores: aqui as informações até são evocadas, mas de forma lentificada.", ex: [] },
    { k: "confusos", nome: "Presença de trechos confusos", def: "Não cabe em nenhuma outra categoria; nada é evocado de forma clara e precisa, não se identificam trechos próximos ao alvo.", ex: ['"e o Marcos quando viu da árvore que era de ir lá pra cima"'] },
  ];

  // Parte A — componente CONTROLADO: recebe recontos + updateReconto do DnoiTest,
  // que centraliza salvamento explícito, indicador de "não salvo" e guarda de saída.
  function ParteA({ recontos, updateReconto }) {
    const [instrOpen, setInstrOpen] = useState(true);
    const [estimOpen, setEstimOpen] = useState(true);
    const [recordingNum, setRecordingNum] = useState(null);
    const [analyzingNum, setAnalyzingNum] = useState(null);
    const [errs, setErrs] = useState({});
    const [openTrecho, setOpenTrecho] = useState({});
    const recRef = useRef(null);
    const cfg = loadAiCfg();

    const getR = (num) => recontos[num] || DEF;
    const setR = (num, patch) => updateReconto(num, patch);
    const setErr = (num, msg) => setErrs((e) => ({ ...e, [num]: msg }));

    // === Módulo de transcrição automática (Web Speech API) — DESLIGADO ===
    // Mantido no código para religar depois com outro backend (ex.: Whisper).
    // NÃO é chamado atualmente; a gravação abaixo usa MediaRecorder (só áudio).
    function startRecTranscricaoAutomatica(num) {
      const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
      if (!SR) { setErr(num, "Transcrição automática não suportada."); return; }
      const r = new SR(); r.lang = "pt-BR"; r.continuous = true; r.interimResults = true;
      const base = getR(num).transcript ? getR(num).transcript + " " : "";
      let finalAcc = "";
      r.onresult = (e) => {
        let interim = "";
        for (let i = e.resultIndex; i < e.results.length; i++) { const res = e.results[i]; if (res.isFinal) finalAcc += res[0].transcript + " "; else interim += res[0].transcript; }
        setR(num, { transcript: (base + finalAcc + interim).replace(/\s+/g, " ").trimStart() });
      };
      r.onerror = () => setRecordingNum(null);
      r.onend = () => setRecordingNum((n) => (n === num ? null : n));
      recRef.current = { sr: r }; setRecordingNum(num);
      try { r.start(); } catch (e) { setRecordingNum(null); }
    }

    // Gravação de ÁUDIO (MediaRecorder), sem transcrição ao vivo. O áudio é
    // guardado como data URL (base64) no reconto e entra no salvamento explícito.
    async function startRec(num) {
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.MediaRecorder) {
        setErr(num, "Gravação de áudio não suportada neste navegador."); return;
      }
      stopRec();
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const mr = new MediaRecorder(stream);
        const chunks = [];
        mr.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
        mr.onstop = () => {
          stream.getTracks().forEach((t) => t.stop());
          const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" });
          const reader = new FileReader();
          reader.onloadend = () => setR(num, { audio: reader.result });
          reader.readAsDataURL(blob);
        };
        recRef.current = { mr, stream };
        setErr(num, ""); setRecordingNum(num);
        mr.start();
      } catch (e) {
        setErr(num, "Não foi possível acessar o microfone — verifique a permissão do navegador.");
        setRecordingNum(null);
      }
    }
    function stopRec() {
      const cur = recRef.current;
      if (cur) {
        try {
          if (cur.mr && cur.mr.state !== "inactive") cur.mr.stop();
          if (cur.sr) cur.sr.stop();
        } catch (e) { /* */ }
      }
      setRecordingNum(null);
    }

    async function analisar(num) {
      if (!cfg) { setErr(num, "Configure a IA (no teste TFV) para gerar sugestões — ou marque manualmente."); return; }
      const t = getR(num).transcript;
      if (!t.trim()) { setErr(num, "Grave ou digite a transcrição antes de analisar."); return; }
      setAnalyzingNum(num); setErr(num, "");
      try {
        const res = await analyzeReconto(cfg, t, PARTE_A_ITENS[num] || []);
        const map = {};
        res.forEach((it) => { if (it && it.id) map[it.id] = { checked: !!it.presente, transformada: it.transformada || "", trecho: it.trecho || "" }; });
        setR(num, { sug: map });
      } catch (e) { setErr(num, "Não foi possível analisar com a IA (" + e.message + "). Marque manualmente."); }
      setAnalyzingNum(null);
    }

    // Estado exibido: confirmado (conf) tem prioridade sobre sugestão (sug).
    const isChecked = (r, id) => (r.conf[id] ? !!r.conf[id].checked : (r.sug[id] ? !!r.sug[id].checked : false));
    const transfOf = (r, id) => (r.conf[id] ? (r.conf[id].transformada || "") : (r.sug[id] ? (r.sug[id].transformada || "") : ""));

    function toggleCheck(num, id) {
      const r = getR(num);
      setR(num, { conf: { ...r.conf, [id]: { checked: !isChecked(r, id), transformada: transfOf(r, id) } } });
    }
    function setTransformada(num, id, val) {
      const r = getR(num);
      // escrever uma transformação implica que a informação está presente (marca a casela)
      setR(num, { conf: { ...r.conf, [id]: { checked: val.trim() ? true : isChecked(r, id), transformada: val } } });
    }
    function aceitarTodas(num) {
      const r = getR(num); const conf = { ...r.conf };
      Object.keys(r.sug).forEach((id) => { conf[id] = { checked: !!r.sug[id].checked, transformada: r.sug[id].transformada || "" }; });
      setR(num, { conf });
    }
    const setObs = (num, val) => setR(num, { obs: val });
    const toggleTrecho = (k) => setOpenTrecho((o) => ({ ...o, [k]: !o[k] }));

    const accordionBtn = (open, toggle, icon, title) => (
      <button type="button" onClick={toggle} style={{ width: "100%", display: "flex", alignItems: "center", gap: 10, background: "none", border: "none", padding: "14px 16px", cursor: "pointer", textAlign: "left", color: "var(--text)" }}>
        <span style={SI}><Icon name={icon} size={18} /></span>
        <b style={{ flex: 1, fontSize: 15 }}>{title}</b>
        <Icon name="chevDown" size={18} style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform .2s" }} />
      </button>
    );

    return (
      <>
        <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => setInstrOpen(true)}><Icon name="fileText" size={16} /> Ver instruções</button>
        </div>

        {/* Instruções de Aplicação */}
        <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
          {accordionBtn(instrOpen, () => setInstrOpen((o) => !o), "fileText", "Instruções de Aplicação")}
          {instrOpen && (
            <div style={{ padding: "0 16px 16px" }}>
              <p className="help-text" style={{ marginTop: 0 }}>{PARTE_A_INSTR.orient1}</p>
              <div style={{ background: "var(--primary-soft)", borderLeft: "4px solid var(--primary)", borderRadius: 12, padding: "12px 15px", margin: "10px 0" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
                  <span style={{ ...SI, width: 26, height: 26, borderRadius: 8 }}><Icon name="chat" size={15} /></span>
                  <b style={{ color: "var(--primary)", fontSize: 14 }}>Instrução:</b>
                </div>
                <p style={{ margin: 0, fontStyle: "italic", fontSize: 15.5, lineHeight: 1.7, color: "var(--text)" }}>“{PARTE_A_INSTR.fala}”</p>
              </div>
              <p className="help-text" style={{ marginBottom: 10 }}>{PARTE_A_INSTR.orient2}</p>
              <div style={{ display: "flex", alignItems: "flex-start", gap: 10, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 12, padding: "10px 13px" }}>
                <span style={{ fontSize: 18, lineHeight: 1.2 }}>⚠️</span>
                <p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.5, color: "var(--accent-strong)", fontWeight: 700 }}>{PARTE_A_INSTR.aviso}</p>
              </div>
            </div>
          )}
        </div>

        {/* Estímulos — Texto (com gravador por parágrafo) */}
        <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
          {accordionBtn(estimOpen, () => setEstimOpen((o) => !o), "book", "Estímulos — Texto")}
          {estimOpen && (
            <div style={{ padding: "0 16px 16px" }}>
              <p className="help-text" style={{ marginTop: 0 }}>Leia cada parágrafo em voz alta. Após cada um, use o gravador para registrar o reconto da criança.</p>
              {PARTE_A_ESTIMULOS.map((p, i) => {
                const num = i + 1; const r = getR(num); const rec = recordingNum === num;
                return (
                  <div key={i} style={{ border: "1px solid var(--border)", borderRadius: 12, padding: "12px 14px", marginBottom: 10, background: "var(--surface)" }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
                      <span className="hi-n">{num}</span>
                      <b style={{ fontSize: 13.5, color: "var(--text-2)" }}>Parágrafo {num}</b>
                      {p.duasVezes && <span className="badge-new" style={{ marginLeft: "auto" }}>⚠️ Ler 2x</span>}
                    </div>
                    <p style={{ margin: "0 0 10px", fontSize: 15.5, lineHeight: 1.8, color: "var(--text)" }}>{p.texto}</p>
                    <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
                      {!rec
                        ? <button type="button" className="btn btn-coral btn-sm" onClick={() => startRec(num)}><Icon name="headset" size={15} /> {r.audio ? "Regravar" : "Gravar reconto"}</button>
                        : <button type="button" className="btn btn-sm" onClick={stopRec} style={{ background: "#DC2626", color: "#fff" }}><Icon name="x" size={15} /> Parar</button>}
                      {rec && <span style={{ fontSize: 12.5, color: "#DC2626", fontWeight: 700 }}>● gravando…</span>}
                      {!rec && r.audio && <span style={{ fontSize: 12, color: "var(--primary)", fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 4 }}><Icon name="clipboardCheck" size={13} /> áudio gravado</span>}
                    </div>
                    {errs[num] && <p className="help-text" style={{ color: "#DC2626", marginTop: 6 }}>{errs[num]}</p>}
                  </div>
                );
              })}
            </div>
          )}
        </div>

        {/* Registro do reconto — tabela com o resultado do áudio */}
        <div className="form-card">
          <div className="sec-head"><span className="si"><Icon name="clipboardList" size={18} /></span> Registro do reconto — correção assistida</div>
          <p className="help-text" style={{ marginTop: 0, display: "flex", alignItems: "flex-start", gap: 6 }}>
            <Icon name="sparkles" size={14} /> <span>Sugestões geradas por IA — revise cada item antes de confirmar. A pontuação conta apenas os itens confirmados pelo examinador.</span>
          </p>
          {PARTE_A_ESTIMULOS.map((p, i) => {
            const num = i + 1; const r = getR(num); const itens = PARTE_A_ITENS[num] || [];
            const temSug = Object.keys(r.sug).length > 0;
            const essTot = itens.filter((u) => u.ess).length;
            const essOk = itens.filter((u) => u.ess && r.conf[u.id] && r.conf[u.id].checked).length;
            const presOk = itens.filter((u) => r.conf[u.id] && r.conf[u.id].checked).length;
            return (
              <div key={i} style={{ border: "1px solid var(--border)", borderRadius: 14, padding: "14px 16px", marginBottom: 14 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
                  <span className="hi-n">{num}</span><b style={{ fontSize: 14.5 }}>Parágrafo {num}</b>
                  <button type="button" className="btn btn-ghost btn-sm" style={{ marginLeft: "auto" }} onClick={() => analisar(num)} disabled={analyzingNum === num || !r.transcript.trim()}>
                    <Icon name="sparkles" size={15} /> {analyzingNum === num ? "Analisando…" : (temSug ? "Reanalisar" : "Analisar com IA")}
                  </button>
                </div>

                {r.audio
                  ? <AudioPlayer src={r.audio} />
                  : <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", borderRadius: 10, background: "var(--surface-2)", color: "var(--text-3)", fontSize: 13, fontWeight: 600, marginBottom: 8 }}><Icon name="headset" size={15} /> Nenhum áudio gravado</div>}
                <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>Reconto</label>
                <textarea className="ctrl" style={{ minHeight: 60, marginTop: 4 }} placeholder="Ouça o áudio e transcreva o reconto da criança" value={r.transcript} onChange={(e) => setR(num, { transcript: e.target.value })} />

                {temSug && (
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap", margin: "10px 0 2px" }}>
                    <button type="button" className="btn btn-ghost btn-xs" onClick={() => aceitarTodas(num)}><Icon name="clipboardCheck" size={13} /> Aceitar todas</button>
                    <button type="button" className="btn btn-ghost btn-xs" onClick={() => setR(num, { sug: {} })}><Icon name="x" size={13} /> Limpar sugestões</button>
                  </div>
                )}
                {errs[num] && <p className="help-text" style={{ color: "#DC2626", marginTop: 6 }}>{errs[num]}</p>}

                {/* Cabeçalho da tabela (estilo Anexo B) */}
                <div style={{ display: "flex", gap: 10, alignItems: "center", marginTop: 12, paddingBottom: 6, borderBottom: "2px solid var(--border-2)" }}>
                  <span style={{ width: 22, flex: "none", fontSize: 11, fontWeight: 800, color: "var(--text-3)", textAlign: "center" }}>✓</span>
                  <span style={{ flex: 1, fontSize: 11, fontWeight: 800, color: "var(--text-3)", letterSpacing: ".03em" }}>INFORMAÇÃO</span>
                  <span style={{ flex: 1, minWidth: 120, fontSize: 11, fontWeight: 800, color: "var(--text-3)", letterSpacing: ".03em" }}>INFORMAÇÕES TRANSFORMADAS</span>
                </div>

                {itens.map((u) => {
                  const checked = isChecked(r, u.id);
                  const suggested = !r.conf[u.id] && r.sug[u.id] && r.sug[u.id].checked;
                  const sugTransf = !r.conf[u.id] && r.sug[u.id] && r.sug[u.id].transformada;
                  const tkey = num + "-" + u.id;
                  return (
                    <div key={u.id} style={{ borderBottom: "1px solid var(--border)", padding: "9px 0" }}>
                      <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
                        <button type="button" onClick={() => toggleCheck(num, u.id)} aria-label="Marcar informação" title={suggested ? "Sugestão da IA — clique para confirmar" : "Marcar"}
                          style={{ width: 22, height: 22, flex: "none", borderRadius: 6, cursor: "pointer", display: "grid", placeItems: "center", marginTop: 1,
                            border: checked ? "none" : ("2px " + (suggested ? "dashed var(--primary)" : "solid var(--border-2)")),
                            background: checked ? "var(--primary)" : "transparent", color: "#fff" }}>
                          {checked ? <Icon name="clipboardCheck" size={13} stroke={3} /> : (suggested ? <span style={{ fontSize: 11 }}>✨</span> : null)}
                        </button>

                        <div style={{ flex: 1, minWidth: 140, fontSize: 14, lineHeight: 1.5, color: "var(--text)" }}>
                          {u.ess && <span title="Ideia essencial" style={{ color: "var(--primary)", fontWeight: 800, marginRight: 4 }}>➔</span>}
                          <b style={{ fontWeight: 700, color: "var(--text-3)", marginRight: 5 }}>{u.id}</b>
                          {renderItemText(u.t)}
                          {suggested && (
                            <button type="button" onClick={() => toggleTrecho(tkey)} style={{ display: "block", marginTop: 3, background: "none", border: "none", color: "var(--primary)", fontSize: 12, fontWeight: 700, cursor: "pointer", padding: 0 }}>
                              ✨ {openTrecho[tkey] ? "ocultar trecho" : "ver trecho"}
                            </button>
                          )}
                          {suggested && openTrecho[tkey] && (
                            <div style={{ marginTop: 4, background: "var(--primary-soft)", borderRadius: 8, padding: "7px 10px", fontSize: 12.5, fontStyle: "italic", color: "var(--text-2)" }}>
                              {r.sug[u.id].trecho ? "“" + r.sug[u.id].trecho + "”" : "— (sem trecho correspondente)"}
                            </div>
                          )}
                        </div>

                        <input className="ctrl" style={{ flex: 1, minWidth: 120, fontSize: 13, padding: "6px 9px", borderStyle: sugTransf ? "dashed" : "solid" }}
                          placeholder="—" value={transfOf(r, u.id)} onChange={(e) => setTransformada(num, u.id, e.target.value)} />
                      </div>
                    </div>
                  );
                })}

                <div className="hay-scores" style={{ marginTop: 12 }}>
                  <div className="hsc"><span>Informações essenciais</span><b>{essOk}/{essTot}</b></div>
                  <div className="hsc"><span>Informações presentes</span><b>{presOk}/{itens.length}</b></div>
                </div>

                <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)", display: "block", marginTop: 12 }}>Análise qualitativa — comportamentos comunicativos desviantes (não pontua)</label>
                <textarea className="ctrl" style={{ minHeight: 46, marginTop: 4 }} placeholder="Observações do parágrafo…" value={r.obs || ""} onChange={(e) => setObs(num, e.target.value)} />
              </div>
            );
          })}

          {(() => {
            let essG = 0, presG = 0;
            [1, 2, 3, 4, 5].forEach((n) => {
              const rr = getR(n);
              (PARTE_A_ITENS[n] || []).forEach((u) => { if (rr.conf[u.id] && rr.conf[u.id].checked) { presG += 1; if (u.ess) essG += 1; } });
            });
            return (
              <div style={{ borderTop: "2px solid var(--border-2)", paddingTop: 12, marginTop: 2 }}>
                <p className="section-label" style={{ marginTop: 0 }}>TOTAIS GERAIS · itens confirmados</p>
                <div className="hay-scores">
                  <div className="hsc"><span>Essenciais</span><b>{essG}/{ESS_TOTAL}</b></div>
                  <div className="hsc"><span>Presentes</span><b>{presG}/{PRES_TOTAL}</b></div>
                  <div className="hsc"><span>Detalhes (pres − ess)</span><b>{presG - essG}</b></div>
                </div>
              </div>
            );
          })()}
        </div>

        {/* Comportamentos comunicativos observados (registro clínico complementar) */}
        <div className="form-card">
          <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Comportamentos comunicativos observados</div>
          <p className="help-text" style={{ marginTop: 0 }}>Registro clínico complementar sugerido pelo manual — sem análise formal no reconto parcial. Marque os que observar. <b>Não pontuam.</b></p>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            {COMPORTAMENTOS.map((cp) => {
              const on = !!(recontos.aComport || {})[cp.k];
              return (
                <button key={cp.k} type="button" title={cp.def} onClick={() => updateReconto("aComport", { [cp.k]: !on })}
                  style={{ fontSize: 12.5, fontWeight: 700, padding: "6px 12px", borderRadius: 99, cursor: "pointer", border: "1.5px solid " + (on ? "var(--primary)" : "var(--border-2)"), background: on ? "var(--primary-soft)" : "transparent", color: on ? "var(--primary)" : "var(--text-2)" }}>
                  {on ? "✓ " : ""}{cp.nome}
                </button>
              );
            })}
          </div>
        </div>
      </>
    );
  }

  /* ============================================================
     PARTE B — Reconto Integral da História
     ============================================================ */
  const PARTE_B_INSTR = {
    orient: "O examinador lê o texto inteiro, sem pausa. Ao final, a criança reconta a história completa com as próprias palavras. Em seguida, o examinador faz algumas perguntas sobre a história.",
    fala: "Agora, você irá ouvir o mesmo texto, mas, desta vez, eu vou lê-lo inteiro, sem pausa. No final, eu gostaria que você recontasse a história, com as suas palavras. Depois, eu vou lhe fazer algumas perguntas sobre a história.",
  };

  // Grade de correção por estrutura narrativa (6 seções). Cada item: id · t (título) ·
  // ideia (ideia geral) · ok/no (exemplos corretos/incorretos) · obs (opcional).
  const PARTE_B_SECOES = [
    { key: "s1", n: 1, titulo: "SITUAÇÃO", itens: [
      { id: "1.1", t: "Um homem", ideia: "o sujeito que faz a ação deve ser um homem.", ok: ['"Marcos"', '"o fazendeiro"', '"o Mário" (ou outros nomes de homem)', '"o rapaz"', '"o moço"'], no: ['nome de mulher', 'plural'] },
      { id: "1.2", t: "Ele cava um poço", ideia: "deve dar a ideia de que o homem estava cavando um poço, um buraco.", ok: ['"estava cavando um poço"', '"foi abrir um buraco"', '"fez um poço"'], no: ['"ele pintou o poço"', '"ele escalou o poço"'] },
      { id: "1.3", t: "Ele quase terminou", ideia: "deve dar a ideia de que ele quase finaliza o trabalho.", ok: ['"quando estava quase terminando"', '"estava perto de terminar"'], no: ['"ele terminou o poço"'] },
    ] },
    { key: "s2", n: 2, titulo: "CONFLITO INICIAL", itens: [
      { id: "2.1", t: "O poço enche de terra", ideia: "o poço (buraco) deve encher de terra ou sinônimo.", ok: ['"o poço ficou cheio de terra"', '"cheio de areia"'], no: ['"cheio de água"', '"cheio de lama/barro"'] },
    ] },
    { key: "s3", n: 3, titulo: "PLANO INTERNO", itens: [
      { id: "3.1", t: "Ele tem uma ideia, elabora uma estratégia", ideia: "ter uma ideia, pensar numa estratégia, num plano.", ok: ['"ele teve uma ideia"', '"pensou no que podia fazer"', '"acendeu uma luz na cabeça"'], no: ['"alguém deu uma ideia para ele"'] },
    ] },
    { key: "s4", n: 4, titulo: "TENTATIVA", itens: [
      { id: "4.1", t: "Ele deixa suas roupas perto do poço", ideia: "as roupas devem ser deixadas por Marcos perto do poço, denotando a intencionalidade da ação envolvida no plano.", ok: ['"pegou a camisa e o chapéu e largou na beira do poço"'], no: ['"tirou a camisa e o chapéu" (sem local)', '"resolveu tirar a roupa e as esconde"'] },
      { id: "4.2", t: "Ele esconde sua enxada e sua pá", ideia: "as ferramentas devem ser escondidas, não podem ficar expostas na cena.", ok: ['"esconde a enxada e a pá"', '"esconde as ferramentas"'], no: ['"deixou por ali"', '"botou as ferramentas ao lado do poço"', '"colocou a enxada e a pá na beira do poço"'], obs: "Se forem mencionados outros elementos do mesmo campo semântico (martelo, foice), marcar e registrar na análise qualitativa como léxico impreciso." },
      { id: "4.3", t: "Ele se esconde numa árvore", ideia: "intenção clara de esconder-se na árvore (a árvore deve ser mencionada como local do esconderijo).", ok: ['"subiu na árvore para se esconder"', '"subiu na árvore e se escondeu lá em cima"'], no: ['"subiu na árvore" (sem menção a esconder-se)', '"se escondeu" (sem a árvore)'] },
    ] },
    { key: "s5", n: 5, titulo: "CONSEQUÊNCIA", itens: [
      { id: "5.1", t: "Um vizinho acredita que ele está no poço", ideia: "o vizinho imagina, pensa, fala que Marcos está dentro do poço.", ok: ['"chegou um vizinho, viu a roupa e pensou que ele estava lá dentro do poço"'], no: ['"ele tinha caído no poço"', '"ele tinha se escondido no poço"'] },
      { id: "5.2", t: "Ele chama seus amigos para ajudá-lo", ideia: "chamar outras pessoas para ajudar, solicitar auxílio na tarefa, pedir socorro.", ok: ['"começou a chamar por socorro"', '"chamou por ajuda"'], no: ['"Marcos pediu socorro para os vizinhos"'] },
      { id: "5.3", t: "Juntos, eles cavam o poço", ideia: "ato de cavar, tirar a terra do poço pelos vizinhos.", ok: ['"eles vieram e limparam o poço"', '"os vizinhos foram acudir"'], no: ['"e começaram a cavar" (sem a ideia de esvaziar)'] },
    ] },
    { key: "s6", n: 6, titulo: "REAÇÕES", itens: [
      { id: "6.1", t: "Marcos desce da árvore", ideia: "Marcos deve de fato descer da árvore, sair de onde estava escondido.", ok: ['"ele desceu" (aceito sem mencionar a árvore, desde que o esconderijo na árvore tenha sido referido antes no reconto)'], no: ['"ele saiu do poço"', '"Marcos volta do trabalho"'] },
      { id: "6.2", t: "Ele agradece aos seus vizinhos", ideia: "agradecimento de Marcos a todos, independentemente da razão.", ok: ['"falou muito obrigado"', '"agradeceu aos vizinhos"'], no: ['"pediu desculpas aos vizinhos"', '"riu dos vizinhos"'] },
    ] },
  ];
  const PARTE_B_ITENS_FLAT = PARTE_B_SECOES.reduce((a, s) => a.concat(s.itens), []);
  const PARTE_B_TOTAL = PARTE_B_ITENS_FLAT.length;

  function parteBHasData(rs) {
    const b = rs && rs.b; if (!b) return false;
    return (b.audio) || (b.transcript && b.transcript.trim()) ||
      (b.conf && Object.keys(b.conf).some((id) => b.conf[id].checked || (b.conf[id].transformada || "").trim())) ||
      (b.coment && Object.keys(b.coment).some((k) => (b.coment[k] || "").trim())) ||
      (b.comport && Object.keys(b.comport).some((k) => b.comport[k])) ||
      (b.outrasObs || "").trim() || b.inferMomento || (b.inquerito || "").trim();
  }

  const RECONTO_B_PROMPT = `Você corrige o RECONTO INTEGRAL de uma criança (DNOI — Parte B). Recebe: (1) a TRANSCRIÇÃO da fala da criança recontando a história inteira; (2) a lista de UNIDADES de informação, cada uma com id, a IDEIA principal e exemplos CORRETOS (aceitos) e INCORRETOS (recusados).

Para CADA unidade decida:
- presente=true se a criança menciona a IDEIA principal da unidade OU um sinônimo/informação equivalente aceitável (use os exemplos corretos como referência; não exija literalidade).
- presente=false se a criança não menciona a ideia, ou se corresponde apenas aos exemplos incorretos.
- Se a criança evoca a ideia COM acréscimos, omissões parciais ou modificações, mantenha presente=true e escreva a versão da criança em "transformada".

REGRAS DE PONTUAÇÃO: acréscimos de informações não presentes no texto NÃO penalizam (presente continua true). Se a informação exigir GRANDE inferência do examinador (falta de clareza, fluência ou dificuldade expressiva), considere presente=false.

Baseie-se APENAS na transcrição. "trecho" = trecho literal da fala que justifica (string vazia se ausente). Português do Brasil.

Responda APENAS com JSON válido: {"itens":[{"id":"1.1","presente":true,"transformada":"","trecho":"..."}]}`;

  async function analyzeRecontoB(cfg, transcript, itens) {
    const payload = { transcricao: transcript, unidades: itens.map((u) => ({ id: u.id, titulo: u.t, ideia_principal: u.ideia, exemplos_corretos: u.ok, exemplos_incorretos: u.no })) };
    const res = await fetch(cfg.baseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
      body: JSON.stringify({ model: cfg.model || "gpt-4o-mini", temperature: 0.2, messages: [{ role: "system", content: RECONTO_B_PROMPT }, { role: "user", content: JSON.stringify(payload) }] }),
    });
    const j = await res.json();
    let txt = (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || "";
    txt = txt.replace(/```json|```/g, "").trim();
    const parsed = JSON.parse(txt);
    return parsed.itens || [];
  }

  // Parte B — componente CONTROLADO (recontos.b): instruções + texto corrido +
  // gravador único + player/transcrição manual + grade por estrutura narrativa.
  function ParteB({ recontos, updateReconto }) {
    const [instrOpen, setInstrOpen] = useState(true);
    const [textoOpen, setTextoOpen] = useState(true);
    const [recording, setRecording] = useState(false);
    const [analyzing, setAnalyzing] = useState(false);
    const [err, setErr] = useState("");
    const [openHelp, setOpenHelp] = useState({});
    const [openTrecho, setOpenTrecho] = useState({});
    const [openComp, setOpenComp] = useState({});
    const recRef = useRef(null);
    const cfg = loadAiCfg();

    const b = recontos.b || { audio: "", transcript: "", sug: {}, conf: {}, coment: {} };
    const conf = b.conf || {};
    const sug = b.sug || {};
    const setB = (patch) => updateReconto("b", patch);

    const accordionBtn = (open, toggle, icon, title) => (
      <button type="button" onClick={toggle} style={{ width: "100%", display: "flex", alignItems: "center", gap: 10, background: "none", border: "none", padding: "14px 16px", cursor: "pointer", textAlign: "left", color: "var(--text)" }}>
        <span style={SI}><Icon name={icon} size={18} /></span>
        <b style={{ flex: 1, fontSize: 15 }}>{title}</b>
        <Icon name="chevDown" size={18} style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform .2s" }} />
      </button>
    );

    function startRec() {
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.MediaRecorder) { setErr("Gravação de áudio não suportada neste navegador."); return; }
      stopRec();
      navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
        const mr = new MediaRecorder(stream); const chunks = [];
        mr.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
        mr.onstop = () => {
          stream.getTracks().forEach((t) => t.stop());
          const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" });
          const reader = new FileReader();
          reader.onloadend = () => setB({ audio: reader.result });
          reader.readAsDataURL(blob);
        };
        recRef.current = { mr, stream }; setErr(""); setRecording(true); mr.start();
      }).catch(() => { setErr("Não foi possível acessar o microfone — verifique a permissão do navegador."); setRecording(false); });
    }
    function stopRec() {
      const cur = recRef.current;
      if (cur && cur.mr && cur.mr.state !== "inactive") { try { cur.mr.stop(); } catch (e) { /* */ } }
      setRecording(false);
    }

    async function analisar() {
      if (!cfg) { setErr("Configure a IA (no teste TFV) para gerar sugestões — ou marque manualmente."); return; }
      if (!(b.transcript || "").trim()) { setErr("Grave e transcreva o reconto antes de analisar."); return; }
      setAnalyzing(true); setErr("");
      try {
        const res = await analyzeRecontoB(cfg, b.transcript, PARTE_B_ITENS_FLAT);
        const map = {};
        res.forEach((it) => { if (it && it.id) map[it.id] = { checked: !!it.presente, transformada: it.transformada || "", trecho: it.trecho || "" }; });
        setB({ sug: map });
      } catch (e) { setErr("Não foi possível analisar com a IA (" + e.message + "). Marque manualmente."); }
      setAnalyzing(false);
    }

    const isChk = (id) => (conf[id] ? !!conf[id].checked : (sug[id] ? !!sug[id].checked : false));
    const transfOf = (id) => (conf[id] ? (conf[id].transformada || "") : (sug[id] ? (sug[id].transformada || "") : ""));
    const toggleCheck = (id) => setB({ conf: { ...conf, [id]: { checked: !isChk(id), transformada: transfOf(id) } } });
    const setTransformada = (id, val) => setB({ conf: { ...conf, [id]: { checked: val.trim() ? true : isChk(id), transformada: val } } });
    const aceitarTodas = () => { const c = { ...conf }; Object.keys(sug).forEach((id) => { c[id] = { checked: !!sug[id].checked, transformada: sug[id].transformada || "" }; }); setB({ conf: c }); };
    const setComent = (sk, val) => setB({ coment: { ...(b.coment || {}), [sk]: val } });
    const temSug = Object.keys(sug).length > 0;
    const totalOk = PARTE_B_ITENS_FLAT.filter((u) => conf[u.id] && conf[u.id].checked).length;

    return (
      <>
        <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => setInstrOpen(true)}><Icon name="fileText" size={16} /> Ver instruções</button>
        </div>

        {/* Instruções */}
        <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
          {accordionBtn(instrOpen, () => setInstrOpen((o) => !o), "fileText", "Instruções de Aplicação")}
          {instrOpen && (
            <div style={{ padding: "0 16px 16px" }}>
              <p className="help-text" style={{ marginTop: 0 }}>{PARTE_B_INSTR.orient}</p>
              <div style={{ background: "var(--primary-soft)", borderLeft: "4px solid var(--primary)", borderRadius: 12, padding: "12px 15px", marginTop: 10 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
                  <span style={{ ...SI, width: 26, height: 26, borderRadius: 8 }}><Icon name="chat" size={15} /></span>
                  <b style={{ color: "var(--primary)", fontSize: 14 }}>Instrução:</b>
                </div>
                <p style={{ margin: 0, fontStyle: "italic", fontSize: 15.5, lineHeight: 1.7, color: "var(--text)" }}>“{PARTE_B_INSTR.fala}”</p>
              </div>
            </div>
          )}
        </div>

        {/* Texto para leitura (corrido) + gravador único */}
        <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
          {accordionBtn(textoOpen, () => setTextoOpen((o) => !o), "book", "Texto para leitura (história completa)")}
          {textoOpen && (
            <div style={{ padding: "0 16px 16px" }}>
              <p className="help-text" style={{ marginTop: 0 }}>Leia a história inteira, sem pausa. Depois grave o reconto integral da criança.</p>
              <div style={{ border: "1px solid var(--border)", borderRadius: 12, padding: "14px 16px", background: "var(--surface)" }}>
                {PARTE_A_ESTIMULOS.map((p, i) => (
                  <p key={i} style={{ margin: i === 0 ? 0 : "12px 0 0", fontSize: 15.5, lineHeight: 1.85, color: "var(--text)", textIndent: 18 }}>{p.texto}</p>
                ))}
              </div>
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", marginTop: 12 }}>
                {!recording
                  ? <button type="button" className="btn btn-coral btn-sm" onClick={startRec}><Icon name="headset" size={15} /> {b.audio ? "Regravar reconto integral" : "Gravar reconto integral"}</button>
                  : <button type="button" className="btn btn-sm" onClick={stopRec} style={{ background: "#DC2626", color: "#fff" }}><Icon name="x" size={15} /> Parar</button>}
                {recording && <span style={{ fontSize: 12.5, color: "#DC2626", fontWeight: 700 }}>● gravando…</span>}
                {!recording && b.audio && <span style={{ fontSize: 12, color: "var(--primary)", fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 4 }}><Icon name="clipboardCheck" size={13} /> áudio gravado</span>}
              </div>
              {err && <p className="help-text" style={{ color: "#DC2626", marginTop: 6 }}>{err}</p>}
            </div>
          )}
        </div>

        {/* Registro do reconto integral — player + transcrição + grade */}
        <div className="form-card">
          <div className="sec-head" style={{ flexWrap: "wrap", gap: 8 }}>
            <span className="si"><Icon name="clipboardList" size={18} /></span> Registro do reconto integral
            <button type="button" className="btn btn-ghost btn-sm" style={{ marginLeft: "auto" }} onClick={analisar} disabled={analyzing || !(b.transcript || "").trim()}>
              <Icon name="sparkles" size={15} /> {analyzing ? "Analisando…" : (temSug ? "Reanalisar" : "Analisar com IA")}
            </button>
          </div>
          <p className="help-text" style={{ marginTop: 0, display: "flex", alignItems: "flex-start", gap: 6 }}>
            <Icon name="sparkles" size={14} /> <span>Sugestões geradas por IA — revise cada item antes de confirmar. A pontuação conta apenas os itens confirmados.</span>
          </p>

          {b.audio
            ? <AudioPlayer src={b.audio} />
            : <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", borderRadius: 10, background: "var(--surface-2)", color: "var(--text-3)", fontSize: 13, fontWeight: 600, marginBottom: 8 }}><Icon name="headset" size={15} /> Nenhum áudio gravado</div>}
          <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>Reconto integral</label>
          <textarea className="ctrl" style={{ minHeight: 80, marginTop: 4 }} placeholder="Ouça o áudio e transcreva o reconto da criança" value={b.transcript || ""} onChange={(e) => setB({ transcript: e.target.value })} />

          {temSug && (
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", margin: "10px 0 2px" }}>
              <button type="button" className="btn btn-ghost btn-xs" onClick={aceitarTodas}><Icon name="clipboardCheck" size={13} /> Aceitar todas</button>
              <button type="button" className="btn btn-ghost btn-xs" onClick={() => setB({ sug: {} })}><Icon name="x" size={13} /> Limpar sugestões</button>
            </div>
          )}

          {PARTE_B_SECOES.map((sec) => {
            const subOk = sec.itens.filter((u) => conf[u.id] && conf[u.id].checked).length;
            return (
              <div key={sec.key} style={{ marginTop: 16 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
                  <span className="hi-n">{sec.n}</span>
                  <b style={{ fontSize: 13.5, letterSpacing: ".03em", color: "var(--text-2)" }}>{sec.titulo}</b>
                  <span style={{ marginLeft: "auto", fontSize: 12, fontWeight: 800, color: "var(--primary)" }}>{subOk}/{sec.itens.length}</span>
                </div>

                {/* cabeçalho da tabela */}
                <div style={{ display: "flex", gap: 10, alignItems: "center", paddingBottom: 5, borderBottom: "2px solid var(--border-2)" }}>
                  <span style={{ width: 22, flex: "none", fontSize: 10.5, fontWeight: 800, color: "var(--text-3)", textAlign: "center" }}>✓</span>
                  <span style={{ flex: 1, fontSize: 10.5, fontWeight: 800, color: "var(--text-3)" }}>INFORMAÇÃO</span>
                  <span style={{ flex: 1, minWidth: 120, fontSize: 10.5, fontWeight: 800, color: "var(--text-3)" }}>INFORMAÇÕES TRANSFORMADAS</span>
                </div>

                {sec.itens.map((u) => {
                  const checked = isChk(u.id);
                  const suggested = !conf[u.id] && sug[u.id] && sug[u.id].checked;
                  const sugTransf = !conf[u.id] && sug[u.id] && sug[u.id].transformada;
                  return (
                    <div key={u.id} style={{ borderBottom: "1px solid var(--border)", padding: "9px 0" }}>
                      <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
                        <button type="button" onClick={() => toggleCheck(u.id)} aria-label="Marcar" title={suggested ? "Sugestão da IA — clique para confirmar" : "Marcar"}
                          style={{ width: 22, height: 22, flex: "none", borderRadius: 6, cursor: "pointer", display: "grid", placeItems: "center", marginTop: 1,
                            border: checked ? "none" : ("2px " + (suggested ? "dashed var(--primary)" : "solid var(--border-2)")),
                            background: checked ? "var(--primary)" : "transparent", color: "#fff" }}>
                          {checked ? <Icon name="clipboardCheck" size={13} stroke={3} /> : (suggested ? <span style={{ fontSize: 11 }}>✨</span> : null)}
                        </button>

                        <div style={{ flex: 1, minWidth: 140, fontSize: 14, lineHeight: 1.5, color: "var(--text)" }}>
                          <b style={{ fontWeight: 700, color: "var(--text-3)", marginRight: 5 }}>{u.id}</b>{u.t}
                          <button type="button" onClick={() => setOpenHelp((o) => ({ ...o, [u.id]: !o[u.id] }))} title="Ajuda de correção" style={{ marginLeft: 6, background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--primary)", fontSize: 14 }}>ℹ️</button>
                          {suggested && (
                            <button type="button" onClick={() => setOpenTrecho((o) => ({ ...o, [u.id]: !o[u.id] }))} style={{ display: "block", marginTop: 3, background: "none", border: "none", color: "var(--primary)", fontSize: 12, fontWeight: 700, cursor: "pointer", padding: 0 }}>
                              ✨ {openTrecho[u.id] ? "ocultar trecho" : "ver trecho"}
                            </button>
                          )}
                          {suggested && openTrecho[u.id] && (
                            <div style={{ marginTop: 4, background: "var(--primary-soft)", borderRadius: 8, padding: "7px 10px", fontSize: 12.5, fontStyle: "italic", color: "var(--text-2)" }}>
                              {sug[u.id].trecho ? "“" + sug[u.id].trecho + "”" : "— (sem trecho correspondente)"}
                            </div>
                          )}
                          {openHelp[u.id] && (
                            <div style={{ marginTop: 6, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px" }}>
                              <p style={{ margin: "0 0 8px", fontSize: 13, color: "var(--text-2)" }}><b>Ideia geral:</b> {u.ideia}</p>
                              <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
                                <div style={{ flex: 1, minWidth: 150, background: "var(--primary-soft)", borderRadius: 8, padding: "8px 10px" }}>
                                  <b style={{ fontSize: 11.5, color: "var(--primary)" }}>✔ CORRETO</b>
                                  <ul style={{ margin: "5px 0 0", paddingLeft: 16 }}>{u.ok.map((x, i) => <li key={i} style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--text-2)" }}>{x}</li>)}</ul>
                                </div>
                                <div style={{ flex: 1, minWidth: 150, background: "rgba(239,68,68,.08)", borderRadius: 8, padding: "8px 10px" }}>
                                  <b style={{ fontSize: 11.5, color: "#DC2626" }}>✘ INCORRETO</b>
                                  <ul style={{ margin: "5px 0 0", paddingLeft: 16 }}>{u.no.map((x, i) => <li key={i} style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--text-2)" }}>{x}</li>)}</ul>
                                </div>
                              </div>
                              {u.obs && <p style={{ margin: "8px 0 0", fontSize: 12, color: "var(--text-3)" }}><b>Obs.:</b> {u.obs}</p>}
                            </div>
                          )}
                        </div>

                        <input className="ctrl" style={{ flex: 1, minWidth: 120, fontSize: 13, padding: "6px 9px", borderStyle: sugTransf ? "dashed" : "solid" }}
                          placeholder="—" value={transfOf(u.id)} onChange={(e) => setTransformada(u.id, e.target.value)} />
                      </div>
                    </div>
                  );
                })}

                <label style={{ fontSize: 12, fontWeight: 700, color: "var(--text-3)", display: "block", marginTop: 8 }}>Comentários — {sec.titulo} (análise qualitativa, não pontua)</label>
                <textarea className="ctrl" style={{ minHeight: 40, marginTop: 4 }} placeholder="Comportamentos comunicativos desviantes…" value={(b.coment || {})[sec.key] || ""} onChange={(e) => setComent(sec.key, e.target.value)} />
              </div>
            );
          })}

          <div style={{ borderTop: "2px solid var(--border-2)", paddingTop: 12, marginTop: 14 }}>
            <div className="hay-scores">
              <div className="hsc"><span>Total de informações recontadas</span><b>{totalOk}/{PARTE_B_TOTAL}</b></div>
            </div>
          </div>
        </div>

        {/* Análise Qualitativa — Comportamentos Comunicativos Desviantes */}
        <div className="form-card">
          <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Análise Qualitativa — Comportamentos Comunicativos Desviantes</div>
          <p className="help-text" style={{ marginTop: 0 }}>Estes comportamentos não se propõem a ser exaustivos — outros fatores qualitativos observados clinicamente podem ser registrados nas observações. <b>Não pontuam.</b></p>

          {COMPORTAMENTOS.map((cp) => {
            const on = !!(b.comport || {})[cp.k];
            return (
              <div key={cp.k} style={{ borderTop: "1px solid var(--border)", padding: "9px 0" }}>
                <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
                  <button type="button" onClick={() => setB({ comport: { ...(b.comport || {}), [cp.k]: !on } })} aria-label="Marcar"
                    style={{ width: 22, height: 22, flex: "none", borderRadius: 6, cursor: "pointer", display: "grid", placeItems: "center", marginTop: 1, border: on ? "none" : "2px solid var(--border-2)", background: on ? "var(--primary)" : "transparent", color: "#fff" }}>
                    {on ? <Icon name="clipboardCheck" size={13} stroke={3} /> : null}
                  </button>
                  <div style={{ flex: 1 }}>
                    <span style={{ fontSize: 14, fontWeight: on ? 700 : 600, color: "var(--text)" }}>{cp.nome}</span>
                    <button type="button" onClick={() => setOpenComp((o) => ({ ...o, [cp.k]: !o[cp.k] }))} title="Definição e exemplos" style={{ marginLeft: 6, background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--primary)", fontSize: 14 }}>ℹ️</button>
                    {openComp[cp.k] && (
                      <div style={{ marginTop: 6, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px" }}>
                        <p style={{ margin: 0, fontSize: 13, color: "var(--text-2)", lineHeight: 1.55 }}>{cp.def}</p>
                        {cp.ex.length > 0 && <p style={{ margin: "6px 0 0", fontSize: 12.5, color: "var(--text-3)" }}>Ex.: {cp.ex.join("; ")}</p>}
                        {cp.obs && <p style={{ margin: "6px 0 0", fontSize: 12, color: "var(--accent-strong)" }}><b>Obs.:</b> {cp.obs}</p>}
                      </div>
                    )}
                    {on && <input className="ctrl" style={{ marginTop: 8, fontSize: 13 }} placeholder="Trecho / observação (exemplo observado)…" value={(b.comportTrecho || {})[cp.k] || ""} onChange={(e) => setB({ comportTrecho: { ...(b.comportTrecho || {}), [cp.k]: e.target.value } })} />}
                  </div>
                </div>
              </div>
            );
          })}

          <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)", display: "block", marginTop: 12 }}>Outras observações qualitativas</label>
          <textarea className="ctrl" style={{ minHeight: 48, marginTop: 4 }} placeholder="Outros fatores qualitativos observados…" value={b.outrasObs || ""} onChange={(e) => setB({ outrasObs: e.target.value })} />

          <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px", marginTop: 12 }}>
            <Icon name="help" size={15} />
            <p style={{ margin: 0, fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.5 }}>Acréscimos de informações não presentes no texto original <b>não descontam pontos</b> — apenas entram aqui. Quando uma informação exigir <b>grande inferência</b> do examinador (falta de clareza/fluência), marque-a como <b>incorreta</b> na grade e registre o comportamento aqui.</p>
          </div>
        </div>

        {/* Processamento da Inferência */}
        <div className="form-card">
          <div className="sec-head"><span className="si"><Icon name="sparkles" size={18} /></span> Processamento da Inferência</div>
          <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>Momento em que a criança demonstrou compreender a inferência</label>
          <select className="ctrl" style={{ marginTop: 4 }} value={b.inferMomento || ""} onChange={(e) => setB({ inferMomento: e.target.value })}>
            <option value="">Selecione…</option>
            <option>No Título 1</option>
            <option>Nas questões de compreensão</option>
            <option>No Título 2</option>
            <option>Durante o reconto</option>
            <option>Não demonstrou</option>
            <option value="Outro">Outro (especificar)</option>
          </select>
          {b.inferMomento === "Outro" && <input className="ctrl" style={{ marginTop: 8 }} placeholder="Especifique…" value={b.inferOutro || ""} onChange={(e) => setB({ inferOutro: e.target.value })} />}

          <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)", display: "block", marginTop: 12 }}>Inquérito informal (não pontuado)</label>
          <textarea className="ctrl" style={{ minHeight: 48, marginTop: 4 }} placeholder="Ex.: por que Marcos subiu na árvore e deixou as roupas na beira do poço…" value={b.inquerito || ""} onChange={(e) => setB({ inquerito: e.target.value })} />
          <p className="help-text" style={{ marginBottom: 0 }}>Registro apenas — não pontua.</p>
        </div>
      </>
    );
  }

  /* ============================================================
     PARTE C — Avaliação da Compreensão do Texto
     ============================================================ */
  // Bloco de fala literal para a criança (reutilizável nos blocos de título).
  function FalaDestacada({ children }) {
    return (
      <div style={{ background: "var(--primary-soft)", borderLeft: "4px solid var(--primary)", borderRadius: 12, padding: "12px 15px", margin: "4px 0 12px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 5 }}>
          <span style={{ ...SI, width: 26, height: 26, borderRadius: 8 }}><Icon name="chat" size={15} /></span>
          <b style={{ color: "var(--primary)", fontSize: 14 }}>Pergunte à criança:</b>
        </div>
        <p style={{ margin: 0, fontStyle: "italic", fontSize: 15.5, lineHeight: 1.7, color: "var(--text)" }}>“{children}”</p>
      </div>
    );
  }

  // Ajuda de pontuação de título (0/1/2) — usada no Título 1 e no Título 2.
  const PARTE_C_TITULO_AJUDA = [
    { n: 0, desc: "título sem relação / inadequado à história.", ex: ['"Pedro, o cavalheiro"', '"O grande momento"', '"A inundação na fazenda"'] },
    { n: 1, desc: "título relacionado à história, mas sem a inferência central.", ex: ['"A correria dos vizinhos"', '"Marcos e o poço"', '"O grande favor"'] },
    { n: 2, desc: "título demonstrando que a inferência foi feita (Marcos achou uma estratégia para não precisar cavar o poço novamente, pregando uma peça nos vizinhos).", ex: ['"Marcos, o sacana"', '"Marcos, o aproveitador"', '"Os vizinhos enganados"'] },
  ];
  function TituloAjuda() {
    return (
      <div style={{ marginTop: 8, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px" }}>
        {PARTE_C_TITULO_AJUDA.map((lv) => (
          <div key={lv.n} style={{ marginBottom: 8 }}>
            <b style={{ fontSize: 12.5, color: "var(--primary)" }}>Pontuação {lv.n}</b>
            <span style={{ fontSize: 12.5, color: "var(--text-2)" }}> — {lv.desc}</span>
            <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>Ex.: {lv.ex.join("; ")}</div>
          </div>
        ))}
      </div>
    );
  }

  const PARTE_C_QUESTOES = [
    { id: "q1", pergunta: "O que Marcos estava fazendo depois de vários dias?", ok: ['Cavando um poço', 'Cavando um buraco', 'Fazendo um buraco', 'Fazendo um poço', 'Um poço', 'Abrindo um poço'], no: ['Cavando', 'Trabalhando', 'Trabalhando na roça', 'Trabalhando no campo'] },
    { id: "q2", pergunta: "O trabalho já havia terminado?", ok: ['Não', 'Quase'], no: ['Talvez', 'Sim'] },
    { id: "q3", pergunta: "Durante a noite, o que caiu no buraco?", ok: ['Terra', 'Areia', 'Caiu a terra toda'], no: ['Água', 'Marcos'] },
    { id: "q4", pergunta: "O que Marcos colocou na beira do poço?", ok: ['Chapéu e camisa', 'Boné e camisa', 'Chapéu e blusa', 'Roupas', 'Vestimentas'], no: ['Sapato', 'Ferramentas', 'Chapéu e sapato', 'Chapéu, picareta e pá'] },
    { id: "q5", pergunta: "O que ele fez com a sua enxada e a sua pá?", ok: ['Escondeu', 'Guardou'], no: ['Colocou na árvore', 'Deixou de lado', 'Botou atrás da árvore', 'Acho que foi enterrada lá no poço'] },
    { id: "q6", pergunta: "Onde ele se escondeu depois disso?", ok: ['Na árvore', 'Em cima da árvore'], no: ['Na beira da árvore', 'No poço', 'Atrás da árvore'] },
    { id: "q7", pergunta: "Onde seus vizinhos pensaram que Marcos estava?", ok: ['Dentro do poço', 'No fundo do poço', 'Enterrado', 'Soterrado dentro do poço', 'No buraco'], no: ['Na árvore', 'Dentro de casa', 'Na fazenda vizinha', 'Passeando pela fazenda'] },
    { id: "q8", pergunta: "O que fizeram os vizinhos?", ok: ['Cavaram o poço', 'Tiraram a terra do poço', 'Cavaram o poço para ele', 'Ajudaram a esvaziar o poço'], no: ['Chamaram ajuda', 'Salvaram o Marcos', 'Gritaram por socorro'] },
    { id: "q9", pergunta: "Quando Marcos desceu da árvore?", ok: ['Quando o poço estava (quase) vazio', 'Quando eles terminaram de cavar', 'Quando tiraram toda a terra', 'Depois de o poço estar limpo'], no: ['Quando viu aquilo', 'Mais tarde', 'Depois que eles gritaram por socorro', 'No fim da história'] },
    { id: "q10", pergunta: "Será que os vizinhos ficaram contentes? Por quê?", ok: ['Não', 'Nem um pouco contentes', 'Porque eles foram enganados / usados / sacaneados / explorados pelo Marcos', 'Sim (se o examinando já tiver demonstrado que realizou a inferência)'], no: ['Sim (quando o examinando não fez a inferência anteriormente)', 'Sim porque viram que Marcos não estava em perigo, estava bem e fora de risco'] },
    { id: "q11", pergunta: "O que podemos pensar de Marcos?", ok: ['Safado', 'Sacana', 'Sem vergonha', 'Irresponsável', 'Inteligente', 'Malandro', 'Esperto', 'Brincalhão', 'Aproveitador', 'Não foi legal com os vizinhos'], no: ['Amigão', 'Bom vizinho', 'Trabalhador', 'Leal'], obs: "Poderá ser feito um inquérito, caso a criança não compreenda a pergunta. Questionar sobre o que podemos pensar de Marcos, de suas atitudes na história, daquilo que ele fez." },
  ];
  const PARTE_C_TOTAL_Q = PARTE_C_QUESTOES.length;

  function parteCHasData(rs) {
    const c = rs && rs.c; if (!c) return false;
    if (c.audio) return true;
    if (c.titulo1 && ((c.titulo1.texto || "").trim() || c.titulo1.score != null)) return true;
    if (c.titulo2 && (c.titulo2.manteve != null || (c.titulo2.texto || "").trim() || c.titulo2.score != null)) return true;
    if (c.questoes && Object.keys(c.questoes).some((id) => (c.questoes[id].resposta || "").trim() || c.questoes[id].correto != null)) return true;
    return false;
  }

  const RECONTO_C_PROMPT = `Você corrige a AVALIAÇÃO DE COMPREENSÃO de uma criança (DNOI — Parte C). Recebe: (1) as respostas da criança a 11 perguntas, cada uma com id, pergunta, a resposta da criança e exemplos de respostas CORRETAS e INCORRETAS; (2) o título 1 e o título 2 dados pela criança.

Para CADA questão, decida correto=true se a resposta corresponde à ideia de alguma resposta correta (sinônimos aceitos, sem exigir literalidade); correto=false se não corresponde ou corresponde apenas aos exemplos incorretos. "trecho" = a parte da resposta que justifica.

Para os títulos, atribua pontuação: 0 = sem relação/inadequado; 1 = relacionado à história mas SEM a inferência central (Marcos pregou uma peça nos vizinhos para não precisar recavar o poço); 2 = demonstra que a inferência foi feita. Se um título estiver vazio, use null.

Responda APENAS com JSON válido: {"questoes":[{"id":"q1","correto":true,"trecho":"..."}],"titulo1":0,"titulo2":0}`;

  async function analyzeParteC(cfg, dados) {
    const res = await fetch(cfg.baseUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": "Bearer " + cfg.key },
      body: JSON.stringify({ model: cfg.model || "gpt-4o-mini", temperature: 0.2, messages: [{ role: "system", content: RECONTO_C_PROMPT }, { role: "user", content: JSON.stringify(dados) }] }),
    });
    const j = await res.json();
    let txt = (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || "";
    txt = txt.replace(/```json|```/g, "").trim();
    return JSON.parse(txt);
  }

  // Parte C — componente CONTROLADO (recontos.c): instruções + gravador/player +
  // Título 1 → 11 Questões → Título 2 + resumo. Registro manual, IA como sugestão.
  function ParteC({ recontos, updateReconto }) {
    const [instrOpen, setInstrOpen] = useState(true);
    const [recording, setRecording] = useState(false);
    const [analyzing, setAnalyzing] = useState(false);
    const [err, setErr] = useState("");
    const [openHelp, setOpenHelp] = useState({});
    const [t1HelpOpen, setT1HelpOpen] = useState(false);
    const [t2HelpOpen, setT2HelpOpen] = useState(false);
    const recRef = useRef(null);
    const cfg = loadAiCfg();

    const c = recontos.c || {};
    const t1 = c.titulo1 || { texto: "", score: null };
    const t2 = c.titulo2 || { manteve: null, texto: "", score: null };
    const questoes = c.questoes || {};
    const sug = c.sug || { questoes: {}, titulo1: null, titulo2: null };
    const setC = (patch) => updateReconto("c", patch);
    const setT1 = (patch) => setC({ titulo1: { ...t1, ...patch } });
    const setT2 = (patch) => setC({ titulo2: { ...t2, ...patch } });
    const setQ = (id, patch) => setC({ questoes: { ...questoes, [id]: { ...(questoes[id] || { resposta: "", correto: null }), ...patch } } });

    function startRec() {
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.MediaRecorder) { setErr("Gravação de áudio não suportada neste navegador."); return; }
      stopRec();
      navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
        const mr = new MediaRecorder(stream); const chunks = [];
        mr.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
        mr.onstop = () => {
          stream.getTracks().forEach((t) => t.stop());
          const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" });
          const reader = new FileReader();
          reader.onloadend = () => setC({ audio: reader.result });
          reader.readAsDataURL(blob);
        };
        recRef.current = { mr, stream }; setErr(""); setRecording(true); mr.start();
      }).catch(() => { setErr("Não foi possível acessar o microfone — verifique a permissão do navegador."); setRecording(false); });
    }
    function stopRec() {
      const cur = recRef.current;
      if (cur && cur.mr && cur.mr.state !== "inactive") { try { cur.mr.stop(); } catch (e) { /* */ } }
      setRecording(false);
    }

    async function analisar() {
      if (!cfg) { setErr("Configure a IA (no teste TFV) para gerar sugestões — ou marque manualmente."); return; }
      const algumaResposta = PARTE_C_QUESTOES.some((q) => (questoes[q.id] && (questoes[q.id].resposta || "").trim())) || (t1.texto || "").trim();
      if (!algumaResposta) { setErr("Registre as respostas antes de analisar."); return; }
      setAnalyzing(true); setErr("");
      try {
        const dados = {
          questoes: PARTE_C_QUESTOES.map((q) => ({ id: q.id, pergunta: q.pergunta, resposta: (questoes[q.id] && questoes[q.id].resposta) || "", corretos: q.ok, incorretos: q.no })),
          titulo1: t1.texto || "", titulo2: t2.texto || "",
        };
        const r = await analyzeParteC(cfg, dados);
        const qmap = {};
        (r.questoes || []).forEach((it) => { if (it && it.id) qmap[it.id] = { correto: !!it.correto, trecho: it.trecho || "" }; });
        setC({ sug: { questoes: qmap, titulo1: (r.titulo1 == null ? null : r.titulo1), titulo2: (r.titulo2 == null ? null : r.titulo2) } });
      } catch (e) { setErr("Não foi possível analisar com a IA (" + e.message + "). Marque manualmente."); }
      setAnalyzing(false);
    }
    function aceitarTodas() {
      const q = { ...questoes };
      Object.keys(sug.questoes || {}).forEach((id) => { q[id] = { ...(q[id] || { resposta: "" }), correto: sug.questoes[id].correto }; });
      const patch = { questoes: q };
      if (sug.titulo1 != null) patch.titulo1 = { ...t1, score: sug.titulo1 };
      if (sug.titulo2 != null) patch.titulo2 = { ...t2, score: sug.titulo2 };
      setC(patch);
    }

    // Seletores (com estado de sugestão tracejada)
    const scoreBtns = (value, sugValue, onPick) => (
      <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
        {[0, 1, 2].map((n) => {
          const active = value === n; const suggested = value == null && sugValue === n;
          return <button key={n} type="button" onClick={() => onPick(n)}
            style={{ width: 34, height: 34, borderRadius: 8, fontWeight: 800, fontSize: 14, cursor: "pointer",
              border: suggested ? "2px dashed var(--primary)" : ("1.5px solid " + (active ? "var(--primary)" : "var(--border-2)")),
              background: active ? "var(--primary)" : "transparent", color: active ? "#fff" : (suggested ? "var(--primary)" : "var(--text-2)") }}>{n}</button>;
        })}
        {value == null && sugValue != null && <span style={{ fontSize: 11.5, color: "var(--primary)", fontWeight: 700 }}>✨ sugestão</span>}
      </div>
    );
    const ciBtns = (id) => {
      const value = questoes[id] ? questoes[id].correto : null;
      const sv = (value == null && sug.questoes[id]) ? sug.questoes[id].correto : null;
      return (
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
          {[{ v: true, l: "Correto", col: "#16A34A" }, { v: false, l: "Incorreto", col: "#DC2626" }].map((o) => {
            const active = value === o.v; const suggested = sv === o.v;
            return <button key={o.l} type="button" onClick={() => setQ(id, { correto: o.v })}
              style={{ fontSize: 12, fontWeight: 700, padding: "5px 12px", borderRadius: 8, cursor: "pointer",
                border: suggested ? ("2px dashed " + o.col) : ("1.5px solid " + (active ? o.col : "var(--border)")),
                background: active ? o.col : "var(--surface-2)", color: active ? "#fff" : (suggested ? o.col : "var(--text-2)") }}>{suggested ? "✨ " : ""}{o.l}</button>;
          })}
        </div>
      );
    };

    const qCorretas = PARTE_C_QUESTOES.filter((q) => questoes[q.id] && questoes[q.id].correto === true).length;
    const temSug = Object.keys(sug.questoes || {}).length > 0 || sug.titulo1 != null || sug.titulo2 != null;

    return (
      <>
        <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => setInstrOpen(true)}><Icon name="fileText" size={16} /> Ver instruções</button>
        </div>

        {/* Instruções */}
        <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
          <button type="button" onClick={() => setInstrOpen((o) => !o)} style={{ width: "100%", display: "flex", alignItems: "center", gap: 10, background: "none", border: "none", padding: "14px 16px", cursor: "pointer", textAlign: "left", color: "var(--text)" }}>
            <span style={SI}><Icon name="fileText" size={18} /></span>
            <b style={{ flex: 1, fontSize: 15 }}>Instruções de Aplicação</b>
            <Icon name="chevDown" size={18} style={{ transform: instrOpen ? "rotate(180deg)" : "none", transition: "transform .2s" }} />
          </button>
          {instrOpen && (
            <div style={{ padding: "0 16px 16px" }}>
              <p className="help-text" style={{ marginTop: 0 }}>Aplicada logo após o reconto integral. O examinador faz as perguntas oralmente e registra as respostas da criança, na sequência: <b>Título 1 → Questões de Compreensão → Título 2</b>.</p>
            </div>
          )}
        </div>

        {/* Gravação da conversa (perguntas e respostas) */}
        <div className="form-card">
          <div className="sec-head"><span className="si"><Icon name="headset" size={18} /></span> Gravação (perguntas e respostas)</div>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", marginBottom: 8 }}>
            {!recording
              ? <button type="button" className="btn btn-coral btn-sm" onClick={startRec}><Icon name="headset" size={15} /> {c.audio ? "Regravar" : "Gravar"}</button>
              : <button type="button" className="btn btn-sm" onClick={stopRec} style={{ background: "#DC2626", color: "#fff" }}><Icon name="x" size={15} /> Parar</button>}
            {recording && <span style={{ fontSize: 12.5, color: "#DC2626", fontWeight: 700 }}>● gravando…</span>}
          </div>
          {c.audio
            ? <AudioPlayer src={c.audio} />
            : <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 12px", borderRadius: 10, background: "var(--surface-2)", color: "var(--text-3)", fontSize: 13, fontWeight: 600 }}><Icon name="headset" size={15} /> Nenhum áudio gravado</div>}
          {err && <p className="help-text" style={{ color: "#DC2626", marginTop: 6 }}>{err}</p>}
        </div>

        {/* Título 1 */}
        <div className="form-card">
          <div className="sec-head" style={{ flexWrap: "wrap", gap: 8 }}>
            <span className="si"><Icon name="chat" size={18} /></span> Título 1
            <button type="button" className="btn btn-ghost btn-xs" style={{ marginLeft: "auto" }} onClick={() => setT1HelpOpen((v) => !v)}>ℹ️ Ajuda de pontuação</button>
          </div>
          <FalaDestacada>Que título você daria para esta história?</FalaDestacada>
          <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>Título dado pela criança (exatamente como ela disse)</label>
          <input className="ctrl" style={{ marginTop: 4 }} placeholder="Transcreva o título…" value={t1.texto} onChange={(e) => setT1({ texto: e.target.value })} />
          <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginTop: 10 }}>
            <b style={{ fontSize: 13, color: "var(--text-2)" }}>Pontuação:</b>
            {scoreBtns(t1.score, sug.titulo1, (n) => setT1({ score: n }))}
          </div>
          {t1HelpOpen && <TituloAjuda />}
        </div>

        {/* Questões de Compreensão */}
        <div className="form-card">
          <div className="sec-head" style={{ flexWrap: "wrap", gap: 8 }}>
            <span className="si"><Icon name="clipboardList" size={18} /></span> Questões de Compreensão
            <button type="button" className="btn btn-ghost btn-sm" style={{ marginLeft: "auto" }} onClick={analisar} disabled={analyzing}>
              <Icon name="sparkles" size={15} /> {analyzing ? "Analisando…" : (temSug ? "Reanalisar" : "Analisar com IA")}
            </button>
          </div>
          <p className="help-text" style={{ marginTop: 0, display: "flex", alignItems: "flex-start", gap: 6 }}>
            <Icon name="sparkles" size={14} /> <span>Sugestões geradas por IA — revise cada item antes de confirmar. Só contam os itens confirmados.</span>
          </p>
          {temSug && (
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 6 }}>
              <button type="button" className="btn btn-ghost btn-xs" onClick={aceitarTodas}><Icon name="clipboardCheck" size={13} /> Aceitar todas</button>
              <button type="button" className="btn btn-ghost btn-xs" onClick={() => setC({ sug: { questoes: {}, titulo1: null, titulo2: null } })}><Icon name="x" size={13} /> Limpar sugestões</button>
            </div>
          )}

          {PARTE_C_QUESTOES.map((q, i) => (
            <div key={q.id} style={{ border: "1px solid var(--border)", borderRadius: 12, padding: "12px 14px", marginBottom: 10 }}>
              <div style={{ display: "flex", alignItems: "flex-start", gap: 8, marginBottom: 8 }}>
                <span className="hi-n" style={{ flex: "none" }}>{i + 1}</span>
                <b style={{ fontSize: 14.5, lineHeight: 1.4, color: "var(--text)" }}>{q.pergunta}</b>
                <button type="button" onClick={() => setOpenHelp((o) => ({ ...o, [q.id]: !o[q.id] }))} title="Ajuda de correção" style={{ marginLeft: "auto", flex: "none", background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--primary)", fontSize: 16 }}>ℹ️</button>
              </div>
              <input className="ctrl" placeholder="Resposta da criança…" value={(questoes[q.id] && questoes[q.id].resposta) || ""} onChange={(e) => setQ(q.id, { resposta: e.target.value })} />
              <div style={{ marginTop: 8 }}>{ciBtns(q.id)}</div>
              {openHelp[q.id] && (
                <div style={{ marginTop: 8, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px", display: "flex", gap: 10, flexWrap: "wrap" }}>
                  <div style={{ flex: 1, minWidth: 150, background: "var(--primary-soft)", borderRadius: 8, padding: "8px 10px" }}>
                    <b style={{ fontSize: 11.5, color: "var(--primary)" }}>✔ CORRETO</b>
                    <ul style={{ margin: "5px 0 0", paddingLeft: 16 }}>{q.ok.map((x, k) => <li key={k} style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--text-2)" }}>{x}</li>)}</ul>
                  </div>
                  <div style={{ flex: 1, minWidth: 150, background: "rgba(239,68,68,.08)", borderRadius: 8, padding: "8px 10px" }}>
                    <b style={{ fontSize: 11.5, color: "#DC2626" }}>✘ INCORRETO</b>
                    <ul style={{ margin: "5px 0 0", paddingLeft: 16 }}>{q.no.map((x, k) => <li key={k} style={{ fontSize: 12.5, lineHeight: 1.5, color: "var(--text-2)" }}>{x}</li>)}</ul>
                  </div>
                  {q.obs && <p style={{ margin: 0, width: "100%", fontSize: 12, color: "var(--text-3)" }}><b>Obs.:</b> {q.obs}</p>}
                </div>
              )}
            </div>
          ))}

          <div className="hay-scores" style={{ marginTop: 4 }}>
            <div className="hsc"><span>Questões corretas</span><b>{qCorretas}/{PARTE_C_TOTAL_Q}</b></div>
          </div>
        </div>

        {/* Título 2 */}
        <div className="form-card">
          <div className="sec-head" style={{ flexWrap: "wrap", gap: 8 }}>
            <span className="si"><Icon name="chat" size={18} /></span> Título 2
            <button type="button" className="btn btn-ghost btn-xs" style={{ marginLeft: "auto" }} onClick={() => setT2HelpOpen((v) => !v)}>ℹ️ Ajuda de pontuação</button>
          </div>
          <FalaDestacada>E agora, você deixaria o mesmo título?</FalaDestacada>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            {[{ v: true, l: "Sim, mantém" }, { v: false, l: "Não, novo título" }].map((o) => {
              const active = t2.manteve === o.v;
              return <button key={o.l} type="button"
                onClick={() => setT2(o.v ? { manteve: true, score: (t2.score != null ? t2.score : t1.score) } : { manteve: false })}
                style={{ fontSize: 13, fontWeight: 700, padding: "7px 14px", borderRadius: 8, cursor: "pointer", border: "1.5px solid " + (active ? "var(--primary)" : "var(--border-2)"), background: active ? "var(--primary)" : "transparent", color: active ? "#fff" : "var(--text-2)" }}>{o.l}</button>;
            })}
          </div>
          {t2.manteve === false && (
            <div style={{ marginTop: 12 }}>
              <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>Novo título dado pela criança</label>
              <input className="ctrl" style={{ marginTop: 4 }} placeholder="Transcreva o novo título…" value={t2.texto} onChange={(e) => setT2({ texto: e.target.value })} />
            </div>
          )}
          {t2.manteve != null && (
            <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginTop: 12 }}>
              <b style={{ fontSize: 13, color: "var(--text-2)" }}>Pontuação:</b>
              {scoreBtns(t2.score, sug.titulo2, (n) => setT2({ score: n }))}
              {t2.manteve === true && <span style={{ fontSize: 12, color: "var(--text-3)" }}>(repetida do Título 1 — editável)</span>}
            </div>
          )}
          {t2HelpOpen && <TituloAjuda />}
        </div>

        {/* Resumo da Parte C */}
        <div className="form-card" style={{ background: "var(--primary-soft)", border: "1px solid var(--primary)" }}>
          <div className="sec-head"><span className="si"><Icon name="clipboardCheck" size={18} /></span> Resumo da Parte C</div>
          <div className="hay-scores">
            <div className="hsc"><span>Título 1 (0–2)</span><b>{t1.score != null ? t1.score : "—"}</b></div>
            <div className="hsc"><span>Questões corretas</span><b>{qCorretas}/{PARTE_C_TOTAL_Q}</b></div>
            <div className="hsc"><span>Título 2</span><b>{t2.manteve == null ? "—" : (t2.manteve ? "Manteve" : "Novo") + (t2.score != null ? " · " + t2.score : "")}</b></div>
          </div>
        </div>
      </>
    );
  }

  function DnoiTest({ onBack, onOpenPatient }) {
    const [view, setView] = useState("ident"); // ident | menu | a | b | c
    const [pid, setPid] = useState("");
    const [avalId, setAvalId] = useState(null);       // instância de avaliação atual
    const [readonly, setReadonly] = useState(false);  // avaliação finalizada = consulta
    const [date, setDate] = useState((window.todayISO ? window.todayISO() : "2026-06-04"));
    const [escolaTipo, setEscolaTipo] = useState("");
    const [idade, setIdade] = useState("");
    const [status, setStatus] = useState(EMPTY_STATUS);
    const [err, setErr] = useState("");

    // Todos os dados da avaliação (áudios, transcrições, marcações, qualitativa)
    // ficam em MEMÓRIA e só persistem na INSTÂNCIA (avalId) ao clicar em salvar.
    const [recontos, setRecontos] = useState({});
    const [savedJson, setSavedJson] = useState("{}"); // snapshot do último salvamento
    const [savedAt, setSavedAt] = useState(null);
    const [concluida, setConcluida] = useState({ a: false, b: false, c: false });
    const [dirty, setDirty] = useState(false);
    const [saving, setSaving] = useState(false);
    const [toast, setToast] = useState(null);          // { kind: "ok"|"err", text }
    const [leaveGuard, setLeaveGuard] = useState(null); // { proceed: fn } navegação interceptada
    const [showClear, setShowClear] = useState(false);  // confirmação de "apagar tudo"

    const patient = (window.PATIENTS || []).find((p) => p.id === pid);
    const activePart = PARTES.find((p) => p.key === view);
    const listaAvals = pid ? avalsDoPaciente(pid) : []; // avaliações desta criança (recalcula por render)

    // Migração única do formato antigo → instâncias.
    useEffect(() => {
      try { const n = migrarDnoiAntigo(); if (n) setToast({ kind: "ok", text: n + " avaliação(ões) antiga(s) migrada(s)." }); } catch (e) { /* */ }
    }, []);

    // Aviso do navegador ao fechar/atualizar a aba com alterações não salvas.
    useEffect(() => {
      if (!dirty) return;
      const h = (e) => { e.preventDefault(); e.returnValue = ""; return ""; };
      window.addEventListener("beforeunload", h);
      return () => window.removeEventListener("beforeunload", h);
    }, [dirty]);

    // Toast some sozinho.
    useEffect(() => {
      if (!toast) return;
      const id = setTimeout(() => setToast(null), toast.kind === "err" ? 5000 : 2600);
      return () => clearTimeout(id);
    }, [toast]);

    function goTop() { const el = document.querySelector(".main-scroll"); if (el) el.scrollTop = 0; }

    // Zera a instância carregada em memória (sem tocar no paciente selecionado).
    function limparInstancia() {
      setAvalId(null); setReadonly(false); setStatus(EMPTY_STATUS);
      setRecontos({}); setSavedJson("{}"); setSavedAt(null);
      setConcluida({ a: false, b: false, c: false }); setDirty(false);
    }
    // Volta a tela ao estado LIMPO (pronta para nova aplicação com qualquer criança).
    function resetTudo() {
      limparInstancia(); setPid(""); setDate((window.todayISO ? window.todayISO() : "2026-06-04")); setEscolaTipo(""); setIdade("");
      setErr(""); setView("ident");
    }

    // Apaga TODAS as avaliações DNOI DESTA CRIANÇA (paciente selecionado).
    function apagarAvalsDoPaciente() {
      if (!pid) return;
      try {
        const all = loadAvals();
        Object.keys(all).forEach((id) => { if (all[id] && all[id].pid === pid) delete all[id]; });
        saveAvals(all);
        localStorage.removeItem("educare-dnoi-reconto-" + pid); // chaves antigas (se restarem)
        localStorage.removeItem("educare-dnoi-" + pid);
      } catch (e) { /* */ }
      setShowClear(false); limparInstancia();
      setToast({ kind: "ok", text: "Avaliações desta criança apagadas." });
    }

    function pickPatient(id) {
      setPid(id);
      const p = (window.PATIENTS || []).find((x) => x.id === id);
      setEscolaTipo((p && p.school_type) || "");
      setIdade(p && p.age ? p.age + " anos" : "");
      limparInstancia(); // nada é carregado até "Iniciar nova" ou "Retomar/Ver"
    }

    // Cria uma NOVA instância vazia para o paciente selecionado.
    function novaAvaliacao() {
      if (!pid) { setErr("Selecione o paciente."); return; }
      const id = newAvalId();
      const aval = { id, pid, patientName: patient ? patient.name : "", date, escolaTipo, idade, status: EMPTY_STATUS, recontos: {}, finalizada: false, savedAt: null, createdAt: Date.now(), finishedAt: null };
      try { putAval(aval); } catch (e) { /* */ }
      setAvalId(id); setReadonly(false); setStatus(EMPTY_STATUS);
      setRecontos({}); setSavedJson("{}"); setSavedAt(null);
      setConcluida({ a: false, b: false, c: false }); setDirty(false);
      setErr(""); setView("menu"); goTop();
    }

    // Abre uma instância existente (retomar em andamento, ou ver finalizada em consulta).
    function abrirAval(a, ro) {
      setAvalId(a.id); setReadonly(!!ro);
      setPid(a.pid); setDate(a.date || (window.todayISO ? window.todayISO() : "2026-06-04")); setEscolaTipo(a.escolaTipo || ""); setIdade(a.idade || "");
      const st = a.status || EMPTY_STATUS;
      setStatus(st); setRecontos(a.recontos || {}); setSavedJson(JSON.stringify(a.recontos || {})); setSavedAt(a.savedAt);
      setConcluida({ a: st.a === "concluida", b: st.b === "concluida", c: st.c === "concluida" }); setDirty(false);
      setErr(""); setView("menu"); goTop();
    }

    function updateReconto(num, patch) {
      if (readonly) return;
      setRecontos((rs) => ({ ...rs, [num]: { ...(rs[num] || DEF), ...patch } }));
      setDirty(true);
    }
    function toggleConcluida(key) { if (readonly) return; setConcluida((c) => ({ ...c, [key]: !c[key] })); setDirty(true); }

    const computeStatus = () => ({
      a: concluida.a ? "concluida" : (parteAHasData(recontos) ? "andamento" : "nao"),
      b: concluida.b ? "concluida" : (parteBHasData(recontos) ? "andamento" : "nao"),
      c: concluida.c ? "concluida" : (parteCHasData(recontos) ? "andamento" : "nao"),
    });

    // Persiste a instância atual no store (localStorage hoje; trocar por Supabase aqui).
    function persistAval(finalizar) {
      const when = new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
      const nextStatus = computeStatus();
      const prev = loadAval(avalId);
      const aval = { id: avalId, pid, patientName: patient ? patient.name : "", date, escolaTipo, idade, status: nextStatus, recontos, finalizada: !!finalizar, savedAt: when, createdAt: (prev && prev.createdAt) || Date.now(), finishedAt: finalizar ? Date.now() : (prev && prev.finishedAt) || null };
      putAval(aval); // pode lançar (quota) → tratado por quem chama
      return { when, nextStatus, aval };
    }

    // SALVAMENTO EXPLÍCITO — único ponto de persistência. Nunca descarta em erro.
    async function saveAvaliacao() {
      if (readonly) return false;
      if (!pid || !avalId) { setErr("Inicie uma avaliação primeiro."); return false; }
      setSaving(true); setToast(null);
      try {
        const { when, nextStatus } = persistAval(false);
        setStatus(nextStatus); setSavedJson(JSON.stringify(recontos)); setSavedAt(when); setDirty(false);
        setToast({ kind: "ok", text: "Avaliação salva" });
        return true;
      } catch (e) {
        setToast({ kind: "err", text: window.classifyStorageError ? window.classifyStorageError(e) : "Não foi possível salvar — tente novamente." });
        return false;
      } finally { setSaving(false); }
    }

    // Grava um resumo da avaliação finalizada no prontuário do paciente.
    function gravarNoProntuario(aval) {
      if (!window.addAvaliacao) return;
      const rc = aval.recontos || {};
      let essA = 0, presA = 0;
      [1, 2, 3, 4, 5].forEach((n) => { const r = rc[n]; if (r && r.conf) (PARTE_A_ITENS[n] || []).forEach((u) => { if (r.conf[u.id] && r.conf[u.id].checked) { presA++; if (u.ess) essA++; } }); });
      let okB = 0; const b = rc.b; if (b && b.conf) PARTE_B_ITENS_FLAT.forEach((u) => { if (b.conf[u.id] && b.conf[u.id].checked) okB++; });
      const c = rc.c || {}; const t1 = (c.titulo1 && c.titulo1.score != null) ? c.titulo1.score : "—";
      const qC = c.questoes ? PARTE_C_QUESTOES.filter((x) => c.questoes[x.id] && c.questoes[x.id].correto === true).length : 0;
      const t2 = (c.titulo2 && c.titulo2.score != null) ? c.titulo2.score : "—";
      const result = `A ${essA}/${ESS_TOTAL} ess · B ${okB}/${PARTE_B_TOTAL} · C ${qC}/${PARTE_C_QUESTOES.length}`;
      const note = `DNOI finalizado. Reconto Parcial: ${essA}/${ESS_TOTAL} essenciais, ${presA}/${PRES_TOTAL} presentes. Reconto Integral: ${okB}/${PARTE_B_TOTAL}. Compreensão: Título 1 = ${t1}, ${qC}/${PARTE_C_QUESTOES.length} questões, Título 2 = ${t2}.`;
      window.addAvaliacao({ pid: aval.pid, name: INSTRUMENT_NAME, date: aval.date, result, note, sheet: { kind: "dnoi-final", avalId: aval.id, status: aval.status, resumo: { essA, presA, okB, t1, qC, t2 } } });
    }

    // Finaliza: salva a instância como finalizada, grava no prontuário e limpa a tela.
    async function finalizarAvaliacao() {
      if (readonly || !avalId) return;
      setSaving(true); setToast(null);
      try {
        const { aval } = persistAval(true);
        gravarNoProntuario(aval);
        setDirty(false);
        setToast({ kind: "ok", text: "Avaliação finalizada e salva no prontuário" });
        resetTudo(); goTop();
      } catch (e) {
        console.error("[DNOI] Falha ao finalizar:", e);
        setToast({ kind: "err", text: "Não foi possível finalizar — tente novamente." });
      } finally { setSaving(false); }
    }

    // Guarda de saída: intercepta a navegação quando há alterações não salvas.
    function guardedLeave(proceed) {
      if (dirty) setLeaveGuard({ proceed });
      else proceed();
    }
    function discardAndLeave() {
      try { setRecontos(JSON.parse(savedJson)); } catch (e) { setRecontos({}); }
      setConcluida({ a: status.a === "concluida", b: status.b === "concluida", c: status.c === "concluida" }); setDirty(false);
      const proceed = leaveGuard && leaveGuard.proceed; setLeaveGuard(null);
      if (proceed) proceed();
    }
    async function saveAndLeave() {
      const ok = await saveAvaliacao();
      if (!ok) return; // erro: mantém tudo, não sai
      const proceed = leaveGuard && leaveGuard.proceed; setLeaveGuard(null);
      if (proceed) proceed();
    }
    // Salva a avaliação e só navega após a confirmação do salvamento.
    async function salvarEAvancar(nextView) {
      if (readonly) { setView(nextView); goTop(); return; }
      const ok = await saveAvaliacao();
      if (!ok) return; // falha: permanece na tela; o toast de erro já é exibido
      setView(nextView); goTop();
    }

    function openPart(key) { setView(key); goTop(); } // status muda apenas ao salvar

    const concluidas = PARTES.filter((p) => status[p.key] === "concluida").length;

    return (
      <div className="form-wrap">
        <div className="hay-top">
          <button className="pt-back" onClick={() => guardedLeave(onBack)}><Icon name="chevL" size={16} /> Voltar para avaliações</button>
        </div>
        <div className="page-head" style={{ marginBottom: 14 }}>
          <div><h1><Icon name="chat" size={25} /> DNOI — Denominação Oral de Imagens <span style={{ fontSize: 15, fontWeight: 700, color: "var(--text-3)" }}>(4 a 12 anos)</span></h1><p className="sub">Reconto e compreensão de história. Aplicada em papel — registre aqui o desempenho por parte.</p></div>
        </div>

        {/* ---------- 1) Identificação ---------- */}
        {view === "ident" && (
          <>
            <div className="form-card">
              <div className="sec-head"><span className="si"><Icon name="users" size={18} /></span> Identificação</div>
              <div className="fld"><label>Paciente <span className="req">*</span></label><window.PatientPicker value={pid} onChange={pickPatient} /></div>
              <div className="grid-2">
                <div className="fld">
                  <label>Escola</label>
                  <select className="ctrl" value={escolaTipo} onChange={(e) => setEscolaTipo(e.target.value)}>
                    <option value="">Pública ou particular…</option>
                    <option value="publica">Pública</option>
                    <option value="privada">Particular</option>
                  </select>
                </div>
                <div className="fld"><label>Idade</label><input className="ctrl" value={idade} onChange={(e) => setIdade(e.target.value)} placeholder="Ex: 9 anos" /></div>
              </div>
              <div className="fld" style={{ marginBottom: 0 }}><label>Data</label><input className="ctrl" type="date" value={date} onChange={(e) => setDate(e.target.value)} /></div>
            </div>

            {err && <p className="form-err" style={{ display: "flex", alignItems: "center", gap: 7, color: "#EF4444", fontWeight: 700, fontSize: 13.5, margin: "12px 0 0" }}><Icon name="x" size={15} /> {err}</p>}

            <div className="hay-nav">
              <button className="btn btn-ghost btn-sm" onClick={onBack}><Icon name="chevL" size={16} /> Cancelar</button>
              <button className="btn btn-primary btn-sm" onClick={novaAvaliacao} disabled={!pid}><Icon name="plus" size={16} /> Iniciar nova avaliação</button>
            </div>

            {pid && listaAvals.length > 0 && (
              <>
                <p className="section-label">AVALIAÇÕES DESTA CRIANÇA</p>
                {listaAvals.map((a) => {
                  const done = PARTES.filter((pp) => a.status && a.status[pp.key] === "concluida").length;
                  return (
                    <div className="team-row" key={a.id}>
                      <span className="ic-icon" style={{ width: 38, height: 38, flex: "none" }}><Icon name="chat" size={18} /></span>
                      <div className="tr-info" style={{ flex: 1 }}>
                        <b>{a.patientName || "—"} {a.finalizada && <span className="tag ok" style={{ marginLeft: 4, fontSize: 10.5, padding: "1px 7px" }}><i className="d"></i>Finalizada</span>}</b>
                        <span>{a.date} · {done}/{PARTES.length} partes{a.finalizada ? "" : " · em andamento"}</span>
                      </div>
                      {a.finalizada
                        ? <button className="btn btn-ghost btn-sm" onClick={() => abrirAval(a, true)}><Icon name="eye" size={15} /> Ver</button>
                        : <button className="btn btn-primary btn-sm" onClick={() => abrirAval(a, false)}><Icon name="arrowRight" size={15} /> Retomar</button>}
                    </div>
                  );
                })}
              </>
            )}

            {pid && listaAvals.length > 0 && (
              <div style={{ marginTop: 14, textAlign: "center" }}>
                <button type="button" onClick={() => setShowClear(true)} style={{ background: "none", border: "none", cursor: "pointer", color: "#DC2626", fontSize: 12.5, fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 6 }}>
                  <Icon name="x" size={14} /> Apagar as avaliações de {patient ? patient.name : "esta criança"}
                </button>
              </div>
            )}
          </>
        )}

        {/* ---------- 2) Seleção de etapas ---------- */}
        {view === "menu" && (
          <>
            <div className="hay-sheet-id" style={{ marginBottom: 16 }}>
              <span><b>Aluno(a):</b> {patient ? patient.name : "—"}</span>
              {idade && <span><b>Idade:</b> {idade}</span>}
              {escolaTipo && <span><b>Escola:</b> {tipoLabel(escolaTipo)}</span>}
              <span><b>Data:</b> {date}</span>
            </div>

            <p className="section-label">ETAPAS DO TESTE — sequência sugerida A → B → C · {concluidas}/{PARTES.length} concluídas</p>

            <div>
              {PARTES.map((pt, i) => {
                const st = STATUS[status[pt.key]];
                return (
                  <React.Fragment key={pt.key}>
                    <button type="button" className="instr-card" onClick={() => openPart(pt.key)}
                      style={{ cursor: "pointer", textAlign: "left", width: "100%", marginBottom: 0 }}>
                      <div className="ic-head" style={{ marginBottom: 0, width: "100%", alignItems: "center" }}>
                        <span className="ic-icon" style={{ position: "relative" }}>
                          <Icon name={pt.icon} size={22} />
                          <span style={{ position: "absolute", top: -7, left: -7, width: 20, height: 20, borderRadius: "50%", background: "var(--primary)", color: "#fff", fontSize: 11, fontWeight: 800, display: "grid", placeItems: "center", border: "2px solid var(--surface)" }}>{pt.n}</span>
                        </span>
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <h4 style={{ marginBottom: 2 }}>{pt.titulo}</h4>
                          <p className="desc" style={{ margin: 0 }}>{pt.desc}</p>
                        </div>
                        <span className={"tag " + st.tag} style={{ flex: "none" }}><i className="d"></i>{st.l}</span>
                      </div>
                    </button>
                    {i < PARTES.length - 1 && (
                      <div style={{ display: "grid", placeItems: "center", color: "var(--text-3)", margin: "6px 0" }}><Icon name="chevDown" size={20} /></div>
                    )}
                  </React.Fragment>
                );
              })}
            </div>

            <div className="hay-nav" style={{ marginTop: 16 }}>
              <button className="btn btn-ghost btn-sm" onClick={() => { setView("ident"); goTop(); }}><Icon name="chevL" size={16} /> Voltar aos dados</button>
              {onOpenPatient && pid && <button className="btn btn-ghost btn-sm" onClick={() => onOpenPatient(pid)}><Icon name="users" size={16} /> Ver no prontuário</button>}
            </div>
          </>
        )}

        {/* ---------- 3) Parte (A/B/C) — placeholder ---------- */}
        {activePart && (
          <>
            <div className="hay-sheet-id" style={{ marginBottom: 16 }}>
              <span><b>Aluno(a):</b> {patient ? patient.name : "—"}</span>
              {idade && <span><b>Idade:</b> {idade}</span>}
              <span><b>Data:</b> {date}</span>
              <span><b>Status:</b> {STATUS[status[activePart.key]].l}</span>
            </div>

            <div style={{ marginBottom: 12 }}>
              <h2 style={{ fontSize: 17, fontWeight: 800, margin: 0, display: "flex", alignItems: "center", gap: 8 }}><Icon name={activePart.icon} size={20} /> {activePart.titulo}</h2>
            </div>

            {activePart.key === "a" ? (
              <ParteA recontos={recontos} updateReconto={updateReconto} />
            ) : activePart.key === "b" ? (
              <ParteB recontos={recontos} updateReconto={updateReconto} />
            ) : (
              <ParteC recontos={recontos} updateReconto={updateReconto} />
            )}

            {readonly ? (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => { setView("menu"); goTop(); }}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <span style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}><Icon name="eye" size={14} /> Modo consulta — avaliação finalizada (somente leitura)</span>
              </div>
            ) : (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => guardedLeave(() => { setView("menu"); goTop(); })}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <label style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13.5, fontWeight: 700, color: "var(--text-2)", cursor: "pointer" }}>
                  <input type="checkbox" checked={concluida[activePart.key]} onChange={() => toggleConcluida(activePart.key)} /> Marcar Parte {activePart.key.toUpperCase()} como concluída
                </label>
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginLeft: "auto", flexWrap: "wrap" }}>
                  {dirty
                    ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 700, color: "var(--accent-strong)" }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)" }} /> Alterações não salvas</span>
                    : (savedAt && <span style={{ fontSize: 12.5, fontWeight: 700, color: "var(--primary)" }}>Salvo ✓ {savedAt}</span>)}
                  <button className="btn btn-primary btn-sm" onClick={saveAvaliacao} disabled={saving || !dirty}>
                    <Icon name="clipboardCheck" size={16} /> {saving ? "Salvando…" : "Salvar avaliação"}
                  </button>
                  {concluida[activePart.key] && (
                    activePart.next
                      ? <button className="btn btn-primary btn-sm" onClick={() => salvarEAvancar(activePart.next)} disabled={saving}>{saving ? "Salvando…" : "Ir para Parte " + activePart.next.toUpperCase()} <Icon name="arrowRight" size={16} /></button>
                      : <button className="btn btn-primary btn-sm" onClick={finalizarAvaliacao} disabled={saving}><Icon name="clipboardCheck" size={16} /> {saving ? "Salvando…" : "Finalizar avaliação"}</button>
                  )}
                </div>
              </div>
            )}
          </>
        )}

        {leaveGuard && (
          <div className="modal-scrim" onClick={() => setLeaveGuard(null)}>
            <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 440 }}>
              <div className="modal-head">
                <div><h3>Alterações não salvas</h3><p>Há alterações não salvas nesta avaliação.</p></div>
                <button className="x" onClick={() => setLeaveGuard(null)} aria-label="Fechar"><Icon name="x" size={18} /></button>
              </div>
              <div className="modal-body">
                <p className="help-text" style={{ marginTop: 0 }}>Deseja salvar antes de sair?</p>
                <div className="form-actions" style={{ marginTop: 14, display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setLeaveGuard(null)}>Cancelar</button>
                  <button className="btn btn-ghost btn-sm" onClick={discardAndLeave}>Sair sem salvar</button>
                  <button className="btn btn-primary btn-sm" onClick={saveAndLeave} disabled={saving}><Icon name="clipboardCheck" size={15} /> {saving ? "Salvando…" : "Salvar e sair"}</button>
                </div>
              </div>
            </div>
          </div>
        )}

        {showClear && (
          <div className="modal-scrim" onClick={() => setShowClear(false)}>
            <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 440 }}>
              <div className="modal-head">
                <div><h3>Apagar as avaliações desta criança?</h3><p>Esta ação não pode ser desfeita.</p></div>
                <button className="x" onClick={() => setShowClear(false)} aria-label="Fechar"><Icon name="x" size={18} /></button>
              </div>
              <div className="modal-body">
                <p className="help-text" style={{ marginTop: 0 }}>Todas as avaliações DNOI de <b>{patient ? patient.name : "esta criança"}</b> (em andamento e finalizadas) serão apagadas permanentemente. As demais crianças não são afetadas.</p>
                <div className="form-actions" style={{ marginTop: 14, display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setShowClear(false)}>Cancelar</button>
                  <button className="btn btn-sm" style={{ background: "#DC2626", color: "#fff" }} onClick={apagarAvalsDoPaciente}><Icon name="x" size={15} /> Apagar</button>
                </div>
              </div>
            </div>
          </div>
        )}

        {toast && (
          <div style={{ position: "fixed", left: "50%", bottom: 22, transform: "translateX(-50%)", zIndex: 90, display: "flex", alignItems: "center", gap: 9, padding: "11px 16px", borderRadius: 12, boxShadow: "var(--shadow-lg)", fontSize: 13.5, fontWeight: 700, color: "#fff", background: toast.kind === "err" ? "#DC2626" : "var(--primary)" }}>
            <Icon name={toast.kind === "err" ? "x" : "clipboardCheck"} size={16} /> {toast.text}
          </div>
        )}
      </div>
    );
  }

  window.DnoiTest = DnoiTest;
})();
