oh-my-knowledge 0.35.0 → 0.37.0
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.
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/dist/assets/agent-skills/omk/SKILL.md +4 -2
- package/dist/assets/agent-skills/omk/references/commands.md +121 -2
- package/dist/authoring/evolver.js +1 -1
- package/dist/cli/commands/doctor.js +21 -5
- package/dist/cli/commands/eval/index.js +7 -6
- package/dist/cli/commands/evolve.d.ts +11 -0
- package/dist/cli/commands/evolve.js +105 -5
- package/dist/cli/commands/list.d.ts +25 -0
- package/dist/cli/commands/list.js +118 -0
- package/dist/cli/commands/promote.d.ts +24 -0
- package/dist/cli/commands/promote.js +158 -0
- package/dist/cli/commands/rollback.d.ts +20 -0
- package/dist/cli/commands/rollback.js +95 -0
- package/dist/cli/commands/sample.d.ts +1 -0
- package/dist/cli/commands/sample.js +10 -4
- package/dist/cli/lib/cell-format.d.ts +17 -0
- package/dist/cli/lib/cell-format.js +19 -0
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/list.js +36 -0
- package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/promote.js +54 -0
- package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/rollback.js +22 -0
- package/dist/cli/lib/i18n-dict.d.ts +4 -1
- package/dist/cli/lib/i18n-dict.js +6 -0
- package/dist/cli/lib/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- package/dist/cli/lib/run-tally.js +1 -1
- package/dist/cli/lib/shared.js +1 -1
- package/dist/cli/lib/source-probe.d.ts +15 -0
- package/dist/cli/lib/source-probe.js +129 -0
- package/dist/doctor/endpoint-rule.d.ts +60 -0
- package/dist/doctor/endpoint-rule.js +381 -0
- package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
- package/dist/doctor/health/load-custom-dimensions.js +43 -3
- package/dist/doctor/index.js +9 -2
- package/dist/eval-core/evaluation-reporting.d.ts +1 -0
- package/dist/eval-core/evaluation-reporting.js +6 -4
- package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
- package/dist/eval-workflows/run-evaluation.js +2 -2
- package/dist/executors/codex-cli.js +5 -1
- package/dist/inputs/skill-loader.js +6 -3
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-view.d.ts +58 -0
- package/dist/managed/list-view.js +70 -0
- package/dist/managed/promote-gate.d.ts +40 -0
- package/dist/managed/promote-gate.js +37 -0
- package/dist/managed/store.d.ts +25 -2
- package/dist/managed/store.js +137 -11
- package/dist/observability/experience.d.ts +2 -0
- package/dist/observability/experience.js +31 -2
- package/dist/observability/inbox.js +31 -5
- package/dist/observability/review-state.js +22 -11
- package/dist/observability/soft-standards/llm-extractor.js +3 -3
- package/dist/observability/soft-standards/skill-standards-store.d.ts +1 -0
- package/dist/observability/soft-standards/skill-standards-store.js +77 -14
- package/dist/observability/soft-standards/types.d.ts +2 -2
- package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
- package/dist/renderer/doctor-detail-renderer.js +114 -0
- package/dist/renderer/html-renderer.d.ts +3 -2
- package/dist/renderer/html-renderer.js +102 -102
- package/dist/renderer/icons.d.ts +29 -0
- package/dist/renderer/icons.js +66 -0
- package/dist/renderer/layout.js +81 -68
- package/dist/renderer/observation-inbox/styles.d.ts +1 -1
- package/dist/renderer/observation-inbox/styles.js +53 -53
- package/dist/renderer/report-shell.d.ts +77 -0
- package/dist/renderer/report-shell.js +223 -0
- package/dist/renderer/skill-detail-renderer.js +147 -352
- package/dist/renderer/skill-health-renderer.js +50 -73
- package/dist/renderer/skill-list-renderer.js +358 -306
- package/dist/renderer/summary.js +272 -162
- package/dist/renderer/test-view.d.ts +4 -3
- package/dist/renderer/test-view.js +386 -135
- package/dist/server/report-server.js +142 -28
- package/dist/server/report-store.d.ts +1 -1
- package/dist/server/report-store.js +16 -15
- package/dist/server/skill-index.js +5 -4
- package/dist/types/doctor.d.ts +24 -2
- package/dist/types/doctor.js +1 -1
- package/dist/types/managed.d.ts +17 -3
- package/dist/types/observability.d.ts +6 -6
- package/dist/types/report.d.ts +9 -4
- package/package.json +1 -1
|
@@ -1,345 +1,397 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill 列表页 —
|
|
3
|
-
*
|
|
4
|
-
* ┌─ skill-name ───────────────────────────────── ⚠ N 待优化 · ↗ 趋势 ─┐
|
|
5
|
-
* │ ┌─────┐ 健康摘要一句话 │
|
|
6
|
-
* │ │ 🟡 │ 🩺 7/8 通过 · 🧪 5 用例 80% 4.2/5 · 👁 30 段 95% 稳定 │
|
|
7
|
-
* │ │ 良好 │ │
|
|
8
|
-
* │ │ 85 │ │
|
|
9
|
-
* │ └─────┘ │
|
|
10
|
-
* └────────────────────────────────────────────────────────────────────┘
|
|
2
|
+
* Skill 列表页 — 仪表盘布局,精确参照内部 SkillHealth dashboard。
|
|
11
3
|
*/
|
|
12
4
|
import { layout, e, DEFAULT_LANG } from './layout.js';
|
|
13
5
|
import { assessHealth } from './skill-detail-renderer.js';
|
|
14
|
-
|
|
6
|
+
import { icon } from './icons.js';
|
|
7
|
+
// ── icons (Lucide SVG inline) ── 维度/状态图标统一走共享 icons.ts;此处仅保留弹框用的 help 图标。
|
|
8
|
+
const IC = {
|
|
9
|
+
helpCircle: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
10
|
+
};
|
|
11
|
+
function fmtDate(ts) {
|
|
15
12
|
if (!ts)
|
|
16
|
-
return
|
|
13
|
+
return '';
|
|
17
14
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const mins = Math.floor(diff / 60000);
|
|
21
|
-
if (mins < 60)
|
|
22
|
-
return lang === 'zh' ? `${mins} 分钟前` : `${mins}m ago`;
|
|
23
|
-
const hours = Math.floor(mins / 60);
|
|
24
|
-
if (hours < 24)
|
|
25
|
-
return lang === 'zh' ? `${hours} 小时前` : `${hours}h ago`;
|
|
26
|
-
return lang === 'zh' ? `${Math.floor(hours / 24)} 天前` : `${Math.floor(hours / 24)}d ago`;
|
|
15
|
+
const d = new Date(ts);
|
|
16
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
27
17
|
}
|
|
28
18
|
catch {
|
|
29
19
|
return '';
|
|
30
20
|
}
|
|
31
21
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let trendDelta = '';
|
|
39
|
-
if (entry.evalHistory.length >= 2) {
|
|
40
|
-
const first = entry.evalHistory[0].compositeScore;
|
|
41
|
-
const last = entry.evalHistory[entry.evalHistory.length - 1].compositeScore;
|
|
42
|
-
if (first != null && last != null) {
|
|
43
|
-
const d = last - first;
|
|
44
|
-
trendDir = d > 0.1 ? 'up' : d < -0.1 ? 'down' : 'flat';
|
|
45
|
-
trendDelta = (d > 0 ? '+' : '') + d.toFixed(2);
|
|
46
|
-
}
|
|
22
|
+
function fmtShort(ts) {
|
|
23
|
+
if (!ts)
|
|
24
|
+
return '-';
|
|
25
|
+
try {
|
|
26
|
+
const d = new Date(ts);
|
|
27
|
+
return `${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
47
28
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const passPctOf = (h) => totalOf(h) > 0 ? (h.passCount / totalOf(h)) * 100 : 0;
|
|
51
|
-
const first = passPctOf(entry.doctorHistory[0]);
|
|
52
|
-
const last = passPctOf(entry.doctorHistory[entry.doctorHistory.length - 1]);
|
|
53
|
-
const d = last - first;
|
|
54
|
-
trendDir = d > 5 ? 'up' : d < -5 ? 'down' : 'flat';
|
|
55
|
-
trendDelta = (d > 0 ? '+' : '') + d.toFixed(0) + '%';
|
|
29
|
+
catch {
|
|
30
|
+
return '-';
|
|
56
31
|
}
|
|
57
|
-
return { warnCount, highWarn, trendDir, trendDelta };
|
|
58
32
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
parts.push(lang === 'zh' ? `eval 通过率 ${pct}%` : `eval ${pct}% pass`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (entry.observe) {
|
|
77
|
-
if (entry.observe.confidence === 'underpowered') {
|
|
78
|
-
// Too few segments to claim stable/flaky/unstable — surface as indicative only.
|
|
79
|
-
parts.push(lang === 'zh' ? 'observe 样本不足(仅供参考)' : 'observe low N (indicative)');
|
|
33
|
+
// 综合健康配色:≥85 绿 / ≥60 琥珀 / 其余红。与 report-shell.healthColor 同口径,
|
|
34
|
+
// 保证同一个分数在首页列表与报告页 Hero 颜色一致(90 两处都是绿,不再首页橙、详情红)。
|
|
35
|
+
function scoreColor(s) { return s >= 85 ? '#1f9d63' : s >= 60 ? '#d97706' : '#dc2626'; }
|
|
36
|
+
function aggregate(entries, iMap, lang) {
|
|
37
|
+
let dP = 0, dW = 0, dF = 0, eP = 0, eF = 0, eSamples = 0, eSum = 0, eCt = 0, withObs = 0, hSum = 0, hCt = 0;
|
|
38
|
+
let eAssertP = 0, eAssertT = 0;
|
|
39
|
+
const gc = { excellent: 0, good: 0, fair: 0, unhealthy: 0, unscored: 0 };
|
|
40
|
+
const dH = [], eH = [], oH = [];
|
|
41
|
+
for (const ent of entries) {
|
|
42
|
+
const h = assessHealth(ent, iMap.get(ent.skillName) ?? [], lang);
|
|
43
|
+
gc[h.grade]++;
|
|
44
|
+
if (h.score != null) {
|
|
45
|
+
hSum += h.score;
|
|
46
|
+
hCt++;
|
|
80
47
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
parts.push(`observe ${labels[entry.observe.healthBand]}`);
|
|
48
|
+
if (ent.doctor) {
|
|
49
|
+
dP += ent.doctor.passCount;
|
|
50
|
+
dW += ent.doctor.warnCount;
|
|
51
|
+
dF += ent.doctor.failCount;
|
|
86
52
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
53
|
+
if (ent.eval) {
|
|
54
|
+
eP += ent.eval.passCount;
|
|
55
|
+
eF += ent.eval.failCount;
|
|
56
|
+
eSamples += ent.eval.totalSamples;
|
|
57
|
+
if (ent.eval.compositeScore != null) {
|
|
58
|
+
eSum += ent.eval.compositeScore;
|
|
59
|
+
eCt++;
|
|
60
|
+
}
|
|
61
|
+
eAssertP += ent.eval.passCount;
|
|
62
|
+
eAssertT += ent.eval.passCount + ent.eval.failCount;
|
|
94
63
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (entry.doctor) {
|
|
102
|
-
const total = entry.doctor.passCount + entry.doctor.warnCount + entry.doctor.failCount;
|
|
103
|
-
items.push(`<span class="sl-metric">🩺 ${entry.doctor.passCount}/${total} ${lang === 'zh' ? '通过' : 'pass'}</span>`);
|
|
104
|
-
}
|
|
105
|
-
if (entry.eval) {
|
|
106
|
-
const total = entry.eval.passCount + entry.eval.failCount;
|
|
107
|
-
const pct = total > 0 ? Math.round((entry.eval.passCount / total) * 100) : 0;
|
|
108
|
-
const score = entry.eval.compositeScore != null ? entry.eval.compositeScore.toFixed(2) : '—';
|
|
109
|
-
items.push(`<span class="sl-metric">🧪 ${entry.eval.totalSamples} ${lang === 'zh' ? '用例' : 'samples'} · ${pct}% · ${score}/5</span>`);
|
|
110
|
-
}
|
|
111
|
-
if (entry.observe) {
|
|
112
|
-
if (entry.observe.confidence === 'underpowered') {
|
|
113
|
-
// 段数过少,不报「X% 稳定」硬指标 —— 只标段数 + 仅供参考,跟摘要口径一致,避免同一张卡自相矛盾。
|
|
114
|
-
items.push(`<span class="sl-metric">👁 ${entry.observe.segmentCount} ${lang === 'zh' ? '段 · 样本不足' : 'segs · low N'}</span>`);
|
|
64
|
+
if (ent.observe)
|
|
65
|
+
withObs++;
|
|
66
|
+
for (const x of ent.doctorHistory) {
|
|
67
|
+
const t = x.passCount + x.warnCount + x.failCount;
|
|
68
|
+
if (t > 0)
|
|
69
|
+
dH.push(Math.round((x.passCount / t) * 100));
|
|
115
70
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
71
|
+
for (const x of ent.evalHistory) {
|
|
72
|
+
if (x.compositeScore != null)
|
|
73
|
+
eH.push(Math.round((x.compositeScore / 5) * 100));
|
|
119
74
|
}
|
|
75
|
+
for (const x of ent.observeHistory)
|
|
76
|
+
oH.push(Math.round((1 - x.gapRate) * 100));
|
|
120
77
|
}
|
|
121
|
-
return
|
|
78
|
+
return { totalSkills: entries.length, dP, dW, dF, eP, eF, eSamples, eAvg: eCt > 0 ? eSum / eCt : null, eAssertP, eAssertT, withObs, lastTs: entries.flatMap((x) => [x.doctor?.timestamp, x.eval?.timestamp, x.observe?.generatedAt]).filter((x) => Boolean(x)).sort().pop() ?? null, gc, greenCt: gc.good + gc.excellent, composite: hCt > 0 ? Math.round(hSum / hCt) : null, dHist: dH, eHist: eH, oHist: oH };
|
|
122
79
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const arrow = t.trendDir === 'up' ? '↗' : t.trendDir === 'down' ? '↘' : '→';
|
|
127
|
-
const cls = t.trendDir === 'up' ? 'up' : t.trendDir === 'down' ? 'down' : 'flat';
|
|
128
|
-
const label = lang === 'zh'
|
|
129
|
-
? (t.trendDir === 'up' ? '上升' : t.trendDir === 'down' ? '下降' : '持平')
|
|
130
|
-
: (t.trendDir === 'up' ? 'up' : t.trendDir === 'down' ? 'down' : 'flat');
|
|
131
|
-
return `<span class="sl-trend sl-trend--${cls}" title="${lang === 'zh' ? '基于历史评测综合分变化' : 'Based on historical eval composite delta'}">${arrow} ${label}${t.trendDelta ? ` (${e(t.trendDelta)})` : ''}</span>`;
|
|
80
|
+
function ring(score, size, sw) {
|
|
81
|
+
const r = (size - sw) / 2, c = 2 * Math.PI * r, off = c * (1 - (score ?? 0) / 100), clr = score != null ? scoreColor(score) : '#9ca3af', cx = size / 2;
|
|
82
|
+
return `<div style="position:relative;width:${size}px;height:${size}px"><svg width="${size}" height="${size}"><circle cx="${cx}" cy="${cx}" r="${r}" fill="none" stroke="#edf0f7" stroke-width="${sw}"/>${score != null ? `<circle cx="${cx}" cy="${cx}" r="${r}" fill="none" stroke="${clr}" stroke-width="${sw}" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${off.toFixed(2)}" stroke-linecap="round" transform="rotate(-90 ${cx} ${cx})" style="transition:stroke-dashoffset .6s"/>` : ''}</svg><div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center"><span style="font-size:${Math.round(size * 0.36)}px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;color:${clr}">${score ?? '—'}</span></div></div>`;
|
|
132
83
|
}
|
|
133
|
-
function
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
? `<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
${renderTrendBadge(summary, lang)}
|
|
155
|
-
</div>
|
|
156
|
-
<div class="sl-card-summary">${renderHealthSummary(entry, insights, lang)}</div>
|
|
157
|
-
<div class="sl-card-metrics">${renderMetrics(entry, lang)}</div>
|
|
158
|
-
</a>`;
|
|
159
|
-
}
|
|
160
|
-
function renderSummaryBar(idx, allEntries, insightsByEntry, lang) {
|
|
161
|
-
const s = idx.summary;
|
|
162
|
-
if (s.totalSkills === 0)
|
|
163
|
-
return '';
|
|
164
|
-
// grade 重新统计(用 assessHealth 而不是 entry.band,跟卡片显示一致)
|
|
165
|
-
const gradeCounts = { excellent: 0, good: 0, fair: 0, unhealthy: 0, unscored: 0 };
|
|
166
|
-
for (const ent of allEntries) {
|
|
167
|
-
const ins = insightsByEntry.get(ent.skillName) ?? [];
|
|
168
|
-
const h = assessHealth(ent, ins, lang);
|
|
169
|
-
gradeCounts[h.grade]++;
|
|
170
|
-
}
|
|
171
|
-
const totalSamples = allEntries.reduce((sum, ent) => sum + (ent.eval?.totalSamples ?? 0), 0);
|
|
172
|
-
const totalWarn = allEntries.reduce((sum, ent) => sum + (insightsByEntry.get(ent.skillName)?.length ?? 0), 0);
|
|
173
|
-
const totalHighWarn = allEntries.reduce((sum, ent) => sum + (insightsByEntry.get(ent.skillName)?.filter((i) => i.severity === 'high').length ?? 0), 0);
|
|
174
|
-
const lastTs = allEntries.flatMap((ent) => [ent.doctor?.timestamp, ent.eval?.timestamp, ent.observe?.generatedAt])
|
|
175
|
-
.filter((x) => Boolean(x)).sort().pop();
|
|
176
|
-
const overview = lang === 'zh'
|
|
177
|
-
? `${s.totalSkills} skill · ${lang === 'zh' ? '共' : 'across'} ${totalSamples} ${lang === 'zh' ? '用例' : 'samples'}${lastTs ? ` · ${lang === 'zh' ? '最近' : 'last'} ${relTime(lastTs, lang)}` : ''}${totalWarn > 0 ? ` · ⚠ ${totalWarn} ${lang === 'zh' ? '待优化' : 'todo'}${totalHighWarn > 0 ? `(${totalHighWarn} ${lang === 'zh' ? '高优' : 'high'})` : ''}` : ''}`
|
|
178
|
-
: `${s.totalSkills} skills · ${totalSamples} samples${lastTs ? ` · last ${relTime(lastTs, lang)}` : ''}${totalWarn > 0 ? ` · ⚠ ${totalWarn} todo${totalHighWarn > 0 ? ` (${totalHighWarn} high)` : ''}` : ''}`;
|
|
179
|
-
const greenCount = gradeCounts.good + gradeCounts.excellent;
|
|
180
|
-
const healthyCount = greenCount + gradeCounts.fair;
|
|
181
|
-
const healthRate = s.totalSkills > 0 ? Math.round((healthyCount / s.totalSkills) * 100) : 0;
|
|
182
|
-
const healthRateText = lang === 'zh'
|
|
183
|
-
? `健康率 ${healthyCount}/${s.totalSkills} (${healthRate}%)`
|
|
184
|
-
: `Health ${healthyCount}/${s.totalSkills} (${healthRate}%)`;
|
|
185
|
-
const gradeRow = `
|
|
186
|
-
<button class="sl-filter-pill sl-filter-pill--all sl-filter-pill--active" data-filter="all">${lang === 'zh' ? '全部' : 'All'} ${s.totalSkills}</button>
|
|
187
|
-
<button class="sl-filter-pill sl-filter-pill--red" data-filter="red">🔴 ${gradeCounts.unhealthy}</button>
|
|
188
|
-
<button class="sl-filter-pill sl-filter-pill--yellow" data-filter="yellow">🟡 ${gradeCounts.fair}</button>
|
|
189
|
-
<button class="sl-filter-pill sl-filter-pill--green" data-filter="green">🟢 ${greenCount}</button>
|
|
190
|
-
<button class="sl-filter-pill sl-filter-pill--gray" data-filter="gray">⚪ ${gradeCounts.unscored}</button>
|
|
191
|
-
<span class="sl-health-rate">${healthRateText}</span>
|
|
192
|
-
`;
|
|
193
|
-
return `<div class="sl-summary-card">
|
|
194
|
-
<div class="sl-summary-line">${overview}</div>
|
|
195
|
-
<div class="sl-summary-pills">${gradeRow}</div>
|
|
84
|
+
function renderPanel(a, lang) {
|
|
85
|
+
const zh = lang === 'zh';
|
|
86
|
+
const dTotal = a.dP + a.dW + a.dF;
|
|
87
|
+
const dScore = dTotal > 0 ? Math.round(((a.dP + a.dW * 0.5) / dTotal) * 100) : null;
|
|
88
|
+
// 页头(品牌已在常驻顶栏,这里不再重复 brand badge)。
|
|
89
|
+
const phead = `<div class="phead">
|
|
90
|
+
<h1>${zh ? 'Skill 健康评测工作台' : 'Skill Health Dashboard'}</h1>
|
|
91
|
+
<span class="phead-sub">${a.totalSkills} ${zh ? '个 skill' : 'skills'}</span>
|
|
92
|
+
<span class="phead-when">${icon('clock', { size: 13 })} ${zh ? '更新于' : 'updated'} ${fmtDate(a.lastTs)}</span>
|
|
93
|
+
</div>`;
|
|
94
|
+
// 综合健康(细线总览首格):ring + 标签 + 说明入口。
|
|
95
|
+
const ovMain = `<div class="ov-cell ov-main">
|
|
96
|
+
${ring(a.composite, 60, 6)}
|
|
97
|
+
<div class="ov-main-lbl"><b>${zh ? '综合健康' : 'Overall health'}<span class="ov-help" onclick="document.getElementById('ep-help-modal').style.display='flex'">${IC.helpCircle}</span></b><span>${zh ? '加权 体检 / 评测 / 观察' : 'Weighted doctor / eval / observe'}</span></div>
|
|
98
|
+
</div>`;
|
|
99
|
+
const doctorCell = `<div class="ov-cell">
|
|
100
|
+
<div class="ov-top">${icon('doctor', { size: 15, cls: 'ov-ico' })}<span>${zh ? '健康体检' : 'Doctor'}</span></div>
|
|
101
|
+
${dScore != null
|
|
102
|
+
? `<div class="ov-big" style="color:${scoreColor(dScore)}">${dScore}<small> / 100</small></div>
|
|
103
|
+
<div class="ov-meta"><span class="c-pass">${zh ? '通过' : 'Pass'} <i>${a.dP}</i></span><span class="c-warn">${zh ? '警告' : 'Warn'} <i>${a.dW}</i></span><span class="c-fail">${zh ? '失败' : 'Fail'} <i>${a.dF}</i></span></div>`
|
|
104
|
+
: `<div class="ov-big ov-empty-v">${zh ? '未运行' : 'Not run'}</div>`}
|
|
196
105
|
</div>`;
|
|
106
|
+
const evalCell = `<div class="ov-cell">
|
|
107
|
+
<div class="ov-top">${icon('eval', { size: 15, cls: 'ov-ico' })}<span>${zh ? '用例评测' : 'Eval'}</span></div>
|
|
108
|
+
${a.eAvg != null
|
|
109
|
+
? `<div class="ov-big" style="color:${scoreColor(Math.round((a.eAvg / 5) * 100))}">${a.eAvg.toFixed(1)}<small> / 5</small></div>
|
|
110
|
+
<div class="ov-meta"><span>${zh ? '断言' : 'Assert'} <i>${a.eAssertP}/${a.eAssertT}</i></span><span>${zh ? '用例' : 'Samples'} <i>${a.eSamples}</i></span></div>`
|
|
111
|
+
: `<div class="ov-big ov-empty-v">${zh ? '未运行' : 'Not run'}</div>`}
|
|
112
|
+
</div>`;
|
|
113
|
+
const obsCell = `<div class="ov-cell">
|
|
114
|
+
<div class="ov-top">${icon('observe', { size: 15, cls: 'ov-ico' })}<span>${zh ? '生产观察' : 'Observe'}</span></div>
|
|
115
|
+
${a.withObs > 0
|
|
116
|
+
? `<div class="ov-big">${a.withObs}<small> skill</small></div><div class="ov-meta ov-meta--muted">${zh ? '个 skill 有线上数据' : 'with traces'}</div>`
|
|
117
|
+
: `<div class="ov-big ov-empty-v">${zh ? '暂无数据' : 'No data'}</div><div class="ov-meta ov-meta--muted">${zh ? '接入线上调用后自动采集' : 'Auto-collected once invoked'}</div>`}
|
|
118
|
+
</div>`;
|
|
119
|
+
return `${phead}<div class="ov">${ovMain}${doctorCell}${evalCell}${obsCell}</div>
|
|
120
|
+
|
|
121
|
+
<div id="ep-help-modal" class="ep-modal-overlay" onclick="if(event.target===this)this.style.display='none'">
|
|
122
|
+
<div class="ep-modal">
|
|
123
|
+
<div class="ep-modal-head"><strong>${zh ? '综合健康计算说明' : 'Health Score'}</strong><button onclick="document.getElementById('ep-help-modal').style.display='none'">✕</button></div>
|
|
124
|
+
<div class="ep-modal-body">
|
|
125
|
+
<p><b>${zh ? '综合健康' : 'Overall Health'}</b> = ${zh ? '已评分 skill 综合分的算术平均(未跑任何可信维度的 skill 不计入)' : 'Arithmetic mean of scored skills’ composites (skills with no trusted dimension are excluded)'}。</p>
|
|
126
|
+
<p>${zh ? '单 skill 综合分 = 已运行维度各自归一到 0-100 后取算术平均:健康体检 =(通过 + 0.5×警告)/ 规则数;用例评测 = 综合分 / 5;生产观察 = 覆盖率 × 色带系数(黄 0.85 / 红 0.6),样本量不足时不计入。' : 'Per-skill composite = arithmetic mean of the dimensions that ran, each normalized to 0-100: doctor = (pass + 0.5×warn) / rules; eval = composite / 5; observe = coverage × band factor (yellow 0.85 / red 0.6), skipped when underpowered.'}</p>
|
|
127
|
+
<p>${zh ? '下方三格分别为:体检按规则数合并的通过率、评测为各 skill 综合分的平均、观察为有线上数据的 skill 数。' : 'The three cells below: doctor pools rule counts, eval averages per-skill composites, observe counts skills with traces.'}</p>
|
|
128
|
+
<p>${zh ? '分数配色:≥85 绿 / 60-84 琥珀 / <60 红。' : 'Score colors: ≥85 green / 60-84 amber / <60 red.'}</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>`;
|
|
197
132
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
133
|
+
// renderSkillModal 已移除:首页点行直接进 skill 详情页(doctor/eval/observe 已在该页合一),弹框预览冗余。
|
|
134
|
+
function renderRow(entry, insights, langQ, lang) {
|
|
135
|
+
const h = assessHealth(entry, insights, lang);
|
|
136
|
+
// 点行直接进该 skill 最新 eval 报告(无 eval 则最新 doctor);跨维度/历史在报告页内切换。
|
|
137
|
+
const amp = lang === DEFAULT_LANG ? '' : `&lang=${lang}`;
|
|
138
|
+
const href = entry.eval
|
|
139
|
+
? `/reports/${encodeURIComponent(entry.eval.reportId)}${langQ}`
|
|
140
|
+
: entry.doctor
|
|
141
|
+
? `/doctors/${encodeURIComponent(entry.doctor.reportId)}?skill=${encodeURIComponent(entry.skillName)}${amp}`
|
|
142
|
+
: `/runs${langQ}`;
|
|
143
|
+
const dT = entry.doctor ? entry.doctor.passCount + entry.doctor.warnCount + entry.doctor.failCount : 0;
|
|
144
|
+
const dP = dT > 0 ? Math.round(((entry.doctor.passCount + entry.doctor.warnCount * 0.5) / dT) * 100) : null;
|
|
145
|
+
const eP = entry.eval?.compositeScore != null ? Math.round((entry.eval.compositeScore / 5) * 100) : null;
|
|
146
|
+
const oP = entry.observe ? Math.round((1 - entry.observe.gapRate) * 100) : null;
|
|
147
|
+
const mkBar = (p, t) => { const c = p != null ? scoreColor(p) : '#edf0f7'; return `<div class="t-bar" title="${e(t)} ${p ?? '—'}"><div class="t-bar-f" style="width:${p ?? 0}%;background:${c}"></div></div>`; };
|
|
148
|
+
const hasScore = h.score != null;
|
|
149
|
+
const clr = hasScore ? scoreColor(h.score) : '#9ca3af';
|
|
150
|
+
const lastTs = [entry.doctor?.timestamp, entry.eval?.timestamp, entry.observe?.generatedAt].filter((s) => Boolean(s)).sort().pop() ?? null;
|
|
151
|
+
const reportCount = entry.doctorHistory.length + entry.evalHistory.length + entry.observeHistory.length;
|
|
152
|
+
const zh = lang === 'zh';
|
|
153
|
+
let doctorStatus;
|
|
154
|
+
if (!entry.doctor) {
|
|
155
|
+
doctorStatus = `<span class="t-dash">—</span>`;
|
|
205
156
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
items.push(lang === 'zh'
|
|
209
|
-
? `👀 ${noObserve} 个 skill 没跑过 observe — 跑 omk observe 接生产数据`
|
|
210
|
-
: `👀 ${noObserve} skills missing observe — run omk observe`);
|
|
157
|
+
else if (entry.doctor.failCount === 0 && entry.doctor.warnCount === 0) {
|
|
158
|
+
doctorStatus = `<span class="t-chk t-chk--ok">✓ ${zh ? '全通过' : 'All pass'}</span>`;
|
|
211
159
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
160
|
+
else {
|
|
161
|
+
const parts = [`<span class="t-c-pass">${entry.doctor.passCount} ${zh ? '过' : 'P'}</span>`];
|
|
162
|
+
if (entry.doctor.warnCount > 0)
|
|
163
|
+
parts.push(`<span class="t-c-warn">${entry.doctor.warnCount} ${zh ? '警' : 'W'}</span>`);
|
|
164
|
+
if (entry.doctor.failCount > 0)
|
|
165
|
+
parts.push(`<span class="t-c-fail">${entry.doctor.failCount} ${zh ? '败' : 'F'}</span>`);
|
|
166
|
+
doctorStatus = `<span class="t-chk">${parts.join('')}</span>`;
|
|
216
167
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
168
|
+
const evalTotal = entry.eval ? entry.eval.passCount + entry.eval.failCount : 0;
|
|
169
|
+
const evalStatus = entry.eval
|
|
170
|
+
? `<span class="t-eval-meta"><b>${entry.eval.compositeScore != null ? entry.eval.compositeScore.toFixed(1) : '-'}</b><span class="t-eval-unit">/5</span> <span class="t-eval-sep">·</span> ${evalTotal > 0 ? `${entry.eval.passCount}/${evalTotal}` : '-'} <span class="t-eval-dim">${entry.eval.totalSamples}${zh ? '例' : ''}</span></span>`
|
|
171
|
+
: `<span class="t-dash">—</span>`;
|
|
172
|
+
// 整行点击直接进 skill 详情页(去掉中间弹框,更流畅)。
|
|
173
|
+
// 跳转地址走 data-href + 底部脚本的事件委托,不内联 onclick:skill 名含引号时
|
|
174
|
+
// encodeURIComponent 不会编码单引号,裸拼进 onclick 字符串会变成 JS 注入面。
|
|
175
|
+
return `<tr class="t-row" data-color="${h.color}" data-name="${e(entry.skillName.toLowerCase())}" data-href="${e(href)}" style="cursor:pointer">
|
|
176
|
+
<td class="t-skill"><span class="t-name">${e(entry.skillName)}</span></td>
|
|
177
|
+
<td class="t-score">${hasScore ? `<div class="t-eval"><div class="t-eval-num" style="color:${clr}"><strong>${h.score}</strong></div><div class="t-eval-bars">${mkBar(dP, zh ? '体检' : 'Doctor')}${mkBar(eP, zh ? '评测' : 'Eval')}${mkBar(oP, zh ? '观察' : 'Observe')}</div></div>` : `<span class="t-na">${zh ? '未评测' : 'N/A'}</span>`}</td>
|
|
178
|
+
<td>${doctorStatus}</td>
|
|
179
|
+
<td>${evalStatus}</td>
|
|
180
|
+
<td class="t-reports">${reportCount || '-'}</td>
|
|
181
|
+
<td class="t-time">${fmtShort(lastTs)}</td>
|
|
182
|
+
<td class="t-act"><a href="${e(href)}" class="t-link">${zh ? '查看详情' : 'Details'} →</a></td>
|
|
183
|
+
</tr>`;
|
|
223
184
|
}
|
|
224
|
-
const
|
|
225
|
-
/*
|
|
226
|
-
.
|
|
227
|
-
.
|
|
228
|
-
.
|
|
229
|
-
.
|
|
230
|
-
.
|
|
231
|
-
.
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
.
|
|
235
|
-
.
|
|
236
|
-
.
|
|
237
|
-
.
|
|
238
|
-
.
|
|
185
|
+
const CSS = `
|
|
186
|
+
/* ═══ 总览(细线 4 格,低饱和) ═══ */
|
|
187
|
+
.phead{display:flex;align-items:baseline;gap:12px;margin-bottom:18px}
|
|
188
|
+
.phead h1{font-size:20px;font-weight:700;margin:0;color:var(--text-primary);letter-spacing:-.01em}
|
|
189
|
+
.phead-sub{color:var(--text-muted);font-size:13px}
|
|
190
|
+
.phead-when{margin-left:auto;color:var(--text-muted);font-size:12.5px;display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}
|
|
191
|
+
.phead-when svg{color:var(--text-faint)}
|
|
192
|
+
.ov{display:grid;grid-template-columns:220px 1fr 1fr 1fr;gap:1px;background:var(--border);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;margin-bottom:22px;box-shadow:0 8px 24px rgba(31,41,55,4%)}
|
|
193
|
+
.ov-cell{background:#fff;padding:18px 20px}
|
|
194
|
+
.ov-main{display:flex;align-items:center;gap:16px}
|
|
195
|
+
.ov-main-lbl b{display:flex;align-items:center;gap:5px;font-size:13.5px;color:var(--text-primary);font-weight:600;margin-bottom:3px}
|
|
196
|
+
.ov-main-lbl span{font-size:12px;color:var(--text-secondary)}
|
|
197
|
+
.ov-help{display:inline-flex;color:var(--text-faint);cursor:pointer}
|
|
198
|
+
.ov-help:hover{color:var(--accent)}
|
|
199
|
+
.ov-top{display:flex;align-items:center;gap:8px;color:var(--text-secondary);font-size:12.5px;font-weight:500;margin-bottom:12px}
|
|
200
|
+
.ov-ico{color:var(--text-muted)}
|
|
201
|
+
.ov-big{font-size:26px;font-weight:700;letter-spacing:-.02em;line-height:1;color:var(--text-primary);font-variant-numeric:tabular-nums}
|
|
202
|
+
.ov-big small{font-size:13px;color:var(--text-muted);font-weight:500}
|
|
203
|
+
.ov-empty-v{font-size:14px;color:var(--text-muted);font-weight:500}
|
|
204
|
+
.ov-meta{margin-top:10px;display:flex;flex-wrap:wrap;gap:12px;font-size:12px;color:var(--text-secondary)}
|
|
205
|
+
.ov-meta i{font-style:normal;font-weight:600;font-variant-numeric:tabular-nums}
|
|
206
|
+
.ov-meta--muted{color:var(--text-muted)}
|
|
207
|
+
.c-pass{color:#1f9d63!important}.c-warn{color:#d97706!important}.c-fail{color:#dc2626!important}.c-na{color:#9ca3af!important}
|
|
208
|
+
@media(max-width:860px){.ov{grid-template-columns:1fr 1fr}.ov-main{grid-column:1 / -1}}
|
|
209
|
+
@media(max-width:560px){.ov{grid-template-columns:1fr}.ov-main{grid-column:auto}}
|
|
239
210
|
|
|
240
|
-
/*
|
|
241
|
-
.
|
|
211
|
+
/* ═══ Help Modal ═══ */
|
|
212
|
+
.ep-modal-overlay{display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.45);align-items:center;justify-content:center}
|
|
213
|
+
.ep-modal{background:#fff;border-radius:12px;max-width:520px;width:90%;padding:24px;box-shadow:0 20px 60px rgba(0,0,0,.18)}
|
|
214
|
+
.ep-modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
|
215
|
+
.ep-modal-head strong{font-size:16px;color:#182033}
|
|
216
|
+
.ep-modal-head button{background:none;border:none;font-size:18px;color:#637083;cursor:pointer;padding:4px 8px;border-radius:4px}
|
|
217
|
+
.ep-modal-head button:hover{background:#f8f9fd}
|
|
218
|
+
.ep-modal-body{color:#637083;font-size:13px;line-height:1.7}
|
|
219
|
+
.ep-modal-body p{margin:0 0 8px}
|
|
220
|
+
.ep-modal-body b{color:#182033}
|
|
242
221
|
|
|
243
|
-
|
|
244
|
-
.
|
|
245
|
-
.
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
.
|
|
249
|
-
.
|
|
222
|
+
/* ═══ Table Panel ═══ */
|
|
223
|
+
.t-panel{padding:12px 14px;background:#fff;border:1px solid #e4e8f1;border-radius:8px;box-shadow:0 8px 24px rgba(31,41,55,4%)}
|
|
224
|
+
.t-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}
|
|
225
|
+
.t-head-l{display:inline-flex;align-items:center;gap:8px}
|
|
226
|
+
.t-head h2{margin:0;color:#182033;font-size:16px;font-weight:700;line-height:1.4}
|
|
227
|
+
.t-head-ct{font-size:12px;font-weight:500;color:#637083}
|
|
228
|
+
.t-filters{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px}
|
|
229
|
+
.t-search{width:220px;padding:5px 10px 5px 30px;border:1px solid #e4e8f1;border-radius:6px;font-size:13px;color:#182033;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='M21 21l-4.3-4.3'/%3E%3C/svg%3E") 8px center no-repeat;outline:none;font-family:inherit}
|
|
230
|
+
.t-search:focus{border-color:#4f46e5;box-shadow:0 0 0 2px rgba(79,70,229,10%)}
|
|
231
|
+
.t-seg{display:inline-flex;padding:2px;background:#f0f1f3;border-radius:6px;gap:0}
|
|
232
|
+
.t-seg-btn{padding:4px 12px;font-size:12px;font-weight:400;border:none;cursor:pointer;background:transparent;color:#637083;transition:all .2s;outline:none;font-family:inherit;border-radius:4px}
|
|
233
|
+
.t-seg-btn:hover{color:#182033}
|
|
234
|
+
.t-seg-btn--on{background:#fff;color:#182033;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
250
235
|
|
|
251
|
-
/*
|
|
252
|
-
.
|
|
253
|
-
.
|
|
254
|
-
.
|
|
255
|
-
.
|
|
256
|
-
.
|
|
257
|
-
.
|
|
258
|
-
.
|
|
259
|
-
.
|
|
260
|
-
.
|
|
261
|
-
.
|
|
262
|
-
.
|
|
263
|
-
.
|
|
264
|
-
.
|
|
236
|
+
/* ═══ Table ═══ */
|
|
237
|
+
.t-tbl{width:100%;border-collapse:collapse}
|
|
238
|
+
.t-tbl th{text-align:left;padding:10px 16px;font-size:11px;font-weight:600;color:#9ca3af;background:transparent;border-bottom:1px solid #e4e8f1;white-space:nowrap;letter-spacing:.03em;text-transform:uppercase}
|
|
239
|
+
.t-tbl td{text-align:left;padding:18px 16px;border-bottom:1px solid #f1f3f7;vertical-align:middle}
|
|
240
|
+
.t-tbl tr:last-child td{border-bottom:none}
|
|
241
|
+
.t-tbl tbody tr{transition:background .12s}
|
|
242
|
+
.t-tbl tbody tr:hover td{background:#fafbff}
|
|
243
|
+
.t-skill{min-width:0}
|
|
244
|
+
.t-name{color:#182033;text-decoration:none;font-size:14px;font-weight:600;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
245
|
+
.t-name:hover{color:#4f46e5}
|
|
246
|
+
.t-score{}
|
|
247
|
+
.t-eval{display:flex;align-items:center;gap:10px}
|
|
248
|
+
.t-eval-num{flex-shrink:0;width:30px;text-align:right}
|
|
249
|
+
.t-eval-num strong{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
|
|
250
|
+
.t-eval-bars{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
|
|
251
|
+
.t-bar{height:3px;background:#eef1f6;border-radius:2px;overflow:hidden;cursor:pointer}
|
|
252
|
+
.t-bar-f{height:100%;border-radius:2px}
|
|
253
|
+
.t-na{display:inline-block;padding:2px 10px;font-size:12px;color:#9ca3af;background:#f6f8fb;border-radius:10px}
|
|
254
|
+
.t-chk{display:inline-flex;gap:8px;font-size:12px;font-variant-numeric:tabular-nums}
|
|
255
|
+
.t-chk--ok{color:#9ca3af;font-weight:500}
|
|
256
|
+
.t-c-pass{color:#1f9d63;font-weight:600}
|
|
257
|
+
.t-c-warn{color:#d97706;font-weight:600}
|
|
258
|
+
.t-c-fail{color:#dc2626;font-weight:600}
|
|
259
|
+
.t-dash{color:#cbd2dd}
|
|
260
|
+
.t-eval-meta{font-size:12.5px;color:#637083;font-variant-numeric:tabular-nums}
|
|
261
|
+
.t-eval-meta b{font-size:13px;font-weight:700;color:#182033}
|
|
262
|
+
.t-eval-unit{color:#9ca3af}
|
|
263
|
+
.t-eval-sep{color:#cbd2dd;margin:0 2px}
|
|
264
|
+
.t-eval-dim{color:#9ca3af}
|
|
265
|
+
.t-reports{font-size:13px;color:#637083;font-variant-numeric:tabular-nums}
|
|
266
|
+
.t-time{font-size:12px;color:#9ca3af;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
267
|
+
.t-act{}
|
|
268
|
+
.t-link{font-size:12px;color:#4f46e5;text-decoration:none;font-weight:500;transition:color .15s;opacity:0}
|
|
269
|
+
.t-tbl tbody tr:hover .t-link{opacity:1}
|
|
270
|
+
.t-link:hover{text-decoration:underline}
|
|
271
|
+
.t-row--hidden{display:none!important}
|
|
272
|
+
.t-filt-out{display:none!important}
|
|
265
273
|
|
|
266
|
-
/*
|
|
267
|
-
.
|
|
268
|
-
.
|
|
269
|
-
.
|
|
270
|
-
.
|
|
271
|
-
.
|
|
272
|
-
.sl-card-metrics { display:flex;gap:8px;flex-wrap:wrap }
|
|
273
|
-
.sl-metric { font-size:11.5px;color:var(--text-secondary);font-variant-numeric:tabular-nums;background:var(--bg-soft);padding:3px 9px;border-radius:5px;line-height:1.5 }
|
|
274
|
-
.sl-warn-badge { display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:11px;font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;line-height:1.4 }
|
|
275
|
-
.sl-warn-badge--high { background:rgba(156,74,63,.14);color:#9c4a3f }
|
|
276
|
-
.sl-warn-badge--med { background:rgba(176,128,48,.14);color:#b08030 }
|
|
277
|
-
.sl-warn-badge--ok { background:rgba(94,130,82,.14);color:#5e8252 }
|
|
278
|
-
.sl-trend { font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;padding:1px 7px;border-radius:8px;letter-spacing:0.01em }
|
|
279
|
-
.sl-trend--up { background:rgba(94,130,82,.14);color:#5e8252 }
|
|
280
|
-
.sl-trend--down { background:rgba(156,74,63,.14);color:#9c4a3f }
|
|
281
|
-
.sl-trend--flat { background:var(--bg-soft);color:var(--text-muted) }
|
|
282
|
-
.sl-card-arrow { color:var(--text-muted);font-size:18px;font-weight:300;line-height:1;margin-top:2px }
|
|
274
|
+
/* ═══ Pager ═══ */
|
|
275
|
+
.t-pager{display:flex;align-items:center;justify-content:flex-end;gap:6px;padding:12px 14px 4px;font-size:12px;color:#637083}
|
|
276
|
+
.t-pg-btn{padding:4px 10px;border:1px solid #e4e8f1;border-radius:4px;background:#fff;color:#637083;cursor:pointer;font-size:12px;font-family:inherit;outline:none}
|
|
277
|
+
.t-pg-btn:hover{border-color:#4f46e5;color:#4f46e5}
|
|
278
|
+
.t-pg-btn:disabled{opacity:.4;cursor:default;border-color:#e4e8f1;color:#9ca3af}
|
|
279
|
+
.t-pg-info{font-variant-numeric:tabular-nums}
|
|
283
280
|
|
|
284
|
-
/*
|
|
285
|
-
.sl-next-steps { margin-top:24px;padding:14px 18px;background:var(--bg-soft);border-radius:6px }
|
|
286
|
-
.sl-next-steps-title { font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:8px;letter-spacing:0.02em }
|
|
287
|
-
.sl-next-steps ul { margin:0;padding-left:18px;color:var(--text-secondary);font-size:13.5px;line-height:1.8 }
|
|
288
|
-
.sl-empty-state { text-align:center;padding:60px 20px;color:var(--text-muted) }
|
|
289
|
-
.sl-empty-state code { background:var(--bg-soft);padding:2px 6px;border-radius:3px;font-family:"SF Mono",Menlo,monospace;font-size:13px }
|
|
290
|
-
.sl-legacy-hint { margin-top:24px;text-align:center;font-size:11.5px;color:var(--text-muted) }
|
|
291
|
-
.sl-legacy-hint a { color:var(--accent) }
|
|
281
|
+
/* ═══ Legacy ═══ */
|
|
292
282
|
|
|
293
|
-
/*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
283
|
+
/* ═══ Skill Detail Modal ═══ */
|
|
284
|
+
.sd-overlay{display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.45);align-items:center;justify-content:center}
|
|
285
|
+
.sd-modal{background:#fff;border-radius:12px;width:90%;max-width:760px;max-height:85vh;overflow:hidden;position:relative;box-shadow:0 20px 60px rgba(0,0,0,.18);display:flex;flex-direction:column}
|
|
286
|
+
.sd-close{position:absolute;top:14px;right:14px;background:none;border:none;font-size:18px;color:#637083;cursor:pointer;padding:4px 8px;border-radius:4px;z-index:1}
|
|
287
|
+
.sd-close:hover{background:#f8f9fd;color:#182033}
|
|
288
|
+
.sd-body{padding:24px 24px 22px;overflow-y:auto}
|
|
289
|
+
.sd-header{padding-bottom:16px;margin-bottom:16px;border-bottom:1px solid #e4e8f1}
|
|
290
|
+
.sd-header h2{margin:0;font-size:18px;font-weight:700;color:#182033;letter-spacing:-.2px}
|
|
291
|
+
.sd-meta{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:8px;color:#637083;font-size:12px}
|
|
292
|
+
.sd-meta span{display:inline-flex;align-items:center;gap:4px}
|
|
293
|
+
.sd-overall{display:flex;align-items:center;justify-content:center;gap:24px;padding:20px 24px;margin-bottom:16px;background:linear-gradient(135deg,#f8faff 0%,#f4f0ff 100%);border:1px solid #e4e8f1;border-radius:12px}
|
|
294
|
+
.sd-overall-meta{display:flex;flex-direction:column;align-items:flex-start;gap:4px}
|
|
295
|
+
.sd-overall-label{color:#637083;font-size:12px;font-weight:500}
|
|
296
|
+
.sd-overall-grade{display:inline-flex;align-items:baseline;gap:8px;font-size:28px;font-weight:800;line-height:1}
|
|
297
|
+
.sd-overall-grade small{font-size:12px;font-weight:600;color:#637083}
|
|
298
|
+
.sd-overall-hint{color:#637083;font-size:11px}
|
|
299
|
+
.sd-dim{position:relative;padding:14px 16px;margin-bottom:12px;background:#fff;border:1px solid #e4e8f1;border-radius:10px}
|
|
300
|
+
.sd-dim:last-of-type{margin-bottom:0}
|
|
301
|
+
.dim-doctor{background:rgba(37,99,235,6%);border-color:rgba(37,99,235,18%)}
|
|
302
|
+
.dim-eval{background:rgba(124,58,237,6%);border-color:rgba(124,58,237,18%)}
|
|
303
|
+
.dim-observe{background:rgba(5,150,105,6%);border-color:rgba(5,150,105,18%)}
|
|
304
|
+
.sd-dim-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}
|
|
305
|
+
.sd-dim-title{display:flex;align-items:center;gap:8px;color:#182033;font-size:14px;font-weight:700}
|
|
306
|
+
.sd-dim-title svg{flex-shrink:0;box-sizing:content-box;width:16px;height:16px;padding:6px;border-radius:8px}
|
|
307
|
+
.dim-doctor .sd-dim-title svg{color:#2563eb;background:rgba(37,99,235,12%)}
|
|
308
|
+
.dim-eval .sd-dim-title svg{color:#7c3aed;background:rgba(124,58,237,12%)}
|
|
309
|
+
.dim-observe .sd-dim-title svg{color:#059669;background:rgba(5,150,105,12%)}
|
|
310
|
+
.sd-dim-title span{font-size:14px;font-weight:700;color:#182033}
|
|
311
|
+
.sd-dim-title small{font-size:12px;font-weight:500;color:#637083;margin-left:4px}
|
|
312
|
+
.sd-dim-score{font-size:24px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1}
|
|
313
|
+
.sd-dim-score small{font-size:12px;font-weight:500;color:#637083}
|
|
314
|
+
.sd-dim-na{font-size:12px;color:#637083}
|
|
315
|
+
.sd-dim-bar{height:6px;background:rgba(255,255,255,70%);border-radius:4px;overflow:hidden;margin-bottom:12px}
|
|
316
|
+
.sd-dim-bar-fill{height:100%;border-radius:4px;transition:width .4s}
|
|
317
|
+
.sd-dim-empty{padding:12px 14px;color:#637083;font-size:12px;line-height:1.5;background:rgba(100,116,139,6%);border-radius:6px}
|
|
318
|
+
.m-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
|
319
|
+
.m-grid:has(> :nth-child(3):last-child){grid-template-columns:repeat(3,1fr)}
|
|
320
|
+
.m-cell{display:flex;align-items:center;gap:8px;padding:8px 10px;background:#fff;border:1px solid rgba(0,0,0,4%);border-radius:8px}
|
|
321
|
+
.m-cell-icon{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:7px;flex-shrink:0}
|
|
322
|
+
.m-cell-meta{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
323
|
+
.m-cell-meta span{font-size:11px;color:#637083;font-weight:500;line-height:1.2}
|
|
324
|
+
.m-cell-meta b{font-size:16px;font-weight:800;color:#182033;font-variant-numeric:tabular-nums;line-height:1.1}
|
|
325
|
+
.m-cell-meta b small{font-size:11px;font-weight:600;color:#637083;margin-left:1px}
|
|
326
|
+
.m-pass .m-cell-icon{background:rgba(31,157,99,14%);color:#1f9d63}
|
|
327
|
+
.m-warn .m-cell-icon{background:rgba(217,119,6,16%);color:#d97706}
|
|
328
|
+
.m-fail .m-cell-icon{background:rgba(220,38,38,14%);color:#dc2626}
|
|
329
|
+
.m-total .m-cell-icon{background:rgba(37,99,235,14%);color:#2563eb}
|
|
330
|
+
.m-eval .m-cell-icon{background:rgba(124,58,237,14%);color:#7c3aed}
|
|
331
|
+
.m-avg .m-cell-icon{background:rgba(99,102,241,14%);color:#6366f1}
|
|
332
|
+
.sd-history{margin-top:10px;padding-top:8px;border-top:1px solid #e4e8f1}
|
|
333
|
+
.sd-history-title{font-size:11px;font-weight:600;color:#637083;margin-bottom:6px}
|
|
334
|
+
.sd-history-item{display:flex;align-items:center;gap:10px;padding:4px 8px;border-radius:4px;font-size:12px;background:rgba(79,70,229,4%);font-weight:500;color:#182033}
|
|
335
|
+
.sd-history-date{min-width:100px;color:#637083}
|
|
336
|
+
.sd-history-score{font-weight:700;font-variant-numeric:tabular-nums;min-width:30px}
|
|
337
|
+
.sd-history-link{margin-left:auto;color:#4f46e5;font-weight:500;text-decoration:none;white-space:nowrap}
|
|
338
|
+
.sd-history-link:hover{text-decoration:underline}
|
|
339
|
+
.sd-guide{display:flex;align-items:center;gap:6px;margin-top:16px;padding:8px 12px;background:rgba(79,70,229,5%);border:1px solid rgba(79,70,229,12%);border-radius:6px;font-size:12px;color:#637083}
|
|
340
|
+
.sd-guide a{color:#4f46e5;font-weight:600;text-decoration:none}
|
|
341
|
+
.sd-guide a:hover{text-decoration:underline}
|
|
299
342
|
`;
|
|
300
343
|
export function renderSkillList(idx, lang = DEFAULT_LANG) {
|
|
301
344
|
const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
? '
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
345
|
+
const iMap = idx.insightsBySkill;
|
|
346
|
+
const a = aggregate(idx.entries, iMap, lang);
|
|
347
|
+
const zh = lang === 'zh';
|
|
348
|
+
if (idx.entries.length === 0) {
|
|
349
|
+
return layout('OMK Studio', `<main><div class="t-panel"><div style="display:flex;align-items:center;gap:16px;padding:24px 20px"><div style="display:flex;align-items:center;justify-content:center;width:48px;height:48px;color:#637083;background:#f8f9fd;border:1px solid #e4e8f1;border-radius:8px">${icon('eval', { size: 24 })}</div><div><strong style="color:#182033;font-size:14px">${zh ? '暂无 skill 报告' : 'No skill reports'}</strong><br><span style="color:#637083;font-size:12px">${zh ? '先跑一次评测:' : 'Run:'} <code style="padding:1px 6px;background:rgba(79,70,229,10%);color:#4f46e5;font-size:12px;border-radius:4px">omk eval --treatment my-skill</code></span></div></div></div></main><style>${CSS}</style>`, lang);
|
|
350
|
+
}
|
|
351
|
+
const rows = idx.entries.map((ent) => renderRow(ent, iMap.get(ent.skillName) ?? [], langQ, lang)).join('');
|
|
352
|
+
return layout(zh ? 'OMK 评测' : 'OMK Eval', `<main>
|
|
353
|
+
${renderPanel(a, lang)}
|
|
354
|
+
<div class="t-panel">
|
|
355
|
+
<div class="t-head">
|
|
356
|
+
<div class="t-head-l"><h2>${zh ? '明细' : 'Details'}</h2><span class="t-head-ct">${a.totalSkills} skill</span></div>
|
|
357
|
+
<div class="t-filters">
|
|
358
|
+
<input type="text" class="t-search" id="t-search" placeholder="${zh ? '搜索 skill 名称...' : 'Search...'}" />
|
|
359
|
+
<div class="t-seg">
|
|
360
|
+
<button class="t-seg-btn t-seg-btn--on" data-filter="all">${zh ? '全部' : 'All'}</button>
|
|
361
|
+
<button class="t-seg-btn" data-filter="red">${zh ? '异常' : 'Bad'}</button>
|
|
362
|
+
<button class="t-seg-btn" data-filter="yellow">${zh ? '待改进' : 'Fair'}</button>
|
|
363
|
+
<button class="t-seg-btn" data-filter="green">${zh ? '正常' : 'OK'}</button>
|
|
364
|
+
<button class="t-seg-btn" data-filter="gray">${zh ? '未评估' : 'N/A'}</button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
<div style="overflow-x:auto">
|
|
369
|
+
<table class="t-tbl">
|
|
370
|
+
<colgroup><col style="width:200px"><col style="width:180px"><col style="width:90px"><col style="width:130px"><col style="width:50px"><col style="width:100px"><col style="width:80px"></colgroup>
|
|
371
|
+
<thead><tr><th>Skill</th><th>${zh ? '综合分' : 'Score'}</th><th>Doctor</th><th>Eval</th><th>${zh ? '报告数' : 'Runs'}</th><th>${zh ? '更新时间' : 'Updated'}</th><th>${zh ? '操作' : ''}</th></tr></thead>
|
|
372
|
+
<tbody>${rows}</tbody>
|
|
373
|
+
</table>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="t-pager" id="t-pager"></div>
|
|
324
376
|
</div>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
(function(){
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
377
|
+
</main><style>${CSS}</style>
|
|
378
|
+
<script>!function(){
|
|
379
|
+
var PS=10,pg=1,bs=document.querySelectorAll('.t-seg-btn'),s=document.getElementById('t-search'),pgr=document.getElementById('t-pager');
|
|
380
|
+
function vis(){return Array.from(document.querySelectorAll('.t-row')).filter(function(r){return !r.classList.contains('t-filt-out')})}
|
|
381
|
+
function filt(){
|
|
382
|
+
var a=document.querySelector('.t-seg-btn--on'),fl=a?a.getAttribute('data-filter'):'all',q=(s?s.value:'').toLowerCase();
|
|
383
|
+
document.querySelectorAll('.t-row').forEach(function(r){r.classList.toggle('t-filt-out',!(( fl==='all'||r.getAttribute('data-color')===fl)&&(!q||(r.getAttribute('data-name')||'').indexOf(q)!==-1)))});
|
|
384
|
+
pg=1;page()
|
|
385
|
+
}
|
|
386
|
+
function page(){
|
|
387
|
+
var rs=vis(),t=rs.length,pp=Math.max(1,Math.ceil(t/PS));if(pg>pp)pg=pp;
|
|
388
|
+
var st=(pg-1)*PS,en=st+PS;
|
|
389
|
+
rs.forEach(function(r,i){r.classList.toggle('t-row--hidden',i<st||i>=en)});
|
|
390
|
+
if(pgr)pgr.innerHTML=t<=PS?'':\`<span class="t-pg-info">\${st+1}-\${Math.min(en,t)} / \${t}</span><button class="t-pg-btn" onclick="window.__pg(-1)" \${pg<=1?'disabled':''}>‹ ${zh ? '上一页' : 'Prev'}</button><button class="t-pg-btn" onclick="window.__pg(1)" \${pg>=pp?'disabled':''}>${zh ? '下一页' : 'Next'} ›</button>\`
|
|
391
|
+
}
|
|
392
|
+
window.__pg=function(d){pg+=d;page()};
|
|
393
|
+
bs.forEach(function(b){b.addEventListener('click',function(){bs.forEach(function(x){x.classList.remove('t-seg-btn--on')});b.classList.add('t-seg-btn--on');filt()})});
|
|
394
|
+
document.addEventListener('click',function(ev){var el=ev.target instanceof Element?ev.target:null;if(!el||el.closest('a,button'))return;var r=el.closest('.t-row[data-href]');if(r)location.href=r.getAttribute('data-href')});
|
|
395
|
+
if(s)s.addEventListener('input',filt);page()
|
|
396
|
+
}()</script>`, lang);
|
|
345
397
|
}
|