| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>3D Naive Bayes Visualizer</title>
|
| <script src="https://cdn.tailwindcss.com"></script>
|
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| <style>
|
| :root {
|
| --background: #05080a;
|
| --primary: #ff0055;
|
| --secondary: #ffcc00;
|
| --accent: #00ffff;
|
| --surface: #0a1014;
|
| --border: #1e293b;
|
| }
|
|
|
| body {
|
| background-color: var(--background);
|
| color: #f8fafc;
|
| font-family: 'Space Grotesk', sans-serif;
|
| margin: 0;
|
| overflow-x: hidden;
|
| }
|
|
|
| .glass {
|
| background: rgba(10, 16, 20, 0.85);
|
| backdrop-filter: blur(12px);
|
| border: 1px solid rgba(30, 41, 59, 0.5);
|
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
| }
|
|
|
| .canvas-container {
|
| width: 100%;
|
| height: 50vh;
|
| min-height: 400px;
|
| max-height: 600px;
|
| position: relative;
|
| background: radial-gradient(circle at center, #111827 0%, #000000 100%);
|
| border-radius: 1rem;
|
| overflow: hidden;
|
| border: 1px solid var(--border);
|
| cursor: grab;
|
| }
|
|
|
| .canvas-container:active { cursor: grabbing; }
|
|
|
| .learning-log {
|
| height: 120px;
|
| overflow-y: auto;
|
| scrollbar-width: thin;
|
| }
|
|
|
|
|
| ::-webkit-scrollbar { width: 6px; }
|
| ::-webkit-scrollbar-track { background: #0f172a; }
|
| ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
|
|
| input[type=range] {
|
| -webkit-appearance: none;
|
| width: 100%;
|
| background: transparent;
|
| }
|
|
|
| input[type=range]::-webkit-slider-runnable-track {
|
| width: 100%;
|
| height: 6px;
|
| cursor: pointer;
|
| background: #1e293b;
|
| border-radius: 3px;
|
| }
|
|
|
| input[type=range]::-webkit-slider-thumb {
|
| height: 18px;
|
| width: 18px;
|
| border-radius: 50%;
|
| background: var(--accent);
|
| cursor: pointer;
|
| -webkit-appearance: none;
|
| margin-top: -6px;
|
| box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
|
| }
|
|
|
| .math-box {
|
| font-family: 'JetBrains Mono', monospace;
|
| font-size: 10px;
|
| }
|
|
|
| .hidden { display: none; }
|
| </style>
|
| </head>
|
| <body class="p-4 md:p-6 lg:p-8">
|
|
|
| <div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_380px] gap-6 lg:gap-8">
|
|
|
| <div class="flex flex-col gap-4">
|
| <header>
|
| <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
|
| <div>
|
| <h1 class="text-3xl md:text-4xl font-bold text-white mb-2">3D Naive Bayes <span class="text-cyan-400">Viz</span></h1>
|
| <p class="text-slate-400 text-sm max-w-xl">
|
| A classifier that learns by shaping 3D probability clouds. It assumes dimensions (Weight, Sweetness, Color) are independent.
|
| </p>
|
| <div class="absolute left-1/2 -translate-x-1/2 flex items-center"> <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio> <a href="/naive_bayes" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"> Back to Core </a> </div>
|
| </div>
|
| </div>
|
| </header>
|
|
|
| <div class="relative">
|
|
|
| <div class="canvas-container shadow-2xl" id="container">
|
| <div id="three-canvas" class="w-full h-full"></div>
|
|
|
|
|
| <div class="absolute top-4 left-4 pointer-events-none flex flex-col gap-2 z-10 max-w-[200px]">
|
| <div class="glass px-3 py-2 rounded-lg">
|
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div>
|
| <div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase font-bold animate-pulse">Waiting to Learn</div>
|
| </div>
|
| <div class="glass px-3 py-2 rounded-lg flex flex-col gap-1">
|
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest mb-1">Legend</div>
|
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ff0055] shadow-[0_0_8px_#ff0055]"></span> Apple Class</div>
|
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ffcc00] shadow-[0_0_8px_#ffcc00]"></span> Orange Class</div>
|
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-white border border-slate-500"></span> Mystery Fruit</div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="absolute top-4 right-4 pointer-events-none text-right z-10">
|
| <div class="glass px-3 py-2 rounded-lg">
|
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Input Features</div>
|
| <div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1 uppercase">W:0.0 S:0.0 C:0.0</div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="absolute bottom-4 left-4 pointer-events-none z-10 hidden md:block">
|
| <div class="glass px-3 py-2 rounded-lg text-[10px] text-slate-400">
|
| <b>Left-Click</b> Rotate | <b>Right-Click</b> Pan | <b>Shift+Drag</b> Lift Y-Axis
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="mt-4 md:mt-0 md:absolute md:bottom-4 md:right-4 z-20 w-full md:w-80">
|
| <div class="glass p-4 rounded-xl shadow-xl border border-slate-700/50">
|
| <div class="flex justify-between items-center mb-3">
|
| <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prediction Engine</div>
|
| <div id="uncertainty-badge" class="hidden text-[9px] bg-slate-700 text-white px-2 py-0.5 rounded font-mono">UNCERTAIN</div>
|
| </div>
|
|
|
|
|
| <div class="space-y-1 mb-3">
|
| <div class="flex justify-between text-[10px] text-[#ff0055] font-bold uppercase items-center">
|
| <span>Apple Likelihood</span>
|
| <span id="prob-a-val" class="text-xs">50%</span>
|
| </div>
|
| <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
| <div id="bar-a" class="h-full bg-[#ff0055] transition-all duration-300" style="width: 50%"></div>
|
| </div>
|
| <div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
|
| <span>Prior(<span id="prior-a">.5</span>)</span>
|
| <span>×</span>
|
| <span>L(<span id="total-l-a">0</span>)</span>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="space-y-1 mb-4">
|
| <div class="flex justify-between text-[10px] text-[#ffcc00] font-bold uppercase items-center">
|
| <span>Orange Likelihood</span>
|
| <span id="prob-b-val" class="text-xs">50%</span>
|
| </div>
|
| <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
| <div id="bar-b" class="h-full bg-[#ffcc00] transition-all duration-300" style="width: 50%"></div>
|
| </div>
|
| <div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
|
| <span>Prior(<span id="prior-b">.5</span>)</span>
|
| <span>×</span>
|
| <span>L(<span id="total-l-b">0</span>)</span>
|
| </div>
|
| </div>
|
|
|
| <div class="pt-3 border-t border-slate-700/50 text-center">
|
| <div class="text-[9px] text-slate-500 uppercase font-bold mb-1">Final Classification</div>
|
| <div id="result-text" class="text-2xl font-bold text-white uppercase tracking-tighter">NEUTRAL</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass rounded-xl p-4 flex flex-col h-40">
|
| <h3 class="text-xs font-bold text-white mb-2 flex items-center gap-2 shrink-0">
|
| <span class="text-cyan-400">⚡</span> System Logs
|
| </h3>
|
| <div id="learning-log" class="learning-log text-xs font-mono text-slate-400 space-y-1.5 pr-2">
|
| <div>> System initialized.</div>
|
| <div>> Waiting for training data...</div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <aside class="space-y-6">
|
| <div class="glass rounded-xl p-5 space-y-6">
|
| <h2 class="text-lg font-bold text-white flex items-center gap-2">
|
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-cyan-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.532 1.532 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.532 1.532 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /></svg>
|
| Control Panel
|
| </h2>
|
|
|
| <div class="space-y-4">
|
| <div>
|
| <div class="flex justify-between items-center mb-2">
|
| <label class="text-xs font-bold text-slate-400 uppercase">Search Variance (Alpha)</label>
|
| <span id="alpha-value" class="text-cyan-400 font-mono text-sm bg-cyan-950 px-2 rounded">1.00</span>
|
| </div>
|
| <input type="range" id="alpha-slider" min="0.1" max="5.0" step="0.1" value="1.0">
|
| <p class="text-[10px] text-slate-500 mt-2">
|
| Higher alpha = wider probability clouds (High Bias). Lower alpha = tighter clouds (High Variance).
|
| </p>
|
| </div>
|
|
|
| <div class="grid grid-cols-1 gap-3 pt-2">
|
| <button id="btn-toggle" class="group relative flex items-center justify-center gap-2 py-3 rounded-lg font-bold text-black bg-cyan-400 hover:bg-cyan-300 transition-all overflow-hidden">
|
| <div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
|
| <span id="toggle-icon">▶</span> <span id="toggle-text">Start Training</span>
|
| </button>
|
| <button id="btn-reset" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 hover:border-slate-600 transition-all font-bold text-sm text-slate-300">
|
| Reset Simulation
|
| </button>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass rounded-xl p-5 space-y-3">
|
| <h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-1">Data Scenario</h3>
|
| <div class="grid grid-cols-2 gap-2">
|
| <button onclick="selectScenario(0)" id="scen-0" class="text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all">Easy (Clustered)</button>
|
| <button onclick="selectScenario(1)" id="scen-1" class="text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all">Hard (Scattered)</button>
|
| </div>
|
| <p class="text-[10px] text-slate-500 leading-relaxed pt-2 border-t border-slate-800">
|
| Switching scenarios resets the robot's memory.
|
| </p>
|
| </div>
|
|
|
|
|
| <div class="glass rounded-xl p-5 border-l-2 border-cyan-400">
|
| <h3 class="text-xs font-bold text-cyan-400 uppercase tracking-widest mb-2">How it works</h3>
|
| <p class="text-[11px] text-slate-300 leading-relaxed">
|
| The robot calculates the center (mean) and spread (variance) of the points it sees.
|
| <br><br>
|
| To predict the mystery fruit, it measures the distance to each cloud center relative to the cloud's size.
|
| <br><br>
|
| <span class="text-white font-bold">Naive Assumption:</span> It processes Width, Sweetness, and Color completely separately, then multiplies the results.
|
| </p>
|
| </div>
|
| </aside>
|
| </div>
|
|
|
| <script>
|
|
|
| const SCENARIOS = [
|
| { name: 'Clustered', points: 80, spread: 1.2 },
|
| { name: 'Scattered', points: 200, spread: 3.5 }
|
| ];
|
|
|
| let currentScenario = SCENARIOS[0];
|
| let dataPoints = [];
|
| let processedIdx = 0;
|
| let isRunning = false;
|
| let loopId = null;
|
| let alpha = 1.0;
|
|
|
|
|
| let model = [
|
| { name: 'Apple', color: 0xff0055, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 },
|
| { name: 'Orange', color: 0xffcc00, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 }
|
| ];
|
|
|
|
|
| const container = document.getElementById('container');
|
| const scene = new THREE.Scene();
|
|
|
|
|
| const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
|
|
|
| let cameraRadius = 35;
|
| let cameraPhi = Math.PI / 2.5;
|
| let cameraTheta = Math.PI / 4;
|
|
|
| function updateCameraPosition() {
|
| camera.position.x = cameraRadius * Math.sin(cameraPhi) * Math.sin(cameraTheta);
|
| camera.position.y = cameraRadius * Math.cos(cameraPhi);
|
| camera.position.z = cameraRadius * Math.sin(cameraPhi) * Math.cos(cameraTheta);
|
| camera.lookAt(0, 0, 0);
|
| }
|
| updateCameraPosition();
|
|
|
| const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
|
| renderer.setSize(container.clientWidth, container.clientHeight);
|
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| document.getElementById('three-canvas').appendChild(renderer.domElement);
|
|
|
|
|
| scene.add(new THREE.AmbientLight(0xffffff, 0.7));
|
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| dirLight.position.set(10, 20, 10);
|
| scene.add(dirLight);
|
|
|
|
|
| const grid = new THREE.GridHelper(30, 30, 0x1e293b, 0x0f172a);
|
| grid.position.y = -5;
|
| scene.add(grid);
|
|
|
|
|
| function createAxis(start, end, color) {
|
| const points = [start, end];
|
| const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
| const material = new THREE.LineBasicMaterial({ color: color });
|
| return new THREE.Line(geometry, material);
|
| }
|
|
|
| scene.add(createAxis(new THREE.Vector3(-15, -5, 0), new THREE.Vector3(15, -5, 0), 0x334155));
|
| scene.add(createAxis(new THREE.Vector3(0, -15, 0), new THREE.Vector3(0, 15, 0), 0x334155));
|
| scene.add(createAxis(new THREE.Vector3(0, -5, -15), new THREE.Vector3(0, -5, 15), 0x334155));
|
|
|
|
|
| const pointGroup = new THREE.Group();
|
| scene.add(pointGroup);
|
|
|
|
|
| const mysteryFruitMesh = new THREE.Mesh(
|
| new THREE.SphereGeometry(0.7, 32, 32),
|
| new THREE.MeshStandardMaterial({
|
| color: 0xffffff,
|
| roughness: 0.2,
|
| metalness: 0.1,
|
| emissive: 0xffffff,
|
| emissiveIntensity: 0.2
|
| })
|
| );
|
| mysteryFruitMesh.position.set(0, 0, 0);
|
| scene.add(mysteryFruitMesh);
|
|
|
|
|
| const ringGeo = new THREE.RingGeometry(0.8, 0.9, 32);
|
| const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
|
| const ring = new THREE.Mesh(ringGeo, ringMat);
|
| ring.rotation.x = -Math.PI / 2;
|
| mysteryFruitMesh.add(ring);
|
|
|
|
|
| const measureLines = new THREE.LineSegments(
|
| new THREE.BufferGeometry(),
|
| new THREE.LineDashedMaterial({ color: 0x00ffff, dashSize: 0.4, gapSize: 0.2, opacity: 0.5, transparent: true })
|
| );
|
|
|
| scene.add(measureLines);
|
|
|
|
|
| const clouds = model.map(m => {
|
| const mesh = new THREE.Mesh(
|
| new THREE.SphereGeometry(1, 32, 32),
|
| new THREE.MeshBasicMaterial({
|
| color: m.color,
|
| transparent: true,
|
| opacity: 0.1,
|
| wireframe: true,
|
| depthWrite: false
|
| })
|
| );
|
| mesh.visible = false;
|
| scene.add(mesh);
|
| return mesh;
|
| });
|
|
|
|
|
|
|
| function gaussian(x, mean, variance) {
|
|
|
| const v = Math.max(variance, 0.1) + (alpha * 0.2);
|
|
|
| return (1 / Math.sqrt(2 * Math.PI * v)) * Math.exp(-Math.pow(x - mean, 2) / (2 * v)) + 1e-9;
|
| }
|
|
|
| function generateData() {
|
| dataPoints = [];
|
| pointGroup.clear();
|
|
|
|
|
| const appleCenter = { x: -6, y: -2, z: -4 };
|
| const orangeCenter = { x: 6, y: 3, z: 4 };
|
|
|
| model.forEach((m, i) => {
|
| const center = i === 0 ? appleCenter : orangeCenter;
|
|
|
| for (let j = 0; j < currentScenario.points / 2; j++) {
|
| const x = center.x + (Math.random() - 0.5) * currentScenario.spread * 5;
|
| const y = center.y + (Math.random() - 0.5) * currentScenario.spread * 5;
|
| const z = center.z + (Math.random() - 0.5) * currentScenario.spread * 5;
|
|
|
| dataPoints.push({ x, y, z, classId: i });
|
|
|
| const p = new THREE.Mesh(
|
| new THREE.SphereGeometry(0.2, 8, 8),
|
| new THREE.MeshBasicMaterial({ color: m.color, transparent: true, opacity: 0.6 })
|
| );
|
| p.position.set(x, y, z);
|
| p.visible = false;
|
| pointGroup.add(p);
|
| }
|
| });
|
|
|
| dataPoints.sort(() => Math.random() - 0.5);
|
| }
|
|
|
| function trainStep() {
|
| if (processedIdx >= dataPoints.length) {
|
| stopTraining();
|
| addLog("Training complete. Model optimized.");
|
| return;
|
| }
|
|
|
|
|
| const batchSize = 2;
|
| for(let k=0; k<batchSize && processedIdx < dataPoints.length; k++) {
|
| const p = dataPoints[processedIdx];
|
| const m = model[p.classId];
|
|
|
|
|
| m.count++;
|
| const lr = 1.0 / (m.count + 5);
|
|
|
|
|
| const oldMean = { ...m.mean };
|
| m.mean.x += lr * (p.x - m.mean.x);
|
| m.mean.y += lr * (p.y - m.mean.y);
|
| m.mean.z += lr * (p.z - m.mean.z);
|
|
|
|
|
| const varLr = 0.1;
|
| m.var.x = (1 - varLr) * m.var.x + varLr * Math.pow(p.x - m.mean.x, 2);
|
| m.var.y = (1 - varLr) * m.var.y + varLr * Math.pow(p.y - m.mean.y, 2);
|
| m.var.z = (1 - varLr) * m.var.z + varLr * Math.pow(p.z - m.mean.z, 2);
|
|
|
| pointGroup.children[processedIdx].visible = true;
|
| processedIdx++;
|
| }
|
|
|
| if (processedIdx % 10 === 0) updateVisuals();
|
| predict();
|
| }
|
|
|
| function updateVisuals() {
|
| model.forEach((m, i) => {
|
| clouds[i].visible = true;
|
| clouds[i].position.set(m.mean.x, m.mean.y, m.mean.z);
|
|
|
|
|
|
|
| const sx = Math.sqrt(m.var.x) * 2.5 + alpha;
|
| const sy = Math.sqrt(m.var.y) * 2.5 + alpha;
|
| const sz = Math.sqrt(m.var.z) * 2.5 + alpha;
|
|
|
| clouds[i].scale.set(sx, sy, sz);
|
| });
|
| }
|
|
|
| function predict() {
|
| const p = mysteryFruitMesh.position;
|
| const totalPoints = Math.max(1, processedIdx);
|
|
|
| model.forEach(m => {
|
|
|
|
|
|
|
| const prior = (m.count + 1) / (totalPoints + 2);
|
|
|
|
|
| const lW = gaussian(p.x, m.mean.x, m.var.x);
|
| const lC = gaussian(p.y, m.mean.y, m.var.y);
|
| const lS = gaussian(p.z, m.mean.z, m.var.z);
|
|
|
|
|
| m.lastCalc = { prior, lW, lS, lC };
|
| m.score = prior * lW * lS * lC;
|
| });
|
|
|
|
|
| const totalScore = model[0].score + model[1].score;
|
|
|
| let probA = 0, probB = 0;
|
|
|
| if (totalScore > 0) {
|
| probA = (model[0].score / totalScore) * 100;
|
| probB = (model[1].score / totalScore) * 100;
|
| } else {
|
|
|
| probA = 50;
|
| probB = 50;
|
| }
|
|
|
| updateUI(probA, probB);
|
| updateMeasurementLines();
|
| }
|
|
|
| function updateUI(probA, probB) {
|
|
|
| document.getElementById('bar-a').style.width = `${probA}%`;
|
| document.getElementById('bar-b').style.width = `${probB}%`;
|
|
|
|
|
| document.getElementById('prob-a-val').innerText = probA.toFixed(1) + '%';
|
| document.getElementById('prob-b-val').innerText = probB.toFixed(1) + '%';
|
|
|
|
|
| document.getElementById('prior-a').innerText = model[0].lastCalc?.prior.toFixed(2) || '0.5';
|
| document.getElementById('prior-b').innerText = model[1].lastCalc?.prior.toFixed(2) || '0.5';
|
|
|
|
|
| const totLA = (model[0].lastCalc?.lW + model[0].lastCalc?.lC + model[0].lastCalc?.lS) || 0;
|
| const totLB = (model[1].lastCalc?.lW + model[1].lastCalc?.lC + model[1].lastCalc?.lS) || 0;
|
| document.getElementById('total-l-a').innerText = totLA.toFixed(2);
|
| document.getElementById('total-l-b').innerText = totLB.toFixed(2);
|
|
|
|
|
| const resultText = document.getElementById('result-text');
|
| const badge = document.getElementById('uncertainty-badge');
|
|
|
| if (Math.abs(probA - probB) < 2) {
|
|
|
| resultText.innerText = "NEUTRAL";
|
| resultText.style.color = "#94a3b8";
|
| badge.classList.remove('hidden');
|
| } else if (probA > probB) {
|
| resultText.innerText = "APPLE";
|
| resultText.style.color = "#ff0055";
|
| badge.classList.add('hidden');
|
| } else {
|
| resultText.innerText = "ORANGE";
|
| resultText.style.color = "#ffcc00";
|
| badge.classList.add('hidden');
|
| }
|
| }
|
|
|
| function updateMeasurementLines() {
|
| const p = mysteryFruitMesh.position;
|
|
|
| const points = [
|
| p.x, p.y, p.z, p.x, -5, p.z,
|
| p.x, -5, p.z, p.x, -5, 0,
|
| p.x, -5, p.z, 0, -5, p.z
|
| ];
|
|
|
| const vertices = [];
|
| for(let i=0; i<points.length; i+=3) {
|
| vertices.push(new THREE.Vector3(points[i], points[i+1], points[i+2]));
|
| }
|
| measureLines.geometry.setFromPoints(vertices);
|
| measureLines.computeLineDistances();
|
|
|
| document.getElementById('pos-display').innerText =
|
| `W:${p.x.toFixed(1)} S:${p.z.toFixed(1)} C:${p.y.toFixed(1)}`;
|
| }
|
|
|
|
|
|
|
| function startTraining() {
|
| if (isRunning) return;
|
| isRunning = true;
|
| document.getElementById('toggle-text').innerText = 'Pause';
|
| document.getElementById('toggle-icon').innerText = '⏸';
|
| document.getElementById('status-text').innerText = 'Training...';
|
| document.getElementById('status-text').classList.remove('animate-pulse');
|
|
|
| loopId = setInterval(trainStep, 50);
|
| }
|
|
|
| function stopTraining() {
|
| isRunning = false;
|
| document.getElementById('toggle-text').innerText = 'Resume';
|
| document.getElementById('toggle-icon').innerText = '▶';
|
| document.getElementById('status-text').innerText = 'Paused / Idle';
|
| clearInterval(loopId);
|
| }
|
|
|
| function reset() {
|
| stopTraining();
|
| processedIdx = 0;
|
|
|
| model.forEach(m => {
|
| m.count = 0;
|
| m.mean = {x:0, y:0, z:0};
|
| m.var = {x:1, y:1, z:1};
|
| m.score = 0;
|
| });
|
|
|
| clouds.forEach(c => c.visible = false);
|
| document.getElementById('toggle-text').innerText = 'Start Training';
|
| document.getElementById('toggle-icon').innerText = '▶';
|
| document.getElementById('status-text').innerText = 'Ready';
|
| document.getElementById('status-text').classList.add('animate-pulse');
|
|
|
|
|
| mysteryFruitMesh.position.set(0, 0, 0);
|
|
|
| generateData();
|
| addLog("Memory wiped. System reset.");
|
| predict();
|
| }
|
|
|
| function selectScenario(idx) {
|
| currentScenario = SCENARIOS[idx];
|
|
|
|
|
| document.getElementById('scen-0').className = idx === 0
|
| ? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]"
|
| : "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all";
|
|
|
| document.getElementById('scen-1').className = idx === 1
|
| ? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]"
|
| : "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all";
|
|
|
| reset();
|
| }
|
|
|
| function addLog(msg) {
|
| const log = document.getElementById('learning-log');
|
| const div = document.createElement('div');
|
| const time = new Date().toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit'});
|
| div.innerHTML = `<span class="text-slate-600">[${time}]</span> ${msg}`;
|
| log.prepend(div);
|
| }
|
|
|
|
|
| document.getElementById('btn-toggle').onclick = () => isRunning ? stopTraining() : startTraining();
|
| document.getElementById('btn-reset').onclick = reset;
|
| document.getElementById('alpha-slider').oninput = (e) => {
|
| alpha = parseFloat(e.target.value);
|
| document.getElementById('alpha-value').innerText = alpha.toFixed(2);
|
| updateVisuals();
|
| predict();
|
| };
|
|
|
|
|
| let isMouseDown = false;
|
| let mouseButton = 0;
|
| let prevMouse = { x: 0, y: 0 };
|
|
|
| container.addEventListener('mousedown', (e) => {
|
| isMouseDown = true;
|
| mouseButton = e.button;
|
| prevMouse = { x: e.clientX, y: e.clientY };
|
| });
|
|
|
| window.addEventListener('mouseup', () => isMouseDown = false);
|
| container.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
| container.addEventListener('mousemove', (e) => {
|
| if (!isMouseDown) return;
|
| const dx = e.clientX - prevMouse.x;
|
| const dy = e.clientY - prevMouse.y;
|
|
|
|
|
| if (mouseButton === 0 && !e.shiftKey) {
|
| cameraTheta -= dx * 0.01;
|
| cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi - dy * 0.01));
|
| updateCameraPosition();
|
| }
|
|
|
| else if (e.shiftKey) {
|
| mysteryFruitMesh.position.y -= dy * 0.1;
|
| mysteryFruitMesh.position.y = Math.max(-10, Math.min(10, mysteryFruitMesh.position.y));
|
| predict();
|
| }
|
|
|
| else if (mouseButton === 2 || (mouseButton === 0 && e.ctrlKey)) {
|
|
|
| const speed = 0.1;
|
| const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
| forward.y = 0; forward.normalize();
|
| const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
|
| right.y = 0; right.normalize();
|
|
|
| mysteryFruitMesh.position.addScaledVector(right, dx * speed);
|
| mysteryFruitMesh.position.addScaledVector(forward, dy * speed);
|
|
|
|
|
| ['x', 'y', 'z'].forEach(axis => {
|
| mysteryFruitMesh.position[axis] = Math.max(-14, Math.min(14, mysteryFruitMesh.position[axis]));
|
| });
|
|
|
| predict();
|
| }
|
| prevMouse = { x: e.clientX, y: e.clientY };
|
| });
|
|
|
|
|
| container.addEventListener('wheel', (e) => {
|
| e.preventDefault();
|
| cameraRadius = Math.max(10, Math.min(60, cameraRadius + e.deltaY * 0.05));
|
| updateCameraPosition();
|
| }, { passive: false });
|
|
|
| window.addEventListener('resize', () => {
|
| camera.aspect = container.clientWidth / container.clientHeight;
|
| camera.updateProjectionMatrix();
|
| renderer.setSize(container.clientWidth, container.clientHeight);
|
| });
|
|
|
|
|
| function animate() {
|
| requestAnimationFrame(animate);
|
|
|
| ring.rotation.z -= 0.02;
|
| renderer.render(scene, camera);
|
| }
|
|
|
|
|
| generateData();
|
| predict();
|
| animate();
|
| updateUI(50, 50);
|
| </script>
|
| </body>
|
| </html> |