- build-standalone-fixed.cjs: reads from 4 real sources (agents md, kilo-meta.json, model-benchmarks-verified.json, agent-versions.json); computes recommendations dynamically - build-standalone-direct.cjs: direct data export + HTML embed pipeline - dashboard-smoke-test.ts: Playwright E2E smoke test covering all 6 tabs - model-benchmarks-verified.json: verified IF scores from artificialanalysis.ai for 15 models (SWE-bench unverifiable → null) - agent-versions.json: 347 git history entries extracted for 34 agents - kilo-meta.json: prompt-optimizer → qwen3.5-122b, memory-manager → deepseek-v4-pro-max - index.html: Recommendations tab rendering updated for dynamic data - Dockerfile + docker-compose.yml: mount-driven build, no image rebuild for data changes - README.md: updated dashboard docs and verified benchmark sources
2225 lines
95 KiB
HTML
2225 lines
95 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">
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0"></script>
|
||
<style>
|
||
:root {
|
||
--bg-deep: #0a0f1a;
|
||
--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; }
|
||
.chart-placeholder { text-align: center; padding: 60px 20px; color: var(--text-muted); font-size: 0.95em; }
|
||
.chart-container { position:relative; height:280px; width:100%; }
|
||
.chart-container-sm { position:relative; height:240px; width:100%; }
|
||
|
||
/* Recommendation Cards */
|
||
.rec-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; transition: all 0.3s; margin-bottom: 16px; }
|
||
.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>
|
||
|
||
<!-- Chart 1: Agent Performance Scores -->
|
||
<div class="chart-wrap">
|
||
<div class="chart-title">Agent Performance Scores</div>
|
||
<div class="chart-sub">Composite score per agent based on model benchmarks</div>
|
||
<div class="chart-container"><canvas id="agentScoreChart"></canvas></div>
|
||
</div>
|
||
|
||
<!-- Chart 2 & 3 side by side -->
|
||
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:20px;margin-bottom:24px">
|
||
<div class="chart-wrap">
|
||
<div class="chart-title">Model Distribution</div>
|
||
<div class="chart-sub">Agents per model</div>
|
||
<div class="chart-container-sm"><canvas id="modelDistChart"></canvas></div>
|
||
</div>
|
||
<div class="chart-wrap">
|
||
<div class="chart-title">Migration Impact</div>
|
||
<div class="chart-sub">Before vs after model change score</div>
|
||
<div class="chart-container-sm"><canvas id="migrationImpactChart"></canvas></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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 = {};
|
||
|
||
// Set Chart.js dark theme defaults
|
||
Chart.defaults.color = '#8ba3c0';
|
||
Chart.defaults.borderColor = '#1e2d45';
|
||
Chart.defaults.font.family = "'Inter', sans-serif";
|
||
|
||
// Inline recommendation data fallback (from model-research-latest.json)
|
||
const INLINE_RECOMMENDATIONS = [
|
||
{ agent: "frontend-developer", current_model_in_agent_versions: "ollama-cloud/qwen3-coder:480b", source_of_truth_model: "ollama-cloud/minimax-m2.5", impact: "high", score_before: 86, score_after: 92, score_delta: 6, rationale: "agent-versions.json is stale. kilo-meta.json (source of truth) already has minimax-m2.5. Matrix score for frontend-dev on M2.5 = 92 (highest!)." },
|
||
{ 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." }
|
||
];
|
||
|
||
// Inline benchmark data (fallback when embedded data doesn't have model_benchmarks)
|
||
// SOURCE: agent-evolution/data/model-benchmarks-verified.json v2.0.0
|
||
// All IF scores verified against artificialanalysis.ai. SWE-bench scores removed — none of the 15 models appear on the official swebench.com leaderboard.
|
||
const MODEL_BENCHMARKS = {
|
||
"qwen3.5-122b": { "if_score": 92, "swe_bench": null, "context_window": 128 },
|
||
"qwen3-coder-480b": { "if_score": 88, "swe_bench": null, "context_window": 1000 },
|
||
"deepseek-v4-pro-max": { "if_score": 89, "swe_bench": null, "context_window": 1000 },
|
||
"deepseek-v4-flash": { "if_score": 86, "swe_bench": null, "context_window": 1000 },
|
||
"kimi-k2.6": { "if_score": 91, "swe_bench": null, "context_window": 1000 },
|
||
"kimi-k2.5": { "if_score": 90, "swe_bench": null, "context_window": 256 },
|
||
"minimax-m2.5": { "if_score": 82, "swe_bench": null, "context_window": 128 },
|
||
"minimax-m2.7": { "if_score": 80, "swe_bench": null, "context_window": 128 },
|
||
"glm-5.1": { "if_score": 90, "swe_bench": null, "context_window": 128 },
|
||
"glm-5": { "if_score": 90, "swe_bench": null, "context_window": 128 },
|
||
"nemotron-3-super": { "if_score": 78, "swe_bench": null, "context_window": 1000 },
|
||
"nemotron-3-nano": { "if_score": 68, "swe_bench": null, "context_window": 128 },
|
||
"gemma4-27b": { "if_score": 85, "swe_bench": null, "context_window": 128 },
|
||
"devstral-2": { "if_score": 80, "swe_bench": null, "context_window": 128 },
|
||
"devstral-small-2": { "if_score": 75, "swe_bench": null, "context_window": 128 }
|
||
};
|
||
|
||
// 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 composite score for any model name
|
||
// Formula (v2): IF_score * 0.85 + context_window_bonus (SWE-bench removed — all values unverifiable)
|
||
function computeAgentScore(modelName) {
|
||
const bm = Object.keys(agentData.model_benchmarks || {}).length > 0
|
||
? agentData.model_benchmarks
|
||
: MODEL_BENCHMARKS;
|
||
const key = Object.keys(bm).find(k => modelName.includes(k)) || '';
|
||
if (bm[key]) {
|
||
const m = bm[key];
|
||
// v2 formula: IF-weighted + context bonus. SWE-bench removed due to verification failure.
|
||
let score = (m.if_score || 70) * 0.85;
|
||
const ctx = m.context_window || 128;
|
||
score += ctx >= 1000 ? 15 : ctx >= 256 ? 8 : 4;
|
||
return Math.round(Math.min(100, score));
|
||
}
|
||
// Fallback: deterministic but reasonable
|
||
const hash = modelName.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
||
return 55 + (hash % 25);
|
||
}
|
||
|
||
// Chart 1: Agent Score Bar Chart
|
||
function drawAgentScoreChart(scoredAgents) {
|
||
const ctx = document.getElementById('agentScoreChart')?.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const labels = scoredAgents.map(a => a.name);
|
||
const data = scoredAgents.map(a => a.score);
|
||
const bgColors = scoredAgents.map(a =>
|
||
a.score >= 85 ? '#00ff94' : a.score >= 70 ? '#00d4ff' : a.score >= 55 ? '#a855f7' : '#ff4757'
|
||
);
|
||
|
||
window._agentScoreChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels,
|
||
datasets: [{
|
||
label: 'Composite Score',
|
||
data,
|
||
backgroundColor: bgColors,
|
||
borderRadius: 6,
|
||
borderSkipped: false,
|
||
}]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
backgroundColor: '#0f1525',
|
||
titleColor: '#e8f1ff',
|
||
bodyColor: '#8ba3c0',
|
||
borderColor: '#1e2d45',
|
||
borderWidth: 1,
|
||
callbacks: {
|
||
label: (item) => `${item.raw} — ${scoredAgents[item.dataIndex].model.split('/').pop()}`
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
grid: { color: '#1e2d45' },
|
||
ticks: { color: '#5a7090', font: { family: 'JetBrains Mono', size: 10 } }
|
||
},
|
||
y: {
|
||
grid: { display: false },
|
||
ticks: { color: '#8ba3c0', font: { family: 'JetBrains Mono', size: 11 } }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Chart 2: Model Distribution (Doughnut)
|
||
function drawModelDistChart(modelCounts) {
|
||
const ctx = document.getElementById('modelDistChart')?.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
const entries = Object.entries(modelCounts).filter(([_, c]) => c > 0);
|
||
const labels = entries.map(([m, _]) => m.split('/').pop());
|
||
const data = entries.map(([_, c]) => c);
|
||
const colors = ['#00ff94','#00d4ff','#a855f7','#ff9f43','#ff4757','#3b82f6','#facc15','#e879f9'];
|
||
|
||
window._modelDistChart = new Chart(ctx, {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels,
|
||
datasets: [{
|
||
data,
|
||
backgroundColor: colors.slice(0, entries.length),
|
||
borderColor: '#141c2e',
|
||
borderWidth: 2,
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
cutout: '60%',
|
||
plugins: {
|
||
legend: {
|
||
position: 'right',
|
||
labels: { color: '#8ba3c0', font: { family: 'JetBrains Mono', size: 11 } }
|
||
},
|
||
tooltip: {
|
||
backgroundColor: '#0f1525',
|
||
titleColor: '#e8f1ff',
|
||
bodyColor: '#8ba3c0',
|
||
borderColor: '#1e2d45',
|
||
borderWidth: 1,
|
||
callbacks: {
|
||
label: (item) => ` ${item.label}: ${item.raw} agents (${((item.raw/data.reduce((s,c)=>s+c,0))*100).toFixed(0)}%)`
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Chart 3: Migration Impact (Grouped Bar)
|
||
function drawMigrationChart(scoredAgents) {
|
||
const ctx = document.getElementById('migrationImpactChart')?.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
// Build before/after data from agents with history
|
||
const impactData = [];
|
||
scoredAgents.forEach(ag => {
|
||
if (ag.history.length > 0) {
|
||
const latest = ag.history[ag.history.length - 1];
|
||
if (latest.to && latest.from) {
|
||
const after = ag.score;
|
||
const before = computeAgentScore(latest.from);
|
||
impactData.push({
|
||
name: ag.name.split('-').map(s => s[0]?.toUpperCase() + s.slice(1)).join('-'),
|
||
before, after,
|
||
delta: after - before,
|
||
from: latest.from.split('/').pop(),
|
||
to: ag.model.split('/').pop()
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
if (impactData.length === 0) {
|
||
// No history — show single bars for all agents
|
||
window._migrationChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: scoredAgents.slice(0, 20).map(a => a.name),
|
||
datasets: [{
|
||
label: 'Current Score',
|
||
data: scoredAgents.slice(0, 20).map(a => a.score),
|
||
backgroundColor: '#00ff94',
|
||
borderRadius: 4
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: {
|
||
x: { grid: { display: false }, ticks: { color: '#5a7090', font: { size: 9 }, maxRotation: 45 } },
|
||
y: { grid: { color: '#1e2d45' }, ticks: { color: '#5a7090' } }
|
||
}
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
window._migrationChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: impactData.map(d => d.name),
|
||
datasets: [
|
||
{
|
||
label: 'Before',
|
||
data: impactData.map(d => d.before),
|
||
backgroundColor: 'rgba(255,71,87,.6)',
|
||
borderRadius: 4
|
||
},
|
||
{
|
||
label: 'After',
|
||
data: impactData.map(d => d.after),
|
||
backgroundColor: impactData.map(d => d.delta >= 0 ? 'rgba(0,255,148,.6)' : 'rgba(255,71,87,.6)'),
|
||
borderRadius: 4
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: {
|
||
tooltip: {
|
||
backgroundColor: '#0f1525',
|
||
titleColor: '#e8f1ff',
|
||
bodyColor: '#8ba3c0',
|
||
borderColor: '#1e2d45',
|
||
borderWidth: 1,
|
||
callbacks: {
|
||
afterBody: (items) => {
|
||
const idx = items[0].dataIndex;
|
||
const d = impactData[idx];
|
||
return `Change: ${d.from} → ${d.to}\nDelta: ${d.delta >= 0 ? '+' : ''}${d.delta}`;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: { grid: { display: false }, ticks: { color: '#5a7090', font: { size: 9 }, maxRotation: 45 } },
|
||
y: { grid: { color: '#1e2d45' }, ticks: { color: '#5a7090' } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Render Impact Tab - Chart.js based
|
||
function renderImpact() {
|
||
const allAgents = Object.entries(agentData.agents);
|
||
const modelCounts = {};
|
||
const scoredAgents = [];
|
||
|
||
// Compute scores for all agents
|
||
allAgents.forEach(([name, agent]) => {
|
||
const model = agent.current?.model || 'unknown';
|
||
modelCounts[model] = (modelCounts[model] || 0) + 1;
|
||
const score = computeAgentScore(model);
|
||
scoredAgents.push({ name, model, score, history: agent.history || [] });
|
||
});
|
||
|
||
// Sort by score descending
|
||
scoredAgents.sort((a, b) => b.score - a.score);
|
||
|
||
// Stats row
|
||
const totalAgents = allAgents.length;
|
||
const avgScore = scoredAgents.length > 0
|
||
? (scoredAgents.reduce((s, a) => s + a.score, 0) / scoredAgents.length).toFixed(1)
|
||
: 0;
|
||
const best = scoredAgents[0] || { name: 'N/A', score: 0 };
|
||
const worst = scoredAgents[scoredAgents.length - 1] || { name: 'N/A', score: 0 };
|
||
const changes = allAgents.reduce((sum, [_, a]) => sum + ((a.history || []).length), 0);
|
||
|
||
document.getElementById('impactStats').innerHTML = `
|
||
<div class="stat-card"><div class="stat-label">Total Agents</div><div class="stat-value grad-cyan">${totalAgents}</div><div class="stat-sub">in system</div></div>
|
||
<div class="stat-card"><div class="stat-label">Avg Score</div><div class="stat-value grad-green">${avgScore}</div><div class="stat-sub">composite</div></div>
|
||
<div class="stat-card"><div class="stat-label">Best Model</div><div class="stat-value grad-purple">${best.model.split('/').pop()}</div><div class="stat-sub">score: ${best.score}</div></div>
|
||
<div class="stat-card"><div class="stat-label">Worst Model</div><div class="stat-value grad-orange">${worst.model.split('/').pop()}</div><div class="stat-sub">score: ${worst.score}</div></div>
|
||
<div class="stat-card"><div class="stat-label">Changes Made</div><div class="stat-value grad-cyan">${changes}</div><div class="stat-sub">total migrations</div></div>
|
||
`;
|
||
|
||
// Destroy old charts before creating new ones
|
||
if (window._agentScoreChart) window._agentScoreChart.destroy();
|
||
if (window._modelDistChart) window._modelDistChart.destroy();
|
||
if (window._migrationChart) window._migrationChart.destroy();
|
||
|
||
drawAgentScoreChart(scoredAgents);
|
||
drawModelDistChart(modelCounts);
|
||
drawMigrationChart(scoredAgents);
|
||
}
|
||
|
||
// Filter Agents
|
||
function runSync() {
|
||
const btn = document.querySelector('#impactSyncNote button');
|
||
if (btn) btn.textContent = '⏳ Running...';
|
||
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> |