/* chapter write page: 大纲编辑 + 上下文面板 + SSE 流式生成 + 暂停/继续/重写. */ import { escapeHtml, fetchJson, postJson, showError } from "../utils.js"; import { createSSEStream } from "../components/sse-client.js"; const NOVEL_ID = window.NOVEL_ID; const CHAPTER_ID = window.CHAPTER_ID; const alertsEl = document.getElementById("cwAlerts"); const outlineEl = document.getElementById("cwOutline"); const outputEl = document.getElementById("cwOutput"); const wordCountEl = document.getElementById("cwWordCount"); const ctxSummaryEl = document.getElementById("cwContextSummary"); const btn = { generate: document.getElementById("cwGenerate"), pause: document.getElementById("cwPause"), resume: document.getElementById("cwResume"), regen: document.getElementById("cwRegen"), edit: document.getElementById("cwEdit"), save: document.getElementById("cwSave"), approve: document.getElementById("cwApprove"), saveOutline: document.getElementById("cwSaveOutline"), }; let stream = null; let accumulated = ""; // 已生成文本 (暂停断点) let editing = false; /* ---------------- 状态管理 ---------------- */ function setStreaming(on) { btn.generate.disabled = on; btn.pause.disabled = !on; btn.regen.disabled = on || !accumulated; btn.resume.disabled = on || !accumulated; btn.saveOutline.disabled = on; if (on) { btn.edit.disabled = true; btn.save.disabled = true; btn.approve.disabled = true; } } function setIdleWithContent() { btn.generate.disabled = false; btn.pause.disabled = true; btn.resume.disabled = true; btn.regen.disabled = false; btn.edit.disabled = false; btn.save.disabled = false; btn.approve.disabled = false; } function renderOutput() { outputEl.innerHTML = accumulated ? escapeHtml(accumulated) : '点击"生成"开始写作…'; outputEl.scrollTop = outputEl.scrollHeight; wordCountEl.textContent = accumulated ? "当前字数: " + accumulated.length : ""; } /* ---------------- 加载章节 + 上下文 ---------------- */ async function loadChapter() { try { const ch = await fetchJson("/api/chapters/" + CHAPTER_ID); document.getElementById("cwTitle").textContent = "第" + (ch.volume || 1) + "卷 第" + (ch.chapter_number || 1) + "章 " + (ch.title || ""); outlineEl.value = ch.outline || ""; if (ch.content) { accumulated = ch.content; renderOutput(); setIdleWithContent(); } } catch (err) { showError(alertsEl, "加载章节失败: " + err.message); } } async function loadContext() { const el = document.getElementById("cwContext"); try { const ctx = await fetchJson("/api/chapters/" + CHAPTER_ID + "/context"); let html = ""; html += "
前文摘要 (" + ctx.earlier_summaries.length + ")"; html += ctx.earlier_summaries.length ? ctx.earlier_summaries.map((s) => "

第" + s.volume + "卷 第" + s.chapter_number + "章 " + escapeHtml(s.title || "") + ": " + escapeHtml(s.summary) + "

").join("") : '

'; html += "
"; html += "
前章正文 (" + ctx.prev_chapters.length + ")"; html += ctx.prev_chapters.length ? ctx.prev_chapters.map((c) => "
第" + c.volume + "卷 第" + c.chapter_number + "章 " + escapeHtml(c.title || "") + "" + "
"
                + escapeHtml(c.content) + "
").join("") : '

无 (这是第一章)

'; html += "
"; html += "
相关角色 (" + ctx.characters.length + ")"; html += ctx.characters.length ? ctx.characters.map((c) => "

" + escapeHtml(c.name) + " (" + escapeHtml(c.role || "") + "): " + escapeHtml((c.description || "").slice(0, 200)) + "

").join("") : '

'; html += "
"; html += "
未回收伏笔 (" + ctx.open_foreshadowing.length + ")"; html += ctx.open_foreshadowing.length ? ctx.open_foreshadowing.map((f) => "

" + escapeHtml(f.description) + "

").join("") : '

'; html += "
"; html += '

上下文总大小: ' + ctx.total_chars + " 字符

"; el.innerHTML = html; } catch (err) { el.innerHTML = '
' + escapeHtml(err.message) + "
"; } } /* ---------------- 生成控制 ---------------- */ function startStream(prefix) { if (stream) stream.close(); setStreaming(true); ctxSummaryEl.innerHTML = ""; const params = new URLSearchParams({ csrf_token: document.querySelector('meta[name="csrf-token"]').content }); if (prefix) params.append("prefix", prefix); stream = createSSEStream("/api/chapters/" + CHAPTER_ID + "/generate?" + params.toString(), { onContext: (summary) => { ctxSummaryEl.innerHTML = '
' + escapeHtml(summary) + "
"; }, onToken: (t) => { accumulated += t; renderOutput(); }, onDone: (content) => { accumulated = content; renderOutput(); stream = null; setIdleWithContent(); }, onError: (msg) => { showError(alertsEl, "生成失败: " + msg); stream = null; setStreaming(false); btn.resume.disabled = !accumulated; }, }); } btn.generate.addEventListener("click", () => { accumulated = ""; renderOutput(); startStream(null); }); btn.pause.addEventListener("click", () => { if (stream) { stream.close(); stream = null; } setStreaming(false); btn.resume.disabled = !accumulated; btn.regen.disabled = !accumulated; }); btn.resume.addEventListener("click", () => startStream(accumulated)); btn.regen.addEventListener("click", () => { if (stream) { stream.close(); stream = null; } accumulated = ""; renderOutput(); startStream(null); }); btn.edit.addEventListener("click", () => { editing = !editing; outputEl.contentEditable = editing ? "true" : "false"; btn.edit.textContent = editing ? "完成编辑" : "编辑"; if (editing) outputEl.focus(); else accumulated = outputEl.innerText; }); btn.save.addEventListener("click", async () => { if (editing) accumulated = outputEl.innerText; try { await postJson("/api/chapters/" + CHAPTER_ID, { content: accumulated }); renderOutput(); ctxSummaryEl.innerHTML = '
已保存
'; } catch (err) { showError(alertsEl, "保存失败: " + err.message); } }); btn.approve.addEventListener("click", async () => { if (editing) accumulated = outputEl.innerText; try { await postJson("/api/chapters/" + CHAPTER_ID, { content: accumulated, status: "done" }); ctxSummaryEl.innerHTML = '
本章已通过 (status=done)
'; } catch (err) { showError(alertsEl, "操作失败: " + err.message); } }); btn.saveOutline.addEventListener("click", async () => { try { await postJson("/api/chapters/" + CHAPTER_ID, { outline: outlineEl.value }); ctxSummaryEl.innerHTML = '
大纲已保存
'; loadContext(); } catch (err) { showError(alertsEl, "保存大纲失败: " + err.message); } }); /* ---------------- init ---------------- */ loadChapter(); loadContext();