接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。
直聘插入按模块回馈,并记录请求骨架便于排查。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+642
-84
@@ -1,5 +1,6 @@
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { tryParseJsonDoc } from "./json-doc.js";
|
||||
import { formatBodyHtml } from "./markdown.js";
|
||||
import {
|
||||
getActiveQuestions,
|
||||
getModuleOpeningPrompt,
|
||||
@@ -7,8 +8,8 @@ import {
|
||||
isQuestionCardDismissed,
|
||||
renderQuestionsCard,
|
||||
} from "./questions-ui.js";
|
||||
import { displayWorkerLabel, formatWorkerDisplayTitle, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import { formatBodyHtml } from "./markdown.js";
|
||||
import { displayCreationModeLabel, displayWorkerLabel, formatWorkerDisplayTitle, isFlowPlanReview, reviewComposerCopy } from "./display-labels.js";
|
||||
import { fillPlayAux, hidePlayAuxTabs } from "./play-aux.js";
|
||||
import {
|
||||
PRESENT_SHELL_IDS,
|
||||
parsePresentDoc,
|
||||
@@ -47,7 +48,7 @@ const MSG_LABEL = {
|
||||
orchestrator_thinking: "编排器 · 思考",
|
||||
orchestrator_prompt: "编排器",
|
||||
orchestrator_assessment: "编排器 · 内容评价",
|
||||
dictate_reply: "Boss直聘",
|
||||
dictate_reply: "对话落盘",
|
||||
worker_running: "执行单元",
|
||||
worker_output: "执行单元",
|
||||
worker_questions: "提问",
|
||||
@@ -62,6 +63,16 @@ function esc(s) {
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/** 正文 HTML;缺导入或渲染失败时退回纯文本,避免整页对话空白 */
|
||||
function safeBodyHtml(text) {
|
||||
try {
|
||||
if (typeof formatBodyHtml === "function") return formatBodyHtml(text);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return esc(text).replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function escAttr(s) {
|
||||
return esc(s).replace(/"/g, """);
|
||||
}
|
||||
@@ -314,6 +325,367 @@ const msgMenuState = {
|
||||
handlers: null,
|
||||
};
|
||||
|
||||
const MSG_PEEK_SIZE_KEY = "wa.msg-peek.size";
|
||||
/** 拖出后仍须留在视口内的侧/底条带(px) */
|
||||
const MSG_PEEK_KEEP_VISIBLE = 48;
|
||||
/** 标题栏必须可见的顶边距(px)——保证可拖 */
|
||||
const MSG_PEEK_TOP_MARGIN = 8;
|
||||
const MSG_PEEK_MIN_W = 280;
|
||||
const MSG_PEEK_MIN_H = 180;
|
||||
|
||||
const msgPeekState = {
|
||||
wired: false,
|
||||
dragging: false,
|
||||
resizing: false,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
resizeStartX: 0,
|
||||
resizeStartY: 0,
|
||||
resizeStartW: 0,
|
||||
resizeStartH: 0,
|
||||
lastLeft: null,
|
||||
lastTop: null,
|
||||
};
|
||||
|
||||
function hideMsgPeek() {
|
||||
const panel = document.getElementById("msg-peek-panel");
|
||||
if (!panel || panel.hidden) return;
|
||||
panel.hidden = true;
|
||||
panel.classList.remove("is-dragging", "is-resizing");
|
||||
msgPeekState.dragging = false;
|
||||
msgPeekState.resizing = false;
|
||||
const body = document.getElementById("msg-peek-body");
|
||||
if (body) body.innerHTML = "";
|
||||
}
|
||||
|
||||
function loadMsgPeekSize() {
|
||||
try {
|
||||
const raw = localStorage.getItem(MSG_PEEK_SIZE_KEY);
|
||||
if (!raw) return null;
|
||||
const o = JSON.parse(raw);
|
||||
const width = Number(o?.width);
|
||||
const height = Number(o?.height);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
||||
return { width, height };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveMsgPeekSize(width, height) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
MSG_PEEK_SIZE_KEY,
|
||||
JSON.stringify({
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
function defaultMsgPeekSize() {
|
||||
return {
|
||||
width: Math.min(680, Math.max(MSG_PEEK_MIN_W, window.innerWidth * 0.92)),
|
||||
height: Math.min(760, Math.max(MSG_PEEK_MIN_H, window.innerHeight * 0.82)),
|
||||
};
|
||||
}
|
||||
|
||||
function clampMsgPeekSize(width, height) {
|
||||
const maxW = Math.max(MSG_PEEK_MIN_W, window.innerWidth - 16);
|
||||
const maxH = Math.max(MSG_PEEK_MIN_H, window.innerHeight - MSG_PEEK_TOP_MARGIN * 2);
|
||||
return {
|
||||
width: Math.min(Math.max(MSG_PEEK_MIN_W, width), maxW),
|
||||
height: Math.min(Math.max(MSG_PEEK_MIN_H, height), maxH),
|
||||
};
|
||||
}
|
||||
|
||||
function applyMsgPeekSize(panel) {
|
||||
const saved = loadMsgPeekSize();
|
||||
const base = saved ?? defaultMsgPeekSize();
|
||||
const size = clampMsgPeekSize(base.width, base.height);
|
||||
panel.style.width = `${size.width}px`;
|
||||
panel.style.height = `${size.height}px`;
|
||||
panel.style.maxHeight = "none";
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶边:标题栏始终留在视口内(可拖)。
|
||||
* 左右/底:允许部分拖出,但至少 KEEP 像素相交。
|
||||
*/
|
||||
function clampMsgPeekPosition(panel, left, top) {
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const w = rect.width || panel.offsetWidth;
|
||||
const h = rect.height || panel.offsetHeight;
|
||||
const keep = MSG_PEEK_KEEP_VISIBLE;
|
||||
const minLeft = keep - w;
|
||||
const maxLeft = window.innerWidth - keep;
|
||||
const minTop = MSG_PEEK_TOP_MARGIN;
|
||||
const maxTop = Math.max(minTop, window.innerHeight - keep);
|
||||
return {
|
||||
left: Math.min(Math.max(minLeft, left), maxLeft),
|
||||
top: Math.min(Math.max(minTop, top), maxTop),
|
||||
};
|
||||
}
|
||||
|
||||
function isMsgPeekFullyOffscreen(panel) {
|
||||
const r = panel.getBoundingClientRect();
|
||||
return (
|
||||
r.right <= 0 ||
|
||||
r.left >= window.innerWidth ||
|
||||
r.bottom <= 0 ||
|
||||
r.top >= window.innerHeight
|
||||
);
|
||||
}
|
||||
|
||||
/** 每次打开:记忆尺寸 + 屏幕居中(标题栏保证可见) */
|
||||
function placeMsgPeek(panel) {
|
||||
const size = applyMsgPeekSize(panel);
|
||||
panel.style.visibility = "hidden";
|
||||
panel.hidden = false;
|
||||
const centerLeft = (window.innerWidth - size.width) / 2;
|
||||
const centerTop = (window.innerHeight - size.height) / 2;
|
||||
const pos = clampMsgPeekPosition(panel, centerLeft, centerTop);
|
||||
panel.style.left = `${pos.left}px`;
|
||||
panel.style.top = `${pos.top}px`;
|
||||
msgPeekState.lastLeft = pos.left;
|
||||
msgPeekState.lastTop = pos.top;
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
function wireMsgPeekPanel() {
|
||||
if (msgPeekState.wired) return;
|
||||
const panel = document.getElementById("msg-peek-panel");
|
||||
if (!panel) return;
|
||||
msgPeekState.wired = true;
|
||||
|
||||
document.getElementById("msg-peek-close")?.addEventListener("click", () => {
|
||||
hideMsgPeek();
|
||||
});
|
||||
|
||||
const head = panel.querySelector("[data-peek-drag]");
|
||||
head?.addEventListener("pointerdown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest(".msg-peek-close")) return;
|
||||
if (msgPeekState.resizing) return;
|
||||
const rect = panel.getBoundingClientRect();
|
||||
msgPeekState.dragging = true;
|
||||
msgPeekState.offsetX = e.clientX - rect.left;
|
||||
msgPeekState.offsetY = e.clientY - rect.top;
|
||||
panel.classList.add("is-dragging");
|
||||
head.setPointerCapture?.(e.pointerId);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const resizeHandle = panel.querySelector("[data-peek-resize]");
|
||||
resizeHandle?.addEventListener("pointerdown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (msgPeekState.dragging) return;
|
||||
const rect = panel.getBoundingClientRect();
|
||||
msgPeekState.resizing = true;
|
||||
msgPeekState.resizeStartX = e.clientX;
|
||||
msgPeekState.resizeStartY = e.clientY;
|
||||
msgPeekState.resizeStartW = rect.width;
|
||||
msgPeekState.resizeStartH = rect.height;
|
||||
panel.classList.add("is-resizing");
|
||||
resizeHandle.setPointerCapture?.(e.pointerId);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
const onMove = (e) => {
|
||||
if (msgPeekState.dragging) {
|
||||
const next = clampMsgPeekPosition(
|
||||
panel,
|
||||
e.clientX - msgPeekState.offsetX,
|
||||
e.clientY - msgPeekState.offsetY,
|
||||
);
|
||||
panel.style.left = `${next.left}px`;
|
||||
panel.style.top = `${next.top}px`;
|
||||
msgPeekState.lastLeft = next.left;
|
||||
msgPeekState.lastTop = next.top;
|
||||
return;
|
||||
}
|
||||
if (msgPeekState.resizing) {
|
||||
const dw = e.clientX - msgPeekState.resizeStartX;
|
||||
const dh = e.clientY - msgPeekState.resizeStartY;
|
||||
const size = clampMsgPeekSize(
|
||||
msgPeekState.resizeStartW + dw,
|
||||
msgPeekState.resizeStartH + dh,
|
||||
);
|
||||
panel.style.width = `${size.width}px`;
|
||||
panel.style.height = `${size.height}px`;
|
||||
}
|
||||
};
|
||||
const onUp = (e) => {
|
||||
if (msgPeekState.dragging) {
|
||||
msgPeekState.dragging = false;
|
||||
panel.classList.remove("is-dragging");
|
||||
head?.releasePointerCapture?.(e.pointerId);
|
||||
if (!panel.hidden && isMsgPeekFullyOffscreen(panel)) {
|
||||
hideMsgPeek();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (msgPeekState.resizing) {
|
||||
msgPeekState.resizing = false;
|
||||
panel.classList.remove("is-resizing");
|
||||
resizeHandle?.releasePointerCapture?.(e.pointerId);
|
||||
if (!panel.hidden) {
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const size = clampMsgPeekSize(rect.width, rect.height);
|
||||
panel.style.width = `${size.width}px`;
|
||||
panel.style.height = `${size.height}px`;
|
||||
saveMsgPeekSize(size.width, size.height);
|
||||
const pos = clampMsgPeekPosition(
|
||||
panel,
|
||||
Number.parseFloat(panel.style.left) || rect.left,
|
||||
Number.parseFloat(panel.style.top) || rect.top,
|
||||
);
|
||||
panel.style.left = `${pos.left}px`;
|
||||
panel.style.top = `${pos.top}px`;
|
||||
msgPeekState.lastLeft = pos.left;
|
||||
msgPeekState.lastTop = pos.top;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", onUp);
|
||||
window.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
function openContentPeek(payload) {
|
||||
wireMsgPeekPanel();
|
||||
const panel = document.getElementById("msg-peek-panel");
|
||||
const titleEl = document.getElementById("msg-peek-title");
|
||||
const subEl = document.getElementById("msg-peek-subtitle");
|
||||
const bodyEl = document.getElementById("msg-peek-body");
|
||||
if (!panel || !titleEl || !bodyEl) return;
|
||||
|
||||
titleEl.textContent = payload?.title || "完整内容";
|
||||
if (subEl) {
|
||||
const subtitle = String(payload?.subtitle || "").trim();
|
||||
if (subtitle) {
|
||||
subEl.hidden = false;
|
||||
subEl.textContent = subtitle;
|
||||
} else {
|
||||
subEl.hidden = true;
|
||||
subEl.textContent = "";
|
||||
}
|
||||
}
|
||||
bodyEl.innerHTML = payload?.html || `<p class="empty-sm">(无正文)</p>`;
|
||||
|
||||
placeMsgPeek(panel);
|
||||
bodyEl.scrollTop = 0;
|
||||
document.getElementById("msg-peek-close")?.focus?.();
|
||||
}
|
||||
|
||||
function formatDictatePeekHtml(content) {
|
||||
const trimmed = String(content ?? "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
// 与侧栏同一路径:勿走 formatArtifactBodyHtml(## 分段会丢掉前置 JSON/正文)
|
||||
return `<div class="dictate-product-body">${safeBodyHtml(trimmed)}</div>`;
|
||||
}
|
||||
|
||||
const dictateProductContentByEl = new WeakMap();
|
||||
|
||||
function rememberDictateProductContent(el, content) {
|
||||
if (!el) return;
|
||||
const text = String(content ?? "");
|
||||
dictateProductContentByEl.set(el, text);
|
||||
// 短文仍写 dataset,便于调试;长文只放 WeakMap,避免属性截断
|
||||
if (text.length <= 4000) el.dataset.originalText = text;
|
||||
else delete el.dataset.originalText;
|
||||
}
|
||||
|
||||
function readDictateProductContent(card) {
|
||||
if (!card) return "";
|
||||
if (dictateProductContentByEl.has(card)) {
|
||||
return dictateProductContentByEl.get(card) ?? "";
|
||||
}
|
||||
return card.dataset.originalText || "";
|
||||
}
|
||||
|
||||
const dictateProductMenuState = {
|
||||
card: null,
|
||||
wired: false,
|
||||
};
|
||||
|
||||
function hideDictateProductMenu() {
|
||||
const menu = document.getElementById("dictate-product-menu");
|
||||
if (menu) menu.hidden = true;
|
||||
dictateProductMenuState.card = null;
|
||||
}
|
||||
|
||||
function showDictateProductMenu(card, x, y) {
|
||||
const menu = document.getElementById("dictate-product-menu");
|
||||
if (!menu || !card) return;
|
||||
hideMsgMenu();
|
||||
hideDictateProductMenu();
|
||||
dictateProductMenuState.card = card;
|
||||
menu.hidden = false;
|
||||
menu.style.left = `${x}px`;
|
||||
menu.style.top = `${y}px`;
|
||||
const rect = menu.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) {
|
||||
menu.style.left = `${Math.max(4, window.innerWidth - rect.width - 4)}px`;
|
||||
}
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
menu.style.top = `${Math.max(4, window.innerHeight - rect.height - 4)}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function wireDictateProductMenu() {
|
||||
if (dictateProductMenuState.wired) return;
|
||||
const menu = document.getElementById("dictate-product-menu");
|
||||
if (!menu) return;
|
||||
dictateProductMenuState.wired = true;
|
||||
menu.addEventListener("click", async (e) => {
|
||||
const action = e.target
|
||||
.closest("[data-dictate-product-action]")
|
||||
?.getAttribute("data-dictate-product-action");
|
||||
const card = dictateProductMenuState.card;
|
||||
hideDictateProductMenu();
|
||||
if (!action || !card) return;
|
||||
const content = readDictateProductContent(card);
|
||||
const tagEl = card.querySelector(".dictate-product-tag");
|
||||
const title =
|
||||
tagEl?.getAttribute("title")?.trim() ||
|
||||
tagEl?.textContent?.trim() ||
|
||||
"产物";
|
||||
if (action === "peek") {
|
||||
openContentPeek({
|
||||
title,
|
||||
html: formatDictatePeekHtml(content),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === "copy") {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wireDictateProductContextMenu(root) {
|
||||
if (!root || root.dataset.dictateProductMenuWired) return;
|
||||
root.dataset.dictateProductMenuWired = "1";
|
||||
wireDictateProductMenu();
|
||||
root.addEventListener("contextmenu", (e) => {
|
||||
const card = e.target.closest(".dictate-product");
|
||||
if (!card || !root.contains(card)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showDictateProductMenu(card, e.clientX, e.clientY);
|
||||
});
|
||||
}
|
||||
|
||||
function showMsgMenu(card, x, y) {
|
||||
const menu = document.getElementById("msg-action-menu");
|
||||
if (!menu || !card) return;
|
||||
@@ -510,7 +882,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">${formatBodyHtml(original)}</div>`;
|
||||
editing.outerHTML = `<div class="msg-body">${safeBodyHtml(original)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,7 +1149,7 @@ function isWorkspaceNav() {
|
||||
|
||||
function syncExpandLabel() {
|
||||
const label = document.getElementById("rail-expand-label");
|
||||
if (label) label.textContent = activeRailTab === "log" ? "志" : "书";
|
||||
if (label) label.textContent = activeRailTab === "log" ? "调度" : "作品";
|
||||
}
|
||||
|
||||
export function setRailTab(tab, { expand = false } = {}) {
|
||||
@@ -862,7 +1234,11 @@ export function resetRailChrome() {
|
||||
railUserExpanded = true;
|
||||
coordRailCollapsed = true;
|
||||
document.body.dataset.railCanCollapse = "0";
|
||||
document.body.classList.remove("rail-collapsed", "coord-rail-collapsed");
|
||||
document.body.classList.remove(
|
||||
"rail-collapsed",
|
||||
"coord-rail-collapsed",
|
||||
"play-workspace",
|
||||
);
|
||||
const rail = document.getElementById("coord-rail");
|
||||
if (rail) {
|
||||
rail.hidden = true;
|
||||
@@ -1418,10 +1794,21 @@ function splitTaggedArtifactSections(text) {
|
||||
hits.push({ title: m[1].trim(), index: m.index, headerEnd: m.index + m[0].length });
|
||||
}
|
||||
if (!hits.length) return [{ title: "", content: trimmed }];
|
||||
return hits.map((h, i) => {
|
||||
const sections = [];
|
||||
// 保留首个 ## 之前的前置正文(常见:JSON / 散文 + ## 小节)
|
||||
if (hits[0].index > 0) {
|
||||
const preamble = trimmed.slice(0, hits[0].index).trim();
|
||||
if (preamble) sections.push({ title: "", content: preamble });
|
||||
}
|
||||
for (let i = 0; i < hits.length; i++) {
|
||||
const h = hits[i];
|
||||
const end = i + 1 < hits.length ? hits[i + 1].index : trimmed.length;
|
||||
return { title: h.title, content: trimmed.slice(h.headerEnd, end).trim() };
|
||||
});
|
||||
sections.push({
|
||||
title: h.title,
|
||||
content: trimmed.slice(h.headerEnd, end).trim(),
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
const FRAGMENT_HEADER_KEYS = new Set([
|
||||
@@ -1638,24 +2025,25 @@ function renderPlainCompactCard(title, content, opts = {}) {
|
||||
return "";
|
||||
}
|
||||
const preview = clampPreviewText(content, 160);
|
||||
const looksJson = /^\s*[{\[]/.test(String(content || ""));
|
||||
if (opts.flat) {
|
||||
const body = looksJson
|
||||
? `<p class="ws-muted artifact-parse-hint">未能解析为可展示产物。</p>
|
||||
<pre class="review-feed-plain">${esc(content)}</pre>`
|
||||
: `<div class="dictate-product-body">${safeBodyHtml(content)}</div>`;
|
||||
return `<article class="artifact-flat-review">
|
||||
<header class="artifact-flat-meta">
|
||||
<nav class="artifact-crumb-path artifact-flat-path" aria-label="产物">
|
||||
<span class="artifact-crumb-root">${esc(title || "产物")}</span>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="artifact-flat-body">
|
||||
<p class="ws-muted artifact-parse-hint">未能解析为可展示产物。</p>
|
||||
<pre class="review-feed-plain">${esc(content)}</pre>
|
||||
</div>
|
||||
<div class="artifact-flat-body">${body}</div>
|
||||
</article>`;
|
||||
}
|
||||
const openAttr = opts.open ? " open" : "";
|
||||
const looksJson = /^\s*[{\[]/.test(String(content || ""));
|
||||
const body = looksJson
|
||||
? `<p class="ws-muted artifact-parse-hint">未能解析为 JSON,以下为原文(可展开原始块排查)。</p><pre class="review-feed-plain">${esc(content)}</pre>`
|
||||
: `<pre class="review-feed-plain">${esc(content)}</pre>`;
|
||||
: `<div class="dictate-product-body">${safeBodyHtml(content)}</div>`;
|
||||
return `<details class="artifact-compact"${openAttr}>
|
||||
<summary class="artifact-compact-sum">
|
||||
<div class="artifact-compact-row">
|
||||
@@ -1987,37 +2375,126 @@ function renderContextFragmentHtml(doc, opts = {}) {
|
||||
}
|
||||
|
||||
/** 开场白终节点:1~多条候选用呈现壳展示,选定后由程序落库 */
|
||||
function collectOpeningCandidateTexts(body) {
|
||||
const texts = [];
|
||||
const push = (value) => {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text && !texts.includes(text)) texts.push(text);
|
||||
function openingPersonaFrom(row, fallback) {
|
||||
const p = row?.用户角色 || row?.meta?.用户角色 || fallback || null;
|
||||
if (!p || typeof p !== "object") return null;
|
||||
const name = String(p.名字 || p.name || "").trim();
|
||||
if (!name) return null;
|
||||
return {
|
||||
名字: name,
|
||||
简介: String(p.简介 || p.description || "").trim(),
|
||||
};
|
||||
push(body.开场白全文);
|
||||
}
|
||||
|
||||
function openingTextFrom(raw) {
|
||||
if (typeof raw === "string") return raw.trim();
|
||||
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
||||
if (
|
||||
raw.schema === "present.v1" ||
|
||||
raw.blocks != null ||
|
||||
raw.shell ||
|
||||
raw.shell_id
|
||||
) {
|
||||
try {
|
||||
return JSON.stringify(raw);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return String(raw.正文 || raw.全文 || raw.text || raw.开场白 || raw.内容 || "").trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function openingVariablesFrom(row) {
|
||||
const list = Array.isArray(row?.开场变量)
|
||||
? row.开场变量
|
||||
: Array.isArray(row?.meta?.开场变量)
|
||||
? row.meta.开场变量
|
||||
: [];
|
||||
return list.filter((item) => item && typeof item === "object");
|
||||
}
|
||||
|
||||
function collectOpeningCandidates(body) {
|
||||
const fallback = openingPersonaFrom(body, null);
|
||||
const sharedVars = openingVariablesFrom(body);
|
||||
const rows = [];
|
||||
const seen = new Set();
|
||||
const push = (text, persona, variables) => {
|
||||
const value = openingTextFrom(text);
|
||||
if (!value || seen.has(value)) return;
|
||||
seen.add(value);
|
||||
rows.push({
|
||||
text: value,
|
||||
persona: persona || fallback,
|
||||
variables: variables?.length ? variables : sharedVars,
|
||||
});
|
||||
};
|
||||
push(body.开场白全文, fallback, sharedVars);
|
||||
if (Array.isArray(body.开场白候选)) {
|
||||
for (const item of body.开场白候选) {
|
||||
if (typeof item === "string") push(item);
|
||||
if (typeof item === "string") push(item, fallback, sharedVars);
|
||||
else if (item && typeof item === "object") {
|
||||
push(item.全文 || item.text || item.开场白 || item.内容);
|
||||
push(
|
||||
item.正文 || item.全文 || item.text || item.开场白 || item.内容,
|
||||
openingPersonaFrom(item, fallback),
|
||||
openingVariablesFrom(item),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderOpeningPersonaHtml(persona) {
|
||||
if (!persona?.名字) return "";
|
||||
return `<section class="artifact-block"><h4>用户角色</h4><p><strong>${esc(
|
||||
String(persona.名字),
|
||||
)}</strong>${
|
||||
persona.简介 ? ` · ${esc(String(persona.简介))}` : ""
|
||||
}</p></section>`;
|
||||
}
|
||||
|
||||
function renderOpeningVarsHtml(vars) {
|
||||
if (!Array.isArray(vars) || !vars.length) return "";
|
||||
return `<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>`;
|
||||
}
|
||||
|
||||
function openingPresentPacket(text) {
|
||||
const trimmed = String(text || "").trim();
|
||||
if (!trimmed) return presentFromPlain("", "prose");
|
||||
if (trimmed.startsWith("{")) {
|
||||
const parsed = tryParseJsonDoc(trimmed);
|
||||
const view = parsed ? parsePresentDoc(parsed) : null;
|
||||
if (view) return view.packet;
|
||||
}
|
||||
return presentFromPlain(trimmed, "prose");
|
||||
}
|
||||
|
||||
function renderOpeningSetupBodyHtml(body) {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||
const candidates = collectOpeningCandidateTexts(body);
|
||||
const candidates = collectOpeningCandidates(body);
|
||||
if (!candidates.length) return "";
|
||||
const total = candidates.length;
|
||||
const anyCandidateVars = candidates.some((row) => row.variables?.length);
|
||||
const slides = candidates
|
||||
.map((text, i) => {
|
||||
.map((row, 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"),
|
||||
openingPresentPacket(row.text),
|
||||
esc,
|
||||
)}</article>`;
|
||||
)}${renderOpeningPersonaHtml(row.persona)}${renderOpeningVarsHtml(row.variables)}</article>`;
|
||||
})
|
||||
.join("");
|
||||
const nav =
|
||||
@@ -2028,20 +2505,10 @@ function renderOpeningSetupBodyHtml(body) {
|
||||
<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 varsHtml =
|
||||
!anyCandidateVars && Array.isArray(body.开场变量) && body.开场变量.length
|
||||
? renderOpeningVarsHtml(body.开场变量)
|
||||
: "";
|
||||
const space = body.用户可行动空间
|
||||
? `<section class="artifact-block"><h4>可行动空间</h4>${renderProseHtml(
|
||||
body.用户可行动空间,
|
||||
@@ -2117,7 +2584,8 @@ function renderSpecialtyBodyHtml(body, skill) {
|
||||
if (
|
||||
body.风格与写法 != null ||
|
||||
body.推进与决策 != null ||
|
||||
skillName.includes("叙事指南")
|
||||
skillName.includes("叙事指南") ||
|
||||
skillName.includes("故事推进")
|
||||
) {
|
||||
const html = renderNarrativeBodyHtml(body);
|
||||
if (html) return html;
|
||||
@@ -4925,7 +5393,7 @@ function renderSpeakWorkspace(view) {
|
||||
wr.message ||
|
||||
"描述本轮行动、对话或想说的话;由游玩管线推进。";
|
||||
} else if (view.creationMode === "dictate") {
|
||||
title = "Boss 直聘";
|
||||
title = displayCreationModeLabel("dictate");
|
||||
hint =
|
||||
view.hints?.[0] ||
|
||||
wr.message ||
|
||||
@@ -4976,7 +5444,7 @@ function renderSpeakWorkspace(view) {
|
||||
? workspaceViewSwitchHtml("flow")
|
||||
: `<span class="workspace-surface-kicker">${
|
||||
view.creationMode === "dictate"
|
||||
? "Boss直聘"
|
||||
? displayCreationModeLabel("dictate")
|
||||
: canOfferSaveProduct(view)
|
||||
? "落档"
|
||||
: "说话"
|
||||
@@ -4993,34 +5461,79 @@ function renderSpeakWorkspace(view) {
|
||||
</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-empty-block">
|
||||
<p class="dictate-products-empty-title">尚无产物</p>
|
||||
<p class="dictate-products-empty-desc">Agent 落盘后会出现在这里。点击行展开,右键可查看全文或复制。</p>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="dictate-products-list">${items
|
||||
return `<div class="dictate-products-list" role="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>
|
||||
const order =
|
||||
p.order != null && Number.isFinite(Number(p.order))
|
||||
? Number(p.order)
|
||||
: null;
|
||||
const shortTag = tag
|
||||
.replace(/^设计\./, "")
|
||||
.replace(/^用户\./, "")
|
||||
.replace(/#/g, " · ");
|
||||
return `<details class="dictate-product dictate-product--rail" role="listitem">
|
||||
<summary class="dictate-product-sum">
|
||||
<span class="dictate-product-sum-top">
|
||||
<span class="dictate-product-tag" title="${esc(tag)}">${esc(shortTag || tag || "未命名")}</span>
|
||||
${
|
||||
order != null
|
||||
? `<span class="dictate-product-order" title="相对序">${order}</span>`
|
||||
: ""
|
||||
}
|
||||
<span class="dictate-product-chevron" aria-hidden="true"></span>
|
||||
</span>
|
||||
${
|
||||
preview
|
||||
? `<span class="dictate-product-preview">${esc(preview)}${
|
||||
content.length > 72 ? "…" : ""
|
||||
}</span>`
|
||||
: `<span class="dictate-product-preview is-empty">(空)</span>`
|
||||
}
|
||||
</summary>
|
||||
<div class="dictate-product-body">${formatBodyHtml(content)}</div>
|
||||
<div class="dictate-product-body" data-pending="1"></div>
|
||||
</details>`;
|
||||
})
|
||||
.join("")}</div>`;
|
||||
}
|
||||
|
||||
function hydrateDictateProductBody(card) {
|
||||
if (!card) return;
|
||||
const body = card.querySelector(".dictate-product-body");
|
||||
if (!body || body.dataset.pending !== "1") return;
|
||||
const content = readDictateProductContent(card);
|
||||
body.innerHTML = content.trim()
|
||||
? safeBodyHtml(content)
|
||||
: `<p class="empty-sm">(无正文)</p>`;
|
||||
delete body.dataset.pending;
|
||||
}
|
||||
|
||||
function wireDictateProductAccordion(root) {
|
||||
if (!root || root.dataset.dictateProductAccordionWired) return;
|
||||
root.dataset.dictateProductAccordionWired = "1";
|
||||
root.addEventListener("toggle", (e) => {
|
||||
const card = e.target;
|
||||
if (!(card instanceof HTMLDetailsElement)) return;
|
||||
if (!card.classList.contains("dictate-product--rail")) return;
|
||||
if (!card.open) return;
|
||||
hydrateDictateProductBody(card);
|
||||
root.querySelectorAll("details.dictate-product--rail[open]").forEach((other) => {
|
||||
if (other !== card) other.open = false;
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function fillCoordRailProducts(view) {
|
||||
const coordRail = document.getElementById("coord-rail");
|
||||
const drawerBody = document.getElementById("coord-drawer-body");
|
||||
@@ -5032,7 +5545,15 @@ function fillCoordRailProducts(view) {
|
||||
coordRailCollapsed = false;
|
||||
}
|
||||
coordRail.hidden = false;
|
||||
if (drawerBody) drawerBody.innerHTML = renderDictateProductsList(view);
|
||||
if (drawerBody) {
|
||||
drawerBody.innerHTML = renderDictateProductsList(view);
|
||||
const cards = drawerBody.querySelectorAll(".dictate-product");
|
||||
cards.forEach((el, i) => {
|
||||
rememberDictateProductContent(el, items[i]?.content ?? "");
|
||||
});
|
||||
wireDictateProductContextMenu(drawerBody);
|
||||
wireDictateProductAccordion(drawerBody);
|
||||
}
|
||||
if (drawerCount) {
|
||||
drawerCount.textContent = String(items.length);
|
||||
drawerCount.title = items.length
|
||||
@@ -5108,6 +5629,10 @@ function mountReviewWorkspace(stage, view, handlers, opts = {}) {
|
||||
sourceMessage: resolveReviewSourceMessage(view),
|
||||
leaveStepHtml: leaveStepControlHtml(view),
|
||||
}) + askSlot;
|
||||
const reviewEl = stage.querySelector(".workspace-review");
|
||||
if (reviewEl && view.reviewArtifact?.body) {
|
||||
reviewEl.dataset.originalText = String(view.reviewArtifact.body);
|
||||
}
|
||||
wireMessageFeedActions(stage, handlers);
|
||||
wireMessageContextMenu(stage, handlers);
|
||||
wireCreationFlowGraph(stage, handlers);
|
||||
@@ -5176,6 +5701,8 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
if (drawerBody) drawerBody.innerHTML = "";
|
||||
if (drawerCount) drawerCount.textContent = "";
|
||||
}
|
||||
document.body.classList.toggle("play-workspace", false);
|
||||
hidePlayAuxTabs();
|
||||
// 默认把询问卡停回主列底部槽位
|
||||
if (panelFeed) ensureQuestionsHostIn(panelFeed);
|
||||
|
||||
@@ -5195,36 +5722,41 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
|
||||
if (!stage) return;
|
||||
|
||||
// 转述:主柱对话流;右侧已持久化;stage 仅一行条
|
||||
// 转述:对话画在 stage 内(design-workspace 会藏掉 #message-feed)
|
||||
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">Boss直聘</span><span class="dictate-stage-recipe">配方 · ${recipeName}</span></header>`
|
||||
: `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">Boss直聘</span></header>`;
|
||||
const dictateKicker = displayCreationModeLabel("dictate");
|
||||
const head = recipeName
|
||||
? `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">${dictateKicker}</span><span class="dictate-stage-recipe">配方 · ${recipeName}</span></header>`
|
||||
: `<header class="dictate-stage-bar"><span class="workspace-surface-kicker">${dictateKicker}</span></header>`;
|
||||
stage.innerHTML = `${head}<div class="dictate-feed" id="dictate-feed"></div>`;
|
||||
const dictateFeed = stage.querySelector("#dictate-feed");
|
||||
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);
|
||||
if (dictateFeed) {
|
||||
appendMessagesToFeed(dictateFeed, 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>`;
|
||||
dictateFeed.appendChild(pending);
|
||||
wireRetryRun(pending, handlers);
|
||||
}
|
||||
dictateFeed.scrollTop = dictateFeed.scrollHeight;
|
||||
}
|
||||
feed.scrollTop = feed.scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5282,6 +5814,14 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
}
|
||||
|
||||
// —— 游玩 / 转述对话流 ——
|
||||
if (isPlayView(view) && coordRail) {
|
||||
document.body.classList.add("play-workspace");
|
||||
coordRailCollapsed = false;
|
||||
document.body.classList.remove("coord-rail-collapsed");
|
||||
fillPlayAux(view, handlers, { busy: loading });
|
||||
syncCoordRailChrome();
|
||||
}
|
||||
|
||||
appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, {
|
||||
emptyText: isPlayView(view)
|
||||
? "开场已就绪。在下方说你要做什么。"
|
||||
@@ -5342,6 +5882,7 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
}
|
||||
|
||||
for (const msg of visible) {
|
||||
try {
|
||||
const isUser = msg.role === "user";
|
||||
const kind = msg.kind ?? (isUser ? "user_input" : "system_info");
|
||||
const body = msgBody(msg, view);
|
||||
@@ -5383,7 +5924,7 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
? formatPlayPresentHtml(body, view)
|
||||
: kind === "worker_output"
|
||||
? formatArtifactBodyHtml(body)
|
||||
: formatBodyHtml(body);
|
||||
: safeBodyHtml(body);
|
||||
|
||||
const process = !isUser && isPlayProcessMessage(msg, view);
|
||||
const playFinal = !isUser && isPlayFinalReply(msg, view);
|
||||
@@ -5410,6 +5951,15 @@ function appendMessagesToFeed(feed, visible, view, handlers, activeQuestions, op
|
||||
</div>`;
|
||||
}
|
||||
feed.appendChild(card);
|
||||
} catch (err) {
|
||||
console.error("render message failed", msg?.id, err);
|
||||
const fallback = document.createElement("article");
|
||||
fallback.className = "msg system msg-assistant-row";
|
||||
fallback.innerHTML = `<div class="msg-bubble"><div class="msg-body">${esc(
|
||||
msgBody(msg, view) || "(这条消息渲染失败)",
|
||||
).replace(/\n/g, "<br>")}</div></div>`;
|
||||
feed.appendChild(fallback);
|
||||
}
|
||||
}
|
||||
wireMessageFeedActions(feed, handlers);
|
||||
wireMessageContextMenu(feed, handlers);
|
||||
@@ -5534,17 +6084,25 @@ export function renderWorkspace(view, loading, _onPickSkill, handlers = {}) {
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest("#msg-action-menu")) hideMsgMenu();
|
||||
if (!e.target.closest("#dictate-product-menu")) hideDictateProductMenu();
|
||||
if (!e.target.closest("#flow-node-menu") && !e.target.closest("[data-flow-done]")) {
|
||||
hideFlowNodeMenu();
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
const peek = document.getElementById("msg-peek-panel");
|
||||
if (peek && !peek.hidden) {
|
||||
hideMsgPeek();
|
||||
return;
|
||||
}
|
||||
hideMsgMenu();
|
||||
hideDictateProductMenu();
|
||||
hideFlowNodeMenu();
|
||||
}
|
||||
});
|
||||
document.addEventListener("scroll", () => {
|
||||
hideMsgMenu();
|
||||
hideDictateProductMenu();
|
||||
hideFlowNodeMenu();
|
||||
}, true);
|
||||
|
||||
+786
-235
File diff suppressed because it is too large
Load Diff
+15
-2
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 与 docs/ui-glossary.md、src/server/display-labels.ts 保持同步。
|
||||
* 用户侧口径:配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元。
|
||||
* 用户侧口径:进料方式 / 配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元。
|
||||
*/
|
||||
|
||||
const STAGE_LABELS = {
|
||||
@@ -37,9 +37,12 @@ const FIXED_TOPIC_LABELS = {
|
||||
"aesthetics-interaction": "美学纲领与交互范式",
|
||||
interaction: "交互范式",
|
||||
narrative_guide: "叙事指南与故事推进",
|
||||
"narrative-guide": "叙事指南",
|
||||
"story-progression": "故事推进",
|
||||
input_protocol: "输入协议",
|
||||
core_premise: "核心前提",
|
||||
aesthetics: "美学纲领",
|
||||
protagonist: "主角设定",
|
||||
};
|
||||
|
||||
const PHASE_UNIT_LABELS = {
|
||||
@@ -52,11 +55,21 @@ export function displayStageLabel(id) {
|
||||
return STAGE_LABELS[id] ?? id;
|
||||
}
|
||||
|
||||
export function displayCreationModeLabel(id) {
|
||||
if (!id) return "";
|
||||
const map = {
|
||||
recipe: "工序编排",
|
||||
dictate: "对话落盘",
|
||||
};
|
||||
return map[id] ?? id;
|
||||
}
|
||||
|
||||
export function displaySkillPackLabel(id) {
|
||||
if (!id) return "";
|
||||
const map = {
|
||||
"world-simulator": "世界模拟器",
|
||||
"world-simulator": "回合推演",
|
||||
"expand-assistant": "扩写助手",
|
||||
快穿短局: "快穿短局",
|
||||
文本生成器: "文本生成器",
|
||||
"text-generator": "文本生成器",
|
||||
交互式长文生成器: "交互式长文生成器",
|
||||
|
||||
+44
-24
@@ -10,7 +10,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<link rel="stylesheet" href="/styles.css?v=20260921-repeatable-split" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
@@ -18,9 +18,14 @@
|
||||
if (!/^(paper|modern|dark|green)$/.test(t)) t = "modern";
|
||||
document.documentElement.setAttribute("data-color-theme", t);
|
||||
document.documentElement.setAttribute("data-theme", t);
|
||||
var c = localStorage.getItem("wa-present-chrome") || "default";
|
||||
if (!/^(default|messenger|book|terminal)$/.test(c)) c = "default";
|
||||
document.documentElement.setAttribute("data-present-chrome", c);
|
||||
var fs = localStorage.getItem("wa-ui-font-scale") || "100";
|
||||
var legacy = { sm: 88, md: 100, lg: 112, xl: 125 };
|
||||
var percent = legacy[fs] != null ? legacy[fs] : Number(fs);
|
||||
if (!isFinite(percent)) percent = 100;
|
||||
if (percent > 0 && percent <= 2.5) percent = Math.round(percent * 100);
|
||||
percent = Math.min(150, Math.max(80, Math.round(percent / 5) * 5));
|
||||
document.documentElement.setAttribute("data-ui-font-scale", String(percent));
|
||||
document.documentElement.style.setProperty("--ui-font-scale", String(percent / 100));
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
@@ -36,7 +41,7 @@
|
||||
aria-controls="rail-body"
|
||||
title="展开侧栏"
|
||||
>
|
||||
<span class="rail-expand-label" id="rail-expand-label">书</span>
|
||||
<span class="rail-expand-label" id="rail-expand-label">作品</span>
|
||||
</button>
|
||||
<div class="rail-body" id="rail-body">
|
||||
<header class="panel-head rail-head">
|
||||
@@ -54,9 +59,9 @@
|
||||
<button type="button" class="btn-sm rail-collapse" id="btn-rail-collapse" title="收起侧栏" aria-label="收起侧栏">‹</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="本书侧栏" hidden>
|
||||
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="本书">
|
||||
<span class="rail-tab-label">本书</span>
|
||||
<nav class="rail-tabs" id="rail-tabs" role="tablist" aria-label="作品侧栏" hidden>
|
||||
<button type="button" class="rail-tab active" data-rail-tab="books" role="tab" aria-selected="true" title="实例">
|
||||
<span class="rail-tab-label">实例</span>
|
||||
</button>
|
||||
<button type="button" class="rail-tab" data-rail-tab="log" role="tab" aria-selected="false" title="调度">
|
||||
<span class="rail-tab-label">调度</span>
|
||||
@@ -122,19 +127,12 @@
|
||||
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
|
||||
</button>
|
||||
<div class="more-menu-sep" role="separator"></div>
|
||||
<div class="more-menu-label">选壳</div>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-present-chrome="default" role="menuitem">
|
||||
<span class="more-theme-copy"><span class="more-theme-name">默认</span><span class="more-theme-desc">跟随壳本身</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-present-chrome="messenger" role="menuitem">
|
||||
<span class="more-theme-copy"><span class="more-theme-name">讯息</span><span class="more-theme-desc">气泡对话感</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-present-chrome="book" role="menuitem">
|
||||
<span class="more-theme-copy"><span class="more-theme-name">书页</span><span class="more-theme-desc">阅读排版感</span></span>
|
||||
</button>
|
||||
<button type="button" class="more-menu-item more-theme-item" data-present-chrome="terminal" role="menuitem">
|
||||
<span class="more-theme-copy"><span class="more-theme-name">终端</span><span class="more-theme-desc">等宽终端感</span></span>
|
||||
</button>
|
||||
<div class="more-menu-label">字号</div>
|
||||
<div class="more-font-scale" role="group" aria-label="字号">
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
|
||||
<span class="more-font-value" id="theme-font-scale-value">100</span>
|
||||
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -198,8 +196,8 @@
|
||||
进料方式
|
||||
<select id="select-creation-mode" required>
|
||||
<option value="" selected disabled>请选择…</option>
|
||||
<option value="recipe">Worker 式(工序图)</option>
|
||||
<option value="dictate">Boss 直聘(对话直填)</option>
|
||||
<option value="recipe">工序编排</option>
|
||||
<option value="dictate">对话落盘</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="dialog-desc" id="creation-mode-desc" hidden></p>
|
||||
@@ -221,6 +219,11 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<div id="book-enter-pop" class="book-enter-pop" hidden role="menu" aria-label="打开作品">
|
||||
<button type="button" class="book-enter-item" data-enter="design" role="menuitem">创作</button>
|
||||
<button type="button" class="book-enter-item" data-enter="play" role="menuitem">游玩</button>
|
||||
</div>
|
||||
|
||||
<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-play" data-action="play">用产物开玩</button>
|
||||
@@ -237,6 +240,11 @@
|
||||
<button type="button" class="context-menu-item" data-save-menu-action="edit-products">编辑产物</button>
|
||||
</menu>
|
||||
|
||||
<menu id="dictate-product-menu" class="context-menu" hidden>
|
||||
<button type="button" class="context-menu-item" data-dictate-product-action="peek">查看完整内容</button>
|
||||
<button type="button" class="context-menu-item" data-dictate-product-action="copy">复制全文</button>
|
||||
</menu>
|
||||
|
||||
<menu id="msg-action-menu" class="context-menu" hidden>
|
||||
<button type="button" class="context-menu-item" id="msg-menu-context" data-msg-menu-action="view-context" hidden>查看上下文</button>
|
||||
<button type="button" class="context-menu-item" id="msg-menu-compare" data-msg-menu-action="compare" hidden>对比</button>
|
||||
@@ -246,6 +254,18 @@
|
||||
<button type="button" class="context-menu-item context-menu-danger" id="msg-menu-delete" data-msg-menu-action="restart">从这里重开</button>
|
||||
</menu>
|
||||
|
||||
<div id="msg-peek-panel" class="msg-peek-panel" hidden role="dialog" aria-modal="false" aria-labelledby="msg-peek-title">
|
||||
<header class="msg-peek-head" data-peek-drag>
|
||||
<div class="msg-peek-head-text">
|
||||
<h2 class="msg-peek-title" id="msg-peek-title">完整内容</h2>
|
||||
<p class="msg-peek-subtitle" id="msg-peek-subtitle" hidden></p>
|
||||
</div>
|
||||
<button type="button" class="msg-peek-close" id="msg-peek-close" aria-label="关闭">×</button>
|
||||
</header>
|
||||
<div class="msg-peek-body" id="msg-peek-body"></div>
|
||||
<div class="msg-peek-resize" data-peek-resize="se" title="拖动调整大小" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<menu id="flow-node-menu" class="context-menu" hidden>
|
||||
<button type="button" class="context-menu-item" id="flow-menu-view" data-flow-menu-action="view">查看产物</button>
|
||||
<button type="button" class="context-menu-item" id="flow-menu-reenter" data-flow-menu-action="reenter">进入继续修改</button>
|
||||
@@ -308,6 +328,6 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
<script type="module" src="/app.js?v=20260921-repeatable-split"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 游玩辅助 Tab:表格 / 开场与角色 / 对话。
|
||||
* 隐藏格默认挡住,点字段才露;手改带格子 rev。
|
||||
*/
|
||||
import { parsePresentDoc } from "./present-shells.js";
|
||||
|
||||
let playAuxTab = "table";
|
||||
let revealedHidden = new Set();
|
||||
let editingKey = null;
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function coerceValue(prev, raw) {
|
||||
const text = String(raw ?? "").trim();
|
||||
if (typeof prev === "boolean") {
|
||||
if (text === "true" || text === "是" || text === "1") return true;
|
||||
if (text === "false" || text === "否" || text === "0") return false;
|
||||
return prev;
|
||||
}
|
||||
if (typeof prev === "number") {
|
||||
const n = Number(text);
|
||||
return Number.isFinite(n) ? n : prev;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function previewOpening(text) {
|
||||
let source = String(text || "").trim();
|
||||
if (source.startsWith("{")) {
|
||||
try {
|
||||
const view = parsePresentDoc(JSON.parse(source));
|
||||
const body = view?.packet?.blocks?.body;
|
||||
if (typeof body === "string" && body.trim()) source = body.trim();
|
||||
} catch {
|
||||
/* 保持原文 */
|
||||
}
|
||||
}
|
||||
const flat = source.replace(/\s+/g, " ").trim();
|
||||
if (flat.length <= 72) return flat;
|
||||
return `${flat.slice(0, 72)}…`;
|
||||
}
|
||||
|
||||
function rowHtml(row, { hidden, revealed, editing, busy }) {
|
||||
const shown = !hidden || revealed;
|
||||
const val = shown ? formatValue(row.value) : "••••••";
|
||||
const edit =
|
||||
shown && row.editable && editing
|
||||
? `<form class="play-table-edit" data-table-edit="${esc(row.key)}">
|
||||
<input type="text" name="value" value="${esc(formatValue(row.value))}" ${busy ? "disabled" : ""} />
|
||||
<button type="submit" class="btn-sm btn-primary" ${busy ? "disabled" : ""}>保存</button>
|
||||
<button type="button" class="btn-sm" data-table-cancel>取消</button>
|
||||
</form>`
|
||||
: "";
|
||||
const action =
|
||||
shown && row.editable && !editing
|
||||
? `<button type="button" class="play-table-fix" data-table-fix="${esc(row.key)}" ${busy ? "disabled" : ""}>改</button>`
|
||||
: "";
|
||||
return `<li class="play-table-row${hidden ? " is-secret" : ""}${revealed ? " is-open" : ""}" data-table-key="${esc(row.key)}" data-hidden="${hidden ? "1" : "0"}">
|
||||
<button type="button" class="play-table-hit" data-table-reveal="${esc(row.key)}" ${hidden && !revealed ? "" : hidden ? "" : "tabindex='-1'"}>
|
||||
<span class="play-table-key">${esc(row.key)}</span>
|
||||
<span class="play-table-val">${esc(val)}</span>
|
||||
</button>
|
||||
${action}
|
||||
${edit}
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function tablePanelHtml(table, busy) {
|
||||
if (!table?.rows?.length) {
|
||||
return `<p class="play-aux-empty">本局没有状态表。</p>`;
|
||||
}
|
||||
const visible = table.rows.filter((r) => r.visibility !== "hidden");
|
||||
const hidden = table.rows.filter((r) => r.visibility === "hidden");
|
||||
const vis =
|
||||
visible.length > 0
|
||||
? `<ul class="play-table-list">${visible
|
||||
.map((row) =>
|
||||
rowHtml(row, {
|
||||
hidden: false,
|
||||
revealed: true,
|
||||
editing: editingKey === row.key,
|
||||
busy,
|
||||
}),
|
||||
)
|
||||
.join("")}</ul>`
|
||||
: `<p class="play-aux-empty">没有可见字段。</p>`;
|
||||
const hid = hidden.length
|
||||
? `<details class="play-table-secrets">
|
||||
<summary>不可见 ${hidden.length}</summary>
|
||||
<ul class="play-table-list">${hidden
|
||||
.map((row) =>
|
||||
rowHtml(row, {
|
||||
hidden: true,
|
||||
revealed: revealedHidden.has(row.key),
|
||||
editing: editingKey === row.key,
|
||||
busy,
|
||||
}),
|
||||
)
|
||||
.join("")}</ul>
|
||||
</details>`
|
||||
: "";
|
||||
return `<div class="play-table" data-table-tag="${esc(table.tag)}">${vis}${hid}</div>`;
|
||||
}
|
||||
|
||||
function openingPanelHtml(aux) {
|
||||
const openings = aux?.openings ?? [];
|
||||
if (!openings.length) {
|
||||
return `<p class="play-aux-empty">没有可选开场。使用当前用户角色卡。</p>`;
|
||||
}
|
||||
const cards = openings
|
||||
.map((item) => {
|
||||
const selected = item.index === aux.selectedOpeningIndex;
|
||||
const persona = item.persona;
|
||||
return `<article class="play-open-card${selected ? " is-selected" : ""}" data-opening-index="${item.index}" tabindex="0" role="button">
|
||||
<p class="play-open-text">${esc(previewOpening(item.text))}</p>
|
||||
${
|
||||
persona
|
||||
? `<p class="play-open-persona"><strong>${esc(persona.name)}</strong>${
|
||||
persona.description
|
||||
? ` · ${esc(persona.description)}`
|
||||
: ""
|
||||
}</p>`
|
||||
: `<p class="play-open-persona muted">无预设角色</p>`
|
||||
}
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
const usingOpening = aux.openingPersonaChoice === "opening";
|
||||
return `<div class="play-open">
|
||||
${cards}
|
||||
<button type="button" class="btn-sm play-open-refuse"${usingOpening ? "" : " disabled"}>不用预设角色</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function tabBtn(id, label, current) {
|
||||
return `<button type="button" class="play-aux-tab${current === id ? " is-current" : ""}" data-play-aux-tab="${id}" ${current === id ? 'aria-current="page"' : ""}>${esc(label)}</button>`;
|
||||
}
|
||||
|
||||
export function fillPlayAux(view, handlers = {}, extra = {}) {
|
||||
const rail = document.getElementById("coord-rail");
|
||||
const drawerBody = document.getElementById("coord-drawer-body");
|
||||
const title = document.getElementById("coord-rail-title");
|
||||
const drawerCount = document.getElementById("coord-drawer-count");
|
||||
if (!rail || !drawerBody) return;
|
||||
rail.hidden = false;
|
||||
|
||||
const aux = view.playAux;
|
||||
|
||||
const head = rail.querySelector(".coord-rail-head");
|
||||
if (head) {
|
||||
let tabs = head.querySelector(".play-aux-tabs");
|
||||
if (!tabs) {
|
||||
tabs = document.createElement("nav");
|
||||
tabs.className = "play-aux-tabs";
|
||||
tabs.setAttribute("aria-label", "游玩辅助");
|
||||
const collapse = head.querySelector(".coord-rail-collapse");
|
||||
if (collapse) head.insertBefore(tabs, collapse);
|
||||
else head.appendChild(tabs);
|
||||
}
|
||||
tabs.hidden = false;
|
||||
tabs.innerHTML = [
|
||||
tabBtn("table", "表格", playAuxTab),
|
||||
tabBtn("opening", "开场", playAuxTab),
|
||||
].join("");
|
||||
tabs.onclick = (e) => {
|
||||
const btn = e.target.closest("[data-play-aux-tab]");
|
||||
if (!btn) return;
|
||||
const id = btn.getAttribute("data-play-aux-tab");
|
||||
if (!id || id === playAuxTab) return;
|
||||
playAuxTab = id;
|
||||
fillPlayAux(view, handlers, extra);
|
||||
};
|
||||
}
|
||||
if (title) title.textContent = "游玩";
|
||||
if (drawerCount) drawerCount.textContent = "";
|
||||
|
||||
const busy = Boolean(extra.busy);
|
||||
if (playAuxTab === "table") {
|
||||
drawerBody.innerHTML = tablePanelHtml(aux?.table, busy);
|
||||
wireTable(drawerBody, aux?.table, handlers, busy, () =>
|
||||
fillPlayAux(view, handlers, extra),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (playAuxTab === "opening") {
|
||||
drawerBody.innerHTML = openingPanelHtml(aux);
|
||||
wireOpening(drawerBody, handlers, busy);
|
||||
}
|
||||
}
|
||||
|
||||
export function currentPlayAuxTab() {
|
||||
return playAuxTab;
|
||||
}
|
||||
|
||||
export function hidePlayAuxTabs() {
|
||||
const tabs = document.querySelector(".play-aux-tabs");
|
||||
if (tabs) tabs.hidden = true;
|
||||
}
|
||||
|
||||
function wireTable(root, table, handlers, busy, rerender) {
|
||||
root.querySelectorAll("[data-table-reveal]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const key = btn.getAttribute("data-table-reveal");
|
||||
const row = btn.closest(".play-table-row");
|
||||
if (!key || !row) return;
|
||||
if (row.getAttribute("data-hidden") === "1" && !revealedHidden.has(key)) {
|
||||
revealedHidden.add(key);
|
||||
rerender?.();
|
||||
}
|
||||
});
|
||||
});
|
||||
root.querySelectorAll("[data-table-fix]").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
editingKey = btn.getAttribute("data-table-fix");
|
||||
rerender?.();
|
||||
});
|
||||
});
|
||||
root.querySelectorAll("[data-table-cancel]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
editingKey = null;
|
||||
rerender?.();
|
||||
});
|
||||
});
|
||||
root.querySelectorAll("[data-table-edit]").forEach((form) => {
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
const key = form.getAttribute("data-table-edit");
|
||||
const row = table?.rows?.find((r) => r.key === key);
|
||||
if (!key || !row) return;
|
||||
const next = coerceValue(
|
||||
row.value,
|
||||
form.querySelector("input[name=value]")?.value,
|
||||
);
|
||||
editingKey = null;
|
||||
handlers.onPatchTable?.(table.tag, [
|
||||
{ key, value: next, expectedRev: row.rev },
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wireOpening(root, handlers, busy) {
|
||||
root.querySelectorAll("[data-opening-index]").forEach((card) => {
|
||||
const go = () => {
|
||||
if (busy) return;
|
||||
const index = Number(card.getAttribute("data-opening-index"));
|
||||
if (!Number.isFinite(index)) return;
|
||||
const hasPersona = Boolean(card.querySelector(".play-open-persona strong"));
|
||||
handlers.onSelectOpening?.(index, hasPersona ? true : undefined);
|
||||
};
|
||||
card.addEventListener("click", go);
|
||||
card.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
go();
|
||||
}
|
||||
});
|
||||
});
|
||||
root.querySelector(".play-open-refuse")?.addEventListener("click", () => {
|
||||
if (busy) return;
|
||||
handlers.onRefuseOpeningPersona?.();
|
||||
});
|
||||
}
|
||||
+1
-10
@@ -277,16 +277,7 @@ export function renderPresentShellHtml(packet, esc, opts = {}) {
|
||||
typeof tweaks.tone_chrome === "string" && tweaks.tone_chrome.trim()
|
||||
? tweaks.tone_chrome.trim()
|
||||
: "default";
|
||||
if (opts.applyUserChrome !== false) {
|
||||
try {
|
||||
const pref = document.documentElement.getAttribute("data-present-chrome");
|
||||
if (pref === "messenger" || pref === "book" || pref === "terminal") {
|
||||
tone = pref;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
// 用户「选壳」菜单已移除;气质跟呈现壳 / tweaks 本身,不再读全局 data-present-chrome
|
||||
|
||||
const blocks = packet.blocks ?? {};
|
||||
const regions = shellDefaultRegions(shell);
|
||||
|
||||
+821
-92
File diff suppressed because it is too large
Load Diff
+85
-5
@@ -1,4 +1,4 @@
|
||||
/** Color themes + present chrome — aligned with llm_workflow_engine ThemeToggle. */
|
||||
/** Color themes + UI font scale — aligned with llm_workflow_engine ThemeToggle. */
|
||||
|
||||
export const COLOR_THEMES = [
|
||||
{
|
||||
@@ -27,7 +27,22 @@ export const COLOR_THEMES = [
|
||||
},
|
||||
];
|
||||
|
||||
/** 呈现壳气质(tone_chrome),与色主题同属「主题」菜单 */
|
||||
/** 全局字号:百分比(100 = 默认),+/- 步进调节 */
|
||||
export const UI_FONT_SCALE_MIN = 80;
|
||||
export const UI_FONT_SCALE_MAX = 150;
|
||||
export const UI_FONT_SCALE_STEP = 5;
|
||||
export const UI_FONT_SCALE_DEFAULT = 100;
|
||||
|
||||
const LEGACY_FONT_SCALE_IDS = {
|
||||
sm: 88,
|
||||
md: 100,
|
||||
lg: 112,
|
||||
xl: 125,
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 选壳已从主题菜单移除;壳气质跟呈现壳本身。保留常量以免旧引用报错。
|
||||
*/
|
||||
export const PRESENT_CHROMES = [
|
||||
{ id: "default", name: "默认", description: "跟随壳本身" },
|
||||
{ id: "messenger", name: "讯息", description: "气泡对话感" },
|
||||
@@ -36,6 +51,7 @@ export const PRESENT_CHROMES = [
|
||||
];
|
||||
|
||||
const STORAGE_KEY = "wa-color-theme";
|
||||
const FONT_SCALE_STORAGE_KEY = "wa-ui-font-scale";
|
||||
const CHROME_STORAGE_KEY = "wa-present-chrome";
|
||||
const THEME_IDS = new Set(COLOR_THEMES.map((t) => t.id));
|
||||
const CHROME_IDS = new Set(PRESENT_CHROMES.map((t) => t.id));
|
||||
@@ -76,11 +92,72 @@ export function initColorTheme() {
|
||||
return applyColorTheme(getStoredColorTheme());
|
||||
}
|
||||
|
||||
function clampFontPercent(n) {
|
||||
const stepped = Math.round(n / UI_FONT_SCALE_STEP) * UI_FONT_SCALE_STEP;
|
||||
return Math.min(
|
||||
UI_FONT_SCALE_MAX,
|
||||
Math.max(UI_FONT_SCALE_MIN, stepped),
|
||||
);
|
||||
}
|
||||
|
||||
/** 解析为百分比整数;兼容旧档 sm/md/lg/xl 与 0.x~2 倍缩放 */
|
||||
export function normalizeUiFontScalePercent(raw) {
|
||||
if (raw == null) return UI_FONT_SCALE_DEFAULT;
|
||||
const key = String(raw).trim();
|
||||
if (Object.prototype.hasOwnProperty.call(LEGACY_FONT_SCALE_IDS, key)) {
|
||||
return LEGACY_FONT_SCALE_IDS[key];
|
||||
}
|
||||
let n = Number(key);
|
||||
if (!Number.isFinite(n)) return UI_FONT_SCALE_DEFAULT;
|
||||
if (n > 0 && n <= 2.5) n = Math.round(n * 100);
|
||||
return clampFontPercent(n);
|
||||
}
|
||||
|
||||
export function getStoredUiFontScale() {
|
||||
try {
|
||||
return normalizeUiFontScalePercent(localStorage.getItem(FONT_SCALE_STORAGE_KEY));
|
||||
} catch {
|
||||
return UI_FONT_SCALE_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 用 getStoredUiFontScale / applyUiFontScale(percent) */
|
||||
export function fontScaleById(id) {
|
||||
const percent = normalizeUiFontScalePercent(id);
|
||||
return { id: String(percent), scale: percent / 100, name: `${percent}`, description: "" };
|
||||
}
|
||||
|
||||
export function applyUiFontScale(raw) {
|
||||
const percent = normalizeUiFontScalePercent(raw);
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-ui-font-scale", String(percent));
|
||||
root.style.setProperty("--ui-font-scale", String(percent / 100));
|
||||
try {
|
||||
localStorage.setItem(FONT_SCALE_STORAGE_KEY, String(percent));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return percent;
|
||||
}
|
||||
|
||||
export function bumpUiFontScale(deltaSteps) {
|
||||
const cur = getStoredUiFontScale();
|
||||
const steps = Number(deltaSteps);
|
||||
const delta = Number.isFinite(steps) ? steps : 0;
|
||||
return applyUiFontScale(cur + delta * UI_FONT_SCALE_STEP);
|
||||
}
|
||||
|
||||
export function initUiFontScale() {
|
||||
return applyUiFontScale(getStoredUiFontScale());
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function normalizePresentChrome(id) {
|
||||
const v = String(id || "").trim();
|
||||
return CHROME_IDS.has(v) ? v : DEFAULT_CHROME;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function getStoredPresentChrome() {
|
||||
try {
|
||||
return normalizePresentChrome(localStorage.getItem(CHROME_STORAGE_KEY));
|
||||
@@ -89,13 +166,15 @@ export function getStoredPresentChrome() {
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function chromeById(id) {
|
||||
const cid = normalizePresentChrome(id);
|
||||
return PRESENT_CHROMES.find((t) => t.id === cid) || PRESENT_CHROMES[0];
|
||||
}
|
||||
|
||||
export function applyPresentChrome(id) {
|
||||
const chrome = normalizePresentChrome(id);
|
||||
/** @deprecated 菜单已移除;调用仍把属性钉为 default,避免旧存档残留 */
|
||||
export function applyPresentChrome(_id) {
|
||||
const chrome = DEFAULT_CHROME;
|
||||
document.documentElement.setAttribute("data-present-chrome", chrome);
|
||||
try {
|
||||
localStorage.setItem(CHROME_STORAGE_KEY, chrome);
|
||||
@@ -111,6 +190,7 @@ export function applyPresentChrome(id) {
|
||||
return chrome;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function initPresentChrome() {
|
||||
return applyPresentChrome(getStoredPresentChrome());
|
||||
return applyPresentChrome(DEFAULT_CHROME);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user