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_output: "执行单元", 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) => `
  1. ${esc(l)}
  2. `).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 `
  1. ${esc(t.tag)}${filled}${note}
  2. `; }) .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 ``; } 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(`

    数据怎么流

    ${flow}
    `); } 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( `

    待澄清

    `, ); } 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 ``; } 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); const paramsText = formatFlowParams(s.params); const paramsMissing = s.paramsMissing?.length > 0 ? `缺参:${esc(s.paramsMissing.join("、"))}` : ""; const paramsHtml = paramsText ? `${esc(paramsText)}` : ""; return `
  • ${esc(String(s.order))} ${nameLabel}${occ} ${paramsHtml} ${paramsMissing} 依赖:${deps}
  • `; }) .join(""); return `

    创作流程

    ${brief} ${statusHtml}
      ${rows}
    `; } function formatFlowParams(params) { if (!params || typeof params !== "object" || Array.isArray(params)) return ""; const parts = Object.entries(params) .filter(([, v]) => v != null && String(v).trim() !== "") .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`); return parts.length ? parts.join(" · ") : ""; } 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 `
    待验收 ${esc(displayWorkerLabel(review.workerId) || "产物")}
    ${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) => `
    ${esc(t.name)} ${fmtTime(t.at)}
    ${esc(t.summary || "—")}
    `, ) .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 ? "" : `
    ${headInner}
    `} ${!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 = `
    ${esc(label)}流式输出中
    ${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);