Spaces:
Paused
Paused
| <html lang="da"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WidgeTDC | Omni-Link v4.0 (Haptic)</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Chosen Palette: Kinetic Cyan / Void Black */ | |
| :root { | |
| --bg-void: #020203; | |
| --panel-glass: rgba(15, 23, 42, 0.85); | |
| --border-subtle: rgba(56, 189, 248, 0.15); | |
| --neon-cyan: #22d3ee; | |
| --neon-purple: #c084fc; | |
| --neon-amber: #fbbf24; /* For Nudge Energy */ | |
| --font-display: 'Orbitron', sans-serif; | |
| --font-code: 'JetBrains Mono', monospace; | |
| } | |
| body { | |
| background-color: var(--bg-void); | |
| color: #e2e8f0; | |
| font-family: var(--font-code); | |
| overflow: hidden; | |
| margin: 0; | |
| } | |
| .font-display { font-family: var(--font-display); } | |
| /* Canvas & Layers */ | |
| #neural-canvas { position: absolute; inset: 0; z-index: 0; } | |
| .ui-layer { position: absolute; inset: 0; pointer-events: none; z-index: 10; display: flex; flex-direction: column; } | |
| .pointer-auto { pointer-events: auto; } | |
| /* Glass Panels */ | |
| .glass-panel { | |
| background: var(--panel-glass); | |
| border: 1px solid var(--border-subtle); | |
| backdrop-filter: blur(12px); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* NUDGE Button Animation */ | |
| .nudge-btn { | |
| background: linear-gradient(45deg, rgba(251, 191, 36, 0.1), rgba(245, 158, 11, 0.2)); | |
| border: 1px solid rgba(251, 191, 36, 0.5); | |
| box-shadow: 0 0 10px rgba(251, 191, 36, 0.1); | |
| transition: all 0.1s; | |
| } | |
| .nudge-btn:active { | |
| transform: scale(0.95); | |
| background: rgba(251, 191, 36, 0.4); | |
| box-shadow: 0 0 25px rgba(251, 191, 36, 0.6); | |
| } | |
| .nudge-btn:hover { | |
| border-color: var(--neon-amber); | |
| box-shadow: 0 0 15px rgba(251, 191, 36, 0.3); | |
| } | |
| /* Inspector Slide */ | |
| .inspector-panel { | |
| transform: translateX(110%); | |
| } | |
| .inspector-panel.active { transform: translateX(0); } | |
| /* Chat Stream */ | |
| .chat-msg { opacity: 0; animation: slideIn 0.2s forwards; margin-bottom: 4px; padding: 4px 8px; border-left: 2px solid transparent; font-size: 11px; } | |
| .chat-msg.sys { border-color: var(--neon-cyan); background: linear-gradient(90deg, rgba(34,211,238,0.05), transparent); } | |
| .chat-msg.user { border-color: var(--neon-purple); background: linear-gradient(90deg, rgba(192,132,252,0.05), transparent); } | |
| .chat-msg.alert { border-color: var(--neon-amber); color: var(--neon-amber); } | |
| @keyframes slideIn { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: translateX(0); } } | |
| /* Omni Bar */ | |
| .omni-input { background: transparent; border: none; outline: none; width: 100%; color: white; font-family: var(--font-code); } | |
| /* CRT Scanline */ | |
| .scanlines { | |
| position: fixed; inset: 0; pointer-events: none; z-index: 50; opacity: 0.08; | |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); | |
| background-size: 100% 2px, 3px 100%; | |
| } | |
| /* Syntax Highlighting */ | |
| .json-key { color: var(--neon-cyan); } | |
| .json-str { color: #86efac; } | |
| .json-num { color: var(--neon-purple); } | |
| </style> | |
| <!-- CONFIRMATION: NO SVG graphics used. NO Mermaid JS used. --> | |
| </head> | |
| <body> | |
| <div class="scanlines"></div> | |
| <canvas id="neural-canvas"></canvas> | |
| <!-- UI LAYER --> | |
| <div class="ui-layer p-4 md:p-6"> | |
| <!-- HEADER --> | |
| <header class="flex justify-between items-start pointer-auto mb-4"> | |
| <div> | |
| <h1 class="font-display text-2xl text-white tracking-widest drop-shadow-[0_0_15px_rgba(34,211,238,0.5)]"> | |
| OMNI-LINK <span class="text-amber-400 text-xs align-top font-bold">v4.0 HAPTIC</span> | |
| </h1> | |
| <div class="flex items-center gap-2 mt-1"> | |
| <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div> | |
| <div class="text-[10px] text-gray-500 font-mono tracking-widest">NEURAL PHYSICS ENGINE: ACTIVE</div> | |
| </div> | |
| </div> | |
| <div class="glass-panel px-3 py-2 rounded text-[10px] font-mono text-gray-400 flex flex-col gap-1 items-end"> | |
| <div>NODES: <span id="stat-nodes" class="text-white">0</span></div> | |
| <div>ENERGY: <span id="stat-energy" class="text-amber-400">0%</span></div> | |
| </div> | |
| </header> | |
| <!-- MIDDLE AREA --> | |
| <div class="flex-1 flex gap-4 overflow-hidden relative"> | |
| <!-- LEFT: STREAM --> | |
| <div class="w-80 glass-panel rounded-lg flex flex-col pointer-auto z-20 mb-20 transition-opacity hover:opacity-100 opacity-90"> | |
| <div class="p-2 border-b border-gray-800 bg-black/40 flex justify-between"> | |
| <span class="font-display text-[10px] text-cyan-400 tracking-wider">NEURAL LOG</span> | |
| <span class="text-[10px] text-gray-600">LIVE</span> | |
| </div> | |
| <div id="chat-stream" class="flex-1 overflow-y-auto p-2 scrollbar-hide"></div> | |
| </div> | |
| <!-- RIGHT: INSPECTOR (Slide-in) --> | |
| <div id="inspector" class="absolute right-0 top-0 bottom-20 w-96 glass-panel rounded-lg inspector-panel flex flex-col pointer-auto z-30 border-l-2 border-l-amber-500/50 shadow-[0_0_50px_rgba(0,0,0,0.8)]"> | |
| <!-- Inspector Header --> | |
| <div class="p-4 border-b border-gray-800 bg-black/40 flex justify-between items-center"> | |
| <div> | |
| <h2 class="font-display text-sm text-amber-400">SYNAPTIC CONTROLLER</h2> | |
| <span id="inspect-id-sm" class="text-[9px] text-gray-500 font-mono block">UUID: --</span> | |
| </div> | |
| <button onclick="closeInspector()" class="text-gray-500 hover:text-white text-xl">×</button> | |
| </div> | |
| <!-- Node Visualizer (Mini) --> | |
| <div class="p-4 bg-gradient-to-b from-gray-900 to-black border-b border-gray-800 text-center relative overflow-hidden group"> | |
| <div class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-10"></div> | |
| <h1 id="inspect-label" class="text-2xl font-bold text-white relative z-10 break-words">Node Name</h1> | |
| <span id="inspect-type" class="text-xs text-cyan-500 font-mono relative z-10 border border-cyan-900/50 px-2 py-0.5 rounded bg-cyan-900/10 mt-2 inline-block">TYPE</span> | |
| <!-- THE BIG NUDGE BUTTON --> | |
| <button id="btn-nudge" class="nudge-btn w-full mt-6 py-3 rounded text-amber-400 font-display font-bold tracking-widest text-sm relative z-10 group-hover:text-white flex justify-center items-center gap-2"> | |
| <span class="text-xl">⚡</span> NUDGE NODE | |
| </button> | |
| <div class="text-[9px] text-gray-600 mt-2 font-mono">Applies 50N impulse force to cluster</div> | |
| </div> | |
| <!-- Data View --> | |
| <div class="flex-1 overflow-y-auto p-4 space-y-3 font-mono"> | |
| <div class="text-[10px] text-gray-500 uppercase tracking-wider">Deep Storage Memory</div> | |
| <div class="bg-black/60 rounded p-3 border border-gray-800 text-[10px] overflow-x-auto shadow-inner"> | |
| <pre id="inspect-json" class="whitespace-pre-wrap break-words"></pre> | |
| </div> | |
| </div> | |
| <!-- Footer Actions --> | |
| <div class="p-3 border-t border-gray-800 grid grid-cols-2 gap-2"> | |
| <button onclick="flyToSelection()" class="p-2 bg-gray-800/50 hover:bg-cyan-900/30 text-cyan-400 text-[10px] rounded border border-gray-700">FOCUS CAM</button> | |
| <button class="p-2 bg-gray-800/50 hover:bg-red-900/30 text-red-400 text-[10px] rounded border border-gray-700">PRUNE LINK</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- BOTTOM: OMNI BAR --> | |
| <footer class="fixed bottom-6 left-4 right-4 md:left-10 md:right-10 pointer-auto z-40 max-w-5xl mx-auto"> | |
| <div class="glass-panel rounded-full px-4 py-3 flex items-center gap-3 border border-gray-700 shadow-[0_10px_40px_rgba(0,0,0,0.5)] focus-within:border-cyan-500 focus-within:shadow-[0_0_20px_rgba(6,182,212,0.3)] transition-all"> | |
| <span class="text-cyan-500 font-bold animate-pulse">></span> | |
| <input id="omni-input" type="text" class="omni-input" placeholder="Enter command, query, or /nudge [id]..." autocomplete="off"> | |
| <div class="hidden md:flex gap-2 text-[9px] text-gray-500 font-mono"> | |
| <span class="px-1.5 py-0.5 bg-gray-800 rounded">ENTER</span> EXECUTE | |
| <span class="px-1.5 py-0.5 bg-gray-800 rounded text-amber-500">/NUDGE</span> | |
| <span class="px-1.5 py-0.5 bg-gray-800 rounded text-cyan-500">/INJECT</span> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // --- 1. CONFIGURATION --- | |
| const config = { | |
| colors: { | |
| bg: '#020203', | |
| nodeBase: '#3b82f6', | |
| nodeAgent: '#c084fc', | |
| nodeInjected: '#22d3ee', | |
| link: '#1e293b', | |
| linkActive: '#38bdf8', | |
| ripple: '#fbbf24' | |
| }, | |
| physics: { | |
| friction: 0.94, | |
| spring: 0.03, | |
| repulsion: 200 | |
| } | |
| }; | |
| // --- 2. STATE --- | |
| const state = { | |
| nodes: [], | |
| links: [], | |
| ripples: [], // Visual shockwaves | |
| camera: { x: 0, y: 0, zoom: 1.2, targetX: 0, targetY: 0 }, | |
| selection: null, | |
| drag: { active: false, node: null }, | |
| lastTick: Date.now() | |
| }; | |
| // --- 3. DATA ENGINE --- | |
| function uuid() { return Math.random().toString(36).substr(2, 6).toUpperCase(); } | |
| function createNode(label, type, x, y) { | |
| return { | |
| id: uuid(), | |
| label: label, | |
| type: type, | |
| x: x || (Math.random()-0.5)*800, | |
| y: y || (Math.random()-0.5)*600, | |
| vx: 0, vy: 0, // Velocity | |
| mass: type === 'Agent' ? 5 : 1, | |
| radius: type === 'Agent' ? 20 : 8, | |
| flash: 0, // Visual flash intensity (0-1) | |
| data: { | |
| created: new Date().toISOString(), | |
| stimulus_count: 0, | |
| status: "DORMANT", | |
| memory_block: "0x" + Math.floor(Math.random()*100000).toString(16) | |
| } | |
| }; | |
| } | |
| function initGraph() { | |
| // Core Agents | |
| const c1 = createNode("Gemini (Architect)", "Agent", -100, -50); | |
| const c2 = createNode("Claude (Captain)", "Agent", 100, -50); | |
| const c3 = createNode("WidgeTDC Core", "Agent", 0, 100); | |
| state.nodes.push(c1, c2, c3); | |
| state.links.push({source: c1, target: c2}, {source: c2, target: c3}, {source: c3, target: c1}); | |
| // Random Memories/Files | |
| for(let i=0; i<30; i++) { | |
| const n = createNode(`Memory_${i}`, "Memory"); | |
| state.nodes.push(n); | |
| // Connect to random agent or node | |
| const target = state.nodes[Math.floor(Math.random() * (state.nodes.length-1))]; | |
| if (target !== n) state.links.push({source: n, target: target}); | |
| } | |
| log("System", "Neural Graph Loaded. Haptic feedback enabled."); | |
| updateStats(); | |
| } | |
| // --- 4. PHYSICS & RENDER LOOP --- | |
| const canvas = document.getElementById('neural-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| function physics() { | |
| // 1. Repulsion (Nodes push apart) | |
| for(let i=0; i<state.nodes.length; i++) { | |
| const n1 = state.nodes[i]; | |
| for(let j=i+1; j<state.nodes.length; j++) { | |
| const n2 = state.nodes[j]; | |
| const dx = n1.x - n2.x; | |
| const dy = n1.y - n2.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy) || 1; | |
| if (dist < 200) { | |
| const force = config.physics.repulsion / (dist * dist); | |
| const fx = (dx / dist) * force; | |
| const fy = (dy / dist) * force; | |
| n1.vx += fx; n1.vy += fy; | |
| n2.vx -= fx; n2.vy -= fy; | |
| } | |
| } | |
| } | |
| // 2. Spring (Links pull together) | |
| state.links.forEach(l => { | |
| const dx = l.target.x - l.source.x; | |
| const dy = l.target.y - l.source.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| const force = (dist - 100) * config.physics.spring; | |
| const fx = (dx / dist) * force; | |
| const fy = (dy / dist) * force; | |
| l.source.vx += fx; l.source.vy += fy; | |
| l.target.vx -= fx; l.target.vy -= fy; | |
| }); | |
| // 3. Update Position & Friction | |
| let totalEnergy = 0; | |
| state.nodes.forEach(n => { | |
| n.vx *= config.physics.friction; | |
| n.vy *= config.physics.friction; | |
| n.x += n.vx; | |
| n.y += n.vy; | |
| n.flash *= 0.9; // Fade flash | |
| totalEnergy += Math.abs(n.vx) + Math.abs(n.vy); | |
| }); | |
| // Update Energy Stat | |
| document.getElementById('stat-energy').innerText = Math.min(100, Math.round(totalEnergy * 10)) + "%"; | |
| // Camera Smoothing | |
| state.camera.x += (state.camera.targetX - state.camera.x) * 0.1; | |
| state.camera.y += (state.camera.targetY - state.camera.y) * 0.1; | |
| } | |
| function toScreen(x, y) { | |
| return { | |
| x: x * state.camera.zoom + state.camera.x + canvas.width/2, | |
| y: y * state.camera.zoom + state.camera.y + canvas.height/2 | |
| }; | |
| } | |
| function render() { | |
| physics(); | |
| // Clear | |
| ctx.fillStyle = config.colors.bg; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw Links | |
| ctx.lineWidth = 1; | |
| state.links.forEach(l => { | |
| const s = toScreen(l.source.x, l.source.y); | |
| const t = toScreen(l.target.x, l.target.y); | |
| // Highlight links connected to selection or during flash | |
| const isActive = state.selection === l.source || state.selection === l.target || l.source.flash > 0.1 || l.target.flash > 0.1; | |
| ctx.beginPath(); | |
| ctx.moveTo(s.x, s.y); | |
| ctx.lineTo(t.x, t.y); | |
| ctx.strokeStyle = isActive ? config.colors.linkActive : config.colors.link; | |
| ctx.globalAlpha = isActive ? 0.8 : 0.3; | |
| ctx.stroke(); | |
| ctx.globalAlpha = 1; | |
| }); | |
| // Draw Nodes | |
| state.nodes.forEach(n => { | |
| const p = toScreen(n.x, n.y); | |
| const r = n.radius * state.camera.zoom; | |
| // Flash Effect (Nudge) | |
| if (n.flash > 0.01) { | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, r + (n.flash * 20), 0, Math.PI*2); | |
| ctx.fillStyle = config.colors.ripple; | |
| ctx.globalAlpha = n.flash * 0.5; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| } | |
| // Node Body | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, r, 0, Math.PI*2); | |
| // Color Logic | |
| if (state.selection === n) ctx.fillStyle = "#fff"; | |
| else if (n.type === 'Agent') ctx.fillStyle = config.colors.nodeAgent; | |
| else if (n.type === 'Injected') ctx.fillStyle = config.colors.nodeInjected; | |
| else ctx.fillStyle = config.colors.nodeBase; | |
| ctx.fill(); | |
| // Selection Ring | |
| if (state.selection === n) { | |
| ctx.strokeStyle = config.colors.ripple; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Label always visible when selected | |
| ctx.fillStyle = "#fff"; | |
| ctx.font = "12px monospace"; | |
| ctx.fillText(n.label, p.x + r + 8, p.y + 4); | |
| } | |
| }); | |
| // Draw Ripples | |
| for(let i=state.ripples.length-1; i>=0; i--) { | |
| const r = state.ripples[i]; | |
| const p = toScreen(r.x, r.y); | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, r.radius * state.camera.zoom, 0, Math.PI*2); | |
| ctx.strokeStyle = config.colors.ripple; | |
| ctx.globalAlpha = r.opacity; | |
| ctx.stroke(); | |
| r.radius += 5; | |
| r.opacity -= 0.03; | |
| if (r.opacity <= 0) state.ripples.splice(i, 1); | |
| } | |
| ctx.globalAlpha = 1; | |
| requestAnimationFrame(render); | |
| } | |
| // --- 5. INTERACTION LOGIC --- | |
| // HAPTIC ENGINE: The Nudge Logic | |
| function nudgeNode(node, force = 50) { | |
| if (!node) return; | |
| // 1. Physics Impulse | |
| const angle = Math.random() * Math.PI * 2; | |
| node.vx += Math.cos(angle) * force; | |
| node.vy += Math.sin(angle) * force; | |
| // 2. Visual Flash & Ripple | |
| node.flash = 1.0; | |
| state.ripples.push({x: node.x, y: node.y, radius: node.radius + 5, opacity: 1}); | |
| // 3. Propagate to neighbors (Chain Reaction) | |
| state.links.forEach(l => { | |
| if (l.source === node) { | |
| l.target.vx += Math.cos(angle) * (force * 0.5); | |
| l.target.vy += Math.sin(angle) * (force * 0.5); | |
| l.target.flash = 0.5; | |
| } | |
| if (l.target === node) { | |
| l.source.vx += Math.cos(angle) * (force * 0.5); | |
| l.source.vy += Math.sin(angle) * (force * 0.5); | |
| l.source.flash = 0.5; | |
| } | |
| }); | |
| // 4. Data Update | |
| node.data.stimulus_count++; | |
| node.data.status = "STIMULATED"; | |
| node.data.last_nudge = new Date().toISOString(); | |
| // 5. Update Inspector if open | |
| if (state.selection === node) { | |
| renderInspector(node); | |
| } | |
| log("Physics", `Impulse applied to ${node.label} (${force}N)`, "alert"); | |
| } | |
| // Standard Interaction | |
| canvas.addEventListener('mousedown', e => { | |
| const m = toWorld(e.clientX, e.clientY); | |
| // Hit test | |
| const hit = state.nodes.find(n => { | |
| const dx = n.x - m.x; const dy = n.y - m.y; | |
| return Math.sqrt(dx*dx + dy*dy) < n.radius + 10; | |
| }); | |
| if (hit) { | |
| state.selection = hit; | |
| state.drag.active = true; | |
| state.drag.node = hit; | |
| openInspector(hit); | |
| } else { | |
| state.drag.active = false; | |
| // Pan logic handled by generic mouse movement relative to center if needed, | |
| // but keeping it simple: click bg to close inspector | |
| closeInspector(); | |
| } | |
| }); | |
| canvas.addEventListener('mousemove', e => { | |
| if (state.drag.active) { | |
| const m = toWorld(e.clientX, e.clientY); | |
| state.drag.node.x = m.x; | |
| state.drag.node.y = m.y; | |
| state.drag.node.vx = 0; state.drag.node.vy = 0; // Stop physics while dragging | |
| } | |
| }); | |
| canvas.addEventListener('mouseup', () => state.drag.active = false); | |
| canvas.addEventListener('wheel', e => { | |
| state.camera.zoom *= e.deltaY > 0 ? 0.9 : 1.1; | |
| }); | |
| // Inspector UI | |
| function openInspector(node) { | |
| const panel = document.getElementById('inspector'); | |
| renderInspector(node); | |
| panel.classList.add('active'); | |
| // Bind Nudge Button | |
| const btn = document.getElementById('btn-nudge'); | |
| // Clone node to remove old listeners | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', () => nudgeNode(node)); | |
| } | |
| function renderInspector(node) { | |
| document.getElementById('inspect-label').innerText = node.label; | |
| document.getElementById('inspect-type').innerText = node.type.toUpperCase(); | |
| document.getElementById('inspect-id-sm').innerText = "UUID: " + node.id; | |
| const json = JSON.stringify(node.data, null, 2); | |
| document.getElementById('inspect-json').innerHTML = highlightJSON(json); | |
| } | |
| function closeInspector() { | |
| document.getElementById('inspector').classList.remove('active'); | |
| state.selection = null; | |
| } | |
| function flyToSelection() { | |
| if (!state.selection) return; | |
| state.camera.targetX = -state.selection.x; | |
| state.camera.targetY = -state.selection.y; | |
| // Immediate snap for this version to ensure it works | |
| state.camera.x = -state.selection.x; | |
| state.camera.y = -state.selection.y; | |
| } | |
| // Omni Bar | |
| const input = document.getElementById('omni-input'); | |
| input.addEventListener('keydown', e => { | |
| if (e.key === 'Enter') { | |
| const cmd = input.value.trim(); | |
| input.value = ""; | |
| log("User", cmd, "user"); | |
| processCommand(cmd); | |
| } | |
| }); | |
| function processCommand(cmd) { | |
| if (cmd.startsWith('/nudge')) { | |
| const id = cmd.split(' ')[1]; | |
| if (!id && state.selection) { | |
| nudgeNode(state.selection); | |
| } else if (id) { | |
| const target = state.nodes.find(n => n.id === id || n.label.includes(id)); | |
| if (target) { | |
| nudgeNode(target); | |
| state.camera.x = -target.x; state.camera.y = -target.y; // Fly to it | |
| } else { | |
| log("System", "Target not found", "alert"); | |
| } | |
| } else { | |
| log("System", "Select a node or provide ID", "alert"); | |
| } | |
| } else if (cmd.startsWith('/inject')) { | |
| // Simulating injection | |
| const newNode = createNode("Injected_Node", "Injected"); | |
| newNode.data.source = "Omni-Bar Command"; | |
| state.nodes.push(newNode); | |
| log("System", "Node injected successfully."); | |
| nudgeNode(newNode, 20); // Gentle birth nudge | |
| } else { | |
| setTimeout(() => log("Architect", "I hear you. The graph is listening."), 500); | |
| } | |
| } | |
| function log(src, msg, type='sys') { | |
| const div = document.createElement('div'); | |
| div.className = `chat-msg ${type}`; | |
| div.innerHTML = `<span class="font-bold opacity-75">${src}:</span> ${msg}`; | |
| document.getElementById('chat-stream').appendChild(div); | |
| // Limit log | |
| const list = document.getElementById('chat-stream'); | |
| if (list.children.length > 20) list.removeChild(list.firstChild); | |
| list.scrollTop = list.scrollHeight; | |
| } | |
| function updateStats() { | |
| document.getElementById('stat-nodes').innerText = state.nodes.length; | |
| } | |
| function highlightJSON(json) { | |
| return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { | |
| let cls = 'json-num'; | |
| if (/^"/.test(match)) { | |
| if (/:$/.test(match)) cls = 'json-key'; | |
| else cls = 'json-str'; | |
| } | |
| return '<span class="' + cls + '">' + match + '</span>'; | |
| }); | |
| } | |
| // --- 6. START --- | |
| // Converts screen coords to world coords helper for mouse events | |
| function toWorld(x, y) { | |
| return { | |
| x: (x - canvas.width/2 - state.camera.x) / state.camera.zoom, | |
| y: (y - canvas.height/2 - state.camera.y) / state.camera.zoom | |
| }; | |
| } | |
| initGraph(); | |
| render(); | |
| </script> | |
| </body> | |
| </html> | |