diff --git a/Makefile b/Makefile
index 26c08c7..69320d7 100644
--- a/Makefile
+++ b/Makefile
@@ -523,7 +523,7 @@ $(EVALS_DIR)/public/data/metadata.json: $(PATTERN_VIZ)
@$(MAKE) evals-generate
# Deploy to GitHub Pages (builds to docs/evals/)
-evals-deploy: evals-generate
+evals-deploy: evals-generate metrics-history
@echo "Deploying evaluation dashboard to docs/evals/..."
@mkdir -p docs/evals
@cp -r $(EVALS_DIR)/public/* docs/evals/
diff --git a/tools/evals/public/app.js b/tools/evals/public/app.js
index 450ad6f..458a6b3 100644
--- a/tools/evals/public/app.js
+++ b/tools/evals/public/app.js
@@ -23,8 +23,12 @@ let data = {
sensitivity: null,
fills: null,
metricsHistory: null,
+ iterations: null,
};
+// Iterations state
+let filteredIterations = [];
+
let currentView = 'overview';
let player = null;
let playerUI = null;
@@ -97,6 +101,17 @@ async function loadData() {
console.log('Metrics history not available (run "make metrics-history" to generate)');
}
+ // Try to load iterations data (optional - may not exist yet)
+ try {
+ const iterationsRes = await fetch('data/iterations.json');
+ if (iterationsRes.ok) {
+ data.iterations = await iterationsRes.json();
+ filteredIterations = data.iterations.iterations || [];
+ }
+ } catch (e) {
+ console.log('Iterations data not available');
+ }
+
return true;
} catch (err) {
console.error('Error loading data:', err);
@@ -823,6 +838,283 @@ function renderFillsView() {
gridContainer.innerHTML = fillConfigHtml + renderFillPatternGrid(fillPatterns, selectedEntry.energy);
}
+// ============================================================================
+// Iterations View
+// ============================================================================
+
+function renderIterationsView() {
+ // Render summary stats
+ const summary = data.iterations?.summary || { total: 0, successful: 0, failed: 0, successRate: 0 };
+ $('#stat-total').textContent = summary.total;
+ $('#stat-success').textContent = summary.successful;
+ $('#stat-failed').textContent = summary.failed;
+ $('#stat-rate').textContent = `${(summary.successRate * 100).toFixed(1)}%`;
+
+ // Render timeline
+ renderIterationsTimeline();
+
+ // Render iteration cards
+ renderIterationCards();
+
+ // Setup event listeners
+ setupIterationsEventListeners();
+}
+
+function renderIterationsTimeline() {
+ const container = $('#iterations-timeline-chart');
+
+ if (!data.metricsHistory || !data.metricsHistory.timeline || data.metricsHistory.timeline.length < 2) {
+ container.innerHTML = `
+
+
Not enough historical data to display timeline
+
Run evaluations and tag baselines to track progress over time
+
+ `;
+ return;
+ }
+
+ container.innerHTML = renderTimelineChart(data.metricsHistory, 900, 280);
+}
+
+function renderIterationCards() {
+ const container = $('#iterations-list');
+
+ if (!filteredIterations || filteredIterations.length === 0) {
+ container.innerHTML = `
+
+
No iterations found.
+
Run /iterate "goal" to start hill-climbing.
+
+ `;
+ return;
+ }
+
+ const cards = filteredIterations.map(createIterationCard).join('');
+ container.innerHTML = cards;
+
+ // Add event listeners to cards
+ $$('.card-header').forEach(header => {
+ header.addEventListener('click', toggleIterationCardDetails);
+ });
+
+ $$('.toggle-details').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ toggleIterationCardDetails.call(btn.closest('.card-header'), e);
+ });
+ });
+}
+
+function createIterationCard(iteration) {
+ const statusClass = iteration.status || 'unknown';
+ const statusLabel = iteration.status || 'Unknown';
+
+ // Lever info
+ const leverInfo = iteration.lever ? `
+
+ ${iteration.lever.primary}: ${iteration.lever.oldValue !== null ? iteration.lever.oldValue.toFixed(2) : '?'}
+ → ${iteration.lever.newValue !== null ? iteration.lever.newValue.toFixed(2) : '?'}
+ ${iteration.lever.delta ? `(${iteration.lever.delta})` : ''}
+
+ ` : '';
+
+ // Metrics info
+ const metricsInfo = iteration.metrics ? `
+
+ ${iteration.metrics.target ? `
+
+
Target Metric
+
+ ${iteration.metrics.target.delta}
+
+
+ ` : ''}
+ ${iteration.metrics.maxRegression ? `
+
+
Max Regression
+
+ ${iteration.metrics.maxRegression}
+
+
+ ` : ''}
+
+ ` : '';
+
+ // Links
+ const links = [];
+ if (iteration.pr && iteration.pr !== 'null') {
+ links.push(`View PR ${iteration.pr}`);
+ }
+ if (iteration.commit && iteration.commit !== 'null') {
+ links.push(`${iteration.commit.substring(0, 7)}`);
+ }
+ if (iteration.branch && iteration.branch !== 'null') {
+ links.push(`${iteration.branch}`);
+ }
+
+ return `
+
+
+
+
+ ${createIterationCardDetails(iteration)}
+
+
+
+
+ `;
+}
+
+function createIterationCardDetails(iteration) {
+ const sections = [];
+
+ if (iteration.sections?.goal) {
+ sections.push(`
+
+
Goal
+
${escapeHTML(iteration.sections.goal)}
+
+ `);
+ }
+
+ if (iteration.sections?.decision) {
+ sections.push(`
+
+
Decision
+
${escapeHTML(iteration.sections.decision)}
+
+ `);
+ }
+
+ if (iteration.sections?.notes) {
+ sections.push(`
+
+
Notes
+
${escapeHTML(iteration.sections.notes)}
+
+ `);
+ }
+
+ if (sections.length === 0) {
+ return 'No additional details available
';
+ }
+
+ return sections.join('');
+}
+
+function toggleIterationCardDetails(e) {
+ const card = e.currentTarget.closest('.iteration-card');
+ const details = card.querySelector('.card-details');
+ const toggleBtn = card.querySelector('.toggle-details');
+ const toggleText = toggleBtn.querySelector('.toggle-text');
+ const toggleIcon = toggleBtn.querySelector('.toggle-icon');
+
+ if (details.classList.contains('expanded')) {
+ details.classList.remove('expanded');
+ toggleText.textContent = 'Show Details';
+ toggleIcon.classList.remove('expanded');
+ } else {
+ details.classList.add('expanded');
+ toggleText.textContent = 'Hide Details';
+ toggleIcon.classList.add('expanded');
+ }
+}
+
+function setupIterationsEventListeners() {
+ const searchInput = $('#search-iterations');
+ const filterStatus = $('#filter-status');
+ const sortSelect = $('#sort-iterations');
+
+ // Remove existing listeners by replacing elements (simple approach)
+ if (searchInput && !searchInput.dataset.listenerAttached) {
+ searchInput.addEventListener('input', applyIterationsFilters);
+ searchInput.dataset.listenerAttached = 'true';
+ }
+
+ if (filterStatus && !filterStatus.dataset.listenerAttached) {
+ filterStatus.addEventListener('change', applyIterationsFilters);
+ filterStatus.dataset.listenerAttached = 'true';
+ }
+
+ if (sortSelect && !sortSelect.dataset.listenerAttached) {
+ sortSelect.addEventListener('change', applyIterationsSort);
+ sortSelect.dataset.listenerAttached = 'true';
+ }
+}
+
+function applyIterationsFilters() {
+ const searchTerm = ($('#search-iterations')?.value || '').toLowerCase();
+ const statusFilter = $('#filter-status')?.value || 'all';
+
+ filteredIterations = (data.iterations?.iterations || []).filter(iteration => {
+ // Status filter
+ if (statusFilter !== 'all' && iteration.status !== statusFilter) {
+ return false;
+ }
+
+ // Search filter
+ if (searchTerm) {
+ const searchableText = [
+ iteration.id,
+ iteration.goal,
+ iteration.lever?.primary,
+ iteration.sections?.goal,
+ iteration.sections?.decision,
+ iteration.sections?.notes
+ ].filter(Boolean).join(' ').toLowerCase();
+
+ if (!searchableText.includes(searchTerm)) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ applyIterationsSort();
+}
+
+function applyIterationsSort() {
+ const sortBy = $('#sort-iterations')?.value || 'newest';
+
+ switch (sortBy) {
+ case 'oldest':
+ filteredIterations.sort((a, b) => a.id.localeCompare(b.id));
+ break;
+ case 'impact':
+ filteredIterations.sort((a, b) => {
+ const deltaA = parseFloat(a.metrics?.target?.delta?.replace('%', '') || '0');
+ const deltaB = parseFloat(b.metrics?.target?.delta?.replace('%', '') || '0');
+ return deltaB - deltaA;
+ });
+ break;
+ case 'newest':
+ default:
+ filteredIterations.sort((a, b) => b.id.localeCompare(a.id));
+ break;
+ }
+
+ renderIterationCards();
+}
+
+function escapeHTML(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
// ============================================================================
// Navigation
// ============================================================================
@@ -841,7 +1133,7 @@ function showView(viewName) {
case 'sweeps': renderSweepsView(); break;
case 'seeds': renderSeedsView(); break;
case 'fills': renderFillsView(); break;
- case 'sensitivity': renderSensitivityView(); break;
+ case 'iterations': renderIterationsView(); break;
}
}
@@ -865,7 +1157,7 @@ function showView(viewName) {
function getViewFromHash() {
const hash = window.location.hash.slice(1); // Remove the '#'
- const validViews = ['overview', 'presets', 'sweeps', 'seeds', 'fills', 'sensitivity'];
+ const validViews = ['overview', 'presets', 'sweeps', 'seeds', 'fills', 'iterations'];
return validViews.includes(hash) ? hash : 'overview';
}
diff --git a/tools/evals/public/index.html b/tools/evals/public/index.html
index 951f41a..6a2b048 100644
--- a/tools/evals/public/index.html
+++ b/tools/evals/public/index.html
@@ -24,8 +24,7 @@ DuoPulse v5 Pattern Evaluation Dashboard
-
- Iterations
+
@@ -99,11 +98,65 @@ Fill System Evaluation
-
-
- Parameter Sensitivity Analysis
- How algorithm weights affect Pentagon metrics. Click a cell to see the sweep curve.
-
+
+
+ Iteration History
+ Track hill-climbing iterations and algorithm improvements over time.
+
+
+
+
+
+
+
Metrics Progress Over Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/evals/public/js/pentagon.js b/tools/evals/public/js/pentagon.js
index b1e0e21..8aa8003 100644
--- a/tools/evals/public/js/pentagon.js
+++ b/tools/evals/public/js/pentagon.js
@@ -2,15 +2,15 @@
* Pentagon radar chart rendering
*/
-import { METRIC_KEYS, METRIC_LABELS, ZONE_COLORS } from './constants.js';
+import { METRIC_KEYS, METRIC_LABELS, ZONE_COLORS, ZONES } from './constants.js';
import { parseTargetRange } from './utils.js';
/**
- * Generate SVG for a pentagon radar chart
+ * Generate SVG for a pentagon radar chart with interactive zone tooltips
* @param {Object} pentagonStats - Stats with total metrics and zone data
* @param {Object} metricDefs - Metric definitions with target ranges
* @param {number} size - SVG size in pixels
- * @returns {string} SVG markup
+ * @returns {string} SVG markup wrapped in a container with tooltip
*/
export function renderPentagonRadarSVG(pentagonStats, metricDefs = {}, size = 380) {
const cx = size / 2;
@@ -21,14 +21,31 @@ export function renderPentagonRadarSVG(pentagonStats, metricDefs = {}, size = 38
const total = pentagonStats.total || {};
const values = METRIC_KEYS.map(k => total[k] ?? 0.5);
- let svg = `