sky2 / skydiscover /extras /monitor /dashboard.html
JustinTX's picture
Add files using upload-large-folder tool
7f611c5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SkyDiscover Live Monitor</title>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<style>
/* ── Dark theme ─────────────────────────────────── */
[data-theme="dark"] {
--bg: #0d1117;
--bg2: #161b22;
--bg3: #21262d;
--text: #e6edf3;
--text-dim: #7d8590;
--accent: #ff7b72;
--green: #3fb950;
--orange: #d29922;
--blue: #58a6ff;
--purple: #bc8cff;
--cyan: #56d4dd;
--border: #30363d;
--shadow: rgba(0,0,0,0.3);
--tag-parent-bg: rgba(255,123,114,0.15);
--tag-child-bg: rgba(63,185,80,0.15);
--tag-context-bg: rgba(188,140,255,0.15);
--tag-ctxby-bg: rgba(88,166,255,0.15);
--hf-active-bg: rgba(63,185,80,0.08);
--hf-active-border: rgba(63,185,80,0.2);
}
/* ── Light theme (default) ───────────────────────── */
:root, [data-theme="light"] {
--bg: #f0f2f5;
--bg2: #ffffff;
--bg3: #e8ecf0;
--text: #1f2328;
--text-dim: #656d76;
--accent: #cf222e;
--green: #1a7f37;
--orange: #bc4c00;
--blue: #0969da;
--purple: #8250df;
--cyan: #0598c1;
--border: #d0d7de;
--shadow: rgba(0,0,0,0.08);
--tag-parent-bg: rgba(207,34,46,0.08);
--tag-child-bg: rgba(26,127,55,0.08);
--tag-context-bg: rgba(130,80,223,0.08);
--tag-ctxby-bg: rgba(9,105,218,0.08);
--hf-active-bg: rgba(26,127,55,0.06);
--hf-active-border: rgba(26,127,55,0.2);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
transition: background-color 0.25s ease, color 0.25s ease;
}
.header, .stats-bar, .panel, .summary-panel, .hf-panel, .code-block, .metric-item {
transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
}
.header h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.5px;
}
.header h1 span { color: var(--accent); }
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-badge.connected { background: var(--tag-child-bg); color: var(--green); }
.status-badge.disconnected { background: var(--tag-parent-bg); color: var(--accent); }
/* Stats Bar */
.stats-bar {
display: flex;
align-items: center;
gap: 24px;
padding: 10px 20px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.stat {
display: flex;
align-items: baseline;
gap: 6px;
font-size: 13px;
}
.stat-label { color: var(--text-dim); }
.stat-value { font-weight: 600; font-size: 15px; }
.stat-value.best { color: var(--green); }
.search-box {
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
}
.search-box input {
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
width: 160px;
}
/* Main Plot */
.plot-container {
padding: 10px 20px;
}
#scatter-plot {
width: 100%;
height: 38vh;
min-height: 250px;
}
/* Bottom Panels */
.bottom-panels {
display: grid;
grid-template-columns: 0.6fr 1.7fr 1.7fr;
gap: 12px;
padding: 0 20px 20px;
}
.panel {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
overflow: auto;
max-height: 50vh;
box-shadow: 0 1px 3px var(--shadow);
}
.panel h3 {
font-size: 14px;
margin-bottom: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.detail-row {
display: flex;
gap: 8px;
margin-bottom: 6px;
font-size: 13px;
}
.detail-label {
color: var(--text-dim);
min-width: 80px;
flex-shrink: 0;
}
.detail-value {
color: var(--text);
word-break: break-all;
}
.detail-value code {
background: var(--bg);
padding: 1px 5px;
border-radius: 3px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 12px;
}
.detail-value.score { color: var(--green); font-weight: 600; }
.detail-value.score code { color: var(--green); }
.code-block {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 10px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 12px;
white-space: pre-wrap;
overflow-x: auto;
max-height: 30vh;
margin-top: 8px;
color: var(--text);
line-height: 1.5;
text-align: left;
}
.code-tabs {
display: flex;
gap: 8px;
margin-top: 8px;
margin-bottom: 4px;
}
.code-tab {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-dim);
}
.code-tab.active {
background: var(--bg3);
color: var(--text);
border-color: var(--accent);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 6px;
margin-top: 8px;
}
.metric-item {
background: var(--bg);
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
}
.metric-item .key { color: var(--text-dim); }
.metric-item .val { color: var(--green); font-weight: 600; }
.lineage-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
margin-right: 4px;
}
.tag-parent { background: var(--tag-parent-bg); color: var(--accent); }
.tag-context { background: var(--tag-context-bg); color: var(--purple); }
.tag-child { background: var(--tag-child-bg); color: var(--green); }
.tag-ctx-usage { background: var(--tag-ctxby-bg); color: var(--blue); }
.lineage-tag[onclick] { cursor: pointer; transition: filter 0.15s; }
.lineage-tag[onclick]:hover { filter: brightness(1.3); }
.lineage-section { margin-bottom: 2px; }
.lineage-section .detail-value { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; }
.empty-state {
color: var(--text-dim);
text-align: center;
padding: 40px;
font-size: 14px;
}
/* Human Feedback Panel */
.hf-panel {
margin: 0 20px 12px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.3s;
}
.hf-panel.active {
border-color: var(--green);
}
.hf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
cursor: pointer;
user-select: none;
}
.hf-header:hover { background: rgba(255,255,255,0.02); }
.hf-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
}
.hf-status {
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.hf-status.active { background: var(--tag-child-bg); color: var(--green); }
.hf-status.inactive { background: var(--bg3); color: var(--text-dim); }
.hf-status.disabled { background: var(--tag-parent-bg); color: var(--accent); }
.hf-toggle { color: var(--text-dim); font-size: 12px; }
.hf-body {
padding: 0 14px 14px;
display: none;
}
.hf-panel.open .hf-body { display: block; }
.hf-help {
font-size: 11px;
color: var(--text-dim);
margin-bottom: 8px;
line-height: 1.5;
}
.hf-input-row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.hf-textarea {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 10px;
border-radius: 4px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 12px;
resize: vertical;
min-height: 48px;
max-height: 120px;
line-height: 1.4;
}
.hf-textarea:focus { outline: none; border-color: var(--cyan); }
.hf-textarea::placeholder { color: var(--text-dim); }
.hf-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.hf-btn.send {
background: var(--green);
color: #fff;
}
[data-theme="dark"] .hf-btn.send { color: #0d1117; }
.hf-btn.send:hover { filter: brightness(1.1); }
.hf-btn.clear {
background: var(--tag-parent-bg);
color: var(--accent);
border: 1px solid var(--tag-parent-bg);
}
.hf-btn.clear:hover { filter: brightness(1.1); }
.hf-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.hf-current {
margin-top: 8px;
padding: 8px 10px;
background: var(--hf-active-bg);
border: 1px solid var(--hf-active-border);
border-radius: 4px;
font-size: 12px;
color: var(--green);
font-family: 'Fira Code', 'Consolas', monospace;
white-space: pre-wrap;
line-height: 1.4;
display: none;
}
.hf-current.visible { display: block; }
/* Human Feedback Mode Toggle */
.hf-mode-toggle {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.hf-mode-toggle label {
font-size: 11px;
color: var(--text-dim);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.hf-mode-btn {
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
font-weight: 600;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-dim);
transition: all 0.2s;
}
.hf-mode-btn.active {
background: var(--cyan);
color: #0d1117;
border-color: var(--cyan);
}
[data-theme="light"] .hf-mode-btn.active { color: #fff; }
/* System Prompt Viewer */
.hf-prompt-viewer {
margin-top: 10px;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.hf-prompt-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--bg3);
cursor: pointer;
font-size: 11px;
color: var(--text-dim);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
user-select: none;
}
.hf-prompt-header:hover { background: var(--bg); }
.hf-prompt-body {
display: none;
padding: 10px;
background: var(--bg);
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 11px;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
line-height: 1.5;
color: var(--text);
}
.hf-prompt-viewer.open .hf-prompt-body { display: block; }
/* Human Feedback History Timeline */
.hf-history {
margin-top: 10px;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.hf-history-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--bg3);
cursor: pointer;
font-size: 11px;
color: var(--text-dim);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
user-select: none;
}
.hf-history-header:hover { background: var(--bg); }
.hf-history-body {
display: none;
max-height: 200px;
overflow-y: auto;
}
.hf-history.open .hf-history-body { display: block; }
.hf-history-entry {
padding: 8px 10px;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.hf-history-entry:last-child { border-bottom: none; }
.hf-history-entry .meta {
display: flex;
gap: 10px;
align-items: center;
font-size: 10px;
color: var(--text-dim);
margin-bottom: 4px;
}
.hf-history-entry .meta .iter {
color: var(--cyan);
font-weight: 600;
}
.hf-history-entry .meta .mode-tag {
padding: 0 6px;
border-radius: 3px;
font-weight: 600;
text-transform: uppercase;
font-size: 9px;
}
.hf-history-entry .meta .mode-tag.append {
background: rgba(86,212,221,0.15);
color: var(--cyan);
}
.hf-history-entry .meta .mode-tag.replace {
background: rgba(255,123,114,0.15);
color: var(--accent);
}
.hf-history-entry .feedback-text {
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 11px;
color: var(--text);
white-space: pre-wrap;
line-height: 1.4;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
/* AI Summary Panel */
.summary-panel {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
overflow: auto;
max-height: 50vh;
box-shadow: 0 1px 3px var(--shadow);
}
.summary-panel h3 {
font-size: 14px;
margin-bottom: 10px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
display: flex;
align-items: center;
justify-content: space-between;
}
.summary-refresh-btn {
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--cyan);
font-size: 11px;
cursor: pointer;
font-weight: 600;
text-transform: none;
letter-spacing: 0;
}
.summary-refresh-btn:hover { background: var(--bg3); }
.summary-refresh-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.summary-body {
font-size: 13px;
line-height: 1.6;
color: var(--text);
word-wrap: break-word;
}
.summary-body h2 {
font-size: 12px;
font-weight: 700;
color: var(--cyan);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 12px 0 6px;
padding-bottom: 3px;
border-bottom: 1px solid var(--border);
}
.summary-body h2:first-child { margin-top: 0; }
.summary-body ul {
margin: 4px 0;
padding-left: 18px;
}
.summary-body li {
margin-bottom: 3px;
line-height: 1.5;
}
.summary-body p {
margin: 4px 0;
}
.summary-body strong {
color: var(--green);
}
.summary-body code {
background: var(--bg);
padding: 1px 5px;
border-radius: 3px;
font-size: 12px;
color: var(--orange);
}
.summary-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--border);
border-top-color: var(--cyan);
border-radius: 50%;
animation: sum-spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes sum-spin { to { transform: rotate(360deg); } }
.summary-meta {
font-size: 11px;
color: var(--text-dim);
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid var(--border);
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<h1>SkyDiscover <span>Live Monitor</span></h1>
<div style="display:flex; align-items:center; gap:10px;">
<button id="theme-toggle" onclick="window.toggleTheme()" style="font-size:11px; padding:4px 12px; background:var(--bg); border:1px solid var(--border); color:var(--text-dim); border-radius:4px; cursor:pointer; font-weight:600; transition:all 0.25s;">Theme: Light</button>
<div id="status-badge" class="status-badge disconnected">Disconnected</div>
</div>
</div>
<!-- Stats Bar -->
<div class="stats-bar">
<div class="stat">
<span class="stat-label">Programs:</span>
<span class="stat-value" id="stat-programs">0</span>
</div>
<div class="stat">
<span class="stat-label">Iter:</span>
<span class="stat-value" id="stat-iteration">0</span>
</div>
<div class="stat">
<span class="stat-label">Best:</span>
<span class="stat-value best" id="stat-best">--</span>
</div>
<div class="stat">
<span class="stat-label">Since best:</span>
<span class="stat-value" id="stat-since">--</span>
</div>
<div class="stat">
<span class="stat-label">Elapsed:</span>
<span class="stat-value" id="stat-elapsed">--</span>
</div>
<div style="margin-left:auto; display:flex; align-items:center; gap:8px;">
<button id="color-toggle" onclick="window.toggleColoring()" style="font-size:11px; padding:3px 10px; background:var(--bg); border:1px solid var(--border); color:var(--cyan); border-radius:4px; cursor:pointer; font-weight:600;">Color: Score</button>
<div class="search-box" style="margin-left:0;">
<span class="stat-label">Filter:</span>
<input type="text" id="filter-input" placeholder="island, id...">
</div>
</div>
</div>
<!-- Plot -->
<div class="plot-container">
<div id="scatter-plot"></div>
</div>
<!-- Human Feedback Panel -->
<div class="hf-panel open" id="hf-panel">
<div class="hf-header" onclick="window.toggleHf()">
<div class="hf-title">
Human Feedback
<span class="hf-status inactive" id="hf-status">Inactive</span>
</div>
<span class="hf-toggle" id="hf-toggle-icon">&#9660;</span>
</div>
<div class="hf-body">
<div class="hf-help">
Type feedback to guide the LLM. In <strong>Append</strong> mode your text is added after the system prompt.
In <strong>Replace</strong> mode it replaces the entire system prompt.
</div>
<!-- Mode Toggle -->
<div class="hf-mode-toggle">
<label>Mode:</label>
<button class="hf-mode-btn active" id="hf-mode-append" onclick="window.setHfMode('append')">Append</button>
<button class="hf-mode-btn" id="hf-mode-replace" onclick="window.setHfMode('replace')">Replace</button>
</div>
<div class="hf-input-row">
<textarea class="hf-textarea" id="hf-input" rows="3"
placeholder="e.g. Focus on hexagonal packing. Try numpy vectorization."></textarea>
<div style="display:flex; flex-direction:column; gap:6px;">
<button class="hf-btn send" id="hf-send-btn" onclick="window.sendFeedback()">Send</button>
<button class="hf-btn clear" id="hf-clear-btn" onclick="window.clearFeedback()">Clear</button>
</div>
</div>
<div class="hf-current" id="hf-current"></div>
<!-- System Prompt Viewer -->
<div class="hf-prompt-viewer" id="hf-prompt-viewer">
<div class="hf-prompt-header" onclick="window.togglePromptViewer()">
<span>Current System Prompt</span>
<span id="hf-prompt-toggle">&#9654;</span>
</div>
<div class="hf-prompt-body" id="hf-prompt-body">(Prompt will appear after the first iteration runs)</div>
</div>
<!-- Feedback History -->
<div class="hf-history" id="hf-history">
<div class="hf-history-header" onclick="window.toggleHfHistory()">
<span>Feedback History (<span id="hf-history-count">0</span>)</span>
<span id="hf-history-toggle">&#9654;</span>
</div>
<div class="hf-history-body" id="hf-history-body">
<div style="padding:12px;color:var(--text-dim);font-size:12px;text-align:center;">No feedback sent yet.</div>
</div>
</div>
</div>
</div>
<!-- Bottom Panels -->
<div class="bottom-panels">
<div class="panel" id="best-panel">
<h3>Best Program</h3>
<div id="best-content" class="empty-state">Waiting for data...</div>
</div>
<div class="panel" id="detail-panel">
<h3>Selected Program</h3>
<div id="detail-content" class="empty-state">Click a point on the scatter plot</div>
</div>
<div class="summary-panel" id="summary-panel">
<h3>
AI Summary
<button class="summary-refresh-btn" id="summary-refresh-btn" onclick="window.requestSummary()">Refresh</button>
</h3>
<div id="summary-content">
<div class="empty-state" style="padding:20px;">Waiting for data...</div>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ─── Theme ──────────────────────────────────────────────────
function getThemeColor(varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
}
function isDarkTheme() {
return (document.documentElement.getAttribute('data-theme') || 'light') === 'dark';
}
// Theme-aware color palettes (Plotly can't read CSS vars, so we resolve them)
function plotColors() {
const dark = isDarkTheme();
return {
paperBg: dark ? '#0d1117' : '#f0f2f5',
plotBg: dark ? '#0d1117' : '#ffffff',
gridColor: dark ? '#21262d' : '#e1e4e8',
fontColor: dark ? '#e6edf3' : '#1f2328',
bestLine: dark ? '#56d4dd' : '#0969da',
scoreLow: dark ? '#ff7b72' : '#cf222e',
scoreMid: dark ? '#d29922' : '#d4880f',
scoreHigh: dark ? '#3fb950' : '#1a7f37',
dotOutline: dark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.15)',
arrowParent: dark ? '#ff7b72' : '#cf222e',
arrowChild: dark ? '#3fb950' : '#1a7f37',
arrowContext: dark ? '#bc8cff' : '#8250df',
arrowCtxBy: dark ? '#58a6ff' : '#0969da',
selectedRing: dark ? '#ffffff' : '#1f2328',
};
}
window.toggleTheme = function() {
const html = document.documentElement;
const current = html.getAttribute('data-theme') || 'light';
const next = current === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
document.getElementById('theme-toggle').textContent = next === 'dark' ? 'Theme: Dark' : 'Theme: Light';
rebuildPlot();
if (selectedId && programMap[selectedId]) {
showLineageArrows(programMap[selectedId]);
highlightSelected(programMap[selectedId]);
}
};
// ─── State ──────────────────────────────────────────────────
let programs = []; // Array of program objects
let programMap = {}; // id -> program
let bestProgramId = null;
let stats = {};
let ws = null;
let selectedId = null;
let bestLine = []; // [iter, score] pairs for best-so-far line
let maxIteration = 0; // Track highest iteration for best line extension
let filterText = '';
let plotInitialized = false;
let colorByIsland = false;
let hfEnabled = false;
let hfActive = false;
let hfMode = 'append';
let hfCurrentPrompt = '';
let hfHistory = [];
let summaryEnabled = false;
let summaryGenerating = false;
let programSummaryCache = {}; // program_id -> summary text
// ─── Reverse indexes for lineage ────────────────────────────
let childrenIndex = {}; // parent_id -> [child_id, ...]
let contextUsedByIndex = {}; // context_id -> [program_id that used it, ...]
function rebuildIndexes() {
childrenIndex = {};
contextUsedByIndex = {};
programs.forEach(p => addToIndexes(p));
}
function addToIndexes(p) {
if (p.parent_id) {
if (!childrenIndex[p.parent_id]) childrenIndex[p.parent_id] = [];
childrenIndex[p.parent_id].push(p.id);
}
(p.context_ids || []).forEach(cid => {
if (!contextUsedByIndex[cid]) contextUsedByIndex[cid] = [];
contextUsedByIndex[cid].push(p.id);
});
}
// ─── WebSocket ─────────────────────────────────────────────
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
// Same host:port as the HTTP dashboard (single-port server)
ws = new WebSocket(`${proto}//${location.host}`);
ws.onopen = () => {
document.getElementById('status-badge').className = 'status-badge connected';
document.getElementById('status-badge').textContent = 'Connected';
};
ws.onclose = () => {
document.getElementById('status-badge').className = 'status-badge disconnected';
document.getElementById('status-badge').textContent = 'Disconnected';
setTimeout(connect, 2000);
};
ws.onerror = () => {
ws.close();
};
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data);
handleMessage(msg);
} catch(e) {}
};
}
function handleMessage(msg) {
switch(msg.type) {
case 'init_state':
programs = msg.programs || [];
bestProgramId = msg.best_program_id;
stats = msg.stats || {};
programMap = {};
bestLine = [];
maxIteration = 0;
programs.forEach(p => {
programMap[p.id] = p;
if (p.iteration > maxIteration) maxIteration = p.iteration;
});
rebuildIndexes();
rebuildBestLine();
rebuildPlot();
updateStats();
updateBestPanel();
updateHfState(msg.human_feedback_enabled, msg.feedback_active, msg.feedback_text, null, msg.human_feedback_mode, msg.human_feedback_current_prompt, msg.human_feedback_history);
summaryModelName = msg.summary_model || '';
updateSummaryState(msg.summary_enabled, msg.summary_generating, msg.summary_text);
break;
case 'new_program':
const p = msg.program;
p.human_feedback_active = !!msg.feedback_active;
programs.push(p);
programMap[p.id] = p;
addToIndexes(p);
if (p.iteration > maxIteration) maxIteration = p.iteration;
// Client-side best tracking: compare scores directly for resilience
const isBest = msg.is_best ||
(typeof p.score === 'number' && p.score > (bestProgramId && programMap[bestProgramId] ? (programMap[bestProgramId].score || -Infinity) : -Infinity));
if (isBest) bestProgramId = p.id;
stats = msg.stats || stats;
rebuildBestLine();
extendPlot(p);
updateStats();
if (isBest) updateBestPanel();
break;
case 'discovery_complete':
document.getElementById('status-badge').textContent = 'Complete';
document.getElementById('status-badge').className = 'status-badge connected';
break;
case 'program_solution':
showProgramCode(msg);
break;
case 'program_summary':
programSummaryCache[msg.program_id] = msg.summary || '';
if (msg.program_id === selectedId) {
const sumEl = document.getElementById('program-summary');
if (sumEl) sumEl.innerHTML = renderMarkdown(msg.summary || '(no summary)');
}
break;
case 'image_data':
if (msg.program_id === selectedId) {
const imgEl = document.getElementById('program-image');
const imgLoading = document.getElementById('image-loading');
if (imgEl) { imgEl.src = msg.data_url; imgEl.style.display = 'block'; }
if (imgLoading) imgLoading.style.display = 'none';
}
// Best panel image
if (msg.program_id && msg.program_id.startsWith('best_')) {
const bestImgEl = document.getElementById('best-program-image');
if (bestImgEl) { bestImgEl.src = msg.data_url; bestImgEl.style.display = 'block'; }
}
break;
case 'feedback_ack':
updateHfState(true, msg.feedback_active, msg.feedback_text, msg.error, msg.human_feedback_mode);
break;
case 'human_feedback_mode_ack':
updateHfModeUI(msg.human_feedback_mode);
break;
case 'system_prompt':
updatePromptViewer(msg.prompt_text);
break;
case 'human_feedback_history':
updateHfHistory(msg.history);
break;
case 'summary_update':
updateSummaryState(msg.summary_enabled, msg.summary_generating, msg.summary_text);
break;
case 'heartbeat':
break;
}
}
// ─── Best-so-far line (Pareto frontier) ─────────────────────
// bestLineX / bestLineY: direct line connecting programs where a new best was achieved.
let bestLineX = [];
let bestLineY = [];
function rebuildBestLine() {
bestLineX = [];
bestLineY = [];
let runBest = -Infinity;
const sorted = [...programs].sort((a,b) => a.iteration - b.iteration);
sorted.forEach(p => {
const s = typeof p.score === 'number' ? p.score : 0;
if (s > runBest) {
runBest = s;
bestLineX.push(p.iteration);
bestLineY.push(runBest);
}
});
}
// ─── Plot ──────────────────────────────────────────────────
// Vivid palette that contrasts well in both dark and light themes
const ISLAND_COLORS = ['#e5484d', '#30a46c', '#e5a000', '#3e63dd', '#8e4ec6', '#12a594', '#e54666', '#0090ff', '#f76b15', '#46a758'];
function rebuildPlot() {
const tc = plotColors();
// Split programs into normal and human-feedback-active
const normalPrograms = programs.filter(p => !p.human_feedback_active);
const hfPrograms = programs.filter(p => p.human_feedback_active);
const normalScores = normalPrograms.map(p => typeof p.score === 'number' ? p.score : 0);
let normalMarker;
if (colorByIsland) {
normalMarker = {
color: normalPrograms.map(p => ISLAND_COLORS[(p.island || 0) % ISLAND_COLORS.length]),
size: 7,
opacity: 0.85,
line: { width: 0.5, color: tc.dotOutline },
};
} else {
normalMarker = {
color: normalScores,
colorscale: [[0, tc.scoreLow], [0.5, tc.scoreMid], [1, tc.scoreHigh]],
size: 7,
opacity: 0.85,
line: { width: 0.5, color: tc.dotOutline },
};
}
const scatter = {
x: normalPrograms.map(p => p.iteration),
y: normalPrograms.map(p => p.score),
customdata: normalPrograms.map(p => p.id),
mode: 'markers',
type: 'scatter',
marker: normalMarker,
text: normalPrograms.map(p => hoverText(p)),
hoverinfo: 'text',
name: 'Programs',
};
// Human feedback trace: diamond markers with cyan ring
const hfScores = hfPrograms.map(p => typeof p.score === 'number' ? p.score : 0);
let hfMarker;
if (colorByIsland) {
hfMarker = {
color: hfPrograms.map(p => ISLAND_COLORS[(p.island || 0) % ISLAND_COLORS.length]),
size: 9,
symbol: 'diamond',
opacity: 0.95,
line: { width: 2, color: tc.bestLine },
};
} else {
hfMarker = {
color: hfScores,
colorscale: [[0, tc.scoreLow], [0.5, tc.scoreMid], [1, tc.scoreHigh]],
size: 9,
symbol: 'diamond',
opacity: 0.95,
line: { width: 2, color: tc.bestLine },
};
}
const hfScatter = {
x: hfPrograms.map(p => p.iteration),
y: hfPrograms.map(p => p.score),
customdata: hfPrograms.map(p => p.id),
mode: 'markers',
type: 'scatter',
marker: hfMarker,
text: hfPrograms.map(p => hoverText(p) + '\n[Human-Guided]'),
hoverinfo: 'text',
name: 'Human-Guided',
};
const bestTrace = {
x: bestLineX,
y: bestLineY,
mode: 'lines+markers',
type: 'scatter',
line: { color: tc.bestLine, width: 3 },
marker: { color: tc.bestLine, size: 8, symbol: 'star' },
name: 'Best so far',
hoverinfo: 'y',
};
const layout = {
paper_bgcolor: tc.paperBg,
plot_bgcolor: tc.plotBg,
font: { color: tc.fontColor, size: 11 },
margin: { t: 30, r: 30, b: 50, l: 60 },
xaxis: {
title: 'Iteration',
gridcolor: tc.gridColor,
zerolinecolor: tc.gridColor,
},
yaxis: {
title: 'Score',
gridcolor: tc.gridColor,
zerolinecolor: tc.gridColor,
},
legend: {
bgcolor: 'rgba(0,0,0,0)',
font: { size: 11, color: tc.fontColor },
},
hovermode: 'closest',
};
Plotly.newPlot('scatter-plot', [scatter, hfScatter, bestTrace], layout, {
responsive: true,
displayModeBar: true,
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
});
document.getElementById('scatter-plot').on('plotly_click', onPlotClick);
plotInitialized = true;
}
function extendPlot(p) {
if (!plotInitialized) {
rebuildPlot();
return;
}
// In island coloring mode, rebuild fully since extendTraces can't handle string colors properly
if (colorByIsland) {
rebuildPlot();
return;
}
const tc = plotColors();
const plotDiv = document.getElementById('scatter-plot');
// Trace 0 = normal programs, Trace 1 = human-feedback programs, Trace 2 = best line
const traceIdx = p.human_feedback_active ? 1 : 0;
const hoverLabel = p.human_feedback_active ? hoverText(p) + '\n[Human-Guided]' : hoverText(p);
Plotly.extendTraces(plotDiv, {
x: [[p.iteration]],
y: [[p.score]],
customdata: [[p.id]],
'marker.color': [[typeof p.score === 'number' ? p.score : 0]],
text: [[hoverLabel]],
}, [traceIdx]);
// Update best line trace (index 2)
if (bestLineX.length > 0) {
Plotly.restyle(plotDiv, {
x: [bestLineX],
y: [bestLineY],
'line.color': tc.bestLine,
'marker.color': tc.bestLine,
}, [2]);
}
}
function hoverText(p) {
let text = `ID: ${p.id.substring(0,8)}...\nIter: ${p.iteration}\nScore: ${fmt(p.score)}`;
if (p.island != null) text += `\nIsland: ${p.island}`;
if (typeof p.parent_score === 'number' && typeof p.score === 'number') {
const d = p.score - p.parent_score;
text += `\nDelta: ${d >= 0 ? '+' : ''}${d.toFixed(4)}`;
}
return text;
}
function onPlotClick(data) {
if (!data.points || data.points.length === 0) return;
const pt = data.points[0];
if (pt.curveNumber > 1) return; // Only scatter traces (0=normal, 1=human-feedback)
// Use customdata (program ID) for reliable lookup
const pid = pt.customdata;
if (pid && programMap[pid]) {
selectProgram(programMap[pid]);
}
}
// ─── Lineage Arrows ──────────────────────────────────────
function showLineageArrows(p) {
const plotDiv = document.getElementById('scatter-plot');
const tc = plotColors();
const annotations = [];
// Parent arrow (red) β€” bold, high contrast
if (p.parent_id && programMap[p.parent_id]) {
const parent = programMap[p.parent_id];
annotations.push({
ax: parent.iteration, ay: parent.score,
x: p.iteration, y: p.score,
xref: 'x', yref: 'y', axref: 'x', ayref: 'y',
showarrow: true,
arrowhead: 3, arrowsize: 1.5, arrowwidth: 2.5,
arrowcolor: tc.arrowParent,
opacity: 0.9,
});
}
// Context arrows (purple)
(p.context_ids || []).forEach(cid => {
if (programMap[cid]) {
const ctx = programMap[cid];
annotations.push({
ax: ctx.iteration, ay: ctx.score,
x: p.iteration, y: p.score,
xref: 'x', yref: 'y', axref: 'x', ayref: 'y',
showarrow: true,
arrowhead: 3, arrowsize: 1.2, arrowwidth: 2,
arrowcolor: tc.arrowContext,
opacity: 0.75,
});
}
});
// Children arrows (green) β€” using index
(childrenIndex[p.id] || []).forEach(cid => {
const child = programMap[cid];
if (child) {
annotations.push({
ax: p.iteration, ay: p.score,
x: child.iteration, y: child.score,
xref: 'x', yref: 'y', axref: 'x', ayref: 'y',
showarrow: true,
arrowhead: 3, arrowsize: 1.2, arrowwidth: 2,
arrowcolor: tc.arrowChild,
opacity: 0.7,
});
}
});
// "Used as context by" arrows (blue)
(contextUsedByIndex[p.id] || []).forEach(uid => {
const user = programMap[uid];
if (user) {
annotations.push({
ax: p.iteration, ay: p.score,
x: user.iteration, y: user.score,
xref: 'x', yref: 'y', axref: 'x', ayref: 'y',
showarrow: true,
arrowhead: 3, arrowsize: 1, arrowwidth: 1.5,
arrowcolor: tc.arrowCtxBy,
opacity: 0.6,
});
}
});
Plotly.relayout(plotDiv, { annotations: annotations });
}
// ─── Lineage helpers ─────────────────────────────────────────
function programTag(id, fallbackScore, cssClass) {
const prog = programMap[id];
const s = prog ? fmt(prog.score) : (fallbackScore != null ? fmt(fallbackScore) : '?');
const iter = prog ? prog.iteration : '?';
return `<span class="lineage-tag ${cssClass}" onclick="window.navigateToProgram('${id}')" title="Click to navigate">${id.substring(0,8)}... (${s}, i${iter})</span>`;
}
function scoreDelta(from, to) {
if (typeof from !== 'number' || typeof to !== 'number') return '';
const d = to - from;
const color = d >= 0 ? 'var(--green)' : 'var(--accent)';
return ` <span style="color:${color};font-size:11px">(${d >= 0 ? '+' : ''}${d.toFixed(4)})</span>`;
}
window.navigateToProgram = function(id) {
const prog = programMap[id];
if (!prog) return;
selectProgram(prog);
};
// ─── Selection ─────────────────────────────────────────────
function deselectProgram() {
selectedId = null;
// Clear detail panel
document.getElementById('detail-content').innerHTML = '<div class="empty-state">Click a point on the scatter plot</div>';
// Clear arrows
Plotly.relayout('scatter-plot', { annotations: [] });
// Reset marker sizes on both normal (0) and human-feedback (1) traces
if (plotInitialized && programs.length <= 5000) {
const tc = plotColors();
const normalProgs = programs.filter(p => !p.human_feedback_active);
const hfProgs = programs.filter(p => p.human_feedback_active);
Plotly.restyle('scatter-plot', {
'marker.size': [normalProgs.map(() => 7)],
'marker.opacity': [normalProgs.map(() => 0.85)],
'marker.line.width': [normalProgs.map(() => 0.5)],
'marker.line.color': [normalProgs.map(() => tc.dotOutline)],
}, [0]);
if (hfProgs.length > 0) {
Plotly.restyle('scatter-plot', {
'marker.size': [hfProgs.map(() => 9)],
'marker.opacity': [hfProgs.map(() => 0.95)],
'marker.line.width': [hfProgs.map(() => 2)],
'marker.line.color': [hfProgs.map(() => tc.bestLine)],
}, [1]);
}
}
}
function selectProgram(p) {
// Toggle: clicking the same program deselects it
if (selectedId === p.id) {
deselectProgram();
return;
}
selectedId = p.id;
showLineageArrows(p);
highlightSelected(p);
// Request full code
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'request_program_solution', program_id: p.id }));
// Request per-program summary (server will cache)
if (!programSummaryCache[p.id]) {
ws.send(JSON.stringify({ type: 'request_program_summary', program_id: p.id }));
}
}
const el = document.getElementById('detail-content');
// Parent (clickable + delta)
let parentHtml = '<span style="color:var(--text-dim)">None (initial)</span>';
if (p.parent_id) {
parentHtml = programTag(p.parent_id, p.parent_score, 'tag-parent') + scoreDelta(p.parent_score, p.score);
}
// Children (clickable + deltas)
const children = childrenIndex[p.id] || [];
let childrenHtml = '';
if (children.length > 0) {
childrenHtml = children.slice(0, 20).map(cid => {
const child = programMap[cid];
return programTag(cid, null, 'tag-child') + (child ? scoreDelta(p.score, child.score) : '');
}).join(' ');
if (children.length > 20) childrenHtml += `<span style="color:var(--text-dim);font-size:11px"> +${children.length - 20} more</span>`;
} else {
childrenHtml = '<span style="color:var(--text-dim)">None</span>';
}
// Human feedback indicator
let hfIndicator = '';
if (p.human_feedback_active) {
hfIndicator = '<div class="detail-row"><span class="detail-label">Feedback:</span><span class="detail-value" style="color:var(--cyan);font-weight:600;">Human-Guided</span></div>';
}
// Label type
const label = p.label_type || 'unknown';
const labelColors = { exploration: 'var(--blue)', exploitation: 'var(--green)', diverge: 'var(--purple)', initial: 'var(--text-dim)' };
const labelColor = labelColors[label] || 'var(--orange)';
// Metrics grid
let metricsHtml = '';
if (p.metrics && Object.keys(p.metrics).length > 0) {
metricsHtml = '<div class="metrics-grid">';
for (const [k, v] of Object.entries(p.metrics)) {
metricsHtml += `<div class="metric-item"><span class="key">${k}:</span> <span class="val">${fmt(v)}</span></div>`;
}
metricsHtml += '</div>';
}
// Per-program summary
let summaryHtml;
if (programSummaryCache[p.id]) {
summaryHtml = renderMarkdown(programSummaryCache[p.id]);
} else if (!p.parent_id) {
summaryHtml = '<span style="color:var(--text-dim)">Initial seed program.</span>';
} else {
summaryHtml = '<span style="color:var(--text-dim)"><span class="summary-spinner"></span>Generating...</span>';
}
el.innerHTML = `
<div class="detail-row"><span class="detail-label">Iteration:</span><span class="detail-value"><code>${p.iteration}</code></span></div>
<div class="detail-row"><span class="detail-label">Score:</span><span class="detail-value score"><code>${fmt(p.score)}</code></span></div>
<div class="detail-row"><span class="detail-label">Generation:</span><span class="detail-value"><code>${p.generation || 0}</code></span></div>
${p.island != null ? `<div class="detail-row"><span class="detail-label">Island:</span><span class="detail-value"><code>${p.island}</code></span></div>` : ''}
${hfIndicator}
<div class="lineage-section detail-row"><span class="detail-label">Parent:</span><span class="detail-value">${parentHtml}</span></div>
<div class="lineage-section detail-row"><span class="detail-label">Children (${children.length}):</span><span class="detail-value">${childrenHtml}</span></div>
${metricsHtml ? '<div style="margin-top:10px;font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;">Problem-Specific Metrics</div>' + metricsHtml : ''}
<div style="margin-top:8px;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:4px;">
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">What changed</div>
<div id="program-summary" class="summary-body" style="font-size:12px;line-height:1.5">${summaryHtml}</div>
</div>
${p.image_path ? `
<div id="image-viewer" style="margin:12px 0;text-align:center;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;">
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;">Generated Image</div>
<img id="program-image" src="" style="max-width:100%;max-height:300px;border-radius:4px;display:none;" />
<div id="image-loading" style="color:var(--text-dim);font-size:12px;padding:20px 0;">Loading image...</div>
</div>
` : ''}
<div class="code-tabs">
<div class="code-tab active" onclick="showTab('code')">${p.image_path ? 'Prompt' : 'Code'}</div>
<div class="code-tab" onclick="showTab('parent-code')">${p.image_path ? 'Parent Prompt' : 'Parent Code'}</div>
</div>
<div id="code-display" class="code-block">${escapeHtml(p.code_snippet || '...')}\n\n(Loading full ${p.image_path ? 'prompt' : 'code'}...)</div>
<div id="parent-code-display" class="code-block" style="display:none">Loading...</div>
`;
}
function highlightSelected(p) {
if (!plotInitialized || programs.length > 5000) return;
const tc = plotColors();
// Highlight on normal trace (0)
const normalProgs = programs.filter(pr => !pr.human_feedback_active);
const nSizes = normalProgs.map(pr => pr.id === p.id ? 15 : 7);
const nOpacities = normalProgs.map(pr => pr.id === p.id ? 1.0 : 0.85);
const nLineWidths = normalProgs.map(pr => pr.id === p.id ? 2.5 : 0.5);
const nLineColors = normalProgs.map(pr => pr.id === p.id ? tc.selectedRing : tc.dotOutline);
Plotly.restyle('scatter-plot', {
'marker.size': [nSizes],
'marker.opacity': [nOpacities],
'marker.line.width': [nLineWidths],
'marker.line.color': [nLineColors],
}, [0]);
// Highlight on human-feedback trace (1)
const hfProgs = programs.filter(pr => pr.human_feedback_active);
if (hfProgs.length > 0) {
const hSizes = hfProgs.map(pr => pr.id === p.id ? 15 : 9);
const hOpacities = hfProgs.map(pr => pr.id === p.id ? 1.0 : 0.95);
const hLineWidths = hfProgs.map(pr => pr.id === p.id ? 3 : 2);
const hLineColors = hfProgs.map(pr => pr.id === p.id ? tc.selectedRing : tc.bestLine);
Plotly.restyle('scatter-plot', {
'marker.size': [hSizes],
'marker.opacity': [hOpacities],
'marker.line.width': [hLineWidths],
'marker.line.color': [hLineColors],
}, [1]);
}
}
// Global tab switcher
window.showTab = function(tab) {
const codeEl = document.getElementById('code-display');
const parentEl = document.getElementById('parent-code-display');
if (!codeEl || !parentEl) return;
document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
if (tab === 'solution') {
codeEl.style.display = 'block';
parentEl.style.display = 'none';
document.querySelectorAll('.code-tab')[0].classList.add('active');
} else {
codeEl.style.display = 'none';
parentEl.style.display = 'block';
document.querySelectorAll('.code-tab')[1].classList.add('active');
}
};
function showProgramCode(msg) {
if (msg.program_id !== selectedId) return;
const codeEl = document.getElementById('code-display');
const parentEl = document.getElementById('parent-code-display');
if (codeEl) codeEl.textContent = msg.code || '(no code)';
if (parentEl) parentEl.textContent = msg.parent_code || '(no parent code)';
// Request image if this program has one (image evolution mode)
const p = programMap[msg.program_id];
if (p && p.image_path && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'request_image', program_id: p.id, image_path: p.image_path }));
}
}
// ─── Stats ─────────────────────────────────────────────────
function updateStats() {
document.getElementById('stat-programs').textContent = stats.total_programs || programs.length;
document.getElementById('stat-iteration').textContent = stats.current_iteration || (programs.length > 0 ? programs[programs.length-1].iteration : 0);
document.getElementById('stat-best').textContent = fmt(stats.best_score);
document.getElementById('stat-since').textContent = stats.iterations_since_improvement != null ? stats.iterations_since_improvement : '--';
document.getElementById('stat-elapsed').textContent = formatElapsed(stats.elapsed_seconds);
}
function updateBestPanel() {
const el = document.getElementById('best-content');
if (!bestProgramId || !programMap[bestProgramId]) {
el.innerHTML = '<div class="empty-state">Waiting for data...</div>';
return;
}
const p = programMap[bestProgramId];
let bestMetricsHtml = '';
if (p.metrics && Object.keys(p.metrics).length > 0) {
bestMetricsHtml = '<div style="margin-top:10px;font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;">Problem-Specific Metrics</div>';
bestMetricsHtml += '<div class="metrics-grid">';
for (const [k, v] of Object.entries(p.metrics)) {
bestMetricsHtml += `<div class="metric-item"><span class="key">${k}:</span> <span class="val">${fmt(v)}</span></div>`;
}
bestMetricsHtml += '</div>';
}
el.innerHTML = `
<div style="cursor:pointer; text-align:left;" onclick="window.navigateToProgram('${p.id}')" title="Click to select in detail panel">
<div class="detail-row"><span class="detail-label">Score:</span><span class="detail-value score"><code>${fmt(p.score)}</code></span></div>
<div class="detail-row"><span class="detail-label">Iteration:</span><span class="detail-value"><code>${p.iteration}</code></span></div>
<div class="detail-row"><span class="detail-label">Generation:</span><span class="detail-value"><code>${p.generation || 0}</code></span></div>
${p.island != null ? `<div class="detail-row"><span class="detail-label">Island:</span><span class="detail-value"><code>${p.island}</code></span></div>` : ''}
${bestMetricsHtml}
${p.image_path ? `<div id="best-image-container" style="margin-top:10px;text-align:center;">
<img id="best-program-image" src="" style="max-width:100%;max-height:200px;border-radius:4px;display:none;" />
</div>` : ''}
</div>
`;
// Request best image if available
if (p.image_path && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'request_image', program_id: 'best_' + p.id, image_path: p.image_path }));
}
}
// ─── Helpers ───────────────────────────────────────────────
function fmt(v) {
if (v == null) return '--';
if (typeof v === 'number') return v.toFixed(4);
return String(v);
}
function formatElapsed(seconds) {
if (seconds == null) return '--';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
if (m === 0) return `${s}s`;
return `${m}m ${s}s`;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ─── Human Feedback (Human Feedback) ────────────────────────────────
function updateHfState(enabled, active, text, error, mode, currentPrompt, history) {
hfEnabled = !!enabled;
hfActive = !!active;
const panel = document.getElementById('hf-panel');
const status = document.getElementById('hf-status');
const sendBtn = document.getElementById('hf-send-btn');
const clearBtn = document.getElementById('hf-clear-btn');
const input = document.getElementById('hf-input');
const current = document.getElementById('hf-current');
if (!hfEnabled) {
status.className = 'hf-status disabled';
status.textContent = 'Not Enabled';
panel.classList.remove('active');
sendBtn.disabled = true;
clearBtn.disabled = true;
input.disabled = true;
input.placeholder = 'Human feedback not enabled. Set human_feedback_enabled: true in config.';
current.classList.remove('visible');
return;
}
sendBtn.disabled = false;
input.disabled = false;
input.placeholder = 'e.g. Focus on hexagonal packing. Try numpy vectorization.';
if (hfActive && text) {
status.className = 'hf-status active';
status.textContent = 'Active';
panel.classList.add('active');
clearBtn.disabled = false;
current.textContent = text;
current.classList.add('visible');
} else {
status.className = 'hf-status inactive';
status.textContent = 'Inactive';
panel.classList.remove('active');
clearBtn.disabled = true;
current.classList.remove('visible');
}
if (error) {
status.className = 'hf-status disabled';
status.textContent = error;
}
// Update mode UI
if (mode) updateHfModeUI(mode);
// Update prompt viewer
if (currentPrompt !== undefined) updatePromptViewer(currentPrompt);
// Update history
if (history !== undefined) updateHfHistory(history);
}
// ─── Human Feedback Mode Toggle ──────────────────────────────────────────
window.setHfMode = function(mode) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'set_human_feedback_mode', mode: mode }));
};
function updateHfModeUI(mode) {
hfMode = mode;
const appendBtn = document.getElementById('hf-mode-append');
const replaceBtn = document.getElementById('hf-mode-replace');
if (appendBtn) appendBtn.classList.toggle('active', mode === 'append');
if (replaceBtn) replaceBtn.classList.toggle('active', mode === 'replace');
}
// ─── System Prompt Viewer ─────────────────────────────────────
window.togglePromptViewer = function() {
const viewer = document.getElementById('hf-prompt-viewer');
const icon = document.getElementById('hf-prompt-toggle');
viewer.classList.toggle('open');
icon.innerHTML = viewer.classList.contains('open') ? '&#9660;' : '&#9654;';
// Request latest prompt when opening
if (viewer.classList.contains('open') && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'request_system_prompt' }));
}
};
function updatePromptViewer(promptText) {
hfCurrentPrompt = promptText;
const body = document.getElementById('hf-prompt-body');
if (body) {
body.textContent = promptText || '(Prompt will appear after the first iteration runs)';
}
}
// ─── Human Feedback Feedback History ─────────────────────────────────────
window.toggleHfHistory = function() {
const el = document.getElementById('hf-history');
const icon = document.getElementById('hf-history-toggle');
el.classList.toggle('open');
icon.innerHTML = el.classList.contains('open') ? '&#9660;' : '&#9654;';
// Refresh history when opening
if (el.classList.contains('open') && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'request_human_feedback_history' }));
}
};
function updateHfHistory(history) {
hfHistory = history || [];
const countEl = document.getElementById('hf-history-count');
const bodyEl = document.getElementById('hf-history-body');
if (countEl) countEl.textContent = hfHistory.length;
if (!bodyEl) return;
if (hfHistory.length === 0) {
bodyEl.innerHTML = '<div style="padding:12px;color:var(--text-dim);font-size:12px;text-align:center;">No feedback sent yet.</div>';
return;
}
// Render entries newest-first
let html = '';
for (let i = hfHistory.length - 1; i >= 0; i--) {
const entry = hfHistory[i];
const ts = new Date(entry.timestamp * 1000);
const timeStr = ts.toLocaleTimeString();
const modeClass = entry.mode === 'replace' ? 'replace' : 'append';
html += '<div class="hf-history-entry">' +
'<div class="meta">' +
'<span class="iter">Iter ' + entry.iteration + '</span>' +
'<span>' + timeStr + '</span>' +
'<span class="mode-tag ' + modeClass + '">' + entry.mode + '</span>' +
'</div>' +
'<div class="feedback-text">' + escapeHtml(entry.text) + '</div>' +
'</div>';
}
bodyEl.innerHTML = html;
}
// ─── Human Feedback Controls ─────────────────────────────────────────────
window.toggleHf = function() {
const panel = document.getElementById('hf-panel');
const icon = document.getElementById('hf-toggle-icon');
panel.classList.toggle('open');
icon.innerHTML = panel.classList.contains('open') ? '&#9660;' : '&#9654;';
};
window.sendFeedback = function() {
const input = document.getElementById('hf-input');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'set_feedback', text: text }));
input.value = '';
};
window.clearFeedback = function() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'clear_feedback' }));
};
// Allow Ctrl+Enter to send feedback
document.getElementById('hf-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
window.sendFeedback();
}
});
// ─── AI Summary ──────────────────────────────────────────────
// Lightweight markdown→HTML for LLM summary output
function renderMarkdown(md) {
const lines = md.split('\n');
let html = '';
let inList = false;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// ## Header
if (/^##\s+(.+)/.test(line)) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h2>' + escapeHtml(line.replace(/^##\s+/, '')) + '</h2>';
continue;
}
// Bullet: - text or * text
if (/^\s*[-*]\s+(.+)/.test(line)) {
if (!inList) { html += '<ul>'; inList = true; }
const bullet = line.replace(/^\s*[-*]\s+/, '');
html += '<li>' + inlineMarkdown(bullet) + '</li>';
continue;
}
// Anything else: paragraph
if (inList) { html += '</ul>'; inList = false; }
const trimmed = line.trim();
if (trimmed) {
html += '<p>' + inlineMarkdown(trimmed) + '</p>';
}
}
if (inList) html += '</ul>';
return html;
}
// Inline markdown: **bold**, `code`, escaping
function inlineMarkdown(text) {
let s = escapeHtml(text);
// **bold**
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// `code`
s = s.replace(/`(.+?)`/g, '<code>$1</code>');
return s;
}
function updateSummaryState(enabled, generating, text) {
summaryEnabled = !!enabled;
summaryGenerating = !!generating;
const content = document.getElementById('summary-content');
const refreshBtn = document.getElementById('summary-refresh-btn');
refreshBtn.disabled = summaryGenerating;
if (!summaryEnabled && !text) {
content.innerHTML = '<div class="empty-state" style="padding:20px;">AI summary not configured.</div>';
refreshBtn.disabled = true;
return;
}
if (summaryGenerating) {
let spinHtml = '<div style="padding:12px;color:var(--text-dim);font-size:13px;"><span class="summary-spinner"></span>Generating summary...</div>';
if (text) {
spinHtml += '<div class="summary-body" style="opacity:0.5">' + renderMarkdown(text) + '</div>';
}
content.innerHTML = spinHtml;
return;
}
if (text) {
let html = '<div class="summary-body">' + renderMarkdown(text) + '</div>';
html += '<div class="summary-meta">' + escapeHtml(summaryModelName || 'AI') + ' | ' + programs.length + ' programs</div>';
content.innerHTML = html;
} else {
content.innerHTML = '<div class="empty-state" style="padding:20px;">Click Refresh to generate a summary.</div>';
}
}
let summaryModelName = '';
window.requestSummary = function() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ type: 'request_summary' }));
};
// ─── Island coloring toggle ──────────────────────────────────
window.toggleColoring = function() {
colorByIsland = !colorByIsland;
document.getElementById('color-toggle').textContent = colorByIsland ? 'Color: Island' : 'Color: Score';
rebuildPlot();
};
// ─── Filter ────────────────────────────────────────────────
document.getElementById('filter-input').addEventListener('input', (e) => {
filterText = e.target.value.toLowerCase().trim();
});
// ─── Init ──────────────────────────────────────────────────
rebuildPlot();
connect();
})();
</script>
</body>
</html>