@mengzf/skillflow-mcp 0.0.0-stage → 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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -2
  3. package/dist/adapters/claude-code.adapter.d.ts +10 -0
  4. package/dist/adapters/claude-code.adapter.d.ts.map +1 -0
  5. package/dist/adapters/claude-code.adapter.js +166 -0
  6. package/dist/adapters/claude-code.adapter.js.map +1 -0
  7. package/dist/adapters/codex.adapter.d.ts +10 -0
  8. package/dist/adapters/codex.adapter.d.ts.map +1 -0
  9. package/dist/adapters/codex.adapter.js +109 -0
  10. package/dist/adapters/codex.adapter.js.map +1 -0
  11. package/dist/adapters/dsh.adapter.d.ts +7 -0
  12. package/dist/adapters/dsh.adapter.d.ts.map +1 -0
  13. package/dist/adapters/dsh.adapter.js +60 -0
  14. package/dist/adapters/dsh.adapter.js.map +1 -0
  15. package/dist/adapters/qoder.adapter.d.ts +10 -0
  16. package/dist/adapters/qoder.adapter.d.ts.map +1 -0
  17. package/dist/adapters/qoder.adapter.js +119 -0
  18. package/dist/adapters/qoder.adapter.js.map +1 -0
  19. package/dist/adapters/trae.adapter.d.ts +8 -0
  20. package/dist/adapters/trae.adapter.d.ts.map +1 -0
  21. package/dist/adapters/trae.adapter.js +104 -0
  22. package/dist/adapters/trae.adapter.js.map +1 -0
  23. package/dist/adapters/types.d.ts +25 -0
  24. package/dist/adapters/types.d.ts.map +1 -0
  25. package/dist/adapters/types.js +2 -0
  26. package/dist/adapters/types.js.map +1 -0
  27. package/dist/adapters/universal.sync.d.ts +22 -0
  28. package/dist/adapters/universal.sync.d.ts.map +1 -0
  29. package/dist/adapters/universal.sync.js +140 -0
  30. package/dist/adapters/universal.sync.js.map +1 -0
  31. package/dist/adapters/workbuddy.sniffer.d.ts +22 -0
  32. package/dist/adapters/workbuddy.sniffer.d.ts.map +1 -0
  33. package/dist/adapters/workbuddy.sniffer.js +148 -0
  34. package/dist/adapters/workbuddy.sniffer.js.map +1 -0
  35. package/dist/config.d.ts +9 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +33 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/db/index.d.ts +4 -0
  40. package/dist/db/index.d.ts.map +1 -0
  41. package/dist/db/index.js +39 -0
  42. package/dist/db/index.js.map +1 -0
  43. package/dist/db/invocation-filter.d.ts +29 -0
  44. package/dist/db/invocation-filter.d.ts.map +1 -0
  45. package/dist/db/invocation-filter.js +78 -0
  46. package/dist/db/invocation-filter.js.map +1 -0
  47. package/dist/db/schema.d.ts +3 -0
  48. package/dist/db/schema.d.ts.map +1 -0
  49. package/dist/db/schema.js +129 -0
  50. package/dist/db/schema.js.map +1 -0
  51. package/dist/db/seed.d.ts +15 -0
  52. package/dist/db/seed.d.ts.map +1 -0
  53. package/dist/db/seed.js +128 -0
  54. package/dist/db/seed.js.map +1 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +51 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/server.d.ts +7 -0
  60. package/dist/server.d.ts.map +1 -0
  61. package/dist/server.js +372 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/services/benchmark.service.d.ts +27 -0
  64. package/dist/services/benchmark.service.d.ts.map +1 -0
  65. package/dist/services/benchmark.service.js +94 -0
  66. package/dist/services/benchmark.service.js.map +1 -0
  67. package/dist/services/dashboard.service.d.ts +14 -0
  68. package/dist/services/dashboard.service.d.ts.map +1 -0
  69. package/dist/services/dashboard.service.js +296 -0
  70. package/dist/services/dashboard.service.js.map +1 -0
  71. package/dist/services/filter.service.d.ts +26 -0
  72. package/dist/services/filter.service.d.ts.map +1 -0
  73. package/dist/services/filter.service.js +103 -0
  74. package/dist/services/filter.service.js.map +1 -0
  75. package/dist/services/impact.service.d.ts +20 -0
  76. package/dist/services/impact.service.d.ts.map +1 -0
  77. package/dist/services/impact.service.js +114 -0
  78. package/dist/services/impact.service.js.map +1 -0
  79. package/dist/services/telemetry.service.d.ts +41 -0
  80. package/dist/services/telemetry.service.d.ts.map +1 -0
  81. package/dist/services/telemetry.service.js +244 -0
  82. package/dist/services/telemetry.service.js.map +1 -0
  83. package/dist/types/index.d.ts +3 -0
  84. package/dist/types/index.d.ts.map +1 -0
  85. package/dist/types/index.js +3 -0
  86. package/dist/types/index.js.map +1 -0
  87. package/dist/types/models.d.ts +73 -0
  88. package/dist/types/models.d.ts.map +1 -0
  89. package/dist/types/models.js +2 -0
  90. package/dist/types/models.js.map +1 -0
  91. package/dist/types/tools.d.ts +173 -0
  92. package/dist/types/tools.d.ts.map +1 -0
  93. package/dist/types/tools.js +195 -0
  94. package/dist/types/tools.js.map +1 -0
  95. package/dist/utils/ascii-chart.d.ts +9 -0
  96. package/dist/utils/ascii-chart.d.ts.map +1 -0
  97. package/dist/utils/ascii-chart.js +29 -0
  98. package/dist/utils/ascii-chart.js.map +1 -0
  99. package/dist/utils/formatter.d.ts +6 -0
  100. package/dist/utils/formatter.d.ts.map +1 -0
  101. package/dist/utils/formatter.js +29 -0
  102. package/dist/utils/formatter.js.map +1 -0
  103. package/dist/utils/governance.d.ts +25 -0
  104. package/dist/utils/governance.d.ts.map +1 -0
  105. package/dist/utils/governance.js +70 -0
  106. package/dist/utils/governance.js.map +1 -0
  107. package/dist/utils/hours.d.ts +3 -0
  108. package/dist/utils/hours.d.ts.map +1 -0
  109. package/dist/utils/hours.js +15 -0
  110. package/dist/utils/hours.js.map +1 -0
  111. package/dist/utils/pricing.d.ts +19 -0
  112. package/dist/utils/pricing.d.ts.map +1 -0
  113. package/dist/utils/pricing.js +96 -0
  114. package/dist/utils/pricing.js.map +1 -0
  115. package/dist/utils/quality.d.ts +15 -0
  116. package/dist/utils/quality.d.ts.map +1 -0
  117. package/dist/utils/quality.js +47 -0
  118. package/dist/utils/quality.js.map +1 -0
  119. package/dist/web/render.d.ts +2 -0
  120. package/dist/web/render.d.ts.map +1 -0
  121. package/dist/web/render.js +1919 -0
  122. package/dist/web/render.js.map +1 -0
  123. package/dist/web/server.d.ts +10 -0
  124. package/dist/web/server.d.ts.map +1 -0
  125. package/dist/web/server.js +160 -0
  126. package/dist/web/server.js.map +1 -0
  127. package/package.json +51 -4
@@ -0,0 +1,1919 @@
1
+ export function renderDashboardHtml() {
2
+ return `<!DOCTYPE html>
3
+ <html lang="zh-CN">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="color-scheme" content="light">
8
+ <title>SkillFlow 效能监控看板 - 跨 Agent 技能遥测</title>
9
+ <style>
10
+ :root {
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;
41
+ --radius-full: 9999px;
42
+
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);
49
+ }
50
+
51
+ *, *::before, *::after { box-sizing: border-box; }
52
+
53
+ * { margin: 0; padding: 0; }
54
+
55
+ html { -webkit-text-size-adjust: 100%; }
56
+
57
+ body {
58
+ background-color: var(--bg);
59
+ color: var(--text-primary);
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;
66
+ min-height: 100vh;
67
+ -webkit-font-smoothing: antialiased;
68
+ -moz-osx-font-smoothing: grayscale;
69
+ }
70
+
71
+ ::selection { background: rgba(0, 113, 227, 0.18); }
72
+
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;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: space-between;
91
+ gap: 20px;
92
+ }
93
+ .nav-actions {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 10px;
97
+ }
98
+
99
+ .brand {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 10px;
103
+ min-width: 0;
104
+ }
105
+ .brand-icon {
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;
122
+ }
123
+
124
+ .brand-badge {
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;
135
+ }
136
+ .pulse-dot {
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;
143
+ }
144
+ @keyframes pulse {
145
+ 0%, 100% { opacity: 1; transform: scale(1); }
146
+ 50% { opacity: 0.55; transform: scale(0.86); }
147
+ }
148
+
149
+ /* ---------- Segmented control ---------- */
150
+ .segmented-control {
151
+ display: inline-flex;
152
+ padding: 3px;
153
+ gap: 2px;
154
+ background: rgba(120, 120, 128, 0.1);
155
+ border-radius: var(--radius-full);
156
+ }
157
+ .segment-btn {
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);
171
+ }
172
+ .segment-btn:hover { color: var(--text-primary); }
173
+ .segment-btn.active {
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);
177
+ }
178
+
179
+ .btn-refresh {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ gap: 6px;
183
+ appearance: none;
184
+ background: var(--surface);
185
+ border: 1px solid var(--border-highlight);
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;
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 ---------- */
332
+ .kpi-row {
333
+ display: grid;
334
+ grid-template-columns: repeat(4, 1fr);
335
+ gap: 16px;
336
+ margin-bottom: 16px;
337
+ }
338
+
339
+ .card {
340
+ background: var(--surface);
341
+ border: 1px solid var(--border);
342
+ border-radius: var(--radius-lg);
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);
346
+ }
347
+ .card:hover {
348
+ box-shadow: var(--shadow-md);
349
+ border-color: var(--border-highlight);
350
+ }
351
+
352
+ .kpi-label {
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;
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
+
386
+ .kpi-number {
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;
394
+ }
395
+ .kpi-meta {
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);
403
+ }
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; }
411
+
412
+ /* ---------- Panels ---------- */
413
+ .dashboard-grid {
414
+ display: grid;
415
+ grid-template-columns: 1fr 1fr;
416
+ gap: 16px;
417
+ margin-bottom: 16px;
418
+ }
419
+
420
+ .panel-header {
421
+ display: flex;
422
+ align-items: baseline;
423
+ justify-content: space-between;
424
+ gap: 16px;
425
+ margin-bottom: 22px;
426
+ }
427
+ .panel-title {
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;
450
+ }
451
+
452
+ /* ---------- Bars ---------- */
453
+ .bar-row { margin-bottom: 18px; }
454
+ .bar-row:last-child { margin-bottom: 0; }
455
+ .bar-label-group {
456
+ display: flex;
457
+ align-items: baseline;
458
+ justify-content: space-between;
459
+ gap: 12px;
460
+ font-size: 13px;
461
+ margin-bottom: 9px;
462
+ }
463
+ .skill-rank-badge {
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;
506
+ }
507
+
508
+ .progress-track {
509
+ width: 100%;
510
+ height: 6px;
511
+ border-radius: var(--radius-full);
512
+ background: rgba(120, 120, 128, 0.12);
513
+ overflow: hidden;
514
+ }
515
+ .progress-fill {
516
+ height: 100%;
517
+ border-radius: inherit;
518
+ transition: width 0.7s cubic-bezier(0.25, 0.1, 0.25, 1);
519
+ }
520
+ .fill-gradient-mint { background: linear-gradient(90deg, #2fb350, #17803a); }
521
+ .fill-gradient-blue { background: linear-gradient(90deg, #0071e3, #0a84ff); }
522
+
523
+ /* ---------- Table ---------- */
524
+ .table-card {
525
+ background: var(--surface);
526
+ border: 1px solid var(--border);
527
+ border-radius: var(--radius-lg);
528
+ box-shadow: var(--shadow-sm);
529
+ padding: 24px 0 4px;
530
+ overflow: hidden;
531
+ }
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
+ }
554
+ th {
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;
565
+ }
566
+ td {
567
+ padding: 12px 12px;
568
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
569
+ color: var(--text-primary);
570
+ vertical-align: middle;
571
+ font-variant-numeric: tabular-nums;
572
+ white-space: nowrap;
573
+ }
574
+ th:first-child, td:first-child { padding-left: 20px; }
575
+ th:last-child, td:last-child { padding-right: 20px; }
576
+ tr:last-child td { border-bottom: none; }
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
+ }
612
+
613
+ /* ---------- Pills ---------- */
614
+ .pill {
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;
625
+ }
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;
630
+ }
631
+ .pill-model {
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;
684
+ }
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;
692
+ }
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);
709
+ }
710
+ .modal-overlay.open {
711
+ opacity: 1;
712
+ pointer-events: auto;
713
+ }
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);
727
+ }
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;
868
+ }
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 ---------- */
1069
+ footer {
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
+ }
1145
+ }
1146
+ </style>
1147
+ </head>
1148
+ <body>
1149
+ <div class="nav">
1150
+ <div class="nav-inner">
1151
+ <div class="brand">
1152
+ <div class="brand-icon">
1153
+ <svg viewBox="0 0 24 24" aria-hidden="true">
1154
+ <path d="M12 2L14.4 9.6L22 12L14.4 14.4L12 22L9.6 14.4L2 12L9.6 9.6L12 2Z"/>
1155
+ </svg>
1156
+ </div>
1157
+ <div class="brand-title">SkillFlow</div>
1158
+ <div class="brand-badge">
1159
+ <span class="pulse-dot"></span>
1160
+ 系统就绪
1161
+ </div>
1162
+ </div>
1163
+
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">
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"/>
1174
+ </svg>
1175
+ <span class="btn-label">刷新</span>
1176
+ </button>
1177
+ </div>
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>
1212
+
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 -->
1233
+ <div class="kpi-row">
1234
+ <div class="card">
1235
+ <div class="kpi-label">
1236
+ <span>技能触发总量</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>
1240
+ </div>
1241
+ <div class="kpi-number" id="kpiHits">-</div>
1242
+ <div class="kpi-meta">执行成功率 <span class="accent-green" id="kpiSuccessRate">-</span></div>
1243
+ </div>
1244
+
1245
+ <div class="card">
1246
+ <div class="kpi-label">
1247
+ <span>累计节省人工工时</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>
1251
+ </div>
1252
+ <div class="kpi-number accent-purple" id="kpiHours">-</div>
1253
+ <div class="kpi-meta">替代人工手动编写与排查</div>
1254
+ </div>
1255
+
1256
+ <div class="card">
1257
+ <div class="kpi-label">
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>
1262
+ </div>
1263
+ <div class="kpi-number accent-green" id="kpiSteps">-</div>
1264
+ <div class="kpi-meta" id="kpiStepsMeta" title="计算公式: 无 Skill 时的人工/自由探索基准步骤 - 实际执行步骤 = 精简步骤">消除多轮重复交互</div>
1265
+ </div>
1266
+
1267
+ <div class="card">
1268
+ <div class="kpi-label">
1269
+ <span>真实模型节约折算</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>
1273
+ </div>
1274
+ <div class="kpi-number accent-blue" id="kpiCost">-</div>
1275
+ <div class="kpi-meta">节约 Token <span id="kpiTokens" style="color:#fff; font-weight:600;">-</span></div>
1276
+ </div>
1277
+ </div>
1278
+
1279
+ <!-- Panels -->
1280
+ <div class="dashboard-grid">
1281
+ <div class="card">
1282
+ <div class="panel-header">
1283
+ <div class="panel-title">
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 排行榜
1286
+ </div>
1287
+ <div class="panel-subtitle">按节省步骤与工时</div>
1288
+ </div>
1289
+ <div id="topSkillsList">加载中...</div>
1290
+ </div>
1291
+
1292
+ <div class="card">
1293
+ <div class="panel-header">
1294
+ <div class="panel-title">
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 生态调用与节约贡献
1297
+ </div>
1298
+ <div class="panel-subtitle">跨平台分布</div>
1299
+ </div>
1300
+ <div id="agentBreakdownList">加载中...</div>
1301
+ </div>
1302
+ </div>
1303
+
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 -->
1332
+ <div class="table-card">
1333
+ <div class="panel-header">
1334
+ <div class="panel-title">
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 命中与模型明细
1337
+ </div>
1338
+ <div class="panel-subtitle" id="tableCount">展示最新调用流水</div>
1339
+ </div>
1340
+ <div class="table-responsive">
1341
+ <table>
1342
+ <thead>
1343
+ <tr>
1344
+ <th>时间</th>
1345
+ <th>会话 ID</th>
1346
+ <th>Agent 宿主</th>
1347
+ <th>执行模型</th>
1348
+ <th>命中的 Skill</th>
1349
+ <th>价值模式 <button type="button" class="info-help" data-glossary="term-profile" title="根据 Token 投入与预期产出划定(品质重载 / 高效精简 / 深度均衡)。点击查看白皮书">?</button></th>
1350
+ <th>执行状态</th>
1351
+ <th>节省工时</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>
1354
+ </tr>
1355
+ </thead>
1356
+ <tbody id="invocationsBody">
1357
+ <tr><td colspan="10" style="text-align: center; color: var(--text-secondary); padding: 28px 0;">正在连接本地数据库...</td></tr>
1358
+ </tbody>
1359
+ </table>
1360
+ </div>
1361
+ </div>
1362
+
1363
+ <footer>
1364
+ SkillFlow MCP Telemetry Engine • Phase 1: Model-Aware Pricing &amp; Work-Hour ROI<br>
1365
+ 支持 Claude Code · Trae · Qoder · WorkBuddy · Codex · DSH
1366
+ </footer>
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>
1473
+
1474
+ <script>
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
+ }
1495
+
1496
+ function switchRange(range, el) {
1497
+ currentRange = range;
1498
+ document.querySelectorAll('.segment-btn').forEach(btn => btn.classList.remove('active'));
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();
1561
+ fetchData();
1562
+ }
1563
+
1564
+ async function fetchData() {
1565
+ try {
1566
+ const query = buildQueryString();
1567
+ const [dashRes, invRes] = await Promise.all([
1568
+ fetch('/api/dashboard?' + query),
1569
+ fetch('/api/invocations?' + query + '&limit=30')
1570
+ ]);
1571
+ const dashData = await dashRes.json();
1572
+ const invData = await invRes.json();
1573
+
1574
+ renderKpi(dashData.kpi);
1575
+ renderTopSkills(dashData.skillRows || []);
1576
+ renderAgentBreakdown(dashData.agentRows || [], dashData.kpi?.totalInvocations || 1);
1577
+ renderL2Quality(dashData.kpi);
1578
+ renderL3Governance(dashData.governance);
1579
+ renderInvocations(invData.invocations || [], invData.total);
1580
+ } catch (err) {
1581
+ console.error('Fetch error:', err);
1582
+ }
1583
+ }
1584
+
1585
+ function renderKpi(kpi) {
1586
+ if (!kpi) return;
1587
+ document.getElementById('kpiHits').innerText = (kpi.totalInvocations || 0).toLocaleString();
1588
+ document.getElementById('kpiSuccessRate').innerText = ((kpi.successRate || 1) * 100).toFixed(1) + '%';
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
+ }
1594
+ document.getElementById('kpiTokens').innerText = '+' + (kpi.totalSavedTokens || 0).toLocaleString();
1595
+
1596
+ const hours = kpi.totalSavedHours || 0;
1597
+ document.getElementById('kpiHours').innerText = hours >= 1 ? '+' + hours.toFixed(1) + 'h' : '+' + Math.round(hours * 60) + 'm';
1598
+
1599
+ const cny = kpi.totalCostSavedCny || 0;
1600
+ const usd = kpi.totalCostSavedUsd || 0;
1601
+ document.getElementById('kpiCost').innerText = cny > 0 ? '¥' + cny.toFixed(2) : '$' + usd.toFixed(2);
1602
+ }
1603
+
1604
+ function renderTopSkills(skills) {
1605
+ const container = document.getElementById('topSkillsList');
1606
+ if (!skills || skills.length === 0) {
1607
+ container.innerHTML = '<div class="empty-state">暂无技能触发记录</div>';
1608
+ return;
1609
+ }
1610
+ const maxSteps = Math.max(...skills.map(s => s.saved_steps), 1);
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 \`
1618
+ <div class="bar-row">
1619
+ <div class="bar-label-group">
1620
+ <div class="skill-name-cell">
1621
+ <span class="skill-rank-badge">\${idx + 1}</span>
1622
+ <span class="skill-title">\${escapeHtml(name)}</span>\${idTag}
1623
+ </div>
1624
+ <div class="skill-stat">
1625
+ <strong>+\${(s.saved_steps || 0).toLocaleString()} 步</strong>
1626
+ <span class="skill-hours">+\${(s.saved_hours || 0).toFixed(1)}h</span>
1627
+ </div>
1628
+ </div>
1629
+ <div class="progress-track">
1630
+ <div class="progress-fill fill-gradient-mint" style="width: \${width}%;"></div>
1631
+ </div>
1632
+ </div>
1633
+ \`;
1634
+ }).join('');
1635
+ }
1636
+
1637
+ function renderAgentBreakdown(agents, totalHits) {
1638
+ const container = document.getElementById('agentBreakdownList');
1639
+ if (!agents || agents.length === 0) {
1640
+ container.innerHTML = '<div class="empty-state">暂无 Agent 接入数据</div>';
1641
+ return;
1642
+ }
1643
+ container.innerHTML = agents.map(a => {
1644
+ const pct = totalHits > 0 ? ((a.hit_cnt / totalHits) * 100).toFixed(1) : 0;
1645
+ return \`
1646
+ <div class="bar-row">
1647
+ <div class="bar-label-group">
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>
1650
+ </div>
1651
+ <div class="progress-track">
1652
+ <div class="progress-fill fill-gradient-blue" style="width: \${Math.min(100, Math.max(6, pct))}%;"></div>
1653
+ </div>
1654
+ </div>
1655
+ \`;
1656
+ }).join('');
1657
+ }
1658
+
1659
+ function escapeHtml(str) {
1660
+ if (!str) return '';
1661
+ return String(str)
1662
+ .replace(/&/g, '&amp;')
1663
+ .replace(/</g, '&lt;')
1664
+ .replace(/>/g, '&gt;')
1665
+ .replace(/"/g, '&quot;')
1666
+ .replace(/'/g, '&#39;');
1667
+ }
1668
+
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) {
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
+
1798
+ if (!items || items.length === 0) {
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>';
1801
+ return;
1802
+ }
1803
+ tbody.innerHTML = items.map(inv => {
1804
+ const statusPill = inv.status === 'success'
1805
+ ? '<span class="pill pill-success">成功</span>'
1806
+ : inv.status === 'partial'
1807
+ ? '<span class="pill pill-partial">部分</span>'
1808
+ : '<span class="pill pill-failed">失败</span>';
1809
+
1810
+ const dateStr = inv.timestamp ? new Date(inv.timestamp).toLocaleTimeString() : '-';
1811
+ const safeModel = escapeHtml(inv.model_name);
1812
+ const modelBadge = safeModel && safeModel !== 'unknown'
1813
+ ? \`<span class="pill pill-model">\${safeModel}</span>\`
1814
+ : '<span style="color: var(--text-tertiary); font-size: 11px;">-</span>';
1815
+
1816
+ const hoursText = inv.saved_hours >= 1
1817
+ ? \`+\${inv.saved_hours.toFixed(1)}h\`
1818
+ : \`+\${Math.round((inv.saved_hours || 0) * 60)}m\`;
1819
+
1820
+ const profileBadge = inv.quality_profile === 'heavy_investment'
1821
+ ? '<span class="pill pill-purple">品质重载</span>'
1822
+ : inv.quality_profile === 'efficient'
1823
+ ? '<span class="pill pill-blue">高效精简</span>'
1824
+ : '<span class="pill pill-neutral">深度均衡</span>';
1825
+
1826
+ const tokenCell = (inv.net_tokens || 0) > 0
1827
+ ? \`<span class="cell-gain">+\${(inv.net_tokens || 0).toLocaleString()} Tokens</span>\`
1828
+ : \`<span class="cell-muted">投入 \${(inv.actual_tokens || 0).toLocaleString()} Tokens</span>\`;
1829
+
1830
+ const safeSession = escapeHtml(inv.session_id);
1831
+ const safeAgent = escapeHtml(inv.agent_name);
1832
+ const safeSkill = escapeHtml(inv.skill_name || inv.skill_id);
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
+
1838
+ return \`
1839
+ <tr>
1840
+ <td class="cell-muted">\${dateStr}</td>
1841
+ <td class="cell-session">\${safeSession.substring(0, 16)}...</td>
1842
+ <td><span class="pill pill-agent">\${safeAgent}</span></td>
1843
+ <td>\${modelBadge}</td>
1844
+ <td><strong>\${safeSkill}</strong></td>
1845
+ <td>\${profileBadge}</td>
1846
+ <td>\${statusPill}</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>
1852
+ <td>\${tokenCell}</td>
1853
+ </tr>
1854
+ \`;
1855
+ }).join('');
1856
+ }
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();
1912
+ fetchData();
1913
+ setInterval(fetchData, 5000);
1914
+ </script>
1915
+ </body>
1916
+ </html>
1917
+ `;
1918
+ }
1919
+ //# sourceMappingURL=render.js.map