import {
renderWorkspace,
updateLiveStreamPanel,
resetRailChrome,
setRailTab,
fillStepArtifactDialog,
canOfferSaveProduct,
canOfferEnterPlay,
} 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 {
clearQuestionCardState,
collectQuestionAnswers,
getActiveQuestions,
isModuleOpeningWaiting,
renderQuestionsCard,
} from "./questions-ui.js";
import {
applyColorTheme,
applyPresentChrome,
getStoredColorTheme,
getStoredPresentChrome,
initColorTheme,
initPresentChrome,
themeById,
chromeById,
} from "./theme.js";
let sessionId = null;
let activeBookId = null;
let lastView = null;
let books = [];
let composerForceInput = false;
let pendingComposerDraft = "";
let pendingComposerDraftSeq = 0;
/** 正在提交底栏:重绘时不要把已捕获的正文再塞进 pending */
let skipComposerStash = false;
let sidebarNav = { level: "root" };
let bookSelectMode = false;
let selectedBookIds = new Set();
let activePlaySaveId = null;
const playSavesByBook = new Map();
const playWorkingInstanceByBook = new Map();
const playSavesLoading = new Set();
let bookMenuBookId = null;
let livePollTimer = null;
let inflightSeq = 0;
const LIVE_POLL_MS = 280;
const $ = (id) => document.getElementById(id);
const PHASE = { idle: "待命", running: "执行�?, waiting_user: "等待�?, done: "已完�?, error: "出错" };
/** 用户任务三态(表层);底层 waitingReason 映射进来 */
const USER_TASK = {
speak: { id: "speak", label: "说话", title: "继续�? },
answer: { id: "answer", label: "答题", title: "回答问题" },
review: { id: "review", label: "验收", title: "验收产物" },
};
function reviewCopyFromView(view) {
return reviewComposerCopy(view?.reviewArtifact?.workerId, {
hasQuestions: Boolean(getActiveQuestions(view)?.questions?.length),
outputTags: view?.reviewArtifact?.outputTags,
});
}
function isPlayView(view) {
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
}
/**
* �?waitingReason 收成用户三态:说话 / 答题+自由发挥 / 批阅�?
* @returns {{ id: 'speak'|'answer'|'review'|'busy'|'idle'|'error', label: string, title: string, hint: string|null }}
*/
function resolveUserTask(view, loading) {
if (loading || isAgentBusy(view, loading)) {
return {
id: "busy",
label: "执行�?,
title: view.focus?.action ?? "总管�?Worker 执行中�?,
hint: null,
};
}
if (composerForceInput) {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "说明意见",
hint: "写完发送即可�?,
};
}
if (view.phase === "done") {
return { id: "idle", label: "已完�?, title: "会话已结�?, hint: null };
}
if (view.phase === "error") {
return {
id: "error",
label: "出错",
title: "说明后重�?,
hint: view.hints?.[0] ?? "执行出错,可在下方重�?,
};
}
const wr = view.waitingReason;
const hasQuestions = Boolean(getActiveQuestions(view)?.questions?.length);
const moduleOpening = isModuleOpeningWaiting(view);
if (isPlayView(view)) {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "继续�?,
hint: "你说一句,世界推进一轮�?,
};
}
if (canOfferSaveProduct(view)) {
return {
id: "speak",
label: "可保�?,
title: "落档产物",
hint: view.hasProduct
? "可再保存一份新定稿,或用已有产物开玩。补节点仍点图�?
: "点「保存定稿」拆出产物。创作流程还在,之后用产物开玩�?,
};
}
if (isFlowPlanReview(view) || wr?.kind === "pick_creation_step") {
return {
id: "speak",
label: "选节�?,
title: "选要做的节点",
hint: "点可进入的节点即确认并开始;要改排在底栏写意见再发�?,
};
}
if (wr?.kind === "review_artifact") {
const copy = reviewCopyFromView(view);
return {
id: "review",
label: copy.taskLabel,
title: copy.taskTitle,
hint: copy.hint,
};
}
// 能力默认问题:对齐美学纲领开局 �?说话面,不进「答题�?
if (moduleOpening) {
const stepName = view.openingGuide?.stepName?.trim();
return {
id: "speak",
label: USER_TASK.speak.label,
title: stepName || "按引导先说几�?,
hint: "想到什么写什么,不必整齐;写完发送即可�?,
};
}
if (hasQuestions || wr?.kind === "worker_questions") {
return {
id: "answer",
label: USER_TASK.answer.label,
title: "回答问题",
hint: "先点选项作答;也可以在底栏自由补充�?,
};
}
if (wr?.kind === "approve_step") {
const proposed = view.proposedNextStep;
return {
id: "speak",
label: "确认",
title: proposed?.name
? proposed.mode === "revise"
? `回头修改 · ${proposed.name}`
: `接下来生�?· ${proposed.name}`
: "确认下一�?,
hint: proposed
? proposedOutputCopy(proposed)
: view.focus?.detail ?? "确认执行,或说明意见�?,
};
}
if (wr?.kind === "next_intent") {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "下一步想写什�?,
hint: "说说接下来想做什么;可留空,也可说回头改某步。发送后会展示下一节点�?,
};
}
if (wr?.kind === "revision") {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "说明修改意见",
hint: "写清楚要改哪里;发送后在现有产物上修改�?,
};
}
if (wr?.kind === "intake") {
const hasUser = (view.messages ?? []).some((m) => m.role === "user");
return {
id: "speak",
label: USER_TASK.speak.label,
title: hasUser ? "继续补充" : "描述你想创作什�?,
hint: hasUser
? "继续说细节,或等信息齐后确认�?
: "用几句话说明题材、玩法或爽点即可�?,
};
}
if (wr?.kind === "input" || wr?.kind === "skill_selection") {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "继续�?,
hint: view.hints?.[0] ?? "直接输入你的想法或补充�?,
};
}
if (view.phase === "waiting_user") {
return {
id: "speak",
label: USER_TASK.speak.label,
title: "继续�?,
hint: view.hints?.[0] ?? null,
};
}
return {
id: "idle",
label: PHASE[view.phase] ?? view.phase,
title: PHASE[view.phase] ?? "待命",
hint: null,
};
}
function applyUserTaskChrome(task) {
document.body.dataset.userTask = task?.id || "";
}
/** @type {{ id: string, name: string, description: string }[]} */
let directors = [];
async function api(path, options = {}) {
const res = await fetch(path, {
headers: { "Content-Type": "application/json" },
...options,
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "请求失败");
return data;
}
function esc(s) {
return String(s).replace(/&/g, "&").replace(//g, ">");
}
function proposedOutputCopy(proposed) {
const params = proposed?.params;
const target =
params && typeof params === "object"
? params.target ?? params.object ?? params["对象"] ?? params["生成对象"]
: null;
const targetText =
typeof target === "string" || typeof target === "number"
? String(target).trim()
: "";
if (proposed?.mode === "revise") {
if (targetText) {
return `将在既有�?{targetText}」的${proposed.name}上继续改,不是从零再生成。`;
}
return `将在既有�?{proposed?.name || "该步"}」上继续改,不是从零再生成。`;
}
if (targetText) {
return `将生成�?{targetText}」的${proposed.name}。`;
}
return `接下来将生成�?{proposed?.name || "下一项内�?}。`;
}
/** 确认下一步:给人看「生成什么」;�?target 时顺带露出后�?id,便于改对象时同步�?*/
function proposeVisibleFields(proposed) {
if (proposed?.kind === "prior-artifact") return [];
const missing = new Set(proposed?.paramsMissing || []);
const specs = Array.isArray(proposed?.paramSpecs) ? proposed.paramSpecs : [];
const byKey = new Map(specs.map((s) => [s.key, s]));
const out = [];
const push = (key) => {
const spec = byKey.get(key);
if (!spec || out.some((s) => s.key === key)) return;
out.push(spec);
};
if (byKey.has("target")) {
push("target");
// 对象与后�?id 绑定:改女租客→丧尸时,rule_id 也要换成 zombies 一类英�?
if (byKey.has("rule_id")) push("rule_id");
} else if (byKey.has("rule_id") && (missing.has("rule_id") || proposed?.name === "具体实例")) {
push("rule_id");
}
for (const key of missing) push(key);
return out;
}
function proposeFieldLabel(field) {
if (field.key === "rule_id" && field.label === "规则 id") return "后台 id";
return field.label || field.key;
}
function proposeFieldHint(field, proposed) {
if (field.key === "rule_id" && proposed?.paramSpecs?.some((s) => s.key === "target")) {
return "英文 kebab-case,须与上方对象对应;改对象时一并改(如丧尸→zombies�?;
}
return field.hint || field.key;
}
function statusFor(view, loading) {
const task = resolveUserTask(view, loading);
if (task.id === "busy") return { cls: "running", text: task.title };
if (task.id === "error") return { cls: "", text: task.label };
if (task.id === "idle" && view.phase === "done") return { cls: "done", text: "已完�? };
if (task.id === "speak" || task.id === "answer" || task.id === "review") {
return { cls: "waiting", text: task.label };
}
return { cls: "", text: task.label };
}
function isAgentBusy(view, loading) {
if (loading) return true;
return view.phase === "running" && !view.waitingReason;
}
function resolveComposer(view, loading) {
const task = resolveUserTask(view, loading);
applyUserTaskChrome(task);
if (task.id === "busy") {
return { mode: "waiting", text: task.title, task };
}
if (view.phase === "done") {
return { mode: "idle", text: "会话已结�?, task };
}
if (view.phase === "error") {
const send = view.actions?.find((a) => a.type === "send_message");
return {
mode: "input",
task,
taskTitle: task.title,
taskHint: task.hint,
placeholder: send?.placeholder ?? "说明后重试,或直接发送继续�?,
hint: null,
};
}
if (isPlayView(view)) {
const send = view.actions?.find((a) => a.type === "send_message");
return {
mode: "input",
task,
taskTitle: task.title || "继续�?,
taskHint: task.hint,
placeholder: send?.placeholder ?? "说你要做什么�?,
hint: null,
submitLabel: "发�?,
};
}
const confirmIntake = view.actions?.find((a) => a.type === "confirm_intake");
if (view.waitingReason?.kind === "intake" && view.intake) {
const hasUser = (view.messages ?? []).some((m) => m.role === "user");
const send = view.actions?.find((a) => a.type === "send_message");
return {
mode: view.intake.ready && confirmIntake ? "intake_ready" : "intake",
task,
taskTitle: task.title,
taskHint: task.hint,
intake: view.intake,
intakePrompt: hasUser ? view.intakePrompt : null,
showIntakePanel: hasUser,
confirmIntake,
placeholder: send?.placeholder ?? "描述你想创作什么�?,
};
}
const approve = view.actions?.find((a) => a.type === "approve");
const accept = view.actions?.find((a) => a.type === "accept");
if (approve && view.proposedNextStep && !composerForceInput) {
return {
mode: "propose_step",
task,
taskTitle: task.title,
taskHint: task.hint,
proposed: view.proposedNextStep,
primary: approve,
hint: null,
};
}
if (approve && !composerForceInput) {
return {
mode: "action",
task,
taskTitle: task.title,
taskHint: task.hint,
primary: approve,
primaryType: "approve",
hint: null,
showReject: true,
};
}
if (accept && view.waitingReason?.kind !== "review_artifact" && !composerForceInput) {
return {
mode: "action",
task,
taskTitle: "验收产物",
taskHint: "同意就接受;要改可先点「说明意见」�?,
primary: accept,
primaryType: "accept",
hint: null,
showReject: true,
};
}
const send = view.actions?.find((a) => a.type === "send_message");
const hasQuestionsCard = Boolean(getActiveQuestions(view)?.questions?.length);
if (
(view.waitingReason?.kind === "pick_creation_step" || isFlowPlanReview(view)) &&
!composerForceInput
) {
return {
mode: "pick_step",
task,
taskTitle: "选要做的节点",
taskHint: "点可进入的节点即确认并开始;虚线原型点一下增殖。要改排在底栏写意见再发�?,
placeholder: send?.placeholder ?? "例如:补舞台骨架、把某步改到开场白之前�?,
hint: null,
submitLabel: "发�?,
requiresText: true,
};
}
if (view.waitingReason?.kind === "review_artifact") {
const parseBroken = Boolean(view.reviewArtifact?.workerSetView?.parseError);
const copy = reviewCopyFromView(view);
return {
mode: "input",
task,
taskTitle: copy.taskTitle,
taskHint: copy.hint,
placeholder: send?.placeholder ?? copy.placeholder,
hint: null,
submitLabel: copy.submitLabel,
emptyEnterHint: copy.emptyEnterHint,
acceptAction: accept
? {
label: copy.acceptLabel,
title: copy.hint,
tone: copy.tone,
disabled: parseBroken,
disabledTitle: parseBroken
? "规格解析失败,请先写修改意见再改产物"
: undefined,
}
: null,
};
}
if (hasQuestionsCard && !composerForceInput) {
return {
mode: "input",
task,
taskTitle: task.title,
taskHint: task.hint,
placeholder: send?.placeholder ?? "也可在此自由补充�?,
hint: null,
submitLabel: "补充并发�?,
};
}
if (
send ||
view.waitingReason?.kind === "input" ||
view.waitingReason?.kind === "worker_questions" ||
view.waitingReason?.kind === "revision" ||
view.waitingReason?.kind === "next_intent" ||
view.waitingReason?.kind === "pick_creation_step" ||
composerForceInput
) {
const wr = view.waitingReason;
let placeholder = send?.placeholder ?? "输入你想说的�?;
let submitLabel = "发�?;
if (wr?.kind === "next_intent") {
placeholder = send?.placeholder ?? "下一步想写什么?(可留空)�?;
submitLabel = "继续";
} else if (wr?.kind === "approve_step" && composerForceInput) {
placeholder =
"例如:补开局 NPC、改成丧尸怪物规则(后�?id 也换�?zombies)�?;
submitLabel = "发送意�?;
} else if (wr?.kind === "revision") {
placeholder = "说明要改哪里�?;
submitLabel = "按意见修�?;
} else if (wr?.kind === "worker_questions" && !hasQuestionsCard) {
placeholder = isModuleOpeningWaiting(view)
? "想到什么写什么�?
: "直接回答问题�?;
} else if (
view.openingGuide?.text &&
!(view.messages ?? []).some((m) => m.role === "user")
) {
placeholder = "按上方引导写几句�?;
}
return {
mode: "input",
task,
taskTitle: task.title,
taskHint: task.hint,
placeholder,
hint: null,
submitLabel,
allowEmpty: wr?.kind === "next_intent",
};
}
const finish = view.actions?.find((a) => a.type === "finish");
if (finish) {
return {
mode: "action",
task,
taskTitle: "结束",
taskHint: null,
primary: finish,
primaryType: "finish",
hint: null,
showReject: false,
};
}
return { mode: "idle", text: "暂无可用操作", task };
}
function composerModeChip(spec) {
const id = spec.task?.id;
/* 说话由左�?emoji「更多」承担,不再显示文字 chip */
if (id !== "answer" && id !== "review") return "";
const label = spec.task.label || "";
const tip = [spec.taskTitle, spec.taskHint].filter(Boolean).join(" �?");
const tone = spec.acceptAction?.tone ? ` data-tone="${esc(spec.acceptAction.tone)}"` : "";
return `${esc(label)}`;
}
function composerInputShell(spec, { textareaHtml, trailing = "" } = {}) {
const chip = composerModeChip(spec);
const tone = spec.acceptAction?.tone
? ` data-tone="${esc(spec.acceptAction.tone)}"`
: "";
return `
`;
}
function syncThemeMenuUi() {
const colorId = document.documentElement.getAttribute("data-color-theme") || getStoredColorTheme();
const chromeId =
document.documentElement.getAttribute("data-present-chrome") || getStoredPresentChrome();
const colorMeta = themeById(colorId);
const chromeMeta = chromeById(chromeId);
const sum = $("theme-menu-sum");
if (sum) {
sum.title = `主题 · ${colorMeta.name} · ${chromeMeta.name}`;
if (!sum.querySelector(".hamburger-icon")) {
sum.innerHTML = ``;
}
}
document.querySelectorAll("[data-color-theme].more-theme-item").forEach((btn) => {
btn.classList.toggle("is-active", btn.getAttribute("data-color-theme") === colorMeta.id);
});
document.querySelectorAll("[data-present-chrome].more-theme-item").forEach((btn) => {
btn.classList.toggle("is-active", btn.getAttribute("data-present-chrome") === chromeMeta.id);
});
}
function leaveStepButtonHtml(spec) {
if (!spec.leaveStep) return "";
return ``;
}
function stashComposerDraftFromDom() {
if (skipComposerStash) return;
const input = $("composer-input");
if (!input) return;
const v = String(input.value ?? "");
if (v) {
pendingComposerDraft = v;
pendingComposerDraftSeq = 0;
}
}
function keepComposerDraft(text) {
const v = String(text ?? "");
if (!v) return;
pendingComposerDraft = v;
pendingComposerDraftSeq = 0;
}
function dropComposerDraft() {
pendingComposerDraft = "";
pendingComposerDraftSeq = 0;
}
/** 发送成功前调用,避�?loading 重绘把已发内容又填回输入�?*/
function clearComposerInput() {
dropComposerDraft();
const input = $("composer-input");
if (!input) return;
input.value = "";
autosizeComposerInput(input);
}
function renderComposer(view, loading) {
stashComposerDraftFromDom();
if (pendingComposerDraft) composerForceInput = true;
const host = $("composer");
const root = $("composer-main") || host;
if (!root || !host) return;
if (host._enterHandler) {
document.removeEventListener("keydown", host._enterHandler);
host._enterHandler = null;
}
const spec = resolveComposer(view, loading);
const leaveAct = view.actions?.find((a) => a.type === "leave_step");
if (leaveAct && spec.mode !== "waiting" && spec.mode !== "idle") {
spec.leaveStep = {
label: leaveAct.label || "返回节点",
title: "先不写这一步,回到节点选择",
};
}
if (spec.mode === "waiting") {
root.innerHTML = `
${esc(spec.text)}