diff --git a/web/src/pages/evolution.ts b/web/src/pages/evolution.ts index 2d7fc9c..5ad1b41 100644 --- a/web/src/pages/evolution.ts +++ b/web/src/pages/evolution.ts @@ -94,17 +94,17 @@ function renderDashboard(container: HTMLElement, data: EvolutionLiveData): void
-
+

Meta Tracker Best fitness per island over generations

-
+

Lineage Tree Program ancestry (top 80 by fitness)

-
+

Generation Log

@@ -509,9 +509,52 @@ function renderDashboard(container: HTMLElement, data: EvolutionLiveData): void renderLiveStatus(document.getElementById('live-status')!, data.cycle); renderStatistics(document.getElementById('statistics')!, data.totals); renderActivityFeed(document.getElementById('activity-feed')!, data.recent_activity || []); - renderMetaChart(document.getElementById('meta-chart')!, data.meta_snapshots ?? []); - renderLineageTree(document.getElementById('lineage-tree')!, data.lineage ?? []); - renderGenerationLog(document.getElementById('generation-log')!, data.generation_log ?? []); + + // Below-the-fold sections: render immediately if already visible (polling update), + // otherwise defer with IntersectionObserver. + const belowFoldSections = container.querySelectorAll('.evo-below-fold'); + const rendered = new Set(); + + function renderSection(name: string): void { + if (rendered.has(name)) return; + rendered.add(name); + switch (name) { + case 'meta': + renderMetaChart(document.getElementById('meta-chart')!, data.meta_snapshots ?? []); + break; + case 'lineage': + renderLineageTree(document.getElementById('lineage-tree')!, data.lineage ?? []); + break; + case 'genlog': + renderGenerationLog(document.getElementById('generation-log')!, data.generation_log ?? []); + break; + } + } + + if (belowFoldSections.length > 0) { + const observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + const section = (entry.target as HTMLElement).dataset.evoSection; + if (section) { + renderSection(section); + observer.unobserve(entry.target); + } + } + }, { rootMargin: '200px' }); + + belowFoldSections.forEach(el => { + const name = el.dataset.evoSection; + if (!name) return; + // If already in viewport (e.g. polling update on small screen), render immediately + const rect = el.getBoundingClientRect(); + if (rect.top < window.innerHeight + 200) { + renderSection(name); + } else { + observer.observe(el); + } + }); + } } // ── Island Status ────────────────────────────────────────────────────────────── @@ -932,36 +975,17 @@ function renderLineageTree(container: HTMLElement, nodes: LineageNode[]): void { // ── Generation Log Table ─────────────────────────────────────────────────────── +const GEN_LOG_BATCH = 30; + function renderGenerationLog(container: HTMLElement, log: GenerationEntry[]): void { if (!log || log.length === 0) { container.innerHTML = '

No generation history yet.

'; return; } - const rows = log.map(e => { - const color = ISLAND_COLORS[e.island] ?? '#94a3b8'; - const bestPct = (e.best_fitness * 100).toFixed(1); - const avgPct = (e.avg_fitness * 100).toFixed(1); - const barWidth = Math.round(e.best_fitness * 100); - return ` - - ${e.generation} - ${escapeHtml(e.island)} - ${e.count} - ${e.promoted} - -
- ${bestPct}% -
-
-
-
- - ${avgPct}% - ${formatTimestamp(e.evaluated_at)} - - `; - }); + // Show first batch immediately, paginate the rest + const initial = log.slice(0, GEN_LOG_BATCH); + const remaining = log.slice(GEN_LOG_BATCH); container.innerHTML = ` @@ -976,11 +1000,64 @@ function renderGenerationLog(container: HTMLElement, log: GenerationEntry[]): vo - - ${rows.join('')} + + ${initial.map(renderGenRow).join('')}
Timestamp
`; + + if (remaining.length > 0) { + let offset = 0; + const total = remaining.length; + const btn = document.createElement('button'); + btn.className = 'btn secondary show-more-btn'; + btn.type = 'button'; + + function updateBtn(): void { + const left = total - offset; + if (left <= 0) { btn.remove(); return; } + const next = Math.min(GEN_LOG_BATCH, left); + btn.textContent = `Show ${next} more generations (${left} remaining)`; + btn.setAttribute('aria-label', `Show ${next} more generations, ${left} remaining`); + } + + btn.addEventListener('click', () => { + const batch = remaining.slice(offset, offset + GEN_LOG_BATCH); + const tbody = document.getElementById('gen-log-tbody'); + if (!tbody) return; + tbody.insertAdjacentHTML('beforeend', batch.map(renderGenRow).join('')); + offset += batch.length; + updateBtn(); + }); + + updateBtn(); + container.appendChild(btn); + } +} + +function renderGenRow(e: GenerationEntry): string { + const color = ISLAND_COLORS[e.island] ?? '#94a3b8'; + const bestPct = (e.best_fitness * 100).toFixed(1); + const avgPct = (e.avg_fitness * 100).toFixed(1); + const barWidth = Math.round(e.best_fitness * 100); + return ` + + ${e.generation} + ${escapeHtml(e.island)} + ${e.count} + ${e.promoted} + +
+ ${bestPct}% +
+
+
+
+ + ${avgPct}% + ${formatTimestamp(e.evaluated_at)} + + `; } // ── Helpers ────────────────────────────────────────────────────────────────────