Skip to content

Commit 4d1b449

Browse files
algonormativeclaude
andcommitted
feat(evals): improve dashboard UX with integrated iterations and tooltips
- Integrate iterations page as hash-based view in main dashboard nav - Add interactive zone conformance tooltips to pentagon radar chart - Improve timeline chart: date-based x-axis, clickable git SHA tooltips - Remove sensitivity tab from navigation - Add metrics-history to evals-deploy target for CI builds Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d3e0a8a commit 4d1b449

6 files changed

Lines changed: 608 additions & 39 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ $(EVALS_DIR)/public/data/metadata.json: $(PATTERN_VIZ)
523523
@$(MAKE) evals-generate
524524

525525
# Deploy to GitHub Pages (builds to docs/evals/)
526-
evals-deploy: evals-generate
526+
evals-deploy: evals-generate metrics-history
527527
@echo "Deploying evaluation dashboard to docs/evals/..."
528528
@mkdir -p docs/evals
529529
@cp -r $(EVALS_DIR)/public/* docs/evals/

tools/evals/public/app.js

Lines changed: 294 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ let data = {
2323
sensitivity: null,
2424
fills: null,
2525
metricsHistory: null,
26+
iterations: null,
2627
};
2728

29+
// Iterations state
30+
let filteredIterations = [];
31+
2832
let currentView = 'overview';
2933
let player = null;
3034
let playerUI = null;
@@ -97,6 +101,17 @@ async function loadData() {
97101
console.log('Metrics history not available (run "make metrics-history" to generate)');
98102
}
99103

104+
// Try to load iterations data (optional - may not exist yet)
105+
try {
106+
const iterationsRes = await fetch('data/iterations.json');
107+
if (iterationsRes.ok) {
108+
data.iterations = await iterationsRes.json();
109+
filteredIterations = data.iterations.iterations || [];
110+
}
111+
} catch (e) {
112+
console.log('Iterations data not available');
113+
}
114+
100115
return true;
101116
} catch (err) {
102117
console.error('Error loading data:', err);
@@ -823,6 +838,283 @@ function renderFillsView() {
823838
gridContainer.innerHTML = fillConfigHtml + renderFillPatternGrid(fillPatterns, selectedEntry.energy);
824839
}
825840

841+
// ============================================================================
842+
// Iterations View
843+
// ============================================================================
844+
845+
function renderIterationsView() {
846+
// Render summary stats
847+
const summary = data.iterations?.summary || { total: 0, successful: 0, failed: 0, successRate: 0 };
848+
$('#stat-total').textContent = summary.total;
849+
$('#stat-success').textContent = summary.successful;
850+
$('#stat-failed').textContent = summary.failed;
851+
$('#stat-rate').textContent = `${(summary.successRate * 100).toFixed(1)}%`;
852+
853+
// Render timeline
854+
renderIterationsTimeline();
855+
856+
// Render iteration cards
857+
renderIterationCards();
858+
859+
// Setup event listeners
860+
setupIterationsEventListeners();
861+
}
862+
863+
function renderIterationsTimeline() {
864+
const container = $('#iterations-timeline-chart');
865+
866+
if (!data.metricsHistory || !data.metricsHistory.timeline || data.metricsHistory.timeline.length < 2) {
867+
container.innerHTML = `
868+
<div class="no-timeline-data">
869+
<p>Not enough historical data to display timeline</p>
870+
<p class="hint">Run evaluations and tag baselines to track progress over time</p>
871+
</div>
872+
`;
873+
return;
874+
}
875+
876+
container.innerHTML = renderTimelineChart(data.metricsHistory, 900, 280);
877+
}
878+
879+
function renderIterationCards() {
880+
const container = $('#iterations-list');
881+
882+
if (!filteredIterations || filteredIterations.length === 0) {
883+
container.innerHTML = `
884+
<div class="no-iterations">
885+
<p>No iterations found.</p>
886+
<p class="hint">Run <code>/iterate "goal"</code> to start hill-climbing.</p>
887+
</div>
888+
`;
889+
return;
890+
}
891+
892+
const cards = filteredIterations.map(createIterationCard).join('');
893+
container.innerHTML = cards;
894+
895+
// Add event listeners to cards
896+
$$('.card-header').forEach(header => {
897+
header.addEventListener('click', toggleIterationCardDetails);
898+
});
899+
900+
$$('.toggle-details').forEach(btn => {
901+
btn.addEventListener('click', (e) => {
902+
e.stopPropagation();
903+
toggleIterationCardDetails.call(btn.closest('.card-header'), e);
904+
});
905+
});
906+
}
907+
908+
function createIterationCard(iteration) {
909+
const statusClass = iteration.status || 'unknown';
910+
const statusLabel = iteration.status || 'Unknown';
911+
912+
// Lever info
913+
const leverInfo = iteration.lever ? `
914+
<div class="card-lever">
915+
${iteration.lever.primary}: ${iteration.lever.oldValue !== null ? iteration.lever.oldValue.toFixed(2) : '?'}
916+
${iteration.lever.newValue !== null ? iteration.lever.newValue.toFixed(2) : '?'}
917+
${iteration.lever.delta ? `(${iteration.lever.delta})` : ''}
918+
</div>
919+
` : '';
920+
921+
// Metrics info
922+
const metricsInfo = iteration.metrics ? `
923+
<div class="card-metrics">
924+
${iteration.metrics.target ? `
925+
<div class="metric-item">
926+
<div class="metric-label">Target Metric</div>
927+
<div class="metric-value ${iteration.metrics.target.delta?.startsWith('+') ? 'positive' : (iteration.metrics.target.delta?.startsWith('-') ? 'negative' : '')}">
928+
${iteration.metrics.target.delta}
929+
</div>
930+
</div>
931+
` : ''}
932+
${iteration.metrics.maxRegression ? `
933+
<div class="metric-item">
934+
<div class="metric-label">Max Regression</div>
935+
<div class="metric-value ${iteration.metrics.maxRegression.startsWith('-') ? 'negative' : 'positive'}">
936+
${iteration.metrics.maxRegression}
937+
</div>
938+
</div>
939+
` : ''}
940+
</div>
941+
` : '';
942+
943+
// Links
944+
const links = [];
945+
if (iteration.pr && iteration.pr !== 'null') {
946+
links.push(`<a href="https://github.com/chronick/duopulse/pull/${iteration.pr.replace('#', '')}" class="card-link" target="_blank">View PR ${iteration.pr}</a>`);
947+
}
948+
if (iteration.commit && iteration.commit !== 'null') {
949+
links.push(`<a href="https://github.com/chronick/duopulse/commit/${iteration.commit}" class="card-link" target="_blank"><code>${iteration.commit.substring(0, 7)}</code></a>`);
950+
}
951+
if (iteration.branch && iteration.branch !== 'null') {
952+
links.push(`<span class="card-link"><code>${iteration.branch}</code></span>`);
953+
}
954+
955+
return `
956+
<div class="iteration-card ${statusClass}" id="${iteration.id}">
957+
<div class="card-header">
958+
<div class="card-title-row">
959+
<div class="card-id">#${iteration.id}</div>
960+
<div class="card-status ${statusClass}">${statusLabel}</div>
961+
</div>
962+
<h3 class="card-goal">${escapeHTML(iteration.goal)}</h3>
963+
${leverInfo}
964+
${metricsInfo}
965+
${links.length > 0 ? `<div class="card-links">${links.join('')}</div>` : ''}
966+
</div>
967+
968+
<div class="card-details" id="details-${iteration.id}">
969+
${createIterationCardDetails(iteration)}
970+
</div>
971+
972+
<button class="toggle-details">
973+
<span class="toggle-text">Show Details</span>
974+
<span class="toggle-icon">▼</span>
975+
</button>
976+
</div>
977+
`;
978+
}
979+
980+
function createIterationCardDetails(iteration) {
981+
const sections = [];
982+
983+
if (iteration.sections?.goal) {
984+
sections.push(`
985+
<div class="card-details-section">
986+
<h4>Goal</h4>
987+
<pre>${escapeHTML(iteration.sections.goal)}</pre>
988+
</div>
989+
`);
990+
}
991+
992+
if (iteration.sections?.decision) {
993+
sections.push(`
994+
<div class="card-details-section">
995+
<h4>Decision</h4>
996+
<pre>${escapeHTML(iteration.sections.decision)}</pre>
997+
</div>
998+
`);
999+
}
1000+
1001+
if (iteration.sections?.notes) {
1002+
sections.push(`
1003+
<div class="card-details-section">
1004+
<h4>Notes</h4>
1005+
<pre>${escapeHTML(iteration.sections.notes)}</pre>
1006+
</div>
1007+
`);
1008+
}
1009+
1010+
if (sections.length === 0) {
1011+
return '<p>No additional details available</p>';
1012+
}
1013+
1014+
return sections.join('');
1015+
}
1016+
1017+
function toggleIterationCardDetails(e) {
1018+
const card = e.currentTarget.closest('.iteration-card');
1019+
const details = card.querySelector('.card-details');
1020+
const toggleBtn = card.querySelector('.toggle-details');
1021+
const toggleText = toggleBtn.querySelector('.toggle-text');
1022+
const toggleIcon = toggleBtn.querySelector('.toggle-icon');
1023+
1024+
if (details.classList.contains('expanded')) {
1025+
details.classList.remove('expanded');
1026+
toggleText.textContent = 'Show Details';
1027+
toggleIcon.classList.remove('expanded');
1028+
} else {
1029+
details.classList.add('expanded');
1030+
toggleText.textContent = 'Hide Details';
1031+
toggleIcon.classList.add('expanded');
1032+
}
1033+
}
1034+
1035+
function setupIterationsEventListeners() {
1036+
const searchInput = $('#search-iterations');
1037+
const filterStatus = $('#filter-status');
1038+
const sortSelect = $('#sort-iterations');
1039+
1040+
// Remove existing listeners by replacing elements (simple approach)
1041+
if (searchInput && !searchInput.dataset.listenerAttached) {
1042+
searchInput.addEventListener('input', applyIterationsFilters);
1043+
searchInput.dataset.listenerAttached = 'true';
1044+
}
1045+
1046+
if (filterStatus && !filterStatus.dataset.listenerAttached) {
1047+
filterStatus.addEventListener('change', applyIterationsFilters);
1048+
filterStatus.dataset.listenerAttached = 'true';
1049+
}
1050+
1051+
if (sortSelect && !sortSelect.dataset.listenerAttached) {
1052+
sortSelect.addEventListener('change', applyIterationsSort);
1053+
sortSelect.dataset.listenerAttached = 'true';
1054+
}
1055+
}
1056+
1057+
function applyIterationsFilters() {
1058+
const searchTerm = ($('#search-iterations')?.value || '').toLowerCase();
1059+
const statusFilter = $('#filter-status')?.value || 'all';
1060+
1061+
filteredIterations = (data.iterations?.iterations || []).filter(iteration => {
1062+
// Status filter
1063+
if (statusFilter !== 'all' && iteration.status !== statusFilter) {
1064+
return false;
1065+
}
1066+
1067+
// Search filter
1068+
if (searchTerm) {
1069+
const searchableText = [
1070+
iteration.id,
1071+
iteration.goal,
1072+
iteration.lever?.primary,
1073+
iteration.sections?.goal,
1074+
iteration.sections?.decision,
1075+
iteration.sections?.notes
1076+
].filter(Boolean).join(' ').toLowerCase();
1077+
1078+
if (!searchableText.includes(searchTerm)) {
1079+
return false;
1080+
}
1081+
}
1082+
1083+
return true;
1084+
});
1085+
1086+
applyIterationsSort();
1087+
}
1088+
1089+
function applyIterationsSort() {
1090+
const sortBy = $('#sort-iterations')?.value || 'newest';
1091+
1092+
switch (sortBy) {
1093+
case 'oldest':
1094+
filteredIterations.sort((a, b) => a.id.localeCompare(b.id));
1095+
break;
1096+
case 'impact':
1097+
filteredIterations.sort((a, b) => {
1098+
const deltaA = parseFloat(a.metrics?.target?.delta?.replace('%', '') || '0');
1099+
const deltaB = parseFloat(b.metrics?.target?.delta?.replace('%', '') || '0');
1100+
return deltaB - deltaA;
1101+
});
1102+
break;
1103+
case 'newest':
1104+
default:
1105+
filteredIterations.sort((a, b) => b.id.localeCompare(a.id));
1106+
break;
1107+
}
1108+
1109+
renderIterationCards();
1110+
}
1111+
1112+
function escapeHTML(text) {
1113+
const div = document.createElement('div');
1114+
div.textContent = text;
1115+
return div.innerHTML;
1116+
}
1117+
8261118
// ============================================================================
8271119
// Navigation
8281120
// ============================================================================
@@ -841,7 +1133,7 @@ function showView(viewName) {
8411133
case 'sweeps': renderSweepsView(); break;
8421134
case 'seeds': renderSeedsView(); break;
8431135
case 'fills': renderFillsView(); break;
844-
case 'sensitivity': renderSensitivityView(); break;
1136+
case 'iterations': renderIterationsView(); break;
8451137
}
8461138
}
8471139

@@ -865,7 +1157,7 @@ function showView(viewName) {
8651157

8661158
function getViewFromHash() {
8671159
const hash = window.location.hash.slice(1); // Remove the '#'
868-
const validViews = ['overview', 'presets', 'sweeps', 'seeds', 'fills', 'sensitivity'];
1160+
const validViews = ['overview', 'presets', 'sweeps', 'seeds', 'fills', 'iterations'];
8691161
return validViews.includes(hash) ? hash : 'overview';
8701162
}
8711163

0 commit comments

Comments
 (0)