在新建作品流程中暴露转述进料,并加上产物栏与清空对话。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 02:50:32 +08:00
co-authored by Cursor
parent 692ba887d0
commit 5574b0d08e
4 changed files with 827 additions and 170 deletions
+207 -69
View File
@@ -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 = `<div class="msg-body">${esc(original)}</div>`;
editing.outerHTML = `<div class="msg-body">${formatBodyHtml(original)}</div>`;
}
}
@@ -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))
? `<p class="workspace-intent-meta">配方:${esc(view.selectedRecipe.name)}</p>`
: "";
const intake =
@@ -4931,7 +4976,11 @@ function renderSpeakWorkspace(view) {
flowInteractive
? workspaceViewSwitchHtml("flow")
: `<span class="workspace-surface-kicker">${
canOfferSaveProduct(view) ? "落档" : "说话"
view.creationMode === "dictate"
? "转述"
: canOfferSaveProduct(view)
? "落档"
: "说话"
}</span>`
}
</div>
@@ -4945,14 +4994,53 @@ function renderSpeakWorkspace(view) {
</section>`;
}
function renderAnswerWorkspaceShell(view) {
return `<section class="workspace-answer" id="workspace-answer-slot">
<header class="workspace-answer-head workspace-answer-head--slim">
${leaveStepControlHtml(view)}
<span class="workspace-surface-kicker">答题</span>
<p class="workspace-answer-hint">点字母选中,文案可改;点卡片展开,点顶条收起。</p>
</header>
</section>`;
/** 转述式:产物列表(右侧栏) */
function renderDictateProductsList(view) {
const items = Array.isArray(view.dictateProducts) ? view.dictateProducts : [];
if (!items.length) {
return `<p class="coord-empty">尚无写入。Agent 落盘产物后会出现在这里。</p>`;
}
return `<div class="dictate-products-list">${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 `<details class="dictate-product dictate-product--rail" open>
<summary>
<span class="dictate-product-tag">${esc(tag)}${
p.order != null && Number.isFinite(Number(p.order))
? ` · ${Number(p.order)}`
: ""
}</span>
<span class="dictate-product-preview">${esc(preview)}${
content.length > 72 ? "…" : ""
}</span>
</summary>
<div class="dictate-product-body">${formatBodyHtml(content)}</div>
</details>`;
})
.join("")}</div>`;
}
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
? `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">转述</span><span class="dictate-stage-recipe">配方 · ${recipeName}</span></header>`
: `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">转述</span></header>`;
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 = `
<div class="msg-bubble">
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
</div>`;
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 = `
<div class="msg-bubble">
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
</div>`;
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
? `<p class="msg-q-index">${esc(body)}</p>`
: 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 = `
<div class="msg-bubble">
<header class="msg-head"><span class="msg-tag">${esc(label)}</span><span class="msg-live-indicator">流式输出中</span>${renderRetryRunButton()}</header>
<div class="msg-body msg-live-body">${renderLiveStreamBody(live, view)}</div>
</div>`;
feed.appendChild(pending);
wireRetryRun(pending, handlers);
}
feed.scrollTop = feed.scrollHeight;
if (stage && !stage.hidden) stage.scrollTop = 0;
}
/** 轮询时仅更新流式 pending 卡片,避免整页重绘;不自动滚到底,保持用户当前阅读位置 */
+192 -34
View File
@@ -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 = "" } = {}) {
</div>`;
}
function composerMoreMenuHtml() {
const on = isMarkdownRenderEnabled();
return `<details class="more-menu composer-more-menu" id="composer-more-menu">
<summary class="btn-sm more-menu-sum" title="更多选项" aria-label="更多选项">
<span class="hamburger-icon" aria-hidden="true"></span>
</summary>
<div class="more-menu-panel composer-more-panel" role="menu">
<div class="more-menu-label">显示</div>
<button
type="button"
class="more-menu-item more-toggle-item${on ? " is-active" : ""}"
data-toggle-markdown
role="menuitemcheckbox"
aria-checked="${on ? "true" : "false"}"
>
<span class="more-theme-copy">
<span class="more-theme-name">Markdown 渲染</span>
<span class="more-theme-desc">消息与产物按 Markdown 显示</span>
</span>
<span class="more-toggle-mark" aria-hidden="true"></span>
</button>
</div>
</details>`;
}
function composerFormHtml(shell) {
return `<form class="composer-form" id="composer-form">${composerMoreMenuHtml()}${shell}</form>`;
}
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) {
<button type="button" class="btn composer-btn-icon" data-act="retry_run" title="重roll" aria-label="重roll">🔄</button>`,
},
);
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
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}
<form class="composer-form" id="composer-form">${shell}</form>`;
${composerFormHtml(shell)}`;
wireComposerForm();
root.querySelector("[data-act=confirm_intake]")?.addEventListener("click", () => runAction("confirm_intake"));
applyPendingComposerDraft();
@@ -843,7 +888,7 @@ function renderComposer(view, loading) {
textareaHtml: `<textarea id="composer-input" rows="1" placeholder="${esc(spec.placeholder)}"></textarea>`,
trailing: `${saveProduct}${enterPlay}<button type="submit" class="${sendCls}" data-act="pick-replan" disabled title="写下意见后再发;点图上节点即确认并进入">${esc(spec.submitLabel || "发送")}</button>`,
});
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
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)}</button>`
: "";
const clearDictate =
view?.creationMode === "dictate" && !isPlayView(view)
? `<button type="button" class="btn" data-act="clear-dictate" title="清空对话,保留产物">清空对话</button>`
: "";
const saveProduct = canOfferSaveProduct(view)
? `<button type="button" class="btn btn-primary" data-act="save-product">保存定稿</button>`
: "";
const enterPlay = canOfferEnterPlay(view)
? `<button type="button" class="btn${
canOfferSaveProduct(view) ? "" : " btn-primary"
}" data-act="enter-play">${
view.hasProduct ? "用产物开玩" : "开玩"
}</button>`
: "";
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: `<textarea id="composer-input" rows="1" placeholder="${esc(placeholder)}"></textarea>`,
trailing: `<button type="button" class="btn composer-btn${
trailing: `${clearDictate}${saveProduct}${enterPlay}<button type="button" class="btn composer-btn${
sendIsPrimary ? " btn-primary" : ""
}" data-act="revise" title="${esc(
acceptOnEmpty
@@ -881,9 +940,18 @@ function renderComposer(view, loading) {
: "Enter 发送"
)}">${esc(submitLabel)}</button>${acceptBtn}`,
});
root.innerHTML = `<form class="composer-form" id="composer-form">${shell}</form>`;
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 = `<option value="">暂无配方</option>`;
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 = `<option value="">请先选择进料方式</option>`;
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 = `<option value="">加载失败</option>`;
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();
+17 -8
View File
@@ -160,21 +160,21 @@
</div>
<div id="questions-card-host" class="questions-card-host" hidden></div>
</section>
<aside id="coord-rail" class="coord-rail" hidden aria-label="此前对话">
<aside id="coord-rail" class="coord-rail" hidden aria-label="侧栏">
<button
type="button"
class="coord-rail-toggle"
id="btn-coord-toggle"
aria-expanded="false"
title="展开 / 收起此前对话"
title="展开 / 收起侧栏"
>
<span class="coord-rail-toggle-label">对话</span>
<span class="coord-rail-toggle-label" id="coord-rail-toggle-label">对话</span>
<span id="coord-drawer-count" class="coord-drawer-count"></span>
</button>
<div class="coord-rail-panel">
<header class="coord-rail-head">
<span class="coord-rail-title">此前对话</span>
<button type="button" class="btn-sm coord-rail-collapse" id="btn-coord-collapse" title="收起" aria-label="收起此前对话">›</button>
<span class="coord-rail-title" id="coord-rail-title">此前对话</span>
<button type="button" class="btn-sm coord-rail-collapse" id="btn-coord-collapse" title="收起" aria-label="收起侧栏">›</button>
</header>
<div id="coord-drawer-body" class="coord-drawer-body"></div>
</div>
@@ -192,11 +192,20 @@
<dialog id="dialog-new-book" class="dialog">
<form method="dialog" id="form-new-book">
<h2>新建作品</h2>
<p class="dialog-desc">选择配方,再起名。配方是本局方法起点,之后按对话谈成工作流计划与运行规格(可调味)。</p>
<p class="dialog-desc">先选进料方式,再选对应配方(两套目录互不共享)。</p>
<label class="field">
进料方式
<select id="select-creation-mode" required>
<option value="" selected disabled>请选择…</option>
<option value="recipe">节点流程</option>
<option value="dictate">转述整理(对话直填)</option>
</select>
</label>
<p class="dialog-desc" id="creation-mode-desc" hidden></p>
<label class="field" id="field-director" hidden>
配方
<select id="select-director" required>
<option value="">加载中…</option>
<select id="select-director">
<option value="">请先选择进料方式</option>
</select>
</label>
<p class="dialog-desc" id="director-desc" hidden></p>
+411 -59
View File
@@ -1,7 +1,7 @@
/* Functional layout — agent-first, no production-line chrome */
/* Functional layout — agent-first, no production-line chrome */
:root {
/* Default = modern;实际以色主题 data-color-theme 为准 */
/* Default = modern;实际以色主题 data-color-theme 为准 */
--bg: #f8f8f8;
--surface: #fff;
--border: #e0e0e0;
@@ -29,7 +29,7 @@
color: var(--text);
}
/* 仿纸暖黄(护眼)*/
/* 仿纸暖黄(护眼)*/
html[data-color-theme="paper"] {
--bg: #f5ecd7;
--surface: #fbf8f2;
@@ -41,7 +41,7 @@ html[data-color-theme="paper"] {
--shadow: 0 8px 24px rgba(62, 58, 53, 0.1);
}
/* 现代清爽 */
/* 现代清爽 */
html[data-color-theme="modern"] {
--bg: #f8f8f8;
--surface: #fff;
@@ -53,7 +53,7 @@ html[data-color-theme="modern"] {
--shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
/* 夜间暗色 */
/* 夜间暗色 */
html[data-color-theme="dark"] {
--bg: #1e1e1e;
--surface: #2d2d30;
@@ -65,7 +65,7 @@ html[data-color-theme="dark"] {
--shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
}
/* 豆沙绿 */
/* 豆沙绿 */
html[data-color-theme="green"] {
--bg: #dce5d9;
--surface: #f4f9f2;
@@ -82,7 +82,7 @@ html {
color: var(--text);
}
/* 开创作后缩进左侧栏 */
/* 开创作后缩进左侧栏 */
body.rail-collapsed {
--books-w: var(--books-w-collapsed);
}
@@ -91,7 +91,7 @@ body.rail-collapsed {
html, body {
height: 100%;
margin: 0;
overflow: hidden; /* 浏览器层永不出现页面滚动条 */
overflow: hidden; /* 浏览器层永不出现页面滚动条 */
background: var(--bg);
color: var(--text);
}
@@ -311,8 +311,8 @@ body[data-nav-level="root"] .burst-badge { display: none; }
.rail-tab-panel[hidden] { display: none !important; }
body.rail-collapsed .panel-rail { align-items: stretch; }
/* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */
/* panel-main:上=悬停遮盖 chrome;下=工作区(左中右在 main-body / head 内) */
/* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */
/* panel-main:上=悬停遮盖 chrome;下=工作区(左中右在 main-body / head 内) */
.panel-main {
display: grid;
grid-template-rows: minmax(0, 1fr);
@@ -332,7 +332,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
overflow: hidden;
}
/* 顶缘热区 + 遮盖条:不占文档流,悬停/聚焦才展开 */
/* 顶缘热区 + 遮盖条:不占文档流,悬停/聚焦才展开 */
.main-chrome {
position: absolute;
top: 0;
@@ -425,7 +425,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
}
@media (hover: none) {
/* 触控无悬停:顶缘仍可点开,聚焦后保持 */
/* 触控无悬停:顶缘仍可点开,聚焦后保持 */
.main-chrome {
height: 12px;
}
@@ -453,7 +453,7 @@ body.design-workspace.coord-rail-collapsed .main-body {
position: relative;
}
/* 验收态:协调流收窄,产物占主工作面 */
/* 验收态:协调流收窄,产物占主工作面 */
.workspace-stage {
flex: 1 1 auto;
min-height: 0;
@@ -546,7 +546,7 @@ body.design-workspace.coord-rail-collapsed .main-body {
}
.workspace-raw-body { margin-top: 8px; }
/* 创作伪 0 层:右侧协调栏(横向折叠,不占主柱高度) */
/* 创作伪 0 层:右侧协调栏(横向折叠,不占主柱高度) */
.coord-rail {
display: none;
min-width: 0;
@@ -560,6 +560,9 @@ body.design-workspace .coord-rail:not([hidden]) {
display: flex;
flex-direction: row;
align-items: stretch;
min-width: 0;
max-width: 100%;
overflow-x: hidden;
}
.coord-rail-toggle {
flex: 0 0 var(--coord-w-collapsed, 40px);
@@ -646,8 +649,10 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
}
.coord-drawer-body {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
@@ -895,7 +900,7 @@ body.is-reviewing .msg-bubble {
max-width: 92%;
}
/* 创作伪 0 层:产物铺满主柱,对话退居右侧栏 */
/* 创作伪 0 层:产物铺满主柱,对话退居右侧栏 */
body.design-workspace .message-feed {
display: none !important;
}
@@ -909,7 +914,7 @@ body.design-workspace .workspace-stage {
min-height: 0;
}
body.design-workspace[data-design-surface="answer"] .workspace-stage {
overflow: hidden; /* 滚动只在询问卡内,避免 stage↔card 双滚 */
overflow: hidden; /* 滚动只在询问卡内,避免 stage↔card 双滚 */
}
body.design-workspace .workspace-review {
max-width: none;
@@ -1075,7 +1080,7 @@ body.design-workspace[data-design-surface="answer"] .questions-card-host {
margin-top: 0;
padding: 0;
}
/* 答题/验收工作面已是 surface,询问不再套圆角卡片 */
/* 答题/验收工作面已是 surface,询问不再套圆角卡片 */
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-open:not([hidden]),
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
margin: 0;
@@ -1102,6 +1107,46 @@ body.design-workspace[data-design-surface="answer"] .qcard-pager,
.workspace-review-ask .qcard-pager {
right: 0;
}
/* 转述:主柱对话;stage 一行条;右侧已持久化(栏内上下滚、正文换行) */
body.design-workspace[data-design-surface="dictate"] .message-feed {
display: flex !important;
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
body.design-workspace[data-design-surface="dictate"] .workspace-stage {
order: -1;
flex: 0 0 auto;
overflow: visible;
border-bottom: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
border-top: none;
}
body.design-workspace[data-design-surface="dictate"] {
--coord-w: clamp(300px, 28vw, 380px);
}
body.design-workspace[data-design-surface="dictate"] .panel-feed {
overflow: hidden;
}
.dictate-stage-bar {
display: flex;
align-items: baseline;
gap: 10px;
padding: 8px var(--workspace-pad-x, 24px);
margin: 0;
}
.dictate-stage-bar .workspace-surface-kicker {
margin-bottom: 0;
}
.dictate-stage-recipe {
font-size: 12px;
color: var(--muted);
}
body.design-workspace[data-design-surface="dictate"] .workspace-surface-kicker {
color: var(--accent);
}
body.design-workspace[data-design-surface="speak"] .workspace-surface-kicker {
color: var(--accent);
}
@@ -1116,7 +1161,7 @@ body.design-workspace[data-design-surface="review"][data-review-kind="opening"]
color: var(--accent);
}
/* 挂载追问嵌在产物验收面下方:与产物同宽,随主柱动态适应 */
/* 挂载追问嵌在产物验收面下方:与产物同宽,随主柱动态适应 */
.workspace-review-ask {
flex: 0 0 auto;
margin-top: 0;
@@ -1878,7 +1923,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
.panel-head-actions { display: flex; gap: 4px; align-items: center; }
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* Books — breadcrumb + list */
/* Books — breadcrumb + list */
.path-bar {
display: flex;
align-items: center;
@@ -2126,7 +2171,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
}
.sidebar-empty { padding: 12px; color: var(--muted); font-size: 13px; text-align: center; }
/* Skill picker — primary action when starting */
/* Skill picker — primary action when starting */
.skill-picker {
padding: 16px;
border-bottom: 2px solid var(--accent);
@@ -2144,7 +2189,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
.skill-card-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
.skill-card-tag { font-size: 11px; color: var(--accent); margin-top: 6px; }
/* Feed — SillyTavern 式对话气泡 */
/* Feed — SillyTavern 式对话气泡 */
.message-feed {
flex: 1;
overflow-y: auto;
@@ -2356,11 +2401,11 @@ body.rail-collapsed .message-feed {
}
.msg-play-process > summary::-webkit-details-marker { display: none; }
.msg-play-process > summary::before {
content: "▸";
content: "▸";
font-size: 10px;
color: var(--muted);
}
.msg-play-process[open] > summary::before { content: "▾"; }
.msg-play-process[open] > summary::before { content: "▾"; }
.msg-play-process .msg-tag {
font-weight: 600;
color: var(--muted);
@@ -2492,7 +2537,7 @@ body.rail-collapsed .message-feed {
margin: 0 0 8px;
}
/* 主对话验收卡(旧):保留选择器以免历史类名失效 */
/* 主对话验收卡(旧):保留选择器以免历史类名失效 */
.msg-review {
align-self: stretch;
width: 100%;
@@ -2529,7 +2574,7 @@ body.rail-collapsed .message-feed {
.msg-review .ws-focus-hint { margin-bottom: 0; font-size: 11px; }
.msg-review .review-worker-set { margin: 0 0 6px; }
/* 顶栏主题菜单:色主题 + 选壳 */
/* 顶栏主题菜单:色主题 + 选壳 */
.more-menu {
position: relative;
display: inline-block;
@@ -2628,6 +2673,119 @@ body.rail-collapsed .message-feed {
background: var(--accent-soft);
color: var(--accent);
}
.more-toggle-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
}
.more-toggle-item .more-theme-copy {
flex: 1 1 auto;
min-width: 0;
}
.more-toggle-mark {
flex: 0 0 auto;
width: 1.1em;
text-align: center;
font-size: 12px;
color: var(--muted);
}
.more-toggle-mark::before {
content: "";
}
.more-toggle-item.is-active {
background: var(--accent-soft);
color: var(--accent);
}
.more-toggle-item.is-active .more-toggle-mark::before {
content: "✓";
color: var(--accent);
font-weight: 700;
}
/* Markdown body (optional render) */
.msg-body .md-p,
.dictate-product-body .md-p,
.present-region-body .md-p {
margin: 0 0 0.55em;
line-height: 1.55;
}
.msg-body .md-p:last-child,
.dictate-product-body .md-p:last-child,
.present-region-body .md-p:last-child {
margin-bottom: 0;
}
.msg-body .md-h,
.dictate-product-body .md-h,
.present-region-body .md-h {
margin: 0.7em 0 0.35em;
font-weight: 650;
line-height: 1.35;
}
.msg-body .md-h:first-child,
.dictate-product-body .md-h:first-child,
.present-region-body .md-h:first-child {
margin-top: 0;
}
.msg-body h1.md-h { font-size: 1.25em; }
.msg-body h2.md-h { font-size: 1.15em; }
.msg-body h3.md-h,
.msg-body h4.md-h,
.msg-body h5.md-h,
.msg-body h6.md-h { font-size: 1.05em; }
.msg-body .md-ul,
.msg-body .md-ol,
.dictate-product-body .md-ul,
.dictate-product-body .md-ol,
.present-region-body .md-ul,
.present-region-body .md-ol {
margin: 0.35em 0 0.55em;
padding-left: 1.35em;
}
.msg-body .md-quote,
.dictate-product-body .md-quote,
.present-region-body .md-quote {
margin: 0.45em 0;
padding: 0.15em 0 0.15em 0.75em;
border-left: 3px solid color-mix(in srgb, var(--accent) 45%, var(--border));
color: var(--muted);
}
.msg-body .md-pre,
.dictate-product-body .md-pre,
.present-region-body .md-pre {
margin: 0.45em 0;
padding: 8px 10px;
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--bg) 80%, var(--surface));
font-size: 12px;
line-height: 1.45;
}
.msg-body .md-code,
.dictate-product-body .md-code,
.present-region-body .md-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.92em;
padding: 0.05em 0.35em;
border-radius: 4px;
background: color-mix(in srgb, var(--bg) 70%, var(--surface));
}
.msg-body .md-a,
.dictate-product-body .md-a,
.present-region-body .md-a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.msg-body .md-hr,
.dictate-product-body .md-hr,
.present-region-body .md-hr {
border: none;
border-top: 1px solid var(--border);
margin: 0.75em 0;
}
.more-theme-icon {
flex: 0 0 auto;
width: 1.25em;
@@ -2656,7 +2814,7 @@ body.rail-collapsed .message-feed {
color: color-mix(in srgb, var(--accent) 70%, var(--muted));
}
/* 提问索引气泡:决策在下方询问卡 */
/* 提问索引气泡:决策在下方询问卡 */
.msg-bubble-index {
width: auto;
max-width: 70%;
@@ -2680,7 +2838,7 @@ body.rail-collapsed .message-feed {
.msg-q-list li { margin: 0.35rem 0; }
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
/* 游玩:终稿铺满阅读区;字号跟正文阅读走,不跟创作验收的大字稿走 */
/* 游玩:终稿铺满阅读区;字号跟正文阅读走,不跟创作验收的大字稿走 */
body[data-lifecycle="play"] .message-feed {
padding: 10px 12px 16px;
gap: 10px;
@@ -2741,7 +2899,7 @@ body[data-lifecycle="play"] .msg-play-final .present-shell--chat_monitor .presen
background: transparent;
}
/* 产物紧凑卡:横向主轴,预览一行 */
/* 产物紧凑卡:横向主轴,预览一行 */
.artifact-compact-list {
display: flex;
flex-direction: column;
@@ -2754,7 +2912,7 @@ body[data-lifecycle="play"] .msg-play-final .present-shell--chat_monitor .presen
overflow: hidden;
}
/* 验收扁平:一条 meta,正文直接读,不套 details/预览摘要 */
/* 验收扁平:一条 meta,正文直接读,不套 details/预览摘要 */
.artifact-flat-review {
display: flex;
flex-direction: column;
@@ -2845,8 +3003,8 @@ body.design-workspace .artifact-flat-review {
border-radius: 999px;
background: var(--accent-soft);
}
.artifact-compact-toggle::before { content: "展开"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "收起"; }
.artifact-compact-toggle::before { content: "展开"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "收起"; }
.artifact-compact-preview {
margin: 0;
font-family: var(--font-read);
@@ -2902,7 +3060,7 @@ body.design-workspace .artifact-flat-review {
}
.ws-focus-body .artifact-compact-list { margin: 0; }
/* 展开区:mosaic / 瓷砖网格 */
/* 展开区:mosaic / 瓷砖网格 */
.af-mosaic {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -2914,7 +3072,7 @@ body.design-workspace .af-mosaic {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}
/* 美学验收:左契约 / 右诊断 */
/* 美学验收:左契约 / 右诊断 */
.af-split {
display: flex;
flex-direction: column;
@@ -2997,7 +3155,7 @@ body.design-workspace .af-mosaic {
min-width: 0;
}
/* 美学卡:阅读层级 — 内核最大,范式次之,右栏更密更沉 */
/* 美学卡:阅读层级 — 内核最大,范式次之,右栏更密更沉 */
.af-split--aesthetics .af-split-product {
gap: 22px;
}
@@ -3084,7 +3242,7 @@ body.design-workspace .af-mosaic {
}
}
/* 旧布局类名保留,避免残留引用崩样式 */
/* 旧布局类名保留,避免残留引用崩样式 */
.af-review-stack,
.af-review-layout {
display: flex;
@@ -3355,7 +3513,7 @@ body.design-workspace .af-mosaic {
word-break: break-word;
}
/* 行内完备度 / 评分 */
/* 行内完备度 / 评分 */
.pct-inline {
display: inline-flex;
align-items: baseline;
@@ -3386,14 +3544,14 @@ body.design-workspace .af-mosaic {
gap: 5px;
}
/* 技能专用正文卡 */
/* 技能专用正文卡 */
.skill-view {
display: flex;
flex-direction: column;
gap: 14px;
}
/* 美学纲领:主栏契约 / 辅栏诊断(无第二层盒子) */
/* 美学纲领:主栏契约 / 辅栏诊断(无第二层盒子) */
.skill-view.skill-view-aesthetics {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(15.5rem, 0.88fr);
@@ -4061,7 +4219,7 @@ body.design-workspace .af-mosaic {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #2a6) 20%, transparent);
}
/* 询问卡:对话流里 host 即卡片;创作工作面上剥壳,见上方 flatten 规则 */
/* 询问卡:对话流里 host 即卡片;创作工作面上剥壳,见上方 flatten 规则 */
.questions-card-host {
--qcard-max: 48vh;
flex: 0 1 auto;
@@ -4145,7 +4303,7 @@ body.design-workspace .af-mosaic {
.questions-card-host.is-collapsed .qcard-float {
display: none;
}
/* 悬浮控件层:不占文档流高度 */
/* 悬浮控件层:不占文档流高度 */
.qcard-float {
position: absolute;
inset: 0;
@@ -4331,7 +4489,7 @@ body.design-workspace .af-mosaic {
}
.ws-slot-chip.ws-slot-on { background: var(--accent-soft); border-color: transparent; }
.ws-slot-chip.ws-slot-off { opacity: 0.55; text-decoration: line-through; }
/* 产物结构化展示 — 对齐 qcard / flow-step / ws-badge */
/* 产物结构化展示 — 对齐 qcard / flow-step / ws-badge */
.artifact-friendly { display: flex; flex-direction: column; gap: 10px; }
.artifact-list { margin: 0; padding-left: 18px; line-height: 1.5; }
.artifact-hero {
@@ -4431,7 +4589,7 @@ body.design-workspace .af-mosaic {
.artifact-kv-row.complex .artifact-kv-key { margin-bottom: 2px; }
.artifact-kv-val { min-width: 0; font-size: 13px; line-height: 1.45; }
/* 自适应定义行:够宽并排,不够整组折行——同行规则,不按字数抽签 */
/* 自适应定义行:够宽并排,不够整组折行——同行规则,不按字数抽签 */
.af-def-list {
display: flex;
flex-direction: column;
@@ -4488,7 +4646,7 @@ body.design-workspace .af-mosaic {
flex-basis: 14rem;
}
/* 百分比进度条 */
/* 百分比进度条 */
.pct-meter {
display: flex;
flex-direction: column;
@@ -4626,7 +4784,7 @@ body.design-workspace .af-mosaic {
.artifact-raw > summary:hover { color: var(--text); }
.artifact-raw .json-pretty { margin-top: 6px; max-height: min(40vh, 360px); }
/* 指针 tag / 扁平面包屑:一行路径 + 不起眼原文 */
/* 指针 tag / 扁平面包屑:一行路径 + 不起眼原文 */
.artifact-crumb {
display: flex;
flex-wrap: wrap;
@@ -4895,7 +5053,7 @@ body.design-workspace .af-mosaic {
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
.timeline-empty { color: var(--muted); }
/* Composer — emoji 更多在左 / 输入 / 发送;菜单向上弹出不被裁切 */
/* Composer — 左汉堡 / 输入 / 发送;菜单向上弹出不被裁切 */
.composer {
border-top: 1px solid var(--border);
padding: 6px var(--workspace-pad-x, 16px) 8px;
@@ -4910,6 +5068,9 @@ body.design-workspace .af-mosaic {
align-items: flex-end;
gap: 8px;
}
.composer:has(.composer-more-menu[open]) {
z-index: 120;
}
.composer > .status {
flex: 0 0 auto;
align-self: center;
@@ -4924,9 +5085,16 @@ body.design-workspace .af-mosaic {
min-width: 0;
min-height: 0;
max-height: min(50vh, 420px);
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
/* 唯一描边:输入 + 操作同框 */
#composer-main:has(.composer-more-menu[open]) {
overflow: visible;
}
.main-stack:has(.composer-more-menu[open]) {
overflow: visible;
}
/* 唯一描边:输入 + 操作同框 */
.composer-input-shell {
flex: 1 1 auto;
min-width: 0;
@@ -5043,14 +5211,48 @@ body.design-workspace .af-mosaic {
.composer-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
.composer-form {
display: flex;
gap: 0;
gap: 8px;
align-items: flex-end;
min-height: 0;
max-height: 100%;
width: 100%;
}
.composer-form .composer-input-shell {
width: 100%;
flex: 1 1 auto;
width: auto;
min-width: 0;
position: relative;
z-index: 1;
}
.composer-more-menu {
align-self: center;
position: relative;
z-index: 2;
flex: 0 0 auto;
}
.composer-more-menu[open] {
z-index: 130;
}
.composer-more-menu .more-menu-sum {
height: var(--composer-control-h);
min-height: var(--composer-control-h);
min-width: var(--composer-control-h);
border: 1px solid transparent;
background: transparent;
color: var(--muted);
border-radius: 8px;
}
.composer-more-menu .more-menu-sum:hover {
color: var(--accent);
background: var(--accent-soft);
}
.composer-more-menu .more-menu-panel {
left: 0;
right: auto;
top: auto;
bottom: calc(100% + 6px);
min-width: 220px;
z-index: 140;
}
.composer-shell-actions > .btn,
.composer-shell-actions > .btn-primary,
@@ -5080,7 +5282,7 @@ body.design-workspace .af-mosaic {
font-size: 16px;
line-height: 1;
}
/* 验收态:主按钮承接原「批阅」语义 */
/* 验收态:主按钮承接原「批阅」语义 */
.composer-btn-review {
background: #15803d;
border-color: #15803d;
@@ -5163,9 +5365,9 @@ body.design-workspace .af-mosaic {
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
.dialog-head-row { display: flex; justify-content: space-between; align-items: center; }
/* —— 游玩呈现壳 present.v1 ——
* 主工作面 = body(随 1000~2000 字撑高)。
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
/* —— 游玩呈现壳 present.v1 ——
* 主工作面 = body(随 1000~2000 字撑高)。
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
*/
.present-shell {
white-space: normal;
@@ -5235,7 +5437,7 @@ body.design-workspace .af-mosaic {
.present-region-body > p:last-child {
margin-bottom: 0;
}
/* P0:正文舞台 —— 随字数伸展(1000~2000 靠外层滚动,不塞进小框) */
/* P0:正文舞台 —— 随字数伸展(1000~2000 靠外层滚动,不塞进小框) */
.present-stage {
flex: 1 1 auto;
min-height: 0;
@@ -5264,7 +5466,7 @@ body.design-workspace .af-mosaic {
line-height: 1.7;
max-width: none;
}
/* 双栏:侧栏钉住,正文再长也能对照 */
/* 双栏:侧栏钉住,正文再长也能对照 */
.present-board-side,
.present-split-side,
.present-reader-aside {
@@ -5274,7 +5476,7 @@ body.design-workspace .af-mosaic {
max-height: calc(100dvh - 3rem);
overflow-y: auto;
}
/* P2:监控芯片 */
/* P2:监控芯片 */
.present-chips {
display: flex;
flex-wrap: wrap;
@@ -5316,7 +5518,7 @@ body.design-workspace .af-mosaic {
letter-spacing: 0.01em;
line-height: 1.4;
}
/* P1:行动 */
/* P1:行动 */
.present-action-row {
display: flex;
flex-wrap: wrap;
@@ -5340,7 +5542,7 @@ body.design-workspace .af-mosaic {
line-height: 1.45;
font-size: 13px;
}
/* —— 各壳布局(结构差在同时可见关系;不靠渐变区分) —— */
/* —— 各壳布局(结构差在同时可见关系;不靠渐变区分) —— */
.present-shell--chat_monitor {
--present-accent: #0369a1;
}
@@ -5582,3 +5784,153 @@ body.design-workspace .af-mosaic {
min-width: 4.5em;
text-align: center;
}
/* תÊöÕûÀí£ºÒѳ־û¯²úÎïչʾ */
.dictate-products {
margin-top: 14px;
border: 1px solid var(--border, #d8d4cc);
background: var(--surface-2, #f7f5f1);
padding: 10px 12px;
}
.dictate-products-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.dictate-products-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted, #6b6560);
}
.dictate-products-count {
font-size: 12px;
color: var(--muted, #6b6560);
font-variant-numeric: tabular-nums;
}
.dictate-products-empty {
margin: 0;
font-size: 13px;
color: var(--muted, #6b6560);
line-height: 1.45;
}
.dictate-products-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.dictate-product {
border: 1px solid var(--border, #e2ddd4);
background: var(--surface, #fff);
}
.dictate-product > summary {
display: grid;
grid-template-columns: minmax(7rem, 11rem) 1fr;
gap: 8px;
align-items: baseline;
padding: 8px 10px;
cursor: pointer;
list-style: none;
}
.dictate-product > summary::-webkit-details-marker {
display: none;
}
.dictate-product-tag {
font-size: 12px;
font-weight: 600;
color: var(--fg, #1c1917);
word-break: break-all;
}
.dictate-product-preview {
font-size: 12px;
color: var(--muted, #6b6560);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.dictate-product-body {
margin: 0;
padding: 0 10px 10px;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 240px;
overflow: auto;
color: var(--fg, #1c1917);
font-family: inherit;
}
/* 转述:右侧栏产物列表 — 只上下滚,长文换行不横滑 */
.dictate-product--rail {
border: 1px solid var(--border, #e2ddd4);
background: var(--surface, #fff);
margin-bottom: 6px;
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.dictate-product--rail > summary {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
cursor: pointer;
list-style: none;
min-width: 0;
}
.dictate-product--rail > summary::-webkit-details-marker { display: none; }
.dictate-product--rail .dictate-product-tag {
font-size: 12px;
font-weight: 600;
word-break: break-all;
}
.dictate-product--rail .dictate-product-preview {
font-size: 12px;
color: var(--muted, #6b6560);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
max-width: 100%;
}
.dictate-product--rail .dictate-product-body {
margin: 0;
padding: 0 10px 10px;
font-size: 12px;
line-height: 1.55;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow-x: hidden;
font-family: inherit;
color: var(--fg, #1c1917);
}
.dictate-products-list {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
width: 100%;
max-width: 100%;
}
body[data-coord-rail-mode="products"] .coord-drawer-body {
padding: 8px 10px 12px;
overflow-x: hidden;
overflow-y: auto;
min-width: 0;
}
body[data-coord-rail-mode="products"] .coord-rail,
body[data-coord-rail-mode="products"] .coord-rail-panel {
min-width: 0;
overflow-x: hidden;
}