| <!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> |
| |
| [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); |
| } |
| |
| |
| :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 { |
| 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 { |
| 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; |
| } |
| |
| |
| .plot-container { |
| padding: 10px 20px; |
| } |
| #scatter-plot { |
| width: 100%; |
| height: 38vh; |
| min-height: 250px; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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; } |
| |
| |
| .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; } |
| |
| |
| .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; } |
| |
| |
| .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; |
| } |
| |
| |
| .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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="plot-container"> |
| <div id="scatter-plot"></div> |
| </div> |
|
|
| |
| <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">▼</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> |
| |
| <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> |
| |
| <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">▶</span> |
| </div> |
| <div class="hf-prompt-body" id="hf-prompt-body">(Prompt will appear after the first iteration runs)</div> |
| </div> |
| |
| <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">▶</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> |
|
|
| |
| <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'; |
| |
| |
| function getThemeColor(varName) { |
| return getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); |
| } |
| |
| function isDarkTheme() { |
| return (document.documentElement.getAttribute('data-theme') || 'light') === 'dark'; |
| } |
| |
| |
| 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]); |
| } |
| }; |
| |
| |
| let programs = []; |
| let programMap = {}; |
| let bestProgramId = null; |
| let stats = {}; |
| let ws = null; |
| let selectedId = null; |
| let bestLine = []; |
| let maxIteration = 0; |
| 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 = {}; |
| |
| |
| let childrenIndex = {}; |
| let contextUsedByIndex = {}; |
| |
| 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); |
| }); |
| } |
| |
| |
| function connect() { |
| const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| |
| 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; |
| |
| 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'; |
| } |
| |
| 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; |
| } |
| } |
| |
| |
| |
| 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); |
| } |
| }); |
| } |
| |
| |
| |
| const ISLAND_COLORS = ['#e5484d', '#30a46c', '#e5a000', '#3e63dd', '#8e4ec6', '#12a594', '#e54666', '#0090ff', '#f76b15', '#46a758']; |
| |
| function rebuildPlot() { |
| const tc = plotColors(); |
| |
| |
| 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', |
| }; |
| |
| |
| 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; |
| } |
| |
| |
| if (colorByIsland) { |
| rebuildPlot(); |
| return; |
| } |
| |
| const tc = plotColors(); |
| const plotDiv = document.getElementById('scatter-plot'); |
| |
| |
| 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]); |
| |
| |
| 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; |
| |
| |
| const pid = pt.customdata; |
| if (pid && programMap[pid]) { |
| selectProgram(programMap[pid]); |
| } |
| } |
| |
| |
| function showLineageArrows(p) { |
| const plotDiv = document.getElementById('scatter-plot'); |
| const tc = plotColors(); |
| const annotations = []; |
| |
| |
| 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, |
| }); |
| } |
| |
| |
| (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, |
| }); |
| } |
| }); |
| |
| |
| (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, |
| }); |
| } |
| }); |
| |
| |
| (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 }); |
| } |
| |
| |
| 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); |
| }; |
| |
| |
| function deselectProgram() { |
| selectedId = null; |
| |
| document.getElementById('detail-content').innerHTML = '<div class="empty-state">Click a point on the scatter plot</div>'; |
| |
| Plotly.relayout('scatter-plot', { annotations: [] }); |
| |
| 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) { |
| |
| if (selectedId === p.id) { |
| deselectProgram(); |
| return; |
| } |
| |
| selectedId = p.id; |
| showLineageArrows(p); |
| highlightSelected(p); |
| |
| |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify({ type: 'request_program_solution', program_id: p.id })); |
| |
| if (!programSummaryCache[p.id]) { |
| ws.send(JSON.stringify({ type: 'request_program_summary', program_id: p.id })); |
| } |
| } |
| |
| const el = document.getElementById('detail-content'); |
| |
| |
| 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); |
| } |
| |
| |
| 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>'; |
| } |
| |
| |
| 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>'; |
| } |
| |
| |
| 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)'; |
| |
| |
| 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>'; |
| } |
| |
| |
| 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(); |
| |
| 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]); |
| |
| 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]); |
| } |
| } |
| |
| |
| 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)'; |
| |
| |
| 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 })); |
| } |
| } |
| |
| |
| 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> |
| `; |
| |
| 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 })); |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| if (mode) updateHfModeUI(mode); |
| |
| |
| if (currentPrompt !== undefined) updatePromptViewer(currentPrompt); |
| |
| |
| if (history !== undefined) updateHfHistory(history); |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| 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') ? '▼' : '▶'; |
| |
| 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)'; |
| } |
| } |
| |
| |
| 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') ? '▼' : '▶'; |
| |
| 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; |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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') ? '▼' : '▶'; |
| }; |
| |
| 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' })); |
| }; |
| |
| |
| document.getElementById('hf-input').addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { |
| e.preventDefault(); |
| window.sendFeedback(); |
| } |
| }); |
| |
| |
| |
| 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]; |
| |
| |
| if (/^##\s+(.+)/.test(line)) { |
| if (inList) { html += '</ul>'; inList = false; } |
| html += '<h2>' + escapeHtml(line.replace(/^##\s+/, '')) + '</h2>'; |
| continue; |
| } |
| |
| |
| if (/^\s*[-*]\s+(.+)/.test(line)) { |
| if (!inList) { html += '<ul>'; inList = true; } |
| const bullet = line.replace(/^\s*[-*]\s+/, ''); |
| html += '<li>' + inlineMarkdown(bullet) + '</li>'; |
| continue; |
| } |
| |
| |
| if (inList) { html += '</ul>'; inList = false; } |
| const trimmed = line.trim(); |
| if (trimmed) { |
| html += '<p>' + inlineMarkdown(trimmed) + '</p>'; |
| } |
| } |
| if (inList) html += '</ul>'; |
| return html; |
| } |
| |
| |
| function inlineMarkdown(text) { |
| let s = escapeHtml(text); |
| |
| s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
| |
| 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' })); |
| }; |
| |
| |
| window.toggleColoring = function() { |
| colorByIsland = !colorByIsland; |
| document.getElementById('color-toggle').textContent = colorByIsland ? 'Color: Island' : 'Color: Score'; |
| rebuildPlot(); |
| }; |
| |
| |
| document.getElementById('filter-input').addEventListener('input', (e) => { |
| filterText = e.target.value.toLowerCase().trim(); |
| }); |
| |
| |
| rebuildPlot(); |
| connect(); |
| |
| })(); |
| </script> |
| </body> |
| </html> |
|
|