From 5574b0d08e74bcd832b958fb2d955555b680bcd4 Mon Sep 17 00:00:00 2001 From: moranzhi Date: Wed, 9 Sep 2026 02:50:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E6=96=B0=E5=BB=BA=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=B8=AD=E6=9A=B4=E9=9C=B2=E8=BD=AC=E8=BF=B0?= =?UTF-8?q?=E8=BF=9B=E6=96=99=EF=BC=8C=E5=B9=B6=E5=8A=A0=E4=B8=8A=E4=BA=A7?= =?UTF-8?q?=E7=89=A9=E6=A0=8F=E4=B8=8E=E6=B8=85=E7=A9=BA=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- web/agent-ui.js | 276 +++++++++++++++++++++------- web/app.js | 226 +++++++++++++++++++---- web/index.html | 25 ++- web/styles.css | 470 ++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 827 insertions(+), 170 deletions(-) diff --git a/web/agent-ui.js b/web/agent-ui.js index 3c0b7f4..022b7eb 100644 --- a/web/agent-ui.js +++ b/web/agent-ui.js @@ -8,6 +8,7 @@ import { renderQuestionsCard, } from "./questions-ui.js"; import { displayWorkerLabel, formatWorkerDisplayTitle, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js"; +import { formatBodyHtml } from "./markdown.js"; import { PRESENT_SHELL_IDS, parsePresentDoc, @@ -32,6 +33,7 @@ const MSG_CLASS = { orchestrator_thinking: "agent", orchestrator_prompt: "agent", orchestrator_assessment: "agent", + dictate_reply: "agent", worker_running: "skill", worker_output: "skill", worker_questions: "questions", @@ -45,6 +47,7 @@ const MSG_LABEL = { orchestrator_thinking: "编排器 · 思考", orchestrator_prompt: "编排器", orchestrator_assessment: "编排器 · 内容评价", + dictate_reply: "转述整理", worker_running: "执行单元", worker_output: "执行单元", worker_questions: "提问", @@ -96,6 +99,11 @@ export function isCreationCloserDone(view) { } export function canOfferSaveProduct(view) { + if (!view || isPlayView(view)) return false; + if (view.creationMode === "dictate") { + const products = view.dictateProducts; + return Array.isArray(products) && products.length > 0; + } return isCreationSealedWaiting(view) || isCreationCloserDone(view); } @@ -490,7 +498,7 @@ function cancelInlineEdit(card) { card.querySelector(".msg-foot")?.removeAttribute("hidden"); const editing = card.querySelector(".msg-body-editing"); if (editing) { - editing.outerHTML = `
${esc(original)}
`; + editing.outerHTML = `
${formatBodyHtml(original)}
`; } } @@ -689,19 +697,34 @@ export function renderSkillPicker(_view, _onPick) { let activeRailTab = "books"; /** 用户手动展开侧栏后,在本会话保持展开,直到再点收起 */ let railUserExpanded = false; -/** 创作验收右侧「此前对话」:默认收起,把宽度留给产物 */ +/** 创作右侧栏:history=此前对话;products=转述产物 */ +let coordRailMode = "history"; +/** 创作验收右侧栏:默认收起(对话);转述看产物时默认展开 */ let coordRailCollapsed = true; function syncCoordRailChrome() { const rail = document.getElementById("coord-rail"); const toggle = document.getElementById("btn-coord-toggle"); + const toggleLabel = document.getElementById("coord-rail-toggle-label"); + const title = document.getElementById("coord-rail-title"); + const products = coordRailMode === "products"; document.body.classList.toggle("coord-rail-collapsed", coordRailCollapsed); + document.body.dataset.coordRailMode = coordRailMode; + if (toggleLabel) toggleLabel.textContent = products ? "产物" : "对话"; + if (title) title.textContent = products ? "已持久化" : "此前对话"; if (toggle) { toggle.setAttribute("aria-expanded", coordRailCollapsed ? "false" : "true"); - toggle.title = coordRailCollapsed ? "展开此前对话" : "收起此前对话"; + toggle.title = coordRailCollapsed + ? products + ? "展开产物" + : "展开此前对话" + : products + ? "收起产物" + : "收起此前对话"; } if (rail) { rail.dataset.collapsed = coordRailCollapsed ? "1" : "0"; + rail.setAttribute("aria-label", products ? "已持久化产物" : "此前对话"); } } @@ -4668,10 +4691,12 @@ function getRollbackMessageId(messages, messageId) { return messageIndex >= 0 ? messages[messageIndex + 1]?.id ?? null : null; } -/** @returns {"speak"|"answer"|"review"|"busy"|null} */ +/** @returns {"speak"|"answer"|"review"|"busy"|"dictate"|null} */ function resolveDesignSurface(view, loading) { if (isPlayView(view)) return null; if (view.phase === "done") return null; + // 转述:主面始终是对话流;右侧才是产物 + if (view.creationMode === "dictate") return "dictate"; const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason)); if (busy) return "busy"; if (view.phase === "error") return "speak"; @@ -4850,8 +4875,12 @@ function renderSpeakWorkspace(view) { if (canOfferSaveProduct(view)) { title = "可以落档了"; hint = view.hasProduct - ? "产物已拆出,可开玩;创作流程还在,补节点或再保存一份新定稿。" - : "保存后创作流程仍在;拆出的产物可以单独开玩、存档。"; + ? view.creationMode === "dictate" + ? "产物已拆出,可开玩;创作对话还在,可继续改产物或再保存一份新定稿。" + : "产物已拆出,可开玩;创作流程还在,补节点或再保存一份新定稿。" + : view.creationMode === "dictate" + ? "保存后创作对话仍在;拆出的产物可以单独开玩、存档。" + : "保存后创作流程仍在;拆出的产物可以单独开玩、存档。"; } else if (wr?.kind === "pick_creation_step" || isFlowPlanReview(view)) { title = "选要做的节点"; hint = "点图上可进入的节点即开始;虚线原型点一下增殖。要看技能池点上方「技能」。要改排在底栏写意见再发。"; @@ -4890,14 +4919,30 @@ function renderSpeakWorkspace(view) { ? proposedOutputCopy(view.proposedNextStep) : view.focus?.detail || "确认执行,或在底栏说明意见。"; } else if (wr?.kind === "input") { - title = "继续说"; - hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。"; + if (isPlayView(view)) { + title = "继续游玩"; + hint = + view.hints?.[0] || + wr.message || + "描述本轮行动、对话或想说的话;由游玩管线推进。"; + } else if (view.creationMode === "dictate") { + title = "转述整理"; + hint = + view.hints?.[0] || + wr.message || + "直接说体验与设定;右侧「已持久化」会列出 Agent 写入的内容。"; + } else { + title = "继续说"; + hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。"; + } } else if (view.uiPrompt && !hasUserMessages(view)) { title = "描述你想创作什么"; hint = String(view.uiPrompt).trim().slice(0, 280); } const recipe = - view.selectedRecipe?.name && !hasUserMessages(view) + view.selectedRecipe?.name && + !isPlayView(view) && + (view.creationMode === "dictate" || !hasUserMessages(view)) ? `

配方:${esc(view.selectedRecipe.name)}

` : ""; const intake = @@ -4931,7 +4976,11 @@ function renderSpeakWorkspace(view) { flowInteractive ? workspaceViewSwitchHtml("flow") : `${ - canOfferSaveProduct(view) ? "落档" : "说话" + view.creationMode === "dictate" + ? "转述" + : canOfferSaveProduct(view) + ? "落档" + : "说话" }` } @@ -4945,14 +4994,53 @@ function renderSpeakWorkspace(view) { `; } -function renderAnswerWorkspaceShell(view) { - return `
-
- ${leaveStepControlHtml(view)} - 答题 -

点字母选中,文案可改;点卡片展开,点顶条收起。

-
-
`; +/** 转述式:产物列表(右侧栏) */ +function renderDictateProductsList(view) { + const items = Array.isArray(view.dictateProducts) ? view.dictateProducts : []; + if (!items.length) { + return `

尚无写入。Agent 落盘产物后会出现在这里。

`; + } + return `
${items + .map((p) => { + const tag = String(p.tag ?? "").trim(); + const content = String(p.content ?? ""); + const preview = content.replace(/\s+/g, " ").trim().slice(0, 72); + return `
+ + ${esc(tag)}${ + p.order != null && Number.isFinite(Number(p.order)) + ? ` · ${Number(p.order)}` + : "" + } + ${esc(preview)}${ + content.length > 72 ? "…" : "" + } + +
${formatBodyHtml(content)}
+
`; + }) + .join("")}
`; +} + +function fillCoordRailProducts(view) { + const coordRail = document.getElementById("coord-rail"); + const drawerBody = document.getElementById("coord-drawer-body"); + const drawerCount = document.getElementById("coord-drawer-count"); + if (!coordRail) return; + const items = Array.isArray(view.dictateProducts) ? view.dictateProducts : []; + if (coordRailMode !== "products") { + coordRailMode = "products"; + coordRailCollapsed = false; + } + coordRail.hidden = false; + if (drawerBody) drawerBody.innerHTML = renderDictateProductsList(view); + if (drawerCount) { + drawerCount.textContent = String(items.length); + drawerCount.title = items.length + ? `已持久化 ${items.length} 项` + : "尚无产物"; + } + syncCoordRailChrome(); } function fillCoordRail(visible, view, handlers) { @@ -4960,6 +5048,18 @@ function fillCoordRail(visible, view, handlers) { const drawerBody = document.getElementById("coord-drawer-body"); const drawerCount = document.getElementById("coord-drawer-count"); if (!coordRail) return; + + // 转述产物栏只在创作层;游玩层与配方开玩一样走回合历史 + if (view.creationMode === "dictate" && !isPlayView(view)) { + fillCoordRailProducts(view); + return; + } + + if (coordRailMode !== "history") { + coordRailMode = "history"; + coordRailCollapsed = true; + } + const visibleIds = new Set(visible.map((message) => message.id)); const history = (view.messages ?? []).filter((message) => { if (isReviewSidecarQuestionStub(message)) return false; @@ -4967,7 +5067,6 @@ function fillCoordRail(visible, view, handlers) { }); const traceCount = history.filter((message) => message.contextTrace).length; const packs = groupCoordTurnPacks(history); - // 侧栏是回看档案:打开先看最近一轮,往下才是更早的对话 const ordered = [...packs].reverse(); const hiddenIds = new Set( history.filter((message) => !visibleIds.has(message.id)).map((message) => message.id), @@ -5105,6 +5204,39 @@ export function renderMessageFeed(view, loading, handlers = {}) { if (!stage) return; + // 转述:主柱对话流;右侧已持久化;stage 仅一行条 + if (surface === "dictate") { + stage.hidden = false; + const recipeName = view.selectedRecipe?.name + ? esc(view.selectedRecipe.name) + : ""; + stage.innerHTML = recipeName + ? `
转述配方 · ${recipeName}
` + : `
转述
`; + if (panelFeed) ensureQuestionsHostIn(panelFeed); + appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, { + emptyText: + "直接说你想要的体验与设定。我会整理进右侧产物,并在回复里跟你确认、建议补充。", + loading, + }); + 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)}流式输出中${renderRetryRunButton()}
+
${renderLiveStreamBody(live, view)}
+
`; + feed.appendChild(pending); + wireRetryRun(pending, handlers); + } + feed.scrollTop = feed.scrollHeight; + return; + } + if (surface === "busy" || loading) { stage.hidden = false; const live = view.liveStream; @@ -5158,29 +5290,63 @@ export function renderMessageFeed(view, loading, handlers = {}) { return; } - // —— 游玩:保持原对话流 —— + // —— 游玩 / 转述对话流 —— + appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, { + emptyText: isPlayView(view) + ? "开场已就绪。在下方说你要做什么。" + : view.uiPrompt + ? `${view.uiPrompt}${ + view.selectedRecipe?.name + ? `\n\n已选配方:${view.selectedRecipe.name}` + : view.recipes?.length + ? "\n\n(请先在新建作品时选定配方)" + : "" + }` + : view.waitingReason?.kind === "worker_questions" + ? "在下方回答提问。" + : intake + ? "在下方描述你想创作什么;Agent 会收成 Worker 集供你验收。" + : "在下方继续对话。", + loading, + emptyClass: isPlayView(view) ? "empty" : "empty empty-intake", + }); + + if (reviewingNow && view.reviewArtifact && !loading && stage && !isPlayView(view)) { + const hasHungAsk = Boolean(activeQuestions?.questions?.length); + mountReviewWorkspace(stage, view, handlers, { + hideAskSidecar: hasHungAsk, + mountAskCard: hasHungAsk, + }); + } + + 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)}流式输出中${renderRetryRunButton()}
+
${renderLiveStreamBody(live, view)}
+
`; + feed.appendChild(pending); + wireRetryRun(pending, handlers); + } + + feed.scrollTop = feed.scrollHeight; + if (stage && !stage.hidden) stage.scrollTop = 0; +} + +function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, opts = {}) { + const loading = Boolean(opts.loading); if (!visible.length && !loading) { const p = document.createElement("p"); - p.className = "empty"; - if (isPlayView(view)) { - p.textContent = "开场已就绪。在下方说你要做什么。"; - } 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 = "在下方回答提问。"; - } else if (intake) { - p.textContent = "在下方描述你想创作什么;Agent 会收成 Worker 集供你验收。"; - } else { - p.textContent = "在下方继续对话。"; - } - p.classList.add("empty-intake"); + p.className = opts.emptyClass || "empty empty-intake"; + p.textContent = opts.emptyText || "在下方继续对话。"; feed.appendChild(p); + wireMessageFeedActions(feed, handlers); + wireMessageContextMenu(feed, handlers); return; } @@ -5217,8 +5383,7 @@ export function renderMessageFeed(view, loading, handlers = {}) { card.dataset.originalText = body; const questionsActive = kind === "worker_questions" && Boolean(activeQuestions); - const showThinking = - !isUser && msg.thinking && isPlayView(view); + const showThinking = !isUser && msg.thinking && isPlayView(view); const innerBody = questionsActive ? `

${esc(body)}

` : kind === "worker_questions" @@ -5227,7 +5392,7 @@ export function renderMessageFeed(view, loading, handlers = {}) { ? formatPlayPresentHtml(body, view) : kind === "worker_output" ? formatArtifactBodyHtml(body) - : esc(body); + : formatBodyHtml(body); const process = !isUser && isPlayProcessMessage(msg, view); const playFinal = !isUser && isPlayFinalReply(msg, view); @@ -5255,35 +5420,8 @@ export function renderMessageFeed(view, loading, handlers = {}) { } feed.appendChild(card); } - wireMessageFeedActions(feed, handlers); wireMessageContextMenu(feed, handlers); - - if (reviewingNow && view.reviewArtifact && !loading && stage && !isPlayView(view)) { - const hasHungAsk = Boolean(activeQuestions?.questions?.length); - mountReviewWorkspace(stage, view, handlers, { - hideAskSidecar: hasHungAsk, - mountAskCard: hasHungAsk, - }); - } - - 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)}流式输出中${renderRetryRunButton()}
-
${renderLiveStreamBody(live, view)}
-
`; - feed.appendChild(pending); - wireRetryRun(pending, handlers); - } - - feed.scrollTop = feed.scrollHeight; - if (stage && !stage.hidden) stage.scrollTop = 0; } /** 轮询时仅更新流式 pending 卡片,避免整页重绘;不自动滚到底,保持用户当前阅读位置 */ diff --git a/web/app.js b/web/app.js index b5764f1..5c99b96 100644 --- a/web/app.js +++ b/web/app.js @@ -27,6 +27,11 @@ import { themeById, chromeById, } from "./theme.js"; +import { + applyMarkdownRender, + initMarkdownRender, + isMarkdownRenderEnabled, +} from "./markdown.js"; let sessionId = null; let activeBookId = null; @@ -124,8 +129,10 @@ function resolveUserTask(view, loading) { label: "可保存", title: "落档产物", hint: view.hasProduct - ? "可再保存一份新定稿,或用已有产物开玩。补节点仍点图。" - : "点「保存定稿」拆出产物。创作流程还在,之后用产物开玩。", + ? "可再保存一份新定稿,或用已有产物开玩。" + : view.creationMode === "dictate" + ? "点「保存定稿」拆出产物。创作对话还在,之后用产物开玩。" + : "点「保存定稿」拆出产物。创作流程还在,之后用产物开玩。", }; } @@ -565,6 +572,43 @@ function composerInputShell(spec, { textareaHtml, trailing = "" } = {}) { `; } +function composerMoreMenuHtml() { + const on = isMarkdownRenderEnabled(); + return `
+ + + + +
`; +} + +function composerFormHtml(shell) { + return `
${composerMoreMenuHtml()}${shell}
`; +} + +function syncMarkdownMenuUi() { + const mdOn = isMarkdownRenderEnabled(); + document.querySelectorAll("[data-toggle-markdown]").forEach((btn) => { + btn.classList.toggle("is-active", mdOn); + btn.setAttribute("aria-checked", mdOn ? "true" : "false"); + }); +} + function syncThemeMenuUi() { const colorId = document.documentElement.getAttribute("data-color-theme") || getStoredColorTheme(); const chromeId = @@ -584,6 +628,7 @@ function syncThemeMenuUi() { document.querySelectorAll("[data-present-chrome].more-theme-item").forEach((btn) => { btn.classList.toggle("is-active", btn.getAttribute("data-present-chrome") === chromeMeta.id); }); + syncMarkdownMenuUi(); } function stashComposerDraftFromDom() { @@ -652,7 +697,7 @@ function renderComposer(view, loading) { `, }, ); - root.innerHTML = `
${shell}
`; + root.innerHTML = composerFormHtml(shell); root.querySelector("#composer-form")?.addEventListener("submit", (e) => e.preventDefault()); root.querySelector("[data-act=abort_run]")?.addEventListener("click", () => abortRun()); root.querySelector("[data-act=retry_run]")?.addEventListener("click", () => retryRun()); @@ -682,7 +727,7 @@ function renderComposer(view, loading) { root.innerHTML = ` ${intakePanel} ${confirm} -
${shell}
`; + ${composerFormHtml(shell)}`; wireComposerForm(); root.querySelector("[data-act=confirm_intake]")?.addEventListener("click", () => runAction("confirm_intake")); applyPendingComposerDraft(); @@ -843,7 +888,7 @@ function renderComposer(view, loading) { textareaHtml: ``, trailing: `${saveProduct}${enterPlay}`, }); - root.innerHTML = `
${shell}
`; + root.innerHTML = composerFormHtml(shell); wireComposerForm({ requiresText: true }); root.querySelector("[data-act=save-product]")?.addEventListener("click", () => { void saveCurrentInstance(); @@ -866,14 +911,28 @@ function renderComposer(view, loading) { : spec.acceptAction.title || spec.acceptAction.label )}" ${spec.acceptAction.disabled ? "disabled" : ""}>${esc(spec.acceptAction.label)}` : ""; + const clearDictate = + view?.creationMode === "dictate" && !isPlayView(view) + ? `` + : ""; + const saveProduct = canOfferSaveProduct(view) + ? `` + : ""; + const enterPlay = canOfferEnterPlay(view) + ? `` + : ""; const submitLabel = spec.submitLabel || "发送"; - const sendIsPrimary = !acceptOnEmpty; + const sendIsPrimary = !acceptOnEmpty && !saveProduct && !enterPlay; const placeholder = acceptOnEmpty ? `${spec.placeholder || "修改意见…"}(${spec.emptyEnterHint || "空 Enter=确认"})` : spec.placeholder; const shell = composerInputShell(spec, { textareaHtml: ``, - trailing: `${acceptBtn}`, }); - root.innerHTML = `
${shell}
`; + root.innerHTML = composerFormHtml(shell); wireComposerForm({ acceptOnEmpty }); root.querySelector("[data-act=accept]")?.addEventListener("click", () => runAction("accept")); + root.querySelector("[data-act=clear-dictate]")?.addEventListener("click", () => { + void clearDictateDialogue(); + }); + root.querySelector("[data-act=save-product]")?.addEventListener("click", () => { + void saveCurrentInstance(); + }); + root.querySelector("[data-act=enter-play]")?.addEventListener("click", () => { + enterPlayNow(); + }); applyPendingComposerDraft(); } @@ -1964,6 +2032,23 @@ async function abortRun() { } } +async function clearDictateDialogue() { + if (!sessionId) return; + if (!confirm("清空对话?产物会保留。")) return; + const seq = beginUiRequest(); + try { + const view = await api( + `/api/sessions/${encodeURIComponent(sessionId)}/dictate/clear-dialogue`, + { method: "POST", body: "{}" }, + ); + if (isStaleUiRequest(seq)) return; + renderSession(view, false); + } catch (err) { + if (isStaleUiRequest(seq)) return; + alert(err.message); + } +} + function startLivePoll() { stopLivePoll(); if (!sessionId) return; @@ -2350,38 +2435,90 @@ function openNewBookDialog() { async function populateDirectorSelect() { const sel = $("select-director"); const desc = $("director-desc"); - if (!sel) return; - try { - const data = await api("/api/directors"); - const directors = data.directors ?? []; + const modeSel = $("select-creation-mode"); + const modeDesc = $("creation-mode-desc"); + const fieldDirector = $("field-director"); + let recipeDirectors = []; + let dictateDirectors = []; + + const fillOptions = (list) => { + if (!sel) return; sel.innerHTML = ""; - if (!directors.length) { + if (!list.length) { sel.innerHTML = ``; - if (desc) { - desc.hidden = false; - desc.textContent = "尚未配置配方选项(recipes/catalog.yaml)。"; + sel.required = false; + return; + } + for (const d of list) { + const opt = document.createElement("option"); + opt.value = d.id; + opt.textContent = d.name || d.id; + if (d.declaration) opt.dataset.declaration = d.declaration; + sel.appendChild(opt); + } + sel.value = list[0].id; + sel.required = true; + }; + + const syncDesc = () => { + if (!sel || !desc) return; + if (!modeSel?.value) { + desc.hidden = true; + desc.textContent = ""; + return; + } + const list = + modeSel.value === "dictate" ? dictateDirectors : recipeDirectors; + const cur = list.find((d) => d.id === sel.value); + const text = (cur?.declaration ?? "").trim(); + desc.hidden = !text; + desc.textContent = text; + }; + + const syncMode = () => { + const mode = modeSel?.value?.trim() || ""; + if (!mode) { + if (fieldDirector) fieldDirector.hidden = true; + if (modeDesc) modeDesc.hidden = true; + if (desc) desc.hidden = true; + if (sel) { + sel.innerHTML = ``; + sel.required = false; } return; } - for (const d of directors) { - const opt = document.createElement("option"); - opt.value = d.id; - opt.textContent = displaySkillPackLabel(d.id) || d.name || d.id; - sel.appendChild(opt); + const dictate = mode === "dictate"; + if (fieldDirector) fieldDirector.hidden = false; + fillOptions(dictate ? dictateDirectors : recipeDirectors); + if (modeDesc) { + modeDesc.hidden = false; + modeDesc.textContent = dictate + ? "转述整理:直接对话写入产物;下方为转述专用配方。" + : "节点流程:按配方预置起点走工作流计划;下方为节点流程配方。"; } - const preferred = - directors.find((d) => d.id === "world-simulator") ?? directors[0]; - sel.value = preferred.id; - const syncDesc = () => { - const cur = directors.find((d) => d.id === sel.value); - if (desc) { - const text = (cur?.declaration ?? "").trim(); - desc.hidden = !text; - desc.textContent = text; - } - }; - sel.onchange = syncDesc; syncDesc(); + }; + + if (modeSel && !modeSel.dataset.bound) { + modeSel.dataset.bound = "1"; + modeSel.addEventListener("change", syncMode); + } + if (sel && !sel.dataset.boundDesc) { + sel.dataset.boundDesc = "1"; + sel.addEventListener("change", syncDesc); + } + if (!sel) return; + try { + const data = await api("/api/directors"); + recipeDirectors = data.recipeDirectors ?? data.directors ?? []; + dictateDirectors = data.dictateDirectors ?? []; + // 打开对话框时重置为「先选进料」 + if (modeSel) { + modeSel.value = ""; + const placeholder = [...modeSel.options].find((o) => !o.value); + if (placeholder) placeholder.selected = true; + } + syncMode(); } catch (err) { sel.innerHTML = ``; if (desc) { @@ -2393,7 +2530,12 @@ async function populateDirectorSelect() { async function createBook() { const title = $("input-book-title").value.trim() || "未命名作品"; + const creationMode = $("select-creation-mode")?.value?.trim() || ""; const recipeId = $("select-director")?.value?.trim(); + if (creationMode !== "recipe" && creationMode !== "dictate") { + alert("请先选择进料方式"); + return; + } if (!recipeId) { alert("请选择配方"); return; @@ -2407,6 +2549,7 @@ async function createBook() { body: JSON.stringify({ title, orchestratorId, + creationMode, recipeId, }), }); @@ -2493,7 +2636,11 @@ function defaultArchiveLabel() { async function saveCurrentInstance() { if (!activeBookId || (!lastView?.playReady && !canOfferSaveProduct(lastView))) { - return alert("须先完成收口或验收 Worker 集"); + return alert( + lastView?.creationMode === "dictate" + ? "请先让 Agent 写入至少一项产物,再保存定稿" + : "须先完成收口或验收 Worker 集", + ); } const label = prompt("落档名称", defaultArchiveLabel()); if (!label?.trim()) return; @@ -2699,6 +2846,16 @@ $("theme-menu")?.addEventListener("click", (e) => { } }); +$("composer")?.addEventListener("click", (e) => { + const menu = e.target.closest("#composer-more-menu"); + const mdBtn = e.target.closest("[data-toggle-markdown]"); + if (!mdBtn || !menu?.contains(mdBtn)) return; + e.preventDefault(); + applyMarkdownRender(!isMarkdownRenderEnabled()); + syncMarkdownMenuUi(); + if (lastView && sessionId) renderSession(lastView, false); +}); + window.addEventListener("wa:session-updated", (e) => { const view = e.detail; if (!view?.id || isCatalogNav() || !sessionId) return; @@ -2719,6 +2876,7 @@ document.addEventListener("click", (e) => { async function init() { initColorTheme(); initPresentChrome(); + initMarkdownRender(); syncThemeMenuUi(); try { void populateDirectorSelect(); diff --git a/web/index.html b/web/index.html index ac40640..9442584 100644 --- a/web/index.html +++ b/web/index.html @@ -160,21 +160,21 @@ -