machinelearningalgorithms / templates /Naive-Bayes-Simulator.html
deedrop1140's picture
Upload 182 files
d0a6b4f verified
<!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; /* Apple Red */
--secondary: #ffcc00; /* Orange Orange */
--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; /* Responsive height */
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;
}
/* Custom Scrollbar */
::-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">
<!-- Left Side: Visualizer -->
<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>
<!-- Centered Button --> <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">
<!-- 3D Canvas -->
<div class="canvas-container shadow-2xl" id="container">
<div id="three-canvas" class="w-full h-full"></div>
<!-- Top Overlays (Inside Canvas) -->
<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>
<!-- Measurement Label (Inside Canvas) -->
<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>
<!-- Instructions Overlay (Mobile/Desktop) -->
<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 &nbsp;|&nbsp; <b>Right-Click</b> Pan &nbsp;|&nbsp; <b>Shift+Drag</b> Lift Y-Axis
</div>
</div>
</div>
<!-- Result Card: Below on Mobile, Absolute Overlay on Desktop -->
<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>
<!-- Apple Calc -->
<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>
<!-- Orange Calc -->
<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>
<!-- The Learning Log -->
<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>
<!-- Right Side: Controls -->
<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>
<!-- Scenario Selection -->
<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>
<!-- Learning Concept Card -->
<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>
// --- DATA CONFIG ---
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;
// Model State
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 }
];
// --- THREE.JS SETUP ---
const container = document.getElementById('container');
const scene = new THREE.Scene();
// Dark gradient background effect via clear color not possible easily, handled via CSS
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);
// Lighting
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);
// Axes & Grid
const grid = new THREE.GridHelper(30, 30, 0x1e293b, 0x0f172a);
grid.position.y = -5;
scene.add(grid);
// Custom Axes
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);
}
// X (Red), Y (Green), Z (Blue)
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));
// Data Points Group
const pointGroup = new THREE.Group();
scene.add(pointGroup);
// Mystery Fruit (Interactive)
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); // Start neutral
scene.add(mysteryFruitMesh);
// Ring indicator for mystery fruit
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);
// Measurement Lines
const measureLines = new THREE.LineSegments(
new THREE.BufferGeometry(),
new THREE.LineDashedMaterial({ color: 0x00ffff, dashSize: 0.4, gapSize: 0.2, opacity: 0.5, transparent: true })
);
// Fixed: Removed premature call to computeLineDistances() on empty geometry
scene.add(measureLines);
// Gaussian Clouds (Visualizing Variance)
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; // Hide until training starts
scene.add(mesh);
return mesh;
});
// --- MATH & LOGIC ---
function gaussian(x, mean, variance) {
// Prevent division by zero or extremely small variance
const v = Math.max(variance, 0.1) + (alpha * 0.2);
// Add tiny epsilon to avoid pure 0 underflow in exp
return (1 / Math.sqrt(2 * Math.PI * v)) * Math.exp(-Math.pow(x - mean, 2) / (2 * v)) + 1e-9;
}
function generateData() {
dataPoints = [];
pointGroup.clear();
// Offsets to make them distinct
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);
}
});
// Shuffle
dataPoints.sort(() => Math.random() - 0.5);
}
function trainStep() {
if (processedIdx >= dataPoints.length) {
stopTraining();
addLog("Training complete. Model optimized.");
return;
}
// Process a batch for speed
const batchSize = 2;
for(let k=0; k<batchSize && processedIdx < dataPoints.length; k++) {
const p = dataPoints[processedIdx];
const m = model[p.classId];
// Online Mean/Variance Update (Welford's algorithm simplified)
m.count++;
const lr = 1.0 / (m.count + 5); // Decaying learning rate for stability
// Update Mean
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);
// Update Variance (Approximation for visualizer)
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);
// Visual scale based on standard deviation (sqrt of variance)
// Add base size so it doesn't disappear
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 => {
// Naive Bayes Formula: P(Class|Features) ∝ P(Class) * P(F1|Class) * P(F2|Class)...
// 1. Prior: Frequency of class (laplace smoothing)
const prior = (m.count + 1) / (totalPoints + 2);
// 2. Likelihoods for each dimension (Gaussian)
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);
// Store for UI
m.lastCalc = { prior, lW, lS, lC };
m.score = prior * lW * lS * lC;
});
// Normalize probabilities
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 {
// Handle 0 score / underflow case
probA = 50;
probB = 50;
}
updateUI(probA, probB);
updateMeasurementLines();
}
function updateUI(probA, probB) {
// Bars
document.getElementById('bar-a').style.width = `${probA}%`;
document.getElementById('bar-b').style.width = `${probB}%`;
// Text
document.getElementById('prob-a-val').innerText = probA.toFixed(1) + '%';
document.getElementById('prob-b-val').innerText = probB.toFixed(1) + '%';
// Details
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';
// Just showing one likelihood sum for brevity in UI
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);
// Winner Logic (with Neutral zone)
const resultText = document.getElementById('result-text');
const badge = document.getElementById('uncertainty-badge');
if (Math.abs(probA - probB) < 2) {
// Too close to call
resultText.innerText = "NEUTRAL";
resultText.style.color = "#94a3b8"; // slate-400
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;
// Lines projecting to axes for visual reference
const points = [
p.x, p.y, p.z, p.x, -5, p.z, // To floor
p.x, -5, p.z, p.x, -5, 0, // On floor to X-axis
p.x, -5, p.z, 0, -5, p.z // On floor to Z-axis
];
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)}`;
}
// --- CONTROLS ---
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;
// Reset math model
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');
// Reset position
mysteryFruitMesh.position.set(0, 0, 0);
generateData();
addLog("Memory wiped. System reset.");
predict();
}
function selectScenario(idx) {
currentScenario = SCENARIOS[idx];
// Update UI buttons
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);
}
// --- EVENTS ---
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();
};
// 3D Interaction Logic
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;
// Left Click (0): Rotate Camera
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();
}
// Shift + Drag: Lift Object
else if (e.shiftKey) {
mysteryFruitMesh.position.y -= dy * 0.1;
mysteryFruitMesh.position.y = Math.max(-10, Math.min(10, mysteryFruitMesh.position.y));
predict();
}
// Right Click (2): Move Object Plane
else if (mouseButton === 2 || (mouseButton === 0 && e.ctrlKey)) {
// Move relative to camera view roughly
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);
// Bounds
['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 };
});
// Zoom
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);
});
// Animation Loop
function animate() {
requestAnimationFrame(animate);
// Spin the ring
ring.rotation.z -= 0.02;
renderer.render(scene, camera);
}
// Init
generateData();
predict();
animate();
updateUI(50, 50); // Initial neutral state
</script>
</body>
</html>