Spaces:
Sleeping
Sleeping
Download viewer/tldraw_viewer.html from sreeramajay/visual_reasoning-env: direct link, hf CLI and curl.
- Browser
- Download file 24.7 kB
-
https://huggingface.co/spaces/sreeramajay/visual_reasoning-env/resolve/main/viewer/tldraw_viewer.html
- Command line
-
hf download hf://spaces/sreeramajay/visual_reasoning-env/viewer/tldraw_viewer.html
-
curl -L -o tldraw_viewer.html https://huggingface.co/spaces/sreeramajay/visual_reasoning-env/resolve/main/viewer/tldraw_viewer.html
24.7 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <title>Visual Reasoning — Live Viewer</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| background: #1a1a2e; | |
| color: #e0e0e0; | |
| } | |
| #app { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| } | |
| .header { | |
| display: flex; | |
| align-items: center; | |
| gap: 24px; | |
| padding: 10px 20px; | |
| background: #16213e; | |
| border-bottom: 1px solid #2a2a4a; | |
| font-size: 13px; | |
| flex-shrink: 0; | |
| } | |
| .header .task { font-weight: 700; color: #4fc3f7; } | |
| .header .scenario { color: #90a4ae; } | |
| .header .step-counter { color: #e0e0e0; } | |
| .header .score-display { font-weight: 700; color: #66bb6a; margin-left: auto; } | |
| .header .ws-status { | |
| font-size: 11px; | |
| padding: 2px 10px; | |
| border-radius: 10px; | |
| border: 1px solid #2a2a4a; | |
| } | |
| .header .ws-status.on { border-color: #66bb6a; color: #66bb6a; } | |
| .header .ws-status.off { border-color: #ef5350; color: #ef5350; } | |
| .body-row { | |
| display: flex; | |
| flex: 1 1 auto; | |
| min-height: 0; | |
| overflow: hidden; | |
| } | |
| .canvas-panel { | |
| flex: 1 1 auto; | |
| min-width: 0; | |
| background: #0f0f23; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 12px; | |
| } | |
| .canvas-panel svg { | |
| width: 100%; | |
| height: 100%; | |
| max-width: 100%; | |
| max-height: 100%; | |
| } | |
| .side-panel { | |
| width: 340px; | |
| flex-shrink: 0; | |
| background: #16213e; | |
| border-left: 1px solid #2a2a4a; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .side-section { | |
| padding: 12px 16px; | |
| border-bottom: 1px solid #2a2a4a; | |
| flex-shrink: 0; | |
| } | |
| .side-section h3 { | |
| font-size: 10px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: #4fc3f7; | |
| margin-bottom: 8px; | |
| } | |
| .concept { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 2px 0; | |
| font-size: 12px; | |
| } | |
| .concept .check { | |
| width: 16px; | |
| text-align: center; | |
| font-size: 13px; | |
| } | |
| .concept.covered .check { color: #66bb6a; } | |
| .concept.uncovered .check { color: #616161; } | |
| .concept.covered { color: #c8e6c9; } | |
| .concept.uncovered { color: #757575; } | |
| .score-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 4px; | |
| font-size: 12px; | |
| } | |
| .score-label { | |
| width: 90px; | |
| flex-shrink: 0; | |
| color: #90a4ae; | |
| font-size: 11px; | |
| } | |
| .score-bar-track { | |
| flex: 1; | |
| height: 16px; | |
| background: #0f0f23; | |
| border-radius: 3px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .score-bar { | |
| height: 100%; | |
| border-radius: 3px; | |
| display: flex; | |
| align-items: center; | |
| padding-left: 6px; | |
| font-size: 10px; | |
| font-weight: 600; | |
| color: #fff; | |
| min-width: 30px; | |
| transition: width 0.3s ease; | |
| } | |
| .narration-panel { | |
| flex: 1 1 auto; | |
| overflow-y: auto; | |
| padding: 12px 16px; | |
| min-height: 0; | |
| } | |
| .narration-panel h3 { | |
| font-size: 10px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: #4fc3f7; | |
| margin-bottom: 8px; | |
| } | |
| .narration-entry { | |
| padding: 6px 0; | |
| border-bottom: 1px solid #1a1a2e; | |
| font-size: 12px; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .narration-entry .step-label { | |
| font-weight: 600; | |
| color: #4fc3f7; | |
| font-size: 11px; | |
| } | |
| .narration-entry .narr-text { | |
| color: #cfd8dc; | |
| margin-top: 2px; | |
| line-height: 1.4; | |
| } | |
| .narration-entry .reward-badge { | |
| font-size: 10px; | |
| font-weight: 700; | |
| margin-left: 8px; | |
| } | |
| .narration-entry .reward-badge.pos { color: #66bb6a; } | |
| .narration-entry .reward-badge.neg { color: #ef5350; } | |
| .voiceover-bar { | |
| flex-shrink: 0; | |
| padding: 10px 20px; | |
| background: #16213e; | |
| border-top: 1px solid #2a2a4a; | |
| font-size: 13px; | |
| color: #b0bec5; | |
| font-style: italic; | |
| min-height: 40px; | |
| transition: opacity 1s ease; | |
| } | |
| .voiceover-bar.faded { opacity: 0.3; } | |
| .overlay { | |
| position: fixed; | |
| top: 0; left: 0; right: 0; | |
| background: rgba(15, 15, 35, 0.95); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 20px 20px 24px; | |
| z-index: 1000; | |
| border-bottom: 2px solid #2a2a4a; | |
| cursor: pointer; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .overlay.hidden { display: none; } | |
| .overlay h1 { | |
| font-size: 24px; | |
| margin-bottom: 8px; | |
| } | |
| .overlay h1.success { color: #66bb6a; } | |
| .overlay h1.failure { color: #ef5350; } | |
| .overlay .final-score { | |
| font-size: 20px; | |
| color: #90a4ae; | |
| margin-bottom: 24px; | |
| } | |
| .reward-chart { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 4px; | |
| height: 120px; | |
| padding: 0 20px; | |
| } | |
| .reward-chart .bar { | |
| width: 28px; | |
| background: #4fc3f7; | |
| border-radius: 3px 3px 0 0; | |
| min-height: 2px; | |
| transition: height 0.3s ease; | |
| position: relative; | |
| } | |
| .reward-chart .bar .bar-label { | |
| position: absolute; | |
| bottom: -18px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 9px; | |
| color: #90a4ae; | |
| } | |
| .error-flash { | |
| position: fixed; | |
| top: 60px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #d32f2f; | |
| color: #fff; | |
| padding: 8px 20px; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| z-index: 999; | |
| animation: fadeIn 0.3s ease; | |
| display: none; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 0.4; } | |
| 50% { opacity: 1; } | |
| 100% { opacity: 0.4; } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .highlight-glow { | |
| animation: pulse 1.2s ease-in-out infinite; | |
| } | |
| .entity-new { | |
| animation: fadeIn 0.3s ease; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <div class="header"> | |
| <span class="task" id="hdr-task">--</span> | |
| <span class="scenario" id="hdr-scenario">--</span> | |
| <span class="step-counter" id="hdr-step">Step 0/0</span> | |
| <span class="score-display" id="hdr-score">Score: 0.00</span> | |
| <span class="ws-status off" id="hdr-ws">disconnected</span> | |
| </div> | |
| <div class="body-row"> | |
| <div class="canvas-panel"> | |
| <svg id="canvas" viewBox="-50 -50 850 650" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <marker id="arrow" markerWidth="10" markerHeight="10" refX="10" refY="5" orient="auto" markerUnits="strokeWidth"> | |
| <path d="M0,0 L10,5 L0,10" fill="#90a4ae"/> | |
| </marker> | |
| </defs> | |
| </svg> | |
| </div> | |
| <div class="side-panel"> | |
| <div class="side-section" id="checklist-section"> | |
| <h3>Checklist</h3> | |
| <div id="checklist-container"></div> | |
| </div> | |
| <div class="side-section" id="breakdown-section"> | |
| <h3>Score Breakdown</h3> | |
| <div id="breakdown-container"></div> | |
| </div> | |
| <div class="narration-panel" id="narration-panel"> | |
| <h3>Narration History</h3> | |
| <div id="narration-container"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="voiceover-bar faded" id="voiceover-bar"></div> | |
| </div> | |
| <div class="overlay hidden" id="overlay"> | |
| <h1 id="overlay-title"></h1> | |
| <div class="final-score" id="overlay-score"></div> | |
| <div class="reward-chart" id="reward-chart"></div> | |
| </div> | |
| <div class="error-flash" id="error-flash"></div> | |
| <script> | |
| const ROLE_COLORS = { | |
| default: '#455a64', | |
| current: '#4fc3f7', | |
| visited: '#66bb6a', | |
| frontier: '#ffa726', | |
| done: '#2e7d32', | |
| pivot: '#ef5350', | |
| root: '#ab47bc', | |
| error: '#d32f2f', | |
| inactive: '#616161', | |
| comparing: '#ffee58' | |
| }; | |
| const SCALE = 50; | |
| const SVG_NS = 'http://www.w3.org/2000/svg'; | |
| let state = { | |
| taskName: '', | |
| scenarioId: '', | |
| goal: '', | |
| checklist: [], | |
| coverage: [], | |
| maxSteps: 0, | |
| currentStep: 0, | |
| score: 0, | |
| breakdown: {}, | |
| entities: {}, | |
| relations: [], | |
| layout: {}, | |
| annotations: [], | |
| notes: [], | |
| narrations: [], | |
| highlighted: new Set() | |
| }; | |
| let voiceoverTimer = null; | |
| let errorTimer = null; | |
| const $ = (id) => document.getElementById(id); | |
| function scoreBarColor(val) { | |
| if (val >= 0.7) return '#66bb6a'; | |
| if (val >= 0.4) return '#ffa726'; | |
| return '#ef5350'; | |
| } | |
| function penaltyBarColor(val) { | |
| if (val <= 0) return '#66bb6a'; | |
| if (val < 0.1) return '#ffa726'; | |
| return '#ef5350'; | |
| } | |
| function clearSVG() { | |
| const svg = $('canvas'); | |
| const defs = svg.querySelector('defs'); | |
| while (svg.lastChild && svg.lastChild !== defs) { | |
| svg.removeChild(svg.lastChild); | |
| } | |
| } | |
| function svgEl(tag, attrs) { | |
| const el = document.createElementNS(SVG_NS, tag); | |
| for (const [k, v] of Object.entries(attrs || {})) { | |
| el.setAttribute(k, String(v)); | |
| } | |
| return el; | |
| } | |
| function getPos(eid) { | |
| const p = state.layout[eid]; | |
| if (!p) return null; | |
| return { x: Number(p.x) * SCALE, y: Number(p.y) * SCALE }; | |
| } | |
| function deriveHighlighted() { | |
| const hl = new Set(); | |
| for (const a of state.annotations) { | |
| if (a && a.text === '[highlight]') hl.add(a.target_id); | |
| } | |
| return hl; | |
| } | |
| function renderSVG() { | |
| clearSVG(); | |
| const svg = $('canvas'); | |
| const highlighted = deriveHighlighted(); | |
| for (const [eid, ent] of Object.entries(state.entities)) { | |
| if (!ent || ent.entity_type !== 'region') continue; | |
| const bounds = ent.bounds; | |
| if (!bounds) continue; | |
| const x0 = Number(bounds.x0) * SCALE; | |
| const y0 = Number(bounds.y0) * SCALE; | |
| const x1 = Number(bounds.x1) * SCALE; | |
| const y1 = Number(bounds.y1) * SCALE; | |
| const rect = svgEl('rect', { | |
| x: x0, y: y0, width: x1 - x0, height: y1 - y0, | |
| fill: 'rgba(100,150,200,0.1)', stroke: '#4fc3f7', | |
| 'stroke-dasharray': '5,5', 'stroke-width': 1 | |
| }); | |
| svg.appendChild(rect); | |
| const title = svgEl('text', { | |
| x: x0 + 8, y: y0 + 16, | |
| fill: '#4fc3f7', 'font-size': '12', 'font-family': 'sans-serif', opacity: '0.8' | |
| }); | |
| title.textContent = ent.title || eid; | |
| svg.appendChild(title); | |
| } | |
| for (const rel of state.relations) { | |
| if (!rel || !rel.src || !rel.dst) continue; | |
| const srcPos = getPos(rel.src); | |
| const dstPos = getPos(rel.dst); | |
| if (!srcPos || !dstPos) continue; | |
| const line = svgEl('line', { | |
| x1: srcPos.x, y1: srcPos.y, x2: dstPos.x, y2: dstPos.y, | |
| stroke: '#90a4ae', 'stroke-width': 1.5, | |
| 'marker-end': 'url(#arrow)' | |
| }); | |
| svg.appendChild(line); | |
| if (rel.label) { | |
| const mx = (srcPos.x + dstPos.x) / 2; | |
| const my = (srcPos.y + dstPos.y) / 2; | |
| const label = svgEl('text', { | |
| x: mx, y: my - 6, | |
| fill: '#b0bec5', 'font-size': '10', 'font-family': 'sans-serif', | |
| 'text-anchor': 'middle' | |
| }); | |
| label.textContent = rel.label; | |
| svg.appendChild(label); | |
| } | |
| } | |
| for (const [eid, ent] of Object.entries(state.entities)) { | |
| if (!ent) continue; | |
| const type = ent.entity_type || 'node'; | |
| if (type === 'region') continue; | |
| const pos = getPos(eid); | |
| if (!pos) continue; | |
| const role = ent.role || 'default'; | |
| const fillColor = ROLE_COLORS[role] || ROLE_COLORS.default; | |
| const isHighlighted = highlighted.has(eid); | |
| if (isHighlighted) { | |
| const glow = svgEl('circle', { | |
| cx: pos.x, cy: pos.y, r: 28, | |
| fill: 'none', stroke: '#fdd835', 'stroke-width': 2, | |
| class: 'highlight-glow' | |
| }); | |
| svg.appendChild(glow); | |
| } | |
| if (type === 'pointer') { | |
| const tri = svgEl('polygon', { | |
| points: `${pos.x - 8},${pos.y - 8} ${pos.x + 10},${pos.y} ${pos.x - 8},${pos.y + 8}`, | |
| fill: fillColor, stroke: '#e0e0e0', 'stroke-width': 0.5, | |
| class: 'entity-new' | |
| }); | |
| svg.appendChild(tri); | |
| const idxLabel = svgEl('text', { | |
| x: pos.x + 16, y: pos.y + 4, | |
| fill: '#e0e0e0', 'font-size': '11', 'font-family': 'sans-serif' | |
| }); | |
| idxLabel.textContent = `${eid}@${ent.value != null ? ent.value : ''}`; | |
| svg.appendChild(idxLabel); | |
| } else if (type === 'container') { | |
| const w = 80, h = 50; | |
| const rect = svgEl('rect', { | |
| x: pos.x - w / 2, y: pos.y - h / 2, width: w, height: h, | |
| rx: 8, ry: 8, | |
| fill: 'rgba(69,90,100,0.3)', stroke: fillColor, 'stroke-width': 1.5, | |
| class: 'entity-new' | |
| }); | |
| svg.appendChild(rect); | |
| const titleText = ent.title || eid; | |
| const titleEl = svgEl('text', { | |
| x: pos.x, y: pos.y - h / 2 - 6, | |
| fill: '#b0bec5', 'font-size': '10', 'font-family': 'sans-serif', | |
| 'text-anchor': 'middle' | |
| }); | |
| titleEl.textContent = titleText; | |
| svg.appendChild(titleEl); | |
| const contents = (ent.contents || []).join(', '); | |
| if (contents) { | |
| const contEl = svgEl('text', { | |
| x: pos.x, y: pos.y + 4, | |
| fill: '#e0e0e0', 'font-size': '11', 'font-family': 'monospace', | |
| 'text-anchor': 'middle' | |
| }); | |
| contEl.textContent = contents; | |
| svg.appendChild(contEl); | |
| } | |
| } else { | |
| const circle = svgEl('circle', { | |
| cx: pos.x, cy: pos.y, r: 20, | |
| fill: fillColor, | |
| stroke: role === 'error' ? '#ff8a80' : '#e0e0e0', | |
| 'stroke-width': role === 'error' ? 2 : 1, | |
| opacity: role === 'inactive' ? 0.5 : 1, | |
| class: 'entity-new' | |
| }); | |
| svg.appendChild(circle); | |
| if (role === 'error') { | |
| const slash = svgEl('line', { | |
| x1: pos.x - 14, y1: pos.y - 14, | |
| x2: pos.x + 14, y2: pos.y + 14, | |
| stroke: '#ff8a80', 'stroke-width': 2 | |
| }); | |
| svg.appendChild(slash); | |
| } | |
| const valText = ent.value != null ? String(ent.value) : eid; | |
| const label = svgEl('text', { | |
| x: pos.x, y: pos.y + 4, | |
| fill: '#fff', 'font-size': '12', 'font-weight': 'bold', | |
| 'font-family': 'sans-serif', 'text-anchor': 'middle' | |
| }); | |
| label.textContent = valText; | |
| svg.appendChild(label); | |
| } | |
| } | |
| const textAnns = new Map(); | |
| for (const a of state.annotations) { | |
| if (!a || a.text === '[highlight]') continue; | |
| const list = textAnns.get(a.target_id) || []; | |
| list.push(a.text); | |
| textAnns.set(a.target_id, list); | |
| } | |
| for (const [targetId, texts] of textAnns.entries()) { | |
| const pos = getPos(targetId); | |
| if (!pos) continue; | |
| const joined = texts.join(' | '); | |
| const ann = svgEl('text', { | |
| x: pos.x + 8, y: pos.y + 28, | |
| fill: '#ffa726', 'font-size': '10', 'font-family': 'sans-serif', | |
| opacity: '0.9' | |
| }); | |
| ann.textContent = joined; | |
| svg.appendChild(ann); | |
| } | |
| const regionBounds = {}; | |
| for (const [eid, ent] of Object.entries(state.entities)) { | |
| if (ent && ent.entity_type === 'region' && ent.bounds) { | |
| regionBounds[eid] = ent.bounds; | |
| } | |
| } | |
| for (const note of state.notes) { | |
| if (!note || !note.text) continue; | |
| let nx, ny; | |
| if (note.region && regionBounds[note.region]) { | |
| const b = regionBounds[note.region]; | |
| nx = Number(b.x0) * SCALE + 10; | |
| ny = Number(b.y1) * SCALE - 10; | |
| } else { | |
| nx = 350; | |
| ny = 550; | |
| } | |
| const bg = svgEl('rect', { | |
| x: nx - 4, y: ny - 14, width: 160, height: 22, | |
| rx: 4, ry: 4, | |
| fill: 'rgba(22,33,62,0.85)', stroke: '#2a2a4a', 'stroke-width': 0.5 | |
| }); | |
| svg.appendChild(bg); | |
| const txt = svgEl('text', { | |
| x: nx, y: ny, | |
| fill: '#b0bec5', 'font-size': '10', 'font-family': 'sans-serif' | |
| }); | |
| txt.textContent = note.text.length > 40 ? note.text.slice(0, 40) + '...' : note.text; | |
| svg.appendChild(txt); | |
| } | |
| } | |
| function updateHeader() { | |
| $('hdr-task').textContent = state.taskName || '--'; | |
| $('hdr-scenario').textContent = state.scenarioId || '--'; | |
| $('hdr-step').textContent = `Step ${state.currentStep}/${state.maxSteps}`; | |
| $('hdr-score').textContent = `Score: ${state.score.toFixed(2)}`; | |
| } | |
| function updateChecklist() { | |
| const container = $('checklist-container'); | |
| container.innerHTML = ''; | |
| for (const c of state.checklist) { | |
| const covered = state.coverage.includes(c); | |
| const div = document.createElement('div'); | |
| div.className = `concept ${covered ? 'covered' : 'uncovered'}`; | |
| div.innerHTML = `<span class="check">${covered ? '✓' : '○'}</span> ${c}`; | |
| container.appendChild(div); | |
| } | |
| } | |
| function updateBreakdown() { | |
| const container = $('breakdown-container'); | |
| container.innerHTML = ''; | |
| const bd = state.breakdown || {}; | |
| const allKeys = Object.keys(bd).filter(k => k !== 'overall_score' && k !== 'phase' && typeof bd[k] === 'number'); | |
| const subKeys = allKeys.filter(k => !k.startsWith('penalty_')); | |
| const penKeys = allKeys.filter(k => k.startsWith('penalty_')); | |
| for (const k of subKeys) { | |
| const val = Number(bd[k]) || 0; | |
| const pct = Math.max(0, Math.min(100, val * 100)); | |
| const label = k.replace(/_score$/, '').replace(/_/g, ' '); | |
| const row = document.createElement('div'); | |
| row.className = 'score-row'; | |
| row.innerHTML = ` | |
| <span class="score-label">${label}</span> | |
| <div class="score-bar-track"> | |
| <div class="score-bar" style="width:${pct}%; background:${scoreBarColor(val)};">${val.toFixed(2)}</div> | |
| </div>`; | |
| container.appendChild(row); | |
| } | |
| if (penKeys.length) { | |
| const header = document.createElement('div'); | |
| header.style.cssText = 'margin-top:8px;margin-bottom:4px;font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#ef5350;font-weight:600;'; | |
| header.textContent = 'Penalties (lower = better)'; | |
| container.appendChild(header); | |
| for (const k of penKeys) { | |
| const val = Number(bd[k]) || 0; | |
| const pct = Math.max(0, Math.min(100, val * 100)); | |
| const label = k.replace(/^penalty_/, '').replace(/_/g, ' '); | |
| const row = document.createElement('div'); | |
| row.className = 'score-row'; | |
| row.innerHTML = ` | |
| <span class="score-label">${label}</span> | |
| <div class="score-bar-track"> | |
| <div class="score-bar" style="width:${pct}%; background:${penaltyBarColor(val)};">${val.toFixed(2)}</div> | |
| </div>`; | |
| container.appendChild(row); | |
| } | |
| } | |
| if (bd.overall_score != null) { | |
| const val = Number(bd.overall_score); | |
| const pct = Math.max(0, Math.min(100, val * 100)); | |
| const row = document.createElement('div'); | |
| row.className = 'score-row'; | |
| row.style.marginTop = '6px'; | |
| row.style.borderTop = '1px solid #2a2a4a'; | |
| row.style.paddingTop = '6px'; | |
| row.innerHTML = ` | |
| <span class="score-label" style="font-weight:700;">overall</span> | |
| <div class="score-bar-track"> | |
| <div class="score-bar" style="width:${pct}%; background:${scoreBarColor(val)};">${val.toFixed(3)}</div> | |
| </div>`; | |
| container.appendChild(row); | |
| } | |
| } | |
| function addNarration(step, narration, reward) { | |
| const container = $('narration-container'); | |
| const entry = document.createElement('div'); | |
| entry.className = 'narration-entry'; | |
| const rClass = reward >= 0 ? 'pos' : 'neg'; | |
| const rSign = reward >= 0 ? '+' : ''; | |
| entry.innerHTML = ` | |
| <span class="step-label">Step ${step}</span> | |
| <span class="reward-badge ${rClass}">${rSign}${reward.toFixed(2)}</span> | |
| <div class="narr-text">${narration || ''}</div>`; | |
| container.appendChild(entry); | |
| const panel = $('narration-panel'); | |
| panel.scrollTop = panel.scrollHeight; | |
| } | |
| function showVoiceover(text) { | |
| const bar = $('voiceover-bar'); | |
| bar.textContent = text || ''; | |
| bar.classList.remove('faded'); | |
| if (voiceoverTimer) clearTimeout(voiceoverTimer); | |
| voiceoverTimer = setTimeout(() => bar.classList.add('faded'), 4000); | |
| } | |
| function flashError(text) { | |
| const el = $('error-flash'); | |
| el.textContent = text; | |
| el.style.display = 'block'; | |
| if (errorTimer) clearTimeout(errorTimer); | |
| errorTimer = setTimeout(() => { el.style.display = 'none'; }, 4000); | |
| } | |
| function handleReset(msg) { | |
| $('overlay').classList.add('hidden'); | |
| state.taskName = msg.task_name || ''; | |
| state.scenarioId = msg.scenario_id || ''; | |
| state.goal = msg.goal || ''; | |
| state.checklist = msg.checklist || []; | |
| state.coverage = msg.coverage || []; | |
| state.maxSteps = msg.max_steps || 0; | |
| state.currentStep = 0; | |
| state.score = 0; | |
| state.breakdown = msg.score_breakdown || {}; | |
| state.entities = msg.entities || {}; | |
| state.relations = msg.relations || []; | |
| state.layout = msg.layout || {}; | |
| state.annotations = msg.annotations || []; | |
| state.notes = msg.notes || []; | |
| state.narrations = []; | |
| updateHeader(); | |
| updateChecklist(); | |
| updateBreakdown(); | |
| $('narration-container').innerHTML = ''; | |
| $('voiceover-bar').textContent = state.goal; | |
| $('voiceover-bar').classList.remove('faded'); | |
| renderSVG(); | |
| } | |
| function handleStep(msg) { | |
| state.currentStep = msg.step || state.currentStep; | |
| state.score = msg.score != null ? msg.score : state.score; | |
| state.breakdown = msg.score_breakdown || state.breakdown; | |
| state.coverage = msg.coverage || state.coverage; | |
| state.entities = msg.entities || state.entities; | |
| state.relations = msg.relations || state.relations; | |
| state.layout = msg.layout || state.layout; | |
| state.annotations = msg.annotations || state.annotations; | |
| state.notes = msg.notes || state.notes; | |
| state.maxSteps = state.currentStep + (msg.remaining_step_budget || 0); | |
| updateHeader(); | |
| updateChecklist(); | |
| updateBreakdown(); | |
| renderSVG(); | |
| const narration = msg.narration || ''; | |
| const reward = msg.reward || 0; | |
| addNarration(msg.step, narration, reward); | |
| showVoiceover(narration); | |
| if (msg.error) flashError(msg.error); | |
| } | |
| function handleEnd(msg) { | |
| const overlay = $('overlay'); | |
| const title = $('overlay-title'); | |
| const score = $('overlay-score'); | |
| const chart = $('reward-chart'); | |
| overlay.classList.remove('hidden'); | |
| title.textContent = `Episode Done — ${msg.task_name || ''}`; | |
| title.className = msg.success ? 'success' : 'failure'; | |
| score.textContent = `Score: ${(msg.score || 0).toFixed(3)} | Steps: ${msg.steps || 0}`; | |
| chart.innerHTML = ''; | |
| const rewards = msg.rewards || []; | |
| const maxR = Math.max(0.01, ...rewards.map(r => Math.abs(r))); | |
| for (let i = 0; i < rewards.length; i++) { | |
| const r = rewards[i]; | |
| const h = Math.max(2, (Math.abs(r) / maxR) * 100); | |
| const bar = document.createElement('div'); | |
| bar.className = 'bar'; | |
| bar.style.height = h + 'px'; | |
| bar.style.background = r >= 0 ? '#4fc3f7' : '#ef5350'; | |
| bar.innerHTML = `<span class="bar-label">${i + 1}</span>`; | |
| chart.appendChild(bar); | |
| } | |
| overlay.onclick = () => overlay.classList.add('hidden'); | |
| } | |
| function handleShutdown() { | |
| showVoiceover('All episodes complete. Server shutting down.'); | |
| } | |
| let ws; | |
| function connect() { | |
| ws = new WebSocket(`ws://${location.host}/ws`); | |
| ws.onopen = () => { | |
| $('hdr-ws').textContent = 'connected'; | |
| $('hdr-ws').className = 'ws-status on'; | |
| }; | |
| ws.onclose = () => { | |
| $('hdr-ws').textContent = 'disconnected'; | |
| $('hdr-ws').className = 'ws-status off'; | |
| setTimeout(connect, 2000); | |
| }; | |
| ws.onerror = () => {}; | |
| ws.onmessage = (e) => { | |
| let msg; | |
| try { msg = JSON.parse(e.data); } catch { return; } | |
| if (msg.type === 'reset') handleReset(msg); | |
| else if (msg.type === 'step') handleStep(msg); | |
| else if (msg.type === 'end') handleEnd(msg); | |
| else if (msg.type === 'shutdown') handleShutdown(msg); | |
| }; | |
| } | |
| connect(); | |
| </script> | |
| </body> | |
| </html> | |