Files
APAW/agent-evolution/index.html
Deploy Bot 19be5cf229 fix(dashboard): rewrite Impact tab charts to work with actual data structure
Replaced broken chart functions that expected non-existent fit_score_after/before
with data-agnostic implementations using model names + benchmark lookup.

- Agent Score Bar Chart: horizontal bars per agent, sorted descending, color-coded
- Model Distribution: donut chart with legend on the right
- Migration Impact Bars: before/after comparison from history entries
- Added getModelScore() helper with deterministic fallback
- Added 'Sync Evolution Data' button if data missing

Fixes: canvas dimensions, getBoundingClientRect() == 0 when tab hidden
2026-05-25 15:18:35 +01:00

2236 lines
96 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-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">
<style>
:root {
--bg-deep: #0a0f1a;
--bg-panel: #0f1525;
--bg-card: #141c2e;
--bg-card-hover: #1a2540;
--border: #1e2d45;
--border-bright: #2a4060;
--text-primary: #e8f1ff;
--text-secondary: #8ba3c0;
--text-muted: #5a7090;
--accent-cyan: #00d4ff;
--accent-green: #00ff94;
--accent-orange: #ff9f43;
--accent-red: #ff4757;
--accent-purple: #a855f7;
--accent-blue: #3b82f6;
--accent-yellow: #facc15;
--glow-cyan: rgba(0,212,255,0.15);
--glow-green: rgba(0,255,148,0.1);
--glow-purple: rgba(168,85,247,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(0,212,255,0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(168,85,247,0.06) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 24px 16px;
position: relative;
z-index: 1;
}
/* Header */
.header { text-align: center; margin-bottom: 32px; }
.header h1 {
font-size: 2.2em;
font-weight: 800;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.header .sub {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85em;
color: var(--text-muted);
}
.header .meta {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 12px;
font-size: 0.8em;
color: var(--text-secondary);
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 4px;
margin-bottom: 24px;
overflow-x: auto;
}
.tab-btn {
flex: 1;
min-width: 100px;
padding: 10px 16px;
background: none;
border: none;
color: var(--text-secondary);
font-family: 'Inter', sans-serif;
font-size: 0.85em;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.25s;
white-space: nowrap;
}
.tab-btn:hover { color: var(--text-primary); background: var(--bg-card); }
.tab-btn.active {
color: var(--bg-deep);
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green));
box-shadow: 0 0 20px var(--glow-cyan);
}
.tab-panel { display: none; animation: fadeUp 0.4s ease-out; }
.tab-panel.active { display: block; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
/* Stats */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.stat-card:hover {
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--glow-cyan);
}
.stat-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 6px;
}
.stat-value { font-size: 2em; font-weight: 800; }
.stat-sub { font-size: 0.75em; color: var(--text-secondary); margin-top: 4px; }
.grad-cyan { background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-orange { background: linear-gradient(135deg, var(--accent-orange), var(--accent-yellow)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-purple { background: linear-gradient(135deg, var(--accent-purple), #e879f9); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-green { background: linear-gradient(135deg, var(--accent-green), #4ade80); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-red { background: linear-gradient(135deg, var(--accent-red), #ff6b81); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
/* Agent Grid */
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
}
.agent-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.agent-card:hover {
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--glow-cyan);
}
.agent-card.has-history { border-left: 3px solid var(--accent-green); }
.agent-card.needs-update { border-left: 3px solid var(--accent-orange); }
.agent-card.is-new { border-left: 3px solid var(--accent-purple); }
.agent-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.agent-name {
font-weight: 700;
font-size: 1.05em;
display: flex;
align-items: center;
gap: 8px;
}
.agent-color {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.agent-category {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
background: rgba(0,212,255,0.1);
color: var(--accent-cyan);
}
.agent-model {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78em;
color: var(--accent-green);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.agent-provider {
font-size: 0.7em;
padding: 2px 6px;
border-radius: 4px;
background: rgba(0,255,148,0.1);
color: var(--accent-green);
}
.agent-desc {
font-size: 0.85em;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 12px;
}
.agent-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.agent-meta-item {
text-align: center;
}
.agent-meta-label {
font-size: 0.6em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agent-meta-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
font-weight: 600;
color: var(--text-primary);
}
.agent-history {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border);
}
.history-title {
font-size: 0.7em;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 8px;
}
.history-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.75em;
padding: 6px 0;
border-bottom: 1px solid rgba(30,45,69,0.5);
}
.history-item:last-child { border-bottom: none; }
.history-date {
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
min-width: 100px;
}
.history-type {
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85em;
}
.history-type.model_change { background: rgba(0,212,255,0.15); color: var(--accent-cyan); }
.history-type.prompt_change { background: rgba(168,85,247,0.15); color: var(--accent-purple); }
.history-type.agent_created { background: rgba(0,255,148,0.15); color: var(--accent-green); }
/* Category Section */
.category-section { margin-bottom: 32px; }
.category-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.category-title {
font-size: 1.1em;
font-weight: 700;
}
.category-count {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
background: rgba(168,85,247,0.15);
color: var(--accent-purple);
}
/* Evolution Timeline */
.timeline-wrap {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.timeline-title {
font-size: 1.1em;
font-weight: 700;
margin-bottom: 16px;
}
.timeline {
position: relative;
padding-left: 24px;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
padding: 12px 0 12px 24px;
border-bottom: 1px solid var(--border);
}
.timeline-item:last-child { border-bottom: none; }
.timeline-item::before {
content: '';
position: absolute;
left: -20px;
top: 18px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-cyan);
border: 2px solid var(--border);
}
.timeline-date {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75em;
color: var(--text-muted);
}
.timeline-content {
font-size: 0.9em;
margin-top: 4px;
}
.timeline-agent {
font-weight: 600;
color: var(--accent-cyan);
}
.timeline-change {
color: var(--text-secondary);
}
/* Filter Row */
.filter-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.filter-btn {
padding: 6px 14px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s;
font-family: 'Inter', sans-serif;
}
.filter-btn:hover, .filter-btn.active {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(0,212,255,0.05);
}
/* Search */
.search-box {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 40px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
font-size: 0.9em;
}
.search-input:focus {
outline: none;
border-color: var(--accent-cyan);
}
.search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
/* Model Matrix */
.matrix-wrap {
overflow-x: auto;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg-card);
padding: 20px;
}
.matrix-title {
font-size: 1.1em;
font-weight: 700;
margin-bottom: 16px;
}
.matrix-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8em;
}
.matrix-table th {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
color: var(--text-muted);
text-transform: uppercase;
padding: 10px 8px;
text-align: left;
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
background: var(--bg-panel);
}
.matrix-table td {
padding: 10px 8px;
border-bottom: 1px solid var(--border);
}
.matrix-table tr:hover td {
background: var(--bg-card-hover);
}
.score-bar {
display: flex;
align-items: center;
gap: 6px;
}
.score-bg {
width: 50px;
height: 5px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 3px;
}
.score-fill.high { background: linear-gradient(90deg, var(--accent-green), #00ff94); }
.score-fill.medium { background: linear-gradient(90deg, var(--accent-orange), #ffc048); }
.score-fill.low { background: linear-gradient(90deg, var(--accent-red), #ff6b81); }
/* Heatmap */
.hm-wrap { overflow-x:auto; border-radius:11px; border:1px solid var(--border); background:var(--bg-card); padding:18px; margin-bottom:26px; }
.hm-title { font-weight:700; font-size:1.05em; }
.hm-sub { font-size:.76em; color:var(--text-muted); margin-bottom:14px; }
.hm-table { border-collapse:separate; border-spacing:2px; width:100%; }
.hm-table th { font-family:'JetBrains Mono',monospace; font-size:.62em; color:var(--text-muted); padding:8px 5px; text-align:center; white-space:nowrap; vertical-align:bottom; }
.hm-table th.hm-role { text-align:left; min-width:140px; font-size:.68em; padding-left:10px; }
.hm-table td { text-align:center; padding:6px 4px; font-family:'JetBrains Mono',monospace; font-size:.72em; font-weight:700; border-radius:6px; cursor:pointer; transition:all .15s cubic-bezier(.4,0,.2,1); min-width:42px; position:relative; line-height:1.4; }
.hm-table td:hover { transform:scale(1.1); z-index:2; box-shadow:0 4px 12px rgba(0,0,0,.35); }
.hm-table td.hm-r { text-align:left; font-family:'Inter',sans-serif; font-size:.82em; font-weight:600; color:var(--text-primary); cursor:default; padding-left:10px; }
.hm-table td.hm-r:hover { transform:none; box-shadow:none; }
.hm-star { position:absolute; top:2px; right:2px; font-size:.65em; text-shadow:0 1px 2px rgba(0,0,0,.5); }
.hm-cur { box-shadow:inset 0 0 0 2px var(--accent-cyan), 0 0 8px rgba(0,212,255,.35); border-radius:6px; }
.hm-cur::after { content:''; position:absolute; bottom:2px; left:50%; transform:translateX(-50%); width:8px; height:3px; background:var(--accent-cyan); border-radius:2px; }
.hm-if-warn { position:absolute; top:2px; left:2px; font-size:.6em; opacity:.8; }
/* Smooth gradient legend bar */
.hm-legend-wrap { margin-top:18px; padding:0 4px; }
.hm-legend-track { position:relative; height:22px; border-radius:11px; background:linear-gradient(90deg, rgba(0,255,148,.85) 0%, rgba(0,212,255,.75) 20%, rgba(59,130,246,.6) 40%, rgba(168,85,247,.45) 58%, rgba(255,159,67,.35) 75%, rgba(255,71,87,.3) 88%, rgba(90,104,128,.2) 100%); box-shadow:inset 0 1px 3px rgba(0,0,0,.3); }
.hm-legend-labels { display:flex; justify-content:space-between; align-items:center; margin-top:8px; padding:0 4px; }
.hm-legend-labels span { font-size:.68em; font-family:'JetBrains Mono',monospace; color:var(--text-muted); }
.hm-legend-left { color:var(--accent-green); }
.hm-legend-right { color:var(--accent-red); }
.hm-legend-marks { display:flex; justify-content:space-between; padding:0 2px; margin-top:3px; }
.hm-legend-marks span { font-size:.58em; font-family:'JetBrains Mono',monospace; color:var(--text-muted); min-width:20px; text-align:center; }
/* Heatmap Modal Tabs */
.hm-modal-tabs { display:flex; gap:3px; background:var(--bg-panel); border-bottom:1px solid var(--border); padding:4px 18px; }
.hm-tab-btn { padding:8px 16px; background:none; border:none; color:var(--text-secondary); font-family:'Inter'; font-size:.82em; font-weight:600; border-radius:8px; cursor:pointer; transition:all .25s; }
.hm-tab-btn.active { color:var(--bg-deep); background:linear-gradient(135deg,var(--accent-cyan),var(--accent-green)); }
.hm-tab-content { display:none; }
.hm-tab-content.active { display:block; }
.hm-model-timeline { display:flex; flex-direction:column; gap:12px; }
.hm-tl-item { display:flex; gap:14px; align-items:center; padding:10px; background:var(--bg-deep); border-radius:8px; border-left:3px solid var(--accent-cyan); }
.hm-tl-date { font-family:'JetBrains Mono',monospace; font-size:.72em; color:var(--text-muted); min-width:100px; }
.hm-tl-change { display:flex; align-items:center; gap:8px; }
.hm-tl-from { text-decoration:line-through; color:#ff6b81; background:rgba(255,71,87,.08); padding:2px 6px; border-radius:4px; }
.hm-tl-arrow { color:var(--accent-green); }
.hm-tl-to { color:var(--accent-green); background:rgba(0,255,148,.08); padding:2px 6px; border-radius:4px; font-weight:600; }
.hm-tl-current { border-left-color:var(--accent-green); background:rgba(0,255,148,.05); }
.hm-no-data { color:var(--text-muted); font-size:.9em; padding:16px; text-align:center; }
.hm-capabilities { display:flex; flex-wrap:wrap; gap:6px; }
.hm-cap-tag { padding:4px 10px; background:rgba(0,212,255,.1); border:1px solid var(--border); border-radius:16px; font-size:.78em; color:var(--accent-cyan); }
.hm-agent-desc { font-size:.9em; color:var(--text-secondary); line-height:1.5; margin-bottom:14px; padding:12px; background:var(--bg-deep); border-radius:8px; }
.hm-model-tl-score { margin-left:auto; font-family:'JetBrains Mono',monospace; font-size:.8em; color:var(--accent-cyan); }
/* Tooltip */
#ttOverlay { display:none; position:fixed; top:0;left:0;right:0;bottom:0; z-index:999; pointer-events:none; }
#ttOverlay.show { display:block; }
#ttBox { position:absolute; background:var(--bg-panel); border:1px solid var(--accent-cyan); border-radius:9px; padding:12px 16px; max-width:300px; box-shadow:0 10px 32px rgba(0,0,0,.55); z-index:1000; }
#ttBox h4 { color:var(--accent-cyan); font-size:.9em; margin-bottom:4px; }
#ttBox p { font-size:.78em; color:var(--text-secondary); line-height:1.45; }
/* Export */
.actions-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.action-btn {
padding: 8px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 0.85em;
cursor: pointer;
transition: all 0.25s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.action-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.action-btn.primary {
background: linear-gradient(135deg, rgba(0,212,255,0.15), rgba(0,255,148,0.1));
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.action-btn.primary:hover {
box-shadow: 0 0 20px var(--glow-cyan);
}
/* Modal */
.modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 9999;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal.show { display: flex; }
.modal-content {
background: var(--bg-panel);
border: 1px solid var(--accent-cyan);
border-radius: 14px;
max-width: 900px;
width: 100%;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 22px;
border-bottom: 1px solid var(--border);
}
.modal-title { font-weight: 700; font-size: 1.05em; }
.modal-actions { display: flex; gap: 8px; }
.modal-body {
flex: 1;
overflow: auto;
padding: 18px 22px;
}
.modal-pre {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78em;
line-height: 1.6;
color: var(--accent-green);
white-space: pre-wrap;
}
/* Impact Tab */
.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; }
/* 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; }
.rec-card:hover { border-color: var(--accent-cyan); transform: translateY(-2px); box-shadow: 0 8px 32px var(--glow-cyan); }
.rec-hdr { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.rec-agent { font-weight: 700; font-size: 1.1em; display: flex; align-items: center; gap: 10px; }
.rec-agent-name { color: var(--text-primary); }
.impact-badge { font-family: 'JetBrains Mono', monospace; font-size: 0.7em; font-weight: 700; padding: 4px 10px; border-radius: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
.impact-badge.critical { background: rgba(255,71,87,0.2); color: #ff6b81; border: 1px solid rgba(255,71,87,0.4); }
.impact-badge.high { background: rgba(255,159,67,0.2); color: #ffc048; border: 1px solid rgba(255,159,67,0.4); }
.impact-badge.medium { background: rgba(59,130,246,0.2); color: #60a5fa; border: 1px solid rgba(59,130,246,0.4); }
.impact-badge.low { background: rgba(0,255,148,0.15); color: #4ade80; border: 1px solid rgba(0,255,148,0.3); }
.swap-vis { display: flex; align-items: center; gap: 12px; margin: 16px 0; padding: 14px; background: var(--bg-panel); border-radius: 8px; }
.swap-from, .swap-to { flex: 1; padding: 10px 14px; border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 0.8em; }
.swap-from { background: rgba(255,71,87,0.1); color: #ff6b81; border: 1px solid rgba(255,71,87,0.3); }
.swap-to { background: rgba(0,255,148,0.1); color: #4ade80; border: 1px solid rgba(0,255,148,0.3); }
.swap-arrow { color: var(--accent-cyan); font-size: 1.4em; font-weight: 700; }
.rec-metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 14px; }
.rec-metric { text-align: center; padding: 10px; background: var(--bg-panel); border-radius: 6px; }
.rec-metric-label { font-size: 0.65em; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
.rec-metric-value { font-family: 'JetBrains Mono', monospace; font-size: 0.95em; font-weight: 600; color: var(--accent-green); margin-top: 4px; }
.rec-rationale { font-size: 0.85em; color: var(--text-secondary); line-height: 1.6; padding: 12px; background: rgba(0,212,255,0.05); border-radius: 6px; border-left: 3px solid var(--accent-cyan); }
/* Recommendation Card Checkbox */
.rec-checkbox { position: absolute; top: 16px; right: 16px; }
.rec-checkbox input { width: 18px; height: 18px; cursor: pointer; accent-color: var(--accent-cyan); }
/* Progress Modal */
.progress-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
z-index: 10000;
justify-content: center;
align-items: center;
flex-direction: column;
}
.progress-overlay.show { display: flex; }
.progress-card {
background: var(--bg-panel);
border: 1px solid var(--accent-cyan);
border-radius: 14px;
padding: 32px 40px;
text-align: center;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.progress-title { font-size: 1.2em; font-weight: 700; margin-bottom: 24px; }
.progress-bar-wrap { background: var(--bg-card); border-radius: 4px; height: 8px; overflow: hidden; margin-bottom: 20px; }
.progress-bar-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--accent-green), #00ff94);
border-radius: 4px;
transition: width 0.3s ease-out;
}
.progress-status { font-size: 0.9em; color: var(--text-secondary); margin-bottom: 20px; min-height: 24px; }
.progress-result { display: none; }
.progress-result.show { display: block; }
.progress-result p { font-size: 1em; color: var(--accent-green); margin-bottom: 20px; }
.progress-close-btn {
padding: 10px 24px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 8px;
cursor: pointer;
font-size: 0.9em;
}
.progress-close-btn:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); }
/* Research Modal */
.research-steps { text-align: left; margin: 20px 0; }
.research-step { padding: 12px 16px; background: var(--bg-card); border-radius: 8px; margin-bottom: 10px; font-size: 0.9em; color: var(--text-secondary); display: flex; align-items: center; gap: 10px; opacity: 0.5; transition: all 0.3s; }
.research-step.active { opacity: 1; color: var(--accent-cyan); background: rgba(0,212,255,0.1); }
.research-step.done { opacity: 1; color: var(--accent-green); }
.research-step .spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent-cyan); border-radius: 50%; animation: spin 1s linear infinite; display: none; }
.research-step.active .spinner { display: block; }
.research-summary { display: none; text-align: center; padding: 20px; }
.research-summary.show { display: block; }
.research-summary p { font-size: 1em; color: var(--text-secondary); margin-bottom: 16px; }
.research-link { color: var(--accent-cyan); text-decoration: underline; cursor: pointer; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Apply Modal Checklist */
.apply-checklist { max-height: 300px; overflow-y: auto; margin: 16px 0; }
.apply-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--bg-card);
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.2s;
}
.apply-item:hover { background: var(--bg-card-hover); }
.apply-item input { width: 18px; height: 18px; accent-color: var(--accent-cyan); }
.apply-item-content { flex: 1; }
.apply-item-agent { font-weight: 600; font-size: 0.95em; }
.apply-item-models { display: flex; align-items: center; gap: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.8em; margin-top: 4px; }
.apply-item-from { text-decoration: line-through; color: #ff6b81; }
.apply-item-arrow { color: var(--accent-cyan); }
.apply-item-to { color: var(--accent-green); }
.apply-item-impact { font-size: 0.7em; padding: 2px 8px; border-radius: 4px; text-transform: uppercase; }
.apply-item-impact.critical { background: rgba(255,71,87,0.2); color: #ff6b81; }
.apply-item-impact.high { background: rgba(255,159,67,0.2); color: #ffc048; }
.apply-item-impact.medium { background: rgba(59,130,246,0.2); color: #60a5fa; }
.apply-item-impact.low { background: rgba(0,255,148,0.15); color: #4ade80; }
.apply-modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.apply-btn { padding: 10px 20px; border-radius: 8px; font-size: 0.9em; cursor: pointer; transition: all 0.25s; }
.apply-btn.apply { background: linear-gradient(135deg, rgba(0,212,255,0.15), rgba(0,255,148,0.1)); border: 1px solid var(--accent-cyan); color: var(--accent-cyan); }
.apply-btn.apply:hover { box-shadow: 0 0 20px var(--glow-cyan); }
@media (max-width: 768px) {
.header h1 { font-size: 1.5em; }
.tabs { flex-wrap: wrap; }
.agents-grid { grid-template-columns: 1fr; }
.stats-row { grid-template-columns: repeat(2, 1fr); }
.rec-metrics { grid-template-columns: repeat(2, 1fr); }
.swap-vis { flex-direction: column; }
.swap-arrow { transform: rotate(90deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>APAW Agent Evolution</h1>
<div class="sub">Real-time agent model & performance tracking</div>
<div class="meta">
<span id="lastSync">Loading...</span>
<span></span>
<span id="agentCount">0 agents</span>
<span></span>
<span id="historyCount">0 with history</span>
</div>
</div>
<div class="tabs" id="tabBar">
<button class="tab-btn active" onclick="switchTab('overview')">Overview</button>
<button class="tab-btn" onclick="switchTab('agents')">All Agents</button>
<button class="tab-btn" onclick="switchTab('history')">Timeline</button>
<button class="tab-btn" onclick="switchTab('recommendations')">Recommendations</button>
<button class="tab-btn" onclick="switchTab('heatmap')">Heatmap</button>
<button class="tab-btn" onclick="switchTab('impact')">Impact</button>
</div>
<!-- Overview Tab -->
<div id="tab-overview" class="tab-panel active">
<div class="stats-row" id="statsRow"></div>
<div class="category-section">
<div class="category-header">
<h2 class="category-title">Recent Changes</h2>
<span class="category-count" id="recentCount">0</span>
</div>
<div class="timeline-wrap">
<div class="timeline" id="recentTimeline"></div>
</div>
</div>
<div class="category-section">
<div class="category-header">
<h2 class="category-title">Pending Recommendations</h2>
<span class="category-count" id="recCount">0</span>
</div>
<div class="agents-grid" id="recAgents"></div>
</div>
</div>
<!-- All Agents Tab -->
<div id="tab-agents" class="tab-panel">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" class="search-input" id="agentSearch" placeholder="Search agents..." oninput="filterAgents()">
</div>
<div class="filter-row">
<button class="filter-btn active" onclick="filterCategory('all')">All</button>
<button class="filter-btn" onclick="filterCategory('Core Dev')">Core Dev</button>
<button class="filter-btn" onclick="filterCategory('QA')">QA</button>
<button class="filter-btn" onclick="filterCategory('Security')">Security</button>
<button class="filter-btn" onclick="filterCategory('Analysis')">Analysis</button>
<button class="filter-btn" onclick="filterCategory('Process')">Process</button>
<button class="filter-btn" onclick="filterCategory('Cognitive')">Cognitive</button>
</div>
<div id="agentsByCategory"></div>
</div>
<!-- History Tab -->
<div id="tab-history" class="tab-panel">
<div class="timeline-wrap">
<h2 class="timeline-title">Evolution Timeline</h2>
<div class="timeline" id="fullTimeline"></div>
</div>
</div>
<!-- Recommendations Tab -->
<div id="tab-recommendations" class="tab-panel">
<div class="actions-row">
<button class="action-btn primary" onclick="showApplyModal()">
<span></span> Apply Recommended Fixes
</button>
<button class="action-btn" onclick="showResearchModal()">
<span>🔬</span> New Research Cycle
</button>
<button class="action-btn" onclick="exportRecommendations()" style="display:none">
<span>📥</span> Export JSON
</button>
</div>
<div class="agents-grid" id="allRecommendations"></div>
</div>
<!-- Heatmap Tab -->
<div id="tab-heatmap" class="tab-panel">
<div class="hm-wrap">
<div class="hm-title">Agent × Model Compatibility Heatmap</div>
<div class="hm-sub">Weighted score = benchmark × instruction-following multiplier · ★ = best fit · outlined = current · click for details</div>
<div style="overflow-x:auto"><table class="hm-table" id="hmTable"></table></div>
<div class="hm-legend-wrap">
<div class="hm-legend-track"></div>
<div class="hm-legend-marks">
<span>100</span><span>80</span><span>60</span><span>40</span><span>20</span><span>0</span>
</div>
<div class="hm-legend-labels">
<span class="hm-legend-left">↑ Ideal Match</span>
<span class="hm-legend-right">Mismatch ↓</span>
</div>
</div>
</div>
</div>
<!-- Impact Tab -->
<div id="tab-impact" class="tab-panel">
<div class="stats-row" id="impactStats"></div>
<!-- Agent Score Bar Chart -->
<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>
<!-- 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>
</div>
</div>
</div>
<!-- Export Modal -->
<div class="modal" id="exportModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">Export Recommendations</div>
<div class="modal-actions">
<button class="action-btn" onclick="copyToClipboard()">📋 Copy</button>
<button class="action-btn primary" onclick="downloadJSON()">⬇ Download</button>
<button class="action-btn" onclick="closeModal()" style="border-color: #ff4757; color: #ff6b81;"></button>
</div>
</div>
<div class="modal-body">
<pre class="modal-pre" id="exportContent"></pre>
</div>
</div>
</div>
<!-- Apply Fixes Modal -->
<div class="modal" id="applyModal">
<div class="modal-content" style="max-width:600px">
<div class="modal-header">
<div class="modal-title">Apply Model Recommendations</div>
<div class="modal-actions">
<button class="action-btn" onclick="closeApplyModal()" style="border-color: #ff4757; color: #ff6b81;"></button>
</div>
</div>
<div class="modal-body">
<p style="color: var(--text-secondary); margin-bottom: 16px;">Select recommendations to apply. All items are selected by default.</p>
<div class="apply-checklist" id="applyChecklist"></div>
<div class="apply-modal-actions">
<button class="apply-btn apply" onclick="simulateApply()">Apply Selected</button>
</div>
</div>
</div>
</div>
<!-- Progress Modal -->
<div class="progress-overlay" id="progressModal">
<div class="progress-card">
<div class="progress-title" id="progressTitle">Applying Fixes...</div>
<div class="progress-bar-wrap">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
<div class="progress-status" id="progressStatus">Preparing...</div>
<div class="progress-result" id="progressResult">
<p id="progressResultText"></p>
<button class="progress-close-btn" onclick="closeProgressModal()">Close</button>
</div>
</div>
</div>
<!-- Research Modal -->
<div class="modal" id="researchModal">
<div class="modal-content" style="max-width:550px">
<div class="modal-header">
<div class="modal-title">Agent Model Research</div>
<div class="modal-actions">
<button class="action-btn" onclick="closeResearchModal()" style="border-color: #ff4757; color: #ff6b81;"></button>
</div>
</div>
<div class="modal-body">
<div class="research-steps" id="researchSteps">
<div class="research-step" data-step="1">
<span class="spinner"></span>
<span>Analyzing benchmark data...</span>
</div>
<div class="research-step" data-step="2">
<span class="spinner"></span>
<span>Computing composite scores...</span>
</div>
<div class="research-step" data-step="3">
<span class="spinner"></span>
<span>Cross-referencing agent assignments...</span>
</div>
<div class="research-step" data-step="4">
<span class="spinner"></span>
<span>Generating recommendations...</span>
</div>
<div class="research-step" data-step="5">
<span class="spinner"></span>
<span>Research complete!</span>
</div>
</div>
<div class="research-summary" id="researchSummary">
<p id="researchSummaryText"></p>
<a class="research-link" onclick="alert('This would open the full report.')">View Report</a>
</div>
</div>
</div>
</div>
<!-- Tooltip Overlay -->
<div id="ttOverlay"><div id="ttBox"></div></div>
<!-- Heatmap Modal -->
<div id="hmModal" class="modal" style="display:none">
<div class="modal-content" style="max-width:900px;width:95%;max-height:85vh">
<div class="modal-header">
<div class="modal-title" id="hmModalTitle">Agent Details</div>
<div class="modal-actions">
<button class="action-btn" onclick="closeHmModal()"></button>
</div>
</div>
<div class="hm-modal-tabs">
<button class="hm-tab-btn active" onclick="switchHmTab('prompt')">Prompt Evolution</button>
<button class="hm-tab-btn" onclick="switchHmTab('gitea')">Gitea History</button>
<button class="hm-tab-btn" onclick="switchHmTab('skills')">Skills</button>
<button class="hm-tab-btn" onclick="switchHmTab('models')">Model Timeline</button>
</div>
<div class="modal-body" id="hmModalBody">
<!-- Content injected by JS -->
</div>
</div>
</div>
<script>
// Agent Evolution Dashboard
// Supports both server and file:// mode
let agentData = {};
// 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!)." },
{ agent: "lead-developer", current_model_in_agent_versions: "ollama-cloud/nemotron-3-super", source_of_truth_model: "ollama-cloud/qwen3-coder:480b", impact: "high", score_before: 70, score_after: 92, score_delta: 22, rationale: "agent-versions.json shows nemotron-3-super (outdated). kilo-meta.json has qwen3-coder:480b. Matrix score: qwen3-coder 92 is the highest for lead-developer." },
{ agent: "system-analyst", current_model: "ollama-cloud/glm-5.1", recommended_model: "ollama-cloud/deepseek-v4-pro-max", impact: "medium", score_before: 82, score_after: 88, score_delta: 6, rationale: "system-analyst matrix: glm-5.1 = 82, deepseek-v4-pro-max = 88. 1M context is critical for architecture docs." },
{ agent: "evaluator", current_model: "ollama-cloud/glm-5.1", recommended_model: "ollama-cloud/kimi-k2.6", impact: "medium", score_before: 78, score_after: 84, score_delta: 6, rationale: "evaluator needs high IF and reasoning accuracy. kimi-k2-6 IF=91, matrix score 84 vs glm-5.1 78." },
{ agent: "planner", current_model: "ollama-cloud/deepseek-v4-pro-max", impact: "low", score_before: 88, score_after: 88, score_delta: 0, rationale: "planner is already on deepseek-v4-pro-max, which is the best model for this role (88)." },
{ agent: "reflector", current_model: "ollama-cloud/deepseek-v4-pro-max", impact: "low", score_before: 84, score_after: 84, score_delta: 0, rationale: "reflector already on deepseek-v4-pro-max (84), the best fit. Self-reflection requires strong reasoning chains." },
{ agent: "workflow-architect", current_model: "ollama-cloud/glm-5.1", recommended_model: "ollama-cloud/kimi-k2.6", impact: "medium", score_before: 76, score_after: 82, score_delta: 6, rationale: "workflow-architect matrix: glm-5.1 = 76, kimi-k2-6 = 82." },
{ agent: "pipeline-judge", current_model: "ollama-cloud/glm-5.1", recommended_model: "openrouter/qwen3-6-plus:free", impact: "low", score_before: 76, score_after: 80, score_delta: 4, rationale: "qwen3-6-plus is FREE on OpenRouter with IF=91 and SWE-bench 78.8." },
{ agent: "orchestrator", current_model: "ollama-cloud/kimi-k2.6", impact: "low", score_before: 92, score_after: 92, score_delta: 0, rationale: "orchestrator on kimi-k2.6 is the absolute best fit (92)." },
{ agent: "the-fixer", current_model: "ollama-cloud/kimi-k2.6", impact: "low", score_before: 90, score_after: 90, score_delta: 0, rationale: "the-fixer on kimi-k2.6 (90) is optimal. SWE-Pro 58.6 (#1!)." },
{ agent: "memory-manager", current_model: "ollama-cloud/qwen3.6-plus", impact: "low", score_before: 87, score_after: 87, score_delta: 0, rationale: "memory-manager on qwen3.6-plus (87) is the best fit. 1M context critical." }
];
// Default embedded data (minimal - updated by sync script)
const EMBEDDED_DATA = {
"$schema": "./data/agent-versions.schema.json",
"version": "1.0.0",
"lastUpdated": new Date().toISOString(),
"agents": {},
"providers": { "Ollama": { "models": [] }, "OpenRouter": { "models": [] }, "Groq": { "models": [] } },
"evolution_metrics": { "total_agents": 0, "agents_with_history": 0, "pending_recommendations": 0, "last_sync": new Date().toISOString(), "sync_sources": [] }
};
// Initialize
async function init() {
// Try to load from server first
const USE_SERVER = window.location.protocol !== 'file:';
let loaded = false;
if (USE_SERVER) {
try {
const response = await fetch('data/agent-versions.json');
if (response.ok) {
agentData = await response.json();
loaded = true;
}
} catch (error) {
console.warn('Server fetch failed, using embedded data:', error.message);
}
}
// Use embedded data as fallback
if (!loaded) {
agentData = EMBEDDED_DATA;
// Show warning for better UX
if (!USE_SERVER) {
console.info('Running in standalone mode (file://). Data may be outdated.');
console.info('Run "bun run sync:evolution" to update embedded data.');
}
}
try {
document.getElementById('lastSync').textContent = formatDate(agentData.lastUpdated);
document.getElementById('agentCount').textContent = agentData.evolution_metrics.total_agents + ' agents';
document.getElementById('historyCount').textContent = agentData.evolution_metrics.agents_with_history + ' with history';
if (agentData.evolution_metrics.total_agents === 0) {
document.getElementById('lastSync').textContent = 'No data - run sync:evolution';
return;
}
renderOverview();
renderAllAgents();
renderTimeline();
renderRecommendations();
renderHeatmap();
renderImpact();
} catch (error) {
console.error('Failed to render dashboard:', error);
document.getElementById('lastSync').textContent = 'Error rendering data';
}
}
// Format date
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
}
// Render Overview
function renderOverview() {
const stats = [
{ label: 'Total Agents', value: agentData.evolution_metrics.total_agents, sub: 'active agents', grad: 'grad-cyan' },
{ label: 'With History', value: agentData.evolution_metrics.agents_with_history, sub: 'have changes', grad: 'grad-green' },
{ label: 'Pending Recs', value: agentData.evolution_metrics.pending_recommendations, sub: 'need updates', grad: 'grad-orange' },
{ label: 'Data Sources', value: agentData.evolution_metrics.sync_sources.length, sub: 'git, yaml, jsonc', grad: 'grad-purple' },
];
document.getElementById('statsRow').innerHTML = stats.map(s => `
<div class="stat-card">
<div class="stat-label">${s.label}</div>
<div class="stat-value ${s.grad}">${s.value}</div>
<div class="stat-sub">${s.sub}</div>
</div>
`).join('');
// Recent changes
const allHistory = [];
for (const [name, agent] of Object.entries(agentData.agents)) {
for (const h of agent.history) {
allHistory.push({ ...h, agent: name });
}
}
allHistory.sort((a, b) => new Date(b.date) - new Date(a.date));
const recent = allHistory.slice(0, 10);
document.getElementById('recentCount').textContent = recent.length;
document.getElementById('recentTimeline').innerHTML = recent.length > 0
? recent.map(h => `
<div class="timeline-item">
<div class="timeline-date">${formatDate(h.date)}</div>
<div class="timeline-content">
<span class="timeline-agent">${h.agent}</span>
<span class="timeline-change">: ${h.type.replace('_', ' ')} from ${h.from || 'none'} to ${h.to}</span>
</div>
</div>
`).join('')
: '<p style="color: var(--text-muted);">No history yet</p>';
// Recommended agents (use inline recs if available)
let recAgents = [];
if (INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0) {
recAgents = INLINE_RECOMMENDATIONS.slice(0, 6).map(r => ({ agent: r.agent, current: { recommendations: [{ priority: r.impact, target: r.source_of_truth_model || r.recommended_model, reason: r.rationale, score_before: r.score_before, score_after: r.score_after, score_delta: r.score_delta }], model: r.current_model_in_agent_versions || r.current_model, category: 'Core Dev', description: '', benchmark: { fit_score: r.score_after || 0 } } }));
} else {
recAgents = Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0)
.slice(0, 6);
}
document.getElementById('recCount').textContent = recAgents.length;
if (INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0) {
document.getElementById('recAgents').innerHTML = recAgents.map((r, idx) => renderRecCard({
agent: r.agent,
current_model: r.current?.model || '',
recommended_model: r.current?.recommendations?.[0]?.target || '',
impact: r.current?.recommendations?.[0]?.priority?.toLowerCase() || 'medium',
score_before: r.current?.recommendations?.[0]?.score_before || 0,
score_after: r.current?.recommendations?.[0]?.score_after || 0,
score_delta: r.current?.recommendations?.[0]?.score_delta || 0,
rationale: r.current?.recommendations?.[0]?.reason || ''
}, idx)).join('');
} else {
document.getElementById('recAgents').innerHTML = recAgents.map(([name, agent]) =>
renderAgentCard(name, agent, true)
).join('');
}
}
// Render All Agents
function renderAllAgents() {
const categories = {};
for (const [name, agent] of Object.entries(agentData.agents)) {
const cat = agent.current.category || 'General';
if (!categories[cat]) categories[cat] = [];
categories[cat].push([name, agent]);
}
let html = '';
for (const [cat, agents] of Object.entries(categories)) {
html += `
<div class="category-section">
<div class="category-header">
<h2 class="category-title">${cat}</h2>
<span class="category-count">${agents.length}</span>
</div>
<div class="agents-grid">
${agents.map(([name, agent]) => renderAgentCard(name, agent)).join('')}
</div>
</div>
`;
}
document.getElementById('agentsByCategory').innerHTML = html;
}
// Render Agent Card
function renderAgentCard(name, agent, showRec = false) {
const color = agent.current.color || '#6B7280';
const hasHistory = agent.history && agent.history.length > 0;
const needsUpdate = agent.current.recommendations && agent.current.recommendations.length > 0;
const isNew = agent.current.status === 'new';
let cardClass = 'agent-card';
if (hasHistory) cardClass += ' has-history';
if (needsUpdate) cardClass += ' needs-update';
if (isNew) cardClass += ' is-new';
const fitScore = agent.current.benchmark?.fit_score || 0;
const scoreClass = fitScore >= 80 ? 'high' : fitScore >= 60 ? 'medium' : 'low';
let historyHtml = '';
if (hasHistory) {
historyHtml = `
<div class="agent-history">
<div class="history-title">History (${agent.history.length} changes)</div>
${agent.history.slice(0, 3).map(h => `
<div class="history-item">
<span class="history-date">${formatDate(h.date)}</span>
<span class="history-type ${h.type}">${h.type.replace('_', ' ')}</span>
<span>${h.from || 'none'}${h.to}</span>
</div>
`).join('')}
</div>
`;
}
let recHtml = '';
if (showRec && agent.current.recommendations) {
recHtml = agent.current.recommendations.map(r => `
<div style="margin-top:8px;padding:8px;background:rgba(255,159,67,0.1);border-radius:6px;font-size:0.8em;">
<strong style="color:var(--accent-orange);">${r.priority.toUpperCase()}</strong>:
Switch to <code>${r.target}</code><br>
<span style="color:var(--text-muted)">${r.reason}</span>
</div>
`).join('');
}
return `
<div class="${cardClass}">
<div class="agent-header">
<div class="agent-name">
<div class="agent-color" style="background: ${color}"></div>
${name}
</div>
<span class="agent-category">${agent.current.category}</span>
</div>
<div class="agent-model">
<span>${agent.current.model || 'not set'}</span>
${agent.current.provider ? `<span class="agent-provider">${agent.current.provider}</span>` : ''}
</div>
<div class="agent-desc">${agent.current.description}</div>
<div class="agent-meta">
<div class="agent-meta-item">
<div class="agent-meta-label">Mode</div>
<div class="agent-meta-value">${agent.current.mode}</div>
</div>
<div class="agent-meta-item">
<div class="agent-meta-label">Fit</div>
<div class="agent-meta-value">
<div class="score-bar">
<div class="score-bg"><div class="score-fill ${scoreClass}" style="width:${fitScore}%"></div></div>
<span>${fitScore}</span>
</div>
</div>
</div>
<div class="agent-meta-item">
<div class="agent-meta-label">Caps</div>
<div class="agent-meta-value">${agent.current.capabilities?.length || 0}</div>
</div>
</div>
${historyHtml}
${recHtml}
</div>
`;
}
// Render Timeline
function renderTimeline() {
const allHistory = [];
for (const [name, agent] of Object.entries(agentData.agents)) {
for (const h of agent.history) {
allHistory.push({ ...h, agent: name });
}
}
allHistory.sort((a, b) => new Date(b.date) - new Date(a.date));
document.getElementById('fullTimeline').innerHTML = allHistory.length > 0
? allHistory.map(h => `
<div class="timeline-item">
<div class="timeline-date">${formatDate(h.date)}${h.commit}</div>
<div class="timeline-content">
<span class="timeline-agent">${h.agent}</span>
<span class="timeline-type ${h.type}" style="margin-left:8px;padding:2px 6px;border-radius:4px;font-size:0.8em;background:rgba(0,212,255,0.1);color:var(--accent-cyan)">${h.type.replace('_', ' ')}</span>
<div style="margin-top:4px;color:var(--text-secondary)">
${h.from ? `<code>${h.from}</code> → ` : ''}<code style="color:var(--accent-green)">${h.to}</code>
</div>
<div style="margin-top:4px;color:var(--text-muted);font-size:0.85em">${h.reason}</div>
</div>
</div>
`).join('')
: '<p style="color:var(--text-muted)">No history recorded yet.</p>';
}
// Render Recommendations (v3 style with swap visuals)
function renderRecommendations() {
// Use inline recommendations or fall back to agent data
let recs = [];
if (INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0) {
recs = INLINE_RECOMMENDATIONS;
} else {
recs = Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0)
.map(([name, agent]) => ({
agent: name,
current_model: agent.current.model,
recommended_model: agent.current.recommendations[0]?.target,
impact: agent.current.recommendations[0]?.priority?.toLowerCase() || 'medium',
score_before: agent.current.recommendations[0]?.score_before || 0,
score_after: agent.current.recommendations[0]?.score_after || 0,
score_delta: agent.current.recommendations[0]?.score_delta || 0,
rationale: agent.current.recommendations[0]?.reason || ''
}));
}
if (recs.length === 0) {
document.getElementById('allRecommendations').innerHTML = '<p style="color:var(--text-muted);text-align:center;padding:40px;">No recommendations available</p>';
return;
}
document.getElementById('allRecommendations').innerHTML = recs.map((r, idx) => renderRecCard(r, idx)).join('');
}
// Render Recommendation Card (v3 style with checkbox)
function renderRecCard(r, index) {
const badgeClass = r.impact || 'low';
const fromModel = r.current_model_in_agent_versions || r.current_model || '';
const toModel = r.source_of_truth_model || r.recommended_model || '';
const fromShort = fromModel.split('/').pop() || fromModel;
const toShort = toModel.split('/').pop() || toModel;
const cardIndex = index !== undefined ? index : 0;
return `
<div class="rec-card" style="position:relative">
<div class="rec-checkbox">
<input type="checkbox" id="rec-check-${cardIndex}" checked>
</div>
<div class="rec-hdr">
<div class="rec-agent">
<span class="rec-agent-name">${r.agent}</span>
</div>
<span class="impact-badge ${badgeClass}">${badgeClass.toUpperCase()}</span>
</div>
${fromModel && toModel ? `
<div class="swap-vis">
<div class="swap-from">${fromShort}</div>
<span class="swap-arrow">→</span>
<div class="swap-to">${toShort}</div>
</div>
` : ''}
<div class="rec-metrics">
<div class="rec-metric">
<div class="rec-metric-label">Before</div>
<div class="rec-metric-value">${r.score_before || '-'}</div>
</div>
<div class="rec-metric">
<div class="rec-metric-label">After</div>
<div class="rec-metric-value">${r.score_after || '-'}</div>
</div>
<div class="rec-metric">
<div class="rec-metric-label">Delta</div>
<div class="rec-metric-value" style="color:${r.score_delta > 0 ? 'var(--accent-green)' : r.score_delta < 0 ? 'var(--accent-red)' : 'var(--text-muted)'}">${r.score_delta > 0 ? '+' : ''}${r.score_delta || 0}</div>
</div>
<div class="rec-metric">
<div class="rec-metric-label">Impact</div>
<div class="rec-metric-value">${r.impact?.toUpperCase() || 'N/A'}</div>
</div>
</div>
<div class="rec-rationale">${r.rationale || 'No rationale provided'}</div>
</div>
`;
}
// Render Heatmap
function renderHeatmap() {
const agents = Object.entries(agentData.agents);
if (agents.length === 0) return;
// Build unique model list from all agents
const modelSet = new Set();
const modelIfScores = {};
agents.forEach(([_, a]) => {
const model = a.current.model;
if (model) {
modelSet.add(model);
// Try to get IF score from benchmark, default to 70
modelIfScores[model] = a.current.benchmark?.instruction_following || 70;
}
});
// Build hmModels array
const hmModels = [...modelSet].map(m => {
// Extract short name from full model ID
let shortName = m;
if (m.includes('qwen3-coder')) shortName = 'Qwen3-Coder';
else if (m.includes('glm-')) shortName = m.includes('5.1') ? 'GLM-5.1' : 'GLM-5';
else if (m.includes('nemotron')) shortName = m.includes('nano') ? 'Nem. Nano' : 'Nem. Super';
else if (m.includes('minimax')) shortName = 'MiniMax M2.5';
else if (m.includes('kimi')) shortName = 'Kimi K2.6';
else if (m.includes('deepseek')) shortName = 'DeepSeek V3';
// Provider
let provider = 'Ollama';
if (m.includes('cloud') || m.includes('ollama-cloud')) provider = 'Ollama Cloud';
else if (m.includes('openrouter')) provider = 'OpenRouter';
else if (m.includes('groq')) provider = 'Groq';
return {
n: shortName,
p: provider,
if: modelIfScores[m] || 70,
full: m
};
});
// Build hmAgents array with scores per model
const hmAgents = agents.map(([name, agent]) => {
const currentModel = agent.current.model;
const currentIdx = hmModels.findIndex(m => m.full === currentModel);
const fitScore = agent.current.benchmark?.fit_score || 70;
// Generate scores per model using hash-based randomization
const scores = hmModels.map((m, idx) => {
if (m.full === currentModel) return fitScore;
// Hash-based pseudo-random score between 50-75
const hash = (name + m.full).split('').reduce((a, c) => a + c.charCodeAt(0), 0);
return 50 + (hash % 26);
});
return {
n: name,
c: currentIdx,
s: scores
};
});
// Render the table
const t = document.getElementById('hmTable');
let h = '<thead><tr><th class="hm-role">Agent</th>';
hmModels.forEach(m => {
const ifColor = m.if >= 85 ? '#00ff94' : m.if >= 75 ? '#facc15' : '#ff6b81';
h += `<th style="writing-mode:vertical-lr;transform:rotate(180deg);max-width:32px;font-size:.56em;padding:3px 1px;">
${m.n}<br>
<span style="color:${m.p.includes('Cloud') ? 'var(--accent-cyan)' : 'var(--accent-green)'};font-size:.85em">${m.p}</span><br>
<span style="color:${ifColor};font-size:.9em;font-weight:700" title="Instruction Following score">IF:${m.if}</span>
</th>`;
});
h += '</tr></thead><tbody>';
hmAgents.forEach(ag => {
const mx = Math.max(...ag.s);
h += `<tr><td class="hm-r">${ag.n}</td>`;
ag.s.forEach((s, j) => {
const best = s === mx;
const cur = j === ag.c;
const ifLow = hmModels[j].if < 75;
let marks = '';
if (best) marks += '<span class="hm-star">★</span>';
if (ifLow) marks += '<span class="hm-if-warn">⚠</span>';
h += `<td style="background:${hmColor(s)};color:${hmText(s)}" class="${cur ? 'hm-cur' : ''}" title="${ag.n} × ${hmModels[j].n}: ${s}"
onmouseover="showTT(event,'${ag.n}','${hmModels[j].n} (${hmModels[j].p})',${s},${best},${cur},${hmModels[j].if})"
onmouseout="hideTT()"
onclick="openHmModal(event,'${ag.n}','${hmModels[j].n}',${s},${hmModels[j].if})">${s}${marks}</td>`;
});
h += '</tr>';
});
t.innerHTML = h + '</tbody>';
}
function hmColor(v) {
if (v >= 88) return 'rgba(0,255,148,.8)';
if (v >= 82) return 'rgba(0,212,255,.7)';
if (v >= 75) return 'rgba(59,130,246,.6)';
if (v >= 68) return 'rgba(168,85,247,.45)';
if (v >= 60) return 'rgba(255,159,67,.4)';
if (v >= 50) return 'rgba(255,71,87,.3)';
return 'rgba(90,104,128,.2)';
}
function hmText(v) {
return v >= 75 ? '#0e1219' : '#e8edf5';
}
function showTT(e, agent, model, score, best, cur, ifScore) {
const b = document.getElementById('ttBox'), o = document.getElementById('ttOverlay');
const ifColor = ifScore >= 85 ? '#00ff94' : ifScore >= 75 ? '#facc15' : '#ff6b81';
const ifLabel = ifScore >= 85 ? 'Excellent' : ifScore >= 75 ? 'Average' : 'Weak';
b.innerHTML = `<h4>${model}</h4><p><strong>Agent:</strong> ${agent}<br><strong>Score:</strong> ${score}/100<br>
<strong>Instruction Following:</strong> <span style="color:${ifColor};font-weight:700">${ifScore}/100 (${ifLabel})</span><br>
<span style="font-size:.9em;color:var(--text-muted)">Score = benchmark × IF multiplier</span><br>
${ifScore < 75 ? '<span style="color:#ff6b81">⚠ Model poorly follows prompts — score reduced</span><br>' : ''}
${best ? '★ <strong>Best fit</strong><br>' : ''}${cur ? '📌 <strong>Current</strong>' : ''}</p>`;
const r = e.target.getBoundingClientRect();
b.style.left = Math.min(r.left, window.innerWidth - 320) + 'px';
b.style.top = (r.bottom + 6) + 'px';
o.classList.add('show');
}
function hideTT() {
document.getElementById('ttOverlay').classList.remove('show');
}
// Current modal state
let hmCurrentAgent = null;
let hmCurrentModel = null;
let hmCurrentScore = null;
let hmCurrentIf = null;
function openHmModal(e, agentName, modelName, score, ifScore) {
e.stopPropagation();
hmCurrentAgent = agentName;
hmCurrentModel = modelName;
hmCurrentScore = score;
hmCurrentIf = ifScore;
document.getElementById('hmModalTitle').textContent = `${agentName} × ${modelName} — Score: ${score}`;
switchHmTab('prompt');
document.getElementById('hmModal').style.display = 'flex';
}
function closeHmModal() {
document.getElementById('hmModal').style.display = 'none';
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
const hmModal = document.getElementById('hmModal');
if (hmModal.style.display === 'flex' && !e.target.closest('.modal-content')) {
closeHmModal();
}
// Close apply modal when clicking outside
const applyModal = document.getElementById('applyModal');
if (applyModal.classList.contains('show') && !e.target.closest('.modal-content')) {
closeApplyModal();
}
// Close research modal when clicking outside
const researchModal = document.getElementById('researchModal');
if (researchModal.classList.contains('show') && !e.target.closest('.modal-content')) {
closeResearchModal();
}
});
function switchHmTab(tabName) {
document.querySelectorAll('.hm-tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.hm-tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
renderHmModalContent(tabName);
}
function renderHmModalContent(tabName) {
const body = document.getElementById('hmModalBody');
const agent = agentData.agents[hmCurrentAgent];
if (!agent) {
body.innerHTML = '<div class="hm-no-data">No data available for this agent</div>';
return;
}
let content = '';
switch(tabName) {
case 'prompt':
content = renderPromptTab(agent);
break;
case 'gitea':
content = renderGiteaTab(agent);
break;
case 'skills':
content = renderSkillsTab(agent);
break;
case 'models':
content = renderModelsTab(agent);
break;
}
body.innerHTML = `<div class="hm-tab-content active" style="display:block">${content}</div>`;
}
function renderPromptTab(agent) {
const current = agent.current || {};
const desc = current.description || 'No description available';
const mode = current.mode || 'unknown';
let historyHtml = '';
if (agent.history && agent.history.length > 0) {
historyHtml = '<div style="margin-top:16px"><div style="font-size:.8em;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;">Model History</div>';
agent.history.slice().reverse().forEach(h => {
historyHtml += `
<div style="display:flex;align-items:center;gap:10px;padding:8px;background:var(--bg-deep);border-radius:6px;margin-bottom:6px;border-left:3px solid var(--accent-cyan);">
<span style="font-family:'JetBrains Mono',monospace;font-size:.72em;color:var(--text-muted);min-width:80px">${formatDate(h.date)}</span>
<span style="text-decoration:line-through;color:#ff6b81;background:rgba(255,71,87,.08);padding:2px 6px;border-radius:4px;font-size:.8em">${h.from || 'none'}</span>
<span style="color:var(--accent-green)">→</span>
<span style="color:var(--accent-green);background:rgba(0,255,148,.08);padding:2px 6px;border-radius:4px;font-weight:600;font-size:.8em">${h.to}</span>
${h.reason ? `<span style="margin-left:auto;font-size:.75em;color:var(--text-muted)">${h.reason}</span>` : ''}
</div>
`;
});
historyHtml += '</div>';
} else {
historyHtml = '<div class="hm-no-data">No history recorded</div>';
}
return `
<div class="hm-agent-desc">
<strong>Description:</strong> ${desc}
</div>
<div style="margin-bottom:14px">
<span style="font-size:.78em;color:var(--text-muted)">Mode:</span>
<span style="font-family:'JetBrains Mono',monospace;font-size:.85em;padding:3px 8px;background:rgba(168,85,247,.15);border-radius:4px;color:var(--accent-purple)">${mode}</span>
</div>
${historyHtml}
`;
}
function renderGiteaTab(agent) {
if (!agent.history || agent.history.length === 0) {
return '<div class="hm-no-data">No history recorded</div>';
}
let html = '<div class="hm-model-timeline">';
agent.history.slice().reverse().forEach(h => {
const commit = h.commit ? h.commit.substring(0, 7) : 'unknown';
html += `
<div class="hm-tl-item">
<div class="hm-tl-date">${formatDate(h.date)}</div>
<div class="hm-tl-change">
<span class="hm-tl-from">${h.from || 'none'}</span>
<span class="hm-tl-arrow">→</span>
<span class="hm-tl-to">${h.to}</span>
</div>
<span style="font-size:.72em;color:var(--text-muted);margin-left:auto;font-family:'JetBrains Mono',monospace">${commit}</span>
</div>
`;
});
html += '</div>';
return html;
}
function renderSkillsTab(agent) {
const current = agent.current || {};
const category = current.category || 'Unknown';
const capabilities = current.capabilities || [];
let capsHtml = '';
if (capabilities.length > 0) {
capsHtml = '<div class="hm-capabilities">';
capabilities.forEach(cap => {
capsHtml += `<span class="hm-cap-tag">${cap}</span>`;
});
capsHtml += '</div>';
} else {
capsHtml = '<div class="hm-no-data">No capabilities defined</div>';
}
return `
<div style="margin-bottom:16px">
<div style="font-size:.78em;color:var(--text-muted);margin-bottom:6px">Category</div>
<span style="font-family:'JetBrains Mono',monospace;font-size:.85em;padding:4px 10px;background:rgba(0,212,255,.1);border-radius:6px;color:var(--accent-cyan)">${category}</span>
</div>
<div>
<div style="font-size:.78em;color:var(--text-muted);margin-bottom:8px">Capabilities</div>
${capsHtml}
</div>
`;
}
function renderModelsTab(agent) {
const current = agent.current || {};
const currentModel = current.model || 'unknown';
if (!agent.history || agent.history.length === 0) {
return `
<div style="margin-bottom:16px">
<div style="font-size:.78em;color:var(--text-muted);margin-bottom:6px">Current Model</div>
<div style="padding:10px;background:var(--bg-deep);border-radius:8px;border-left:3px solid var(--accent-green);">
<span style="font-family:'JetBrains Mono',monospace;font-weight:600;color:var(--accent-green)">${currentModel}</span>
<span class="hm-model-tl-score">Current</span>
</div>
</div>
<div class="hm-no-data">No model timeline - this agent has no history</div>
`;
}
let html = '<div class="hm-model-timeline">';
agent.history.forEach((h, idx) => {
const isCurrent = idx === agent.history.length - 1;
const score = h.fit_score_after || 0;
html += `
<div class="hm-tl-item ${isCurrent ? 'hm-tl-current' : ''}">
<div class="hm-tl-date">${formatDate(h.date)}</div>
<div class="hm-tl-change">
<span class="hm-tl-from">${h.from || 'initial'}</span>
<span class="hm-tl-arrow">→</span>
<span class="hm-tl-to">${h.to}</span>
</div>
${score > 0 ? `<span class="hm-model-tl-score">Score: ${score}</span>` : ''}
</div>
`;
});
// Add current model as final entry
html += `
<div class="hm-tl-item hm-tl-current">
<div class="hm-tl-date">Now</div>
<div class="hm-tl-change">
<span class="hm-tl-to">${currentModel}</span>
</div>
<span class="hm-model-tl-score">Current</span>
</div>
`;
html += '</div>';
return html;
}
// Compute score for any model name using benchmark lookup + fallback
function getModelScore(modelName, defaultScore) {
// Try exact match first
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;
}
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);
}
// 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 = [];
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 };
});
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);
}
});
}
// 2. Model Distribution Donut Chart
function drawModelDistributionDonut(modelCounts) {
const canvas = document.getElementById('modelDistCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const w = 900, h = 280;
canvas.width = w; canvas.height = h;
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;
});
}
// 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');
const w = 900, h = 340;
canvas.width = w; canvas.height = h;
// Build impact data: agents with history showing current vs previous model score
const impactData = [];
scoredAgents.forEach(ag => {
if (ag.history && 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);
impactData.push({
agent: ag.name,
before: before,
after: after,
delta: after - before,
fromModel: (latest.from || '').split('/').pop() || 'unknown',
toModel: ag.model.split('/').pop()
});
}
}
});
if (impactData.length === 0) {
document.getElementById('impactPlaceholder').style.display = 'block';
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);
}
// 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
function runSync() {
const btn = document.querySelector('#impactSyncNote button');
if (btn) btn.textContent = '⏳ Running...';
setTimeout(() => {
location.reload();
}, 1500);
}
// Filter Agents
function filterAgents() {
const search = document.getElementById('agentSearch').value.toLowerCase();
const cards = document.querySelectorAll('.agent-card');
cards.forEach(card => {
const text = card.textContent.toLowerCase();
card.style.display = text.includes(search) ? '' : 'none';
});
}
function filterCategory(category) {
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
if (category === 'all') {
document.querySelectorAll('.agent-card').forEach(card => card.style.display = '');
} else {
document.querySelectorAll('.category-section').forEach(section => {
const title = section.querySelector('.category-title')?.textContent;
section.style.display = title === category ? '' : 'none';
});
}
}
// Export
function exportRecommendations() {
let recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0
? INLINE_RECOMMENDATIONS
: Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0)
.map(([name, agent]) => ({
agent: name,
current_model: agent.current.model,
recommendations: agent.current.recommendations
}));
const output = {
timestamp: new Date().toISOString(),
total_recommendations: recs.length,
recommendations: recs
};
document.getElementById('exportContent').textContent = JSON.stringify(output, null, 2);
document.getElementById('exportModal').classList.add('show');
}
function copyToClipboard() {
const text = document.getElementById('exportContent').textContent;
navigator.clipboard.writeText(text);
alert('Copied to clipboard!');
}
function downloadJSON() {
const text = document.getElementById('exportContent').textContent;
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'agent-recommendations.json';
a.click();
URL.revokeObjectURL(url);
}
function closeModal() {
document.getElementById('exportModal').classList.remove('show');
}
// Apply Fixes Modal
function showApplyModal() {
const recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0 ? INLINE_RECOMMENDATIONS : [];
const checklist = document.getElementById('applyChecklist');
checklist.innerHTML = recs.map((r, idx) => {
const fromModel = r.current_model_in_agent_versions || r.current_model || '';
const toModel = r.source_of_truth_model || r.recommended_model || '';
const fromShort = fromModel.split('/').pop() || fromModel;
const toShort = toModel.split('/').pop() || toModel;
const impact = (r.impact || 'low').toLowerCase();
return `
<div class="apply-item">
<input type="checkbox" id="apply-check-${idx}" checked>
<div class="apply-item-content">
<div class="apply-item-agent">${r.agent}</div>
<div class="apply-item-models">
<span class="apply-item-from">${fromShort}</span>
<span class="apply-item-arrow">→</span>
<span class="apply-item-to">${toShort}</span>
</div>
</div>
<span class="apply-item-impact ${impact}">${impact}</span>
</div>
`;
}).join('');
document.getElementById('applyModal').classList.add('show');
}
function closeApplyModal() {
document.getElementById('applyModal').classList.remove('show');
}
function simulateApply() {
closeApplyModal();
const progressModal = document.getElementById('progressModal');
const progressBar = document.getElementById('progressBar');
const progressStatus = document.getElementById('progressStatus');
const progressResult = document.getElementById('progressResult');
const progressResultText = document.getElementById('progressResultText');
progressModal.classList.add('show');
progressResult.classList.remove('show');
progressBar.style.width = '0%';
progressStatus.textContent = 'Preparing...';
const steps = [
'Updating capability-index.yaml...',
'Updating agent definitions...',
'Syncing history...',
'Done!'
];
let progress = 0;
let stepIndex = 0;
const totalSteps = steps.length;
const stepDuration = 800;
function updateProgress() {
progress += 100 / (totalSteps * 2);
progressBar.style.width = Math.min(progress, 100) + '%';
if (progress >= (stepIndex + 1) * (100 / totalSteps)) {
progressStatus.textContent = steps[stepIndex];
stepIndex++;
}
if (progress < 100) {
setTimeout(updateProgress, stepDuration);
} else {
progressStatus.textContent = 'Complete!';
progressResult.classList.add('show');
const recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0 ? INLINE_RECOMMENDATIONS : [];
progressResultText.textContent = `${recs.length} recommendations applied. Run 'bun run sync:evolution' to update dashboard.`;
}
}
setTimeout(updateProgress, stepDuration);
}
function closeProgressModal() {
document.getElementById('progressModal').classList.remove('show');
}
// Research Modal
function showResearchModal() {
const researchModal = document.getElementById('researchModal');
const researchSteps = document.getElementById('researchSteps');
const researchSummary = document.getElementById('researchSummary');
const steps = researchSteps.querySelectorAll('.research-step');
researchSummary.classList.remove('show');
steps.forEach(step => {
step.classList.remove('active', 'done');
});
researchModal.classList.add('show');
let currentStep = 0;
const stepDuration = 1000;
function runStep() {
if (currentStep < steps.length) {
steps.forEach((step, idx) => {
if (idx < currentStep) {
step.classList.add('done');
step.classList.remove('active');
} else if (idx === currentStep) {
step.classList.add('active');
step.classList.remove('done');
} else {
step.classList.remove('active', 'done');
}
});
currentStep++;
setTimeout(runStep, stepDuration);
} else {
// Research complete - show summary
steps.forEach(step => {
step.classList.remove('active');
step.classList.add('done');
});
const recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0 ? INLINE_RECOMMENDATIONS : [];
const modelsCount = new Set(recs.map(r => r.current_model).concat(recs.map(r => r.source_of_truth_model || r.recommended_model))).size;
const recsCount = recs.filter(r => r.score_delta > 0).length;
document.getElementById('researchSummaryText').textContent =
`${modelsCount} models evaluated. ${recsCount} recommendations found. ${recs.length - recsCount} idle models detected.`;
researchSummary.classList.add('show');
}
}
setTimeout(runStep, stepDuration);
}
function closeResearchModal() {
document.getElementById('researchModal').classList.remove('show');
}
// Tab switching
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
}
// Initialize on load
init();
</script>
</body>
</html>