Files
APAW/agent-evolution/index.html
¨NW¨ 15a7b4b7a4 feat: add Agent Evolution Dashboard
- Create agent-evolution/ directory with standalone dashboard
- Add interactive HTML dashboard with agent/model matrix
- Add heatmap view for agent-model compatibility scores
- Add recommendations tab with optimization suggestions
- Add Gitea integration preparation (history timeline)
- Add Docker configuration for deployment
- Add build scripts for standalone HTML generation
- Add sync scripts for agent data synchronization
- Add milestone and issues documentation
- Add skills and rules for evolution sync
- Update AGENTS.md with dashboard documentation
- Update package.json with evolution scripts

Features:
- 28 agents with model assignments and fit scores
- 8 models with benchmarks (SWE-bench, RULER, Terminal)
- 11 recommendations for model optimization
- History timeline with agent changes
- Interactive modal windows for model details
- Filter and search functionality
- Russian language interface
- Works offline (file://) with embedded data

Docker:
- Dockerfile for standalone deployment
- docker-compose.evolution.yml
- docker-run.sh/docker-run.bat scripts

NPM scripts:
- sync:evolution - sync and build dashboard
- evolution:open - open in browser
- evolution:dashboard - start dev server

Status: PAUSED - foundation complete, Gitea integration pending
2026-04-05 19:58:59 +01:00

1062 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APAW Agent Evolution Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg-deep: #0a0f1a;
--bg-panel: #0f1525;
--bg-card: #141c2e;
--bg-card-hover: #1a2540;
--border: #1e2d45;
--border-bright: #2a4060;
--text-primary: #e8f1ff;
--text-secondary: #8ba3c0;
--text-muted: #5a7090;
--accent-cyan: #00d4ff;
--accent-green: #00ff94;
--accent-orange: #ff9f43;
--accent-red: #ff4757;
--accent-purple: #a855f7;
--accent-blue: #3b82f6;
--accent-yellow: #facc15;
--glow-cyan: rgba(0,212,255,0.15);
--glow-green: rgba(0,255,148,0.1);
--glow-purple: rgba(168,85,247,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(0,212,255,0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(168,85,247,0.06) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 24px 16px;
position: relative;
z-index: 1;
}
/* Header */
.header { text-align: center; margin-bottom: 32px; }
.header h1 {
font-size: 2.2em;
font-weight: 800;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.header .sub {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85em;
color: var(--text-muted);
}
.header .meta {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 12px;
font-size: 0.8em;
color: var(--text-secondary);
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 4px;
margin-bottom: 24px;
overflow-x: auto;
}
.tab-btn {
flex: 1;
min-width: 100px;
padding: 10px 16px;
background: none;
border: none;
color: var(--text-secondary);
font-family: 'Inter', sans-serif;
font-size: 0.85em;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
transition: all 0.25s;
white-space: nowrap;
}
.tab-btn:hover { color: var(--text-primary); background: var(--bg-card); }
.tab-btn.active {
color: var(--bg-deep);
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green));
box-shadow: 0 0 20px var(--glow-cyan);
}
.tab-panel { display: none; animation: fadeUp 0.4s ease-out; }
.tab-panel.active { display: block; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
/* Stats */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.stat-card:hover {
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--glow-cyan);
}
.stat-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.65em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 6px;
}
.stat-value { font-size: 2em; font-weight: 800; }
.stat-sub { font-size: 0.75em; color: var(--text-secondary); margin-top: 4px; }
.grad-cyan { background: linear-gradient(135deg, var(--accent-cyan), var(--accent-green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-orange { background: linear-gradient(135deg, var(--accent-orange), var(--accent-yellow)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-purple { background: linear-gradient(135deg, var(--accent-purple), #e879f9); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-green { background: linear-gradient(135deg, var(--accent-green), #4ade80); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.grad-red { background: linear-gradient(135deg, var(--accent-red), #ff6b81); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
/* Agent Grid */
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
}
.agent-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.agent-card:hover {
border-color: var(--accent-cyan);
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--glow-cyan);
}
.agent-card.has-history { border-left: 3px solid var(--accent-green); }
.agent-card.needs-update { border-left: 3px solid var(--accent-orange); }
.agent-card.is-new { border-left: 3px solid var(--accent-purple); }
.agent-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.agent-name {
font-weight: 700;
font-size: 1.05em;
display: flex;
align-items: center;
gap: 8px;
}
.agent-color {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.agent-category {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
background: rgba(0,212,255,0.1);
color: var(--accent-cyan);
}
.agent-model {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78em;
color: var(--accent-green);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.agent-provider {
font-size: 0.7em;
padding: 2px 6px;
border-radius: 4px;
background: rgba(0,255,148,0.1);
color: var(--accent-green);
}
.agent-desc {
font-size: 0.85em;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 12px;
}
.agent-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.agent-meta-item {
text-align: center;
}
.agent-meta-label {
font-size: 0.6em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agent-meta-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
font-weight: 600;
color: var(--text-primary);
}
.agent-history {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border);
}
.history-title {
font-size: 0.7em;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 8px;
}
.history-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.75em;
padding: 6px 0;
border-bottom: 1px solid rgba(30,45,69,0.5);
}
.history-item:last-child { border-bottom: none; }
.history-date {
font-family: 'JetBrains Mono', monospace;
color: var(--text-muted);
min-width: 100px;
}
.history-type {
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85em;
}
.history-type.model_change { background: rgba(0,212,255,0.15); color: var(--accent-cyan); }
.history-type.prompt_change { background: rgba(168,85,247,0.15); color: var(--accent-purple); }
.history-type.agent_created { background: rgba(0,255,148,0.15); color: var(--accent-green); }
/* Category Section */
.category-section { margin-bottom: 32px; }
.category-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.category-title {
font-size: 1.1em;
font-weight: 700;
}
.category-count {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
background: rgba(168,85,247,0.15);
color: var(--accent-purple);
}
/* Evolution Timeline */
.timeline-wrap {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.timeline-title {
font-size: 1.1em;
font-weight: 700;
margin-bottom: 16px;
}
.timeline {
position: relative;
padding-left: 24px;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
padding: 12px 0 12px 24px;
border-bottom: 1px solid var(--border);
}
.timeline-item:last-child { border-bottom: none; }
.timeline-item::before {
content: '';
position: absolute;
left: -20px;
top: 18px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-cyan);
border: 2px solid var(--border);
}
.timeline-date {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75em;
color: var(--text-muted);
}
.timeline-content {
font-size: 0.9em;
margin-top: 4px;
}
.timeline-agent {
font-weight: 600;
color: var(--accent-cyan);
}
.timeline-change {
color: var(--text-secondary);
}
/* Filter Row */
.filter-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.filter-btn {
padding: 6px 14px;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 20px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s;
font-family: 'Inter', sans-serif;
}
.filter-btn:hover, .filter-btn.active {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(0,212,255,0.05);
}
/* Search */
.search-box {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 40px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
font-size: 0.9em;
}
.search-input:focus {
outline: none;
border-color: var(--accent-cyan);
}
.search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
/* Model Matrix */
.matrix-wrap {
overflow-x: auto;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg-card);
padding: 20px;
}
.matrix-title {
font-size: 1.1em;
font-weight: 700;
margin-bottom: 16px;
}
.matrix-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8em;
}
.matrix-table th {
font-family: 'JetBrains Mono', monospace;
font-size: 0.7em;
color: var(--text-muted);
text-transform: uppercase;
padding: 10px 8px;
text-align: left;
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
background: var(--bg-panel);
}
.matrix-table td {
padding: 10px 8px;
border-bottom: 1px solid var(--border);
}
.matrix-table tr:hover td {
background: var(--bg-card-hover);
}
.score-bar {
display: flex;
align-items: center;
gap: 6px;
}
.score-bg {
width: 50px;
height: 5px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 3px;
}
.score-fill.high { background: linear-gradient(90deg, var(--accent-green), #00ff94); }
.score-fill.medium { background: linear-gradient(90deg, var(--accent-orange), #ffc048); }
.score-fill.low { background: linear-gradient(90deg, var(--accent-red), #ff6b81); }
/* 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;
}
@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); }
}
</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('matrix')">Model Matrix</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="exportRecommendations()">
<span>📥</span> Export JSON
</button>
</div>
<div class="agents-grid" id="allRecommendations"></div>
</div>
<!-- Matrix Tab -->
<div id="tab-matrix" class="tab-panel">
<div class="matrix-wrap">
<h2 class="matrix-title">Agent × Model Matrix</h2>
<table class="matrix-table" id="matrixTable">
<thead id="matrixHead"></thead>
<tbody id="matrixBody"></tbody>
</table>
</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>
<script>
// Agent Evolution Dashboard
// Supports both server and file:// mode
let agentData = {};
// 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();
renderMatrix();
} 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('ru-RU', {
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
const recAgents = Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0)
.slice(0, 6);
document.getElementById('recCount').textContent = recAgents.length;
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
function renderRecommendations() {
const recs = Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0);
document.getElementById('allRecommendations').innerHTML = recs.map(([name, agent]) =>
renderAgentCard(name, agent, true)
).join('');
}
// Render Matrix
function renderMatrix() {
const agents = Object.entries(agentData.agents);
const models = [...new Set(agents.map(([_, a]) => a.current.model).filter(Boolean))];
// Header
document.getElementById('matrixHead').innerHTML = `
<tr>
<th>Agent</th>
<th>Model</th>
<th>Provider</th>
<th>Fit Score</th>
<th>Category</th>
<th>Status</th>
</tr>
`;
// Body
document.getElementById('matrixBody').innerHTML = agents.map(([name, agent]) => {
const fit = agent.current.benchmark?.fit_score || 0;
const scoreClass = fit >= 80 ? 'high' : fit >= 60 ? 'medium' : 'low';
const status = agent.current.status === 'new' ? '🆕 New' :
agent.current.recommendations?.length > 0 ? '⚠️ Update' : '✅ OK';
return `
<tr>
<td><strong>${name}</strong></td>
<td><code style="color:var(--accent-green)">${agent.current.model || '—'}</code></td>
<td>${agent.current.provider || '—'}</td>
<td>
<div class="score-bar">
<div class="score-bg"><div class="score-fill ${scoreClass}" style="width:${fit}%"></div></div>
<span>${fit}</span>
</div>
</td>
<td>${agent.current.category}</td>
<td>${status}</td>
</tr>
`;
}).join('');
}
// 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() {
const 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,
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');
}
// 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>