@mengzf/skillflow-mcp 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +48 -7
  2. package/dist/adapters/claude-code.adapter.d.ts +10 -0
  3. package/dist/adapters/claude-code.adapter.d.ts.map +1 -0
  4. package/dist/adapters/claude-code.adapter.js +166 -0
  5. package/dist/adapters/claude-code.adapter.js.map +1 -0
  6. package/dist/adapters/codex.adapter.d.ts +10 -0
  7. package/dist/adapters/codex.adapter.d.ts.map +1 -0
  8. package/dist/adapters/codex.adapter.js +109 -0
  9. package/dist/adapters/codex.adapter.js.map +1 -0
  10. package/dist/adapters/dsh.adapter.d.ts +7 -0
  11. package/dist/adapters/dsh.adapter.d.ts.map +1 -0
  12. package/dist/adapters/dsh.adapter.js +60 -0
  13. package/dist/adapters/dsh.adapter.js.map +1 -0
  14. package/dist/adapters/qoder.adapter.d.ts +10 -0
  15. package/dist/adapters/qoder.adapter.d.ts.map +1 -0
  16. package/dist/adapters/qoder.adapter.js +119 -0
  17. package/dist/adapters/qoder.adapter.js.map +1 -0
  18. package/dist/adapters/trae.adapter.d.ts +8 -0
  19. package/dist/adapters/trae.adapter.d.ts.map +1 -0
  20. package/dist/adapters/trae.adapter.js +104 -0
  21. package/dist/adapters/trae.adapter.js.map +1 -0
  22. package/dist/adapters/types.d.ts +25 -0
  23. package/dist/adapters/types.d.ts.map +1 -0
  24. package/dist/adapters/types.js +2 -0
  25. package/dist/adapters/types.js.map +1 -0
  26. package/dist/adapters/universal.sync.d.ts +22 -0
  27. package/dist/adapters/universal.sync.d.ts.map +1 -0
  28. package/dist/adapters/universal.sync.js +140 -0
  29. package/dist/adapters/universal.sync.js.map +1 -0
  30. package/dist/adapters/workbuddy.sniffer.d.ts.map +1 -1
  31. package/dist/adapters/workbuddy.sniffer.js +8 -3
  32. package/dist/adapters/workbuddy.sniffer.js.map +1 -1
  33. package/dist/db/invocation-filter.d.ts +29 -0
  34. package/dist/db/invocation-filter.d.ts.map +1 -0
  35. package/dist/db/invocation-filter.js +78 -0
  36. package/dist/db/invocation-filter.js.map +1 -0
  37. package/dist/db/schema.d.ts.map +1 -1
  38. package/dist/db/schema.js +28 -2
  39. package/dist/db/schema.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +16 -12
  42. package/dist/server.js.map +1 -1
  43. package/dist/services/dashboard.service.d.ts +2 -0
  44. package/dist/services/dashboard.service.d.ts.map +1 -1
  45. package/dist/services/dashboard.service.js +145 -40
  46. package/dist/services/dashboard.service.js.map +1 -1
  47. package/dist/services/filter.service.d.ts +26 -0
  48. package/dist/services/filter.service.d.ts.map +1 -0
  49. package/dist/services/filter.service.js +103 -0
  50. package/dist/services/filter.service.js.map +1 -0
  51. package/dist/services/telemetry.service.d.ts +9 -1
  52. package/dist/services/telemetry.service.d.ts.map +1 -1
  53. package/dist/services/telemetry.service.js +105 -36
  54. package/dist/services/telemetry.service.js.map +1 -1
  55. package/dist/types/models.d.ts +13 -1
  56. package/dist/types/models.d.ts.map +1 -1
  57. package/dist/types/tools.d.ts +44 -5
  58. package/dist/types/tools.d.ts.map +1 -1
  59. package/dist/types/tools.js +47 -1
  60. package/dist/types/tools.js.map +1 -1
  61. package/dist/utils/governance.d.ts +25 -0
  62. package/dist/utils/governance.d.ts.map +1 -0
  63. package/dist/utils/governance.js +70 -0
  64. package/dist/utils/governance.js.map +1 -0
  65. package/dist/utils/pricing.js +1 -1
  66. package/dist/utils/pricing.js.map +1 -1
  67. package/dist/web/render.d.ts.map +1 -1
  68. package/dist/web/render.js +1565 -239
  69. package/dist/web/render.js.map +1 -1
  70. package/dist/web/server.d.ts.map +1 -1
  71. package/dist/web/server.js +52 -28
  72. package/dist/web/server.js.map +1 -1
  73. package/package.json +1 -1
@@ -4,336 +4,1239 @@ export function renderDashboardHtml() {
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="color-scheme" content="light">
7
8
  <title>SkillFlow 效能监控看板 - 跨 Agent 技能遥测</title>
8
9
  <style>
9
10
  :root {
10
- --bg: #000000;
11
- --surface: rgba(28, 28, 30, 0.72);
12
- --surface-elevated: rgba(44, 44, 46, 0.8);
13
- --border: rgba(255, 255, 255, 0.08);
14
- --border-highlight: rgba(255, 255, 255, 0.16);
15
-
16
- --text-primary: #f5f5f7;
17
- --text-secondary: #86868b;
18
- --text-tertiary: #636366;
19
-
20
- --apple-blue: #2997ff;
21
- --apple-blue-glow: rgba(41, 151, 255, 0.25);
22
- --apple-green: #30d158;
23
- --apple-green-glow: rgba(48, 209, 88, 0.25);
24
- --apple-purple: #bf5af2;
25
- --apple-purple-glow: rgba(191, 90, 242, 0.25);
26
- --apple-orange: #ff9f0a;
27
- --apple-red: #ff453a;
28
-
29
- --radius-sm: 10px;
30
- --radius-md: 16px;
31
- --radius-lg: 24px;
11
+ /* Surfaces — Apple light canvas */
12
+ --bg: #f5f5f7;
13
+ --surface: #ffffff;
14
+ --surface-sunken: #fafafc;
15
+ --surface-hover: #f5f5f7;
16
+
17
+ /* Hairlines */
18
+ --border: rgba(0, 0, 0, 0.07);
19
+ --border-highlight: rgba(0, 0, 0, 0.14);
20
+ --hairline: rgba(60, 60, 67, 0.12);
21
+
22
+ /* Ink */
23
+ --text-primary: #1d1d1f;
24
+ --text-secondary: #5f5f66;
25
+ --text-tertiary: #6e6e73;
26
+
27
+ /* Accents (text-safe, WCAG AA on white) */
28
+ --apple-blue: #0066cc;
29
+ --blue-fill: #0071e3;
30
+ --apple-blue-glow: rgba(0, 113, 227, 0.16);
31
+ --apple-green: #1a7f37;
32
+ --green-fill: #34c759;
33
+ --apple-green-glow: rgba(52, 199, 89, 0.16);
34
+ --apple-purple: #7d3ca8;
35
+ --apple-orange: #a35200;
36
+ --apple-red: #d70015;
37
+
38
+ --radius-sm: 8px;
39
+ --radius-md: 12px;
40
+ --radius-lg: 20px;
32
41
  --radius-full: 9999px;
33
42
 
34
- --blur: blur(32px) saturate(180%);
35
- --shadow-card: 0 16px 36px -12px rgba(0, 0, 0, 0.65), 0 0 0 1px var(--border);
43
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
44
+ --shadow-md: 0 10px 26px -14px rgba(0, 0, 0, 0.22), 0 1px 2px rgba(0, 0, 0, 0.04);
45
+ --shadow-card: var(--shadow-sm);
46
+
47
+ --blur: saturate(180%) blur(20px);
48
+ --ease: cubic-bezier(0.25, 0.1, 0.25, 1);
36
49
  }
37
50
 
38
- * { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
51
+ *, *::before, *::after { box-sizing: border-box; }
52
+
53
+ * { margin: 0; padding: 0; }
54
+
55
+ html { -webkit-text-size-adjust: 100%; }
39
56
 
40
57
  body {
41
58
  background-color: var(--bg);
42
- background-image:
43
- radial-gradient(1200px circle at 50% -180px, rgba(41, 151, 255, 0.12), transparent 70%),
44
- radial-gradient(800px circle at 90% 400px, rgba(191, 90, 242, 0.06), transparent 60%);
45
- background-attachment: fixed;
46
59
  color: var(--text-primary);
47
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
60
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
61
+ "Segoe UI Variable Text", "Segoe UI", "PingFang SC", "Hiragino Sans GB",
62
+ "Microsoft YaHei", system-ui, sans-serif;
63
+ font-size: 15px;
64
+ line-height: 1.5;
65
+ letter-spacing: -0.006em;
48
66
  min-height: 100vh;
49
- padding: 32px 24px 60px;
50
- letter-spacing: -0.012em;
67
+ -webkit-font-smoothing: antialiased;
68
+ -moz-osx-font-smoothing: grayscale;
51
69
  }
52
70
 
53
- .container { max-width: 1240px; margin: 0 auto; }
71
+ ::selection { background: rgba(0, 113, 227, 0.18); }
54
72
 
55
- header {
73
+ /* ---------- Navigation bar ---------- */
74
+ .nav {
75
+ position: sticky;
76
+ top: 0;
77
+ z-index: 50;
78
+ background: rgba(255, 255, 255, 0.72);
79
+ backdrop-filter: var(--blur);
80
+ -webkit-backdrop-filter: var(--blur);
81
+ border-bottom: 1px solid var(--border);
82
+ }
83
+ .nav-inner {
84
+ max-width: 1560px;
85
+ margin: 0 auto;
86
+ padding: 0 32px;
87
+ min-height: 60px;
56
88
  display: flex;
89
+ align-items: center;
57
90
  justify-content: space-between;
91
+ gap: 20px;
92
+ }
93
+ .nav-actions {
94
+ display: flex;
58
95
  align-items: center;
59
- padding: 14px 20px;
60
- background: rgba(22, 22, 24, 0.65);
61
- backdrop-filter: var(--blur);
62
- -webkit-backdrop-filter: var(--blur);
63
- border: 1px solid var(--border-highlight);
64
- border-radius: var(--radius-lg);
65
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
66
- margin-bottom: 32px;
67
- flex-wrap: wrap;
68
- gap: 16px;
96
+ gap: 10px;
69
97
  }
70
98
 
71
- .brand { display: flex; align-items: center; gap: 14px; }
99
+ .brand {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 10px;
103
+ min-width: 0;
104
+ }
72
105
  .brand-icon {
73
- width: 36px; height: 36px;
74
- border-radius: var(--radius-sm);
75
- background: linear-gradient(135deg, #2c2c2e, #1c1c1e);
76
- border: 1px solid var(--border-highlight);
77
- display: flex; align-items: center; justify-content: center;
78
- box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.2);
106
+ width: 28px;
107
+ height: 28px;
108
+ flex: none;
109
+ border-radius: 8px;
110
+ display: grid;
111
+ place-items: center;
112
+ background: linear-gradient(180deg, #0a84ff, #0071e3);
113
+ box-shadow: 0 1px 2px rgba(0, 113, 227, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.35);
114
+ }
115
+ .brand-icon svg { width: 15px; height: 15px; fill: #ffffff; }
116
+ .brand-title {
117
+ font-size: 16px;
118
+ font-weight: 600;
119
+ letter-spacing: -0.015em;
120
+ color: var(--text-primary);
121
+ white-space: nowrap;
79
122
  }
80
- .brand-icon svg { width: 20px; height: 20px; fill: var(--apple-blue); }
81
- .brand-title { font-size: 19px; font-weight: 600; letter-spacing: -0.025em; color: #fff; }
82
123
 
83
124
  .brand-badge {
84
- display: inline-flex; align-items: center; gap: 6px;
85
- font-size: 11px; color: var(--apple-green);
86
- background: rgba(48, 209, 88, 0.12);
87
- border: 1px solid rgba(48, 209, 88, 0.25);
88
- padding: 3px 10px; border-radius: var(--radius-full); font-weight: 500;
125
+ display: inline-flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ margin-left: 4px;
129
+ padding-left: 12px;
130
+ border-left: 1px solid var(--hairline);
131
+ font-size: 12px;
132
+ font-weight: 500;
133
+ color: var(--apple-green);
134
+ white-space: nowrap;
89
135
  }
90
136
  .pulse-dot {
91
- width: 6px; height: 6px; border-radius: 50%;
92
- background: var(--apple-green);
93
- box-shadow: 0 0 8px var(--apple-green);
94
- animation: pulse 2s infinite ease-in-out;
137
+ width: 6px;
138
+ height: 6px;
139
+ border-radius: 50%;
140
+ background: var(--green-fill);
141
+ box-shadow: 0 0 0 3px var(--apple-green-glow);
142
+ animation: pulse 2.4s var(--ease) infinite;
95
143
  }
96
144
  @keyframes pulse {
97
145
  0%, 100% { opacity: 1; transform: scale(1); }
98
- 50% { opacity: 0.4; transform: scale(0.85); }
146
+ 50% { opacity: 0.55; transform: scale(0.86); }
99
147
  }
100
148
 
149
+ /* ---------- Segmented control ---------- */
101
150
  .segmented-control {
102
151
  display: inline-flex;
103
- background: rgba(0, 0, 0, 0.45);
104
- border: 1px solid var(--border);
105
- padding: 3px; border-radius: var(--radius-full);
152
+ padding: 3px;
153
+ gap: 2px;
154
+ background: rgba(120, 120, 128, 0.1);
155
+ border-radius: var(--radius-full);
106
156
  }
107
157
  .segment-btn {
108
- background: transparent; border: none;
109
- color: var(--text-secondary); font-size: 13px; font-weight: 500;
110
- padding: 6px 16px; border-radius: var(--radius-full);
111
- cursor: pointer; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
158
+ appearance: none;
159
+ border: none;
160
+ background: transparent;
161
+ color: var(--text-secondary);
162
+ font: inherit;
163
+ font-size: 13px;
164
+ font-weight: 500;
165
+ letter-spacing: -0.01em;
166
+ padding: 6px 14px;
167
+ border-radius: var(--radius-full);
168
+ cursor: pointer;
169
+ white-space: nowrap;
170
+ transition: color 0.2s var(--ease), background-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
112
171
  }
113
172
  .segment-btn:hover { color: var(--text-primary); }
114
173
  .segment-btn.active {
115
- background: rgba(255, 255, 255, 0.16);
116
- color: #ffffff;
117
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.2);
174
+ background: var(--surface);
175
+ color: var(--text-primary);
176
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
118
177
  }
119
178
 
120
179
  .btn-refresh {
121
- display: inline-flex; align-items: center; gap: 7px;
122
- background: rgba(255, 255, 255, 0.08);
180
+ display: inline-flex;
181
+ align-items: center;
182
+ gap: 6px;
183
+ appearance: none;
184
+ background: var(--surface);
123
185
  border: 1px solid var(--border-highlight);
124
- color: var(--text-primary); font-size: 13px; font-weight: 500;
125
- padding: 7px 15px; border-radius: var(--radius-full);
126
- cursor: pointer; transition: all 0.2s ease;
186
+ color: var(--text-primary);
187
+ font: inherit;
188
+ font-size: 13px;
189
+ font-weight: 500;
190
+ letter-spacing: -0.01em;
191
+ padding: 7px 14px;
192
+ border-radius: var(--radius-full);
193
+ cursor: pointer;
194
+ white-space: nowrap;
195
+ box-shadow: var(--shadow-sm);
196
+ transition: background-color 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.2s var(--ease);
197
+ }
198
+ .btn-refresh:hover { background: var(--surface-sunken); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
199
+ .btn-refresh:active { transform: scale(0.975); }
200
+ .btn-refresh svg { width: 13px; height: 13px; fill: currentColor; opacity: 0.85; }
201
+ /* Outline icons must not inherit the solid fill used by the refresh glyph */
202
+ .btn-help svg {
203
+ fill: none;
204
+ stroke: currentColor;
205
+ stroke-width: 1.7;
206
+ stroke-linecap: round;
207
+ stroke-linejoin: round;
208
+ opacity: 0.9;
209
+ }
210
+
211
+ :where(.segment-btn, .btn-refresh):focus-visible {
212
+ outline: 2px solid var(--blue-fill);
213
+ outline-offset: 2px;
214
+ }
215
+
216
+ /* ---------- Page shell ---------- */
217
+ .container {
218
+ max-width: 1560px;
219
+ margin: 0 auto;
220
+ padding: 26px 32px 72px;
127
221
  }
128
- .btn-refresh:hover { background: rgba(255, 255, 255, 0.14); transform: translateY(-1px); }
129
- .btn-refresh svg { width: 13px; height: 13px; fill: currentColor; }
130
222
 
131
- /* KPI Row */
223
+ /* Compact header: title and subtitle share one baseline row */
224
+ .page-head {
225
+ display: flex;
226
+ align-items: baseline;
227
+ flex-wrap: wrap;
228
+ gap: 2px 16px;
229
+ margin-bottom: 18px;
230
+ }
231
+ .page-head h1 {
232
+ font-size: 26px;
233
+ font-weight: 600;
234
+ line-height: 1.25;
235
+ letter-spacing: -0.022em;
236
+ color: var(--text-primary);
237
+ white-space: nowrap;
238
+ }
239
+ .page-desc {
240
+ font-size: 13.5px;
241
+ line-height: 1.4;
242
+ letter-spacing: -0.004em;
243
+ color: var(--text-tertiary);
244
+ padding-left: 16px;
245
+ border-left: 1px solid var(--hairline);
246
+ }
247
+
248
+ /* ---------- Query filters ---------- */
249
+ .filters {
250
+ display: flex;
251
+ flex-wrap: wrap;
252
+ align-items: flex-end;
253
+ gap: 14px 16px;
254
+ background: var(--surface);
255
+ border: 1px solid var(--border);
256
+ border-radius: var(--radius-lg);
257
+ box-shadow: var(--shadow-sm);
258
+ padding: 16px 20px 18px;
259
+ margin-bottom: 20px;
260
+ }
261
+ .filter-field {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 7px;
265
+ min-width: 0;
266
+ }
267
+ .filter-label {
268
+ font-size: 11.5px;
269
+ font-weight: 500;
270
+ letter-spacing: 0.02em;
271
+ color: var(--text-tertiary);
272
+ padding-left: 2px;
273
+ }
274
+ .filter-field--range .segmented-control { align-items: center; height: 36px; }
275
+
276
+ .select {
277
+ appearance: none;
278
+ -webkit-appearance: none;
279
+ height: 36px;
280
+ min-width: 148px;
281
+ max-width: 210px;
282
+ padding: 0 32px 0 12px;
283
+ font: inherit;
284
+ font-size: 13px;
285
+ font-weight: 500;
286
+ letter-spacing: -0.008em;
287
+ color: var(--text-primary);
288
+ background-color: var(--surface);
289
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.75 6 7.75 9 4.75' fill='none' stroke='%236e6e73' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
290
+ background-repeat: no-repeat;
291
+ background-position: right 11px center;
292
+ border: 1px solid var(--border-highlight);
293
+ border-radius: 10px;
294
+ cursor: pointer;
295
+ text-overflow: ellipsis;
296
+ transition: border-color 0.2s var(--ease), background-color 0.2s var(--ease), color 0.2s var(--ease);
297
+ }
298
+ .select:hover { background-color: var(--surface-sunken); }
299
+ .select:focus-visible { outline: 2px solid var(--blue-fill); outline-offset: 1px; }
300
+ .select.is-active {
301
+ border-color: rgba(0, 113, 227, 0.45);
302
+ background-color: rgba(0, 113, 227, 0.045);
303
+ color: var(--apple-blue);
304
+ }
305
+
306
+ .filter-actions {
307
+ margin-left: auto;
308
+ display: flex;
309
+ align-items: center;
310
+ gap: 8px;
311
+ }
312
+ .btn-reset {
313
+ appearance: none;
314
+ height: 36px;
315
+ padding: 0 16px;
316
+ font: inherit;
317
+ font-size: 13px;
318
+ font-weight: 500;
319
+ letter-spacing: -0.008em;
320
+ color: var(--text-primary);
321
+ background: var(--surface);
322
+ border: 1px solid var(--border-highlight);
323
+ border-radius: var(--radius-full);
324
+ cursor: pointer;
325
+ transition: background-color 0.2s var(--ease), opacity 0.2s var(--ease);
326
+ }
327
+ .btn-reset:hover:not(:disabled) { background: var(--surface-sunken); }
328
+ .btn-reset:focus-visible { outline: 2px solid var(--blue-fill); outline-offset: 2px; }
329
+ .btn-reset:disabled { opacity: 0.38; cursor: default; }
330
+
331
+ /* ---------- KPI cards ---------- */
132
332
  .kpi-row {
133
333
  display: grid;
134
334
  grid-template-columns: repeat(4, 1fr);
135
- gap: 18px;
136
- margin-bottom: 28px;
335
+ gap: 16px;
336
+ margin-bottom: 16px;
137
337
  }
138
- @media (max-width: 1024px) { .kpi-row { grid-template-columns: repeat(2, 1fr); } }
139
- @media (max-width: 600px) { .kpi-row { grid-template-columns: 1fr; } }
140
338
 
141
339
  .card {
142
340
  background: var(--surface);
143
- backdrop-filter: var(--blur);
144
- -webkit-backdrop-filter: var(--blur);
145
341
  border: 1px solid var(--border);
146
342
  border-radius: var(--radius-lg);
147
- padding: 24px;
148
- box-shadow: var(--shadow-card);
149
- position: relative; overflow: hidden;
150
- transition: transform 0.25s ease, border-color 0.25s ease;
343
+ padding: 22px 24px 24px;
344
+ box-shadow: var(--shadow-sm);
345
+ transition: box-shadow 0.3s var(--ease), border-color 0.3s var(--ease);
151
346
  }
152
- .card:hover { border-color: var(--border-highlight); transform: translateY(-2px); }
153
- .card::before {
154
- content: ""; position: absolute;
155
- top: 0; left: 0; right: 0; height: 1px;
156
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
347
+ .card:hover {
348
+ box-shadow: var(--shadow-md);
349
+ border-color: var(--border-highlight);
157
350
  }
158
351
 
159
352
  .kpi-label {
160
- font-size: 13px; color: var(--text-secondary);
161
- font-weight: 500; margin-bottom: 12px;
162
- display: flex; align-items: center; justify-content: space-between;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: space-between;
356
+ gap: 12px;
357
+ font-size: 13px;
358
+ font-weight: 500;
359
+ letter-spacing: -0.005em;
360
+ color: var(--text-secondary);
361
+ margin-bottom: 18px;
362
+ }
363
+ .kpi-icon {
364
+ width: 30px;
365
+ height: 30px;
366
+ flex: none;
367
+ border-radius: 9px;
368
+ display: grid;
369
+ place-items: center;
370
+ }
371
+ .kpi-icon svg {
372
+ width: 16px;
373
+ height: 16px;
374
+ fill: none;
375
+ stroke: currentColor;
376
+ stroke-width: 1.7;
377
+ stroke-linecap: round;
378
+ stroke-linejoin: round;
163
379
  }
380
+ .kpi-icon svg .fill { fill: currentColor; stroke: none; }
381
+ .tint-blue { background: rgba(0, 113, 227, 0.1); color: var(--apple-blue); }
382
+ .tint-purple { background: rgba(125, 60, 168, 0.1); color: var(--apple-purple); }
383
+ .tint-green { background: rgba(26, 127, 55, 0.1); color: var(--apple-green); }
384
+ .tint-amber { background: rgba(163, 82, 0, 0.1); color: var(--apple-orange); }
385
+
164
386
  .kpi-number {
165
- font-size: 38px; font-weight: 700;
166
- letter-spacing: -0.035em; line-height: 1.05;
167
- margin-bottom: 8px; font-variant-numeric: tabular-nums;
387
+ font-size: 38px;
388
+ font-weight: 600;
389
+ line-height: 1.06;
390
+ letter-spacing: -0.032em;
391
+ color: var(--text-primary);
392
+ font-variant-numeric: tabular-nums;
393
+ margin-bottom: 8px;
168
394
  }
169
395
  .kpi-meta {
170
- font-size: 12px; color: var(--text-tertiary);
171
- display: flex; align-items: center; gap: 6px;
396
+ display: flex;
397
+ align-items: center;
398
+ flex-wrap: wrap;
399
+ gap: 4px;
400
+ font-size: 12.5px;
401
+ letter-spacing: -0.004em;
402
+ color: var(--text-tertiary);
172
403
  }
173
- .accent-green { color: var(--apple-green); }
174
- .accent-blue { color: var(--apple-blue); }
175
- .accent-purple { color: var(--apple-purple); }
404
+ .accent-green,
405
+ .accent-blue,
406
+ .accent-purple { font-weight: 600; }
176
407
 
177
- /* Content Grid */
408
+ /* The token figure is injected with an inline colour by the renderer;
409
+ keep it in ink so it stays legible on a light surface. */
410
+ #kpiTokens { color: var(--text-primary) !important; }
411
+
412
+ /* ---------- Panels ---------- */
178
413
  .dashboard-grid {
179
414
  display: grid;
180
415
  grid-template-columns: 1fr 1fr;
181
- gap: 20px;
182
- margin-bottom: 28px;
416
+ gap: 16px;
417
+ margin-bottom: 16px;
183
418
  }
184
- @media (max-width: 860px) { .dashboard-grid { grid-template-columns: 1fr; } }
185
419
 
186
420
  .panel-header {
187
- display: flex; justify-content: space-between; align-items: center;
188
- margin-bottom: 20px; padding-bottom: 12px;
189
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
421
+ display: flex;
422
+ align-items: baseline;
423
+ justify-content: space-between;
424
+ gap: 16px;
425
+ margin-bottom: 22px;
190
426
  }
191
427
  .panel-title {
192
- font-size: 16px; font-weight: 600;
193
- letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px;
428
+ display: inline-flex;
429
+ align-items: center;
430
+ gap: 8px;
431
+ font-size: 15px;
432
+ font-weight: 600;
433
+ letter-spacing: -0.014em;
434
+ color: var(--text-primary);
435
+ }
436
+ .panel-title svg {
437
+ width: 16px;
438
+ height: 16px;
439
+ flex: none;
440
+ fill: none;
441
+ stroke: var(--text-tertiary);
442
+ stroke-width: 1.7;
443
+ stroke-linecap: round;
444
+ stroke-linejoin: round;
445
+ }
446
+ .panel-subtitle {
447
+ font-size: 12px;
448
+ color: var(--text-tertiary);
449
+ white-space: nowrap;
194
450
  }
195
- .panel-subtitle { font-size: 12px; color: var(--text-secondary); }
196
451
 
197
- .bar-row { margin-bottom: 16px; }
452
+ /* ---------- Bars ---------- */
453
+ .bar-row { margin-bottom: 18px; }
454
+ .bar-row:last-child { margin-bottom: 0; }
198
455
  .bar-label-group {
199
- display: flex; justify-content: space-between;
200
- font-size: 13px; margin-bottom: 7px;
456
+ display: flex;
457
+ align-items: baseline;
458
+ justify-content: space-between;
459
+ gap: 12px;
460
+ font-size: 13px;
461
+ margin-bottom: 9px;
201
462
  }
202
463
  .skill-rank-badge {
203
- display: inline-flex; align-items: center; justify-content: center;
204
- width: 18px; height: 18px; border-radius: 50%;
205
- background: rgba(255, 255, 255, 0.1);
206
- color: var(--text-secondary); font-size: 10px; font-weight: 600; margin-right: 8px;
464
+ display: inline-grid;
465
+ place-items: center;
466
+ width: 20px;
467
+ height: 20px;
468
+ margin-right: 8px;
469
+ border-radius: 50%;
470
+ background: rgba(120, 120, 128, 0.1);
471
+ color: var(--text-secondary);
472
+ font-size: 11px;
473
+ font-weight: 600;
474
+ vertical-align: 1px;
475
+ }
476
+ .skill-title {
477
+ font-weight: 500;
478
+ letter-spacing: -0.006em;
479
+ color: var(--text-primary);
480
+ }
481
+ .skill-name-cell {
482
+ display: flex;
483
+ align-items: center;
484
+ min-width: 0;
485
+ overflow: hidden;
486
+ white-space: nowrap;
487
+ }
488
+ .skill-id {
489
+ font-size: 11px;
490
+ color: var(--text-tertiary);
491
+ margin-left: 6px;
492
+ overflow: hidden;
493
+ text-overflow: ellipsis;
494
+ }
495
+ .skill-hours {
496
+ font-size: 11px;
497
+ color: var(--text-tertiary);
498
+ margin-left: 8px;
499
+ font-variant-numeric: tabular-nums;
500
+ }
501
+ .skill-stat {
502
+ color: var(--text-secondary);
503
+ font-size: 12.5px;
504
+ font-variant-numeric: tabular-nums;
505
+ white-space: nowrap;
207
506
  }
208
- .skill-title { font-weight: 500; color: #fff; }
209
- .skill-stat { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
210
507
 
211
508
  .progress-track {
212
- background: rgba(255, 255, 255, 0.06);
213
- height: 8px; border-radius: var(--radius-full);
214
- overflow: hidden; width: 100%;
509
+ width: 100%;
510
+ height: 6px;
511
+ border-radius: var(--radius-full);
512
+ background: rgba(120, 120, 128, 0.12);
513
+ overflow: hidden;
215
514
  }
216
515
  .progress-fill {
217
- height: 100%; border-radius: var(--radius-full);
218
- transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
219
- }
220
- .fill-gradient-mint {
221
- background: linear-gradient(90deg, #30d158, #34c759);
222
- box-shadow: 0 0 10px var(--apple-green-glow);
223
- }
224
- .fill-gradient-blue {
225
- background: linear-gradient(90deg, #0071e3, #2997ff);
226
- box-shadow: 0 0 10px var(--apple-blue-glow);
516
+ height: 100%;
517
+ border-radius: inherit;
518
+ transition: width 0.7s cubic-bezier(0.25, 0.1, 0.25, 1);
227
519
  }
520
+ .fill-gradient-mint { background: linear-gradient(90deg, #2fb350, #17803a); }
521
+ .fill-gradient-blue { background: linear-gradient(90deg, #0071e3, #0a84ff); }
228
522
 
229
- /* Table */
523
+ /* ---------- Table ---------- */
230
524
  .table-card {
231
525
  background: var(--surface);
232
- backdrop-filter: var(--blur);
233
- -webkit-backdrop-filter: var(--blur);
234
526
  border: 1px solid var(--border);
235
527
  border-radius: var(--radius-lg);
236
- padding: 24px; box-shadow: var(--shadow-card);
528
+ box-shadow: var(--shadow-sm);
529
+ padding: 24px 0 4px;
237
530
  overflow: hidden;
238
531
  }
239
- .table-responsive { overflow-x: auto; margin-top: 8px; }
240
- table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
532
+ .table-card .panel-header {
533
+ padding: 0 24px;
534
+ margin-bottom: 18px;
535
+ }
536
+ .table-responsive {
537
+ overflow-x: auto;
538
+ scrollbar-width: thin;
539
+ scrollbar-color: rgba(0, 0, 0, 0.12) transparent;
540
+ }
541
+ .table-responsive::-webkit-scrollbar { height: 6px; }
542
+ .table-responsive::-webkit-scrollbar-thumb {
543
+ background: rgba(0, 0, 0, 0.14);
544
+ border-radius: var(--radius-full);
545
+ }
546
+ .table-responsive::-webkit-scrollbar-track { background: transparent; }
547
+
548
+ table {
549
+ width: 100%;
550
+ border-collapse: separate;
551
+ border-spacing: 0;
552
+ font-size: 13px;
553
+ }
241
554
  th {
242
- text-align: left; font-weight: 500; color: var(--text-secondary);
243
- padding: 12px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.08);
244
- font-size: 12px;
555
+ text-align: left;
556
+ font-size: 11.5px;
557
+ font-weight: 600;
558
+ letter-spacing: 0.01em;
559
+ color: var(--text-tertiary);
560
+ background: rgba(120, 120, 128, 0.06);
561
+ border-top: 1px solid var(--border);
562
+ border-bottom: 1px solid var(--border);
563
+ padding: 10px 12px;
564
+ white-space: nowrap;
245
565
  }
246
566
  td {
247
- padding: 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.04);
248
- color: var(--text-primary); vertical-align: middle;
567
+ padding: 12px 12px;
568
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
569
+ color: var(--text-primary);
570
+ vertical-align: middle;
249
571
  font-variant-numeric: tabular-nums;
572
+ white-space: nowrap;
250
573
  }
574
+ th:first-child, td:first-child { padding-left: 20px; }
575
+ th:last-child, td:last-child { padding-right: 20px; }
251
576
  tr:last-child td { border-bottom: none; }
252
- tr:hover td { background: rgba(255, 255, 255, 0.03); }
577
+ tbody tr { transition: background-color 0.15s var(--ease); }
578
+ tbody tr:hover td { background: var(--surface-sunken); }
253
579
 
580
+ /* Cell typography */
581
+ .cell-muted { color: var(--text-tertiary); font-size: 12px; }
582
+ .cell-session {
583
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
584
+ font-size: 11px;
585
+ color: var(--text-tertiary);
586
+ letter-spacing: -0.01em;
587
+ }
588
+ .cell-hours {
589
+ color: var(--text-primary);
590
+ font-weight: 500;
591
+ }
592
+ .cell-steps {
593
+ font-size: 13px;
594
+ font-weight: 600;
595
+ line-height: 1.2;
596
+ color: var(--apple-green);
597
+ }
598
+ .cell-steps-note {
599
+ font-size: 11px;
600
+ font-weight: 400;
601
+ line-height: 1.2;
602
+ color: var(--text-tertiary);
603
+ margin-top: 3px;
604
+ font-variant-numeric: tabular-nums;
605
+ }
606
+ .cell-gain {
607
+ font-size: 12.5px;
608
+ font-weight: 600;
609
+ color: var(--apple-green);
610
+ font-variant-numeric: tabular-nums;
611
+ }
612
+
613
+ /* ---------- Pills ---------- */
254
614
  .pill {
255
- display: inline-flex; align-items: center; gap: 5px;
256
- padding: 3px 10px; border-radius: var(--radius-full);
257
- font-size: 11px; font-weight: 500;
615
+ display: inline-flex;
616
+ align-items: center;
617
+ gap: 5px;
618
+ padding: 2px 8px;
619
+ border-radius: 6px;
620
+ font-size: 11.5px;
621
+ font-weight: 500;
622
+ line-height: 1.6;
623
+ letter-spacing: -0.002em;
624
+ white-space: nowrap;
258
625
  }
259
- .pill-agent {
260
- background: rgba(41, 151, 255, 0.12); color: var(--apple-blue);
261
- border: 1px solid rgba(41, 151, 255, 0.25);
626
+ .pill-agent { background: rgba(0, 113, 227, 0.09); color: var(--apple-blue); }
627
+ .pill-neutral {
628
+ background: rgba(120, 120, 128, 0.1);
629
+ color: #48484a;
262
630
  }
263
631
  .pill-model {
264
- background: rgba(191, 90, 242, 0.12); color: var(--apple-purple);
265
- border: 1px solid rgba(191, 90, 242, 0.25);
266
- font-family: ui-monospace, monospace; font-size: 11px;
632
+ background: rgba(120, 120, 128, 0.12);
633
+ color: #48484a;
634
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
635
+ font-size: 11px;
636
+ }
637
+ .pill-purple { background: rgba(125, 60, 168, 0.09); color: var(--apple-purple); }
638
+ .pill-blue { background: rgba(0, 113, 227, 0.09); color: var(--apple-blue); }
639
+ .pill-success { background: rgba(26, 127, 55, 0.09); color: var(--apple-green); }
640
+ .pill-partial { background: rgba(163, 82, 0, 0.09); color: var(--apple-orange); }
641
+ .pill-failed { background: rgba(215, 0, 21, 0.09); color: var(--apple-red); }
642
+
643
+ /* ---------- Info Help Indicator ---------- */
644
+ .info-help {
645
+ display: inline-flex;
646
+ align-items: center;
647
+ justify-content: center;
648
+ width: 15px;
649
+ height: 15px;
650
+ border-radius: 50%;
651
+ font-size: 10px;
652
+ font-weight: 600;
653
+ line-height: 1;
654
+ color: var(--text-tertiary);
655
+ background: rgba(120, 120, 128, 0.12);
656
+ border: 1px solid rgba(0, 0, 0, 0.08);
657
+ cursor: pointer;
658
+ margin-left: 5px;
659
+ vertical-align: middle;
660
+ transition: all 0.2s var(--ease);
661
+ user-select: none;
662
+ flex: none;
663
+ }
664
+ .info-help:hover {
665
+ background: var(--blue-fill);
666
+ color: #ffffff;
667
+ border-color: var(--blue-fill);
668
+ transform: scale(1.15);
669
+ box-shadow: 0 2px 6px var(--apple-blue-glow);
670
+ }
671
+ /* Inside dense headers the marker should read as a quiet annotation */
672
+ th .info-help,
673
+ .mini-stat-label .info-help,
674
+ .subhead .info-help,
675
+ .inline-stats .info-help {
676
+ width: 14px;
677
+ height: 14px;
678
+ font-size: 9.5px;
679
+ background: transparent;
680
+ border-color: rgba(0, 0, 0, 0.16);
681
+ color: var(--text-tertiary);
682
+ margin-left: 3px;
683
+ transform: none;
267
684
  }
268
- .pill-purple {
269
- background: rgba(191, 90, 242, 0.12); color: var(--apple-purple);
270
- border: 1px solid rgba(191, 90, 242, 0.25);
685
+ th .info-help:hover,
686
+ .mini-stat-label .info-help:hover,
687
+ .subhead .info-help:hover,
688
+ .inline-stats .info-help:hover {
689
+ background: var(--blue-fill);
690
+ border-color: var(--blue-fill);
691
+ color: #fff;
271
692
  }
272
- .pill-blue {
273
- background: rgba(41, 151, 255, 0.12); color: var(--apple-blue);
274
- border: 1px solid rgba(41, 151, 255, 0.25);
693
+
694
+ /* ---------- Modal & Glossary Whitepaper ---------- */
695
+ .modal-overlay {
696
+ position: fixed;
697
+ inset: 0;
698
+ z-index: 100;
699
+ background: rgba(0, 0, 0, 0.4);
700
+ backdrop-filter: blur(12px);
701
+ -webkit-backdrop-filter: blur(12px);
702
+ display: flex;
703
+ align-items: center;
704
+ justify-content: center;
705
+ padding: 24px;
706
+ opacity: 0;
707
+ pointer-events: none;
708
+ transition: opacity 0.25s var(--ease);
275
709
  }
276
- .pill-success {
277
- background: rgba(48, 209, 88, 0.12); color: var(--apple-green);
278
- border: 1px solid rgba(48, 209, 88, 0.25);
710
+ .modal-overlay.open {
711
+ opacity: 1;
712
+ pointer-events: auto;
279
713
  }
280
- .pill-partial {
281
- background: rgba(255, 159, 10, 0.12); color: var(--apple-orange);
282
- border: 1px solid rgba(255, 159, 10, 0.25);
714
+ .modal-sheet {
715
+ background: var(--surface);
716
+ border: 1px solid var(--border-highlight);
717
+ border-radius: var(--radius-lg);
718
+ box-shadow: 0 24px 64px -16px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.06);
719
+ width: 100%;
720
+ max-width: 860px;
721
+ max-height: 85vh;
722
+ display: flex;
723
+ flex-direction: column;
724
+ overflow: hidden;
725
+ transform: scale(0.96) translateY(12px);
726
+ transition: transform 0.25s var(--ease);
283
727
  }
284
- .pill-failed {
285
- background: rgba(255, 69, 58, 0.12); color: var(--apple-red);
286
- border: 1px solid rgba(255, 69, 58, 0.25);
728
+ .modal-overlay.open .modal-sheet {
729
+ transform: scale(1) translateY(0);
730
+ }
731
+ .modal-header {
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: space-between;
735
+ padding: 20px 28px;
736
+ border-bottom: 1px solid var(--border);
737
+ background: var(--surface);
738
+ gap: 16px;
739
+ }
740
+ .modal-title-group h2 {
741
+ font-size: 18px;
742
+ font-weight: 600;
743
+ letter-spacing: -0.016em;
744
+ color: var(--text-primary);
745
+ }
746
+ .modal-title-group p {
747
+ font-size: 13px;
748
+ color: var(--text-secondary);
749
+ margin-top: 2px;
750
+ }
751
+ .modal-close {
752
+ appearance: none;
753
+ border: none;
754
+ background: rgba(120, 120, 128, 0.12);
755
+ width: 30px;
756
+ height: 30px;
757
+ border-radius: 50%;
758
+ display: grid;
759
+ place-items: center;
760
+ font-size: 14px;
761
+ color: var(--text-secondary);
762
+ cursor: pointer;
763
+ transition: all 0.15s ease;
764
+ flex: none;
765
+ }
766
+ .modal-close:hover {
767
+ background: rgba(120, 120, 128, 0.22);
768
+ color: var(--text-primary);
769
+ }
770
+ .modal-search {
771
+ padding: 12px 28px;
772
+ background: var(--surface);
773
+ border-bottom: 1px solid var(--border);
774
+ }
775
+ .glossary-search {
776
+ width: 100%;
777
+ height: 36px;
778
+ padding: 0 14px;
779
+ font: inherit;
780
+ font-size: 13px;
781
+ color: var(--text-primary);
782
+ background: var(--surface-sunken);
783
+ border: 1px solid var(--border-highlight);
784
+ border-radius: 10px;
785
+ outline: none;
786
+ transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease), background-color 0.2s var(--ease);
787
+ }
788
+ .glossary-search::placeholder { color: var(--text-tertiary); }
789
+ .glossary-search:focus {
790
+ background: var(--surface);
791
+ border-color: var(--blue-fill);
792
+ box-shadow: 0 0 0 3px var(--apple-blue-glow);
793
+ }
794
+
795
+ .modal-body {
796
+ padding: 20px 28px 28px;
797
+ overflow-y: auto;
798
+ display: flex;
799
+ flex-direction: column;
800
+ gap: 12px;
801
+ background: var(--bg);
802
+ }
803
+ .glossary-card {
804
+ background: var(--surface);
805
+ border: 1px solid var(--border);
806
+ border-radius: 14px;
807
+ padding: 18px 20px;
808
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
809
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
810
+ }
811
+ .glossary-card.highlight {
812
+ border-color: var(--blue-fill);
813
+ background: rgba(0, 113, 227, 0.04);
814
+ }
815
+ .glossary-title-row {
816
+ display: flex;
817
+ align-items: center;
818
+ justify-content: space-between;
819
+ margin-bottom: 8px;
820
+ }
821
+ .glossary-term {
822
+ font-size: 15px;
823
+ font-weight: 600;
824
+ color: var(--text-primary);
825
+ display: flex;
826
+ align-items: center;
827
+ gap: 8px;
828
+ }
829
+ .glossary-def {
830
+ font-size: 13.5px;
831
+ line-height: 1.55;
832
+ color: var(--text-secondary);
833
+ margin-bottom: 8px;
834
+ }
835
+ .glossary-formula {
836
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
837
+ font-size: 12px;
838
+ line-height: 1.5;
839
+ background: rgba(0, 113, 227, 0.05);
840
+ border-left: 3px solid var(--blue-fill);
841
+ padding: 7px 12px;
842
+ border-radius: 0 6px 6px 0;
843
+ color: var(--text-primary);
844
+ margin: 9px 0;
845
+ }
846
+ .glossary-scene {
847
+ font-size: 12.5px;
848
+ line-height: 1.5;
849
+ color: var(--text-tertiary);
850
+ }
851
+
852
+ /* ---------- Panel body primitives ---------- */
853
+ .dashboard-grid > .card {
854
+ display: flex;
855
+ flex-direction: column;
856
+ }
857
+ #l2QualityList,
858
+ #l3GovernanceList {
859
+ display: flex;
860
+ flex-direction: column;
861
+ flex: 1;
862
+ }
863
+ .panel-body {
864
+ display: flex;
865
+ flex-direction: column;
866
+ gap: 14px;
867
+ flex: 1;
287
868
  }
288
869
 
870
+ /* Small section heading inside a panel */
871
+ .subhead {
872
+ display: flex;
873
+ align-items: center;
874
+ justify-content: space-between;
875
+ gap: 10px;
876
+ font-size: 12px;
877
+ font-weight: 600;
878
+ letter-spacing: -0.002em;
879
+ color: var(--text-secondary);
880
+ margin-bottom: 8px;
881
+ }
882
+ .subhead > span:first-child { display: inline-flex; align-items: center; }
883
+ .subhead-note {
884
+ font-size: 11.5px;
885
+ font-weight: 400;
886
+ color: var(--text-tertiary);
887
+ white-space: nowrap;
888
+ font-variant-numeric: tabular-nums;
889
+ }
890
+ .subhead-note strong { color: var(--apple-blue); font-weight: 600; }
891
+
892
+ /* Two-segment ratio bar */
893
+ .split-bar {
894
+ display: flex;
895
+ gap: 3px;
896
+ height: 6px;
897
+ border-radius: var(--radius-full);
898
+ overflow: hidden;
899
+ }
900
+ .split-bar-seg { height: 100%; border-radius: var(--radius-full); }
901
+ .split-bar-explicit { background: linear-gradient(90deg, #0071e3, #0a84ff); }
902
+ .split-bar-implicit { background: rgba(120, 120, 128, 0.22); }
903
+
904
+ /* Compact stat tile */
905
+ .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
906
+ .mini-stat {
907
+ background: var(--bg);
908
+ border: 1px solid transparent;
909
+ border-radius: var(--radius-md);
910
+ padding: 12px 14px 13px;
911
+ min-width: 0;
912
+ }
913
+ .mini-stat-label {
914
+ display: flex;
915
+ align-items: center;
916
+ gap: 3px;
917
+ font-size: 12px;
918
+ letter-spacing: -0.002em;
919
+ color: var(--text-secondary);
920
+ margin-bottom: 5px;
921
+ white-space: nowrap;
922
+ }
923
+ .mini-stat-value {
924
+ font-size: 21px;
925
+ font-weight: 600;
926
+ line-height: 1.15;
927
+ letter-spacing: -0.022em;
928
+ color: var(--text-primary);
929
+ font-variant-numeric: tabular-nums;
930
+ }
931
+ .mini-stat-note {
932
+ font-size: 11.5px;
933
+ line-height: 1.4;
934
+ color: var(--text-tertiary);
935
+ margin-top: 3px;
936
+ }
937
+ .is-warn { color: var(--apple-orange); }
938
+
939
+ /* Bottom inline stats row (divided, no heavy rules) */
940
+ .inline-stats {
941
+ display: grid;
942
+ grid-template-columns: 1fr 1fr;
943
+ margin-top: auto;
944
+ padding-top: 12px;
945
+ border-top: 1px solid var(--border);
946
+ font-size: 12px;
947
+ color: var(--text-tertiary);
948
+ }
949
+ .inline-stats > div {
950
+ display: flex;
951
+ align-items: center;
952
+ gap: 5px;
953
+ min-width: 0;
954
+ }
955
+ .inline-stats > div + div {
956
+ padding-left: 14px;
957
+ border-left: 1px solid var(--border);
958
+ }
959
+ .inline-stats strong {
960
+ color: var(--text-primary);
961
+ font-weight: 600;
962
+ font-variant-numeric: tabular-nums;
963
+ }
964
+ .inline-stats .is-danger { color: var(--apple-red); }
965
+
966
+ /* Health score list */
967
+ .health-list {
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: 2px;
971
+ margin: 0 -8px;
972
+ }
973
+ .health-row {
974
+ display: flex;
975
+ align-items: center;
976
+ justify-content: space-between;
977
+ gap: 10px;
978
+ padding: 6px 8px;
979
+ border-radius: 8px;
980
+ font-size: 12.5px;
981
+ transition: background-color 0.15s var(--ease);
982
+ }
983
+ .health-row:hover { background: var(--surface-hover); }
984
+ .health-name {
985
+ font-weight: 600;
986
+ color: var(--text-primary);
987
+ letter-spacing: -0.004em;
988
+ min-width: 0;
989
+ overflow: hidden;
990
+ text-overflow: ellipsis;
991
+ white-space: nowrap;
992
+ }
993
+ .health-id {
994
+ font-size: 11px;
995
+ font-weight: 400;
996
+ color: var(--text-tertiary);
997
+ margin-left: 6px;
998
+ }
999
+ .grade-pill {
1000
+ font-size: 10.5px;
1001
+ padding: 1px 7px;
1002
+ font-variant-numeric: tabular-nums;
1003
+ flex: none;
1004
+ }
1005
+
1006
+ /* Advisory blocks (dormant skills, follow-up feedback) */
1007
+ .notice {
1008
+ padding-top: 12px;
1009
+ border-top: 1px solid var(--border);
1010
+ }
1011
+ .notice-title {
1012
+ display: flex;
1013
+ align-items: center;
1014
+ gap: 6px;
1015
+ font-size: 12px;
1016
+ font-weight: 600;
1017
+ letter-spacing: -0.002em;
1018
+ color: var(--text-secondary);
1019
+ margin-bottom: 5px;
1020
+ }
1021
+ .notice-badge {
1022
+ display: inline-flex;
1023
+ align-items: center;
1024
+ justify-content: center;
1025
+ min-width: 18px;
1026
+ height: 17px;
1027
+ padding: 0 6px;
1028
+ border-radius: var(--radius-full);
1029
+ background: rgba(163, 82, 0, 0.1);
1030
+ color: var(--apple-orange);
1031
+ font-size: 10.5px;
1032
+ font-weight: 600;
1033
+ font-variant-numeric: tabular-nums;
1034
+ }
1035
+ .notice-body {
1036
+ font-size: 12px;
1037
+ line-height: 1.55;
1038
+ color: var(--text-tertiary);
1039
+ }
1040
+ .notice-item {
1041
+ font-size: 12px;
1042
+ line-height: 1.5;
1043
+ color: var(--text-tertiary);
1044
+ margin-top: 5px;
1045
+ }
1046
+ .notice-item strong { color: var(--text-primary); font-weight: 600; }
1047
+ .notice-count {
1048
+ font-size: 11px;
1049
+ color: var(--text-tertiary);
1050
+ font-variant-numeric: tabular-nums;
1051
+ }
1052
+ .dot {
1053
+ width: 6px;
1054
+ height: 6px;
1055
+ border-radius: 50%;
1056
+ flex: none;
1057
+ }
1058
+ .dot-warn { background: var(--apple-orange); }
1059
+ .dot-info { background: var(--apple-purple); }
1060
+
1061
+ .empty-state {
1062
+ padding: 30px 0;
1063
+ text-align: center;
1064
+ font-size: 13px;
1065
+ color: var(--text-tertiary);
1066
+ }
1067
+
1068
+ /* ---------- Footer ---------- */
289
1069
  footer {
290
- text-align: center; margin-top: 48px;
291
- font-size: 12px; color: var(--text-tertiary);
1070
+ margin-top: 44px;
1071
+ padding-top: 26px;
1072
+ border-top: 1px solid var(--border);
1073
+ text-align: center;
1074
+ font-size: 12px;
1075
+ line-height: 1.9;
1076
+ letter-spacing: -0.004em;
1077
+ color: var(--text-tertiary);
1078
+ }
1079
+
1080
+ /* ---------- Responsive ---------- */
1081
+ @media (max-width: 1024px) {
1082
+ .kpi-row { grid-template-columns: repeat(2, 1fr); }
1083
+ }
1084
+ @media (max-width: 860px) {
1085
+ .dashboard-grid { grid-template-columns: 1fr; }
1086
+ }
1087
+ /* Filter bar collapses to a two-column grid before the selects would wrap awkwardly. */
1088
+ @media (max-width: 960px) {
1089
+ .filters {
1090
+ display: grid;
1091
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1092
+ gap: 12px;
1093
+ }
1094
+ .filter-field--range { grid-column: 1 / -1; }
1095
+ .filter-field--range .segmented-control { width: 100%; }
1096
+ .filters .segment-btn { flex: 1; }
1097
+ .select { width: 100%; max-width: none; min-width: 0; }
1098
+ .filter-actions { grid-column: 1 / -1; margin-left: 0; }
1099
+ .btn-reset { width: 100%; }
1100
+ }
1101
+ @media (max-width: 900px) {
1102
+ /* Subtitle falls to its own line — drop the inline divider */
1103
+ .page-head { flex-direction: column; gap: 3px; }
1104
+ .page-desc { padding-left: 0; border-left: 0; }
1105
+ }
1106
+ @media (max-width: 760px) {
1107
+ .nav-inner {
1108
+ flex-wrap: wrap;
1109
+ gap: 10px;
1110
+ padding: 10px 20px 12px;
1111
+ }
1112
+ .nav-actions { width: 100%; justify-content: flex-end; }
1113
+ .container { padding: 20px 20px 56px; }
1114
+ .page-head h1 { font-size: 21px; }
1115
+ .page-desc { font-size: 13px; }
1116
+ .filters { padding: 14px 16px 16px; }
1117
+ }
1118
+ @media (max-width: 560px) {
1119
+ .kpi-row { grid-template-columns: 1fr; }
1120
+ .brand-badge { display: none; }
1121
+ .kpi-number { font-size: 34px; }
1122
+ .nav-actions { gap: 8px; }
1123
+ .segment-btn { font-size: 12px; padding: 6px 10px; }
1124
+ .btn-refresh { padding: 7px 11px; }
1125
+ .panel-header {
1126
+ flex-direction: column;
1127
+ align-items: flex-start;
1128
+ gap: 4px;
1129
+ }
1130
+ .table-card .panel-header { padding: 0 20px; }
1131
+ th:first-child, td:first-child { padding-left: 20px; }
1132
+ th:last-child, td:last-child { padding-right: 20px; }
1133
+ }
1134
+ @media (max-width: 400px) {
1135
+ .btn-label { display: none; }
1136
+ .btn-refresh { width: 32px; height: 32px; padding: 0; justify-content: center; }
1137
+ }
1138
+
1139
+ @media (prefers-reduced-motion: reduce) {
1140
+ *, *::before, *::after {
1141
+ animation-duration: 0.001ms !important;
1142
+ animation-iteration-count: 1 !important;
1143
+ transition-duration: 0.001ms !important;
1144
+ }
292
1145
  }
293
1146
  </style>
294
1147
  </head>
295
1148
  <body>
296
- <div class="container">
297
- <!-- Top Bar -->
298
- <header>
1149
+ <div class="nav">
1150
+ <div class="nav-inner">
299
1151
  <div class="brand">
300
1152
  <div class="brand-icon">
301
- <svg viewBox="0 0 24 24">
1153
+ <svg viewBox="0 0 24 24" aria-hidden="true">
302
1154
  <path d="M12 2L14.4 9.6L22 12L14.4 14.4L12 22L9.6 14.4L2 12L9.6 9.6L12 2Z"/>
303
1155
  </svg>
304
1156
  </div>
305
- <div>
306
- <div class="brand-title">SkillFlow</div>
307
- </div>
1157
+ <div class="brand-title">SkillFlow</div>
308
1158
  <div class="brand-badge">
309
1159
  <span class="pulse-dot"></span>
310
1160
  系统就绪
311
1161
  </div>
312
1162
  </div>
313
1163
 
314
- <div class="segmented-control">
315
- <button class="segment-btn active" onclick="switchRange('all', this)">全部历史</button>
316
- <button class="segment-btn" onclick="switchRange('today', this)">今日</button>
317
- <button class="segment-btn" onclick="switchRange('7days', this)">近 7 天</button>
318
- <button class="segment-btn" onclick="switchRange('30days', this)">近 30 天</button>
319
- </div>
320
-
321
- <div>
322
- <button class="btn-refresh" onclick="fetchData()">
323
- <svg viewBox="0 0 24 24">
1164
+ <div class="nav-actions">
1165
+ <button type="button" class="btn-refresh btn-help" onclick="openGlossary()" title="查看指标定义、推导公式与业务白皮书">
1166
+ <svg viewBox="0 0 24 24" aria-hidden="true" style="width:14px;height:14px;">
1167
+ <circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>
1168
+ </svg>
1169
+ <span class="btn-label">指标术语手册</span>
1170
+ </button>
1171
+ <button type="button" class="btn-refresh" onclick="refreshAll()">
1172
+ <svg viewBox="0 0 24 24" aria-hidden="true">
324
1173
  <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
325
1174
  </svg>
326
- 刷新
1175
+ <span class="btn-label">刷新</span>
327
1176
  </button>
328
1177
  </div>
329
- </header>
1178
+ </div>
1179
+ </div>
1180
+
1181
+ <main class="container">
1182
+ <div class="page-head">
1183
+ <h1>SkillFlow 效能监控看板</h1>
1184
+ <p class="page-desc">跨 Agent 技能遥测 · 汇总技能命中、节省工时与模型成本的真实收益</p>
1185
+ </div>
1186
+
1187
+ <!-- Query filters: applies to KPI, rankings, agent split and the feed -->
1188
+ <section class="filters" aria-label="查询条件">
1189
+ <div class="filter-field filter-field--range">
1190
+ <span class="filter-label">时间范围</span>
1191
+ <div class="segmented-control" role="group" aria-label="时间范围">
1192
+ <button type="button" class="segment-btn active" data-range="all" onclick="switchRange('all', this)">全部历史</button>
1193
+ <button type="button" class="segment-btn" data-range="today" onclick="switchRange('today', this)">今日</button>
1194
+ <button type="button" class="segment-btn" data-range="7days" onclick="switchRange('7days', this)">近 7 天</button>
1195
+ <button type="button" class="segment-btn" data-range="30days" onclick="switchRange('30days', this)">近 30 天</button>
1196
+ </div>
1197
+ </div>
1198
+
1199
+ <label class="filter-field">
1200
+ <span class="filter-label">Agent 宿主</span>
1201
+ <select class="select" id="filterAgent" onchange="onFilterChange('agent', this)">
1202
+ <option value="">全部 Agent</option>
1203
+ </select>
1204
+ </label>
1205
+
1206
+ <label class="filter-field">
1207
+ <span class="filter-label">Skill 技能</span>
1208
+ <select class="select" id="filterSkill" onchange="onFilterChange('skill', this)">
1209
+ <option value="">全部 Skill</option>
1210
+ </select>
1211
+ </label>
330
1212
 
331
- <!-- 4 Apple Pro Hero KPI Cards -->
1213
+ <label class="filter-field">
1214
+ <span class="filter-label">执行模型</span>
1215
+ <select class="select" id="filterModel" onchange="onFilterChange('model', this)">
1216
+ <option value="">全部模型</option>
1217
+ </select>
1218
+ </label>
1219
+
1220
+ <label class="filter-field">
1221
+ <span class="filter-label">执行状态</span>
1222
+ <select class="select" id="filterStatus" onchange="onFilterChange('status', this)">
1223
+ <option value="">全部状态</option>
1224
+ </select>
1225
+ </label>
1226
+
1227
+ <div class="filter-actions">
1228
+ <button type="button" class="btn-reset" id="btnReset" onclick="resetFilters()" disabled>重置</button>
1229
+ </div>
1230
+ </section>
1231
+
1232
+ <!-- KPI Row -->
332
1233
  <div class="kpi-row">
333
1234
  <div class="card">
334
1235
  <div class="kpi-label">
335
1236
  <span>技能触发总量</span>
336
- <span style="font-size: 16px;">⚡</span>
1237
+ <span class="kpi-icon tint-blue" aria-hidden="true">
1238
+ <svg viewBox="0 0 24 24"><path class="fill" d="M13.2 2.4 4.6 13.3c-.4.5 0 1.3.7 1.3h4.5l-1.1 6.6c-.1.8.9 1.3 1.4.6l8.4-10.9c.4-.5 0-1.3-.7-1.3h-4.5l1.2-6.6c.1-.8-.9-1.3-1.3-.6Z"/></svg>
1239
+ </span>
337
1240
  </div>
338
1241
  <div class="kpi-number" id="kpiHits">-</div>
339
1242
  <div class="kpi-meta">执行成功率 <span class="accent-green" id="kpiSuccessRate">-</span></div>
@@ -342,7 +1245,9 @@ export function renderDashboardHtml() {
342
1245
  <div class="card">
343
1246
  <div class="kpi-label">
344
1247
  <span>累计节省人工工时</span>
345
- <span style="font-size: 16px;">⏱️</span>
1248
+ <span class="kpi-icon tint-purple" aria-hidden="true">
1249
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.2V12l3.2 2"/></svg>
1250
+ </span>
346
1251
  </div>
347
1252
  <div class="kpi-number accent-purple" id="kpiHours">-</div>
348
1253
  <div class="kpi-meta">替代人工手动编写与排查</div>
@@ -350,29 +1255,34 @@ export function renderDashboardHtml() {
350
1255
 
351
1256
  <div class="card">
352
1257
  <div class="kpi-label">
353
- <span>精简流程步骤</span>
354
- <span style="font-size: 16px;">🌱</span>
1258
+ <span>精简流程步骤 <button type="button" class="info-help" data-glossary="term-steps" title="计算公式: 无 Skill 时基准步骤 - 实际执行步骤 = 节省步骤。点击查看白皮书">?</button></span>
1259
+ <span class="kpi-icon tint-green" aria-hidden="true">
1260
+ <svg viewBox="0 0 24 24"><path d="M4 7.5h16"/><path d="M4 12.5h11"/><path d="M4 17.5h6"/></svg>
1261
+ </span>
355
1262
  </div>
356
1263
  <div class="kpi-number accent-green" id="kpiSteps">-</div>
357
- <div class="kpi-meta">消除多轮重复交互</div>
1264
+ <div class="kpi-meta" id="kpiStepsMeta" title="计算公式: 无 Skill 时的人工/自由探索基准步骤 - 实际执行步骤 = 精简步骤">消除多轮重复交互</div>
358
1265
  </div>
359
1266
 
360
1267
  <div class="card">
361
1268
  <div class="kpi-label">
362
1269
  <span>真实模型节约折算</span>
363
- <span style="font-size: 16px;">🪙</span>
1270
+ <span class="kpi-icon tint-amber" aria-hidden="true">
1271
+ <svg viewBox="0 0 24 24"><path d="M8.6 6.2 12 10.4l3.4-4.2"/><path d="M12 10.4V18"/><path d="M8.8 12.6h6.4"/><path d="M8.8 15.4h6.4"/></svg>
1272
+ </span>
364
1273
  </div>
365
1274
  <div class="kpi-number accent-blue" id="kpiCost">-</div>
366
1275
  <div class="kpi-meta">节约 Token <span id="kpiTokens" style="color:#fff; font-weight:600;">-</span></div>
367
1276
  </div>
368
1277
  </div>
369
1278
 
370
- <!-- Charts Section -->
1279
+ <!-- Panels -->
371
1280
  <div class="dashboard-grid">
372
1281
  <div class="card">
373
1282
  <div class="panel-header">
374
1283
  <div class="panel-title">
375
- <span>🏆</span> 价值贡献最大 Skill 排行榜
1284
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20V11"/><path d="M10 20V4.5"/><path d="M16 20v-6"/></svg>
1285
+ 价值贡献最大 Skill 排行榜
376
1286
  </div>
377
1287
  <div class="panel-subtitle">按节省步骤与工时</div>
378
1288
  </div>
@@ -382,7 +1292,8 @@ export function renderDashboardHtml() {
382
1292
  <div class="card">
383
1293
  <div class="panel-header">
384
1294
  <div class="panel-title">
385
- <span>🤖</span> 各 Agent 生态调用与节约贡献
1295
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.2 20.4 7.6 12 12 3.6 7.6 12 3.2Z"/><path d="M4 12.2 12 16.6l8-4.4"/><path d="M4 16.6 12 21l8-4.4"/></svg>
1296
+ 各 Agent 生态调用与节约贡献
386
1297
  </div>
387
1298
  <div class="panel-subtitle">跨平台分布</div>
388
1299
  </div>
@@ -390,11 +1301,39 @@ export function renderDashboardHtml() {
390
1301
  </div>
391
1302
  </div>
392
1303
 
393
- <!-- Live Invocations Table -->
1304
+ <!-- L2 & L3 Governance Section -->
1305
+ <div class="dashboard-grid">
1306
+ <!-- Engineering Quality -->
1307
+ <div class="card">
1308
+ <div class="panel-header">
1309
+ <div class="panel-title">
1310
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
1311
+ 工程质量与意图纯度
1312
+ </div>
1313
+ <div class="panel-subtitle">上下文与调用指标</div>
1314
+ </div>
1315
+ <div id="l2QualityList">加载中...</div>
1316
+ </div>
1317
+
1318
+ <!-- Asset Evolution -->
1319
+ <div class="card">
1320
+ <div class="panel-header">
1321
+ <div class="panel-title">
1322
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
1323
+ 资产治理与盲区反哺
1324
+ </div>
1325
+ <div class="panel-subtitle">健康评分与进化建议</div>
1326
+ </div>
1327
+ <div id="l3GovernanceList">加载中...</div>
1328
+ </div>
1329
+ </div>
1330
+
1331
+ <!-- Live invocations table -->
394
1332
  <div class="table-card">
395
- <div class="panel-header" style="margin-bottom: 8px;">
1333
+ <div class="panel-header">
396
1334
  <div class="panel-title">
397
- <span>⏱️</span> 实时 Skill 命中与模型明细
1335
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12.5h3.6l2.6-7 4 14 2.6-7H21"/></svg>
1336
+ 实时 Skill 命中与模型明细
398
1337
  </div>
399
1338
  <div class="panel-subtitle" id="tableCount">展示最新调用流水</div>
400
1339
  </div>
@@ -407,41 +1346,227 @@ export function renderDashboardHtml() {
407
1346
  <th>Agent 宿主</th>
408
1347
  <th>执行模型</th>
409
1348
  <th>命中的 Skill</th>
410
- <th>价值模式</th>
1349
+ <th>价值模式 <button type="button" class="info-help" data-glossary="term-profile" title="根据 Token 投入与预期产出划定(品质重载 / 高效精简 / 深度均衡)。点击查看白皮书">?</button></th>
411
1350
  <th>执行状态</th>
412
1351
  <th>节省工时</th>
413
- <th>节省步骤</th>
414
- <th>Token 收益 / 投入</th>
1352
+ <th title="计算公式: 无 Skill 时的人工/自由探索基准步骤 - 本次实际执行步骤 = 节省步骤">节省步骤 <button type="button" class="info-help" data-glossary="term-steps" title="无该技能基准步骤 - 实际执行步骤 = 节省步骤。点击查看白皮书">?</button></th>
1353
+ <th>Token 收益 / 投入 <button type="button" class="info-help" data-glossary="term-tokens" title="基准预估 Token - 实际消耗 Token = 净收益。点击查看白皮书">?</button></th>
415
1354
  </tr>
416
1355
  </thead>
417
1356
  <tbody id="invocationsBody">
418
- <tr><td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
1357
+ <tr><td colspan="10" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
419
1358
  </tbody>
420
1359
  </table>
421
1360
  </div>
422
1361
  </div>
423
1362
 
424
1363
  <footer>
425
- SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing & Work-Hour ROI<br>
1364
+ SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing &amp; Work-Hour ROI<br>
426
1365
  支持 Claude Code · Trae · Qoder · WorkBuddy · Codex · DSH
427
1366
  </footer>
428
- </div>
1367
+
1368
+ <!-- Apple Frosted Glass Glossary Modal -->
1369
+ <div class="modal-overlay" id="glossaryModal" onclick="onModalBackdropClick(event)">
1370
+ <div class="modal-sheet" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
1371
+ <div class="modal-header">
1372
+ <div class="modal-title-group">
1373
+ <h2 id="modalTitle">指标术语与算法度量白皮书</h2>
1374
+ <p>核心概念定义、数学核算推演公式与业务效能解读</p>
1375
+ </div>
1376
+ <button type="button" class="modal-close" onclick="closeGlossary()" aria-label="关闭">✕</button>
1377
+ </div>
1378
+ <div class="modal-search">
1379
+ <input type="text" id="glossarySearch" class="glossary-search" placeholder="搜索名词、公式或关键字..." oninput="filterGlossary(this.value)">
1380
+ </div>
1381
+ <div class="modal-body" id="glossaryCardsList">
1382
+ <!-- 1. 节省步骤 -->
1383
+ <div class="glossary-card" id="term-steps">
1384
+ <div class="glossary-title-row">
1385
+ <div class="glossary-term">节省步骤 (Saved Workflow Steps)</div>
1386
+ <span class="pill pill-success">业务感知</span>
1387
+ </div>
1388
+ <div class="glossary-def">没有该技能辅助时人工或自由探索的多轮交互步骤,减去本次实际交互步骤。</div>
1389
+ <div class="glossary-formula">节省步骤 = 基准预估步骤 (Baseline) - 实际交互步骤 (Actual)</div>
1390
+ <div class="glossary-scene"><strong>典型场景</strong>:例如写长文原本需经“初始提问 ➡️ 纠错去AI味 ➡️ 调整文风 ➡️ 补例子 ➡️ 调排版 ➡️ 定稿”共 6 轮交互。使用技能后 1 轮直接生成符合预期的成稿,净消除 5 轮人机拉扯。</div>
1391
+ </div>
1392
+
1393
+ <!-- 2. Token 收益 / 投入 -->
1394
+ <div class="glossary-card" id="term-tokens">
1395
+ <div class="glossary-title-row">
1396
+ <div class="glossary-term">Token 收益 / 投入 (Net Tokens)</div>
1397
+ <span class="pill pill-success">业务感知</span>
1398
+ </div>
1399
+ <div class="glossary-def">衡量本次技能执行的真实 Token 增减差值(净 ROI 收益)。</div>
1400
+ <div class="glossary-formula">Net Tokens = 基准预估 Token - 本次实际消耗 Token</div>
1401
+ <div class="glossary-scene"><strong>业务意义</strong>:若消耗低于基准则显示为绿色节约;若技能挂载了长篇规约导致消耗超额(如活人感写作),系统不虚标“节省”,而是真实显示为「投入 X 万 Tokens」,实事求是。</div>
1402
+ </div>
1403
+
1404
+ <!-- 3. 价值模式 -->
1405
+ <div class="glossary-card" id="term-profile">
1406
+ <div class="glossary-title-row">
1407
+ <div class="glossary-term">价值模式 (Quality Profile)</div>
1408
+ <span class="pill pill-purple">价值定位</span>
1409
+ </div>
1410
+ <div class="glossary-def">根据 Token 投入与预期产出比,由算法自动给技能打上的价值分类标签。</div>
1411
+ <div class="glossary-formula">高效精简型 (Net &gt; 0) | 品质重载型 (Actual &gt; 2 × Baseline) | 深度均衡型 (Balanced)</div>
1412
+ <div class="glossary-scene"><strong>三类划分</strong>:<br>• <strong>高效精简型</strong>:消耗少、响应极快,以高流程压缩比为主。<br>• <strong>品质重载型</strong>:注入超长参考知识库(如活人感写作),以高 Token 投入换取媲美专家的成稿质感。<br>• <strong>深度均衡型</strong>:标准复杂度的交互任务。</div>
1413
+ </div>
1414
+
1415
+ <!-- 4. 二次返工纠错率 -->
1416
+ <div class="glossary-card" id="term-corrections">
1417
+ <div class="glossary-title-row">
1418
+ <div class="glossary-term">二次返工纠错率 (Correction Rate)</div>
1419
+ <span class="pill pill-partial">工程质量</span>
1420
+ </div>
1421
+ <div class="glossary-def">技能交付后,用户紧接着在后续对话中提出“重写、写错了、重新来、不对”等负向纠错意图的调用比例。</div>
1422
+ <div class="glossary-formula">纠错率 = (存在重试纠错的调用次数 / 总调用次数) × 100%</div>
1423
+ <div class="glossary-scene"><strong>业务意义</strong>:效能的“水分过滤器”。纠错率高的技能说明规则存在盲区或模型理解偏差,并未真正为开发者省心,需要针对性优化提示词。</div>
1424
+ </div>
1425
+
1426
+ <!-- 5. 上下文产出率 -->
1427
+ <div class="glossary-card" id="term-yield">
1428
+ <div class="glossary-title-row">
1429
+ <div class="glossary-term">上下文产出率 (Yield Ratio)</div>
1430
+ <span class="pill pill-blue">上下文效率</span>
1431
+ </div>
1432
+ <div class="glossary-def">最终生成的有效内容 Token,与前置注入的规约参考文档 Token 之间的比值。</div>
1433
+ <div class="glossary-formula">上下文产出率 = (生成内容 Tokens / 挂载参考文档 Tokens) × 100%</div>
1434
+ <div class="glossary-scene"><strong>业务意义</strong>:监控 Prompt 是否过于冗长膨胀。防止某个技能每次挂载 5 万字参考资料却只生成了 50 字回答(过低的 Yield 意味着严重的算力与延迟浪费)。</div>
1435
+ </div>
1436
+
1437
+ <!-- 6. 工具链调用总深度 -->
1438
+ <div class="glossary-card" id="term-chain">
1439
+ <div class="glossary-title-row">
1440
+ <div class="glossary-term">工具链调用总深度 (Tool Chain Depth)</div>
1441
+ <span class="pill pill-blue">自动化编排</span>
1442
+ </div>
1443
+ <div class="glossary-def">技能在单次任务中,自动驱动底层子工具(如读写文件、代码替换、命令行执行、网络抓取)的连续调用次数。</div>
1444
+ <div class="glossary-formula">总深度 = 累加所有关联 Tool Invocations 次数</div>
1445
+ <div class="glossary-scene"><strong>业务意义</strong>:反映技能作为复杂 Agent 智能体的自闭环行动能力。调用链越深,代表承担的系统自动化工作量越重。</div>
1446
+ </div>
1447
+
1448
+ <!-- 7. 阻断异常 -->
1449
+ <div class="glossary-card" id="term-failures">
1450
+ <div class="glossary-title-row">
1451
+ <div class="glossary-term">阻断异常 (Tool Failures &amp; Blockages)</div>
1452
+ <span class="pill pill-failed">稳定性</span>
1453
+ </div>
1454
+ <div class="glossary-def">技能在自动编排调用工具链时,因权限拒绝(Permission Denied)、命令语法报错或超时导致的异常中断次数。</div>
1455
+ <div class="glossary-formula">阻断异常 = 子工具抛错 / 中断失败总次数</div>
1456
+ <div class="glossary-scene"><strong>业务意义</strong>:直接评估技能在沙箱环境中的跨系统健壮性,排查是否存在跨平台环境 Bug。</div>
1457
+ </div>
1458
+
1459
+ <!-- 8. 活跃技能健康评分 -->
1460
+ <div class="glossary-card" id="term-health">
1461
+ <div class="glossary-title-row">
1462
+ <div class="glossary-term">活跃技能健康评分 (Health Score)</div>
1463
+ <span class="pill pill-purple">资产治理</span>
1464
+ </div>
1465
+ <div class="glossary-def">综合成功率 (40%)、低返工率 (30%)、工具链稳定性 (15%) 与产出比 (15%) 动态加权计算出的 0~100 分综合健康度评级。</div>
1466
+ <div class="glossary-formula">综合健康分 = 100 - (低成功率罚分 × 40) - (纠错率罚分 × 30) - (工具阻断罚分 × 15)</div>
1467
+ <div class="glossary-scene"><strong>评级标准</strong>:A+ (95-100分 顶尖) · A (85-94分 优秀) · B (70-84分 良好) · C (50-69分 需优化) · D (&lt;50分 需重构)。</div>
1468
+ </div>
1469
+ </div>
1470
+ </div>
1471
+ </div>
1472
+ </main>
429
1473
 
430
1474
  <script>
431
1475
  let currentRange = 'all';
1476
+ const activeFilters = { agent: '', skill: '', model: '', status: '' };
1477
+ const FILTER_QUERY_KEYS = { agent: 'agent_name', skill: 'skill_id', model: 'model_name', status: 'status' };
1478
+
1479
+ function buildQueryString() {
1480
+ const params = new URLSearchParams();
1481
+ params.set('time_range', currentRange);
1482
+ Object.keys(FILTER_QUERY_KEYS).forEach(key => {
1483
+ const value = activeFilters[key];
1484
+ if (value) {
1485
+ params.set(FILTER_QUERY_KEYS[key], value);
1486
+ }
1487
+ });
1488
+ return params.toString();
1489
+ }
1490
+
1491
+ function hasActiveFilter() {
1492
+ const anyFilter = Object.keys(activeFilters).some(key => !!activeFilters[key]);
1493
+ return anyFilter || currentRange !== 'all';
1494
+ }
432
1495
 
433
1496
  function switchRange(range, el) {
434
1497
  currentRange = range;
435
1498
  document.querySelectorAll('.segment-btn').forEach(btn => btn.classList.remove('active'));
436
1499
  el.classList.add('active');
1500
+ syncFilterState();
1501
+ fetchData();
1502
+ }
1503
+
1504
+ function onFilterChange(key, el) {
1505
+ activeFilters[key] = el.value || '';
1506
+ syncFilterState();
1507
+ fetchData();
1508
+ }
1509
+
1510
+ function resetFilters() {
1511
+ currentRange = 'all';
1512
+ Object.keys(activeFilters).forEach(key => { activeFilters[key] = ''; });
1513
+ document.querySelectorAll('.segment-btn').forEach(btn => {
1514
+ btn.classList.toggle('active', btn.getAttribute('data-range') === 'all');
1515
+ });
1516
+ document.querySelectorAll('.select').forEach(sel => { sel.value = ''; });
1517
+ syncFilterState();
1518
+ fetchData();
1519
+ }
1520
+
1521
+ function syncFilterState() {
1522
+ document.querySelectorAll('.select').forEach(sel => {
1523
+ sel.classList.toggle('is-active', !!sel.value);
1524
+ });
1525
+ const resetBtn = document.getElementById('btnReset');
1526
+ if (resetBtn) {
1527
+ resetBtn.disabled = !hasActiveFilter();
1528
+ }
1529
+ }
1530
+
1531
+ async function loadFilterOptions() {
1532
+ try {
1533
+ const res = await fetch('/api/filters');
1534
+ const options = await res.json();
1535
+ fillSelect('filterAgent', options.agents || [], '全部 Agent');
1536
+ fillSelect('filterSkill', options.skills || [], '全部 Skill');
1537
+ fillSelect('filterModel', options.models || [], '全部模型');
1538
+ fillSelect('filterStatus', options.statuses || [], '全部状态');
1539
+ syncFilterState();
1540
+ } catch (err) {
1541
+ console.error('Filter options error:', err);
1542
+ }
1543
+ }
1544
+
1545
+ function fillSelect(id, options, placeholder) {
1546
+ const el = document.getElementById(id);
1547
+ if (!el) {
1548
+ return;
1549
+ }
1550
+ const selected = el.value;
1551
+ const items = ['<option value="">' + placeholder + '</option>'];
1552
+ options.forEach(option => {
1553
+ items.push('<option value="' + escapeHtml(option.value) + '">' + escapeHtml(option.label) + '</option>');
1554
+ });
1555
+ el.innerHTML = items.join('');
1556
+ el.value = options.some(option => option.value === selected) ? selected : '';
1557
+ }
1558
+
1559
+ function refreshAll() {
1560
+ loadFilterOptions();
437
1561
  fetchData();
438
1562
  }
439
1563
 
440
1564
  async function fetchData() {
441
1565
  try {
1566
+ const query = buildQueryString();
442
1567
  const [dashRes, invRes] = await Promise.all([
443
- fetch('/api/dashboard?time_range=' + currentRange),
444
- fetch('/api/invocations?limit=30')
1568
+ fetch('/api/dashboard?' + query),
1569
+ fetch('/api/invocations?' + query + '&limit=30')
445
1570
  ]);
446
1571
  const dashData = await dashRes.json();
447
1572
  const invData = await invRes.json();
@@ -449,7 +1574,9 @@ export function renderDashboardHtml() {
449
1574
  renderKpi(dashData.kpi);
450
1575
  renderTopSkills(dashData.skillRows || []);
451
1576
  renderAgentBreakdown(dashData.agentRows || [], dashData.kpi?.totalInvocations || 1);
452
- renderInvocations(invData.invocations || []);
1577
+ renderL2Quality(dashData.kpi);
1578
+ renderL3Governance(dashData.governance);
1579
+ renderInvocations(invData.invocations || [], invData.total);
453
1580
  } catch (err) {
454
1581
  console.error('Fetch error:', err);
455
1582
  }
@@ -460,6 +1587,10 @@ export function renderDashboardHtml() {
460
1587
  document.getElementById('kpiHits').innerText = (kpi.totalInvocations || 0).toLocaleString();
461
1588
  document.getElementById('kpiSuccessRate').innerText = ((kpi.successRate || 1) * 100).toFixed(1) + '%';
462
1589
  document.getElementById('kpiSteps').innerText = '+' + (kpi.totalSavedSteps || 0).toLocaleString() + ' 步';
1590
+ const stepsMetaEl = document.getElementById('kpiStepsMeta');
1591
+ if (stepsMetaEl) {
1592
+ stepsMetaEl.innerText = '消除 ' + (kpi.totalSavedSteps || 0).toLocaleString() + ' 轮人机校准交互';
1593
+ }
463
1594
  document.getElementById('kpiTokens').innerText = '+' + (kpi.totalSavedTokens || 0).toLocaleString();
464
1595
 
465
1596
  const hours = kpi.totalSavedHours || 0;
@@ -473,34 +1604,40 @@ export function renderDashboardHtml() {
473
1604
  function renderTopSkills(skills) {
474
1605
  const container = document.getElementById('topSkillsList');
475
1606
  if (!skills || skills.length === 0) {
476
- container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无技能触发记录</div>';
1607
+ container.innerHTML = '<div class="empty-state">暂无技能触发记录</div>';
477
1608
  return;
478
1609
  }
479
1610
  const maxSteps = Math.max(...skills.map(s => s.saved_steps), 1);
480
- container.innerHTML = skills.slice(0, 5).map((s, idx) => \`
1611
+ container.innerHTML = skills.slice(0, 5).map((s, idx) => {
1612
+ const name = s.skill_name || s.skill_id;
1613
+ const idTag = s.skill_id && s.skill_id !== name
1614
+ ? '<span class="skill-id">(' + escapeHtml(s.skill_id) + ')</span>'
1615
+ : '';
1616
+ const width = Math.min(100, Math.max(6, Math.round(s.saved_steps / maxSteps * 100)));
1617
+ return \`
481
1618
  <div class="bar-row">
482
1619
  <div class="bar-label-group">
483
- <div>
1620
+ <div class="skill-name-cell">
484
1621
  <span class="skill-rank-badge">\${idx + 1}</span>
485
- <span class="skill-title">\${s.skill_name || s.skill_id}</span>
486
- <span style="color: var(--text-tertiary); font-size: 11px; margin-left: 4px;">(\${s.skill_id})</span>
1622
+ <span class="skill-title">\${escapeHtml(name)}</span>\${idTag}
487
1623
  </div>
488
1624
  <div class="skill-stat">
489
1625
  <strong>+\${(s.saved_steps || 0).toLocaleString()} 步</strong>
490
- <span style="color: var(--apple-purple); font-size: 11px; margin-left: 6px;">+\${(s.saved_hours || 0).toFixed(1)}h</span>
1626
+ <span class="skill-hours">+\${(s.saved_hours || 0).toFixed(1)}h</span>
491
1627
  </div>
492
1628
  </div>
493
1629
  <div class="progress-track">
494
- <div class="progress-fill fill-gradient-mint" style="width: \${Math.min(100, Math.max(6, Math.round(s.saved_steps / maxSteps * 100)))}%;"></div>
1630
+ <div class="progress-fill fill-gradient-mint" style="width: \${width}%;"></div>
495
1631
  </div>
496
1632
  </div>
497
- \`).join('');
1633
+ \`;
1634
+ }).join('');
498
1635
  }
499
1636
 
500
1637
  function renderAgentBreakdown(agents, totalHits) {
501
1638
  const container = document.getElementById('agentBreakdownList');
502
1639
  if (!agents || agents.length === 0) {
503
- container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无 Agent 接入数据</div>';
1640
+ container.innerHTML = '<div class="empty-state">暂无 Agent 接入数据</div>';
504
1641
  return;
505
1642
  }
506
1643
  container.innerHTML = agents.map(a => {
@@ -508,8 +1645,8 @@ export function renderDashboardHtml() {
508
1645
  return \`
509
1646
  <div class="bar-row">
510
1647
  <div class="bar-label-group">
511
- <span class="pill pill-agent">\${a.agent_name}</span>
512
- <span class="skill-stat">\${a.hit_cnt} 次 (\${pct}%) | <span style="color: var(--apple-blue);">+\${(a.saved_tokens || 0).toLocaleString()} Tokens</span></span>
1648
+ <span class="pill pill-agent">\${escapeHtml(a.agent_name)}</span>
1649
+ <span class="skill-stat">\${a.hit_cnt} 次 · \${pct}%<span class="skill-hours" style="margin-left:10px;">+\${(a.saved_tokens || 0).toLocaleString()} Tokens</span></span>
513
1650
  </div>
514
1651
  <div class="progress-track">
515
1652
  <div class="progress-fill fill-gradient-blue" style="width: \${Math.min(100, Math.max(6, pct))}%;"></div>
@@ -529,10 +1666,138 @@ export function renderDashboardHtml() {
529
1666
  .replace(/'/g, '&#39;');
530
1667
  }
531
1668
 
532
- function renderInvocations(items) {
1669
+ function renderL2Quality(kpi) {
1670
+ const container = document.getElementById('l2QualityList');
1671
+ if (!container) return;
1672
+ if (!kpi || !kpi.totalInvocations) {
1673
+ container.innerHTML = '<div class="empty-state">暂无工程指标数据</div>';
1674
+ return;
1675
+ }
1676
+
1677
+ const explicitPct = Number(((kpi.explicitRate || 1) * 100).toFixed(0));
1678
+ const implicitPct = Math.max(0, 100 - explicitPct);
1679
+ const correctionPct = ((kpi.correctionRate || 0) * 100).toFixed(1);
1680
+ const yieldPct = (kpi.yieldRatio || 100).toFixed(1);
1681
+ const toolDepth = kpi.totalToolDepth || 0;
1682
+ const toolFailures = kpi.totalToolFailures || 0;
1683
+ const correctionWarn = Number(correctionPct) > 10;
1684
+
1685
+ let html = '<div class="panel-body">';
1686
+
1687
+ /* 意图纯度:显式 / 隐式 两段式比例条 */
1688
+ html += '<div>';
1689
+ html += '<div class="subhead"><span>意图触发纯度</span>';
1690
+ html += '<span class="subhead-note">显式 <strong>' + explicitPct + '%</strong> · 隐式 ' + implicitPct + '%</span></div>';
1691
+ html += '<div class="split-bar">';
1692
+ html += '<div class="split-bar-seg split-bar-explicit" style="width: ' + explicitPct + '%;"></div>';
1693
+ html += '<div class="split-bar-seg split-bar-implicit" style="width: ' + implicitPct + '%;"></div>';
1694
+ html += '</div></div>';
1695
+
1696
+ /* 两个核心质量指标 */
1697
+ html += '<div class="stat-grid">';
1698
+
1699
+ html += '<div class="mini-stat">';
1700
+ html += '<div class="mini-stat-label">二次返工纠错率';
1701
+ html += '<button type="button" class="info-help" data-glossary="term-corrections" title="后续轮次出现重写/修改提示的比例。点击查看白皮书">?</button>';
1702
+ html += '</div>';
1703
+ html += '<div class="mini-stat-value' + (correctionWarn ? ' is-warn' : '') + '">' + correctionPct + '%</div>';
1704
+ html += '<div class="mini-stat-note">' + (kpi.totalCorrections || 0) + ' 次重试纠错</div>';
1705
+ html += '</div>';
1706
+
1707
+ html += '<div class="mini-stat">';
1708
+ html += '<div class="mini-stat-label">上下文产出率';
1709
+ html += '<button type="button" class="info-help" data-glossary="term-yield" title="有效输出 Token / 规约文档 Token 比率。点击查看白皮书">?</button>';
1710
+ html += '</div>';
1711
+ html += '<div class="mini-stat-value">' + yieldPct + '%</div>';
1712
+ html += '<div class="mini-stat-note">规约投入 ' + (kpi.totalContextTokens || 0).toLocaleString() + ' Tokens</div>';
1713
+ html += '</div>';
1714
+
1715
+ html += '</div>';
1716
+
1717
+ /* 工具链健康度 */
1718
+ html += '<div class="inline-stats">';
1719
+ html += '<div>工具链深度 <strong>' + toolDepth + ' 次</strong>';
1720
+ html += '<button type="button" class="info-help" data-glossary="term-chain" title="技能驱动底层子工具的连续调用次数。点击查看白皮书">?</button></div>';
1721
+ html += '<div>阻断异常 <strong' + (toolFailures > 0 ? ' class="is-danger"' : '') + '>' + toolFailures + ' 次</strong>';
1722
+ html += '<button type="button" class="info-help" data-glossary="term-failures" title="子工具调用被拒绝或报错阻断的次数。点击查看白皮书">?</button></div>';
1723
+ html += '</div>';
1724
+
1725
+ html += '</div>';
1726
+ container.innerHTML = html;
1727
+ }
1728
+
1729
+ function renderL3Governance(gov) {
1730
+ const container = document.getElementById('l3GovernanceList');
1731
+ if (!container) return;
1732
+ if (!gov) {
1733
+ container.innerHTML = '<div class="empty-state">暂无治理指标数据</div>';
1734
+ return;
1735
+ }
1736
+
1737
+ const leaderboard = gov.healthLeaderboard || [];
1738
+ const dormants = gov.dormantSkills || [];
1739
+ const modifiers = gov.followUpModifiers || [];
1740
+
1741
+ let html = '<div class="panel-body">';
1742
+
1743
+ if (leaderboard.length > 0) {
1744
+ html += '<div>';
1745
+ html += '<div class="subhead"><span>活跃技能健康评分';
1746
+ html += '<button type="button" class="info-help" data-glossary="term-health" title="综合成功率、低返工率、工具链稳定性打出的 0-100 综合分。点击查看白皮书">?</button>';
1747
+ html += '</span><span class="subhead-note">满分 100</span></div>';
1748
+ html += '<div class="health-list">';
1749
+ leaderboard.slice(0, 3).forEach(item => {
1750
+ const badgeClass = item.grade === 'A+' || item.grade === 'A' ? 'pill-success' : item.grade === 'B' ? 'pill-blue' : 'pill-partial';
1751
+ const idTag = item.skill_id && item.skill_id !== item.skill_name
1752
+ ? '<span class="health-id">(' + escapeHtml(item.skill_id) + ')</span>'
1753
+ : '';
1754
+ html += '<div class="health-row">';
1755
+ html += '<span class="health-name">' + escapeHtml(item.skill_name) + idTag + '</span>';
1756
+ html += '<span class="pill grade-pill ' + badgeClass + '">' + escapeHtml(item.grade) + ' · ' + item.score + ' 分</span>';
1757
+ html += '</div>';
1758
+ });
1759
+ html += '</div></div>';
1760
+ }
1761
+
1762
+ if (dormants.length > 0) {
1763
+ const names = dormants.slice(0, 3).map(d => escapeHtml(d.skill_name || d.skill_id)).join('、');
1764
+ html += '<div class="notice">';
1765
+ html += '<div class="notice-title"><span class="dot dot-warn"></span>僵尸技能待治理';
1766
+ html += '<span class="notice-badge">' + dormants.length + '</span></div>';
1767
+ html += '<div class="notice-body">' + names + (dormants.length > 3 ? ' 等' : '') + ',已超 14 天未调起,建议归档或优化触发词。</div>';
1768
+ html += '</div>';
1769
+ }
1770
+
1771
+ if (modifiers.length > 0) {
1772
+ html += '<div class="notice">';
1773
+ html += '<div class="notice-title"><span class="dot dot-info"></span>用户高频追加诉求反哺</div>';
1774
+ modifiers.slice(0, 2).forEach(m => {
1775
+ html += '<div class="notice-item"><strong>' + escapeHtml(m.category) + '</strong>';
1776
+ html += '<span class="notice-count">· ' + m.count + ' 次</span><br>' + escapeHtml(m.recommendation) + '</div>';
1777
+ });
1778
+ html += '</div>';
1779
+ }
1780
+
1781
+ html += '</div>';
1782
+ container.innerHTML = html;
1783
+ }
1784
+
1785
+ function renderInvocations(items, total) {
533
1786
  const tbody = document.getElementById('invocationsBody');
1787
+ const countEl = document.getElementById('tableCount');
1788
+ const filtered = hasActiveFilter();
1789
+
1790
+ if (countEl) {
1791
+ if (!items || items.length === 0) {
1792
+ countEl.innerText = filtered ? '当前筛选条件下无记录' : '展示最新调用流水';
1793
+ } else {
1794
+ countEl.innerText = '共 ' + (total || items.length).toLocaleString() + ' 条 · 展示最新 ' + items.length + ' 条';
1795
+ }
1796
+ }
1797
+
534
1798
  if (!items || items.length === 0) {
535
- tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 32px 0;">暂无近期 Skill 调用流水</td></tr>';
1799
+ const emptyText = filtered ? '当前筛选条件下没有匹配的调用记录' : '暂无近期 Skill 调用流水';
1800
+ tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; color: var(--text-secondary); padding: 32px 0;">' + emptyText + '</td></tr>';
536
1801
  return;
537
1802
  }
538
1803
  tbody.innerHTML = items.map(inv => {
@@ -555,34 +1820,95 @@ export function renderDashboardHtml() {
555
1820
  const profileBadge = inv.quality_profile === 'heavy_investment'
556
1821
  ? '<span class="pill pill-purple">品质重载</span>'
557
1822
  : inv.quality_profile === 'efficient'
558
- ? '<span class="pill pill-success">高效精简</span>'
559
- : '<span class="pill pill-blue">深度均衡</span>';
1823
+ ? '<span class="pill pill-blue">高效精简</span>'
1824
+ : '<span class="pill pill-neutral">深度均衡</span>';
560
1825
 
561
1826
  const tokenCell = (inv.net_tokens || 0) > 0
562
- ? \`<span style="color: var(--apple-green); font-weight: 600;">+\${(inv.net_tokens || 0).toLocaleString()} Tokens</span>\`
563
- : \`<span style="color: var(--apple-purple); font-size: 11px;">投入 \${(inv.actual_tokens || 0).toLocaleString()} Tokens</span>\`;
1827
+ ? \`<span class="cell-gain">+\${(inv.net_tokens || 0).toLocaleString()} Tokens</span>\`
1828
+ : \`<span class="cell-muted">投入 \${(inv.actual_tokens || 0).toLocaleString()} Tokens</span>\`;
564
1829
 
565
1830
  const safeSession = escapeHtml(inv.session_id);
566
1831
  const safeAgent = escapeHtml(inv.agent_name);
567
1832
  const safeSkill = escapeHtml(inv.skill_name || inv.skill_id);
568
1833
 
1834
+ const bSteps = inv.baseline_steps || (inv.saved_steps + (inv.actual_steps || 1));
1835
+ const aSteps = inv.actual_steps || 1;
1836
+ const formulaTip = '计算推演: 无该技能预估需 ' + bSteps + ' 步交互 - 实际执行 ' + aSteps + ' 步 = 精简 ' + inv.saved_steps + ' 步';
1837
+
569
1838
  return \`
570
1839
  <tr>
571
- <td style="color: var(--text-secondary); font-size: 12px;">\${dateStr}</td>
572
- <td style="font-family: ui-monospace, monospace; font-size: 11px; color: var(--text-secondary);">\${safeSession.substring(0, 16)}...</td>
1840
+ <td class="cell-muted">\${dateStr}</td>
1841
+ <td class="cell-session">\${safeSession.substring(0, 16)}...</td>
573
1842
  <td><span class="pill pill-agent">\${safeAgent}</span></td>
574
1843
  <td>\${modelBadge}</td>
575
1844
  <td><strong>\${safeSkill}</strong></td>
576
1845
  <td>\${profileBadge}</td>
577
1846
  <td>\${statusPill}</td>
578
- <td style="color: var(--apple-purple); font-weight: 500;">\${hoursText}</td>
579
- <td style="color: var(--apple-green); font-weight: 600;">+\${inv.saved_steps} 步</td>
1847
+ <td class="cell-hours">\${hoursText}</td>
1848
+ <td title="\${formulaTip}">
1849
+ <div class="cell-steps">+\${inv.saved_steps} 步</div>
1850
+ <div class="cell-steps-note">基准 \${bSteps} → 实际 \${aSteps}</div>
1851
+ </td>
580
1852
  <td>\${tokenCell}</td>
581
1853
  </tr>
582
1854
  \`;
583
1855
  }).join('');
584
1856
  }
585
1857
 
1858
+ function openGlossary(termId) {
1859
+ const modal = document.getElementById('glossaryModal');
1860
+ if (!modal) return;
1861
+ modal.classList.add('open');
1862
+ document.body.style.overflow = 'hidden';
1863
+
1864
+ if (termId) {
1865
+ setTimeout(function() {
1866
+ const card = document.getElementById(termId);
1867
+ if (card) {
1868
+ card.scrollIntoView({ behavior: 'smooth', block: 'center' });
1869
+ card.classList.add('highlight');
1870
+ setTimeout(function() { card.classList.remove('highlight'); }, 2200);
1871
+ }
1872
+ }, 120);
1873
+ }
1874
+ }
1875
+
1876
+ function closeGlossary() {
1877
+ const modal = document.getElementById('glossaryModal');
1878
+ if (!modal) return;
1879
+ modal.classList.remove('open');
1880
+ document.body.style.overflow = '';
1881
+ }
1882
+
1883
+ function onModalBackdropClick(e) {
1884
+ if (e.target && e.target.id === 'glossaryModal') {
1885
+ closeGlossary();
1886
+ }
1887
+ }
1888
+
1889
+ function filterGlossary(q) {
1890
+ const query = (q || '').trim().toLowerCase();
1891
+ const cards = document.querySelectorAll('.glossary-card');
1892
+ cards.forEach(function(card) {
1893
+ const text = card.innerText.toLowerCase();
1894
+ card.style.display = (!query || text.includes(query)) ? 'block' : 'none';
1895
+ });
1896
+ }
1897
+
1898
+ window.addEventListener('keydown', function(e) {
1899
+ if (e.key === 'Escape') closeGlossary();
1900
+ });
1901
+
1902
+ document.addEventListener('click', function(e) {
1903
+ const btn = e.target && e.target.closest ? e.target.closest('[data-glossary]') : null;
1904
+ if (btn) {
1905
+ e.preventDefault();
1906
+ const termId = btn.getAttribute('data-glossary');
1907
+ openGlossary(termId);
1908
+ }
1909
+ });
1910
+
1911
+ loadFilterOptions();
586
1912
  fetchData();
587
1913
  setInterval(fetchData, 5000);
588
1914
  </script>