// 拆解结果报告 — SPA 内 React 组件，替代旧 result.html / app.js iframe 嵌入。
// 数据来自 GET /api/jobs/{id}/result，shape 对齐 schemas_v4.AnalysisResultV4。
// 12 个 section 只读展示，不带可编辑功能（M3.5 再说）。

const RR = (() => {
  // ============== 工具 ==============
  const safeArr = (x) => Array.isArray(x) ? x : [];
  const safeStr = (x) => typeof x === "string" ? x : x == null ? "" : String(x);
  const fmtSec = (s) => {
    const n = Number(s);
    if (!isFinite(n) || n < 0) return "—";
    if (n < 60) return `${n.toFixed(1)}s`;
    const m = Math.floor(n / 60); const r = n - m * 60;
    return `${m}m${r.toFixed(0)}s`;
  };
  const pct = (v) => `${Math.round((Number(v) || 0) * 100)}%`;

  // ============== 容器 ==============
  const Section = ({ idx, title, badge, children, style }) => (
    <section className="rr-sec" style={style}>
      <div className="rr-sec-hd">
        <span className="rr-sec-idx">§ {String(idx).padStart(2, "0")}</span>
        <h3 className="rr-sec-title">{title}</h3>
        {badge && <span className="rr-sec-badge">{badge}</span>}
      </div>
      <div className="rr-sec-body">{children}</div>
    </section>
  );

  const Card = ({ title, children, style }) => (
    <div className="rr-card" style={style}>
      {title && <div className="rr-card-title">{title}</div>}
      <div className="rr-card-body">{children}</div>
    </div>
  );

  const KV = ({ k, v, mono = false }) => (
    <div className="rr-kv">
      <span className="rr-kv-k">{k}</span>
      <span className={`rr-kv-v ${mono ? "mono" : ""}`}>{v || "—"}</span>
    </div>
  );

  const Chip = ({ children, tone }) => (
    <span className={`rr-chip ${tone || ""}`}>{children}</span>
  );

  const Empty = ({ msg = "暂无数据" }) => (
    <div className="rr-empty">{msg}</div>
  );

  const Loading = () => (
    <div className="rr-loading">
      <Icon name="refresh" size={14} />
      <span>加载拆解结果…</span>
    </div>
  );

  const ErrorBox = ({ message }) => (
    <div className="rr-error">
      <Icon name="close" size={12} />
      <div className="col" style={{ gap: 4 }}>
        <span>无法加载拆解结果</span>
        <span style={{ fontSize: 11, color: "var(--fg-3)" }}>{message}</span>
      </div>
    </div>
  );

  // ============== 1. Header ==============
  const ResultHeader = ({ meta = {}, jobId }) => {
    const dl = (kind) => {
      window.open(`/api/jobs/${encodeURIComponent(jobId)}/download/${kind}`, "_blank");
    };
    return (
      <div className="rr-header">
        <div className="col" style={{ flex: 1, minWidth: 0, gap: 4 }}>
          <div className="rr-job-id">JOB · {jobId?.slice(0, 12) || "—"}</div>
          <h2 className="rr-title">{meta.title || meta.video_filename || "未命名拆解任务"}</h2>
          <div className="rr-meta-line">
            {meta.industry && <span>{meta.industry}</span>}
            {meta.duration_sec && <span>· 时长 {fmtSec(meta.duration_sec)}</span>}
            {meta.resolution && <span>· {meta.resolution}</span>}
            {meta.platform_hint && <span>· {meta.platform_hint}</span>}
          </div>
        </div>
        <div className="row" style={{ gap: 6 }}>
          <button className="btn ghost" onClick={() => dl("video")} title="下载原视频">
            <Icon name="download" size={11} />原视频
          </button>
          <button className="btn ghost" onClick={() => dl("json")} title="下载 JSON 拆解结果">
            <Icon name="download" size={11} />JSON
          </button>
          <button className="btn ghost" onClick={() => dl("md")} title="下载 Markdown 报告">
            <Icon name="download" size={11} />MD
          </button>
        </div>
      </div>
    );
  };

  // ============== 2. SummaryCards ==============
  const SummaryCards = ({ stream, dual }) => {
    const sv = stream || {};
    const dd = dual || {};
    const ct = dd.content_tone || {};
    const rer = dd.real_estate_relevance || {};
    return (
      <Section idx={0} title="分类总览">
        <div className="rr-grid-4">
          <Card title="一级 · 流量形态">
            <div className="rr-big">{sv.stream_label || "—"}</div>
            <div className="rr-conf">置信度 {pct(sv.confidence)}</div>
            {sv.reasoning && <div className="rr-reason">{sv.reasoning}</div>}
            {safeArr(sv.signals).length > 0 && (
              <div className="rr-chip-flow">
                {sv.signals.map((s, i) => <Chip key={i}>{s}</Chip>)}
              </div>
            )}
          </Card>
          <Card title="内容调性">
            <div className="rr-big">{ct.tone_label || "—"}</div>
            <div className="rr-conf">置信度 {pct(ct.confidence)}</div>
            {ct.reasoning && <div className="rr-reason">{ct.reasoning}</div>}
            {safeArr(ct.key_evidence).length > 0 && (
              <div className="rr-chip-flow">
                {ct.key_evidence.map((s, i) => <Chip key={i} tone="cyan">{s}</Chip>)}
              </div>
            )}
          </Card>
          <Card title="房产相关度">
            <div className="rr-big">{rer.relevance_label || "—"}</div>
            <div className="rr-conf">置信度 {pct(rer.confidence)}</div>
            {rer.reasoning && <div className="rr-reason">{rer.reasoning}</div>}
            {safeArr(rer.hit_keywords).length > 0 && (
              <div className="rr-chip-flow">
                {rer.hit_keywords.map((s, i) => <Chip key={i} tone="purple">{s}</Chip>)}
              </div>
            )}
            {rer.migration_value && (
              <div className="rr-reason"><b>迁移价值：</b>{rer.migration_value}</div>
            )}
          </Card>
          <Card title="综合评估">
            <div className="rr-reason" style={{ marginTop: 0 }}>
              {dd.overall_assessment || "—"}
            </div>
          </Card>
        </div>
      </Section>
    );
  };

  // ============== 3. HookSection ==============
  const HookSection = ({ hook }) => {
    const h = hook || {};
    return (
      <Section idx={1} title="钩子拆解（前 3 秒）"
               badge={h.hook_type || null}>
        {!h.hook_type && !h.reusable_template ? <Empty /> : (
          <>
            <div className="rr-quote">{h.reusable_template || "—"}</div>
            <div className="rr-grid-3">
              <Card title="核心触发词">
                <div className="rr-chip-flow">
                  {safeArr(h.core_trigger_words).length === 0 ? <span className="rr-muted">—</span> :
                    h.core_trigger_words.map((w, i) => <Chip key={i} tone="cyan">{w}</Chip>)}
                </div>
              </Card>
              <Card title="情绪触发点">
                <div className="rr-text">{h.emotional_trigger || "—"}</div>
              </Card>
              <Card title="为什么有效">
                <div className="rr-text">{h.why_it_works || "—"}</div>
              </Card>
            </div>
          </>
        )}
      </Section>
    );
  };

  // ============== 4. CopyStructureTable ==============
  const CopyStructureTable = ({ cs }) => {
    const segs = safeArr(cs?.segments);
    return (
      <Section idx={2} title="文案结构（按时间分段）"
               badge={cs?.overall_structure_pattern || null}>
        {segs.length === 0 ? <Empty msg="未拆出文案分段" /> : (
          <>
            {cs.narrative_arc && <div className="rr-reason">{cs.narrative_arc}</div>}
            <table className="rr-table">
              <thead>
                <tr>
                  <th>#</th><th>时间</th><th>段类型</th><th>转化钩子</th>
                  <th>文案要点</th><th>功能描述</th>
                </tr>
              </thead>
              <tbody>
                {segs.map((s, i) => (
                  <tr key={i}>
                    <td className="mono">{i + 1}</td>
                    <td className="mono">{fmtSec(s.start_sec)}–{fmtSec(s.end_sec)}</td>
                    <td><Chip tone="cyan">{s.segment_type || "—"}</Chip></td>
                    <td>{s.conversion_hook_type || "—"}</td>
                    <td>{s.key_text || "—"}</td>
                    <td>{s.function_description || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </>
        )}
      </Section>
    );
  };

  // ============== 5. VisualRhythmCards ==============
  const VisualRhythmCards = ({ visual }) => {
    const v = visual || {};
    const dist = v.shot_distribution || {};
    const distEntries = Object.entries(dist);
    const cuts = Number(v.avg_cut_frequency) || 0;
    return (
      <Section idx={3} title="视觉节奏">
        <div className="rr-grid-2">
          <Card title="切镜节奏">
            <KV k="平均切镜频率" v={cuts ? `${cuts.toFixed(2)} 次/秒` : "—"} />
            <KV k="字幕风格" v={v.subtitle_style} />
            <KV k="封面逻辑" v={v.cover_logic} />
          </Card>
          <Card title="景别分布 / 色调">
            {distEntries.length > 0 && (
              <div className="rr-bar-flow">
                {distEntries.map(([k, val]) => (
                  <div key={k} className="rr-bar-row">
                    <span className="rr-bar-k">{k}</span>
                    <div className="rr-bar-track">
                      <span className="rr-bar-fill" style={{ width: pct(val) }} />
                    </div>
                    <span className="rr-bar-v mono">{pct(val)}</span>
                  </div>
                ))}
              </div>
            )}
            {safeArr(v.color_tone_keywords).length > 0 && (
              <div className="rr-chip-flow" style={{ marginTop: 10 }}>
                {v.color_tone_keywords.map((c, i) => <Chip key={i} tone="purple">{c}</Chip>)}
              </div>
            )}
          </Card>
        </div>
        {v.visual_model_prompt && (
          <Card title="视频大模型整体提示词" style={{ marginTop: 10 }}>
            <pre className="rr-pre">{v.visual_model_prompt}</pre>
          </Card>
        )}
        {v.camera_language_notes && (
          <Card title="镜头语言备注" style={{ marginTop: 10 }}>
            <div className="rr-text">{v.camera_language_notes}</div>
          </Card>
        )}
      </Section>
    );
  };

  // ============== 6. EmotionCurveSVG ==============
  const EmotionCurveSVG = ({ emotion }) => {
    const e = emotion || {};
    const points = safeArr(e.emotion_points);
    if (points.length === 0) return (
      <Section idx={4} title="情绪曲线"><Empty /></Section>
    );
    // 轨迹归一化：横轴 = timestamp，纵轴 = intensity
    const W = 720, H = 140, P = 24;
    const ts = points.map(p => Number(p.timestamp_sec) || 0);
    const tsMax = Math.max(...ts, 1);
    const polyPoints = points.map((p, i) => {
      const x = P + (Number(p.timestamp_sec) / tsMax) * (W - 2 * P);
      const y = H - P - (Number(p.intensity) || 0) * (H - 2 * P);
      return `${x.toFixed(1)},${y.toFixed(1)}`;
    }).join(" ");
    return (
      <Section idx={4} title="情绪曲线"
               badge={e.dominant_emotion || null}>
        {e.emotion_arc_summary && <div className="rr-reason">{e.emotion_arc_summary}</div>}
        <div className="rr-svg-wrap">
          <svg viewBox={`0 0 ${W} ${H}`} className="rr-svg">
            {/* gridlines */}
            {[0.25, 0.5, 0.75].map(g => {
              const y = H - P - g * (H - 2 * P);
              return <line key={g} x1={P} y1={y} x2={W - P} y2={y}
                           stroke="var(--line-soft)" strokeDasharray="2 4" />;
            })}
            {/* baseline */}
            <line x1={P} y1={H - P} x2={W - P} y2={H - P} stroke="var(--line-1)" />
            {/* curve */}
            <polyline points={polyPoints} fill="none"
                      stroke="var(--cyan)" strokeWidth="1.6"
                      strokeLinecap="round" strokeLinejoin="round" />
            {/* dots */}
            {points.map((p, i) => {
              const x = P + (Number(p.timestamp_sec) / tsMax) * (W - 2 * P);
              const y = H - P - (Number(p.intensity) || 0) * (H - 2 * P);
              return (
                <circle key={i} cx={x} cy={y} r={p.is_climax ? 5 : 3}
                        fill={p.is_climax ? "var(--pink)" : "var(--cyan)"}
                        stroke="var(--bg-0)" strokeWidth="1.5">
                  <title>{`${fmtSec(p.timestamp_sec)} · ${p.emotion} · ${pct(p.intensity)}`}</title>
                </circle>
              );
            })}
          </svg>
        </div>
        <div className="rr-chip-flow">
          {points.map((p, i) => (
            <Chip key={i} tone={p.is_climax ? "pink" : "cyan"}>
              {fmtSec(p.timestamp_sec)} · {p.emotion} · {pct(p.intensity)}
              {p.is_climax && " ★"}
            </Chip>
          ))}
        </div>
      </Section>
    );
  };

  // ============== 7. CommentInsightsCards ==============
  const CommentInsightsCards = ({ comments }) => {
    const c = comments || {};
    return (
      <Section idx={5} title="评论区拆解">
        <div className="rr-grid-2">
          <Card title="高频痛点">
            <ul className="rr-ul">
              {safeArr(c.high_freq_pain_points).map((p, i) => <li key={i}>{p}</li>)}
              {safeArr(c.high_freq_pain_points).length === 0 && <li className="rr-muted">—</li>}
            </ul>
          </Card>
          <Card title="问题盲区">
            <ul className="rr-ul">
              {safeArr(c.question_blind_spots).map((p, i) => <li key={i}>{p}</li>)}
              {safeArr(c.question_blind_spots).length === 0 && <li className="rr-muted">—</li>}
            </ul>
          </Card>
          <Card title="负面反馈">
            <ul className="rr-ul">
              {safeArr(c.negative_feedback_notes).map((p, i) => <li key={i}>{p}</li>)}
              {safeArr(c.negative_feedback_notes).length === 0 && <li className="rr-muted">—</li>}
            </ul>
          </Card>
          <Card title="传播力评估">
            <pre className="rr-pre" style={{ maxHeight: 140 }}>
              {safeStr(c.viral_potential_assessment) || "—"}
            </pre>
          </Card>
        </div>
        {safeArr(c.insights).length > 0 && (
          <Card title="深入洞察" style={{ marginTop: 10 }}>
            <table className="rr-table">
              <thead>
                <tr><th>类型</th><th>描述</th><th>可执行建议</th><th>置信度</th></tr>
              </thead>
              <tbody>
                {c.insights.map((ins, i) => (
                  <tr key={i}>
                    <td><Chip tone="purple">{ins.insight_type || "—"}</Chip></td>
                    <td>{ins.description || "—"}</td>
                    <td>{ins.actionable || "—"}</td>
                    <td className="mono">{pct(ins.confidence)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </Card>
        )}
      </Section>
    );
  };

  // ============== 8. AidsTable ==============
  const AidsTable = ({ aids }) => {
    const a = aids || {};
    if (!a.applicable) return (
      <Section idx={6} title="AIDIS 模型分析"
               badge="不适用">
        <div className="rr-reason">
          {a.overall_comment || "本视频不适合用 AIDIS 框架（A·Attention / I·Interest / D·Desire / I·Information / S·Solution）拆解。"}
        </div>
      </Section>
    );
    const stages = safeArr(a.stages);
    return (
      <Section idx={6} title="AIDIS 模型分析" badge="适用">
        {a.overall_comment && <div className="rr-reason">{a.overall_comment}</div>}
        <table className="rr-table">
          <thead>
            <tr>
              <th>阶段</th><th>占比</th><th>达标</th><th>文案片段</th><th>说明</th>
            </tr>
          </thead>
          <tbody>
            {stages.map((s, i) => (
              <tr key={i}>
                <td><Chip tone="cyan">{s.stage_label || s.stage}</Chip></td>
                <td>
                  <div className="rr-bar-track" style={{ minWidth: 80 }}>
                    <span className="rr-bar-fill" style={{ width: pct(s.char_percent) }} />
                  </div>
                  <span className="mono" style={{ fontSize: 10, marginLeft: 6 }}>{pct(s.char_percent)}</span>
                </td>
                <td>{s.meets_criterion ? <Chip tone="green">✓</Chip> : <Chip tone="pink">✗</Chip>}</td>
                <td className="mono" style={{ fontSize: 11 }}>{s.script_excerpt || "—"}</td>
                <td>{s.notes || "—"}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Section>
    );
  };

  // ============== 9. RawScriptViewer ==============
  const RawScriptViewer = ({ raw, timed }) => {
    const [tab, setTab] = useState("plain");
    const hasTimed = safeArr(timed).length > 0;
    return (
      <Section idx={7} title="原文案逐字稿">
        <div className="rr-tabs">
          <button className={`rr-tab ${tab === "plain" ? "active" : ""}`} onClick={() => setTab("plain")}>
            纯文本
          </button>
          {hasTimed && (
            <button className={`rr-tab ${tab === "timed" ? "active" : ""}`} onClick={() => setTab("timed")}>
              带时间戳
            </button>
          )}
        </div>
        {tab === "plain" ? (
          <pre className="rr-pre rr-pre-script">{safeStr(raw) || "—"}</pre>
        ) : (
          <table className="rr-table">
            <thead><tr><th>时间</th><th>内容</th></tr></thead>
            <tbody>
              {timed.map((seg, i) => (
                <tr key={i}>
                  <td className="mono">{fmtSec(seg.start_sec)}–{fmtSec(seg.end_sec)}</td>
                  <td>{seg.text}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </Section>
    );
  };

  // ============== 10. RewriteSection ==============
  const RewriteSection = ({ rewrite }) => {
    const r = rewrite || {};
    if (!r.strategy || r.strategy === "not_applicable") return (
      <Section idx={8} title="二创 + 分镜" badge="不适用">
        <div className="rr-reason">
          {r.skipped_reason || "本视频未生成二创建议（房产相关度判定为无迁移价值）。"}
        </div>
      </Section>
    );
    return (
      <Section idx={8} title="二创文案" badge={r.strategy_label || r.strategy}>
        <Card title="二创文案">
          <pre className="rr-pre rr-pre-script">{safeStr(r.rewritten_script) || "—"}</pre>
        </Card>
        <div className="rr-grid-2" style={{ marginTop: 10 }}>
          <Card title="保留元素">
            <ul className="rr-ul">
              {safeArr(r.preserved_elements).map((p, i) => <li key={i}>{p}</li>)}
              {safeArr(r.preserved_elements).length === 0 && <li className="rr-muted">—</li>}
            </ul>
          </Card>
          <Card title="替换元素">
            <ul className="rr-ul">
              {safeArr(r.changed_elements).map((p, i) => <li key={i}>{p}</li>)}
              {safeArr(r.changed_elements).length === 0 && <li className="rr-muted">—</li>}
            </ul>
          </Card>
        </div>
        <div className="rr-muted" style={{ marginTop: 10, fontSize: 12 }}>
          画面级 AIGC 提示词与时长打包属于复刻流程（chunk_regroup → r_chunks），见 /remix 复刻入口；
          aspect_ratio: {r.aspect_ratio || "9:16"}{r.negative_prompt ? ` · negative: ${r.negative_prompt}` : ""}
        </div>
      </Section>
    );
  };

  // ============== 11. VideoGenSection ==============
  const VideoGenSection = ({ video }) => {
    const v = video || {};
    if (!v.status || v.status === "pending") return null;
    const shots = safeArr(v.shots);
    return (
      <Section idx={9} title="视频生成" badge={v.platform || null}>
        <KV k="状态" v={
          <Chip tone={v.status === "completed" ? "green" : v.status === "failed" ? "pink" : "cyan"}>
            {v.status}
          </Chip>
        } />
        {v.error && <div className="rr-error" style={{ marginTop: 8 }}>{v.error}</div>}
        {v.final_video_path && (
          <video controls className="rr-video"
                 src={`/static/${v.final_video_path.replace(/^app\/static\//, "")}`} />
        )}
        {shots.length > 0 && (
          <table className="rr-table" style={{ marginTop: 10 }}>
            <thead><tr><th>#</th><th>状态</th><th>平台</th><th>外部任务</th><th>错误</th></tr></thead>
            <tbody>
              {shots.map((s, i) => (
                <tr key={i}>
                  <td className="mono">{(s.shot_index ?? i) + 1}</td>
                  <td>
                    <Chip tone={s.status === "completed" ? "green" : s.status === "failed" ? "pink" : "cyan"}>
                      {s.status || "—"}
                    </Chip>
                  </td>
                  <td>{s.platform || "—"}</td>
                  <td className="mono" style={{ fontSize: 10 }}>{s.external_task_id || "—"}</td>
                  <td>{s.error || "—"}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </Section>
    );
  };

  // ============== 12.0 RewriteTextCell · 段级二创可编辑单元格 (M5 加) ==============
  // 显示 rewrite_text；点"编辑"切到 textarea；保存调 PATCH /jobs/{id}/chunks/{cid}/rewrite-text
  const RewriteTextCell = ({ jobId, chunkId, value, editedAt, onSaved }) => {
    const [editing, setEditing] = useState(false);
    const [draft, setDraft] = useState(value || "");
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState("");

    React.useEffect(() => { setDraft(value || ""); }, [value]);

    if (!editing) {
      return (
        <div>
          {value ? <div>{value}</div> : <span className="rr-muted">—</span>}
          <div className="row gap-4" style={{ marginTop: 4, alignItems: "center" }}>
            <button onClick={() => { setEditing(true); setErr(""); }}
              style={{ fontSize: 10, padding: "2px 8px", borderRadius: 3,
                       background: "var(--bg-1)", color: "var(--cyan)",
                       border: "1px solid var(--cyan)", cursor: "pointer" }}>
              ✎ 编辑
            </button>
            {editedAt && (
              <span style={{ fontSize: 9, color: "var(--orange)" }}>· 已人工编辑</span>
            )}
          </div>
        </div>
      );
    }
    const save = async () => {
      const trimmed = (draft || "").trim();
      if (!trimmed) { setErr("不能为空"); return; }
      setBusy(true); setErr("");
      try {
        await API.jobs.patchRewriteText(jobId, chunkId, trimmed);
        setEditing(false);
        onSaved && onSaved();
      } catch (e) {
        setErr(e.message || String(e));
      } finally { setBusy(false); }
    };
    return (
      <div className="col gap-3">
        <textarea value={draft} disabled={busy}
          onChange={(e) => setDraft(e.target.value)}
          style={{ width: "100%", minHeight: 72, fontSize: 12, lineHeight: 1.5,
                   padding: 6, borderRadius: 3, fontFamily: "inherit",
                   border: "1px solid var(--cyan)", boxSizing: "border-box" }}/>
        {err && <div style={{ fontSize: 10, color: "var(--pink)" }}>{err}</div>}
        <div className="row gap-4">
          <button onClick={save} disabled={busy}
            style={{ fontSize: 10, padding: "3px 10px", borderRadius: 3,
                     background: "var(--cyan)", color: "var(--bg-0)",
                     border: "1px solid var(--cyan)", cursor: busy ? "wait" : "pointer" }}>
            {busy ? "保存中…" : "✓ 保存"}
          </button>
          <button onClick={() => { setEditing(false); setDraft(value || ""); setErr(""); }}
            disabled={busy}
            style={{ fontSize: 10, padding: "3px 10px", borderRadius: 3,
                     background: "var(--bg-1)", color: "var(--fg-1)",
                     border: "1px solid var(--line-1)", cursor: "pointer" }}>
            取消
          </button>
        </div>
      </div>
    );
  };


  // ============== 12. ChunkAlignmentSection (M4) ==============
  // 原 ASR chunks 与 R2 复刻 rewrite_chunks 的 1-1 对齐展示。
  // 老 _v4.json（无 chunks 字段）下整段不渲染。
  const ChunkAlignmentSection = ({ jobId, chunks, rewriteChunks, onUpdated }) => {
    const cs = safeArr(chunks);
    if (cs.length === 0) return null;   // 老链路或 ASR 跳过：不渲染
    const rcs = safeArr(rewriteChunks);
    const rcMap = {};
    rcs.forEach(rc => { if (rc && rc.chunk_id) rcMap[rc.chunk_id] = rc; });
    const aligned = rcs.length === cs.length;

    return (
      <Section idx={9} title="第一轮 chunks（c_chunks）对齐表"
        badge={`${cs.length} chunks · ${aligned ? "1-1 对齐" : `复刻 ${rcs.length} 条`}`}
      >
        <div className="rr-muted" style={{ marginBottom: 8 }}>
          按豆包 Seed-ASR 2.0 句切产出的第一轮 chunks（c_001…），文本已经过 LLM 同音字修正。
          R2 二创产出的 rewrite_chunks 与之严格 1:1（每条 = 该 chunk 的口播稿）。
          画面级 AIGC 提示词与时长打包属于复刻流程的第二轮 r_chunks，见下方。
        </div>
        <div style={{ overflowX: "auto" }}>
          <table className="rr-table" style={{ minWidth: 920 }}>
            <thead>
              <tr>
                <th style={{ width: 64 }}>chunk_id</th>
                <th style={{ width: 88 }}>起止 / 时长</th>
                <th style={{ minWidth: 200 }}>原文</th>
                <th style={{ minWidth: 200 }}>复刻文</th>
                <th style={{ minWidth: 240 }}>AIGC 提示词</th>
                <th style={{ width: 80 }}>切片</th>
              </tr>
            </thead>
            <tbody>
              {cs.map((c, i) => {
                const rc = rcMap[c.chunk_id];
                const dur = (c.duration_sec ?? (c.t_end - c.t_start)) || 0;
                const promptShort = rc && rc.aigc_prompt
                  ? (rc.aigc_prompt.length > 100 ? rc.aigc_prompt.slice(0, 100) + "…" : rc.aigc_prompt)
                  : "";
                return (
                  <tr key={c.chunk_id || i}>
                    <td className="mono">{c.chunk_id}</td>
                    <td className="mono rr-muted">
                      {fmtSec(c.t_start)}–{fmtSec(c.t_end)}<br/>
                      <span style={{ fontSize: 11 }}>{dur.toFixed(2)}s</span>
                    </td>
                    <td>
                      {safeStr(c.text) || <span className="rr-muted">—</span>}
                      {c.text_original && c.text_original !== c.text && (
                        <div className="rr-muted" style={{ fontSize: 11, marginTop: 4 }}>
                          原文：{c.text_original}
                        </div>
                      )}
                    </td>
                    <td>
                      {rc ? (
                        <RewriteTextCell jobId={jobId} chunkId={c.chunk_id}
                          value={rc.rewrite_text} editedAt={rc.edited_manually_at}
                          onSaved={onUpdated}/>
                      ) : <span className="rr-muted">—</span>}
                    </td>
                    <td className="rr-muted" style={{ fontSize: 12 }}>{promptShort || "—"}</td>
                    <td>
                      {c.clip_path
                        ? <span className="mono" style={{ color: "#10b981" }} title={c.clip_path}>✓</span>
                        : <span className="rr-muted">—</span>}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        {!aligned && (
          <div className="rr-warn" style={{ marginTop: 8 }}>
            ⚠️ rewrite_chunks 长度（{rcs.length}）≠ chunks 长度（{cs.length}），R2 对齐失败；请重新拆解
          </div>
        )}
      </Section>
    );
  };

  // ============== TranscriptLinkSection（原始文件包 + 修正记录）==============
  const TranscriptLinkSection = ({ meta }) => {
    const tp = meta?.transcript_path;
    const cp = meta?.corrected_transcript_path;
    if (!tp && !cp) return null;
    return (
      <Section idx={10} title="原始文件包" badge="ASR + LLM 修正稿">
        <div className="rr-muted" style={{ marginBottom: 8 }}>
          豆包 ASR 完整原始返回 + LLM 修正后的逐字稿；下游 chunks / 拆解 / 复刻都从这里读。
        </div>
        <ul className="rr-ul">
          {tp && <li><a href={`/${tp}`} target="_blank" rel="noopener" className="mono">{tp}</a> · 豆包原始（含 audio_info / utterances / words[]）</li>}
          {cp && <li><a href={`/${cp}`} target="_blank" rel="noopener" className="mono">{cp}</a> · LLM 修正后稿子（含 corrections 修正记录）</li>}
        </ul>
      </Section>
    );
  };

  // ============== RegroupedChunkSection（第二轮 r_chunks，复刻产物）==============
  const RegroupedChunkSection = ({ chunkRegroup }) => {
    const cr = chunkRegroup || null;
    const r_chunks = safeArr(cr && cr.r_chunks);
    return (
      <Section idx={11} title="第二轮 chunks（r_chunks · AIGC 视频片段计划）"
        badge={r_chunks.length ? `${r_chunks.length} 段 · ${cr.platform || ''}` : "等复刻"}
      >
        {!r_chunks.length ? (
          <div className="rr-muted">
            第二轮 chunks 由复刻流程的 chunk_regroup stage 产出（每个 r_chunk 对应一次云端
            视频生成调用，时长对齐 platform 档位）。当前任务尚未触发复刻，去 /remix 选择
            视频生成 platform + 凭据后即可生成。
          </div>
        ) : (
          <div style={{ overflowX: "auto" }}>
            <table className="rr-table" style={{ minWidth: 920 }}>
              <thead>
                <tr>
                  <th style={{ width: 64 }}>r_chunk_id</th>
                  <th style={{ minWidth: 120 }}>包含 c_chunks</th>
                  <th style={{ width: 88 }}>时长(s)</th>
                  <th style={{ minWidth: 240 }}>AIGC 提示词（画面）</th>
                  <th style={{ minWidth: 200 }}>口播文案（拼接）</th>
                  <th style={{ width: 120 }}>原画面引用</th>
                </tr>
              </thead>
              <tbody>
                {r_chunks.map((rc, i) => {
                  const useOrig = rc.use_original_clip_user != null
                    ? rc.use_original_clip_user
                    : !!rc.use_original_clip_default;
                  return (
                    <tr key={rc.chunk_id || i}>
                      <td className="mono">{rc.chunk_id}</td>
                      <td className="mono rr-muted">{(rc.source_chunk_ids || []).join(", ")}</td>
                      <td className="mono">{(rc.duration_sec || 0).toFixed(1)}</td>
                      <td className="rr-muted" style={{ fontSize: 12 }}>{safeStr(rc.aigc_prompt)}</td>
                      <td>{safeStr(rc.rewrite_text)}</td>
                      <td>
                        <Chip tone={useOrig ? "green" : "cyan"}>
                          {useOrig ? "用原画面" : "纯 AIGC"}
                        </Chip>
                        {rc.use_original_clip_user != null && (
                          <span className="rr-muted" style={{ fontSize: 10, marginLeft: 4 }}>(用户改)</span>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Section>
    );
  };

  // ============== 顶层 ResultReport ==============
  const useState = React.useState;
  const useEffect = React.useEffect;

  const ResultReport = ({ jobId }) => {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(true);

    const fetchData = React.useCallback(() => {
      if (!jobId) { setData(null); setLoading(false); return; }
      setLoading(true);
      setError(null);
      API.jobs.result(jobId)
        .then(d => { setData(d); })
        .catch(e => { setError(e.message || String(e)); })
        .finally(() => { setLoading(false); });
    }, [jobId]);

    useEffect(() => { fetchData(); }, [fetchData]);

    if (loading) return <Loading />;
    if (error) return <ErrorBox message={error} />;
    if (!data) return <ErrorBox message="无返回数据" />;

    // 图文笔记分支：完全不走 12 section 视频拆解视图
    if (data.meta?.is_image_post) {
      return <ImagePostReport meta={data.meta} post={data.image_post || {}} />;
    }
    // 图文复刻 child job：渲染 plan + 出图按钮
    if (data.meta?.is_image_replicate) {
      return <ImageReplicateReport meta={data.meta}
                                    replication={data.image_replication || {}}
                                    childJobId={jobId}
                                    onChange={fetchData}/>;
    }

    return (
      <div className="rr">
        <ResultHeader meta={data.meta || {}} jobId={jobId} />
        <SummaryCards stream={data.stream_verdict} dual={data.dual_dimension} />
        <HookSection hook={data.five_dimensions?.hook} />
        <CopyStructureTable cs={data.five_dimensions?.copy_structure} />
        <VisualRhythmCards visual={data.five_dimensions?.visual_rhythm} />
        <EmotionCurveSVG emotion={data.five_dimensions?.emotion_curve} />
        <CommentInsightsCards comments={data.five_dimensions?.comments} />
        <AidsTable aids={data.aids} />
        <RawScriptViewer raw={data.raw_script} timed={data.raw_script_timed} />
        <RewriteSection rewrite={data.rewrite} />
        <ChunkAlignmentSection jobId={jobId} chunks={data.chunks} rewriteChunks={data.rewrite?.rewrite_chunks} onUpdated={() => fetchData()} />
        <TranscriptLinkSection meta={data.meta} />
        <RegroupedChunkSection chunkRegroup={data.chunk_regroup} />
        <VideoGenSection video={data.video_generation} />
      </div>
    );
  };

  // ============== 图文复刻 child job 视图 ==============
  const ImageReplicateReport = ({ meta, replication, childJobId, onChange }) => {
    const plan = replication.plan || {};
    const gen = replication.image_generation || null;
    const planErr = replication.plan_error;
    const [credentials, setCredentials] = useState([]);
    const [chosenCredId, setChosenCredId] = useState("");
    const [generating, setGenerating] = useState(false);
    const [genMsg, setGenMsg] = useState(null);

    useEffect(() => {
      API.credentials.list().then(d => {
        // 只列支持出图的 provider
        const supported = new Set(["siliconflow", "openai_multimodal",
                                   "openai_compat", "gemini", "seedance"]);
        setCredentials((d.items || []).filter(c => supported.has(c.provider)));
      }).catch(() => setCredentials([]));
    }, []);

    const startGen = async () => {
      if (generating || !chosenCredId) return;
      setGenerating(true); setGenMsg(null);
      try {
        const r = await API.replicate.generateImages(childJobId, Number(chosenCredId));
        setGenMsg({
          kind: r.failed > 0 ? "warn" : "ok",
          msg: `出图完成：成功 ${r.generated} / 失败 ${r.failed} · ${r.provider}:${r.model_used} · ${r.elapsed_sec}s`,
        });
        onChange && onChange();
      } catch (e) {
        setGenMsg({ kind: "err", msg: String(e.message || e) });
      } finally {
        setGenerating(false);
      }
    };

    const hooks = safeArr(plan.hooks);
    const newImgs = safeArr(plan.new_images);
    const techs = safeArr(plan.anti_dup_techniques);
    const genItems = safeArr(gen?.items);
    // 把 gen.items.filename 反查回每张 plan image
    const genByIndex = {};
    for (const it of genItems) genByIndex[it.index] = it;

    return (
      <div className="rr">
        <Section idx={1} title="图文复刻 · 方案概览" badge="image_replicate">
          <div className="row gap-12" style={{ flexWrap: "wrap", alignItems: "center" }}>
            <Chip tone="cyan">公式：{plan.formula_id || "—"}</Chip>
            <Chip>父任务：{(meta.parent_job_id || "").slice(0, 8)}</Chip>
            {meta.template_name && <Chip tone="cyan">模板：{meta.template_name}</Chip>}
            <Chip>目标 {replication.target_image_count || newImgs.length} 张</Chip>
            <span className="mono" style={{ fontSize: 11, color: "var(--fg-3)" }}>
              LLM 耗时 {meta.analyze_elapsed_sec || "—"} s
            </span>
          </div>
          {planErr && (
            <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: 4,
                          background: "color-mix(in oklch, var(--pink) 14%, var(--bg-1))",
                          color: "var(--pink)", fontSize: 11 }}>
              方案生成异常：{planErr}
            </div>
          )}
          {plan.skeleton && (
            <div style={{ marginTop: 10, fontSize: 12, color: "var(--fg-1)" }}>
              <b>骨架：</b>{plan.skeleton}
            </div>
          )}
          {plan.emotion_curve && (
            <div style={{ marginTop: 4, fontSize: 12, color: "var(--fg-2)" }}>
              <b>情绪曲线：</b>{plan.emotion_curve}
            </div>
          )}
        </Section>

        {/* 出图入口 —— 用户 review 完 plan 决定是否真烧 t2i */}
        <Section idx={2} title="出图（父帖原图 ref + prompt 走 i2i）" badge="i2i">
          <div style={{
            padding: "10px 12px", borderRadius: 5,
            border: "1px solid var(--purple)",
            background: "color-mix(in oklch, var(--purple) 8%, var(--bg-1))",
          }}>
            <div style={{ fontSize: 11.5, color: "var(--fg-2)", marginBottom: 8 }}>
              出图逻辑：把父帖第 i 张图作为<b>参考图</b> + plan.new_images[i].prompt_zh
              作为目标描述，喂给 i2i 接口（SiliconFlow FLUX / OpenAI Edits / Gemini Nano Banana / 火山 Seedream）。
              保留爆款的构图、镜头、视觉氛围，只换内容；落到{" "}
              <span className="mono">data/image_clips/{childJobId}/</span>，图片素材库自动收。
            </div>
            <div className="row gap-8" style={{ alignItems: "center", flexWrap: "wrap" }}>
              <select value={chosenCredId} onChange={e => setChosenCredId(e.target.value)}
                      disabled={generating}
                      style={{ fontSize: 11.5, padding: "4px 8px",
                               border: "1px solid var(--line-1)", borderRadius: 4,
                               background: "white", color: "var(--fg-0)",
                               fontFamily: "inherit", maxWidth: 320 }}>
                <option value="">— 选 t2i 凭据 —</option>
                {credentials.map(c => (
                  <option key={c.id} value={c.id}>
                    [{c.provider}] {c.name}
                    {c.extra?.model_name ? ` · ${c.extra.model_name}` : ""}
                    {c.is_active ? " ✓" : ""}
                  </option>
                ))}
              </select>
              <button onClick={startGen} disabled={generating || !chosenCredId}
                      className="btn sm primary"
                      style={{ background: !generating && chosenCredId ? "var(--purple)" : undefined,
                               color: !generating && chosenCredId ? "var(--bg-0)" : undefined,
                               fontWeight: 600 }}>
                {generating ? "生成中…" : (gen ? "重新出图" : `出图（${newImgs.length} 张）→`)}
              </button>
              {credentials.length === 0 && (
                <span style={{ fontSize: 10.5, color: "var(--fg-3)" }}>
                  没有任何 t2i 凭据，去 <a href="/models" style={{ color: "var(--cyan)" }}>模型管理</a> 加一条
                </span>
              )}
            </div>
            {genMsg && (
              <div style={{
                marginTop: 8, padding: "6px 10px", borderRadius: 3, fontSize: 11,
                background: genMsg.kind === "ok"
                  ? "color-mix(in oklch, var(--green) 14%, var(--bg-1))"
                  : genMsg.kind === "warn"
                  ? "color-mix(in oklch, var(--orange) 14%, var(--bg-1))"
                  : "rgba(255,90,90,0.10)",
                color: genMsg.kind === "ok" ? "var(--green)"
                       : genMsg.kind === "warn" ? "var(--orange)" : "var(--pink)",
              }}>
                {genMsg.msg}
              </div>
            )}
            {gen && (
              <div style={{ marginTop: 8, fontSize: 11, color: "var(--fg-2)" }}>
                上次出图：{gen.provider}:{gen.model_name}
                {gen.mode && <Chip tone="cyan" style={{ marginLeft: 6 }}>{gen.mode}</Chip>}
                {gen.ref_image_count ? ` · 用了 ${gen.ref_image_count} 张父图作 ref` : " · 无参考图（纯 t2i）"}
                {" · "}{gen.ran_at?.slice(0, 19).replace("T", " ")}
                {" · "}成功 {gen.ok_count} / 失败 {gen.fail_count}
                {" · "}
                <a href={`/image-library`} style={{ color: "var(--cyan)" }}>去图片素材库看</a>
              </div>
            )}
          </div>
        </Section>

        <Section idx={3} title={`new_images · ${newImgs.length} 条 prompt（每张配父帖 ref）`}>
          {newImgs.length === 0 ? <Empty msg="plan 里没有 new_images"/> : (
            <div className="col gap-8">
              {newImgs.map((img, i) => {
                const result = genByIndex[img.index ?? (i + 1)];
                const url = result?.ok
                  ? `/api/image-library/notes/${childJobId}/img/${result.filename}`
                  : null;
                // ref 图：先看本次 gen items 里有没有给 ref_image，没有就按 (i mod parent_count) 推测
                const refName = result?.ref_image;
                const refUrl = refName && meta.parent_job_id
                  ? `/api/image-library/notes/${meta.parent_job_id}/img/${refName}`
                  : null;
                return (
                  <div key={i} className="card" style={{ padding: 10 }}>
                    <div className="row gap-10" style={{ alignItems: "flex-start" }}>
                      {/* ref 父图（小） */}
                      <div style={{ flexShrink: 0, width: 70, height: 94,
                                    background: "#222", borderRadius: 4,
                                    overflow: "hidden", border: "1px solid var(--line-1)",
                                    position: "relative" }}>
                        {refUrl ? (
                          <>
                            <img src={window.withApiToken(refUrl)} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
                            <span className="mono" style={{
                              position: "absolute", bottom: 2, left: 2, fontSize: 8,
                              background: "rgba(0,0,0,.7)", color: "#fff",
                              padding: "0 3px", borderRadius: 2,
                            }}>ref</span>
                          </>
                        ) : (
                          <div style={{ width: "100%", height: "100%",
                                        display: "flex", alignItems: "center",
                                        justifyContent: "center", fontSize: 9,
                                        color: "var(--fg-3)", textAlign: "center" }}>
                            (无 ref)
                          </div>
                        )}
                      </div>
                      {/* 箭头 */}
                      <div style={{ flexShrink: 0, alignSelf: "center",
                                     fontSize: 16, color: "var(--purple)" }}>→</div>
                      {/* 新图 */}
                      <div style={{ flexShrink: 0, width: 100, height: 134,
                                    background: "#000", borderRadius: 4,
                                    overflow: "hidden", border: "1px solid var(--line-1)" }}>
                        {url ? (
                          <a href={url} target="_blank" rel="noreferrer">
                            <img src={window.withApiToken(url)} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
                          </a>
                        ) : (
                          <div style={{ width: "100%", height: "100%",
                                        display: "flex", alignItems: "center",
                                        justifyContent: "center", fontSize: 10,
                                        color: "var(--fg-3)", textAlign: "center" }}>
                            {result?.error ? "出图失败" : "未出图"}
                          </div>
                        )}
                      </div>
                      <div className="col grow gap-4" style={{ minWidth: 0 }}>
                        <div className="row gap-8" style={{ alignItems: "center", flexWrap: "wrap" }}>
                          <span className="mono" style={{ fontSize: 10.5, color: "var(--fg-3)" }}>#{img.index ?? (i + 1)}</span>
                          <Chip>{img.role || "—"}</Chip>
                          {refName && (
                            <span className="mono" style={{ fontSize: 10, color: "var(--fg-3)" }}>
                              ref: {refName}
                            </span>
                          )}
                          {img.purpose && <span style={{ fontSize: 11, color: "var(--fg-2)" }}>{img.purpose}</span>}
                          {result?.ok && <Chip tone="cyan">已出图</Chip>}
                          {result?.error && <Chip tone="pink">失败</Chip>}
                        </div>
                        <div style={{ fontSize: 12, color: "var(--fg-1)", lineHeight: 1.55,
                                       whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                          {img.prompt_zh || "—"}
                        </div>
                        {result?.error && (
                          <div style={{ fontSize: 10.5, color: "var(--pink)" }}>
                            {result.error}
                          </div>
                        )}
                      </div>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </Section>

        {/* 标题 / 正文 复刻链（原 vs 新对照） */}
        {(plan.title_replicate || plan.caption_replicate) && (
          <Section idx={4} title="文案复刻链（原 → 新）">
            <div className="col gap-10">
              {plan.title_replicate && (
                <Card title="标题复刻">
                  <div className="row gap-10" style={{ alignItems: "stretch", flexWrap: "wrap" }}>
                    <div style={{ flex: 1, minWidth: 240, padding: 8,
                                   border: "1px solid var(--line-1)", borderRadius: 4,
                                   background: "var(--bg-1)" }}>
                      <div className="kpi-l" style={{ marginBottom: 4 }}>原标题</div>
                      <div style={{ fontSize: 13, color: "var(--fg-0)" }}>{plan.title_replicate.original || "—"}</div>
                    </div>
                    <div style={{ flexShrink: 0, alignSelf: "center",
                                   fontSize: 18, color: "var(--purple)" }}>→</div>
                    <div style={{ flex: 1, minWidth: 240, padding: 8,
                                   border: "1px solid var(--purple)", borderRadius: 4,
                                   background: "color-mix(in oklch, var(--purple) 8%, var(--bg-1))" }}>
                      <div className="kpi-l" style={{ marginBottom: 4, color: "var(--purple)" }}>新标题</div>
                      <div style={{ fontSize: 13, color: "var(--fg-0)", fontWeight: 500 }}>{plan.title_replicate.draft || plan.title_draft || "—"}</div>
                    </div>
                  </div>
                  <div style={{ marginTop: 6, fontSize: 11, color: "var(--fg-2)" }}>
                    <b>套用公式：</b>{plan.title_replicate.formula_used || "—"}
                  </div>
                  {safeArr(plan.title_replicate.swap_log).length > 0 && (
                    <div style={{ marginTop: 6, fontSize: 11 }}>
                      <b>字段替换：</b>
                      {plan.title_replicate.swap_log.map((s, i) => (
                        <span key={i} className="mono" style={{ marginRight: 8 }}>
                          [{s.from} → {s.to}]
                        </span>
                      ))}
                    </div>
                  )}
                  {safeArr(plan.title_replicate.anti_dup_techniques_applied).length > 0 && (
                    <div style={{ marginTop: 4, fontSize: 11, color: "var(--fg-3)" }}>
                      <b>用到的降重：</b>{plan.title_replicate.anti_dup_techniques_applied.join(" · ")}
                    </div>
                  )}
                </Card>
              )}
              {plan.caption_replicate && (
                <Card title="正文复刻">
                  <div style={{ fontSize: 11, color: "var(--fg-2)", marginBottom: 6 }}>
                    <b>段落功能序列对齐：</b>
                    {safeArr(plan.caption_replicate.structure_followed).join(" → ")}
                  </div>
                  {safeArr(plan.caption_replicate.section_map).length > 0 && (
                    <table style={{ width: "100%", fontSize: 11.5, borderCollapse: "collapse" }}>
                      <thead>
                        <tr style={{ background: "var(--bg-1)" }}>
                          <th style={{ padding: "6px 8px", textAlign: "left", width: 90 }}>段落</th>
                          <th style={{ padding: "6px 8px", textAlign: "left" }}>原文摘要</th>
                          <th style={{ padding: "6px 8px", textAlign: "left" }}>新文段落</th>
                        </tr>
                      </thead>
                      <tbody>
                        {plan.caption_replicate.section_map.map((s, i) => (
                          <tr key={i} style={{ borderTop: "1px solid var(--line-1)" }}>
                            <td style={{ padding: "6px 8px" }}><Chip>{s.section}</Chip></td>
                            <td style={{ padding: "6px 8px", color: "var(--fg-2)" }}>{s.parent_excerpt || "—"}</td>
                            <td style={{ padding: "6px 8px", color: "var(--fg-0)" }}>{s.new_excerpt || "—"}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  )}
                  <div style={{ marginTop: 10 }}>
                    <div className="kpi-l" style={{ marginBottom: 4 }}>新正文完整版</div>
                    <div style={{ padding: 8, border: "1px solid var(--purple)", borderRadius: 4,
                                   background: "color-mix(in oklch, var(--purple) 6%, var(--bg-1))",
                                   fontSize: 12, color: "var(--fg-1)", lineHeight: 1.7,
                                   whiteSpace: "pre-wrap" }}>
                      {plan.caption_replicate.draft || plan.caption_draft || "—"}
                    </div>
                  </div>
                  {safeArr(plan.caption_replicate.anti_dup_techniques_applied).length > 0 && (
                    <div style={{ marginTop: 6, fontSize: 11, color: "var(--fg-3)" }}>
                      <b>用到的降重：</b>{plan.caption_replicate.anti_dup_techniques_applied.join(" · ")}
                    </div>
                  )}
                </Card>
              )}
              {safeArr(plan.tags_draft).length > 0 && (
                <Card title="标签">
                  <div className="row gap-6" style={{ flexWrap: "wrap" }}>
                    {plan.tags_draft.map((t, i) => <Chip key={i} tone="cyan">{t}</Chip>)}
                  </div>
                </Card>
              )}
            </div>
          </Section>
        )}

        {/* 兼容旧版（没有 title_replicate / caption_replicate 字段时退回纯 draft 卡） */}
        {!plan.title_replicate && !plan.caption_replicate && (plan.title_draft || plan.caption_draft) && (
          <Section idx={4} title="文案 draft">
            <Card title="标题">
              <div style={{ fontSize: 14, fontWeight: 500, color: "var(--fg-0)" }}>
                {plan.title_draft || "—"}
              </div>
            </Card>
            {safeArr(plan.tags_draft).length > 0 && (
              <Card title="标签">
                <div className="row gap-6" style={{ flexWrap: "wrap" }}>
                  {plan.tags_draft.map((t, i) => <Chip key={i} tone="cyan">{t}</Chip>)}
                </div>
              </Card>
            )}
            <Card title="正文">
              <div style={{ fontSize: 12, color: "var(--fg-1)", lineHeight: 1.65,
                             whiteSpace: "pre-wrap" }}>
                {plan.caption_draft || "—"}
              </div>
            </Card>
          </Section>
        )}

        {hooks.length > 0 && (
          <Section idx={5} title="钩子序列">
            <table style={{ width: "100%", fontSize: 11.5, borderCollapse: "collapse" }}>
              <thead>
                <tr style={{ background: "var(--bg-1)" }}>
                  <th style={{ padding: "6px 8px", textAlign: "left" }}>位置</th>
                  <th style={{ padding: "6px 8px", textAlign: "left" }}>钩子类型</th>
                  <th style={{ padding: "6px 8px", textAlign: "left" }}>用户心理触发</th>
                </tr>
              </thead>
              <tbody>
                {hooks.map((h, i) => (
                  <tr key={i} style={{ borderTop: "1px solid var(--line-1)" }}>
                    <td style={{ padding: "6px 8px" }}>{h.position || "—"}</td>
                    <td style={{ padding: "6px 8px" }}><Chip>{h.hook_type || "—"}</Chip></td>
                    <td style={{ padding: "6px 8px" }}>{h.psych_trigger || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </Section>
        )}

        {techs.length > 0 && (
          <Section idx={6} title="启用的防查重工艺">
            <div className="row gap-6" style={{ flexWrap: "wrap" }}>
              {techs.map((t, i) => <Chip key={i}>{t}</Chip>)}
            </div>
          </Section>
        )}
      </div>
    );
  };


  // ============== 图文笔记视图 ==============
  const ImagePostReport = ({ meta, post }) => {
    const urls = safeArr(post.image_urls);
    const names = safeArr(post.image_names);
    const analysis = post.analysis || null;
    const analysisErr = post.analysis_error || (analysis && analysis._error);

    // 图文复刻 entry 状态
    const [imageReplicateTemplates, setImageReplicateTemplates] = useState([]);
    const [chosenReplicateTpl, setChosenReplicateTpl] = useState(() => {
      try { return localStorage.getItem("rr.imageReplicate.tplId") || ""; } catch { return ""; }
    });
    const [rulePacks, setRulePacks] = useState([]);
    const [chosenRulePackId, setChosenRulePackId] = useState(() => {
      try { return localStorage.getItem("rr.imageReplicate.ruleId") || ""; } catch { return ""; }
    });
    const [imageCountInput, setImageCountInput] = useState("");
    const [replicating, setReplicating] = useState(false);
    const [replicateMsg, setReplicateMsg] = useState(null);

    useEffect(() => {
      Promise.all([
        API.templates.list({ category: "image_replicate" }).catch(() => ({ items: [] })),
        API.templates.list({ category: "replicate" }).catch(() => ({ items: [] })),
      ]).then(([a, b]) => {
        setImageReplicateTemplates([...(a.items || []), ...(b.items || [])]);
      });
      API.rulePacks.list().catch(() => ({ items: [] }))
        .then(d => setRulePacks((d.items || []).filter(r => r.enabled !== false)));
    }, []);

    const onTplChange = (v) => {
      setChosenReplicateTpl(v);
      try { localStorage.setItem("rr.imageReplicate.tplId", v); } catch {}
    };
    const onRuleChange = (v) => {
      setChosenRulePackId(v);
      try { localStorage.setItem("rr.imageReplicate.ruleId", v); } catch {}
    };

    const startReplicate = async () => {
      if (replicating || !meta.job_id) return;
      setReplicating(true);
      setReplicateMsg(null);
      try {
        const body = {};
        if (chosenReplicateTpl) body.template_id = Number(chosenReplicateTpl);
        if (imageCountInput) body.image_count = Number(imageCountInput);
        if (chosenRulePackId) body.rule_pack_id = chosenRulePackId;
        const r = await API.replicate.start(meta.job_id, body);
        const tpl = imageReplicateTemplates.find(t => String(t.id) === String(chosenReplicateTpl));
        const rule = rulePacks.find(rp => rp.pack_id === chosenRulePackId);
        const tplLabel  = `模板：${tpl?.name || "默认（图文爆款复刻 v1.0）"}`;
        const ruleLabel = rule ? ` · 规则：${rule.display_name}` : " · 无规则限制";
        setReplicateMsg({
          kind: "ok",
          msg: `已发起图文复刻 · 任务 ${r.child_job_id} · ${tplLabel}${ruleLabel}`,
          childId: r.child_job_id,
        });
      } catch (e) {
        setReplicateMsg({ kind: "err", msg: String(e.message || e) });
      } finally {
        setReplicating(false);
      }
    };
    return (
      <div className="rr">
        <Section idx={1} title="图文笔记 · 概览" badge="image_post">
          <div className="row gap-12" style={{ flexWrap: "wrap", alignItems: "center" }}>
            <Chip tone="cyan">来源：{post.source || meta.source || "xhs_image_post"}</Chip>
            <Chip>共 {post.image_count || urls.length} 张</Chip>
            {meta.industry_key && <Chip>行业：{meta.industry_key}</Chip>}
            {meta.analyze_template_name && (
              <Chip tone="cyan">模板：{meta.analyze_template_name}</Chip>
            )}
            {meta.saved_at && (
              <span className="mono" style={{ fontSize: 11, color: "var(--fg-3)" }}>
                入库 {meta.saved_at.slice(0, 19).replace("T", " ")}
              </span>
            )}
          </div>
          {(post.link_url || meta.source_url) && (
            <div style={{ marginTop: 8 }}>
              <a href={post.link_url || meta.source_url} target="_blank" rel="noreferrer"
                 className="mono" style={{ fontSize: 11, color: "var(--cyan)" }}>
                {post.link_url || meta.source_url}
              </a>
            </div>
          )}
          <div style={{
            marginTop: 12, padding: "10px 12px", borderRadius: 5,
            background: "color-mix(in oklch, var(--cyan) 8%, var(--bg-1))",
            color: "var(--fg-1)", fontSize: 12, lineHeight: 1.6,
          }}>
            这条小红书链接是<b>图文笔记</b>，已自动落 {urls.length} 张图到
            <b>资产 → 图片素材库</b>，并按「图文拆解模板」做了 LLM 拆解。
            <a href={`/image-library`} style={{ marginLeft: 8, color: "var(--cyan)" }}>
              → 去图片素材库
            </a>
          </div>

          {/* 图文复刻入口：选模板 + 张数 → POST /replicate */}
          <div style={{
            marginTop: 10, padding: "10px 12px", borderRadius: 5,
            border: "1px solid var(--purple)",
            background: "color-mix(in oklch, var(--purple) 8%, var(--bg-1))",
          }}>
            <div className="row gap-8" style={{ alignItems: "center", marginBottom: 8 }}>
              <Icon name="magic" size={11} style={{ color: "var(--purple)" }}/>
              <span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--purple)" }}>图文复刻</span>
              <span style={{ fontSize: 10.5, color: "var(--fg-2)" }}>
                按「图文爆款复刻」模板复刻一条新笔记（标题 + 正文 + 每张图的中文 prompt）
              </span>
            </div>
            <div className="row gap-8" style={{ alignItems: "center", flexWrap: "wrap" }}>
              <select value={chosenReplicateTpl}
                      onChange={e => onTplChange(e.target.value)}
                      disabled={replicating}
                      title="不选 = 默认走「图文爆款复刻 Skills 提示词模板 v1.0」"
                      style={{ fontSize: 11.5, padding: "4px 8px",
                               border: "1px solid var(--line-1)", borderRadius: 4,
                               background: "white", color: "var(--fg-0)",
                               fontFamily: "inherit", maxWidth: 280 }}>
                <option value="">默认（图文爆款复刻 v1.0）</option>
                {imageReplicateTemplates.map(t => (
                  <option key={t.id} value={t.id}>
                    {t.name}
                    {t.category === "image_replicate" ? " · 图文专用" : " · 通用"}
                  </option>
                ))}
              </select>
              <select value={chosenRulePackId}
                      onChange={e => onRuleChange(e.target.value)}
                      disabled={replicating}
                      title="选规则模板：复刻 LLM 会在该规则约束下产出。不选 = 无规则限制"
                      style={{ fontSize: 11.5, padding: "4px 8px",
                               border: "1px solid var(--line-1)", borderRadius: 4,
                               background: "white", color: "var(--fg-0)",
                               fontFamily: "inherit", maxWidth: 240 }}>
                <option value="">无规则限制</option>
                {rulePacks.map(rp => (
                  <option key={rp.pack_id} value={rp.pack_id}>{rp.display_name}</option>
                ))}
              </select>
              <input value={imageCountInput}
                     onChange={e => setImageCountInput(e.target.value.replace(/\D/g, ""))}
                     disabled={replicating}
                     placeholder={`图片张数（默认 ${urls.length}）`}
                     style={{ fontSize: 11.5, padding: "4px 8px", width: 130,
                              border: "1px solid var(--line-1)", borderRadius: 4,
                              background: "white", color: "var(--fg-0)",
                              fontFamily: "inherit" }}/>
              <button onClick={startReplicate} disabled={replicating}
                      className="btn sm primary"
                      style={{ background: replicating ? undefined : "var(--purple)",
                               color: replicating ? undefined : "var(--bg-0)",
                               fontWeight: 600 }}>
                {replicating ? "发起中…" : "发起图文复刻 →"}
              </button>
            </div>
            {replicateMsg && (
              <div style={{
                marginTop: 8, padding: "6px 10px", borderRadius: 3, fontSize: 11,
                background: replicateMsg.kind === "ok"
                  ? "color-mix(in oklch, var(--purple) 16%, var(--bg-1))"
                  : "rgba(255,90,90,0.10)",
                color: replicateMsg.kind === "ok" ? "var(--purple)" : "var(--pink)",
              }}>
                {replicateMsg.msg}
                {replicateMsg.childId && (
                  <a href={`/api/jobs/${replicateMsg.childId}/replicate-result`}
                     target="_blank" rel="noreferrer"
                     style={{ marginLeft: 8, color: "var(--cyan)" }}>
                    查看 JSON
                  </a>
                )}
              </div>
            )}
          </div>
        </Section>

        <Section idx={2} title="图片预览">
          {urls.length === 0 ? (
            <Empty msg="没有图片"/>
          ) : (
            <div style={{
              display: "grid",
              gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
              gap: 10,
            }}>
              {urls.map((u, i) => (
                <div key={u} style={{
                  position: "relative", border: "1px solid var(--line-1)",
                  borderRadius: 5, overflow: "hidden", background: "#000",
                  aspectRatio: "3 / 4",
                }}>
                  <a href={u} target="_blank" rel="noreferrer">
                    <img src={window.withApiToken(u)} style={{ width: "100%", height: "100%", objectFit: "cover" }}/>
                  </a>
                  <span className="mono" style={{
                    position: "absolute", top: 4, left: 4, fontSize: 9,
                    background: "rgba(0,0,0,.55)", color: "#fff",
                    padding: "1px 5px", borderRadius: 2,
                  }}>{names[i] || `${i + 1}`}</span>
                </div>
              ))}
            </div>
          )}
        </Section>

        {analysisErr && (
          <Section idx={3} title="LLM 拆解 · 异常">
            <div style={{ padding: "10px 12px", borderRadius: 4,
                          background: "color-mix(in oklch, var(--pink) 14%, var(--bg-1))",
                          color: "var(--pink)", fontSize: 12 }}>
              {analysisErr}
            </div>
            {analysis?._raw_text && (
              <pre style={{
                whiteSpace: "pre-wrap", fontSize: 11, marginTop: 8,
                padding: 10, background: "var(--bg-1)", borderRadius: 4,
                maxHeight: 240, overflow: "auto",
              }}>{analysis._raw_text}</pre>
            )}
          </Section>
        )}

        {/* 原始爆款文案：标题 + 正文 —— downloader 抠下来 */}
        {(post.original_title || post.original_caption) && (
          <Section idx={3} title="原始爆款文案（downloader 抓 XHS HTML 抠的）">
            <div className="col gap-8">
              {post.original_title && (
                <Card title="标题">
                  <div style={{ fontSize: 14, fontWeight: 500, color: "var(--fg-0)" }}>
                    {post.original_title}
                  </div>
                </Card>
              )}
              {post.original_caption && (
                <Card title="正文">
                  <div style={{ fontSize: 12, color: "var(--fg-1)", lineHeight: 1.7,
                                 whiteSpace: "pre-wrap", maxHeight: 400, overflow: "auto" }}>
                    {post.original_caption}
                  </div>
                </Card>
              )}
            </div>
          </Section>
        )}

        {analysis && !analysisErr && (
          <Section idx={(post.original_title || post.original_caption) ? 4 : 3}
                   title="图文拆解（按模板输出）">
            <div className="col gap-10">
              {analysis.stream_verdict && (
                <Card title="分发判定">
                  <KV k="自然 / 信息流" v={analysis.stream_verdict.info_or_natural}/>
                  <KV k="判定理由" v={analysis.stream_verdict.rationale}/>
                </Card>
              )}
              {analysis.title_breakdown && (
                <Card title="标题拆解（爆款公式）">
                  <KV k="原标题" v={analysis.title_breakdown.original}/>
                  <KV k="骨架公式" v={analysis.title_breakdown.formula}/>
                  <KV k="命中钩子" v={safeArr(analysis.title_breakdown.hooks_used).join("、")}/>
                  <KV k="心理触发" v={analysis.title_breakdown.psych_triggers}/>
                  {analysis.title_breakdown.key_elements && (
                    <div style={{ marginTop: 4, fontSize: 11, color: "var(--fg-2)" }}>
                      <b>关键元素：</b>
                      数字 [{safeArr(analysis.title_breakdown.key_elements.numbers).join("、") || "—"}] ·
                      痛点 [{safeArr(analysis.title_breakdown.key_elements.pain_points).join("、") || "—"}] ·
                      效果 [{safeArr(analysis.title_breakdown.key_elements.outcomes).join("、") || "—"}]
                    </div>
                  )}
                  <KV k="降重轴" v={analysis.title_breakdown.anti_dup_axes}/>
                </Card>
              )}
              {analysis.caption_breakdown && (
                <Card title="正文拆解（爆款骨架）">
                  <KV k="段落功能序列" v={safeArr(analysis.caption_breakdown.structure).join(" → ")}/>
                  <KV k="语气" v={analysis.caption_breakdown.tone}/>
                  <KV k="句长节奏" v={analysis.caption_breakdown.length_pattern}/>
                  <KV k="情绪起伏" v={safeArr(analysis.caption_breakdown.emotional_beats).join(" → ")}/>
                  {analysis.caption_breakdown.key_elements && (
                    <div style={{ marginTop: 4, fontSize: 11, color: "var(--fg-2)" }}>
                      <b>关键元素：</b>
                      钩子句 {safeArr(analysis.caption_breakdown.key_elements.hooks).length} 条 ·
                      产品 [{safeArr(analysis.caption_breakdown.key_elements.products).join("、") || "—"}] ·
                      CTA [{safeArr(analysis.caption_breakdown.key_elements.cta).join("、") || "—"}]
                    </div>
                  )}
                  <KV k="降重轴" v={analysis.caption_breakdown.anti_dup_axes}/>
                </Card>
              )}
              {analysis.dual_dimension && (
                <Card title="内容调性 + 行业相关度">
                  <KV k="content_tone" v={analysis.dual_dimension.content_tone}/>
                  <KV k="real_estate_relevance" v={analysis.dual_dimension.real_estate_relevance}/>
                </Card>
              )}
              {analysis.hook && (
                <Card title="钩子">
                  <KV k="类型" v={analysis.hook.type}/>
                  <KV k="一句话" v={analysis.hook.summary}/>
                  <KV k="封面图分析" v={analysis.hook.first_image_critique}/>
                </Card>
              )}
              {analysis.image_breakdown && (
                <Card title={`按图拆解 · ${analysis.image_breakdown.length} 张`}>
                  <table style={{ width: "100%", fontSize: 11.5, borderCollapse: "collapse" }}>
                    <thead>
                      <tr style={{ background: "var(--bg-1)" }}>
                        <th style={{ padding: "6px 8px", textAlign: "left" }}>#</th>
                        <th style={{ padding: "6px 8px", textAlign: "left" }}>角色</th>
                        <th style={{ padding: "6px 8px", textAlign: "left" }}>画面摘要</th>
                        <th style={{ padding: "6px 8px", textAlign: "left" }}>作用</th>
                      </tr>
                    </thead>
                    <tbody>
                      {safeArr(analysis.image_breakdown).map((b, i) => (
                        <tr key={i} style={{ borderTop: "1px solid var(--line-1)" }}>
                          <td className="mono" style={{ padding: "6px 8px", color: "var(--fg-3)" }}>{b.index ?? (i + 1)}</td>
                          <td style={{ padding: "6px 8px" }}><Chip>{b.role || "—"}</Chip></td>
                          <td style={{ padding: "6px 8px" }}>{b.visual_summary || "—"}</td>
                          <td style={{ padding: "6px 8px" }}>{b.purpose || "—"}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </Card>
              )}
              {analysis.copy_structure && (
                <Card title="文案结构">
                  <KV k="叙事骨架" v={analysis.copy_structure.pattern}/>
                  <KV k="节奏" v={analysis.copy_structure.rhythm}/>
                </Card>
              )}
              {analysis.visual_rhythm && (
                <Card title="视觉节奏">
                  <KV k="风格统一性" v={analysis.visual_rhythm.consistency}/>
                  <KV k="出彩页" v={safeArr(analysis.visual_rhythm.highlight_pages).join("、")}/>
                  <KV k="拖后腿页" v={safeArr(analysis.visual_rhythm.weak_pages).join("、")}/>
                </Card>
              )}
              {analysis.emotion_curve && (
                <Card title="情绪曲线">
                  <div style={{ fontSize: 12, color: "var(--fg-1)" }}>{analysis.emotion_curve}</div>
                </Card>
              )}
              {analysis.aids && (
                <Card title="AIDIS">
                  {Object.entries(analysis.aids).map(([k, v]) => (
                    <KV key={k} k={k} v={v}/>
                  ))}
                </Card>
              )}
              {analysis.rewrite_suggestion && (
                <Card title="复刻建议">
                  <div style={{ fontSize: 12, color: "var(--fg-1)" }}>{analysis.rewrite_suggestion}</div>
                </Card>
              )}
            </div>
          </Section>
        )}
      </div>
    );
  };

  return { ResultReport };
})();

const ResultReport = RR.ResultReport;
Object.assign(window, { ResultReport });
