引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+514
-73
@@ -12,6 +12,7 @@ import {
|
||||
parsePresentDoc,
|
||||
presentFromPlain,
|
||||
renderPresentShellHtml,
|
||||
stripPresentSourceFences,
|
||||
} from "./present-shells.js";
|
||||
|
||||
const HIDE_KINDS = new Set(["worker_stub"]);
|
||||
@@ -57,6 +58,10 @@ function esc(s) {
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return esc(s).replace(/"/g, """);
|
||||
}
|
||||
|
||||
function renderThinkingBlock(thinking, { open = false } = {}) {
|
||||
const text = (thinking ?? "").trim();
|
||||
if (!text) return "";
|
||||
@@ -66,23 +71,39 @@ function renderThinkingBlock(thinking, { open = false } = {}) {
|
||||
</details>`;
|
||||
}
|
||||
|
||||
function isPlayView(view) {
|
||||
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
|
||||
}
|
||||
|
||||
function renderLiveStreamBody(live, view) {
|
||||
const thinking = (live?.thinking || view?.agentThinking || "").trim();
|
||||
const output = (live?.output ?? "").trim();
|
||||
const showOutput =
|
||||
Boolean(output) && (view?.lifecycleStage ?? document.body.dataset.lifecycle) === "play";
|
||||
const parts = [];
|
||||
if (thinking) {
|
||||
parts.push(
|
||||
`<section class="msg-live-section"><header>思考</header><pre class="msg-live-pre">${esc(thinking)}</pre></section>`,
|
||||
);
|
||||
}
|
||||
if (showOutput) {
|
||||
if (output) {
|
||||
parts.push(
|
||||
`<section class="msg-live-section"><header>输出</header><pre class="msg-live-pre">${esc(output)}</pre></section>`,
|
||||
);
|
||||
}
|
||||
return parts.length ? parts.join("") : "…";
|
||||
return parts.length
|
||||
? parts.join("")
|
||||
: `<p class="msg-live-empty">正在生成,尚未收到内容</p>`;
|
||||
}
|
||||
|
||||
function renderRetryRunButton() {
|
||||
return `<button type="button" class="msg-action msg-action-primary" data-act="retry_run" title="停止当前生成并重试">↻</button>`;
|
||||
}
|
||||
|
||||
function wireRetryRun(root, handlers) {
|
||||
root?.querySelector("[data-act=retry_run]")?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlers.onRetryRun?.();
|
||||
});
|
||||
}
|
||||
|
||||
function fmtTime(iso) {
|
||||
@@ -141,13 +162,19 @@ function hideMsgMenu() {
|
||||
const menu = document.getElementById("msg-action-menu");
|
||||
if (menu) menu.hidden = true;
|
||||
msgMenuState.messageId = null;
|
||||
msgMenuState.contextMessageId = null;
|
||||
msgMenuState.rollbackMessageId = null;
|
||||
msgMenuState.rollbackMode = "keep";
|
||||
msgMenuState.restoreDraft = "";
|
||||
msgMenuState.card = null;
|
||||
}
|
||||
|
||||
const msgMenuState = {
|
||||
messageId: null,
|
||||
contextMessageId: null,
|
||||
rollbackMessageId: null,
|
||||
rollbackMode: "keep",
|
||||
restoreDraft: "",
|
||||
card: null,
|
||||
handlers: null,
|
||||
};
|
||||
@@ -155,25 +182,38 @@ const msgMenuState = {
|
||||
function showMsgMenu(card, x, y) {
|
||||
const menu = document.getElementById("msg-action-menu");
|
||||
if (!menu || !card) return;
|
||||
const isReview = card.dataset.review === "1";
|
||||
const isReadOnly = card.dataset.readOnly === "1";
|
||||
const canEdit = !isReview && card.dataset.canEdit === "1";
|
||||
const canDelete = !isReview && !isReadOnly && Boolean(card.dataset.messageId);
|
||||
const canRollback = !isReview && !isReadOnly && Boolean(card.dataset.rollbackMessageId);
|
||||
const hasContext = card.dataset.hasContext === "1";
|
||||
const pack = card.closest(".coord-pack");
|
||||
const rollbackHost = pack || card;
|
||||
// 上下文按单次请求(这条消息)区分,不能回落到整包里的另一条
|
||||
const contextTarget = card.dataset.hasContext === "1" ? card : null;
|
||||
const isReview = card.dataset.review === "1" || pack?.dataset.review === "1";
|
||||
const isReadOnly = card.dataset.readOnly === "1" || pack?.dataset.readOnly === "1";
|
||||
const canEdit = !isReview && !pack && card.dataset.canEdit === "1";
|
||||
const canDelete = !isReview && !isReadOnly && !pack && Boolean(card.dataset.messageId);
|
||||
const rollbackMessageId = rollbackHost.dataset.rollbackMessageId ?? null;
|
||||
const rollbackMode = rollbackHost.dataset.rollbackMode || "keep";
|
||||
const canRollback = !isReview && !isReadOnly && Boolean(rollbackMessageId);
|
||||
const hasContext = Boolean(contextTarget);
|
||||
const editBtn = document.getElementById("msg-menu-edit");
|
||||
const delBtn = document.getElementById("msg-menu-delete");
|
||||
const rollbackBtn = document.getElementById("msg-menu-rollback");
|
||||
const ctxBtn = document.getElementById("msg-menu-context");
|
||||
if (editBtn) editBtn.toggleAttribute("hidden", !canEdit);
|
||||
if (delBtn) delBtn.toggleAttribute("hidden", !canDelete);
|
||||
if (rollbackBtn) rollbackBtn.toggleAttribute("hidden", !canRollback);
|
||||
if (rollbackBtn) {
|
||||
rollbackBtn.toggleAttribute("hidden", !canRollback);
|
||||
rollbackBtn.textContent =
|
||||
rollbackMode === "pending-input" ? "回到这一轮输入" : "回退到这里";
|
||||
}
|
||||
if (ctxBtn) ctxBtn.toggleAttribute("hidden", !hasContext);
|
||||
menu.hidden = false;
|
||||
menu.style.left = `${x}px`;
|
||||
menu.style.top = `${y}px`;
|
||||
msgMenuState.messageId = card.dataset.messageId ?? null;
|
||||
msgMenuState.rollbackMessageId = card.dataset.rollbackMessageId ?? null;
|
||||
msgMenuState.contextMessageId = contextTarget?.dataset.messageId ?? card.dataset.messageId ?? null;
|
||||
msgMenuState.rollbackMessageId = rollbackMessageId;
|
||||
msgMenuState.rollbackMode = rollbackMode;
|
||||
msgMenuState.restoreDraft = rollbackHost.dataset.restoreDraft ?? "";
|
||||
msgMenuState.card = card;
|
||||
const rect = menu.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) {
|
||||
@@ -193,12 +233,15 @@ function wireMsgActionMenu(handlers) {
|
||||
menu.addEventListener("click", async (e) => {
|
||||
const action = e.target.closest("[data-msg-menu-action]")?.getAttribute("data-msg-menu-action");
|
||||
const messageId = msgMenuState.messageId;
|
||||
const contextMessageId = msgMenuState.contextMessageId || messageId;
|
||||
const rollbackMessageId = msgMenuState.rollbackMessageId;
|
||||
const rollbackMode = msgMenuState.rollbackMode;
|
||||
const restoreDraft = msgMenuState.restoreDraft;
|
||||
const card = msgMenuState.card;
|
||||
hideMsgMenu();
|
||||
if (!messageId || !action) return;
|
||||
const bodyEl = card?.querySelector(".msg-body:not(.msg-body-editing), .coord-text");
|
||||
const body = bodyEl?.textContent ?? card?.dataset.originalText ?? "";
|
||||
const body = card?.dataset.originalText || bodyEl?.textContent || "";
|
||||
|
||||
if (action === "copy") {
|
||||
try {
|
||||
@@ -209,7 +252,7 @@ function wireMsgActionMenu(handlers) {
|
||||
return;
|
||||
}
|
||||
if (action === "view-context") {
|
||||
msgMenuState.handlers?.onViewContext?.(messageId);
|
||||
msgMenuState.handlers?.onViewContext?.(contextMessageId);
|
||||
return;
|
||||
}
|
||||
if (action === "edit") {
|
||||
@@ -218,8 +261,20 @@ function wireMsgActionMenu(handlers) {
|
||||
}
|
||||
if (action === "rollback") {
|
||||
if (!rollbackMessageId) return;
|
||||
if (!confirm("回退到这条消息?之后的对话和创作状态将被移除。")) return;
|
||||
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
||||
const pendingInput = rollbackMode === "pending-input";
|
||||
const confirmed = pendingInput
|
||||
? confirm(
|
||||
"回到这一轮的待输入?这一轮及之后的对话和创作状态将被移除,原文会填回底栏。",
|
||||
)
|
||||
: confirm("回退到这条消息?之后的对话和创作状态将被移除。");
|
||||
if (!confirmed) return;
|
||||
if (pendingInput) {
|
||||
const restore = msgMenuState.handlers?.onRollbackToInput;
|
||||
if (typeof restore === "function") restore(rollbackMessageId, restoreDraft);
|
||||
else msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
||||
} else {
|
||||
msgMenuState.handlers?.onDeleteMessage?.(rollbackMessageId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (action === "delete") {
|
||||
@@ -234,7 +289,7 @@ function wireMessageContextMenu(feed, handlers) {
|
||||
feed.dataset.contextWired = "1";
|
||||
wireMsgActionMenu(handlers);
|
||||
feed.addEventListener("contextmenu", (e) => {
|
||||
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line");
|
||||
const bubble = e.target.closest(".msg-bubble, .workspace-review, .coord-line, .coord-pack");
|
||||
const card = e.target.closest("[data-message-id]");
|
||||
if (!bubble || !card || card.classList.contains("msg-pending")) return;
|
||||
e.preventDefault();
|
||||
@@ -423,22 +478,40 @@ function formatQuestionsHtml(body) {
|
||||
}
|
||||
|
||||
/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */
|
||||
function isPlayHousekeepingMessage(msg) {
|
||||
const title = String(msg.title ?? "");
|
||||
const text = String(msg.text ?? msg.body ?? "").trim();
|
||||
if (title === "上下文已压缩" || title === "阶段机") return true;
|
||||
if (/^\[上下文已压缩\]/.test(text)) return true;
|
||||
if (/^\[裁决合并\]/.test(text)) return true;
|
||||
if (/^\[旁观维护\]/.test(text)) return true;
|
||||
if (/归档\s*\d+\s*个过程 tag/.test(text)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldRenderPlayPresent(msg, view) {
|
||||
if (view?.lifecycleStage !== "play") return false;
|
||||
if (!isPlayView(view)) return false;
|
||||
if (msg.role === "user") return false;
|
||||
const kind = msg.kind ?? "";
|
||||
if (kind === "worker_questions" || kind === "orchestrator_thinking") return false;
|
||||
if (kind === "worker_output") return false; // 已走 formatArtifactBodyHtml
|
||||
if (isPlayHousekeepingMessage(msg)) return false;
|
||||
const actor = String(msg.actor ?? "");
|
||||
if (actor === "narrator" || /用户展示|开场白/.test(String(msg.title ?? ""))) {
|
||||
return true;
|
||||
}
|
||||
// 助手可见终稿:无特殊 kind 的长文
|
||||
return kind === "system_info" || kind === "worker_stub" || !kind;
|
||||
const title = String(msg.title ?? "");
|
||||
return actor === "narrator" || actor === "round-present" || /用户展示|开场白/.test(title);
|
||||
}
|
||||
|
||||
function isPlayFinalReply(msg, view) {
|
||||
if (!isPlayView(view) || msg.role === "user") return false;
|
||||
return shouldRenderPlayPresent(msg, view);
|
||||
}
|
||||
|
||||
function isPlayProcessMessage(msg, view) {
|
||||
if (!isPlayView(view) || msg.role === "user") return false;
|
||||
return !isPlayFinalReply(msg, view);
|
||||
}
|
||||
|
||||
function formatPlayPresentHtml(body, view) {
|
||||
const trimmed = (body || "").trim();
|
||||
const trimmed = stripPresentSourceFences((body || "").trim());
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
const tweaks = view?.presentationTweaks;
|
||||
const fallbackShell =
|
||||
@@ -562,7 +635,7 @@ function wireRailChrome() {
|
||||
|
||||
function hasStartedCreation(view) {
|
||||
if (!view?.id) return false;
|
||||
if (view.lifecycleStage === "play") return true;
|
||||
if (isPlayView(view)) return true;
|
||||
if (hasUserMessages(view)) return true;
|
||||
const reason = view.waitingReason?.kind;
|
||||
if (reason && reason !== "intake") return true;
|
||||
@@ -626,6 +699,7 @@ function shouldShowInFeed(msg, view) {
|
||||
if (HIDE_KINDS.has(kind)) return false;
|
||||
if (FEED_HIDDEN_KINDS.has(kind)) return false;
|
||||
if (msg.compressed) return false;
|
||||
if (isPlayView(view) && isPlayHousekeepingMessage(msg)) return false;
|
||||
if (isSkillSelectionMessage(msg)) return false;
|
||||
if (isReviewSidecarQuestionStub(msg)) return false;
|
||||
if (
|
||||
@@ -635,11 +709,11 @@ function shouldShowInFeed(msg, view) {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact") {
|
||||
if (kind === "worker_output" && view.waitingReason?.kind === "review_artifact" && !isPlayView(view)) {
|
||||
return false;
|
||||
}
|
||||
// 创作:编排器思考进「调度」,不占主区;游玩保持原样
|
||||
if (kind === "orchestrator_thinking" && view.lifecycleStage !== "play") {
|
||||
// 创作:编排器思考进「调度」,不占主区;游玩保持原样(默认折叠)
|
||||
if (kind === "orchestrator_thinking" && !isPlayView(view)) {
|
||||
return false;
|
||||
}
|
||||
if (kind === "orchestrator_prompt") return false;
|
||||
@@ -661,10 +735,10 @@ function maybeSyncRail(view) {
|
||||
}
|
||||
|
||||
export function renderLifecycle(view) {
|
||||
const toggle = document.getElementById("lifecycle-toggle");
|
||||
if (!toggle) return;
|
||||
const stage = view.lifecycleStage ?? "design";
|
||||
document.body.dataset.lifecycle = stage;
|
||||
const toggle = document.getElementById("lifecycle-toggle");
|
||||
if (!toggle) return;
|
||||
toggle.querySelectorAll("[data-stage]").forEach((btn) => {
|
||||
const s = btn.getAttribute("data-stage");
|
||||
btn.classList.toggle("active", s === stage);
|
||||
@@ -1541,7 +1615,7 @@ function renderKnownArtifactHtml(doc, opts = {}) {
|
||||
if (schema === "context-fragment.v1" || isContextFragmentLike(doc)) {
|
||||
return renderContextFragmentHtml(doc, opts);
|
||||
}
|
||||
if (schema === "context-order.v1" || Array.isArray(doc.slots)) {
|
||||
if (schema === "context-order.v1" || Array.isArray(doc.slots) || Array.isArray(doc.agents)) {
|
||||
const orderHtml = renderContextOrderHtml(doc);
|
||||
if (orderHtml) return orderHtml;
|
||||
}
|
||||
@@ -1666,6 +1740,105 @@ function renderContextFragmentHtml(doc, opts = {}) {
|
||||
return `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
|
||||
}
|
||||
|
||||
/** 开场白终节点:1~多条候选用呈现壳展示,选定后由程序落库 */
|
||||
function collectOpeningCandidateTexts(body) {
|
||||
const texts = [];
|
||||
const push = (value) => {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !texts.includes(text)) texts.push(text);
|
||||
};
|
||||
push(body.开场白全文);
|
||||
if (Array.isArray(body.开场白候选)) {
|
||||
for (const item of body.开场白候选) {
|
||||
if (typeof item === "string") push(item);
|
||||
else if (item && typeof item === "object") {
|
||||
push(item.全文 || item.text || item.开场白 || item.内容);
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
function renderOpeningSetupBodyHtml(body) {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||
const candidates = collectOpeningCandidateTexts(body);
|
||||
if (!candidates.length) return "";
|
||||
const total = candidates.length;
|
||||
const slides = candidates
|
||||
.map((text, i) => {
|
||||
const hidden = i === 0 ? "" : " hidden";
|
||||
const selected = i === 0 ? " is-selected" : "";
|
||||
return `<article class="opening-candidate${selected}" data-opening-index="${i}"${hidden}>${renderPresentShellHtml(
|
||||
presentFromPlain(text, "prose"),
|
||||
esc,
|
||||
)}</article>`;
|
||||
})
|
||||
.join("");
|
||||
const nav =
|
||||
total > 1
|
||||
? `<div class="opening-picker-nav">
|
||||
<button type="button" class="opening-picker-btn" data-opening-delta="-1">上一版</button>
|
||||
<span class="opening-picker-pos" data-opening-pos>1 / ${total}</span>
|
||||
<button type="button" class="opening-picker-btn" data-opening-delta="1">下一版</button>
|
||||
</div>`
|
||||
: "";
|
||||
const vars = Array.isArray(body.开场变量) ? body.开场变量 : [];
|
||||
const varsHtml = vars.length
|
||||
? `<section class="artifact-block"><h4>开场变量</h4><ul class="artifact-list">${vars
|
||||
.map((row) => {
|
||||
const name = row?.名 || row?.name || row?.key || "";
|
||||
const val = row?.值 !== undefined ? row.值 : row?.value;
|
||||
const note = row?.依据 || row?.note || "";
|
||||
const noteHtml = note
|
||||
? ` <span class="muted">(${esc(String(note))})</span>`
|
||||
: "";
|
||||
return `<li><strong>${esc(String(name))}</strong>:${esc(String(val ?? ""))}${noteHtml}</li>`;
|
||||
})
|
||||
.join("")}</ul></section>`
|
||||
: "";
|
||||
const space = body.用户可行动空间
|
||||
? `<section class="artifact-block"><h4>可行动空间</h4>${renderProseHtml(
|
||||
body.用户可行动空间,
|
||||
)}</section>`
|
||||
: "";
|
||||
return `<div class="opening-setup-body">
|
||||
<div class="opening-picker" data-opening-picker="1" data-selected="0" data-total="${total}">
|
||||
${slides}
|
||||
${nav}
|
||||
</div>
|
||||
${varsHtml}
|
||||
${space}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function wireOpeningPicker() {
|
||||
document.querySelectorAll("[data-opening-picker]").forEach((root) => {
|
||||
if (root.dataset.wired === "1") return;
|
||||
root.dataset.wired = "1";
|
||||
const total = Math.max(1, Number(root.getAttribute("data-total")) || 1);
|
||||
const show = (index) => {
|
||||
const next = ((index % total) + total) % total;
|
||||
root.setAttribute("data-selected", String(next));
|
||||
root.querySelectorAll("[data-opening-index]").forEach((el) => {
|
||||
const i = Number(el.getAttribute("data-opening-index"));
|
||||
const on = i === next;
|
||||
el.toggleAttribute("hidden", !on);
|
||||
el.classList.toggle("is-selected", on);
|
||||
});
|
||||
const pos = root.querySelector("[data-opening-pos]");
|
||||
if (pos) pos.textContent = `${next + 1} / ${total}`;
|
||||
};
|
||||
root.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-opening-delta]");
|
||||
if (!btn || !root.contains(btn)) return;
|
||||
const delta = Number(btn.getAttribute("data-opening-delta"));
|
||||
if (delta !== 1 && delta !== -1) return;
|
||||
const cur = Number(root.getAttribute("data-selected") || 0);
|
||||
show(cur + delta);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */
|
||||
function renderSpecialtyBodyHtml(body, skill) {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||
@@ -1703,6 +1876,22 @@ function renderSpecialtyBodyHtml(body, skill) {
|
||||
const html = renderNarrativeBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
if (
|
||||
body.呈现壳 != null ||
|
||||
body.可见块 != null ||
|
||||
(body.示例 != null && (skillName.includes("正文组成") || skillName.includes("回复格式")))
|
||||
) {
|
||||
const html = renderReplyFormatBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
if (
|
||||
body.开场白全文 != null ||
|
||||
body.开场白候选 != null ||
|
||||
skillName.includes("开场白")
|
||||
) {
|
||||
const html = renderOpeningSetupBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
|
||||
const looksAesthetics =
|
||||
body.设定逻辑 != null ||
|
||||
@@ -1782,6 +1971,101 @@ function renderSpecialtyBodyHtml(body, skill) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** 正文组成 · 壳适配 + 示例灌数(present 壳预览,验收美化) */
|
||||
function renderReplyFormatBodyHtml(body) {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||
const shellDoc = body.呈现壳 && typeof body.呈现壳 === "object" ? body.呈现壳 : {};
|
||||
const shellId =
|
||||
typeof shellDoc.shell_id === "string" && PRESENT_SHELL_IDS.includes(shellDoc.shell_id)
|
||||
? shellDoc.shell_id
|
||||
: typeof shellDoc.shell_id === "string"
|
||||
? shellDoc.shell_id
|
||||
: "";
|
||||
const tweaks =
|
||||
shellDoc.微调 && typeof shellDoc.微调 === "object" && !Array.isArray(shellDoc.微调)
|
||||
? shellDoc.微调
|
||||
: {};
|
||||
const example = body.示例 && typeof body.示例 === "object" ? body.示例 : null;
|
||||
const packetRaw = example?.灌数;
|
||||
const presentView =
|
||||
packetRaw && typeof packetRaw === "object"
|
||||
? parsePresentDoc(
|
||||
{
|
||||
...packetRaw,
|
||||
shell: packetRaw.shell || packetRaw.shell_id || shellId || "prose",
|
||||
},
|
||||
shellId || "prose",
|
||||
)
|
||||
: null;
|
||||
|
||||
const parts = [];
|
||||
parts.push(`<div class="af-split-banner" role="note">
|
||||
<span><b>本步看壳与美化</b></span>
|
||||
<span>示例非正史 · 文笔不重要</span>
|
||||
</div>`);
|
||||
|
||||
const metaBits = [];
|
||||
if (shellId) {
|
||||
metaBits.push(`<span class="ws-badge ws-badge-continue">壳 ${esc(shellId)}</span>`);
|
||||
}
|
||||
if (shellDoc.为何选它) {
|
||||
metaBits.push(`<span class="ws-muted">${esc(String(shellDoc.为何选它))}</span>`);
|
||||
}
|
||||
if (body.版式隐喻) {
|
||||
metaBits.push(`<span class="ws-muted">隐喻:${esc(String(body.版式隐喻))}</span>`);
|
||||
}
|
||||
if (metaBits.length) {
|
||||
parts.push(`<div class="artifact-chip-row reply-format-shell-meta">${metaBits.join("")}</div>`);
|
||||
}
|
||||
|
||||
if (presentView) {
|
||||
const nature = example?.性质 ? esc(String(example.性质)) : "验收预览|非正史";
|
||||
const explain = example?.用来说明 ? `<p class="ws-muted">${esc(String(example.用来说明))}</p>` : "";
|
||||
parts.push(`<section class="af-panel af-panel-wide reply-format-preview">
|
||||
<h4>版式预览 <span class="ws-badge">${nature}</span></h4>
|
||||
${explain}
|
||||
${renderPresentShellHtml(presentView.packet, esc, { tweaks })}
|
||||
</section>`);
|
||||
} else {
|
||||
parts.push(
|
||||
`<section class="af-panel"><p class="ws-muted">(暂无示例灌数;定稿时应给 present.v1 预览)</p></section>`,
|
||||
);
|
||||
}
|
||||
|
||||
const contractKeys = [
|
||||
"依据的体验与呈现",
|
||||
"呈现壳",
|
||||
"版式隐喻",
|
||||
"可见块",
|
||||
"监控栏锚点",
|
||||
"隐藏段",
|
||||
"前端拆分与美化",
|
||||
"拼装与终稿",
|
||||
];
|
||||
const used = new Set(["示例"]);
|
||||
for (const key of contractKeys) {
|
||||
if (body[key] == null || body[key] === "") continue;
|
||||
used.add(key);
|
||||
parts.push(
|
||||
`<details class="skill-example reply-format-contract"><summary>${esc(key)}</summary>${renderStructuredValueHtml(
|
||||
body[key],
|
||||
0,
|
||||
)}</details>`,
|
||||
);
|
||||
}
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (used.has(k) || v == null || v === "") continue;
|
||||
parts.push(
|
||||
`<details class="skill-example reply-format-contract"><summary>${esc(k)}</summary>${renderStructuredValueHtml(
|
||||
v,
|
||||
0,
|
||||
)}</details>`,
|
||||
);
|
||||
}
|
||||
|
||||
return `<div class="artifact-friendly artifact-reply-format">${parts.join("")}</div>`;
|
||||
}
|
||||
|
||||
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
|
||||
function renderNarrativeBodyHtml(body) {
|
||||
const order = [
|
||||
@@ -2528,7 +2812,10 @@ function renderSelfScoreHtml(自评) {
|
||||
|
||||
function renderProbeHtml(追问) {
|
||||
if (!追问 || typeof 追问 !== "object") return "";
|
||||
const qs = Array.isArray(追问.题目) ? 追问.题目 : [];
|
||||
const qs = Array.isArray(追问.题目)
|
||||
? 追问.题目.filter((q) => q && typeof q === "object" && (q.问 || q.prompt))
|
||||
: [];
|
||||
if (!qs.length) return "";
|
||||
const cards = [];
|
||||
if (追问.导语) {
|
||||
cards.push(`<p class="artifact-probe-lead">${esc(String(追问.导语))}</p>`);
|
||||
@@ -2555,7 +2842,13 @@ function renderProbeHtml(追问) {
|
||||
}
|
||||
|
||||
function renderContextOrderHtml(doc) {
|
||||
if (!Array.isArray(doc.slots) || !doc.slots.length) return null;
|
||||
const slots =
|
||||
Array.isArray(doc.slots) && doc.slots.length
|
||||
? doc.slots
|
||||
: Array.isArray(doc.agents)
|
||||
? doc.agents.filter((a) => a && a.enabled !== false)
|
||||
: [];
|
||||
if (!slots.length) return null;
|
||||
const parts = [];
|
||||
if (doc.brief) {
|
||||
parts.push(
|
||||
@@ -2564,7 +2857,7 @@ function renderContextOrderHtml(doc) {
|
||||
}
|
||||
for (const slot of doc.slots) {
|
||||
if (!slot || typeof slot !== "object") continue;
|
||||
const title = slot.label || slot.ref || "槽";
|
||||
const title = slot.label || slot.ref || slot.id || "槽";
|
||||
const inserts = Array.isArray(slot.inserts) ? slot.inserts : [];
|
||||
if (!inserts.length && Array.isArray(slot.order)) {
|
||||
// 扁平序:直接是条目列表
|
||||
@@ -2919,7 +3212,7 @@ function renderCreationFlowView(flowView) {
|
||||
return `<div class="review-parse-error" role="alert">
|
||||
<div class="review-parse-error-title">流程无法解析</div>
|
||||
<p>${esc(flowView.parseError)}</p>
|
||||
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。</p>
|
||||
<p class="review-parse-error-hint">需要 JSON:steps 数组,每步含 name(与可选 id)、depends_on、可选 params;可含 status=open|closed。回头修改用 mode=revise 与 revises。</p>
|
||||
</div>`;
|
||||
}
|
||||
if (!flowView.steps?.length) return "";
|
||||
@@ -2943,7 +3236,9 @@ function renderCreationFlowView(flowView) {
|
||||
? s.depends_on.map((d) => esc(d)).join("、")
|
||||
: "无";
|
||||
const occ =
|
||||
s.occurrence && s.occurrence > 1
|
||||
s.mode === "revise"
|
||||
? `<span class="flow-occ flow-occ-revise">回头修改</span>`
|
||||
: s.occurrence && s.occurrence > 1
|
||||
? `<span class="flow-occ">第 ${esc(String(s.occurrence))} 次</span>`
|
||||
: s.repeatable
|
||||
? `<span class="flow-occ">可反复</span>`
|
||||
@@ -3068,12 +3363,16 @@ function renderReviewWorkspace(review, opts = {}) {
|
||||
asPresentFallback:
|
||||
review.workerId === "narrator" ||
|
||||
review.workerId === "opening-generator" ||
|
||||
(Array.isArray(review.outputTags) &&
|
||||
review.outputTags.includes("设计.开场白与开场变量")) ||
|
||||
/用户展示|开场白/.test(review.body || ""),
|
||||
});
|
||||
const contextId = review.sourceMessageId || review.id || "";
|
||||
const hasContext = review.contextTrace ? "1" : "0";
|
||||
const label = displayWorkerLabel(review.workerId) || "产物";
|
||||
const copy = reviewComposerCopy(review.workerId);
|
||||
const copy = reviewComposerCopy(review.workerId, {
|
||||
outputTags: review.outputTags,
|
||||
});
|
||||
const sourceMsg =
|
||||
opts.sourceMessage ||
|
||||
(review.sourceMessageId
|
||||
@@ -3211,25 +3510,65 @@ export function renderAgentPanel(view, loading) {
|
||||
timelineEl.scrollTop = timelineEl.scrollHeight;
|
||||
}
|
||||
|
||||
function coordOriginalText(msg) {
|
||||
return String(msg?.body ?? msg?.text ?? "").trim();
|
||||
}
|
||||
|
||||
/** 一轮 = 一次用户输入 + 直到下一次输入前的全部输出。 */
|
||||
function groupCoordTurnPacks(messages) {
|
||||
const packs = [];
|
||||
for (const msg of messages) {
|
||||
const isUser = msg.role === "user";
|
||||
const last = packs[packs.length - 1];
|
||||
if (isUser || !last) {
|
||||
packs.push({ user: isUser ? msg : null, outputs: isUser ? [] : [msg] });
|
||||
} else {
|
||||
last.outputs.push(msg);
|
||||
}
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
|
||||
function packAnchorMessage(pack) {
|
||||
return pack.user || pack.outputs[0] || null;
|
||||
}
|
||||
|
||||
function packLastMessage(pack) {
|
||||
return pack.outputs[pack.outputs.length - 1] || pack.user || null;
|
||||
}
|
||||
|
||||
function renderCoordLine(msg, view, opts = {}) {
|
||||
const isUser = msg.role === "user";
|
||||
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
|
||||
const who = isUser ? "你" : msgLabel(msg);
|
||||
const full = coordOriginalText(msg) || msgBody(msg, view);
|
||||
const raw = msgBody(msg, view).replace(/\s+/g, " ").trim();
|
||||
const text = raw.length > 320 ? `${raw.slice(0, 320)}…` : raw;
|
||||
const nav = renderVariantNavHtml(msg);
|
||||
const inPack = opts.inPack === true;
|
||||
const allMessages = view.messages ?? [];
|
||||
const messageIndex = allMessages.findIndex((item) => item.id === msg.id);
|
||||
const rollbackMessageId = messageIndex >= 0 ? allMessages[messageIndex + 1]?.id : null;
|
||||
const rollbackMessageId = inPack ? null : getRollbackMessageId(allMessages, msg.id);
|
||||
const rollbackAttr = rollbackMessageId
|
||||
? ` data-rollback-message-id="${esc(rollbackMessageId)}"`
|
||||
: "";
|
||||
const contextAttr = msg.contextTrace ? ` data-has-context="1"` : "";
|
||||
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
|
||||
const originalAttr = full ? ` data-original-text="${escAttr(full)}"` : "";
|
||||
const menu = inPack
|
||||
? ""
|
||||
: `<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>`;
|
||||
const trace = msg.contextTrace;
|
||||
const variantTotal = Math.max(1, Number(msg.branchTotal) || 1);
|
||||
const variantIndex = Math.min(variantTotal, Math.max(1, (msg.branchIndex ?? 0) + 1));
|
||||
const requestBits = [
|
||||
opts.requestIndex ? `请求 ${opts.requestIndex}` : "",
|
||||
variantTotal > 1 ? `${variantIndex}/${variantTotal} 版` : "",
|
||||
].filter(Boolean);
|
||||
const requestHint = requestBits.length ? requestBits.join(" · ") : "本次请求";
|
||||
const traceMeta = trace
|
||||
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的完整请求上下文">
|
||||
? `<button type="button" class="coord-context-trigger" data-context-trigger aria-label="查看 ${esc(who)} 的${esc(requestHint)}上下文">
|
||||
<span class="coord-context-label">保留上下文</span>
|
||||
<span class="coord-context-request">${esc(requestHint)}</span>
|
||||
<span>${trace.messages?.length ?? 0} 段</span>
|
||||
<span>${((trace.charCount ?? 0) / 1024).toFixed(1)} KB</span>
|
||||
</button>
|
||||
@@ -3240,14 +3579,73 @@ function renderCoordLine(msg, view, opts = {}) {
|
||||
</div>`
|
||||
: "";
|
||||
const traceClass = trace ? " coord-line--trace" : "";
|
||||
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}>
|
||||
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这条消息的操作菜单" title="更多操作">⋯</button>
|
||||
return `<div class="coord-line ${isUser ? "coord-user" : "coord-agent"}${traceClass}" data-message-id="${esc(msg.id)}" data-kind="${esc(kind)}"${rollbackAttr}${contextAttr}${readOnlyAttr}${originalAttr}>
|
||||
${menu}
|
||||
<div class="coord-who"><span>${esc(who)}</span><span class="coord-kind">${esc(kind)}</span><span class="coord-time">${fmtTime(msg.createdAt)}</span>${nav}</div>
|
||||
<div class="coord-text">${esc(text || "(空)")}</div>
|
||||
${traceMeta}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderCoordPack(pack, view, opts = {}) {
|
||||
const anchor = packAnchorMessage(pack);
|
||||
if (!anchor) return "";
|
||||
const last = packLastMessage(pack);
|
||||
const pending = Boolean(pack.user) && pack.outputs.length === 0;
|
||||
const userText = pack.user ? coordOriginalText(pack.user) : "";
|
||||
const allMessages = view.messages ?? [];
|
||||
let rollbackMode = "";
|
||||
let rollbackId = "";
|
||||
if (pack.user) {
|
||||
rollbackMode = "pending-input";
|
||||
rollbackId = pack.user.id;
|
||||
} else if (last) {
|
||||
const nextId = getRollbackMessageId(allMessages, last.id);
|
||||
if (nextId) {
|
||||
rollbackMode = "keep";
|
||||
rollbackId = nextId;
|
||||
}
|
||||
}
|
||||
const inCount = pack.user ? 1 : 0;
|
||||
const outCount = pack.outputs.length;
|
||||
const meta = pending ? "待输入" : `${inCount} 入 · ${outCount} 出`;
|
||||
const rollbackAttr = rollbackId
|
||||
? ` data-rollback-message-id="${esc(rollbackId)}" data-rollback-mode="${esc(rollbackMode)}"`
|
||||
: "";
|
||||
const restoreAttr =
|
||||
rollbackMode === "pending-input" && userText
|
||||
? ` data-restore-draft="${escAttr(userText)}"`
|
||||
: "";
|
||||
const originalAttr = userText ? ` data-original-text="${escAttr(userText)}"` : "";
|
||||
const readOnlyAttr = opts.traceOnly ? ` data-read-only="1"` : "";
|
||||
const tracedOutputCount = pack.outputs.filter((msg) => msg.contextTrace).length;
|
||||
let requestIndex = 0;
|
||||
const lines = [
|
||||
pack.user
|
||||
? renderCoordLine(pack.user, view, {
|
||||
inPack: true,
|
||||
traceOnly: opts.traceOnly || opts.hiddenIds?.has(pack.user.id),
|
||||
})
|
||||
: "",
|
||||
...pack.outputs.map((msg) => {
|
||||
const thisRequest =
|
||||
tracedOutputCount > 1 && msg.contextTrace ? ++requestIndex : 0;
|
||||
return renderCoordLine(msg, view, {
|
||||
inPack: true,
|
||||
traceOnly: opts.traceOnly || opts.hiddenIds?.has(msg.id),
|
||||
requestIndex: thisRequest || undefined,
|
||||
});
|
||||
}),
|
||||
].join("");
|
||||
return `<article class="coord-pack${pending ? " coord-pack--pending" : ""}" data-message-id="${esc(anchor.id)}"${rollbackAttr}${restoreAttr}${originalAttr}${readOnlyAttr}>
|
||||
<button type="button" class="coord-line-menu" data-msg-menu-trigger aria-label="打开这一轮的操作菜单" title="更多操作">⋯</button>
|
||||
<header class="coord-pack-head">
|
||||
<span class="coord-pack-meta">${esc(meta)}</span>
|
||||
</header>
|
||||
${lines}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function getRollbackMessageId(messages, messageId) {
|
||||
const messageIndex = messages.findIndex((item) => item.id === messageId);
|
||||
return messageIndex >= 0 ? messages[messageIndex + 1]?.id ?? null : null;
|
||||
@@ -3255,7 +3653,7 @@ function getRollbackMessageId(messages, messageId) {
|
||||
|
||||
/** @returns {"speak"|"answer"|"review"|"busy"|null} */
|
||||
function resolveDesignSurface(view, loading) {
|
||||
if (view.lifecycleStage === "play") return null;
|
||||
if (isPlayView(view)) return null;
|
||||
if (view.phase === "done") return null;
|
||||
const busy = Boolean(loading || (view.phase === "running" && !view.waitingReason));
|
||||
if (busy) return "busy";
|
||||
@@ -3300,6 +3698,12 @@ function proposedOutputCopy(proposed) {
|
||||
typeof target === "string" || typeof target === "number"
|
||||
? String(target).trim()
|
||||
: "";
|
||||
if (proposed?.mode === "revise") {
|
||||
if (targetText) {
|
||||
return `将在既有「${targetText}」的${proposed.name}上继续改,不是从零再生成。`;
|
||||
}
|
||||
return `将在既有「${proposed?.name || "该步"}」上继续改,不是从零再生成。`;
|
||||
}
|
||||
if (targetText) {
|
||||
return `将生成「${targetText}」的${proposed.name}。`;
|
||||
}
|
||||
@@ -3347,17 +3751,24 @@ function renderSpeakWorkspace(view) {
|
||||
hint = "写清楚要改哪里;发送后在现有产物上修改。";
|
||||
} else if (wr?.kind === "next_intent") {
|
||||
title = "下一步想写什么";
|
||||
hint = "说说接下来想做什么;可留空,发送后会展示下一节点供确认。";
|
||||
hint = "说说接下来想做什么;可留空,也可说回头改某步。发送后会展示下一节点供确认。";
|
||||
} else if (wr?.kind === "approve_step") {
|
||||
title = view.proposedNextStep?.name
|
||||
? `接下来生成 · ${view.proposedNextStep.name}`
|
||||
? view.proposedNextStep.mode === "revise"
|
||||
? `回头修改 · ${view.proposedNextStep.name}`
|
||||
: `接下来生成 · ${view.proposedNextStep.name}`
|
||||
: "确认下一步";
|
||||
hint = view.proposedNextStep
|
||||
? proposedOutputCopy(view.proposedNextStep)
|
||||
: view.focus?.detail || "确认执行,或在底栏说明意见。";
|
||||
} else if (wr?.kind === "input") {
|
||||
title = "继续说";
|
||||
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
|
||||
if (wr.message && /创作已收口/.test(wr.message)) {
|
||||
title = "创作已收口";
|
||||
hint = wr.message;
|
||||
} else {
|
||||
title = "继续说";
|
||||
hint = view.hints?.[0] || wr.message || "直接输入你的想法或补充。";
|
||||
}
|
||||
} else if (view.uiPrompt && !hasUserMessages(view)) {
|
||||
title = "描述你想创作什么";
|
||||
hint = String(view.uiPrompt).trim().slice(0, 280);
|
||||
@@ -3400,19 +3811,25 @@ function fillCoordRail(visible, view, handlers) {
|
||||
return visibleIds.has(message.id) || Boolean(message.contextTrace);
|
||||
});
|
||||
const traceCount = history.filter((message) => message.contextTrace).length;
|
||||
const packs = groupCoordTurnPacks(history);
|
||||
const hiddenIds = new Set(
|
||||
history.filter((message) => !visibleIds.has(message.id)).map((message) => message.id),
|
||||
);
|
||||
coordRail.hidden = false;
|
||||
if (drawerBody) {
|
||||
drawerBody.innerHTML = history.length
|
||||
? history
|
||||
.map((m) => renderCoordLine(m, view, { traceOnly: !visibleIds.has(m.id) }))
|
||||
.join("")
|
||||
drawerBody.innerHTML = packs.length
|
||||
? packs.map((pack) => renderCoordPack(pack, view, { hiddenIds })).join("")
|
||||
: `<p class="coord-empty">暂无对话摘要</p>`;
|
||||
wireMessageFeedActions(drawerBody, handlers);
|
||||
wireMessageContextMenu(drawerBody, handlers);
|
||||
}
|
||||
if (drawerCount) {
|
||||
drawerCount.textContent = history.length ? String(history.length) : "";
|
||||
drawerCount.title = traceCount ? `${traceCount} 条保留了完整请求上下文` : "";
|
||||
drawerCount.textContent = packs.length ? String(packs.length) : "";
|
||||
drawerCount.title = traceCount
|
||||
? `${packs.length} 轮,其中 ${traceCount} 条保留了完整请求上下文`
|
||||
: packs.length
|
||||
? `${packs.length} 轮对话`
|
||||
: "";
|
||||
}
|
||||
syncCoordRailChrome();
|
||||
}
|
||||
@@ -3444,6 +3861,7 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
|
||||
}) + askSlot;
|
||||
wireMessageFeedActions(stage, handlers);
|
||||
wireMessageContextMenu(stage, handlers);
|
||||
wireOpeningPicker();
|
||||
if (opts.mountAskCard) {
|
||||
const askHost = document.getElementById("workspace-review-ask");
|
||||
if (askHost) ensureQuestionsHostIn(askHost);
|
||||
@@ -3466,13 +3884,16 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
const surface = resolveDesignSurface(view, loading);
|
||||
const designWorkspace = surface != null;
|
||||
const reviewingNow =
|
||||
view.waitingReason?.kind === "review_artifact" && Boolean(view.reviewArtifact);
|
||||
!isPlayView(view) &&
|
||||
view.waitingReason?.kind === "review_artifact" &&
|
||||
Boolean(view.reviewArtifact);
|
||||
|
||||
document.body.classList.toggle("is-reviewing", reviewingNow);
|
||||
document.body.classList.toggle("design-workspace", designWorkspace);
|
||||
if (reviewingNow) {
|
||||
document.body.dataset.reviewKind = reviewComposerCopy(
|
||||
view.reviewArtifact?.workerId,
|
||||
{ outputTags: view.reviewArtifact?.outputTags },
|
||||
).kind;
|
||||
} else {
|
||||
delete document.body.dataset.reviewKind;
|
||||
@@ -3529,9 +3950,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
<header class="workspace-pending-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>`;
|
||||
wireRetryRun(stage, handlers);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3565,8 +3988,8 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
if (!visible.length && !loading) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "empty";
|
||||
if (view.lifecycleStage === "play") {
|
||||
p.textContent = "游玩模式:Agent 将按 Worker 集调度,推进世界与叙事。";
|
||||
if (isPlayView(view)) {
|
||||
p.textContent = "开场已就绪。在下方说你要做什么。";
|
||||
} else if (view.uiPrompt) {
|
||||
const recipeLine = view.selectedRecipe?.name
|
||||
? `\n\n已选配方:${view.selectedRecipe.name}`
|
||||
@@ -3621,31 +4044,48 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
|
||||
const questionsActive = kind === "worker_questions" && Boolean(activeQuestions);
|
||||
const showThinking =
|
||||
!isUser && msg.thinking && view.lifecycleStage === "play";
|
||||
const bodyHtml = questionsActive
|
||||
!isUser && msg.thinking && isPlayView(view);
|
||||
const innerBody = questionsActive
|
||||
? `<p class="msg-q-index">${esc(body)}</p>`
|
||||
: kind === "worker_questions"
|
||||
? formatQuestionsHtml(body)
|
||||
: kind === "worker_output"
|
||||
? formatArtifactBodyHtml(body)
|
||||
: shouldRenderPlayPresent(msg, view)
|
||||
? formatPlayPresentHtml(body, view)
|
||||
: shouldRenderPlayPresent(msg, view)
|
||||
? formatPlayPresentHtml(body, view)
|
||||
: kind === "worker_output"
|
||||
? formatArtifactBodyHtml(body)
|
||||
: esc(body);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
|
||||
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
|
||||
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
||||
<div class="msg-body">${bodyHtml}</div>
|
||||
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
|
||||
</div>`;
|
||||
const process = !isUser && isPlayProcessMessage(msg, view);
|
||||
const playFinal = !isUser && isPlayFinalReply(msg, view);
|
||||
if (process) {
|
||||
card.classList.add("msg-play-process-row");
|
||||
card.innerHTML = `
|
||||
<details class="msg-play-process">
|
||||
<summary>
|
||||
<span class="msg-tag">${esc(label)}</span>
|
||||
${showSubtitle ? `<span class="msg-subtitle">${esc(subtitle)}</span>` : ""}
|
||||
<span class="msg-time">${fmtTime(msg.createdAt)}</span>
|
||||
</summary>
|
||||
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
||||
<div class="msg-body">${innerBody}</div>
|
||||
</details>`;
|
||||
} else {
|
||||
if (playFinal) card.classList.add("msg-play-final");
|
||||
card.innerHTML = `
|
||||
<div class="msg-bubble${questionsActive ? " msg-bubble-index" : ""}">
|
||||
${isUser ? "" : `<header class="msg-head">${headInner}${menuTrigger}</header>`}
|
||||
${showThinking ? renderThinkingBlock(msg.thinking) : ""}
|
||||
<div class="msg-body">${innerBody}</div>
|
||||
${isUser ? `<footer class="msg-foot">${headInner}${menuTrigger}</footer>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
feed.appendChild(card);
|
||||
}
|
||||
|
||||
wireMessageFeedActions(feed, handlers);
|
||||
wireMessageContextMenu(feed, handlers);
|
||||
|
||||
if (reviewingNow && view.reviewArtifact && !loading && stage) {
|
||||
if (reviewingNow && view.reviewArtifact && !loading && stage && !isPlayView(view)) {
|
||||
const hasHungAsk = Boolean(activeQuestions?.questions?.length);
|
||||
mountReviewWorkspace(stage, view, handlers, {
|
||||
hideAskSidecar: hasHungAsk,
|
||||
@@ -3661,10 +4101,11 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
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></header>
|
||||
<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;
|
||||
@@ -3682,7 +4123,7 @@ export function updateLiveStreamPanel(view) {
|
||||
if (labelEl && live?.label) labelEl.textContent = live.label;
|
||||
const next = renderLiveStreamBody(live, view);
|
||||
const hadThinking = Boolean(body.querySelector(".msg-live-pre"));
|
||||
if (next === "…" && hadThinking) return;
|
||||
if (next.includes("msg-live-empty") && hadThinking) return;
|
||||
body.innerHTML = next;
|
||||
const pre = body.querySelector(".msg-live-pre:last-of-type");
|
||||
if (pre) pre.scrollTop = pre.scrollHeight;
|
||||
|
||||
+464
-275
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,10 @@ export function formatWorkerDisplayTitle(workerId, action = null) {
|
||||
export function reviewComposerCopy(workerId, opts = {}) {
|
||||
const hasQuestions = Boolean(opts.hasQuestions);
|
||||
const id = String(workerId ?? "").trim();
|
||||
const openingReview =
|
||||
id === "opening-generator" ||
|
||||
(Array.isArray(opts.outputTags) &&
|
||||
opts.outputTags.includes("设计.开场白与开场变量"));
|
||||
|
||||
if (id === "design-flow") {
|
||||
return {
|
||||
@@ -120,7 +124,7 @@ export function reviewComposerCopy(workerId, opts = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "opening-generator") {
|
||||
if (id === "opening-generator" || openingReview) {
|
||||
return {
|
||||
chip: "开场",
|
||||
kicker: "待选定",
|
||||
|
||||
+22
-32
@@ -35,7 +35,6 @@
|
||||
|
||||
<div class="rail-panels">
|
||||
<div class="rail-tab-panel active" id="rail-panel-books" role="tabpanel" data-rail-panel="books">
|
||||
<nav class="path-bar" id="book-path-bar" aria-label="面包屑" hidden></nav>
|
||||
<div class="book-list" id="book-list"></div>
|
||||
<div id="book-box-overlay" class="book-box-overlay" hidden aria-hidden="true"></div>
|
||||
</div>
|
||||
@@ -48,39 +47,11 @@
|
||||
</aside>
|
||||
|
||||
<main class="panel panel-main">
|
||||
<header class="main-head" id="main-head">
|
||||
<div class="main-head-left">
|
||||
<h1 class="work-title" id="work-title">未打开作品</h1>
|
||||
<span class="work-meta" id="work-meta"></span>
|
||||
</div>
|
||||
<div class="main-head-center">
|
||||
<div class="lifecycle-toggle" id="lifecycle-toggle" role="group" aria-label="创作 / 游玩">
|
||||
<button type="button" class="lifecycle-btn active" data-stage="design">创作</button>
|
||||
<button type="button" class="lifecycle-btn" data-stage="play" disabled>游玩</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-head-right">
|
||||
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>
|
||||
<button type="button" class="btn-sm" id="btn-save-play" hidden>保存游玩</button>
|
||||
<button type="button" class="btn-sm" id="btn-export" disabled>导出</button>
|
||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
||||
<details class="more-menu">
|
||||
<summary class="btn-sm more-menu-sum" title="更多页面">更多</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
|
||||
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
|
||||
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
|
||||
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-body">
|
||||
<section class="panel-feed" id="panel-feed">
|
||||
<div id="skill-picker" class="skill-picker" hidden></div>
|
||||
<div id="message-feed" class="message-feed">
|
||||
<p class="empty">点击左侧 + 新建作品</p>
|
||||
<p class="empty">正在打开作品…</p>
|
||||
</div>
|
||||
<div id="workspace-stage" class="workspace-stage" hidden aria-label="验收工作面"></div>
|
||||
<div id="questions-card-host" class="questions-card-host" hidden></div>
|
||||
@@ -106,7 +77,22 @@
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<footer class="composer" id="composer"></footer>
|
||||
<footer class="composer" id="composer">
|
||||
<span class="status" id="status-pill"><span class="status-dot" id="status-dot"></span><span id="status-text">—</span></span>
|
||||
<div id="composer-main"></div>
|
||||
<details class="more-menu">
|
||||
<summary class="btn-sm more-menu-sum" title="更多">更多</summary>
|
||||
<div class="more-menu-panel" role="menu">
|
||||
<button type="button" class="more-menu-item" id="btn-save-instance" hidden role="menuitem">保存定稿</button>
|
||||
<button type="button" class="more-menu-item" id="btn-save-play" hidden role="menuitem">保存游玩</button>
|
||||
<button type="button" class="more-menu-item" id="btn-export" disabled role="menuitem">导出</button>
|
||||
<a class="more-menu-item" href="/modules.html" role="menuitem">技能</a>
|
||||
<a class="more-menu-item" href="/shells.html" role="menuitem">呈现壳</a>
|
||||
<a class="more-menu-item" href="/stats.html" role="menuitem">Token</a>
|
||||
<a class="more-menu-item" href="/settings.html" role="menuitem">设置</a>
|
||||
</div>
|
||||
</details>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -133,7 +119,11 @@
|
||||
</dialog>
|
||||
|
||||
<menu id="book-action-menu" class="context-menu" hidden>
|
||||
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-open" data-action="open">打开创作</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-play" data-action="play">进入游玩</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-save-instance" data-action="save-instance">保存定稿</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-save-play" data-action="save-play">保存游玩</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-export" data-action="export">导出</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-rename" data-action="rename">重命名</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-duplicate" data-action="duplicate">复制备份</button>
|
||||
<button type="button" class="context-menu-item" id="book-menu-select" data-action="select-mode">多选</button>
|
||||
|
||||
+16
-1
@@ -82,6 +82,20 @@ function formatBlockContent(value) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
|
||||
export function stripPresentSourceFences(raw) {
|
||||
let s = String(raw ?? "").trim();
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const next = s
|
||||
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
|
||||
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
|
||||
.trim();
|
||||
if (next === s) break;
|
||||
s = next;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function normalizeBlocks(raw) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
||||
const out = {};
|
||||
@@ -90,6 +104,7 @@ function normalizeBlocks(raw) {
|
||||
}
|
||||
if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文;
|
||||
if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控;
|
||||
if (typeof out.body === "string") out.body = stripPresentSourceFences(out.body);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -162,7 +177,7 @@ export function presentFromPlain(text, shell = "prose") {
|
||||
return {
|
||||
schema: "present.v1",
|
||||
shell: isPresentShellId(shell) ? shell : "prose",
|
||||
blocks: { body: text ?? "" },
|
||||
blocks: { body: stripPresentSourceFences(text ?? "") },
|
||||
meta: { suggested_actions: [] },
|
||||
};
|
||||
}
|
||||
|
||||
+20
-34
@@ -4,6 +4,7 @@
|
||||
* 点发送即答复 → 收起本轮卡(卡上无独立发送钮)。
|
||||
* 产物验收挂载题:选项 + 底栏意见一并随「按意见修改」写回产物;空内容点该钮不等于接受。
|
||||
* 分页 / 跳过为悬浮控件,不占独立 header/footer 行。
|
||||
* 点顶栏横条折叠成一行,再点展开。
|
||||
*
|
||||
* 能力「默认问题」(id=module-opening) 不是追问:对齐美学纲领开局,走说话面 + openingGuide。
|
||||
*/
|
||||
@@ -259,7 +260,7 @@ export function clearQuestionCardState(host, view) {
|
||||
host._qState = null;
|
||||
host.hidden = true;
|
||||
host.innerHTML = "";
|
||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
||||
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||
host.removeAttribute("role");
|
||||
host.removeAttribute("aria-label");
|
||||
}
|
||||
@@ -275,7 +276,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
if (!active?.questions?.length) {
|
||||
host.hidden = true;
|
||||
host.innerHTML = "";
|
||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
||||
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||
host.removeAttribute("role");
|
||||
host.removeAttribute("aria-label");
|
||||
host._qDismissed = null;
|
||||
@@ -290,7 +291,7 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
) {
|
||||
host.hidden = true;
|
||||
host.innerHTML = "";
|
||||
host.classList.remove("is-open", "is-expanded", "has-pager", "has-skip");
|
||||
host.classList.remove("is-open", "is-expanded", "is-collapsed", "has-pager", "has-skip");
|
||||
host.removeAttribute("role");
|
||||
host.removeAttribute("aria-label");
|
||||
return false;
|
||||
@@ -305,14 +306,14 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
sessionId: view.id,
|
||||
key: qKey,
|
||||
page: 0,
|
||||
expanded: false,
|
||||
collapsed: false,
|
||||
// questionId -> { optionId?, text, otherText? }
|
||||
answers: {},
|
||||
drafts: {}, // option key -> edited label
|
||||
};
|
||||
if (typeof state.expanded !== "boolean") state.expanded = false;
|
||||
if (typeof state.collapsed !== "boolean") state.collapsed = false;
|
||||
host._qState = state;
|
||||
host.classList.toggle("is-expanded", state.expanded);
|
||||
host.classList.toggle("is-collapsed", state.collapsed);
|
||||
|
||||
const { questions, pageSize, assessment, optional, skipLabel, dismissOnAccept } =
|
||||
active;
|
||||
@@ -362,8 +363,13 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
${pagerHtml}
|
||||
${skipHtml}
|
||||
</div>
|
||||
<button type="button" class="qcard-top" data-qtop title="${state.expanded ? "点击收起" : "点击展开"}" aria-label="${state.expanded ? "收起询问卡" : "展开询问卡"}">
|
||||
<button type="button" class="qcard-top" data-qtop title="${state.collapsed ? "展开追问" : "折叠追问"}" aria-expanded="${state.collapsed ? "false" : "true"}" aria-label="${state.collapsed ? "展开追问" : "折叠追问"}">
|
||||
<span class="qcard-top-grip" aria-hidden="true"></span>
|
||||
<span class="qcard-collapsed-meta">${esc(
|
||||
pageCount > 1
|
||||
? `追问 ${state.page + 1}/${pageCount} · ${pageQs[0]?.prompt ?? "询问"}`
|
||||
: `追问 · ${pageQs[0]?.prompt ?? "询问"}`,
|
||||
)}</span>
|
||||
</button>
|
||||
<div class="qcard-body">
|
||||
${dismissNote}
|
||||
@@ -414,40 +420,21 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
.join("")}
|
||||
</div>`;
|
||||
|
||||
const setExpanded = (next) => {
|
||||
state.expanded = next;
|
||||
host.classList.toggle("is-expanded", next);
|
||||
const setCollapsed = (next) => {
|
||||
state.collapsed = next;
|
||||
host.classList.toggle("is-collapsed", next);
|
||||
const top = host.querySelector("[data-qtop]");
|
||||
if (top) {
|
||||
top.title = next ? "点击收起" : "点击展开";
|
||||
top.setAttribute("aria-label", next ? "收起询问卡" : "展开询问卡");
|
||||
top.title = next ? "展开追问" : "折叠追问";
|
||||
top.setAttribute("aria-expanded", next ? "false" : "true");
|
||||
top.setAttribute("aria-label", next ? "展开追问" : "折叠追问");
|
||||
}
|
||||
};
|
||||
|
||||
host.querySelector("[data-qtop]")?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setExpanded(!state.expanded);
|
||||
});
|
||||
|
||||
host.querySelector(".qcard-body")?.addEventListener("click", (e) => {
|
||||
const t = e.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
if (
|
||||
t.closest(
|
||||
".qcard-letter, .qcard-other-input, [contenteditable=true], .qcard-nav, .qcard-skip, .qcard-pager",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 展开后点题干 / 评估区收起;收起时点内容区展开
|
||||
if (state.expanded) {
|
||||
if (t.closest(".qcard-prompt, .qcard-assessment, .qcard-optional-note")) {
|
||||
setExpanded(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setExpanded(true);
|
||||
setCollapsed(!state.collapsed);
|
||||
});
|
||||
|
||||
host.querySelector("[data-qskip]")?.addEventListener("click", () => {
|
||||
@@ -488,7 +475,6 @@ export function renderQuestionsCard(host, view, _handlers = {}) {
|
||||
section.querySelectorAll(".qcard-opt").forEach((li) => {
|
||||
li.classList.toggle("is-selected", li.getAttribute("data-oid") === oid);
|
||||
});
|
||||
if (!state.expanded) setExpanded(true);
|
||||
_handlers.onAnswersChange?.();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -800,6 +800,226 @@ code {
|
||||
background: rgba(24, 24, 27, 0.4);
|
||||
}
|
||||
|
||||
/* —— Preset probe —— */
|
||||
.probe-dialog {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
background: var(--st-surface);
|
||||
color: var(--st-text);
|
||||
width: min(1080px, calc(100% - 24px));
|
||||
height: min(780px, calc(100% - 24px));
|
||||
max-width: none;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.probe-dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.probe-dialog::backdrop {
|
||||
background: rgba(24, 24, 27, 0.45);
|
||||
}
|
||||
|
||||
.probe-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 18px 12px;
|
||||
border-bottom: 1px solid var(--st-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.probe-head h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.probe-sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--st-muted);
|
||||
}
|
||||
|
||||
.probe-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.probe-composer,
|
||||
.probe-inspect {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px 18px 20px;
|
||||
}
|
||||
|
||||
.probe-composer {
|
||||
border-right: 1px solid var(--st-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.probe-composer label,
|
||||
.probe-extras label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--st-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.probe-composer textarea,
|
||||
.probe-extras textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
padding: 9px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--st-border-strong);
|
||||
background: var(--st-bg);
|
||||
color: var(--st-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.probe-composer textarea:focus,
|
||||
.probe-extras textarea:focus {
|
||||
outline: none;
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 0 0 3px var(--st-accent-soft);
|
||||
background: var(--st-surface);
|
||||
}
|
||||
|
||||
.probe-extras {
|
||||
border: 1px solid var(--st-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px 10px;
|
||||
background: var(--st-bg);
|
||||
}
|
||||
|
||||
.probe-extras summary {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--st-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.probe-extras[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.probe-composer-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.probe-status {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--st-muted);
|
||||
}
|
||||
|
||||
.probe-inspect h4 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
color: var(--st-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.probe-empty {
|
||||
margin: 8px 0;
|
||||
color: var(--st-faint);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.probe-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.probe-msg {
|
||||
border: 1px solid var(--st-border);
|
||||
border-radius: 8px;
|
||||
background: var(--st-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.probe-msg header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--st-border);
|
||||
background: var(--st-surface);
|
||||
}
|
||||
|
||||
.probe-role {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--st-accent);
|
||||
}
|
||||
|
||||
.probe-role.user {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.probe-role.assistant {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.probe-index {
|
||||
font-size: 11px;
|
||||
color: var(--st-faint);
|
||||
}
|
||||
|
||||
.probe-msg pre,
|
||||
.probe-reply pre {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, "Cascadia Code", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.probe-reply {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.probe-dialog {
|
||||
height: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.probe-body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.probe-composer {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--st-border);
|
||||
max-height: 42vh;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-dialog form {
|
||||
padding: 22px;
|
||||
display: flex;
|
||||
|
||||
@@ -78,6 +78,57 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<dialog id="preset-probe-dialog" class="probe-dialog">
|
||||
<header class="probe-head">
|
||||
<div>
|
||||
<h3>试跑预设</h3>
|
||||
<p class="probe-sub" id="probe-preset-name"></p>
|
||||
</div>
|
||||
<button type="button" class="btn-secondary" id="probe-close">关闭</button>
|
||||
</header>
|
||||
<div class="probe-body">
|
||||
<section class="probe-composer">
|
||||
<label>
|
||||
本轮输入
|
||||
<textarea id="probe-message" rows="4" placeholder="发给模型的这一轮"></textarea>
|
||||
</label>
|
||||
<details class="probe-extras">
|
||||
<summary>试跑用设定 / 历史</summary>
|
||||
<label>
|
||||
历史前(设定)
|
||||
<textarea id="probe-lore-before" rows="3"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
对话历史
|
||||
<textarea id="probe-history" rows="4"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
历史后(状态)
|
||||
<textarea id="probe-lore-after" rows="3"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
本轮后(可选)
|
||||
<textarea id="probe-post-turn" rows="2"></textarea>
|
||||
</label>
|
||||
</details>
|
||||
<div class="probe-composer-actions">
|
||||
<button type="button" class="btn-primary" id="probe-send">发送一轮</button>
|
||||
<p class="probe-status" id="probe-status" hidden></p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="probe-inspect" aria-live="polite">
|
||||
<h4>发出的上下文</h4>
|
||||
<div id="probe-messages" class="probe-messages">
|
||||
<p class="probe-empty">发送后显示实际拼进请求的消息。</p>
|
||||
</div>
|
||||
<div id="probe-reply-wrap" class="probe-reply" hidden>
|
||||
<h4>模型回复</h4>
|
||||
<pre id="probe-reply"></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="profile-dialog" class="profile-dialog">
|
||||
<form id="profile-form" method="dialog">
|
||||
<h3 id="profile-dialog-title">新增 API</h3>
|
||||
|
||||
+140
@@ -28,6 +28,29 @@ const sectionStorageEl = document.getElementById("section-storage");
|
||||
const activeSettingsBarEl = document.getElementById("active-settings-bar");
|
||||
const settingsToastEl = document.getElementById("settings-toast");
|
||||
const contextTraceKeepEl = document.getElementById("context-trace-keep");
|
||||
const probeDialog = document.getElementById("preset-probe-dialog");
|
||||
const probePresetNameEl = document.getElementById("probe-preset-name");
|
||||
const probeMessageEl = document.getElementById("probe-message");
|
||||
const probeLoreBeforeEl = document.getElementById("probe-lore-before");
|
||||
const probeHistoryEl = document.getElementById("probe-history");
|
||||
const probeLoreAfterEl = document.getElementById("probe-lore-after");
|
||||
const probePostTurnEl = document.getElementById("probe-post-turn");
|
||||
const probeSendEl = document.getElementById("probe-send");
|
||||
const probeStatusEl = document.getElementById("probe-status");
|
||||
const probeMessagesEl = document.getElementById("probe-messages");
|
||||
const probeReplyWrapEl = document.getElementById("probe-reply-wrap");
|
||||
const probeReplyEl = document.getElementById("probe-reply");
|
||||
|
||||
const DEFAULT_PROBE_CONTEXT = {
|
||||
message: "我推开门。",
|
||||
loreBefore: "一座雨夜的港口旅馆。柜台点着油灯。",
|
||||
history: "店员:今晚只剩阁楼那间。\n你:好。",
|
||||
loreAfter: "时间:深夜。地点:旅馆大厅。",
|
||||
postTurn: "",
|
||||
};
|
||||
|
||||
let probingPresetId = null;
|
||||
let probeBusy = false;
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const res = await fetch(path, {
|
||||
@@ -192,6 +215,7 @@ function renderPresets() {
|
||||
</div>
|
||||
<div class="config-card-actions">
|
||||
${activateButtonHtml(active, p.id, "activate-preset")}
|
||||
<button type="button" data-action="probe-preset" data-id="${p.id}">试跑</button>
|
||||
<button type="button" data-action="toggle-preset-entries" data-id="${p.id}">
|
||||
${expanded ? "收起条目" : "编辑条目"}
|
||||
</button>
|
||||
@@ -476,6 +500,8 @@ presetsListEl.addEventListener("click", async (e) => {
|
||||
try {
|
||||
if (action === "activate-preset") {
|
||||
await activatePreset(id);
|
||||
} else if (action === "probe-preset") {
|
||||
openProbeDialog(id);
|
||||
} else if (action === "toggle-preset-entries") {
|
||||
await togglePresetEntries(id);
|
||||
} else if (action === "delete-preset") {
|
||||
@@ -573,6 +599,120 @@ presetFileEl.addEventListener("change", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
function resetProbeForm() {
|
||||
probeMessageEl.value = DEFAULT_PROBE_CONTEXT.message;
|
||||
probeLoreBeforeEl.value = DEFAULT_PROBE_CONTEXT.loreBefore;
|
||||
probeHistoryEl.value = DEFAULT_PROBE_CONTEXT.history;
|
||||
probeLoreAfterEl.value = DEFAULT_PROBE_CONTEXT.loreAfter;
|
||||
probePostTurnEl.value = DEFAULT_PROBE_CONTEXT.postTurn;
|
||||
probeStatusEl.hidden = true;
|
||||
probeStatusEl.textContent = "";
|
||||
probeReplyWrapEl.hidden = true;
|
||||
probeReplyEl.textContent = "";
|
||||
probeMessagesEl.innerHTML =
|
||||
'<p class="probe-empty">发送后显示实际拼进请求的消息。</p>';
|
||||
}
|
||||
|
||||
function openProbeDialog(presetId) {
|
||||
const preset = state.presets.find((p) => p.id === presetId);
|
||||
probingPresetId = presetId;
|
||||
probePresetNameEl.textContent = preset?.name ?? presetId;
|
||||
resetProbeForm();
|
||||
probeDialog.showModal();
|
||||
probeMessageEl.focus();
|
||||
probeMessageEl.select();
|
||||
}
|
||||
|
||||
function renderProbeMessages(messages) {
|
||||
if (!messages?.length) {
|
||||
probeMessagesEl.innerHTML =
|
||||
'<p class="probe-empty">这次没有拼出任何消息。</p>';
|
||||
return;
|
||||
}
|
||||
probeMessagesEl.innerHTML = messages
|
||||
.map(
|
||||
(m, i) => `
|
||||
<article class="probe-msg">
|
||||
<header>
|
||||
<span class="probe-role ${escapeHtml(m.role)}">${escapeHtml(m.role)}</span>
|
||||
<span class="probe-index">#${i + 1}</span>
|
||||
</header>
|
||||
<pre>${escapeHtml(m.content)}</pre>
|
||||
</article>`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function setProbeBusy(busy) {
|
||||
probeBusy = busy;
|
||||
probeSendEl.disabled = busy;
|
||||
probeSendEl.textContent = busy ? "发送中…" : "发送一轮";
|
||||
}
|
||||
|
||||
async function sendProbe() {
|
||||
if (probeBusy || !probingPresetId) return;
|
||||
const message = probeMessageEl.value.trim();
|
||||
if (!message) {
|
||||
probeStatusEl.hidden = false;
|
||||
probeStatusEl.textContent = "请填写本轮输入";
|
||||
probeMessageEl.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setProbeBusy(true);
|
||||
probeStatusEl.hidden = false;
|
||||
probeStatusEl.textContent = "正在拼装上下文…";
|
||||
probeReplyWrapEl.hidden = true;
|
||||
|
||||
try {
|
||||
const result = await api(`/api/presets/${probingPresetId}/probe`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
loreBefore: probeLoreBeforeEl.value,
|
||||
history: probeHistoryEl.value,
|
||||
loreAfter: probeLoreAfterEl.value,
|
||||
postTurn: probePostTurnEl.value,
|
||||
}),
|
||||
});
|
||||
renderProbeMessages(result.messages);
|
||||
if (result.reply) {
|
||||
probeReplyWrapEl.hidden = false;
|
||||
probeReplyEl.textContent = result.reasoning
|
||||
? `${result.reasoning}\n\n——\n\n${result.reply}`
|
||||
: result.reply;
|
||||
}
|
||||
if (result.error) {
|
||||
probeStatusEl.textContent = `上下文已拼装,生成失败:${result.error}`;
|
||||
} else if (result.skipReason) {
|
||||
probeStatusEl.textContent = result.skipReason;
|
||||
} else if (result.completed) {
|
||||
probeStatusEl.textContent = `已发送 ${result.messages.length} 条`;
|
||||
} else {
|
||||
probeStatusEl.textContent = `已拼装 ${result.messages.length} 条`;
|
||||
}
|
||||
} catch (err) {
|
||||
probeStatusEl.textContent = err.message;
|
||||
} finally {
|
||||
setProbeBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("probe-close")?.addEventListener("click", () => {
|
||||
probeDialog.close();
|
||||
});
|
||||
|
||||
probeSendEl?.addEventListener("click", () => {
|
||||
sendProbe().catch((err) => showToast(err.message, true));
|
||||
});
|
||||
|
||||
probeMessageEl?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
sendProbe().catch((err) => showToast(err.message, true));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btn-save-context-keep")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const n = Number(contextTraceKeepEl?.value ?? 5);
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
<p class="st-rail-hint">按「要同时看见什么」选,不是按好看选。</p>
|
||||
<nav class="shells-list" id="shell-list" role="tablist"></nav>
|
||||
<p class="st-rail-note">
|
||||
主任务永远是读正文(常 600~3000 字)。侧栏只换壳;长度/语气在预览顶栏。
|
||||
主任务永远是读正文(常 500~2000 字)。侧栏只换壳;长度/语气在预览顶栏。
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 呈现壳预览:主工作面 = 壳画布。
|
||||
* 按「要同时看见什么」选壳;正文按 600~3000 字撑高滚动。
|
||||
* 按「要同时看见什么」选壳;正文按 500~2000 字撑高滚动。
|
||||
*/
|
||||
import {
|
||||
PRESENT_SHELL_IDS,
|
||||
@@ -56,7 +56,7 @@ const TONES = [
|
||||
const LENGTHS = [
|
||||
{ id: "short", label: "短" },
|
||||
{ id: "mid", label: "中 ·800" },
|
||||
{ id: "long", label: "长 ·2500" },
|
||||
{ id: "long", label: "长 ·2000" },
|
||||
];
|
||||
|
||||
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
|
||||
|
||||
+331
-45
@@ -18,8 +18,8 @@
|
||||
--workspace-pad-x: clamp(24px, 4.5vw, 72px);
|
||||
--workspace-pad-y: clamp(20px, 3vh, 40px);
|
||||
--head-h: 48px;
|
||||
--composer-min: 56px;
|
||||
--composer-control-h: 40px;
|
||||
--composer-min: 0px;
|
||||
--composer-control-h: 36px;
|
||||
--font-ui: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", system-ui, sans-serif;
|
||||
--font-read: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", Georgia, serif;
|
||||
font-family: var(--font-ui);
|
||||
@@ -201,7 +201,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
|
||||
/* composer æå
容èªéåºï¼ä¸é´åºåæå©ä½é«åº¦ï¼æ´é¡µéå¨è§å£å
*/
|
||||
.panel-main {
|
||||
display: grid;
|
||||
grid-template-rows: var(--head-h) minmax(0, 1fr) auto;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -435,7 +435,7 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
.coord-empty {
|
||||
margin: 16px 8px;
|
||||
@@ -443,6 +443,55 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
.coord-pack {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px 4px 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg);
|
||||
min-width: 0;
|
||||
cursor: context-menu;
|
||||
}
|
||||
.coord-pack:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 28%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent-soft) 28%, var(--bg));
|
||||
}
|
||||
.coord-pack--pending {
|
||||
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
||||
}
|
||||
.coord-pack-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 2px 28px 2px 8px;
|
||||
}
|
||||
.coord-pack-meta {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.coord-pack--pending .coord-pack-meta {
|
||||
color: var(--accent);
|
||||
}
|
||||
.coord-pack > .coord-line-menu {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
.coord-pack:hover > .coord-line-menu,
|
||||
.coord-pack:focus-within > .coord-line-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
.coord-pack .coord-line {
|
||||
background: transparent;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.coord-pack .coord-line:hover {
|
||||
background: color-mix(in srgb, var(--accent-soft) 35%, transparent);
|
||||
}
|
||||
.coord-line {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -565,6 +614,10 @@ body:not(.coord-rail-collapsed) .coord-rail-toggle {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.coord-context-request {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.coord-trace-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -596,6 +649,14 @@ body.design-workspace .workspace-pending {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.workspace-pending-head .msg-action {
|
||||
margin-left: auto;
|
||||
}
|
||||
.msg-live-empty {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
body.is-reviewing .message-feed {
|
||||
flex: 0 1 auto;
|
||||
@@ -752,6 +813,33 @@ body.design-workspace[data-design-surface="answer"] .questions-card-host {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* 答题/验收工作面已是 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;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
body.design-workspace[data-design-surface="answer"] .qcard-body,
|
||||
.workspace-review-ask .qcard-body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
body.design-workspace[data-design-surface="answer"] .qcard-top,
|
||||
.workspace-review-ask .qcard-top,
|
||||
body.design-workspace[data-design-surface="answer"] .questions-card-host.is-collapsed .qcard-top,
|
||||
.workspace-review-ask .questions-card-host.is-collapsed .qcard-top {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
body.design-workspace[data-design-surface="answer"] .qcard-skip,
|
||||
.workspace-review-ask .qcard-skip,
|
||||
body.design-workspace[data-design-surface="answer"] .qcard-pager,
|
||||
.workspace-review-ask .qcard-pager {
|
||||
right: 0;
|
||||
}
|
||||
body.design-workspace[data-design-surface="speak"] .workspace-surface-kicker {
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -766,25 +854,23 @@ body.design-workspace[data-design-surface="review"][data-review-kind="opening"]
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* 挂载追问嵌在产物验收面下方,与产物同一工作面 */
|
||||
/* 挂载追问嵌在产物验收面下方:与产物同宽,随主柱动态适应 */
|
||||
.workspace-review-ask {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 0;
|
||||
min-height: 0;
|
||||
max-width: var(--workspace-max, 920px);
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
padding: 0 var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
|
||||
max-width: none;
|
||||
align-self: stretch;
|
||||
padding: 8px var(--workspace-pad-x, 24px) var(--workspace-pad-y, 16px);
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||
}
|
||||
.workspace-review-ask .questions-card-host {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border-top: none;
|
||||
}
|
||||
.workspace-review-ask .questions-card-host.is-open:not([hidden]) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
body.design-workspace .ws-creation-flow h4 { display: none; }
|
||||
body.design-workspace .flow-brief {
|
||||
@@ -1058,6 +1144,32 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
border-radius: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.explorer-row.has-twist {
|
||||
grid-template-columns: 16px minmax(0, 1fr) auto;
|
||||
}
|
||||
.explorer-twist {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: var(--muted);
|
||||
user-select: none;
|
||||
}
|
||||
.explorer-row.has-twist .explorer-main { grid-column: 2; }
|
||||
.explorer-row.has-twist .explorer-del { grid-column: 3; }
|
||||
.explorer-row.child {
|
||||
padding-left: 28px;
|
||||
min-height: 28px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.explorer-row.child .explorer-name { font-size: 12px; font-weight: 400; }
|
||||
.explorer-row:hover { background: var(--bg); }
|
||||
.explorer-row.active { background: var(--accent-soft); }
|
||||
.explorer-row.active .explorer-name { color: var(--accent); font-weight: 600; }
|
||||
@@ -1119,7 +1231,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
||||
color: var(--muted); letter-spacing: 0.04em;
|
||||
}
|
||||
.explorer-hint {
|
||||
margin: 4px 8px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
||||
margin: 4px 8px 4px 28px; font-size: 11px; color: var(--muted); line-height: 1.4;
|
||||
}
|
||||
.sidebar-empty { padding: 12px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||
|
||||
@@ -1214,6 +1326,9 @@ body.rail-collapsed .message-feed {
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.msg-head [data-act="retry_run"] {
|
||||
margin-left: auto;
|
||||
}
|
||||
.msg-foot {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -1339,6 +1454,41 @@ body.rail-collapsed .message-feed {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
.msg-play-process-row {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.msg-play-process {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.msg-play-process > summary {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
}
|
||||
.msg-play-process > summary::-webkit-details-marker { display: none; }
|
||||
.msg-play-process > summary::before {
|
||||
content: "▸";
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.msg-play-process[open] > summary::before { content: "▾"; }
|
||||
.msg-play-process .msg-tag {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.msg-play-process .msg-body {
|
||||
padding: 0 10px 10px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.msg-live-indicator {
|
||||
margin-left: 8px;
|
||||
@@ -1519,7 +1669,7 @@ body.rail-collapsed .message-feed {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 4px);
|
||||
z-index: 40;
|
||||
z-index: 80;
|
||||
min-width: 132px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
@@ -1530,13 +1680,28 @@ body.rail-collapsed .message-feed {
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
.composer .more-menu-panel {
|
||||
top: auto;
|
||||
bottom: calc(100% + 4px);
|
||||
}
|
||||
.more-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.more-menu-item[hidden] { display: none !important; }
|
||||
.more-menu-item:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.more-menu-item:hover {
|
||||
background: var(--bg);
|
||||
@@ -1567,27 +1732,50 @@ 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: 14px 18px 18px;
|
||||
gap: 14px;
|
||||
padding: 16px 18px 22px;
|
||||
gap: 16px;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-assistant-row .msg-bubble {
|
||||
width: min(100%, 44rem);
|
||||
max-width: 100%;
|
||||
border-color: color-mix(in srgb, var(--border) 70%, transparent);
|
||||
box-shadow: none;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-assistant-row .msg-head {
|
||||
padding-top: 6px;
|
||||
}
|
||||
body[data-lifecycle="play"] .present-shell {
|
||||
border-radius: 10px;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-user-row .msg-bubble {
|
||||
width: min(100%, 28rem);
|
||||
max-width: 90%;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .msg-bubble {
|
||||
width: min(100%, 46rem);
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .present-shell {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .present-shell-badge {
|
||||
display: none;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .present-stage .present-region--body .present-region-body,
|
||||
body[data-lifecycle="play"] .msg-play-final .present-stage.present-region--body .present-region-body {
|
||||
font-size: 18.5px;
|
||||
line-height: 1.88;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
body[data-lifecycle="play"] .msg-play-final .present-shell--chat_monitor .present-stage {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 产物紧凑卡:横向主轴,预览一行 */
|
||||
.artifact-compact-list {
|
||||
@@ -2455,7 +2643,8 @@ body.design-workspace .af-mosaic {
|
||||
color: var(--text);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.propose-sync-hint {
|
||||
.propose-sync-hint,
|
||||
.propose-plan-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
@@ -2544,6 +2733,9 @@ body.design-workspace .af-mosaic {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.flow-occ-revise {
|
||||
color: var(--text, #222);
|
||||
}
|
||||
.flow-order {
|
||||
grid-row: 1 / -1;
|
||||
align-self: center;
|
||||
@@ -2685,10 +2877,9 @@ body.design-workspace .af-mosaic {
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #2a6) 20%, transparent);
|
||||
}
|
||||
|
||||
/* 询问卡:host 本身即卡片(不再套内层 .qcard) */
|
||||
/* 询问卡:对话流里 host 即卡片;创作工作面上剥壳,见上方 flatten 规则 */
|
||||
.questions-card-host {
|
||||
--qcard-max: 48vh;
|
||||
--qcard-max-expanded: 82vh;
|
||||
flex: 0 1 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@@ -2710,16 +2901,17 @@ body.design-workspace .af-mosaic {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface, #fff);
|
||||
transition: max-height 0.22s ease;
|
||||
transition: max-height 0.18s ease;
|
||||
}
|
||||
.questions-card-host.is-open.is-expanded:not([hidden]) {
|
||||
max-height: var(--qcard-max-expanded);
|
||||
.questions-card-host.is-open.is-collapsed:not([hidden]) {
|
||||
max-height: none;
|
||||
}
|
||||
.qcard-top {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-height: 18px;
|
||||
margin: 0;
|
||||
padding: 4px 12px 0;
|
||||
@@ -2734,18 +2926,40 @@ body.design-workspace .af-mosaic {
|
||||
.qcard-top-grip {
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--muted, #888) 35%, transparent);
|
||||
}
|
||||
.questions-card-host.is-expanded .qcard-top {
|
||||
min-height: 22px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.questions-card-host.is-expanded .qcard-top:hover .qcard-top-grip {
|
||||
.qcard-top:hover .qcard-top-grip {
|
||||
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
|
||||
}
|
||||
.questions-card-host:not(.is-expanded) .qcard-body {
|
||||
cursor: pointer;
|
||||
.qcard-collapsed-meta {
|
||||
display: none;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
line-height: 1.35;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.questions-card-host.is-collapsed .qcard-top {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.questions-card-host.is-collapsed .qcard-top:hover .qcard-top-grip {
|
||||
background: color-mix(in srgb, var(--muted, #888) 55%, transparent);
|
||||
}
|
||||
.questions-card-host.is-collapsed .qcard-collapsed-meta {
|
||||
display: block;
|
||||
}
|
||||
.questions-card-host.is-collapsed .qcard-body,
|
||||
.questions-card-host.is-collapsed .qcard-float {
|
||||
display: none;
|
||||
}
|
||||
/* 悬浮控件层:不占文档流高度 */
|
||||
.qcard-float {
|
||||
@@ -3472,6 +3686,7 @@ body.design-workspace .af-mosaic {
|
||||
border: none; background: transparent; padding: 8px 12px;
|
||||
font-size: 13px; cursor: pointer; border-radius: 4px;
|
||||
}
|
||||
.context-menu-item[hidden] { display: none; }
|
||||
.context-menu-item:hover { background: var(--bg); }
|
||||
.context-menu-separator-before { border-top: 1px solid var(--border); }
|
||||
.context-menu-danger { color: var(--danger); }
|
||||
@@ -3491,18 +3706,46 @@ body.design-workspace .af-mosaic {
|
||||
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||
.timeline-empty { color: var(--muted); }
|
||||
|
||||
/* Composer — 单层输入壳;底栏本身不再套第二层卡片 */
|
||||
/* Composer — 单行:状态 / 输入 / 更多;菜单向上弹出不被裁切 */
|
||||
.composer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 10px var(--workspace-pad-x, 16px) 12px;
|
||||
padding: 6px var(--workspace-pad-x, 16px) 8px;
|
||||
min-height: var(--composer-min);
|
||||
max-height: min(50vh, 420px);
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
background: var(--bg);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.composer > .status {
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.composer:has(.composer-mode-chip) > .status {
|
||||
display: none;
|
||||
}
|
||||
#composer-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-height: min(50vh, 420px);
|
||||
overflow: auto;
|
||||
}
|
||||
.composer > .more-menu {
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.composer .more-menu-sum {
|
||||
height: calc(var(--composer-control-h) - 8px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 唯一描边:输入 + 操作同框 */
|
||||
.composer-input-shell {
|
||||
@@ -3584,7 +3827,7 @@ body.design-workspace .af-mosaic {
|
||||
overflow-y: hidden;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 8px 4px;
|
||||
padding: 6px 4px;
|
||||
line-height: 1.5;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
@@ -3603,7 +3846,16 @@ body.design-workspace .af-mosaic {
|
||||
margin-right: 2px;
|
||||
}
|
||||
.composer-waiting, .composer-idle {
|
||||
padding: 12px; text-align: center; color: var(--muted); font-size: 13px;
|
||||
padding: 6px 0; text-align: left; color: var(--muted); font-size: 13px;
|
||||
}
|
||||
.composer-waiting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.composer-waiting .btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.composer-hint { margin: 0 0 8px; font-size: 12px; color: var(--muted); }
|
||||
.composer-form {
|
||||
@@ -3656,7 +3908,7 @@ body.design-workspace .af-mosaic {
|
||||
overflow-y: hidden;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 8px 4px;
|
||||
padding: 6px 4px;
|
||||
line-height: 1.5;
|
||||
background: transparent;
|
||||
field-sizing: content;
|
||||
@@ -3666,7 +3918,7 @@ body.design-workspace .af-mosaic {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.composer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 0; }
|
||||
.composer-actions .btn-primary { flex: 1; }
|
||||
|
||||
/* Intake (from intake-ui.js) */
|
||||
@@ -3729,7 +3981,7 @@ body.design-workspace .af-mosaic {
|
||||
}
|
||||
|
||||
/* —— 游玩呈现壳 present.v1 ——
|
||||
* 主工作面 = body(随 600~3000 字撑高)。
|
||||
* 主工作面 = body(随 500~2000 字撑高)。
|
||||
* 视觉:少盒子、少渐变;层级靠字号/间距/对比,不靠装饰抢注意力。
|
||||
*/
|
||||
.present-shell {
|
||||
@@ -3793,7 +4045,7 @@ body.design-workspace .af-mosaic {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
/* P0:正文舞台 —— 随字数伸展(600~3000 靠外层滚动,不塞进小框) */
|
||||
/* P0:正文舞台 —— 随字数伸展(500~2000 靠外层滚动,不塞进小框) */
|
||||
.present-stage {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -4085,3 +4337,37 @@ body.design-workspace .af-mosaic {
|
||||
color: #e7e5e4;
|
||||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.opening-setup-body {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.opening-picker {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.opening-picker-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.opening-picker-btn {
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elev, transparent);
|
||||
color: inherit;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.opening-picker-btn:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.opening-picker-pos {
|
||||
font-size: 12px;
|
||||
color: var(--muted, #888);
|
||||
min-width: 4.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user