1. filterCategory: fix inline event.target → uses btn parameter - All Agents tab filter buttons now correctly toggle active class 2. exportRecommendations/showApplyModal: read from agentData, not removed INLINE_RECOMMENDATIONS - Apply modal shows real recommendations - Export generates JSON with real data 3. Heatmap cell click: add showCellDetail modal with Chart.js line chart + prompt history - onclick='showCellDetail(model, agent)' on every td - renderCellChart computes score history from agent.history - prompt_change items filtered and displayed 4. watch-db.cjs: incremental DB sync tool - Polls git for changes in .kilo/agents/*.md and kilo-meta.json - Detects model_change vs prompt_change by comparing with previous version - Exports to JSON after sync, logs to .kilo/logs/watch-db.log - SIGINT/SIGTERM graceful shutdown - Trigger: npm run evolution:watch
261 lines
11 KiB
JavaScript
261 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Build unified dashboard data by calling export script:
|
||
* 1. parse files → export to JSON
|
||
* 2. embed in HTML
|
||
*
|
||
* Run: node agent-evolution/scripts/build-standalone-fixed.cjs
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const HTML_FILE = path.join(__dirname, '../index.html');
|
||
const OUTPUT_FILE = path.join(__dirname, '../index.standalone.html');
|
||
|
||
try {
|
||
// Step 1: Export data to JSON
|
||
console.log('Exporting data to JSON...');
|
||
const jsonData = require('./export-data-direct.cjs');
|
||
|
||
// ---------- Read HTML ----------
|
||
let html = fs.readFileSync(HTML_FILE, 'utf-8');
|
||
|
||
// ---------- Remove old hardcoded constants ----------
|
||
// Remove INLINE_RECOMMENDATIONS (lines ~1004-1016)
|
||
const inlineRecPattern = /const INLINE_RECOMMENDATIONS = \[[\s\S]*?\];/;
|
||
html = html.replace(inlineRecPattern, 'const INLINE_RECOMMENDATIONS = []; // REMOVED — data now comes from agentData, not hardcoded');
|
||
|
||
// Remove MODEL_BENCHMARKS line ~1021 (will be embedded in JSON)
|
||
const bmPattern = /const MODEL_BENCHMARKS = \{[\s\S]*?\n\};/;
|
||
html = html.replace(bmPattern, '/* MODEL_BENCHMARKS removed — data now in EMBEDDED_DATA.model_benchmarks */');
|
||
|
||
// ---------- Replace EMBEDDED_DATA section ----------
|
||
const startMarker = '// Default embedded data (minimal - updated by sync script)';
|
||
const endMarker = '};';
|
||
|
||
const startIdx = html.indexOf(startMarker);
|
||
if (startIdx === -1) throw new Error('Start marker not found');
|
||
|
||
// Find the start of the EMBEDDED_DATA object
|
||
const dataStartIdx = html.indexOf('const EMBEDDED_DATA = {', startIdx);
|
||
if (dataStartIdx === -1) throw new Error('EMBEDDED_DATA start not found');
|
||
|
||
// Find the end of the EMBEDDED_DATA object (the closing brace followed by semicolon)
|
||
const dataEndIdx = html.indexOf(endMarker, dataStartIdx) + endMarker.length;
|
||
if (dataEndIdx === -1) throw new Error('EMBEDDED_DATA end not found');
|
||
|
||
// Create properly formatted JSON without HTML escaping
|
||
const jsonStr = JSON.stringify(jsonData, null, 2);
|
||
|
||
// Ensure HTML characters are not escaped in string literals
|
||
// This is a workaround for JSON.stringify escaping < and > in some environments
|
||
const safeJsonStr = jsonStr
|
||
.replace(/\\u003c/g, '<')
|
||
.replace(/\\u003e/g, '>');
|
||
|
||
const embeddedData = `// Unified data from REAL sources (${new Date().toISOString()})
|
||
// Sources: .kilo/agents/*.md + kilo-meta.json + model-benchmarks-verified.json
|
||
const EMBEDDED_DATA = ${safeJsonStr};`;
|
||
|
||
html = html.substring(0, dataStartIdx) + embeddedData + html.substring(dataEndIdx);
|
||
|
||
// ---------- Replace init function ----------
|
||
const initStartPattern = /\/\/ Initialize\s*\n\s*async function init\(\)\s*\{/;
|
||
const initStart = html.match(initStartPattern);
|
||
if (initStart) {
|
||
let brace = 0, inFn = false, endIdx = initStart.index;
|
||
for (let i = initStart.index; i < html.length; i++) {
|
||
if (html[i] === '{') { brace++; inFn = true; }
|
||
else if (html[i] === '}') { brace--; if (inFn && brace === 0) { endIdx = i + 1; break; } }
|
||
}
|
||
|
||
const newInit = `// Initialize
|
||
async function init() {
|
||
agentData = 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';
|
||
return;
|
||
}
|
||
renderOverview();
|
||
renderAllAgents();
|
||
renderTimeline();
|
||
renderRecommendations();
|
||
renderHeatmap();
|
||
renderImpact();
|
||
} catch (error) { console.error('Render error:', error); }
|
||
}`;
|
||
html = html.substring(0, initStart.index) + newInit + html.substring(endIdx);
|
||
}
|
||
|
||
// ---------- Replace renderHeatmap function ----------
|
||
const heatmapStartPattern = /function renderHeatmap\(\)\s*\{/;
|
||
const heatmapStart = html.match(heatmapStartPattern);
|
||
if (heatmapStart) {
|
||
let brace = 0, inFn = false, endIdx = heatmapStart.index;
|
||
for (let i = heatmapStart.index; i < html.length; i++) {
|
||
if (html[i] === '{') { brace++; inFn = true; }
|
||
else if (html[i] === '}') { brace--; if (inFn && brace === 0) { endIdx = i + 1; break; } }
|
||
}
|
||
|
||
const newHeatmap = `// Render Heatmap (read from agentData.model_benchmarks)
|
||
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';
|
||
else if (m.includes('qwen3.5')) shortName = 'Qwen3.5';
|
||
else if (m.includes('gemma4')) shortName = 'Gemma4';
|
||
|
||
// 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="showCellDetail(\'' + hmModels[j].full + '\', \'' + ag.n + '\')">' + s + marks + '</td>';
|
||
});
|
||
h += '</tr>';
|
||
});
|
||
t.innerHTML = h + '</tbody>';
|
||
}`;
|
||
|
||
html = html.substring(0, heatmapStart.index) + newHeatmap + html.substring(endIdx);
|
||
}
|
||
|
||
// ---------- Replace renderRecommendations function ----------
|
||
const recStartPattern = /function renderRecommendations\(\)\s*\{/;
|
||
const recStart = html.match(recStartPattern);
|
||
if (recStart) {
|
||
let brace = 0, inFn = false, endIdx = recStart.index;
|
||
for (let i = recStart.index; i < html.length; i++) {
|
||
if (html[i] === '{') { brace++; inFn = true; }
|
||
else if (html[i] === '}') { brace--; if (inFn && brace === 0) { endIdx = i + 1; break; } }
|
||
}
|
||
|
||
const newRec = `// Render Recommendations (only use agentData.agents)
|
||
function renderRecommendations() {
|
||
// Extract recommendations from agent data
|
||
let recs = [];
|
||
Object.entries(agentData.agents).forEach(([name, agent]) => {
|
||
if (agent.current.recommendations && agent.current.recommendations.length > 0) {
|
||
agent.current.recommendations.forEach(rec => {
|
||
recs.push({
|
||
agent: name,
|
||
current_model: agent.current.model,
|
||
recommended_model: rec.target,
|
||
impact: rec.priority || 'medium',
|
||
score_before: rec.score_before || 0,
|
||
score_after: rec.score_after || 0,
|
||
score_delta: rec.score_delta || 0,
|
||
rationale: rec.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('');
|
||
}`;
|
||
|
||
html = html.substring(0, recStart.index) + newRec + html.substring(endIdx);
|
||
}
|
||
|
||
// ---------- Write ----------
|
||
fs.writeFileSync(OUTPUT_FILE, html);
|
||
fs.writeFileSync(path.join(__dirname, '../data/index.html'), html);
|
||
|
||
console.log('\nBuilt standalone dashboard');
|
||
console.log(' Output:', OUTPUT_FILE);
|
||
console.log(' Size:', (fs.statSync(OUTPUT_FILE).size / 1024).toFixed(1), 'KB');
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error.message);
|
||
console.error(error.stack);
|
||
process.exit(1);
|
||
} |