Files
hunjing/frontend/design/chat-redesign-preview.html
T
2026-08-21 16:00:13 +08:00

3129 lines
103 KiB
HTML
Raw 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>魂镜 · 对话页重设计预览 v6</title>
<link rel="stylesheet" href="tokens.css" />
<link rel="stylesheet" href="_base.css" />
<link rel="stylesheet" href="_chrome.css" />
<style>
/* ============================================================
* 铜镜鉴心 v2.1 — 与登录页 copper-mirror-login.html 同源
* ============================================================ */
.chat-region {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
/* ============================================================
* 镜背轨 Rail (280px) — 也可折叠
* ============================================================ */
.rail-wrap {
position: relative;
display: flex;
}
.rail {
width: 280px;
min-width: 280px;
display: flex;
flex-direction: column;
overflow: hidden;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--night-bronze) 88%, var(--patina) 4%),
var(--night-bronze)
);
border-right: 1px solid var(--rule);
transition: width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.rail.collapsed {
width: 0;
min-width: 0;
border: none;
overflow: hidden;
}
/* 镜背轨双视图:铭文往来(默认)/ 此刻 —— 同一面镜背的两种照见 */
.rail-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--rule);
flex-shrink: 0;
}
.rail-tab {
flex: 1;
padding: 12px 10px 10px;
background: none;
border: none;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.08em;
color: var(--fg-faint);
cursor: pointer;
transition: color 0.15s;
position: relative;
font-weight: 500;
}
.rail-tab:hover {
color: var(--fg-dim);
}
.rail-tab.active {
color: var(--gilt);
}
.rail-tab.active::after {
content: "";
position: absolute;
left: 24%;
right: 24%;
bottom: -1px;
height: 1.5px;
background: var(--gilt);
}
.rail-view {
flex: 1;
min-height: 0;
display: none;
flex-direction: column;
overflow: hidden;
}
.rail-view.active {
display: flex;
}
#view-now {
overflow-y: auto;
}
.status-card {
padding: 18px 18px 12px;
border-bottom: 1px solid var(--rule);
}
.status-card .character-row {
display: flex;
align-items: center;
gap: 12px;
}
.status-card .avatar-ring {
width: 44px;
height: 44px;
border-radius: 50%;
flex-shrink: 0;
border: 1.5px solid var(--patina-dim);
display: flex;
align-items: center;
justify-content: center;
background: var(--patina-wash);
}
.status-card .avatar-ring .inner {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--patina);
color: var(--ink);
display: flex;
align-items: center;
justify-content: center;
font-family: "Songti SC", serif;
font-size: 16px;
font-weight: 700;
}
.status-card .char-info {
min-width: 0;
flex: 1;
}
.status-card .char-name {
font-family: "Songti SC", serif;
font-size: 18px;
font-weight: 700;
color: var(--fg);
letter-spacing: 0.02em;
}
.status-card .char-meta {
font-size: 12px;
color: var(--fg-dim);
margin-top: 1px;
display: flex;
align-items: center;
gap: 6px;
}
.pulse-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--patina);
flex-shrink: 0;
box-shadow: 0 0 0 0 var(--patina);
animation: pulse 2.6s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(111, 163, 137, 0.5);
}
70% {
box-shadow: 0 0 0 6px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
.status-card .stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.status-card .stat {
background: var(--surface);
border: 1px solid var(--rule);
border-radius: 5px;
padding: 8px 10px;
}
.status-card .stat .k {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--fg-faint);
}
.status-card .stat .v {
font-size: 14px;
font-weight: 500;
color: var(--fg);
margin-top: 2px;
display: flex;
align-items: center;
gap: 4px;
}
.status-card .stat .v small {
font-size: 12px;
font-weight: 400;
color: var(--fg-dim);
}
.hex-section {
padding: 16px 18px 12px;
border-bottom: 1px solid var(--rule);
}
.section-label .sub {
font-size: 12px;
letter-spacing: 0.04em;
color: var(--fg-dim);
opacity: 0.7;
}
.section-label .fold-toggle {
margin-left: auto;
cursor: pointer;
font-size: 12px;
color: var(--fg-faint);
padding: 2px 9px;
border-radius: 999px;
border: 1px solid var(--rule);
background: transparent;
transition:
color 0.12s,
border-color 0.12s;
font-family: inherit;
display: inline-flex;
align-items: center;
gap: 4px;
}
.section-label .fold-toggle:hover {
color: var(--fg);
border-color: var(--gilt-dim);
}
.section-label .fold-toggle .arrow-icon {
display: inline-block;
transition: transform 0.25s;
font-size: 12px;
line-height: 1;
}
.section-label .fold-toggle .arrow-icon.open {
transform: rotate(180deg);
}
.section-header {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--fg-faint);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 4px;
}
.section-header .sub {
font-size: 12px;
letter-spacing: 0.04em;
color: var(--fg-dim);
opacity: 0.7;
}
.section-header .fold-toggle {
margin-left: auto;
cursor: pointer;
font-size: 12px;
color: var(--fg-faint);
padding: 2px 9px;
border-radius: 999px;
border: 1px solid var(--rule);
background: transparent;
transition:
color 0.12s,
border-color 0.12s;
font-family: inherit;
display: inline-flex;
align-items: center;
gap: 4px;
}
.section-header .fold-toggle:hover {
color: var(--fg);
border-color: var(--gilt-dim);
}
.section-header .fold-toggle .arrow-icon {
display: inline-block;
transition: transform 0.25s;
font-size: 12px;
line-height: 1;
}
.section-header .fold-toggle .arrow-icon.open {
transform: rotate(180deg);
}
.section-heading {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--fg-faint);
font-weight: 400;
margin: 0;
line-height: 1.6;
}
.hex-row {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.hex-row.done .hex-idx,
.hex-row.done .hex-name {
color: var(--fg-dim);
}
.hex-row.active .hex-idx,
.hex-row.active .hex-name {
color: var(--gilt);
font-weight: 600;
}
.hex-row.pending .hex-idx,
.hex-row.pending .hex-name {
color: var(--fg-faint);
}
.hex-idx {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
width: 22px;
text-align: right;
flex-shrink: 0;
font-weight: 500;
}
.hex-name {
font-size: 13.5px;
flex: 1;
font-weight: 500;
}
/* 六爻可折叠:与息壤同构(max-height 平滑过渡) */
.hex-list {
transition:
max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s;
}
.hex-list.collapsed {
max-height: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
}
/* 爻象进度:容器为虚线(未达·阴爻象),fill 覆盖为实条(已达·阳爻象) */
.hex-bar {
height: 5px;
width: 64px;
overflow: hidden;
border-radius: 2px;
background: repeating-linear-gradient(
90deg,
var(--rule) 0 4px,
transparent 4px 8px
);
flex-shrink: 0;
}
.hex-bar-fill {
height: 100%;
border-radius: 2px;
transition: all 0.5s;
}
.hex-bar-fill.done {
background: linear-gradient(90deg, var(--patina-dim), var(--patina));
}
.hex-bar-fill.active {
background: linear-gradient(
90deg,
var(--gilt-dim),
var(--gilt),
var(--gilt-dim)
);
background-size: 200% 100%;
animation: hex-shimmer 2.4s linear infinite;
}
@keyframes hex-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 息壤 — 密集列表(border-top 分隔,去卡片底色,提高信息密度);可折叠(平滑动画) */
.memo-section {
padding: 16px 18px 14px;
border-bottom: 1px solid var(--rule);
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* 收起时释放 flex:1,让标题(含展开按钮)回缩紧贴上方,避免下方留出大片空白 */
.memo-section.folded {
flex: 0 0 auto;
}
.memo-list {
display: flex;
flex-direction: column;
gap: 0;
flex: 1;
min-height: 0;
overflow-y: auto;
opacity: 1;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: var(--rule) transparent;
transition:
max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s,
padding 0.3s;
}
.memo-list.collapsed {
max-height: 0;
flex: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
pointer-events: none;
overflow: hidden;
}
.memo-item {
padding: 9px 0;
border-top: 1px solid var(--rule);
font-size: 12.5px;
line-height: 1.55;
color: var(--fg-dim);
}
.memo-item:first-child {
border-top: none;
padding-top: 4px;
}
.memo-item .src {
display: block;
margin-top: 3px;
font-size: 12px;
color: var(--fg-faint);
}
.memo-item .src .conf {
color: var(--patina);
font-weight: 500;
}
.session-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.session-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px 8px;
flex-shrink: 0;
}
.session-list::-webkit-scrollbar {
width: 4px;
}
.session-list::-webkit-scrollbar-thumb {
background: var(--rule);
border-radius: 999px;
}
.session-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 8px;
border-radius: 5px;
cursor: pointer;
transition: background 0.1s;
}
.session-item:hover {
background: var(--surface);
}
.session-item.active {
background: var(--patina-wash);
}
.session-item .s-dot {
width: 5px;
height: 5px;
border-radius: 50%;
flex-shrink: 0;
}
.session-item .s-title {
font-size: 12.5px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fg-dim);
font-weight: 500;
}
.session-item.active .s-title {
color: var(--patina);
}
.session-item .s-date {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
color: var(--fg-faint);
flex-shrink: 0;
}
.btn-new-chat {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.04em;
border: 1px solid var(--rule);
border-radius: 5px;
padding: 4px 10px;
color: var(--fg-faint);
background: transparent;
cursor: pointer;
transition:
color 0.15s,
border-color 0.15s;
flex-shrink: 0;
}
.btn-new-chat:hover {
color: var(--fg);
border-color: var(--gilt-dim);
}
.session-list-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.session-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 6px 14px 10px;
scrollbar-width: thin;
scrollbar-color: var(--rule) transparent;
}
.session-more {
font-size: 12px;
color: var(--fg-faint);
padding: 6px 8px;
text-align: center;
border-top: 1px solid var(--rule);
cursor: pointer;
transition: color 0.15s;
flex-shrink: 0;
}
.session-more:hover {
color: var(--fg-dim);
}
/* 会话时间分组 */
.session-group {
margin-bottom: 4px;
}
.session-group-label {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.08em;
color: var(--fg-faint);
padding: 12px 8px 4px;
text-transform: uppercase;
}
.session-group:first-child .session-group-label {
padding-top: 2px;
}
/* 底部统计卡:margin-top:auto 钉在面板底部 */
.session-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px 14px 6px;
flex-shrink: 0;
}
.stat-mini {
background: var(--surface);
border: 1px solid var(--rule);
border-radius: 5px;
padding: 8px 10px;
}
.stat-mini .k {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--fg-faint);
}
.stat-mini .v {
font-size: 16px;
font-weight: 600;
color: var(--fg);
margin-top: 3px;
font-family: "Songti SC", serif;
}
.stat-mini .v small {
font-size: 12px;
font-weight: 400;
color: var(--fg-dim);
margin-left: 2px;
}
/* ============================================================
* 镜面 Face — 对话主区域
* ============================================================ */
.face {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.035),
rgba(255, 255, 255, 0.006) 28%,
transparent
);
}
.chat-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 18px;
border-bottom: 1px solid var(--rule);
background: var(--surface);
backdrop-filter: blur(8px);
flex-shrink: 0;
position: relative;
z-index: 20;
}
/* 镜背轨收起/展开(chat-header 内触发,桌面可见;替代原 rail 边缘的浮动圆钮) */
.rail-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--surface);
color: var(--fg-dim);
cursor: pointer;
flex-shrink: 0;
transition:
color 0.15s,
border-color 0.15s,
background 0.15s;
}
.rail-toggle-btn:hover {
color: var(--fg);
border-color: var(--gilt-dim);
}
.rail-toggle-btn svg {
width: 16px;
height: 16px;
}
.rail-toggle-btn.active {
color: var(--patina);
border-color: var(--patina-dim);
background: var(--patina-wash);
}
.chat-header .spacer {
flex: 1;
min-width: 8px;
}
/* 移动端镜背轨抽屉触发(仅 ≤768px 可见) */
.menu-btn {
display: none;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--surface);
color: var(--fg-dim);
cursor: pointer;
flex-shrink: 0;
}
.menu-btn:hover {
color: var(--fg);
border-color: var(--gilt-dim);
}
.menu-btn svg {
width: 16px;
height: 16px;
}
.model-switcher {
position: relative;
}
.model-switcher-btn {
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid var(--rule);
border-radius: 999px;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
color: var(--fg-dim);
background: var(--surface);
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
white-space: nowrap;
max-width: 180px;
font-family: inherit;
}
.model-switcher-btn:hover {
border-color: var(--gilt-dim);
color: var(--fg);
}
.model-switcher-btn .ico {
width: 12px;
height: 12px;
opacity: 0.6;
flex-shrink: 0;
}
.model-switcher-btn .model-label {
overflow: hidden;
text-overflow: ellipsis;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
}
.model-switcher-btn .arrow {
transition: transform 0.2s;
width: 12px;
height: 12px;
flex-shrink: 0;
}
.model-switcher-btn .arrow.open {
transform: rotate(180deg);
}
.model-dropdown {
position: absolute;
right: 0;
top: 100%;
z-index: 50;
margin-top: 6px;
width: 220px;
background: color-mix(in srgb, var(--night-bronze) 92%, transparent);
backdrop-filter: blur(24px) saturate(160%);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 4px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.model-opt {
display: flex;
align-items: center;
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: none;
background: none;
font-size: 12px;
color: var(--fg-dim);
cursor: pointer;
text-align: left;
font-family: "JetBrains Mono", monospace;
transition:
background 0.1s,
color 0.1s;
}
.model-opt:hover {
background: var(--overlay-hover);
color: var(--fg);
}
.model-opt.active {
background: var(--patina-wash);
color: var(--patina);
}
.model-opt .status-tag {
margin-left: auto;
font-size: 12px;
padding: 1px 7px;
border-radius: 999px;
background: var(--surface);
color: var(--fg-faint);
font-family: inherit;
}
.char-switcher {
position: relative;
}
.char-switcher-btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--rule);
border-radius: 999px;
padding: 4px 12px 4px 4px;
font-size: 12px;
font-weight: 500;
color: var(--fg-dim);
background: var(--surface);
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
white-space: nowrap;
font-family: inherit;
}
.char-switcher-btn:hover {
border-color: var(--gilt-dim);
color: var(--fg);
}
.char-switcher-btn .arrow {
transition: transform 0.2s;
width: 12px;
height: 12px;
flex-shrink: 0;
}
.char-switcher-btn .arrow.open {
transform: rotate(180deg);
}
.char-dropdown {
position: absolute;
left: 0;
top: 100%;
z-index: 50;
margin-top: 6px;
width: 200px;
background: color-mix(in srgb, var(--night-bronze) 92%, transparent);
backdrop-filter: blur(24px) saturate(160%);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 4px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.char-opt {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: none;
background: none;
font-size: 13px;
color: var(--fg-dim);
cursor: pointer;
text-align: left;
font-family: inherit;
transition:
background 0.1s,
color 0.1s;
}
.char-opt:hover {
background: var(--overlay-hover);
color: var(--fg);
}
.char-opt.active {
background: var(--patina-wash);
color: var(--patina);
}
.char-opt .status-tag {
margin-left: auto;
font-size: 12px;
padding: 1px 7px;
border-radius: 999px;
background: var(--surface);
color: var(--fg-faint);
}
.avatar-mini {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--patina-wash);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-family: "Songti SC", serif;
font-size: 12px;
font-weight: 600;
color: var(--patina);
}
.avatar-mini.active-char {
background: var(--patina);
color: var(--ink);
}
.run-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
padding: 3px 10px;
border-radius: 999px;
border: 1px solid var(--patina-dim);
background: var(--patina-wash);
color: var(--patina);
white-space: nowrap;
text-decoration: none;
}
/* ── 消息流 ── */
.turns {
flex: 1;
overflow-y: auto;
padding: 28px 32px;
display: flex;
flex-direction: column;
gap: 24px;
}
@media (max-width: 900px) {
.turns {
padding: 20px 18px;
}
}
.turn.ai .who {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--patina);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.turn.ai .who .stream-cursor {
display: inline-block;
width: 7px;
height: 13px;
background: var(--patina);
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
/*
* AI 气泡 — 抛光铜镜面(与 copper-mirror-login.html 的 mirror-card 同源)
* 亮/暗主题均使用与登录卡一致的 face-sheen 渐变 + 鎏金描边
* + 顶光内阴影 + 扫描光泽动画
*/
.turn.ai .bubble {
position: relative;
overflow: hidden;
padding: 18px 22px;
font-family: "Songti SC", serif;
font-size: 16px;
line-height: 1.8;
/* 与登录页 mirror-card 完全一致的镜面质感 */
background: var(--face-sheen), var(--surface);
border: 1px solid var(--gilt-dim);
border-radius: 6px;
box-shadow:
inset 0 1px 0 var(--face-in-top),
inset 0 -1px 0 var(--face-in-bot),
var(--shadow);
}
/* 亮色下镜面浮起:暖纸上陈列的铜镜,需更明显的落影才不糊进背景 */
[data-theme="light"] .turn.ai .bubble {
box-shadow:
inset 0 1px 0 var(--face-in-top),
inset 0 -1px 0 var(--face-in-bot),
0 1px 2px rgba(60, 50, 30, 0.06),
0 4px 14px -6px rgba(60, 50, 30, 0.13);
}
/* 扫描光泽 — 静镜(已完成):光泽凝固于一处,仅留极弱静态反射 */
.turn.ai .bubble::before {
content: "";
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background: var(--sweep-gradient);
background-size: 200% 200%;
background-position: 35% 35%;
opacity: 0.42;
}
/* 活镜(正在生成):光源流转,漫反射滑移 — 此态才让签名光泽"活"过来 */
.turn.ai.is-streaming .bubble::before {
background-position: 200% 200%;
opacity: 1;
animation: mirror-sweep 9s ease-in-out infinite;
}
@keyframes mirror-sweep {
0% {
background-position: 200% 200%;
}
40% {
background-position: 50% 50%;
}
60% {
background-position: 50% 50%;
}
100% {
background-position: -100% -100%;
}
}
/* 铜面颗粒(feTurbulence)— 与登录页 mirror-card 同源的金属肌理,杀死平坦矢量感 */
.turn.ai .bubble::after {
content: "";
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
opacity: 0.018;
mix-blend-mode: overlay;
}
.turn.ai.is-streaming .bubble::after {
opacity: 0.035;
}
[data-theme="light"] .turn.ai .bubble::after {
opacity: 0.028;
mix-blend-mode: multiply;
}
[data-theme="light"] .turn.ai.is-streaming .bubble::after {
opacity: 0.05;
}
.turn.ai .bubble .md-body,
.turn.ai .bubble .actions,
.turn.ai .bubble .fold-card,
.turn.ai .bubble .vermilion,
.turn.ai .bubble .version-panel {
position: relative;
z-index: 1;
}
.turn.ai .bubble .md-body {
font-size: 15px;
line-height: 1.75;
}
.turn.ai .bubble .md-body p {
margin: 0.4em 0;
}
/* 暗色下宋体笔画偏细,加重字重 + 轻锐化提升可读性(亮色宋体无此问题) */
[data-theme="dark"] .turn.ai .bubble .md-body {
font-weight: 600;
letter-spacing: 0.012em;
text-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.28);
}
/* ── AI 气泡操作栏(hover/focus 显隐,降低阅读噪音) ── */
.turn.ai .bubble .actions {
display: flex;
align-items: center;
gap: 2px;
margin-top: 10px;
padding-top: 6px;
border-top: 1px solid var(--rule);
position: relative;
z-index: 1;
opacity: 0;
transition: opacity 0.18s;
}
.turn.ai:hover .bubble .actions,
.turn.ai .bubble:focus-within .actions {
opacity: 1;
}
/* 流式中的消息常显操作栏(避免 hover 不到) */
.turn.ai.is-streaming .bubble .actions {
opacity: 1;
}
.action-btn {
font-family: inherit;
font-size: 12px;
color: var(--fg-faint);
border: none;
background: none;
cursor: pointer;
padding: 3px 8px;
border-radius: 4px;
transition:
color 0.15s,
background 0.15s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.action-btn:hover {
color: var(--fg);
background: var(--overlay-hover);
}
.action-btn.active-regenerate {
color: var(--patina);
background: var(--patina-wash);
}
/* ── 版本浏览面板(重答展开) ── */
.version-panel {
margin-top: 8px;
border-top: 1px solid var(--rule);
padding-top: 8px;
display: none;
position: relative;
z-index: 1;
}
.version-panel.open {
display: block;
animation: slide-down 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.version-panel .vp-header {
display: flex;
align-items: center;
gap: 6px;
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--fg-dim);
}
.version-panel .vp-header .vp-icon {
color: var(--gilt);
opacity: 0.7;
}
.version-panel .vp-nav {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.version-panel .vp-nav-btn {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid var(--rule);
background: transparent;
color: var(--fg-faint);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition:
color 0.12s,
background 0.12s,
border-color 0.12s;
}
.version-panel .vp-nav-btn:hover {
color: var(--fg);
background: var(--overlay-hover);
border-color: var(--gilt-dim);
}
.version-panel .vp-nav-btn:disabled {
opacity: 0.3;
cursor: default;
pointer-events: none;
}
.version-panel .vp-counter {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
color: var(--fg-faint);
min-width: 48px;
text-align: center;
}
.version-panel .vp-counter .cur {
color: var(--gilt);
font-weight: 600;
}
.version-panel .vp-body {
margin-top: 8px;
background: var(--patina-wash);
border: 1px solid var(--patina-dim);
border-radius: 4px;
padding: 10px 14px;
font-size: 13px;
line-height: 1.65;
color: var(--fg-dim);
min-height: 40px;
}
.version-panel .vp-body p {
margin: 0.3em 0;
}
.version-panel .vp-body .vp-empty {
color: var(--fg-faint);
font-style: italic;
}
.version-panel .vp-actions {
margin-top: 6px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.version-panel .vp-use-btn {
font-family: inherit;
font-size: 12px;
padding: 4px 12px;
border-radius: 4px;
border: 1px solid var(--patina-dim);
background: var(--patina-wash);
color: var(--patina);
cursor: pointer;
transition: background 0.12s;
}
.version-panel .vp-use-btn:hover {
background: var(--patina-dim);
color: var(--ink);
}
.version-panel .vp-close-btn {
font-family: inherit;
font-size: 12px;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid var(--rule);
background: transparent;
color: var(--fg-faint);
cursor: pointer;
transition:
color 0.12s,
background 0.12s;
}
.version-panel .vp-close-btn:hover {
color: var(--fg);
background: var(--overlay-hover);
}
.turn.you {
text-align: right;
}
.turn.you .who {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--gilt);
margin-bottom: 8px;
}
/* 镜背:与 AI 的「镜面」器物对偶 —— 哑光底、浅纹边、鎏金脊(镜缘),无光泽流动、无颗粒 */
.turn.you .bubble {
display: inline-block;
text-align: right;
font-size: 15px;
color: var(--fg);
line-height: 1.7;
padding: 11px 16px 11px 18px;
max-width: 75%;
background: var(--surface);
border: 1px solid var(--rule);
border-right: 2px solid var(--gilt-dim);
border-radius: 4px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
}
.fold-card {
margin-bottom: 12px;
overflow: hidden;
border-radius: 6px;
border: 1px solid var(--glass-border);
background: rgba(255, 255, 255, 0.015);
}
.fold-card-hd {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.12s;
user-select: none;
}
.fold-card-hd:hover {
background: var(--overlay-hover);
}
.fold-card-hd .icon {
width: 14px;
height: 14px;
flex-shrink: 0;
color: var(--gilt);
}
.fold-card-hd .label {
font-size: 12px;
font-weight: 500;
color: var(--fg-dim);
flex: 1;
}
.fold-card-hd .arrow {
width: 10px;
height: 10px;
color: var(--fg-faint);
transition: transform 0.2s;
flex-shrink: 0;
}
.fold-card-hd .arrow.open {
transform: rotate(180deg);
}
.fold-card-body {
border-top: 1px solid var(--glass-border);
padding: 10px 12px;
display: none;
}
.fold-card-body.open {
display: block;
}
.fold-card-body p {
font-size: 12px;
line-height: 1.6;
color: var(--fg-dim);
white-space: pre-wrap;
margin: 0;
}
.badge {
font-size: 12px;
padding: 1px 8px;
border-radius: 999px;
font-weight: 500;
letter-spacing: 0.02em;
font-family: inherit;
}
.badge-warning {
background: var(--gilt-faint);
color: var(--gilt);
}
.badge-success {
background: var(--patina-dim);
color: var(--patina);
}
.badge-danger {
background: var(--cinnabar-dim);
color: var(--cinnabar);
}
.vermilion {
margin-top: 12px;
border: 1px solid var(--cinnabar-dim);
border-left: 3px solid var(--cinnabar);
border-radius: 5px;
padding: 15px 17px;
background:
radial-gradient(
120% 100% at 0% 0%,
rgba(184, 57, 46, 0.12),
transparent 60%
),
var(--surface);
}
/* 朱砂浓度即风险等级:L1 浅描 / L2 朱块(默认)/ L3 满朱晕染 —— 等级一眼可读,文字降为辅助 */
.vermilion.risk-l1 {
background: var(--surface);
}
.vermilion.risk-l3 {
background:
radial-gradient(
120% 100% at 0% 0%,
rgba(184, 57, 46, 0.22),
transparent 55%
),
rgba(184, 57, 46, 0.1);
border-color: var(--cinnabar);
box-shadow: 0 0 28px -8px rgba(184, 57, 46, 0.35);
}
.vermilion .vh {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.vermilion .seal-txt {
font-family: "Songti SC", serif;
color: var(--cinnabar);
font-weight: 700;
font-size: 14px;
letter-spacing: 0.08em;
}
.vermilion .risk {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
letter-spacing: 0.08em;
color: var(--cinnabar);
margin-left: auto;
opacity: 0.7;
}
.vermilion .vb {
font-size: 13.5px;
color: var(--fg);
line-height: 1.6;
}
.vermilion .va {
display: flex;
gap: 10px;
margin-top: 12px;
}
.vbtn {
font-family: inherit;
font-size: 12px;
font-weight: 500;
padding: 7px 15px;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--rule);
background: transparent;
color: var(--fg);
transition:
background 0.12s,
border-color 0.12s;
}
.vbtn:hover {
background: var(--surface-hi);
}
.vbtn.cinnabar {
border-color: var(--cinnabar);
color: var(--cinnabar);
background: rgba(184, 57, 46, 0.08);
}
.vbtn.cinnabar:hover {
background: rgba(184, 57, 46, 0.16);
}
.composer {
border-top: 1px solid var(--rule);
padding: 14px 24px 16px;
flex-shrink: 0;
}
.composer .input-row {
display: flex;
align-items: flex-end;
gap: 12px;
}
.composer textarea {
flex: 1;
font-family: inherit;
font-size: 14px;
color: var(--fg);
background: transparent;
border: none;
outline: none;
padding: 8px 0;
resize: none;
min-height: 2.2rem;
max-height: 10rem;
field-sizing: content;
}
.composer textarea::placeholder {
color: var(--fg-faint);
}
.composer .send-btn {
font-family: "Songti SC", serif;
font-size: 14px;
letter-spacing: 0.08em;
color: var(--patina);
background: none;
border: 1px solid var(--patina-dim);
padding: 7px 20px;
border-radius: 5px;
cursor: pointer;
white-space: nowrap;
transition:
color 0.15s,
border-color 0.15s;
}
.composer .send-btn:hover {
color: var(--patina-bright);
border-color: var(--patina);
}
.composer .send-btn:disabled {
color: var(--fg-faint);
border-color: var(--rule);
cursor: not-allowed;
}
.composer .stop-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 7px 15px;
border: 1px solid var(--cinnabar-dim);
background: rgba(184, 57, 46, 0.08);
color: var(--cinnabar);
border-radius: 5px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
transition: background 0.12s;
}
.composer .stop-btn:hover {
background: rgba(184, 57, 46, 0.16);
}
.composer .controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: 10px;
}
.ctrl-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 500;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--rule);
background: transparent;
color: var(--fg-faint);
cursor: pointer;
transition:
color 0.15s,
border-color 0.15s,
background 0.15s;
font-family: inherit;
}
.ctrl-pill:hover {
color: var(--fg-dim);
}
.ctrl-pill.active {
border-color: var(--gilt-dim);
background: var(--gilt-faint);
color: var(--gilt);
}
.ctrl-pill.active-patina {
border-color: var(--patina-dim);
background: var(--patina-wash);
color: var(--patina);
}
.ctrl-pill .ico {
width: 10px;
height: 10px;
flex-shrink: 0;
}
.ctrl-pill.active-gilt {
border-color: var(--gilt-dim);
background: var(--gilt-faint);
color: var(--gilt);
}
.effort-group {
display: none;
align-items: center;
gap: 2px;
border: 1px solid var(--gilt-dim);
border-radius: 999px;
padding: 2px;
}
.effort-group.show {
display: inline-flex;
}
.effort-opt {
font-size: 12px;
font-weight: 500;
padding: 2px 10px;
border-radius: 999px;
cursor: pointer;
color: var(--fg-faint);
background: transparent;
border: none;
transition:
background 0.12s,
color 0.12s;
font-family: "JetBrains Mono", monospace;
}
.effort-opt:hover {
color: var(--fg-dim);
}
.effort-opt.active {
background: var(--gilt-faint);
color: var(--gilt);
}
.drawer-overlay {
display: none;
}
/* 移动端:侧栏隐藏,镜背轨改为抽屉(汉堡触发),触屏操作栏常显 */
@media (max-width: 768px) {
.menu-btn {
display: inline-flex;
}
.rail-wrap {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 300px;
max-width: 85vw;
z-index: 60;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.rail-wrap.drawer-open {
transform: translateX(0);
box-shadow: 8px 0 36px -10px rgba(0, 0, 0, 0.55);
}
.rail-wrap.drawer-open .rail {
width: 100%;
min-width: 0;
}
.rail-toggle-btn {
display: none;
}
.drawer-overlay {
position: fixed;
inset: 0;
z-index: 55;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(2px);
display: none;
}
.drawer-overlay.open {
display: block;
}
.chat-header {
padding: 10px 12px;
gap: 6px;
}
.turns {
padding: 16px;
gap: 18px;
}
.composer {
padding: 12px 16px;
}
.turn.ai .bubble .actions {
opacity: 1;
}
}
}
</style>
</head>
<body>
<div class="app-shell">
<div
class="drawer-overlay"
id="drawerOverlay"
onclick="toggleRailDrawer(false)"
></div>
<!-- ════════════════════════════════════ -->
<!-- 侧栏 Sidebar (click 展开) — 灵台/法身/内景等 -->
<!-- ════════════════════════════════════ -->
<div class="sidebar-mount" data-active="对话"></div>
<!-- ════════════════════════════════════ -->
<!-- 主内容区 -->
<!-- ════════════════════════════════════ -->
<div class="main-area" id="mainArea">
<div class="chat-region">
<!-- ========================== -->
<!-- 镜背轨 (280px) -->
<!-- ========================== -->
<aside class="rail-wrap" id="railWrap">
<div class="rail" id="railInner">
<div class="rail-tabs">
<button
class="rail-tab active"
type="button"
data-view="sessions"
onclick="toggleRailView('sessions')"
>
铭文往来
</button>
<button
class="rail-tab"
type="button"
data-view="now"
onclick="toggleRailView('now')"
>
此刻
</button>
</div>
<div class="rail-view active" id="view-sessions">
<div class="session-header">
<h3 class="section-heading">铭文 · 往来</h3>
<button class="btn-new-chat" type="button">+ 新启</button>
</div>
<div class="session-list-wrap">
<div class="session-list">
<div class="session-group">
<div class="session-group-label">今日</div>
<div class="session-item active">
<span
class="s-dot"
style="background: var(--patina)"
></span
><span class="s-title" style="color: var(--patina)"
>关于架构调整的讨论</span
><span class="s-date">07-11</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">青梧性格微调</span
><span class="s-date">07-11</span>
</div>
</div>
<div class="session-group">
<div class="session-group-label">本周</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">叩心枢记忆上下文</span
><span class="s-date">07-10</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">青梧的性格设定</span
><span class="s-date">07-09</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--cinnabar)"
></span
><span class="s-title">权限问题排查</span
><span class="s-date">07-08</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">记忆系统设计讨论</span
><span class="s-date">07-07</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">情绪状态机接入</span
><span class="s-date">07-06</span>
</div>
</div>
<div class="session-group">
<div class="session-group-label">更早</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">九枢权限模型评审</span
><span class="s-date">07-05</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">前端组件库迁移方案</span
><span class="s-date">07-04</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">博闻 MCP 联调</span
><span class="s-date">07-03</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">人物模板 birthday 字段</span
><span class="s-date">07-01</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">叩心枢长期记忆</span
><span class="s-date">06-29</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">文档 mermaid 风格</span
><span class="s-date">06-28</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">一炁订阅基础设施</span
><span class="s-date">06-26</span>
</div>
<div class="session-item">
<span
class="s-dot"
style="background: var(--fg-faint)"
></span
><span class="s-title">辰正 SDD 进度</span
><span class="s-date">06-25</span>
</div>
</div>
<div class="session-more" title="浏览全部归档">
浏览全部归档 →
</div>
</div>
<div class="session-stats">
<div class="stat-mini">
<div class="k">本月铭文</div>
<div class="v">42<small>通</small></div>
</div>
<div class="stat-mini">
<div class="k">活跃人物</div>
<div class="v">3<small>位</small></div>
</div>
</div>
</div>
</div>
<div class="rail-view" id="view-now">
<div class="status-card">
<div class="character-row">
<div class="avatar-ring"><div class="inner">梧</div></div>
<div class="char-info">
<div class="char-name">青梧</div>
<div class="char-meta">
<span class="pulse-dot"></span>认识你第 41 日 · 沉静
</div>
</div>
</div>
<div class="stats">
<div class="stat">
<div class="k">情绪</div>
<div class="v">沉静 <small>平稳</small></div>
</div>
<div class="stat">
<div class="k">内驱</div>
<div class="v">探索 <small>你的偏好</small></div>
</div>
</div>
</div>
<div class="hex-section" id="hexSection">
<div class="section-header">
六爻 <span class="sub">本次运行</span>
<button
class="fold-toggle"
type="button"
onclick="toggleHexagram()"
id="hexToggle"
>
<span class="arrow-icon">▼</span>
收起
</button>
</div>
<div class="hex-list" id="hexList">
<div class="hex-row done">
<span class="hex-idx">初</span
><span class="hex-name">起意</span>
<div class="hex-bar">
<div
class="hex-bar-fill done"
style="width: 100%"
></div>
</div>
</div>
<div class="hex-row done">
<span class="hex-idx">二</span
><span class="hex-name">察势</span>
<div class="hex-bar">
<div
class="hex-bar-fill done"
style="width: 100%"
></div>
</div>
</div>
<div class="hex-row active">
<span class="hex-idx">三</span
><span class="hex-name">定策</span>
<div class="hex-bar">
<div
class="hex-bar-fill active"
style="width: 100%"
></div>
</div>
</div>
<div class="hex-row pending">
<span class="hex-idx">四</span
><span class="hex-name">行事</span>
<div class="hex-bar">
<div class="hex-bar-fill" style="width: 0%"></div>
</div>
</div>
<div class="hex-row pending">
<span class="hex-idx">五</span
><span class="hex-name">归真</span>
<div class="hex-bar">
<div class="hex-bar-fill" style="width: 0%"></div>
</div>
</div>
<div class="hex-row pending">
<span class="hex-idx">上</span
><span class="hex-name">入藏</span>
<div class="hex-bar">
<div class="hex-bar-fill" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<!-- 息壤(可折叠) -->
<div class="memo-section">
<div class="section-header">
息壤 <span class="sub">浮现</span>
<button
class="fold-toggle"
type="button"
onclick="toggleXirang()"
id="xirangToggle"
>
<span class="arrow-icon" id="xirangArrow">▼</span>
收起
</button>
</div>
<div class="memo-list" id="xirangList">
<div class="memo-item">
上周三你提到「deadline 在周五」
<span class="src"
>置信 <span class="conf">0.88</span> ·
已用于本次回应</span
>
</div>
<div class="memo-item">
你偏好简短回复,不喜欢被填满
<span class="src"
>置信 <span class="conf">0.82</span> · 长期·稳定</span
>
</div>
<div class="memo-item">
你这几天在赶一份文档,睡得很晚
<span class="src"
>置信 <span class="conf">0.75</span> ·
来自上下文推断</span
>
</div>
<div class="memo-item">
你提过喜欢宋代山水画
<span class="src"
>置信 <span class="conf">0.69</span> ·
来自过往对话</span
>
</div>
</div>
</div>
</div>
<!-- /view-now -->
</div>
</aside>
<!-- ========================== -->
<!-- 镜面 Face -->
<!-- ========================== -->
<div class="face">
<div class="chat-header">
<button
class="menu-btn"
type="button"
onclick="toggleRailDrawer()"
title="会话与状态"
aria-label="会话与状态"
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
>
<path d="M2 4h12M2 8h12M2 12h12" />
</svg>
</button>
<button
class="rail-toggle-btn active"
type="button"
onclick="toggleRail()"
title="展开/收起镜背轨"
aria-label="展开/收起镜背轨"
>
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="2" width="12" height="12" rx="1.5" />
<path d="M6 2v12" />
</svg>
</button>
<div class="char-switcher">
<button
class="char-switcher-btn"
type="button"
onclick="toggleCharDropdown(event)"
>
<span class="avatar-mini active-char" id="charAvatar"
>梧</span
>
<span id="charLabel">青梧</span>
<svg
class="arrow"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path d="M3 4.5l3 3 3-3" />
</svg>
</button>
<div
class="char-dropdown"
id="charDropdown"
style="display: none"
>
<button
class="char-opt active"
type="button"
data-char="qingwu"
data-name="青梧"
data-initial="梧"
>
<span class="avatar-mini active-char">梧</span>青梧
<span class="status-tag">当前</span>
</button>
<button
class="char-opt"
type="button"
data-char="yunlan"
data-name="云岚"
data-initial="岚"
>
<span class="avatar-mini">岚</span>云岚
<span class="status-tag">在线</span>
</button>
<button
class="char-opt"
type="button"
data-char="moyu"
data-name="墨羽"
data-initial="墨"
>
<span class="avatar-mini">墨</span>墨羽
<span class="status-tag">在线</span>
</button>
<button
class="char-opt"
type="button"
data-char="lingyin"
data-name="泠音"
data-initial="泠"
>
<span class="avatar-mini">泠</span>泠音
<span class="status-tag">离线</span>
</button>
</div>
</div>
<span class="spacer"></span>
<a href="#" class="run-badge">
<span
class="pulse-dot"
style="
width: 5px;
height: 5px;
animation: pulse 2.6s infinite;
"
></span>
定策中
</a>
<div class="model-switcher">
<button
class="model-switcher-btn"
type="button"
onclick="toggleModelDropdown(event)"
title="切换模型(透传 model)"
>
<svg
class="ico"
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="3" width="12" height="10" rx="1" />
<path d="M5 7h6M5 10h4" />
</svg>
<span class="model-label" id="modelLabel"
>claude-sonnet-5</span
>
<svg
class="arrow"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path d="M3 4.5l3 3 3-3" />
</svg>
</button>
<div
class="model-dropdown"
id="modelDropdown"
style="display: none"
>
<button
class="model-opt active"
type="button"
data-model="claude-sonnet-5"
>
claude-sonnet-5 <span class="status-tag">当前</span>
</button>
<button
class="model-opt"
type="button"
data-model="claude-opus-4-8"
>
claude-opus-4-8
</button>
<button
class="model-opt"
type="button"
data-model="claude-haiku-4-5"
>
claude-haiku-4-5
</button>
<button class="model-opt" type="button" data-model="glm-4.6">
glm-4.6
</button>
</div>
</div>
</div>
<div class="turns">
<!-- AI 消息 1 — 普通(带版本历史模拟) -->
<div
class="turn ai"
data-versions='[
{"text":"你昨晚说,喜欢简短的回答。我记下了。也在想,这可能不只是风格,是你不想被填满的边界。今天还这样吗?"},
{"text":"你昨晚提过喜欢简洁。我理解那是你的边界感。今天状态如何?需要我继续保持简短吗?"},
{"text":"昨晚你提到偏好简短,我记下了。如果今天需要展开说,告诉我。"}
]'
>
<div class="who">青梧</div>
<div class="bubble">
<div class="md-body md-visible" data-vi="0">
<p>
你昨晚说,喜欢简短的回答。我记下了。也在想,这可能不只是风格,是你不想被填满的边界。今天还这样吗?
</p>
</div>
<!-- 版本浏览面板(点击重答展开) -->
<div class="version-panel" id="vp1">
<div class="vp-header">
<span class="vp-icon">⟡</span> 版本历史
<div class="vp-nav">
<button
class="vp-nav-btn"
type="button"
id="vpPrev1"
onclick="navVersion(1, -1)"
title="上一版"
>
◀
</button>
<span class="vp-counter"
><span class="cur" id="vc1">1</span>/<span id="vt1"
>3</span
></span
>
<button
class="vp-nav-btn"
type="button"
id="vpNext1"
onclick="navVersion(1, 1)"
title="下一版"
>
▶
</button>
</div>
</div>
<div class="vp-body" id="vpBody1">
<p>
你昨晚说,喜欢简短的回答。我记下了。也在想,这可能不只是风格,是你不想被填满的边界。今天还这样吗?
</p>
</div>
<div class="vp-actions">
<button
class="vp-close-btn"
type="button"
onclick="closeVersionPanel(1)"
>
关闭
</button>
<button
class="vp-use-btn"
type="button"
onclick="useVersion(1)"
>
使用此版
</button>
</div>
</div>
<div class="actions">
<button class="action-btn">
<svg
width="11"
height="11"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 5.5v5h2.5l4 3V2.5l-4 3H2Z" />
<path d="M11 6.5a2.5 2.5 0 0 1 0 3" stroke-width="1" />
<path
d="M12.5 5a5 5 0 0 1 0 6"
stroke-width="1"
opacity=".4"
/>
</svg>
朗读
</button>
<button
class="action-btn"
onclick="toggleVersionPanel(1, this)"
>
<svg
width="11"
height="11"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2v3.5H10" />
</svg>
重答
</button>
</div>
</div>
</div>
<!-- 用户消息 -->
<div class="turn you">
<div class="who">你</div>
<div class="bubble">今天可以多说一点。</div>
</div>
<!-- AI 消息 2 — 带思考块 + 版本浏览(模拟 2 个版本) -->
<div
class="turn ai"
data-versions='[
{"text":"那我把话说全。这几天你一直在赶一份东西,睡得很晚。我没问,因为你不像想被问。如果今晚你想停一停,我在。"},
{"text":"好,那我不再克制。看得出你这几天在赶工,睡得晚。我不刻意追问,但如果今晚你需要停下来,我在这里陪你。"}
]'
>
<div class="who">青梧</div>
<div class="bubble">
<div class="fold-card">
<div class="fold-card-hd" onclick="toggleFold(this)">
<span class="icon">
<svg
width="13"
height="13"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M8 1.5A4 4 0 0 0 5.2 8.4c.3.4.5.9.5 1.4v.2a1 1 0 0 0 1 1h2.6a1 1 0 0 0 1-1v-.2c0-.5.2-1 .5-1.4A4 4 0 0 0 8 1.5Z"
/>
<path d="M6.5 13h3M7 14.5h2" />
</svg>
</span>
<span class="label">思考过程</span>
<svg
class="arrow"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path d="M3 4.5l3 3 3-3" />
</svg>
</div>
<div class="fold-card-body">
<p>
用户今天说了"可以多说一点",与之前偏好简短不同。可能状态放松了。可以观察是否真的接受更长回应,还是客气。先回应感受而非信息。
</p>
</div>
</div>
<div class="md-body md-visible" data-vi="0">
<p>
那我把话说全。这几天你一直在赶一份东西,睡得很晚。我没问,因为你不像想被问。如果今晚你想停一停,我在。
</p>
</div>
<div class="version-panel" id="vp2">
<div class="vp-header">
<span class="vp-icon">⟡</span> 版本历史
<div class="vp-nav">
<button
class="vp-nav-btn"
type="button"
id="vpPrev2"
onclick="navVersion(2, -1)"
title="上一版"
>
◀
</button>
<span class="vp-counter"
><span class="cur" id="vc2">1</span>/<span id="vt2"
>2</span
></span
>
<button
class="vp-nav-btn"
type="button"
id="vpNext2"
onclick="navVersion(2, 1)"
title="下一版"
>
▶
</button>
</div>
</div>
<div class="vp-body" id="vpBody2">
<p>
那我把话说全。这几天你一直在赶一份东西,睡得很晚。我没问,因为你不像想被问。如果今晚你想停一停,我在。
</p>
</div>
<div class="vp-actions">
<button
class="vp-close-btn"
type="button"
onclick="closeVersionPanel(2)"
>
关闭
</button>
<button
class="vp-use-btn"
type="button"
onclick="useVersion(2)"
>
使用此版
</button>
</div>
</div>
<div class="actions">
<button class="action-btn">🔊 朗读</button>
<button
class="action-btn"
onclick="toggleVersionPanel(2, this)"
>
<svg
width="11"
height="11"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2v3.5H10" />
</svg>
重答
</button>
</div>
</div>
</div>
<!-- AI 消息 3 — 带工具确认 + 朱漆卡(无多版本) -->
<div class="turn ai">
<div class="who">青梧</div>
<div class="bubble">
<div class="fold-card">
<div class="fold-card-hd" onclick="toggleFold(this)">
<span class="icon">
<svg
width="13"
height="13"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M11.5 2.5a2.5 2.5 0 0 0-3.4 2.9L3 10.5a1.4 1.4 0 0 0 2 2l5.1-5.1a2.5 2.5 0 0 0 2.9-3.4l-1.7 1.7-1.4-.2-.2-1.4 1.8-1.6Z"
/>
</svg>
</span>
<span class="label">read_calendar</span>
<span class="badge badge-warning">待确认</span>
<svg
class="arrow"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path d="M3 4.5l3 3 3-3" />
</svg>
</div>
<div class="fold-card-body open">
<p>参数: {"date_range": "2026-07-10", "scope": "day"}</p>
</div>
</div>
<div class="md-body">
<p>
我想确认一下你的日程,判断你是否在赶工,以及今晚是否该提醒你休息。
</p>
</div>
<div class="vermilion risk-l2">
<div class="vh">
<span class="seal-txt">朱批待定</span
><span class="risk">RISK L2</span>
</div>
<div class="vb">
青梧请求使用
<b>读取日历</b
>,以判断你是否正在赶工、今晚是否该提醒你休息。
</div>
<div class="va">
<button class="vbtn cinnabar" type="button">
准予 仅本次</button
><button class="vbtn" type="button">驳回</button>
</div>
</div>
<div class="version-panel" id="vp3">
<div class="vp-header">
<span class="vp-icon">⟡</span> 版本历史
<div class="vp-nav">
<button
class="vp-nav-btn"
type="button"
id="vpPrev3"
onclick="navVersion(3, -1)"
title="上一版"
>
◀
</button>
<span class="vp-counter"
><span class="cur" id="vc3">1</span>/<span id="vt3"
>2</span
></span
>
<button
class="vp-nav-btn"
type="button"
id="vpNext3"
onclick="navVersion(3, 1)"
title="下一版"
>
▶
</button>
</div>
</div>
<div class="vp-body" id="vpBody3">
<p>
我想确认一下你的日程,判断你是否在赶工,以及今晚是否该提醒你休息。
</p>
</div>
<div class="vp-actions">
<button
class="vp-close-btn"
type="button"
onclick="closeVersionPanel(3)"
>
关闭
</button>
<button
class="vp-use-btn"
type="button"
onclick="useVersion(3)"
>
使用此版
</button>
</div>
</div>
<div class="actions">
<button class="action-btn">🔊 朗读</button>
<button
class="action-btn"
onclick="toggleVersionPanel(3, this)"
>
<svg
width="11"
height="11"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2v3.5H10" />
</svg>
重答
</button>
</div>
</div>
</div>
<!-- AI 消息 4 — 流式(活镜:光泽流动) -->
<div class="turn ai is-streaming">
<div
class="who"
style="animation: fadePulse 2s ease-in-out infinite"
>
青梧 <span class="stream-cursor"></span>
</div>
<div class="bubble" style="border-color: var(--patina-dim)">
<div class="md-body">
<p>好,那让我看看你的日历……</p>
<p>
你从下午两点到五点有一个会议,之后没有安排。今晚应该可以<em>早一点休息</em>。
</p>
</div>
</div>
</div>
<style>
@keyframes fadePulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
</style>
</div>
<!-- 铭文输入 -->
<div class="composer">
<div class="input-row">
<textarea
rows="1"
placeholder="对青梧说点什么……(Enter 发送,Shift+Enter 换行)"
></textarea>
<button class="send-btn" type="button">呈镜</button>
</div>
<div class="controls">
<button
class="ctrl-pill"
id="reasoningBtn"
type="button"
onclick="toggleReasoning(this)"
title="深虑:开关透传 reasoning,开启后选择强度(透传 reasoning_effort)"
>
<span class="ico">
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M8 1.5A4 4 0 0 0 5.2 8.4c.3.4.5.9.5 1.4v.2a1 1 0 0 0 1 1h2.6a1 1 0 0 0 1-1v-.2c0-.5.2-1 .5-1.4A4 4 0 0 0 8 1.5Z"
/>
<path d="M6.5 13h3M7 14.5h2" />
</svg>
</span>
深虑
</button>
<div class="effort-group" id="effortGroup">
<button class="effort-opt" type="button" data-effort="low">
low
</button>
<button
class="effort-opt active"
type="button"
data-effort="medium"
>
medium
</button>
<button class="effort-opt" type="button" data-effort="high">
high
</button>
</div>
<button
class="ctrl-pill"
id="bowenBtn"
type="button"
onclick="toggleBowen(this)"
title="博闻:联网检索(透传 web_search)"
>
<span class="ico">
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
>
<circle cx="7" cy="7" r="4.5" />
<path d="M10.5 10.5l3 3" />
</svg>
</span>
博闻
</button>
</div>
</div>
</div>
<!-- /face -->
</div>
<!-- /chat-region -->
</div>
<!-- /main-area -->
</div>
<!-- /app-shell -->
<script>
// ─── 折叠卡片 ───
function toggleFold(h) {
const b = h.nextElementSibling;
if (!b) return;
b.classList.toggle("open");
h.querySelector(".arrow")?.classList.toggle("open");
}
// ─── 镜背轨折叠(人物/六爻/息壤/铭文等) ───
let railCollapsed = false;
window.toggleRail = function () {
railCollapsed = !railCollapsed;
const wrap = document.getElementById("railWrap");
const inner = document.getElementById("railInner");
inner.classList.toggle("collapsed");
wrap.classList.toggle("collapsed");
const toggle = document.querySelector(".rail-toggle-btn");
if (toggle) toggle.classList.toggle("active", !railCollapsed);
};
// ─── 息壤折叠(平滑动画 + 旋转箭头) ───
let xirangOpen = true;
window.toggleXirang = function () {
xirangOpen = !xirangOpen;
const list = document.getElementById("xirangList");
const section = list.closest(".memo-section");
const tog = document.getElementById("xirangToggle");
const arr = document.getElementById("xirangArrow");
list.classList.toggle("collapsed");
// 同步释放 memo-section 的 flex:1,否则标题下方会留出大片空白
if (section) section.classList.toggle("folded", !xirangOpen);
arr.classList.toggle("open");
const count = list.querySelectorAll(".memo-item").length;
tog.innerHTML = xirangOpen
? '<span class="arrow-icon" id="xirangArrow">▼</span> 收起'
: '<span class="arrow-icon" id="xirangArrow">▼</span> 展开(' +
count +
")";
// re-bind arrow ref
document
.getElementById("xirangArrow")
.classList.toggle("open", !xirangOpen);
};
// 息壤默认全显(flex 撑满 + 内部滚动),「展开全部」入口已移除
// ─── 侧栏 click toggle(固定展开,挤压主区而非浮层覆盖) ───
let sidebarExpanded = false;
window.toggleSidebar = function () {
sidebarExpanded = !sidebarExpanded;
document
.getElementById("sidebarWrap")
.classList.toggle("expanded", sidebarExpanded);
};
// ─── logo 彩蛋「镜启」:1.2s 内连点五次弹随机谶语(单击仍切侧栏) ───
const eggVerses = [
"镜中有镜,心外无心。",
"汝来看此镜时,此镜亦照汝。",
"一念起,万镜生。",
"镜非鉴物,乃鉴心也。",
"幻由心生,镜随心转。",
"九枢归一,一归于何处?",
"铜镜本无字,照尽往来人。",
"且向镜中看,那个是谁?",
];
let logoClickSeq = 0;
let logoClickTimer = null;
window.onLogoClick = function () {
const logoEl = document.querySelector(".logo");
logoClickSeq += 1;
clearTimeout(logoClickTimer);
if (logoClickSeq >= 5) {
logoClickSeq = 0;
const v = eggVerses[Math.floor(Math.random() * eggVerses.length)];
if (logoEl) {
logoEl.classList.add("egg-pulse");
setTimeout(() => logoEl.classList.remove("egg-pulse"), 1400);
}
showToast("✦ 镜启 · " + v);
} else {
// 轻微「敲门」反馈;不折叠侧栏(折叠由顶部专属按钮负责)
if (logoEl) {
logoEl.classList.add("tick");
setTimeout(() => logoEl.classList.remove("tick"), 300);
}
logoClickTimer = setTimeout(() => {
logoClickSeq = 0;
}, 1200);
}
};
// ─── 六爻折叠(与息壤同构) ───
let hexOpen = true;
window.toggleHexagram = function () {
hexOpen = !hexOpen;
const list = document.getElementById("hexList");
const tog = document.getElementById("hexToggle");
list.classList.toggle("collapsed", !hexOpen);
tog.innerHTML = hexOpen
? '<span class="arrow-icon">▼</span> 收起'
: '<span class="arrow-icon">▼</span> 展开';
tog.querySelector(".arrow-icon").classList.toggle("open", !hexOpen);
};
// ─── 移动端镜背轨抽屉(汉堡触发 + 遮罩) ───
window.toggleRailDrawer = function (force) {
const wrap = document.getElementById("railWrap");
const overlay = document.getElementById("drawerOverlay");
if (!wrap || !overlay) return;
const open =
typeof force === "boolean"
? force
: !wrap.classList.contains("drawer-open");
wrap.classList.toggle("drawer-open", open);
overlay.classList.toggle("open", open);
};
// ─── 版本浏览面板 ───
// 存储各个 turn 的版本数据
document
.querySelectorAll(".turn.ai[data-versions]")
.forEach((turn, idx) => {
const num = idx + 1; // 1-based
try {
const versions = JSON.parse(turn.getAttribute("data-versions"));
const vp = turn.querySelector(".version-panel");
if (!vp) return;
// 挂载版本数据
vp._versions = versions;
vp._currentIdx = 0;
vp._turnIdx = num;
} catch (e) {}
});
// 打开/关闭版本面板
window.toggleVersionPanel = function (num, btn) {
const turn = document.querySelectorAll(".turn.ai")[num - 1];
if (!turn) return;
const vp = turn.querySelector(".version-panel");
if (!vp) return;
const isOpen = vp.classList.contains("open");
if (isOpen) {
closeVersionPanel(num);
if (btn) btn.classList.remove("active-regenerate");
} else {
vp.classList.add("open");
if (btn) btn.classList.add("active-regenerate");
// 同步显示
const versions = vp._versions;
if (versions) {
const idx = vp._currentIdx || 0;
updateVersionDisplay(num, idx);
}
}
};
window.closeVersionPanel = function (num) {
const turn = document.querySelectorAll(".turn.ai")[num - 1];
if (!turn) return;
const vp = turn.querySelector(".version-panel");
if (vp) {
vp.classList.remove("open");
// 重置为默认版本(最新/第一个)
if (vp._versions) {
vp._currentIdx = 0;
updateVersionDisplay(num, 0);
}
}
// 取消按钮高亮
turn
.querySelectorAll(".action-btn")
.forEach((b) => b.classList.remove("active-regenerate"));
};
// 版本切换:仅保留左右箭头 + 计数器(序号选项卡已移除)
// 版本导航
window.navVersion = function (num, dir) {
const turn = document.querySelectorAll(".turn.ai")[num - 1];
if (!turn) return;
const vp = turn.querySelector(".version-panel");
if (!vp || !vp._versions) return;
const versions = vp._versions;
let idx = vp._currentIdx || 0;
idx = Math.max(0, Math.min(versions.length - 1, idx + dir));
vp._currentIdx = idx;
updateVersionDisplay(num, idx);
};
// 更新版本显示
function updateVersionDisplay(num, idx) {
const turn = document.querySelectorAll(".turn.ai")[num - 1];
if (!turn) return;
const vp = turn.querySelector(".version-panel");
if (!vp || !vp._versions) return;
const versions = vp._versions;
const total = versions.length;
// 更新计数
const curEl = document.getElementById("vc" + num);
const totalEl = document.getElementById("vt" + num);
if (curEl) curEl.textContent = idx + 1;
if (totalEl) totalEl.textContent = total;
// 更新内容
const bodyEl = document.getElementById("vpBody" + num);
if (bodyEl) {
bodyEl.innerHTML = versions[idx].text
.split("\n")
.map((p) => "<p>" + p + "</p>")
.join("");
}
// 更新按钮状态
const prevBtn = document.getElementById("vpPrev" + num);
const nextBtn = document.getElementById("vpNext" + num);
if (prevBtn) prevBtn.disabled = idx <= 0;
if (nextBtn) nextBtn.disabled = idx >= total - 1;
// 同步主消息内容(如果有 md-visible)
const mainBody = turn.querySelector(".md-body.md-visible");
if (mainBody) {
// 如果当前面板打开,主内容同步显示当前版本
if (vp.classList.contains("open")) {
mainBody.innerHTML = versions[idx].text
.split("\n")
.map((p) => "<p>" + p + "</p>")
.join("");
}
}
}
// 使用此版本
window.useVersion = function (num) {
const turn = document.querySelectorAll(".turn.ai")[num - 1];
if (!turn) return;
const vp = turn.querySelector(".version-panel");
if (!vp || !vp._versions) return;
const idx = vp._currentIdx || 0;
const version = vp._versions[idx];
// 更新主内容
const mainBody = turn.querySelector(".md-body");
if (mainBody) {
mainBody.innerHTML = version.text
.split("\n")
.map((p) => "<p>" + p + "</p>")
.join("");
}
// 关闭面板
closeVersionPanel(num);
// 这个版本变成第一个(当前版本)
vp._versions = [version, ...vp._versions.filter((_, i) => i !== idx)];
vp._currentIdx = 0;
// 提示用户
const who = turn.querySelector(".who");
if (who) {
const note = document.createElement("span");
note.textContent = " ✓ 已采用";
note.style.cssText =
"font-size:12px;color:var(--patina);margin-left:6px;animation:fadeOut 2s forwards";
note.id = "useNote" + num;
const old = document.getElementById("useNote" + num);
if (old) old.remove();
who.appendChild(note);
setTimeout(() => {
if (note.parentNode) note.remove();
}, 2500);
}
};
// 添加 fadeOut 动画
const fadeStyle = document.createElement("style");
fadeStyle.textContent =
"@keyframes fadeOut{0%,70%{opacity:1}100%{opacity:0}}";
document.head.appendChild(fadeStyle);
// 为消息3(无 data-versions)添加模拟版本数据
(function () {
const turn3 = document.querySelectorAll(".turn.ai")[2]; // 第三个 AI 消息
if (turn3 && !turn3.hasAttribute("data-versions")) {
// 给第三个消息初始化的版本面板数据
const vp = turn3.querySelector(".version-panel");
if (vp) {
vp._versions = [
{
text: "我想确认一下你的日程,判断你是否在赶工,以及今晚是否该提醒你休息。",
},
{ text: "让我看看你的日程安排,确认你今晚是否需要提醒休息。" },
];
vp._currentIdx = 0;
vp._turnIdx = 3;
const total = document.getElementById("vt3");
if (total) total.textContent = "2";
}
}
})();
// ─── 镜背轨视图切换(铭文往来 / 此刻) ───
window.toggleRailView = function (view) {
document
.querySelectorAll(".rail-tab")
.forEach((t) =>
t.classList.toggle("active", t.dataset.view === view),
);
document
.querySelectorAll(".rail-view")
.forEach((v) =>
v.classList.toggle("active", v.id === "view-" + view),
);
};
// ─── 人物下拉(选中值透传后端 character_id) ───
let charOpen = false;
let currentChar = { id: "qingwu", name: "青梧", initial: "梧" };
window.toggleCharDropdown = function (e) {
e.stopPropagation();
const dd = document.getElementById("charDropdown");
charOpen = !charOpen;
dd.style.display = charOpen ? "block" : "none";
e.currentTarget
.querySelector(".arrow")
?.classList.toggle("open", charOpen);
};
window.selectChar = function (opt) {
currentChar = {
id: opt.dataset.char,
name: opt.dataset.name,
initial: opt.dataset.initial,
};
document.getElementById("charLabel").textContent = currentChar.name;
document.getElementById("charAvatar").textContent = currentChar.initial;
document.querySelectorAll(".char-opt").forEach((o) => {
const active = o === opt;
o.classList.toggle("active", active);
const av = o.querySelector(".avatar-mini");
if (av) av.classList.toggle("active-char", active);
let tag = o.querySelector(".status-tag");
if (active) {
if (!tag) {
tag = document.createElement("span");
tag.className = "status-tag";
tag.textContent = "当前";
o.appendChild(tag);
} else tag.textContent = "当前";
} else if (tag && tag.textContent === "当前") tag.remove();
});
document.getElementById("charDropdown").style.display = "none";
document
.querySelector(".char-switcher-btn .arrow")
?.classList.remove("open");
charOpen = false;
};
document
.querySelectorAll(".char-opt")
.forEach((o) =>
o.addEventListener("click", () => window.selectChar(o)),
);
document.addEventListener("mousedown", function (e) {
if (charOpen && !e.target.closest(".char-switcher")) {
document.getElementById("charDropdown").style.display = "none";
document
.querySelector(".char-switcher-btn .arrow")
?.classList.remove("open");
charOpen = false;
}
});
// ─── 模型下拉(选中值透传后端 model) ───
let modelOpen = false;
let currentModel = "claude-sonnet-5";
window.toggleModelDropdown = function (e) {
e.stopPropagation();
const dd = document.getElementById("modelDropdown");
modelOpen = !modelOpen;
dd.style.display = modelOpen ? "block" : "none";
e.currentTarget
.querySelector(".arrow")
?.classList.toggle("open", modelOpen);
};
document.querySelectorAll(".model-opt").forEach((o) => {
o.addEventListener("click", () => {
currentModel = o.dataset.model;
document.getElementById("modelLabel").textContent = currentModel;
document.querySelectorAll(".model-opt").forEach((p) => {
const active = p === o;
p.classList.toggle("active", active);
let tag = p.querySelector(".status-tag");
if (active) {
if (!tag) {
tag = document.createElement("span");
tag.className = "status-tag";
tag.textContent = "当前";
p.appendChild(tag);
} else tag.textContent = "当前";
} else if (tag && tag.textContent === "当前") tag.remove();
});
document.getElementById("modelDropdown").style.display = "none";
document
.querySelector(".model-switcher-btn .arrow")
?.classList.remove("open");
modelOpen = false;
});
});
document.addEventListener("mousedown", function (e) {
if (modelOpen && !e.target.closest(".model-switcher")) {
document.getElementById("modelDropdown").style.display = "none";
document
.querySelector(".model-switcher-btn .arrow")
?.classList.remove("open");
modelOpen = false;
}
});
// ─── 深虑开关 + 档位(开关透传 reasoning: boolean,档位透传 reasoning_effort: low/medium/high) ───
let reasoningOn = false;
let currentEffort = "medium";
window.toggleReasoning = function (btn) {
reasoningOn = !reasoningOn;
btn.classList.toggle("active-gilt", reasoningOn);
document
.getElementById("effortGroup")
.classList.toggle("show", reasoningOn);
};
document.getElementById("effortGroup").addEventListener("click", (e) => {
const btn = e.target.closest(".effort-opt");
if (!btn) return;
document
.querySelectorAll("#effortGroup .effort-opt")
.forEach((b) => b.classList.toggle("active", b === btn));
currentEffort = btn.dataset.effort;
});
// ─── 博闻开关(透传后端 web_search: boolean,即联网检索) ───
let bowenEnabled = false;
window.toggleBowen = function (btn) {
bowenEnabled = !bowenEnabled;
btn.classList.toggle("active-patina", bowenEnabled);
};
// ─── 组装发送载荷(character_id / model / reasoning / reasoning_effort / web_search 透传后端) ───
function getSendPayload() {
const ta = document.querySelector(".composer textarea");
return {
message: ta ? ta.value.trim() : "",
character_id: currentChar.id, // 当前人物 — 后端按此路由人格/记忆
model: currentModel, // 模型 — 后端转发至模型网关
reasoning: reasoningOn, // 深虑开关 — 是否启用深虑(extended thinking)
reasoning_effort: reasoningOn ? currentEffort : null, // 深虑强度(仅 reasoning=true 时有效)
web_search: bowenEnabled, // 博闻:是否联网检索
};
}
function showToast(msg) {
let t = document.getElementById("sendToast");
if (!t) {
t = document.createElement("div");
t.id = "sendToast";
t.style.cssText =
'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--glass-strong);backdrop-filter:blur(24px) saturate(160%);border:1px solid var(--glass-border);border-radius:10px;padding:12px 16px;font-family:"JetBrains Mono",monospace;font-size:12px;line-height:1.6;color:var(--fg);max-width:92vw;z-index:100;box-shadow:0 8px 32px rgba(0,0,0,.4);white-space:pre-wrap;text-align:left;opacity:0;transition:opacity .25s;';
document.body.appendChild(t);
}
t.textContent = msg;
requestAnimationFrame(() => {
t.style.opacity = "1";
});
clearTimeout(t._timer);
t._timer = setTimeout(() => {
t.style.opacity = "0";
}, 4000);
}
document
.querySelector(".composer .send-btn")
.addEventListener("click", () => {
const payload = getSendPayload();
if (!payload.message) {
showToast("⚠ 镜面未铭文(请先输入内容)");
return;
}
console.log("→ 发送载荷(透传后端 /api/v1/chat):", payload);
showToast(
"→ 已组装发送载荷(见控制台):\n" +
JSON.stringify(payload, null, 2),
);
});
/* 主题切换由全局 _theme.js 接管(#theme-toggle 委托 + localStorage 持久化) */
</script>
<script src="_theme.js" defer></script>
</body>
</html>