壳完善

This commit is contained in:
2026-08-14 14:25:31 +08:00
parent 120d67ee82
commit 20938b04d7
24 changed files with 1495 additions and 426 deletions

View File

@@ -370,9 +370,15 @@ function formatPlayPresentHtml(body, view) {
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,
)
[
"prose",
"chat_monitor",
"spotlight",
"turn_panel",
"split_board",
"choice_dock",
"chapter_reader",
].includes(tweaks.shell_id)
? tweaks.shell_id
: "prose";
const parsed = tryParseJsonDoc(trimmed);
@@ -1404,6 +1410,14 @@ function renderSpecialtyBodyHtml(body, skill) {
const html = renderMechanismBodyHtml(body);
if (html) return html;
}
if (
body.风格与写法 != null ||
body.推进与决策 != null ||
skillName.includes("叙事指南")
) {
const html = renderNarrativeBodyHtml(body);
if (html) return html;
}
const looksAesthetics =
body.设定逻辑 != null ||
@@ -1432,6 +1446,60 @@ function renderSpecialtyBodyHtml(body, skill) {
return parts.length ? `<div class="af-mosaic">${parts.join("")}</div>` : "";
}
/** 叙事指南 · 一份全文结构化卡(不强调双投影裁剪) */
function renderNarrativeBodyHtml(body) {
const order = [
"依据的体验",
"叙事纲领",
"风格与遣词",
"笔墨焦点",
"禁忌与不偏好",
"情境备用",
"推进与决策",
"内容与表达",
"风格与写法",
];
const used = new Set();
const parts = [];
// 若仍是旧的「风格与写法 / 推进与决策」两大包,整块展示
if (body.风格与写法 != null || body.推进与决策 != null) {
if (Array.isArray(body.依据的体验) && body.依据的体验.length) {
parts.push(
`<section class="af-panel"><h4>依据的体验</h4>${skillProseList(body.依据的体验)}</section>`,
);
used.add("依据的体验");
}
if (body.风格与写法 != null) {
parts.push(
`<section class="af-panel af-panel-wide"><h4>风格与写法</h4>${renderStructuredValueHtml(body.风格与写法, 0)}</section>`,
);
used.add("风格与写法");
}
if (body.推进与决策 != null) {
parts.push(
`<section class="af-panel af-panel-wide"><h4>推进与决策</h4>${renderStructuredValueHtml(body.推进与决策, 0)}</section>`,
);
used.add("推进与决策");
}
} else {
for (const key of order) {
if (body[key] == null) continue;
used.add(key);
const wide = key === "风格与遣词" || key === "推进与决策";
parts.push(
`<section class="af-panel${wide ? " af-panel-wide" : ""}"><h4>${esc(key)}</h4>${renderStructuredValueHtml(body[key], 0)}</section>`,
);
}
}
for (const [k, v] of Object.entries(body)) {
if (used.has(k) || v == null || v === "") continue;
parts.push(
`<section class="af-panel"><h4>${esc(k)}</h4>${renderStructuredValueHtml(v, 0)}</section>`,
);
}
return parts.length ? `<div class="af-mosaic af-narrative">${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>`;

View File

@@ -61,6 +61,7 @@
</div>
<div class="main-head-right">
<a class="link" href="/modules.html">技能</a>
<a class="link" href="/shells.html">呈现壳</a>
<a class="link" href="/stats.html">Token</a>
<a class="link" href="/settings.html">设置</a>
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>

View File

@@ -13,6 +13,7 @@
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html" aria-current="page">技能</a>
<a href="/shells.html">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html">设置</a>
</nav>

View File

@@ -1,19 +1,31 @@
/**
* 游玩呈现壳渲染P2 最小适配
* 游玩呈现壳渲染P2
* 契约docs/play-presentation-shells.md · present.v1
*
* 信息层级约定(所有壳共用):
* P0 body — 最大字号与面积,阅读主轴
* P1 actions — 需要决策时醒目,但不压过正文
* P2 monitor — 芯片密排,一眼扫完
* P3 header / footer / aside — 辅助,紧凑
*/
export const PRESENT_SHELL_IDS = [
"prose",
"chat_monitor",
"spotlight",
"turn_panel",
"split_board",
"choice_dock",
"chapter_reader",
];
const SHELL_LABELS = {
prose: "纯散文",
chat_monitor: "对话+监控",
spotlight: "场面主视",
turn_panel: "回合面板",
split_board: "双栏看板",
choice_dock: "选择坞",
chapter_reader: "章节阅读",
};
@@ -26,20 +38,30 @@ const REGION_LABELS = {
hidden: "隐藏",
};
/** 默认开建议行动的壳 */
const ACTIONS_DEFAULT_ON = new Set(["turn_panel", "choice_dock"]);
function isPresentShellId(v) {
return typeof v === "string" && PRESENT_SHELL_IDS.includes(v);
}
function shellDefaultRegions(shell) {
export function shellDefaultRegions(shell) {
switch (shell) {
case "chat_monitor":
return ["monitor", "body", "footer"];
case "spotlight":
return ["monitor", "body", "footer"];
case "turn_panel":
return ["monitor", "header", "body", "footer", "aside"];
return ["monitor", "header", "body", "aside", "footer"];
case "split_board":
return ["header", "body", "aside", "footer"];
case "choice_dock":
return ["monitor", "body", "footer"];
case "chapter_reader":
return ["header", "body", "aside", "footer"];
case "prose":
default:
return ["body"];
return ["body", "footer"];
}
}
@@ -71,9 +93,35 @@ function normalizeBlocks(raw) {
return out;
}
function renderChipsHtml(value, esc) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const entries = Object.entries(value).filter(([k]) => k != null && String(k).trim());
if (!entries.length) return null;
return `<div class="present-chips" role="list">${entries
.map(
([k, v]) =>
`<span class="present-chip" role="listitem"><span class="present-chip-k">${esc(k)}</span><span class="present-chip-v">${esc(
typeof v === "string" ? v : JSON.stringify(v),
)}</span></span>`,
)
.join("")}</div>`;
}
function regionInnerHtml(region, rawValue, esc) {
if (region === "monitor") {
const chips = renderChipsHtml(rawValue, esc);
if (chips) return chips;
}
if (region === "aside" && rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)) {
const chips = renderChipsHtml(rawValue, esc);
if (chips) return chips;
}
const text = formatBlockContent(rawValue).trim();
return `<div class="present-region-body">${esc(text).replace(/\n/g, "<br>")}</div>`;
}
/**
* @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;
@@ -110,7 +158,6 @@ export function parsePresentDoc(doc, fallbackShell = "prose") {
};
}
/** 纯文本 → prose 包(供调用方在非 JSON 时使用) */
export function presentFromPlain(text, shell = "prose") {
return {
schema: "present.v1",
@@ -120,6 +167,65 @@ export function presentFromPlain(text, shell = "prose") {
};
}
function regionTitle(region, labels, _shell) {
// 正文默认不挂标题(面积留给文字);创作填了 block_labels.body 才显示
if (region === "body") {
return labels.body || labels["正文"] || null;
}
return (
labels[region] ||
labels[REGION_LABELS[region]] ||
REGION_LABELS[region] ||
region
);
}
function renderRegion(region, rawValue, esc, { labels, emptyStates, shell }) {
let hasContent =
rawValue !== undefined &&
rawValue !== null &&
(typeof rawValue === "string"
? rawValue.trim()
: Array.isArray(rawValue)
? rawValue.length
: typeof rawValue === "object"
? Object.keys(rawValue).length
: true);
let displayRaw = rawValue;
if (!hasContent && emptyStates[region]) {
displayRaw = String(emptyStates[region]);
hasContent = true;
}
if (!hasContent && region !== "body") return "";
if (!hasContent && region === "body") displayRaw = "(暂无正文)";
const title = regionTitle(region, labels, shell);
const titleHtml = title
? `<h4 class="present-region-title">${esc(title)}</h4>`
: "";
return `
<section class="present-region present-region--${esc(region)}" data-region="${esc(region)}">
${titleHtml}
${regionInnerHtml(region, displayRaw, esc)}
</section>`;
}
function renderActionsHtml(actions, esc) {
if (!actions.length) return "";
return `
<section class="present-region present-region--actions" data-region="actions">
<h4 class="present-region-title">建议行动</h4>
<div class="present-action-row" role="list">${actions
.map(
(a) =>
`<span class="present-action" role="listitem">${esc(a)}</span>`,
)
.join("")}</div>
</section>`;
}
/**
* @param {object} packet present.v1
* @param {(s: string) => string} esc
@@ -141,7 +247,7 @@ export function renderPresentShellHtml(packet, esc, opts = {}) {
const showActions =
typeof tweaks.show_suggested_actions === "boolean"
? tweaks.show_suggested_actions
: shell === "turn_panel";
: ACTIONS_DEFAULT_ON.has(shell);
const tone =
typeof tweaks.tone_chrome === "string" && tweaks.tone_chrome.trim()
? tweaks.tone_chrome.trim()
@@ -149,41 +255,96 @@ export function renderPresentShellHtml(packet, esc, opts = {}) {
const blocks = packet.blocks ?? {};
const regions = shellDefaultRegions(shell);
const parts = [];
const ctx = { labels, emptyStates, shell };
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 by = {};
for (const r of regions) {
by[r] = renderRegion(r, blocks[r], esc, ctx);
}
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 actionsHtml = renderActionsHtml(actions, esc);
let layoutHtml = "";
switch (shell) {
case "chat_monitor":
layoutHtml = `
<div class="present-stack">
${by.monitor || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
case "spotlight":
layoutHtml = `
<div class="present-stack present-stack--spotlight">
${by.monitor || ""}
<div class="present-stage present-stage--hero">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
case "turn_panel":
layoutHtml = `
<div class="present-board">
<div class="present-board-top">${by.monitor || ""}${by.header || ""}</div>
<div class="present-board-mid">
<div class="present-stage">${by.body || ""}</div>
<div class="present-board-side">${by.aside || ""}${by.footer || ""}</div>
</div>
${actionsHtml}
</div>`;
break;
case "split_board":
layoutHtml = `
<div class="present-split">
<div class="present-split-main">
${by.header || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
</div>
<div class="present-split-side">${by.aside || ""}</div>
${actionsHtml}
</div>`;
break;
case "choice_dock":
layoutHtml = `
<div class="present-stack present-stack--dock">
${by.monitor || ""}
<div class="present-stage">${by.body || ""}</div>
${by.footer || ""}
<div class="present-dock">${actionsHtml}</div>
</div>`;
break;
case "chapter_reader":
layoutHtml = `
<div class="present-reader">
<div class="present-reader-main">
${by.header || ""}
<div class="present-stage present-stage--read">${by.body || ""}</div>
${by.footer || ""}
</div>
<div class="present-reader-aside">${by.aside || ""}</div>
</div>`;
break;
case "prose":
default:
layoutHtml = `
<div class="present-stack present-stack--prose">
<div class="present-stage present-stage--read">${by.body || ""}</div>
${by.footer || ""}
${actionsHtml}
</div>`;
break;
}
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("")}
${layoutHtml}
</div>`;
}
export { SHELL_LABELS, REGION_LABELS, isPresentShellId };

View File

@@ -12,6 +12,7 @@
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html">技能</a>
<a href="/shells.html">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html" aria-current="page">设置</a>
</nav>

173
web/shells.css Normal file
View File

@@ -0,0 +1,173 @@
/* 呈现壳预览页
* 注意:本页也引入 styles.csspresent 样式),会给 body 设 overflow:hidden
* 必须在此覆盖,否则侧栏后几项点不到。
*/
html,
body.shells-page {
height: auto;
min-height: 100%;
overflow: auto;
}
.shells-page .st-shell.shells-shell {
max-width: 1180px;
align-items: start;
}
.shells-page .st-rail {
--st-rail-w: 220px;
width: var(--st-rail-w);
position: sticky;
top: var(--st-top-h, 48px);
max-height: calc(100dvh - var(--st-top-h, 48px));
overflow-y: auto;
min-height: 0;
align-self: start;
}
.shells-tone-label {
margin-top: 1rem !important;
}
.shells-density {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
align-items: baseline;
margin: 0.5rem 0 0.35rem;
padding: 8px 10px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent-soft, #e8f0fe) 70%, transparent);
font-size: 13px;
line-height: 1.45;
}
.shells-density span {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--muted, #666);
text-transform: uppercase;
}
.shells-list {
display: flex;
flex-direction: column;
gap: 4px;
flex: 0 0 auto;
}
.shells-tones {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.shells-tone-chip {
font: inherit;
font-size: 12px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--border, #ddd);
background: var(--surface, #fff);
cursor: pointer;
color: inherit;
}
.shells-tone-chip.active {
border-color: transparent;
background: var(--accent-soft, #e8f0fe);
font-weight: 600;
}
.shells-body {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.shells-meta p {
margin: 0 0 0.35rem;
line-height: 1.5;
}
.shells-preview-label,
.shells-tweaks h2,
.shells-help h2 {
margin: 0 0 0.5rem;
font-size: 13px;
font-weight: 700;
color: var(--muted, #666);
}
.shells-preview {
min-height: 280px;
padding: 16px;
border-radius: 12px;
border: 1px dashed var(--border, #ccc);
background:
radial-gradient(ellipse at 20% 0%, rgba(15, 118, 110, 0.06), transparent 50%),
var(--bg, #f6f6f4);
}
.shells-preview .present-shell {
max-width: 640px;
margin: 0 auto;
box-shadow: 0 10px 28px rgba(28, 25, 23, 0.07);
}
.shells-preview[data-shell="turn_panel"] .present-shell,
.shells-preview[data-shell="split_board"] .present-shell,
.shells-preview[data-shell="chapter_reader"] .present-shell {
max-width: 780px;
}
/* 预览区不裁切长正文 */
.shells-preview {
overflow: visible;
}
.shells-tweak-grid {
display: flex;
flex-direction: column;
gap: 10px;
}
.shells-tweak-grid .field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
}
.shells-tweak-grid input[type="text"] {
font: inherit;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--border, #ddd);
}
.shells-help ul {
margin: 0;
padding-left: 1.2rem;
line-height: 1.55;
font-size: 13px;
}
.shells-help .muted {
font-size: 12px;
color: var(--muted, #888);
}
@media (max-width: 800px) {
.shells-page .st-rail {
position: static;
max-height: none;
width: auto;
overflow: visible;
}
.shells-preview .present-shell {
max-width: 100%;
}
}

74
web/shells.html Normal file
View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>呈现壳预览 · Writing Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
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="/settings.css" />
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/shells.css" />
</head>
<body class="settings-page shells-page">
<header class="st-top">
<a class="st-brand" href="/">Writing Agent</a>
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html">技能</a>
<a href="/shells.html" aria-current="page">呈现壳</a>
<a href="/stats.html">Token</a>
<a href="/settings.html">设置</a>
</nav>
</header>
<div class="st-shell shells-shell">
<aside class="st-rail" aria-label="壳列表">
<p class="st-rail-label">呈现壳</p>
<nav class="shells-list" id="shell-list" role="tablist"></nav>
<p class="st-rail-label shells-tone-label">装饰语气</p>
<div class="shells-tones" id="tone-list" role="group" aria-label="tone_chrome"></div>
<p class="st-rail-label shells-tone-label">正文长度</p>
<div class="shells-tones" id="length-list" role="group" aria-label="body length"></div>
<p class="st-rail-note">
正文按 600~3000 字设计:随字数撑高、页面滚动;侧栏/选择坞钉住。创作只改内容绑定。
</p>
</aside>
<main class="st-main">
<header class="st-main-head">
<div>
<h1 id="panel-title">呈现壳预览</h1>
<p class="st-sub" id="panel-subtitle">用示例数据看布局与可调项</p>
</div>
</header>
<div class="st-body shells-body">
<section class="shells-meta" id="shell-meta"></section>
<section class="shells-preview-wrap">
<h2 class="shells-preview-label">预览</h2>
<div id="shell-preview" class="shells-preview"></div>
</section>
<section class="shells-tweaks">
<h2>本页可调(对应创作「微调轴」)</h2>
<div class="shells-tweak-grid" id="tweak-controls"></div>
</section>
<section class="shells-help">
<h2>设计意图</h2>
<ul>
<li>正文默认装得下单轮 <strong>600~3000 字</strong>:区域随内容长高,由页面/消息区滚动,不塞进固定小框。</li>
<li>双栏壳:侧栏 sticky选择坞选项区 sticky 贴底——长文读完控件还在。</li>
<li>壳差在「谁占主柱」;可改的是字段/显示名/行动开关/tone不是重画布局。</li>
</ul>
<p class="muted">说明文档:<code>docs/play-presentation-shells.md</code></p>
</section>
</div>
</main>
</div>
<script type="module" src="/shells.js"></script>
</body>
</html>

358
web/shells.js Normal file
View File

@@ -0,0 +1,358 @@
/**
* 呈现壳预览页:切换壳 / 装饰语气 / 正文长度,用示例数据渲染 present-shells。
* 正文设计目标:单轮常见 600~3000 字 —— 随字数撑高,外层滚动;侧栏/选项坞钉住。
*/
import {
PRESENT_SHELL_IDS,
renderPresentShellHtml,
} from "./present-shells.js";
const SHELL_INFO = {
prose: {
name: "纯散文",
blurb: "整块都是正文,无 HUD。长文直接往下滚。",
density: "正文随字数伸展;无侧栏争面积",
scenes: "轻对话、纯叙事、不要状态栏",
},
chat_monitor: {
name: "对话 + 监控",
blurb: "顶栏芯片固定矮;主聊/场面再长也只往下长。",
density: "监控一行;正文撑高滚动",
scenes: "网恋 / 日常扮演 / 轻 AIRP",
},
spotlight: {
name: "场面主视",
blurb: "芯片一行 + 大正文柱。适合一幕写满。",
density: "正文主导并伸展;监控不占高",
scenes: "电影感 RP、一幕一景、少机制",
},
turn_panel: {
name: "回合面板",
blurb: "场面叙述再长,右侧交互物钉在视口内对照。",
density: "正文撑高;侧栏 sticky行动在下",
scenes: "资源、检定、场景交互、跑团感",
},
split_board: {
name: "双栏看板",
blurb: "左栏长文滚动,右栏线索钉住——对照读。",
density: "左正文伸展;右侧栏 sticky",
scenes: "调查、推理、多线索并行",
},
choice_dock: {
name: "选择坞",
blurb: "先读完长局面;选项坞 sticky 贴视口底,不用翻回顶。",
density: "正文撑高;底部坞钉住",
scenes: "选项驱动、AVG、分支关口",
},
chapter_reader: {
name: "章节阅读",
blurb: "章题 + 长阅读柱;窄进度侧栏 sticky。",
density: "正文伸展为主;侧栏窄且钉住",
scenes: "长文 / 爽文 / 先纲后章",
},
};
const TONES = [
{ id: "default", label: "默认" },
{ id: "messenger", label: "讯息感" },
{ id: "book", label: "书页感" },
{ id: "terminal", label: "终端感" },
];
const LENGTHS = [
{ id: "short", label: "短(~120" },
{ id: "mid", label: "中(~800" },
{ id: "long", label: "长(~2500" },
];
/** 中等长度 ≈800 字 */
const BODY_MID = `雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。水从伞骨边缘连成线,砸在柏油路上,溅起细碎的白点。
路口的灯跳成绿色。你们一起迈步——这一次,谁也没有先松开。对面便利店的玻璃上映着两个人并肩的轮廓,忽明忽暗,像谁随手画的草稿。
「伞……」她终于开口,声音被雨声压得很低,「明天不用还。」
你点头,又觉得这回答太轻,补了一句:「那我请你喝热的?」她侧过脸,雨丝粘在睫毛上,笑意只闪了一下,像怕被看穿。
电梯门开的时候,楼道里干燥得过分。她把袋子换到另一只手,钥匙在指间转了一圈,忽然停住:「其实我今天本来想绕路的。」
「为什么?」
「因为预报说会停。结果没有。」她把钥匙推进锁孔,「所以——只好借你的伞了。」
门开了一条缝。暖光漏出来,照在你们脚边的水渍上。她没有立刻进去,只是抬眼看你,像在等一句还没说出口的话。你把伞收拢,水仍顺着伞尖滴到地毯边缘。谁也没有提「进来坐坐」,但空气里已经有了那句话的形状。`;
/** 长文 ≈2500 字(重复段落拼成,预览用) */
const BODY_LONG = `${BODY_MID}
天桥下的风从另一头灌过来,把塑料袋吹得啪啪响。你想起上一次走过这里,还是夏天,蝉声吵得人没法好好说话;如今只剩雨,以及偶尔驶过的车灯,把积水切成一条条亮痕。
便利店里的收音机断断续续播着交通路况。店员打着哈欠换班,玻璃门开合时带进一阵湿冷。你站在门口抖伞,余光看见货架尽头有人停了很久——拿起一瓶水,又放下,再拿起,像在跟自己商量一件比购物更大的事。
她的短信迟了三分钟才到:「到家了吗?」你盯着屏幕上的光标,打了又删:刚进小区 / 伞还在我这 / 明天……最后只留下一句:「刚到。伞明天给你。」她回了个省略号,又补:「不用那么快。」
你把手机塞回口袋,忽然觉得「不用那么快」四个字比天气预报更准。有些东西一旦说破就会散,只好让它停在半湿半干之间。
楼道灯感应得慢,你摸黑上了两级台阶才亮。隔壁门缝里漏出电视剧的笑声,被雨声削成模糊的边。你靠着墙站了一会儿,听自己的呼吸从急变匀,才继续往上走。
到家后你把伞撑在阳台,水沿着伞骨汇成细流,滴进盆里,一声一声,像在数今晚还剩多少没说清的话。窗玻璃上凝着雾,你用指尖随便划了一道,很快又糊住——外面的城市灯火依旧,只是被雨帘隔成另一层世界。
你泡了杯热水,坐到桌前,本想写点什么,笔尖却停在空白处。纸页吸走一点水汽,皱起细纹。你写下一句又涂掉:关于伞,关于绿灯,关于她说「绕路」时那种几乎听不见的停顿。最后只留下日期,和一行没写完的「明天」。
夜里雨势小了些。你关灯前又看了一眼阳台——伞还在,水还在滴,只是间隔更长了,像什么人放慢了脚步,却没有真的离开。
(——以下为加长段,模拟单轮 2000+ 字场面——)
次日清晨,天边只剩薄云。你带着伞出门,金属骨架在布套里轻轻碰撞。电梯里遇见邻居遛狗,它的爪子在地垫上留下两串湿印,很快被后来的人踩散。你想:痕迹就是这样,被人看见时已不是原样。
到了约定的路口,她比你早到半分钟,手里多了一杯热饮,杯套上印着歪歪扭扭的笑脸。「还给你。」你把伞递过去。她却摇头:「今天晴。」伞停在两人之间,像一句还没决定归属的句子。
你们沿着河堤走。水面上漂着昨夜的落叶,偶有涟漪被风抹平。她讲起最近在忙的展览,讲到一半又停住,说其实不太想聊工作。于是话题滑向更碎的东西:哪家面馆换了老板、哪条巷子的灯坏了三天、哪一次下雨你们刚好都没带伞。
「所以才需要借。」她说。
「借完还要还。」你说。
「有的不用还。」她看着河对岸的塔吊,「有的还了,反而像没发生过。」
塔吊缓缓旋转,吊臂在灰蓝天空里划出钝角。你忽然明白,呈现壳要解决的不是「字大一点」,而是这一长段话落在界面里时,读者还愿不愿意往下看——状态栏不能抢戏,选项不能在中途消失,侧栏得钉住好让人对照线索。
你们在桥上站了一会儿。风把伞套吹得鼓起,又瘪下去。她把热饮塞进你手里:「暖的。你昨晚肯定没睡好。」你没有否认。杯壁的温度从掌心爬上手腕,像把雨夜重新烘了一遍,只留下能说出口的那一层。
分开前,她抬手点了点伞:「这个,先放你那儿。等下一场雨。」
你点头。伞重新沉进包里,重量不大,却让背包的一侧微微倾斜——刚好提醒你:故事还会继续,而界面必须装得下继续。`;
const BODY_SHORT = {
prose:
"雨还在下。你把伞往她那边偏了偏,她没说话,只是把购物袋往怀里收紧了一点。\n\n路口的灯跳成绿色。你们一起迈步——这一次谁也没有先松开。",
chat_monitor:
"【短信】\n她到家了吗\n你刚进小区。伞借你的明天还给你\n她……不用那么快。",
spotlight:
"水从铁栏杆上砸下来,把对面那人的轮廓冲得忽明忽暗。\n\n他抬起头——认出是你的瞬间肩膀松了一下又立刻绷紧。",
turn_panel:
"货架后传来塑料袋摩擦声。你屏住呼吸,左手按住刀柄——也许只是风。\n\n收银台抽屉半开里面有未拆封的电池。",
split_board:
"抽屉夹层里有一张折痕发白的收据。日期是失踪前夜,商户栏写着「北港五金」。\n\n窗台积灰被抹过一道——有人近来开过窗。",
choice_dock:
"她把钥匙放在桌上,没有推过来,也没有收回。\n\n「你要是现在走我就当今晚什么都没发生。」",
chapter_reader:
"  天亮得比往日晚。林晚站在窗边,把画册合上,指尖在封面上停了一息。\n\n  「今天」她说「我们换条路回家。」",
};
const SAMPLE_META = {
prose: {
blocks: { footer: "本段为示例备注(可关)" },
meta: { suggested_actions: [] },
},
chat_monitor: {
blocks: {
monitor: { 好感: "暖络", 今日话题: "下雨天", 体力: "还好" },
footer: "",
},
meta: { suggested_actions: ["回一句关心", "约周末还伞"] },
},
spotlight: {
blocks: {
monitor: { 地点: "天桥下", 天气: "暴雨", 张力: "紧" },
footer: "",
},
meta: { suggested_actions: ["先开口", "递伞过去"] },
},
turn_panel: {
blocks: {
monitor: { 体力: "6/10", 物资: "压缩饼干×2", 威胁: "低" },
header: "第 12 回合 · 废弃便利店",
footer: "检定:潜行(通过)",
aside: { 可交互: "电池 / 后门 / 死角", 噪音: "低" },
},
meta: { suggested_actions: ["摸电池", "从后门离开", "大声喝问"] },
},
split_board: {
blocks: {
header: "案发现场 · 二楼书房",
aside: {
线索: "北港收据",
证物: "窗台指痕",
待查: "五金店监控",
},
footer: "",
},
meta: { suggested_actions: ["去北港", "采指纹", "先问管家"] },
},
choice_dock: {
blocks: {
monitor: { 信任: "中", 时间: "子夜前" },
footer: "",
},
meta: {
suggested_actions: ["拿走钥匙留下", "把钥匙推回去", "什么也不说,坐着"],
},
},
chapter_reader: {
blocks: {
header: "卷三 · 第 7 章 雨停之前",
aside: "进度:卷三 7/18\n下一钩母亲来电",
footer: "",
},
meta: { suggested_actions: [] },
},
};
function pickBody(shell, length) {
if (length === "short") return BODY_SHORT[shell] || BODY_SHORT.prose;
if (length === "mid") return BODY_MID;
return BODY_LONG;
}
function countChars(s) {
return Array.from(String(s ?? "").replace(/\s/g, "")).length;
}
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
const state = {
shell: "spotlight",
tone: "default",
length: "mid",
showActions: true,
blockLabels: {},
};
function buildTweaks() {
return {
tone_chrome: state.tone,
show_suggested_actions: state.showActions,
block_labels: { ...state.blockLabels },
empty_states: {
aside: "(侧栏空:可在创作里改空态文案)",
footer: "(文末空)",
},
};
}
function buildPacket() {
const base = SAMPLE_META[state.shell] || SAMPLE_META.prose;
const body = pickBody(state.shell, state.length);
return {
schema: "present.v1",
shell: state.shell,
blocks: { ...base.blocks, body },
meta: base.meta,
_chars: countChars(body),
};
}
function render() {
const info = SHELL_INFO[state.shell] || {
name: state.shell,
blurb: "",
scenes: "",
density: "",
};
const packet = buildPacket();
document.getElementById("panel-title").textContent = info.name;
document.getElementById("panel-subtitle").textContent = `${info.scenes} · 正文约 ${packet._chars} 字(不计空白)`;
document.getElementById("shell-meta").innerHTML = `
<p><strong>${esc(info.name)}</strong><code>${esc(state.shell)}</code></p>
<p>${esc(info.blurb)}</p>
<p class="shells-density"><span>长文策略</span>${esc(info.density)} · 当前试读 <strong>${packet._chars}</strong> 字</p>
<p class="muted">适用:${esc(info.scenes)}。请用上方「正文长度」切到长文,确认侧栏/选项是否仍可用。</p>
`;
const preview = document.getElementById("shell-preview");
preview.dataset.shell = state.shell;
preview.innerHTML = renderPresentShellHtml(packet, esc, { tweaks: buildTweaks() });
document.querySelectorAll("[data-shell]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-shell") === state.shell);
});
document.querySelectorAll("[data-tone]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-tone") === state.tone);
});
document.querySelectorAll("[data-length]").forEach((btn) => {
btn.classList.toggle("active", btn.getAttribute("data-length") === state.length);
});
const showActionsEl = document.getElementById("tweak-show-actions");
if (showActionsEl) showActionsEl.checked = state.showActions;
}
function mount() {
const list = document.getElementById("shell-list");
list.innerHTML = PRESENT_SHELL_IDS.map((id) => {
const info = SHELL_INFO[id];
return `<button type="button" class="st-rail-item" data-shell="${esc(id)}" role="tab">
${esc(info?.name || id)}
</button>`;
}).join("");
document.getElementById("tone-list").innerHTML = TONES.map(
(t) =>
`<button type="button" class="shells-tone-chip" data-tone="${esc(t.id)}">${esc(t.label)}</button>`,
).join("");
document.getElementById("length-list").innerHTML = LENGTHS.map(
(t) =>
`<button type="button" class="shells-tone-chip" data-length="${esc(t.id)}">${esc(t.label)}</button>`,
).join("");
document.getElementById("tweak-controls").innerHTML = `
<label class="field">
<input type="checkbox" id="tweak-show-actions" />
显示「建议行动」(对应微调轴 show_suggested_actions
</label>
<label class="field">
正文区显示名block_labels.body
<input type="text" id="tweak-label-body" placeholder="例如:短信记录 / 场面 / 本章" />
</label>
<label class="field">
监控区显示名block_labels.monitor
<input type="text" id="tweak-label-monitor" placeholder="例如:状态 / 今日" />
</label>
`;
list.addEventListener("click", (e) => {
const btn = e.target.closest("[data-shell]");
if (!btn) return;
state.shell = btn.getAttribute("data-shell");
render();
});
document.getElementById("tone-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-tone]");
if (!btn) return;
state.tone = btn.getAttribute("data-tone");
render();
});
document.getElementById("length-list").addEventListener("click", (e) => {
const btn = e.target.closest("[data-length]");
if (!btn) return;
state.length = btn.getAttribute("data-length");
render();
});
const syncLabels = () => {
state.showActions = document.getElementById("tweak-show-actions").checked;
const body = document.getElementById("tweak-label-body").value.trim();
const monitor = document.getElementById("tweak-label-monitor").value.trim();
state.blockLabels = {};
if (body) state.blockLabels.body = body;
if (monitor) state.blockLabels.monitor = monitor;
render();
};
document.getElementById("tweak-controls").addEventListener("change", syncLabels);
document.getElementById("tweak-controls").addEventListener("input", syncLabels);
render();
}
mount();

View File

@@ -67,7 +67,7 @@
<body>
<div class="stats-page">
<header>
<p><a href="/">← 对话</a> · <a href="/modules.html">技能</a> · <a href="/settings.html">设置</a></p>
<p><a href="/">← 对话</a> · <a href="/modules.html">技能</a> · <a href="/shells.html">呈现壳</a> · <a href="/settings.html">设置</a></p>
<h1>Token 统计</h1>
<p class="stats-muted">按时间戳筛选调用记录(存于本地 stats/*.jsonl</p>
</header>

View File

@@ -1,4 +1,4 @@
/* Functional layout <EFBFBD> agent-first, no production-line chrome */
/* Functional layout — agent-first, no production-line chrome */
:root {
--bg: #f4f4f5;
@@ -22,7 +22,7 @@
color: var(--text);
}
/* 寮€鍒涗綔鍚庣缉杩涘乏渚ф爮 */
/* 开创作后缩进左侧栏 */
body.rail-collapsed {
--books-w: var(--books-w-collapsed);
}
@@ -31,7 +31,7 @@ body.rail-collapsed {
html, body {
height: 100%;
margin: 0;
overflow: hidden; /* 娴忚<EFBFBD>鍣ㄥ眰姘镐笉鍑虹幇椤甸潰婊氬姩鏉<EFBFBD> */
overflow: hidden; /* 浏览器层永不出现页面滚动条 */
}
button, input, textarea { font: inherit; }
a.link { color: var(--accent); text-decoration: none; }
@@ -138,7 +138,7 @@ body.rail-collapsed .rail-tab {
}
body.rail-collapsed .rail-panels { display: none; }
body.rail-collapsed .panel-rail { align-items: stretch; }
/* composer 鎸夊唴瀹硅嚜閫傚簲锛涗腑闂村尯鍚冩帀鍓╀綑楂樺害锛屾暣椤甸攣鍦ㄨ<EFBFBD> */
/* composer 按内容自适应;中间区吃掉剩余高度,整页锁在视口内 */
.panel-main {
display: grid;
grid-template-rows: var(--head-h) minmax(0, 1fr) auto;
@@ -195,7 +195,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
.panel-head-actions { display: flex; gap: 4px; align-items: center; }
.btn-sm.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* Books <EFBFBD> explorer breadcrumb + list */
/* Books — explorer breadcrumb + list */
.path-bar {
display: flex; align-items: center; flex-wrap: nowrap; gap: 2px;
margin: 6px 8px 4px; padding: 6px 10px;
@@ -267,7 +267,7 @@ body.rail-collapsed .panel-rail { align-items: stretch; }
body[data-lifecycle="play"] { --accent: #16a34a; --accent-soft: #dcfce7; --bg: #f0fdf4; }
body[data-lifecycle="play"] .panel-rail { background: #f7fef9; }
/* Skill picker <EFBFBD> primary action when starting */
/* Skill picker — primary action when starting */
.skill-picker {
padding: 16px;
border-bottom: 2px solid var(--accent);
@@ -285,7 +285,7 @@ body[data-lifecycle="play"] .panel-rail { background: #f7fef9; }
.skill-card-desc { font-size: 12px; color: var(--muted); line-height: 1.4; }
.skill-card-tag { font-size: 11px; color: var(--accent); margin-top: 6px; }
/* Feed <EFBFBD> SillyTavern 寮忓<EFBFBD>璇濇皵娉<EFBFBD> */
/* Feed — SillyTavern 式对话气泡 */
.message-feed {
flex: 1;
overflow-y: auto;
@@ -557,7 +557,7 @@ body.rail-collapsed .message-feed {
margin: 0 0 8px;
}
/* 涓诲<EFBFBD>璇濋獙鏀跺崱锛氬悆婊腑闂存爮 */
/* 主对话验收卡:吃满中间栏 */
.msg-review {
align-self: stretch;
width: 100%;
@@ -594,7 +594,7 @@ body.rail-collapsed .message-feed {
.msg-review .ws-focus-hint { margin-bottom: 0; font-size: 11px; }
.msg-review .review-worker-set { margin: 0 0 6px; }
/* х墿绱у噾鍗★細妯<EFBFBD>悜涓昏酱锛岄<EFBFBD>瑙堜竴琛<EFBFBD> */
/* 产物紧凑卡:横向主轴,预览一行 */
.artifact-compact-list {
display: flex;
flex-direction: column;
@@ -650,8 +650,8 @@ body.rail-collapsed .message-feed {
border-radius: 999px;
background: var(--accent-soft);
}
.artifact-compact-toggle::before { content: "灞曞紑"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "鏀惰捣"; }
.artifact-compact-toggle::before { content: "展开"; }
.artifact-compact[open] .artifact-compact-toggle::before { content: "收起"; }
.artifact-compact-preview {
margin: 0;
font-family: var(--font-read);
@@ -707,7 +707,7 @@ body.rail-collapsed .message-feed {
}
.ws-focus-body .artifact-compact-list { margin: 0; }
/* 灞曞紑鍖猴細mosaic / 鐡风爾缃戞牸锛屽悆婊℃í鍚戝<E98D9A><EFBFBD> */
/* 展开区:mosaic / 瓷砖网格,吃满横向宽度 */
.af-mosaic {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -818,7 +818,7 @@ body.rail-collapsed .af-mosaic {
gap: 5px;
}
/* 鎶€鑳戒笓鐢ㄦ<EFBFBD>鏂囧崱 */
/* 技能专用正文卡 */
.skill-view {
display: flex;
flex-direction: column;
@@ -1244,7 +1244,7 @@ body.rail-collapsed .af-mosaic {
.msg-q-list li { margin: 0.35rem 0; }
.msg-q-lead { margin: 0; line-height: 1.55; font-size: 14px; }
/* Cursor 寮忚<EFBFBD><EFBFBD>崱锛氳创鍦ㄦ秷鎭<EFBFBD>祦涓嬫柟锛屽崰婊″崗璋冨垪瀹藉害锛屼笉閬<EFBFBD>洊鍘嗗彶 */
/* Cursor 式询问卡:贴在消息流下方,占满协调列宽度,不遮盖历史 */
.questions-card-host {
flex: 0 1 auto;
width: 100%;
@@ -1255,7 +1255,7 @@ body.rail-collapsed .af-mosaic {
min-height: 0;
}
.questions-card-host[hidden] { display: none !important; }
/* 寮€鍚<EFBFBD>椂鍗<EFBFBD> feed 鍓╀綑楂樺害鐨勪笂闄愶紝涓嶆妸鏁撮〉鎾戝嚭瑙嗗彛锛涘嬁涓<E5AC81> [hidden] 骞跺瓨鎶<EFBFBD> display */
/* 开启时占 feed 剩余高度的上限,不把整页撑出视口;勿与 [hidden] 并存抢 display */
.questions-card-host.is-open:not([hidden]) {
display: flex;
flex-direction: column;
@@ -1276,7 +1276,7 @@ body.rail-collapsed .af-mosaic {
background: var(--surface, #fff);
overflow: hidden;
}
/* <EFBFBD>诞鎺т欢灞傦細涓嶅崰鏂囨。娴侀珮搴<EFBFBD> */
/* 悬浮控件层:不占文档流高度 */
.qcard-float {
position: absolute;
inset: 0;
@@ -1349,7 +1349,7 @@ body.rail-collapsed .af-mosaic {
flex: 1 1 auto;
min-height: 0;
padding: 10px 12px;
overflow: auto; /* 浠呭崱鍐呮粴鍔<EFBFBD>紱椤甸潰鏈<EFBFBD>韩涓嶆粴 */
overflow: auto; /* 仅卡内滚动;页面本身不滚 */
}
.qcard.has-pager .qcard-body { padding-top: 36px; }
.qcard.has-skip .qcard-body { padding-bottom: 40px; }
@@ -1461,7 +1461,7 @@ body.rail-collapsed .af-mosaic {
}
.ws-slot-chip.ws-slot-on { background: var(--accent-soft); border-color: transparent; }
.ws-slot-chip.ws-slot-off { opacity: 0.55; text-decoration: line-through; }
/* х墿缁撴瀯鍖栧睍绀<EFBFBD><> 瀵归綈 qcard / flow-step / ws-badge */
/* 产物结构化展示 — 对齐 qcard / flow-step / ws-badge */
.artifact-friendly { display: flex; flex-direction: column; gap: 10px; }
.artifact-list { margin: 0; padding-left: 18px; line-height: 1.5; }
.artifact-hero {
@@ -1561,7 +1561,7 @@ body.rail-collapsed .af-mosaic {
.artifact-kv-row.complex .artifact-kv-key { margin-bottom: 2px; }
.artifact-kv-val { min-width: 0; font-size: 13px; line-height: 1.45; }
/* 鐧惧垎姣旇繘搴︽潯 */
/* 百分比进度条 */
.pct-meter {
display: flex;
flex-direction: column;
@@ -1913,7 +1913,7 @@ body.rail-collapsed .af-mosaic {
.timeline-item { padding: 6px 0; border-bottom: 1px solid var(--border); }
.timeline-empty { color: var(--muted); }
/* Composer <EFBFBD> 楂樺害闅忓唴瀹癸紝浣嗘湁涓婇檺锛岄伩鍏嶆尋鐖嗚<E99096><EFBFBD> */
/* Composer — 高度随内容,但有上限,避免挤爆视口 */
.composer {
border-top: 1px solid var(--border);
padding: 10px 12px;
@@ -2001,81 +2001,362 @@ body.rail-collapsed .af-mosaic {
body:not(.rail-collapsed) .rail-panels { display: none; }
}
/* —— 游玩呈现壳 present.v1P2 最小) —— */
/* —— 游玩呈现壳 present.v1 —— */
.present-shell {
--present-ink: #1c1917;
--present-muted: #78716c;
--present-line: #e7e5e4;
--present-soft: #f5f5f4;
--present-accent: #0f766e;
--present-body: 17.5px;
--present-body-lh: 1.78;
--present-body-hero: 19px;
--present-body-read: 18.5px;
display: flex;
flex-direction: column;
gap: 10px;
gap: 0;
position: relative;
padding: 10px 12px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface);
padding: 14px 16px 16px;
border: 1px solid var(--present-line);
border-radius: 12px;
background: var(--surface, #fff);
color: var(--present-ink);
}
.present-shell-badge {
align-self: flex-start;
font-size: 11px;
margin-bottom: 10px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--muted);
padding: 1px 7px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--present-muted);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--surface-2, rgba(0, 0, 0, 0.03));
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-stack,
.present-board,
.present-split,
.present-reader {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.present-region {
display: flex;
flex-direction: column;
gap: 4px;
gap: 6px;
min-width: 0;
}
.present-region-title {
margin: 0;
font-size: 11px;
font-weight: 700;
color: var(--muted);
letter-spacing: 0.04em;
color: var(--present-muted);
letter-spacing: 0.05em;
}
.present-region-body {
margin: 0;
line-height: 1.55;
line-height: 1.65;
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;
/* P0正文舞台 —— 随字数伸展600~3000 靠外层滚动,不塞进小框) */
.present-stage {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
}
.present-region--body {
min-width: 0;
}
.present-region--body .present-region-body,
.present-stage .present-region--body .present-region-body,
.present-stage.present-region--body .present-region-body {
font-size: var(--present-body);
line-height: var(--present-body-lh);
letter-spacing: 0.01em;
max-height: none;
overflow: visible;
}
.present-stage--hero .present-region-body {
font-size: var(--present-body-hero);
line-height: 1.82;
padding: 4px 2px 8px;
}
.present-stage--read .present-region-body {
font-family: var(--font-read);
font-size: var(--present-body-read);
line-height: 1.9;
max-width: 42rem;
}
/* 双栏:侧栏钉住,正文再长也能对照 */
.present-board-side,
.present-split-side,
.present-reader-aside {
position: sticky;
top: 0.75rem;
align-self: start;
max-height: calc(100dvh - 3rem);
overflow-y: auto;
}
/* P2监控芯片 */
.present-chips {
display: flex;
flex-wrap: wrap;
gap: 6px 8px;
}
.present-chip {
display: inline-flex;
align-items: baseline;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface-2, rgba(0, 0, 0, 0.03));
border: 1px solid var(--present-line);
background: var(--present-soft);
font-size: 12px;
line-height: 1.35;
}
.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;
.present-chip-k {
color: var(--present-muted);
font-weight: 600;
}
.present-chip-v {
color: var(--present-ink);
font-weight: 500;
}
.present-region--monitor .present-region-body,
.present-region--aside .present-region-body,
.present-region--footer .present-region-body {
font-size: 12.5px;
line-height: 1.45;
color: var(--present-ink);
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-region--header .present-region-body {
font-size: 14px;
font-weight: 650;
letter-spacing: 0.01em;
line-height: 1.4;
}
/* P1行动 */
.present-action-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.present-action {
display: inline-flex;
align-items: center;
padding: 7px 12px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--present-accent) 35%, var(--present-line));
background: color-mix(in srgb, var(--present-accent) 8%, #fff);
color: var(--present-ink);
font-size: 13px;
font-weight: 550;
line-height: 1.3;
}
.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-shell--chat_monitor {
--present-accent: #0369a1;
background:
linear-gradient(180deg, #f0f9ff 0%, #fff 48px);
}
.present-tone--book .present-region--body {
max-width: 42rem;
.present-shell--chat_monitor .present-stage {
padding: 14px 16px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: #fff;
}
.present-tone--terminal .present-region-body {
.present-shell--spotlight {
--present-accent: #b45309;
padding-top: 14px;
}
.present-shell--spotlight .present-stack--spotlight {
gap: 14px;
}
.present-shell--spotlight .present-stage--hero {
padding: 6px 4px 4px;
}
.present-shell--spotlight .present-region--monitor .present-region-title {
display: none;
}
.present-shell--turn_panel {
--present-accent: #047857;
background: linear-gradient(165deg, #f0fdf4 0%, #fff 28%);
}
.present-board-top {
display: flex;
flex-direction: column;
gap: 8px;
}
.present-board-mid {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(140px, 0.85fr);
gap: 12px;
align-items: start;
}
.present-board-side {
display: flex;
flex-direction: column;
gap: 8px;
}
.present-shell--turn_panel .present-stage {
padding: 16px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: #fff;
}
.present-shell--split_board {
--present-accent: #4338ca;
}
.present-split {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(150px, 0.9fr);
gap: 14px 16px;
align-items: start;
}
.present-split-main {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.present-split-side {
min-width: 0;
padding: 10px;
border-radius: 10px;
border: 1px solid var(--present-line);
background: var(--present-soft);
}
.present-split .present-region--actions {
grid-column: 1 / -1;
}
.present-shell--choice_dock {
--present-accent: #be123c;
}
.present-stack--dock {
gap: 10px;
}
.present-dock {
margin-top: 4px;
padding: 12px;
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--present-accent) 28%, var(--present-line));
background: color-mix(in srgb, var(--present-accent) 6%, #fff);
/* 长正文时选项坞钉在视口底,读完仍看得到 */
position: sticky;
bottom: 0;
z-index: 2;
box-shadow: 0 -8px 20px color-mix(in srgb, #fff 70%, transparent);
}
.present-dock .present-region-title {
margin-bottom: 2px;
}
.present-dock .present-action {
padding: 9px 14px;
font-size: 13.5px;
border-width: 1.5px;
}
.present-shell--chapter_reader {
--present-accent: #44403c;
background:
linear-gradient(90deg, transparent 0, transparent),
linear-gradient(180deg, #fafaf9 0%, #fff 40%);
}
.present-reader {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(120px, 0.42fr);
gap: 18px;
align-items: start;
}
.present-reader-main {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.present-reader-aside {
font-size: 12px;
padding-top: 4px;
border-left: 1px solid var(--present-line);
padding-left: 14px;
}
.present-shell--chapter_reader .present-region--header .present-region-body {
font-family: var(--font-read);
font-size: 20px;
font-weight: 600;
}
.present-shell--prose .present-stage--read {
padding: 6px 2px;
}
.present-shell--prose .present-region--footer .present-region-body {
opacity: 0.85;
}
@media (max-width: 640px) {
.present-board-mid,
.present-split,
.present-reader {
grid-template-columns: 1fr;
}
.present-board-side,
.present-split-side,
.present-reader-aside {
position: static;
max-height: none;
}
.present-reader-aside {
border-left: none;
border-top: 1px solid var(--present-line);
padding-left: 0;
padding-top: 12px;
}
}
.present-tone--messenger .present-stage {
border-left: 3px solid #7dd3fc;
padding-left: 12px;
}
.present-tone--book .present-stage--read .present-region-body,
.present-tone--book .present-stage .present-region-body {
font-family: var(--font-read);
max-width: 38rem;
}
.present-tone--terminal {
--present-ink: #d6d3d1;
--present-muted: #a8a29e;
--present-line: #3f3f46;
--present-soft: #27272a;
background: #18181b;
color: #e7e5e4;
}
.present-tone--terminal .present-chip,
.present-tone--terminal .present-stage,
.present-tone--terminal .present-dock,
.present-tone--terminal .present-split-side {
background: #27272a;
border-color: #3f3f46;
}
.present-tone--terminal .present-region-body,
.present-tone--terminal .present-action {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
}
.present-tone--terminal .present-region--body .present-region-body {
font-size: calc(var(--present-body) - 0.5px);
}
.present-tone--terminal .present-action {
font-size: 13px;
color: #e7e5e4;
background: #3f3f46;
}