创作降本:取消同轮retry、两轮history;听书创作/播放分层与前端配套。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+106
-27
@@ -134,7 +134,9 @@ export function canOfferSaveProduct(view) {
|
||||
|
||||
export function canOfferEnterPlay(view) {
|
||||
if (!view || isPlayView(view)) return false;
|
||||
if (view.creationMode === "listen_serial") return false;
|
||||
if (view.creationMode === "listen_serial") {
|
||||
return Boolean(view.playReady);
|
||||
}
|
||||
return canOfferSaveProduct(view) || Boolean(view.hasProduct);
|
||||
}
|
||||
|
||||
@@ -1618,10 +1620,23 @@ function syncCoordRailChrome() {
|
||||
const title = document.getElementById("coord-rail-title");
|
||||
const products = coordRailMode === "products";
|
||||
const listen = coordRailMode === "listen";
|
||||
const listenPlay = listen && rail?.dataset.listenPlay === "1";
|
||||
document.body.classList.toggle("coord-rail-collapsed", coordRailCollapsed);
|
||||
document.body.dataset.coordRailMode = coordRailMode;
|
||||
const shortLabel = listen ? "正文" : products ? "产物" : "对话";
|
||||
const longLabel = listen ? "大纲与正文" : products ? "已持久化" : "此前对话";
|
||||
const shortLabel = listen
|
||||
? listenPlay
|
||||
? "正文"
|
||||
: "本局"
|
||||
: products
|
||||
? "产物"
|
||||
: "对话";
|
||||
const longLabel = listen
|
||||
? listenPlay
|
||||
? "大纲与正文"
|
||||
: "本局设定"
|
||||
: products
|
||||
? "已持久化"
|
||||
: "此前对话";
|
||||
if (toggleLabel) toggleLabel.textContent = shortLabel;
|
||||
if (title) title.textContent = longLabel;
|
||||
if (toggle) {
|
||||
@@ -5736,10 +5751,11 @@ function getRollbackMessageId(messages, messageId) {
|
||||
|
||||
/** @returns {"speak"|"answer"|"review"|"busy"|"dictate"|"listen"|null} */
|
||||
function resolveDesignSurface(view, loading) {
|
||||
// 听书:创作/播放都用同一壳;播放层右侧才写大纲与正文
|
||||
if (view.creationMode === "listen_serial") return "listen";
|
||||
if (isPlayView(view)) return null;
|
||||
if (view.phase === "done") return null;
|
||||
// 听书 / 转述:主面始终是对话流;右侧为大纲正文或产物
|
||||
if (view.creationMode === "listen_serial") return "listen";
|
||||
// 转述:主面始终是对话流;右侧为产物
|
||||
if (view.creationMode === "dictate") return "dictate";
|
||||
const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason));
|
||||
if (busy) return "busy";
|
||||
@@ -5973,20 +5989,24 @@ function renderSpeakWorkspace(view) {
|
||||
title = displayCreationModeLabel("listen_serial");
|
||||
const ls = view.listenSerial;
|
||||
const phase = ls?.phase;
|
||||
if (phase === "awaiting_confirm") {
|
||||
hint = "回复「确认」开始生成大纲与正文;或继续改需求。";
|
||||
} else if (phase === "soft_stop") {
|
||||
hint = "本段已兑现。输入「下一波」或「收工」,或一句意见改向。";
|
||||
} else if (phase === "running") {
|
||||
hint =
|
||||
view.hints?.[0] ||
|
||||
wr.message ||
|
||||
"说「继续」补货;不满意直接一句意见改后面。";
|
||||
if (ls?.playActive) {
|
||||
if (phase === "soft_stop") {
|
||||
hint = "本段已兑现。输入「下一波」或「收工」,或一句意见改向。";
|
||||
} else {
|
||||
hint =
|
||||
view.hints?.[0] ||
|
||||
wr.message ||
|
||||
"说「继续」补货;不满意直接一句意见改后面。";
|
||||
}
|
||||
} else if (phase === "awaiting_confirm") {
|
||||
hint = "回复「确认」锁定本局;确认后点「播放」写大纲与正文。";
|
||||
} else if (phase === "ready") {
|
||||
hint = "本局已确认。点「播放」生成大纲与正文;也可继续改爽点。";
|
||||
} else {
|
||||
hint =
|
||||
view.hints?.[0] ||
|
||||
wr.message ||
|
||||
"用几句话描述题材与爽点;右侧是大纲与正文。";
|
||||
"用几句话描述题材与爽点;确认后进「播放」写大纲与正文。";
|
||||
}
|
||||
} else if (view.creationMode === "dictate") {
|
||||
title = displayCreationModeLabel("dictate");
|
||||
@@ -6029,7 +6049,13 @@ function renderSpeakWorkspace(view) {
|
||||
}
|
||||
<button type="button" class="btn${
|
||||
canOfferSaveProduct(view) ? "" : " btn-primary"
|
||||
}" data-enter-play>${view.hasProduct ? "用产物开玩" : "开玩"}</button>
|
||||
}" data-enter-play>${
|
||||
view.creationMode === "listen_serial"
|
||||
? "播放"
|
||||
: view.hasProduct
|
||||
? "用产物开玩"
|
||||
: "开玩"
|
||||
}</button>
|
||||
</div>`
|
||||
: "";
|
||||
return `<section class="workspace-intent">
|
||||
@@ -6174,7 +6200,7 @@ function fillCoordRailProducts(view) {
|
||||
function renderListenSerialRail(view) {
|
||||
const ls = view.listenSerial;
|
||||
if (!ls) {
|
||||
return `<p class="coord-empty">确认开书后,这里显示大纲与正文缓冲。</p>`;
|
||||
return `<p class="coord-empty">开书后,这里显示本局爽点与世界。</p>`;
|
||||
}
|
||||
const brief = [
|
||||
ls.thrill ? `<p class="listen-brief"><strong>爽点</strong> ${esc(ls.thrill)}</p>` : "",
|
||||
@@ -6183,6 +6209,28 @@ function renderListenSerialRail(view) {
|
||||
? `<p class="listen-brief"><strong>结构</strong> ${esc(ls.structure)}</p>`
|
||||
: "",
|
||||
].join("");
|
||||
|
||||
// 创作层:只展示 brief,大纲/正文/TTS 在播放层
|
||||
if (!ls.playActive) {
|
||||
const ready = Boolean(view.playReady);
|
||||
const phaseHint =
|
||||
ls.phase === "awaiting_confirm"
|
||||
? "回复「确认」锁定本局。"
|
||||
: ls.phase === "ready"
|
||||
? "本局已确认,可进播放写大纲与正文。"
|
||||
: "先定爽点与世界,确认后再播放。";
|
||||
return `<div class="listen-rail listen-rail--design">
|
||||
<p class="listen-meta">创作 · 阶段 ${esc(ls.phase)}</p>
|
||||
${brief || `<p class="coord-empty">尚无本局设定</p>`}
|
||||
<p class="listen-rail-hint">${esc(phaseHint)}</p>
|
||||
<div class="listen-rail-actions">
|
||||
<button type="button" class="btn-sm btn-primary" data-enter-play ${
|
||||
ready ? "" : "disabled"
|
||||
} title="${ready ? "进入播放,生成大纲与正文" : "确认本局后可播放"}">播放</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const outline = (ls.outline ?? [])
|
||||
.map((o) => {
|
||||
const mark = o.status === "done" ? "✓" : o.status === "cut" ? "×" : "·";
|
||||
@@ -6206,12 +6254,13 @@ function renderListenSerialRail(view) {
|
||||
const consumed = Number(ls.consumedChars) || 0;
|
||||
const total = (ls.sections ?? []).reduce((n, s) => n + String(s.text || "").length, 0);
|
||||
const hasSections = (ls.sections ?? []).length > 0;
|
||||
return `<div class="listen-rail">
|
||||
return `<div class="listen-rail listen-rail--play">
|
||||
<p class="listen-meta">未读约 ${unread} 字 / 缓冲目标 ${target} 字 · 阶段 ${esc(ls.phase)} · Edge TTS</p>
|
||||
${brief}
|
||||
${outline ? `<ul class="listen-outline">${outline}</ul>` : ""}
|
||||
${outline ? `<ul class="listen-outline">${outline}</ul>` : `<p class="coord-empty">正在准备大纲…</p>`}
|
||||
<div class="listen-sections">${sections || `<p class="coord-empty">尚无正文</p>`}</div>
|
||||
<div class="listen-rail-actions">
|
||||
<button type="button" class="btn-sm" data-listen-leave-design="1" title="回到创作改爽点">回创作</button>
|
||||
<button type="button" class="btn-sm" data-listen-tts-chain="1" ${hasSections ? "" : "disabled"} title="从第一节连续播放">连续听</button>
|
||||
<button type="button" class="btn-sm" data-listen-tts-stop="1" title="停止播放">停</button>
|
||||
<button type="button" class="btn-sm" data-listen-consume="all" ${total <= consumed ? "disabled" : ""}>标记全部已读</button>
|
||||
@@ -6229,12 +6278,24 @@ function fillCoordRailListen(view, handlers) {
|
||||
coordRailMode = "listen";
|
||||
coordRailCollapsed = false;
|
||||
}
|
||||
const playActive = Boolean(view.listenSerial?.playActive);
|
||||
coordRail.dataset.listenPlay = playActive ? "1" : "0";
|
||||
coordRail.hidden = false;
|
||||
if (drawerBody) {
|
||||
drawerBody.innerHTML = renderListenSerialRail(view);
|
||||
if (!drawerBody.dataset.listenConsumeWired) {
|
||||
drawerBody.dataset.listenConsumeWired = "1";
|
||||
drawerBody.addEventListener("click", (e) => {
|
||||
const enterBtn = e.target.closest("[data-enter-play]");
|
||||
if (enterBtn && drawerBody.contains(enterBtn) && !enterBtn.disabled) {
|
||||
handlers?.onEnterPlay?.();
|
||||
return;
|
||||
}
|
||||
const leaveBtn = e.target.closest("[data-listen-leave-design]");
|
||||
if (leaveBtn && drawerBody.contains(leaveBtn)) {
|
||||
handlers?.onLeaveListenPlay?.();
|
||||
return;
|
||||
}
|
||||
const stopBtn = e.target.closest("[data-listen-tts-stop]");
|
||||
if (stopBtn && drawerBody.contains(stopBtn)) {
|
||||
handlers?.onListenStop?.();
|
||||
@@ -6270,8 +6331,14 @@ function fillCoordRailListen(view, handlers) {
|
||||
}
|
||||
const n = view.listenSerial?.sections?.length ?? 0;
|
||||
if (drawerCount) {
|
||||
drawerCount.textContent = n ? String(n) : "";
|
||||
drawerCount.title = n ? `${n} 节正文` : "尚无正文";
|
||||
if (playActive) {
|
||||
drawerCount.textContent = n ? String(n) : "";
|
||||
drawerCount.title = n ? `${n} 节正文` : "尚无正文";
|
||||
} else {
|
||||
const hasBrief = Boolean(view.listenSerial?.thrill || view.listenSerial?.world);
|
||||
drawerCount.textContent = hasBrief ? "·" : "";
|
||||
drawerCount.title = hasBrief ? "本局设定" : "尚无本局设定";
|
||||
}
|
||||
}
|
||||
syncCoordRailChrome();
|
||||
}
|
||||
@@ -6282,17 +6349,19 @@ function fillCoordRail(visible, view, handlers) {
|
||||
const drawerCount = document.getElementById("coord-drawer-count");
|
||||
if (!coordRail) return;
|
||||
|
||||
if (view.creationMode === "listen_serial" && !isPlayView(view)) {
|
||||
if (view.creationMode === "listen_serial") {
|
||||
fillCoordRailListen(view, handlers);
|
||||
return;
|
||||
}
|
||||
|
||||
// 转述产物栏只在创作层;游玩层与配方开玩一样走回合历史
|
||||
if (view.creationMode === "dictate" && !isPlayView(view)) {
|
||||
delete coordRail.dataset.listenPlay;
|
||||
fillCoordRailProducts(view);
|
||||
return;
|
||||
}
|
||||
|
||||
delete coordRail.dataset.listenPlay;
|
||||
if (coordRailMode !== "history") {
|
||||
coordRailMode = "history";
|
||||
coordRailCollapsed = true;
|
||||
@@ -6553,9 +6622,13 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
surface === "dictate" && view.selectedRecipe?.name
|
||||
? esc(view.selectedRecipe.name)
|
||||
: "";
|
||||
const kicker = displayCreationModeLabel(
|
||||
surface === "listen" ? "listen_serial" : "dictate",
|
||||
);
|
||||
const listenPlay = surface === "listen" && Boolean(view.listenSerial?.playActive);
|
||||
const kicker =
|
||||
surface === "listen"
|
||||
? listenPlay
|
||||
? "听书播放"
|
||||
: displayCreationModeLabel("listen_serial")
|
||||
: displayCreationModeLabel("dictate");
|
||||
const head = recipeName
|
||||
? `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">${kicker}</span><span class="dictate-stage-recipe">配方 · ${recipeName}</span></header>`
|
||||
: `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">${kicker}</span></header>`;
|
||||
@@ -6566,7 +6639,9 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
appendMessagesToFeed(dictateFeed, visible, view, handlers, activeQuestions, {
|
||||
emptyText:
|
||||
surface === "listen"
|
||||
? "用几句话描述题材与爽点。确认后我会写大纲和正文;右侧可看缓冲与大纲。"
|
||||
? listenPlay
|
||||
? "右侧是大纲与正文。说「继续」补货,或一句意见改向。"
|
||||
: "用几句话描述题材与爽点。确认本局后点「播放」写大纲与正文。"
|
||||
: "直接说你想要的体验与设定。我会整理进右侧产物,并在回复里跟你确认、建议补充。",
|
||||
loading,
|
||||
});
|
||||
@@ -6578,7 +6653,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
const label =
|
||||
live?.label ??
|
||||
view.focus?.action ??
|
||||
(surface === "listen" ? "连载中" : "整理中");
|
||||
(surface === "listen"
|
||||
? listenPlay
|
||||
? "写稿中"
|
||||
: "定局中"
|
||||
: "整理中");
|
||||
pending.innerHTML = `
|
||||
<div class="msg-bubble">
|
||||
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
|
||||
|
||||
+175
-42
@@ -10,7 +10,12 @@ import {
|
||||
} from "./agent-ui.js";
|
||||
import { downloadMarkdown, sessionToMarkdown } from "./export.js";
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { displaySkillPackLabel, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import {
|
||||
displayCreationModeLabel,
|
||||
displaySkillPackLabel,
|
||||
isFlowPlanReview,
|
||||
reviewComposerCopy,
|
||||
} from "./display-labels.js";
|
||||
import {
|
||||
clearQuestionCardState,
|
||||
collectQuestionAnswers,
|
||||
@@ -236,6 +241,33 @@ function resolveUserTask(view, loading) {
|
||||
const hasQuestions = Boolean(getActiveQuestions(view)?.questions?.length);
|
||||
const moduleOpening = isModuleOpeningWaiting(view);
|
||||
|
||||
if (view.creationMode === "listen_serial") {
|
||||
const ls = view.listenSerial;
|
||||
if (ls?.playActive) {
|
||||
return {
|
||||
id: "speak",
|
||||
label: "播放",
|
||||
title: ls.phase === "soft_stop" ? "本段已兑现" : "听书播放",
|
||||
hint:
|
||||
ls.phase === "soft_stop"
|
||||
? "「下一波」或「收工」,或一句意见改向。"
|
||||
: "说「继续」补货;不满意直接一句意见。",
|
||||
};
|
||||
}
|
||||
const phase = ls?.phase;
|
||||
return {
|
||||
id: "speak",
|
||||
label: "听书",
|
||||
title: displayCreationModeLabel("listen_serial"),
|
||||
hint:
|
||||
phase === "awaiting_confirm"
|
||||
? "回复「确认」锁定本局;确认后点「播放」。"
|
||||
: phase === "ready"
|
||||
? "点「播放」写大纲与正文;也可继续改爽点。"
|
||||
: "用几句话描述题材与爽点。",
|
||||
};
|
||||
}
|
||||
|
||||
if (isPlayView(view)) {
|
||||
return {
|
||||
id: "speak",
|
||||
@@ -492,6 +524,31 @@ function resolveComposer(view, loading) {
|
||||
};
|
||||
}
|
||||
|
||||
if (view.creationMode === "listen_serial") {
|
||||
const send = view.actions?.find((a) => a.type === "send_message");
|
||||
const ls = view.listenSerial;
|
||||
let placeholder = "描述你想写什么…";
|
||||
if (ls?.playActive) {
|
||||
placeholder =
|
||||
ls.phase === "soft_stop"
|
||||
? "下一波 / 收工 / 一句改向…"
|
||||
: "继续 / 一句意见改向…";
|
||||
} else if (ls?.phase === "awaiting_confirm") {
|
||||
placeholder = "确认 / 或继续改需求…";
|
||||
} else if (ls?.phase === "ready") {
|
||||
placeholder = "继续改爽点,或点右侧「播放」…";
|
||||
}
|
||||
return {
|
||||
mode: "input",
|
||||
task,
|
||||
taskTitle: task.title || displayCreationModeLabel("listen_serial"),
|
||||
taskHint: task.hint,
|
||||
placeholder: send?.placeholder ?? placeholder,
|
||||
hint: null,
|
||||
submitLabel: "发送",
|
||||
};
|
||||
}
|
||||
|
||||
if (isPlayView(view)) {
|
||||
const send = view.actions?.find((a) => a.type === "send_message");
|
||||
return {
|
||||
@@ -1990,11 +2047,22 @@ function showEnterPicker(bookId, anchorEl) {
|
||||
el.classList.remove("is-picking");
|
||||
});
|
||||
anchorEl.classList.add("is-picking");
|
||||
const book = books.find((b) => b.id === bookId);
|
||||
const listen = bookCreationMode(book) === "listen_serial";
|
||||
const designBtn = pop.querySelector("[data-enter=design]");
|
||||
if (designBtn) designBtn.textContent = "创作";
|
||||
const playBtn = pop.querySelector("[data-enter=play]");
|
||||
const ready = bookPlayReady(bookId);
|
||||
if (playBtn) {
|
||||
playBtn.disabled = !ready;
|
||||
playBtn.title = ready ? "进入游玩" : "验收后可游玩";
|
||||
playBtn.textContent = listen ? "播放" : "游玩";
|
||||
playBtn.disabled = listen ? false : !ready;
|
||||
playBtn.title = listen
|
||||
? ready
|
||||
? "进入播放,写大纲与正文"
|
||||
: "确认本局后可播放(未确认会提示)"
|
||||
: ready
|
||||
? "进入游玩"
|
||||
: "验收后可游玩";
|
||||
}
|
||||
pop.hidden = false;
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
@@ -2149,6 +2217,9 @@ function getNavBook() {
|
||||
|
||||
function catalogBookMeta(book) {
|
||||
if (bookCreationMode(book) === "listen_serial") {
|
||||
if (lastView?.bookId === book.id && lastView.playReady) {
|
||||
return lastView.playLayerActive ? "播放中" : "可播放";
|
||||
}
|
||||
return "听书";
|
||||
}
|
||||
const saves = playSavesByBook.get(book.id);
|
||||
@@ -2582,7 +2653,6 @@ function renderBookList() {
|
||||
|
||||
for (const book of modeBooks) {
|
||||
const selected = selectedBookIds.has(book.id);
|
||||
const listen = bookCreationMode(book) === "listen_serial";
|
||||
list.appendChild(
|
||||
renderExplorerRow({
|
||||
name: book.title,
|
||||
@@ -2597,10 +2667,6 @@ function renderBookList() {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (listen) {
|
||||
void openBook(book.id);
|
||||
return;
|
||||
}
|
||||
if (enterPickerBookId === book.id) {
|
||||
hideEnterPicker();
|
||||
return;
|
||||
@@ -2884,9 +2950,15 @@ async function openBookDesign(bookId) {
|
||||
|
||||
async function openCurrentPlay(bookId) {
|
||||
hideEnterPicker();
|
||||
const instanceId = playingInstanceIdForBook({ id: bookId });
|
||||
if (instanceId) ensureSpecExpanded(bookId, instanceId);
|
||||
else navigateToBook(bookId);
|
||||
const book = books.find((b) => b.id === bookId);
|
||||
const listen = bookCreationMode(book) === "listen_serial";
|
||||
if (!listen) {
|
||||
const instanceId = playingInstanceIdForBook({ id: bookId });
|
||||
if (instanceId) ensureSpecExpanded(bookId, instanceId);
|
||||
else navigateToBook(bookId);
|
||||
} else {
|
||||
navigateToBook(bookId);
|
||||
}
|
||||
const alreadyHere =
|
||||
activeBookId === bookId &&
|
||||
isPlayNow() &&
|
||||
@@ -2900,6 +2972,11 @@ async function openCurrentPlay(bookId) {
|
||||
if (activeBookId !== bookId || !sessionId) {
|
||||
await openBook(bookId);
|
||||
}
|
||||
if (listen) {
|
||||
activePlaySaveId = null;
|
||||
await enterListenPlay();
|
||||
return;
|
||||
}
|
||||
if (activePlaySaveId && lastView?.playLayerActive) {
|
||||
try {
|
||||
const data = await api(
|
||||
@@ -2923,10 +3000,15 @@ function renderHeader(view, loading) {
|
||||
document.title = `${title} · Writing Agent`;
|
||||
$("status-dot").className = `status-dot ${st.cls}`;
|
||||
$("status-text").textContent = st.text;
|
||||
const listenMode = view.creationMode === "listen_serial";
|
||||
const showSavePlay = Boolean(
|
||||
view.bookId && view.playReady && view.lifecycleStage === "play" && view.playLayerActive,
|
||||
!listenMode &&
|
||||
view.bookId &&
|
||||
view.playReady &&
|
||||
view.lifecycleStage === "play" &&
|
||||
view.playLayerActive,
|
||||
);
|
||||
const showSaveInstance = Boolean(view.bookId && view.playReady);
|
||||
const showSaveInstance = Boolean(!listenMode && view.bookId && view.playReady);
|
||||
const btnPlay = $("btn-save-play");
|
||||
if (btnPlay) btnPlay.hidden = !showSavePlay;
|
||||
const btnInst = $("btn-save-instance");
|
||||
@@ -3030,6 +3112,23 @@ function isStaleUiRequest(seq) {
|
||||
return seq !== inflightSeq;
|
||||
}
|
||||
|
||||
/** 真值 / 程序投影指纹:旁观改表后即使消息条数不变也要刷正文壳 */
|
||||
function sessionTruthSig(view) {
|
||||
const rows = view?.playAux?.table?.rows;
|
||||
if (Array.isArray(rows) && rows.length) {
|
||||
return rows.map((r) => `${r.key}:${r.rev ?? 0}`).join(",");
|
||||
}
|
||||
const projected = view?.presentationProjection;
|
||||
if (projected && typeof projected === "object") {
|
||||
try {
|
||||
return `p:${JSON.stringify(projected).length}:${Object.keys(projected).sort().join(",")}`;
|
||||
} catch {
|
||||
return "p:?";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function sessionStructuralSig(view) {
|
||||
const messages = view?.messages ?? [];
|
||||
const last = messages[messages.length - 1];
|
||||
@@ -3042,6 +3141,8 @@ function sessionStructuralSig(view) {
|
||||
view?.phase ?? "",
|
||||
view?.waitingReason?.kind ?? "",
|
||||
view?.runActive ? "1" : "0",
|
||||
// 副 LLM 填表落后于主世界出稿;表 rev / 投影变了必须重渲染 present
|
||||
sessionTruthSig(view),
|
||||
].join("|");
|
||||
}
|
||||
|
||||
@@ -3173,13 +3274,23 @@ function startLivePoll() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 结构未变时仍并入真值投影,避免 lastView 卡在旁观改表前的旧壳数据
|
||||
const truthChanged = sessionTruthSig(view) !== sessionTruthSig(lastView);
|
||||
lastView = {
|
||||
...lastView,
|
||||
liveStream: view.liveStream,
|
||||
agentThinking: view.agentThinking,
|
||||
runActive: view.runActive,
|
||||
dictateProducts: view.dictateProducts ?? lastView.dictateProducts,
|
||||
playAux: view.playAux ?? lastView.playAux,
|
||||
presentationProjection:
|
||||
view.presentationProjection ?? lastView.presentationProjection,
|
||||
};
|
||||
if (truthChanged) {
|
||||
livePollStructuralSig = sessionStructuralSig(view);
|
||||
renderSession(view, true);
|
||||
return;
|
||||
}
|
||||
updateLiveStreamPanel(view);
|
||||
} catch {
|
||||
livePollFailCount += 1;
|
||||
@@ -3247,6 +3358,7 @@ function renderSession(view, loading = false) {
|
||||
onSpawnCreationStep: (moduleName) => runAction("spawn_step", { moduleName }),
|
||||
onLeaveCreationStep: () => runAction("leave_step"),
|
||||
onEnterPlay: () => enterPlayNow(),
|
||||
onLeaveListenPlay: () => void setLifecycle("design"),
|
||||
onSaveProduct: () => void saveCurrentInstance(),
|
||||
onPatchTable: (tag, cells) => void patchPlayTable(tag, cells),
|
||||
onSelectOpening: (index, usePersona) => void selectPlayOpening(index, usePersona),
|
||||
@@ -3546,6 +3658,10 @@ async function runAction(action, extra = {}) {
|
||||
}
|
||||
|
||||
function enterPlayNow() {
|
||||
if (lastView?.creationMode === "listen_serial") {
|
||||
void enterListenPlay();
|
||||
return;
|
||||
}
|
||||
if (activeBookId) {
|
||||
void openCurrentPlay(activeBookId);
|
||||
return;
|
||||
@@ -3555,6 +3671,25 @@ function enterPlayNow() {
|
||||
})();
|
||||
}
|
||||
|
||||
async function enterListenPlay() {
|
||||
if (!sessionId) return;
|
||||
const seq = beginUiRequest();
|
||||
try {
|
||||
renderSession(lastView, true);
|
||||
const view = await api(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}/listen/play`,
|
||||
{ method: "POST", body: "{}" },
|
||||
);
|
||||
if (isStaleUiRequest(seq)) return;
|
||||
if (activeBookId) navigateToBook(activeBookId);
|
||||
renderSession(view, false);
|
||||
} catch (err) {
|
||||
if (isStaleUiRequest(seq)) return;
|
||||
alert(err.message || "无法进入播放");
|
||||
if (lastView) renderSession({ ...lastView, hints: [err.message] }, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function patchPlayTable(tag, cells) {
|
||||
if (!sessionId) return;
|
||||
try {
|
||||
@@ -3598,6 +3733,13 @@ async function selectPlayOpening(index, usePersona) {
|
||||
|
||||
async function setLifecycle(stage, opts) {
|
||||
if (!sessionId) return;
|
||||
if (lastView?.creationMode === "listen_serial") {
|
||||
if (stage === "play") {
|
||||
await enterListenPlay();
|
||||
return;
|
||||
}
|
||||
// design:离开播放层
|
||||
}
|
||||
if (stage === lastView?.lifecycleStage) {
|
||||
if (stage === "design") return;
|
||||
if (
|
||||
@@ -3608,7 +3750,13 @@ async function setLifecycle(stage, opts) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (stage === "play" && !lastView?.playReady && !canOfferEnterPlay(lastView) && !lastView?.hasProduct) {
|
||||
if (
|
||||
stage === "play" &&
|
||||
lastView?.creationMode !== "listen_serial" &&
|
||||
!lastView?.playReady &&
|
||||
!canOfferEnterPlay(lastView) &&
|
||||
!lastView?.hasProduct
|
||||
) {
|
||||
alert("请先保存产物,或完成收口后再开玩");
|
||||
return;
|
||||
}
|
||||
@@ -3629,7 +3777,9 @@ async function setLifecycle(stage, opts) {
|
||||
navigateToBook(activeBookId);
|
||||
}
|
||||
renderSession(view, false);
|
||||
if (activeBookId) void fetchPlaySavesForBook(activeBookId, true);
|
||||
if (activeBookId && lastView?.creationMode !== "listen_serial") {
|
||||
void fetchPlaySavesForBook(activeBookId, true);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message || "无法切换阶段");
|
||||
if (lastView) renderSession(lastView, false);
|
||||
@@ -3657,26 +3807,16 @@ function resolveNewBookCreationMode(opts = {}) {
|
||||
function openNewBookDialog(opts = {}) {
|
||||
$("input-book-title").value = "";
|
||||
const locked = resolveNewBookCreationMode(opts);
|
||||
const modeField = $("field-creation-mode");
|
||||
const modeSel = $("input-creation-mode");
|
||||
if (!locked) {
|
||||
alert("请先进入「对话落盘」或「听书连载」文件夹,再新建作品。");
|
||||
return;
|
||||
}
|
||||
const lockedInput = $("input-creation-mode-locked");
|
||||
const desc = $("dialog-new-book-desc");
|
||||
|
||||
if (locked) {
|
||||
if (modeField) modeField.hidden = true;
|
||||
if (lockedInput) lockedInput.value = locked;
|
||||
if (modeSel) modeSel.value = locked;
|
||||
if (desc) {
|
||||
desc.textContent = `将创建到「${creationModeFolderLabel(locked)}」,填写名称即可。`;
|
||||
}
|
||||
} else {
|
||||
// 仍在目录根:须先选方式,或临时露出下拉
|
||||
if (modeField) modeField.hidden = false;
|
||||
if (lockedInput) lockedInput.value = "";
|
||||
if (modeSel) modeSel.value = "dictate";
|
||||
if (desc) desc.textContent = "先选创作方式,再填写名称。";
|
||||
if (lockedInput) lockedInput.value = locked;
|
||||
if (desc) {
|
||||
desc.textContent = `将创建到「${creationModeFolderLabel(locked)}」,填写名称即可。`;
|
||||
}
|
||||
|
||||
$("dialog-new-book").showModal();
|
||||
$("input-book-title").focus();
|
||||
}
|
||||
@@ -3684,9 +3824,9 @@ function openNewBookDialog(opts = {}) {
|
||||
async function createBook() {
|
||||
const title = $("input-book-title").value.trim() || "未命名作品";
|
||||
const locked = ($("input-creation-mode-locked")?.value || "").trim();
|
||||
const fromNav = resolveNewBookCreationMode();
|
||||
const creationMode =
|
||||
locked === "listen_serial" ||
|
||||
$("input-creation-mode")?.value === "listen_serial"
|
||||
locked === "listen_serial" || fromNav === "listen_serial"
|
||||
? "listen_serial"
|
||||
: "dictate";
|
||||
$("btn-create-book").disabled = true;
|
||||
@@ -4277,14 +4417,7 @@ $("lifecycle-toggle")?.addEventListener("click", (e) => {
|
||||
if (stage === "play") enterPlayNow();
|
||||
else if (stage === "design") void setLifecycle("design");
|
||||
});
|
||||
$("btn-new-book")?.addEventListener("click", () => {
|
||||
const locked = resolveNewBookCreationMode();
|
||||
if (!locked && sidebarNav.level === "root") {
|
||||
alert("请先进入「对话落盘」或「听书连载」文件夹,再新建作品。");
|
||||
return;
|
||||
}
|
||||
openNewBookDialog(locked ? { presetMode: locked } : {});
|
||||
});
|
||||
$("btn-new-book")?.addEventListener("click", () => openNewBookDialog());
|
||||
$("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close());
|
||||
$("book-enter-pop")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -201,13 +201,6 @@
|
||||
名称
|
||||
<input id="input-book-title" type="text" placeholder="未命名作品" maxlength="80" autofocus />
|
||||
</label>
|
||||
<label class="field" id="field-creation-mode" hidden>
|
||||
创作方式
|
||||
<select id="input-creation-mode">
|
||||
<option value="dictate" selected>对话落盘(可玩规格)</option>
|
||||
<option value="listen_serial">听书连载(连续消费)</option>
|
||||
</select>
|
||||
</label>
|
||||
<input type="hidden" id="input-creation-mode-locked" value="" />
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="btn" value="cancel" id="btn-cancel-new">取消</button>
|
||||
|
||||
@@ -1457,6 +1457,12 @@ body.design-workspace[data-design-surface="dictate"] .panel-feed,
|
||||
body.design-workspace[data-design-surface="listen"] .panel-feed {
|
||||
overflow: hidden;
|
||||
}
|
||||
.listen-rail-hint {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.82rem;
|
||||
opacity: 0.72;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.listen-rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user