引入游玩呈现壳与旁观维护槽,完善结算合并、十分制自评与技能产物 UI。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
532
web/agent-ui.js
532
web/agent-ui.js
@@ -1,6 +1,12 @@
|
||||
import { renderIntakePanel } from "./intake-ui.js";
|
||||
import { getActiveQuestions, renderQuestionsCard } from "./questions-ui.js";
|
||||
import { displayWorkerLabel, formatWorkerDisplayTitle } from "./display-labels.js";
|
||||
import {
|
||||
PRESENT_SHELL_IDS,
|
||||
parsePresentDoc,
|
||||
presentFromPlain,
|
||||
renderPresentShellHtml,
|
||||
} from "./present-shells.js";
|
||||
|
||||
const HIDE_KINDS = new Set(["worker_stub"]);
|
||||
|
||||
@@ -341,6 +347,44 @@ function formatQuestionsHtml(body) {
|
||||
return `<ol class="msg-q-list">${lines.map((l) => `<li>${esc(l)}</li>`).join("")}</ol>`;
|
||||
}
|
||||
|
||||
/** play 期助手终稿(含已验收的用户展示)用呈现壳渲染 */
|
||||
function shouldRenderPlayPresent(msg, view) {
|
||||
if (view?.lifecycleStage !== "play") 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
|
||||
const actor = String(msg.actor ?? "");
|
||||
if (actor === "narrator" || /用户展示|开场白/.test(String(msg.title ?? ""))) {
|
||||
return true;
|
||||
}
|
||||
// 助手可见终稿:无特殊 kind 的长文
|
||||
return kind === "system_info" || kind === "worker_stub" || !kind;
|
||||
}
|
||||
|
||||
function formatPlayPresentHtml(body, view) {
|
||||
const trimmed = (body || "").trim();
|
||||
if (!trimmed) return `<p class="empty-sm">(无正文)</p>`;
|
||||
const tweaks = view?.presentationTweaks;
|
||||
const fallbackShell =
|
||||
tweaks?.shell_id && PRESENT_SHELL_IDS?.includes?.(tweaks.shell_id)
|
||||
? tweaks.shell_id
|
||||
: tweaks?.shell_id &&
|
||||
["prose", "chat_monitor", "turn_panel", "chapter_reader"].includes(
|
||||
tweaks.shell_id,
|
||||
)
|
||||
? tweaks.shell_id
|
||||
: "prose";
|
||||
const parsed = tryParseJsonDoc(trimmed);
|
||||
const presentView = parsed ? parsePresentDoc(parsed, fallbackShell) : null;
|
||||
if (presentView) {
|
||||
return renderPresentShellHtml(presentView.packet, esc, { tweaks });
|
||||
}
|
||||
return renderPresentShellHtml(presentFromPlain(trimmed, fallbackShell), esc, {
|
||||
tweaks,
|
||||
});
|
||||
}
|
||||
|
||||
/** 旧会话 skill 选择提示,新流程不再展示 */
|
||||
const SKILL_SELECTION_RE = /请选择创作 skill|请选择创作类型/;
|
||||
|
||||
@@ -985,6 +1029,13 @@ function splitTaggedArtifactSections(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 轻度修复模型常出的尾逗号,便于友好渲染而不是整墙原文 */
|
||||
function softenJsonText(s) {
|
||||
return String(s || "")
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/,\s*([\]}])/g, "$1");
|
||||
}
|
||||
|
||||
function tryParseJsonDoc(text) {
|
||||
const tryParse = (s) => {
|
||||
try {
|
||||
@@ -993,7 +1044,7 @@ function tryParseJsonDoc(text) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const cleaned = stripCodeFences(text);
|
||||
const cleaned = softenJsonText(stripCodeFences(text));
|
||||
let parsed = tryParse(cleaned);
|
||||
if (parsed != null) return parsed;
|
||||
const objStart = cleaned.indexOf("{");
|
||||
@@ -1082,9 +1133,9 @@ function summarizeArtifactDoc(doc, tagTitle = "") {
|
||||
.map((d) => {
|
||||
if (!d || typeof d !== "object") return "";
|
||||
const name = d.名 || d.维度 || "?";
|
||||
const pct = parseCompleteness(d.分数);
|
||||
if (pct == null) return "";
|
||||
return `<span class="pct-pill tone-${pctTone(pct)}">${esc(String(name))} ${Math.round(pct)}%</span>`;
|
||||
const ten = parseTenScore(d.分数);
|
||||
if (ten == null) return "";
|
||||
return `<span class="pct-pill tone-${tenTone(ten)}">${esc(String(name))} ${formatTenScore(ten)}/10</span>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (pills.length) scoresHtml = pills.join("");
|
||||
@@ -1125,6 +1176,10 @@ function renderArtifactCompactCard(doc, tagTitle, rawSlice, opts = {}) {
|
||||
function renderPlainCompactCard(title, content, opts = {}) {
|
||||
const preview = clampPreviewText(content, 160);
|
||||
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>`;
|
||||
return `<details class="artifact-compact"${openAttr}>
|
||||
<summary class="artifact-compact-sum">
|
||||
<div class="artifact-compact-row">
|
||||
@@ -1135,7 +1190,7 @@ function renderPlainCompactCard(title, content, opts = {}) {
|
||||
<span class="artifact-compact-toggle" aria-hidden="true"></span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="artifact-compact-body"><pre class="review-feed-plain">${esc(content)}</pre></div>
|
||||
<div class="artifact-compact-body">${body}</div>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
@@ -1159,6 +1214,14 @@ function formatArtifactBodyHtml(body, opts = {}) {
|
||||
if (parsed != null && typeof parsed === "object") {
|
||||
return renderArtifactCompactCard(parsed, sec.title, sec.content, cardOpts);
|
||||
}
|
||||
const presentTag =
|
||||
/用户展示|开场白/.test(sec.title || "") || opts.asPresentFallback;
|
||||
if (presentTag) {
|
||||
return `<div class="artifact-compact-list">${renderPresentShellHtml(
|
||||
presentFromPlain(sec.content, "prose"),
|
||||
esc,
|
||||
)}</div>`;
|
||||
}
|
||||
return renderPlainCompactCard(sec.title || "产物", sec.content, cardOpts);
|
||||
});
|
||||
return `<div class="artifact-compact-list">${cards.join("")}</div>`;
|
||||
@@ -1169,6 +1232,10 @@ function formatArtifactBodyHtml(body, opts = {}) {
|
||||
return `<div class="artifact-compact-list">${renderArtifactCompactCard(parsed, "", trimmed, cardOpts)}</div>`;
|
||||
}
|
||||
|
||||
if (opts.asPresentFallback) {
|
||||
return renderPresentShellHtml(presentFromPlain(trimmed, "prose"), esc);
|
||||
}
|
||||
|
||||
return `<div class="artifact-compact-list">${renderPlainCompactCard("产物", trimmed, cardOpts)}</div>`;
|
||||
}
|
||||
|
||||
@@ -1178,6 +1245,12 @@ function renderKnownArtifactHtml(doc, opts = {}) {
|
||||
return `<div class="artifact-friendly">${renderStructuredValueHtml(doc, 0)}</div>`;
|
||||
}
|
||||
const schema = doc.schema;
|
||||
const presentView = parsePresentDoc(doc);
|
||||
if (presentView) {
|
||||
return renderPresentShellHtml(presentView.packet, esc, {
|
||||
tweaks: opts.shellTweaks,
|
||||
});
|
||||
}
|
||||
if (schema === "settlement.v1" || isSettlementLike(doc)) {
|
||||
return renderSettlementSectionsHtml(doc);
|
||||
}
|
||||
@@ -1199,6 +1272,7 @@ function renderKnownArtifactHtml(doc, opts = {}) {
|
||||
|
||||
function renderPlaySlotsHtml(doc) {
|
||||
const slotLabels = {
|
||||
auditor: "旁观维护",
|
||||
gm: "主世界层",
|
||||
"world-simulator": "主世界层",
|
||||
narrator: "叙事转述",
|
||||
@@ -1301,14 +1375,41 @@ function renderContextFragmentHtml(doc, opts = {}) {
|
||||
return `<div class="artifact-friendly artifact-context-fragment">${parts.join("")}</div>`;
|
||||
}
|
||||
|
||||
/** 美学纲领等:顶层板块并排 mosaic,子项用瓷砖网格 */
|
||||
/** 按技能分流正文视图;无专用模板则返回 ""(调用方走通用结构化) */
|
||||
function renderSpecialtyBodyHtml(body, skill) {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return "";
|
||||
const skillName = typeof skill === "string" ? skill : "";
|
||||
|
||||
if (
|
||||
body.rules != null ||
|
||||
body.必要性判断 != null ||
|
||||
skillName.includes("生成规则")
|
||||
) {
|
||||
const html = renderGenerationRulesBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
if (
|
||||
body.社会结构 != null ||
|
||||
body.世界状况 != null ||
|
||||
skillName.includes("舞台骨架")
|
||||
) {
|
||||
const html = renderWorldBlueprintBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
if (
|
||||
body.支撑点 != null ||
|
||||
(Array.isArray(body.依据的核心体验) && body.覆盖检验 != null) ||
|
||||
skillName.includes("实现机制")
|
||||
) {
|
||||
const html = renderMechanismBodyHtml(body);
|
||||
if (html) return html;
|
||||
}
|
||||
|
||||
const looksAesthetics =
|
||||
body.设定逻辑 != null ||
|
||||
body.交互范式 != null ||
|
||||
body.美学纲领 != null ||
|
||||
(typeof skill === "string" && skill.includes("美学"));
|
||||
skillName.includes("美学");
|
||||
if (!looksAesthetics) return "";
|
||||
|
||||
const order = ["设定逻辑", "交互范式", "美学纲领"];
|
||||
@@ -1331,6 +1432,361 @@ function renderSpecialtyBodyHtml(body, skill) {
|
||||
return parts.length ? `<div class="af-mosaic">${parts.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
function skillSection(title, inner, cls = "") {
|
||||
if (!inner) return "";
|
||||
return `<section class="skill-card-section${cls ? ` ${cls}` : ""}"><h4>${esc(title)}</h4>${inner}</section>`;
|
||||
}
|
||||
|
||||
function skillChipRow(items) {
|
||||
const chips = (items || []).filter(Boolean).map((t) => `<span class="ws-badge ws-badge-continue">${esc(String(t))}</span>`);
|
||||
return chips.length ? `<div class="artifact-chip-row">${chips.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
function skillProseList(arr) {
|
||||
if (!Array.isArray(arr) || !arr.length) return "";
|
||||
return `<ul class="artifact-list">${arr.map((x) => `<li>${esc(String(x))}</li>`).join("")}</ul>`;
|
||||
}
|
||||
|
||||
function skillKvBlock(obj, keys) {
|
||||
if (!obj || typeof obj !== "object") return "";
|
||||
const entries = (keys && keys.length ? keys.map((k) => [k, obj[k]]) : Object.entries(obj)).filter(
|
||||
([, v]) => v != null && v !== "",
|
||||
);
|
||||
if (!entries.length) return "";
|
||||
return `<div class="skill-kv">${entries
|
||||
.map(([k, v]) => {
|
||||
if (Array.isArray(v)) {
|
||||
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${skillProseList(v)}</div></div>`;
|
||||
}
|
||||
if (v && typeof v === "object") {
|
||||
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${renderStructuredValueHtml(v, 1)}</div></div>`;
|
||||
}
|
||||
return `<div class="skill-kv-row"><span class="skill-kv-key">${esc(String(k))}</span><div class="skill-kv-val">${renderProseHtml(String(v))}</div></div>`;
|
||||
})
|
||||
.join("")}</div>`;
|
||||
}
|
||||
|
||||
/** 生成规则 · 专用正文卡 */
|
||||
function renderGenerationRulesBodyHtml(body) {
|
||||
const parts = [];
|
||||
if (body.本步参数 && typeof body.本步参数 === "object") {
|
||||
const p = body.本步参数;
|
||||
parts.push(
|
||||
skillSection(
|
||||
"本步参数",
|
||||
skillChipRow([
|
||||
p.target && `对象:${p.target}`,
|
||||
p.rule_id && `rule_id:${p.rule_id}`,
|
||||
p.lifecycle_intent && `生命周期:${p.lifecycle_intent}`,
|
||||
]) || skillKvBlock(p),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body.必要性判断 && typeof body.必要性判断 === "object") {
|
||||
const n = body.必要性判断;
|
||||
const verdict = n.结论 != null ? String(n.结论) : "";
|
||||
const tone = /无需|跳过/.test(verdict) ? "warn" : "ok";
|
||||
parts.push(
|
||||
skillSection(
|
||||
"必要性判断",
|
||||
`${verdict ? `<p class="skill-verdict tone-${tone}">${esc(verdict)}</p>` : ""}${skillKvBlock(n, [
|
||||
"生成对象",
|
||||
"对游玩的重要性",
|
||||
"基础生成的不足",
|
||||
])}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const rules = Array.isArray(body.rules) ? body.rules : [];
|
||||
if (rules.length) {
|
||||
const cards = rules.map((rule, i) => renderOneGenerationRuleCard(rule, i)).filter(Boolean);
|
||||
parts.push(skillSection("规则", `<div class="skill-rule-stack">${cards.join("")}</div>`));
|
||||
} else if (body.必要性判断) {
|
||||
parts.push(skillSection("规则", `<p class="ws-muted">本步未建立专门规则(rules 为空)</p>`));
|
||||
}
|
||||
|
||||
if (body.增量说明 != null && String(body.增量说明).trim()) {
|
||||
parts.push(skillSection("增量说明", renderProseHtml(String(body.增量说明))));
|
||||
}
|
||||
|
||||
const used = new Set(["本步参数", "必要性判断", "rules", "增量说明"]);
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (used.has(k) || v == null || v === "") continue;
|
||||
parts.push(skillSection(k, renderStructuredValueHtml(v, 0)));
|
||||
}
|
||||
return parts.length ? `<div class="skill-view skill-view-generation-rules">${parts.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
function renderOneGenerationRuleCard(rule, index) {
|
||||
if (!rule || typeof rule !== "object") return "";
|
||||
const title = rule.对象 || rule.rule_id || `规则 ${index + 1}`;
|
||||
const head = `<header class="skill-rule-head">
|
||||
<div class="skill-rule-title">${esc(String(title))}</div>
|
||||
${skillChipRow([
|
||||
rule.rule_id && `id · ${rule.rule_id}`,
|
||||
rule.生命周期 && String(rule.生命周期),
|
||||
])}
|
||||
</header>`;
|
||||
|
||||
const blocks = [];
|
||||
if (rule.上下文策略 && typeof rule.上下文策略 === "object") {
|
||||
const flags = Object.entries(rule.上下文策略)
|
||||
.map(([k, v]) => `<span class="ws-slot-chip ${v ? "ws-slot-on" : "ws-slot-off"}">${esc(k)}</span>`)
|
||||
.join("");
|
||||
blocks.push(`<div class="skill-sub"><div class="skill-sub-title">上下文策略</div><div class="ws-slot-row">${flags}</div></div>`);
|
||||
}
|
||||
if (rule.数量 != null) {
|
||||
blocks.push(`<div class="skill-sub"><div class="skill-sub-title">数量</div>${skillKvBlock(typeof rule.数量 === "object" ? rule.数量 : { 值: rule.数量 })}</div>`);
|
||||
}
|
||||
if (rule.生成与描写 && typeof rule.生成与描写 === "object") {
|
||||
const g = rule.生成与描写;
|
||||
const methodKeys = ["依据", "方法", "硬约束", "字段间约束", "变化维度", "禁止项", "去重规则", "校验"];
|
||||
blocks.push(
|
||||
`<div class="skill-sub"><div class="skill-sub-title">生成与描写</div>${methodKeys
|
||||
.filter((k) => Array.isArray(g[k]) && g[k].length)
|
||||
.map(
|
||||
(k) =>
|
||||
`<div class="skill-method"><span class="skill-method-label">${esc(k)}</span>${skillProseList(g[k])}</div>`,
|
||||
)
|
||||
.join("")}</div>`,
|
||||
);
|
||||
}
|
||||
if (rule.产物格式 && typeof rule.产物格式 === "object") {
|
||||
blocks.push(renderProductSchemaCard(rule.产物格式));
|
||||
}
|
||||
const pools = Array.isArray(rule.池) ? rule.池 : [];
|
||||
if (pools.length) {
|
||||
blocks.push(
|
||||
`<div class="skill-sub"><div class="skill-sub-title">池(${pools.length})</div><div class="skill-pool-grid">${pools
|
||||
.map((p) => {
|
||||
if (!p || typeof p !== "object") return "";
|
||||
const n = Array.isArray(p.条目) ? p.条目.length : 0;
|
||||
return `<article class="skill-pool-card">
|
||||
<div class="skill-pool-name">${esc(String(p.名称 || p.pool_id || "池"))}</div>
|
||||
${skillChipRow([
|
||||
p.绑定字段 && `绑 · ${p.绑定字段}`,
|
||||
p.用途 && String(p.用途),
|
||||
n ? `${n} 条` : "",
|
||||
])}
|
||||
${p.说明 ? `<p class="skill-pool-desc">${esc(clampPreviewText(String(p.说明), 120))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.join("")}</div></div>`,
|
||||
);
|
||||
}
|
||||
|
||||
return `<article class="skill-rule-card">${head}${blocks.join("")}</article>`;
|
||||
}
|
||||
|
||||
function renderProductSchemaCard(fmt) {
|
||||
const schema = fmt.schema && typeof fmt.schema === "object" ? fmt.schema : null;
|
||||
const fields = schema
|
||||
? Object.entries(schema)
|
||||
.map(([name, spec]) => {
|
||||
if (!spec || typeof spec !== "object") {
|
||||
return `<tr><td>${esc(name)}</td><td colspan="3">${esc(String(spec))}</td></tr>`;
|
||||
}
|
||||
const type = spec.type != null ? String(spec.type) : "—";
|
||||
const req = spec.required === true ? "必填" : spec.required === false ? "可选" : "—";
|
||||
const desc = spec.description != null ? clampPreviewText(String(spec.description), 80) : "";
|
||||
const extra = [];
|
||||
if (Array.isArray(spec.allowed_values) && spec.allowed_values.length) {
|
||||
extra.push(
|
||||
`枚举 ${spec.allowed_values
|
||||
.slice(0, 6)
|
||||
.map((x) => (x && typeof x === "object" ? x.value : x))
|
||||
.map(String)
|
||||
.join("/")}${spec.allowed_values.length > 6 ? "…" : ""}`,
|
||||
);
|
||||
}
|
||||
if (spec.minimum != null || spec.maximum != null) {
|
||||
extra.push(`[${spec.minimum ?? "…"}, ${spec.maximum ?? "…"}]`);
|
||||
}
|
||||
return `<tr>
|
||||
<td><code>${esc(name)}</code></td>
|
||||
<td>${esc(type)}</td>
|
||||
<td>${esc(req)}</td>
|
||||
<td>${esc(desc)}${extra.length ? `<div class="ws-muted">${esc(extra.join(" · "))}</div>` : ""}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("")
|
||||
: "";
|
||||
|
||||
return `<div class="skill-sub">
|
||||
<div class="skill-sub-title">产物格式</div>
|
||||
${skillChipRow([fmt.格式 && String(fmt.格式), fmt.批量时 && `批量 · ${fmt.批量时}`])}
|
||||
${
|
||||
fields
|
||||
? `<div class="skill-table-wrap"><table class="skill-field-table"><thead><tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr></thead><tbody>${fields}</tbody></table></div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
fmt.示例形状 && typeof fmt.示例形状 === "object"
|
||||
? `<details class="skill-example"><summary>示例形状</summary><pre class="json-pretty"><code>${highlightJson(JSON.stringify(fmt.示例形状, null, 2))}</code></pre></details>`
|
||||
: ""
|
||||
}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** 舞台骨架 · 专用正文卡 */
|
||||
function renderWorldBlueprintBodyHtml(body) {
|
||||
const parts = [];
|
||||
if (Array.isArray(body.依据的体验) && body.依据的体验.length) {
|
||||
parts.push(skillSection("依据的体验", skillProseList(body.依据的体验)));
|
||||
}
|
||||
if (body.舞台尺度) {
|
||||
parts.push(skillSection("舞台尺度", skillKvBlock(body.舞台尺度)));
|
||||
}
|
||||
if (body.基底与变造) {
|
||||
parts.push(skillSection("基底与变造", skillKvBlock(body.基底与变造)));
|
||||
}
|
||||
|
||||
const social = Array.isArray(body.社会结构) ? body.社会结构 : [];
|
||||
if (social.length) {
|
||||
parts.push(
|
||||
skillSection(
|
||||
"社会结构",
|
||||
`<div class="skill-entity-grid">${social
|
||||
.map((s) => {
|
||||
if (!s || typeof s !== "object") return "";
|
||||
return `<article class="skill-entity-card">
|
||||
<div class="skill-entity-name">${esc(String(s.名称 || "?"))}</div>
|
||||
${skillChipRow([s.性质, s.细化程度])}
|
||||
${s.在舞台上的位置 ? `<p>${esc(clampPreviewText(String(s.在舞台上的位置), 140))}</p>` : ""}
|
||||
${s.服务体验 ? `<p class="ws-muted">服务:${esc(clampPreviewText(String(s.服务体验), 100))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.join("")}</div>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const status = Array.isArray(body.世界状况) ? body.世界状况 : [];
|
||||
if (status.length) {
|
||||
parts.push(
|
||||
skillSection(
|
||||
"世界状况",
|
||||
`<div class="skill-entity-grid">${status
|
||||
.map((s) => {
|
||||
if (!s || typeof s !== "object") return "";
|
||||
return `<article class="skill-entity-card">
|
||||
<div class="skill-entity-name">${esc(String(s.名称 || "?"))}</div>
|
||||
${skillChipRow([s.性质, s.作用范围, s.节奏或触发])}
|
||||
${s.是什么 ? `<p>${esc(clampPreviewText(String(s.是什么), 140))}</p>` : ""}
|
||||
${s.如何影响运转 ? `<p class="ws-muted">${esc(clampPreviewText(String(s.如何影响运转), 120))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.join("")}</div>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const zones = Array.isArray(body.关键舞台区) ? body.关键舞台区 : [];
|
||||
if (zones.length) {
|
||||
parts.push(
|
||||
skillSection(
|
||||
"关键舞台区",
|
||||
`<div class="skill-entity-grid">${zones
|
||||
.map((z) => {
|
||||
if (!z || typeof z !== "object") return "";
|
||||
return `<article class="skill-entity-card">
|
||||
<div class="skill-entity-name">${esc(String(z.名称 || "?"))}</div>
|
||||
${z.是什么 ? `<p>${esc(clampPreviewText(String(z.是什么), 120))}</p>` : ""}
|
||||
${z.为何需要 ? `<p class="ws-muted">${esc(clampPreviewText(String(z.为何需要), 100))}</p>` : ""}
|
||||
${Array.isArray(z.格局要点) && z.格局要点.length ? skillProseList(z.格局要点) : ""}
|
||||
</article>`;
|
||||
})
|
||||
.join("")}</div>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(body.未展开范围) && body.未展开范围.length) {
|
||||
parts.push(skillSection("未展开范围", skillProseList(body.未展开范围)));
|
||||
}
|
||||
if (body.覆盖检验 && typeof body.覆盖检验 === "object") {
|
||||
parts.push(skillSection("覆盖检验", skillKvBlock(body.覆盖检验)));
|
||||
}
|
||||
|
||||
const used = new Set([
|
||||
"依据的体验",
|
||||
"舞台尺度",
|
||||
"基底与变造",
|
||||
"社会结构",
|
||||
"世界状况",
|
||||
"关键舞台区",
|
||||
"未展开范围",
|
||||
"覆盖检验",
|
||||
]);
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (used.has(k) || v == null || v === "") continue;
|
||||
parts.push(skillSection(k, renderStructuredValueHtml(v, 0)));
|
||||
}
|
||||
return parts.length ? `<div class="skill-view skill-view-world-blueprint">${parts.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
/** 实现机制 · 专用正文卡 */
|
||||
function renderMechanismBodyHtml(body) {
|
||||
const parts = [];
|
||||
if (Array.isArray(body.依据的核心体验) && body.依据的核心体验.length) {
|
||||
parts.push(skillSection("依据的核心体验", skillProseList(body.依据的核心体验)));
|
||||
}
|
||||
const points = Array.isArray(body.支撑点) ? body.支撑点 : [];
|
||||
if (points.length) {
|
||||
parts.push(
|
||||
skillSection(
|
||||
"支撑点",
|
||||
`<div class="skill-entity-grid">${points
|
||||
.map((p) => {
|
||||
if (!p || typeof p !== "object") return "";
|
||||
const pct = parseCompleteness(p.完备度);
|
||||
const mini = pct != null ? renderMiniPctHtml(pct) : "";
|
||||
const morph = p.切面形态 && typeof p.切面形态 === "object" ? p.切面形态 : null;
|
||||
return `<article class="skill-entity-card">
|
||||
<header class="skill-entity-head"><div class="skill-entity-name">${esc(String(p.名称 || "?"))}</div>${mini}</header>
|
||||
${skillChipRow([p.归属元素, p.支撑切面])}
|
||||
${morph?.概述 ? `<p>${esc(clampPreviewText(String(morph.概述), 140))}</p>` : ""}
|
||||
${p.如何支撑 ? `<p class="ws-muted">${esc(clampPreviewText(String(p.如何支撑), 120))}</p>` : ""}
|
||||
${p.缺失后果 ? `<p class="artifact-fact pending">缺失:${esc(clampPreviewText(String(p.缺失后果), 100))}</p>` : ""}
|
||||
</article>`;
|
||||
})
|
||||
.join("")}</div>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const rels = Array.isArray(body.支撑点关系) ? body.支撑点关系 : [];
|
||||
if (rels.length) {
|
||||
parts.push(
|
||||
skillSection(
|
||||
"支撑点关系",
|
||||
`<ul class="artifact-list">${rels
|
||||
.map((r) => {
|
||||
if (!r || typeof r !== "object") return "";
|
||||
const who = Array.isArray(r.涉及) ? r.涉及.join(" · ") : "";
|
||||
return `<li><strong>${esc(String(r.关系 || "关系"))}</strong>${who ? ` · ${esc(who)}` : ""}${
|
||||
r.体验作用 ? `<div class="ws-muted">${esc(String(r.体验作用))}</div>` : ""
|
||||
}</li>`;
|
||||
})
|
||||
.join("")}</ul>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (body.覆盖检验 && typeof body.覆盖检验 === "object") {
|
||||
parts.push(skillSection("覆盖检验", skillKvBlock(body.覆盖检验)));
|
||||
}
|
||||
const used = new Set(["依据的核心体验", "支撑点", "支撑点关系", "覆盖检验"]);
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (used.has(k) || v == null || v === "") continue;
|
||||
parts.push(skillSection(k, renderStructuredValueHtml(v, 0)));
|
||||
}
|
||||
return parts.length ? `<div class="skill-view skill-view-mechanism">${parts.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 完备度等:0–100 百分制(正文诊断块仍用 %)。
|
||||
* 自评维度请用 parseTenScore。
|
||||
*/
|
||||
function parseCompleteness(v) {
|
||||
if (v == null) return null;
|
||||
if (typeof v === "number" && Number.isFinite(v)) {
|
||||
@@ -1349,6 +1805,38 @@ function parseCompleteness(v) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 自评分数:0–10;兼容旧百分数(>10…100 → /10) */
|
||||
function parseTenScore(v) {
|
||||
if (v == null) return null;
|
||||
const fromNumber = (n) => {
|
||||
if (!Number.isFinite(n) || n < 0) return null;
|
||||
if (n <= 10) return Math.round(n * 10) / 10;
|
||||
if (n <= 100) return Math.round((n / 10) * 10) / 10;
|
||||
return null;
|
||||
};
|
||||
if (typeof v === "number") return fromNumber(v);
|
||||
const s = String(v).trim();
|
||||
if (!s) return null;
|
||||
const slash = s.match(/^(\d+(?:\.\d+)?)\s*\/\s*10$/i);
|
||||
if (slash) return fromNumber(Number(slash[1]));
|
||||
const withPct = s.match(/^(\d+(?:\.\d+)?)\s*%$/);
|
||||
if (withPct) return fromNumber(Number(withPct[1]));
|
||||
const bare = s.match(/^(\d+(?:\.\d+)?)$/);
|
||||
if (bare) return fromNumber(Number(bare[1]));
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatTenScore(ten) {
|
||||
if (ten == null || !Number.isFinite(ten)) return "?";
|
||||
return Number.isInteger(ten) ? String(ten) : String(Math.round(ten * 10) / 10);
|
||||
}
|
||||
|
||||
function tenTone(ten) {
|
||||
if (ten < 4) return "low";
|
||||
if (ten < 7) return "mid";
|
||||
return "high";
|
||||
}
|
||||
|
||||
function pctTone(pct) {
|
||||
if (pct < 40) return "low";
|
||||
if (pct < 70) return "mid";
|
||||
@@ -1369,6 +1857,22 @@ function renderPctMeterHtml(pct, label = "完备度") {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** 自评十分制条 */
|
||||
function renderTenMeterHtml(ten, label = "评分") {
|
||||
if (ten == null || !Number.isFinite(ten)) return "";
|
||||
const score = Math.max(0, Math.min(10, ten));
|
||||
const width = Math.round(score * 10);
|
||||
const tone = tenTone(score);
|
||||
const shown = formatTenScore(score);
|
||||
return `<div class="pct-meter tone-${tone}" role="meter" aria-valuenow="${score}" aria-valuemin="0" aria-valuemax="10" aria-label="${esc(label)} ${shown}/10">
|
||||
<div class="pct-meter-head">
|
||||
<span class="pct-meter-label">${esc(label)}</span>
|
||||
<span class="pct-meter-value">${shown}/10</span>
|
||||
</div>
|
||||
<div class="pct-meter-track"><i style="width:${width}%"></i></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/** 瓷砖用迷你百分比(数字 + 细条),省纵向空间 */
|
||||
function renderMiniPctHtml(pct) {
|
||||
if (pct == null || !Number.isFinite(pct)) return "";
|
||||
@@ -1559,14 +2063,14 @@ function renderSelfScoreHtml(自评) {
|
||||
.map((d) => {
|
||||
if (!d || typeof d !== "object") return "";
|
||||
const name = d.名 || d.维度 || d.name || "?";
|
||||
const pct = parseCompleteness(d.分数);
|
||||
const ten = parseTenScore(d.分数);
|
||||
const note = d.说明
|
||||
? `<p class="artifact-score-note">${esc(clampPreviewText(String(d.说明), 80))}</p>`
|
||||
: "";
|
||||
if (pct == null) {
|
||||
if (ten == null) {
|
||||
return `<div class="artifact-score"><div class="pct-meter-head"><span class="pct-meter-label">${esc(String(name))}</span><span class="ws-muted">未评分</span></div>${note}</div>`;
|
||||
}
|
||||
return `<div class="artifact-score">${renderPctMeterHtml(pct, String(name))}${note}</div>`;
|
||||
return `<div class="artifact-score">${renderTenMeterHtml(ten, String(name))}${note}</div>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let weak = "";
|
||||
@@ -2085,6 +2589,10 @@ function renderReviewFeedCard(review, opts = {}) {
|
||||
const artifactHtml = formatArtifactBodyHtml(review.body, {
|
||||
defaultOpen: true,
|
||||
hideAskSidecar: opts.hideAskSidecar === true,
|
||||
asPresentFallback:
|
||||
review.workerId === "narrator" ||
|
||||
review.workerId === "opening-generator" ||
|
||||
/用户展示|开场白/.test(review.body || ""),
|
||||
});
|
||||
const contextId = review.sourceMessageId || review.id || "";
|
||||
const hasContext = review.contextTrace ? "1" : "0";
|
||||
@@ -2293,7 +2801,9 @@ export function renderMessageFeed(view, loading, handlers = {}) {
|
||||
? formatQuestionsHtml(body)
|
||||
: kind === "worker_output"
|
||||
? formatArtifactBodyHtml(body)
|
||||
: esc(body)
|
||||
: shouldRenderPlayPresent(msg, view)
|
||||
? formatPlayPresentHtml(body, view)
|
||||
: esc(body)
|
||||
}</div>
|
||||
${isUser ? `<footer class="msg-foot">${headInner}</footer>` : ""}
|
||||
</div>`;
|
||||
|
||||
189
web/present-shells.js
Normal file
189
web/present-shells.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 游玩呈现壳渲染(P2 最小适配)
|
||||
* 契约:docs/play-presentation-shells.md · present.v1
|
||||
*/
|
||||
|
||||
export const PRESENT_SHELL_IDS = [
|
||||
"prose",
|
||||
"chat_monitor",
|
||||
"turn_panel",
|
||||
"chapter_reader",
|
||||
];
|
||||
|
||||
const SHELL_LABELS = {
|
||||
prose: "纯散文",
|
||||
chat_monitor: "对话+监控",
|
||||
turn_panel: "回合面板",
|
||||
chapter_reader: "章节阅读",
|
||||
};
|
||||
|
||||
const REGION_LABELS = {
|
||||
monitor: "监控",
|
||||
header: "抬头",
|
||||
body: "正文",
|
||||
footer: "文末",
|
||||
aside: "侧栏",
|
||||
hidden: "隐藏",
|
||||
};
|
||||
|
||||
function isPresentShellId(v) {
|
||||
return typeof v === "string" && PRESENT_SHELL_IDS.includes(v);
|
||||
}
|
||||
|
||||
function shellDefaultRegions(shell) {
|
||||
switch (shell) {
|
||||
case "chat_monitor":
|
||||
return ["monitor", "body", "footer"];
|
||||
case "turn_panel":
|
||||
return ["monitor", "header", "body", "footer", "aside"];
|
||||
case "chapter_reader":
|
||||
return ["header", "body", "aside", "footer"];
|
||||
default:
|
||||
return ["body"];
|
||||
}
|
||||
}
|
||||
|
||||
function formatBlockContent(value) {
|
||||
if (value === undefined || value === null) return "";
|
||||
if (typeof value === "string") return value;
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((x) => (typeof x === "string" ? x : JSON.stringify(x)))
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return Object.entries(value)
|
||||
.map(([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`)
|
||||
.join("\n");
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function normalizeBlocks(raw) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
||||
const out = {};
|
||||
for (const k of ["monitor", "header", "body", "footer", "aside", "hidden"]) {
|
||||
if (raw[k] !== undefined) out[k] = raw[k];
|
||||
}
|
||||
if (out.body === undefined && raw.正文 !== undefined) out.body = raw.正文;
|
||||
if (out.monitor === undefined && raw.监控 !== undefined) out.monitor = raw.监控;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ ok: boolean, packet: object, fallbackPlain: boolean } | null}
|
||||
* null = 不是 present 包(交给其它渲染器)
|
||||
*/
|
||||
export function parsePresentDoc(doc, fallbackShell = "prose") {
|
||||
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
||||
const looks =
|
||||
doc.schema === "present.v1" ||
|
||||
(isPresentShellId(doc.shell) && doc.blocks != null) ||
|
||||
(isPresentShellId(doc.shell_id) && doc.blocks != null);
|
||||
if (!looks) return null;
|
||||
|
||||
const shell = isPresentShellId(doc.shell)
|
||||
? doc.shell
|
||||
: isPresentShellId(doc.shell_id)
|
||||
? doc.shell_id
|
||||
: fallbackShell;
|
||||
const meta =
|
||||
doc.meta && typeof doc.meta === "object" && !Array.isArray(doc.meta)
|
||||
? doc.meta
|
||||
: {};
|
||||
return {
|
||||
ok: true,
|
||||
fallbackPlain: false,
|
||||
packet: {
|
||||
schema: "present.v1",
|
||||
shell,
|
||||
blocks: normalizeBlocks(doc.blocks ?? doc.区域),
|
||||
meta: {
|
||||
suggested_actions: Array.isArray(meta.suggested_actions)
|
||||
? meta.suggested_actions.filter((x) => typeof x === "string")
|
||||
: Array.isArray(doc.suggested_actions)
|
||||
? doc.suggested_actions.filter((x) => typeof x === "string")
|
||||
: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 纯文本 → prose 包(供调用方在非 JSON 时使用) */
|
||||
export function presentFromPlain(text, shell = "prose") {
|
||||
return {
|
||||
schema: "present.v1",
|
||||
shell: isPresentShellId(shell) ? shell : "prose",
|
||||
blocks: { body: text ?? "" },
|
||||
meta: { suggested_actions: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} packet present.v1
|
||||
* @param {(s: string) => string} esc
|
||||
* @param {{ tweaks?: object }} [opts]
|
||||
*/
|
||||
export function renderPresentShellHtml(packet, esc, opts = {}) {
|
||||
if (!packet || typeof packet !== "object") return "";
|
||||
const shell = isPresentShellId(packet.shell) ? packet.shell : "prose";
|
||||
const tweaks = opts.tweaks ?? {};
|
||||
const labels = {
|
||||
...(tweaks.block_labels && typeof tweaks.block_labels === "object"
|
||||
? tweaks.block_labels
|
||||
: {}),
|
||||
};
|
||||
const emptyStates =
|
||||
tweaks.empty_states && typeof tweaks.empty_states === "object"
|
||||
? tweaks.empty_states
|
||||
: {};
|
||||
const showActions =
|
||||
typeof tweaks.show_suggested_actions === "boolean"
|
||||
? tweaks.show_suggested_actions
|
||||
: shell === "turn_panel";
|
||||
const tone =
|
||||
typeof tweaks.tone_chrome === "string" && tweaks.tone_chrome.trim()
|
||||
? tweaks.tone_chrome.trim()
|
||||
: "default";
|
||||
|
||||
const blocks = packet.blocks ?? {};
|
||||
const regions = shellDefaultRegions(shell);
|
||||
const parts = [];
|
||||
|
||||
for (const region of regions) {
|
||||
if (region === "hidden") continue;
|
||||
let text = formatBlockContent(blocks[region]).trim();
|
||||
if (!text && emptyStates[region]) text = String(emptyStates[region]);
|
||||
if (!text && region !== "body") continue;
|
||||
if (!text && region === "body") text = "(暂无正文)";
|
||||
|
||||
const title =
|
||||
labels[region] ||
|
||||
labels[REGION_LABELS[region]] ||
|
||||
REGION_LABELS[region] ||
|
||||
region;
|
||||
const isBody = region === "body";
|
||||
parts.push(`
|
||||
<section class="present-region present-region--${esc(region)}" data-region="${esc(region)}">
|
||||
${isBody && shell === "prose" ? "" : `<h4 class="present-region-title">${esc(title)}</h4>`}
|
||||
<div class="present-region-body">${esc(text).replace(/\n/g, "<br>")}</div>
|
||||
</section>`);
|
||||
}
|
||||
|
||||
const actions = showActions ? packet.meta?.suggested_actions ?? [] : [];
|
||||
if (actions.length) {
|
||||
parts.push(`
|
||||
<section class="present-region present-region--actions" data-region="actions">
|
||||
<h4 class="present-region-title">建议行动</h4>
|
||||
<ul class="present-actions">${actions.map((a) => `<li>${esc(a)}</li>`).join("")}</ul>
|
||||
</section>`);
|
||||
}
|
||||
|
||||
const shellLabel = SHELL_LABELS[shell] || shell;
|
||||
return `
|
||||
<div class="present-shell present-shell--${esc(shell)} present-tone--${esc(tone)}" data-shell="${esc(shell)}">
|
||||
<div class="present-shell-badge" title="呈现壳">${esc(shellLabel)}</div>
|
||||
${parts.join("")}
|
||||
</div>`;
|
||||
}
|
||||
243
web/styles.css
243
web/styles.css
@@ -817,6 +817,170 @@ body.rail-collapsed .af-mosaic {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 鎶€鑳戒笓鐢ㄦ<E990A2>鏂囧崱 */
|
||||
.skill-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.skill-card-section > h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.skill-kv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.skill-kv-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 120px) 1fr;
|
||||
gap: 8px 12px;
|
||||
align-items: start;
|
||||
}
|
||||
.skill-kv-key {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
padding-top: 2px;
|
||||
}
|
||||
.skill-kv-val {
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.skill-kv-row { grid-template-columns: 1fr; gap: 2px; }
|
||||
}
|
||||
.skill-verdict {
|
||||
margin: 0 0 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.skill-verdict.tone-ok {
|
||||
background: color-mix(in srgb, var(--ok) 14%, var(--surface));
|
||||
color: var(--ok);
|
||||
}
|
||||
.skill-verdict.tone-warn {
|
||||
background: color-mix(in srgb, var(--warn) 18%, var(--surface));
|
||||
color: color-mix(in srgb, var(--warn) 80%, #000);
|
||||
}
|
||||
.skill-rule-stack,
|
||||
.skill-entity-grid,
|
||||
.skill-pool-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.skill-entity-grid,
|
||||
.skill-pool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.skill-rule-card,
|
||||
.skill-entity-card,
|
||||
.skill-pool-card {
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--surface) 92%, var(--bg));
|
||||
}
|
||||
.skill-rule-head,
|
||||
.skill-entity-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.skill-rule-title,
|
||||
.skill-entity-name,
|
||||
.skill-pool-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.skill-sub {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
|
||||
}
|
||||
.skill-sub-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.skill-method {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.skill-method-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.skill-table-wrap {
|
||||
overflow-x: auto;
|
||||
margin-top: 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.skill-field-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.skill-field-table th,
|
||||
.skill-field-table td {
|
||||
padding: 7px 9px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
.skill-field-table th {
|
||||
background: var(--bg);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
.skill-field-table tr:last-child td { border-bottom: 0; }
|
||||
.skill-field-table code {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.skill-example {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.skill-example summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.skill-pool-desc {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.artifact-parse-hint {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.skill-entity-card > p {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.pct-mini {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1836,3 +2000,82 @@ body.rail-collapsed .af-mosaic {
|
||||
body:not(.rail-collapsed) .path-bar { display: none !important; }
|
||||
body:not(.rail-collapsed) .rail-panels { display: none; }
|
||||
}
|
||||
|
||||
/* —— 游玩呈现壳 present.v1(P2 最小) —— */
|
||||
.present-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
padding: 10px 12px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.present-shell-badge {
|
||||
align-self: flex-start;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
padding: 1px 7px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
.present-region {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.present-region-title {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.present-region-body {
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.present-region--monitor .present-region-body,
|
||||
.present-region--aside .present-region-body {
|
||||
font-size: 13px;
|
||||
color: var(--text, inherit);
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
.present-shell--chat_monitor .present-region--monitor {
|
||||
order: -1;
|
||||
}
|
||||
.present-shell--turn_panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.present-shell--chapter_reader .present-region--header .present-region-body {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.present-actions {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
line-height: 1.45;
|
||||
font-size: 13px;
|
||||
}
|
||||
.present-tone--messenger .present-region--body {
|
||||
border-left: 3px solid var(--border);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.present-tone--book .present-region--body {
|
||||
max-width: 42rem;
|
||||
}
|
||||
.present-tone--terminal .present-region-body {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user