/** * RL Dashboard - Mode-aware visualization. * MANUAL → static overlay, AUTO → heuristic flow graph, AI → neural network cloud. */ import { ResourceTracker } from '../core/resource-tracker.js'; import { api, Poller } from '../core/api.js'; import { el, $, setText, empty } from '../core/dom.js'; let tracker = null; let statsPoller = null; let historyPoller = null; let metricsGraph = null; let modelCloud = null; let heuristicGraph = null; let _currentMode = 'AUTO'; export async function mount(container) { tracker = new ResourceTracker('rl-dashboard'); container.innerHTML = ''; container.appendChild(buildLayout()); await fetchStats(); await fetchHistory(); await fetchExperiences(); statsPoller = new Poller(fetchStats, 5000); historyPoller = new Poller(async () => { await fetchHistory(); await fetchExperiences(); }, 10000); statsPoller.start(); historyPoller.start(); } export function unmount() { if (statsPoller) { statsPoller.stop(); statsPoller = null; } if (historyPoller) { historyPoller.stop(); historyPoller = null; } if (metricsGraph) { metricsGraph.destroy(); metricsGraph = null; } if (modelCloud) { modelCloud.destroy(); modelCloud = null; } if (heuristicGraph) { heuristicGraph.destroy(); heuristicGraph = null; } if (tracker) { tracker.cleanupAll(); tracker = null; } } /* ======================== Mini Metrics Canvas ======================== */ class MultiMetricGraph { constructor(canvasId) { this.data = { epsilon: new Array(100).fill(0), reward: new Array(100).fill(0), loss: new Array(100).fill(0), }; this.colors = { epsilon: '#00d4ff', reward: '#00ff6a', loss: '#ff4169' }; this.canvas = document.getElementById(canvasId); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); this._onResize = () => this.resize(); window.addEventListener('resize', this._onResize); this.resize(); this.animate(); } destroy() { window.removeEventListener('resize', this._onResize); if (this._raf) cancelAnimationFrame(this._raf); } resize() { const p = this.canvas.parentElement; this.canvas.width = Math.max(1, p.offsetWidth); this.canvas.height = Math.max(1, p.offsetHeight); this.width = this.canvas.width; this.height = this.canvas.height; } update(stats) { if (!stats) return; this.data.epsilon.shift(); this.data.reward.shift(); this.data.loss.shift(); this.data.epsilon.push(Number(stats.epsilon || 0)); const recent = Array.isArray(stats.recent_activity) ? stats.recent_activity : []; const r = recent.length ? Number(recent[0].reward || 0) : 0; const prevR = this.data.reward[this.data.reward.length - 1] || 0; this.data.reward.push(prevR * 0.8 + r * 0.2); const l = Number(stats.last_loss || 0); const prevL = this.data.loss[this.data.loss.length - 1] || 0; this.data.loss.push(prevL * 0.9 + l * 0.1); } animate() { this._raf = requestAnimationFrame(() => this.animate()); this.ctx.clearRect(0, 0, this.width, this.height); this.drawLine(this.data.epsilon, this.colors.epsilon, 1.0); this.drawLine(this.data.reward, this.colors.reward, 10.0); this.drawLine(this.data.loss, this.colors.loss, 5.0); } drawLine(data, color, maxVal) { if (data.length < 2) return; const stepX = this.width / (data.length - 1); this.ctx.beginPath(); data.forEach((val, i) => { const x = i * stepX; const y = this.height - (Math.max(0, val) / Math.max(0.001, maxVal)) * this.height * 0.8 - 5; if (i === 0) this.ctx.moveTo(x, y); else this.ctx.lineTo(x, y); }); this.ctx.strokeStyle = color; this.ctx.lineWidth = 2; this.ctx.stroke(); } } /* ======================== Heuristic Flow Graph (AUTO mode) ======================== */ class HeuristicGraph { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); this.tick = 0; this.signals = []; this.recentActions = []; /* Flow nodes representing the heuristic pipeline */ this.flowNodes = [ { id: 'scan', label: 'SCAN', icon: '\uD83D\uDD0D', color: [0, 200, 255] }, { id: 'analyze', label: 'ANALYZE', icon: '\uD83E\uDDE0', color: [140, 100, 255] }, { id: 'decide', label: 'DECIDE', icon: '\u2696\uFE0F', color: [255, 200, 60] }, { id: 'execute', label: 'EXECUTE', icon: '\u26A1', color: [0, 255, 120] }, { id: 'result', label: 'RESULT', icon: '\uD83C\uDFAF', color: [255, 100, 180] }, ]; this.resizeObserver = new ResizeObserver(() => this.resize()); this.resizeObserver.observe(this.canvas.parentElement); this.resize(); this.animate(); } destroy() { if (this.resizeObserver) this.resizeObserver.disconnect(); if (this.raf) cancelAnimationFrame(this.raf); } resize() { const p = this.canvas.parentElement; this.width = Math.max(1, p.offsetWidth); this.height = Math.max(1, p.offsetHeight); this.canvas.width = this.width; this.canvas.height = this.height; this.layoutNodes(); } layoutNodes() { const n = this.flowNodes.length; const padX = 60, padY = 50; const spaceX = (this.width - 2 * padX) / Math.max(1, n - 1); const cy = this.height / 2; this.flowNodes.forEach((node, i) => { node.x = padX + i * spaceX; node.y = cy; node.r = Math.min(30, this.width / (n * 3)); }); } triggerActivity(activityList) { for (const act of activityList) { const reward = Number(act.reward || 0); this.signals.push({ progress: 0, speed: 0.006 + Math.random() * 0.004, color: reward > 0 ? '#00ffa0' : '#ff3333', action: act.action || '', reward, }); this.recentActions.unshift({ action: act.action || '', reward, time: Date.now() }); if (this.recentActions.length > 8) this.recentActions.pop(); } } animate() { this.raf = requestAnimationFrame(() => this.animate()); this.tick += 0.015; this.ctx.clearRect(0, 0, this.width, this.height); this.drawBackground(); this.drawConnections(); this.drawSignals(); this.drawNodes(); this.drawLabels(); this.drawActionFeed(); this.updateSignals(); } drawBackground() { const ctx = this.ctx; /* Subtle radial gradient */ const grad = ctx.createRadialGradient(this.width / 2, this.height / 2, 0, this.width / 2, this.height / 2, this.width / 2); grad.addColorStop(0, 'rgba(0, 255, 160, 0.03)'); grad.addColorStop(1, 'transparent'); ctx.fillStyle = grad; ctx.fillRect(0, 0, this.width, this.height); /* "HEURISTIC ENGINE" watermark */ ctx.save(); ctx.font = `bold ${Math.min(16, this.width / 30)}px monospace`; ctx.fillStyle = 'rgba(0, 255, 160, 0.08)'; ctx.textAlign = 'center'; ctx.fillText('HEURISTIC ENGINE', this.width / 2, 24); ctx.restore(); } drawConnections() { const ctx = this.ctx; const nodes = this.flowNodes; for (let i = 0; i < nodes.length - 1; i++) { const a = nodes[i], b = nodes[i + 1]; const pulse = 0.3 + Math.sin(this.tick * 2 + i) * 0.2; /* Animated dashed line */ ctx.save(); ctx.strokeStyle = `rgba(0, 255, 160, ${pulse})`; ctx.lineWidth = 2; ctx.setLineDash([8, 6]); ctx.lineDashOffset = -this.tick * 30; ctx.beginPath(); ctx.moveTo(a.x + a.r + 4, a.y); ctx.lineTo(b.x - b.r - 4, b.y); ctx.stroke(); ctx.restore(); /* Arrow head */ const ax = b.x - b.r - 8; ctx.fillStyle = `rgba(0, 255, 160, ${pulse + 0.2})`; ctx.beginPath(); ctx.moveTo(ax, a.y - 5); ctx.lineTo(ax + 8, a.y); ctx.lineTo(ax, a.y + 5); ctx.closePath(); ctx.fill(); } } drawNodes() { const ctx = this.ctx; for (let i = 0; i < this.flowNodes.length; i++) { const n = this.flowNodes[i]; const pulse = 0.7 + Math.sin(this.tick * 2.5 + i * 1.2) * 0.3; const [r, g, b] = n.color; /* Outer glow */ ctx.save(); ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2); ctx.fillStyle = `rgba(${r},${g},${b},${0.08 * pulse})`; ctx.shadowBlur = 20; ctx.shadowColor = `rgba(${r},${g},${b},0.4)`; ctx.fill(); ctx.restore(); /* Node circle */ ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); const grad = ctx.createRadialGradient(n.x, n.y - n.r * 0.3, 0, n.x, n.y, n.r); grad.addColorStop(0, `rgba(${r},${g},${b},${0.3 * pulse})`); grad.addColorStop(1, `rgba(${r},${g},${b},${0.08})`); ctx.fillStyle = grad; ctx.fill(); /* Border ring */ ctx.strokeStyle = `rgba(${r},${g},${b},${0.5 * pulse})`; ctx.lineWidth = 1.5; ctx.stroke(); /* Icon */ ctx.save(); ctx.font = `${n.r * 0.7}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = `rgba(${r},${g},${b},${0.9})`; ctx.fillText(n.icon, n.x, n.y); ctx.restore(); } } drawLabels() { const ctx = this.ctx; ctx.save(); ctx.font = `bold ${Math.min(11, this.width / 50)}px monospace`; ctx.textAlign = 'center'; for (const n of this.flowNodes) { const [r, g, b] = n.color; ctx.fillStyle = `rgba(${r},${g},${b},0.8)`; ctx.fillText(n.label, n.x, n.y + n.r + 16); } ctx.restore(); } drawSignals() { const ctx = this.ctx; const nodes = this.flowNodes; for (const sig of this.signals) { /* Map progress (0→1) across the full pipeline */ const totalSegments = nodes.length - 1; const segFloat = sig.progress * totalSegments; const segIdx = Math.min(Math.floor(segFloat), totalSegments - 1); const segT = segFloat - segIdx; const a = nodes[segIdx]; const b = nodes[Math.min(segIdx + 1, nodes.length - 1)]; const x = a.x + (b.x - a.x) * segT; const y = a.y + Math.sin(segT * Math.PI) * -15; /* arc upward */ ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fillStyle = sig.color; ctx.shadowBlur = 16; ctx.shadowColor = sig.color; ctx.fill(); ctx.shadowBlur = 0; /* Trail */ ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); } } drawActionFeed() { if (!this.recentActions.length) return; const ctx = this.ctx; ctx.save(); const fs = Math.min(11, this.width / 50); ctx.font = `${fs}px monospace`; ctx.textAlign = 'left'; const x = 12, startY = this.height - 12; const lineH = fs + 4; const maxShow = Math.min(this.recentActions.length, Math.floor((this.height * 0.3) / lineH)); for (let i = 0; i < maxShow; i++) { const act = this.recentActions[i]; const age = (Date.now() - act.time) / 1000; const alpha = Math.max(0.2, 1 - age / 30); const color = act.reward > 0 ? `rgba(0,255,160,${alpha})` : `rgba(255,60,60,${alpha})`; ctx.fillStyle = color; const prefix = act.reward > 0 ? '\u2713' : '\u2717'; ctx.fillText(`${prefix} ${act.action}`, x, startY - i * lineH); } ctx.restore(); } updateSignals() { for (const sig of this.signals) sig.progress += sig.speed; this.signals = this.signals.filter(s => s.progress < 1); } } /* ======================== Abstract Model Cloud (AI mode) ======================== */ class ModelCloud { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); this.tooltip = document.getElementById('brain-tooltip'); this.nodes = []; this.links = []; this.signals = []; this.seenActivities = new Set(); this.tick = 0; this.hoverIndex = -1; this.meta = { model_loaded: false, model_version: null, model_param_count: 0, model_layer_count: 0, model_feature_count: 0, }; this.resizeObserver = new ResizeObserver(() => this.resize()); this.resizeObserver.observe(this.canvas.parentElement); this.resize(); this.onMouseMove = (e) => this.handleMouseMove(e); this.onMouseLeave = () => { this.hoverIndex = -1; if (this.tooltip) this.tooltip.style.display = 'none'; }; this.canvas.addEventListener('mousemove', this.onMouseMove); this.canvas.addEventListener('mouseleave', this.onMouseLeave); this.generateNetwork(); this.animate(); } destroy() { if (this.resizeObserver) this.resizeObserver.disconnect(); if (this.canvas) { if (this.onMouseMove) this.canvas.removeEventListener('mousemove', this.onMouseMove); if (this.onMouseLeave) this.canvas.removeEventListener('mouseleave', this.onMouseLeave); } if (this.raf) cancelAnimationFrame(this.raf); } resize() { const p = this.canvas.parentElement; this.width = Math.max(1, p.offsetWidth); this.height = Math.max(1, p.offsetHeight); this.canvas.width = this.width; this.canvas.height = this.height; this.generateNetwork(); } triggerActivity(activityList) { if (!this.seenActivities) this.seenActivities = new Set(); for (const act of activityList) { const actHash = act.action + '_' + act.reward + '_' + (act.timestamp || Date.now() + Math.random()); if (this.seenActivities.has(actHash)) continue; this.seenActivities.add(actHash); if (this.seenActivities.size > 150) { const iter = this.seenActivities.values(); this.seenActivities.delete(iter.next().value); } const inputNodes = this.nodes.filter(n => n.layer === 0); if (!inputNodes.length) continue; const startNode = inputNodes[Math.floor(Math.random() * inputNodes.length)]; this.signals.push({ sourceNode: startNode, targetNode: null, progress: 0, speed: 0.008 + Math.random() * 0.004, reward: Number(act.reward || 0), action: act.action || 'Unknown', layer: 0, color: Number(act.reward || 0) > 0 ? '#00ffa0' : '#ff3333' }); } } updateFromStats(stats) { const oldMeta = JSON.stringify(this.meta); this.meta = { model_loaded: !!stats.model_loaded, model_version: stats.model_version || null, model_param_count: Number(stats.model_param_count || 0), model_layer_count: Number(stats.model_layer_count || 0), model_feature_count: Number(stats.model_feature_count || 0), }; if (oldMeta !== JSON.stringify(this.meta)) this.generateNetwork(); } generateNetwork() { this.nodes = []; this.links = []; if (this.width < 50 || this.height < 50) return; let numLayers = Math.max(3, Math.min(10, this.meta.model_layer_count || 3)); if (!this.meta.model_loaded) numLayers = 3; const maxNodesPerLayer = Math.max(4, Math.min(15, Math.ceil(Math.log10(Math.max(10, this.meta.model_param_count)) * 2))); const paddingX = 60, paddingY = 60; const layerSpacing = (this.width - 2 * paddingX) / Math.max(1, numLayers - 1); const layers = []; for (let i = 0; i < numLayers; i++) { let nodeCount = maxNodesPerLayer; if (i === 0) nodeCount = Math.max(3, Math.ceil(maxNodesPerLayer * 0.6)); if (i === numLayers - 1) nodeCount = Math.max(2, Math.ceil(maxNodesPerLayer * 0.4)); if (i > 0 && i < numLayers - 1) nodeCount = Math.max(3, nodeCount - Math.floor(Math.random() * 3)); const layerNodes = []; const nodeSpacing = (this.height - 2 * paddingY) / Math.max(1, nodeCount - 1); for (let j = 0; j < nodeCount; j++) { const energy = 0.2 + Math.random() * 0.8; layerNodes.push({ id: `${i}-${j}`, layer: i, index: j, x: paddingX + i * layerSpacing, y: paddingY + j * nodeSpacing + (this.height - 2 * paddingY - (nodeCount - 1) * nodeSpacing) / 2, baseX: paddingX + i * layerSpacing, baseY: paddingY + j * nodeSpacing + (this.height - 2 * paddingY - (nodeCount - 1) * nodeSpacing) / 2, r: 2 + energy * 4, energy, phase: Math.random() * Math.PI * 2, cluster: i, }); } layers.push(layerNodes); this.nodes.push(...layerNodes); } for (let i = 0; i < numLayers - 1; i++) { const currentLayer = layers[i], nextLayer = layers[i + 1]; for (const nodeA of currentLayer) { const connectionCount = Math.max(1, Math.ceil(Math.random() * nextLayer.length * 0.8)); const targets = [...nextLayer].sort(() => 0.5 - Math.random()).slice(0, connectionCount); for (const nodeB of targets) { this.links.push({ source: nodeA, target: nodeB, weight: Math.random() * 0.8 + 0.2, activePhase: Math.random() * Math.PI * 2 }); } } } } handleMouseMove(e) { const rect = this.canvas.getBoundingClientRect(); const mx = e.clientX - rect.left, my = e.clientY - rect.top; this.hoverIndex = -1; for (let i = 0; i < this.nodes.length; i++) { const n = this.nodes[i]; const dx = mx - n.x, dy = my - n.y; if (dx * dx + dy * dy <= (n.r + 6) * (n.r + 6)) { this.hoverIndex = i; break; } } if (!this.tooltip || this.hoverIndex < 0) { if (this.tooltip) this.tooltip.style.display = 'none'; return; } const n = this.nodes[this.hoverIndex]; this.tooltip.style.display = 'block'; this.tooltip.innerHTML = `Neural Node
Layer ${n.layer + 1}
Activation ${(n.energy * 100).toFixed(1)}%`; const tx = Math.min(this.width - 180, mx + 12), ty = Math.min(this.height - 80, my + 12); this.tooltip.style.left = `${Math.max(8, tx)}px`; this.tooltip.style.top = `${Math.max(8, ty)}px`; } animate() { this.raf = requestAnimationFrame(() => this.animate()); this.tick += 0.015; this.ctx.clearRect(0, 0, this.width, this.height); this.updateNodes(); this.updateSignals(); this.drawLinks(); this.drawNodes(); this.drawSignals(); } updateSignals() { const maxLayer = this.nodes.length > 0 ? Math.max(...this.nodes.map(n => n.layer)) : 3; this.signals = this.signals.filter(s => s.layer < maxLayer); for (const sig of this.signals) { if (!sig.targetNode) { const possibleLinks = this.links.filter(l => l.source === sig.sourceNode); if (possibleLinks.length > 0) { sig.targetNode = possibleLinks[Math.floor(Math.random() * possibleLinks.length)].target; } else { sig.layer = 999; continue; } } sig.progress += sig.speed; if (sig.progress >= 1) { sig.sourceNode = sig.targetNode; sig.targetNode = null; sig.progress = 0; sig.layer++; sig.sourceNode.energy = Math.min(1.0, sig.sourceNode.energy + 0.6); } } } updateNodes() { for (const n of this.nodes) { n.x = n.baseX + Math.cos(this.tick + n.phase) * 6; n.y = n.baseY + Math.sin(this.tick * 0.8 + n.phase) * 6; } } drawLinks() { for (const link of this.links) { const a = link.source, b = link.target; const activeSignal = Math.sin(this.tick * 3 + link.activePhase); const intensity = activeSignal > 0.7 ? 0.6 : 0.15; const alpha = link.weight * intensity; this.ctx.strokeStyle = activeSignal > 0.8 ? `rgba(0, 255, 160, ${alpha * 2})` : `rgba(90, 200, 255, ${alpha})`; this.ctx.lineWidth = link.weight * 1.5; this.ctx.beginPath(); this.ctx.moveTo(a.x, a.y); const midX = (a.x + b.x) / 2; const cp1x = a.x + (midX - a.x) * 0.5, cp1y = a.y; const cp2x = b.x - (b.x - midX) * 0.5, cp2y = b.y; this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, b.x, b.y); this.ctx.stroke(); } } drawNodes() { for (let i = 0; i < this.nodes.length; i++) { const n = this.nodes[i]; const pulse = 0.55 + Math.sin(this.tick * 2 + n.phase) * 0.45; const rr = n.r * (0.8 + pulse * 0.3); const isHover = i === this.hoverIndex; const color = clusterColor(n.cluster, n.energy); this.ctx.beginPath(); this.ctx.arc(n.x, n.y, rr + (isHover ? 3 : 0), 0, Math.PI * 2); this.ctx.fillStyle = color; this.ctx.shadowBlur = isHover ? 20 : (pulse * 10 + 4); this.ctx.shadowColor = color; this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(n.x, n.y, rr * 0.4, 0, Math.PI * 2); this.ctx.fillStyle = '#ffffff'; this.ctx.fill(); this.ctx.shadowBlur = 0; } } drawSignals() { for (const sig of this.signals) { if (!sig.targetNode) continue; const a = sig.sourceNode, b = sig.targetNode, p = sig.progress; const midX = (a.x + b.x) / 2; const cp1x = a.x + (midX - a.x) * 0.5, cp1y = a.y; const cp2x = b.x - (b.x - midX) * 0.5, cp2y = b.y; const mt = 1 - p, mt2 = mt * mt, mt3 = mt2 * mt; const p2 = p * p, p3 = p2 * p; const x = mt3 * a.x + 3 * mt2 * p * cp1x + 3 * mt * p2 * cp2x + p3 * b.x; const y = mt3 * a.y + 3 * mt2 * p * cp1y + 3 * mt * p2 * cp2y + p3 * b.y; this.ctx.beginPath(); this.ctx.arc(x, y, 5, 0, Math.PI * 2); this.ctx.fillStyle = sig.color; this.ctx.shadowBlur = 18; this.ctx.shadowColor = sig.color; this.ctx.fill(); this.ctx.shadowBlur = 0; } } } function fmtInt(v) { try { return Number(v || 0).toLocaleString(); } catch { return String(v || 0); } } function clusterColor(cluster, energy) { const palette = [[0,220,255],[0,255,160],[180,140,255],[255,120,180],[255,200,90]]; const base = palette[Math.abs(cluster) % palette.length]; const a = 0.25 + Math.max(0.0, Math.min(1.0, energy)) * 0.7; return `rgba(${base[0]},${base[1]},${base[2]},${a})`; } /* ======================== Layout ======================== */ function buildLayout() { const style = ` .rl-dash { display: flex; flex-direction: column; height: 100%; min-height: 0; gap: 15px; padding: 15px; } .rl-head { display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .rl-title { margin:0; font-size: 1.4rem; font-weight: 700; color: #fff; letter-spacing: 0.5px; } .rl-mode-group { display: flex; background: rgba(0,0,0,0.4); border-radius: 8px; padding: 4px; border: 1px solid rgba(255,255,255,0.05); } .rl-mode-btn { border: none; background: transparent; color: #888; padding: 6px 14px; font-size: 0.8rem; font-weight: 600; cursor: pointer; border-radius: 6px; transition: 0.3s; } .rl-mode-btn.active { background: rgba(0, 255, 160, 0.15); color: #00ffa0; box-shadow: 0 0 0 1px rgba(0, 255, 160, 0.4) inset; } .rl-main-grid { display: grid; grid-template-columns: 320px 1fr 300px; gap: 15px; flex: 1; min-height: 0; } .rl-panel { background: rgba(10, 14, 18, 0.6); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 12px; backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); display: flex; flex-direction: column; overflow: hidden; } .rl-panel-header { padding: 12px 14px; font-size: 0.75rem; font-weight: 700; color: #9bb; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid rgba(255,255,255,0.03); background: rgba(0,0,0,0.2); flex-shrink: 0; display:flex; justify-content: space-between; align-items: center; } .rl-panel-body { padding: 12px; flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; } .rl-canvas-area { flex: 1; position: relative; background: radial-gradient(circle at center, rgba(0,255,160,0.03) 0%, transparent 60%); } .rl-metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 12px 14px; background: rgba(0,0,0,0.2); border-bottom: 1px solid rgba(255,255,255,0.03); } .rl-metric { text-align: center; } .rl-metric-val { font-size: 1.3rem; font-weight: 800; font-family: 'Fira Code', monospace; line-height: 1; } .rl-metric-lbl { font-size: 0.6rem; color: #666; text-transform: uppercase; margin-top: 6px; letter-spacing: 0.5px; } .rl-graph-container { height: 100px; padding: 0 14px 14px; flex-shrink: 0; } .rl-tag { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); padding: 5px 8px; border-radius: 6px; font-size: 0.7rem; color: #ccc; white-space: nowrap; margin-bottom: 4px; } .rl-scrollable::-webkit-scrollbar { width: 4px; } .rl-scrollable::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; } .rl-table { width: 100%; font-size: 0.75rem; text-align: left; border-collapse: collapse; } .rl-table th { padding: 6px; color: #666; font-weight: normal; border-bottom: 1px solid rgba(255,255,255,0.05); } .rl-table td { padding: 6px; border-bottom: 1px solid rgba(255,255,255,0.02); color: #ccc; } /* Manual mode overlay */ .rl-manual-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; background: rgba(5, 7, 9, 0.85); z-index: 5; } .rl-manual-icon { font-size: 3rem; opacity: 0.6; } .rl-manual-title { font-size: 1.4rem; font-weight: 800; color: #ffd166; letter-spacing: 2px; text-transform: uppercase; } .rl-manual-sub { font-size: 0.85rem; color: #888; text-align: center; max-width: 280px; line-height: 1.5; } .rl-manual-badge { padding: 6px 16px; border: 1px solid rgba(255,209,102,0.3); border-radius: 8px; background: rgba(255,209,102,0.08); color: #ffd166; font-size: 0.75rem; font-weight: 700; letter-spacing: 1px; animation: rl-pulse 2s ease-in-out infinite; } @keyframes rl-pulse { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } } /* Center panel header mode badge */ .rl-mode-badge { padding: 2px 10px; border-radius: 4px; font-size: 0.65rem; font-weight: 800; letter-spacing: 1px; } .rl-mode-badge.manual { background: rgba(255,209,102,0.15); color: #ffd166; border: 1px solid rgba(255,209,102,0.3); } .rl-mode-badge.auto { background: rgba(0,220,255,0.15); color: #00dcff; border: 1px solid rgba(0,220,255,0.3); } .rl-mode-badge.ai { background: rgba(0,255,160,0.15); color: #00ffa0; border: 1px solid rgba(0,255,160,0.3); } @media (max-width: 1200px) { .rl-main-grid { grid-template-columns: 1fr 1fr; } .rl-panel-center { grid-column: 1 / -1; height: 350px; order: -1; } } @media (max-width: 800px) { .rl-dash { padding: 10px; gap: 10px; overflow-y: auto; display: block; } .rl-head { margin-bottom: 10px; flex-wrap: wrap; gap: 10px; } .rl-main-grid { display: flex; flex-direction: column; overflow: visible; } .rl-panel { flex: none; height: 350px; } .rl-panel-center { height: 300px; } } `; return el('div', { class: 'rl-dash page-main' }, [ el('style', {}, [style]), el('div', { class: 'rl-head' }, [ el('h2', { class: 'rl-title' }, ['AI Dashboard']), el('div', { class: 'rl-mode-group' }, [ el('button', { class: 'rl-mode-btn', id: 'mode-manual', onclick: () => setOperationMode('MANUAL') }, ['MANUAL']), el('button', { class: 'rl-mode-btn', id: 'mode-auto', onclick: () => setOperationMode('AUTO') }, ['AUTO']), el('button', { class: 'rl-mode-btn', id: 'mode-ai', onclick: () => setOperationMode('AI') }, ['AI']), ]) ]), el('div', { class: 'rl-main-grid' }, [ /* Left Panel: Stats & Graph */ el('div', { class: 'rl-panel' }, [ el('div', { class: 'rl-panel-header' }, ['Model Status']), el('div', { class: 'rl-metrics-grid' }, [ el('div', { class: 'rl-metric' }, [el('div', { class: 'rl-metric-val', id: 'val-episodes' }, ['0']), el('div', { class: 'rl-metric-lbl' }, ['Episodes'])]), el('div', { class: 'rl-metric' }, [el('div', { class: 'rl-metric-val', id: 'val-epsilon', style: 'color:cyan' }, ['0.00']), el('div', { class: 'rl-metric-lbl' }, ['Epsilon'])]), el('div', { class: 'rl-metric' }, [el('div', { class: 'rl-metric-val', id: 'val-qsize' }, ['0']), el('div', { class: 'rl-metric-lbl' }, ['Q-Size'])]), ]), el('div', { class: 'rl-graph-container' }, [ el('canvas', { id: 'metrics-canvas', style: 'width:100%; height:100%;' }), ]), el('div', { class: 'rl-panel-header', style: 'border-top: 1px solid rgba(255,255,255,0.03);' }, ['Model Manifest']), el('div', { class: 'rl-panel-body rl-scrollable' }, [ el('div', { id: 'model-manifest', style: 'display:flex; flex-wrap:wrap; gap:6px;' }) ]) ]), /* Center: Canvas (mode-aware) */ el('div', { class: 'rl-panel rl-panel-center' }, [ el('div', { class: 'rl-panel-header' }, [ el('span', { id: 'center-panel-title' }, ['Neural Network Architecture']), el('span', { id: 'center-mode-badge', class: 'rl-mode-badge auto' }, ['AUTO']), ]), el('div', { class: 'rl-canvas-area', id: 'brain-canvas-area' }, [ el('canvas', { id: 'brain-canvas', style: 'width:100%; height:100%; position:absolute; top:0; left:0;' }), el('div', { id: 'brain-tooltip', style: 'position:absolute; top:0; left:0; background:rgba(0,0,0,0.85); border:1px solid var(--acid); color:#fff; padding:6px 10px; border-radius:6px; font-size:0.75rem; pointer-events:none; display:none; z-index:10; white-space:nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.5);' }), /* Manual mode overlay (hidden by default) */ el('div', { class: 'rl-manual-overlay', id: 'manual-overlay', style: 'display:none;' }, [ el('div', { class: 'rl-manual-icon' }, ['\uD83D\uDD79\uFE0F']), el('div', { class: 'rl-manual-title' }, ['Manual Mode']), el('div', { class: 'rl-manual-sub' }, ['AI and heuristic engines are paused. Actions are triggered manually by the operator.']), el('div', { class: 'rl-manual-badge' }, ['OPERATOR CONTROL']), ]), ]) ]), /* Right Panel: Recent & Confidence */ el('div', { class: 'rl-panel' }, [ el('div', { class: 'rl-panel-header' }, ['Log Feed & Signals']), el('div', { class: 'rl-panel-body rl-scrollable' }, [ el('div', { style: 'margin-bottom: 12px;' }, [ el('div', { style: 'font-size:0.75rem; color:#888; margin-bottom:8px; display:flex; justify-content:space-between;' }, [ el('span', {}, ['LATEST SIGNALS']), el('span', { style: 'color:var(--acid);' }, ['\u25CF LIVE']) ]), el('div', { id: 'confidence-bars', style: 'display:flex; flex-direction:column; gap:8px;' }), ]), el('div', { style: 'margin-bottom: 12px;' }, [ el('div', { style: 'font-size:0.75rem; color:#888; margin-bottom:8px;' }, ['RECENT EXPERIENCES']), el('div', { id: 'experience-feed', style: 'display:flex; flex-direction:column; gap:6px;' }), ]), el('div', {}, [ el('div', { style: 'font-size:0.75rem; color:#888; margin-bottom:8px;' }, ['DATA SYNC']), el('table', { class: 'rl-table' }, [ el('thead', {}, [el('tr', {}, [el('th', {}, ['Time']), el('th', {}, ['Rec.']), el('th', {}, ['Status'])])]), el('tbody', { id: 'history-body' }), ]), ]) ]) ]) ]) ]); } /* ======================== Mode-aware canvas switching ======================== */ function switchCanvasMode(mode) { const m = String(mode).toUpperCase().trim(); if (m === _currentMode) return; _currentMode = m; const overlay = $('#manual-overlay'); const canvas = document.getElementById('brain-canvas'); const title = $('#center-panel-title'); const badge = $('#center-mode-badge'); if (badge) { badge.textContent = m; badge.className = `rl-mode-badge ${m.toLowerCase()}`; } if (m === 'MANUAL') { /* Show manual overlay, hide canvas visualizations */ if (overlay) overlay.style.display = 'flex'; if (canvas) canvas.style.opacity = '0.1'; if (title) title.textContent = 'Manual Mode'; /* Destroy active visualizations */ if (modelCloud) { modelCloud.destroy(); modelCloud = null; } if (heuristicGraph) { heuristicGraph.destroy(); heuristicGraph = null; } } else if (m === 'AUTO') { /* Show heuristic flow graph */ if (overlay) overlay.style.display = 'none'; if (canvas) canvas.style.opacity = '1'; if (title) title.textContent = 'Heuristic Engine'; /* Destroy neural cloud, create heuristic graph */ if (modelCloud) { modelCloud.destroy(); modelCloud = null; } if (!heuristicGraph && canvas) { heuristicGraph = new HeuristicGraph('brain-canvas'); if (tracker) tracker.trackResource(() => heuristicGraph && heuristicGraph.destroy()); } } else { /* AI mode: show neural network */ if (overlay) overlay.style.display = 'none'; if (canvas) canvas.style.opacity = '1'; if (title) title.textContent = 'Neural Network Architecture'; /* Destroy heuristic, create neural cloud */ if (heuristicGraph) { heuristicGraph.destroy(); heuristicGraph = null; } if (!modelCloud && canvas) { modelCloud = new ModelCloud('brain-canvas'); if (tracker) tracker.trackResource(() => modelCloud && modelCloud.destroy()); } } } /* ======================== Fetchers ======================== */ async function fetchStats() { try { const data = await api.get('/api/rl/stats'); if (!data || !tracker) return; if (!metricsGraph && document.getElementById('metrics-canvas')) { metricsGraph = new MultiMetricGraph('metrics-canvas'); if (tracker) tracker.trackResource(() => metricsGraph && metricsGraph.destroy()); } if (metricsGraph) metricsGraph.update(data); const mode = data.mode || (data.ai_mode ? 'AI' : data.manual_mode ? 'MANUAL' : 'AUTO'); updateModeUI(mode); switchCanvasMode(mode); /* Update stats (only for AI/Auto) */ if (modelCloud) modelCloud.updateFromStats(data); setText($('#val-episodes'), data.episodes ?? 0); setText($('#val-epsilon'), Number(data.epsilon || 0).toFixed(4)); setText($('#val-qsize'), data.q_table_size ?? 0); updateManifest(data); if (Array.isArray(data.recent_activity) && data.recent_activity.length) { renderConfidenceBars(data.recent_activity); if (modelCloud) modelCloud.triggerActivity(data.recent_activity); if (heuristicGraph) heuristicGraph.triggerActivity(data.recent_activity); } } catch (e) { console.error(e); } } function updateManifest(data) { const manifest = $('#model-manifest'); if (!manifest) return; empty(manifest); const tags = [ `MODE: ${_currentMode}`, `MODEL: ${data.model_loaded ? 'LOADED' : 'HEURISTIC'}`, `VERSION: ${data.model_version || 'N/A'}`, `PARAMS: ${fmtInt(data.model_param_count || 0)}`, `LAYERS: ${data.model_layer_count || 0}`, `FEATURES: ${data.model_feature_count || 0}`, `SAMPLES: ${fmtInt(data.training_samples || 0)}`, ]; tags.forEach((txt) => manifest.appendChild(el('div', { class: 'rl-tag' }, [txt]))); } function renderConfidenceBars(activity) { const container = $('#confidence-bars'); if (!container) return; empty(container); activity.forEach((act) => { const reward = Number(act.reward || 0); const color = reward > 0 ? 'var(--acid)' : '#ff3333'; const success = reward > 0; container.appendChild(el('div', { style: 'display:flex; flex-direction:column; gap:4px;' }, [ el('div', { style: 'display:flex; justify-content:space-between; font-size:0.75rem; color:#ccc;' }, [ el('span', {}, [act.action || '-']), el('span', { style: `color:${color}; font-weight:bold;` }, [success ? 'CONFIDENT' : 'UNCERTAIN']), ]), el('div', { style: 'height:3px; background:rgba(255,255,255,0.05); border-radius:2px; overflow:hidden' }, [ el('div', { style: `height:100%; background:${color}; width:${Math.min(Math.abs(reward) * 5, 100)}%; transition:width 0.45s ease-out` }), ]), ])); }); } async function fetchHistory() { try { const data = await api.get('/api/rl/history'); if (!data || !tracker || !Array.isArray(data.history)) return; const tbody = $('#history-body'); empty(tbody); data.history.forEach((row) => { const ts = String(row.timestamp || ''); const parsed = new Date(ts.includes('Z') ? ts : `${ts}Z`); tbody.appendChild(el('tr', {}, [ el('td', {}, [Number.isFinite(parsed.getTime()) ? parsed.toLocaleTimeString() : ts]), el('td', {}, [String(row.record_count || 0)]), el('td', { style: 'color:var(--acid)' }, ['COMPLETED']), ])); }); } catch (e) { console.error(e); } } async function fetchExperiences() { try { const data = await api.get('/api/rl/experiences'); if (!data || !tracker || !Array.isArray(data.experiences)) return; const container = $('#experience-feed'); empty(container); data.experiences.forEach((exp) => { let color = '#ccc'; if (exp.reward > 0) color = 'var(--acid)'; if (exp.reward < 0) color = '#ff3333'; container.appendChild(el('div', { style: `padding:6px 8px; background:rgba(255,255,255,0.02); border-radius:6px; border-left:2px solid ${color}; font-size:0.75rem;`, }, [ el('div', { style: 'display:flex;justify-content:space-between; margin-bottom:3px;' }, [ el('strong', { style: `color:${color};` }, [exp.action_name || '-']), el('span', { style: `font-weight:bold; color:${color};` }, [exp.reward > 0 ? `+${exp.reward}` : `${exp.reward}`]), ]), el('div', { style: 'color:#888;' }, [ el('span', {}, [new Date(String(exp.timestamp || '').includes('Z') ? exp.timestamp : `${exp.timestamp}Z`).toLocaleTimeString()]), ' - ', el('span', {}, [exp.success ? 'SUCCESS' : 'FAIL']), ]), ])); }); } catch (e) { console.error(e); } } function updateModeUI(mode) { if (!mode) return; const m = String(mode).toUpperCase().trim(); ['MANUAL', 'AUTO', 'AI'].forEach((v) => { const btn = $(`#mode-${v.toLowerCase()}`); if (!btn) return; btn.classList.toggle('active', v === m); }); } async function setOperationMode(mode) { try { const data = await api.post('/api/rl/config', { mode }); if (data.status === 'ok') { updateModeUI(data.mode); switchCanvasMode(data.mode); if (window.toast) window.toast(`Operation Mode: ${data.mode}`); const bc = new BroadcastChannel('bjorn_mode_sync'); bc.postMessage({ mode: data.mode }); bc.close(); } else if (window.toast) { window.toast(`Error: ${data.message}`, 'error'); } } catch (err) { console.error(err); if (window.toast) window.toast('Communication Error', 'error'); } }