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
2236 lines
96 KiB
HTML
2236 lines
96 KiB
HTML
<!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> |