Files
APAW/agent-evolution/scripts/build-standalone-fixed.cjs
Deploy Bot 7f1269a370 fix(dashboard): 3 UI bugs + new DB watch tool
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
2026-05-25 21:50:55 +01:00

261 lines
11 KiB
JavaScript
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.
#!/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);
}