import { renderIntakePanel } from "./intake-ui.js";
import { getActiveQuestions, renderQuestionsCard } from "./questions-ui.js";
import { displayWorkerLabel, formatWorkerDisplayTitle } from "./display-labels.js";
const HIDE_KINDS = new Set(["worker_stub"]);
/** 中间对话区不展示的内部调度消息(仅出现在右侧「历史」) */
const FEED_HIDDEN_KINDS = new Set([
"agent_tool",
"orchestrator_decision",
"worker_running",
]);
const MSG_CLASS = {
user_input: "user",
agent_tool: "agent",
orchestrator_decision: "agent",
orchestrator_thinking: "agent",
orchestrator_prompt: "agent",
orchestrator_assessment: "agent",
worker_running: "skill",
worker_output: "skill",
worker_questions: "questions",
error: "error",
};
const MSG_LABEL = {
user_input: "你",
agent_tool: "工具",
orchestrator_decision: "导演",
orchestrator_thinking: "导演 · 思考",
orchestrator_prompt: "导演",
orchestrator_assessment: "导演 · 内容评价",
worker_running: "Worker",
worker_output: "Worker",
worker_questions: "提问",
error: "错误",
system_info: "系统",
};
function esc(s) {
return String(s)
.replace(/&/g, "&")
.replace(//g, ">");
}
function renderThinkingBlock(thinking, { open = false } = {}) {
const text = (thinking ?? "").trim();
if (!text) return "";
return `
思考过程
${esc(text)}
`;
}
function renderLiveStreamBody(live) {
if (!live) return "…";
const parts = [];
if (live.thinking?.trim()) {
parts.push(
`${esc(live.thinking.trim())}`,
);
}
if (live.output?.trim()) {
parts.push(
`${esc(live.output.trim())}`,
);
}
return parts.length ? parts.join("") : "…";
}
function fmtTime(iso) {
if (!iso) return "";
return new Date(iso).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}
function msgLabel(msg) {
const k = msg.kind ?? (msg.role === "user" ? "user_input" : "system_info");
if (k.startsWith("worker") && msg.actor) {
if (k === "worker_questions") return formatWorkerDisplayTitle(msg.actor, "questions");
if (k === "worker_output") return formatWorkerDisplayTitle(msg.actor, "output");
if (k === "worker_running") return formatWorkerDisplayTitle(msg.actor, "running");
return displayWorkerLabel(msg.actor);
}
return MSG_LABEL[k] ?? k;
}
function canEditMessage(msg) {
return msg.role === "user";
}
function canRefreshMessage(msg) {
if (msg.role === "user") return false;
const kind = msg.kind ?? "system_info";
return kind === "worker_questions" || kind === "worker_output";
}
function renderMsgVariantBadge(msg) {
const total = msg.branchTotal ?? 1;
if (total <= 1) return "";
const index = (msg.branchIndex ?? 0) + 1;
return `${index}/${total}`;
}
function hideMsgMenu() {
const menu = document.getElementById("msg-action-menu");
if (menu) menu.hidden = true;
msgMenuState.messageId = null;
}
const msgMenuState = { messageId: null, handlers: null };
function showMsgMenu(card, x, y) {
const menu = document.getElementById("msg-action-menu");
if (!menu || !card) return;
const isReview = card.dataset.review === "1";
const canEdit = !isReview && card.dataset.canEdit === "1";
const canDelete = !isReview && Boolean(card.dataset.messageId);
const hasContext = card.dataset.hasContext === "1";
const editBtn = document.getElementById("msg-menu-edit");
const delBtn = document.getElementById("msg-menu-delete");
const ctxBtn = document.getElementById("msg-menu-context");
if (editBtn) editBtn.toggleAttribute("hidden", !canEdit);
if (delBtn) delBtn.toggleAttribute("hidden", !canDelete);
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
menu.hidden = false;
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
msgMenuState.messageId = card.dataset.messageId ?? null;
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
}
if (rect.bottom > window.innerHeight) {
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
}
}
function wireMsgActionMenu(handlers) {
const menu = document.getElementById("msg-action-menu");
if (!menu) return;
msgMenuState.handlers = handlers;
if (menu.dataset.wired) return;
menu.dataset.wired = "1";
menu.addEventListener("click", async (e) => {
const action = e.target.closest("[data-msg-menu-action]")?.getAttribute("data-msg-menu-action");
const messageId = msgMenuState.messageId;
hideMsgMenu();
if (!messageId || !action) return;
const card = document.querySelector(`[data-message-id="${messageId}"]`);
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing)");
const body = bodyEl?.textContent ?? card?.dataset.originalText ?? "";
if (action === "copy") {
try {
await navigator.clipboard.writeText(body);
} catch {
/* ignore */
}
return;
}
if (action === "view-context") {
msgMenuState.handlers?.onViewContext?.(messageId);
return;
}
if (action === "edit") {
startInlineEdit(card, body);
return;
}
if (action === "delete") {
if (!confirm("删除此消息及之后的对话?")) return;
msgMenuState.handlers?.onDeleteMessage?.(messageId);
}
});
}
function wireMessageContextMenu(feed, handlers) {
if (!feed || feed.dataset.contextWired) return;
feed.dataset.contextWired = "1";
wireMsgActionMenu(handlers);
feed.addEventListener("contextmenu", (e) => {
const bubble = e.target.closest(".msg-bubble");
const card = e.target.closest("[data-message-id]");
if (!bubble || !card || card.classList.contains("msg-pending")) return;
e.preventDefault();
showMsgMenu(card, e.clientX, e.clientY);
});
}
function startInlineEdit(card, bodyText) {
if (!card || card.classList.contains("is-editing")) return;
card.classList.add("is-editing");
card.dataset.originalText = bodyText;
card.querySelector(".msg-foot")?.setAttribute("hidden", "");
const bodyEl = card.querySelector(".msg-body");
if (!bodyEl) return;
const messageId = card.dataset.messageId ?? "";
bodyEl.outerHTML = `
Ctrl/⌘ + Enter 保存 · Esc 取消
`;
const ta = card.querySelector(".msg-edit-input");
if (ta) {
ta.focus();
const len = ta.value.length;
ta.setSelectionRange(len, len);
ta.style.height = "auto";
ta.style.height = `${Math.min(ta.scrollHeight, 320)}px`;
ta.addEventListener("input", () => {
ta.style.height = "auto";
ta.style.height = `${Math.min(ta.scrollHeight, 320)}px`;
});
ta.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
cancelInlineEdit(card);
}
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
card.querySelector("[data-msg-action=edit-save]")?.click();
}
});
}
}
function cancelInlineEdit(card) {
if (!card) return;
const original = card.dataset.originalText ?? "";
card.classList.remove("is-editing");
delete card.dataset.originalText;
card.querySelector(".msg-foot")?.removeAttribute("hidden");
const editing = card.querySelector(".msg-body-editing");
if (editing) {
editing.outerHTML = `${esc(original)}
`;
}
}
function wireMessageFeedActions(feed, handlers) {
if (!feed || feed.dataset.actionsWired) return;
feed.dataset.actionsWired = "1";
feed.addEventListener("click", async (e) => {
const btn = e.target.closest("[data-msg-action]");
if (!btn || btn.disabled) return;
const action = btn.getAttribute("data-msg-action");
const messageId = btn.getAttribute("data-msg-id");
if (!messageId && action !== "edit-cancel" && action !== "edit-save") return;
const card = btn.closest("[data-message-id]");
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing)");
const body = bodyEl?.textContent ?? card?.dataset.originalText ?? "";
if (action === "copy") {
const copyText =
card?.querySelector(".msg-edit-input")?.value ??
card?.querySelector(".msg-body")?.textContent ??
body;
try {
await navigator.clipboard.writeText(copyText);
} catch {
/* ignore */
}
return;
}
if (action === "edit") {
startInlineEdit(card, body);
return;
}
if (action === "edit-cancel") {
cancelInlineEdit(card);
return;
}
if (action === "edit-save") {
const ta = card?.querySelector(".msg-edit-input");
const next = ta?.value?.trim() ?? "";
const original = (card?.dataset.originalText ?? body).trim();
cancelInlineEdit(card);
if (!next || next === original) return;
handlers.onEditMessage?.(messageId, next);
return;
}
if (action === "refresh") {
handlers.onRefreshMessage?.(messageId);
return;
}
if (action === "variant-prev") {
handlers.onSwitchVariant?.(messageId, "prev");
return;
}
if (action === "variant-next") {
handlers.onSwitchVariant?.(messageId, "next");
}
});
}
function msgBody(msg, view) {
let body = (msg.body ?? msg.text ?? "").trim();
if (msg.kind === "worker_questions") {
if (getActiveQuestions(view)) {
const qs = view.waitingReason?.questions ?? [];
const n = Array.isArray(qs) ? qs.length : 0;
return `提问中 · ${n || "?"} 题(请在下方询问卡作答)`;
}
if (!body) {
const wr = view.waitingReason;
if (wr?.kind === "worker_questions") {
const qs = wr.questions ?? [];
const prompts = qs.map((q) =>
typeof q === "string" ? q : q?.prompt,
).filter(Boolean);
if (prompts.length) body = prompts.map((q) => `- ${q}`).join("\n");
}
if (!body) body = "请补充当前 Worker 需要的信息。";
}
}
return body;
}
function formatQuestionsHtml(body) {
const lines = body
.split(/\n+/)
.map((l) => l.replace(/^[-*•]\s*/, "").trim())
.filter(Boolean);
if (lines.length <= 1) {
return `${esc(body)}
`;
}
return `${lines.map((l) => `- ${esc(l)}
`).join("")}
`;
}
/** 旧会话 skill 选择提示,新流程不再展示 */
const SKILL_SELECTION_RE = /请选择创作 skill|请选择创作类型/;
/** @deprecated 旧会话可能仍处于 skill_selection;新作品不再展示选包 UI */
export function renderSkillPicker(_view, _onPick) {
const el = document.getElementById("skill-picker");
if (!el) return;
el.hidden = true;
el.innerHTML = "";
}
let activeAgentTab = "timeline";
let lastAutoTabReason = null;
let userPinnedAgentTab = false;
export function setAgentTab(tab, { user = false } = {}) {
activeAgentTab = tab;
if (user) userPinnedAgentTab = true;
const tabs = document.getElementById("agent-tabs");
tabs?.querySelectorAll(".agent-tab").forEach((btn) => {
const id = btn.getAttribute("data-tab");
const active = id === tab && !btn.hidden;
btn.classList.toggle("active", active);
btn.setAttribute("aria-selected", active ? "true" : "false");
});
for (const id of ["timeline", "review", "progress", "board"]) {
const panel = document.getElementById(`agent-panel-${id}`);
panel?.classList.toggle("active", tab === id);
panel?.toggleAttribute("hidden", tab !== id);
}
}
function wireAgentTabs() {
const tabs = document.getElementById("agent-tabs");
if (!tabs || tabs.dataset.wired) return;
tabs.dataset.wired = "1";
tabs.addEventListener("click", (e) => {
const btn = e.target.closest(".agent-tab");
if (!btn || btn.hidden) return;
setAgentTab(btn.getAttribute("data-tab") ?? "timeline", { user: true });
});
}
function hasUserMessages(view) {
return (view.messages ?? []).some((m) => m.role === "user");
}
function isSkillSelectionMessage(msg) {
const body = (msg.body ?? msg.text ?? "").trim();
return SKILL_SELECTION_RE.test(body);
}
function isStartupPromptMessage(msg, view) {
if (msg.role === "user") return false;
const body = (msg.body ?? msg.text ?? "").trim();
const prompt = (view.intakePrompt ?? "").trim();
return Boolean(prompt && body === prompt);
}
function shouldShowInFeed(msg, view) {
if (msg.role === "user") return true;
const kind = msg.kind ?? "system_info";
if (HIDE_KINDS.has(kind)) return false;
if (FEED_HIDDEN_KINDS.has(kind)) return false;
if (msg.compressed) return false;
if (isSkillSelectionMessage(msg)) return false;
if (
view.waitingReason?.kind === "intake" &&
!hasUserMessages(view) &&
isStartupPromptMessage(msg, view)
) {
return false;
}
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact") {
return false;
}
if (kind === "orchestrator_prompt") return false;
return true;
}
function maybeAutoSwitchTab(view) {
const reason = view.waitingReason?.kind ?? null;
// 创作验收改在主对话展示,不再自动跳到侧栏「验收」Tab
if (reason === "review_artifact") {
if (lastAutoTabReason !== "review_artifact" && activeAgentTab === "review") {
setAgentTab("timeline");
}
} else if (!userPinnedAgentTab && reason === "worker_questions") {
if (lastAutoTabReason !== "worker_questions" && activeAgentTab === "review") {
setAgentTab("timeline");
}
}
lastAutoTabReason = reason;
}
export function renderLifecycle(view) {
const toggle = document.getElementById("lifecycle-toggle");
if (!toggle) return;
const stage = view.lifecycleStage ?? "design";
document.body.dataset.lifecycle = stage;
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
const s = btn.getAttribute("data-stage");
btn.classList.toggle("active", s === stage);
if (s === "play") {
btn.disabled = !view.playReady;
btn.title = view.playReady
? "进入游玩 / 写作"
: "请先验收 Worker 集(创作定稿)后再切换";
} else {
btn.disabled = false;
}
});
}
export function renderSkillGuide(view) {
wireAgentTabs();
const progressTabBtn = document.getElementById("agent-tab-btn-progress");
const list = document.getElementById("skill-guide-list");
const workerPanel = document.getElementById("worker-set-user-panel");
if (!list) return;
const hasCatalog =
view.lifecycleStage === "design" && (view.skillCatalog?.length ?? 0) > 0;
const hasWorkerView = Boolean(
view.workerSetView?.workers?.length || view.workerSetView?.contextTags?.length,
);
const hasFlowView = Boolean(view.creationFlowView?.steps?.length);
const show = hasCatalog || hasWorkerView || hasFlowView;
if (progressTabBtn) {
progressTabBtn.hidden = !show;
}
if (workerPanel) {
const parts = [];
if (hasFlowView && view.lifecycleStage === "design") {
parts.push(renderCreationFlowView(view.creationFlowView));
}
if (hasWorkerView && view.lifecycleStage === "design") {
parts.push(renderWorkerSetUserView(view.workerSetView, { compact: true }));
}
workerPanel.innerHTML = parts.join("");
workerPanel.hidden = !workerPanel.innerHTML;
}
if (!show) {
list.innerHTML = "";
if (activeAgentTab === "progress") setAgentTab("timeline");
return;
}
list.innerHTML = (view.skillCatalog ?? [])
.map(
(s) => `
${esc(s.label)} ${esc(s.id)}${s.runCount && s.runCount > 1 ? ` ×${s.runCount}` : ""}
${esc(s.purpose)}
`,
)
.join("");
}
function renderTagList(tags, emptyLabel) {
if (!tags?.length) {
return `${esc(emptyLabel)}
`;
}
return `${tags
.map((t, i) => {
const filled =
t.filled === true
? `✓`
: t.filled === false
? `○`
: "";
const note = t.note ? `${esc(t.note)}` : "";
return `${esc(t.tag)}${filled}${note} `;
})
.join("")}
`;
}
function howLabel(how) {
if (how === "prompt_body") return "注入正文";
if (how === "resident") return "常驻挂载";
return "读入黑板";
}
function formatFocusBodyHtml(body) {
if (body == null) return `(本块尚无正文,请在下方对话中补充)
`;
if (typeof body === "string") {
const t = body.trim();
return t
? `${esc(t)}`
: `(本块尚无正文,请在下方对话中补充)
`;
}
try {
const pretty = JSON.stringify(body, null, 2);
return `${highlightJson(pretty)}
`;
} catch {
return `${esc(String(body))}`;
}
}
function renderFocusUnitBanner(focusUnit) {
if (!focusUnit) return "";
const isContext =
focusUnit.kind === "fixed" ||
(typeof focusUnit.id === "string" &&
(focusUnit.id.startsWith("fixed:") || focusUnit.id.startsWith("resident:")));
const kindLabel =
focusUnit.kind === "worker"
? "创造 Worker"
: focusUnit.kind === "phase"
? "创作阶段"
: "填充上下文";
const hint = isContext
? "请重点审阅高亮区:这是本次要写入 / 验收的上下文正文。"
: focusUnit.kind === "worker"
? "请重点审阅当前 Worker 规格;展示名应使用中文。"
: "请重点审阅当前创作单位。";
const bodyHtml = isContext || focusUnit.kind === "phase"
? `${formatFocusBodyHtml(focusUnit.body)}
`
: "";
return `
${esc(kindLabel)}
${esc(focusUnit.label)}
${esc(focusUnit.id)}
${esc(hint)}
${bodyHtml}
`;
}
function renderContextTagCards(contextTags, opts = {}) {
if (!contextTags?.length) return "";
const compact = opts.compact === true;
const focusId = opts.focusId || "";
const focusContext = opts.focusContext === true;
const fixedFirst = [...contextTags].sort((a, b) => {
const rank = (k) => (k === "fixed" ? 0 : k === "resident" ? 1 : 2);
return rank(a.kind) - rank(b.kind);
});
const cards = fixedFirst
.filter((c) => (compact ? c.kind !== "board" : true))
.map((c) => {
const kindBadge =
c.kind === "fixed"
? `纲领`
: c.kind === "resident"
? `常驻`
: `黑板`;
const filled =
c.filled === true
? `✓`
: `○`;
const mountChips = (c.mounts || [])
.slice(0, compact ? 4 : 12)
.map(
(m) =>
`${esc(howLabel(m.how))}${esc(m.workerName)}`,
)
.join("");
const more =
(c.mounts?.length ?? 0) > (compact ? 4 : 12)
? `+${c.mounts.length - (compact ? 4 : 12)}`
: "";
const isFocus =
focusId &&
(c.id === focusId ||
(focusId.startsWith("fixed:aesthetics") && c.id.startsWith("fixed:aesthetics")) ||
(focusId === "fixed:aesthetics" && c.id.startsWith("fixed:aesthetics:")));
const dim = focusContext && focusId && !isFocus ? " ws-dimmed" : "";
const focusCls = isFocus ? " ws-context-focus" : "";
return `
${esc(c.label)}
${kindBadge}
${filled}
${isFocus ? `本次` : ""}
${c.preview && !compact ? `${esc(c.preview)}
` : ""}
塞进
${esc(c.mountSummary || "尚未指定 Worker")}
${mountChips || more ? `${mountChips}${more}
` : ""}
`;
})
.join("");
if (!cards) return "";
const sectionCls = focusContext ? " ws-section-focus-context" : "";
return `
${focusContext ? "固定上下文 · 本次要填" : "固定上下文"}
${
focusContext
? "高亮卡片是本次验收对象;其它块仅供对照。"
: "与 Worker 解耦:先看纲领挂到谁,再看下方分工。同一块可挂多个 Worker。"
}
${cards}
`;
}
function renderWorkerSetUserView(userView, opts = {}) {
if (!userView) return "";
if (userView.parseError) {
return `
规格无法解析
${esc(userView.parseError)}
Worker 集必须是 JSON 对象。若模型把说明/提问写进了规格字段,请在下方发送修改意见,让它重写后再接受。
`;
}
const compact = opts.compact === true;
const parts = [];
const focusUnit = userView.focusUnit;
const focusId = focusUnit?.id || "";
const focusContext =
focusUnit?.kind === "fixed" ||
(typeof focusId === "string" &&
(focusId.startsWith("fixed:") || focusId.startsWith("resident:")));
const focusWorkerRef =
focusUnit?.kind === "worker" && focusId.startsWith("worker:")
? focusId.slice("worker:".length)
: "";
// 当前单位横幅:填充上下文时高亮正文;创造 worker 时标明焦点
const focusBanner = renderFocusUnitBanner(focusUnit);
if (focusBanner) parts.push(focusBanner);
if (userView.headline) {
parts.push(
`这次体验
${esc(userView.headline)}
`,
);
}
if (userView.interactionParadigm) {
parts.push(
`交互范式
${esc(userView.interactionParadigm)}
`,
);
}
if (userView.coreWorker) {
parts.push(
`核心 Worker
${esc(userView.coreWorker)} — 负责推剧情并产出本轮实质内容
`,
);
}
if (!compact && userView.reasoning) {
parts.push(
`分工推理
${esc(userView.reasoning)}
`,
);
}
if (userView.playModeLabel) {
parts.push(
`游玩方式
${esc(userView.playModeLabel)}${userView.playModeHint ? ` — ${esc(userView.playModeHint)}` : ""}
`,
);
}
// 固定上下文独立区(在 worker 之前;填充模式下高亮本次块)
const contextSection = renderContextTagCards(userView.contextTags, {
compact,
focusId,
focusContext,
});
if (contextSection) parts.push(contextSection);
if (userView.inputProtocol?.length && !userView.contextTags?.some((c) => c.id === "fixed:input_protocol")) {
const rows = userView.inputProtocol
.map((p) => `${esc(p.label)}:${esc(p.value)}`)
.join("");
parts.push(``);
}
if (userView.workers?.length) {
const cards = userView.workers
.map((w) => {
const isFocusWorker = focusWorkerRef && w.id === focusWorkerRef;
const statusBadge =
w.status === "gap"
? `待补 SKILL`
: `就绪`;
const idLine = w.id ? `${esc(w.id)}` : "";
const roleBadge = w.roleLabel
? `${esc(w.roleLabel)}`
: "";
const acceptBadge = w.acceptanceLabel
? `${esc(w.acceptanceLabel)}`
: "";
const explicitNote = w.context?.explicit
? ""
: `部分读入来自包内默认模板
`;
const rationale = w.rationale
? `为何需要${esc(w.rationale)}
`
: "";
const merge = w.mergeConsidered
? `合并考量${esc(w.mergeConsidered)}
`
: "";
const reads = w.readsSummary
? `上下文${esc(w.readsSummary)}
`
: "";
const dim =
focusWorkerRef && !isFocusWorker
? " ws-dimmed"
: focusContext
? " ws-dimmed"
: "";
const focusCls = isFocusWorker ? " ws-worker-focus" : "";
return `
${w.order}
${esc(w.displayName)}
${isFocusWorker ? `本次` : ""}
${roleBadge}
${acceptBadge}
${statusBadge}
${idLine}
用来干嘛${esc(w.purpose)}
何时调用${esc(w.invokeWhen)}
${rationale}
${merge}
${reads}
${w.gapNote ? `${esc(w.gapNote)}
` : ""}
写入黑板
${w.writes?.length ? w.writes.map((t) => `${esc(t)}`).join(" ") : "—"}
${explicitNote}
`;
})
.join("");
parts.push(
`游玩 Worker${compact ? "(摘要)" : ""}${focusWorkerRef ? " · 本次规格" : ""}
${cards}
`,
);
}
if (!compact && userView.tagFlow?.length) {
const flow = userView.tagFlow
.map(
(edge) =>
`${edge.from.map((t) => `${esc(t)}`).join(" + ")}→${edge.to.map((t) => `${esc(t)}`).join(" + ")}
`,
)
.join("");
parts.push(``);
}
if (userView.creationUnits?.length) {
const units = userView.creationUnits
.map((u) => {
const kindLabel =
u.kind === "worker"
? "Worker"
: u.kind === "phase"
? "阶段"
: u.flavor === "resident"
? "常驻块"
: "固定上下文";
const filled = u.accepted
? "已验收"
: u.current
? "进行中"
: u.filled
? "已写"
: "待谈";
const weight = u.weighty ? " · 纲领" : "";
const cls = u.accepted
? "ws-task-filled"
: u.current
? "ws-task-planned ws-unit-current"
: "ws-task-planned";
return `${esc(kindLabel)}${esc(weight)}${esc(u.label)}${filled}${u.detail ? ` — ${esc(u.detail)}` : ""}`;
})
.join("");
parts.push(
`创作单位
纲领与 Worker 同级;不必先写完 Worker 再填上下文。
`,
);
}
if (userView.designTasks?.length) {
const tasks = userView.designTasks
.map((t) => {
const cls =
t.status === "filled"
? "ws-task-filled"
: t.status === "skipped"
? "ws-task-skipped"
: "ws-task-planned";
const statusLabel =
t.status === "filled" ? "已填" : t.status === "skipped" ? "跳过" : "待填";
return `${esc(t.label)}${esc(t.id)}${statusLabel}${t.skipReason ? ` — ${esc(t.skipReason)}` : ""}`;
})
.join("");
parts.push(
``,
);
}
if (userView.openQuestions?.length) {
parts.push(
`待澄清
${userView.openQuestions.map((q) => `- ${esc(q)}
`).join("")}
`,
);
}
if (userView.notes && !compact) {
parts.push(`补充说明
${esc(userView.notes)}
`);
}
return `${parts.join("")}
`;
}
/** 尝试把正文美化为缩进 JSON;失败则原样 pre */
function formatArtifactBodyHtml(body) {
const trimmed = (body || "").trim();
if (!trimmed) {
return `(无正文)
`;
}
const tryParse = (text) => {
try {
return JSON.parse(text);
} catch {
return null;
}
};
let parsed = tryParse(trimmed);
if (!parsed) {
const start = trimmed.indexOf("{");
const end = trimmed.lastIndexOf("}");
if (start >= 0 && end > start) {
parsed = tryParse(trimmed.slice(start, end + 1));
}
}
if (!parsed) {
const start = trimmed.indexOf("[");
const end = trimmed.lastIndexOf("]");
if (start >= 0 && end > start) {
parsed = tryParse(trimmed.slice(start, end + 1));
}
}
if (parsed != null && typeof parsed === "object") {
const pretty = JSON.stringify(parsed, null, 2);
return `${highlightJson(pretty)}
`;
}
return `${esc(trimmed)}`;
}
/** 极简 JSON 着色(仅 key / string / 其它) */
function highlightJson(pretty) {
return pretty
.split("\n")
.map((line) => {
const m = /^(\s*)("(?:\\.|[^"\\])*")(\s*:\s*)(.*)$/.exec(line);
if (m) {
const [, indent, key, colon, rest] = m;
let valueHtml = esc(rest);
if (/^"/.test(rest.trim())) {
valueHtml = `${esc(rest)}`;
} else if (/^(true|false|null)\b/.test(rest.trim())) {
valueHtml = `${esc(rest)}`;
} else if (/^-?\d/.test(rest.trim())) {
valueHtml = `${esc(rest)}`;
}
return `${indent}${esc(key)}${esc(colon)}${valueHtml}`;
}
return esc(line);
})
.join("\n");
}
function renderCreationFlowView(flowView) {
if (!flowView) return "";
if (flowView.parseError) {
return `
流程无法解析
${esc(flowView.parseError)}
需要 JSON:steps 数组,每步含 name(与可选 id)与 depends_on;可含 status=open|closed。
`;
}
if (!flowView.steps?.length) return "";
const brief = flowView.brief
? `${esc(flowView.brief)}
`
: "";
const statusLabel =
flowView.status === "open"
? "可继续追加"
: flowView.status === "closed"
? "已收口"
: "";
const statusHtml = statusLabel
? `${esc(statusLabel)}
`
: "";
const rows = flowView.steps
.map((s) => {
const deps =
s.depends_on?.length > 0
? s.depends_on.map((d) => esc(d)).join("、")
: "无";
const occ =
s.occurrence && s.occurrence > 1
? `第 ${esc(String(s.occurrence))} 次`
: s.repeatable
? `可反复`
: "";
const nameLabel =
s.id && s.id !== s.name
? `${esc(s.name)} (${esc(s.id)})`
: esc(s.name);
return `
${esc(String(s.order))}
${nameLabel}${occ}
依赖:${deps}
`;
})
.join("");
return `
创作流程
${brief}
${statusHtml}
${rows}
`;
}
function renderReviewFeedCard(review) {
const flowHtml = review.creationFlowView
? renderCreationFlowView(review.creationFlowView)
: "";
const structured = !flowHtml && review.workerSetView
? renderWorkerSetUserView(review.workerSetView)
: "";
const jsonHtml = formatArtifactBodyHtml(review.body);
const contextId = review.sourceMessageId || review.id || "";
const hasContext = review.contextTrace ? "1" : "0";
const structuredBlock = flowHtml || structured;
return `
${review.summary ? `
${esc(review.summary)}
` : ""}
接受后:本单位过程讨论会折叠,产物进入前情。确认请用底栏「接受目前产物」;要改则在底栏输入修改意见后发送。
${structuredBlock ? `
${structuredBlock}
` : ""}
${structuredBlock ? "规格 JSON" : "产物正文"}
${jsonHtml}
`;
}
/** 侧栏验收 Tab 已弃用:验收改在主对话;此处仅隐藏 Tab */
export function renderReviewPanel(view, _handlers = {}) {
const tabBtn = document.getElementById("agent-tab-btn-review");
const panel = document.getElementById("review-artifact-panel");
if (!tabBtn || !panel) return;
tabBtn.hidden = true;
panel.innerHTML = "";
if (activeAgentTab === "review") setAgentTab("timeline");
}
export function renderAgentPanel(view, loading) {
const focusEl = document.getElementById("agent-focus");
const traceEl = document.getElementById("tool-trace");
const timelineEl = document.getElementById("agent-timeline");
const badgeEl = document.getElementById("burst-badge");
if (!focusEl || !timelineEl) return;
const f = view.focus;
if (f) {
focusEl.innerHTML = `
${esc(f.actorLabel)}
${esc(loading ? "处理中…" : f.action)}
${f.detail ? `${esc(f.detail)}
` : ""}`;
} else {
focusEl.innerHTML = `待命
`;
}
if (badgeEl && view.burst) {
const show = view.burst.count > 0 || view.phase === "running";
badgeEl.hidden = !show;
badgeEl.textContent = `burst ${view.burst.count}/${view.burst.max}`;
}
if (traceEl) {
const trace = view.toolTrace ?? [];
if (!trace.length) {
traceEl.hidden = true;
traceEl.innerHTML = "";
} else {
traceEl.hidden = false;
traceEl.innerHTML = trace
.map(
(t) => `
`,
)
.join("");
}
}
const items = [];
for (const msg of view.messages ?? []) {
if (msg.role === "user") {
items.push({ kind: "user_input", title: "你", body: msg.text, at: msg.createdAt });
continue;
}
const kind = msg.kind ?? "system_info";
if (HIDE_KINDS.has(kind)) continue;
if (isSkillSelectionMessage(msg)) continue;
items.push({
kind,
title: msg.title ?? msgLabel(msg),
body: msgBody(msg, view).slice(0, 160),
at: msg.createdAt,
});
}
if (loading && f) {
const live = view.liveStream;
items.push({
kind: "pending",
title: live?.label ?? f.action,
body: [live?.thinking, live?.output].filter(Boolean).join("\n").slice(0, 200) || (f.detail ?? ""),
at: "",
});
}
if (!items.length) {
timelineEl.innerHTML = `调度记录将出现在这里
`;
return;
}
timelineEl.innerHTML = items
.map(
(it) => `
${esc(it.title)}
${fmtTime(it.at)}
${esc(it.body)}
`,
)
.join("");
timelineEl.scrollTop = timelineEl.scrollHeight;
}
export function renderMessageFeed(view, loading, handlers = {}) {
const feed = document.getElementById("message-feed");
if (!feed) return;
if (feed.querySelector(".msg.is-editing")) {
return;
}
const intake =
view.waitingReason?.kind === "intake" && view.intake?.fields?.length;
const showIntakePanel = intake && hasUserMessages(view);
feed.innerHTML = "";
if (showIntakePanel) {
const box = document.createElement("div");
box.className = "intake-panel";
box.innerHTML = renderIntakePanel(view.intake, { variant: "feed" });
feed.appendChild(box);
}
const visible = (view.messages ?? []).filter((m) => shouldShowInFeed(m, view));
if (!visible.length && !loading) {
const reviewing =
view.waitingReason?.kind === "review_artifact" && view.reviewArtifact;
if (!reviewing) {
const p = document.createElement("p");
p.className = "empty";
if (view.lifecycleStage === "play") {
p.textContent = "游玩模式:Agent 将按 Worker 集调度,推进世界与叙事。";
} else if (view.uiPrompt) {
const recipeLine = view.selectedRecipe?.name
? `\n\n已选导演:${view.selectedRecipe.name}`
: view.recipes?.length
? "\n\n(请先在新建作品时选定导演)"
: "";
p.textContent = `${view.uiPrompt}${recipeLine}`;
p.classList.add("empty-intake");
} else if (view.waitingReason?.kind === "worker_questions") {
p.textContent = "在下方回答 Skill 的提问。";
} else if (intake) {
p.textContent = "在下方描述你想创作什么;Agent 会收成 Worker 集供你验收。";
} else {
p.textContent = "在下方继续对话。";
}
p.classList.add("empty-intake");
feed.appendChild(p);
return;
}
}
for (const msg of visible) {
const isUser = msg.role === "user";
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
const body = msgBody(msg, view);
const card = document.createElement("article");
card.className = `msg ${MSG_CLASS[kind] ?? "system"}${isUser ? " msg-user-row" : " msg-assistant-row"}`;
card.dataset.messageId = msg.id;
const label = msgLabel(msg);
const subtitle = (msg.title ?? "").trim();
const showSubtitle =
!isUser &&
kind !== "orchestrator_thinking" &&
subtitle.length > 0 &&
!label.includes(subtitle) &&
subtitle !== label;
const headInner = isUser
? `${fmtTime(msg.createdAt)}${renderMsgVariantBadge(msg)}`
: `
${esc(label)}
${showSubtitle ? `${esc(subtitle)}` : ""}
${renderMsgVariantBadge(msg)}
${fmtTime(msg.createdAt)}`;
card.dataset.canEdit = canEditMessage(msg) ? "1" : "0";
card.dataset.hasContext = msg.contextTrace ? "1" : "0";
card.dataset.originalText = body;
card.innerHTML = `
${!isUser && kind === "worker_questions" ? `
需要你回答
` : ""}
${isUser ? "" : `
`}
${!isUser && msg.thinking ? renderThinkingBlock(msg.thinking) : ""}
${kind === "worker_questions" ? formatQuestionsHtml(body) : esc(body)}
${isUser ? `` : ""}
`;
feed.appendChild(card);
}
wireMessageFeedActions(feed, handlers);
wireMessageContextMenu(feed, handlers);
if (
view.waitingReason?.kind === "review_artifact" &&
view.reviewArtifact &&
!loading
) {
const wrap = document.createElement("div");
wrap.innerHTML = renderReviewFeedCard(view.reviewArtifact);
const card = wrap.firstElementChild;
if (card) feed.appendChild(card);
}
if (loading) {
const pending = document.createElement("article");
pending.className = "msg agent msg-assistant-row msg-pending";
pending.id = "msg-live-pending";
const live = view.liveStream;
const label = live?.label ?? view.focus?.action ?? "处理中";
pending.innerHTML = `
${renderLiveStreamBody(live)}
`;
feed.appendChild(pending);
}
feed.scrollTop = feed.scrollHeight;
}
/** 轮询时仅更新流式 pending 卡片,避免整页重绘 */
export function updateLiveStreamPanel(view) {
const pending = document.getElementById("msg-live-pending");
if (!pending) return;
const body = pending.querySelector(".msg-live-body");
if (!body) return;
const live = view.liveStream;
const labelEl = pending.querySelector(".msg-tag");
if (labelEl && live?.label) labelEl.textContent = live.label;
body.innerHTML = renderLiveStreamBody(live);
const feed = document.getElementById("message-feed");
if (feed) feed.scrollTop = feed.scrollHeight;
}
export function renderBoardPanel(view) {
const panel = document.getElementById("board-panel");
if (!panel) return;
const board = view.boardPanel;
if (!board) {
panel.innerHTML = `尚无黑板条目。Worker 写入 tag 后会出现在这里。
`;
return;
}
const tagRow = (row, badge) => `
${esc(row.tag)}
${badge}
${esc(row.source)} · ${esc(String(row.updatedAt).slice(0, 19))}
${esc(row.preview)}
`;
const finals = (board.finals ?? []).map((r) => tagRow(r, "定稿")).join("");
const active = (board.active ?? []).map((r) => tagRow(r, "进行中")).join("");
panel.innerHTML = `
定稿摘要(给下一 Worker)
${
board.brief
? `
${esc(board.brief)}`
: `
验收产物后生成。下一 Worker 会自动读到此处。
`
}
终产物 tag ${board.finals?.length ? `(${board.finals.length})` : ""}
${finals ? `
` : `
暂无
`}
当前活跃 tag ${board.active?.length ? `(${board.active.length})` : ""}
${active ? `
` : `
暂无
`}
${
board.archivedCount
? `已压缩归档 ${board.archivedCount} 个过程 tag(默认不进入后续 Worker 取数)
`
: ""
}
`;
panel.querySelectorAll("[data-edit-tag]").forEach((btn) => {
btn.addEventListener("click", () => {
const tag = btn.getAttribute("data-edit-tag");
if (!tag || !view.id) return;
const current =
[...(board.finals ?? []), ...(board.active ?? [])].find((r) => r.tag === tag)
?.preview ?? "";
const next = prompt(`编辑黑板「${tag}」\n(完整内容请从导出或后续增强编辑器查看;此处为预览级修改)`, current);
if (next == null) return;
void fetch(`/api/sessions/${encodeURIComponent(view.id)}/board`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tag, content: next }),
})
.then(async (res) => {
const data = await res.json();
if (!res.ok) throw new Error(data.error || "写入失败");
window.dispatchEvent(new CustomEvent("wa:session-updated", { detail: data }));
})
.catch((err) => alert(err.message));
});
});
}
export function renderWorkspace(view, loading, _onPickSkill, handlers = {}) {
renderLifecycle(view);
renderSkillPicker(view);
maybeAutoSwitchTab(view);
const qHost = document.getElementById("questions-card-host");
if (qHost) {
renderQuestionsCard(qHost, view, {
onSkipQuestions: () => handlers.onSkipQuestions?.(),
});
}
renderMessageFeed(view, loading, handlers);
renderSkillGuide(view);
renderReviewPanel(view, handlers);
renderBoardPanel(view);
renderAgentPanel(view, loading);
}
document.addEventListener("click", (e) => {
if (!e.target.closest("#msg-action-menu")) hideMsgMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") hideMsgMenu();
});
document.addEventListener("scroll", hideMsgMenu, true);