deedrop1140 commited on
Commit
0406d78
·
verified ·
1 Parent(s): b96bb0c

Upload SVM_Simulator_3D.html

Browse files
Files changed (1) hide show
  1. templates/SVM_Simulator_3D.html +624 -0
templates/SVM_Simulator_3D.html ADDED
@@ -0,0 +1,624 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>SVM 3D Simulator</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- Babel for JSX (Standalone) -->
12
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
13
+
14
+ <!-- Google Fonts -->
15
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@400;500;700&display=swap" rel="stylesheet">
16
+
17
+ <!-- Import Map to resolve dependencies -->
18
+ <script type="importmap">
19
+ {
20
+ "imports": {
21
+ "react": "https://esm.sh/react@18.2.0",
22
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
23
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
24
+ "three": "https://esm.sh/three@0.161.0",
25
+ "@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.16?external=react,react-dom,three",
26
+ "@react-three/drei": "https://esm.sh/@react-three/drei@9.102.0?external=react,react-dom,three,@react-three/fiber"
27
+ }
28
+ }
29
+ </script>
30
+ {% raw %}
31
+
32
+ <style>
33
+ :root {
34
+ --font-sans: 'Inter', sans-serif;
35
+ --font-display: 'Outfit', sans-serif;
36
+ }
37
+ body {
38
+ font-family: var(--font-sans);
39
+ background-color: #020817;
40
+ color: #f8fafc;
41
+ margin: 0;
42
+ overflow: hidden;
43
+ touch-action: none; /* Prevent scrolling on mobile while interacting with canvas */
44
+ }
45
+ .font-display { font-family: var(--font-display); }
46
+
47
+ /* Glassmorphism */
48
+ .glass-panel {
49
+ background: rgba(15, 23, 42, 0.75);
50
+ backdrop-filter: blur(12px);
51
+ border: 1px solid rgba(255, 255, 255, 0.1);
52
+ border-radius: 0.75rem;
53
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
54
+ }
55
+
56
+ /* Animations */
57
+ @keyframes slideUp {
58
+ from { opacity: 0; transform: translateY(20px); }
59
+ to { opacity: 1; transform: translateY(0); }
60
+ }
61
+ .animate-slide-up {
62
+ animation: slideUp 0.6s ease-out forwards;
63
+ }
64
+
65
+ @keyframes pulseGlow {
66
+ 0%, 100% { box-shadow: 0 0 5px #ffd60a; }
67
+ 50% { box-shadow: 0 0 15px #ffd60a; }
68
+ }
69
+ .animate-pulse-glow {
70
+ animation: pulseGlow 2s infinite;
71
+ }
72
+
73
+ /* Neon Text Effects */
74
+ .neon-text-primary { text-shadow: 0 0 10px rgba(56, 189, 248, 0.5); }
75
+ .neon-text-accent { text-shadow: 0 0 10px rgba(255, 214, 10, 0.5); }
76
+
77
+ /* Custom Scrollbar */
78
+ ::-webkit-scrollbar { width: 4px; }
79
+ ::-webkit-scrollbar-track { background: transparent; }
80
+ ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; }
81
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); }
82
+
83
+ /* Hide scrollbar for clean mobile UI but allow scroll */
84
+ .no-scrollbar::-webkit-scrollbar { display: none; }
85
+ .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
86
+
87
+ /* Button Glows */
88
+ .glow-primary { box-shadow: 0 0 15px rgba(56, 189, 248, 0.3); }
89
+ .glow-accent { box-shadow: 0 0 15px rgba(255, 214, 10, 0.3); }
90
+
91
+ .bg-classA { background-color: #00d4ff; }
92
+ .bg-classB { background-color: #ff006e; }
93
+ .bg-supportVector { background-color: #ffd60a; }
94
+ .bg-hyperplane { background-color: #44ff44; }
95
+ .bg-marginLineSVM { background-color: #ef4444; }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div id="root"></div>
100
+
101
+ <script type="text/babel" data-type="module">
102
+ import React, { useState, useEffect, useCallback, useMemo, useRef, Suspense, createContext, useContext } from 'react';
103
+ import { createRoot } from 'react-dom/client';
104
+ import * as THREE from 'three';
105
+ import { Canvas, useFrame } from '@react-three/fiber';
106
+ import { OrbitControls, Stars, Grid } from '@react-three/drei';
107
+
108
+ // --- Utils: Icons ---
109
+ const PlayIcon = ({ className }) => (
110
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polygon points="5 3 19 12 5 21 5 3"/></svg>
111
+ );
112
+ const RotateCcwIcon = ({ className }) => (
113
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74-2.74L3 12"/><path d="M3 3v9h9"/></svg>
114
+ );
115
+ const EyeIcon = ({ className }) => (
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
117
+ );
118
+ const EyeOffIcon = ({ className }) => (
119
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/></svg>
120
+ );
121
+ const LayersIcon = ({ className }) => (
122
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>
123
+ );
124
+ const ChevronLeftIcon = ({ className }) => (
125
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m15 18-6-6 6-6"/></svg>
126
+ );
127
+ const ChevronRightIcon = ({ className }) => (
128
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m9 18 6-6-6-6"/></svg>
129
+ );
130
+
131
+ // --- Utils: SVM Logic ---
132
+ const generateSVMData = () => {
133
+ const points = [];
134
+ const w1 = 0.5, w2 = -0.3, b = 0.5;
135
+
136
+ for(let i=0; i<40; i++) {
137
+ const x = (Math.random() - 0.5) * 8;
138
+ const z = (Math.random() - 0.5) * 8;
139
+ const boundaryY = -(w1 * x + w2 * z + b);
140
+ const isClassA = Math.random() > 0.5;
141
+ const gap = 0.8 + Math.random() * 2;
142
+ const y = isClassA ? boundaryY + gap : boundaryY - gap;
143
+ points.push({ position: [x, y, z], classLabel: isClassA ? 1 : -1 });
144
+ }
145
+ return points;
146
+ };
147
+
148
+ const computeHyperplane = (dataPoints) => ({ w1: 0.5, w2: -0.3, b: 0.5 });
149
+
150
+ const findSupportVectors = (dataPoints, params, margin) => {
151
+ const indices = [];
152
+ const { w1, w2, b } = params;
153
+ dataPoints.forEach((point, index) => {
154
+ const [x, y, z] = point.position;
155
+ const predictedY = -(w1 * x + w2 * z + b);
156
+ const distance = Math.abs(y - predictedY);
157
+ if (distance < margin + 0.5) indices.push(index);
158
+ });
159
+ return indices;
160
+ };
161
+
162
+ // --- UI Components ---
163
+ const Button = ({ children, variant = 'default', size = 'default', className = '', ...props }) => {
164
+ const baseStyles = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 touch-manipulation";
165
+ const variants = {
166
+ default: "bg-slate-100 text-slate-900 hover:bg-slate-100/90 shadow",
167
+ outline: "border border-slate-700 bg-slate-900/50 hover:bg-slate-800 text-slate-100",
168
+ ghost: "hover:bg-slate-800 text-slate-100"
169
+ };
170
+ const sizes = {
171
+ default: "h-8 md:h-9 px-3 md:px-4 py-1 md:py-2 text-xs md:text-sm",
172
+ sm: "h-7 md:h-8 rounded-md px-2 md:px-3 text-[10px] md:text-xs",
173
+ icon: "h-8 w-8 md:h-9 md:w-9"
174
+ };
175
+ return (
176
+ <button className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`} {...props}>
177
+ {children}
178
+ </button>
179
+ );
180
+ };
181
+
182
+ // Toast System
183
+ const ToastContext = createContext(null);
184
+ const ToastProvider = ({ children }) => {
185
+ const [toasts, setToasts] = useState([]);
186
+
187
+ const addToast = useCallback((message, type = 'info') => {
188
+ const id = Date.now();
189
+ setToasts(prev => [...prev, { id, message, type }]);
190
+ setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000);
191
+ }, []);
192
+
193
+ return (
194
+ <ToastContext.Provider value={addToast}>
195
+ {children}
196
+ <div className="fixed bottom-20 left-4 right-4 md:bottom-4 md:right-4 md:left-auto md:w-auto z-[60] flex flex-col gap-2 pointer-events-none">
197
+ {toasts.map(t => (
198
+ <div key={t.id} className={`p-4 rounded-lg shadow-lg text-sm font-medium animate-slide-up mx-auto md:mx-0 w-full md:w-72 text-center md:text-left ${
199
+ t.type === 'success' ? 'bg-green-600/90 text-white backdrop-blur' : 'bg-slate-800/90 text-white border border-slate-700 backdrop-blur'
200
+ }`}>{t.message}</div>
201
+ ))}
202
+ </div>
203
+ </ToastContext.Provider>
204
+ );
205
+ };
206
+ const useToast = () => {
207
+ const context = useContext(ToastContext);
208
+ if (!context) throw new Error("useToast must be used within ToastProvider");
209
+ return { info: (msg) => context(msg, 'info'), success: (msg) => context(msg, 'success') };
210
+ };
211
+
212
+
213
+ // --- 3D Components ---
214
+ const DataPoint = ({ position, classLabel, isSupportVector }) => {
215
+ const meshRef = useRef(null);
216
+ const glowRef = useRef(null);
217
+ useFrame((state) => {
218
+ if (meshRef.current) {
219
+ meshRef.current.position.y = position[1] + Math.sin(state.clock.elapsedTime * 2 + position[0]) * 0.03;
220
+ if (isSupportVector) meshRef.current.scale.setScalar(1 + Math.sin(state.clock.elapsedTime * 3) * 0.15);
221
+ }
222
+ if (glowRef.current && isSupportVector) glowRef.current.rotation.z = state.clock.elapsedTime * 0.5;
223
+ });
224
+ const color = classLabel === 1 ? "#00d4ff" : "#ff006e";
225
+ const size = isSupportVector ? 0.28 : 0.22;
226
+ return (
227
+ <group position={position}>
228
+ <mesh ref={meshRef}>
229
+ <sphereGeometry args={[size, 32, 32]} />
230
+ <meshPhongMaterial color={color} emissive={color} emissiveIntensity={isSupportVector ? 1 : 0.6} shininess={100} />
231
+ </mesh>
232
+ <mesh>
233
+ <sphereGeometry args={[size * 0.6, 16, 16]} />
234
+ <meshBasicMaterial color={color} transparent opacity={0.9} />
235
+ </mesh>
236
+ <mesh>
237
+ <sphereGeometry args={[size * 1.4, 16, 16]} />
238
+ <meshBasicMaterial color={color} transparent opacity={0.15} />
239
+ </mesh>
240
+ {isSupportVector && (
241
+ <mesh ref={glowRef} rotation={[Math.PI / 2, 0, 0]}>
242
+ <torusGeometry args={[0.4, 0.03, 16, 32]} />
243
+ <meshBasicMaterial color="#ffd60a" transparent opacity={0.8} />
244
+ </mesh>
245
+ )}
246
+ </group>
247
+ );
248
+ };
249
+
250
+ const Hyperplane = ({ params }) => {
251
+ const meshRef = useRef(null);
252
+ useFrame((state) => {
253
+ if (meshRef.current) meshRef.current.material.emissiveIntensity = 0.3 + Math.sin(state.clock.elapsedTime * 2) * 0.1;
254
+ });
255
+ const geometry = useMemo(() => {
256
+ const { w1, w2, b } = params;
257
+ const size = 7;
258
+ const segments = 30;
259
+ const positions = [], colors = [], indices = [];
260
+ for (let i = 0; i <= segments; i++) {
261
+ for (let j = 0; j <= segments; j++) {
262
+ const x = (i / segments - 0.5) * size * 2;
263
+ const z = (j / segments - 0.5) * size * 2;
264
+ const y = -(w1 * x + w2 * z + b);
265
+ const clampedY = Math.max(-4, Math.min(4, y));
266
+ positions.push(x, clampedY, z);
267
+ const normalizedY = (clampedY + 4) / 8;
268
+ const normalizedX = i / segments;
269
+ colors.push(0.2 + normalizedX * 0.8, 0.8 - normalizedY * 0.6, 0.4);
270
+ }
271
+ }
272
+ for (let i = 0; i < segments; i++) {
273
+ for (let j = 0; j < segments; j++) {
274
+ const a = i * (segments + 1) + j;
275
+ const b_idx = a + 1;
276
+ const c = a + segments + 1;
277
+ const d = c + 1;
278
+ indices.push(a, b_idx, c);
279
+ indices.push(b_idx, d, c);
280
+ }
281
+ }
282
+ const geo = new THREE.BufferGeometry();
283
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
284
+ geo.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
285
+ geo.setIndex(indices);
286
+ geo.computeVertexNormals();
287
+ return geo;
288
+ }, [params]);
289
+ const wireframeGeometry = useMemo(() => new THREE.WireframeGeometry(geometry), [geometry]);
290
+ return (
291
+ <group>
292
+ <mesh ref={meshRef} geometry={geometry}>
293
+ <meshPhongMaterial vertexColors transparent opacity={0.6} side={THREE.DoubleSide} emissive="#44ff44" emissiveIntensity={0.2} shininess={100} />
294
+ </mesh>
295
+ <lineSegments geometry={wireframeGeometry}>
296
+ <lineBasicMaterial color="#00ffff" transparent opacity={0.3} />
297
+ </lineSegments>
298
+ </group>
299
+ );
300
+ };
301
+
302
+ const MarginPlanes = ({ params, margin }) => {
303
+ const createPlaneGeometry = (offset) => {
304
+ const { w1, w2, b } = params;
305
+ const size = 6;
306
+ const segments = 20;
307
+ const positions = [], indices = [];
308
+ for (let i = 0; i <= segments; i++) {
309
+ for (let j = 0; j <= segments; j++) {
310
+ const x = (i / segments - 0.5) * size * 2;
311
+ const z = (j / segments - 0.5) * size * 2;
312
+ const y = -(w1 * x + w2 * z + b) + offset;
313
+ positions.push(x, Math.max(-3, Math.min(3, y)), z);
314
+ }
315
+ }
316
+ for (let i = 0; i < segments; i++) {
317
+ for (let j = 0; j < segments; j++) {
318
+ const a = i * (segments + 1) + j;
319
+ const b_idx = a + 1;
320
+ const c = a + segments + 1;
321
+ const d = c + 1;
322
+ indices.push(a, b_idx, c);
323
+ indices.push(b_idx, d, c);
324
+ }
325
+ }
326
+ const geo = new THREE.BufferGeometry();
327
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
328
+ geo.setIndex(indices);
329
+ geo.computeVertexNormals();
330
+ return geo;
331
+ };
332
+ const upperGeometry = useMemo(() => createPlaneGeometry(margin), [params, margin]);
333
+ const lowerGeometry = useMemo(() => createPlaneGeometry(-margin), [params, margin]);
334
+ const color = '#ef4444';
335
+ return (
336
+ <>
337
+ <mesh geometry={upperGeometry}><meshStandardMaterial color={color} transparent opacity={0.4} side={THREE.DoubleSide} emissive={color} emissiveIntensity={0.2} /></mesh>
338
+ <mesh geometry={lowerGeometry}><meshStandardMaterial color={color} transparent opacity={0.4} side={THREE.DoubleSide} emissive={color} emissiveIntensity={0.2} /></mesh>
339
+ </>
340
+ );
341
+ };
342
+
343
+ const GridFloor = () => (
344
+ <Grid position={[0, -3, 0]} args={[20, 20]} cellSize={1} cellThickness={0.5} cellColor="#1a3a4a" sectionSize={5} sectionThickness={1} sectionColor="#2a5a6a" fadeDistance={25} fadeStrength={1} infiniteGrid={true} />
345
+ );
346
+
347
+ const Scene3D = ({ dataPoints, supportVectorIndices, showHyperplane, showMargins, hyperplaneParams, margin, sceneRotation }) => (
348
+ <Canvas camera={{ position: [8, 6, 8], fov: 50 }}>
349
+ <Suspense fallback={null}>
350
+ <group rotation={[0, sceneRotation, 0]}>
351
+ <ambientLight intensity={0.4} />
352
+ <pointLight position={[10, 10, 10]} intensity={1.5} color="#00d4ff" />
353
+ <pointLight position={[-10, 10, -10]} intensity={1} color="#ff006e" />
354
+ <pointLight position={[0, -10, 0]} intensity={0.5} color="#ffd60a" />
355
+ <directionalLight position={[5, 5, 5]} intensity={0.8} />
356
+ <spotLight position={[0, 15, 0]} intensity={1} angle={0.5} penumbra={0.5} color="#ffffff" />
357
+ <Stars radius={100} depth={50} count={3000} factor={4} saturation={0} fade speed={1} />
358
+ <GridFloor />
359
+ {dataPoints.map((point, index) => (
360
+ <DataPoint key={index} position={point.position} classLabel={point.classLabel} isSupportVector={supportVectorIndices.includes(index)} />
361
+ ))}
362
+ {showHyperplane && <Hyperplane params={hyperplaneParams} />}
363
+ {showMargins && showHyperplane && <MarginPlanes params={hyperplaneParams} margin={margin} />}
364
+ </group>
365
+ <OrbitControls enablePan={true} enableZoom={true} enableRotate={true} minDistance={5} maxDistance={30} autoRotateSpeed={0.5} />
366
+ </Suspense>
367
+ </Canvas>
368
+ );
369
+
370
+ // --- Panel Components ---
371
+ const ControlPanel = ({
372
+ showHyperplane, onToggleHyperplane,
373
+ showMargins, onToggleMargins,
374
+ onRegenerateData, onRunAlgorithm,
375
+ isRunning, margin, onMarginChange,
376
+ isResultView, onResetView
377
+ }) => {
378
+ // NEW: Result View Logic for Mobile Optimization
379
+ if (isResultView) {
380
+ return (
381
+ <div className="glass-panel p-3 md:p-4 animate-slide-up w-full text-center">
382
+ <div className="bg-green-500/20 border border-green-500/50 rounded-lg p-2 mb-3">
383
+ <p className="text-green-400 text-sm font-semibold flex items-center justify-center gap-2">
384
+ <span className="relative flex h-3 w-3">
385
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
386
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
387
+ </span>
388
+ Separation Complete
389
+ </p>
390
+ </div>
391
+ <Button onClick={onResetView} className="w-full bg-slate-700 hover:bg-slate-600 text-white shadow-lg h-10 font-bold border border-slate-600">
392
+ <RotateCcwIcon className="w-4 h-4 mr-2" />
393
+ Continue Editing
394
+ </Button>
395
+ </div>
396
+ );
397
+ }
398
+
399
+ // Default View
400
+ return (
401
+ <div className="glass-panel p-3 md:p-4 space-y-2 md:space-y-4 animate-slide-up w-full">
402
+ <h2 className="font-display text-lg font-bold text-white neon-text-primary hidden md:block">Controls</h2>
403
+ <div className="space-y-2">
404
+ <Button onClick={onRunAlgorithm} disabled={isRunning} className="w-full bg-green-500 hover:bg-green-600 text-white glow-primary font-semibold shadow-lg">
405
+ <PlayIcon className="w-4 h-4 mr-2" />
406
+ {isRunning ? "Running..." : "Find Hyperplane"}
407
+ </Button>
408
+ <Button onClick={onRegenerateData} variant="outline" className="w-full shadow-lg">
409
+ <RotateCcwIcon className="w-4 h-4 mr-2" />
410
+ New Data Points
411
+ </Button>
412
+ </div>
413
+ <div className="space-y-2">
414
+ <label className="text-sm text-slate-400 hidden md:block">Visibility</label>
415
+ <div className="flex gap-2">
416
+ <Button variant={showHyperplane ? "default" : "outline"} size="sm" onClick={onToggleHyperplane} className="flex-1">
417
+ {showHyperplane ? <EyeIcon className="w-4 h-4" /> : <EyeOffIcon className="w-4 h-4" />}
418
+ <span className="ml-2">Plane</span>
419
+ </Button>
420
+ <Button variant={showMargins ? "default" : "outline"} size="sm" onClick={onToggleMargins} className="flex-1">
421
+ <LayersIcon className="w-4 h-4" />
422
+ <span className="ml-2">Margins</span>
423
+ </Button>
424
+ </div>
425
+ </div>
426
+ <div className="space-y-2 hidden md:block">
427
+ <label className="text-sm text-slate-400">Margin Width: {margin.toFixed(2)}</label>
428
+ <input type="range" min="0.3" max="2" step="0.1" value={margin} onChange={(e) => onMarginChange(parseFloat(e.target.value))} className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-cyan-500" />
429
+ </div>
430
+ </div>
431
+ );
432
+ };
433
+
434
+ const LegendPanel = () => (
435
+ <div className="glass-panel p-3 md:p-4 space-y-2 md:space-y-3 animate-slide-up w-full">
436
+ <h2 className="font-display text-lg font-bold hidden md:block">Legend</h2>
437
+ <div className="space-y-2 text-xs md:text-sm">
438
+ <div className="flex items-center gap-3"><div className="w-4 h-4 rounded-full bg-classA glow-primary flex-shrink-0" /><span className="text-slate-200">Class A (Positive)</span></div>
439
+ <div className="flex items-center gap-3"><div className="w-4 h-4 rounded-full bg-classB shadow-[0_0_10px_#ff006e] flex-shrink-0" /><span className="text-slate-200">Class B (Negative)</span></div>
440
+ <div className="flex items-center gap-3"><div className="w-4 h-4 rounded-full bg-supportVector glow-accent animate-pulse-glow flex-shrink-0" /><span className="text-slate-200">Support Vectors</span></div>
441
+ <div className="flex items-center gap-3"><div className="w-4 h-2 rounded bg-hyperplane opacity-60 flex-shrink-0" /><span className="text-slate-200">Decision Boundary</span></div>
442
+ <div className="flex items-center gap-3"><div className="w-4 h-2 rounded opacity-80 bg-marginLineSVM flex-shrink-0" /><span className="text-slate-200">Margin Boundaries (Red)</span></div>
443
+ </div>
444
+ </div>
445
+ );
446
+
447
+ const TutorialPanel = () => {
448
+ const [currentStep, setCurrentStep] = useState(0);
449
+ const svmSteps = [
450
+ { title: "What is SVM?", content: "Support Vector Machine (SVM) finds the best boundary (hyperplane) to separate two classes." },
451
+ { title: "Data Points", content: "Cyan points are Class A, Magenta are Class B. The goal is to separate them cleanly." },
452
+ { title: "The Hyperplane", content: "The green transparent surface is the decision boundary. One side is A, the other B." },
453
+ { title: "Support Vectors", content: "YELLOW glowing points are Support Vectors! They define where the boundary sits." },
454
+ { title: "The Margin (Red)", content: "RED planes show the MARGIN. SVM maximizes this street width. Points should be OUTSIDE." },
455
+ { title: "Max Margin", content: "A wider margin means the model is more confident and generalizes better." },
456
+ ];
457
+ const steps = svmSteps;
458
+ const nextStep = () => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
459
+ const prevStep = () => setCurrentStep(prev => Math.max(prev - 1, 0));
460
+ return (
461
+ <div className="glass-panel p-3 md:p-4 space-y-2 md:space-y-4 animate-slide-up w-full">
462
+ <div className="flex items-center justify-between">
463
+ <h2 className="font-display text-md md:text-lg font-bold neon-text-accent">Learn SVM</h2>
464
+ <span className="text-xs text-slate-400">{currentStep + 1} / {steps.length}</span>
465
+ </div>
466
+ <div className="min-h-[60px] md:min-h-[100px]">
467
+ <h3 className="font-display text-sm md:text-md text-cyan-400 mb-1 md:mb-2 font-semibold">{steps[currentStep].title}</h3>
468
+ <p className="text-xs md:text-sm text-slate-300 leading-relaxed">{steps[currentStep].content}</p>
469
+ </div>
470
+ <div className="flex items-center justify-between gap-2">
471
+ <Button variant="outline" size="sm" onClick={prevStep} disabled={currentStep === 0}><ChevronLeftIcon className="w-4 h-4" /></Button>
472
+ <div className="flex gap-1">
473
+ {steps.map((_, i) => (
474
+ <div key={i} className={`w-1.5 h-1.5 rounded-full transition-all ${i === currentStep ? "bg-yellow-400 scale-125" : i < currentStep ? "bg-cyan-500/50" : "bg-slate-700"}`} />
475
+ ))}
476
+ </div>
477
+ <Button variant="outline" size="sm" onClick={nextStep} disabled={currentStep === steps.length - 1}><ChevronRightIcon className="w-4 h-4" /></Button>
478
+ </div>
479
+ </div>
480
+ );
481
+ };
482
+
483
+ // --- Main App Component ---
484
+ const App = () => {
485
+ const toast = useToast();
486
+ const [dataPoints, setDataPoints] = useState([]);
487
+ const [supportVectorIndices, setSupportVectorIndices] = useState([]);
488
+ const [hyperplaneParams, setHyperplaneParams] = useState({ w1: 0.3, w2: 0.1, b: 0 });
489
+ const [margin, setMargin] = useState(0.8);
490
+ const [showHyperplane, setShowHyperplane] = useState(false);
491
+ const [showMargins, setShowMargins] = useState(false);
492
+ const [isRunning, setIsRunning] = useState(false);
493
+ const [sceneRotation, setSceneRotation] = useState(0);
494
+
495
+ // New state for mobile toggle
496
+ const [isResultView, setIsResultView] = useState(false);
497
+
498
+ const regenerateData = useCallback(() => {
499
+ const newData = generateSVMData();
500
+ setDataPoints(newData);
501
+ setSupportVectorIndices([]);
502
+ setShowHyperplane(false);
503
+ setShowMargins(false);
504
+ setIsResultView(false); // Reset view on new data
505
+ if(dataPoints.length > 0) toast.success(`Generated ${newData.length} new data points!`);
506
+ }, [toast, dataPoints.length]);
507
+
508
+ useEffect(() => {
509
+ regenerateData();
510
+ }, []);
511
+
512
+ const runAlgorithm = async () => {
513
+ setIsRunning(true);
514
+ toast.info("Finding optimal hyperplane...");
515
+ await new Promise(r => setTimeout(r, 800));
516
+
517
+ const params = computeHyperplane(dataPoints);
518
+ setHyperplaneParams(params);
519
+ setShowHyperplane(true);
520
+
521
+ await new Promise(r => setTimeout(r, 500));
522
+
523
+ const svIndices = findSupportVectors(dataPoints, params, margin);
524
+ setSupportVectorIndices(svIndices);
525
+ setShowMargins(true);
526
+
527
+ setIsRunning(false);
528
+ setIsResultView(true); // Switch to result view
529
+ toast.success(`Found ${svIndices.length} support vectors!`);
530
+ };
531
+
532
+ return (
533
+ <div className="h-[100dvh] w-full bg-slate-950 overflow-hidden relative font-sans text-slate-100">
534
+ {/* Header */}
535
+ <header className="absolute top-0 left-0 right-0 z-40 p-3 md:p-6 pointer-events-none bg-gradient-to-b from-slate-950/80 to-transparent">
536
+ <div className="flex flex-col md:flex-row items-center md:items-start justify-between gap-2 md:gap-4 pointer-events-auto">
537
+ <div className="text-center md:text-left">
538
+ <h1 className="font-display text-xl md:text-3xl font-bold text-white neon-text-primary">SVM 3D Simulator</h1>
539
+ <p className="text-xs md:text-sm text-slate-400 mt-1 hidden md:block">Interactive Machine Learning Visualization</p>
540
+ </div>
541
+ <div className="hidden md:flex items-center gap-2 text-sm text-slate-500 backdrop-blur-sm bg-slate-900/30 p-2 rounded-full border border-slate-800">
542
+ <span>🖱️ Drag to rotate</span>
543
+ <span>•</span>
544
+ <span>🔍 Scroll to zoom</span>
545
+ </div>
546
+ </div>
547
+ </header>
548
+
549
+ {/* 3D Scene */}
550
+ <div className="absolute inset-0 z-0">
551
+ <Scene3D
552
+ dataPoints={dataPoints}
553
+ supportVectorIndices={supportVectorIndices}
554
+ showHyperplane={showHyperplane}
555
+ showMargins={showMargins}
556
+ hyperplaneParams={hyperplaneParams}
557
+ margin={margin}
558
+ sceneRotation={sceneRotation}
559
+ />
560
+ </div>
561
+
562
+ {/* Controls Container: Mobile Top, Desktop Left */}
563
+ <div className="absolute left-4 right-4 md:right-auto top-[4.5rem] md:top-28 md:w-64 space-y-3 md:space-y-4 z-30 max-h-[45vh] overflow-y-auto md:max-h-none md:overflow-visible no-scrollbar">
564
+ <ControlPanel
565
+ showHyperplane={showHyperplane}
566
+ onToggleHyperplane={() => setShowHyperplane(!showHyperplane)}
567
+ showMargins={showMargins}
568
+ onToggleMargins={() => setShowMargins(!showMargins)}
569
+ onRegenerateData={regenerateData}
570
+ onRunAlgorithm={runAlgorithm}
571
+ isRunning={isRunning}
572
+ margin={margin}
573
+ onMarginChange={setMargin}
574
+ isResultView={isResultView}
575
+ onResetView={() => setIsResultView(false)}
576
+ />
577
+ {/* Hide legend in result view on mobile to save space if needed, though instruction said only buttons invisible */}
578
+ <LegendPanel />
579
+ </div>
580
+
581
+ {/* Tutorial Container: Mobile Bottom, Desktop Right */}
582
+ <div className="absolute left-4 right-4 md:left-auto md:right-4 bottom-20 md:bottom-auto md:top-28 md:w-72 z-20">
583
+ <TutorialPanel />
584
+ </div>
585
+
586
+ {/* Rotation Slider: Hidden on Mobile */}
587
+ <div className="hidden md:flex absolute right-6 top-1/2 -translate-y-1/2 z-20 items-center justify-center bg-slate-900/80 p-3 rounded-xl border border-slate-700 backdrop-blur-md">
588
+ <div className="flex flex-col items-center gap-2 h-64 w-8 relative">
589
+ <span className="text-xs font-medium text-slate-300 absolute -top-6">Rot</span>
590
+ <input
591
+ type="range"
592
+ min={0}
593
+ max={Math.PI * 2}
594
+ step={0.01}
595
+ value={sceneRotation}
596
+ onChange={(e) => setSceneRotation(parseFloat(e.target.value))}
597
+ className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-56 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-cyan-500 -rotate-90 origin-center"
598
+ />
599
+ </div>
600
+ </div>
601
+
602
+ {/* Footer */}
603
+ <footer className="absolute bottom-4 left-0 right-0 z-10 text-center pointer-events-none">
604
+ <div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-full flex flex-col items-center gap-2 pointer-events-auto pb-4 md:pb-0">
605
+ <p className="hidden md:block text-xs text-slate-500 backdrop-blur-sm px-3 py-1 rounded-full mb-2">
606
+ Click "Find Hyperplane" to see the algorithm in action!
607
+ </p>
608
+
609
+ <a href="/svm" onClick={() => {}} className="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 md:py-2 md: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">
610
+ Back to Core
611
+ </a>
612
+ </div>
613
+ </footer>
614
+ </div>
615
+ );
616
+ };
617
+
618
+ const Root = () => (<ToastProvider><App /></ToastProvider>);
619
+ const root = createRoot(document.getElementById('root'));
620
+ root.render(<Root />);
621
+ </script>
622
+ {% endraw %}
623
+ </body>
624
+ </html>