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
+ ? ``
+ : ``;
+ 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 ``;
+}
+
+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 = ``;
+ 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}
- `;
+ ${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 = ``;
+ 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 = ``;
+ 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 @@
-