// 文案二创独立链路 · 通用进度组件 // // 三个页面共用(拆解爆款 / 复刻爆款 / 质检合成):URL 带 ?text_rewrite_job=xxx // 时在该页对应位置内联渲染。组件内部职责: // - SSE 订阅 /api/text-rewrite/jobs/{id}/stream,兜底 5s 轮询 /api/.../get // - 渲染 7 stage 步骤条 + 当前阶段消息 + 百分比 // - 「展开工作内容」可折叠:根据当前 stage 拉对应产物预览 // · chunk_init 完成 → 展示 chunks_doubao 表(句切,d_001...) // · chunk_segment 完成 → chunks_segment 表(语义自然段,s_001...) // · rewrite 完成 → chunks_rewrite 表(段级二创,含可编辑 rewrite_text) // - 交互按钮: // · 编辑(rewrite stage 完成后)→ 调 PATCH /chunks/{cid}/rewrite // · 跳到拆解爆款 / 跳到复刻爆款 / 跳到质检合成 → 带 ?text_rewrite_job=xxx 跳转 const TR_STAGES = [ { id: "downloader", label: "1 · 链接下载" }, { id: "audio_extract", label: "2 · 抽音轨" }, { id: "asr", label: "3 · ASR 转写" }, { id: "transcript_correct", label: "4 · LLM 修字" }, { id: "chunk_init", label: "5 · 句切(豆包)" }, { id: "chunk_segment", label: "6 · 语义段切" }, { id: "rewrite", label: "7 · 段级二创" }, ]; // 三页面自动流转的"下一步"映射 const TR_NEXT_ROUTE = { teardown: { label: "进入复刻爆款", path: "/remix", id: "remix" }, remix: { label: "传送到质检合成", path: "/qc", id: "qc" }, qc: null, // 已是终点 }; // 跳转按钮组:去三个页面看同一个 text_rewrite job 的进度 const TRJumpButtons = ({ jobId, currentRoute }) => { const targets = [ { label: "拆解爆款", path: "/workspace", id: "teardown" }, { label: "复刻爆款", path: "/remix", id: "remix" }, { label: "质检合成", path: "/qc", id: "qc" }, ]; return (
{targets.map(t => { const isHere = currentRoute === t.id; return ( e.preventDefault() : undefined} className="tag mono" style={{ fontSize: 9.5, padding: "2px 8px", textDecoration: "none", cursor: isHere ? "default" : "pointer", background: isHere ? "var(--bg-2)" : "var(--bg-1)", color: isHere ? "var(--fg-3)" : "var(--cyan)", border: `1px solid ${isHere ? "var(--line-1)" : "var(--cyan)"}`, }}> {isHere ? `· ${t.label}(当前)` : `→ ${t.label}`} ); })}
); }; // 流转 CTA:任务在当前页"准备好"时给突出按钮,引导去下一页 const TRFlowCTA = ({ jobId, currentRoute, status, allMatched }) => { const next = TR_NEXT_ROUTE[currentRoute]; if (!next) return null; // 拆解页:任务 completed 即可去复刻 // 复刻页:任务 completed + 所有素材匹配完成才能去质检 const ready = currentRoute === "teardown" ? status === "completed" : currentRoute === "remix" ? (status === "completed" && allMatched) : false; const hint = !ready ? ( currentRoute === "teardown" ? "等待文案拆解完成…" : currentRoute === "remix" ? "请先把所有段的分镜素材都匹配/选定" : "" ) : ""; const url = `${next.path}?text_rewrite_job=${encodeURIComponent(jobId)}`; return ( e.preventDefault()} style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "8px 14px", borderRadius: 4, fontSize: 12, fontWeight: 600, textDecoration: "none", cursor: ready ? "pointer" : "not-allowed", background: ready ? "var(--cyan)" : "var(--bg-2)", color: ready ? "var(--bg-0)" : "var(--fg-3)", border: `1px solid ${ready ? "var(--cyan)" : "var(--line-1)"}`, }} title={hint}> {ready ? "➡" : "⏳"} {next.label} {!ready && hint && ( · {hint} )} ); }; // 单条 stage 步骤标签 const TRStageTag = ({ stage, state, message, pct }) => { const colors = { done: { fg: "var(--green)", bg: "color-mix(in oklch, var(--green) 12%, var(--bg-1))" }, cur: { fg: "var(--cyan)", bg: "color-mix(in oklch, var(--cyan) 16%, var(--bg-1))" }, locked: { fg: "var(--fg-3)", bg: "var(--bg-1)" }, failed: { fg: "var(--pink)", bg: "color-mix(in oklch, var(--pink) 14%, var(--bg-1))" }, }; const c = colors[state] || colors.locked; return (
{state === "done" ? "✓ " : state === "cur" ? "▶ " : ""}{stage.label}
{state === "cur" && (
{pct}%
)}
); }; // chunks_doubao / chunks_segment / chunks_rewrite 表 const TRChunksTable = ({ kind, rows, jobId, onRewriteEdited }) => { const [editing, setEditing] = React.useState(null); // {chunk_id, draft} const [busy, setBusy] = React.useState(false); if (!rows || !rows.length) { return
暂无数据
; } const submitEdit = async () => { if (!editing || !jobId) return; setBusy(true); try { const res = await API.textRewrite.patchRewrite(jobId, editing.chunk_id, { rewrite_text: editing.draft, }); onRewriteEdited && onRewriteEdited(editing.chunk_id, editing.draft, res); setEditing(null); } catch (e) { alert("保存失败:" + (e.message || e)); } finally { setBusy(false); } }; return (
{kind === "doubao" && } {kind === "segment" && } {kind === "rewrite" && } {rows.map((r, i) => { const cid = r.chunk_id; const isEdit = editing && editing.chunk_id === cid; return ( {kind === "doubao" && ( )} {kind === "segment" && ( )}
chunk_id起止 (s)来源 d_chunks{kind === "rewrite" ? "段级二创" : "文本"}操作
{cid}{(r.t_start || 0).toFixed(2)}–{(r.t_end || 0).toFixed(2)}{(r.source_chunk_ids || []).join(", ")} {kind === "rewrite" && isEdit ? (