Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Agent Trajectory 3D Visualizer</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| html, body { | |
| width: 100%; height: 100%; | |
| background: #0a0e1a; | |
| color: #e0e6f0; | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| overflow: hidden; | |
| } | |
| #three-canvas { | |
| position: fixed; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| display: block; | |
| } | |
| /* Header */ | |
| #header { | |
| position: fixed; | |
| top: 10px; left: 50%; | |
| transform: translateX(-50%); | |
| text-align: center; | |
| z-index: 20; | |
| pointer-events: none; | |
| } | |
| #header h1 { | |
| font-size: 14px; font-weight: 700; | |
| color: #7dd3fc; | |
| letter-spacing: 0.05em; | |
| text-shadow: 0 0 16px rgba(125,211,252,0.6); | |
| } | |
| /* Panel base */ | |
| .panel { | |
| position: fixed; | |
| background: rgba(10,14,26,0.88); | |
| border: 1px solid rgba(125,211,252,0.2); | |
| border-radius: 10px; | |
| padding: 10px 14px; | |
| font-size: 11px; | |
| z-index: 20; | |
| backdrop-filter: blur(6px); | |
| } | |
| .panel h3 { | |
| font-size: 10px; letter-spacing: 0.1em; | |
| color: #7dd3fc; margin-bottom: 8px; | |
| text-transform: uppercase; | |
| } | |
| /* Info panel */ | |
| #info-panel { top: 10px; left: 14px; min-width: 190px; } | |
| .info-row { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 4px; color: #94a3b8; } | |
| .info-val { color: #e0e6f0; font-weight: 600; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| /* Legend */ | |
| #legend { top: 10px; right: 14px; } | |
| .leg { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; } | |
| .leg-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; } | |
| .leg-line { width: 18px; height: 3px; border-radius: 2px; flex-shrink: 0; } | |
| /* Score ring */ | |
| #score-ring { position: fixed; bottom: 150px; left: 14px; z-index: 20; } | |
| /* Step log */ | |
| #step-log { | |
| position: fixed; bottom: 150px; right: 14px; | |
| width: 230px; max-height: 200px; overflow-y: auto; | |
| z-index: 20; | |
| } | |
| .log-e { display: flex; gap: 5px; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 10px; } | |
| .log-e:last-child { border-bottom: none; } | |
| .log-s { color: #475569; min-width: 24px; } | |
| .log-a { font-weight: 600; flex: 1; } | |
| .rp { color: #4ade80; } .rn { color: #f87171; } .rz { color: #94a3b8; } | |
| /* Timeline */ | |
| #timeline { | |
| position: fixed; bottom: 16px; left: 50%; | |
| transform: translateX(-50%); | |
| width: min(680px, 92vw); | |
| z-index: 20; | |
| } | |
| #tl-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } | |
| #tl-header h3 { font-size: 10px; color: #7dd3fc; letter-spacing: 0.1em; } | |
| #step-label { font-size: 11px; color: #f0abfc; font-weight: 700; } | |
| #slider { | |
| width: 100%; -webkit-appearance: none; appearance: none; height: 4px; | |
| background: linear-gradient(to right, #7dd3fc 0%, #7dd3fc var(--pct,0%), #1e293b var(--pct,0%)); | |
| border-radius: 4px; outline: none; cursor: pointer; | |
| } | |
| #slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; appearance: none; width: 15px; height: 15px; | |
| border-radius: 50%; background: #7dd3fc; cursor: pointer; | |
| box-shadow: 0 0 8px rgba(125,211,252,0.8); | |
| } | |
| #tl-btns { display: flex; gap: 7px; margin-top: 8px; justify-content: center; } | |
| .tb { | |
| background: rgba(125,211,252,0.1); | |
| border: 1px solid rgba(125,211,252,0.3); | |
| color: #7dd3fc; padding: 4px 12px; | |
| border-radius: 6px; cursor: pointer; font-size: 10px; | |
| transition: all 0.15s; | |
| } | |
| .tb:hover { background: rgba(125,211,252,0.25); } | |
| .tb.active { background: rgba(125,211,252,0.3); } | |
| /* Tooltip */ | |
| #tooltip { | |
| position: fixed; z-index: 30; | |
| background: rgba(10,14,26,0.95); | |
| border: 1px solid rgba(125,211,252,0.4); | |
| border-radius: 6px; padding: 7px 11px; | |
| font-size: 10px; pointer-events: none; | |
| opacity: 0; transition: opacity 0.1s; | |
| max-width: 180px; | |
| } | |
| #tt-title { color: #7dd3fc; margin-bottom: 3px; font-weight: 700; } | |
| /* Loader */ | |
| #loader { | |
| position: fixed; top: 50%; left: 50%; | |
| transform: translate(-50%,-50%); | |
| text-align: center; z-index: 50; color: #7dd3fc; font-size: 13px; | |
| } | |
| .spin { | |
| width: 36px; height: 36px; margin: 0 auto 10px; | |
| border: 3px solid rgba(125,211,252,0.15); | |
| border-top-color: #7dd3fc; | |
| border-radius: 50%; | |
| animation: sp 0.7s linear infinite; | |
| } | |
| @keyframes sp { to { transform: rotate(360deg); } } | |
| #no-data { | |
| position: fixed; top: 50%; left: 50%; | |
| transform: translate(-50%,-50%); | |
| text-align: center; color: #475569; font-size: 13px; | |
| display: none; | |
| } | |
| /* Node Labels Overlay */ | |
| #labels-container { position: fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index: 15; } | |
| .node-label { | |
| position: absolute; color: rgba(224,230,240,0.9); | |
| font-size: 10px; font-weight: 600; padding: 3px 7px; | |
| background: rgba(10,14,26,0.7); border: 1px solid rgba(125,211,252,0.25); | |
| border-radius: 4px; transform: translate(-50%, -200%); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.5); | |
| pointer-events: auto; white-space: nowrap; | |
| opacity: 0; transition: opacity 0.2s, box-shadow 0.2s; | |
| } | |
| .node-label:hover { box-shadow: 0 0 10px rgba(125,211,252,0.6); z-index: 100; } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="three-canvas"></canvas> | |
| <div id="labels-container"></div> | |
| <div id="loader"><div class="spin"></div><p>Loading 3D...</p></div> | |
| <div id="no-data"> | |
| <p style="font-size:28px;margin-bottom:12px">π</p> | |
| <p style="color:#7dd3fc;font-weight:700;margin-bottom:6px">No Episode Loaded</p> | |
| <p>Run an episode first, then click<br><strong style="color:#7dd3fc">Load Trajectory</strong></p> | |
| </div> | |
| <div id="header"><h1>π Agent Trajectory Visualizer β 3D</h1></div> | |
| <!-- Info panel --> | |
| <div class="panel" id="info-panel"> | |
| <h3>Episode Stats</h3> | |
| <div class="info-row"><span>Task</span><span class="info-val" id="st-task">β</span></div> | |
| <div class="info-row"><span>Variant</span><span class="info-val" id="st-var">β</span></div> | |
| <div class="info-row"><span>Steps</span><span class="info-val" id="st-steps">β</span></div> | |
| <div class="info-row"><span>Score</span><span class="info-val" id="st-score">β</span></div> | |
| <div class="info-row"><span>Strategy</span><span class="info-val" id="st-strat">β</span></div> | |
| </div> | |
| <!-- Legend --> | |
| <div class="panel" id="legend"> | |
| <h3>Legend</h3> | |
| <div class="leg"><div class="leg-dot" style="background:#f97316"></div><span>Source file</span></div> | |
| <div class="leg"><div class="leg-dot" style="background:#3b82f6"></div><span>Test file</span></div> | |
| <div class="leg"><div class="leg-dot" style="background:#a855f7"></div><span>Spec / Docs</span></div> | |
| <div class="leg"><div class="leg-dot" style="background:#22c55e"></div><span>Visited</span></div> | |
| <div class="leg"><div class="leg-dot" style="background:#ef4444"></div><span>Bug / Modified</span></div> | |
| <div class="leg"><div class="leg-line" style="background:#facc15"></div><span>Agent path</span></div> | |
| </div> | |
| <!-- Score ring --> | |
| <div id="score-ring"> | |
| <svg width="76" height="76" viewBox="0 0 76 76"> | |
| <circle cx="38" cy="38" r="30" fill="none" stroke="rgba(125,211,252,0.12)" stroke-width="6"/> | |
| <circle id="score-arc" cx="38" cy="38" r="30" fill="none" | |
| stroke="#7dd3fc" stroke-width="6" | |
| stroke-dasharray="0 188" | |
| stroke-linecap="round" | |
| transform="rotate(-90 38 38)" | |
| style="transition:stroke-dasharray 1.2s ease"/> | |
| <text id="score-txt" x="38" y="43" text-anchor="middle" | |
| fill="#e0e6f0" font-size="13" font-weight="700" | |
| font-family="'Segoe UI',sans-serif">0.0</text> | |
| </svg> | |
| </div> | |
| <!-- Step log --> | |
| <div class="panel" id="step-log"> | |
| <h3>Step Log</h3> | |
| <div id="log-list"></div> | |
| </div> | |
| <!-- Tooltip --> | |
| <div id="tooltip"><div id="tt-title"></div><div id="tt-body"></div></div> | |
| <!-- Timeline --> | |
| <div class="panel" id="timeline"> | |
| <div id="tl-header"> | |
| <h3>Timeline Replay</h3> | |
| <span id="step-label">Step 0 / 0</span> | |
| </div> | |
| <input type="range" id="slider" min="0" max="0" value="0" | |
| oninput="onSlider(this.value)"> | |
| <div id="tl-btns"> | |
| <button class="tb" onclick="stepBack()">β Back</button> | |
| <button class="tb" id="play-btn" onclick="togglePlay()">βΆ Play</button> | |
| <button class="tb" onclick="stepFwd()">Forward βΆ</button> | |
| <button class="tb" onclick="resetView()">βΊ Reset</button> | |
| <button class="tb" id="orbit-btn" onclick="toggleOrbit()">π Orbit</button> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // ββ Renderer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const canvas = document.getElementById('three-canvas'); | |
| const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setClearColor(0x0a0e1a, 1); | |
| function resize() { | |
| renderer.setSize(window.innerWidth, window.innerHeight, false); | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| } | |
| window.addEventListener('resize', resize); | |
| // ββ Scene + Camera βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(58, 1, 0.1, 1000); | |
| camera.position.set(0, 8, 24); | |
| camera.lookAt(0, 0, 0); | |
| resize(); | |
| // Lights | |
| scene.add(new THREE.AmbientLight(0x1a2040, 1.2)); | |
| const dl = new THREE.DirectionalLight(0x7dd3fc, 0.5); | |
| dl.position.set(5, 12, 5); | |
| scene.add(dl); | |
| // Grid | |
| const grid = new THREE.GridHelper(50, 25, 0x1e293b, 0x0f172a); | |
| grid.position.y = -3.5; | |
| scene.add(grid); | |
| // Stars | |
| (function() { | |
| const geo = new THREE.BufferGeometry(); | |
| const pos = new Float32Array(900 * 3); | |
| for (let i = 0; i < 900 * 3; i++) pos[i] = (Math.random() - 0.5) * 220; | |
| geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); | |
| scene.add(new THREE.Points(geo, new THREE.PointsMaterial({ color: 0x1e3a5f, size: 0.25 }))); | |
| })(); | |
| // ββ Orbit controls (manual) βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let sph = { theta: 0, phi: 1.1, r: 24 }; | |
| let orbitAuto = false, dragging = false, lastX = 0, lastY = 0; | |
| let lookTarget = new THREE.Vector3(0,0,0); | |
| canvas.addEventListener('mousedown', e => { dragging = true; lastX = e.clientX; lastY = e.clientY; }); | |
| window.addEventListener('mouseup', () => { dragging = false; }); | |
| window.addEventListener('mousemove', e => { | |
| if (dragging) { | |
| sph.theta -= (e.clientX - lastX) * 0.006; | |
| sph.phi = Math.max(0.15, Math.min(1.55, sph.phi - (e.clientY - lastY) * 0.006)); | |
| lastX = e.clientX; lastY = e.clientY; | |
| } else { checkHover(e.clientX, e.clientY); } | |
| }); | |
| canvas.addEventListener('wheel', e => { | |
| sph.r = Math.max(8, Math.min(55, sph.r + e.deltaY * 0.025)); | |
| }); | |
| function updateCamera() { | |
| if (orbitAuto) sph.theta += 0.004; | |
| const sin_p = Math.sin(sph.phi); | |
| camera.position.set( | |
| sph.r * sin_p * Math.sin(sph.theta) + lookTarget.x, | |
| sph.r * Math.cos(sph.phi) + (lookTarget.y * 0.3), | |
| sph.r * sin_p * Math.cos(sph.theta) + lookTarget.z | |
| ); | |
| camera.lookAt(lookTarget); | |
| } | |
| // ββ Scene state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const COLS = { src:0xf97316, test:0x3b82f6, spec:0xa855f7, visited:0x22c55e, bug:0xef4444, agent:0xfbbf24, path:0xfacc15, edge:0x334155 }; | |
| let nodeMap = {}; // filename β { mesh, basePos, labelEl } | |
| let pathLines = [], edgeLines = []; | |
| let agentMesh = null; | |
| let targetAgentPos = new THREE.Vector3(0, 3.5, 0); // For smooth lerp interpolation | |
| let vizData = null; | |
| let curStep = 0, maxStep = 0; | |
| let playing = false, playTimer = null; | |
| let frame = 0; | |
| // ββ Build scene βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function clearScene() { | |
| Object.values(nodeMap).forEach(o => { | |
| scene.remove(o.mesh); | |
| if (o.labelEl && o.labelEl.parentNode) o.labelEl.parentNode.removeChild(o.labelEl); | |
| }); | |
| pathLines.forEach(l => scene.remove(l)); | |
| edgeLines.forEach(l => scene.remove(l)); | |
| if (agentMesh) scene.remove(agentMesh); | |
| nodeMap = {}; pathLines = []; edgeLines = []; agentMesh = null; | |
| } | |
| function buildScene(data) { | |
| clearScene(); | |
| vizData = data; | |
| const files = data.files || []; | |
| const n = files.length; | |
| if (!n) return; | |
| // Layout: circle | |
| files.forEach((f, i) => { | |
| const angle = (i / n) * Math.PI * 2 - Math.PI / 2; | |
| const R = Math.max(5, n * 1.0); | |
| const x = Math.cos(angle) * R; | |
| const z = Math.sin(angle) * R; | |
| const pos = new THREE.Vector3(x, 0, z); | |
| const baseColor = f.is_bug_file ? COLS.bug : | |
| f.type === 'test' ? COLS.test : | |
| f.type === 'spec' ? COLS.spec : COLS.src; | |
| const col = new THREE.Color(baseColor); | |
| // Diverse Geometries | |
| let geo; | |
| if (f.type === 'test') { geo = new THREE.CylinderGeometry(0.5, 0.5, 0.8, 6); } // Hex prism | |
| else if (f.type === 'spec') { geo = new THREE.OctahedronGeometry(0.65); } // Diamond | |
| else { geo = new THREE.BoxGeometry(0.85, 0.85, 0.85); } // Code block Cube | |
| const mat = new THREE.MeshPhongMaterial({ | |
| color: col, emissive: col.clone().multiplyScalar(0.25), | |
| shininess: 90, transparent: true, opacity: 0.85, flatShading: true | |
| }); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| mesh.position.copy(pos); | |
| if (f.type !== 'src') { mesh.rotation.x = 0.2; mesh.rotation.z = Math.random() * 0.5; } | |
| mesh.userData = { file: f, basePos: pos.clone() }; | |
| scene.add(mesh); | |
| // Ring halo | |
| const rg = new THREE.RingGeometry(0.7, 0.82, 32); | |
| const rm = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0.2, side: THREE.DoubleSide }); | |
| const ring = new THREE.Mesh(rg, rm); | |
| ring.rotation.x = Math.PI / 2; | |
| mesh.add(ring); | |
| // HTML Label Overlay | |
| const labelEl = document.createElement('div'); | |
| labelEl.className = 'node-label'; | |
| labelEl.textContent = f.name; | |
| document.getElementById('labels-container').appendChild(labelEl); | |
| nodeMap[f.name] = { mesh, basePos: pos.clone(), labelEl }; | |
| }); | |
| // Dependency edges | |
| (data.dependencies || []).forEach(dep => { | |
| const a = nodeMap[dep.from], b = nodeMap[dep.to]; | |
| if (!a || !b) return; | |
| const geo = new THREE.BufferGeometry().setFromPoints([a.basePos.clone(), b.basePos.clone()]); | |
| const mat = new THREE.LineBasicMaterial({ color: COLS.edge, transparent: true, opacity: 0.35 }); | |
| const line = new THREE.Line(geo, mat); | |
| scene.add(line); | |
| edgeLines.push(line); | |
| }); | |
| // Agent sphere | |
| const ag = new THREE.SphereGeometry(0.32, 14, 14); | |
| const am = new THREE.MeshPhongMaterial({ color: COLS.agent, emissive: 0xfbbf24, emissiveIntensity: 0.9, shininess: 120 }); | |
| agentMesh = new THREE.Mesh(ag, am); | |
| agentMesh.position.set(0, 3, 0); | |
| scene.add(agentMesh); | |
| // Update UI stats | |
| document.getElementById('st-task').textContent = data.task || 'β'; | |
| document.getElementById('st-var').textContent = (data.variant_id || 'β').slice(0, 12); | |
| document.getElementById('st-steps').textContent = (data.steps || []).length; | |
| document.getElementById('st-strat').textContent = data.strategy || 'β'; | |
| updateScore(data.final_score || 0); | |
| maxStep = (data.steps || []).length; | |
| const sl = document.getElementById('slider'); | |
| sl.max = maxStep; sl.value = 0; | |
| curStep = 0; | |
| updateLabel(0, maxStep); | |
| applyStep(0); | |
| } | |
| // ββ Apply step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function applyStep(idx) { | |
| if (!vizData) return; | |
| const steps = vizData.steps || []; | |
| // Reset all nodes | |
| Object.values(nodeMap).forEach(({ mesh, basePos: _ }) => { | |
| const f = mesh.userData.file; | |
| const bc = f.is_bug_file ? COLS.bug : f.type === 'test' ? COLS.test : f.type === 'spec' ? COLS.spec : COLS.src; | |
| mesh.material.color.set(bc); | |
| mesh.material.emissive.set(new THREE.Color(bc).multiplyScalar(0.2)); | |
| mesh.material.opacity = 0.55; | |
| mesh.scale.setScalar(1); | |
| }); | |
| // Remove old path lines | |
| pathLines.forEach(l => scene.remove(l)); | |
| pathLines = []; | |
| // Collect positions for path | |
| const pathPts = []; | |
| const visited = new Set(), modified = new Set(); | |
| for (let i = 0; i < idx; i++) { | |
| const s = steps[i]; | |
| if (!s) continue; | |
| if (s.path && nodeMap[s.path]) { | |
| const p = nodeMap[s.path].basePos.clone().add(new THREE.Vector3(0, 0.15, 0)); | |
| pathPts.push(p); | |
| } | |
| if (s.action === 'read_file' && s.path) visited.add(s.path); | |
| if (s.action === 'write_file' && s.path) modified.add(s.path); | |
| } | |
| // Color visited/modified | |
| visited.forEach(name => { | |
| if (nodeMap[name]) { | |
| nodeMap[name].mesh.material.color.set(COLS.visited); | |
| nodeMap[name].mesh.material.emissive.set(new THREE.Color(COLS.visited).multiplyScalar(0.4)); | |
| nodeMap[name].mesh.material.opacity = 1; | |
| nodeMap[name].mesh.scale.setScalar(1.25); | |
| } | |
| }); | |
| modified.forEach(name => { | |
| if (nodeMap[name]) { | |
| nodeMap[name].mesh.material.color.set(COLS.bug); | |
| nodeMap[name].mesh.material.emissive.set(new THREE.Color(COLS.bug).multiplyScalar(0.5)); | |
| nodeMap[name].mesh.material.opacity = 1; | |
| nodeMap[name].mesh.scale.setScalar(1.45); | |
| } | |
| }); | |
| // Highlight current node | |
| if (idx > 0 && idx <= steps.length) { | |
| const cur = steps[idx - 1]; | |
| if (cur && cur.path && nodeMap[cur.path]) { | |
| nodeMap[cur.path].mesh.scale.setScalar(1.65); | |
| } | |
| } | |
| // Draw precision tube path | |
| if (pathPts.length >= 2) { | |
| // Add an exact curve mapping | |
| const curve = new THREE.CatmullRomCurve3(pathPts); | |
| const geo = new THREE.TubeGeometry(curve, pathPts.length * 8, 0.045, 8, false); | |
| const mat = new THREE.MeshPhongMaterial({ | |
| color: COLS.path, emissive: COLS.path, emissiveIntensity: 0.6, | |
| transparent: true, opacity: 0.8 | |
| }); | |
| const line = new THREE.Mesh(geo, mat); | |
| scene.add(line); pathLines.push(line); | |
| } | |
| // Move agent target (actual animation uses lerp in animate loop) | |
| if (idx > 0 && idx <= steps.length) { | |
| const cur = steps[idx - 1]; | |
| if (cur && cur.path && nodeMap[cur.path]) { | |
| const tp = nodeMap[cur.path].basePos; | |
| targetAgentPos.set(tp.x, tp.y + 1.6, tp.z); | |
| } else { | |
| targetAgentPos.set(0, 2.5, 0); | |
| } | |
| } else { | |
| targetAgentPos.set(0, 3.5, 0); | |
| } | |
| updateLog(steps, idx - 1); | |
| updateLabel(idx, maxStep); | |
| const sl = document.getElementById('slider'); | |
| sl.style.setProperty('--pct', (maxStep > 0 ? idx / maxStep * 100 : 0) + '%'); | |
| } | |
| // ββ Score ring βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateScore(s) { | |
| const c = 2 * Math.PI * 30; | |
| const arc = c * Math.min(1, Math.max(0, s)); | |
| document.getElementById('score-arc').setAttribute('stroke-dasharray', `${arc} ${c}`); | |
| document.getElementById('score-txt').textContent = s.toFixed(2); | |
| document.getElementById('st-score').textContent = s.toFixed(3); | |
| const col = s >= 0.7 ? '#4ade80' : s >= 0.4 ? '#fbbf24' : '#f87171'; | |
| document.getElementById('score-arc').setAttribute('stroke', col); | |
| } | |
| // ββ Step log βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateLog(steps, curIdx) { | |
| const em = { read_file:'π', write_file:'βοΈ', run_tests:'π§ͺ', search_code:'π', submit:'π' }; | |
| const container = document.getElementById('log-list'); | |
| container.innerHTML = ''; | |
| steps.forEach((s, i) => { | |
| const e = document.createElement('div'); | |
| e.className = 'log-e'; | |
| e.style.opacity = i < curIdx ? '0.55' : i === curIdx ? '1' : '0.3'; | |
| if (i === curIdx) e.style.background = 'rgba(125,211,252,0.07)'; | |
| const r = s.reward || 0; | |
| const rc = r > 0 ? 'rp' : r < 0 ? 'rn' : 'rz'; | |
| const name = (s.path || s.action || '').split('/').pop() || s.action; | |
| e.innerHTML = `<span class="log-s">S${s.step}</span><span class="log-a">${em[s.action]||'β’'} ${name}</span><span class="${rc}">${r>0?'+':''}${r.toFixed(2)}</span>`; | |
| container.appendChild(e); | |
| }); | |
| if (curIdx >= 0 && container.children[curIdx]) { | |
| container.children[curIdx].scrollIntoView({ block: 'nearest' }); | |
| } | |
| } | |
| // ββ Hover tooltip ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ray = new THREE.Raycaster(); | |
| const mv = new THREE.Vector2(); | |
| const tt = document.getElementById('tooltip'); | |
| function checkHover(mx, my) { | |
| mv.x = (mx / window.innerWidth) * 2 - 1; | |
| mv.y = -(my / window.innerHeight) * 2 + 1; | |
| ray.setFromCamera(mv, camera); | |
| const meshes = Object.values(nodeMap).map(o => o.mesh); | |
| const hits = ray.intersectObjects(meshes); | |
| if (hits.length) { | |
| const f = hits[0].object.userData.file; | |
| tt.style.opacity = '1'; | |
| tt.style.left = (mx + 12) + 'px'; | |
| tt.style.top = (my - 8) + 'px'; | |
| document.getElementById('tt-title').textContent = f.name; | |
| document.getElementById('tt-body').innerHTML = | |
| `Type: ${f.type}${f.is_bug_file ? '<br>β οΈ Bug location' : ''}${f.visited ? '<br>β Visited' : ''}`; | |
| } else { | |
| tt.style.opacity = '0'; | |
| } | |
| } | |
| // ββ Controls βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function onSlider(v) { curStep = +v; applyStep(curStep); } | |
| function stepFwd() { if (curStep < maxStep) { curStep++; document.getElementById('slider').value = curStep; applyStep(curStep); } } | |
| function stepBack() { if (curStep > 0) { curStep--; document.getElementById('slider').value = curStep; applyStep(curStep); } } | |
| function togglePlay() { | |
| playing = !playing; | |
| document.getElementById('play-btn').textContent = playing ? 'βΈ Pause' : 'βΆ Play'; | |
| if (playing) { | |
| if (curStep >= maxStep) curStep = 0; | |
| playTimer = setInterval(() => { | |
| if (curStep >= maxStep) { playing = false; document.getElementById('play-btn').textContent = 'βΆ Play'; clearInterval(playTimer); return; } | |
| stepFwd(); | |
| }, 850); | |
| } else { | |
| clearInterval(playTimer); | |
| } | |
| } | |
| function toggleOrbit() { | |
| orbitAuto = !orbitAuto; | |
| const btn = document.getElementById('orbit-btn'); | |
| btn.textContent = orbitAuto ? 'βΉ Stop' : 'π Orbit'; | |
| btn.classList.toggle('active', orbitAuto); | |
| } | |
| function resetView() { | |
| sph = { theta: 0, phi: 1.1, r: 24 }; | |
| curStep = 0; | |
| document.getElementById('slider').value = 0; | |
| applyStep(0); | |
| } | |
| function updateLabel(s, m) { document.getElementById('step-label').textContent = `Step ${s} / ${m}`; } | |
| // ββ Animation loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| frame++; | |
| // Dynamic camera focal tracking | |
| lookTarget.lerp(targetAgentPos, 0.03); | |
| updateCamera(); | |
| // Pulsing & moving agent (seamless flow) | |
| if (agentMesh) { | |
| agentMesh.position.lerp(targetAgentPos, 0.08); // Smooth transition | |
| const p = 1 + Math.sin(frame * 0.09) * 0.18; | |
| agentMesh.scale.setScalar(p); | |
| agentMesh.rotation.y += 0.04; | |
| } | |
| // Subtle node float & Update HTML overaly positions | |
| Object.values(nodeMap).forEach(({ mesh, basePos, labelEl }, i) => { | |
| mesh.position.y = basePos.y + Math.sin(frame * 0.018 + i * 1.1) * 0.12; | |
| // Project 3D vector to 2D screen space | |
| if (labelEl) { | |
| const pos = mesh.position.clone(); | |
| pos.project(camera); | |
| const x = (pos.x * 0.5 + 0.5) * window.innerWidth; | |
| const y = (pos.y * -0.5 + 0.5) * window.innerHeight; | |
| // Hide if behind camera or very close to edges | |
| if (pos.z > 0.99 || Math.abs(pos.x) > 1.2 || Math.abs(pos.y) > 1.2) { | |
| labelEl.style.opacity = '0'; | |
| } else { | |
| labelEl.style.left = `${x}px`; | |
| labelEl.style.top = `${y}px`; | |
| labelEl.style.opacity = '1'; | |
| } | |
| } | |
| }); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| // ββ Load data from API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function fetchAndLoad() { | |
| document.getElementById('loader').style.display = 'block'; | |
| document.getElementById('no-data').style.display = 'none'; | |
| try { | |
| // Try to determine base URL from window location | |
| const base = window.location.origin; | |
| const res = await fetch(`${base}/viz-data`, { cache: 'no-store' }); | |
| if (!res.ok) throw new Error('no data'); | |
| const data = await res.json(); | |
| if (data.error || !data.files || data.files.length === 0) { | |
| document.getElementById('loader').style.display = 'none'; | |
| document.getElementById('no-data').style.display = 'block'; | |
| return; | |
| } | |
| buildScene(data); | |
| document.getElementById('loader').style.display = 'none'; | |
| } catch(e) { | |
| document.getElementById('loader').style.display = 'none'; | |
| document.getElementById('no-data').style.display = 'block'; | |
| } | |
| } | |
| // ββ Public API (can be called from parent window) βββββββββββββββββββββββββββββ | |
| window.loadData = function(data) { | |
| if (typeof data === 'string') { try { data = JSON.parse(data); } catch(e) { return; } } | |
| buildScene(data); | |
| document.getElementById('loader').style.display = 'none'; | |
| document.getElementById('no-data').style.display = 'none'; | |
| }; | |
| // Auto-load on init | |
| window.addEventListener('load', fetchAndLoad); | |
| </script> | |
| </body> | |
| </html> | |