feat(dashboard): replace raw Canvas with Chart.js for all Impact tab charts
- Add Chart.js 4.4.7 via CDN + datalabels plugin - Agent Score: horizontal bar chart, sorted descending, color-coded - Model Distribution: doughnut with right-side legend + percentages - Migration Impact: grouped before/after bars with tooltip showing delta - Dark theme defaults: #8ba3c0 text, #1e2d45 grid - Chart instances destroyed before re-render to prevent memory leaks - Responsive: maintainAspectRatio: false
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>APAW Agent Evolution Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-deep: #0a0f1a;
|
||||
@@ -608,8 +610,9 @@
|
||||
.chart-wrap { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 24px; }
|
||||
.chart-title { font-size: 1.1em; font-weight: 700; margin-bottom: 16px; }
|
||||
.chart-sub { font-size: 0.76em; color: var(--text-muted); margin-bottom: 14px; }
|
||||
#impactCanvas { width: 100%; height: 300px; border-radius: 8px; background: var(--bg-panel); }
|
||||
.chart-placeholder { text-align: center; padding: 60px 20px; color: var(--text-muted); font-size: 0.95em; }
|
||||
.chart-container { position:relative; height:280px; width:100%; }
|
||||
.chart-container-sm { position:relative; height:240px; width:100%; }
|
||||
|
||||
/* Recommendation Cards */
|
||||
.rec-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; transition: all 0.3s; margin-bottom: 16px; }
|
||||
@@ -848,39 +851,26 @@
|
||||
<!-- Impact Tab -->
|
||||
<div id="tab-impact" class="tab-panel">
|
||||
<div class="stats-row" id="impactStats"></div>
|
||||
|
||||
<!-- Agent Score Bar Chart -->
|
||||
|
||||
<!-- Chart 1: Agent Performance Scores -->
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Agent Performance Scores</div>
|
||||
<div class="chart-sub">Current fit score per agent — sorted descending</div>
|
||||
<canvas id="historyScoreCanvas" width="900" height="260" style="width:100%;height:260px;border-radius:8px;background:var(--bg-panel)"></canvas>
|
||||
<div id="historyPlaceholder" class="chart-placeholder" style="display:none">No agent score data available.</div>
|
||||
<div class="chart-sub">Composite score per agent based on model benchmarks</div>
|
||||
<div class="chart-container"><canvas id="agentScoreChart"></canvas></div>
|
||||
</div>
|
||||
|
||||
<!-- Model Distribution -->
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Model Distribution</div>
|
||||
<div class="chart-sub">Current models across all agents</div>
|
||||
<canvas id="modelDistCanvas" width="900" height="280" style="width:100%;height:280px;border-radius:8px;background:var(--bg-panel)"></canvas>
|
||||
<div id="modelDistPlaceholder" class="chart-placeholder" style="display:none">No model data available</div>
|
||||
</div>
|
||||
|
||||
<!-- Migration Impact Bars -->
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Migration Impact</div>
|
||||
<div class="chart-sub">Agents with model history — current score vs estimated previous model score</div>
|
||||
<canvas id="impactCanvas" width="900" height="340" style="width:100%;height:340px;border-radius:8px;background:var(--bg-panel)"></canvas>
|
||||
<div id="impactPlaceholder" class="chart-placeholder" style="display:none">No migration data yet</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Note -->
|
||||
<div id="impactSyncNote" class="chart-wrap" style="display:none;text-align:center;padding:40px;">
|
||||
<div style="font-size:1.2em;color:var(--text-secondary);margin-bottom:12px">📊 Data not synced</div>
|
||||
<div style="color:var(--text-muted);margin-bottom:16px">Run the synchronization script to collect agent performance data.</div>
|
||||
<button class="action-btn primary" onclick="runSync()">
|
||||
<span>🔄</span> Sync Evolution Data
|
||||
</button>
|
||||
<pre style="margin-top:12px;color:var(--text-muted);font-size:.8em;font-family:JetBrains Mono,monospace">bun run agent-evolution/scripts/sync-agent-history.ts</pre>
|
||||
|
||||
<!-- Chart 2 & 3 side by side -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:20px;margin-bottom:24px">
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Model Distribution</div>
|
||||
<div class="chart-sub">Agents per model</div>
|
||||
<div class="chart-container-sm"><canvas id="modelDistChart"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Migration Impact</div>
|
||||
<div class="chart-sub">Before vs after model change score</div>
|
||||
<div class="chart-container-sm"><canvas id="migrationImpactChart"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1005,6 +995,11 @@
|
||||
// Supports both server and file:// mode
|
||||
let agentData = {};
|
||||
|
||||
// Set Chart.js dark theme defaults
|
||||
Chart.defaults.color = '#8ba3c0';
|
||||
Chart.defaults.borderColor = '#1e2d45';
|
||||
Chart.defaults.font.family = "'Inter', sans-serif";
|
||||
|
||||
// Inline recommendation data fallback (from model-research-latest.json)
|
||||
const INLINE_RECOMMENDATIONS = [
|
||||
{ agent: "frontend-developer", current_model_in_agent_versions: "ollama-cloud/qwen3-coder:480b", source_of_truth_model: "ollama-cloud/minimax-m2.5", impact: "high", score_before: 86, score_after: 92, score_delta: 6, rationale: "agent-versions.json is stale. kilo-meta.json (source of truth) already has minimax-m2.5. Matrix score for frontend-dev on M2.5 = 92 (highest!)." },
|
||||
@@ -1718,290 +1713,258 @@ function renderModelsTab(agent) {
|
||||
}
|
||||
|
||||
// Compute score for any model name using benchmark lookup + fallback
|
||||
function getModelScore(modelName, defaultScore) {
|
||||
// Try exact match first
|
||||
function computeAgentScore(modelName) {
|
||||
const bm = (agentData.model_benchmarks || {});
|
||||
let score = null;
|
||||
if (bm[modelName]) {
|
||||
score = (bm[modelName].if_score || 70) * 0.6 + (bm[modelName].swe_bench || 0) * 0.3;
|
||||
const ctx = bm[modelName].context_window || 128;
|
||||
const ctxScore = ctx >= 1000 ? 15 : ctx >= 256 ? 8 : 4;
|
||||
score += ctxScore;
|
||||
const key = Object.keys(bm).find(k => modelName.includes(k)) || '';
|
||||
if (bm[key]) {
|
||||
const m = bm[key];
|
||||
let score = (m.if_score || 70) * 0.6 + (m.swe_bench || 0) * 0.3;
|
||||
const ctx = m.context_window || 128;
|
||||
score += ctx >= 1000 ? 15 : ctx >= 256 ? 8 : 4;
|
||||
return Math.round(score);
|
||||
}
|
||||
if (score) return Math.round(score);
|
||||
// Fallback: hash-based deterministic score from model name
|
||||
const hash = modelName.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
||||
return defaultScore || 50 + (hash % 35);
|
||||
return 50 + (hash % 35);
|
||||
}
|
||||
|
||||
// Render Impact Tab - horizontal score bars + donut + before/after bars
|
||||
function renderImpact() {
|
||||
const allAgents = Object.entries(agentData.agents);
|
||||
const modelCounts = {};
|
||||
let totalScore = 0, agentsWithScore = 0;
|
||||
let bestModel = { name: '', score: 0 };
|
||||
let worstModel = { name: '', score: 100 };
|
||||
// Pre-compute all agent scores
|
||||
const scoredAgents = [];
|
||||
// Chart 1: Agent Score Bar Chart
|
||||
function drawAgentScoreChart(scoredAgents) {
|
||||
const ctx = document.getElementById('agentScoreChart')?.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
allAgents.forEach(([name, agent]) => {
|
||||
const model = agent.current?.model || 'unknown';
|
||||
modelCounts[model] = (modelCounts[model] || 0) + 1;
|
||||
const score = getModelScore(model, 70);
|
||||
scoredAgents.push({ name, model, score, history: agent.history || [] });
|
||||
totalScore += score;
|
||||
agentsWithScore++;
|
||||
if (score > bestModel.score) bestModel = { name: model, score };
|
||||
if (score < worstModel.score) worstModel = { name: model, score };
|
||||
});
|
||||
const labels = scoredAgents.map(a => a.name);
|
||||
const data = scoredAgents.map(a => a.score);
|
||||
const bgColors = scoredAgents.map(a =>
|
||||
a.score >= 85 ? '#00ff94' : a.score >= 70 ? '#00d4ff' : a.score >= 55 ? '#a855f7' : '#ff4757'
|
||||
);
|
||||
|
||||
scoredAgents.sort((a, b) => b.score - a.score);
|
||||
const totalAgents = allAgents.length;
|
||||
const avgSystemScore = agentsWithScore > 0 ? (totalScore / agentsWithScore).toFixed(1) : 0;
|
||||
const changesMade = allAgents.reduce((sum, [_, a]) => sum + ((a.history || []).length || 0), 0);
|
||||
|
||||
// Stats row
|
||||
document.getElementById('impactStats').innerHTML = `
|
||||
<div class="stat-card"><div class="stat-label">Total Agents</div><div class="stat-value grad-cyan">${totalAgents}</div><div class="stat-sub">in system</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Avg System Score</div><div class="stat-value grad-green">${avgSystemScore}</div><div class="stat-sub">composite</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Best Model</div><div class="stat-value grad-purple">${bestModel.name.split('/').pop()}</div><div class="stat-sub">score: ${bestModel.score}</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Worst Model</div><div class="stat-value grad-orange">${worstModel.name.split('/').pop()}</div><div class="stat-sub">score: ${worstModel.score}</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Changes Made</div><div class="stat-value grad-cyan">${changesMade}</div><div class="stat-sub">total migrations</div></div>
|
||||
`;
|
||||
|
||||
// Draw all 3 charts
|
||||
drawAgentScoreBars(scoredAgents);
|
||||
drawModelDistributionDonut(modelCounts);
|
||||
drawMigrationImpactBarsFromHistory(scoredAgents);
|
||||
}
|
||||
|
||||
// 1. Agent Score Bar Chart — horizontal bars sorted descending
|
||||
function drawAgentScoreBars(scoredAgents) {
|
||||
const canvas = document.getElementById('historyScoreCanvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const w = 900, h = 260;
|
||||
canvas.width = w; canvas.height = h;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (scoredAgents.length === 0) {
|
||||
document.getElementById('historyPlaceholder').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
document.getElementById('historyPlaceholder').style.display = 'none';
|
||||
|
||||
const padding = { top: 20, right: 40, bottom: 10, left: 140 };
|
||||
const barH = Math.min(28, (h - padding.top - padding.bottom) / scoredAgents.length);
|
||||
const maxScore = Math.max(...scoredAgents.map(a => a.score), 100);
|
||||
const chartW = w - padding.left - padding.right;
|
||||
|
||||
scoredAgents.forEach((ag, i) => {
|
||||
const barW = (ag.score / maxScore) * chartW;
|
||||
const y = padding.top + i * barH;
|
||||
const x = padding.left;
|
||||
const color = ag.score >= 85 ? 'rgba(0,255,148,.7)' : ag.score >= 70 ? 'rgba(0,212,255,.65)' : ag.score >= 55 ? 'rgba(168,85,247,.55)' : 'rgba(255,71,87,.5)';
|
||||
|
||||
// Bar
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y + 3, barW, barH - 6, 4);
|
||||
ctx.fill();
|
||||
|
||||
// Agent name (left)
|
||||
ctx.fillStyle = '#e8f1ff';
|
||||
ctx.font = '12px Inter';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(ag.name, x - 8, y + barH / 2);
|
||||
|
||||
// Score (right of bar)
|
||||
ctx.fillStyle = '#e8f1ff';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.font = 'bold 11px JetBrains Mono';
|
||||
ctx.fillText(ag.score.toString(), x + barW + 8, y + barH / 2);
|
||||
|
||||
// Model short name inside bar if enough space
|
||||
if (barW > 120) {
|
||||
ctx.fillStyle = '#0e1219';
|
||||
ctx.font = '9px JetBrains Mono';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(ag.model.split('/').pop().substring(0, 16), x + 8, y + barH / 2);
|
||||
window._agentScoreChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Composite Score',
|
||||
data,
|
||||
backgroundColor: bgColors,
|
||||
borderRadius: 6,
|
||||
borderSkipped: false,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: '#0f1525',
|
||||
titleColor: '#e8f1ff',
|
||||
bodyColor: '#8ba3c0',
|
||||
borderColor: '#1e2d45',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: (item) => `${item.raw} — ${scoredAgents[item.dataIndex].model.split('/').pop()}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: '#1e2d45' },
|
||||
ticks: { color: '#5a7090', font: { family: 'JetBrains Mono', size: 10 } }
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: '#8ba3c0', font: { family: 'JetBrains Mono', size: 11 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Model Distribution Donut Chart
|
||||
function drawModelDistributionDonut(modelCounts) {
|
||||
const canvas = document.getElementById('modelDistCanvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
// Chart 2: Model Distribution (Doughnut)
|
||||
function drawModelDistChart(modelCounts) {
|
||||
const ctx = document.getElementById('modelDistChart')?.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const w = 900, h = 280;
|
||||
canvas.width = w; canvas.height = h;
|
||||
const entries = Object.entries(modelCounts).filter(([_, c]) => c > 0);
|
||||
const labels = entries.map(([m, _]) => m.split('/').pop());
|
||||
const data = entries.map(([_, c]) => c);
|
||||
const colors = ['#00ff94','#00d4ff','#a855f7','#ff9f43','#ff4757','#3b82f6','#facc15','#e879f9'];
|
||||
|
||||
const modelEntries = Object.entries(modelCounts).filter(([_, c]) => c > 0);
|
||||
if (modelEntries.length === 0) {
|
||||
document.getElementById('modelDistPlaceholder').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
document.getElementById('modelDistPlaceholder').style.display = 'none';
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2 - 120;
|
||||
const centerY = h / 2;
|
||||
const outerRadius = Math.min(w, h) / 2 - 30;
|
||||
const innerRadius = outerRadius * 0.58;
|
||||
const colors = ['#00ff94','#00d4ff','#a855f7','#ff9f43','#ff4757','#3b82f6','#facc15','#e879f9','#4ade80','#fb7185'];
|
||||
const total = modelEntries.reduce((s, [_, c]) => s + c, 0);
|
||||
let startAngle = -Math.PI / 2;
|
||||
|
||||
modelEntries.forEach(([model, count], idx) => {
|
||||
const sliceAngle = (count / total) * Math.PI * 2;
|
||||
const color = colors[idx % colors.length];
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, outerRadius, startAngle, startAngle + sliceAngle);
|
||||
ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
startAngle += sliceAngle;
|
||||
});
|
||||
|
||||
// Center text
|
||||
ctx.fillStyle = '#e8f1ff';
|
||||
ctx.font = 'bold 28px Inter';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(total.toString(), centerX, centerY - 6);
|
||||
ctx.fillStyle = '#8ba3c0';
|
||||
ctx.font = '12px Inter';
|
||||
ctx.fillText('agents', centerX, centerY + 18);
|
||||
|
||||
// Legend on right
|
||||
let legendY = centerY - (modelEntries.length * 22) / 2;
|
||||
const legendX = centerX + outerRadius + 30;
|
||||
modelEntries.forEach(([model, count], idx) => {
|
||||
const color = colors[idx % colors.length];
|
||||
const short = model.split('/').pop();
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(legendX, legendY, 10, 10);
|
||||
ctx.fillStyle = '#e8f1ff';
|
||||
ctx.font = '12px Inter';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${short} — ${count}`, legendX + 16, legendY + 5);
|
||||
const pct = ((count / total) * 100).toFixed(0);
|
||||
ctx.fillStyle = '#5a7090';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.fillText(`${pct}%`, legendX + 200, legendY + 5);
|
||||
legendY += 24;
|
||||
window._modelDistChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
data,
|
||||
backgroundColor: colors.slice(0, entries.length),
|
||||
borderColor: '#141c2e',
|
||||
borderWidth: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '60%',
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#8ba3c0', font: { family: 'JetBrains Mono', size: 11 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: '#0f1525',
|
||||
titleColor: '#e8f1ff',
|
||||
bodyColor: '#8ba3c0',
|
||||
borderColor: '#1e2d45',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: (item) => ` ${item.label}: ${item.raw} agents (${((item.raw/data.reduce((s,c)=>s+c,0))*100).toFixed(0)}%)`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Migration Impact Bars — compare current model vs previous model from history
|
||||
function drawMigrationImpactBarsFromHistory(scoredAgents) {
|
||||
const canvas = document.getElementById('impactCanvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
// Chart 3: Migration Impact (Grouped Bar)
|
||||
function drawMigrationChart(scoredAgents) {
|
||||
const ctx = document.getElementById('migrationImpactChart')?.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const w = 900, h = 340;
|
||||
canvas.width = w; canvas.height = h;
|
||||
|
||||
// Build impact data: agents with history showing current vs previous model score
|
||||
// Build before/after data from agents with history
|
||||
const impactData = [];
|
||||
scoredAgents.forEach(ag => {
|
||||
if (ag.history && ag.history.length > 0) {
|
||||
if (ag.history.length > 0) {
|
||||
const latest = ag.history[ag.history.length - 1];
|
||||
if (latest.to && latest.from) {
|
||||
const after = ag.score; // current score
|
||||
// Estimate "before" score from previous model name
|
||||
const before = getModelScore(latest.from, after - 5);
|
||||
const after = ag.score;
|
||||
const before = computeAgentScore(latest.from);
|
||||
impactData.push({
|
||||
agent: ag.name,
|
||||
before: before,
|
||||
after: after,
|
||||
name: ag.name.split('-').map(s => s[0]?.toUpperCase() + s.slice(1)).join('-'),
|
||||
before, after,
|
||||
delta: after - before,
|
||||
fromModel: (latest.from || '').split('/').pop() || 'unknown',
|
||||
toModel: ag.model.split('/').pop()
|
||||
from: latest.from.split('/').pop(),
|
||||
to: ag.model.split('/').pop()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (impactData.length === 0) {
|
||||
document.getElementById('impactPlaceholder').style.display = 'block';
|
||||
// No history — show single bars for all agents
|
||||
window._migrationChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: scoredAgents.slice(0, 20).map(a => a.name),
|
||||
datasets: [{
|
||||
label: 'Current Score',
|
||||
data: scoredAgents.slice(0, 20).map(a => a.score),
|
||||
backgroundColor: '#00ff94',
|
||||
borderRadius: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { color: '#5a7090', font: { size: 9 }, maxRotation: 45 } },
|
||||
y: { grid: { color: '#1e2d45' }, ticks: { color: '#5a7090' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
document.getElementById('impactPlaceholder').style.display = 'none';
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const padding = { top: 30, right: 40, bottom: 50, left: 45 };
|
||||
const chartW = w - padding.left - padding.right;
|
||||
const chartH = h - padding.top - padding.bottom;
|
||||
const maxScore = Math.max(...impactData.map(d => Math.max(d.before, d.after)), 100);
|
||||
const barGroupW = chartW / impactData.length;
|
||||
const barW = Math.min(22, barGroupW * 0.35);
|
||||
const scaleY = chartH / maxScore;
|
||||
|
||||
// Grid + labels
|
||||
ctx.strokeStyle = '#1e2d45';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillStyle = '#5a7090';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
const y = padding.top + (i * chartH / 4);
|
||||
const val = Math.round(maxScore - (i * maxScore / 4));
|
||||
ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(w - padding.right, y); ctx.stroke();
|
||||
ctx.fillText(val.toString(), padding.left - 8, y + 4);
|
||||
}
|
||||
|
||||
impactData.forEach((d, i) => {
|
||||
const cx = padding.left + i * barGroupW + barGroupW / 2;
|
||||
|
||||
// Before bar (dim red)
|
||||
const bh = d.before * scaleY;
|
||||
const by = padding.top + chartH - bh;
|
||||
ctx.fillStyle = 'rgba(255,71,87,.45)';
|
||||
ctx.fillRect(cx - barW - 3, by, barW, bh);
|
||||
|
||||
// After bar (green if improved, red if regressed)
|
||||
const ah = d.after * scaleY;
|
||||
const ay = padding.top + chartH - ah;
|
||||
ctx.fillStyle = d.delta >= 0 ? 'rgba(0,255,148,.75)' : 'rgba(255,71,87,.75)';
|
||||
ctx.fillRect(cx + 3, ay, barW, ah);
|
||||
|
||||
// Delta label
|
||||
if (Math.abs(d.delta) >= 1) {
|
||||
ctx.fillStyle = d.delta >= 0 ? '#00ff94' : '#ff4757';
|
||||
ctx.font = 'bold 9px JetBrains Mono';
|
||||
ctx.textAlign = 'center';
|
||||
const dy = Math.min(ay, by) - 10;
|
||||
ctx.fillText((d.delta >= 0 ? '+' : '') + d.delta, cx, dy);
|
||||
window._migrationChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: impactData.map(d => d.name),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Before',
|
||||
data: impactData.map(d => d.before),
|
||||
backgroundColor: 'rgba(255,71,87,.6)',
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'After',
|
||||
data: impactData.map(d => d.after),
|
||||
backgroundColor: impactData.map(d => d.delta >= 0 ? 'rgba(0,255,148,.6)' : 'rgba(255,71,87,.6)'),
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
backgroundColor: '#0f1525',
|
||||
titleColor: '#e8f1ff',
|
||||
bodyColor: '#8ba3c0',
|
||||
borderColor: '#1e2d45',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
afterBody: (items) => {
|
||||
const idx = items[0].dataIndex;
|
||||
const d = impactData[idx];
|
||||
return `Change: ${d.from} → ${d.to}\nDelta: ${d.delta >= 0 ? '+' : ''}${d.delta}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { color: '#5a7090', font: { size: 9 }, maxRotation: 45 } },
|
||||
y: { grid: { color: '#1e2d45' }, ticks: { color: '#5a7090' } }
|
||||
}
|
||||
}
|
||||
|
||||
// Agent label (rotated)
|
||||
ctx.save();
|
||||
ctx.translate(cx, h - padding.bottom + 18);
|
||||
ctx.rotate(-0.35);
|
||||
ctx.fillStyle = '#8ba3c0';
|
||||
ctx.font = '9px Inter';
|
||||
ctx.textAlign = 'left';
|
||||
const label = d.agent.length > 12 ? d.agent.substring(0, 10) + '..' : d.agent;
|
||||
ctx.fillText(label, 0, 0);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Legend
|
||||
const ly = 12;
|
||||
ctx.fillStyle = 'rgba(255,71,87,.45)'; ctx.fillRect(padding.left, ly, 12, 10);
|
||||
ctx.fillStyle = '#e8f1ff'; ctx.font = '10px Inter'; ctx.textAlign = 'left'; ctx.fillText('Before', padding.left + 18, ly + 9);
|
||||
ctx.fillStyle = 'rgba(0,255,148,.75)'; ctx.fillRect(padding.left + 70, ly, 12, 10);
|
||||
ctx.fillStyle = '#e8f1ff'; ctx.fillText('After', padding.left + 88, ly + 9);
|
||||
}
|
||||
|
||||
// Sync button handler
|
||||
// Render Impact Tab - Chart.js based
|
||||
function renderImpact() {
|
||||
const allAgents = Object.entries(agentData.agents);
|
||||
const modelCounts = {};
|
||||
const scoredAgents = [];
|
||||
|
||||
// Compute scores for all agents
|
||||
allAgents.forEach(([name, agent]) => {
|
||||
const model = agent.current?.model || 'unknown';
|
||||
modelCounts[model] = (modelCounts[model] || 0) + 1;
|
||||
const score = computeAgentScore(model);
|
||||
scoredAgents.push({ name, model, score, history: agent.history || [] });
|
||||
});
|
||||
|
||||
// Sort by score descending
|
||||
scoredAgents.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Stats row
|
||||
const totalAgents = allAgents.length;
|
||||
const avgScore = scoredAgents.length > 0
|
||||
? (scoredAgents.reduce((s, a) => s + a.score, 0) / scoredAgents.length).toFixed(1)
|
||||
: 0;
|
||||
const best = scoredAgents[0] || { name: 'N/A', score: 0 };
|
||||
const worst = scoredAgents[scoredAgents.length - 1] || { name: 'N/A', score: 0 };
|
||||
const changes = allAgents.reduce((sum, [_, a]) => sum + ((a.history || []).length), 0);
|
||||
|
||||
document.getElementById('impactStats').innerHTML = `
|
||||
<div class="stat-card"><div class="stat-label">Total Agents</div><div class="stat-value grad-cyan">${totalAgents}</div><div class="stat-sub">in system</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Avg Score</div><div class="stat-value grad-green">${avgScore}</div><div class="stat-sub">composite</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Best Model</div><div class="stat-value grad-purple">${best.model.split('/').pop()}</div><div class="stat-sub">score: ${best.score}</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Worst Model</div><div class="stat-value grad-orange">${worst.model.split('/').pop()}</div><div class="stat-sub">score: ${worst.score}</div></div>
|
||||
<div class="stat-card"><div class="stat-label">Changes Made</div><div class="stat-value grad-cyan">${changes}</div><div class="stat-sub">total migrations</div></div>
|
||||
`;
|
||||
|
||||
// Destroy old charts before creating new ones
|
||||
if (window._agentScoreChart) window._agentScoreChart.destroy();
|
||||
if (window._modelDistChart) window._modelDistChart.destroy();
|
||||
if (window._migrationChart) window._migrationChart.destroy();
|
||||
|
||||
drawAgentScoreChart(scoredAgents);
|
||||
drawModelDistChart(modelCounts);
|
||||
drawMigrationChart(scoredAgents);
|
||||
}
|
||||
|
||||
// Filter Agents
|
||||
function runSync() {
|
||||
const btn = document.querySelector('#impactSyncNote button');
|
||||
if (btn) btn.textContent = '⏳ Running...';
|
||||
|
||||
Reference in New Issue
Block a user