Files
HomeAgent/internal/plugins/webui/dashboard.css
JianFeeeee acaf0887c7 fix(webui): 聊天图片"加载成功却显示加载失败"——HTML 属性里的反斜杠转义
## 缺陷

生产截图:图片加载成功了(img 元素在、src 正确),界面却多显示一行
蓝字「图片加载失败"/>」。

根因是内联事件属性里用了反斜杠转义:

    onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"

**HTML 属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,属性在
`<span class=` 之后就闭合了。浏览器切出来的是:

    onerror = "this.parentElement.innerHTML='<span class="

剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。

症状极具误导性:看起来像"图片坏了",实际图片是好的;而那个 onerror
因为语法错误**永不执行** —— 真失败时反而什么都不显示。

## 修法

不用内联属性(那要求以后每次拼 HTML 都记得转义规则),改成
**捕获阶段的事件委托**:

- error 事件**不冒泡**,只能在捕获阶段拿到(注释里写明了)
- 用 createElement + textContent 构造占位,不再拼 innerHTML 字符串
- 只处理 .chat-attachment-img,不影响正文里的 markdown 图片

## 顺带:两个类没有 CSS

`chat-attachment-img` 与 `att-err` 只出现在 JS 里,CSS 里一条都没有。
图片靠内联 style 撑着还能看,失败占位则完全没有样式。已补。

## 判据(2 条,变异验证)

- `TestChatAttachmentImgHasNoBrokenInlineHandler`:不许内联 onerror、
  必须用 DOM API、必须有 error 监听
- `TestDashboardNoEscapedQuoteInHTMLAttributes`:通用扫描,
  任何 `name="...\"...` 形态都会红(下次别人写内联事件也拦得住)

变异:把内联 onerror 放回去 ⇒ 两条同时红。

★ 写判据时踩到的坑(都已修):判据被**自己的注释**打红(注释里必须写出
错误写法来解释),需先用已有的 stripJSComments 剥离;另外我先自己写了一个
stripJSComments,与 dashboard_js_scope_test.go 的重名 —— 应复用已有的。

## 顺带修一条脆弱判据

`TestRTChild_RapidInputCollapsesToLatest` 全量跑时偶发红
("连发 50 条却跑了 5 轮")。

探针查实:连发 50 条只花 ~80µs,而测试 provider 的 ChatStream **同步瞬间
返回**,两者同量级 ⇒ 消费方能在连发过程中抢到几次执行。探针显示
dropped=48,collapse 明明生效了。

所以 `Rounds > 3` 是在测调度抖动,不是测契约。改为断言**覆盖主导**
(dropped >= burst/2)。真实通话每轮几十毫秒,连发微秒级,生产里
"轮次≈1" 自然成立。

变异验证:让 dropped 不计数 ⇒ 判据红(dropped=0(连发 50 条))。

go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
2026-10-31 10:03:43 +08:00

3144 lines
92 KiB
CSS
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.

:root {
/* ===== NapCat Design DNA tokens ===== */
--sakura-100: #ffe4e9;
--sakura-200: #ffcdd9;
--sakura-300: #ff9eb5;
--sakura-400: #ff7fac;
--sakura-500: #f33b7c;
--sakura-600: #c92462;
--sakura-700: #991b4b;
--frost-100: #d7e8ee;
--frost-200: #a9d6e3;
--frost-300: #88c0d0;
--frost-400: #5ea8bf;
--frost-500: #3f88a3;
--success: #17a964;
--warning: #d99a2b;
--error: #db3694;
--info: #3f6ef5;
--glass-bg: rgba(13, 18, 34, 0.6);
--glass-bg-strong: rgba(13, 18, 34, 0.82);
--glass-blur: 14px;
--glass-border: rgba(255, 255, 255, 0.08);
--glass-hover: rgba(255, 255, 255, 0.05);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.28);
--shadow-lg: 0 12px 36px rgba(0, 0, 0, 0.38);
--shadow-glow:
0 0 0 1px rgba(var(--accent-rgb), 0.4),
0 4px 20px rgba(var(--accent-rgb), 0.18);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-pill: 999px;
--font-sans:
"Segoe UI", "Segoe UI Variable Text", -apple-system,
BlinkMacSystemFont, Roboto, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
--font-mono:
"JetBrains Mono", ui-monospace, "SFMono-Regular", "SF Mono", "Menlo",
"Consolas", monospace;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--dur-micro: 150ms;
--dur-normal: 300ms;
/* ===== Compat tokens (rethemed) ===== */
--bg-primary: #0b1020;
--bg-secondary: rgba(17, 24, 44, 0.72);
--bg-card: rgba(17, 24, 44, 0.6);
--bg-input: rgba(13, 18, 34, 0.75);
--bg-hover: rgba(255, 255, 255, 0.06);
--text-primary: #eef1f8;
--text-secondary: #a7b0c4;
--text-muted: #77809a;
--border-color: rgba(255, 255, 255, 0.09);
/* accent 的 rgb 分量:所有 hardcoded rgba(255,127,172,x) 都改用它,
这样换配色(尤其 monochrome)时连阴影/描边一起换,不会残留粉色。 */
--accent-rgb: 255, 127, 172;
--accent-strong-rgb: 243, 59, 124;
--accent: #ff7fac;
--accent-bg: rgba(var(--accent-rgb), 0.14);
--toast-bg: rgba(23, 169, 100, 0.16);
--toast-color: #6ee7a8;
--toast-error-bg: rgba(219, 54, 148, 0.18);
--toast-error-color: #ff9ec6;
--pre-color: #cdd3f5;
--pre-bg: rgba(10, 14, 28, 0.85);
--chat-bg: rgba(10, 14, 28, 0.7);
--msg-user-bg: rgba(var(--accent-rgb), 0.16);
--msg-user-color: #ffb9d0;
--msg-assistant-bg: rgba(136, 192, 208, 0.14);
--msg-assistant-color: #a9d6e3;
--msg-system-bg: rgba(63, 110, 245, 0.16);
--msg-system-color: #a3b8ff;
--msg-bubble-bg: #161b2e;
--msg-bubble-color: #e9edf6;
--msg-bubble-border: rgba(255, 255, 255, 0.1);
--kv-border: rgba(255, 255, 255, 0.07);
--btn-ghost-border: rgba(255, 255, 255, 0.14);
--btn-ghost-hover-bg: rgba(255, 255, 255, 0.07);
--save-btn-border: #d99a2b;
--loading-border: rgba(255, 255, 255, 0.12);
--loading-top: #ff7fac;
--grad-a: rgba(var(--accent-rgb), 0.14);
--grad-b: rgba(136, 192, 208, 0.12);
--grad-c: rgba(var(--accent-strong-rgb), 0.1);
--c-sakura: #ff7fac;
--c-cyan: #2dd4bf;
--c-violet: #a78bfa;
--c-emerald: #34d399;
--c-amber: #fbbf24;
--c-blue: #60a5fa;
}
[data-theme="light"] {
--glass-bg: rgba(255, 255, 255, 0.66);
--glass-bg-strong: rgba(255, 255, 255, 0.88);
--glass-border: rgba(var(--accent-rgb), 0.22);
--glass-hover: rgba(var(--accent-rgb), 0.07);
--shadow-sm: 0 1px 3px rgba(153, 27, 75, 0.08);
--shadow-md: 0 6px 20px rgba(153, 27, 75, 0.1);
--shadow-lg: 0 16px 40px rgba(153, 27, 75, 0.14);
--shadow-glow:
0 0 0 1px rgba(var(--accent-strong-rgb), 0.3), 0 6px 22px rgba(var(--accent-strong-rgb), 0.14);
--bg-primary: #ffffff;
--bg-secondary: rgba(255, 255, 255, 0.78);
--bg-card: rgba(255, 255, 255, 0.72);
--bg-input: rgba(255, 224, 233, 0.55);
--bg-hover: rgba(var(--accent-rgb), 0.08);
--text-primary: #3b2030;
--text-secondary: #7a5c6b;
--text-muted: #a48a96;
--border-color: rgba(201, 36, 98, 0.14);
--accent: #c92462;
--accent-bg: #ffe4e9;
--toast-bg: rgba(23, 169, 100, 0.14);
--toast-color: #128a52;
--toast-error-bg: rgba(219, 54, 148, 0.12);
--toast-error-color: #c2185b;
--pre-color: #5c4060;
--pre-bg: #fff0f5;
--chat-bg: #fff0f5;
--msg-user-bg: #ffe4e9;
--msg-user-color: #c2185b;
--msg-assistant-bg: #e3f2f6;
--msg-assistant-color: #2f7188;
--msg-system-bg: #e6ecfe;
--msg-system-color: #3f6ef5;
--msg-bubble-bg: #ffffff;
--msg-bubble-color: #262a33;
--msg-bubble-border: rgba(201, 36, 98, 0.14);
--kv-border: rgba(201, 36, 98, 0.1);
--btn-ghost-border: rgba(201, 36, 98, 0.22);
--btn-ghost-hover-bg: #ffe4e9;
--save-btn-border: #d99a2b;
--loading-border: rgba(201, 36, 98, 0.18);
--loading-top: #c92462;
--grad-a: rgba(var(--accent-rgb), 0.16);
--grad-b: rgba(136, 192, 208, 0.12);
--grad-c: rgba(var(--accent-strong-rgb), 0.12);
}
[data-color="sakura"][data-theme="light"] {
--accent-rgb: 201, 36, 98;
--accent-strong-rgb: 153, 27, 75;
--bg-primary: #ffffff;
--bg-input: rgba(255, 224, 233, 0.55);
--bg-hover: rgba(var(--accent-rgb), 0.08);
--pre-bg: #fff0f5;
--chat-bg: #fff0f5;
--msg-bubble-border: rgba(201, 36, 98, 0.14);
--btn-ghost-hover-bg: #ffe4e9;
--grad-a: rgba(var(--accent-rgb), 0.14);
--grad-c: rgba(var(--accent-strong-rgb), 0.09);
}
[data-color="cyan"] {
--accent-rgb: 45, 212, 191;
--accent-strong-rgb: 13, 148, 136;
--accent: #2dd4bf;
--accent-bg: rgba(45, 212, 191, 0.14);
--msg-user-bg: rgba(45, 212, 191, 0.16);
--msg-user-color: #99f6e4;
--loading-top: #2dd4bf;
--grad-a: rgba(45, 212, 191, 0.14);
--grad-c: rgba(45, 212, 191, 0.1);
}
[data-color="cyan"][data-theme="light"] {
--accent-rgb: 15, 118, 110;
--accent-strong-rgb: 15, 118, 110;
--accent: #0f766e;
--accent-bg: #ccfbf1;
--msg-user-bg: #ccfbf1;
--msg-user-color: #0f766e;
--loading-top: #0f766e;
--bg-primary: #f0fdfa;
--bg-input: rgba(204, 251, 241, 0.6);
--bg-hover: rgba(13, 148, 136, 0.08);
--pre-bg: #e7f9f3;
--chat-bg: #e7f9f3;
--msg-bubble-border: rgba(13, 148, 136, 0.14);
--btn-ghost-hover-bg: #ccfbf1;
--grad-a: rgba(13, 148, 136, 0.14);
--grad-c: rgba(13, 148, 136, 0.09);
}
[data-color="violet"] {
--accent-rgb: 167, 139, 250;
--accent-strong-rgb: 124, 58, 237;
--accent: #a78bfa;
--accent-bg: rgba(167, 139, 250, 0.14);
--msg-user-bg: rgba(167, 139, 250, 0.16);
--msg-user-color: #ddd6fe;
--loading-top: #a78bfa;
--grad-a: rgba(167, 139, 250, 0.14);
--grad-c: rgba(167, 139, 250, 0.1);
}
[data-color="violet"][data-theme="light"] {
--accent-rgb: 124, 58, 237;
--accent-strong-rgb: 109, 40, 217;
--accent: #7c3aed;
--accent-bg: #ede9fe;
--msg-user-bg: #ede9fe;
--msg-user-color: #7c3aed;
--loading-top: #7c3aed;
--bg-primary: #f8f6ff;
--bg-input: rgba(237, 233, 254, 0.6);
--bg-hover: rgba(124, 58, 237, 0.08);
--pre-bg: #f3f0ff;
--chat-bg: #f3f0ff;
--msg-bubble-border: rgba(124, 58, 237, 0.14);
--btn-ghost-hover-bg: #ede9fe;
--grad-a: rgba(124, 58, 237, 0.13);
--grad-c: rgba(124, 58, 237, 0.08);
}
[data-color="emerald"] {
--accent-rgb: 52, 211, 153;
--accent-strong-rgb: 5, 150, 105;
--accent: #34d399;
--accent-bg: rgba(52, 211, 153, 0.14);
--msg-user-bg: rgba(52, 211, 153, 0.16);
--msg-user-color: #a7f3d0;
--loading-top: #34d399;
--grad-a: rgba(52, 211, 153, 0.14);
--grad-c: rgba(52, 211, 153, 0.1);
}
[data-color="emerald"][data-theme="light"] {
--accent-rgb: 5, 150, 105;
--accent-strong-rgb: 4, 120, 87;
--accent: #059669;
--accent-bg: #d1fae5;
--msg-user-bg: #d1fae5;
--msg-user-color: #059669;
--loading-top: #059669;
--bg-primary: #f0fdf6;
--bg-input: rgba(209, 250, 229, 0.6);
--bg-hover: rgba(5, 150, 105, 0.08);
--pre-bg: #e9f9f0;
--chat-bg: #e9f9f0;
--msg-bubble-border: rgba(5, 150, 105, 0.14);
--btn-ghost-hover-bg: #d1fae5;
--grad-a: rgba(5, 150, 105, 0.14);
--grad-c: rgba(5, 150, 105, 0.09);
}
[data-color="amber"] {
--accent-rgb: 251, 191, 36;
--accent-strong-rgb: 217, 119, 6;
--accent: #fbbf24;
--accent-bg: rgba(251, 191, 36, 0.14);
--msg-user-bg: rgba(251, 191, 36, 0.16);
--msg-user-color: #fde68a;
--loading-top: #fbbf24;
--grad-a: rgba(251, 191, 36, 0.14);
--grad-c: rgba(251, 191, 36, 0.1);
}
[data-color="amber"][data-theme="light"] {
--accent-rgb: 217, 119, 6;
--accent-strong-rgb: 180, 83, 9;
--accent: #d97706;
--accent-bg: #fef3c7;
--msg-user-bg: #fef3c7;
--msg-user-color: #b45309;
--loading-top: #d97706;
--bg-primary: #fffdf4;
--bg-input: rgba(254, 243, 199, 0.6);
--bg-hover: rgba(217, 119, 6, 0.08);
--pre-bg: #fdf6e1;
--chat-bg: #fdf6e1;
--msg-bubble-border: rgba(217, 119, 6, 0.14);
--btn-ghost-hover-bg: #fef3c7;
--grad-a: rgba(217, 119, 6, 0.13);
--grad-c: rgba(217, 119, 6, 0.08);
}
[data-color="blue"] {
--accent-rgb: 96, 165, 250;
--accent-strong-rgb: 37, 99, 235;
--accent: #60a5fa;
--accent-bg: rgba(96, 165, 250, 0.14);
--msg-user-bg: rgba(96, 165, 250, 0.16);
--msg-user-color: #bfdbfe;
--loading-top: #60a5fa;
--grad-a: rgba(96, 165, 250, 0.14);
--grad-c: rgba(96, 165, 250, 0.1);
}
[data-color="blue"][data-theme="light"] {
--accent-rgb: 37, 99, 235;
--accent-strong-rgb: 29, 78, 216;
--accent: #2563eb;
--accent-bg: #dbeafe;
--msg-user-bg: #dbeafe;
--msg-user-color: #2563eb;
--loading-top: #2563eb;
--bg-primary: #f7faff;
--bg-input: rgba(219, 234, 254, 0.6);
--bg-hover: rgba(37, 99, 235, 0.08);
--pre-bg: #eef4ff;
--chat-bg: #eef4ff;
--msg-bubble-border: rgba(37, 99, 235, 0.14);
--btn-ghost-hover-bg: #dbeafe;
--grad-a: rgba(37, 99, 235, 0.13);
--grad-c: rgba(37, 99, 235, 0.08);
}
[data-color="mono"] {
--accent-rgb: 205, 210, 220;
--accent-strong-rgb: 150, 156, 168;
--accent: #cdd2dc;
--accent-bg: rgba(205, 210, 220, 0.12);
--sakura-300: #b9bfcc;
--sakura-400: #cdd2dc;
--sakura-500: #e6e9ef;
--frost-300: #a8aeba;
--frost-400: #8b919d;
--msg-user-bg: rgba(205, 210, 220, 0.12);
--msg-user-color: #e6e9ef;
--msg-assistant-bg: rgba(205, 210, 220, 0.09);
--msg-assistant-color: #c9ced9;
--loading-top: #cdd2dc;
--grad-a: rgba(205, 210, 220, 0.12);
--grad-b: rgba(205, 210, 220, 0.09);
--grad-c: rgba(205, 210, 220, 0.07);
--c-sakura: #cdd2dc;
--shadow-glow:
0 0 0 1px rgba(205, 210, 220, 0.2), 0 4px 20px rgba(0, 0, 0, 0.3);
}
[data-color="mono"][data-theme="light"] {
--accent-rgb: 60, 64, 72;
--accent-strong-rgb: 30, 32, 38;
--accent: #2f333b;
--accent-bg: #ececf0;
--sakura-300: #6b7280;
--sakura-400: #3f434c;
--sakura-500: #23262c;
--frost-300: #7c828e;
--frost-400: #5f6570;
--text-primary: #1f2126;
--text-secondary: #5b5f66;
--text-muted: #8b8f97;
--border-color: rgba(0, 0, 0, 0.1);
--kv-border: rgba(0, 0, 0, 0.08);
--btn-ghost-border: rgba(0, 0, 0, 0.16);
--glass-border: rgba(0, 0, 0, 0.1);
--glass-hover: rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 6px 20px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.1);
--shadow-glow:
0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 20px rgba(0, 0, 0, 0.08);
--msg-user-bg: #ececf0;
--msg-user-color: #23262c;
--msg-assistant-bg: #f0f0f2;
--msg-assistant-color: #3f434c;
--loading-top: #2f333b;
--bg-primary: #ffffff;
--bg-input: rgba(0, 0, 0, 0.045);
--bg-hover: rgba(0, 0, 0, 0.05);
--pre-bg: #f5f5f7;
--chat-bg: #f5f5f7;
--msg-bubble-border: rgba(0, 0, 0, 0.1);
--btn-ghost-hover-bg: #ececf0;
--grad-a: rgba(0, 0, 0, 0.05);
--grad-b: rgba(0, 0, 0, 0.04);
--grad-c: rgba(0, 0, 0, 0.03);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: var(--font-sans);
}
body {
background:
radial-gradient(
900px 700px at 85% -10%,
var(--grad-a),
transparent 60%
),
radial-gradient(
800px 600px at -10% 20%,
var(--grad-b),
transparent 60%
),
radial-gradient(
700px 500px at 50% 110%,
var(--grad-c),
transparent 60%
),
var(--bg-primary);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
font-size: 14px;
text-rendering: optimizeLegibility;
user-select: none;
-webkit-user-select: none;
transition:
background 0.3s,
color 0.2s;
}
.selectable,
.msg .text,
.msg .tc-args,
.msg .tc-result,
.msg .reasoning-body,
pre,
input,
textarea,
.term-buf {
user-select: text;
-webkit-user-select: text;
}
nav {
background: var(--bg-secondary);
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
width: 16rem;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 100;
display: flex;
flex-direction: column;
border-right: 1px solid var(--glass-border);
padding: 16px 12px;
overflow-y: auto;
overflow-x: hidden;
transition:
transform var(--dur-normal) var(--ease-out),
background 0.2s,
border 0.2s;
}
nav h1 {
font-size: 16.5px;
font-weight: 700;
/* 品牌字用 sakura→frost 渐变:比纯色更像「产品」,也不跟导航选中态抢颜色 */
background: linear-gradient(100deg, var(--sakura-300), var(--frost-300));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin: 2px 10px 18px;
white-space: nowrap;
letter-spacing: -0.015em;
display: flex;
align-items: center;
gap: 8px;
}
.nav-links {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
color: var(--text-secondary);
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
border-radius: var(--radius-md);
/* 选中条用 inset 阴影而不是 border-left:border 会把整行文字右推 3px,
hover 时图标跟着跳一下 —— 这种「一像素级不稳」正是廉价感的来源。 */
box-shadow: inset 2px 0 0 transparent;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out),
box-shadow var(--dur-micro) var(--ease-out);
}
.nav-ico {
width: 17px;
height: 17px;
flex-shrink: 0;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.8;
}
nav a:hover {
color: var(--text-primary);
background: var(--glass-hover);
}
nav a.active {
color: var(--accent);
background: var(--accent-bg);
box-shadow: inset 2px 0 0 var(--accent);
font-weight: 600;
}
nav a.active .nav-ico {
opacity: 1;
}
.nav-footer {
border-top: 1px solid var(--glass-border);
padding: 12px 4px 4px;
display: flex;
justify-content: center;
}
.main-wrap {
margin-left: 16rem;
min-height: 100vh;
display: flex;
flex-direction: column;
transition: margin-left var(--dur-normal) var(--ease-out);
}
body.sidebar-collapsed nav {
transform: translateX(-100%);
}
body.sidebar-collapsed .main-wrap {
margin-left: 0;
}
.topbar {
position: sticky;
top: 0;
z-index: 90;
height: 56px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 24px;
background: var(--bg-secondary);
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
border-bottom: 1px solid var(--glass-border);
}
.sidebar-toggle {
background: transparent;
border: 1px solid var(--glass-border);
color: var(--text-secondary);
cursor: pointer;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: 13px;
line-height: 1.2;
transition:
all var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
.sidebar-toggle:hover {
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.5);
transform: scale(1.05);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--text-muted);
animation: breadcrumbIn var(--dur-normal) var(--ease-out);
}
/* 当前页在顶栏里当标题用:字号/字重/颜色都要跟「面包屑碎片」拉开,
否则整条顶栏就是一行灰字,页面没有入口感。 */
.breadcrumb .crumb-current {
font-size: 15px;
font-weight: 650;
color: var(--text-primary);
letter-spacing: -0.01em;
}
.breadcrumb .crumb-home,
.breadcrumb .crumb-sep {
font-size: 12.5px;
opacity: 0.7;
}
@keyframes breadcrumbIn {
from {
opacity: 0;
transform: translateX(-6px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.crumb-home {
color: var(--accent);
font-weight: 600;
}
.crumb-sep {
color: var(--text-muted);
opacity: 0.6;
}
.crumb-current {
color: var(--text-primary);
font-weight: 500;
}
.theme-btn {
background: var(--glass-bg-strong);
border: 1px solid var(--glass-border);
backdrop-filter: blur(8px);
color: var(--text-secondary);
cursor: pointer;
padding: 4px 10px;
border-radius: var(--radius-pill);
font-size: 14px;
line-height: 1;
margin-right: 8px;
transition:
all var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
/* 内核控制按钮组(侧栏底部):与"退出登录"同族,带文字与图标。
两枚按钮等宽、同一行、间距一致;关机用红色悬停提示危险操作。 */
.nav-kernel {
display: flex;
gap: 8px;
margin: 8px 0 4px;
}
.nav-kernel .btn {
flex: 1 1 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 10px;
font-size: 13px;
line-height: 1.2;
}
.nav-kernel .nav-danger:hover {
color: #e5484d;
border-color: rgba(229, 72, 77, 0.5);
background: rgba(229, 72, 77, 0.08);
}
.theme-btn:hover {
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.5);
box-shadow: var(--shadow-glow);
transform: scale(1.06);
}
#bg-layer {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--bg-img, none);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform: scale(1.04);
filter: blur(calc(var(--bg-blur, 0) * 1px));
transition: filter 0.25s var(--ease-out);
}
.palette-pop {
position: fixed;
bottom: 66px;
left: 16px;
width: 216px;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 130px);
overflow-y: auto;
padding: 12px;
background: var(--glass-bg-strong);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: none;
z-index: 300;
}
.palette-pop.on {
display: block;
animation: viewIn 0.16s var(--ease-out);
}
.palette-pop h4 {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
margin: 2px 0 6px;
}
.palette-pop button.cdot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
cursor: pointer;
margin: 3px;
padding: 0;
vertical-align: middle;
transition:
transform 0.15s,
box-shadow 0.15s;
}
.palette-pop button.cdot:hover {
transform: scale(1.25);
}
.palette-pop button.cdot.on {
border-color: var(--text-primary);
box-shadow: var(--shadow-glow);
transform: scale(1.15);
}
.palette-pop input[type="text"] {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 5px 8px;
color: var(--text-primary);
font-size: 11px;
width: 100%;
margin-bottom: 6px;
outline: none;
}
.palette-pop .pp-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
margin-top: 4px;
}
.palette-pop .pp-row .btn {
padding: 3px 9px;
font-size: 11px;
white-space: nowrap;
}
.palette-pop .pp-val {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
min-width: 0;
text-align: right;
margin-left: auto;
}
.palette-pop input[type="range"] {
width: 72px;
margin: 0;
accent-color: var(--accent);
vertical-align: middle;
}
.container {
/* 1240px 是「卡片网格 + 图表」都能读的最宽值;再宽就变成一片空旷的留白。
居中而不是靠左铺满 —— 超宽屏上摊满整个屏幕是「后台模板」的典型观感。 */
padding: 22px 24px 56px;
max-width: 1240px;
margin: 0 auto;
width: 100%;
}
.card {
position: relative;
background: var(--bg-card);
backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 22px;
margin-bottom: 18px;
box-shadow: var(--shadow-sm);
transition:
background 0.2s,
border 0.2s,
box-shadow var(--dur-normal) var(--ease-out);
}
/* hover 只提亮阴影与描边,**不再 translateY**:
整页十几张卡一起「上下浮」,视线扫过时像在抖,是廉价感最明显的一处。 */
.card:hover {
box-shadow: var(--shadow-md);
border-color: rgba(var(--accent-rgb), 0.22);
}
.card.tilt {
transform-style: preserve-3d;
transition:
box-shadow var(--dur-normal) var(--ease-out),
transform 0.15s ease-out;
}
.card.tilt .tilt-glow {
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease-out;
background: radial-gradient(
240px circle at var(--mx, 50%) var(--my, 50%),
rgba(var(--accent-rgb), 0.12),
transparent 70%
);
mix-blend-mode: normal;
}
.card.tilt:hover .tilt-glow {
opacity: 1;
}
@media (hover: none), (prefers-reduced-motion: reduce) {
.card.tilt {
transform: none !important;
}
.card.tilt .tilt-glow {
display: none;
}
}
.card h2 {
/* 标题左侧加一道 accent 短竖:卡片之间就有了同一套「章节起点」信号,
不必靠加粗/变大去抢,长页面翻起来也不迷路。 */
display: flex;
align-items: center;
gap: 9px;
font-size: 15.5px;
font-weight: 650;
margin-bottom: 14px;
color: var(--text-primary);
letter-spacing: -0.012em;
}
.card h2::before {
content: "";
width: 3px;
height: 14px;
border-radius: 2px;
background: linear-gradient(
180deg,
var(--sakura-300),
var(--frost-300)
);
flex-shrink: 0;
}
.card h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin: 16px 0 8px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.02em;
background: linear-gradient(
120deg,
var(--sakura-400),
var(--frost-300)
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
letter-spacing: 0.02em;
}
.stat-card {
padding: 16px 20px;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
box-shadow: 0 0 6px currentColor;
}
.dot-green {
background: var(--success);
color: var(--success);
}
.dot-yellow {
background: var(--warning);
color: var(--warning);
}
.dot-red {
background: var(--error);
color: var(--error);
}
.dot-gray {
background: var(--text-muted);
color: var(--text-muted);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
text-align: left;
padding: 8px 10px;
color: var(--text-muted);
font-weight: 500;
border-bottom: 1px solid var(--border-color);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 8px 10px;
border-bottom: 1px solid var(--kv-border);
}
tr:hover td {
background: var(--bg-hover);
}
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: var(--radius-pill);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
}
.badge-green {
background: rgba(23, 169, 100, 0.16);
color: var(--success);
}
.badge-red {
background: rgba(219, 54, 148, 0.16);
color: var(--error);
}
.badge-yellow {
background: rgba(217, 154, 43, 0.16);
color: var(--warning);
}
.badge-blue {
background: var(--accent-bg);
color: var(--accent);
}
.badge-gray {
background: var(--glass-hover);
color: var(--text-secondary);
}
.btn {
/* 药丸形换成与卡片同一套圆角:满屏药丸按钮是「玩具感」的主要来源之一 */
padding: 7px 14px;
border-radius: var(--radius-md);
border: none;
font-size: 12.5px;
cursor: pointer;
font-weight: 600;
letter-spacing: 0.005em;
line-height: 1.35;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out),
border-color var(--dur-micro) var(--ease-out),
box-shadow var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
.btn:active {
transform: scale(0.96);
}
.btn-primary {
background: linear-gradient(
120deg,
var(--sakura-400),
var(--sakura-500)
);
color: #fff;
box-shadow: 0 2px 12px rgba(var(--accent-rgb), 0.35);
}
.btn-primary:hover {
filter: brightness(1.08);
box-shadow: 0 4px 18px rgba(var(--accent-rgb), 0.45);
}
.btn-danger {
background: var(--error);
color: #fff;
}
.btn-danger:hover {
filter: brightness(1.1);
}
.btn-sm {
padding: 4px 10px;
font-size: 11.5px;
}
.btn-ghost {
background: transparent;
border: 1px solid var(--btn-ghost-border);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--btn-ghost-hover-bg);
color: var(--text-primary);
border-color: rgba(var(--accent-rgb), 0.4);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: tabIn var(--dur-normal) var(--ease-out);
}
@keyframes tabIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
input,
textarea,
select {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 9px 12px;
color: var(--text-primary);
font-size: 13px;
width: 100%;
margin-bottom: 10px;
outline: none;
transition:
border 0.15s,
background 0.2s,
color 0.2s,
box-shadow 0.15s;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--accent);
box-shadow: var(--shadow-glow);
background: var(--bg-input);
}
textarea {
resize: vertical;
min-height: 80px;
font-family: var(--font-mono);
font-size: 12px;
}
label {
display: block;
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 3px;
font-weight: 500;
}
pre {
background: var(--pre-bg);
border-radius: var(--radius-sm);
padding: 12px;
font-size: 12px;
overflow-x: auto;
color: var(--pre-color);
font-family: var(--font-mono);
max-height: 400px;
overflow-y: auto;
}
code {
font-family: var(--font-mono);
font-size: 12px;
color: var(--pre-color);
}
.settings-layout {
display: flex;
flex-direction: column;
gap: 16px;
}
.settings-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
background: var(--bg-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 6px;
box-shadow: var(--shadow-sm);
}
.settings-tabs span {
display: inline-block;
padding: 7px 16px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
text-decoration: none;
border-radius: var(--radius-pill);
border: 1px solid transparent;
transition: all 0.15s;
}
.settings-tabs span:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.settings-tabs span.active {
background: var(--accent-bg);
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.35);
}
.settings-content {
flex: 1;
min-width: 0;
}
.settings-content > .card {
margin-bottom: 16px;
}
.settings-key {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-bottom: 2px;
}
.reasoning {
border-left: 2px solid var(--frost-400);
padding-left: 12px;
margin: 8px 0;
font-size: 12px;
color: var(--text-muted);
}
.reasoning-title {
cursor: pointer;
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
user-select: none;
margin-bottom: 4px;
}
.reasoning-body {
color: var(--text-muted);
line-height: 1.5;
}
.reasoning-body p {
margin: 4px 0;
}
.msg-content .text h1,
.msg-content .text h2,
.msg-content .text h3 {
font-size: 1em;
margin: 8px 0 4px;
color: var(--text-primary);
}
.msg-content .text p {
margin: 4px 0;
line-height: 1.5;
}
.msg-content .text ul,
.msg-content .text ol {
padding-left: 20px;
margin: 4px 0;
}
.msg-content .text li {
margin: 2px 0;
}
.msg-content .text code {
background: var(--pre-bg);
padding: 1px 4px;
border-radius: 3px;
font-size: 11px;
}
.msg-content .text pre {
background: var(--pre-bg);
border-radius: 6px;
padding: 10px;
margin: 8px 0;
overflow-x: auto;
font-size: 11px;
max-height: 300px;
}
.msg-content .text pre code {
background: none;
padding: 0;
}
.msg-content .text blockquote {
border-left: 3px solid var(--border-color);
padding-left: 10px;
margin: 8px 0;
color: var(--text-secondary);
}
.msg-content .text table {
border-collapse: collapse;
margin: 8px 0;
font-size: 12px;
width: 100%;
}
.msg-content .text th,
.msg-content .text td {
border: 1px solid var(--border-color);
padding: 4px 8px;
text-align: left;
}
.msg-content .text img {
max-width: 100%;
border-radius: 6px;
}
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--toast-bg);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
color: var(--toast-color);
padding: 10px 22px;
border-radius: var(--radius-md);
font-size: 13px;
display: none;
z-index: 100;
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-md);
animation: toastIn 0.3s var(--ease-out);
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.toast.error {
background: var(--toast-error-bg);
color: var(--toast-error-color);
}
.toast.success {
background: var(--toast-bg);
color: var(--toast-color);
border-color: rgba(23, 169, 100, 0.35);
}
.toast.warn {
background: rgba(217, 154, 43, 0.16);
color: var(--warning);
border-color: rgba(217, 154, 43, 0.35);
}
.confirm-overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(8, 10, 20, 0.55);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 200;
animation: fadeIn 0.2s ease;
}
.confirm-box {
background: var(--glass-bg-strong);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 24px;
max-width: 380px;
width: 90%;
box-shadow: var(--shadow-lg);
animation: tabIn 0.25s var(--ease-out);
}
.confirm-box h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-primary);
}
.confirm-box p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.55;
margin-bottom: 18px;
word-break: break-all;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 首启人格向导(复用 confirm-* 弹窗)*/
.persona-ta {
width: 100%;
display: none;
margin-bottom: 12px;
font: 12px/1.5 var(--font-mono, monospace);
padding: 8px;
border-radius: var(--radius-md, 8px);
border: 1px solid var(--glass-border);
background: var(--glass-bg);
color: var(--text-primary);
box-sizing: border-box;
}
.persona-warn {
display: none;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
margin-bottom: 10px;
}
.persona-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--text-muted);
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px dashed var(--glass-border);
border-radius: var(--radius-lg);
margin: 8px 0;
}
.empty-state p {
font-size: 14px;
margin-bottom: 8px;
color: var(--text-secondary);
}
.empty-state .icon {
font-size: 40px;
margin-bottom: 12px;
opacity: 0.6;
filter: drop-shadow(0 4px 10px rgba(var(--accent-rgb), 0.3));
}
.chat-layout {
display: flex;
flex-direction: column;
gap: 16px;
height: calc(100vh - 112px);
min-height: 60vh;
}
.chat-tabs {
/* 分段控件(segmented control)而不是一排药丸 chip:
四个页签是同一组互斥选项,「一个容器 + 一个高亮块」比「四个各自带边框的胶囊」
更准确地表达了这层关系,也少四道描边噪声。 */
display: inline-flex;
gap: 3px;
padding: 3px;
flex-wrap: wrap;
align-self: flex-start;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
}
.chat-tabs span {
padding: 6px 14px;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
color: var(--text-muted);
border-radius: 7px;
border: none;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out);
}
.chat-tabs span:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.chat-tabs span.active {
color: var(--accent);
background: var(--accent-bg);
font-weight: 600;
box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.28);
}
.chat-panel {
display: none;
flex: 1;
min-height: 0;
flex-direction: column;
}
.chat-panel.active {
display: flex;
}
.chat-main {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.chat-main .card {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 0;
min-height: 0;
background: transparent;
border: none;
box-shadow: none;
backdrop-filter: none;
padding: 0;
}
.chat-main .card h2 {
flex-shrink: 0;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 20px 28px;
margin-bottom: 0;
display: flex;
flex-direction: column;
/* 消息间距从 6px 拉到 14px:原来每条贴在一起,一屏十几条像一堵墙,
分不清哪句是哪句。 */
gap: 14px;
min-height: 0;
}
.msg {
display: flex;
gap: 10px;
margin-bottom: 0;
align-items: flex-start;
max-width: 100%;
}
.msg-user {
flex-direction: row-reverse;
align-self: flex-end;
}
.msg-assistant {
align-self: flex-start;
}
.msg-system {
align-self: center;
max-width: 90%;
}
.msg-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
/* 细描边把头像从同色背景里“拓”出来,否则暗色下就是一团 */
box-shadow: 0 0 0 1px var(--glass-border);
}
.msg-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* mascot.webp 是**头部方裁**(脸居中),cover 已能把脸填满圆形。
原先 scale(1.5) 是给「全身立绘裁圆」补的——现在图本身换了,
再放大 1.5 会切掉两侧发绳与呆毛,所以降到 1.25 只做轻微推近。 */
.msg-assistant .msg-avatar img {
transform: scale(1.25);
}
.msg-avatar svg {
display: block;
width: 16px;
height: 16px;
}
.msg-user .msg-avatar {
background: var(--msg-user-bg);
color: var(--msg-user-color);
}
.msg-assistant .msg-avatar {
background: transparent;
}
.msg-system .msg-avatar {
background: var(--msg-system-bg);
color: var(--msg-system-color);
}
.msg-content {
min-width: 0;
flex: 1 1 auto;
/* 行长上限:整屏宽的一行文字没人读得下去(~78ch 是舒适区),
也让「同一侧连续多条」自然堆成一列而不是横铺开。 */
max-width: min(78ch, 86%);
}
.msg-content .msg-bubble + .msg-bubble {
margin-top: 6px;
}
.msg-channel {
align-self: flex-start;
}
.msg-channel .msg-avatar.chan-avatar {
color: #ffffff;
font-weight: 700;
font-size: 13px;
text-transform: uppercase;
}
.msg-channel .msg-chan-name {
font-size: 10px;
color: var(--text-muted);
opacity: 0.8;
margin-bottom: 2px;
padding-left: 4px;
letter-spacing: 0.5px;
}
.msg-channel .msg-bubble {
background: var(--msg-bubble-bg);
color: var(--msg-bubble-color);
border-bottom-left-radius: 4px;
border-color: var(--msg-bubble-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}
.msg-bubble {
padding: 10px 14px;
border-radius: 12px;
font-size: 14.5px;
line-height: 1.65;
word-break: break-word;
position: relative;
border: 1px solid transparent;
transition: box-shadow var(--dur-normal) var(--ease-out);
}
.msg-user .msg-bubble {
/* 用户与助手必须**看得出不一样**:原来两侧同底色、只差一个圆角,
整段对话读下来分不清谁说的。用户侧用 accent 渲染,助手侧保持中性面。 */
background: var(--msg-user-bg);
color: var(--text-primary);
border-bottom-right-radius: 5px;
border-color: rgba(var(--accent-rgb), 0.26);
}
.msg-assistant .msg-bubble {
background: var(--msg-bubble-bg);
color: var(--msg-bubble-color);
border-bottom-left-radius: 5px;
border-color: var(--msg-bubble-border);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.msg-system .msg-bubble {
background: var(--msg-system-bg);
color: var(--msg-system-color);
text-align: center;
font-size: 12px;
border-color: var(--msg-bubble-border);
}
.msg-bubble .text {
white-space: pre-wrap;
}
.msg-bubble .text p {
margin: 4px 0;
}
.msg-bubble .text h1,
.msg-bubble .text h2,
.msg-bubble .text h3 {
font-size: 1em;
margin: 8px 0 4px;
}
.msg-bubble .text ul,
.msg-bubble .text ol {
padding-left: 20px;
margin: 4px 0;
}
.msg-bubble .text pre {
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 6px;
padding: 8px;
margin: 4px 0;
overflow-x: auto;
font-size: 11.5px;
line-height: 1.5;
border: 1px solid var(--kv-border);
max-height: 200px;
}
.msg-bubble .text code {
background: var(--pre-bg);
color: var(--pre-color);
padding: 1px 4px;
border-radius: 3px;
font-size: 11.5px;
}
.msg-bubble .text pre code {
background: none;
padding: 0;
}
.msg-bubble .text blockquote {
border-left: 3px solid var(--border-color);
padding-left: 8px;
margin: 4px 0;
opacity: 0.8;
}
.msg-bubble .text table {
border-collapse: collapse;
margin: 4px 0;
font-size: 12px;
width: 100%;
}
.msg-bubble .text th,
.msg-bubble .text td {
border: 1px solid var(--border-color);
padding: 3px 6px;
text-align: left;
}
.msg-bubble .text img {
max-width: 100%;
border-radius: 6px;
}
/* ===== 聊天附件(图片/音频)=====
这两个类此前**只有类名没有样式**:JS 里写着 class="chat-attachment-img"
和 class="att-err",CSS 里一条都没有。图片靠内联 style 撑着还能看,
而失败占位 .att-err 完全没有样式 —— 即使事件接上了也只是一行裸文字。 */
.chat-attachment-img {
max-width: 320px;
max-height: 240px;
border-radius: 10px;
display: block;
cursor: zoom-in;
}
.att-err {
display: inline-block;
padding: 8px 12px;
border: 1px dashed var(--kv-border);
border-radius: 8px;
color: var(--text-muted, #888);
font-size: 12px;
line-height: 1.4;
}
/* ===== 思考卡片(PiDeck 风格)===== */
.reasoning-card {
margin: 6px 0;
border: 1px dashed var(--kv-border);
border-radius: var(--radius-sm);
background: var(--bg-panel, var(--bg-hover));
overflow: hidden;
}
.reasoning-card .reasoning-head {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
user-select: none;
color: var(--text-secondary);
}
.reasoning-card .reasoning-head:hover {
background: var(--bg-hover);
}
.reasoning-card .rc-ico {
color: var(--accent);
flex-shrink: 0;
}
.reasoning-card .rc-title {
font-weight: 600;
color: var(--text-primary);
}
.reasoning-card .rc-chev {
margin-left: auto;
display: inline-flex;
align-items: center;
color: var(--text-muted);
transition: transform 0.2s var(--ease-out);
}
.reasoning-card.open .rc-chev {
transform: rotate(180deg);
}
.reasoning-card .reasoning-body {
padding: 4px 10px 8px;
}
.reasoning-card .reasoning-content {
line-height: 1.5;
color: var(--text-tertiary);
font-size: 11.5px;
}
.reasoning-card .reasoning-content p {
margin: 4px 0;
}
.reasoning-card .reasoning-sweep {
height: 2px;
border-radius: 2px;
margin-top: 6px;
background: linear-gradient(
90deg,
transparent,
var(--accent),
transparent
);
background-size: 200% 100%;
animation: reasoningSweep 1.2s var(--ease-out) infinite;
opacity: 0.6;
}
@keyframes reasoningSweep {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
/* ===== 工具卡片(PiDeck 风格,扁平不套气泡)===== */
.tool-card {
background: var(--bg-panel, var(--bg-hover));
color: var(--text-secondary);
border-radius: var(--radius-sm);
padding: 7px 10px;
margin: 6px 0;
font-size: 11px;
border: 1px solid var(--kv-border);
border-left: 3px solid var(--accent);
cursor: pointer;
transition:
border-color 0.2s var(--ease-out),
background 0.2s var(--ease-out);
}
.tool-card.tc-running {
border-left-color: var(--accent);
}
.tool-card.tc-done {
border-left-color: rgba(23, 169, 100, 0.8);
}
.tool-card.tc-error {
border-left-color: var(--error, #db3694);
}
.tool-card .tc-line {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
user-select: none;
}
.tool-card .tc-ico {
display: inline-flex;
flex-shrink: 0;
color: var(--accent);
}
.tool-card.tc-error .tc-ico {
color: var(--error, #db3694);
}
.tool-card.tc-done .tc-ico {
color: rgba(23, 169, 100, 0.9);
}
.tool-card .tc-name {
font-weight: 600;
color: var(--text-primary);
}
.tool-card .tc-plugin {
font-size: 9.5px;
color: var(--text-muted);
background: var(--pre-bg);
border-radius: 4px;
padding: 0 4px;
line-height: 1.6;
flex-shrink: 0;
}
.tool-card .tc-state {
margin-left: auto;
font-size: 9.5px;
font-weight: 500;
padding: 1px 6px;
border-radius: 999px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
}
.tool-card .tc-run {
background: rgba(63, 110, 245, 0.18);
color: #a3b8ff;
}
.tool-card .tc-done {
background: rgba(23, 169, 100, 0.18);
color: #6ee7a8;
}
.tool-card .tc-deny {
background: rgba(219, 54, 148, 0.18);
color: #ff9ec6;
}
.tool-card .tc-spinner {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid rgba(63, 110, 245, 0.25);
border-top-color: #3f6ef5;
animation: spin 0.7s linear infinite;
display: inline-block;
}
.tool-card .tc-caret {
display: inline-flex;
align-items: center;
color: var(--text-muted);
transition: transform 0.15s var(--ease-out);
}
.tool-card.open .tc-caret {
transform: rotate(180deg);
}
.tool-card .tc-detail {
margin-top: 6px;
}
.tool-card .tc-detail-label {
font-size: 9.5px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-bottom: 2px;
}
.tool-card .tc-args,
.tool-card .tc-result {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.85;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 4px;
padding: 5px 7px;
}
.tool-card .tc-result {
opacity: 0.72;
max-height: 80px;
overflow-y: auto;
}
[data-theme="light"] .tool-card .tc-run {
color: #3f6ef5;
}
[data-theme="light"] .tool-card .tc-done {
color: #128a52;
}
[data-theme="light"] .tool-card .tc-deny {
color: #c2185b;
}
.tool-card.tool-drip-in {
animation: toolDripIn 0.32s var(--ease-out) both;
}
@keyframes toolDripIn {
0% {
opacity: 0;
transform: translateY(-18px) scale(0.96);
}
70% {
opacity: 1;
transform: translateY(2px) scale(1.005);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.msg-bubble .thinking-tools {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.msg-bubble .thinking-tool {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--text-secondary);
background: var(--pre-bg);
border-radius: 999px;
padding: 3px 10px;
}
.msg-bubble .thinking-tool.pill-in {
animation: pillIn 0.25s var(--ease-out) both;
}
@keyframes pillIn {
from {
opacity: 0;
transform: translateX(8px);
}
to {
opacity: 1;
transform: none;
}
}
.msg-bubble .live-spinner {
width: 15px;
height: 15px;
border-radius: 50%;
border: 2px solid var(--msg-user-bg);
border-top-color: var(--accent);
animation: spin 0.7s linear infinite;
flex-shrink: 0;
display: inline-block;
vertical-align: middle;
}
.msg-bubble .live-spinner + .thinking-tools,
.msg-bubble .live-spinner + .text {
margin-left: 12px;
}
.msg-bubble.grow-in {
animation: bubbleGrow 0.5s var(--ease-out) both;
}
.msg-bubble.grow-in .text {
animation: textFadeIn 0.35s ease 0.125s both;
}
@keyframes bubbleGrow {
from {
max-height: 28px;
opacity: 0.4;
}
to {
max-height: 3000px;
opacity: 1;
}
}
@keyframes textFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tool-call.tool-drip-in {
animation: toolDripIn 0.3s var(--ease-out) both;
}
@keyframes toolDripIn {
from {
opacity: 0;
transform: translateY(-14px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
/* 实时终端画面:模拟 agent 操控的终端屏幕 */
.term-screen {
background: #0f1115;
border: 1px solid #2a2e35;
border-radius: 8px;
margin: 6px 0 4px 0;
overflow: hidden;
font-size: 11px;
line-height: 1.45;
}
.term-screen .term-head {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #1a1d23;
color: #9aa0a8;
font-size: 10px;
border-bottom: 1px solid #2a2e35;
font-family: var(--font-mono);
}
.term-screen .term-head .term-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #3fb950;
flex-shrink: 0;
}
.term-screen .term-head .term-dot.stopped {
background: #d29922;
}
.term-screen pre.term-buf {
margin: 0;
padding: 6px 8px;
background: transparent;
color: #d8dee9;
font-family: var(--font-mono);
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
max-height: 260px;
overflow-y: auto;
scrollbar-width: thin;
}
.term-screen pre.term-buf::-webkit-scrollbar {
width: 6px;
}
.term-screen pre.term-buf::-webkit-scrollbar-thumb {
background: #33373d;
border-radius: 3px;
}
.chat-input-row {
display: flex;
gap: 8px;
flex-shrink: 0;
padding-top: 14px;
margin-top: 4px;
/* 输入区与消息流之间加一道分隔:否则最后一条消息与输入框糊在一起,
看不出“消息到头了”。 */
border-top: 1px solid var(--border-color);
}
.chat-input-row input {
flex: 1;
margin-bottom: 0;
}
.chat-input-row button {
flex-shrink: 0;
margin-bottom: 0;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--loading-border);
border-radius: 50%;
border-top-color: var(--loading-top);
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.monaco-like {
font-family: var(--font-mono);
font-size: 12px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 6px;
}
.kv-row {
display: grid;
grid-template-columns: minmax(110px, 180px) 1fr;
gap: 12px;
align-items: baseline;
padding: 8px;
/* 负外边距让 hover 高亮能满宽,而不把内容挤窄 */
margin: 0 -8px;
border-bottom: 1px solid var(--kv-border);
font-size: 13px;
border-radius: var(--radius-sm);
transition: background var(--dur-micro) var(--ease-out);
}
.kv-row:hover {
background: var(--glass-hover);
}
.kv-row:last-child {
border-bottom: none;
}
.kv-row .key {
color: var(--text-muted);
/* 键列不再硬编码 180px 宽度:窄屏/长键名时能自己收,避免把值挤成竖排 */
width: auto;
flex-shrink: 0;
font-size: 12.5px;
}
.kv-row .val {
color: var(--text-primary);
word-break: break-all;
/* 数字等宽:端口/计数/内存这类值上下对齐才好扫 */
font-variant-numeric: tabular-nums;
min-width: 0;
}
.tool-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
background: var(--accent-bg);
color: var(--accent);
margin: 1px;
}
.check-pass {
color: var(--success);
}
.check-fail {
color: var(--error);
}
.check-skip {
color: var(--text-secondary);
}
.memory-graph {
width: 100%;
height: 300px;
background: var(--bg-input);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 13px;
}
.health-panel {
display: grid;
gap: 8px;
}
.health-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--bg-input);
border-radius: var(--radius-sm);
font-size: 13px;
border: 1px solid transparent;
transition:
border var(--dur-micro),
background var(--dur-micro);
}
.health-item:hover {
border-color: rgba(var(--accent-rgb), 0.25);
}
.health-item .check-name {
flex: 1;
}
.health-item .check-status {
font-size: 11px;
font-weight: 500;
}
.fade-in {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#sm-container-chat {
height: 260px;
background: var(--bg-input);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
}
#sm-container-chat canvas {
display: block;
}
/* 星图容器:独立页签 / 总览页 / 聊天面板三处共用同一套样式。
position:relative 是必需的 —— hover 角标(#sm-label)绝对定位在容器上。*/
#sm-container-page,
#sm-container-home {
background: var(--bg-input);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
width: 100%;
}
#sm-container-page canvas,
#sm-container-home canvas {
display: block;
}
/* 节点名覆盖层:盖在 WebGL canvas 上,不吃鼠标事件(否则星图不能拖拽)。*/
#sm-labels {
pointer-events: none;
z-index: 4;
}
/* 选中详情面板:点击节点后的信息反馈。
★ 补它的原因:`starmapSelected` 此前只被赋值、无人读取,
点击节点在界面上没有任何信息反馈——“可读性差”的核心成因之一。*/
#sm-detail {
position: absolute;
right: 10px;
top: 10px;
display: none;
width: 232px;
max-height: 62%;
overflow-y: auto;
padding: 10px 12px;
font-size: 12px;
line-height: 1.5;
color: var(--text-primary);
background: rgba(10, 10, 20, 0.88);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
z-index: 6;
/* 面板本身要能滚动,但不该把滚轮事件漏给下面的星图。*/
pointer-events: auto;
}
#sm-detail .sm-detail-title {
font-weight: 600;
margin-bottom: 2px;
word-break: break-all;
}
#sm-detail .sm-detail-meta,
#sm-detail .sm-detail-id {
color: var(--text-muted);
font-size: 11px;
margin-bottom: 4px;
word-break: break-all;
}
#sm-detail .sm-detail-rel {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 11px;
}
#sm-detail .sm-detail-relrow {
margin-bottom: 2px;
word-break: break-all;
}
#sm-detail .sm-detail-arrow {
color: var(--accent, #7fb5ad);
margin-right: 3px;
}
#sm-detail .sm-detail-nb {
color: var(--text-primary);
}
/* hover 角标:取代原先每节点一张 256x64 CanvasTexture
(1151 张贴图 ≈ 72MB 显存)。DOM 文字更清晰且零 GPU 开销。*/
#sm-label {
position: absolute;
left: 10px;
bottom: 10px;
display: none;
max-width: 60%;
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
color: var(--text-primary);
background: rgba(10, 10, 20, 0.82);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
pointer-events: none;
z-index: 5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toggle-row {
margin-top: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.toggle-row .label-text {
color: var(--text-muted);
font-size: 12px;
}
.toggle-switch {
position: relative;
width: 36px;
height: 20px;
cursor: pointer;
flex-shrink: 0;
}
.toggle-track {
position: absolute;
inset: 0;
background: var(--bg-input);
border-radius: 10px;
transition: all 0.3s;
border: 1px solid var(--border-color);
}
.toggle-track.on {
background: var(--accent-bg);
border-color: var(--accent);
box-shadow: var(--shadow-glow);
}
.toggle-knob {
position: absolute;
width: 16px;
height: 16px;
left: 2px;
top: 2px;
background: var(--text-muted);
border-radius: 50%;
transition: all 0.3s;
}
.toggle-knob.on {
left: 18px;
background: var(--accent);
}
.toggle-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-muted);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn:hover {
background: var(--accent-bg);
color: var(--text-primary);
}
.toggle-btn.on {
background: var(--accent-bg);
color: var(--accent);
border-color: var(--accent);
}
.label-text {
color: var(--text-muted);
font-size: 12px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--loading-border);
border-top: 3px solid var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.sidebar-subnav {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 10px;
}
.sidebar-subnav span {
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
color: var(--text-muted);
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
.sidebar-subnav span:hover {
color: var(--text-primary);
}
.sidebar-subnav span.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
@media (max-width: 900px) {
.chat-layout {
height: auto;
}
.msg {
max-width: 100%;
}
}
@media (max-width: 768px) {
nav {
width: 14rem;
box-shadow: var(--shadow-lg);
}
.main-wrap {
margin-left: 0;
}
nav h1 {
font-size: 15px;
}
.topbar {
padding: 0 12px;
}
.breadcrumb .crumb-home {
display: none;
}
.container {
padding: 12px;
}
.card {
padding: 12px;
}
.grid-2,
.grid-3,
.grid-4 {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 20px;
}
.settings-layout {
flex-direction: column;
}
.settings-tabs {
max-height: 200px;
overflow-y: auto;
}
.kv-row {
flex-direction: column;
gap: 2px;
}
.kv-row .key {
width: auto;
}
.msg-avatar {
width: 24px;
height: 24px;
font-size: 11px;
}
.health-item {
flex-wrap: wrap;
gap: 4px;
}
.health-item .check-name {
flex: auto;
width: 100%;
}
}
/* ===== 8.7 Accessibility ===== */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
::selection {
background: rgba(var(--accent-rgb), 0.35);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ===== Themed scrollbar ===== */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(var(--accent-rgb), 0.25);
border-radius: var(--radius-pill);
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(var(--accent-rgb), 0.45);
background-clip: content-box;
border: 2px solid transparent;
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(var(--accent-rgb), 0.35) transparent;
}
/* ===== 运行态面板(图形化总览)=====
目标是"一眼看懂内核现在在干什么":排队/中断/中断栈/驻留子四个数字,
四级中断队列的条形,中断栈的层叠图,以及通道拓扑。全部用纯 CSS + 内联 SVG,
不引入任何依赖(前端刻意没有构建链)。 */
.rt-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
gap: 10px;
margin: 4px 0 14px;
}
.rt-tile {
position: relative;
padding: 12px 14px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
overflow: hidden;
}
.rt-tile .rt-num {
font-size: 28px;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.5px;
}
.rt-tile .rt-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.rt-tile .rt-sub {
font-size: 10px;
color: var(--text-muted);
opacity: 0.85;
}
.rt-tile.rt-active {
border-color: var(--sakura-400);
box-shadow: var(--shadow-glow);
}
.rt-tile.rt-warn .rt-num {
color: #ffb86b;
}
/* 底部细条:把"占了多少"用长度表达,比纯数字更快读懂 */
.rt-bar {
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
margin-top: 8px;
overflow: hidden;
}
.rt-bar > i {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--sakura-400), var(--frost-300));
transition: width 0.35s var(--ease-out);
}
/* ===== 队列:五个等大表框 =====
此前是五行「名字 | 进度条 | 元数据」的扁条:四级中断全为 0 时,
四行几乎全是空白,既占高度又难看。改成五个等大的框(与 KPI、阶段管道
同一套视觉语言),深度用大字报出来,下面是可见的格槽。 */
.rt-queues {
display: grid;
/* 100px 而不是 150px:两块都要保证「5 个一行」。
否则容器 540px(小窗口侧栏展开时就是这个宽度)会排成「4 个 + 1 个」——
第二行四个空位,看着就是一块坑。宁可框窄一点,也不留孤行。 */
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin-bottom: 8px;
}
.rt-qcell {
display: flex;
flex-direction: column;
gap: 7px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-qhead {
display: flex;
align-items: baseline;
gap: 7px;
min-width: 0;
}
.rt-qhead > b {
font-size: 16px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
}
.rt-qhead > span {
font-size: 10.5px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rt-qnum {
font-size: 26px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.rt-qmeta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 8px;
font-size: 10.5px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* 级别配色:框头与格槽点亮共用同一套色 */
.rt-qcell.rt-lv-4 .rt-qhead > b { color: #ff5c7a; }
.rt-qcell.rt-lv-3 .rt-qhead > b { color: #ffa657; }
.rt-qcell.rt-lv-2 .rt-qhead > b { color: var(--sakura-400); }
.rt-qcell.rt-lv-1 .rt-qhead > b { color: var(--frost-300); }
.rt-qcell.rt-lv-q .rt-qhead > b { color: #a3be8c; }
/* 有积压时整个框描边点亮:一眼看出哪条队列在堵 */
.rt-qcell.rt-lv-4.rt-active { border-color: rgba(255, 92, 122, 0.55); box-shadow: 0 0 0 1px rgba(255, 92, 122, 0.18); }
.rt-qcell.rt-lv-3.rt-active { border-color: rgba(255, 166, 87, 0.55); box-shadow: 0 0 0 1px rgba(255, 166, 87, 0.18); }
.rt-qcell.rt-lv-2.rt-active { border-color: rgba(var(--accent-rgb), 0.55); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.18); }
.rt-qcell.rt-lv-1.rt-active { border-color: rgba(136, 192, 208, 0.55); box-shadow: 0 0 0 1px rgba(136, 192, 208, 0.18); }
.rt-qcell.rt-lv-q.rt-active { border-color: rgba(163, 190, 140, 0.55); box-shadow: 0 0 0 1px rgba(163, 190, 140, 0.18); }
/* 排队队列无级别:用虚线框与四级中断区分开 */
.rt-qcell.rt-qcell-queued {
border-style: dashed;
}
/* 队列格槽:固定可见的「车位」,占用多少一眼可数。
此前是宽度百分比进度条 —— 队列为 0 时宽度为 0,整行只剩文字,
看上去就是「这块空着」。 */
.rt-slots {
display: flex;
align-items: stretch;
gap: 3px;
height: 16px;
min-width: 0;
}
.rt-slots > i {
flex: 1 1 0;
min-width: 3px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.05);
transition:
background 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-slots.rt-lv-4 > i.on { background: #ff5c7a; box-shadow: 0 0 6px rgba(255, 92, 122, 0.45); }
.rt-slots.rt-lv-3 > i.on { background: #ffa657; box-shadow: 0 0 6px rgba(255, 166, 87, 0.4); }
.rt-slots.rt-lv-2 > i.on { background: var(--sakura-400); box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.4); }
.rt-slots.rt-lv-1 > i.on { background: var(--frost-300); box-shadow: 0 0 6px rgba(136, 192, 208, 0.4); }
.rt-slots.rt-lv-q > i.on { background: #a3be8c; box-shadow: 0 0 6px rgba(163, 190, 140, 0.4); }
.rt-slots-more {
flex: 0 0 auto;
align-self: center;
margin-left: 4px;
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* 拓扑图:整张图为 SVG,文字降到最少,其余信息用图形编码
(归属=容器/配色、容量=细条、输出能力=圆点、路由=连线)。 */
.rt-svg-wrap {
overflow-x: auto;
padding: 4px 0 8px;
}
.rt-svg {
min-width: 560px;
font-family: inherit;
display: block;
}
.rt-svg text {
fill: var(--text-primary);
}
/* 拓扑图的文字全部走类(不用 font-size/fill 属性):
1) 字号统一可读(原来 9–11px 且低对比度,实机很难看清);
2) 颜色交给主题变量,切主题/配色时不用重算 SVG。
长名截断由 JS 的 rtClip 负责(SVG 没有 text-overflow)。 */
.rt-topo-svg .tp-name { font-size: 12px; fill: var(--text-primary); }
.rt-topo-svg .tp-sub { font-size: 10.5px; fill: var(--text-muted); font-variant-numeric: tabular-nums; }
.rt-topo-svg .tp-val { font-size: 12.5px; font-weight: 700; fill: var(--text-primary); }
.rt-topo-svg .tp-lab { font-size: 11px; font-weight: 600; }
.rt-topo-svg .tp-warn { font-size: 10.5px; fill: #ffb86b; }
.rt-topo-svg .tp-hint { font-size: 10.5px; fill: var(--text-muted); }
.rt-topo-svg .tp-edge { fill: none; }
.rt-topo-svg .tp-edge-in { stroke-opacity: 0.45; stroke-width: 1.4; }
.rt-topo-svg .tp-edge-out { stroke-opacity: 0.35; stroke-width: 1.2; }
/* 排队队列无级别:虚线框已在 .rt-qcell.rt-qcell-queued 处理 */
/* 中断栈:栈顶在上,做出"层叠现场"的观感 */
.rt-stack {
display: flex;
flex-direction: column-reverse;
gap: 4px;
margin: 2px 0 6px;
}
.rt-frame {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 9px;
border-radius: 8px;
font-size: 11px;
background: rgba(255, 255, 255, 0.05);
border-left: 3px solid var(--sakura-400);
}
.rt-frame .rt-frame-top {
margin-left: auto;
font-size: 10px;
color: var(--text-muted);
}
.rt-empty {
font-size: 11px;
color: var(--text-muted);
padding: 4px 0;
}
/* 通道拓扑:输入 → 内核 → 输出 */
.rt-topo {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 10px;
align-items: center;
margin-top: 6px;
}
.rt-topo-col {
display: flex;
flex-direction: column;
gap: 6px;
}
.rt-topo-col.rt-right {
text-align: right;
}
.rt-chan {
padding: 6px 9px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
font-size: 11px;
cursor: default;
}
.rt-chan .rt-chan-name {
font-weight: 600;
}
.rt-chan .rt-chan-sub {
font-size: 10px;
color: var(--text-muted);
}
.rt-chan .rt-caps {
display: inline-flex;
gap: 3px;
margin-left: 6px;
vertical-align: middle;
}
.rt-cap {
font-size: 9px;
padding: 1px 5px;
border-radius: 999px;
background: rgba(136, 192, 208, 0.16);
color: var(--frost-300);
}
/* —— 通道分配(按归属):容器 + 容量滑块 ——
为什么用容器而不是一排文字:归属是“分组”语义,只有把同一 owner 的
inputch 画进同一个盒子,才能一眼看出“这条输入被划给了哪个驻留子”。 */
.rt-owner {
border: 1px solid var(--border-color);
border-left: 3px solid var(--frost-300);
border-radius: 8px;
padding: 7px 9px;
margin-bottom: 7px;
background: rgba(255, 255, 255, 0.03);
}
.rt-owner.rt-owner-child {
border-left-color: #b48ead;
background: rgba(180, 142, 173, 0.07);
}
.rt-owner-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.rt-owner-name {
font-size: 11px;
font-weight: 600;
}
.rt-owner-meta {
font-size: 10px;
color: var(--text-muted);
}
.rt-assign {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-top: 1px dashed rgba(255, 255, 255, 0.06);
font-size: 11px;
}
.rt-assign .rt-chan-name {
font-weight: 600;
min-width: 84px;
}
.rt-assign .rt-chan-sub {
font-size: 10px;
color: var(--text-muted);
margin-left: auto;
}
.rt-badge-warn {
font-size: 9px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(191, 97, 106, 0.22);
color: #d08770;
}
/* 容量滑块:轨道 + 填充 + 把手 + 读数。 */
.rt-slider {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
}
.rt-slider-label {
font-size: 9px;
color: var(--text-muted);
}
.rt-slider-track {
position: relative;
display: inline-block;
width: 64px;
height: 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.10);
overflow: visible;
}
.rt-slider-track > i {
position: absolute;
left: 0;
top: 0;
bottom: 0;
border-radius: 999px;
background: var(--frost-300);
}
.rt-slider-track > b {
position: absolute;
top: 50%;
width: 9px;
height: 9px;
margin-left: -4.5px;
margin-top: -4.5px;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.rt-slider-val {
font-size: 10px;
min-width: 34px;
color: var(--text-muted);
}
/* 行内迷你条(登记 / 抢占并排) */
.rt-mini {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 6px;
font-size: 10px;
}
.rt-mini-track {
display: inline-block;
width: 30px;
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.10);
overflow: hidden;
vertical-align: middle;
}
.rt-mini-track > i {
display: block;
height: 100%;
border-radius: 999px;
background: var(--frost-300);
}
.rt-core {
width: 74px;
height: 74px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
text-align: center;
line-height: 1.2;
background: radial-gradient(circle at 50% 40%, rgba(var(--accent-strong-rgb), 0.35), rgba(24, 24, 38, 0.9));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glow);
}
.rt-section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.3px;
margin: 14px 0 8px;
text-transform: uppercase;
}
/* ===== 阶段管道:五个等大表框 =====
与中断队列、KPI 同一套视觉语言(等大框 + 大号加粗标题)。
此前是「小圆点 + 一条连接线 + 9px 小字」,在总览里几乎读不出
「现在走到哪一步」,尺寸也远小于旁边的 KPI 框。
现在事件直接落在它所属的框里,不需要靠颜色或图例去猜。 */
.rt-pipe-row {
display: grid;
/* 与 .rt-queues 同一套列宽规则:两块的框宽、换行点保持一致 */
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin: 2px 0 8px;
transition: opacity 0.3s var(--ease-out);
}
.rt-pipe-idle {
opacity: 0.55;
}
.rt-pipe-cell {
display: flex;
flex-direction: column;
gap: 8px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out),
background 0.25s var(--ease-out);
}
.rt-pipe-cell.active {
border-color: var(--accent);
background: rgba(var(--accent-rgb), 0.08);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.22);
}
.rt-pipe-head {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.rt-pipe-head > b {
font-size: 13.5px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rt-pipe-head .rt-ico {
width: 15px;
height: 15px;
color: var(--text-secondary);
}
.rt-pipe-cell.active .rt-pipe-head > b,
.rt-pipe-cell.active .rt-pipe-head .rt-ico {
color: var(--accent);
}
.rt-pipe-events {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 23px;
}
/* 「工具」格的滚动视口:固定一行高,只露最新一条。
工具调用一轮内可能几十次,全部追加会把这一格撑成长条,反而看不出
「现在在调什么」;这里让旧条向上滚出视口,留下最新那条。 */
.rt-pipe-scroll {
flex-wrap: nowrap;
height: 23px;
min-height: 23px;
overflow: hidden;
align-items: center;
}
/* 名称可缩、计数不可缩:窄框里先截断工具名,累计次数始终看得见。 */
.rt-pipe-scroll .rt-chip {
flex: 0 1 auto;
min-width: 0;
max-width: none;
}
/* rtFlashLatestTool 每来一条新调用就重加一次,重置动画时间轴。 */
.rt-chip-enter {
animation: rt-chip-scroll-in 0.3s var(--ease-out);
}
@keyframes rt-chip-scroll-in {
from {
transform: translateY(115%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.rt-scroll-count {
flex: 0 0 auto;
margin-left: auto;
font-style: normal;
font-size: 10.5px;
opacity: 0.6;
font-variant-numeric: tabular-nums;
}
/* 拓扑光点:沿 animateMotion 给的路径跑;不吃鼠标事件,跑完由 JS 摘掉。 */
.rt-spark {
pointer-events: none;
filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.8));
}
/* ===== 设置页「外观」区 ===== */
.ap-dots {
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.ap-dot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid var(--border-color);
cursor: pointer;
padding: 0;
transition: transform 0.12s var(--ease-out);
}
.ap-dot:hover {
transform: scale(1.12);
}
.ap-dot.on {
border-color: var(--text-primary);
box-shadow: 0 0 0 2px var(--accent-bg);
}
/* ===== 总览 KPI(图标 + 数字,静态骨架)=====
节点只在首帧建一次,刷新只改值与类名 —— 这是总览页不再闪的前提。 */
.ov-kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
gap: 10px;
}
.ov-kpi {
display: flex;
flex-direction: column;
gap: 5px;
padding: 12px 14px 11px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
}
.ov-ico svg {
width: 18px;
height: 18px;
fill: none;
stroke: var(--accent);
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
display: block;
}
.ov-val {
display: flex;
align-items: center;
gap: 6px;
font-size: 18px;
font-weight: 700;
line-height: 1.15;
min-width: 0;
}
.ov-val b {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 副行:内核名 + commit。总览只给版本号会分不清是哪个内核、哪次构建。 */
.ov-sub {
font-size: 10px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 12px;
font-variant-numeric: tabular-nums;
}
.ov-lab {
font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-muted);
}
.ov-dot {
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.ov-dot.ok {
background: var(--success);
box-shadow: 0 0 8px rgba(23, 169, 100, 0.55);
}
.ov-dot.warn {
background: var(--warning);
}
.ov-dot.bad {
background: var(--error);
}
/* 开源许可卡:协议标识 + 协议全文 + 源码仓库。
此前只是 KPI 卡底部一行小链接(.ov-foot),在长页面里基本看不见。 */
.ov-legal h2 {
margin-bottom: 6px;
}
.ov-legal-note {
margin: 10px 0 0;
font-size: 11px;
line-height: 1.55;
color: var(--text-muted);
}
/* 行内小图标:一律 SVG,不使用 emoji/符号字符。
事件 chip 用它区分普通工具与 output_* 输出通道。 */
.rt-ico {
width: 12px;
height: 12px;
flex: 0 0 auto;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
vertical-align: -1.5px;
}
.rt-ico-sm {
width: 11px;
height: 11px;
}
.rt-loop {
display: inline-flex;
color: var(--accent);
font-weight: 700;
}
.rt-chip {
display: inline-flex;
align-items: center;
gap: 3px;
/* min-width:0 + overflow:hidden:框窄时 chip 跟着缩,而不是把框撞破 */
min-width: 0;
max-width: 170px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.rt-chip-t {
/* font-weight 继承,不因为换成 <b> 而变粗;省略号靠它生效 */
font-weight: inherit;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rt-chip-tool {
border-color: rgba(136, 192, 208, 0.42);
color: var(--frost-300);
}
.rt-chip-output {
border-color: var(--accent);
color: var(--accent);
}
.rt-chip-stage {
opacity: 0.7;
}
.rt-chip-none {
opacity: 0.35;
border-style: dashed;
}
.rt-chip-n {
font-style: normal;
opacity: 0.75;
font-variant-numeric: tabular-nums;
}
/* ===== 服务入口列表(反代插件服务的「点一下直接打开」) ===== */
/* 与 run-* 系列同样的格槽风格:左状态点、中名称、右动作,避免行内文字挤在一起 */
.svc-list{display:flex;flex-direction:column;gap:6px}
.svc-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:10px;background:var(--bg-input,rgba(13,13,22,.5));border:1px solid var(--border-color,rgba(255,255,255,.09));font-size:13px;transition:border-color .15s,background .15s}
.svc-row:hover{border-color:var(--sakura-400,#ff7fac)}
.svc-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.svc-name{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}
.svc-sub{color:var(--text-muted,#6e7284);font-size:12px;white-space:nowrap}
.svc-tag{font-size:10px;padding:2px 6px;border-radius:6px;background:var(--frost-300,#88c0d0);color:#0d0d16;font-weight:700;letter-spacing:.04em}
.svc-tag-warn{background:var(--danger,#d1383d);color:#fff}
.svc-path{color:var(--text-muted,#6e7284);font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}
.svc-err{color:var(--danger,#d1383d);font-size:11px;margin-left:auto;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}