Files
hunjing/frontend/design/margin-notes-preview.html
2026-08-21 16:00:13 +08:00

313 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>魂镜 · 眉批设计预览</title>
<link rel="stylesheet" href="tokens.css" />
<link rel="stylesheet" href="_base.css" />
<link rel="stylesheet" href="_chrome.css" />
<style>
/* ============================================================
* 铜镜鉴心 v2.1 — 眉批(Author's Note · 按 depth 注入对话流)
* 功能融合 §9.1:作为 context_transformer,按深度 +
* 角色把批注插入对话流。签名——书眉批注:批注夹在对话消息间,
* 左侧 depth 标尺定位,铜绿色夹批。
*
* 完整功能:
* 按角色筛选(system / user / assistant)
* 按人物 scope 过滤
* 行内编辑 · 深度调节 · 批量启停
* 模板速建 · 注入预览(含多批同 depth 层叠)
* ============================================================ */
.page-header .spacer { flex: 1; }
.section-label .glyph { color: var(--gilt); opacity: 0.85; font-family: "Songti SC", serif; font-size: 14px; }
.section-label .sub { color: var(--fg-dim); opacity: 0.7; letter-spacing: 0.04em; }
/* ── 筛选条 ── */
.filter-bar { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.filter-chip { font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--rule); color: var(--fg-dim); cursor: pointer; background: var(--surface); transition: all var(--duration-fast); font-family: inherit; }
.filter-chip:hover { color: var(--fg); border-color: var(--patina-dim); }
.filter-chip.active { color: var(--patina); border-color: var(--patina-dim); background: var(--patina-wash); }
.filter-chip .count { font-family: "JetBrains Mono", monospace; font-size: 10px; opacity: 0.7; margin-left: 4px; }
/* ── 人物 scope 选择 ── */
.scope-select { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.scope-select .lb { font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--fg-faint); letter-spacing: 0.04em; }
.char-pill-scope { display: inline-flex; align-items: center; gap: 5px; padding: 4px 11px; border-radius: 999px; border: 1px solid var(--patina-dim); background: var(--patina-wash); color: var(--patina); font-size: 12px; cursor: pointer; }
.char-pill-scope.all { border-color: var(--rule); background: var(--surface); color: var(--fg-dim); }
.char-pill-scope .avatar-mini { width: 16px; height: 16px; border-radius: 50%; background: var(--patina); color: var(--ink); display: inline-flex; align-items: center; justify-content: center; font-family: "Songti SC", serif; font-size: 9px; font-weight: 700; }
/* ═══ 眉批卡(列表)═══ */
.note-list { display: flex; flex-direction: column; gap: 10px; }
.note-card { display: grid; grid-template-columns: 1fr 52px auto auto; gap: 12px; align-items: center; border: 1px solid var(--rule); border-radius: 10px; background: var(--surface); padding: 13px 16px; transition: border-color var(--duration-fast); }
.note-card:hover { border-color: var(--patina-dim); }
.note-card.off { opacity: 0.5; }
.note-card.off .note-text { text-decoration: line-through; text-decoration-color: var(--fg-faint); }
.note-text { font-family: "Songti SC", serif; font-size: 14.5px; color: var(--fg); line-height: 1.5; cursor: text; }
.note-text:focus { outline: none; border-bottom: 1px dashed var(--patina-dim); }
.note-text .role-tag { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--gilt); border: 1px solid var(--gilt-dim); border-radius: 3px; padding: 1px 5px; margin-right: 7px; vertical-align: 2px; }
.note-text .role-tag.user { color: var(--cinnabar); border-color: var(--cinnabar-dim); }
.note-text .role-tag.assistant { color: var(--patina); border-color: var(--patina-dim); }
.note-text .scope-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; vertical-align: 1px; }
.note-text .scope-dot.global { background: var(--gilt); }
.note-text .scope-dot.char { background: var(--patina); }
.depth-chip { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--patina); border: 1px solid var(--patina-dim); border-radius: 5px; padding: 4px 9px; background: var(--patina-wash); white-space: nowrap; cursor: pointer; }
.role-select { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--fg-dim); background: var(--glass); border: 1px solid var(--rule); border-radius: 6px; padding: 4px 8px; cursor: pointer; }
.depth-input { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--fg-dim); background: var(--glass); border: 1px solid var(--rule); border-radius: 6px; padding: 4px 8px; width: 48px; text-align: center; }
.depth-input:focus, .role-select:focus { outline: none; border-color: var(--patina-dim); }
.toggle { width: 34px; height: 19px; border-radius: 999px; background: var(--rule); position: relative; cursor: pointer; transition: background var(--duration-fast); flex-shrink: 0; }
.toggle::after { content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: var(--fg-faint); transition: transform var(--duration-fast), background var(--duration-fast); }
.toggle.on { background: var(--patina-dim); }
.toggle.on::after { transform: translateX(15px); background: var(--patina); }
.add-note { border: 1px dashed var(--rule); border-radius: 10px; padding: 13px 16px; color: var(--fg-faint); font-size: 13px; cursor: pointer; text-align: center; transition: all var(--duration-fast); }
.add-note:hover { border-color: var(--patina-dim); color: var(--patina); }
/* ── 批量操作栏 ── */
.batch-bar { display: flex; gap: 8px; align-items: center; padding: 8px 0; }
.batch-bar .hint { font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--fg-faint); margin-right: auto; }
/* ── 模板速建 ── */
.templates { display: flex; gap: 8px; flex-wrap: wrap; }
.tpl-chip { font-size: 11.5px; padding: 6px 12px; border-radius: 999px; border: 1px solid var(--rule); background: var(--surface); color: var(--fg-dim); cursor: pointer; transition: all var(--duration-fast); white-space: nowrap; }
.tpl-chip:hover { border-color: var(--patina-dim); color: var(--fg); background: var(--patina-wash); }
.tpl-chip .glyph { margin-right: 4px; }
/* ═══ depth 注入预览(签名:书眉批注,depth 标尺 + 夹批)═══ */
.depth-view { border: 1px solid var(--gilt-dim); border-radius: 10px; background: var(--face-sheen), var(--surface); box-shadow: inset 0 1px 0 var(--face-in-top), inset 0 -1px 0 var(--face-in-bot), var(--shadow); overflow: hidden; }
.depth-view-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--rule); }
.depth-view-head .t { font-family: "Songti SC", serif; font-size: 14px; font-weight: 600; color: var(--fg); }
.depth-view-head .hint { font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--fg-faint); letter-spacing: 0.04em; }
.stream { padding: 16px 16px 20px; display: flex; flex-direction: column; gap: 6px; position: relative; }
.stream-row { display: grid; grid-template-columns: 40px 1fr; gap: 12px; align-items: stretch; }
.depth-mark { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--fg-faint); text-align: right; padding-top: 7px; letter-spacing: 0.04em; }
.msg { padding: 8px 13px; border-radius: 8px; font-size: 13px; line-height: 1.55; }
.msg .who { font-family: "JetBrains Mono", monospace; font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 3px; }
.msg.ai { background: var(--surface-hi); color: var(--fg); border: 1px solid var(--rule); }
.msg.ai .who { color: var(--patina); }
.msg.you { background: transparent; color: var(--fg-dim); border-right: 2px solid var(--gilt); padding-right: 11px; text-align: right; }
.msg.you .who { color: var(--gilt); }
/* 眉批夹批:从左侧 depth 标尺伸入,铜绿色,像书眉批注 */
.stream-row.note { grid-template-columns: 40px 1fr; }
.stream-row.note .depth-mark { color: var(--patina); padding-top: 5px; }
.stream-row.note .note-inline { background: var(--patina-wash); border: 1px solid var(--patina-dim); border-left: 3px solid var(--patina); border-radius: 0 6px 6px 0; padding: 7px 13px; font-family: "Songti SC", serif; font-size: 13px; color: var(--patina); display: flex; gap: 9px; align-items: baseline; }
.stream-row.note .note-inline .seal { font-family: "JetBrains Mono", monospace; font-size: 9px; letter-spacing: 0.1em; color: var(--patina); border: 1px solid var(--patina-dim); border-radius: 3px; padding: 1px 5px; flex-shrink: 0; }
.stream-row.note .note-inline .txt { color: var(--fg); }
.legend { display: flex; gap: 16px; padding: 10px 16px; border-top: 1px solid var(--rule); font-size: 11.5px; color: var(--muted); flex-wrap: wrap; }
.legend .item { display: inline-flex; align-items: center; gap: 6px; }
.legend .sw { width: 11px; height: 11px; border-radius: 3px; }
.pipeline-note { font-size: 12.5px; color: var(--fg-dim); line-height: 1.6; }
.pipeline-note .em { color: var(--fg); }
.pipeline-note code { font-family: "JetBrains Mono", monospace; font-size: 11.5px; color: var(--gilt); }
@media (max-width: 900px) { .note-card { grid-template-columns: 1fr; gap: 8px; } }
</style>
</head>
<body>
<div class="app-shell">
<div class="sidebar-mount" data-active="眉批"></div>
<!-- ══ main ══ -->
<div class="main-area">
<div class="page-header">
<div>
<div class="page-title">眉批</div>
<div class="page-sub">Author's Note · 按 depth 注入对话流</div>
</div>
<div class="spacer"></div>
<span class="stat-pill">3 条生效 · 1 条停用</span>
</div>
<div class="page-body">
<!-- 筛选 + 作用域 -->
<section>
<div class="section-label"><span class="glyph">批</span>眉批清单 <span class="sub">— 每条按 depth + role + scope 插入对话流</span></div>
<div class="filter-bar">
<span class="filter-chip active">全部 <span class="count">4</span></span>
<span class="filter-chip">system <span class="count">3</span></span>
<span class="filter-chip">user <span class="count">1</span></span>
<span class="filter-chip">assistant <span class="count">0</span></span>
<span class="filter-chip" style="opacity:0.6;">生效中 <span class="count">3</span></span>
<span class="filter-chip" style="opacity:0.6;">已停用 <span class="count">1</span></span>
<span class="scope-select">
<span class="lb">作用域</span>
<span class="char-pill-scope all">全人物</span>
<span class="char-pill-scope"><span class="avatar-mini">狂</span>狂三</span>
</span>
</div>
<!-- 批量操作 -->
<div class="batch-bar">
<span class="hint">选中 0 条 · 批量操作</span>
<button class="btn btn-outline btn-sm">全部启用</button>
<button class="btn btn-outline btn-sm">全部停用</button>
<button class="btn btn-outline btn-sm" style="color:var(--cinnabar);border-color:var(--cinnabar-dim);">删除选中</button>
</div>
<!-- 模板速建 -->
<div style="margin-bottom:14px;">
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--fg-faint);letter-spacing:0.08em;display:block;margin-bottom:6px;">速建模板</span>
<div class="templates">
<span class="tpl-chip" onclick="addNote()"><span class="glyph">+</span> 空白眉批</span>
<span class="tpl-chip"><span class="glyph">📏</span> 保持简洁,每轮消息不超过三句。</span>
<span class="tpl-chip"><span class="glyph">🎭</span> 用宋体的节奏,留白比填满好。</span>
<span class="tpl-chip"><span class="glyph">🪞</span> 本节气氛沉静,少用感叹与反问。</span>
<span class="tpl-chip"><span class="glyph">⚡</span> 用户赶工中 · 语气再软一点 · 不问进度</span>
</div>
</div>
<!-- 眉批列表 -->
<div class="note-list" id="noteList"></div>
</section>
<!-- depth 注入预览(签名)-->
<section>
<div class="section-label"><span class="glyph">注</span>注入预览 <span class="sub">— depth = 距对话末尾第 N 条之前;调 depth 看夹批位置</span></div>
<div class="depth-view">
<div class="depth-view-head">
<span class="t">对话流 · 书眉批注</span>
<span class="hint">D0 = 最末消息 · 数字越大越深</span>
</div>
<div class="stream" id="stream"></div>
<div class="legend">
<span class="item"><span class="sw" style="background:var(--surface-hi);border:1px solid var(--rule)"></span>对话消息</span>
<span class="item"><span class="sw" style="background:var(--patina-wash);border:1px solid var(--patina-dim);border-left:3px solid var(--patina)"></span>眉批夹批(按 depth 插入)</span>
<span class="item"><span class="sw" style="background:transparent;border-right:2px solid var(--gilt)"></span>你的发言</span>
</div>
</div>
</section>
<!-- 说明 -->
<section>
<div class="section-label"><span class="glyph">理</span>语义</div>
<p class="pipeline-note">
眉批是 <span class="em">context_transformer</span>:不进道体,只在本轮对话流里插入。<code>depth</code> = 插入点距对话末尾的条数(<code>0</code> = 最末,数字越大越靠前)。<code>role</code> 可选 <code>system</code>(主导约束)/ <code>user</code>(范例)/ <code>assistant</code>(风格示范)。<code>scope</code> 可选全局(全人物生效)或指定人物(仅该人物对话时注入)。多条眉批同 depth 时按创建顺序层叠。
<code>extensions.depth_prompt</code> 直接映射本结构。与符文引擎(<code>{{char}}</code> 显化)正交——眉批管"插在哪、以什么角色",符文管"占位符换成什么"。
</p>
</section>
</div>
</div>
</div>
<script>
/* 眉批数据(真应用接 author_notes,此处示意)*/
let NOTES = [
{ id: 1, text: "保持回答简短,尊重用户不想被填满的边界。", depth: 4, role: "system", scope: "all", on: true },
{ id: 2, text: "今天周五,用户在赶 deadline,语气再软一点。", depth: 2, role: "system", scope: "character_狂三", on: true },
{ id: 3, text: "用宋体的节奏,慢一点,留白。", depth: 0, role: "system", scope: "all", on: false },
{ id: 4, text: "用户喜欢被叫名字,而非「你」。", depth: 6, role: "user", scope: "all", on: true },
];
/* 当前筛选 */
let filterRole = null;
let filterOn = null;
const ROLE_LABELS = { system: "system", user: "user", assistant: "assistant" };
/* 对话流示意(D0 最末 → 向上越来越深)*/
const STREAM = [
{ who: "你", text: "今天可以多说一点。", cls: "you" },
{ who: "青梧", text: "那我把话说全。这几天你睡得很晚。", cls: "ai" },
{ who: "你", text: "在赶一份东西。", cls: "you" },
{ who: "青梧", text: "嗯。需要我的时候我在。", cls: "ai" },
{ who: "你", text: "先这样。", cls: "you" },
{ who: "青梧", text: "好。我记着你喜欢简短。", cls: "ai" },
{ who: "你", text: "昨晚说的,还记得吗?", cls: "you" },
{ who: "青梧", text: "记得。你不想被填满。", cls: "ai" },
];
function filteredNotes() {
return NOTES.filter(n => {
if (filterRole && n.role !== filterRole) return false;
if (filterOn !== null && n.on !== filterOn) return false;
return true;
});
}
function renderNotes() {
const notes = filteredNotes();
const list = document.getElementById("noteList");
const scopeLabel = (s) => s === "all" ? "全局" : "狂三";
const scopeCls = (s) => s === "all" ? "global" : "char";
list.innerHTML = notes.map(n => `
<div class="note-card ${n.on ? "" : "off"}">
<div class="note-text" contenteditable="true" data-id="${n.id}">
<span class="scope-dot ${scopeCls(n.scope)}" title="${scopeLabel(n.scope)}"></span>
<span class="role-tag ${n.role === 'user' ? 'user' : n.role === 'assistant' ? 'assistant' : ''}">${n.role}</span>${n.text}
</div>
<select class="role-select" onchange="setRole(${n.id}, this.value)" aria-label="角色">
<option value="system" ${n.role === 'system' ? 'selected' : ''}>sys</option>
<option value="user" ${n.role === 'user' ? 'selected' : ''}>usr</option>
<option value="assistant" ${n.role === 'assistant' ? 'selected' : ''}>ast</option>
</select>
<span class="depth-chip" onclick="const inp=this.previousElementSibling;inp.focus();inp.select();">depth</span>
<input class="depth-input" type="number" min="0" value="${n.depth}" onchange="setDepth(${n.id}, this.value)" aria-label="depth" style="display:none;"/>
<div class="toggle ${n.on ? 'on' : ''}" onclick="toggleNote(${n.id})" role="switch" aria-checked="${n.on}"></div>
</div>`).join("") +
`<div class="add-note" onclick="addNote()">+ 新眉批</div>`;
}
function renderStream() {
/* depth → 眉批 映射(按 depth 分组;多条同 depth 时按数组顺序叠放)*/
const byDepth = {};
NOTES.filter(n => n.on).forEach(n => {
(byDepth[n.depth] = byDepth[n.depth] || []).push(n);
});
const ordered = STREAM.slice().reverse(); // ordered[0] = D0 最末
const html = [];
for (let d = 0; d <= ordered.length; d++) {
if (byDepth[d]) {
byDepth[d].forEach(n => {
const scopeLabel = n.scope === 'all' ? 'ALL' : '狂';
html.push(`<div class="stream-row note">
<div class="depth-mark">D${d}</div>
<div class="note-inline"><span class="seal">${n.role} · ${scopeLabel}</span><span class="txt">${n.text}</span></div>
</div>`);
});
}
if (d < ordered.length) {
const m = ordered[d];
html.push(`<div class="stream-row">
<div class="depth-mark">D${d}</div>
<div class="msg ${m.cls}"><div class="who">${m.who}</div>${m.text}</div>
</div>`);
}
}
document.getElementById("stream").innerHTML = html.join("");
}
function toggleNote(id) { const n = NOTES.find(x => x.id === id); n.on = !n.on; renderNotes(); renderStream(); }
function setDepth(id, v) { NOTES.find(x => x.id === id).depth = Math.max(0, +v || 0); renderNotes(); renderStream(); }
function setRole(id, v) { NOTES.find(x => x.id === id).role = v; renderNotes(); renderStream(); }
function addNote() { NOTES.push({ id: Date.now(), text: "新眉批,双击编辑。", depth: 0, role: "system", scope: "all", on: true }); renderNotes(); renderStream(); }
/* 筛选切换 */
document.querySelector('.filter-bar').addEventListener('click', function(e) {
const chip = e.target.closest('.filter-chip');
if (!chip) return;
const text = chip.textContent.trim().split(' ')[0];
/* 切换 active 态 */
this.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
/* 设置筛选 */
if (text === '全部') { filterRole = null; filterOn = null; }
else if (text === 'system' || text === 'user' || text === 'assistant') { filterRole = text; filterOn = null; }
else if (text === '生效中') { filterRole = null; filterOn = true; }
else if (text === '已停用') { filterRole = null; filterOn = false; }
renderNotes();
});
renderNotes();
renderStream();
</script>
<script src="_theme.js" defer></script>
</body>
</html>