@mengzf/skillflow-mcp 1.0.0 → 1.0.2

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 (74) 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 +8 -0
  11. package/dist/adapters/dsh.adapter.d.ts.map +1 -0
  12. package/dist/adapters/dsh.adapter.js +75 -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.d.ts.map +1 -1
  66. package/dist/utils/pricing.js +21 -1
  67. package/dist/utils/pricing.js.map +1 -1
  68. package/dist/web/render.d.ts.map +1 -1
  69. package/dist/web/render.js +1751 -241
  70. package/dist/web/render.js.map +1 -1
  71. package/dist/web/server.d.ts.map +1 -1
  72. package/dist/web/server.js +66 -31
  73. package/dist/web/server.js.map +1 -1
  74. package/package.json +1 -1
@@ -4,336 +4,1331 @@ 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;
221
+ }
222
+
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;
127
273
  }
128
- .btn-refresh:hover { background: rgba(255, 255, 255, 0.14); transform: translateY(-1px); }
129
- .btn-refresh svg { width: 13px; height: 13px; fill: currentColor; }
274
+ .filter-field--range .segmented-control { align-items: center; height: 36px; }
130
275
 
131
- /* KPI Row */
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; }
407
+
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; }
176
411
 
177
- /* Content Grid */
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); }
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
+ }
253
612
 
613
+ /* ---------- Pagination ---------- */
614
+ .pagination-bar {
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: space-between;
618
+ padding: 14px 24px;
619
+ border-top: 1px solid var(--border);
620
+ background: var(--surface);
621
+ font-size: 13px;
622
+ color: var(--text-secondary);
623
+ flex-wrap: wrap;
624
+ gap: 14px;
625
+ }
626
+ .pagination-left {
627
+ display: inline-flex;
628
+ align-items: center;
629
+ gap: 16px;
630
+ flex-wrap: wrap;
631
+ }
632
+ .pagination-size {
633
+ display: inline-flex;
634
+ align-items: center;
635
+ gap: 6px;
636
+ font-size: 12.5px;
637
+ color: var(--text-tertiary);
638
+ }
639
+ .pagination-size-label {
640
+ font-size: 12px;
641
+ color: var(--text-tertiary);
642
+ }
643
+ .segmented-control--sm {
644
+ padding: 2px;
645
+ gap: 1px;
646
+ }
647
+ .segmented-control--sm .segment-btn {
648
+ font-size: 11.5px;
649
+ padding: 3px 10px;
650
+ font-variant-numeric: tabular-nums;
651
+ }
652
+ .pagination-info {
653
+ font-size: 12.5px;
654
+ color: var(--text-tertiary);
655
+ font-variant-numeric: tabular-nums;
656
+ }
657
+ .pagination-controls {
658
+ display: inline-flex;
659
+ align-items: center;
660
+ gap: 8px;
661
+ }
662
+ .page-btn {
663
+ display: inline-flex;
664
+ align-items: center;
665
+ gap: 5px;
666
+ appearance: none;
667
+ background: var(--surface);
668
+ border: 1px solid var(--border-highlight);
669
+ color: var(--text-primary);
670
+ font: inherit;
671
+ font-size: 12.5px;
672
+ font-weight: 500;
673
+ padding: 6px 14px;
674
+ border-radius: var(--radius-full);
675
+ cursor: pointer;
676
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
677
+ transition: background-color 0.15s var(--ease), border-color 0.15s var(--ease), opacity 0.15s var(--ease);
678
+ }
679
+ .page-btn:hover:not(:disabled) {
680
+ background: var(--surface-sunken);
681
+ border-color: var(--hairline);
682
+ }
683
+ .page-btn:disabled {
684
+ opacity: 0.35;
685
+ cursor: not-allowed;
686
+ box-shadow: none;
687
+ }
688
+ .page-btn svg {
689
+ width: 13px;
690
+ height: 13px;
691
+ fill: none;
692
+ stroke: currentColor;
693
+ stroke-width: 2;
694
+ stroke-linecap: round;
695
+ stroke-linejoin: round;
696
+ }
697
+ .page-indicator {
698
+ font-size: 12.5px;
699
+ font-weight: 500;
700
+ color: var(--text-secondary);
701
+ font-variant-numeric: tabular-nums;
702
+ padding: 0 6px;
703
+ }
704
+
705
+ /* ---------- Pills ---------- */
254
706
  .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;
707
+ display: inline-flex;
708
+ align-items: center;
709
+ gap: 5px;
710
+ padding: 2px 8px;
711
+ border-radius: 6px;
712
+ font-size: 11.5px;
713
+ font-weight: 500;
714
+ line-height: 1.6;
715
+ letter-spacing: -0.002em;
716
+ white-space: nowrap;
258
717
  }
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);
718
+ .pill-agent { background: rgba(0, 113, 227, 0.09); color: var(--apple-blue); }
719
+ .pill-neutral {
720
+ background: rgba(120, 120, 128, 0.1);
721
+ color: #48484a;
262
722
  }
263
723
  .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;
724
+ background: rgba(120, 120, 128, 0.12);
725
+ color: #48484a;
726
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
727
+ font-size: 11px;
728
+ }
729
+ .pill-purple { background: rgba(125, 60, 168, 0.09); color: var(--apple-purple); }
730
+ .pill-blue { background: rgba(0, 113, 227, 0.09); color: var(--apple-blue); }
731
+ .pill-success { background: rgba(26, 127, 55, 0.09); color: var(--apple-green); }
732
+ .pill-partial { background: rgba(163, 82, 0, 0.09); color: var(--apple-orange); }
733
+ .pill-failed { background: rgba(215, 0, 21, 0.09); color: var(--apple-red); }
734
+
735
+ /* ---------- Info Help Indicator ---------- */
736
+ .info-help {
737
+ display: inline-flex;
738
+ align-items: center;
739
+ justify-content: center;
740
+ width: 15px;
741
+ height: 15px;
742
+ border-radius: 50%;
743
+ font-size: 10px;
744
+ font-weight: 600;
745
+ line-height: 1;
746
+ color: var(--text-tertiary);
747
+ background: rgba(120, 120, 128, 0.12);
748
+ border: 1px solid rgba(0, 0, 0, 0.08);
749
+ cursor: pointer;
750
+ margin-left: 5px;
751
+ vertical-align: middle;
752
+ transition: all 0.2s var(--ease);
753
+ user-select: none;
754
+ flex: none;
755
+ }
756
+ .info-help:hover {
757
+ background: var(--blue-fill);
758
+ color: #ffffff;
759
+ border-color: var(--blue-fill);
760
+ transform: scale(1.15);
761
+ box-shadow: 0 2px 6px var(--apple-blue-glow);
762
+ }
763
+ /* Inside dense headers the marker should read as a quiet annotation */
764
+ th .info-help,
765
+ .mini-stat-label .info-help,
766
+ .subhead .info-help,
767
+ .inline-stats .info-help {
768
+ width: 14px;
769
+ height: 14px;
770
+ font-size: 9.5px;
771
+ background: transparent;
772
+ border-color: rgba(0, 0, 0, 0.16);
773
+ color: var(--text-tertiary);
774
+ margin-left: 3px;
775
+ transform: none;
776
+ }
777
+ th .info-help:hover,
778
+ .mini-stat-label .info-help:hover,
779
+ .subhead .info-help:hover,
780
+ .inline-stats .info-help:hover {
781
+ background: var(--blue-fill);
782
+ border-color: var(--blue-fill);
783
+ color: #fff;
784
+ }
785
+
786
+ /* ---------- Modal & Glossary Whitepaper ---------- */
787
+ .modal-overlay {
788
+ position: fixed;
789
+ inset: 0;
790
+ z-index: 100;
791
+ background: rgba(0, 0, 0, 0.4);
792
+ backdrop-filter: blur(12px);
793
+ -webkit-backdrop-filter: blur(12px);
794
+ display: flex;
795
+ align-items: center;
796
+ justify-content: center;
797
+ padding: 24px;
798
+ opacity: 0;
799
+ pointer-events: none;
800
+ transition: opacity 0.25s var(--ease);
801
+ }
802
+ .modal-overlay.open {
803
+ opacity: 1;
804
+ pointer-events: auto;
805
+ }
806
+ .modal-sheet {
807
+ background: var(--surface);
808
+ border: 1px solid var(--border-highlight);
809
+ border-radius: var(--radius-lg);
810
+ box-shadow: 0 24px 64px -16px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.06);
811
+ width: 100%;
812
+ max-width: 860px;
813
+ max-height: 85vh;
814
+ display: flex;
815
+ flex-direction: column;
816
+ overflow: hidden;
817
+ transform: scale(0.96) translateY(12px);
818
+ transition: transform 0.25s var(--ease);
819
+ }
820
+ .modal-overlay.open .modal-sheet {
821
+ transform: scale(1) translateY(0);
822
+ }
823
+ .modal-header {
824
+ display: flex;
825
+ align-items: center;
826
+ justify-content: space-between;
827
+ padding: 20px 28px;
828
+ border-bottom: 1px solid var(--border);
829
+ background: var(--surface);
830
+ gap: 16px;
831
+ }
832
+ .modal-title-group h2 {
833
+ font-size: 18px;
834
+ font-weight: 600;
835
+ letter-spacing: -0.016em;
836
+ color: var(--text-primary);
837
+ }
838
+ .modal-title-group p {
839
+ font-size: 13px;
840
+ color: var(--text-secondary);
841
+ margin-top: 2px;
842
+ }
843
+ .modal-close {
844
+ appearance: none;
845
+ border: none;
846
+ background: rgba(120, 120, 128, 0.12);
847
+ width: 30px;
848
+ height: 30px;
849
+ border-radius: 50%;
850
+ display: grid;
851
+ place-items: center;
852
+ font-size: 14px;
853
+ color: var(--text-secondary);
854
+ cursor: pointer;
855
+ transition: all 0.15s ease;
856
+ flex: none;
857
+ }
858
+ .modal-close:hover {
859
+ background: rgba(120, 120, 128, 0.22);
860
+ color: var(--text-primary);
861
+ }
862
+ .modal-search {
863
+ padding: 12px 28px;
864
+ background: var(--surface);
865
+ border-bottom: 1px solid var(--border);
866
+ }
867
+ .glossary-search {
868
+ width: 100%;
869
+ height: 36px;
870
+ padding: 0 14px;
871
+ font: inherit;
872
+ font-size: 13px;
873
+ color: var(--text-primary);
874
+ background: var(--surface-sunken);
875
+ border: 1px solid var(--border-highlight);
876
+ border-radius: 10px;
877
+ outline: none;
878
+ transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease), background-color 0.2s var(--ease);
879
+ }
880
+ .glossary-search::placeholder { color: var(--text-tertiary); }
881
+ .glossary-search:focus {
882
+ background: var(--surface);
883
+ border-color: var(--blue-fill);
884
+ box-shadow: 0 0 0 3px var(--apple-blue-glow);
885
+ }
886
+
887
+ .modal-body {
888
+ padding: 20px 28px 28px;
889
+ overflow-y: auto;
890
+ display: flex;
891
+ flex-direction: column;
892
+ gap: 12px;
893
+ background: var(--bg);
894
+ }
895
+ .glossary-card {
896
+ background: var(--surface);
897
+ border: 1px solid var(--border);
898
+ border-radius: 14px;
899
+ padding: 18px 20px;
900
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
901
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
902
+ }
903
+ .glossary-card.highlight {
904
+ border-color: var(--blue-fill);
905
+ background: rgba(0, 113, 227, 0.04);
906
+ }
907
+ .glossary-title-row {
908
+ display: flex;
909
+ align-items: center;
910
+ justify-content: space-between;
911
+ margin-bottom: 8px;
912
+ }
913
+ .glossary-term {
914
+ font-size: 15px;
915
+ font-weight: 600;
916
+ color: var(--text-primary);
917
+ display: flex;
918
+ align-items: center;
919
+ gap: 8px;
920
+ }
921
+ .glossary-def {
922
+ font-size: 13.5px;
923
+ line-height: 1.55;
924
+ color: var(--text-secondary);
925
+ margin-bottom: 8px;
926
+ }
927
+ .glossary-formula {
928
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
929
+ font-size: 12px;
930
+ line-height: 1.5;
931
+ background: rgba(0, 113, 227, 0.05);
932
+ border-left: 3px solid var(--blue-fill);
933
+ padding: 7px 12px;
934
+ border-radius: 0 6px 6px 0;
935
+ color: var(--text-primary);
936
+ margin: 9px 0;
937
+ }
938
+ .glossary-scene {
939
+ font-size: 12.5px;
940
+ line-height: 1.5;
941
+ color: var(--text-tertiary);
942
+ }
943
+
944
+ /* ---------- Panel body primitives ---------- */
945
+ .dashboard-grid > .card {
946
+ display: flex;
947
+ flex-direction: column;
948
+ }
949
+ #l2QualityList,
950
+ #l3GovernanceList {
951
+ display: flex;
952
+ flex-direction: column;
953
+ flex: 1;
954
+ }
955
+ .panel-body {
956
+ display: flex;
957
+ flex-direction: column;
958
+ gap: 14px;
959
+ flex: 1;
960
+ }
961
+
962
+ /* Small section heading inside a panel */
963
+ .subhead {
964
+ display: flex;
965
+ align-items: center;
966
+ justify-content: space-between;
967
+ gap: 10px;
968
+ font-size: 12px;
969
+ font-weight: 600;
970
+ letter-spacing: -0.002em;
971
+ color: var(--text-secondary);
972
+ margin-bottom: 8px;
973
+ }
974
+ .subhead > span:first-child { display: inline-flex; align-items: center; }
975
+ .subhead-note {
976
+ font-size: 11.5px;
977
+ font-weight: 400;
978
+ color: var(--text-tertiary);
979
+ white-space: nowrap;
980
+ font-variant-numeric: tabular-nums;
981
+ }
982
+ .subhead-note strong { color: var(--apple-blue); font-weight: 600; }
983
+
984
+ /* Two-segment ratio bar */
985
+ .split-bar {
986
+ display: flex;
987
+ gap: 3px;
988
+ height: 6px;
989
+ border-radius: var(--radius-full);
990
+ overflow: hidden;
991
+ }
992
+ .split-bar-seg { height: 100%; border-radius: var(--radius-full); }
993
+ .split-bar-explicit { background: linear-gradient(90deg, #0071e3, #0a84ff); }
994
+ .split-bar-implicit { background: rgba(120, 120, 128, 0.22); }
995
+
996
+ /* Compact stat tile */
997
+ .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
998
+ .mini-stat {
999
+ background: var(--bg);
1000
+ border: 1px solid transparent;
1001
+ border-radius: var(--radius-md);
1002
+ padding: 12px 14px 13px;
1003
+ min-width: 0;
1004
+ }
1005
+ .mini-stat-label {
1006
+ display: flex;
1007
+ align-items: center;
1008
+ gap: 3px;
1009
+ font-size: 12px;
1010
+ letter-spacing: -0.002em;
1011
+ color: var(--text-secondary);
1012
+ margin-bottom: 5px;
1013
+ white-space: nowrap;
1014
+ }
1015
+ .mini-stat-value {
1016
+ font-size: 21px;
1017
+ font-weight: 600;
1018
+ line-height: 1.15;
1019
+ letter-spacing: -0.022em;
1020
+ color: var(--text-primary);
1021
+ font-variant-numeric: tabular-nums;
1022
+ }
1023
+ .mini-stat-note {
1024
+ font-size: 11.5px;
1025
+ line-height: 1.4;
1026
+ color: var(--text-tertiary);
1027
+ margin-top: 3px;
1028
+ }
1029
+ .is-warn { color: var(--apple-orange); }
1030
+
1031
+ /* Bottom inline stats row (divided, no heavy rules) */
1032
+ .inline-stats {
1033
+ display: grid;
1034
+ grid-template-columns: 1fr 1fr;
1035
+ margin-top: auto;
1036
+ padding-top: 12px;
1037
+ border-top: 1px solid var(--border);
1038
+ font-size: 12px;
1039
+ color: var(--text-tertiary);
1040
+ }
1041
+ .inline-stats > div {
1042
+ display: flex;
1043
+ align-items: center;
1044
+ gap: 5px;
1045
+ min-width: 0;
267
1046
  }
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);
1047
+ .inline-stats > div + div {
1048
+ padding-left: 14px;
1049
+ border-left: 1px solid var(--border);
271
1050
  }
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);
1051
+ .inline-stats strong {
1052
+ color: var(--text-primary);
1053
+ font-weight: 600;
1054
+ font-variant-numeric: tabular-nums;
275
1055
  }
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);
1056
+ .inline-stats .is-danger { color: var(--apple-red); }
1057
+
1058
+ /* Health score list */
1059
+ .health-list {
1060
+ display: flex;
1061
+ flex-direction: column;
1062
+ gap: 2px;
1063
+ margin: 0 -8px;
1064
+ }
1065
+ .health-row {
1066
+ display: flex;
1067
+ align-items: center;
1068
+ justify-content: space-between;
1069
+ gap: 10px;
1070
+ padding: 6px 8px;
1071
+ border-radius: 8px;
1072
+ font-size: 12.5px;
1073
+ transition: background-color 0.15s var(--ease);
1074
+ }
1075
+ .health-row:hover { background: var(--surface-hover); }
1076
+ .health-name {
1077
+ font-weight: 600;
1078
+ color: var(--text-primary);
1079
+ letter-spacing: -0.004em;
1080
+ min-width: 0;
1081
+ overflow: hidden;
1082
+ text-overflow: ellipsis;
1083
+ white-space: nowrap;
279
1084
  }
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);
1085
+ .health-id {
1086
+ font-size: 11px;
1087
+ font-weight: 400;
1088
+ color: var(--text-tertiary);
1089
+ margin-left: 6px;
283
1090
  }
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);
1091
+ .grade-pill {
1092
+ font-size: 10.5px;
1093
+ padding: 1px 7px;
1094
+ font-variant-numeric: tabular-nums;
1095
+ flex: none;
287
1096
  }
288
1097
 
1098
+ /* Advisory blocks (dormant skills, follow-up feedback) */
1099
+ .notice {
1100
+ padding-top: 12px;
1101
+ border-top: 1px solid var(--border);
1102
+ }
1103
+ .notice-title {
1104
+ display: flex;
1105
+ align-items: center;
1106
+ gap: 6px;
1107
+ font-size: 12px;
1108
+ font-weight: 600;
1109
+ letter-spacing: -0.002em;
1110
+ color: var(--text-secondary);
1111
+ margin-bottom: 5px;
1112
+ }
1113
+ .notice-badge {
1114
+ display: inline-flex;
1115
+ align-items: center;
1116
+ justify-content: center;
1117
+ min-width: 18px;
1118
+ height: 17px;
1119
+ padding: 0 6px;
1120
+ border-radius: var(--radius-full);
1121
+ background: rgba(163, 82, 0, 0.1);
1122
+ color: var(--apple-orange);
1123
+ font-size: 10.5px;
1124
+ font-weight: 600;
1125
+ font-variant-numeric: tabular-nums;
1126
+ }
1127
+ .notice-body {
1128
+ font-size: 12px;
1129
+ line-height: 1.55;
1130
+ color: var(--text-tertiary);
1131
+ }
1132
+ .notice-item {
1133
+ font-size: 12px;
1134
+ line-height: 1.5;
1135
+ color: var(--text-tertiary);
1136
+ margin-top: 5px;
1137
+ }
1138
+ .notice-item strong { color: var(--text-primary); font-weight: 600; }
1139
+ .notice-count {
1140
+ font-size: 11px;
1141
+ color: var(--text-tertiary);
1142
+ font-variant-numeric: tabular-nums;
1143
+ }
1144
+ .dot {
1145
+ width: 6px;
1146
+ height: 6px;
1147
+ border-radius: 50%;
1148
+ flex: none;
1149
+ }
1150
+ .dot-warn { background: var(--apple-orange); }
1151
+ .dot-info { background: var(--apple-purple); }
1152
+
1153
+ .empty-state {
1154
+ padding: 30px 0;
1155
+ text-align: center;
1156
+ font-size: 13px;
1157
+ color: var(--text-tertiary);
1158
+ }
1159
+
1160
+ /* ---------- Footer ---------- */
289
1161
  footer {
290
- text-align: center; margin-top: 48px;
291
- font-size: 12px; color: var(--text-tertiary);
1162
+ margin-top: 44px;
1163
+ padding-top: 26px;
1164
+ border-top: 1px solid var(--border);
1165
+ text-align: center;
1166
+ font-size: 12px;
1167
+ line-height: 1.9;
1168
+ letter-spacing: -0.004em;
1169
+ color: var(--text-tertiary);
1170
+ }
1171
+
1172
+ /* ---------- Responsive ---------- */
1173
+ @media (max-width: 1024px) {
1174
+ .kpi-row { grid-template-columns: repeat(2, 1fr); }
1175
+ }
1176
+ @media (max-width: 860px) {
1177
+ .dashboard-grid { grid-template-columns: 1fr; }
1178
+ }
1179
+ /* Filter bar collapses to a two-column grid before the selects would wrap awkwardly. */
1180
+ @media (max-width: 960px) {
1181
+ .filters {
1182
+ display: grid;
1183
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1184
+ gap: 12px;
1185
+ }
1186
+ .filter-field--range { grid-column: 1 / -1; }
1187
+ .filter-field--range .segmented-control { width: 100%; }
1188
+ .filters .segment-btn { flex: 1; }
1189
+ .select { width: 100%; max-width: none; min-width: 0; }
1190
+ .filter-actions { grid-column: 1 / -1; margin-left: 0; }
1191
+ .btn-reset { width: 100%; }
1192
+ }
1193
+ @media (max-width: 900px) {
1194
+ /* Subtitle falls to its own line — drop the inline divider */
1195
+ .page-head { flex-direction: column; gap: 3px; }
1196
+ .page-desc { padding-left: 0; border-left: 0; }
1197
+ }
1198
+ @media (max-width: 760px) {
1199
+ .nav-inner {
1200
+ flex-wrap: wrap;
1201
+ gap: 10px;
1202
+ padding: 10px 20px 12px;
1203
+ }
1204
+ .nav-actions { width: 100%; justify-content: flex-end; }
1205
+ .container { padding: 20px 20px 56px; }
1206
+ .page-head h1 { font-size: 21px; }
1207
+ .page-desc { font-size: 13px; }
1208
+ .filters { padding: 14px 16px 16px; }
1209
+ }
1210
+ @media (max-width: 560px) {
1211
+ .kpi-row { grid-template-columns: 1fr; }
1212
+ .brand-badge { display: none; }
1213
+ .kpi-number { font-size: 34px; }
1214
+ .nav-actions { gap: 8px; }
1215
+ .segment-btn { font-size: 12px; padding: 6px 10px; }
1216
+ .btn-refresh { padding: 7px 11px; }
1217
+ .panel-header {
1218
+ flex-direction: column;
1219
+ align-items: flex-start;
1220
+ gap: 4px;
1221
+ }
1222
+ .table-card .panel-header { padding: 0 20px; }
1223
+ th:first-child, td:first-child { padding-left: 20px; }
1224
+ th:last-child, td:last-child { padding-right: 20px; }
1225
+ }
1226
+ @media (max-width: 400px) {
1227
+ .btn-label { display: none; }
1228
+ .btn-refresh { width: 32px; height: 32px; padding: 0; justify-content: center; }
1229
+ }
1230
+
1231
+ @media (prefers-reduced-motion: reduce) {
1232
+ *, *::before, *::after {
1233
+ animation-duration: 0.001ms !important;
1234
+ animation-iteration-count: 1 !important;
1235
+ transition-duration: 0.001ms !important;
1236
+ }
292
1237
  }
293
1238
  </style>
294
1239
  </head>
295
1240
  <body>
296
- <div class="container">
297
- <!-- Top Bar -->
298
- <header>
1241
+ <div class="nav">
1242
+ <div class="nav-inner">
299
1243
  <div class="brand">
300
1244
  <div class="brand-icon">
301
- <svg viewBox="0 0 24 24">
1245
+ <svg viewBox="0 0 24 24" aria-hidden="true">
302
1246
  <path d="M12 2L14.4 9.6L22 12L14.4 14.4L12 22L9.6 14.4L2 12L9.6 9.6L12 2Z"/>
303
1247
  </svg>
304
1248
  </div>
305
- <div>
306
- <div class="brand-title">SkillFlow</div>
307
- </div>
1249
+ <div class="brand-title">SkillFlow</div>
308
1250
  <div class="brand-badge">
309
1251
  <span class="pulse-dot"></span>
310
1252
  系统就绪
311
1253
  </div>
312
1254
  </div>
313
1255
 
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">
1256
+ <div class="nav-actions">
1257
+ <button type="button" class="btn-refresh btn-help" onclick="openGlossary()" title="查看指标定义、推导公式与业务白皮书">
1258
+ <svg viewBox="0 0 24 24" aria-hidden="true" style="width:14px;height:14px;">
1259
+ <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"/>
1260
+ </svg>
1261
+ <span class="btn-label">指标术语手册</span>
1262
+ </button>
1263
+ <button type="button" class="btn-refresh" onclick="refreshAll()">
1264
+ <svg viewBox="0 0 24 24" aria-hidden="true">
324
1265
  <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
1266
  </svg>
326
- 刷新
1267
+ <span class="btn-label">刷新</span>
327
1268
  </button>
328
1269
  </div>
329
- </header>
1270
+ </div>
1271
+ </div>
1272
+
1273
+ <main class="container">
1274
+ <div class="page-head">
1275
+ <h1>SkillFlow 效能监控看板</h1>
1276
+ <p class="page-desc">跨 Agent 技能遥测 · 汇总技能命中、节省工时与模型成本的真实收益</p>
1277
+ </div>
330
1278
 
331
- <!-- 4 Apple Pro Hero KPI Cards -->
1279
+ <!-- Query filters: applies to KPI, rankings, agent split and the feed -->
1280
+ <section class="filters" aria-label="查询条件">
1281
+ <div class="filter-field filter-field--range">
1282
+ <span class="filter-label">时间范围</span>
1283
+ <div class="segmented-control" role="group" aria-label="时间范围">
1284
+ <button type="button" class="segment-btn active" data-range="all" onclick="switchRange('all', this)">全部历史</button>
1285
+ <button type="button" class="segment-btn" data-range="today" onclick="switchRange('today', this)">今日</button>
1286
+ <button type="button" class="segment-btn" data-range="7days" onclick="switchRange('7days', this)">近 7 天</button>
1287
+ <button type="button" class="segment-btn" data-range="30days" onclick="switchRange('30days', this)">近 30 天</button>
1288
+ </div>
1289
+ </div>
1290
+
1291
+ <label class="filter-field">
1292
+ <span class="filter-label">Agent 宿主</span>
1293
+ <select class="select" id="filterAgent" onchange="onFilterChange('agent', this)">
1294
+ <option value="">全部 Agent</option>
1295
+ </select>
1296
+ </label>
1297
+
1298
+ <label class="filter-field">
1299
+ <span class="filter-label">Skill 技能</span>
1300
+ <select class="select" id="filterSkill" onchange="onFilterChange('skill', this)">
1301
+ <option value="">全部 Skill</option>
1302
+ </select>
1303
+ </label>
1304
+
1305
+ <label class="filter-field">
1306
+ <span class="filter-label">执行模型</span>
1307
+ <select class="select" id="filterModel" onchange="onFilterChange('model', this)">
1308
+ <option value="">全部模型</option>
1309
+ </select>
1310
+ </label>
1311
+
1312
+ <label class="filter-field">
1313
+ <span class="filter-label">执行状态</span>
1314
+ <select class="select" id="filterStatus" onchange="onFilterChange('status', this)">
1315
+ <option value="">全部状态</option>
1316
+ </select>
1317
+ </label>
1318
+
1319
+ <div class="filter-actions">
1320
+ <button type="button" class="btn-reset" id="btnReset" onclick="resetFilters()" disabled>重置</button>
1321
+ </div>
1322
+ </section>
1323
+
1324
+ <!-- KPI Row -->
332
1325
  <div class="kpi-row">
333
1326
  <div class="card">
334
1327
  <div class="kpi-label">
335
1328
  <span>技能触发总量</span>
336
- <span style="font-size: 16px;">⚡</span>
1329
+ <span class="kpi-icon tint-blue" aria-hidden="true">
1330
+ <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>
1331
+ </span>
337
1332
  </div>
338
1333
  <div class="kpi-number" id="kpiHits">-</div>
339
1334
  <div class="kpi-meta">执行成功率 <span class="accent-green" id="kpiSuccessRate">-</span></div>
@@ -342,7 +1337,9 @@ export function renderDashboardHtml() {
342
1337
  <div class="card">
343
1338
  <div class="kpi-label">
344
1339
  <span>累计节省人工工时</span>
345
- <span style="font-size: 16px;">⏱️</span>
1340
+ <span class="kpi-icon tint-purple" aria-hidden="true">
1341
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7.2V12l3.2 2"/></svg>
1342
+ </span>
346
1343
  </div>
347
1344
  <div class="kpi-number accent-purple" id="kpiHours">-</div>
348
1345
  <div class="kpi-meta">替代人工手动编写与排查</div>
@@ -350,29 +1347,34 @@ export function renderDashboardHtml() {
350
1347
 
351
1348
  <div class="card">
352
1349
  <div class="kpi-label">
353
- <span>精简流程步骤</span>
354
- <span style="font-size: 16px;">🌱</span>
1350
+ <span>精简流程步骤 <button type="button" class="info-help" data-glossary="term-steps" title="计算公式: 无 Skill 时基准步骤 - 实际执行步骤 = 节省步骤。点击查看白皮书">?</button></span>
1351
+ <span class="kpi-icon tint-green" aria-hidden="true">
1352
+ <svg viewBox="0 0 24 24"><path d="M4 7.5h16"/><path d="M4 12.5h11"/><path d="M4 17.5h6"/></svg>
1353
+ </span>
355
1354
  </div>
356
1355
  <div class="kpi-number accent-green" id="kpiSteps">-</div>
357
- <div class="kpi-meta">消除多轮重复交互</div>
1356
+ <div class="kpi-meta" id="kpiStepsMeta" title="计算公式: 无 Skill 时的人工/自由探索基准步骤 - 实际执行步骤 = 精简步骤">消除多轮重复交互</div>
358
1357
  </div>
359
1358
 
360
1359
  <div class="card">
361
1360
  <div class="kpi-label">
362
1361
  <span>真实模型节约折算</span>
363
- <span style="font-size: 16px;">🪙</span>
1362
+ <span class="kpi-icon tint-amber" aria-hidden="true">
1363
+ <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>
1364
+ </span>
364
1365
  </div>
365
1366
  <div class="kpi-number accent-blue" id="kpiCost">-</div>
366
1367
  <div class="kpi-meta">节约 Token <span id="kpiTokens" style="color:#fff; font-weight:600;">-</span></div>
367
1368
  </div>
368
1369
  </div>
369
1370
 
370
- <!-- Charts Section -->
1371
+ <!-- Panels -->
371
1372
  <div class="dashboard-grid">
372
1373
  <div class="card">
373
1374
  <div class="panel-header">
374
1375
  <div class="panel-title">
375
- <span>🏆</span> 价值贡献最大 Skill 排行榜
1376
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20V11"/><path d="M10 20V4.5"/><path d="M16 20v-6"/></svg>
1377
+ 价值贡献最大 Skill 排行榜
376
1378
  </div>
377
1379
  <div class="panel-subtitle">按节省步骤与工时</div>
378
1380
  </div>
@@ -382,7 +1384,8 @@ export function renderDashboardHtml() {
382
1384
  <div class="card">
383
1385
  <div class="panel-header">
384
1386
  <div class="panel-title">
385
- <span>🤖</span> 各 Agent 生态调用与节约贡献
1387
+ <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>
1388
+ 各 Agent 生态调用与节约贡献
386
1389
  </div>
387
1390
  <div class="panel-subtitle">跨平台分布</div>
388
1391
  </div>
@@ -390,11 +1393,39 @@ export function renderDashboardHtml() {
390
1393
  </div>
391
1394
  </div>
392
1395
 
393
- <!-- Live Invocations Table -->
1396
+ <!-- L2 & L3 Governance Section -->
1397
+ <div class="dashboard-grid">
1398
+ <!-- Engineering Quality -->
1399
+ <div class="card">
1400
+ <div class="panel-header">
1401
+ <div class="panel-title">
1402
+ <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>
1403
+ 工程质量与意图纯度
1404
+ </div>
1405
+ <div class="panel-subtitle">上下文与调用指标</div>
1406
+ </div>
1407
+ <div id="l2QualityList">加载中...</div>
1408
+ </div>
1409
+
1410
+ <!-- Asset Evolution -->
1411
+ <div class="card">
1412
+ <div class="panel-header">
1413
+ <div class="panel-title">
1414
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
1415
+ 资产治理与盲区反哺
1416
+ </div>
1417
+ <div class="panel-subtitle">健康评分与进化建议</div>
1418
+ </div>
1419
+ <div id="l3GovernanceList">加载中...</div>
1420
+ </div>
1421
+ </div>
1422
+
1423
+ <!-- Live invocations table -->
394
1424
  <div class="table-card">
395
- <div class="panel-header" style="margin-bottom: 8px;">
1425
+ <div class="panel-header">
396
1426
  <div class="panel-title">
397
- <span>⏱️</span> 实时 Skill 命中与模型明细
1427
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12.5h3.6l2.6-7 4 14 2.6-7H21"/></svg>
1428
+ 实时 Skill 命中与模型明细
398
1429
  </div>
399
1430
  <div class="panel-subtitle" id="tableCount">展示最新调用流水</div>
400
1431
  </div>
@@ -407,41 +1438,277 @@ export function renderDashboardHtml() {
407
1438
  <th>Agent 宿主</th>
408
1439
  <th>执行模型</th>
409
1440
  <th>命中的 Skill</th>
410
- <th>价值模式</th>
1441
+ <th>价值模式 <button type="button" class="info-help" data-glossary="term-profile" title="根据 Token 投入与预期产出划定(品质重载 / 高效精简 / 深度均衡)。点击查看白皮书">?</button></th>
411
1442
  <th>执行状态</th>
412
1443
  <th>节省工时</th>
413
- <th>节省步骤</th>
414
- <th>Token 收益 / 投入</th>
1444
+ <th title="计算公式: 无 Skill 时的人工/自由探索基准步骤 - 本次实际执行步骤 = 节省步骤">节省步骤 <button type="button" class="info-help" data-glossary="term-steps" title="无该技能基准步骤 - 实际执行步骤 = 节省步骤。点击查看白皮书">?</button></th>
1445
+ <th>Token 收益 / 投入 <button type="button" class="info-help" data-glossary="term-tokens" title="基准预估 Token - 实际消耗 Token = 净收益。点击查看白皮书">?</button></th>
415
1446
  </tr>
416
1447
  </thead>
417
1448
  <tbody id="invocationsBody">
418
- <tr><td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
1449
+ <tr><td colspan="10" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
419
1450
  </tbody>
420
1451
  </table>
421
1452
  </div>
1453
+
1454
+ <!-- Pagination bar (page size selector: 20 / 50 / 100) -->
1455
+ <div class="pagination-bar">
1456
+ <div class="pagination-left">
1457
+ <div class="pagination-info" id="paginationInfo">正在计算页码...</div>
1458
+ <div class="pagination-size">
1459
+ <span class="pagination-size-label">每页</span>
1460
+ <div class="segmented-control segmented-control--sm" role="group" aria-label="每页显示条数">
1461
+ <button type="button" class="segment-btn" data-size="20" onclick="onPageSizeChange(20, this)">20</button>
1462
+ <button type="button" class="segment-btn" data-size="50" onclick="onPageSizeChange(50, this)">50</button>
1463
+ <button type="button" class="segment-btn active" data-size="100" onclick="onPageSizeChange(100, this)">100</button>
1464
+ </div>
1465
+ <span class="pagination-size-label">条</span>
1466
+ </div>
1467
+ </div>
1468
+ <div class="pagination-controls">
1469
+ <button type="button" class="page-btn" id="btnPrevPage" onclick="changePage(-1)" disabled>
1470
+ <svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6"/></svg>
1471
+ 上一页
1472
+ </button>
1473
+ <span class="page-indicator" id="pageIndicator">第 1 / 1 页</span>
1474
+ <button type="button" class="page-btn" id="btnNextPage" onclick="changePage(1)" disabled>
1475
+ 下一页
1476
+ <svg viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></svg>
1477
+ </button>
1478
+ </div>
1479
+ </div>
422
1480
  </div>
423
1481
 
424
1482
  <footer>
425
- SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing & Work-Hour ROI<br>
1483
+ SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing &amp; Work-Hour ROI<br>
426
1484
  支持 Claude Code · Trae · Qoder · WorkBuddy · Codex · DSH
427
1485
  </footer>
428
- </div>
1486
+
1487
+ <!-- Apple Frosted Glass Glossary Modal -->
1488
+ <div class="modal-overlay" id="glossaryModal" onclick="onModalBackdropClick(event)">
1489
+ <div class="modal-sheet" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
1490
+ <div class="modal-header">
1491
+ <div class="modal-title-group">
1492
+ <h2 id="modalTitle">指标术语与算法度量白皮书</h2>
1493
+ <p>核心概念定义、数学核算推演公式与业务效能解读</p>
1494
+ </div>
1495
+ <button type="button" class="modal-close" onclick="closeGlossary()" aria-label="关闭">✕</button>
1496
+ </div>
1497
+ <div class="modal-search">
1498
+ <input type="text" id="glossarySearch" class="glossary-search" placeholder="搜索名词、公式或关键字..." oninput="filterGlossary(this.value)">
1499
+ </div>
1500
+ <div class="modal-body" id="glossaryCardsList">
1501
+ <!-- 1. 节省步骤 -->
1502
+ <div class="glossary-card" id="term-steps">
1503
+ <div class="glossary-title-row">
1504
+ <div class="glossary-term">节省步骤 (Saved Workflow Steps)</div>
1505
+ <span class="pill pill-success">业务感知</span>
1506
+ </div>
1507
+ <div class="glossary-def">没有该技能辅助时人工或自由探索的多轮交互步骤,减去本次实际交互步骤。</div>
1508
+ <div class="glossary-formula">节省步骤 = 基准预估步骤 (Baseline) - 实际交互步骤 (Actual)</div>
1509
+ <div class="glossary-scene"><strong>典型场景</strong>:例如写长文原本需经“初始提问 ➡️ 纠错去AI味 ➡️ 调整文风 ➡️ 补例子 ➡️ 调排版 ➡️ 定稿”共 6 轮交互。使用技能后 1 轮直接生成符合预期的成稿,净消除 5 轮人机拉扯。</div>
1510
+ </div>
1511
+
1512
+ <!-- 2. Token 收益 / 投入 -->
1513
+ <div class="glossary-card" id="term-tokens">
1514
+ <div class="glossary-title-row">
1515
+ <div class="glossary-term">Token 收益 / 投入 (Net Tokens)</div>
1516
+ <span class="pill pill-success">业务感知</span>
1517
+ </div>
1518
+ <div class="glossary-def">衡量本次技能执行的真实 Token 增减差值(净 ROI 收益)。</div>
1519
+ <div class="glossary-formula">Net Tokens = 基准预估 Token - 本次实际消耗 Token</div>
1520
+ <div class="glossary-scene"><strong>业务意义</strong>:若消耗低于基准则显示为绿色节约;若技能挂载了长篇规约导致消耗超额(如活人感写作),系统不虚标“节省”,而是真实显示为「投入 X 万 Tokens」,实事求是。</div>
1521
+ </div>
1522
+
1523
+ <!-- 3. 价值模式 -->
1524
+ <div class="glossary-card" id="term-profile">
1525
+ <div class="glossary-title-row">
1526
+ <div class="glossary-term">价值模式 (Quality Profile)</div>
1527
+ <span class="pill pill-purple">价值定位</span>
1528
+ </div>
1529
+ <div class="glossary-def">根据 Token 投入与预期产出比,由算法自动给技能打上的价值分类标签。</div>
1530
+ <div class="glossary-formula">高效精简型 (Net &gt; 0) | 品质重载型 (Actual &gt; 2 × Baseline) | 深度均衡型 (Balanced)</div>
1531
+ <div class="glossary-scene"><strong>三类划分</strong>:<br>• <strong>高效精简型</strong>:消耗少、响应极快,以高流程压缩比为主。<br>• <strong>品质重载型</strong>:注入超长参考知识库(如活人感写作),以高 Token 投入换取媲美专家的成稿质感。<br>• <strong>深度均衡型</strong>:标准复杂度的交互任务。</div>
1532
+ </div>
1533
+
1534
+ <!-- 4. 二次返工纠错率 -->
1535
+ <div class="glossary-card" id="term-corrections">
1536
+ <div class="glossary-title-row">
1537
+ <div class="glossary-term">二次返工纠错率 (Correction Rate)</div>
1538
+ <span class="pill pill-partial">工程质量</span>
1539
+ </div>
1540
+ <div class="glossary-def">技能交付后,用户紧接着在后续对话中提出“重写、写错了、重新来、不对”等负向纠错意图的调用比例。</div>
1541
+ <div class="glossary-formula">纠错率 = (存在重试纠错的调用次数 / 总调用次数) × 100%</div>
1542
+ <div class="glossary-scene"><strong>业务意义</strong>:效能的“水分过滤器”。纠错率高的技能说明规则存在盲区或模型理解偏差,并未真正为开发者省心,需要针对性优化提示词。</div>
1543
+ </div>
1544
+
1545
+ <!-- 5. 上下文产出率 -->
1546
+ <div class="glossary-card" id="term-yield">
1547
+ <div class="glossary-title-row">
1548
+ <div class="glossary-term">上下文产出率 (Yield Ratio)</div>
1549
+ <span class="pill pill-blue">上下文效率</span>
1550
+ </div>
1551
+ <div class="glossary-def">最终生成的有效内容 Token,与前置注入的规约参考文档 Token 之间的比值。</div>
1552
+ <div class="glossary-formula">上下文产出率 = (生成内容 Tokens / 挂载参考文档 Tokens) × 100%</div>
1553
+ <div class="glossary-scene"><strong>业务意义</strong>:监控 Prompt 是否过于冗长膨胀。防止某个技能每次挂载 5 万字参考资料却只生成了 50 字回答(过低的 Yield 意味着严重的算力与延迟浪费)。</div>
1554
+ </div>
1555
+
1556
+ <!-- 6. 工具链调用总深度 -->
1557
+ <div class="glossary-card" id="term-chain">
1558
+ <div class="glossary-title-row">
1559
+ <div class="glossary-term">工具链调用总深度 (Tool Chain Depth)</div>
1560
+ <span class="pill pill-blue">自动化编排</span>
1561
+ </div>
1562
+ <div class="glossary-def">技能在单次任务中,自动驱动底层子工具(如读写文件、代码替换、命令行执行、网络抓取)的连续调用次数。</div>
1563
+ <div class="glossary-formula">总深度 = 累加所有关联 Tool Invocations 次数</div>
1564
+ <div class="glossary-scene"><strong>业务意义</strong>:反映技能作为复杂 Agent 智能体的自闭环行动能力。调用链越深,代表承担的系统自动化工作量越重。</div>
1565
+ </div>
1566
+
1567
+ <!-- 7. 阻断异常 -->
1568
+ <div class="glossary-card" id="term-failures">
1569
+ <div class="glossary-title-row">
1570
+ <div class="glossary-term">阻断异常 (Tool Failures &amp; Blockages)</div>
1571
+ <span class="pill pill-failed">稳定性</span>
1572
+ </div>
1573
+ <div class="glossary-def">技能在自动编排调用工具链时,因权限拒绝(Permission Denied)、命令语法报错或超时导致的异常中断次数。</div>
1574
+ <div class="glossary-formula">阻断异常 = 子工具抛错 / 中断失败总次数</div>
1575
+ <div class="glossary-scene"><strong>业务意义</strong>:直接评估技能在沙箱环境中的跨系统健壮性,排查是否存在跨平台环境 Bug。</div>
1576
+ </div>
1577
+
1578
+ <!-- 8. 活跃技能健康评分 -->
1579
+ <div class="glossary-card" id="term-health">
1580
+ <div class="glossary-title-row">
1581
+ <div class="glossary-term">活跃技能健康评分 (Health Score)</div>
1582
+ <span class="pill pill-purple">资产治理</span>
1583
+ </div>
1584
+ <div class="glossary-def">综合成功率 (40%)、低返工率 (30%)、工具链稳定性 (15%) 与产出比 (15%) 动态加权计算出的 0~100 分综合健康度评级。</div>
1585
+ <div class="glossary-formula">综合健康分 = 100 - (低成功率罚分 × 40) - (纠错率罚分 × 30) - (工具阻断罚分 × 15)</div>
1586
+ <div class="glossary-scene"><strong>评级标准</strong>:A+ (95-100分 顶尖) · A (85-94分 优秀) · B (70-84分 良好) · C (50-69分 需优化) · D (&lt;50分 需重构)。</div>
1587
+ </div>
1588
+ </div>
1589
+ </div>
1590
+ </div>
1591
+ </main>
429
1592
 
430
1593
  <script>
431
1594
  let currentRange = 'all';
1595
+ let currentPage = 1;
1596
+ let pageSize = 100;
1597
+ const activeFilters = { agent: '', skill: '', model: '', status: '' };
1598
+ const FILTER_QUERY_KEYS = { agent: 'agent_name', skill: 'skill_id', model: 'model_name', status: 'status' };
1599
+
1600
+ function buildQueryString() {
1601
+ const params = new URLSearchParams();
1602
+ params.set('time_range', currentRange);
1603
+ Object.keys(FILTER_QUERY_KEYS).forEach(key => {
1604
+ const value = activeFilters[key];
1605
+ if (value) {
1606
+ params.set(FILTER_QUERY_KEYS[key], value);
1607
+ }
1608
+ });
1609
+ return params.toString();
1610
+ }
1611
+
1612
+ function hasActiveFilter() {
1613
+ const anyFilter = Object.keys(activeFilters).some(key => !!activeFilters[key]);
1614
+ return anyFilter || currentRange !== 'all';
1615
+ }
432
1616
 
433
1617
  function switchRange(range, el) {
434
1618
  currentRange = range;
435
- document.querySelectorAll('.segment-btn').forEach(btn => btn.classList.remove('active'));
1619
+ currentPage = 1;
1620
+ document.querySelectorAll('.filter-field--range .segment-btn').forEach(btn => btn.classList.remove('active'));
436
1621
  el.classList.add('active');
1622
+ syncFilterState();
1623
+ fetchData();
1624
+ }
1625
+
1626
+ function onFilterChange(key, el) {
1627
+ activeFilters[key] = el.value || '';
1628
+ currentPage = 1;
1629
+ syncFilterState();
1630
+ fetchData();
1631
+ }
1632
+
1633
+ function onPageSizeChange(size, el) {
1634
+ const newSize = Number(size) || 100;
1635
+ if (pageSize === newSize) return;
1636
+ pageSize = newSize;
1637
+ currentPage = 1;
1638
+ document.querySelectorAll('.pagination-size .segment-btn').forEach(btn => {
1639
+ btn.classList.toggle('active', Number(btn.getAttribute('data-size')) === newSize);
1640
+ });
1641
+ fetchData(1);
1642
+ }
1643
+
1644
+ function resetFilters() {
1645
+ currentRange = 'all';
1646
+ currentPage = 1;
1647
+ pageSize = 100;
1648
+ Object.keys(activeFilters).forEach(key => { activeFilters[key] = ''; });
1649
+ document.querySelectorAll('.filter-field--range .segment-btn').forEach(btn => {
1650
+ btn.classList.toggle('active', btn.getAttribute('data-range') === 'all');
1651
+ });
1652
+ document.querySelectorAll('.pagination-size .segment-btn').forEach(btn => {
1653
+ btn.classList.toggle('active', btn.getAttribute('data-size') === '100');
1654
+ });
1655
+ document.querySelectorAll('.select').forEach(sel => { sel.value = ''; });
1656
+ syncFilterState();
1657
+ fetchData();
1658
+ }
1659
+
1660
+ function syncFilterState() {
1661
+ document.querySelectorAll('.select').forEach(sel => {
1662
+ sel.classList.toggle('is-active', !!sel.value);
1663
+ });
1664
+ const resetBtn = document.getElementById('btnReset');
1665
+ if (resetBtn) {
1666
+ resetBtn.disabled = !hasActiveFilter();
1667
+ }
1668
+ }
1669
+
1670
+ async function loadFilterOptions() {
1671
+ try {
1672
+ const res = await fetch('/api/filters');
1673
+ const options = await res.json();
1674
+ fillSelect('filterAgent', options.agents || [], '全部 Agent');
1675
+ fillSelect('filterSkill', options.skills || [], '全部 Skill');
1676
+ fillSelect('filterModel', options.models || [], '全部模型');
1677
+ fillSelect('filterStatus', options.statuses || [], '全部状态');
1678
+ syncFilterState();
1679
+ } catch (err) {
1680
+ console.error('Filter options error:', err);
1681
+ }
1682
+ }
1683
+
1684
+ function fillSelect(id, options, placeholder) {
1685
+ const el = document.getElementById(id);
1686
+ if (!el) {
1687
+ return;
1688
+ }
1689
+ const selected = el.value;
1690
+ const items = ['<option value="">' + placeholder + '</option>'];
1691
+ options.forEach(option => {
1692
+ items.push('<option value="' + escapeHtml(option.value) + '">' + escapeHtml(option.label) + '</option>');
1693
+ });
1694
+ el.innerHTML = items.join('');
1695
+ el.value = options.some(option => option.value === selected) ? selected : '';
1696
+ }
1697
+
1698
+ function refreshAll() {
1699
+ loadFilterOptions();
437
1700
  fetchData();
438
1701
  }
439
1702
 
440
- async function fetchData() {
1703
+ async function fetchData(page) {
1704
+ if (typeof page === 'number') {
1705
+ currentPage = page;
1706
+ }
441
1707
  try {
1708
+ const query = buildQueryString();
442
1709
  const [dashRes, invRes] = await Promise.all([
443
- fetch('/api/dashboard?time_range=' + currentRange),
444
- fetch('/api/invocations?limit=30')
1710
+ fetch('/api/dashboard?' + query),
1711
+ fetch('/api/invocations?' + query + '&limit=' + pageSize + '&page=' + currentPage)
445
1712
  ]);
446
1713
  const dashData = await dashRes.json();
447
1714
  const invData = await invRes.json();
@@ -449,17 +1716,35 @@ export function renderDashboardHtml() {
449
1716
  renderKpi(dashData.kpi);
450
1717
  renderTopSkills(dashData.skillRows || []);
451
1718
  renderAgentBreakdown(dashData.agentRows || [], dashData.kpi?.totalInvocations || 1);
452
- renderInvocations(invData.invocations || []);
1719
+ renderL2Quality(dashData.kpi);
1720
+ renderL3Governance(dashData.governance);
1721
+ renderInvocations(
1722
+ invData.invocations || [],
1723
+ invData.total || 0,
1724
+ invData.page || currentPage,
1725
+ invData.totalPages || 1,
1726
+ invData.limit || pageSize
1727
+ );
453
1728
  } catch (err) {
454
1729
  console.error('Fetch error:', err);
455
1730
  }
456
1731
  }
457
1732
 
1733
+ function changePage(delta) {
1734
+ const target = currentPage + delta;
1735
+ if (target < 1) return;
1736
+ fetchData(target);
1737
+ }
1738
+
458
1739
  function renderKpi(kpi) {
459
1740
  if (!kpi) return;
460
1741
  document.getElementById('kpiHits').innerText = (kpi.totalInvocations || 0).toLocaleString();
461
1742
  document.getElementById('kpiSuccessRate').innerText = ((kpi.successRate || 1) * 100).toFixed(1) + '%';
462
1743
  document.getElementById('kpiSteps').innerText = '+' + (kpi.totalSavedSteps || 0).toLocaleString() + ' 步';
1744
+ const stepsMetaEl = document.getElementById('kpiStepsMeta');
1745
+ if (stepsMetaEl) {
1746
+ stepsMetaEl.innerText = '消除 ' + (kpi.totalSavedSteps || 0).toLocaleString() + ' 轮人机校准交互';
1747
+ }
463
1748
  document.getElementById('kpiTokens').innerText = '+' + (kpi.totalSavedTokens || 0).toLocaleString();
464
1749
 
465
1750
  const hours = kpi.totalSavedHours || 0;
@@ -473,34 +1758,40 @@ export function renderDashboardHtml() {
473
1758
  function renderTopSkills(skills) {
474
1759
  const container = document.getElementById('topSkillsList');
475
1760
  if (!skills || skills.length === 0) {
476
- container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无技能触发记录</div>';
1761
+ container.innerHTML = '<div class="empty-state">暂无技能触发记录</div>';
477
1762
  return;
478
1763
  }
479
1764
  const maxSteps = Math.max(...skills.map(s => s.saved_steps), 1);
480
- container.innerHTML = skills.slice(0, 5).map((s, idx) => \`
1765
+ container.innerHTML = skills.slice(0, 5).map((s, idx) => {
1766
+ const name = s.skill_name || s.skill_id;
1767
+ const idTag = s.skill_id && s.skill_id !== name
1768
+ ? '<span class="skill-id">(' + escapeHtml(s.skill_id) + ')</span>'
1769
+ : '';
1770
+ const width = Math.min(100, Math.max(6, Math.round(s.saved_steps / maxSteps * 100)));
1771
+ return \`
481
1772
  <div class="bar-row">
482
1773
  <div class="bar-label-group">
483
- <div>
1774
+ <div class="skill-name-cell">
484
1775
  <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>
1776
+ <span class="skill-title">\${escapeHtml(name)}</span>\${idTag}
487
1777
  </div>
488
1778
  <div class="skill-stat">
489
1779
  <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>
1780
+ <span class="skill-hours">+\${(s.saved_hours || 0).toFixed(1)}h</span>
491
1781
  </div>
492
1782
  </div>
493
1783
  <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>
1784
+ <div class="progress-fill fill-gradient-mint" style="width: \${width}%;"></div>
495
1785
  </div>
496
1786
  </div>
497
- \`).join('');
1787
+ \`;
1788
+ }).join('');
498
1789
  }
499
1790
 
500
1791
  function renderAgentBreakdown(agents, totalHits) {
501
1792
  const container = document.getElementById('agentBreakdownList');
502
1793
  if (!agents || agents.length === 0) {
503
- container.innerHTML = '<div style="color: var(--text-secondary); padding: 32px 0; text-align: center;">暂无 Agent 接入数据</div>';
1794
+ container.innerHTML = '<div class="empty-state">暂无 Agent 接入数据</div>';
504
1795
  return;
505
1796
  }
506
1797
  container.innerHTML = agents.map(a => {
@@ -508,8 +1799,8 @@ export function renderDashboardHtml() {
508
1799
  return \`
509
1800
  <div class="bar-row">
510
1801
  <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>
1802
+ <span class="pill pill-agent">\${escapeHtml(a.agent_name)}</span>
1803
+ <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
1804
  </div>
514
1805
  <div class="progress-track">
515
1806
  <div class="progress-fill fill-gradient-blue" style="width: \${Math.min(100, Math.max(6, pct))}%;"></div>
@@ -529,10 +1820,168 @@ export function renderDashboardHtml() {
529
1820
  .replace(/'/g, '&#39;');
530
1821
  }
531
1822
 
532
- function renderInvocations(items) {
1823
+ function renderL2Quality(kpi) {
1824
+ const container = document.getElementById('l2QualityList');
1825
+ if (!container) return;
1826
+ if (!kpi || !kpi.totalInvocations) {
1827
+ container.innerHTML = '<div class="empty-state">暂无工程指标数据</div>';
1828
+ return;
1829
+ }
1830
+
1831
+ const explicitPct = Number(((kpi.explicitRate || 1) * 100).toFixed(0));
1832
+ const implicitPct = Math.max(0, 100 - explicitPct);
1833
+ const correctionPct = ((kpi.correctionRate || 0) * 100).toFixed(1);
1834
+ const yieldPct = (kpi.yieldRatio || 100).toFixed(1);
1835
+ const toolDepth = kpi.totalToolDepth || 0;
1836
+ const toolFailures = kpi.totalToolFailures || 0;
1837
+ const correctionWarn = Number(correctionPct) > 10;
1838
+
1839
+ let html = '<div class="panel-body">';
1840
+
1841
+ /* 意图纯度:显式 / 隐式 两段式比例条 */
1842
+ html += '<div>';
1843
+ html += '<div class="subhead"><span>意图触发纯度</span>';
1844
+ html += '<span class="subhead-note">显式 <strong>' + explicitPct + '%</strong> · 隐式 ' + implicitPct + '%</span></div>';
1845
+ html += '<div class="split-bar">';
1846
+ html += '<div class="split-bar-seg split-bar-explicit" style="width: ' + explicitPct + '%;"></div>';
1847
+ html += '<div class="split-bar-seg split-bar-implicit" style="width: ' + implicitPct + '%;"></div>';
1848
+ html += '</div></div>';
1849
+
1850
+ /* 两个核心质量指标 */
1851
+ html += '<div class="stat-grid">';
1852
+
1853
+ html += '<div class="mini-stat">';
1854
+ html += '<div class="mini-stat-label">二次返工纠错率';
1855
+ html += '<button type="button" class="info-help" data-glossary="term-corrections" title="后续轮次出现重写/修改提示的比例。点击查看白皮书">?</button>';
1856
+ html += '</div>';
1857
+ html += '<div class="mini-stat-value' + (correctionWarn ? ' is-warn' : '') + '">' + correctionPct + '%</div>';
1858
+ html += '<div class="mini-stat-note">' + (kpi.totalCorrections || 0) + ' 次重试纠错</div>';
1859
+ html += '</div>';
1860
+
1861
+ html += '<div class="mini-stat">';
1862
+ html += '<div class="mini-stat-label">上下文产出率';
1863
+ html += '<button type="button" class="info-help" data-glossary="term-yield" title="有效输出 Token / 规约文档 Token 比率。点击查看白皮书">?</button>';
1864
+ html += '</div>';
1865
+ html += '<div class="mini-stat-value">' + yieldPct + '%</div>';
1866
+ html += '<div class="mini-stat-note">规约投入 ' + (kpi.totalContextTokens || 0).toLocaleString() + ' Tokens</div>';
1867
+ html += '</div>';
1868
+
1869
+ html += '</div>';
1870
+
1871
+ /* 工具链健康度 */
1872
+ html += '<div class="inline-stats">';
1873
+ html += '<div>工具链深度 <strong>' + toolDepth + ' 次</strong>';
1874
+ html += '<button type="button" class="info-help" data-glossary="term-chain" title="技能驱动底层子工具的连续调用次数。点击查看白皮书">?</button></div>';
1875
+ html += '<div>阻断异常 <strong' + (toolFailures > 0 ? ' class="is-danger"' : '') + '>' + toolFailures + ' 次</strong>';
1876
+ html += '<button type="button" class="info-help" data-glossary="term-failures" title="子工具调用被拒绝或报错阻断的次数。点击查看白皮书">?</button></div>';
1877
+ html += '</div>';
1878
+
1879
+ html += '</div>';
1880
+ container.innerHTML = html;
1881
+ }
1882
+
1883
+ function renderL3Governance(gov) {
1884
+ const container = document.getElementById('l3GovernanceList');
1885
+ if (!container) return;
1886
+ if (!gov) {
1887
+ container.innerHTML = '<div class="empty-state">暂无治理指标数据</div>';
1888
+ return;
1889
+ }
1890
+
1891
+ const leaderboard = gov.healthLeaderboard || [];
1892
+ const dormants = gov.dormantSkills || [];
1893
+ const modifiers = gov.followUpModifiers || [];
1894
+
1895
+ let html = '<div class="panel-body">';
1896
+
1897
+ if (leaderboard.length > 0) {
1898
+ html += '<div>';
1899
+ html += '<div class="subhead"><span>活跃技能健康评分';
1900
+ html += '<button type="button" class="info-help" data-glossary="term-health" title="综合成功率、低返工率、工具链稳定性打出的 0-100 综合分。点击查看白皮书">?</button>';
1901
+ html += '</span><span class="subhead-note">满分 100</span></div>';
1902
+ html += '<div class="health-list">';
1903
+ leaderboard.slice(0, 3).forEach(item => {
1904
+ const badgeClass = item.grade === 'A+' || item.grade === 'A' ? 'pill-success' : item.grade === 'B' ? 'pill-blue' : 'pill-partial';
1905
+ const idTag = item.skill_id && item.skill_id !== item.skill_name
1906
+ ? '<span class="health-id">(' + escapeHtml(item.skill_id) + ')</span>'
1907
+ : '';
1908
+ html += '<div class="health-row">';
1909
+ html += '<span class="health-name">' + escapeHtml(item.skill_name) + idTag + '</span>';
1910
+ html += '<span class="pill grade-pill ' + badgeClass + '">' + escapeHtml(item.grade) + ' · ' + item.score + ' 分</span>';
1911
+ html += '</div>';
1912
+ });
1913
+ html += '</div></div>';
1914
+ }
1915
+
1916
+ if (dormants.length > 0) {
1917
+ const names = dormants.slice(0, 3).map(d => escapeHtml(d.skill_name || d.skill_id)).join('、');
1918
+ html += '<div class="notice">';
1919
+ html += '<div class="notice-title"><span class="dot dot-warn"></span>僵尸技能待治理';
1920
+ html += '<span class="notice-badge">' + dormants.length + '</span></div>';
1921
+ html += '<div class="notice-body">' + names + (dormants.length > 3 ? ' 等' : '') + ',已超 14 天未调起,建议归档或优化触发词。</div>';
1922
+ html += '</div>';
1923
+ }
1924
+
1925
+ if (modifiers.length > 0) {
1926
+ html += '<div class="notice">';
1927
+ html += '<div class="notice-title"><span class="dot dot-info"></span>用户高频追加诉求反哺</div>';
1928
+ modifiers.slice(0, 2).forEach(m => {
1929
+ html += '<div class="notice-item"><strong>' + escapeHtml(m.category) + '</strong>';
1930
+ html += '<span class="notice-count">· ' + m.count + ' 次</span><br>' + escapeHtml(m.recommendation) + '</div>';
1931
+ });
1932
+ html += '</div>';
1933
+ }
1934
+
1935
+ html += '</div>';
1936
+ container.innerHTML = html;
1937
+ }
1938
+
1939
+ function renderInvocations(items, total, page, totalPages, limit) {
533
1940
  const tbody = document.getElementById('invocationsBody');
1941
+ const countEl = document.getElementById('tableCount');
1942
+ const filtered = hasActiveFilter();
1943
+
1944
+ const totalCount = typeof total === 'number' ? total : (items ? items.length : 0);
1945
+ const currPage = page || 1;
1946
+ const currLimit = limit || pageSize;
1947
+ const pagesCount = totalPages || Math.max(1, Math.ceil(totalCount / currLimit));
1948
+ const startNum = totalCount === 0 ? 0 : (currPage - 1) * currLimit + 1;
1949
+ const endNum = Math.min(totalCount, (currPage - 1) * currLimit + (items ? items.length : 0));
1950
+
1951
+ if (countEl) {
1952
+ if (!items || items.length === 0) {
1953
+ countEl.innerText = filtered ? '当前筛选条件下无记录' : '暂无调用流水';
1954
+ } else {
1955
+ countEl.innerText = '共 ' + totalCount.toLocaleString() + ' 条 · 显示第 ' + startNum + ' - ' + endNum + ' 条 (每页 ' + currLimit + ' 条)';
1956
+ }
1957
+ }
1958
+
1959
+ // Update pagination bar
1960
+ const pagInfo = document.getElementById('paginationInfo');
1961
+ const pageIndicator = document.getElementById('pageIndicator');
1962
+ const btnPrev = document.getElementById('btnPrevPage');
1963
+ const btnNext = document.getElementById('btnNextPage');
1964
+
1965
+ if (pagInfo) {
1966
+ pagInfo.innerText = totalCount === 0 ? '暂无记录' : '共 ' + totalCount.toLocaleString() + ' 条明细 · 第 ' + startNum + ' - ' + endNum + ' 条';
1967
+ }
1968
+ if (pageIndicator) {
1969
+ pageIndicator.innerText = '第 ' + currPage + ' / ' + pagesCount + ' 页';
1970
+ }
1971
+ if (btnPrev) {
1972
+ btnPrev.disabled = currPage <= 1;
1973
+ }
1974
+ if (btnNext) {
1975
+ btnNext.disabled = currPage >= pagesCount;
1976
+ }
1977
+
1978
+ document.querySelectorAll('.pagination-size .segment-btn').forEach(btn => {
1979
+ btn.classList.toggle('active', Number(btn.getAttribute('data-size')) === currLimit);
1980
+ });
1981
+
534
1982
  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>';
1983
+ const emptyText = filtered ? '当前筛选条件下没有匹配的调用记录' : '暂无近期 Skill 调用流水';
1984
+ tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; color: var(--text-secondary); padding: 32px 0;">' + emptyText + '</td></tr>';
536
1985
  return;
537
1986
  }
538
1987
  tbody.innerHTML = items.map(inv => {
@@ -555,34 +2004,95 @@ export function renderDashboardHtml() {
555
2004
  const profileBadge = inv.quality_profile === 'heavy_investment'
556
2005
  ? '<span class="pill pill-purple">品质重载</span>'
557
2006
  : inv.quality_profile === 'efficient'
558
- ? '<span class="pill pill-success">高效精简</span>'
559
- : '<span class="pill pill-blue">深度均衡</span>';
2007
+ ? '<span class="pill pill-blue">高效精简</span>'
2008
+ : '<span class="pill pill-neutral">深度均衡</span>';
560
2009
 
561
2010
  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>\`;
2011
+ ? \`<span class="cell-gain">+\${(inv.net_tokens || 0).toLocaleString()} Tokens</span>\`
2012
+ : \`<span class="cell-muted">投入 \${(inv.actual_tokens || 0).toLocaleString()} Tokens</span>\`;
564
2013
 
565
2014
  const safeSession = escapeHtml(inv.session_id);
566
2015
  const safeAgent = escapeHtml(inv.agent_name);
567
2016
  const safeSkill = escapeHtml(inv.skill_name || inv.skill_id);
568
2017
 
2018
+ const bSteps = inv.baseline_steps || (inv.saved_steps + (inv.actual_steps || 1));
2019
+ const aSteps = inv.actual_steps || 1;
2020
+ const formulaTip = '计算推演: 无该技能预估需 ' + bSteps + ' 步交互 - 实际执行 ' + aSteps + ' 步 = 精简 ' + inv.saved_steps + ' 步';
2021
+
569
2022
  return \`
570
2023
  <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>
2024
+ <td class="cell-muted">\${dateStr}</td>
2025
+ <td class="cell-session">\${safeSession.substring(0, 16)}...</td>
573
2026
  <td><span class="pill pill-agent">\${safeAgent}</span></td>
574
2027
  <td>\${modelBadge}</td>
575
2028
  <td><strong>\${safeSkill}</strong></td>
576
2029
  <td>\${profileBadge}</td>
577
2030
  <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>
2031
+ <td class="cell-hours">\${hoursText}</td>
2032
+ <td title="\${formulaTip}">
2033
+ <div class="cell-steps">+\${inv.saved_steps} 步</div>
2034
+ <div class="cell-steps-note">基准 \${bSteps} → 实际 \${aSteps}</div>
2035
+ </td>
580
2036
  <td>\${tokenCell}</td>
581
2037
  </tr>
582
2038
  \`;
583
2039
  }).join('');
584
2040
  }
585
2041
 
2042
+ function openGlossary(termId) {
2043
+ const modal = document.getElementById('glossaryModal');
2044
+ if (!modal) return;
2045
+ modal.classList.add('open');
2046
+ document.body.style.overflow = 'hidden';
2047
+
2048
+ if (termId) {
2049
+ setTimeout(function() {
2050
+ const card = document.getElementById(termId);
2051
+ if (card) {
2052
+ card.scrollIntoView({ behavior: 'smooth', block: 'center' });
2053
+ card.classList.add('highlight');
2054
+ setTimeout(function() { card.classList.remove('highlight'); }, 2200);
2055
+ }
2056
+ }, 120);
2057
+ }
2058
+ }
2059
+
2060
+ function closeGlossary() {
2061
+ const modal = document.getElementById('glossaryModal');
2062
+ if (!modal) return;
2063
+ modal.classList.remove('open');
2064
+ document.body.style.overflow = '';
2065
+ }
2066
+
2067
+ function onModalBackdropClick(e) {
2068
+ if (e.target && e.target.id === 'glossaryModal') {
2069
+ closeGlossary();
2070
+ }
2071
+ }
2072
+
2073
+ function filterGlossary(q) {
2074
+ const query = (q || '').trim().toLowerCase();
2075
+ const cards = document.querySelectorAll('.glossary-card');
2076
+ cards.forEach(function(card) {
2077
+ const text = card.innerText.toLowerCase();
2078
+ card.style.display = (!query || text.includes(query)) ? 'block' : 'none';
2079
+ });
2080
+ }
2081
+
2082
+ window.addEventListener('keydown', function(e) {
2083
+ if (e.key === 'Escape') closeGlossary();
2084
+ });
2085
+
2086
+ document.addEventListener('click', function(e) {
2087
+ const btn = e.target && e.target.closest ? e.target.closest('[data-glossary]') : null;
2088
+ if (btn) {
2089
+ e.preventDefault();
2090
+ const termId = btn.getAttribute('data-glossary');
2091
+ openGlossary(termId);
2092
+ }
2093
+ });
2094
+
2095
+ loadFilterOptions();
586
2096
  fetchData();
587
2097
  setInterval(fetchData, 5000);
588
2098
  </script>