Spaces:
Sleeping
Sleeping
Download server/static/app.js from ArshPathan/material_forge_env: direct link, hf CLI and curl.
- Browser
- Download file 27.5 kB
-
https://huggingface.co/spaces/ArshPathan/material_forge_env/resolve/main/server/static/app.js
- Command line
-
hf download hf://spaces/ArshPathan/material_forge_env/server/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/ArshPathan/material_forge_env/resolve/main/server/static/app.js
27.5 kB
| /** | |
| * MaterialForge β Crystal Structure Design Lab | |
| * Modular Frontend Application | |
| */ | |
| class MaterialForgeApp { | |
| constructor() { | |
| this.API_BASE = window.location.origin; | |
| this.WS_URL = this.API_BASE.replace(/^http/i, 'ws') + '/ws'; | |
| this.ws = null; | |
| this.pendingRequest = null; | |
| this.connectingPromise = null; | |
| this.state = { | |
| rewardHistory: [], | |
| bestReward: 0, | |
| currentObs: null, | |
| selectedAtom: 'A', | |
| selectedAction: 'place', | |
| selectedCell: { r: 0, c: 0 }, | |
| hoverPos: { r: -1, c: -1 } | |
| }; | |
| // Configuration | |
| this.ATOM_COLORS = { A: '#e74c3c', B: '#3b82f6', C: '#f59e0b', P: '#10b981', '.': '#111827' }; | |
| this.ATOM_GLOW = { A: '#e74c3c', B: '#3b82f6', C: '#f59e0b', P: '#10b981' }; | |
| this.CELL_SIZE = 38; | |
| this.GRID_SIZE = 8; | |
| this.PAD = 4; | |
| this.init(); | |
| } | |
| /* ββ Initialization ββ */ | |
| async init() { | |
| this.cacheElements(); | |
| this.bindEvents(); | |
| this.setupCharts(); | |
| this.drawLattice(null); | |
| await this.checkHealth(); | |
| this.setLoading(false); | |
| this.log('Laboratory Protocol Initialized', 'ok'); | |
| // Health telemetry polling | |
| setInterval(() => this.checkHealth(), 30000); | |
| } | |
| cacheElements() { | |
| this.els = { | |
| loading: document.getElementById('loading-overlay'), | |
| statusDot: document.getElementById('status-dot'), | |
| statusText: document.getElementById('status-text'), | |
| apiBase: document.getElementById('api-base'), | |
| toast: document.getElementById('toast'), | |
| logBox: document.getElementById('log-box'), | |
| canvas: document.getElementById('lattice-canvas'), | |
| ctx: document.getElementById('lattice-canvas').getContext('2d'), | |
| // KPIs | |
| kpi: { | |
| step: document.getElementById('kpi-step'), | |
| reward: document.getElementById('kpi-reward'), | |
| best: document.getElementById('kpi-best'), | |
| orderIndex: document.getElementById('kpi-quality'), | |
| cost: document.getElementById('kpi-cost'), | |
| atoms: document.getElementById('kpi-atoms'), | |
| atomCount: document.getElementById('lat-atom-count'), | |
| episodeId: document.getElementById('episode-id') | |
| }, | |
| // Controls | |
| difficulty: document.getElementById('sel-difficulty'), | |
| scenario: document.getElementById('sel-scenario'), | |
| selectedCell: document.getElementById('selected-cell'), | |
| hoverCell: document.getElementById('hover-cell'), | |
| selectedAtomDisplay: document.getElementById('selected-atom-display'), | |
| quickActionDisplay: document.getElementById('quick-action-display'), | |
| btnReset: document.getElementById('btn-reset'), | |
| btnStep: document.getElementById('btn-step'), | |
| atomSection: document.getElementById('atom-section') | |
| }; | |
| // Resize canvas | |
| this.els.canvas.width = this.GRID_SIZE * this.CELL_SIZE + this.PAD * 2; | |
| this.els.canvas.height = this.GRID_SIZE * this.CELL_SIZE + this.PAD * 2; | |
| } | |
| bindEvents() { | |
| this.els.btnReset.onclick = () => this.doReset(); | |
| this.els.btnStep.onclick = () => this.doStepFromSelection(); | |
| // Canvas interaction | |
| this.els.canvas.addEventListener('click', (e) => this.handleCanvasClick(e)); | |
| this.els.canvas.addEventListener('contextmenu', (e) => this.handleCanvasRightClick(e)); | |
| this.els.canvas.addEventListener('mousemove', (e) => this.handleCanvasHover(e)); | |
| this.els.canvas.addEventListener('mouseleave', () => { | |
| this.state.hoverPos = { r: -1, c: -1 }; | |
| this.els.hoverCell.textContent = '-'; | |
| this.drawLattice(this.state.currentObs ? this.state.currentObs.grid : null); | |
| }); | |
| // Keyboard | |
| document.addEventListener('keydown', (e) => { | |
| if (['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.tagName)) return; | |
| if (e.key === 'Enter') this.doStepFromSelection(); | |
| if (e.key.toLowerCase() === 'r') this.doReset(); | |
| }); | |
| } | |
| /* ββ API Interaction ββ */ | |
| async ensureSocket() { | |
| if (this.ws && this.ws.readyState === WebSocket.OPEN) { | |
| return this.ws; | |
| } | |
| if (this.connectingPromise) { | |
| return this.connectingPromise; | |
| } | |
| this.connectingPromise = new Promise((resolve, reject) => { | |
| const socket = new WebSocket(this.WS_URL); | |
| socket.addEventListener('open', () => { | |
| this.ws = socket; | |
| this.connectingPromise = null; | |
| this.log('Persistent lab session established', 'ok'); | |
| resolve(socket); | |
| }, { once: true }); | |
| socket.addEventListener('message', (event) => this.handleSocketMessage(event)); | |
| socket.addEventListener('close', () => { | |
| this.ws = null; | |
| this.connectingPromise = null; | |
| if (this.pendingRequest) { | |
| this.pendingRequest.reject(new Error('Lab session closed unexpectedly')); | |
| this.pendingRequest = null; | |
| } | |
| }); | |
| socket.addEventListener('error', () => { | |
| if (this.connectingPromise) { | |
| this.connectingPromise = null; | |
| } | |
| reject(new Error('Unable to connect to the lab session')); | |
| }, { once: true }); | |
| }); | |
| return this.connectingPromise; | |
| } | |
| handleSocketMessage(event) { | |
| let payload; | |
| try { | |
| payload = JSON.parse(event.data); | |
| } catch { | |
| if (this.pendingRequest) { | |
| this.pendingRequest.reject(new Error('Received invalid server response')); | |
| this.pendingRequest = null; | |
| } | |
| return; | |
| } | |
| if (!this.pendingRequest) return; | |
| const request = this.pendingRequest; | |
| this.pendingRequest = null; | |
| if (payload.type === 'error') { | |
| request.reject(new Error(payload.data?.message || 'Unknown server error')); | |
| return; | |
| } | |
| request.resolve(payload.data); | |
| } | |
| normalizeObservationResponse(payload) { | |
| const obs = payload?.observation ?? payload ?? {}; | |
| const grid = Array.isArray(obs.grid) ? obs.grid : (Array.isArray(payload?.grid) ? payload.grid : []); | |
| return { | |
| ...obs, | |
| grid, | |
| current_properties: obs.current_properties ?? {}, | |
| target: obs.target ?? {}, | |
| score_breakdown: obs.score_breakdown ?? {}, | |
| metadata: obs.metadata ?? {}, | |
| phase: obs.phase ?? 'amorphous', | |
| step_number: obs.step_number ?? 0, | |
| max_steps: obs.max_steps ?? 50, | |
| total_cost: obs.total_cost ?? 0, | |
| cost_budget: obs.cost_budget ?? 0, | |
| reward: payload?.reward ?? obs.reward ?? 0, | |
| done: payload?.done ?? obs.done ?? false | |
| }; | |
| } | |
| async wsRequest(message) { | |
| const socket = await this.ensureSocket(); | |
| if (this.pendingRequest) { | |
| throw new Error('Another environment request is still in progress'); | |
| } | |
| return new Promise((resolve, reject) => { | |
| this.pendingRequest = { resolve, reject }; | |
| try { | |
| socket.send(JSON.stringify(message)); | |
| } catch (error) { | |
| this.pendingRequest = null; | |
| reject(error); | |
| } | |
| }); | |
| } | |
| async checkHealth() { | |
| try { | |
| this.els.apiBase.textContent = window.location.host; | |
| const h = await fetch(this.API_BASE + '/health').then(r => r.json()); | |
| this.els.statusDot.classList.remove('offline'); | |
| this.els.statusText.textContent = (h.status === 'ok' || h.status === 'healthy') ? 'API Online' : 'Warning'; | |
| return true; | |
| } catch { | |
| this.els.statusDot.classList.add('offline'); | |
| this.els.statusText.textContent = 'API Offline'; | |
| return false; | |
| } | |
| } | |
| /* ββ Business Logic ββ */ | |
| async doReset() { | |
| this.setLoading(true); | |
| try { | |
| const payload = { | |
| difficulty: this.els.difficulty.value, | |
| scenario_name: this.els.scenario.value || undefined | |
| }; | |
| const response = await this.wsRequest({ type: 'reset', data: payload }); | |
| const obs = this.normalizeObservationResponse(response); | |
| this.state.rewardHistory = []; | |
| this.state.bestReward = 0; | |
| this.resetCharts(); | |
| this.updateUI(obs, 0); | |
| this.log(`Protocol Reset β Diff: ${payload.difficulty} | Scenario: ${payload.scenario_name || 'Random'}`, 'ok'); | |
| this.toast('Laboratory environment re-initialized', 'success'); | |
| } catch (e) { | |
| this.log(`Initialization Failure: ${e.message}`, 'err'); | |
| this.toast(`Reset failed: ${e.message}`, 'error'); | |
| } finally { | |
| this.setLoading(false); | |
| } | |
| } | |
| async doStepFromSelection() { | |
| if (!this.state.currentObs) return this.toast('Reset the environment first', 'error'); | |
| const { r, c } = this.state.selectedCell; | |
| const cell = this.state.currentObs.grid?.[r]?.[c] ?? '.'; | |
| if (cell !== '.') { | |
| return this.toast('Cell is occupied. Right click to remove first.', 'error'); | |
| } | |
| await this.executeAction({ | |
| action_type: 'place', | |
| row: r, | |
| col: c, | |
| atom: this.state.selectedAtom | |
| }); | |
| } | |
| async executeAction(action) { | |
| if (!this.state.currentObs) return; | |
| this.els.btnStep.disabled = true; | |
| const row = action.row; | |
| const col = action.col; | |
| try { | |
| const response = await this.wsRequest({ type: 'step', data: action }); | |
| const obs = this.normalizeObservationResponse(response); | |
| const reward = obs.reward ?? 0; | |
| const done = obs.done ?? false; | |
| this.state.rewardHistory.push(reward); | |
| this.state.bestReward = Math.max(this.state.bestReward, reward); | |
| this.updateCharts(reward); | |
| this.updateUI(obs, reward); | |
| const actionStr = `${action.action_type.toUpperCase()} [${row},${col}] ${action.atom ? 'Species: '+action.atom : ''}`; | |
| this.log(`Step ${obs.step_number}: ${actionStr} β Rwd: ${reward.toFixed(4)}${done ? ' [COMPLETE]' : ''}`, done ? 'ok' : ''); | |
| if (done) this.toast(`Episode complete. Optimal reward: ${this.state.bestReward.toFixed(4)}`, 'success'); | |
| } catch (e) { | |
| this.log(`Step error: ${e.message}`, 'err'); | |
| this.toast(`Step failed: ${e.message}`, 'error'); | |
| } finally { | |
| this.els.btnStep.disabled = false; | |
| } | |
| } | |
| /* ββ UI Rendering ββ */ | |
| updateUI(obs, reward) { | |
| this.state.currentObs = obs; | |
| const grid = Array.isArray(obs.grid) ? obs.grid : []; | |
| this.drawLattice(grid); | |
| // KPIs | |
| this.els.kpi.step.textContent = `${obs.step_number} / ${obs.max_steps}`; | |
| this.els.kpi.reward.textContent = (reward ?? 0).toFixed(4); | |
| this.els.kpi.best.textContent = this.state.bestReward.toFixed(4); | |
| this.els.kpi.cost.textContent = `${Math.round(obs.total_cost)} / ${Math.round(obs.cost_budget)}`; | |
| const atomCount = grid.flat().filter(c => c !== '.').length; | |
| this.els.kpi.atoms.textContent = atomCount; | |
| this.els.kpi.atomCount.textContent = `${atomCount} atoms`; | |
| this.els.selectedAtomDisplay.textContent = this.state.selectedAtom; | |
| this.els.selectedCell.textContent = `${this.state.selectedCell.r}, ${this.state.selectedCell.c}`; | |
| this.els.quickActionDisplay.textContent = 'PLACE'; | |
| const orderIdx = obs.score_breakdown?.lattice_order_index ?? 0; | |
| this.els.kpi.orderIndex.textContent = orderIdx.toFixed(3); | |
| this.els.kpi.orderIndex.className = 'kpi-value phase-' + (obs.phase || 'amorphous'); | |
| // Property bars | |
| ['hardness', 'conductivity', 'thermal_resistance', 'elasticity'].forEach(p => { | |
| const curr = obs.current_properties[p] ?? 0; | |
| const tgt = obs.target[p] ?? 0; | |
| document.getElementById(`pv-${p}`).textContent = `${curr.toFixed(0)} / ${tgt.toFixed(0)}`; | |
| document.getElementById(`pb-${p}`).style.width = Math.min(curr, 100) + '%'; | |
| document.getElementById(`pt-${p}`).style.left = Math.min(tgt, 100) + '%'; | |
| }); | |
| const sb = obs.score_breakdown || {}; | |
| document.getElementById('sc-stability').textContent = (sb.structural_stability ?? 0).toFixed(3); | |
| document.getElementById('sc-quality').textContent = (sb.lattice_order_index ?? 0).toFixed(3); | |
| document.getElementById('sb-stability').style.width = ((sb.structural_stability ?? 0) * 100) + '%'; | |
| document.getElementById('sb-quality').style.width = ((sb.lattice_order_index ?? 0) * 100) + '%'; | |
| if (obs.metadata && this.els.kpi.episodeId) { | |
| this.els.kpi.episodeId.textContent = `scenario: ${obs.metadata.scenario_name || 'random'} Β· diff: ${obs.metadata.difficulty || 'β'}`; | |
| } | |
| } | |
| drawLattice(grid) { | |
| const { ctx, canvas } = this.els; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.fillStyle = '#080c14'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| for (let r = 0; r < this.GRID_SIZE; r++) { | |
| for (let c = 0; c < this.GRID_SIZE; c++) { | |
| const cell = (grid && grid[r] && grid[r][c]) || '.'; | |
| const x = this.PAD + c * this.CELL_SIZE; | |
| const y = this.PAD + r * this.CELL_SIZE; | |
| const color = this.ATOM_COLORS[cell] || this.ATOM_COLORS['.']; | |
| ctx.fillStyle = color; | |
| ctx.globalAlpha = cell === '.' ? 0.25 : 0.9; | |
| this.roundRect(ctx, x + 1, y + 1, this.CELL_SIZE - 2, this.CELL_SIZE - 2, 6); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| if (cell !== '.') { | |
| ctx.shadowBlur = 14; | |
| ctx.shadowColor = this.ATOM_GLOW[cell]; | |
| ctx.font = 'bold 16px "JetBrains Mono", monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillStyle = '#fff'; | |
| ctx.fillText(cell, x + this.CELL_SIZE / 2, y + this.CELL_SIZE / 2); | |
| ctx.shadowBlur = 0; | |
| } | |
| ctx.strokeStyle = 'rgba(30,45,69,0.5)'; | |
| ctx.lineWidth = 0.5; | |
| ctx.strokeRect(x + 0.5, y + 0.5, this.CELL_SIZE - 1, this.CELL_SIZE - 1); | |
| } | |
| } | |
| this.drawHover(); | |
| } | |
| drawHover() { | |
| const { r, c } = this.state.hoverPos; | |
| if (r < 0 || c < 0) return; | |
| const x = this.PAD + c * this.CELL_SIZE; | |
| const y = this.PAD + r * this.CELL_SIZE; | |
| this.els.ctx.strokeStyle = 'rgba(0,212,170,0.8)'; | |
| this.els.ctx.lineWidth = 2; | |
| this.roundRect(this.els.ctx, x + 2, y + 2, this.CELL_SIZE - 4, this.CELL_SIZE - 4, 5); | |
| this.els.ctx.stroke(); | |
| } | |
| /* ββ Interaction Handlers ββ */ | |
| handleCanvasClick(e) { | |
| const coords = this.getCanvasCoords(e); | |
| if (coords) { | |
| this.setSelectedCell(coords); | |
| if (!this.state.currentObs) return; | |
| const currentCell = this.state.currentObs.grid?.[coords.r]?.[coords.c] ?? '.'; | |
| if (currentCell !== '.') { | |
| this.toast('Cell is occupied. Right click to remove first.', 'error'); | |
| return; | |
| } | |
| this.executeAction({ | |
| action_type: 'place', | |
| row: coords.r, | |
| col: coords.c, | |
| atom: this.state.selectedAtom | |
| }); | |
| } | |
| } | |
| handleCanvasRightClick(e) { | |
| e.preventDefault(); | |
| const coords = this.getCanvasCoords(e); | |
| if (coords) { | |
| this.setSelectedCell(coords); | |
| if (!this.state.currentObs) return; | |
| const currentCell = this.state.currentObs.grid?.[coords.r]?.[coords.c] ?? '.'; | |
| if (currentCell === '.') { | |
| this.toast('Cell is already empty', 'error'); | |
| return; | |
| } | |
| this.els.quickActionDisplay.textContent = 'REMOVE'; | |
| this.executeAction({ | |
| action_type: 'remove', | |
| row: coords.r, | |
| col: coords.c | |
| }); | |
| } | |
| } | |
| handleCanvasHover(e) { | |
| const coords = this.getCanvasCoords(e); | |
| if (coords && (coords.r !== this.state.hoverPos.r || coords.c !== this.state.hoverPos.c)) { | |
| this.state.hoverPos = coords; | |
| this.els.hoverCell.textContent = `${coords.r}, ${coords.c}`; | |
| if (this.state.currentObs) { | |
| const currentCell = this.state.currentObs.grid?.[coords.r]?.[coords.c] ?? '.'; | |
| this.els.quickActionDisplay.textContent = currentCell === '.' ? 'PLACE' : 'REMOVE ONLY'; | |
| } | |
| this.drawLattice(this.state.currentObs ? this.state.currentObs.grid : null); | |
| } | |
| } | |
| setSelectedCell(coords) { | |
| this.state.selectedCell = coords; | |
| this.els.selectedCell.textContent = `${coords.r}, ${coords.c}`; | |
| this.drawLattice(this.state.currentObs ? this.state.currentObs.grid : null); | |
| } | |
| getCanvasCoords(e) { | |
| const rect = this.els.canvas.getBoundingClientRect(); | |
| const scaleX = this.els.canvas.width / rect.width; | |
| const scaleY = this.els.canvas.height / rect.height; | |
| const col = Math.floor(((e.clientX - rect.left) * scaleX - this.PAD) / this.CELL_SIZE); | |
| const row = Math.floor(((e.clientY - rect.top) * scaleY - this.PAD) / this.CELL_SIZE); | |
| return (row >= 0 && row < 8 && col >= 0 && col < 8) ? { r: row, c: col } : null; | |
| } | |
| /* ββ Charts ββ */ | |
| setupCharts() { | |
| Chart.defaults.color = '#94a3b8'; | |
| Chart.defaults.font.family = 'Inter, system-ui, sans-serif'; | |
| Chart.defaults.plugins.legend.labels.usePointStyle = true; | |
| Chart.defaults.plugins.legend.labels.boxWidth = 8; | |
| Chart.defaults.plugins.legend.labels.boxHeight = 8; | |
| const rewardChartCtx = document.getElementById('reward-chart').getContext('2d'); | |
| const rewardGradient = rewardChartCtx.createLinearGradient(0, 0, 0, 180); | |
| rewardGradient.addColorStop(0, 'rgba(0,212,170,0.30)'); | |
| rewardGradient.addColorStop(1, 'rgba(0,212,170,0.02)'); | |
| this.rewardChart = new Chart(rewardChartCtx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| data: [], | |
| borderColor: '#00d4aa', | |
| backgroundColor: rewardGradient, | |
| fill: true, | |
| tension: 0.35, | |
| pointRadius: 0, | |
| pointHoverRadius: 4, | |
| borderWidth: 2.5 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { intersect: false, mode: 'index' }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: '#0f172a', | |
| borderColor: 'rgba(148,163,184,0.18)', | |
| borderWidth: 1, | |
| displayColors: false, | |
| callbacks: { | |
| label: (ctx) => `Reward ${Number(ctx.parsed.y).toFixed(3)}` | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { grid: { display: false }, ticks: { font: { size: 10 } } }, | |
| y: { | |
| min: -0.05, | |
| max: 1.05, | |
| grid: { color: 'rgba(30,45,69,0.3)' }, | |
| ticks: { | |
| font: { size: 10 }, | |
| callback: (value) => Number(value).toFixed(1) | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| updateCharts(reward) { | |
| this.rewardChart.data.labels.push(this.state.rewardHistory.length); | |
| this.rewardChart.data.datasets[0].data.push(reward); | |
| this.rewardChart.update(); | |
| } | |
| resetCharts() { | |
| this.rewardChart.data.labels = []; | |
| this.rewardChart.data.datasets[0].data = []; | |
| this.rewardChart.update(); | |
| } | |
| /* ββ Utilities ββ */ | |
| setLoading(on) { | |
| this.els.loading.classList.toggle('hidden', !on); | |
| this.els.btnReset.disabled = on; | |
| this.els.btnStep.disabled = on; | |
| } | |
| log(msg, type = '') { | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry'; | |
| entry.innerHTML = `<span class="log-ts">${new Date().toLocaleTimeString()}</span><span class="log-${type}">${msg}</span>`; | |
| this.els.logBox.appendChild(entry); | |
| this.els.logBox.scrollTop = this.els.logBox.scrollHeight; | |
| } | |
| toast(msg, type = '') { | |
| this.els.toast.textContent = msg; | |
| this.els.toast.className = `toast show ${type}`; | |
| clearTimeout(this.toastTimeout); | |
| this.toastTimeout = setTimeout(() => this.els.toast.className = 'toast', 3000); | |
| } | |
| roundRect(ctx, x, y, w, h, r) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + r, y); | |
| ctx.lineTo(x + w - r, y); | |
| ctx.quadraticCurveTo(x + w, y, x + w, y + r); | |
| ctx.lineTo(x + w, y + h - r); | |
| ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); | |
| ctx.lineTo(x + r, y + h); | |
| ctx.quadraticCurveTo(x, y + h, x, y + h - r); | |
| ctx.lineTo(x, y + r); | |
| ctx.quadraticCurveTo(x, y, x + r, y); | |
| ctx.closePath(); | |
| } | |
| selectAtom(atom) { | |
| this.state.selectedAtom = atom; | |
| this.els.selectedAtomDisplay.textContent = atom; | |
| this.els.quickActionDisplay.textContent = 'PLACE'; | |
| document.querySelectorAll('.atom-btn').forEach(b => b.classList.toggle('selected', b.dataset.atom === atom)); | |
| } | |
| } | |
| // Analytics Logic | |
| async function loadBenchmarks() { | |
| const status = document.getElementById('benchmark-status'); | |
| try { | |
| const res = await fetch('/playground/benchmarks.json'); | |
| if (!res.ok) throw new Error('Benchmark data not found. Please run agent_benchmark.py'); | |
| const data = await res.json(); | |
| if (!Array.isArray(data) || data.length === 0) { | |
| throw new Error('Benchmark dataset is empty'); | |
| } | |
| const average = (values) => values.reduce((sum, value) => sum + value, 0) / (values.length || 1); | |
| const avgBest = average(data.map((run) => run.best_reward)); | |
| const avgFinal = average(data.map((run) => run.final_reward)); | |
| const overruns = data.filter((run) => run.total_cost > run.cost_budget).length; | |
| const bestRun = data.reduce((best, run) => (run.best_reward > best.best_reward ? run : best), data[0]); | |
| const topPhase = Object.entries(data.reduce((acc, run) => { | |
| acc[run.phase] = (acc[run.phase] || 0) + 1; | |
| return acc; | |
| }, {})).sort((a, b) => b[1] - a[1])[0]; | |
| const setText = (id, text) => { | |
| const el = document.getElementById(id); | |
| if (el) el.textContent = text; | |
| }; | |
| setText('bm-total-runs', String(data.length)); | |
| setText('bm-avg-best', avgBest.toFixed(3)); | |
| setText('bm-best-run', `${bestRun.scenario} (${bestRun.difficulty}) reached ${bestRun.best_reward.toFixed(3)} in ${bestRun.steps} steps and finished in phase ${bestRun.phase}.`); | |
| setText('analytics-total', String(data.length)); | |
| setText('analytics-avg-best', avgBest.toFixed(3)); | |
| setText('analytics-avg-final', avgFinal.toFixed(3)); | |
| setText('analytics-budget-hit', `${overruns} / ${data.length}`); | |
| status.textContent = `Loaded ${data.length} benchmark episodes. Dominant phase: ${topPhase ? `${topPhase[0]} (${topPhase[1]})` : 'n/a'}.`; | |
| // 1. Phase Distribution (Pie) | |
| const phases = data.reduce((acc, run) => { | |
| acc[run.phase] = (acc[run.phase] || 0) + 1; | |
| return acc; | |
| }, {}); | |
| if (window.phaseChart) window.phaseChart.destroy(); | |
| window.phaseChart = new Chart(document.getElementById('phase-pie-chart'), { | |
| type: 'doughnut', | |
| data: { | |
| labels: Object.keys(phases), | |
| datasets: [{ | |
| data: Object.values(phases), | |
| backgroundColor: ['#00d4aa', '#3b82f6', '#f59e0b', '#8b5cf6'], | |
| borderColor: '#111823', | |
| borderWidth: 3, | |
| hoverOffset: 6 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| cutout: '58%', | |
| plugins: { | |
| legend: { position: 'bottom', labels: { color: '#94a3b8', padding: 16 } }, | |
| tooltip: { | |
| backgroundColor: '#0f172a', | |
| borderColor: 'rgba(148,163,184,0.18)', | |
| borderWidth: 1, | |
| callbacks: { | |
| label: (ctx) => `${ctx.label}: ${ctx.parsed} runs` | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // 2. Scenario Performance (Bar) | |
| if (window.barChart) window.barChart.destroy(); | |
| window.barChart = new Chart(document.getElementById('scenario-bar-chart'), { | |
| type: 'bar', | |
| data: { | |
| labels: data.map((r) => `${r.scenario} Β· ${r.difficulty}`), | |
| datasets: [ | |
| { | |
| label: 'Best Reward', | |
| data: data.map((r) => r.best_reward), | |
| backgroundColor: '#00d4aa', | |
| borderRadius: 10, | |
| barPercentage: 0.7, | |
| categoryPercentage: 0.7, | |
| yAxisID: 'y' | |
| }, | |
| { | |
| label: 'Final Reward', | |
| data: data.map((r) => r.final_reward), | |
| backgroundColor: '#3b82f6', | |
| borderRadius: 10, | |
| barPercentage: 0.7, | |
| categoryPercentage: 0.7, | |
| yAxisID: 'y' | |
| }, | |
| { | |
| type: 'line', | |
| label: 'Budget Used %', | |
| data: data.map((r) => (r.total_cost / r.cost_budget) * 100), | |
| borderColor: '#f59e0b', | |
| backgroundColor: '#f59e0b', | |
| pointRadius: 3, | |
| pointHoverRadius: 5, | |
| tension: 0.3, | |
| borderWidth: 2, | |
| yAxisID: 'yBudget' | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { intersect: false, mode: 'index' }, | |
| scales: { | |
| y: { | |
| min: 0, | |
| max: 1, | |
| position: 'left', | |
| title: { display: true, text: 'Reward' }, | |
| grid: { color: 'rgba(30,45,69,0.3)' }, | |
| ticks: { | |
| color: '#64748b', | |
| callback: (value) => Number(value).toFixed(1) | |
| } | |
| }, | |
| yBudget: { | |
| min: 0, | |
| max: Math.max(140, ...data.map((r) => Math.ceil(((r.total_cost / r.cost_budget) * 100) / 10) * 10)), | |
| position: 'right', | |
| title: { display: true, text: 'Budget %' }, | |
| grid: { display: false }, | |
| ticks: { | |
| color: '#f59e0b', | |
| callback: (value) => `${value}%` | |
| } | |
| }, | |
| x: { | |
| grid: { display : false }, | |
| ticks: { color: '#64748b', font: { size: 10 } } | |
| } | |
| }, | |
| plugins: { | |
| legend: { labels: { color: '#94a3b8', padding: 16 } }, | |
| tooltip: { | |
| backgroundColor: '#0f172a', | |
| borderColor: 'rgba(148,163,184,0.18)', | |
| borderWidth: 1, | |
| callbacks: { | |
| label: (ctx) => { | |
| if (ctx.dataset.yAxisID === 'yBudget') { | |
| return `${ctx.dataset.label}: ${Number(ctx.parsed.y).toFixed(0)}%`; | |
| } | |
| return `${ctx.dataset.label}: ${Number(ctx.parsed.y).toFixed(3)}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| const insights = [ | |
| `Best observed run: ${bestRun.scenario} on ${bestRun.difficulty} difficulty with reward ${bestRun.best_reward.toFixed(3)}.`, | |
| `${overruns} of ${data.length} recorded episodes exceeded the nominal budget, which shows the cost penalty is active.`, | |
| `Average best reward is ${avgBest.toFixed(3)} while average final reward is ${avgFinal.toFixed(3)}, so later-step degradation is visible in the baseline traces.` | |
| ]; | |
| const insightBox = document.getElementById('benchmark-insights'); | |
| if (insightBox) { | |
| insightBox.innerHTML = insights.map((item) => `<div class="insight-item">${item}</div>`).join(''); | |
| } | |
| const tableBody = document.getElementById('benchmark-table-body'); | |
| if (tableBody) { | |
| tableBody.innerHTML = data.map((run) => ` | |
| <tr> | |
| <td>${run.scenario}</td> | |
| <td>${run.difficulty}</td> | |
| <td>${run.best_reward.toFixed(3)}</td> | |
| <td>${run.final_reward.toFixed(3)}</td> | |
| <td>${run.phase}</td> | |
| <td>${run.total_cost.toFixed(0)} / ${run.cost_budget.toFixed(0)}</td> | |
| <td>${run.steps}</td> | |
| </tr> | |
| `).join(''); | |
| } | |
| } catch (e) { | |
| status.textContent = `Notice: ${e.message}`; | |
| status.style.color = '#ef4444'; | |
| } | |
| } | |
| // Instantiate app | |
| window.MF_APP = new MaterialForgeApp(); | |
| setTimeout(loadBenchmarks, 1000); // Initial load attempt | |
| function selectAtom(btn) { window.MF_APP.selectAtom(btn.dataset.atom); } | |