oh-my-knowledge 0.36.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 +86 -3
- package/dist/cli/commands/doctor.js +19 -4
- package/dist/cli/commands/evolve.d.ts +11 -0
- package/dist/cli/commands/evolve.js +105 -5
- package/dist/cli/commands/list.d.ts +5 -26
- package/dist/cli/commands/list.js +17 -142
- 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 +1 -1
- package/dist/cli/lib/i18n-dict/list.js +4 -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 +3 -1
- package/dist/cli/lib/i18n-dict.js +4 -0
- package/dist/cli/lib/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- 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 +8 -1
- package/dist/executors/codex-cli.js +5 -1
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -0
- package/dist/managed/list-view.d.ts +3 -0
- package/dist/managed/list-view.js +8 -4
- 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 +92 -3
- 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 +99 -99
- 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/types/doctor.d.ts +22 -0
- package/dist/types/managed.d.ts +17 -3
- package/package.json +1 -1
package/dist/renderer/summary.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { e, fmtNum, fmtCost, fmtDuration, COLORS, t } from './layout.js';
|
|
2
|
+
import { icon as svgIcon } from './icons.js';
|
|
2
3
|
import { generateAnalysisSummary } from '../analysis/report-diagnostics.js';
|
|
3
4
|
import { pValueCategory } from '../eval-core/statistics.js';
|
|
4
5
|
import { computeVerdict, ENSEMBLE_STRONG_PEARSON, ENSEMBLE_DISSENT_PEARSON } from '../eval-core/verdict.js';
|
|
@@ -363,111 +364,162 @@ export function renderSaturationCurve(saturation, variants, lang) {
|
|
|
363
364
|
</svg>
|
|
364
365
|
${noteHtml}`;
|
|
365
366
|
}
|
|
367
|
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
368
|
+
/**
|
|
369
|
+
* 六维雷达图(SVG)。把六维归一化到 0-1「越外越好」画多边形,直观看一个 variant
|
|
370
|
+
* 的强弱形状 / 多 variant 的形状差异。精确数值由下方数值表兜底(omk 受众要精确数字)。
|
|
371
|
+
*
|
|
372
|
+
* 归一化口径:事实/行为/评委 = 分÷5(绝对);成本/效率按绝对软参考($1/用例、120s/次
|
|
373
|
+
* 记为 0,越省越外);稳定性 = 1 − CV/0.3。null(未测量/单组无对照)画空心点、中性半径。
|
|
374
|
+
*/
|
|
375
|
+
/** 六维 i18n key → SVG 图标名(替代内嵌 emoji)。 */
|
|
376
|
+
const DIM_ICON = {
|
|
377
|
+
dimFact: 'fact', dimBehavior: 'behavior', dimJudge: 'judge',
|
|
378
|
+
dimCost: 'cost', dimEfficiency: 'efficiency', dimStability: 'stability', dimQuality: 'quality',
|
|
379
|
+
};
|
|
380
|
+
/** 取维度标签并去掉历史内嵌 emoji 前缀(i18n 串形如「📋 事实」,图标改由 SVG 渲染)。 */
|
|
381
|
+
function dimText(key, lang) {
|
|
382
|
+
return t(key, lang).replace(/^\S+\s+/, '');
|
|
383
|
+
}
|
|
384
|
+
// 雷达图例点击切换:全局只挂一次 document 委托,toggle 对应 .rad-series 的显隐。
|
|
385
|
+
const RADAR_TOGGLE_SCRIPT = `<script>
|
|
386
|
+
if(!window.__radarToggle){window.__radarToggle=1;document.addEventListener('click',function(ev){var b=ev.target.closest&&ev.target.closest('.rad-leg');if(!b)return;var w=b.closest('.sm-radar');if(!w)return;var g=w.querySelector('.rad-series[data-idx="'+b.getAttribute('data-idx')+'"]');var off=b.classList.toggle('off');b.setAttribute('aria-pressed',String(!off));if(g)g.style.display=off?'none':'';});}
|
|
387
|
+
</script>`;
|
|
388
|
+
function renderRadarChart(entries, dims, lang) {
|
|
389
|
+
const W = 300, H = 250, cx = 150, cy = 118, R = 82;
|
|
390
|
+
const n = dims.length;
|
|
391
|
+
const ang = (i) => (-90 + (360 / n) * i) * Math.PI / 180;
|
|
392
|
+
const pt = (g, i) => [cx + R * g * Math.cos(ang(i)), cy + R * g * Math.sin(ang(i))];
|
|
393
|
+
const fmtPt = (g, i) => pt(g, i).map((x) => x.toFixed(1)).join(',');
|
|
394
|
+
const rings = [0.25, 0.5, 0.75, 1].map((lv) => `<polygon points="${dims.map((_, i) => fmtPt(lv, i)).join(' ')}" class="rad-grid"/>`).join('');
|
|
395
|
+
const spokes = dims.map((_, i) => { const [x, y] = pt(1, i); return `<line x1="${cx}" y1="${cy}" x2="${x.toFixed(1)}" y2="${y.toFixed(1)}" class="rad-spoke"/>`; }).join('');
|
|
396
|
+
const labels = dims.map((d, i) => {
|
|
397
|
+
const [x, y] = pt(1.22, i);
|
|
398
|
+
const anchor = x < cx - 4 ? 'end' : x > cx + 4 ? 'start' : 'middle';
|
|
399
|
+
return `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" dominant-baseline="middle" class="rad-label">${e(d.label)}</text>`;
|
|
400
|
+
}).join('');
|
|
401
|
+
const polys = entries.map((en, idx) => {
|
|
402
|
+
const ptsStr = dims.map((d, i) => fmtPt(en.g[d.key] == null ? 0.5 : Math.max(0.04, en.g[d.key]), i)).join(' ');
|
|
403
|
+
const dots = dims.map((d, i) => {
|
|
404
|
+
const g = en.g[d.key];
|
|
405
|
+
const [x, y] = pt(g == null ? 0.5 : Math.max(0.04, g), i);
|
|
406
|
+
const na = g == null;
|
|
407
|
+
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3" class="rad-dot${na ? ' rad-dot--na' : ''}"${na ? '' : ` style="fill:${en.color}"`}><title>${e(en.name)} · ${e(d.label)}${na ? (lang === 'zh' ? ' 未测量/无对照' : ' n/a') : ''}</title></circle>`;
|
|
408
|
+
}).join('');
|
|
409
|
+
return `<g class="rad-series" data-idx="${idx}"><polygon points="${ptsStr}" class="rad-area" style="fill:${en.color};stroke:${en.color}"/>${dots}</g>`;
|
|
410
|
+
}).join('');
|
|
411
|
+
const svg = `<svg viewBox="0 0 ${W} ${H}" class="rad-svg" role="img" aria-label="${lang === 'zh' ? '六维雷达图' : 'Six-dimension radar'}">${rings}${spokes}${polys}${labels}</svg>`;
|
|
412
|
+
// 多 variant 时给一排可点击图例,点击切换该 variant 多边形显隐(单独看 / 对比子集)。
|
|
413
|
+
const legend = entries.length > 1
|
|
414
|
+
? `<div class="rad-legend">${entries.map((en, idx) => `<button type="button" class="rad-leg" data-idx="${idx}" aria-pressed="true" style="--c:${en.color}"><span class="rad-leg-dot"></span>${e(en.name)}</button>`).join('')}${RADAR_TOGGLE_SCRIPT}</div>`
|
|
415
|
+
: '';
|
|
416
|
+
return `<div class="sm-radar">${svg}${legend}</div>`;
|
|
417
|
+
}
|
|
366
418
|
export function renderSummaryCards(variants, summary, lang, variance) {
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// 仅保留在 report JSON 数据层 + Variance & Significance 表顶层 flat 字段(legacy)。
|
|
373
|
-
// 综合分放在第一列, 紧贴 实验分组. 它是 fact/behavior/judge 三层的等权均值,
|
|
374
|
-
// 在 omk 内部承担「跨 run 排序 / bootstrap CI / verdict 比较信号」三个角色 ——
|
|
375
|
-
// 这里展示 + 表头 ? button 弹 modal 公开计算方式 + 局限, 让用户知情消费。
|
|
376
|
-
// 其余六列保持 layer-first (fact / behavior / judge / cost / efficiency / stability)。
|
|
419
|
+
// 六维:事实 / 行为 / LLM 评价 / 成本 / 效率 / 稳定性。雷达图看形状,数值表给精确值。
|
|
420
|
+
// 综合分 (= (fact + behavior + judge) / 3) 并入数值表首列(大字号 + delta),不再单独一行一卡。
|
|
421
|
+
const isMulti = variants.length > 1;
|
|
422
|
+
const baselineComposite = summary[variants[0]]?.avgCompositeScore;
|
|
423
|
+
const scoringModalId = 'guide-scoring';
|
|
377
424
|
const headerCols = [
|
|
378
|
-
{ key: 'dimFact', label:
|
|
379
|
-
{ key: 'dimBehavior', label:
|
|
380
|
-
{ key: 'dimJudge', label:
|
|
381
|
-
{ key: 'dimCost', label:
|
|
382
|
-
{ key: 'dimEfficiency', label:
|
|
383
|
-
{ key: 'dimStability', label:
|
|
425
|
+
{ key: 'dimFact', label: dimText('dimFact', lang) },
|
|
426
|
+
{ key: 'dimBehavior', label: dimText('dimBehavior', lang) },
|
|
427
|
+
{ key: 'dimJudge', label: dimText('dimJudge', lang) },
|
|
428
|
+
{ key: 'dimCost', label: dimText('dimCost', lang) },
|
|
429
|
+
{ key: 'dimEfficiency', label: dimText('dimEfficiency', lang) },
|
|
430
|
+
{ key: 'dimStability', label: dimText('dimStability', lang) },
|
|
384
431
|
];
|
|
385
|
-
const scoringModalId = '
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
const
|
|
390
|
-
// 渲染单层分数 cell(事实/行为/LLM 评价通用)— 加 heatmap 浅色背景
|
|
391
|
-
function renderLayerCell(varianceMean, summaryValue, detailHtml = '') {
|
|
432
|
+
const compositeHint = `<button type="button" class="hint-btn" onclick="openModal('${scoringModalId}')" aria-label="${e(lang === 'zh' ? '综合分怎么算的?' : 'How is composite computed?')}" aria-haspopup="dialog">?</button>`;
|
|
433
|
+
const thead = `<tr><th data-i18n="variants">${t('variants', lang)}</th><th>${lang === 'zh' ? '综合' : 'Composite'} ${compositeHint}</th>${headerCols.map((c) => `<th data-i18n="${c.key}">${svgIcon(DIM_ICON[c.key], { size: 13, cls: 'sm-th-ico' })}${c.label}</th>`).join('')}</tr>`;
|
|
434
|
+
// 质量层(事实/行为/评委)cell — 1-5 分。只给数值上色(绿/黄/红),不再填单元格底色(底色块突兀;
|
|
435
|
+
// 维度强弱形状已由左侧雷达图传达,表格只需精确数值)。
|
|
436
|
+
const layerDim = (label, varianceMean, summaryValue, detailLines = [], iconName) => {
|
|
392
437
|
const v = varianceMean ?? summaryValue;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
: ''
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
:
|
|
400
|
-
|
|
401
|
-
|
|
438
|
+
const has = typeof v === 'number' && v > 0;
|
|
439
|
+
return {
|
|
440
|
+
label,
|
|
441
|
+
value: has ? v.toFixed(2) : '—',
|
|
442
|
+
valueColor: has ? (v >= 4 ? 'var(--green)' : v >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)',
|
|
443
|
+
bg: '',
|
|
444
|
+
primary: true,
|
|
445
|
+
detailLines,
|
|
446
|
+
icon: iconName,
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
// 注意:detailLines 是「已转义/可信 HTML」契约 —— 既有纯文本(数字 metric)也有原始
|
|
450
|
+
// HTML(如 stabDetails 的 <span>),dimCell/dimMetric 都不再转义。caller push 的内容
|
|
451
|
+
// 里凡含 user 字符串(skill 名 / 用例文本)必须先过 e()。当前全是数字,安全。
|
|
452
|
+
const dimCell = (d) => {
|
|
453
|
+
const cls = d.primary ? 'summary-value summary-value-primary' : 'summary-value';
|
|
454
|
+
const detail = d.detailLines.map((l) => `<div class="summary-detail">${l}</div>`).join('');
|
|
455
|
+
return `<td class="summary-cell"${d.bg ? ` style="background:${d.bg}"` : ''}${d.title ? ` title="${e(d.title)}"` : ''}><div class="${cls}" style="color:${d.valueColor}">${d.value}</div>${detail}</td>`;
|
|
456
|
+
};
|
|
457
|
+
const dimMetric = (d) => {
|
|
458
|
+
const detail = d.detailLines.length > 0 ? `<span class="sm-metric-detail">${d.detailLines.join(' · ')}</span>` : '';
|
|
459
|
+
const ico = d.icon ? svgIcon(d.icon, { size: 14, cls: 'sm-mico' }) : '';
|
|
460
|
+
return `<div class="sm-metric"${d.title ? ` title="${e(d.title)}"` : ''}><span class="sm-metric-label">${ico}${d.label}</span><span class="sm-metric-val" style="color:${d.valueColor}">${d.value}</span>${detail}</div>`;
|
|
461
|
+
};
|
|
462
|
+
// 综合分 cell(表格首列,大字号 + delta vs baseline)。
|
|
463
|
+
function renderCompositeCell(composite, i) {
|
|
464
|
+
const has = typeof composite === 'number' && composite > 0;
|
|
465
|
+
const color = has ? (composite >= 4 ? 'var(--green)' : composite >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)';
|
|
466
|
+
let delta = '';
|
|
467
|
+
if (isMulti && i > 0 && has && typeof baselineComposite === 'number' && baselineComposite > 0) {
|
|
468
|
+
const d = composite - baselineComposite;
|
|
469
|
+
if (Math.abs(d) >= 0.01) {
|
|
470
|
+
delta = `<div class="sm-delta" style="color:${d > 0 ? 'var(--green)' : 'var(--red)'}">${d > 0 ? '+' : ''}${d.toFixed(2)} ${d > 0 ? '↑' : '↓'}</div>`;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return `<td class="summary-cell sm-composite-cell"><div class="summary-value summary-value-primary" style="color:${color}">${has ? composite.toFixed(2) : '—'}</div>${delta}</td>`;
|
|
402
474
|
}
|
|
403
|
-
const
|
|
475
|
+
const avgLabel = lang === 'zh' ? '次' : 'req';
|
|
476
|
+
const costUnreportedTooltip = lang === 'zh'
|
|
477
|
+
? 'executor 不报 USD 成本(如 codex CLI),无法估算'
|
|
478
|
+
: 'executor does not report USD cost (e.g. codex CLI); not measurable';
|
|
479
|
+
let anyStabUnmeasured = false;
|
|
480
|
+
const perVariant = variants.map((v, i) => {
|
|
404
481
|
const s = summary[v] || {};
|
|
405
482
|
const vd = variance?.perVariant[v];
|
|
406
483
|
const color = COLORS[i % COLORS.length];
|
|
407
|
-
|
|
484
|
+
const factVal = vd?.byLayer?.fact?.mean ?? s.avgFactScore;
|
|
485
|
+
const behaviorVal = vd?.byLayer?.behavior?.mean ?? s.avgBehaviorScore;
|
|
486
|
+
const judgeVal = vd?.byLayer?.judge?.mean ?? s.avgJudgeScore;
|
|
408
487
|
const factDetailParts = [];
|
|
409
|
-
if (s.avgFactVerifiedRate != null)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
const factDetail = factDetailParts.length > 0 ? `<div class="summary-detail">${factDetailParts.join(' · ')}</div>` : '';
|
|
414
|
-
const factCell = renderLayerCell(vd?.byLayer?.fact?.mean, s.avgFactScore, factDetail);
|
|
415
|
-
// 行为层 cell(暂无 detail,后续可按工具调用成功率之类扩展)
|
|
416
|
-
const behaviorCell = renderLayerCell(vd?.byLayer?.behavior?.mean, s.avgBehaviorScore);
|
|
417
|
-
// LLM 评价层 cell
|
|
418
|
-
const judgeCell = renderLayerCell(vd?.byLayer?.judge?.mean, s.avgJudgeScore);
|
|
419
|
-
// Cost — only show execution cost (judge cost is tool overhead, not skill cost)
|
|
420
|
-
// execCostReported === false 时(如 codex executor)显示「—」并 tooltip 解释,
|
|
421
|
-
// 跟"真的花了 $0(全 cached / 短 prompt)"区分开。
|
|
488
|
+
if (s.avgFactVerifiedRate != null)
|
|
489
|
+
factDetailParts.push(`${lang === 'zh' ? '验证率' : 'Verified'} ${Math.round(s.avgFactVerifiedRate * 100)}%`);
|
|
490
|
+
// Cost — 只算 execution cost(judge cost 是工具开销,不算 skill 成本)。
|
|
422
491
|
const execCost = s.totalExecCostUSD || 0;
|
|
423
492
|
const costReported = s.execCostReported !== false;
|
|
424
493
|
const hasCost = execCost > 0 || (s.avgTotalTokens || 0) > 0;
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
: `<td class="summary-cell"><span style="color:var(--text-muted)">N/A</span></td>`;
|
|
433
|
-
// Efficiency
|
|
434
|
-
const effDetails = [];
|
|
494
|
+
const costPerSample = (s.totalSamples || 0) > 0 ? execCost / s.totalSamples : execCost;
|
|
495
|
+
const tokenDetail = `${fmtNum(s.avgTotalTokens)} tokens/${t('tokPerReq', lang).replace('tokens/', '')}`;
|
|
496
|
+
const costDim = hasCost
|
|
497
|
+
? { label: dimText('dimCost', lang), value: costReported ? fmtCost(execCost) : fmtCost(0, false), valueColor: costReported ? 'var(--text-primary)' : 'var(--text-muted)', primary: false, bg: '', detailLines: [tokenDetail], title: costReported ? undefined : costUnreportedTooltip, icon: 'cost' }
|
|
498
|
+
: { label: dimText('dimCost', lang), value: 'N/A', valueColor: 'var(--text-muted)', primary: false, bg: '', detailLines: [], icon: 'cost' };
|
|
499
|
+
// Efficiency — 主值是每次耗时;轮次/工具压成一条紧凑副行(去掉「总计」那条,留白更清爽)。
|
|
500
|
+
const effParts = [];
|
|
435
501
|
const displayTurns = s.avgFullNumTurns ?? s.avgNumTurns;
|
|
436
502
|
if ((displayTurns || 0) > 0)
|
|
437
|
-
|
|
503
|
+
effParts.push(`${displayTurns} ${t('turnsPerReq', lang)}`);
|
|
438
504
|
if (s.avgToolCalls != null && s.avgToolCalls > 0) {
|
|
439
|
-
const srPct = s.toolSuccessRate != null ? `
|
|
440
|
-
|
|
505
|
+
const srPct = s.toolSuccessRate != null ? ` ${(s.toolSuccessRate * 100).toFixed(0)}%` : '';
|
|
506
|
+
effParts.push(`${s.avgToolCalls} ${lang === 'zh' ? '工具' : 'tools'}${srPct}`);
|
|
441
507
|
}
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const effCell = `<td class="summary-cell"><div class="summary-value">${fmtDuration(s.avgDurationMs)}<span class="summary-unit">/${avgLabel}</span></div>${effDetail}</td>`;
|
|
448
|
-
// Stability — 多次运行分数一致性(test-retest reliability),统计学定义的稳定性。
|
|
449
|
-
// 主视觉:白话定性词 + ±σ 直观量级,让读者一眼判断。
|
|
450
|
-
// 副区:CV (变异系数 = σ / mean) 分两行展示 + 95% CI(置信区间)。
|
|
451
|
-
// 无 --repeat(variance 缺失) 时主值显示 "—" + 明示需多跑,不虚报 100%——
|
|
452
|
-
// 符合 omk 叙事底线"诚实交代测不到什么"。
|
|
453
|
-
//
|
|
454
|
-
// 跨用例 min~max range 不是稳定性(反映用例难度差异,非 variant 波动),已从此列移除。
|
|
455
|
-
// 成功率 ≠ 稳定性(执行完成率,不是分数一致性),降级到 < 100% 时的副区 alert。
|
|
508
|
+
const durSec = (s.avgDurationMs || 0) / 1000;
|
|
509
|
+
// 副行拆成多条堆叠(表格窄列里逐条独占一行,避免「轮/工具」挤在一行从中间难看换行);
|
|
510
|
+
// solo 的 dimMetric 仍用 ' · ' 合并成一行(够宽)。
|
|
511
|
+
const effDim = { label: dimText('dimEfficiency', lang), value: `${fmtDuration(s.avgDurationMs)}<span class="summary-unit">/${avgLabel}</span>`, valueColor: 'var(--text-primary)', primary: false, bg: '', detailLines: effParts, icon: 'efficiency' };
|
|
512
|
+
// Stability — 多次运行分数一致性(test-retest reliability)。无 --repeat 时显「未测量」不虚报。
|
|
456
513
|
const total = s.totalSamples || 0;
|
|
457
|
-
const successCount = s.successCount || 0;
|
|
458
514
|
const errorCount = s.errorCount || 0;
|
|
459
|
-
const successRate = total > 0 ? Number((successCount / total * 100)
|
|
515
|
+
const successRate = total > 0 ? Number((s.successCount || 0) / total * 100) : 0;
|
|
516
|
+
const cv = (vd && typeof vd.stddev === 'number' && typeof vd.mean === 'number' && vd.mean >= 0.5)
|
|
517
|
+
? Math.abs(vd.stddev / vd.mean) : null;
|
|
460
518
|
const stabDetails = [];
|
|
461
519
|
let stabValue;
|
|
462
520
|
let stabColor;
|
|
463
|
-
|
|
464
|
-
// mean < 0.5 已属全灭场景,直接降级显示"—"。负 mean(理论上不会出现)也走降级。
|
|
465
|
-
if (vd && typeof vd.stddev === 'number' && typeof vd.mean === 'number' && vd.mean >= 0.5) {
|
|
466
|
-
const cv = Math.abs(vd.stddev / vd.mean);
|
|
521
|
+
if (cv != null) {
|
|
467
522
|
const cvPct = cv * 100;
|
|
468
|
-
const sigma = vd.stddev;
|
|
469
|
-
// 主值用白话定性 + ±σ 直观量级,让非统计背景读者一眼判断。
|
|
470
|
-
// CV 和 CI 下沉到副区,给懂的人细看。阈值面向 1-5 分数量纲的经验值。
|
|
471
523
|
let label;
|
|
472
524
|
if (cvPct < 5) {
|
|
473
525
|
label = lang === 'zh' ? '稳定' : 'Stable';
|
|
@@ -481,72 +533,84 @@ export function renderSummaryCards(variants, summary, lang, variance) {
|
|
|
481
533
|
label = lang === 'zh' ? '波动大' : 'Variable';
|
|
482
534
|
stabColor = 'var(--red)';
|
|
483
535
|
}
|
|
484
|
-
stabValue = `${label} · ±${fmtNum(
|
|
485
|
-
|
|
486
|
-
const ciHi = fmtNum(vd.upper, 2);
|
|
487
|
-
stabDetails.push(`CV ${cvPct.toFixed(1)}% · 95% CI [${ciLo}, ${ciHi}]`);
|
|
536
|
+
stabValue = `${label} · ±${fmtNum(vd.stddev, 2)}`;
|
|
537
|
+
stabDetails.push(`CV ${cvPct.toFixed(1)}% · 95% CI [${fmtNum(vd.lower, 2)}, ${fmtNum(vd.upper, 2)}]`);
|
|
488
538
|
}
|
|
489
539
|
else {
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
stabValue = lang === 'zh' ? '
|
|
494
|
-
stabColor = 'var(--
|
|
495
|
-
stabDetails.push(`<span style="color:var(--red)">${lang === 'zh' ? '单轮评测,加 --repeat ≥ 2 才能测 CV' : 'single-run; needs --repeat ≥ 2 to measure CV'}</span>`);
|
|
540
|
+
// 未测量:仅一个安静的词,不在每行重复整句红色告警(那是右侧最大的视觉噪声)。
|
|
541
|
+
// 「需 --repeat ≥ 2」的提示压成表格下方一条脚注,只出现一次。
|
|
542
|
+
anyStabUnmeasured = true;
|
|
543
|
+
stabValue = lang === 'zh' ? '未测量' : 'n/a';
|
|
544
|
+
stabColor = 'var(--text-muted)';
|
|
496
545
|
}
|
|
497
|
-
// Execution-completion alerts:success rate < 100% 时降级到此处,避免和"稳定性"语义混淆。
|
|
498
546
|
if (errorCount > 0) {
|
|
499
|
-
stabDetails.unshift(`<span style="color:var(--red)">${successRate}% ${lang === 'zh' ? '完成率' : 'completed'} · ${errorCount} ${t('errors', lang)}</span>`);
|
|
547
|
+
stabDetails.unshift(`<span style="color:var(--red)">${successRate.toFixed(1)}% ${lang === 'zh' ? '完成率' : 'completed'} · ${errorCount} ${t('errors', lang)}</span>`);
|
|
500
548
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
549
|
+
const stabDim = { label: dimText('dimStability', lang), value: stabValue, valueColor: stabColor, primary: false, bg: '', detailLines: stabDetails, icon: 'stability' };
|
|
550
|
+
const dims = [
|
|
551
|
+
layerDim(dimText('dimFact', lang), vd?.byLayer?.fact?.mean, s.avgFactScore, factDetailParts, 'fact'),
|
|
552
|
+
layerDim(dimText('dimBehavior', lang), vd?.byLayer?.behavior?.mean, s.avgBehaviorScore, [], 'behavior'),
|
|
553
|
+
layerDim(dimText('dimJudge', lang), vd?.byLayer?.judge?.mean, s.avgJudgeScore, [], 'judge'),
|
|
554
|
+
costDim,
|
|
555
|
+
effDim,
|
|
556
|
+
stabDim,
|
|
557
|
+
];
|
|
558
|
+
return {
|
|
559
|
+
name: v,
|
|
560
|
+
color,
|
|
561
|
+
composite: s.avgCompositeScore,
|
|
562
|
+
dims,
|
|
563
|
+
g: {
|
|
564
|
+
fact: typeof factVal === 'number' && factVal > 0 ? clamp01(factVal / 5) : null,
|
|
565
|
+
behavior: typeof behaviorVal === 'number' && behaviorVal > 0 ? clamp01(behaviorVal / 5) : null,
|
|
566
|
+
judge: typeof judgeVal === 'number' && judgeVal > 0 ? clamp01(judgeVal / 5) : null,
|
|
567
|
+
// 成本/效率:绝对软参考,越省越外。$1/用例、120s/次 记为 0。
|
|
568
|
+
cost: hasCost && costReported ? clamp01(1 - costPerSample / 1.0) : null,
|
|
569
|
+
efficiency: durSec > 0 ? clamp01(1 - durSec / 120) : null,
|
|
570
|
+
stability: cv != null ? clamp01(1 - cv / 0.30) : null,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
// 表格行(多 variant):行首颜色点对应雷达多边形,行即图例。
|
|
575
|
+
const rows = perVariant.map((pv, i) => `<tr><td class="sm-name-cell"><span class="sm-row-dot" style="background:${pv.color}"></span><strong>${e(pv.name)}</strong></td>${renderCompositeCell(pv.composite, i)}${pv.dims.map(dimCell).join('')}</tr>`).join('');
|
|
576
|
+
const stabFootnote = anyStabUnmeasured
|
|
577
|
+
? `<p class="sm-foot">${svgIcon('stability', { size: 13, cls: 'sm-foot-ico' })}${lang === 'zh' ? '稳定性需 <code>--repeat ≥ 2</code> 才能测(本次为单轮评测)' : 'Stability needs <code>--repeat ≥ 2</code> (single-run here)'}</p>`
|
|
578
|
+
: '';
|
|
579
|
+
// 指标行列表(单 variant):综合分 + 六维竖排,免去单行表格的空旷/表头冗余。
|
|
580
|
+
const compositeMetric = (() => {
|
|
581
|
+
const c = perVariant[0]?.composite;
|
|
582
|
+
const has = typeof c === 'number' && c > 0;
|
|
583
|
+
const color = has ? (c >= 4 ? 'var(--green)' : c >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)';
|
|
584
|
+
return `<div class="sm-metric sm-metric--composite"><span class="sm-metric-label">${lang === 'zh' ? '综合' : 'Composite'} ${compositeHint}</span><span class="sm-metric-val" style="color:${color}">${has ? c.toFixed(2) : '—'}<small class="sm-metric-unit"> / 5</small></span></div>`;
|
|
585
|
+
})();
|
|
586
|
+
// 六维分两列(列优先填充:左列 = 质量三层 事实/行为/评委,右列 = 资源三项 成本/效率/稳定),
|
|
587
|
+
// 综合分整行领头。单 variant 时撑满横向空间,不再细长一条。
|
|
588
|
+
const metricList = `<div class="sm-metrics">${compositeMetric}<div class="sm-metric-grid">${(perVariant[0]?.dims ?? []).map(dimMetric).join('')}</div></div>`;
|
|
589
|
+
const radarDims = [
|
|
590
|
+
{ key: 'fact', label: lang === 'zh' ? '事实' : 'Fact' },
|
|
591
|
+
{ key: 'behavior', label: lang === 'zh' ? '行为' : 'Behavior' },
|
|
592
|
+
{ key: 'judge', label: lang === 'zh' ? '评委' : 'Judge' },
|
|
593
|
+
{ key: 'cost', label: lang === 'zh' ? '成本' : 'Cost' },
|
|
594
|
+
{ key: 'efficiency', label: lang === 'zh' ? '效率' : 'Eff' },
|
|
595
|
+
{ key: 'stability', label: lang === 'zh' ? '稳定' : 'Stab' },
|
|
596
|
+
];
|
|
597
|
+
const radar = renderRadarChart(perVariant.map((pv) => ({ name: pv.name, color: pv.color, g: pv.g })), radarDims, lang);
|
|
544
598
|
const guideModalId = 'guide-six-dims';
|
|
545
|
-
|
|
599
|
+
// 单 variant 没有对照,标题用「六维评分」;多 variant 才是「六维对比」。
|
|
600
|
+
const sectionTitle = lang === 'zh' ? (isMulti ? '六维对比' : '六维评分') : (isMulti ? 'Six-Dimension Comparison' : 'Six-Dimension Scores');
|
|
601
|
+
const guideTitle = lang === 'zh' ? `如何阅读${sectionTitle}?` : `How to read these six dimensions?`;
|
|
546
602
|
const guideIntro = lang === 'zh'
|
|
547
|
-
? '
|
|
548
|
-
: '
|
|
549
|
-
const
|
|
603
|
+
? '雷达图把六维归一化(越外越好)看强弱形状,下方表格给精确数值。各维含义:'
|
|
604
|
+
: 'The radar normalizes the six dimensions (outer = better) to show shape; the table below gives exact values. The dimensions:';
|
|
605
|
+
const radarNote = lang === 'zh'
|
|
606
|
+
? '雷达归一化口径:事实/行为/评委 = 分÷5;成本、效率按绝对参考折算(约 $1/用例、120s/次 记为最低,越省越靠外);稳定性 = 1 − CV/0.3。空心点 = 未测量或单组无对照。精确数值一律以下方表格为准。'
|
|
607
|
+
: 'Radar normalization: Fact/Behavior/Judge = score÷5; Cost/Efficiency use absolute references (~$1/sample, 120s/req map to 0 — cheaper/faster reaches outward); Stability = 1 − CV/0.3. Hollow dots = not measured or single-variant (no comparison). The table below is the source of truth for exact values.';
|
|
608
|
+
// 六维说明弹框的行首图标:把历史 emoji 映射到统一 SVG 图标(未命中则保留原 emoji 兜底)。
|
|
609
|
+
const EMOJI_ICON = { '📋': 'fact', '🛠️': 'behavior', '💬': 'judge', '💰': 'cost', '⚡': 'efficiency', '🛡️': 'stability' };
|
|
610
|
+
const icon = (emoji) => {
|
|
611
|
+
const name = EMOJI_ICON[emoji];
|
|
612
|
+
return name ? svgIcon(name, { size: 14, style: 'color:var(--text-secondary);vertical-align:-2px;margin-right:4px' }) : `<span aria-hidden="true">${emoji}</span>`;
|
|
613
|
+
};
|
|
550
614
|
// 维度分隔加粗(border-top 2px),让六维的视觉边界更明显。sub 缩进从 28 收到 22。
|
|
551
615
|
const dim = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-primary);font-weight:600"';
|
|
552
616
|
const dimDesc = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-secondary)"';
|
|
@@ -579,7 +643,7 @@ export function renderSummaryCards(variants, summary, lang, variance) {
|
|
|
579
643
|
`;
|
|
580
644
|
const scoringModalHtml = renderScoringModal(scoringModalId, lang);
|
|
581
645
|
return `
|
|
582
|
-
<h2 style="display:flex;align-items:center;gap:4px">${
|
|
646
|
+
<h2 style="display:flex;align-items:center;gap:4px">${sectionTitle} <button type="button" class="hint-btn" onclick="openModal('${guideModalId}')" aria-label="${e(guideTitle)}" aria-haspopup="dialog">?</button></h2>
|
|
583
647
|
<div id="${guideModalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${guideModalId}-title" onclick="if(event.target===this)closeModal('${guideModalId}')">
|
|
584
648
|
<div class="modal-content">
|
|
585
649
|
<div class="modal-header">
|
|
@@ -588,30 +652,76 @@ export function renderSummaryCards(variants, summary, lang, variance) {
|
|
|
588
652
|
</div>
|
|
589
653
|
<p style="font-size:13px;color:var(--text-secondary);margin:4px 0 16px">${e(guideIntro)}</p>
|
|
590
654
|
<table class="modal-table"><tbody>${guideRows}</tbody></table>
|
|
655
|
+
<p style="font-size:12px;color:var(--text-muted);margin:14px 0 0;line-height:1.6;border-top:1px solid var(--border);padding-top:12px">${e(radarNote)}</p>
|
|
591
656
|
</div>
|
|
592
657
|
</div>
|
|
593
658
|
${scoringModalHtml}
|
|
594
|
-
${
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
</table>
|
|
659
|
+
<div class="sm-vis${isMulti ? '' : ' sm-vis--solo'}">
|
|
660
|
+
${radar}
|
|
661
|
+
${isMulti
|
|
662
|
+
? `<div class="table-wrap"><table class="summary-table"><thead>${thead}</thead><tbody>${rows}</tbody></table></div>`
|
|
663
|
+
: metricList}
|
|
600
664
|
</div>
|
|
665
|
+
${stabFootnote}
|
|
601
666
|
<style>
|
|
602
|
-
/*
|
|
603
|
-
.
|
|
604
|
-
.sm-
|
|
605
|
-
.
|
|
606
|
-
.
|
|
607
|
-
.
|
|
608
|
-
.sm-
|
|
609
|
-
.sm-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
.sm-
|
|
613
|
-
.sm-
|
|
614
|
-
.sm-
|
|
667
|
+
/* 对比表减负:行距更松、副行更淡更小、名字列左对齐去掉左色条(行首色点已是图例) */
|
|
668
|
+
.summary-table td { padding:13px 14px }
|
|
669
|
+
.summary-table .sm-name-cell { text-align:left;white-space:nowrap;font-size:14px;max-width:200px;overflow:hidden;text-overflow:ellipsis }
|
|
670
|
+
.summary-table .summary-detail { font-size:11px;color:var(--text-faint);margin-top:4px;line-height:1.4;white-space:nowrap }
|
|
671
|
+
.summary-table .summary-detail + .summary-detail { margin-top:2px }
|
|
672
|
+
.summary-table .summary-value { font-weight:600 }
|
|
673
|
+
.sm-foot { font-size:11.5px;color:var(--text-muted);margin:10px 2px 0;line-height:1.5 }
|
|
674
|
+
.sm-foot code { font-size:11px }
|
|
675
|
+
/* 六维区 — 雷达图 + 数值并排一行(宽屏);窄屏自动换行堆叠。单 variant 用指标列表,多 variant 用对比表。 */
|
|
676
|
+
.sm-vis { display:flex;gap:28px;align-items:center;margin:16px 0 18px }
|
|
677
|
+
.sm-vis .sm-radar { flex:0 0 auto;margin:0;padding:0 }
|
|
678
|
+
.sm-vis .table-wrap { flex:1 1 auto;min-width:0;margin:0 }
|
|
679
|
+
.sm-vis .sm-metrics { flex:0 1 560px;min-width:0 }
|
|
680
|
+
/* 单 variant:雷达放大些(只有一个多边形,大点更好看)+ 居中,避免右侧大片留白。 */
|
|
681
|
+
.sm-vis--solo { justify-content:center;gap:44px }
|
|
682
|
+
.sm-vis--solo .rad-svg { width:360px;height:300px }
|
|
683
|
+
.sm-vis--solo .sm-metrics { flex:0 1 680px }
|
|
684
|
+
@media (max-width:1080px) { .sm-vis { flex-direction:column;align-items:stretch } .sm-vis .sm-metrics { flex:1 1 auto } .sm-vis--solo { justify-content:flex-start } .sm-vis--solo .rad-svg { width:300px;height:250px } }
|
|
685
|
+
/* 指标列表(单 variant)— 综合分整行领头 + 六维分两列(左质量三层 / 右资源三项) */
|
|
686
|
+
.sm-metrics { display:flex;flex-direction:column }
|
|
687
|
+
.sm-metric { display:flex;align-items:baseline;gap:12px;padding:9px 4px }
|
|
688
|
+
.sm-metric--composite { padding:4px 4px 12px;margin-bottom:6px;border-bottom:2px solid var(--border) }
|
|
689
|
+
.sm-metric-label { display:inline-flex;align-items:center;gap:6px;flex:0 0 auto;min-width:92px;color:var(--text-secondary);font-size:13.5px }
|
|
690
|
+
.sm-mico { color:var(--text-muted);flex-shrink:0 }
|
|
691
|
+
.sm-th-ico { color:var(--text-muted);vertical-align:-2px;margin-right:5px }
|
|
692
|
+
.sm-foot-ico { color:var(--text-muted);vertical-align:-2px;margin-right:5px }
|
|
693
|
+
.sm-metric--composite .sm-metric-label { color:var(--text-primary);font-weight:600;font-size:14px }
|
|
694
|
+
.sm-metric-val { flex:0 0 auto;min-width:60px;font-weight:700;font-variant-numeric:tabular-nums;font-family:"SF Mono",Menlo,monospace;font-size:15px }
|
|
695
|
+
.sm-metric--composite .sm-metric-val { font-size:30px;line-height:1 }
|
|
696
|
+
.sm-metric-unit { font-size:13px;color:var(--text-muted);font-weight:400 }
|
|
697
|
+
.sm-metric-detail { flex:1 1 auto;min-width:0;color:var(--text-muted);font-size:12px;line-height:1.5 }
|
|
698
|
+
/* 六维两列网格:列优先填充(左列前 3 = 质量,右列后 3 = 资源),列间一条竖分隔 */
|
|
699
|
+
.sm-metric-grid { display:grid;grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,auto);grid-auto-flow:column }
|
|
700
|
+
.sm-metric-grid .sm-metric { border-bottom:1px solid var(--border) }
|
|
701
|
+
.sm-metric-grid .sm-metric:nth-child(3), .sm-metric-grid .sm-metric:nth-child(6) { border-bottom:none }
|
|
702
|
+
.sm-metric-grid .sm-metric:nth-child(n+4) { padding-left:24px;border-left:1px solid var(--border) }
|
|
703
|
+
@media (max-width:600px) { .sm-metric-grid { grid-template-columns:1fr;grid-auto-flow:row } .sm-metric-grid .sm-metric:nth-child(n+4) { padding-left:4px;border-left:none } .sm-metric-grid .sm-metric:nth-child(3) { border-bottom:1px solid var(--border) } .sm-metric-grid .sm-metric:last-child { border-bottom:none } }
|
|
704
|
+
/* 六维雷达图 — 归一化「越外越好」多边形 */
|
|
705
|
+
.sm-radar { padding:4px }
|
|
706
|
+
.rad-svg { width:300px;height:250px;overflow:visible }
|
|
707
|
+
.rad-grid { fill:none;stroke:var(--border);stroke-width:1 }
|
|
708
|
+
.rad-spoke { stroke:var(--border);stroke-width:1 }
|
|
709
|
+
.rad-label { font-size:11px;fill:var(--text-secondary);font-weight:500 }
|
|
710
|
+
.rad-area { fill-opacity:0.13;stroke-width:2;stroke-linejoin:round }
|
|
711
|
+
.rad-dot { stroke:#fff;stroke-width:1 }
|
|
712
|
+
.rad-dot--na { fill:var(--bg-surface);stroke:var(--text-faint);stroke-width:1.5 }
|
|
713
|
+
.rad-series { transition:opacity .12s }
|
|
714
|
+
.rad-legend { display:flex;flex-wrap:wrap;gap:6px 8px;justify-content:center;margin-top:10px }
|
|
715
|
+
.rad-leg { display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);background:none;border:1px solid var(--border);border-radius:999px;padding:3px 11px;cursor:pointer;transition:.15s }
|
|
716
|
+
.rad-leg-dot { width:9px;height:9px;border-radius:50%;background:var(--c);flex:none }
|
|
717
|
+
.rad-leg:hover { border-color:var(--c);color:var(--text-primary) }
|
|
718
|
+
.rad-leg.off { opacity:.45;text-decoration:line-through }
|
|
719
|
+
.rad-leg.off .rad-leg-dot { background:var(--text-faint) }
|
|
720
|
+
/* 数值表 — 综合分首列大字号;行首颜色点对应雷达多边形;hover 不盖热力底色 */
|
|
721
|
+
.sm-row-dot { display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:7px;vertical-align:middle }
|
|
722
|
+
.sm-composite-cell .summary-value-primary { font-size:1.35rem }
|
|
723
|
+
.sm-delta { font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;margin-top:2px }
|
|
724
|
+
.summary-table tr:hover td { background:transparent }
|
|
615
725
|
</style>`;
|
|
616
726
|
}
|
|
617
727
|
/**
|