deedrop1140 commited on
Commit
42001a3
·
verified ·
1 Parent(s): fc49ccf

Upload 18 files

Browse files
templates/Apriori-Simulator-three.html ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Apriori Algorithm Simulator | Learn Data Mining Interactively</title>
7
+ <!-- Dependencies -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
10
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
11
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
13
+
14
+ <style>
15
+ body {
16
+ font-family: 'Inter', sans-serif;
17
+ background-color: #f8fafc;
18
+ }
19
+ .gradient-hero {
20
+ background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
21
+ }
22
+ .text-gradient {
23
+ background: linear-gradient(to right, #2563eb, #7c3aed);
24
+ -webkit-background-clip: text;
25
+ -webkit-text-fill-color: transparent;
26
+ }
27
+ .shadow-card {
28
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
29
+ }
30
+ .shadow-soft {
31
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
32
+ }
33
+ @keyframes slideUp {
34
+ from { opacity: 0; transform: translateY(20px); }
35
+ to { opacity: 1; transform: translateY(0); }
36
+ }
37
+ .animate-slide-up {
38
+ animation: slideUp 0.5s ease-out forwards;
39
+ }
40
+ @keyframes scaleIn {
41
+ from { opacity: 0; transform: scale(0.9); }
42
+ to { opacity: 1; transform: scale(1); }
43
+ }
44
+ .animate-scale-in {
45
+ animation: scaleIn 0.3s ease-out forwards;
46
+ }
47
+ .gradient-primary {
48
+ background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div id="root"></div>
54
+
55
+ <script type="text/babel">
56
+ const { useState, useEffect, useMemo } = React;
57
+
58
+ // --- Data & Helpers ---
59
+ const groceryItems = [
60
+ { id: "bread", name: "Bread", emoji: "🍞", category: "bakery" },
61
+ { id: "milk", name: "Milk", emoji: "🥛", category: "dairy" },
62
+ { id: "butter", name: "Butter", emoji: "🧈", category: "dairy" },
63
+ { id: "eggs", name: "Eggs", emoji: "🥚", category: "dairy" },
64
+ { id: "cheese", name: "Cheese", emoji: "🧀", category: "dairy" },
65
+ { id: "apple", name: "Apple", emoji: "🍎", category: "fruits" },
66
+ { id: "banana", name: "Banana", emoji: "🍌", category: "fruits" },
67
+ { id: "coffee", name: "Coffee", emoji: "☕", category: "beverages" },
68
+ { id: "cereal", name: "Cereal", emoji: "🥣", category: "breakfast" },
69
+ { id: "yogurt", name: "Yogurt", emoji: "🥛", category: "dairy" },
70
+ ];
71
+
72
+ const getItemEmoji = (id) => groceryItems.find(i => i.id === id)?.emoji || "📦";
73
+ const getItemDisplayName = (id) => groceryItems.find(i => i.id === id)?.name || id;
74
+
75
+ // --- Logic Engine ---
76
+ const transactions = [
77
+ { id: 1, items: ["bread", "milk", "butter"], customer: "👩" },
78
+ { id: 2, items: ["bread", "eggs", "milk"], customer: "👨" },
79
+ { id: 3, items: ["milk", "butter", "cheese"], customer: "👵" },
80
+ { id: 4, items: ["bread", "milk", "butter", "eggs"], customer: "👦" },
81
+ { id: 5, items: ["bread", "butter"], customer: "👧" },
82
+ { id: 6, items: ["milk", "eggs", "cheese"], customer: "👴" },
83
+ { id: 7, items: ["bread", "milk", "butter", "cheese"], customer: "👩‍🦰" },
84
+ { id: 8, items: ["bread", "milk"], customer: "👨‍🦱" },
85
+ ];
86
+
87
+ // --- UI Components ---
88
+
89
+ const Icon = ({ name, size = 20, className = "" }) => {
90
+ const icons = {
91
+ "lightbulb": <><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .5 2.2 1.5 3.1.7.9 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></>,
92
+ "search": <><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>,
93
+ "rotate-ccw": <><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></>,
94
+ "chevron-left": <path d="m15 18-6-6 6-6"/>,
95
+ "chevron-right": <path d="m9 18 6-6 6-6"/>,
96
+ "shopping-cart": <><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></>,
97
+ "sparkles": <><path d="m12 3 1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></>,
98
+ "arrow-right": <><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></>,
99
+ "trending-up": <><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></>
100
+ };
101
+
102
+ return (
103
+ <svg
104
+ xmlns="http://www.w3.org/2000/svg"
105
+ width={size}
106
+ height={size}
107
+ viewBox="0 0 24 24"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ className={className}
114
+ >
115
+ {icons[name] || null}
116
+ </svg>
117
+ );
118
+ };
119
+
120
+ const Button = ({ children, onClick, disabled, variant = "default", size = "md", className = "" }) => {
121
+ const variants = {
122
+ default: "bg-slate-900 text-white hover:bg-slate-800",
123
+ outline: "border border-slate-200 bg-white hover:bg-slate-50 text-slate-700",
124
+ hero: "gradient-primary text-white shadow-lg hover:opacity-90",
125
+ icon: "p-2 border border-slate-200 hover:bg-slate-50"
126
+ };
127
+ const sizes = {
128
+ md: "px-4 py-2",
129
+ lg: "px-8 py-3 text-lg font-bold"
130
+ };
131
+ return (
132
+ <button
133
+ onClick={onClick}
134
+ disabled={disabled}
135
+ className={`inline-flex items-center justify-center rounded-xl transition-all active:scale-95 disabled:opacity-50 disabled:active:scale-100 ${variants[variant]} ${sizes[size]} ${className}`}
136
+ >
137
+ {children}
138
+ </button>
139
+ );
140
+ };
141
+
142
+ // --- Main Simulator ---
143
+ const steps = [
144
+ { id: 0, title: "Meet Sarah, the Store Owner 👩‍💼", subtitle: "She wants to understand her customers better" },
145
+ { id: 1, title: "Sarah looks at shopping receipts 🧾", subtitle: "8 customers visited her store today" },
146
+ { id: 2, title: "Let's count each item! 🔢", subtitle: "How many times did each item appear?" },
147
+ { id: 3, title: "Find the popular items! ⭐", subtitle: "Items bought by at least 3 customers are 'frequent'" },
148
+ { id: 4, title: "Which items are bought TOGETHER? 🤝", subtitle: "Let's check pairs of frequent items" },
149
+ { id: 5, title: "Sarah discovers patterns! 💡", subtitle: "These are called 'Association Rules'" },
150
+ { id: 6, title: "Sarah can use these insights! 🎯", subtitle: "Now she knows how to arrange her store" },
151
+ ];
152
+
153
+ const AprioriSimulator = () => {
154
+ const [currentStep, setCurrentStep] = useState(0);
155
+
156
+ const nextStep = () => currentStep < steps.length - 1 && setCurrentStep(currentStep + 1);
157
+ const prevStep = () => currentStep > 0 && setCurrentStep(currentStep - 1);
158
+ const reset = () => setCurrentStep(0);
159
+
160
+ // Audio Handler
161
+ const playSound = (e) => {
162
+ e.preventDefault(); // Prevent default anchor behavior to allow sound to play
163
+ const audio = document.getElementById('clickSound');
164
+ if (audio) {
165
+ audio.currentTime = 0;
166
+ audio.play().catch(err => console.log("Audio play prevented:", err));
167
+ }
168
+
169
+ // Navigate after a short delay for the sound
170
+ setTimeout(() => {
171
+ window.location.href = "/xgboost-regression";
172
+ }, 150);
173
+ };
174
+
175
+ // Calculations
176
+ const itemCounts = useMemo(() => {
177
+ const counts = {};
178
+ transactions.forEach(t => t.items.forEach(item => counts[item] = (counts[item] || 0) + 1));
179
+ return counts;
180
+ }, []);
181
+
182
+ const frequentItems = Object.entries(itemCounts).filter(([_, c]) => c >= 3).map(([i]) => i);
183
+
184
+ const pairCounts = useMemo(() => {
185
+ const counts = {};
186
+ transactions.forEach(t => {
187
+ for (let i = 0; i < t.items.length; i++) {
188
+ for (let j = i + 1; j < t.items.length; j++) {
189
+ const pair = [t.items[i], t.items[j]].sort().join("+");
190
+ if (frequentItems.includes(t.items[i]) && frequentItems.includes(t.items[j])) {
191
+ counts[pair] = (counts[pair] || 0) + 1;
192
+ }
193
+ }
194
+ }
195
+ });
196
+ return counts;
197
+ }, [frequentItems]);
198
+
199
+ const frequentPairs = Object.entries(pairCounts).filter(([_, c]) => c >= 3).sort((a,b) => b[1]-a[1]);
200
+
201
+ // Define progress bar style outside of JSX to avoid double-curly-brace pattern
202
+ const progressStyle = { width: `${((currentStep + 1) / steps.length) * 100}%` };
203
+
204
+ const renderStepContent = () => {
205
+ switch (currentStep) {
206
+ case 0: return (
207
+ <div className="text-center animate-scale-in">
208
+ <div className="text-8xl mb-6">👩‍💼</div>
209
+ <div className="bg-white rounded-3xl p-8 shadow-card max-w-md mx-auto border border-blue-50">
210
+ <p className="text-xl leading-relaxed text-slate-700">
211
+ "Hi! I'm <strong>Sarah</strong>. I own a small grocery store.
212
+ I noticed some customers buy certain items together..."
213
+ </p>
214
+ <p className="text-xl leading-relaxed mt-4 text-slate-700">
215
+ "I want to find these <strong>patterns</strong> so I can place
216
+ related items close together!"
217
+ </p>
218
+ </div>
219
+ <div className="mt-8 flex justify-center gap-6 text-5xl">
220
+ 🍞 🥛 🧈 🥚 🧀
221
+ </div>
222
+ </div>
223
+ );
224
+ case 1: return (
225
+ <div className="animate-slide-up">
226
+ <div className="grid gap-3 max-w-2xl mx-auto">
227
+ {transactions.map((t, idx) => {
228
+ // Removed double-curly-brace style pattern for Flask compatibility
229
+ const animStyle = { animationDelay: `${idx * 100}ms` };
230
+ return (
231
+ <div key={t.id} className="flex items-center gap-4 bg-white rounded-2xl p-4 shadow-soft border border-slate-100 animate-slide-up" style={animStyle}>
232
+ <div className="text-3xl">{t.customer}</div>
233
+ <div className="text-2xl opacity-50">🛒</div>
234
+ <div className="flex flex-wrap gap-2">
235
+ {t.items.map(item => (
236
+ <span key={item} className="bg-slate-100 text-slate-700 px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1 border border-slate-200">
237
+ <span className="text-lg">{getItemEmoji(item)}</span>
238
+ {getItemDisplayName(item)}
239
+ </span>
240
+ ))}
241
+ </div>
242
+ </div>
243
+ );
244
+ })}
245
+ </div>
246
+ </div>
247
+ );
248
+ case 2: return (
249
+ <div className="animate-scale-in">
250
+ <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 max-w-3xl mx-auto">
251
+ {Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count], idx) => (
252
+ <div key={item} className="bg-white rounded-2xl p-6 shadow-soft text-center border border-slate-100">
253
+ <div className="text-5xl mb-2">{getItemEmoji(item)}</div>
254
+ <div className="font-bold text-lg text-slate-800">{getItemDisplayName(item)}</div>
255
+ <div className="mt-2 text-4xl font-extrabold text-blue-600">{count}</div>
256
+ <div className="text-sm text-slate-400 font-medium uppercase tracking-wider">Times Bought</div>
257
+ </div>
258
+ ))}
259
+ </div>
260
+ </div>
261
+ );
262
+ case 3: return (
263
+ <div className="animate-scale-in">
264
+ <div className="text-center mb-8">
265
+ <div className="inline-block bg-blue-50 text-blue-700 px-6 py-3 rounded-2xl border border-blue-100">
266
+ <p className="text-lg font-medium">
267
+ <strong>Rule:</strong> Bought by <strong>at least 3</strong> customers
268
+ </p>
269
+ </div>
270
+ </div>
271
+ <div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
272
+ {Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count]) => (
273
+ <div key={item} className={`rounded-2xl p-5 flex items-center justify-between border-2 transition-all ${count >= 3 ? "bg-green-50 border-green-200 shadow-sm" : "bg-slate-50 border-slate-100 opacity-50"}`}>
274
+ <div className="flex items-center gap-4">
275
+ <span className="text-4xl">{getItemEmoji(item)}</span>
276
+ <div>
277
+ <div className="font-bold text-slate-800">{getItemDisplayName(item)}</div>
278
+ <div className="text-sm text-slate-500 font-medium">{count} / 8 customers</div>
279
+ </div>
280
+ </div>
281
+ <div className="text-2xl">{count >= 3 ? "✅" : "❌"}</div>
282
+ </div>
283
+ ))}
284
+ </div>
285
+ </div>
286
+ );
287
+ case 4: return (
288
+ <div className="animate-scale-in">
289
+ <div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
290
+ {frequentPairs.map(([pair, count], idx) => {
291
+ const [i1, i2] = pair.split("+");
292
+ // Removed double-curly-brace style pattern for Flask compatibility
293
+ const animStyle = { animationDelay: `${idx * 150}ms` };
294
+ return (
295
+ <div key={pair} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 text-center animate-slide-up" style={animStyle}>
296
+ <div className="flex items-center justify-center gap-4 mb-3">
297
+ <span className="text-5xl">{getItemEmoji(i1)}</span>
298
+ <span className="text-3xl font-bold text-blue-400">+</span>
299
+ <span className="text-5xl">{getItemEmoji(i2)}</span>
300
+ </div>
301
+ <div className="font-bold text-slate-800 text-lg">{getItemDisplayName(i1)} & {getItemDisplayName(i2)}</div>
302
+ <div className="mt-3 text-blue-600 font-extrabold text-2xl">Together {count}x</div>
303
+ </div>
304
+ );
305
+ })}
306
+ </div>
307
+ </div>
308
+ );
309
+ case 5: return (
310
+ <div className="animate-scale-in max-w-2xl mx-auto space-y-4">
311
+ {[
312
+ { from: "bread", to: "milk", p: 86, tip: "Most bread buyers also get milk!" },
313
+ { from: "bread", to: "butter", p: 71, tip: "Bread and butter are best friends!" },
314
+ { from: "milk", to: "butter", p: 57, tip: "Dairy items stick together!" }
315
+ ].map((rule, idx) => (
316
+ <div key={idx} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 border-l-4 border-l-blue-600">
317
+ <div className="flex items-center justify-between flex-wrap gap-4">
318
+ <div className="flex items-center gap-3">
319
+ <span className="text-sm font-bold text-slate-400 uppercase">If buys</span>
320
+ <span className="bg-blue-600 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
321
+ {getItemEmoji(rule.from)} {getItemDisplayName(rule.from)}
322
+ </span>
323
+ </div>
324
+ <div className="text-2xl text-slate-300">➜</div>
325
+ <div className="flex items-center gap-3">
326
+ <span className="text-sm font-bold text-slate-400 uppercase">Then likely buys</span>
327
+ <span className="bg-green-500 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
328
+ {getItemEmoji(rule.to)} {getItemDisplayName(rule.to)}
329
+ </span>
330
+ </div>
331
+ <div className="ml-auto text-3xl font-black text-blue-600">{rule.p}%</div>
332
+ </div>
333
+ <div className="mt-4 flex items-center gap-2 text-slate-500 italic text-sm">
334
+ <Icon name="lightbulb" className="w-4 h-4 text-amber-500" />
335
+ {rule.tip}
336
+ </div>
337
+ </div>
338
+ ))}
339
+ </div>
340
+ );
341
+ case 6: return (
342
+ <div className="text-center animate-scale-in max-w-2xl mx-auto">
343
+ <div className="text-8xl mb-6">🎉</div>
344
+ <h2 className="text-3xl font-black text-slate-800 mb-8">Sarah has a plan!</h2>
345
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
346
+ {[
347
+ { e: "🛒", t: "Store Layout", d: "Put bread near milk and butter" },
348
+ { e: "🏷️", t: "Bundle Deals", d: "Offer 'Bread + Butter' discount packs" },
349
+ { e: "📱", t: "Smart Suggestions", d: "If someone has milk, suggest butter!" },
350
+ { e: "📦", t: "Inventory", d: "Stock more milk when bread is on sale" },
351
+ ].map((tip, idx) => (
352
+ <div key={idx} className="bg-white rounded-2xl p-5 shadow-soft border border-slate-100">
353
+ <div className="text-4xl mb-2">{tip.e}</div>
354
+ <div className="font-bold text-lg text-slate-800">{tip.t}</div>
355
+ <div className="text-slate-500">{tip.d}</div>
356
+ </div>
357
+ ))}
358
+ </div>
359
+ <div className="mt-10 bg-blue-600 text-white rounded-3xl p-8 shadow-xl">
360
+ <h3 className="text-2xl font-bold mb-3">🧠 You mastered Apriori!</h3>
361
+ <p className="text-blue-100 text-lg leading-relaxed">
362
+ You just learned how to find <strong>frequent itemsets</strong> and turn them into
363
+ <strong> association rules</strong> to drive real business decisions!
364
+ </p>
365
+ </div>
366
+ </div>
367
+ );
368
+ default: return null;
369
+ }
370
+ };
371
+
372
+ return (
373
+ <div className="min-h-screen gradient-hero py-12 px-4">
374
+ <div className="max-w-4xl mx-auto">
375
+ {/* Header */}
376
+ <div className="text-center mb-10">
377
+ <h1 className="text-4xl md:text-5xl font-black mb-2 tracking-tight">
378
+ <span className="text-gradient">Apriori Algorithm</span>
379
+ </h1>
380
+ <p className="text-slate-500 text-lg font-medium">Learn how stores find shopping patterns! 🛍️</p>
381
+ </div>
382
+
383
+ {/* Progress Bar */}
384
+ <div className="mb-8">
385
+ <div className="flex justify-between items-center mb-3">
386
+ <span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Step {currentStep + 1} of {steps.length}</span>
387
+ <span className="text-sm font-bold text-blue-600">{steps[currentStep].title}</span>
388
+ </div>
389
+ <div className="h-3 bg-slate-100 rounded-full overflow-hidden shadow-inner border border-slate-50">
390
+ {/* Using pre-defined variable for style to avoid double-curly-brace pattern */}
391
+ <div
392
+ className="h-full gradient-primary transition-all duration-700 ease-in-out"
393
+ style={progressStyle}
394
+ />
395
+ </div>
396
+ </div>
397
+
398
+ {/* Card Content */}
399
+ <div className="min-h-[500px] mb-10">
400
+ <div className="bg-white rounded-[2rem] p-8 shadow-card border border-blue-50">
401
+ <div className="text-center mb-8">
402
+ <h2 className="text-2xl font-black text-slate-800 mb-1">{steps[currentStep].title}</h2>
403
+ <p className="text-slate-400 font-medium">{steps[currentStep].subtitle}</p>
404
+ </div>
405
+ {renderStepContent()}
406
+ </div>
407
+ </div>
408
+
409
+ {/* Navigation */}
410
+ <div className="flex items-center justify-center gap-4">
411
+ <Button variant="outline" size="lg" onClick={reset} disabled={currentStep === 0}>
412
+ <Icon name="rotate-ccw" className="w-5 h-5 mr-2" />
413
+ Reset
414
+ </Button>
415
+ <Button variant="outline" size="lg" onClick={prevStep} disabled={currentStep === 0}>
416
+ <Icon name="chevron-left" className="w-5 h-5 mr-1" />
417
+ Back
418
+ </Button>
419
+ <Button variant="hero" size="lg" onClick={nextStep} className="px-12">
420
+ {currentStep === steps.length - 1 ? "Start Over" : "Next Step"}
421
+ {currentStep < steps.length - 1 && <Icon name="chevron-right" className="ml-2 w-6 h-6" />}
422
+ </Button>
423
+ </div>
424
+
425
+ {/* Centered Button (FIXED) */}
426
+ <div className="mt-12 flex justify-center pb-8">
427
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
428
+ <a
429
+ href="/xgboost-regression"
430
+ onClick={playSound}
431
+ 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 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 cursor-pointer"
432
+ >
433
+ Back to Core
434
+ </a>
435
+ </div>
436
+
437
+ </div>
438
+ </div>
439
+ );
440
+ };
441
+
442
+ const root = ReactDOM.createRoot(document.getElementById('root'));
443
+ root.render(<AprioriSimulator />);
444
+ </script>
445
+ </body>
446
+ </html>
templates/Eclat-Algorithm-three.html ADDED
@@ -0,0 +1,858 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Eclat Algorithm Simulator</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ darkMode: 'class',
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ border: "hsl(var(--border))",
17
+ input: "hsl(var(--input))",
18
+ ring: "hsl(var(--ring))",
19
+ background: "hsl(var(--background))",
20
+ foreground: "hsl(var(--foreground))",
21
+ primary: {
22
+ DEFAULT: "hsl(var(--primary))",
23
+ foreground: "hsl(var(--primary-foreground))",
24
+ },
25
+ secondary: {
26
+ DEFAULT: "hsl(var(--secondary))",
27
+ foreground: "hsl(var(--secondary-foreground))",
28
+ },
29
+ destructive: {
30
+ DEFAULT: "hsl(var(--destructive))",
31
+ foreground: "hsl(var(--destructive-foreground))",
32
+ },
33
+ muted: {
34
+ DEFAULT: "hsl(var(--muted))",
35
+ foreground: "hsl(var(--muted-foreground))",
36
+ },
37
+ accent: {
38
+ DEFAULT: "hsl(var(--accent))",
39
+ foreground: "hsl(var(--accent-foreground))",
40
+ },
41
+ popover: {
42
+ DEFAULT: "hsl(var(--popover))",
43
+ foreground: "hsl(var(--popover-foreground))",
44
+ },
45
+ card: {
46
+ DEFAULT: "hsl(var(--card))",
47
+ foreground: "hsl(var(--card-foreground))",
48
+ },
49
+ success: {
50
+ DEFAULT: "#10b981",
51
+ foreground: "#ffffff"
52
+ }
53
+ },
54
+ borderRadius: {
55
+ lg: "var(--radius)",
56
+ md: "calc(var(--radius) - 2px)",
57
+ sm: "calc(var(--radius) - 4px)",
58
+ },
59
+ animation: {
60
+ 'pulse-glow': 'pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
61
+ },
62
+ keyframes: {
63
+ pulseGlow: {
64
+ '0%, 100%': { opacity: 1, boxShadow: '0 0 10px hsl(var(--primary) / 0.5)' },
65
+ '50%': { opacity: .8, boxShadow: '0 0 20px hsl(var(--primary) / 0.8)' },
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <!-- React & Libraries -->
74
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
75
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
76
+ <script src="https://unpkg.com/framer-motion@10.16.4/dist/framer-motion.js"></script>
77
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
78
+
79
+ <style>
80
+ :root {
81
+ --background: 224 71% 4%;
82
+ --foreground: 213 31% 91%;
83
+
84
+ --card: 224 71% 4%;
85
+ --card-foreground: 213 31% 91%;
86
+
87
+ --popover: 224 71% 4%;
88
+ --popover-foreground: 215 20.2% 65.1%;
89
+
90
+ --primary: 263.4 70% 50.4%;
91
+ --primary-foreground: 210 40% 98%;
92
+
93
+ --secondary: 222.2 47.4% 11.2%;
94
+ --secondary-foreground: 210 40% 98%;
95
+
96
+ --muted: 217.2 32.6% 17.5%;
97
+ --muted-foreground: 215 20.2% 65.1%;
98
+
99
+ --accent: 45 93% 47%;
100
+ --accent-foreground: 210 40% 98%;
101
+
102
+ --destructive: 0 62.8% 30.6%;
103
+ --destructive-foreground: 210 40% 98%;
104
+
105
+ --border: 217.2 32.6% 17.5%;
106
+ --input: 217.2 32.6% 17.5%;
107
+ --ring: 224.3 76.3% 48%;
108
+
109
+ --radius: 0.5rem;
110
+ }
111
+
112
+ body {
113
+ background-color: hsl(var(--background));
114
+ color: hsl(var(--foreground));
115
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
116
+ }
117
+
118
+ .circuit-pattern {
119
+ background-image: radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px);
120
+ background-size: 24px 24px;
121
+ }
122
+
123
+ .text-glow {
124
+ text-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
125
+ }
126
+
127
+ .glow-primary {
128
+ box-shadow: 0 0 15px -3px hsl(var(--primary) / 0.6);
129
+ }
130
+
131
+ .glow-accent {
132
+ box-shadow: 0 0 15px -3px hsl(var(--accent) / 0.4);
133
+ }
134
+
135
+ /* Custom Scrollbar */
136
+ ::-webkit-scrollbar {
137
+ width: 8px;
138
+ }
139
+ ::-webkit-scrollbar-track {
140
+ background: hsl(var(--secondary));
141
+ }
142
+ ::-webkit-scrollbar-thumb {
143
+ background: hsl(var(--muted));
144
+ border-radius: 4px;
145
+ }
146
+ ::-webkit-scrollbar-thumb:hover {
147
+ background: hsl(var(--primary));
148
+ }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div id="root"></div>
153
+
154
+ <!-- We use {% raw %} to prevent Jinja2 from parsing React's curly braces -->
155
+ {% raw %}
156
+ <script type="text/babel">
157
+ const { useState, useEffect, useRef } = React;
158
+ const { motion, AnimatePresence } = window.Motion;
159
+
160
+ // --- UTILS ---
161
+ function cn(...classes) {
162
+ return classes.filter(Boolean).join(' ');
163
+ }
164
+
165
+ // --- ICONS (Lucide replacement) ---
166
+ const Icon = ({ d, className }) => (
167
+ <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}>
168
+ <path d={d} />
169
+ </svg>
170
+ );
171
+
172
+ const Icons = {
173
+ Play: (props) => <Icon d="M5 3l14 9-14 9V3z" {...props} />,
174
+ Pause: (props) => <Icon d="M6 4h4v16H6zm8 0h4v16h-4z" {...props} />,
175
+ RotateCcw: (props) => <Icon d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5" {...props} />,
176
+ ChevronRight: (props) => <Icon d="M9 18l6-6-6-6" {...props} />,
177
+ Database: (props) => <Icon d="M3 5c0-1.1 4.03-2 9-2s9 .9 9 2c0 1.1-4.03 2-9 2s-9-.9-9-2z M3 5v14c0 1.1 4.03 2 9 2s9-.9 9-2V5 M3 12c0 1.1 4.03 2 9 2s9-.9 9-2" {...props} />,
178
+ Info: (props) => <Icon d="M12 16v-4 M12 8h.01 M22 12A10 10 0 1 1 12 2a10 10 0 0 1 10 10z" {...props} />,
179
+ RotateCw: (props) => <Icon d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8 M21 3v5h-5" {...props} />,
180
+ Lightbulb: (props) => <Icon d="M9 18h6 M10 22h4 M15.09 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54M9 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54" {...props} />,
181
+ ChevronDown: (props) => <Icon d="M6 9l6 6 6-6" {...props} />,
182
+ ChevronUp: (props) => <Icon d="M18 15l-6-6-6 6" {...props} />,
183
+ ShoppingCart: (props) => <Icon d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" {...props} />,
184
+ Sparkles: (props) => <Icon d="M12 3l1.912 5.813a2 2 0 0 1 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 1-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 1-1.275-1.275L3 12l5.813-1.912a2 2 0 0 1 1.275-1.275z" {...props} />,
185
+ ArrowRight: (props) => <Icon d="M5 12h14M12 5l7 7-7 7" {...props} />,
186
+ BookOpen: (props) => <Icon d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" {...props} />,
187
+ Zap: (props) => <Icon d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" {...props} />,
188
+ CheckCircle: (props) => <Icon d="M22 11.08V12a10 10 0 1 1-5.93-9.14 M22 4L12 14.01l-3-3" {...props} />,
189
+ XCircle: (props) => <Icon d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" {...props} />,
190
+ Trophy: (props) => <Icon d="M8 21h8M12 17v4M7 4h10c.66 0 1.33.2 2 .59V9a8 8 0 0 1-8 8 8 8 0 0 1-8-8V4.59c.67-.39 1.34-.59 2-.59zM19 10a5 5 0 0 0 0 10M5 10a5 5 0 0 1 0 10" {...props} />,
191
+ TrendingUp: (props) => <Icon d="M23 6l-9.5 9.5-5-5L1 18" {...props} />,
192
+ HelpCircle: (props) => <Icon d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3 M12 17h.01 M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z" {...props} />,
193
+ X: (props) => <Icon d="M18 6L6 18M6 6l12 12" {...props} />,
194
+ Search: (props) => <Icon d="M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z" {...props} />,
195
+ };
196
+
197
+ // --- BASIC UI COMPONENTS ---
198
+ const Button = ({ children, variant = 'default', size = 'default', className, onClick, disabled }) => {
199
+ const variants = {
200
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
201
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
202
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
203
+ ghost: "hover:bg-accent hover:text-accent-foreground",
204
+ };
205
+ const sizes = {
206
+ default: "h-10 px-4 py-2",
207
+ sm: "h-9 rounded-md px-3",
208
+ icon: "h-10 w-10",
209
+ lg: "h-11 rounded-md px-8",
210
+ };
211
+ return (
212
+ <button
213
+ onClick={onClick}
214
+ disabled={disabled}
215
+ className={cn(
216
+ "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
217
+ variants[variant],
218
+ sizes[size],
219
+ className
220
+ )}
221
+ >
222
+ {children}
223
+ </button>
224
+ );
225
+ };
226
+
227
+ const Slider = ({ value, onValueChange, min, max, step, className }) => {
228
+ return (
229
+ <input
230
+ type="range"
231
+ min={min}
232
+ max={max}
233
+ step={step}
234
+ value={value[0]}
235
+ onChange={(e) => onValueChange([parseFloat(e.target.value)])}
236
+ className={cn("w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary", className)}
237
+ />
238
+ );
239
+ };
240
+
241
+ const Input = ({ className, ...props }) => (
242
+ <input
243
+ className={cn("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)}
244
+ {...props}
245
+ />
246
+ );
247
+
248
+ // --- SUB COMPONENTS ---
249
+
250
+ const BeginnerTip = ({ title, children, defaultOpen = false }) => {
251
+ const [isOpen, setIsOpen] = useState(defaultOpen);
252
+ return (
253
+ <motion.div layout className="rounded-lg border border-accent/30 bg-accent/5 overflow-hidden">
254
+ <button onClick={() => setIsOpen(!isOpen)} className="w-full p-3 flex items-center gap-2 text-left hover:bg-accent/10 transition-colors">
255
+ <Icons.Lightbulb className="w-4 h-4 text-accent shrink-0" />
256
+ <span className="text-sm font-medium text-foreground flex-1">{title}</span>
257
+ {isOpen ? <Icons.ChevronUp className="w-4 h-4 text-muted-foreground" /> : <Icons.ChevronDown className="w-4 h-4 text-muted-foreground" />}
258
+ </button>
259
+ {isOpen && (
260
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="px-3 pb-3 text-sm text-foreground/80">
261
+ {children}
262
+ </motion.div>
263
+ )}
264
+ </motion.div>
265
+ );
266
+ };
267
+
268
+ const TransactionPanel = ({ transactions, highlightedTids, step }) => {
269
+ const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
270
+ return (
271
+ <div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
272
+ <div className="p-4 border-b border-border/50 flex items-center gap-3">
273
+ <div className="w-8 h-8 rounded-lg bg-secondary/20 flex items-center justify-center">
274
+ <Icons.ShoppingCart className="w-4 h-4 text-secondary" />
275
+ </div>
276
+ <div>
277
+ <h2 className="font-semibold text-foreground">Transaction Database</h2>
278
+ <p className="text-xs text-muted-foreground">Horizontal Format (Traditional)</p>
279
+ </div>
280
+ </div>
281
+ <div className="p-4 space-y-3">
282
+ {transactions.map((transaction, index) => (
283
+ <motion.div
284
+ key={transaction.id}
285
+ initial={{ opacity: 0, x: -20 }}
286
+ animate={{ opacity: step >= 0 ? 1 : 0.3, x: 0, scale: highlightedTids.includes(transaction.id) ? 1.02 : 1 }}
287
+ transition={{ delay: index * 0.1 }}
288
+ className={`p-3 rounded-lg border transition-all duration-300 ${highlightedTids.includes(transaction.id) ? 'bg-primary/10 border-primary/50 glow-primary' : 'bg-muted/30 border-border/30 hover:border-border/60'}`}
289
+ >
290
+ <div className="flex items-center gap-3">
291
+ <span className={`font-mono text-sm font-bold w-8 h-8 rounded-full flex items-center justify-center ${highlightedTids.includes(transaction.id) ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
292
+ T{transaction.id}
293
+ </span>
294
+ <div className="flex flex-wrap gap-2">
295
+ {transaction.items.map((item, i) => (
296
+ <motion.span key={i} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: index * 0.1 + i * 0.05 }} className="px-2 py-1 rounded-md bg-background/50 border border-border/30 text-sm flex items-center gap-1">
297
+ <span>{itemEmojis[item]}</span><span className="text-foreground/80">{item}</span>
298
+ </motion.span>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ </motion.div>
303
+ ))}
304
+ </div>
305
+ </div>
306
+ );
307
+ };
308
+
309
+ const TidSetPanel = ({ tidSets, step, minSupportCount, onItemHover, highlightedItem }) => {
310
+ const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
311
+ const itemColors = {
312
+ 'Bread': 'from-amber-500/20 to-orange-500/20 border-amber-500/50',
313
+ 'Milk': 'from-blue-500/20 to-cyan-500/20 border-blue-500/50',
314
+ 'Eggs': 'from-yellow-500/20 to-amber-500/20 border-yellow-500/50',
315
+ 'Butter': 'from-yellow-600/20 to-orange-400/20 border-yellow-600/50',
316
+ };
317
+
318
+ if (step < 1) {
319
+ return (
320
+ <div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
321
+ <div className="p-4 border-b border-border/50 flex items-center gap-3">
322
+ <div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center"><Icons.Database className="w-4 h-4 text-primary" /></div>
323
+ <div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Eclat)</p></div>
324
+ </div>
325
+ <div className="p-8 flex items-center justify-center">
326
+ <div className="text-center">
327
+ <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mx-auto mb-4"><Icons.Database className="w-8 h-8 text-muted-foreground/50" /></div>
328
+ <p className="text-muted-foreground text-sm">Press <span className="text-primary font-semibold">Play</span> or <span className="text-primary font-semibold">Next Step</span> to begin</p>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ return (
336
+ <div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
337
+ <div className="p-4 border-b border-border/50 flex items-center gap-3">
338
+ <div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center animate-pulse-glow"><Icons.Database className="w-4 h-4 text-primary" /></div>
339
+ <div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Item → Transaction IDs)</p></div>
340
+ </div>
341
+ <div className="p-4 space-y-3">
342
+ {tidSets.map((tidSet, index) => {
343
+ const isFrequent = tidSet.tids.length >= minSupportCount;
344
+ const isHighlighted = highlightedItem === tidSet.item;
345
+ return (
346
+ <motion.div
347
+ key={tidSet.item}
348
+ initial={{ opacity: 0, x: 20 }}
349
+ animate={{ opacity: 1, x: 0, scale: isHighlighted ? 1.02 : 1 }}
350
+ transition={{ delay: index * 0.15 }}
351
+ onMouseEnter={() => onItemHover(tidSet.item, tidSet.tids)}
352
+ onMouseLeave={() => onItemHover(null)}
353
+ className={`p-3 rounded-lg border bg-gradient-to-r transition-all duration-300 cursor-pointer ${itemColors[tidSet.item]} ${isHighlighted ? 'glow-primary' : ''}`}
354
+ >
355
+ <div className="flex items-center justify-between mb-2">
356
+ <div className="flex items-center gap-2">
357
+ <span className="text-lg">{itemEmojis[tidSet.item]}</span>
358
+ <span className="font-semibold text-foreground">{tidSet.item}</span>
359
+ </div>
360
+ {step >= 2 && (
361
+ <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/20 text-destructive'}`}>
362
+ {isFrequent ? <><Icons.CheckCircle className="w-3 h-3" /> Frequent</> : <><Icons.XCircle className="w-3 h-3" /> Pruned</>}
363
+ </motion.div>
364
+ )}
365
+ </div>
366
+ <div className="flex items-center gap-2">
367
+ <span className="text-xs text-muted-foreground font-mono">TID:</span>
368
+ <div className="flex flex-wrap gap-1">
369
+ {tidSet.tids.map((tid, i) => (
370
+ <motion.span key={tid} initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: index * 0.15 + i * 0.05 }} className="px-2 py-0.5 rounded bg-background/50 text-xs font-mono text-foreground/80 border border-border/30">
371
+ {tid}
372
+ </motion.span>
373
+ ))}
374
+ </div>
375
+ <span className="ml-auto text-xs font-mono text-accent font-bold">|{tidSet.tids.length}|</span>
376
+ </div>
377
+ </motion.div>
378
+ );
379
+ })}
380
+ </div>
381
+ {step >= 1 && (
382
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="p-4 border-t border-border/30 bg-primary/5">
383
+ <p className="text-xs text-muted-foreground"><span className="text-primary font-semibold">🔄 Transformed:</span> Now each item maps to the transactions containing it.</p>
384
+ </motion.div>
385
+ )}
386
+ </div>
387
+ );
388
+ };
389
+
390
+ const IntersectionVisualizer = ({ tidSets, minSupportCount, step }) => {
391
+ const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
392
+ const frequentItems = tidSets.filter(t => t.tids.length >= minSupportCount);
393
+ const intersections = [];
394
+ for (let i = 0; i < frequentItems.length; i++) {
395
+ for (let j = i + 1; j < frequentItems.length; j++) {
396
+ const result = frequentItems[i].tids.filter(tid => frequentItems[j].tids.includes(tid));
397
+ intersections.push({
398
+ item1: frequentItems[i].item,
399
+ item2: frequentItems[j].item,
400
+ tids1: frequentItems[i].tids,
401
+ tids2: frequentItems[j].tids,
402
+ result,
403
+ isFrequent: result.length >= minSupportCount
404
+ });
405
+ }
406
+ }
407
+
408
+ return (
409
+ <div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
410
+ <div className="p-4 border-b border-border/50 flex items-center gap-3">
411
+ <div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center"><Icons.Zap className="w-4 h-4 text-accent" /></div>
412
+ <div><h2 className="font-semibold text-foreground">Set Intersections</h2><p className="text-xs text-muted-foreground">Finding 2-item patterns through TID overlap</p></div>
413
+ </div>
414
+ <div className="p-4">
415
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
416
+ {intersections.map((intersection, index) => (
417
+ <motion.div
418
+ key={`${intersection.item1}-${intersection.item2}`}
419
+ initial={{ opacity: 0, scale: 0.9 }}
420
+ animate={{ opacity: 1, scale: 1 }}
421
+ transition={{ delay: index * 0.1 }}
422
+ className={`p-4 rounded-lg border transition-all ${intersection.isFrequent ? 'bg-success/5 border-success/30' : 'bg-muted/20 border-border/30 opacity-60'}`}
423
+ >
424
+ <div className="flex items-center justify-center gap-2 mb-3">
425
+ <span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item1]} {intersection.item1}</span>
426
+ <span className="text-primary font-mono">∩</span>
427
+ <span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item2]} {intersection.item2}</span>
428
+ </div>
429
+ <div className="flex items-center justify-center gap-2 mb-3 text-xs">
430
+ <div className="flex gap-0.5">
431
+ {intersection.tids1.map(tid => (
432
+ <span key={tid} className={`w-5 h-5 rounded flex items-center justify-center font-mono ${intersection.result.includes(tid) ? 'bg-success/30 text-success' : 'bg-muted/50 text-muted-foreground'}`}>{tid}</span>
433
+ ))}
434
+ </div>
435
+ <Icons.ArrowRight className="w-3 h-3 text-muted-foreground" />
436
+ <div className="flex gap-0.5">
437
+ {intersection.result.length > 0 ? intersection.result.map(tid => (
438
+ <span key={tid} className="w-5 h-5 rounded flex items-center justify-center font-mono bg-success/30 text-success font-bold">{tid}</span>
439
+ )) : <span className="text-muted-foreground">∅</span>}
440
+ </div>
441
+ </div>
442
+ <div className={`text-center text-xs font-medium px-2 py-1 rounded ${intersection.isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/10 text-destructive/70'}`}>
443
+ Support: {intersection.result.length} {intersection.isFrequent ? ' ✓ Frequent' : ' ✗ Pruned'}
444
+ </div>
445
+ </motion.div>
446
+ ))}
447
+ </div>
448
+ </div>
449
+ </div>
450
+ );
451
+ };
452
+
453
+ const FrequentItemsetsPanel = ({ itemsets, step, onItemsetHover }) => {
454
+ const itemEmojis = { 'Bread': '🍞', 'Milk': '🥛', 'Eggs': '🥚', 'Butter': '🧈' };
455
+ const oneItemsets = itemsets.filter(i => i.items.length === 1);
456
+ const twoItemsets = itemsets.filter(i => i.items.length === 2);
457
+ const threeItemsets = itemsets.filter(i => i.items.length === 3);
458
+
459
+ const ItemsetCard = ({ items, support, tids, delay }) => (
460
+ <motion.div
461
+ initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: delay }}
462
+ onMouseEnter={() => onItemsetHover(items.join(','), tids)} onMouseLeave={() => onItemsetHover(null)}
463
+ className="p-2 rounded-lg bg-muted/30 border border-border/30 hover:border-primary/50 transition-all cursor-pointer"
464
+ >
465
+ <div className="flex items-center justify-between">
466
+ <span className="flex items-center gap-1">
467
+ {items.map(item => <span key={item} className="flex items-center gap-1">{itemEmojis[item]} {item}</span>)}
468
+ </span>
469
+ <span className="text-xs font-mono text-accent">{support.toFixed(0)}%</span>
470
+ </div>
471
+ </motion.div>
472
+ );
473
+
474
+ return (
475
+ <div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
476
+ <div className="p-4 border-b border-border/50 flex items-center gap-3">
477
+ <div className="w-8 h-8 rounded-lg bg-success/20 flex items-center justify-center"><Icons.Trophy className="w-4 h-4 text-success" /></div>
478
+ <div><h2 className="font-semibold text-foreground">Frequent Itemsets Discovered</h2><p className="text-xs text-muted-foreground">Patterns that meet minimum support</p></div>
479
+ </div>
480
+ <div className="p-4">
481
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
482
+ <div>
483
+ <h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs font-mono text-primary">1</span> Single Items</h3>
484
+ <div className="space-y-2">{oneItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />)}</div>
485
+ </div>
486
+ <div>
487
+ <h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-secondary/20 flex items-center justify-center text-xs font-mono text-secondary">2</span> Item Pairs {step < 3 && <span className="text-xs text-muted-foreground">(next)</span>}</h3>
488
+ <div className="space-y-2">
489
+ {step >= 3 ? twoItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.TrendingUp className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Continue to find pairs</p></div>}
490
+ </div>
491
+ </div>
492
+ <div>
493
+ <h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center text-xs font-mono text-accent">3</span> Triplets {step < 4 && <span className="text-xs text-muted-foreground">(final)</span>}</h3>
494
+ <div className="space-y-2">
495
+ {step >= 4 ? (threeItemsets.length > 0 ? threeItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-border/30 text-center"><p className="text-xs text-muted-foreground">No 3-itemsets found</p></div>) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.Trophy className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Discover triplets last</p></div>}
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ );
502
+ };
503
+
504
+ const StepExplainer = ({ step, minSupport, minSupportCount }) => {
505
+ const stepContent = [
506
+ { title: "Step 1: Look at Receipts 🧾", simple: "Transactions", description: "Each row is one customer's shopping basket. Goal: find patterns.", color: "primary" },
507
+ { title: "Step 2: Flip the View 🔄", simple: "Vertical Format", description: "Convert 'Receipt → Items' to 'Item → Receipt Numbers'. This is key to Eclat!", color: "primary" },
508
+ { title: "Step 3: Check Popularity ⭐", simple: "Filter by Support", description: `Remove rare items. Must appear in at least ${minSupportCount} receipts (${minSupport}%).`, color: "secondary" },
509
+ { title: "Step 4: Find Pairs 🔗", simple: "Intersections", description: "Find overlapping receipt numbers. Overlap = Bought together.", color: "accent" },
510
+ { title: "Step 5: Bigger Patterns 🏆", simple: "3+ Items", description: "Intersect pairs to find triplets. Keep going until no overlaps remain.", color: "success" }
511
+ ];
512
+ const content = stepContent[step] || stepContent[0];
513
+
514
+ return (
515
+ <AnimatePresence mode="wait">
516
+ <motion.div
517
+ key={step}
518
+ initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: 0.3 }}
519
+ className={`p-5 rounded-xl border backdrop-blur-sm bg-${content.color}/5 border-${content.color}/30`}
520
+ >
521
+ <div className="flex items-center gap-4">
522
+ <div className={`w-12 h-12 rounded-xl flex items-center justify-center text-xl font-bold bg-${content.color}/20 text-${content.color}`}>{step + 1}</div>
523
+ <div>
524
+ <h3 className="font-bold text-foreground text-lg">{content.title}</h3>
525
+ <p className="text-sm text-foreground/80">{content.description}</p>
526
+ </div>
527
+ </div>
528
+ </motion.div>
529
+ </AnimatePresence>
530
+ );
531
+ };
532
+
533
+ const IntroTutorial = ({ onStart }) => {
534
+ const [page, setPage] = useState(0);
535
+ const pages = [
536
+ { title: "Welcome to Eclat! 👋", content: <div className="space-y-4"><p>Imagine owning a store. You want to know: "If someone buys Bread, do they also buy Milk?" Eclat finds these patterns automatically using math!</p></div> },
537
+ { title: "How Eclat Works 🔍", content: <div className="space-y-4"><p>Most algorithms scan receipts one by one (slow). Eclat uses a trick: it lists <strong>Transaction IDs (TIDs)</strong> for each item. Then it just compares lists of numbers!</p></div> },
538
+ { title: "Let's Simluate! 🚀", content: <div className="space-y-4"><p>We have 6 receipts. You can control the speed, hover over items to see connections, and adjust the 'Support' threshold. Ready?</p></div> }
539
+ ];
540
+
541
+ return (
542
+ <div className="fixed inset-0 bg-background/90 backdrop-blur-sm z-50 flex items-center justify-center p-4">
543
+ <motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="max-w-md w-full bg-card border border-border rounded-xl p-6 shadow-2xl">
544
+ <div className="text-center mb-6">
545
+ <h2 className="text-2xl font-bold text-primary mb-2">{pages[page].title}</h2>
546
+ <div className="text-muted-foreground">{pages[page].content}</div>
547
+ </div>
548
+ <div className="flex justify-between items-center">
549
+ <div className="flex gap-1">{pages.map((_, i) => <div key={i} className={`w-2 h-2 rounded-full ${i===page?'bg-primary':'bg-muted'}`} />)}</div>
550
+ {page < pages.length - 1 ? <Button onClick={() => setPage(p => p+1)}>Next <Icons.ChevronRight className="w-4 h-4 ml-1"/></Button> : <Button onClick={onStart}>Start <Icons.Play className="w-4 h-4 ml-1"/></Button>}
551
+ </div>
552
+ </motion.div>
553
+ </div>
554
+ );
555
+ };
556
+
557
+ const GlossaryPanel = ({ isOpen, onClose }) => {
558
+ const terms = [
559
+ { t: "Transaction", d: "A single shopping receipt (e.g., T1)" },
560
+ { t: "TID", d: "Transaction ID (the number of the receipt)" },
561
+ { t: "Support", d: "How frequently an item appears (%)" },
562
+ { t: "Itemset", d: "A collection of one or more items" },
563
+ { t: "Intersection", d: "Finding common numbers in two lists" }
564
+ ];
565
+
566
+ return (
567
+ <AnimatePresence>
568
+ {isOpen && (
569
+ <>
570
+ <div className="fixed inset-0 bg-background/50 z-40" onClick={onClose} />
571
+ <motion.div initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }} className="fixed right-0 top-0 h-full w-80 bg-card border-l border-border z-50 p-6 shadow-xl">
572
+ <div className="flex justify-between items-center mb-6">
573
+ <h2 className="font-bold text-lg">Glossary</h2>
574
+ <button onClick={onClose}><Icons.X className="w-5 h-5" /></button>
575
+ </div>
576
+ <div className="space-y-4">
577
+ {terms.map((item, i) => (
578
+ <div key={i} className="p-3 bg-muted/20 rounded-lg">
579
+ <div className="font-bold text-primary text-sm">{item.t}</div>
580
+ <div className="text-sm text-muted-foreground">{item.d}</div>
581
+ </div>
582
+ ))}
583
+ </div>
584
+ </motion.div>
585
+ </>
586
+ )}
587
+ </AnimatePresence>
588
+ );
589
+ };
590
+
591
+ const TheoryPanel = () => {
592
+ const playSound = () => {
593
+ const audio = document.getElementById('clickSound');
594
+ if (audio) {
595
+ audio.currentTime = 0;
596
+ audio.play();
597
+ }
598
+ };
599
+
600
+ return (
601
+ <section className="mt-8 p-6 rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm relative">
602
+ <div className="flex items-center gap-3 mb-6">
603
+ <div className="w-10 h-10 rounded-lg bg-indigo-500/20 flex items-center justify-center">
604
+ <Icons.BookOpen className="w-5 h-5 text-indigo-400" />
605
+ </div>
606
+ <div>
607
+ <h2 className="text-xl font-bold text-foreground">Theoretical Background</h2>
608
+ <p className="text-sm text-muted-foreground">Understanding the core concepts behind Eclat</p>
609
+ </div>
610
+ </div>
611
+
612
+ <div className="grid md:grid-cols-2 gap-8 mb-12">
613
+ <div className="space-y-4">
614
+ <div className="p-4 rounded-lg bg-muted/20 border border-border/30">
615
+ <h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
616
+ 1. Vertical Data Format
617
+ </h3>
618
+ <p className="text-sm text-muted-foreground leading-relaxed">
619
+ Traditional algorithms like Apriori use a <strong>Horizontal</strong> layout (Transaction ID → List of Items).
620
+ Eclat flips this to a <strong>Vertical</strong> layout (Item → List of Transaction IDs).
621
+ This transformation is the key to its speed, as it avoids repeatedly scanning the entire database.
622
+ </p>
623
+ </div>
624
+
625
+ <div className="p-4 rounded-lg bg-muted/20 border border-border/30">
626
+ <h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
627
+ 2. Set Intersection
628
+ </h3>
629
+ <p className="text-sm text-muted-foreground leading-relaxed">
630
+ To calculate the support of a new itemset (e.g., <span className="font-mono text-accent">{"{A, B}"}</span>), Eclat simply calculates the intersection of their TID sets:
631
+ <br/>
632
+ <code className="bg-background/50 px-1 rounded text-xs mt-1 block w-fit">TID(A) ∩ TID(B) = TID(A,B)</code>
633
+ The size of this resulting set is the support count.
634
+ </p>
635
+ </div>
636
+ </div>
637
+
638
+ <div className="space-y-4">
639
+ <div className="p-4 rounded-lg bg-muted/20 border border-border/30">
640
+ <h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
641
+ 3. Depth-First Search (DFS)
642
+ </h3>
643
+ <p className="text-sm text-muted-foreground leading-relaxed">
644
+ Unlike Apriori which uses Breadth-First Search (finding all pairs, then all triplets), Eclat uses <strong>Depth-First Search</strong>.
645
+ It creates long patterns quickly by extending a specific prefix (e.g., A -> AB -> ABC) before backtracking.
646
+ </p>
647
+ </div>
648
+
649
+ <div className="p-4 rounded-lg bg-muted/20 border border-border/30">
650
+ <h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
651
+ 4. Advantages
652
+ </h3>
653
+ <ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
654
+ <li><strong>Speed:</strong> Generally faster than Apriori for dense datasets.</li>
655
+ <li><strong>Memory:</strong> Does not need to generate candidate sets explicitly.</li>
656
+ <li><strong>Efficiency:</strong> Intersection operations are computationally cheap on modern CPUs.</li>
657
+ </ul>
658
+ </div>
659
+ </div>
660
+ </div>
661
+
662
+ {/* Centered Button with Fixes */}
663
+ <div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center">
664
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
665
+ <a
666
+ href="/eclat"
667
+ onClick={playSound}
668
+ className="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"
669
+ >
670
+ Back to Core
671
+ </a>
672
+ </div>
673
+ </section>
674
+ );
675
+ };
676
+
677
+ // --- MAIN APP COMPONENT ---
678
+
679
+ const SAMPLE_TRANSACTIONS = [
680
+ { id: 1, items: ['Bread', 'Milk', 'Eggs'] },
681
+ { id: 2, items: ['Bread', 'Butter'] },
682
+ { id: 3, items: ['Milk', 'Butter', 'Eggs'] },
683
+ { id: 4, items: ['Bread', 'Milk', 'Butter'] },
684
+ { id: 5, items: ['Bread', 'Milk', 'Eggs', 'Butter'] },
685
+ { id: 6, items: ['Milk', 'Eggs'] },
686
+ ];
687
+ const ITEMS = ['Bread', 'Milk', 'Eggs', 'Butter'];
688
+
689
+ const EclatSimulation = () => {
690
+ const [showTutorial, setShowTutorial] = useState(true);
691
+ const [showGlossary, setShowGlossary] = useState(false);
692
+ const [step, setStep] = useState(0);
693
+ const [isPlaying, setIsPlaying] = useState(false);
694
+ const [minSupport, setMinSupport] = useState(50);
695
+ const [tidSets, setTidSets] = useState([]);
696
+ const [frequentItemsets, setFrequentItemsets] = useState([]);
697
+ const [highlightedTids, setHighlightedTids] = useState([]);
698
+ const [highlightedItem, setHighlightedItem] = useState(null);
699
+
700
+ const totalTransactions = SAMPLE_TRANSACTIONS.length;
701
+ const minSupportCount = Math.ceil((minSupport / 100) * totalTransactions);
702
+
703
+ // Core Logic
704
+ const buildTidSets = () => ITEMS.map(item => ({
705
+ item, tids: SAMPLE_TRANSACTIONS.filter(t => t.items.includes(item)).map(t => t.id)
706
+ }));
707
+
708
+ const intersect = (t1, t2) => t1.filter(x => t2.includes(x));
709
+
710
+ useEffect(() => {
711
+ if (step >= 1) setTidSets(buildTidSets());
712
+ }, [step]);
713
+
714
+ useEffect(() => {
715
+ if (!isPlaying) return;
716
+ const timer = setTimeout(() => {
717
+ if (step < 4) setStep(s => s + 1);
718
+ else setIsPlaying(false);
719
+ }, 3000);
720
+ return () => clearTimeout(timer);
721
+ }, [isPlaying, step]);
722
+
723
+ useEffect(() => {
724
+ if (step >= 2) {
725
+ const currentTidSets = buildTidSets();
726
+ const frequent = [];
727
+
728
+ // 1-itemsets
729
+ currentTidSets.forEach(ts => {
730
+ if (ts.tids.length >= minSupportCount) frequent.push({ items: [ts.item], tids: ts.tids, support: (ts.tids.length / totalTransactions) * 100 });
731
+ });
732
+
733
+ // 2-itemsets
734
+ if (step >= 3) {
735
+ const freqItems = frequent.map(f => f.items[0]);
736
+ for(let i=0; i<freqItems.length; i++) {
737
+ for(let j=i+1; j<freqItems.length; j++) {
738
+ const t1 = currentTidSets.find(t=>t.item===freqItems[i]).tids;
739
+ const t2 = currentTidSets.find(t=>t.item===freqItems[j]).tids;
740
+ const inter = intersect(t1, t2);
741
+ if(inter.length >= minSupportCount) frequent.push({ items: [freqItems[i], freqItems[j]], tids: inter, support: (inter.length/totalTransactions)*100 });
742
+ }
743
+ }
744
+ }
745
+
746
+ // 3-itemsets
747
+ if (step >= 4) {
748
+ const pairs = frequent.filter(f => f.items.length === 2);
749
+ for(let i=0; i<pairs.length; i++) {
750
+ for(let j=i+1; j<pairs.length; j++) {
751
+ const combined = [...new Set([...pairs[i].items, ...pairs[j].items])];
752
+ if(combined.length === 3) {
753
+ const inter = intersect(pairs[i].tids, pairs[j].tids);
754
+ if(inter.length >= minSupportCount && !frequent.some(f=>f.items.length===3 && f.items.every(it=>combined.includes(it)))) {
755
+ frequent.push({ items: combined, tids: inter, support: (inter.length/totalTransactions)*100 });
756
+ }
757
+ }
758
+ }
759
+ }
760
+ }
761
+ setFrequentItemsets(frequent);
762
+ }
763
+ }, [step, minSupport, minSupportCount]);
764
+
765
+ const handleHover = (item, tids = []) => {
766
+ setHighlightedItem(item);
767
+ setHighlightedTids(tids);
768
+ };
769
+
770
+ return (
771
+ <div className="min-h-screen bg-background grid-bg circuit-pattern pb-20">
772
+ {showTutorial && <IntroTutorial onStart={() => setShowTutorial(false)} />}
773
+ <GlossaryPanel isOpen={showGlossary} onClose={() => setShowGlossary(false)} />
774
+
775
+ <header className="border-b border-border/50 backdrop-blur-sm bg-background/80 sticky top-0 z-30">
776
+ <div className="container mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-4">
777
+ <div className="flex items-center gap-3">
778
+ <div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center glow-primary"><Icons.Database className="w-5 h-5 text-primary"/></div>
779
+ <div><h1 className="text-xl font-bold text-foreground text-glow">Eclat Algorithm</h1><p className="text-xs text-muted-foreground">Interactive Learning Simulation</p></div>
780
+ </div>
781
+ <div className="flex items-center gap-3">
782
+ <Button variant="outline" size="sm" onClick={() => setShowGlossary(true)} className="gap-2"><Icons.HelpCircle className="w-4 h-4"/> Glossary</Button>
783
+ <Button variant="ghost" size="sm" onClick={() => setShowTutorial(true)} className="gap-2"><Icons.RotateCw className="w-4 h-4"/> Tutorial</Button>
784
+ </div>
785
+ </div>
786
+ </header>
787
+
788
+ <main className="container mx-auto px-4 py-6">
789
+ <motion.div initial={{opacity:0, y:20}} animate={{opacity:1, y:0}} className="mb-6 p-4 rounded-xl bg-card/50 border border-border/50 backdrop-blur-sm">
790
+ <div className="flex flex-wrap items-center justify-between gap-4">
791
+ <div className="flex flex-wrap items-center gap-3">
792
+ <Button onClick={() => setIsPlaying(!isPlaying)} className="gap-2" variant={isPlaying ? "secondary" : "default"}>
793
+ {isPlaying ? <Icons.Pause className="w-4 h-4"/> : <Icons.Play className="w-4 h-4"/>} {isPlaying ? 'Pause' : 'Play'}
794
+ </Button>
795
+ <Button onClick={() => step < 4 && setStep(s => s+1)} variant="outline" disabled={step >= 4} className="gap-2">
796
+ <Icons.ChevronRight className="w-4 h-4"/> Next
797
+ </Button>
798
+ <Button onClick={() => { setStep(0); setIsPlaying(false); }} variant="ghost" className="gap-2"><Icons.RotateCcw className="w-4 h-4"/> Reset</Button>
799
+ </div>
800
+ <div className="flex items-center gap-4 w-full md:w-auto">
801
+ <span className="text-sm text-muted-foreground whitespace-nowrap">Support: {minSupport}%</span>
802
+ <Slider value={[minSupport]} onValueChange={([v]) => setMinSupport(v)} min={20} max={80} step={10} className="w-full md:w-32"/>
803
+ </div>
804
+ </div>
805
+
806
+ <div className="mt-4 flex flex-wrap items-center gap-2">
807
+ {['Transactions', 'Build TID Sets', 'Frequent 1-Items', '2-Item Intersections', '3-Item Patterns'].map((label, i) => (
808
+ <div key={i} className="flex items-center gap-2">
809
+ <motion.div animate={step === i ? { scale: [1, 1.1, 1] } : {}} className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${step >= i ? 'bg-primary text-primary-foreground glow-primary' : 'bg-muted text-muted-foreground'}`}>{i + 1}</motion.div>
810
+ <span className={`text-xs hidden sm:block ${step >= i ? 'text-foreground' : 'text-muted-foreground'}`}>{label}</span>
811
+ {i < 4 && <Icons.ChevronRight className="w-4 h-4 text-muted-foreground hidden sm:block"/>}
812
+ </div>
813
+ ))}
814
+ </div>
815
+ </motion.div>
816
+
817
+ <StepExplainer step={step} minSupport={minSupport} minSupportCount={minSupportCount} />
818
+
819
+ {step === 0 && <div className="mt-4"><BeginnerTip title="🆕 New to this?" defaultOpen={true}><p>Think of this like looking at <strong>grocery store receipts</strong>. Each row (T1, T2...) is one customer's shopping basket.</p></BeginnerTip></div>}
820
+ {step === 1 && <div className="mt-4"><BeginnerTip title="🔄 Why did we flip the data?"><p><strong>Before:</strong> "Receipt 1 has Bread" (horizontal). <strong>After:</strong> "Bread is in Receipt 1" (vertical). This makes finding pairs faster!</p></BeginnerTip></div>}
821
+
822
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
823
+ <motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
824
+ <TransactionPanel transactions={SAMPLE_TRANSACTIONS} highlightedTids={highlightedTids} step={step} />
825
+ </motion.div>
826
+ <motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
827
+ <TidSetPanel tidSets={tidSets} step={step} minSupportCount={minSupportCount} onItemHover={handleHover} highlightedItem={highlightedItem} />
828
+ </motion.div>
829
+ </div>
830
+
831
+ <AnimatePresence>
832
+ {step >= 3 && (
833
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
834
+ <IntersectionVisualizer tidSets={tidSets} minSupportCount={minSupportCount} step={step} />
835
+ </motion.div>
836
+ )}
837
+ </AnimatePresence>
838
+
839
+ <AnimatePresence>
840
+ {step >= 2 && (
841
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
842
+ <FrequentItemsetsPanel itemsets={frequentItemsets} step={step} onItemsetHover={handleHover} />
843
+ </motion.div>
844
+ )}
845
+ </AnimatePresence>
846
+
847
+ <TheoryPanel />
848
+ </main>
849
+ </div>
850
+ );
851
+ };
852
+
853
+ const root = ReactDOM.createRoot(document.getElementById('root'));
854
+ root.render(<EclatSimulation />);
855
+ </script>
856
+ {% endraw %}
857
+ </body>
858
+ </html>
templates/Gaussian-Mixture-Models.html CHANGED
@@ -177,6 +177,52 @@
177
  <div class="container">
178
  <h1>🌌 Study Guide: Gaussian Mixture Models (GMM)</h1>
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <h2>🔹 Core Concepts</h2>
181
  <div class="story-gmm">
182
  <p><strong>Story-style intuition: The Expert Fruit Sorter</strong></p>
 
177
  <div class="container">
178
  <h1>🌌 Study Guide: Gaussian Mixture Models (GMM)</h1>
179
 
180
+ <!-- button -->
181
+ <div>
182
+ <!-- Audio Element -->
183
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
184
+ but since this is triggered by a click, it should work fine. -->
185
+
186
+
187
+ <a
188
+ href="/gaussian-mixture-three"
189
+ target="_blank"
190
+ onclick="playSound()"
191
+ class="
192
+ cursor-pointer
193
+ inline-block
194
+ relative
195
+ bg-blue-500
196
+ text-white
197
+ font-bold
198
+ py-4 px-8
199
+ rounded-xl
200
+ text-2xl
201
+ transition-all
202
+ duration-150
203
+
204
+ /* 3D Effect (Hard Shadow) */
205
+ shadow-[0_8px_0_rgb(29,78,216)]
206
+
207
+ /* Pressed State (Move down & remove shadow) */
208
+ active:shadow-none
209
+ active:translate-y-[8px]
210
+ ">
211
+ Tap Me!
212
+ </a>
213
+ </div>
214
+
215
+ <script>
216
+ function playSound() {
217
+ const audio = document.getElementById("clickSound");
218
+ if (audio) {
219
+ audio.currentTime = 0;
220
+ audio.play().catch(e => console.log("Audio play failed:", e));
221
+ }
222
+ }
223
+ </script>
224
+ <!-- button -->
225
+
226
  <h2>🔹 Core Concepts</h2>
227
  <div class="story-gmm">
228
  <p><strong>Story-style intuition: The Expert Fruit Sorter</strong></p>
templates/Gradient-Descen.html CHANGED
@@ -150,6 +150,53 @@
150
  <div class="container">
151
  <h1>Gradient Descent Study Guide</h1>
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  <h2>🔹 Core Concepts</h2>
154
  <div class="story">
155
  <p><strong>The Story: The Lost Hiker</strong></p>
 
150
  <div class="container">
151
  <h1>Gradient Descent Study Guide</h1>
152
 
153
+ <!-- button -->
154
+ <div>
155
+ <!-- Audio Element -->
156
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
157
+ but since this is triggered by a click, it should work fine. -->
158
+
159
+
160
+ <a
161
+ href="/gradient-descent-three"
162
+ target="_blank"
163
+ onclick="playSound()"
164
+ class="
165
+ cursor-pointer
166
+ inline-block
167
+ relative
168
+ bg-blue-500
169
+ text-white
170
+ font-bold
171
+ py-4 px-8
172
+ rounded-xl
173
+ text-2xl
174
+ transition-all
175
+ duration-150
176
+
177
+ /* 3D Effect (Hard Shadow) */
178
+ shadow-[0_8px_0_rgb(29,78,216)]
179
+
180
+ /* Pressed State (Move down & remove shadow) */
181
+ active:shadow-none
182
+ active:translate-y-[8px]
183
+ ">
184
+ Tap Me!
185
+ </a>
186
+ </div>
187
+
188
+ <script>
189
+ function playSound() {
190
+ const audio = document.getElementById("clickSound");
191
+ if (audio) {
192
+ audio.currentTime = 0;
193
+ audio.play().catch(e => console.log("Audio play failed:", e));
194
+ }
195
+ }
196
+ </script>
197
+ <!-- button -->
198
+
199
+
200
  <h2>🔹 Core Concepts</h2>
201
  <div class="story">
202
  <p><strong>The Story: The Lost Hiker</strong></p>
templates/Independent-Component-Analysis.html CHANGED
@@ -192,6 +192,53 @@
192
  <div class="container">
193
  <h1>🎙️ Study Guide: Independent Component Analysis (ICA)</h1>
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <h2>🔹 Core Concepts</h2>
196
  <div class="story-ica">
197
  <p><strong>Story-style intuition: The Cocktail Party Problem</strong></p>
 
192
  <div class="container">
193
  <h1>🎙️ Study Guide: Independent Component Analysis (ICA)</h1>
194
 
195
+
196
+ <!-- button -->
197
+ <div>
198
+ <!-- Audio Element -->
199
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
200
+ but since this is triggered by a click, it should work fine. -->
201
+
202
+
203
+ <a
204
+ href="/ica-three"
205
+ target="_blank"
206
+ onclick="playSound()"
207
+ class="
208
+ cursor-pointer
209
+ inline-block
210
+ relative
211
+ bg-blue-500
212
+ text-white
213
+ font-bold
214
+ py-4 px-8
215
+ rounded-xl
216
+ text-2xl
217
+ transition-all
218
+ duration-150
219
+
220
+ /* 3D Effect (Hard Shadow) */
221
+ shadow-[0_8px_0_rgb(29,78,216)]
222
+
223
+ /* Pressed State (Move down & remove shadow) */
224
+ active:shadow-none
225
+ active:translate-y-[8px]
226
+ ">
227
+ Tap Me!
228
+ </a>
229
+ </div>
230
+
231
+ <script>
232
+ function playSound() {
233
+ const audio = document.getElementById("clickSound");
234
+ if (audio) {
235
+ audio.currentTime = 0;
236
+ audio.play().catch(e => console.log("Audio play failed:", e));
237
+ }
238
+ }
239
+ </script>
240
+ <!-- button -->
241
+
242
  <h2>🔹 Core Concepts</h2>
243
  <div class="story-ica">
244
  <p><strong>Story-style intuition: The Cocktail Party Problem</strong></p>
templates/Linear-Discriminant-Analysis.html CHANGED
@@ -177,6 +177,53 @@
177
  <div class="container">
178
  <h1>🔍 Study Guide: Linear Discriminant Analysis (LDA)</h1>
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <h2>🔹 Core Concepts</h2>
181
  <div class="story-lda">
182
  <p><strong>Story-style intuition: The Smart Photographer</strong></p>
 
177
  <div class="container">
178
  <h1>🔍 Study Guide: Linear Discriminant Analysis (LDA)</h1>
179
 
180
+
181
+ <!-- button -->
182
+ <div>
183
+ <!-- Audio Element -->
184
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
185
+ but since this is triggered by a click, it should work fine. -->
186
+
187
+
188
+ <a
189
+ href="/lda-three"
190
+ target="_blank"
191
+ onclick="playSound()"
192
+ class="
193
+ cursor-pointer
194
+ inline-block
195
+ relative
196
+ bg-blue-500
197
+ text-white
198
+ font-bold
199
+ py-4 px-8
200
+ rounded-xl
201
+ text-2xl
202
+ transition-all
203
+ duration-150
204
+
205
+ /* 3D Effect (Hard Shadow) */
206
+ shadow-[0_8px_0_rgb(29,78,216)]
207
+
208
+ /* Pressed State (Move down & remove shadow) */
209
+ active:shadow-none
210
+ active:translate-y-[8px]
211
+ ">
212
+ Tap Me!
213
+ </a>
214
+ </div>
215
+
216
+ <script>
217
+ function playSound() {
218
+ const audio = document.getElementById("clickSound");
219
+ if (audio) {
220
+ audio.currentTime = 0;
221
+ audio.play().catch(e => console.log("Audio play failed:", e));
222
+ }
223
+ }
224
+ </script>
225
+ <!-- button -->
226
+
227
  <h2>🔹 Core Concepts</h2>
228
  <div class="story-lda">
229
  <p><strong>Story-style intuition: The Smart Photographer</strong></p>
templates/Naive-Bayes-Simulator.html ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>3D Naive Bayes Visualizer</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <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">
10
+ <style>
11
+ :root {
12
+ --background: #05080a;
13
+ --primary: #ff0055; /* Apple Red */
14
+ --secondary: #ffcc00; /* Orange Orange */
15
+ --accent: #00ffff;
16
+ --surface: #0a1014;
17
+ --border: #1e293b;
18
+ }
19
+
20
+ body {
21
+ background-color: var(--background);
22
+ color: #f8fafc;
23
+ font-family: 'Space Grotesk', sans-serif;
24
+ margin: 0;
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ .glass {
29
+ background: rgba(10, 16, 20, 0.85);
30
+ backdrop-filter: blur(12px);
31
+ border: 1px solid rgba(30, 41, 59, 0.5);
32
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
33
+ }
34
+
35
+ .canvas-container {
36
+ width: 100%;
37
+ height: 50vh; /* Responsive height */
38
+ min-height: 400px;
39
+ max-height: 600px;
40
+ position: relative;
41
+ background: radial-gradient(circle at center, #111827 0%, #000000 100%);
42
+ border-radius: 1rem;
43
+ overflow: hidden;
44
+ border: 1px solid var(--border);
45
+ cursor: grab;
46
+ }
47
+
48
+ .canvas-container:active { cursor: grabbing; }
49
+
50
+ .learning-log {
51
+ height: 120px;
52
+ overflow-y: auto;
53
+ scrollbar-width: thin;
54
+ }
55
+
56
+ /* Custom Scrollbar */
57
+ ::-webkit-scrollbar { width: 6px; }
58
+ ::-webkit-scrollbar-track { background: #0f172a; }
59
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
60
+
61
+ input[type=range] {
62
+ -webkit-appearance: none;
63
+ width: 100%;
64
+ background: transparent;
65
+ }
66
+
67
+ input[type=range]::-webkit-slider-runnable-track {
68
+ width: 100%;
69
+ height: 6px;
70
+ cursor: pointer;
71
+ background: #1e293b;
72
+ border-radius: 3px;
73
+ }
74
+
75
+ input[type=range]::-webkit-slider-thumb {
76
+ height: 18px;
77
+ width: 18px;
78
+ border-radius: 50%;
79
+ background: var(--accent);
80
+ cursor: pointer;
81
+ -webkit-appearance: none;
82
+ margin-top: -6px;
83
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
84
+ }
85
+
86
+ .math-box {
87
+ font-family: 'JetBrains Mono', monospace;
88
+ font-size: 10px;
89
+ }
90
+
91
+ .hidden { display: none; }
92
+ </style>
93
+ </head>
94
+ <body class="p-4 md:p-6 lg:p-8">
95
+
96
+ <div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_380px] gap-6 lg:gap-8">
97
+ <!-- Left Side: Visualizer -->
98
+ <div class="flex flex-col gap-4">
99
+ <header>
100
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
101
+ <div>
102
+ <h1 class="text-3xl md:text-4xl font-bold text-white mb-2">3D Naive Bayes <span class="text-cyan-400">Viz</span></h1>
103
+ <p class="text-slate-400 text-sm max-w-xl">
104
+ A classifier that learns by shaping 3D probability clouds. It assumes dimensions (Weight, Sweetness, Color) are independent.
105
+ </p>
106
+ <!-- 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>
107
+ </div>
108
+ </div>
109
+ </header>
110
+
111
+ <div class="relative">
112
+ <!-- 3D Canvas -->
113
+ <div class="canvas-container shadow-2xl" id="container">
114
+ <div id="three-canvas" class="w-full h-full"></div>
115
+
116
+ <!-- Top Overlays (Inside Canvas) -->
117
+ <div class="absolute top-4 left-4 pointer-events-none flex flex-col gap-2 z-10 max-w-[200px]">
118
+ <div class="glass px-3 py-2 rounded-lg">
119
+ <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div>
120
+ <div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase font-bold animate-pulse">Waiting to Learn</div>
121
+ </div>
122
+ <div class="glass px-3 py-2 rounded-lg flex flex-col gap-1">
123
+ <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest mb-1">Legend</div>
124
+ <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>
125
+ <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>
126
+ <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>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Measurement Label (Inside Canvas) -->
131
+ <div class="absolute top-4 right-4 pointer-events-none text-right z-10">
132
+ <div class="glass px-3 py-2 rounded-lg">
133
+ <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Input Features</div>
134
+ <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>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Instructions Overlay (Mobile/Desktop) -->
139
+ <div class="absolute bottom-4 left-4 pointer-events-none z-10 hidden md:block">
140
+ <div class="glass px-3 py-2 rounded-lg text-[10px] text-slate-400">
141
+ <b>Left-Click</b> Rotate &nbsp;|&nbsp; <b>Right-Click</b> Pan &nbsp;|&nbsp; <b>Shift+Drag</b> Lift Y-Axis
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Result Card: Below on Mobile, Absolute Overlay on Desktop -->
147
+ <div class="mt-4 md:mt-0 md:absolute md:bottom-4 md:right-4 z-20 w-full md:w-80">
148
+ <div class="glass p-4 rounded-xl shadow-xl border border-slate-700/50">
149
+ <div class="flex justify-between items-center mb-3">
150
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prediction Engine</div>
151
+ <div id="uncertainty-badge" class="hidden text-[9px] bg-slate-700 text-white px-2 py-0.5 rounded font-mono">UNCERTAIN</div>
152
+ </div>
153
+
154
+ <!-- Apple Calc -->
155
+ <div class="space-y-1 mb-3">
156
+ <div class="flex justify-between text-[10px] text-[#ff0055] font-bold uppercase items-center">
157
+ <span>Apple Likelihood</span>
158
+ <span id="prob-a-val" class="text-xs">50%</span>
159
+ </div>
160
+ <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
161
+ <div id="bar-a" class="h-full bg-[#ff0055] transition-all duration-300" style="width: 50%"></div>
162
+ </div>
163
+ <div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
164
+ <span>Prior(<span id="prior-a">.5</span>)</span>
165
+ <span>×</span>
166
+ <span>L(<span id="total-l-a">0</span>)</span>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Orange Calc -->
171
+ <div class="space-y-1 mb-4">
172
+ <div class="flex justify-between text-[10px] text-[#ffcc00] font-bold uppercase items-center">
173
+ <span>Orange Likelihood</span>
174
+ <span id="prob-b-val" class="text-xs">50%</span>
175
+ </div>
176
+ <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden">
177
+ <div id="bar-b" class="h-full bg-[#ffcc00] transition-all duration-300" style="width: 50%"></div>
178
+ </div>
179
+ <div class="math-box text-[9px] text-slate-500 flex justify-between px-1">
180
+ <span>Prior(<span id="prior-b">.5</span>)</span>
181
+ <span>×</span>
182
+ <span>L(<span id="total-l-b">0</span>)</span>
183
+ </div>
184
+ </div>
185
+
186
+ <div class="pt-3 border-t border-slate-700/50 text-center">
187
+ <div class="text-[9px] text-slate-500 uppercase font-bold mb-1">Final Classification</div>
188
+ <div id="result-text" class="text-2xl font-bold text-white uppercase tracking-tighter">NEUTRAL</div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- The Learning Log -->
195
+ <div class="glass rounded-xl p-4 flex flex-col h-40">
196
+ <h3 class="text-xs font-bold text-white mb-2 flex items-center gap-2 shrink-0">
197
+ <span class="text-cyan-400">⚡</span> System Logs
198
+ </h3>
199
+ <div id="learning-log" class="learning-log text-xs font-mono text-slate-400 space-y-1.5 pr-2">
200
+ <div>> System initialized.</div>
201
+ <div>> Waiting for training data...</div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Right Side: Controls -->
207
+ <aside class="space-y-6">
208
+ <div class="glass rounded-xl p-5 space-y-6">
209
+ <h2 class="text-lg font-bold text-white flex items-center gap-2">
210
+ <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>
211
+ Control Panel
212
+ </h2>
213
+
214
+ <div class="space-y-4">
215
+ <div>
216
+ <div class="flex justify-between items-center mb-2">
217
+ <label class="text-xs font-bold text-slate-400 uppercase">Search Variance (Alpha)</label>
218
+ <span id="alpha-value" class="text-cyan-400 font-mono text-sm bg-cyan-950 px-2 rounded">1.00</span>
219
+ </div>
220
+ <input type="range" id="alpha-slider" min="0.1" max="5.0" step="0.1" value="1.0">
221
+ <p class="text-[10px] text-slate-500 mt-2">
222
+ Higher alpha = wider probability clouds (High Bias). Lower alpha = tighter clouds (High Variance).
223
+ </p>
224
+ </div>
225
+
226
+ <div class="grid grid-cols-1 gap-3 pt-2">
227
+ <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">
228
+ <div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
229
+ <span id="toggle-icon">▶</span> <span id="toggle-text">Start Training</span>
230
+ </button>
231
+ <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">
232
+ Reset Simulation
233
+ </button>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Scenario Selection -->
239
+ <div class="glass rounded-xl p-5 space-y-3">
240
+ <h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-1">Data Scenario</h3>
241
+ <div class="grid grid-cols-2 gap-2">
242
+ <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>
243
+ <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>
244
+ </div>
245
+ <p class="text-[10px] text-slate-500 leading-relaxed pt-2 border-t border-slate-800">
246
+ Switching scenarios resets the robot's memory.
247
+ </p>
248
+ </div>
249
+
250
+ <!-- Learning Concept Card -->
251
+ <div class="glass rounded-xl p-5 border-l-2 border-cyan-400">
252
+ <h3 class="text-xs font-bold text-cyan-400 uppercase tracking-widest mb-2">How it works</h3>
253
+ <p class="text-[11px] text-slate-300 leading-relaxed">
254
+ The robot calculates the center (mean) and spread (variance) of the points it sees.
255
+ <br><br>
256
+ To predict the mystery fruit, it measures the distance to each cloud center relative to the cloud's size.
257
+ <br><br>
258
+ <span class="text-white font-bold">Naive Assumption:</span> It processes Width, Sweetness, and Color completely separately, then multiplies the results.
259
+ </p>
260
+ </div>
261
+ </aside>
262
+ </div>
263
+
264
+ <script>
265
+ // --- DATA CONFIG ---
266
+ const SCENARIOS = [
267
+ { name: 'Clustered', points: 80, spread: 1.2 },
268
+ { name: 'Scattered', points: 200, spread: 3.5 }
269
+ ];
270
+
271
+ let currentScenario = SCENARIOS[0];
272
+ let dataPoints = [];
273
+ let processedIdx = 0;
274
+ let isRunning = false;
275
+ let loopId = null;
276
+ let alpha = 1.0;
277
+
278
+ // Model State
279
+ let model = [
280
+ { name: 'Apple', color: 0xff0055, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 },
281
+ { name: 'Orange', color: 0xffcc00, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 }
282
+ ];
283
+
284
+ // --- THREE.JS SETUP ---
285
+ const container = document.getElementById('container');
286
+ const scene = new THREE.Scene();
287
+ // Dark gradient background effect via clear color not possible easily, handled via CSS
288
+
289
+ const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
290
+
291
+ let cameraRadius = 35;
292
+ let cameraPhi = Math.PI / 2.5;
293
+ let cameraTheta = Math.PI / 4;
294
+
295
+ function updateCameraPosition() {
296
+ camera.position.x = cameraRadius * Math.sin(cameraPhi) * Math.sin(cameraTheta);
297
+ camera.position.y = cameraRadius * Math.cos(cameraPhi);
298
+ camera.position.z = cameraRadius * Math.sin(cameraPhi) * Math.cos(cameraTheta);
299
+ camera.lookAt(0, 0, 0);
300
+ }
301
+ updateCameraPosition();
302
+
303
+ const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
304
+ renderer.setSize(container.clientWidth, container.clientHeight);
305
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
306
+ document.getElementById('three-canvas').appendChild(renderer.domElement);
307
+
308
+ // Lighting
309
+ scene.add(new THREE.AmbientLight(0xffffff, 0.7));
310
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
311
+ dirLight.position.set(10, 20, 10);
312
+ scene.add(dirLight);
313
+
314
+ // Axes & Grid
315
+ const grid = new THREE.GridHelper(30, 30, 0x1e293b, 0x0f172a);
316
+ grid.position.y = -5;
317
+ scene.add(grid);
318
+
319
+ // Custom Axes
320
+ function createAxis(start, end, color) {
321
+ const points = [start, end];
322
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
323
+ const material = new THREE.LineBasicMaterial({ color: color });
324
+ return new THREE.Line(geometry, material);
325
+ }
326
+ // X (Red), Y (Green), Z (Blue)
327
+ scene.add(createAxis(new THREE.Vector3(-15, -5, 0), new THREE.Vector3(15, -5, 0), 0x334155));
328
+ scene.add(createAxis(new THREE.Vector3(0, -15, 0), new THREE.Vector3(0, 15, 0), 0x334155));
329
+ scene.add(createAxis(new THREE.Vector3(0, -5, -15), new THREE.Vector3(0, -5, 15), 0x334155));
330
+
331
+ // Data Points Group
332
+ const pointGroup = new THREE.Group();
333
+ scene.add(pointGroup);
334
+
335
+ // Mystery Fruit (Interactive)
336
+ const mysteryFruitMesh = new THREE.Mesh(
337
+ new THREE.SphereGeometry(0.7, 32, 32),
338
+ new THREE.MeshStandardMaterial({
339
+ color: 0xffffff,
340
+ roughness: 0.2,
341
+ metalness: 0.1,
342
+ emissive: 0xffffff,
343
+ emissiveIntensity: 0.2
344
+ })
345
+ );
346
+ mysteryFruitMesh.position.set(0, 0, 0); // Start neutral
347
+ scene.add(mysteryFruitMesh);
348
+
349
+ // Ring indicator for mystery fruit
350
+ const ringGeo = new THREE.RingGeometry(0.8, 0.9, 32);
351
+ const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
352
+ const ring = new THREE.Mesh(ringGeo, ringMat);
353
+ ring.rotation.x = -Math.PI / 2;
354
+ mysteryFruitMesh.add(ring);
355
+
356
+ // Measurement Lines
357
+ const measureLines = new THREE.LineSegments(
358
+ new THREE.BufferGeometry(),
359
+ new THREE.LineDashedMaterial({ color: 0x00ffff, dashSize: 0.4, gapSize: 0.2, opacity: 0.5, transparent: true })
360
+ );
361
+ // Fixed: Removed premature call to computeLineDistances() on empty geometry
362
+ scene.add(measureLines);
363
+
364
+ // Gaussian Clouds (Visualizing Variance)
365
+ const clouds = model.map(m => {
366
+ const mesh = new THREE.Mesh(
367
+ new THREE.SphereGeometry(1, 32, 32),
368
+ new THREE.MeshBasicMaterial({
369
+ color: m.color,
370
+ transparent: true,
371
+ opacity: 0.1,
372
+ wireframe: true,
373
+ depthWrite: false
374
+ })
375
+ );
376
+ mesh.visible = false; // Hide until training starts
377
+ scene.add(mesh);
378
+ return mesh;
379
+ });
380
+
381
+ // --- MATH & LOGIC ---
382
+
383
+ function gaussian(x, mean, variance) {
384
+ // Prevent division by zero or extremely small variance
385
+ const v = Math.max(variance, 0.1) + (alpha * 0.2);
386
+ // Add tiny epsilon to avoid pure 0 underflow in exp
387
+ return (1 / Math.sqrt(2 * Math.PI * v)) * Math.exp(-Math.pow(x - mean, 2) / (2 * v)) + 1e-9;
388
+ }
389
+
390
+ function generateData() {
391
+ dataPoints = [];
392
+ pointGroup.clear();
393
+
394
+ // Offsets to make them distinct
395
+ const appleCenter = { x: -6, y: -2, z: -4 };
396
+ const orangeCenter = { x: 6, y: 3, z: 4 };
397
+
398
+ model.forEach((m, i) => {
399
+ const center = i === 0 ? appleCenter : orangeCenter;
400
+
401
+ for (let j = 0; j < currentScenario.points / 2; j++) {
402
+ const x = center.x + (Math.random() - 0.5) * currentScenario.spread * 5;
403
+ const y = center.y + (Math.random() - 0.5) * currentScenario.spread * 5;
404
+ const z = center.z + (Math.random() - 0.5) * currentScenario.spread * 5;
405
+
406
+ dataPoints.push({ x, y, z, classId: i });
407
+
408
+ const p = new THREE.Mesh(
409
+ new THREE.SphereGeometry(0.2, 8, 8),
410
+ new THREE.MeshBasicMaterial({ color: m.color, transparent: true, opacity: 0.6 })
411
+ );
412
+ p.position.set(x, y, z);
413
+ p.visible = false;
414
+ pointGroup.add(p);
415
+ }
416
+ });
417
+ // Shuffle
418
+ dataPoints.sort(() => Math.random() - 0.5);
419
+ }
420
+
421
+ function trainStep() {
422
+ if (processedIdx >= dataPoints.length) {
423
+ stopTraining();
424
+ addLog("Training complete. Model optimized.");
425
+ return;
426
+ }
427
+
428
+ // Process a batch for speed
429
+ const batchSize = 2;
430
+ for(let k=0; k<batchSize && processedIdx < dataPoints.length; k++) {
431
+ const p = dataPoints[processedIdx];
432
+ const m = model[p.classId];
433
+
434
+ // Online Mean/Variance Update (Welford's algorithm simplified)
435
+ m.count++;
436
+ const lr = 1.0 / (m.count + 5); // Decaying learning rate for stability
437
+
438
+ // Update Mean
439
+ const oldMean = { ...m.mean };
440
+ m.mean.x += lr * (p.x - m.mean.x);
441
+ m.mean.y += lr * (p.y - m.mean.y);
442
+ m.mean.z += lr * (p.z - m.mean.z);
443
+
444
+ // Update Variance (Approximation for visualizer)
445
+ const varLr = 0.1;
446
+ m.var.x = (1 - varLr) * m.var.x + varLr * Math.pow(p.x - m.mean.x, 2);
447
+ m.var.y = (1 - varLr) * m.var.y + varLr * Math.pow(p.y - m.mean.y, 2);
448
+ m.var.z = (1 - varLr) * m.var.z + varLr * Math.pow(p.z - m.mean.z, 2);
449
+
450
+ pointGroup.children[processedIdx].visible = true;
451
+ processedIdx++;
452
+ }
453
+
454
+ if (processedIdx % 10 === 0) updateVisuals();
455
+ predict();
456
+ }
457
+
458
+ function updateVisuals() {
459
+ model.forEach((m, i) => {
460
+ clouds[i].visible = true;
461
+ clouds[i].position.set(m.mean.x, m.mean.y, m.mean.z);
462
+
463
+ // Visual scale based on standard deviation (sqrt of variance)
464
+ // Add base size so it doesn't disappear
465
+ const sx = Math.sqrt(m.var.x) * 2.5 + alpha;
466
+ const sy = Math.sqrt(m.var.y) * 2.5 + alpha;
467
+ const sz = Math.sqrt(m.var.z) * 2.5 + alpha;
468
+
469
+ clouds[i].scale.set(sx, sy, sz);
470
+ });
471
+ }
472
+
473
+ function predict() {
474
+ const p = mysteryFruitMesh.position;
475
+ const totalPoints = Math.max(1, processedIdx);
476
+
477
+ model.forEach(m => {
478
+ // Naive Bayes Formula: P(Class|Features) ∝ P(Class) * P(F1|Class) * P(F2|Class)...
479
+
480
+ // 1. Prior: Frequency of class (laplace smoothing)
481
+ const prior = (m.count + 1) / (totalPoints + 2);
482
+
483
+ // 2. Likelihoods for each dimension (Gaussian)
484
+ const lW = gaussian(p.x, m.mean.x, m.var.x);
485
+ const lC = gaussian(p.y, m.mean.y, m.var.y);
486
+ const lS = gaussian(p.z, m.mean.z, m.var.z);
487
+
488
+ // Store for UI
489
+ m.lastCalc = { prior, lW, lS, lC };
490
+ m.score = prior * lW * lS * lC;
491
+ });
492
+
493
+ // Normalize probabilities
494
+ const totalScore = model[0].score + model[1].score;
495
+
496
+ let probA = 0, probB = 0;
497
+
498
+ if (totalScore > 0) {
499
+ probA = (model[0].score / totalScore) * 100;
500
+ probB = (model[1].score / totalScore) * 100;
501
+ } else {
502
+ // Handle 0 score / underflow case
503
+ probA = 50;
504
+ probB = 50;
505
+ }
506
+
507
+ updateUI(probA, probB);
508
+ updateMeasurementLines();
509
+ }
510
+
511
+ function updateUI(probA, probB) {
512
+ // Bars
513
+ document.getElementById('bar-a').style.width = `${probA}%`;
514
+ document.getElementById('bar-b').style.width = `${probB}%`;
515
+
516
+ // Text
517
+ document.getElementById('prob-a-val').innerText = probA.toFixed(1) + '%';
518
+ document.getElementById('prob-b-val').innerText = probB.toFixed(1) + '%';
519
+
520
+ // Details
521
+ document.getElementById('prior-a').innerText = model[0].lastCalc?.prior.toFixed(2) || '0.5';
522
+ document.getElementById('prior-b').innerText = model[1].lastCalc?.prior.toFixed(2) || '0.5';
523
+
524
+ // Just showing one likelihood sum for brevity in UI
525
+ const totLA = (model[0].lastCalc?.lW + model[0].lastCalc?.lC + model[0].lastCalc?.lS) || 0;
526
+ const totLB = (model[1].lastCalc?.lW + model[1].lastCalc?.lC + model[1].lastCalc?.lS) || 0;
527
+ document.getElementById('total-l-a').innerText = totLA.toFixed(2);
528
+ document.getElementById('total-l-b').innerText = totLB.toFixed(2);
529
+
530
+ // Winner Logic (with Neutral zone)
531
+ const resultText = document.getElementById('result-text');
532
+ const badge = document.getElementById('uncertainty-badge');
533
+
534
+ if (Math.abs(probA - probB) < 2) {
535
+ // Too close to call
536
+ resultText.innerText = "NEUTRAL";
537
+ resultText.style.color = "#94a3b8"; // slate-400
538
+ badge.classList.remove('hidden');
539
+ } else if (probA > probB) {
540
+ resultText.innerText = "APPLE";
541
+ resultText.style.color = "#ff0055";
542
+ badge.classList.add('hidden');
543
+ } else {
544
+ resultText.innerText = "ORANGE";
545
+ resultText.style.color = "#ffcc00";
546
+ badge.classList.add('hidden');
547
+ }
548
+ }
549
+
550
+ function updateMeasurementLines() {
551
+ const p = mysteryFruitMesh.position;
552
+ // Lines projecting to axes for visual reference
553
+ const points = [
554
+ p.x, p.y, p.z, p.x, -5, p.z, // To floor
555
+ p.x, -5, p.z, p.x, -5, 0, // On floor to X-axis
556
+ p.x, -5, p.z, 0, -5, p.z // On floor to Z-axis
557
+ ];
558
+
559
+ const vertices = [];
560
+ for(let i=0; i<points.length; i+=3) {
561
+ vertices.push(new THREE.Vector3(points[i], points[i+1], points[i+2]));
562
+ }
563
+ measureLines.geometry.setFromPoints(vertices);
564
+ measureLines.computeLineDistances();
565
+
566
+ document.getElementById('pos-display').innerText =
567
+ `W:${p.x.toFixed(1)} S:${p.z.toFixed(1)} C:${p.y.toFixed(1)}`;
568
+ }
569
+
570
+ // --- CONTROLS ---
571
+
572
+ function startTraining() {
573
+ if (isRunning) return;
574
+ isRunning = true;
575
+ document.getElementById('toggle-text').innerText = 'Pause';
576
+ document.getElementById('toggle-icon').innerText = '⏸';
577
+ document.getElementById('status-text').innerText = 'Training...';
578
+ document.getElementById('status-text').classList.remove('animate-pulse');
579
+
580
+ loopId = setInterval(trainStep, 50);
581
+ }
582
+
583
+ function stopTraining() {
584
+ isRunning = false;
585
+ document.getElementById('toggle-text').innerText = 'Resume';
586
+ document.getElementById('toggle-icon').innerText = '▶';
587
+ document.getElementById('status-text').innerText = 'Paused / Idle';
588
+ clearInterval(loopId);
589
+ }
590
+
591
+ function reset() {
592
+ stopTraining();
593
+ processedIdx = 0;
594
+ // Reset math model
595
+ model.forEach(m => {
596
+ m.count = 0;
597
+ m.mean = {x:0, y:0, z:0};
598
+ m.var = {x:1, y:1, z:1};
599
+ m.score = 0;
600
+ });
601
+
602
+ clouds.forEach(c => c.visible = false);
603
+ document.getElementById('toggle-text').innerText = 'Start Training';
604
+ document.getElementById('toggle-icon').innerText = '▶';
605
+ document.getElementById('status-text').innerText = 'Ready';
606
+ document.getElementById('status-text').classList.add('animate-pulse');
607
+
608
+ // Reset position
609
+ mysteryFruitMesh.position.set(0, 0, 0);
610
+
611
+ generateData();
612
+ addLog("Memory wiped. System reset.");
613
+ predict();
614
+ }
615
+
616
+ function selectScenario(idx) {
617
+ currentScenario = SCENARIOS[idx];
618
+
619
+ // Update UI buttons
620
+ document.getElementById('scen-0').className = idx === 0
621
+ ? "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)]"
622
+ : "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";
623
+
624
+ document.getElementById('scen-1').className = idx === 1
625
+ ? "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)]"
626
+ : "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";
627
+
628
+ reset();
629
+ }
630
+
631
+ function addLog(msg) {
632
+ const log = document.getElementById('learning-log');
633
+ const div = document.createElement('div');
634
+ const time = new Date().toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit'});
635
+ div.innerHTML = `<span class="text-slate-600">[${time}]</span> ${msg}`;
636
+ log.prepend(div);
637
+ }
638
+
639
+ // --- EVENTS ---
640
+ document.getElementById('btn-toggle').onclick = () => isRunning ? stopTraining() : startTraining();
641
+ document.getElementById('btn-reset').onclick = reset;
642
+ document.getElementById('alpha-slider').oninput = (e) => {
643
+ alpha = parseFloat(e.target.value);
644
+ document.getElementById('alpha-value').innerText = alpha.toFixed(2);
645
+ updateVisuals();
646
+ predict();
647
+ };
648
+
649
+ // 3D Interaction Logic
650
+ let isMouseDown = false;
651
+ let mouseButton = 0;
652
+ let prevMouse = { x: 0, y: 0 };
653
+
654
+ container.addEventListener('mousedown', (e) => {
655
+ isMouseDown = true;
656
+ mouseButton = e.button;
657
+ prevMouse = { x: e.clientX, y: e.clientY };
658
+ });
659
+
660
+ window.addEventListener('mouseup', () => isMouseDown = false);
661
+ container.addEventListener('contextmenu', (e) => e.preventDefault());
662
+
663
+ container.addEventListener('mousemove', (e) => {
664
+ if (!isMouseDown) return;
665
+ const dx = e.clientX - prevMouse.x;
666
+ const dy = e.clientY - prevMouse.y;
667
+
668
+ // Left Click (0): Rotate Camera
669
+ if (mouseButton === 0 && !e.shiftKey) {
670
+ cameraTheta -= dx * 0.01;
671
+ cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi - dy * 0.01));
672
+ updateCameraPosition();
673
+ }
674
+ // Shift + Drag: Lift Object
675
+ else if (e.shiftKey) {
676
+ mysteryFruitMesh.position.y -= dy * 0.1;
677
+ mysteryFruitMesh.position.y = Math.max(-10, Math.min(10, mysteryFruitMesh.position.y));
678
+ predict();
679
+ }
680
+ // Right Click (2): Move Object Plane
681
+ else if (mouseButton === 2 || (mouseButton === 0 && e.ctrlKey)) {
682
+ // Move relative to camera view roughly
683
+ const speed = 0.1;
684
+ const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
685
+ forward.y = 0; forward.normalize();
686
+ const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
687
+ right.y = 0; right.normalize();
688
+
689
+ mysteryFruitMesh.position.addScaledVector(right, dx * speed);
690
+ mysteryFruitMesh.position.addScaledVector(forward, dy * speed);
691
+
692
+ // Bounds
693
+ ['x', 'y', 'z'].forEach(axis => {
694
+ mysteryFruitMesh.position[axis] = Math.max(-14, Math.min(14, mysteryFruitMesh.position[axis]));
695
+ });
696
+
697
+ predict();
698
+ }
699
+ prevMouse = { x: e.clientX, y: e.clientY };
700
+ });
701
+
702
+ // Zoom
703
+ container.addEventListener('wheel', (e) => {
704
+ e.preventDefault();
705
+ cameraRadius = Math.max(10, Math.min(60, cameraRadius + e.deltaY * 0.05));
706
+ updateCameraPosition();
707
+ }, { passive: false });
708
+
709
+ window.addEventListener('resize', () => {
710
+ camera.aspect = container.clientWidth / container.clientHeight;
711
+ camera.updateProjectionMatrix();
712
+ renderer.setSize(container.clientWidth, container.clientHeight);
713
+ });
714
+
715
+ // Animation Loop
716
+ function animate() {
717
+ requestAnimationFrame(animate);
718
+ // Spin the ring
719
+ ring.rotation.z -= 0.02;
720
+ renderer.render(scene, camera);
721
+ }
722
+
723
+ // Init
724
+ generateData();
725
+ predict();
726
+ animate();
727
+ updateUI(50, 50); // Initial neutral state
728
+ </script>
729
+ </body>
730
+ </html>
templates/Neural-Networks-for-Classification-three.html ADDED
@@ -0,0 +1,916 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Neural Network Playground</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <!-- Lucide Icons -->
9
+ <script src="https://unpkg.com/lucide@latest"></script>
10
+
11
+ <style>
12
+ :root {
13
+ --background: 222 47% 6%;
14
+ --foreground: 210 40% 96%;
15
+ --card: 222 47% 8%;
16
+ --primary: 199 89% 48%;
17
+ --primary-foreground: 222 47% 6%;
18
+ --secondary: 280 65% 55%;
19
+ --secondary-foreground: 210 40% 98%;
20
+ --muted: 217 33% 15%;
21
+ --muted-foreground: 215 20% 55%;
22
+ --accent: 142 71% 45%;
23
+ --destructive: 0 84% 60%;
24
+ --destructive-foreground: 210 40% 98%;
25
+ --border: 217 33% 20%;
26
+
27
+ /* Custom neural network colors */
28
+ --node-input: 199 89% 48%;
29
+ --node-hidden: 280 65% 55%;
30
+ --node-positive: 142 71% 45%; /* Class A (Green) */
31
+ --node-negative: 350 89% 60%; /* Class B (Red) */
32
+
33
+ --radius: 0.75rem;
34
+ }
35
+
36
+ body {
37
+ background-color: hsl(var(--background));
38
+ color: hsl(var(--foreground));
39
+ font-family: system-ui, -apple-system, sans-serif;
40
+ }
41
+
42
+ .glass-panel {
43
+ background-color: hsla(var(--card), 0.6);
44
+ backdrop-filter: blur(16px);
45
+ border: 1px solid hsla(var(--border), 0.5);
46
+ border-radius: var(--radius);
47
+ box-shadow: 0 0 30px hsl(199 89% 48% / 0.1);
48
+ }
49
+
50
+ .gradient-text {
51
+ background: linear-gradient(135deg, hsl(199 89% 48%) 0%, hsl(280 65% 55%) 100%);
52
+ -webkit-background-clip: text;
53
+ -webkit-text-fill-color: transparent;
54
+ background-clip: text;
55
+ }
56
+
57
+ /* Animation Utilities */
58
+ @keyframes flow {
59
+ 0% { stroke-dashoffset: 20; }
60
+ 100% { stroke-dashoffset: 0; }
61
+ }
62
+ .animate-flow {
63
+ animation: flow 1s linear infinite;
64
+ }
65
+
66
+ @keyframes pulse-glow {
67
+ 0%, 100% { opacity: 1; filter: brightness(1); }
68
+ 50% { opacity: 0.7; filter: brightness(1.2); }
69
+ }
70
+ .animate-node-pulse {
71
+ animation: pulse-glow 2s ease-in-out infinite;
72
+ }
73
+
74
+ /* Custom Scrollbar */
75
+ ::-webkit-scrollbar { width: 8px; }
76
+ ::-webkit-scrollbar-track { background: hsl(var(--background)); }
77
+ ::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 4px; }
78
+ ::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
79
+
80
+ input[type=range] {
81
+ -webkit-appearance: none;
82
+ background: transparent;
83
+ }
84
+ input[type=range]::-webkit-slider-thumb {
85
+ -webkit-appearance: none;
86
+ height: 16px;
87
+ width: 16px;
88
+ border-radius: 50%;
89
+ background: hsl(var(--primary));
90
+ cursor: pointer;
91
+ margin-top: -6px;
92
+ }
93
+ input[type=range]::-webkit-slider-runnable-track {
94
+ width: 100%;
95
+ height: 4px;
96
+ cursor: pointer;
97
+ background: hsl(var(--muted));
98
+ border-radius: 2px;
99
+ }
100
+
101
+ .btn {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ border-radius: 0.5rem;
106
+ font-size: 0.875rem;
107
+ font-weight: 500;
108
+ transition-colors: 0.15s;
109
+ cursor: pointer;
110
+ }
111
+ .btn:disabled {
112
+ opacity: 0.5;
113
+ pointer-events: none;
114
+ }
115
+ .btn-glass { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: white; }
116
+ .btn-glass:hover { background: rgba(255,255,255,0.1); }
117
+ .btn-accent { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
118
+ .btn-destructive { background: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
119
+ .btn-glow {
120
+ background: hsl(var(--primary));
121
+ color: white;
122
+ box-shadow: 0 0 15px hsl(var(--primary)/0.5);
123
+ }
124
+ .btn-glow:hover { box-shadow: 0 0 25px hsl(var(--primary)/0.6); }
125
+
126
+ .tab-btn {
127
+ flex: 1;
128
+ padding: 0.375rem;
129
+ font-size: 0.875rem;
130
+ font-weight: 500;
131
+ border-radius: 0.375rem;
132
+ transition: all 0.2s;
133
+ color: hsl(var(--muted-foreground));
134
+ }
135
+ .tab-btn.active {
136
+ background-color: hsl(var(--card));
137
+ color: hsl(var(--primary));
138
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
139
+ }
140
+ </style>
141
+ </head>
142
+ <body class="min-h-screen p-4 selection:bg-[hsl(var(--primary))] selection:text-white">
143
+
144
+ <!-- Background Ambience -->
145
+ <div class="fixed inset-0 pointer-events-none overflow-hidden -z-10">
146
+ <div class="absolute top-0 left-1/4 w-96 h-96 bg-[hsl(var(--primary)/0.1)] rounded-full blur-[100px]"></div>
147
+ <div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[hsl(var(--secondary)/0.1)] rounded-full blur-[100px]"></div>
148
+ </div>
149
+
150
+ <!-- Header -->
151
+ <header class="relative z-10 border-b border-white/10 bg-[hsl(var(--background)/0.8)] backdrop-blur-md sticky top-0 mb-8 rounded-xl">
152
+ <div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
153
+ <div class="flex items-center gap-3">
154
+ <div class="p-2 rounded-xl bg-[hsl(var(--primary)/0.2)] animate-pulse">
155
+ <i data-lucide="brain" class="h-6 w-6 text-[hsl(var(--primary))]"></i>
156
+ </div>
157
+ <div>
158
+ <h1 class="text-xl font-bold gradient-text">Neural Network Playground</h1>
159
+ <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="/neural-network-classification" 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>
160
+ <p class="text-xs text-[hsl(var(--muted-foreground))]">Interactive Classification Visualizer</p>
161
+ <p class="text-xxl p-3 text-[hsl(var(--muted-foreground))]">After training you cant train agian and cant change output so if you want add a custom data in predefine data so add before training</p>
162
+ </div>
163
+ </div>
164
+ <div class="hidden md:flex items-center gap-4 text-sm text-[hsl(var(--muted-foreground))]">
165
+ <div class="flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-full">
166
+ <i data-lucide="layers" class="h-4 w-4"></i>
167
+ <span id="header-neurons-count">4 hidden neurons</span>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </header>
172
+
173
+ <!-- Main Content -->
174
+ <main class="relative z-10 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6">
175
+
176
+ <!-- Left Sidebar: Controls -->
177
+ <div class="lg:col-span-3 space-y-6">
178
+ <!-- Control Panel -->
179
+ <div class="glass-panel p-5 space-y-5">
180
+ <div>
181
+ <h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">Data Class</h3>
182
+ <div class="flex gap-2">
183
+ <button onclick="setClass(1)" id="btn-class-a" class="btn btn-accent h-9 px-3 flex-1 text-sm">
184
+ <div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))] mr-2"></div> Class A
185
+ </button>
186
+ <button onclick="setClass(0)" id="btn-class-b" class="btn btn-glass h-9 px-3 flex-1 text-sm">
187
+ <div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))] mr-2"></div> Class B
188
+ </button>
189
+ </div>
190
+ </div>
191
+
192
+ <div>
193
+ <h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
194
+ Hidden Neurons: <span id="neurons-display" class="text-[hsl(var(--primary))]">4</span>
195
+ </h3>
196
+ <div class="flex items-center gap-3">
197
+ <button onclick="changeNeurons(-1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="minus" class="h-4 w-4"></i></button>
198
+ <input type="range" min="1" max="8" value="4" class="flex-1" id="neurons-slider" oninput="changeNeuronsFromSlider(this.value)">
199
+ <button onclick="changeNeurons(1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="plus" class="h-4 w-4"></i></button>
200
+ </div>
201
+ </div>
202
+
203
+ <div>
204
+ <h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
205
+ Learning Rate: <span id="lr-display" class="text-[hsl(var(--secondary))]">0.50</span>
206
+ </h3>
207
+ <input type="range" min="1" max="100" value="50" class="w-full" id="lr-slider" oninput="changeLR(this.value)">
208
+ </div>
209
+
210
+ <div class="flex gap-2">
211
+ <button id="btn-train" onclick="toggleTraining()" class="btn btn-glow flex-1 h-10 px-4">
212
+ <i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network
213
+ </button>
214
+ <button onclick="resetApp()" class="btn btn-glass h-10 w-10 p-0">
215
+ <i data-lucide="rotate-ccw" class="h-4 w-4"></i>
216
+ </button>
217
+ </div>
218
+
219
+ <div id="accuracy-panel" class="text-center py-3 rounded-lg bg-white/5 hidden">
220
+ <span class="text-sm text-[hsl(var(--muted-foreground))]">Accuracy: </span>
221
+ <span id="accuracy-display" class="text-lg font-bold">0.0%</span>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Presets -->
226
+ <div class="glass-panel p-4 space-y-3">
227
+ <h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))]">Presets</h3>
228
+ <div class="grid grid-cols-2 gap-2">
229
+ <button onclick="loadPreset('Linear')" class="btn btn-glass flex flex-col h-auto py-3">
230
+ <i data-lucide="waves" class="h-4 w-4 mb-1"></i> <span class="text-xs">Linear</span>
231
+ </button>
232
+ <button onclick="loadPreset('XOR')" class="btn btn-glass flex flex-col h-auto py-3">
233
+ <i data-lucide="target" class="h-4 w-4 mb-1"></i> <span class="text-xs">XOR</span>
234
+ </button>
235
+ <button onclick="loadPreset('Circle')" class="btn btn-glass flex flex-col h-auto py-3">
236
+ <i data-lucide="circle" class="h-4 w-4 mb-1"></i> <span class="text-xs">Circle</span>
237
+ </button>
238
+ <button onclick="loadPreset('Spiral')" class="btn btn-glass flex flex-col h-auto py-3">
239
+ <i data-lucide="sparkles" class="h-4 w-4 mb-1"></i> <span class="text-xs">Spiral</span>
240
+ </button>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- Logs -->
245
+ <div class="glass-panel p-4 space-y-3">
246
+ <div class="flex justify-between items-center text-sm font-medium text-[hsl(var(--muted-foreground))]">
247
+ <span class="flex items-center gap-2"><i data-lucide="clock" class="h-4 w-4"></i> Training Log</span>
248
+ <span id="epoch-display" class="text-[hsl(var(--primary))] animate-pulse hidden">Epoch 0</span>
249
+ </div>
250
+ <div class="w-full bg-white/10 rounded-full h-1.5 overflow-hidden">
251
+ <div id="progress-bar" class="h-full bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--secondary))]" style="width: 0%"></div>
252
+ </div>
253
+ <div id="logs-container" class="space-y-1 max-h-32 overflow-y-auto">
254
+ <!-- Logs go here -->
255
+ </div>
256
+ </div>
257
+ </div>
258
+
259
+ <!-- Center: Visualizations -->
260
+ <div class="lg:col-span-6 space-y-6">
261
+ <!-- Network Vis -->
262
+ <div class="glass-panel p-6">
263
+ <div class="flex justify-between items-center mb-4">
264
+ <h2 class="text-lg font-semibold flex items-center gap-2">
265
+ <i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i> Network Architecture
266
+ </h2>
267
+ </div>
268
+ <div class="flex justify-center" id="network-container">
269
+ <!-- SVG Network goes here -->
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Data Canvas -->
274
+ <div class="glass-panel p-6">
275
+ <div class="flex justify-between items-center mb-4">
276
+ <h2 class="text-lg font-semibold">Data & Decision Boundary</h2>
277
+ <span class="text-xs px-2 py-1 bg-white/10 rounded text-[hsl(var(--primary))] font-mono">Points: <span id="points-count">0</span></span>
278
+ </div>
279
+ <div class="flex justify-center relative">
280
+ <div class="relative">
281
+ <canvas id="main-canvas" width="300" height="300" class="rounded-lg border border-white/10 cursor-crosshair shadow-2xl bg-black"></canvas>
282
+ <div class="absolute -bottom-6 left-0 right-0 text-center text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">X Coordinate</div>
283
+ <div class="absolute -left-6 top-1/2 -translate-y-1/2 -rotate-90 text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">Y Coordinate</div>
284
+ </div>
285
+ </div>
286
+ <div class="mt-4 flex flex-wrap justify-center gap-4 text-xs text-[hsl(var(--muted-foreground))]">
287
+ <div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))]"></div> Class A</div>
288
+ <div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))]"></div> Class B</div>
289
+ <div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-positive))/0.3]"></div> Prediction A</div>
290
+ <div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-negative))/0.3]"></div> Prediction B</div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- Right Sidebar: Explainers -->
296
+ <div class="lg:col-span-3 space-y-6">
297
+ <div class="w-full">
298
+ <div class="flex bg-white/5 p-1 rounded-lg mb-4">
299
+ <button onclick="switchTab('howItWorks')" id="tab-howItWorks" class="tab-btn active"><i data-lucide="lightbulb" class="h-3 w-3 mr-1 inline"></i> How It Works</button>
300
+ <button onclick="switchTab('learn')" id="tab-learn" class="tab-btn"><i data-lucide="sparkles" class="h-3 w-3 mr-1 inline"></i> Learn</button>
301
+ </div>
302
+
303
+ <div id="content-howItWorks" class="glass-panel p-5 space-y-4">
304
+ <h3 class="text-lg font-semibold gradient-text">Live Prediction (Hover)</h3>
305
+ <div class="space-y-4 text-sm">
306
+ <div>
307
+ <div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-input))]">
308
+ <span class="w-5 h-5 rounded-full bg-[hsl(var(--node-input))/0.2] flex items-center justify-center text-xs">1</span> Input
309
+ </div>
310
+ <div class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs">
311
+ X: <span id="val-x">0.00</span><br>
312
+ Y: <span id="val-y">0.00</span>
313
+ </div>
314
+ </div>
315
+ <div>
316
+ <div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-hidden))]">
317
+ <span class="w-5 h-5 rounded-full bg-[hsl(var(--node-hidden))/0.2] flex items-center justify-center text-xs">2</span> Hidden Layer
318
+ </div>
319
+ <div id="val-hidden" class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs grid grid-cols-4 gap-1">
320
+ <!-- Hidden values -->
321
+ </div>
322
+ </div>
323
+ <div>
324
+ <div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--accent))]">
325
+ <span class="w-5 h-5 rounded-full bg-[hsl(var(--accent))/0.2] flex items-center justify-center text-xs">3</span> Output
326
+ </div>
327
+ <div class="bg-white/5 p-3 rounded-lg border border-white/10">
328
+ <div class="flex justify-between items-center">
329
+ <span class="text-xs text-gray-400">Raw: <span id="val-raw">0.0000</span></span>
330
+ <span id="val-class" class="font-bold text-gray-500">-</span>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ <div id="content-learn" class="glass-panel p-5 space-y-4 hidden">
338
+ <div class="flex items-center gap-3">
339
+ <div class="p-2 rounded-lg bg-[hsl(var(--primary))/0.2]"><i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i></div>
340
+ <h3 class="font-semibold gradient-text">Training Process</h3>
341
+ </div>
342
+ <p class="text-sm text-gray-300 leading-relaxed">
343
+ The network learns by "Backpropagation". It compares its guess to the real label, finds the error, and adjusts the weights backwards from output to input.
344
+ </p>
345
+ <div class="p-3 rounded-lg bg-white/5 text-xs text-gray-400 border border-white/10">
346
+ 💡 <strong>Tip:</strong> If the network gets stuck, try increasing neurons or clicking "Reset" to randomize weights.
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </main>
352
+
353
+ <script>
354
+ // --- Neural Network Logic ---
355
+ class SimpleNeuralNetwork {
356
+ constructor(inputSize, hiddenSize, outputSize, learningRate) {
357
+ this.inputSize = inputSize;
358
+ this.hiddenSize = hiddenSize;
359
+ this.outputSize = outputSize;
360
+ this.learningRate = learningRate;
361
+
362
+ // Xavier initialization
363
+ const scale1 = Math.sqrt(2 / (this.inputSize + this.hiddenSize));
364
+ this.w1 = Array(this.hiddenSize).fill(0).map(() =>
365
+ Array(this.inputSize).fill(0).map(() => (Math.random() * 2 - 1) * scale1)
366
+ );
367
+ this.b1 = Array(this.hiddenSize).fill(0);
368
+
369
+ const scale2 = Math.sqrt(2 / (this.hiddenSize + this.outputSize));
370
+ this.w2 = Array(this.outputSize).fill(0).map(() =>
371
+ Array(this.hiddenSize).fill(0).map(() => (Math.random() * 2 - 1) * scale2)
372
+ );
373
+ this.b2 = Array(this.outputSize).fill(0);
374
+ }
375
+
376
+ sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
377
+ sigmoidDeriv(y) { return y * (1 - y); }
378
+
379
+ forward(inputs) {
380
+ const hActivations = this.w1.map((weights, i) =>
381
+ this.sigmoid(weights.reduce((acc, w, j) => acc + w * inputs[j], 0) + this.b1[i])
382
+ );
383
+ const outputs = this.w2.map((weights, i) =>
384
+ this.sigmoid(weights.reduce((acc, w, j) => acc + w * hActivations[j], 0) + this.b2[i])
385
+ );
386
+ return { activations: [[...inputs], hActivations, outputs], output: outputs[0] };
387
+ }
388
+
389
+ predict(x, y) { return this.forward([x, y]).output; }
390
+
391
+ train(data, batchSize) {
392
+ for(let k = 0; k < batchSize * 5; k++) {
393
+ const point = data[Math.floor(Math.random() * data.length)];
394
+ const inputs = [point.x, point.y];
395
+ const target = [point.label];
396
+
397
+ const { activations } = this.forward(inputs);
398
+ const hActivations = activations[1];
399
+ const outputs = activations[2];
400
+
401
+ const outputErrors = outputs.map((o, i) => target[i] - o);
402
+ const outputGradients = outputs.map((o, i) => outputErrors[i] * this.sigmoidDeriv(o));
403
+
404
+ const hiddenErrors = this.w1.map((_, i) =>
405
+ this.w2.reduce((acc, weights, j) => acc + weights[i] * outputGradients[j], 0)
406
+ );
407
+ const hiddenGradients = hActivations.map((h, i) => hiddenErrors[i] * this.sigmoidDeriv(h));
408
+
409
+ for(let i=0; i<this.outputSize; i++) {
410
+ for(let j=0; j<this.hiddenSize; j++) {
411
+ this.w2[i][j] += this.learningRate * outputGradients[i] * hActivations[j];
412
+ }
413
+ this.b2[i] += this.learningRate * outputGradients[i];
414
+ }
415
+
416
+ for(let i=0; i<this.hiddenSize; i++) {
417
+ for(let j=0; j<this.inputSize; j++) {
418
+ this.w1[i][j] += this.learningRate * hiddenGradients[i] * inputs[j];
419
+ }
420
+ this.b1[i] += this.learningRate * hiddenGradients[i];
421
+ }
422
+ }
423
+ }
424
+ getWeights() { return [this.w1, this.w2]; }
425
+ }
426
+
427
+ // --- Application State ---
428
+ let state = {
429
+ dataPoints: [],
430
+ currentClass: 1,
431
+ hiddenNeurons: 4,
432
+ learningRate: 0.5,
433
+ isTraining: false,
434
+ network: null,
435
+ epoch: 0,
436
+ accuracy: 0,
437
+ activations: null,
438
+ predictions: [], // Store heatmap data here
439
+ logs: [],
440
+ lastProbe: { x: 0, y: 0 } // Track last cursor position for live updates
441
+ };
442
+
443
+ // --- Helper: Generate Grid Predictions ---
444
+ function generatePredictions() {
445
+ const gridSize = 30;
446
+ const grid = [];
447
+ for (let i = 0; i < gridSize; i++) {
448
+ const row = [];
449
+ for (let j = 0; j < gridSize; j++) {
450
+ const x = (j / gridSize) * 2 - 1;
451
+ const y = 1 - (i / gridSize) * 2;
452
+ row.push(state.network.predict(x, y));
453
+ }
454
+ grid.push(row);
455
+ }
456
+ return grid;
457
+ }
458
+
459
+ // --- Initialization ---
460
+ function init() {
461
+ lucide.createIcons();
462
+ state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
463
+ state.predictions = generatePredictions(); // Initial heatmap
464
+ setupCanvas();
465
+ renderUI();
466
+
467
+ // Set initial dummy activations
468
+ state.activations = [[0,0], Array(state.hiddenNeurons).fill(0), [0]];
469
+ updateExplainers();
470
+ }
471
+
472
+ // --- UI Updates ---
473
+ function setClass(c) {
474
+ state.currentClass = c;
475
+ const btnA = document.getElementById('btn-class-a');
476
+ const btnB = document.getElementById('btn-class-b');
477
+
478
+ if(c === 1) {
479
+ btnA.classList.remove('btn-glass'); btnA.classList.add('btn-accent');
480
+ btnB.classList.add('btn-glass'); btnB.classList.remove('btn-destructive');
481
+ } else {
482
+ btnA.classList.add('btn-glass'); btnA.classList.remove('btn-accent');
483
+ btnB.classList.remove('btn-glass'); btnB.classList.add('btn-destructive');
484
+ }
485
+ }
486
+
487
+ function changeNeurons(delta) {
488
+ const newVal = Math.max(1, Math.min(8, state.hiddenNeurons + delta));
489
+ state.hiddenNeurons = newVal;
490
+ document.getElementById('neurons-slider').value = newVal;
491
+ updateNeuronsUI();
492
+ }
493
+
494
+ function changeNeuronsFromSlider(val) {
495
+ state.hiddenNeurons = parseInt(val);
496
+ updateNeuronsUI();
497
+ }
498
+
499
+ function updateNeuronsUI() {
500
+ document.getElementById('neurons-display').innerText = state.hiddenNeurons;
501
+ document.getElementById('header-neurons-count').innerText = state.hiddenNeurons + " hidden neurons";
502
+ resetApp(); // Rebuild network on architecture change
503
+ }
504
+
505
+ function changeLR(val) {
506
+ state.learningRate = val / 100;
507
+ document.getElementById('lr-display').innerText = state.learningRate.toFixed(2);
508
+ if(state.network) state.network.learningRate = state.learningRate;
509
+ }
510
+
511
+ function resetApp() {
512
+ state.dataPoints = [];
513
+ state.isTraining = false;
514
+ state.epoch = 0;
515
+ state.accuracy = 0;
516
+ state.logs = [];
517
+ state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
518
+ state.predictions = generatePredictions(); // Generate initial random boundary
519
+ state.lastProbe = { x: 0, y: 0 };
520
+
521
+ document.getElementById('points-count').innerText = "0";
522
+ document.getElementById('accuracy-panel').classList.add('hidden');
523
+ document.getElementById('epoch-display').classList.add('hidden');
524
+ document.getElementById('progress-bar').style.width = '0%';
525
+ document.getElementById('logs-container').innerHTML = '';
526
+
527
+ const btnTrain = document.getElementById('btn-train');
528
+ btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
529
+ lucide.createIcons();
530
+
531
+ renderCanvas();
532
+ renderNetwork();
533
+
534
+ // Recalc activations for default probe
535
+ state.activations = state.network.forward([0, 0]).activations;
536
+ updateExplainers();
537
+ }
538
+
539
+ function switchTab(tab) {
540
+ document.getElementById('tab-howItWorks').classList.remove('active');
541
+ document.getElementById('tab-learn').classList.remove('active');
542
+ document.getElementById('content-howItWorks').classList.add('hidden');
543
+ document.getElementById('content-learn').classList.add('hidden');
544
+
545
+ document.getElementById('tab-' + tab).classList.add('active');
546
+ document.getElementById('content-' + tab).classList.remove('hidden');
547
+ }
548
+
549
+ // --- Canvas Logic ---
550
+ const canvas = document.getElementById('main-canvas');
551
+ const ctx = canvas.getContext('2d');
552
+ const canvasSize = 300;
553
+ const gridSize = 30;
554
+
555
+ function setupCanvas() {
556
+ canvas.addEventListener('mousedown', handleCanvasClick);
557
+ canvas.addEventListener('mousemove', handleCanvasHover);
558
+ canvas.addEventListener('mouseleave', () => {
559
+ renderCanvas(); // clear hover
560
+ });
561
+ renderCanvas();
562
+ }
563
+
564
+ function handleCanvasClick(e) {
565
+ const rect = canvas.getBoundingClientRect();
566
+ const scaleX = canvas.width / rect.width;
567
+ const scaleY = canvas.height / rect.height;
568
+ const clickX = (e.clientX - rect.left) * scaleX;
569
+ const clickY = (e.clientY - rect.top) * scaleY;
570
+
571
+ const x = (clickX / (canvasSize / 2)) - 1;
572
+ const y = 1 - (clickY / (canvasSize / 2));
573
+
574
+ const point = {
575
+ x: Math.max(-1, Math.min(1, x)),
576
+ y: Math.max(-1, Math.min(1, y)),
577
+ label: state.currentClass
578
+ };
579
+
580
+ state.dataPoints.push(point);
581
+ state.lastProbe = { x, y }; // Update probe
582
+ document.getElementById('points-count').innerText = state.dataPoints.length;
583
+
584
+ // Forward pass for viz
585
+ state.activations = state.network.forward([point.x, point.y]).activations;
586
+ updateExplainers();
587
+
588
+ renderCanvas();
589
+ renderNetwork();
590
+ }
591
+
592
+ function handleCanvasHover(e) {
593
+ const rect = canvas.getBoundingClientRect();
594
+ const scaleX = canvas.width / rect.width;
595
+ const scaleY = canvas.height / rect.height;
596
+ const clickX = (e.clientX - rect.left) * scaleX;
597
+ const clickY = (e.clientY - rect.top) * scaleY;
598
+
599
+ renderCanvas();
600
+
601
+ // Draw hover cursor
602
+ ctx.beginPath();
603
+ ctx.arc(clickX, clickY, 8, 0, Math.PI * 2);
604
+ ctx.strokeStyle = state.currentClass === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
605
+ ctx.setLineDash([4, 4]);
606
+ ctx.stroke();
607
+ ctx.setLineDash([]);
608
+
609
+ // LIVE UPDATE: Calculate network output for current mouse position
610
+ const x = (clickX / (canvasSize / 2)) - 1;
611
+ const y = 1 - (clickY / (canvasSize / 2));
612
+ state.lastProbe = { x, y }; // Update probe tracker
613
+
614
+ if (state.network) {
615
+ state.activations = state.network.forward([x, y]).activations;
616
+ updateExplainers(); // Update the "Output" panel text
617
+ renderNetwork(); // Update the node visualizations/colors
618
+ }
619
+ }
620
+
621
+ function renderCanvas() {
622
+ // Background
623
+ ctx.fillStyle = 'hsl(222, 47%, 8%)';
624
+ ctx.fillRect(0, 0, canvasSize, canvasSize);
625
+
626
+ // Heatmap - Draw if predictions exist
627
+ if (state.predictions && state.predictions.length > 0) {
628
+ const cellSize = canvasSize / gridSize;
629
+ for (let i = 0; i < gridSize; i++) {
630
+ for (let j = 0; j < gridSize; j++) {
631
+ const pred = state.predictions[i][j];
632
+
633
+ const hue = pred > 0.5 ? 142 : 350;
634
+ const lightness = 20 + Math.abs(pred - 0.5) * 40;
635
+ const alpha = 0.3 + Math.abs(pred - 0.5) * 0.4;
636
+ ctx.fillStyle = `hsla(${hue}, 70%, ${lightness}%, ${alpha})`;
637
+ ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize);
638
+ }
639
+ }
640
+ }
641
+
642
+ // Grid
643
+ ctx.strokeStyle = 'hsla(217, 33%, 40%, 0.2)';
644
+ ctx.lineWidth = 1;
645
+ for (let i = 0; i <= canvasSize; i += 30) {
646
+ ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvasSize); ctx.stroke();
647
+ ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvasSize, i); ctx.stroke();
648
+ }
649
+
650
+ // Axes
651
+ ctx.strokeStyle = 'hsla(217, 33%, 50%, 0.5)';
652
+ ctx.lineWidth = 2;
653
+ ctx.beginPath(); ctx.moveTo(canvasSize / 2, 0); ctx.lineTo(canvasSize / 2, canvasSize); ctx.stroke();
654
+ ctx.beginPath(); ctx.moveTo(0, canvasSize / 2); ctx.lineTo(canvasSize, canvasSize / 2); ctx.stroke();
655
+
656
+ // Points
657
+ state.dataPoints.forEach(point => {
658
+ const drawX = (point.x + 1) * (canvasSize / 2);
659
+ const drawY = (1 - point.y) * (canvasSize / 2);
660
+
661
+ ctx.beginPath();
662
+ ctx.arc(drawX, drawY, 6, 0, Math.PI * 2);
663
+ ctx.fillStyle = point.label === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
664
+ ctx.fill();
665
+ ctx.strokeStyle = 'white';
666
+ ctx.lineWidth = 2;
667
+ ctx.stroke();
668
+ });
669
+ }
670
+
671
+ // --- Network Visualizer (SVG) ---
672
+ function renderNetwork() {
673
+ const container = document.getElementById('network-container');
674
+ const width = 500;
675
+ const height = 300;
676
+ const layers = [2, state.hiddenNeurons, 1];
677
+ const layerSpacing = (width - 100) / (layers.length - 1);
678
+
679
+ let svgHtml = `<svg width="${width}" height="${height}" style="overflow: visible;">`;
680
+
681
+ // Calculate positions
682
+ const nodePositions = [];
683
+ layers.forEach((count, layerIdx) => {
684
+ const x = 50 + layerIdx * layerSpacing;
685
+ const maxNodes = Math.max(...layers);
686
+ const vSpacing = (height - 100) / (maxNodes + 1);
687
+ const offset = ((maxNodes - count) * vSpacing) / 2;
688
+ for(let i=0; i<count; i++) {
689
+ nodePositions.push({
690
+ x: x,
691
+ y: 50 + offset + (i+1) * vSpacing,
692
+ layer: layerIdx,
693
+ index: i
694
+ });
695
+ }
696
+ });
697
+
698
+ // Draw Connections
699
+ const weights = state.network.getWeights();
700
+ let fromIndex = 0;
701
+ for(let l=0; l<layers.length-1; l++) {
702
+ const fromCount = layers[l];
703
+ const toCount = layers[l+1];
704
+ const toStartIndex = fromIndex + fromCount;
705
+
706
+ for(let i=0; i<fromCount; i++) {
707
+ for(let j=0; j<toCount; j++) {
708
+ const w = weights[l][j][i];
709
+ const fromNode = nodePositions[fromIndex + i];
710
+ const toNode = nodePositions[toStartIndex + j];
711
+ const opacity = Math.min(0.8, 0.1 + Math.abs(w) * 0.3);
712
+ const color = w > 0 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
713
+ const dash = state.isTraining ? 'stroke-dasharray="4 4" class="animate-flow"' : '';
714
+
715
+ svgHtml += `<line x1="${fromNode.x}" y1="${fromNode.y}" x2="${toNode.x}" y2="${toNode.y}" stroke="${color}" stroke-width="${1 + Math.abs(w)}" stroke-opacity="${opacity}" ${dash} />`;
716
+ }
717
+ }
718
+ fromIndex += fromCount;
719
+ }
720
+
721
+ // Draw Nodes
722
+ nodePositions.forEach(node => {
723
+ let activation = 0;
724
+ if(state.activations) {
725
+ activation = state.activations[node.layer][node.index];
726
+ }
727
+
728
+ let color = 'hsl(280, 65%, 55%)'; // hidden
729
+ if(node.layer === 0) color = 'hsl(199, 89%, 48%)'; // input
730
+ if(node.layer === layers.length - 1) {
731
+ color = activation > 0.5 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
732
+ }
733
+
734
+ const r = 10 + (activation * 5);
735
+ const pulseClass = state.isTraining ? 'class="animate-node-pulse"' : '';
736
+
737
+ svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r+4}" fill="none" stroke="${color}" stroke-opacity="0.3" ${pulseClass} />`;
738
+ svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r}" fill="${color}" />`;
739
+ svgHtml += `<text x="${node.x}" y="${node.y - r - 5}" text-anchor="middle" fill="white" font-size="9">${activation.toFixed(2)}</text>`;
740
+ });
741
+
742
+ svgHtml += `</svg>`;
743
+ container.innerHTML = svgHtml;
744
+ }
745
+
746
+ // --- Explainers ---
747
+ function updateExplainers() {
748
+ if(!state.activations) return;
749
+
750
+ // Input
751
+ document.getElementById('val-x').innerText = state.activations[0][0].toFixed(2);
752
+ document.getElementById('val-y').innerText = state.activations[0][1].toFixed(2);
753
+
754
+ // Hidden
755
+ const hiddenContainer = document.getElementById('val-hidden');
756
+ let hiddenHtml = '';
757
+ state.activations[1].forEach(v => {
758
+ const cls = v > 0.5 ? 'bg-white/10 text-white' : 'text-gray-500';
759
+ hiddenHtml += `<div class="text-center p-1 rounded ${cls}">${v.toFixed(1)}</div>`;
760
+ });
761
+ hiddenContainer.innerHTML = hiddenHtml;
762
+
763
+ // Output
764
+ const outVal = state.activations[2][0];
765
+ document.getElementById('val-raw').innerText = outVal.toFixed(4);
766
+ const classEl = document.getElementById('val-class');
767
+
768
+ // Direct style application
769
+ if(outVal > 0.5) {
770
+ classEl.innerText = "Class A";
771
+ classEl.style.color = "hsl(142, 71%, 45%)"; // Green
772
+ } else {
773
+ classEl.innerText = "Class B";
774
+ classEl.style.color = "hsl(350, 89%, 60%)"; // Red
775
+ }
776
+ }
777
+
778
+ // --- Training Loop ---
779
+ function toggleTraining() {
780
+ if(state.dataPoints.length < 2) {
781
+ alert("Please add at least 2 data points first!");
782
+ return;
783
+ }
784
+ state.isTraining = !state.isTraining;
785
+ const btn = document.getElementById('btn-train');
786
+
787
+ if(state.isTraining) {
788
+ btn.innerHTML = '<i data-lucide="zap" class="h-4 w-4 animate-pulse mr-2"></i> Stop';
789
+ document.getElementById('epoch-display').classList.remove('hidden');
790
+ document.getElementById('accuracy-panel').classList.remove('hidden');
791
+ trainStep();
792
+ } else {
793
+ btn.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
794
+ }
795
+ lucide.createIcons();
796
+ }
797
+
798
+ function trainStep() {
799
+ if(!state.isTraining) return;
800
+ if(state.epoch >= 100) {
801
+ toggleTraining();
802
+ return;
803
+ }
804
+
805
+ state.network.train(state.dataPoints, 10);
806
+ state.epoch++;
807
+
808
+ // Recalculate predictions for the current cursor position ("lastProbe")
809
+ // This ensures the "Live Prediction" text updates instantly as the network learns
810
+ state.activations = state.network.forward([state.lastProbe.x, state.lastProbe.y]).activations;
811
+ updateExplainers();
812
+ renderNetwork(); // Update the nodes/weights visual
813
+
814
+ if(state.epoch % 5 === 0) {
815
+ // Update predictions every 5 epochs
816
+ state.predictions = generatePredictions();
817
+
818
+ // Calc accuracy
819
+ let correct = 0;
820
+ state.dataPoints.forEach(p => {
821
+ if ((state.network.predict(p.x, p.y) > 0.5 ? 1 : 0) === p.label) correct++;
822
+ });
823
+ state.accuracy = correct / state.dataPoints.length;
824
+
825
+ // Update UI
826
+ document.getElementById('epoch-display').innerText = "Epoch " + state.epoch;
827
+ document.getElementById('accuracy-display').innerText = (state.accuracy * 100).toFixed(1) + "%";
828
+ document.getElementById('accuracy-display').className = "text-lg font-bold " + (state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : state.accuracy > 0.5 ? 'text-[hsl(var(--secondary))]' : 'text-[hsl(var(--destructive))]');
829
+
830
+ document.getElementById('progress-bar').style.width = state.epoch + "%";
831
+
832
+ // Add log
833
+ const logItem = `<div class="flex justify-between text-xs py-1 border-b border-white/5 last:border-0">
834
+ <span class="text-[hsl(var(--muted-foreground))]">Epoch ${state.epoch}</span>
835
+ <span class="${state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : 'text-white'}">${(state.accuracy * 100).toFixed(1)}%</span>
836
+ </div>`;
837
+ document.getElementById('logs-container').insertAdjacentHTML('afterbegin', logItem);
838
+
839
+ renderCanvas();
840
+ }
841
+
842
+ requestAnimationFrame(trainStep);
843
+ }
844
+
845
+ // --- Presets ---
846
+ function loadPreset(type) {
847
+ let points = [];
848
+ if (type === 'XOR') {
849
+ for(let i=0; i<20; i++) {
850
+ points.push({ x: -0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 1 });
851
+ points.push({ x: 0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 1 });
852
+ points.push({ x: 0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 0 });
853
+ points.push({ x: -0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 0 });
854
+ }
855
+ } else if (type === 'Circle') {
856
+ for(let i=0; i<40; i++) {
857
+ const angle = Math.random() * Math.PI * 2;
858
+ const r1 = Math.random() * 0.4;
859
+ points.push({ x: Math.cos(angle)*r1, y: Math.sin(angle)*r1, label: 1 });
860
+ const r2 = 0.6 + Math.random() * 0.3;
861
+ points.push({ x: Math.cos(angle)*r2, y: Math.sin(angle)*r2, label: 0 });
862
+ }
863
+ } else if (type === 'Linear') {
864
+ for(let i=0; i<30; i++) {
865
+ points.push({ x: -0.4 - Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 1 });
866
+ points.push({ x: 0.4 + Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 0 });
867
+ }
868
+ } else if (type === 'Spiral') {
869
+ for (let i = 0; i < 60; i++) {
870
+ const r = i / 60;
871
+ const t = 1.75 * i / 60 * 2 * Math.PI;
872
+ points.push({ x: r * Math.sin(t), y: r * Math.cos(t), label: 1 });
873
+ points.push({ x: r * Math.sin(t + Math.PI), y: r * Math.cos(t + Math.PI), label: 0 });
874
+ }
875
+ }
876
+
877
+ // Reset but keep new points and generate initial map
878
+ state.dataPoints = points;
879
+ state.isTraining = false;
880
+ state.epoch = 0;
881
+ state.accuracy = 0;
882
+ state.logs = [];
883
+ state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
884
+ state.predictions = generatePredictions(); // Generate immediate prediction map
885
+ state.lastProbe = { x: 0, y: 0 };
886
+
887
+ document.getElementById('points-count').innerText = points.length;
888
+ document.getElementById('accuracy-panel').classList.add('hidden');
889
+ document.getElementById('epoch-display').classList.add('hidden');
890
+ document.getElementById('progress-bar').style.width = '0%';
891
+ document.getElementById('logs-container').innerHTML = '';
892
+
893
+ const btnTrain = document.getElementById('btn-train');
894
+ btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
895
+ lucide.createIcons();
896
+
897
+ renderCanvas();
898
+ renderNetwork();
899
+
900
+ if(points.length) {
901
+ // Set probe to first point to avoid 0,0 default
902
+ state.lastProbe = { x: points[0].x, y: points[0].y };
903
+ state.activations = state.network.forward([points[0].x, points[0].y]).activations;
904
+ updateExplainers();
905
+ }
906
+ }
907
+
908
+ function renderUI() {
909
+ renderNetwork();
910
+ }
911
+
912
+ // Start
913
+ window.onload = init;
914
+ </script>
915
+ </body>
916
+ </html>
templates/Neural-Networks-for-Classification.html CHANGED
@@ -150,6 +150,54 @@
150
  <div class="container">
151
  <h1>🧠 Study Guide: Neural Networks for Classification</h1>
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  <h2>🔹 Core Concepts</h2>
154
  <div class="story">
155
  <p><strong>Story-style intuition: The Corporate Hierarchy</strong></p>
 
150
  <div class="container">
151
  <h1>🧠 Study Guide: Neural Networks for Classification</h1>
152
 
153
+
154
+ <!-- button -->
155
+ <div>
156
+ <!-- Audio Element -->
157
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
158
+ but since this is triggered by a click, it should work fine. -->
159
+
160
+
161
+ <a
162
+ href="/Neural-Networks-for-Classification-three"
163
+ target="_blank"
164
+ onclick="playSound()"
165
+ class="
166
+ cursor-pointer
167
+ inline-block
168
+ relative
169
+ bg-blue-500
170
+ text-white
171
+ font-bold
172
+ py-4 px-8
173
+ rounded-xl
174
+ text-2xl
175
+ transition-all
176
+ duration-150
177
+
178
+ /* 3D Effect (Hard Shadow) */
179
+ shadow-[0_8px_0_rgb(29,78,216)]
180
+
181
+ /* Pressed State (Move down & remove shadow) */
182
+ active:shadow-none
183
+ active:translate-y-[8px]
184
+ ">
185
+ Tap Me!
186
+ </a>
187
+ </div>
188
+
189
+ <script>
190
+ function playSound() {
191
+ const audio = document.getElementById("clickSound");
192
+ if (audio) {
193
+ audio.currentTime = 0;
194
+ audio.play().catch(e => console.log("Audio play failed:", e));
195
+ }
196
+ }
197
+ </script>
198
+ <!-- button -->
199
+
200
+
201
  <h2>🔹 Core Concepts</h2>
202
  <div class="story">
203
  <p><strong>Story-style intuition: The Corporate Hierarchy</strong></p>
templates/Optimization.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block content %}
4
+ <h1 class="text-4xl font-bold text-gray-800 mb-6">Optimization Method</h1>
5
+ <p class="text-gray-600 text-lg mb-8">Optimization (Gradient Descent): Gradient descent is an optimization algorithm used to minimize the error (loss function) of a model during the training process. It is the underlying mechanism that powers the "gradient" aspect of gradient boosting, allowing the models to iteratively improve their performance.</p>
6
+ <div class="flex flex-col gap-6">
7
+ <div class="card p-6">
8
+ <h2 class="text-2xl font-semibold text-gray-800 mb-4"></h2>
9
+ <a href="/gradient-descent" class="algorithm-box">Gradient Descent</a>
10
+ </div>
11
+
12
+ {% endblock %}
templates/XGBoost-Regression.html CHANGED
@@ -151,6 +151,100 @@
151
  <div class="container">
152
  <h1>🚀 Study Guide: XGBoost Regression</h1>
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  <h2>🔹 Core Concepts</h2>
155
  <div class="story">
156
  <p><strong>Story-style intuition: The Master Craftsman</strong></p>
 
151
  <div class="container">
152
  <h1>🚀 Study Guide: XGBoost Regression</h1>
153
 
154
+
155
+ <!-- button -->
156
+ <div>
157
+ <!-- Audio Element -->
158
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
159
+ but since this is triggered by a click, it should work fine. -->
160
+
161
+
162
+ <a
163
+ href="/xgboost-tree-three"
164
+ target="_blank"
165
+ onclick="playSound()"
166
+ class="
167
+ cursor-pointer
168
+ inline-block
169
+ relative
170
+ bg-blue-500
171
+ text-white
172
+ font-bold
173
+ py-4 px-8
174
+ rounded-xl
175
+ text-2xl
176
+ transition-all
177
+ duration-150
178
+
179
+ /* 3D Effect (Hard Shadow) */
180
+ shadow-[0_8px_0_rgb(29,78,216)]
181
+
182
+ /* Pressed State (Move down & remove shadow) */
183
+ active:shadow-none
184
+ active:translate-y-[8px]
185
+ ">
186
+ Tap Me!
187
+ </a>
188
+ </div>
189
+
190
+ <script>
191
+ function playSound() {
192
+ const audio = document.getElementById("clickSound");
193
+ if (audio) {
194
+ audio.currentTime = 0;
195
+ audio.play().catch(e => console.log("Audio play failed:", e));
196
+ }
197
+ }
198
+ </script>
199
+ <!-- button -->
200
+
201
+ <!-- button -->
202
+ <div>
203
+ <!-- Audio Element -->
204
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
205
+ but since this is triggered by a click, it should work fine. -->
206
+
207
+
208
+ <a
209
+ href="/xgboost-graph-three2"
210
+ target="_blank"
211
+ onclick="playSound()"
212
+ class="
213
+ cursor-pointer
214
+ inline-block
215
+ relative
216
+ bg-blue-500
217
+ text-white
218
+ font-bold
219
+ py-4 px-8
220
+ rounded-xl
221
+ text-2xl
222
+ transition-all
223
+ duration-150
224
+
225
+ /* 3D Effect (Hard Shadow) */
226
+ shadow-[0_8px_0_rgb(29,78,216)]
227
+
228
+ /* Pressed State (Move down & remove shadow) */
229
+ active:shadow-none
230
+ active:translate-y-[8px]
231
+ ">
232
+ Tap Me!
233
+ </a>
234
+ </div>
235
+
236
+ <script>
237
+ function playSound() {
238
+ const audio = document.getElementById("clickSound");
239
+ if (audio) {
240
+ audio.currentTime = 0;
241
+ audio.play().catch(e => console.log("Audio play failed:", e));
242
+ }
243
+ }
244
+ </script>
245
+ <!-- button -->
246
+
247
+
248
  <h2>🔹 Core Concepts</h2>
249
  <div class="story">
250
  <p><strong>Story-style intuition: The Master Craftsman</strong></p>
templates/gmm-threejs.html ADDED
@@ -0,0 +1,977 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>GMM Simulator - Learn Gaussian Mixture Models Visually</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/lucide@latest"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap');
11
+
12
+ :root {
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --accent: 142.1 76.2% 36.3%;
15
+ --cluster-1: 15 85% 60%;
16
+ --cluster-2: 195 80% 50%;
17
+ --cluster-3: 45 90% 55%;
18
+ --cluster-4: 280 65% 60%;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Inter', sans-serif;
23
+ background-color: #f8fafc;
24
+ }
25
+
26
+ .font-mono { font-family: 'JetBrains Mono', monospace; }
27
+
28
+ .shadow-soft {
29
+ box-shadow: 0 2px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05);
30
+ }
31
+
32
+ .animate-pulse-soft {
33
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
34
+ }
35
+
36
+ @keyframes pulse {
37
+ 0%, 100% { opacity: 1; }
38
+ 50% { opacity: .7; }
39
+ }
40
+
41
+ .tab-content { display: none; }
42
+ .tab-content.active { display: block; }
43
+
44
+ .tab-trigger.active {
45
+ background: white;
46
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
47
+ color: black;
48
+ }
49
+
50
+ .step-node.active { background-color: rgb(59 130 246); color: white; }
51
+ .step-node.done { background-color: #f1f5f9; color: #64748b; }
52
+ .step-node.converged { background-color: #10b981; color: white; }
53
+
54
+ .modal-overlay {
55
+ background: rgba(15, 23, 42, 0.6);
56
+ backdrop-filter: blur(4px);
57
+ }
58
+ </style>
59
+ </head>
60
+ <body class="text-slate-900">
61
+
62
+ <div id="app" class="min-h-screen p-4 md:p-6">
63
+ <div class="max-w-7xl mx-auto space-y-6">
64
+
65
+ <!-- Header -->
66
+ <header class="text-center space-y-2">
67
+ <h1 class="text-2xl md:text-4xl font-extrabold tracking-tight">
68
+ EM Algorithm & <span class="text-blue-600">Gaussian Mixture Models</span>
69
+ </h1>
70
+ <p class="text-slate-500 text-sm md:text-base max-w-2xl mx-auto">
71
+ Learn how machines find hidden groups in data. Watch points get assigned and clusters adapt in real-time!
72
+ </p>
73
+ </header>
74
+
75
+ <!-- Tabs Navigation -->
76
+ <div class="flex justify-center">
77
+ <div class="bg-slate-200/50 p-1 rounded-lg inline-flex w-full max-w-md">
78
+ <button onclick="switchTab('simulator')" id="btn-tab-simulator" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all active flex items-center justify-center gap-2">
79
+ <i data-lucide="play" class="w-4 h-4"></i> Interactive Simulator
80
+ </button>
81
+ <button onclick="switchTab('learn')" id="btn-tab-learn" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all flex items-center justify-center gap-2 text-slate-600">
82
+ <i data-lucide="book-open" class="w-4 h-4"></i> Learn EM Algorithm
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Simulator Tab Content -->
88
+ <div id="tab-simulator" class="tab-content active space-y-6">
89
+
90
+ <!-- Quick Facts Grid -->
91
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
92
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
93
+ <i data-lucide="book-open" class="w-6 h-6 mb-2 text-orange-500"></i>
94
+ <h4 class="font-bold text-sm mb-1">GMM = Soft Clustering</h4>
95
+ <p class="text-xs text-slate-500">Points can partially belong to multiple clusters.</p>
96
+ </div>
97
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
98
+ <i data-lucide="zap" class="w-6 h-6 mb-2 text-blue-500"></i>
99
+ <h4 class="font-bold text-sm mb-1">EM is Iterative</h4>
100
+ <p class="text-xs text-slate-500">Alternates between E-step and M-step.</p>
101
+ </div>
102
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
103
+ <i data-lucide="eye" class="w-6 h-6 mb-2 text-emerald-500"></i>
104
+ <h4 class="font-bold text-sm mb-1">Watch the Ellipses</h4>
105
+ <p class="text-xs text-slate-500">Ellipses show the statistical shape of clusters.</p>
106
+ </div>
107
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
108
+ <i data-lucide="brain" class="w-6 h-6 mb-2 text-indigo-500"></i>
109
+ <h4 class="font-bold text-sm mb-1">Convergence</h4>
110
+ <p class="text-xs text-slate-500">Algorithm stops when centers stabilize.</p>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="grid lg:grid-cols-3 gap-6">
115
+ <!-- Left: Canvas and Main Controls -->
116
+ <div class="lg:col-span-2 space-y-4">
117
+
118
+ <!-- Algorithm Flow Indicator -->
119
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
120
+ <h4 class="font-bold text-sm mb-4 text-center">EM Algorithm Flow</h4>
121
+ <div class="flex items-center justify-between">
122
+ <div class="flex flex-col items-center flex-1 text-center">
123
+ <div id="step-init" class="step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all">
124
+ <i data-lucide="circle" class="w-5 h-5"></i>
125
+ </div>
126
+ <span class="text-xs font-medium mt-2">Initialize</span>
127
+ </div>
128
+ <div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-1"></div>
129
+ <div class="flex flex-col items-center flex-1 text-center">
130
+ <div id="step-e" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
131
+ <i data-lucide="circle" class="w-5 h-5"></i>
132
+ </div>
133
+ <span class="text-xs font-medium mt-2">E-Step</span>
134
+ </div>
135
+ <div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-2"></div>
136
+ <div class="flex flex-col items-center flex-1 text-center">
137
+ <div id="step-m" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
138
+ <i data-lucide="refresh-cw" class="w-5 h-5"></i>
139
+ </div>
140
+ <span class="text-xs font-medium mt-2">M-Step</span>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- Canvas Area -->
146
+ <div class="bg-white rounded-2xl shadow-soft border overflow-hidden">
147
+ <div class="p-3 border-b bg-slate-50 flex flex-wrap gap-4" id="legend">
148
+ <!-- Legend items generated via JS -->
149
+ </div>
150
+ <div class="relative p-2 flex justify-center items-center overflow-hidden" style="height: 400px;" id="canvas-container">
151
+ <svg id="gmm-svg" width="100%" height="100%" class="rounded-lg">
152
+ <defs>
153
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
154
+ <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#e2e8f0" stroke-width="0.5" />
155
+ </pattern>
156
+ </defs>
157
+ <rect width="100%" height="100%" fill="url(#grid)" />
158
+ <g id="ellipses-group"></g>
159
+ <g id="points-group"></g>
160
+ </svg>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Main Controls -->
165
+ <div class="bg-white rounded-xl p-5 border shadow-soft space-y-6">
166
+ <div class="flex flex-wrap items-center gap-3">
167
+ <button id="btn-play" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
168
+ <i data-lucide="play" class="w-5 h-5"></i> <span id="btn-play-text">Auto Play</span>
169
+ </button>
170
+ <button id="btn-step" class="bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
171
+ <i data-lucide="skip-forward" class="w-5 h-5"></i> Next Step
172
+ </button>
173
+ <button id="btn-reset" class="border border-slate-200 hover:bg-slate-50 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
174
+ <i data-lucide="rotate-ccw" class="w-5 h-5"></i> Reset
175
+ </button>
176
+ <button id="btn-generate" class="border border-emerald-200 text-emerald-600 hover:bg-emerald-50 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
177
+ <i data-lucide="sparkles" class="w-5 h-5"></i> New Data
178
+ </button>
179
+ </div>
180
+
181
+ <div class="flex flex-wrap items-center justify-between gap-6 pt-2 border-t border-slate-100">
182
+ <div class="flex items-center gap-4">
183
+ <span class="text-sm font-medium text-slate-500 whitespace-nowrap">Speed:</span>
184
+ <input type="range" id="speed-slider" min="500" max="3000" step="100" value="1500" class="w-32 accent-blue-600">
185
+ <span id="speed-text" class="text-xs text-slate-400 w-12 text-center">Medium</span>
186
+ </div>
187
+
188
+ <div class="flex items-center gap-4">
189
+ <div class="bg-slate-100 px-4 py-2 rounded-lg flex items-center gap-2">
190
+ <span class="text-sm font-medium text-slate-500">Iteration:</span>
191
+ <span id="counter-text" class="font-mono text-lg font-bold">0 / 20</span>
192
+ </div>
193
+ <div id="status-badge" class="hidden px-4 py-2 rounded-lg font-semibold text-sm"></div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Right: Info Panels -->
200
+ <div class="space-y-4">
201
+ <!-- Dynamic Explanation -->
202
+ <div id="explanation-card" class="p-5 rounded-xl border-2 bg-blue-50/50 border-blue-200 shadow-soft transition-all">
203
+ <div class="flex items-start gap-4">
204
+ <div class="p-3 rounded-lg bg-white text-blue-600" id="expl-icon">
205
+ <i data-lucide="lightbulb" class="w-6 h-6"></i>
206
+ </div>
207
+ <div class="flex-1">
208
+ <h3 class="font-bold text-lg mb-2" id="expl-title">Ready to Start! 🚀</h3>
209
+ <ul class="space-y-2" id="expl-content">
210
+ <li class="flex items-start gap-2 text-sm text-slate-600">
211
+ <i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
212
+ <span>Press 'Next Step' to see how the EM algorithm works.</span>
213
+ </li>
214
+ <li class="flex items-start gap-2 text-sm text-slate-600">
215
+ <i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
216
+ <span>Watch points get assigned to the most likely cluster center.</span>
217
+ </li>
218
+ </ul>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Log-Likelihood Chart -->
224
+ <div class="bg-white rounded-xl border p-4 shadow-soft">
225
+ <div class="flex items-center justify-between mb-3">
226
+ <div class="flex items-center gap-2">
227
+ <i data-lucide="trending-up" class="w-5 h-5 text-blue-600"></i>
228
+ <h4 class="font-bold">Log-Likelihood</h4>
229
+ </div>
230
+ <div id="converged-badge" class="hidden items-center gap-1 text-emerald-600 text-sm font-medium">
231
+ <i data-lucide="check-circle-2" class="w-4 h-4"></i> Converged!
232
+ </div>
233
+ </div>
234
+ <p class="text-xs text-slate-500 mb-4">
235
+ Measures model fit. Higher values = better grouping.
236
+ </p>
237
+ <div class="h-32 w-full flex items-end gap-1 px-1" id="chart-container">
238
+ <!-- Chart bars generated via JS -->
239
+ <div class="flex-1 h-full flex items-center justify-center text-slate-300 text-xs text-center px-4">
240
+ Start simulation to see progress...
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Key Terms -->
246
+ <div class="bg-white rounded-xl p-5 border shadow-soft">
247
+ <h4 class="font-bold mb-3">Key Terms 📚</h4>
248
+ <div class="space-y-4 text-sm">
249
+ <div>
250
+ <span class="font-semibold text-orange-500">E-Step:</span>
251
+ <p class="text-slate-500 mt-0.5">Calculating the "responsibility" (probability) that each point belongs to each cluster center.</p>
252
+ </div>
253
+ <div>
254
+ <span class="font-semibold text-blue-500">M-Step:</span>
255
+ <p class="text-slate-500 mt-0.5">Moving and stretching clusters to better fit the points assigned to them.</p>
256
+ </div>
257
+ <div>
258
+ <span class="font-semibold text-emerald-500">Convergence:</span>
259
+ <p class="text-slate-500 mt-0.5">The point where the clusters stop moving because they've found the optimal mathematical fit.</p>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <!-- Learn Tab Content -->
268
+ <div id="tab-learn" class="tab-content space-y-8">
269
+ <!-- Introduction Section -->
270
+ <section class="bg-white rounded-2xl border p-6 shadow-soft">
271
+ <div class="flex items-center gap-3 mb-4">
272
+ <div class="p-3 rounded-xl bg-blue-50 text-blue-600">
273
+ <i data-lucide="book-open" class="w-6 h-6"></i>
274
+ </div>
275
+ <h2 class="text-xl font-bold">What is the EM Algorithm?</h2>
276
+ </div>
277
+ <div class="space-y-4 text-slate-600">
278
+ <p>
279
+ <strong class="text-slate-900">EM (Expectation-Maximization)</strong> is a powerful iterative method used to find maximum likelihood estimates of parameters in statistical models, where the model depends on unobserved latent variables.
280
+ </p>
281
+ <div class="bg-slate-50 rounded-xl p-4 border border-slate-100">
282
+ <h4 class="font-semibold text-slate-900 mb-2">🎯 Real-Life Analogy</h4>
283
+ <p class="text-sm">
284
+ Imagine you have a bag of colored marbles, but you're colorblind! You can feel their sizes and weights, but you can't see the colors. EM helps you figure out which marbles are likely the same color based on their shared physical properties.
285
+ </p>
286
+ </div>
287
+ <p>The algorithm works by alternating between two main steps until it finds the best mathematical grouping.</p>
288
+ </div>
289
+ </section>
290
+
291
+ <!-- Detailed Steps Grid -->
292
+ <div class="grid md:grid-cols-2 gap-6">
293
+ <!-- E-Step Details -->
294
+ <section class="bg-gradient-to-br from-orange-50/50 to-white rounded-2xl border border-orange-100 p-6 shadow-sm">
295
+ <div class="flex items-center gap-3 mb-4">
296
+ <div class="p-3 rounded-xl bg-orange-100 text-orange-600">
297
+ <i data-lucide="target" class="w-6 h-6"></i>
298
+ </div>
299
+ <div>
300
+ <h3 class="text-lg font-bold">E-Step (Expectation)</h3>
301
+ <p class="text-xs text-orange-500 font-medium">Assignment Phase</p>
302
+ </div>
303
+ </div>
304
+ <div class="space-y-4">
305
+ <p class="text-sm text-slate-600"><strong>Goal:</strong> Calculate the probability (responsibility) of each cluster for each data point.</p>
306
+ <div class="space-y-2">
307
+ <h4 class="font-semibold text-sm">Process:</h4>
308
+ <ul class="space-y-2 text-sm text-slate-500">
309
+ <li class="flex items-start gap-2"><span class="text-orange-600">•</span> Each point "looks" at all current cluster curves.</li>
310
+ <li class="flex items-start gap-2"><span class="text-orange-600">•</span> It asks: "Given my location, which cluster would likely have produced me?"</li>
311
+ <li class="flex items-start gap-2"><span class="text-orange-600">•</span> Points get colored by their most likely cluster.</li>
312
+ </ul>
313
+ </div>
314
+ <div class="bg-white/80 rounded-lg p-3 border border-orange-100">
315
+ <p class="text-xs text-slate-500 font-mono">responsibility = (fit to cluster) / (total fit to all clusters)</p>
316
+ </div>
317
+ </div>
318
+ </section>
319
+
320
+ <!-- M-Step Details -->
321
+ <section class="bg-gradient-to-br from-blue-50/50 to-white rounded-2xl border border-blue-100 p-6 shadow-sm">
322
+ <div class="flex items-center gap-3 mb-4">
323
+ <div class="p-3 rounded-xl bg-blue-100 text-blue-600">
324
+ <i data-lucide="bar-chart-3" class="w-6 h-6"></i>
325
+ </div>
326
+ <div>
327
+ <h3 class="text-lg font-bold">M-Step (Maximization)</h3>
328
+ <p class="text-xs text-blue-500 font-medium">Update Phase</p>
329
+ </div>
330
+ </div>
331
+ <div class="space-y-4">
332
+ <p class="text-sm text-slate-600"><strong>Goal:</strong> Update cluster parameters (center, shape, weight) based on assigned points.</p>
333
+ <div class="space-y-2">
334
+ <h4 class="font-semibold text-sm">Process:</h4>
335
+ <ul class="space-y-2 text-sm text-slate-500">
336
+ <li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster centers move to the weighted average of points.</li>
337
+ <li class="flex items-start gap-2"><span class="text-blue-600">•</span> The ellipse stretches/shrinks to cover its assigned points better.</li>
338
+ <li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster "popularity" (weight) is updated.</li>
339
+ </ul>
340
+ </div>
341
+ <div class="bg-white/80 rounded-lg p-3 border border-blue-100">
342
+ <p class="text-xs text-slate-500 font-mono">new_mean = average(points × their_responsibilities)</p>
343
+ </div>
344
+ </div>
345
+ </section>
346
+ </div>
347
+
348
+ <!-- Algorithm Flow Visual -->
349
+ <section class="bg-white rounded-2xl border p-6 shadow-soft">
350
+ <div class="flex items-center gap-3 mb-6">
351
+ <div class="p-3 rounded-xl bg-indigo-50 text-indigo-600">
352
+ <i data-lucide="refresh-cw" class="w-6 h-6"></i>
353
+ </div>
354
+ <h2 class="text-xl font-bold">The Complete Cycle</h2>
355
+ </div>
356
+ <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 relative">
357
+ <div class="bg-slate-50 p-4 rounded-xl text-center border">
358
+ <i data-lucide="shuffle" class="w-8 h-8 mx-auto mb-2 text-slate-400"></i>
359
+ <h4 class="font-bold text-sm">1. Initialize</h4>
360
+ <p class="text-xs text-slate-500 mt-1">Random centers and circular shapes</p>
361
+ </div>
362
+ <div class="bg-orange-50 p-4 rounded-xl text-center border border-orange-100">
363
+ <i data-lucide="target" class="w-8 h-8 mx-auto mb-2 text-orange-500"></i>
364
+ <h4 class="font-bold text-sm">2. E-Step</h4>
365
+ <p class="text-xs text-slate-500 mt-1">Assign data points to clusters</p>
366
+ </div>
367
+ <div class="bg-blue-50 p-4 rounded-xl text-center border border-blue-100">
368
+ <i data-lucide="bar-chart-3" class="w-8 h-8 mx-auto mb-2 text-blue-500"></i>
369
+ <h4 class="font-bold text-sm">3. M-Step</h4>
370
+ <p class="text-xs text-slate-500 mt-1">Update cluster parameters</p>
371
+ </div>
372
+ <div class="bg-emerald-50 p-4 rounded-xl text-center border border-emerald-100">
373
+ <i data-lucide="check-circle-2" class="w-8 h-8 mx-auto mb-2 text-emerald-500"></i>
374
+ <h4 class="font-bold text-sm">4. Converged?</h4>
375
+ <p class="text-xs text-slate-500 mt-1">Repeat 2 & 3 until stable</p>
376
+ </div>
377
+ </div>
378
+ </section>
379
+
380
+ <!-- Why it Works Section -->
381
+ <section class="bg-white rounded-2xl border p-6 shadow-soft">
382
+ <div class="flex items-center gap-3 mb-6">
383
+ <div class="p-3 rounded-xl bg-amber-50 text-amber-600">
384
+ <i data-lucide="lightbulb" class="w-6 h-6"></i>
385
+ </div>
386
+ <h2 class="text-xl font-bold">Why Does EM Work?</h2>
387
+ </div>
388
+ <div class="grid md:grid-cols-2 gap-8">
389
+ <div class="space-y-4">
390
+ <h4 class="font-bold text-slate-900">The Chicken-and-Egg Problem 🐔🥚</h4>
391
+ <p class="text-sm text-slate-600 leading-relaxed">
392
+ We have a classic loop: To find the centers, we need to know point assignments. To find point assignments, we need to know the centers.
393
+ EM solves this by starting with a "best guess" and iteratively refining it.
394
+ </p>
395
+ </div>
396
+ <div class="space-y-4">
397
+ <h4 class="font-bold text-slate-900">Guaranteed Improvement 📈</h4>
398
+ <p class="text-sm text-slate-600 leading-relaxed">
399
+ Mathematically, each iteration of EM is guaranteed to increase the <strong>Log-Likelihood</strong> of the model (or leave it unchanged). This means the model always gets better at explaining the data until it hits a maximum.
400
+ </p>
401
+ </div>
402
+ </div>
403
+ </section>
404
+
405
+ <!-- Comparison Table -->
406
+ <section class="bg-white rounded-2xl border p-6 shadow-soft overflow-hidden">
407
+ <h2 class="text-xl font-bold mb-6">GMM vs K-Means: The Difference</h2>
408
+ <div class="overflow-x-auto">
409
+ <table class="w-full text-sm">
410
+ <thead class="bg-slate-50">
411
+ <tr class="text-left">
412
+ <th class="py-3 px-4 font-bold border-b">Feature</th>
413
+ <th class="py-3 px-4 font-bold border-b text-blue-600">K-Means</th>
414
+ <th class="py-3 px-4 font-bold border-b text-orange-600">GMM (EM)</th>
415
+ </tr>
416
+ </thead>
417
+ <tbody class="divide-y text-slate-600">
418
+ <tr>
419
+ <td class="py-4 px-4 font-semibold text-slate-900">Assignment</td>
420
+ <td class="py-4 px-4">Hard (0 or 1)</td>
421
+ <td class="py-4 px-4">Soft (Probabilities)</td>
422
+ </tr>
423
+ <tr>
424
+ <td class="py-4 px-4 font-semibold text-slate-900">Cluster Shape</td>
425
+ <td class="py-4 px-4">Always circular/spherical</td>
426
+ <td class="py-4 px-4">Flexible ellipses (any orientation)</td>
427
+ </tr>
428
+ <tr>
429
+ <td class="py-4 px-4 font-semibold text-slate-900">Model Type</td>
430
+ <td class="py-4 px-4">Distance-based</td>
431
+ <td class="py-4 px-4">Distribution-based</td>
432
+ </tr>
433
+ <tr>
434
+ <td class="py-4 px-4 font-semibold text-slate-900">Use Case</td>
435
+ <td class="py-4 px-4">Simple, distinct groups</td>
436
+ <td class="py-4 px-4">Overlapping, varied group shapes</td>
437
+ </tr>
438
+ </tbody>
439
+ </table>
440
+ </div>
441
+ </section>
442
+
443
+ <!-- Real World Applications Grid -->
444
+ <section class="bg-slate-900 text-white rounded-2xl p-8 shadow-xl">
445
+ <h2 class="text-2xl font-bold mb-6">🌍 Real-World Applications</h2>
446
+ <div class="grid sm:grid-cols-2 md:grid-cols-3 gap-6">
447
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
448
+ <div class="text-2xl mb-2">🖼️</div>
449
+ <h4 class="font-bold text-sm">Image Segmentation</h4>
450
+ <p class="text-xs text-slate-400 mt-1">Grouping pixels by color/texture to separate objects in photos.</p>
451
+ </div>
452
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
453
+ <div class="text-2xl mb-2">🔊</div>
454
+ <h4 class="font-bold text-sm">Speech Recognition</h4>
455
+ <p class="text-xs text-slate-400 mt-1">Identifying different speakers in an audio stream using voice patterns.</p>
456
+ </div>
457
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
458
+ <div class="text-2xl mb-2">📊</div>
459
+ <h4 class="font-bold text-sm">Customer Segmentation</h4>
460
+ <p class="text-xs text-slate-400 mt-1">Finding groups of customers with similar shopping behaviors.</p>
461
+ </div>
462
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
463
+ <div class="text-2xl mb-2">🧬</div>
464
+ <h4 class="font-bold text-sm">Genetics</h4>
465
+ <p class="text-xs text-slate-400 mt-1">Clustering gene expression data to find functional biological groups.</p>
466
+ </div>
467
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
468
+ <div class="text-2xl mb-2">🌤️</div>
469
+ <h4 class="font-bold text-sm">Meteorology</h4>
470
+ <p class="text-xs text-slate-400 mt-1">Classifying climate zones based on temperature and humidity data.</p>
471
+ </div>
472
+ <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
473
+ <div class="text-2xl mb-2">📧</div>
474
+ <h4 class="font-bold text-sm">Spam Detection</h4>
475
+ <p class="text-xs text-slate-400 mt-1">Clustering emails into 'Ham' and 'Spam' based on content features.</p>
476
+ </div>
477
+ </div>
478
+ </section>
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- Intro Modal -->
484
+ <div id="intro-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay">
485
+ <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full p-6 md:p-8 relative">
486
+ <button onclick="closeModal()" class="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-100 transition-colors">
487
+ <i data-lucide="x" class="w-5 h-5"></i>
488
+ </button>
489
+ <div class="text-center mb-8">
490
+ <h2 class="text-2xl md:text-3xl font-extrabold mb-2">Welcome to the <span class="text-blue-600">EM Simulator</span></h2>
491
+ <p class="text-slate-500">Discover how AI learns hidden patterns in data</p>
492
+ </div>
493
+ <div class="space-y-4 mb-8">
494
+ <div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
495
+ <div class="p-3 rounded-lg bg-white text-orange-500 shadow-sm"><i data-lucide="sparkles" class="w-6 h-6"></i></div>
496
+ <div><h3 class="font-bold">What is GMM?</h3><p class="text-sm text-slate-500">A statistical model that groups data points into distinct probability curves (clusters).</p></div>
497
+ </div>
498
+ <div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
499
+ <div class="p-3 rounded-lg bg-white text-blue-500 shadow-sm"><i data-lucide="zap" class="w-6 h-6"></i></div>
500
+ <div><h3 class="font-bold">How it learns</h3><p class="text-sm text-slate-500">It "cycles" between assigning points (E-Step) and updating groups (M-Step) until stable.</p></div>
501
+ </div>
502
+ </div>
503
+ <button onclick="closeModal()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg flex items-center justify-center gap-2">
504
+ Start Learning <i data-lucide="arrow-right" class="w-5 h-5"></i>
505
+ </button>
506
+ </div>
507
+ <!-- Centered Button -->
508
+ <div class="absolute left-1/2 -translate-x-1/2 flex items-center">
509
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
510
+ <a href="/gaussian-mixture-models" 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">
511
+ Back to Core
512
+ </a>
513
+ </div>
514
+ </div>
515
+
516
+ <script>
517
+ // --- CONSTANTS & DATA ---
518
+ const CLUSTER_COLORS = [
519
+ { fill: 'rgba(239, 115, 87, 0.15)', stroke: 'hsl(15, 85%, 60%)', label: 'Coral' },
520
+ { fill: 'rgba(50, 180, 205, 0.15)', stroke: 'hsl(195, 80%, 50%)', label: 'Teal' },
521
+ { fill: 'rgba(245, 185, 66, 0.15)', stroke: 'hsl(45, 90%, 55%)', label: 'Amber' },
522
+ { fill: 'rgba(175, 100, 200, 0.15)', stroke: 'hsl(280, 65%, 60%)', label: 'Violet' }
523
+ ];
524
+ const MAX_ITERATIONS = 20;
525
+ const NUM_POINTS = 100;
526
+
527
+ // --- STATE ---
528
+ let state = {
529
+ points: [],
530
+ clusters: [],
531
+ iteration: 0,
532
+ stepType: 'none', // 'none', 'e-step', 'm-step'
533
+ isPlaying: false,
534
+ converged: false,
535
+ logLikelihoods: [],
536
+ speed: 1500,
537
+ timer: null
538
+ };
539
+
540
+ // --- MATH UTILS ---
541
+ function multivariateNormalPDF(x, y, mean, cov) {
542
+ const dx = x - mean.x;
543
+ const dy = y - mean.y;
544
+ const det = cov.xx * cov.yy - cov.xy * cov.xy;
545
+ const inv = {
546
+ xx: cov.yy / det,
547
+ xy: -cov.xy / det,
548
+ yy: cov.xx / det
549
+ };
550
+ const exponent = -0.5 * (dx * (dx * inv.xx + dy * inv.xy) + dy * (dx * inv.xy + dy * inv.yy));
551
+ return (1 / (2 * Math.PI * Math.sqrt(Math.abs(det)))) * Math.exp(exponent);
552
+ }
553
+
554
+ function getEllipseParams(cov) {
555
+ const trace = cov.xx + cov.yy;
556
+ const det = cov.xx * cov.yy - cov.xy * cov.xy;
557
+ const sqrtDisc = Math.sqrt(Math.pow(trace / 2, 2) - det);
558
+ const lambda1 = trace / 2 + sqrtDisc;
559
+ const lambda2 = trace / 2 - sqrtDisc;
560
+
561
+ const a = Math.sqrt(Math.max(0, lambda1)) * 2;
562
+ const b = Math.sqrt(Math.max(0, lambda2)) * 2;
563
+ const angle = 0.5 * Math.atan2(2 * cov.xy, cov.xx - cov.yy) * (180 / Math.PI);
564
+
565
+ return { a, b, angle };
566
+ }
567
+
568
+ // --- CORE LOGIC (EM ALGORITHM) ---
569
+ function generateData() {
570
+ const points = [];
571
+ const centers = [
572
+ { x: 150, y: 150 },
573
+ { x: 450, y: 250 },
574
+ { x: 250, y: 350 }
575
+ ];
576
+
577
+ for (let i = 0; i < NUM_POINTS; i++) {
578
+ const center = centers[Math.floor(Math.random() * centers.length)];
579
+ points.push({
580
+ x: center.x + (Math.random() - 0.5) * 150,
581
+ y: center.y + (Math.random() - 0.5) * 150,
582
+ responsibilities: []
583
+ });
584
+ }
585
+ state.points = points;
586
+ initClusters();
587
+ resetSimState();
588
+ render();
589
+ }
590
+
591
+ function initClusters() {
592
+ state.clusters = [];
593
+ for (let i = 0; i < 3; i++) {
594
+ state.clusters.push({
595
+ mean: { x: Math.random() * 600, y: Math.random() * 400 },
596
+ covariance: { xx: 2500, xy: 0, yy: 2500 },
597
+ weight: 1 / 3,
598
+ color: CLUSTER_COLORS[i]
599
+ });
600
+ }
601
+ }
602
+
603
+ function resetSimState() {
604
+ state.iteration = 0;
605
+ state.stepType = 'none';
606
+ state.converged = false;
607
+ state.logLikelihoods = [];
608
+ state.points.forEach(p => delete p.clusterIndex);
609
+ stopAutoPlay();
610
+ updateUI();
611
+ }
612
+
613
+ function eStep() {
614
+ let totalLogLikelihood = 0;
615
+
616
+ state.points.forEach(p => {
617
+ let responsibilities = state.clusters.map(c => {
618
+ const prob = multivariateNormalPDF(p.x, p.y, c.mean, c.covariance);
619
+ return c.weight * prob;
620
+ });
621
+
622
+ const sum = responsibilities.reduce((a, b) => a + b, 0);
623
+ totalLogLikelihood += Math.log(sum || 1e-10);
624
+
625
+ if (sum > 0) {
626
+ responsibilities = responsibilities.map(r => r / sum);
627
+ } else {
628
+ responsibilities = state.clusters.map(() => 1 / state.clusters.length);
629
+ }
630
+
631
+ p.responsibilities = responsibilities;
632
+
633
+ // For visualization
634
+ let maxIdx = 0;
635
+ responsibilities.forEach((r, idx) => { if (r > responsibilities[maxIdx]) maxIdx = idx; });
636
+ p.clusterIndex = maxIdx;
637
+ });
638
+
639
+ state.logLikelihoods.push(totalLogLikelihood);
640
+ state.stepType = 'e-step';
641
+
642
+ // Check convergence
643
+ if (state.logLikelihoods.length > 2) {
644
+ const diff = Math.abs(state.logLikelihoods[state.logLikelihoods.length - 1] - state.logLikelihoods[state.logLikelihoods.length - 2]);
645
+ if (diff < 0.1) state.converged = true;
646
+ }
647
+ }
648
+
649
+ function mStep() {
650
+ const N = state.points.length;
651
+
652
+ state.clusters.forEach((c, j) => {
653
+ const sumResp = state.points.reduce((acc, p) => acc + p.responsibilities[j], 0);
654
+
655
+ // Update Weight
656
+ c.weight = sumResp / N;
657
+
658
+ // Update Mean
659
+ if (sumResp > 0) {
660
+ const newMean = state.points.reduce((acc, p) => {
661
+ acc.x += p.responsibilities[j] * p.x;
662
+ acc.y += p.responsibilities[j] * p.y;
663
+ return acc;
664
+ }, { x: 0, y: 0 });
665
+ c.mean.x = newMean.x / sumResp;
666
+ c.mean.y = newMean.y / sumResp;
667
+
668
+ // Update Covariance
669
+ const newCov = state.points.reduce((acc, p) => {
670
+ const dx = p.x - c.mean.x;
671
+ const dy = p.y - c.mean.y;
672
+ acc.xx += p.responsibilities[j] * dx * dx;
673
+ acc.xy += p.responsibilities[j] * dx * dy;
674
+ acc.yy += p.responsibilities[j] * dy * dy;
675
+ return acc;
676
+ }, { xx: 0, xy: 0, yy: 0 });
677
+
678
+ // Add a small value to diagonal for stability
679
+ c.covariance.xx = newCov.xx / sumResp + 10;
680
+ c.covariance.xy = newCov.xy / sumResp;
681
+ c.covariance.yy = newCov.yy / sumResp + 10;
682
+ }
683
+ });
684
+
685
+ state.iteration++;
686
+ state.stepType = 'm-step';
687
+ if (state.iteration >= MAX_ITERATIONS) state.converged = true;
688
+ }
689
+
690
+ // --- RENDER & UI ---
691
+ function render() {
692
+ const pointsGroup = document.getElementById('points-group');
693
+ const ellipsesGroup = document.getElementById('ellipses-group');
694
+
695
+ // Clear
696
+ pointsGroup.innerHTML = '';
697
+ ellipsesGroup.innerHTML = '';
698
+
699
+ // Draw Clusters
700
+ state.clusters.forEach((c, i) => {
701
+ const params = getEllipseParams(c.covariance);
702
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
703
+
704
+ const ellipse = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
705
+ ellipse.setAttribute('cx', c.mean.x);
706
+ ellipse.setAttribute('cy', c.mean.y);
707
+ ellipse.setAttribute('rx', params.a);
708
+ ellipse.setAttribute('ry', params.b);
709
+ ellipse.setAttribute('fill', c.color.fill);
710
+ ellipse.setAttribute('stroke', c.color.stroke);
711
+ ellipse.setAttribute('stroke-width', '2');
712
+ ellipse.setAttribute('transform', `rotate(${params.angle}, ${c.mean.x}, ${c.mean.y})`);
713
+ if (state.isPlaying) ellipse.classList.add('animate-pulse-soft');
714
+
715
+ const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
716
+ center.setAttribute('cx', c.mean.x);
717
+ center.setAttribute('cy', c.mean.y);
718
+ center.setAttribute('r', '6');
719
+ center.setAttribute('fill', c.color.stroke);
720
+ center.setAttribute('stroke', 'white');
721
+ center.setAttribute('stroke-width', '2');
722
+
723
+ g.appendChild(ellipse);
724
+ g.appendChild(center);
725
+ ellipsesGroup.appendChild(g);
726
+ });
727
+
728
+ // Draw Points
729
+ state.points.forEach((p, i) => {
730
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
731
+ circle.setAttribute('cx', p.x);
732
+ circle.setAttribute('cy', p.y);
733
+ circle.setAttribute('r', '5');
734
+ circle.setAttribute('stroke', 'white');
735
+ circle.setAttribute('stroke-width', '1');
736
+
737
+ const color = p.clusterIndex !== undefined ? CLUSTER_COLORS[p.clusterIndex % CLUSTER_COLORS.length].stroke : '#94a3b8';
738
+ circle.setAttribute('fill', color);
739
+ pointsGroup.appendChild(circle);
740
+ });
741
+
742
+ updateUI();
743
+ }
744
+
745
+ function updateUI() {
746
+ // Update Legend
747
+ const legend = document.getElementById('legend');
748
+ if (legend) {
749
+ legend.innerHTML = state.clusters.map((c, i) => `
750
+ <div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white border shadow-sm">
751
+ <div class="w-3 h-3 rounded-full" style="background: ${c.color.stroke}"></div>
752
+ <span class="font-medium text-xs">${c.color.label}</span>
753
+ <span class="text-[10px] text-slate-400 font-mono">(${(c.weight * 100).toFixed(0)}%)</span>
754
+ </div>
755
+ `).join('');
756
+ }
757
+
758
+ // Steps logic
759
+ const sInit = document.getElementById('step-init');
760
+ const sE = document.getElementById('step-e');
761
+ const sM = document.getElementById('step-m');
762
+ const l1 = document.getElementById('line-1');
763
+ const l2 = document.getElementById('line-2');
764
+
765
+ if (sInit && sE && sM) {
766
+ [sInit, sE, sM].forEach(el => el.className = 'step-node w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all bg-slate-100 text-slate-400');
767
+ if (l1 && l2) [l1, l2].forEach(el => el.className = 'h-0.5 flex-1 mx-2 bg-slate-200');
768
+
769
+ if (state.converged) {
770
+ [sInit, sE, sM].forEach(el => el.classList.add('converged'));
771
+ if (l1 && l2) [l1, l2].forEach(el => el.classList.add('bg-emerald-500'));
772
+ } else if (state.stepType === 'e-step') {
773
+ sE.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
774
+ sInit.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
775
+ if (l1) l1.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
776
+ } else if (state.stepType === 'm-step') {
777
+ sM.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
778
+ sE.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
779
+ if (l2) l2.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
780
+ } else {
781
+ sInit.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
782
+ }
783
+ }
784
+
785
+ // Status Badge
786
+ const badge = document.getElementById('status-badge');
787
+ if (badge) {
788
+ if (state.converged) {
789
+ badge.className = 'px-4 py-2 rounded-lg font-semibold text-sm bg-emerald-100 text-emerald-700 block';
790
+ badge.innerText = 'Converged!';
791
+ } else if (state.stepType !== 'none') {
792
+ badge.className = `px-4 py-2 rounded-lg font-semibold text-sm block ${state.stepType === 'e-step' ? 'bg-orange-100 text-orange-700' : 'bg-blue-100 text-blue-700'}`;
793
+ badge.innerText = state.stepType === 'e-step' ? 'E-Step' : 'M-Step';
794
+ } else {
795
+ badge.className = 'hidden';
796
+ }
797
+ }
798
+
799
+ // Explanation Card
800
+ const explCard = document.getElementById('explanation-card');
801
+ const explTitle = document.getElementById('expl-title');
802
+ const explContent = document.getElementById('expl-content');
803
+ const explIcon = document.getElementById('expl-icon');
804
+
805
+ if (explCard && explTitle && explContent && explIcon) {
806
+ if (state.converged) {
807
+ explCard.className = 'p-5 rounded-xl border-2 bg-emerald-50 border-emerald-200 shadow-soft';
808
+ explTitle.innerText = "Converged! 🎉";
809
+ explIcon.className = "p-3 rounded-lg bg-white text-emerald-600";
810
+ explIcon.innerHTML = '<i data-lucide="check-circle-2" class="w-6 h-6"></i>';
811
+ explContent.innerHTML = `
812
+ <li class="flex items-start gap-2 text-sm text-emerald-800 font-medium">Clusters have stopped moving. Optimal fit found!</li>
813
+ <li class="flex items-start gap-2 text-sm text-slate-600">Total iterations: ${state.iteration}</li>
814
+ `;
815
+ } else if (state.stepType === 'e-step') {
816
+ explCard.className = 'p-5 rounded-xl border-2 bg-orange-50 border-orange-200 shadow-soft';
817
+ explTitle.innerText = "E-Step: Expectations 🤔";
818
+ explIcon.className = "p-3 rounded-lg bg-white text-orange-600";
819
+ explIcon.innerHTML = '<i data-lucide="target" class="w-6 h-6"></i>';
820
+ explContent.innerHTML = `
821
+ <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Each point calculates probabilities for all clusters.</li>
822
+ <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Points change color based on the highest probability.</li>
823
+ `;
824
+ } else if (state.stepType === 'm-step') {
825
+ explCard.className = 'p-5 rounded-xl border-2 bg-blue-50 border-blue-200 shadow-soft';
826
+ explTitle.innerText = "M-Step: Update 📊";
827
+ explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
828
+ explIcon.innerHTML = '<i data-lucide="bar-chart-3" class="w-6 h-6"></i>';
829
+ explContent.innerHTML = `
830
+ <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Centers move to the heart of their assigned points.</li>
831
+ <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Ellipses stretch to cover the point spread.</li>
832
+ `;
833
+ } else {
834
+ explCard.className = 'p-5 rounded-xl border-2 bg-slate-50 border-slate-200 shadow-soft';
835
+ explTitle.innerText = "Ready to Start! 🚀";
836
+ explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
837
+ explIcon.innerHTML = '<i data-lucide="lightbulb" class="w-6 h-6"></i>';
838
+ explContent.innerHTML = `
839
+ <li class="flex items-start gap-2 text-sm text-slate-600">Press 'Next Step' or 'Auto Play' to begin.</li>
840
+ `;
841
+ }
842
+ }
843
+
844
+ // Counters
845
+ const counterText = document.getElementById('counter-text');
846
+ if (counterText) counterText.innerText = `${state.iteration} / ${MAX_ITERATIONS}`;
847
+
848
+ const btnPlayText = document.getElementById('btn-play-text');
849
+ if (btnPlayText) btnPlayText.innerText = state.isPlaying ? 'Pause' : 'Auto Play';
850
+
851
+ const btnPlay = document.getElementById('btn-play');
852
+ if (btnPlay) {
853
+ const icon = btnPlay.querySelector('i, svg');
854
+ if (icon) icon.setAttribute('data-lucide', state.isPlaying ? 'pause' : 'play');
855
+ }
856
+
857
+ // Chart
858
+ renderChart();
859
+ if (window.lucide) lucide.createIcons();
860
+ }
861
+
862
+ function renderChart() {
863
+ const container = document.getElementById('chart-container');
864
+ if (!container) return;
865
+ if (state.logLikelihoods.length === 0) return;
866
+
867
+ const min = Math.min(...state.logLikelihoods);
868
+ const max = Math.max(...state.logLikelihoods);
869
+ const range = max - min || 10;
870
+
871
+ container.innerHTML = state.logLikelihoods.map((val, i) => {
872
+ const height = ((val - min) / range * 80) + 10;
873
+ return `<div class="bg-blue-500 rounded-t-sm flex-1 transition-all duration-300" style="height: ${height}%" title="Iter ${i}: ${val.toFixed(2)}"></div>`;
874
+ }).join('');
875
+
876
+ const convergedBadge = document.getElementById('converged-badge');
877
+ if (convergedBadge) {
878
+ if (state.converged) {
879
+ convergedBadge.classList.replace('hidden', 'flex');
880
+ } else {
881
+ convergedBadge.classList.replace('flex', 'hidden');
882
+ }
883
+ }
884
+ }
885
+
886
+ // --- HANDLERS ---
887
+ function performStep() {
888
+ if (state.converged) return;
889
+ if (state.stepType === 'none' || state.stepType === 'm-step') {
890
+ eStep();
891
+ } else {
892
+ mStep();
893
+ }
894
+ render();
895
+ }
896
+
897
+ function toggleAutoPlay() {
898
+ if (state.isPlaying) {
899
+ stopAutoPlay();
900
+ } else {
901
+ if (state.converged) return;
902
+ state.isPlaying = true;
903
+ state.timer = setInterval(() => {
904
+ performStep();
905
+ if (state.converged) stopAutoPlay();
906
+ }, state.speed);
907
+ updateUI();
908
+ }
909
+ }
910
+
911
+ function stopAutoPlay() {
912
+ state.isPlaying = false;
913
+ clearInterval(state.timer);
914
+ updateUI();
915
+ }
916
+
917
+ function switchTab(tabId) {
918
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
919
+ document.querySelectorAll('.tab-trigger').forEach(el => el.classList.remove('active', 'text-slate-900'));
920
+ document.querySelectorAll('.tab-trigger').forEach(el => el.classList.add('text-slate-600'));
921
+
922
+ const tab = document.getElementById(`tab-${tabId}`);
923
+ if (tab) tab.classList.add('active');
924
+
925
+ const btn = document.getElementById(`btn-tab-${tabId}`);
926
+ if (btn) {
927
+ btn.classList.add('active', 'text-slate-900');
928
+ btn.classList.remove('text-slate-600');
929
+ }
930
+ if (window.lucide) lucide.createIcons();
931
+ }
932
+
933
+ function closeModal() {
934
+ const modal = document.getElementById('intro-modal');
935
+ if (modal) modal.classList.add('hidden');
936
+ }
937
+
938
+ // --- INITIALIZATION ---
939
+ window.onload = () => {
940
+ generateData();
941
+
942
+ // Event Listeners
943
+ const btnPlay = document.getElementById('btn-play');
944
+ if (btnPlay) btnPlay.addEventListener('click', toggleAutoPlay);
945
+
946
+ const btnStep = document.getElementById('btn-step');
947
+ if (btnStep) btnStep.addEventListener('click', performStep);
948
+
949
+ const btnReset = document.getElementById('btn-reset');
950
+ if (btnReset) btnReset.addEventListener('click', () => {
951
+ initClusters();
952
+ resetSimState();
953
+ render();
954
+ });
955
+
956
+ const btnGenerate = document.getElementById('btn-generate');
957
+ if (btnGenerate) btnGenerate.addEventListener('click', generateData);
958
+
959
+ const speedSlider = document.getElementById('speed-slider');
960
+ if (speedSlider) {
961
+ speedSlider.addEventListener('input', (e) => {
962
+ state.speed = 3500 - parseInt(e.target.value);
963
+ const text = state.speed > 2000 ? 'Slow' : state.speed > 1000 ? 'Medium' : 'Fast';
964
+ const speedText = document.getElementById('speed-text');
965
+ if (speedText) speedText.innerText = text;
966
+ if (state.isPlaying) {
967
+ stopAutoPlay();
968
+ toggleAutoPlay();
969
+ }
970
+ });
971
+ }
972
+
973
+ if (window.lucide) lucide.createIcons();
974
+ };
975
+ </script>
976
+ </body>
977
+ </html>
templates/gradient-descent-three.html ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Gradient Descent Simulator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <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">
10
+ <style>
11
+ :root {
12
+ --background: #05080a;
13
+ --primary: #00ffff;
14
+ --secondary: #a855f7;
15
+ --accent: #ec4899;
16
+ --surface: #0a1014;
17
+ --border: #1e293b;
18
+ }
19
+
20
+ body {
21
+ background-color: var(--background);
22
+ color: #f8fafc;
23
+ font-family: 'Space Grotesk', sans-serif;
24
+ margin: 0;
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ .gradient-text {
29
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
30
+ -webkit-background-clip: text;
31
+ -webkit-text-fill-color: transparent;
32
+ }
33
+
34
+ .glass {
35
+ background: rgba(10, 16, 20, 0.8);
36
+ backdrop-filter: blur(12px);
37
+ border: 1px solid rgba(30, 41, 59, 0.5);
38
+ }
39
+
40
+ .glow-cyan {
41
+ box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
42
+ }
43
+
44
+ .canvas-container {
45
+ width: 100%;
46
+ height: 350px; /* Default for mobile */
47
+ position: relative;
48
+ background: #000;
49
+ border-radius: 1rem;
50
+ overflow: hidden;
51
+ border: 1px solid var(--border);
52
+ }
53
+
54
+ /* Desktop override */
55
+ @media (min-width: 768px) {
56
+ .canvas-container {
57
+ height: 500px;
58
+ }
59
+ }
60
+
61
+ input[type=range] {
62
+ -webkit-appearance: none;
63
+ width: 100%;
64
+ background: transparent;
65
+ }
66
+
67
+ input[type=range]::-webkit-slider-runnable-track {
68
+ width: 100%;
69
+ height: 6px;
70
+ cursor: pointer;
71
+ background: #1e293b;
72
+ border-radius: 3px;
73
+ }
74
+
75
+ input[type=range]::-webkit-slider-thumb {
76
+ height: 18px;
77
+ width: 18px;
78
+ border-radius: 50%;
79
+ background: var(--primary);
80
+ cursor: pointer;
81
+ -webkit-appearance: none;
82
+ margin-top: -6px;
83
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
84
+ }
85
+
86
+ .hidden { display: none; }
87
+
88
+ @keyframes float {
89
+ 0%, 100% { transform: translateY(0px); }
90
+ 50% { transform: translateY(-10px); }
91
+ }
92
+ .animate-float { animation: float 3s ease-in-out infinite; }
93
+ </style>
94
+ </head>
95
+ <body class="p-4 md:p-8">
96
+
97
+ <div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_350px] gap-8">
98
+ <!-- Left Side: Header & Viewport -->
99
+ <div class="space-y-6">
100
+ <header class="relative flex flex-col md:block">
101
+ <h1 class="text-4xl md:text-5xl font-bold gradient-text text-center md:text-left">Gradient Descent</h1>
102
+
103
+ <!-- Centered Button (Responsive Position) -->
104
+ <div class="order-last md:order-none mt-4 md:mt-0 md:absolute md:left-1/2 md:-translate-x-1/2 md:top-1 flex items-center justify-center">
105
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
106
+ <a href="/gradient-descent" 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">
107
+ Back to Core
108
+ </a>
109
+ </div>
110
+
111
+ <p class="text-slate-400 text-sm max-w-md mt-2 text-center md:text-left mx-auto md:mx-0">
112
+ Optimize parameters by descending the loss landscape. Now with <b>Adaptive Learning Rate</b> logic!
113
+ </p>
114
+ </header>
115
+
116
+
117
+ <div class="canvas-container" id="container">
118
+ <div id="three-canvas"></div>
119
+
120
+ <!-- Viewport Overlays -->
121
+ <div class="absolute top-4 left-4 pointer-events-none">
122
+ <div class="glass px-3 py-2 rounded-lg">
123
+ <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div>
124
+ <div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase">Ready</div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="absolute top-4 right-4 pointer-events-none text-right">
129
+ <div class="glass px-3 py-2 rounded-lg">
130
+ <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Position</div>
131
+ <div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1">X: 3.00 Y: 3.00</div>
132
+ </div>
133
+ </div>
134
+
135
+ <div class="absolute bottom-4 left-4 glass px-3 py-1 rounded-full text-[10px] text-slate-400 font-bold border border-slate-800">
136
+ GOAL AT <span id="goal-coords" class="text-green-400">0, 0</span>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Educational Info -->
141
+ <div class="glass rounded-xl overflow-hidden">
142
+ <button onclick="toggleInfo()" class="w-full flex items-center justify-between p-4 hover:bg-slate-800/30 transition-colors">
143
+ <div class="flex items-center gap-2">
144
+ <svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
145
+ <span class="font-bold">Optimizer Secrets</span>
146
+ </div>
147
+ <svg id="info-arrow" class="w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
148
+ </button>
149
+ <div id="info-content" class="p-4 pt-0 text-sm text-slate-400 leading-relaxed grid md:grid-cols-2 gap-6 border-t border-slate-800 hidden">
150
+ <div class="space-y-2 pt-4">
151
+ <p><strong class="text-white">Momentum (β):</strong> Acts like a physical ball with weight. It keeps moving in the same direction, helping it "roll" through flat valleys and over small local pits.</p>
152
+ </div>
153
+ <div class="space-y-2 pt-4">
154
+ <p><strong class="text-white">The Challenge:</strong> Rosenbrock and Rastrigin are "non-convex" or have "vanishing gradients." Vanilla GD is often too weak to reach the center without help!</p>
155
+ </div>
156
+ <!-- Pro Tip Note -->
157
+ <div class="col-span-full pt-4 border-t border-slate-800/50 mt-2">
158
+ <p class="text-yellow-400 font-medium">
159
+ <span class="mr-2">💡</span> <b>Pro Tip:</b> If the ball gets stuck or vibrates wildly, you must <b>adjust the learning rate</b> or <b>momentum</b>. Or turn on <b>Adaptive Rate</b> to let the algorithm handle it!
160
+ </p>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Right Side: Controls -->
167
+ <aside class="space-y-6">
168
+ <div class="glass rounded-xl p-6 space-y-6">
169
+ <!-- Learning Rate Slider -->
170
+ <div class="space-y-4">
171
+ <div class="flex justify-between items-center">
172
+ <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Learning Rate (α)</label>
173
+ <span id="lr-value" class="text-cyan-400 font-mono bg-cyan-400/10 px-2 py-0.5 rounded border border-cyan-400/20 text-sm">0.050</span>
174
+ </div>
175
+ <input type="range" id="lr-slider" min="0.001" max="0.5" step="0.001" value="0.05">
176
+ </div>
177
+
178
+ <!-- Adaptive Toggle -->
179
+ <div onclick="toggleAdaptive()" class="flex items-center justify-between p-3 rounded-lg bg-slate-800/40 border border-slate-700/50 cursor-pointer hover:bg-slate-800 transition-colors">
180
+ <div class="flex flex-col">
181
+ <span class="text-xs font-bold text-slate-300">Adaptive Rate</span>
182
+ <span class="text-[10px] text-slate-500">Auto-tune α during descent</span>
183
+ </div>
184
+ <div id="adaptive-toggle-bg" class="w-10 h-5 rounded-full bg-slate-700 relative transition-colors">
185
+ <div id="adaptive-toggle-dot" class="absolute left-1 top-1 w-3 h-3 rounded-full bg-white transition-all"></div>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Momentum Slider -->
190
+ <div class="space-y-4">
191
+ <div class="flex justify-between items-center">
192
+ <label class="text-xs font-bold text-slate-500 uppercase tracking-wider">Momentum (β)</label>
193
+ <span id="mom-value" class="text-purple-400 font-mono bg-purple-400/10 px-2 py-0.5 rounded border border-purple-400/20 text-sm">0.10</span>
194
+ </div>
195
+ <input type="range" id="mom-slider" min="0" max="0.99" step="0.01" value="0.1">
196
+ </div>
197
+
198
+ <!-- Playback Buttons -->
199
+ <div class="grid grid-cols-2 gap-3">
200
+ <button id="btn-toggle" class="flex items-center justify-center gap-2 py-3 rounded-lg font-bold transition-all bg-cyan-400 text-black glow-cyan hover:brightness-110">
201
+ <span id="toggle-icon">▶</span> <span id="toggle-text">Start</span>
202
+ </button>
203
+ <button id="btn-step" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 transition-all font-bold">
204
+ Step ➜
205
+ </button>
206
+ </div>
207
+
208
+ <button id="btn-reset" class="w-full flex items-center justify-center gap-2 py-2 text-slate-400 hover:text-white text-sm transition-colors font-medium">
209
+ Reset Position ↺
210
+ </button>
211
+
212
+ <!-- Stats -->
213
+ <div class="grid grid-cols-2 gap-4 pt-4 border-t border-slate-800">
214
+ <div class="text-center">
215
+ <div id="steps-val" class="text-2xl font-mono font-bold text-cyan-400">0</div>
216
+ <div class="text-[10px] text-slate-500 uppercase tracking-tighter">Steps</div>
217
+ </div>
218
+ <div class="text-center">
219
+ <div id="loss-val" class="text-2xl font-mono font-bold text-pink-500">9.00</div>
220
+ <div class="text-[10px] text-slate-500 uppercase tracking-tighter">Loss</div>
221
+ </div>
222
+ </div>
223
+
224
+ <!-- Progress -->
225
+ <div class="p-4 bg-slate-900/50 rounded-lg border border-slate-800">
226
+ <div class="flex justify-between text-xs mb-2">
227
+ <span class="text-slate-500">Distance to Target</span>
228
+ <span id="dist-val" class="font-mono text-slate-300">4.242</span>
229
+ </div>
230
+ <div class="h-1.5 bg-slate-800 rounded-full overflow-hidden">
231
+ <div id="progress-bar" class="h-full bg-cyan-400 transition-all duration-300" style="width: 10%"></div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ <!-- Level Selector -->
237
+ <div class="glass rounded-xl p-4 space-y-3">
238
+ <h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest px-2">Function Landscape</h3>
239
+ <div id="level-list" class="space-y-1">
240
+ <!-- Levels injected here -->
241
+ </div>
242
+ </div>
243
+ </aside>
244
+ </div>
245
+
246
+ <!-- Victory Modal -->
247
+ <div id="victory-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm hidden">
248
+ <div class="glass relative max-w-sm w-full rounded-2xl p-8 text-center animate-float">
249
+ <!-- Close Button (Cross) -->
250
+ <button onclick="closeModal()" class="absolute top-4 right-4 text-slate-500 hover:text-white transition-colors">
251
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
252
+ </button>
253
+
254
+ <div class="w-16 h-16 bg-cyan-400/20 rounded-full flex items-center justify-center mx-auto mb-4">
255
+ <span class="text-3xl text-cyan-400">🏆</span>
256
+ </div>
257
+ <h2 class="text-2xl font-bold gradient-text mb-1">Convergence!</h2>
258
+ <p id="modal-level-name" class="text-slate-400 text-sm mb-6">Level Complete</p>
259
+
260
+ <div class="bg-slate-900/80 rounded-xl p-4 mb-6">
261
+ <div id="modal-steps" class="text-3xl font-mono text-cyan-400 font-bold">0</div>
262
+ <div class="text-xs text-slate-500 uppercase tracking-widest">Total Steps</div>
263
+ </div>
264
+
265
+ <div class="flex gap-3">
266
+ <button onclick="closeModal()" class="flex-1 py-2 rounded-lg border border-slate-700 hover:bg-slate-800 transition-colors text-sm font-semibold">Retry</button>
267
+ <button onclick="nextLevel()" class="flex-1 py-2 rounded-lg bg-cyan-400 text-black font-bold text-sm">Next Level</button>
268
+ </div>
269
+ </div>
270
+ </div>
271
+
272
+ <script>
273
+ // --- LEVEL DEFINITIONS ---
274
+ // Added 'presets' to automatically tune parameters for each landscape
275
+ const LEVELS = [
276
+ {
277
+ id: 1,
278
+ name: 'The Bowl',
279
+ difficulty: 'easy',
280
+ loss: (x, y) => x*x + y*y,
281
+ min: {x: 0, y: 0},
282
+ winRadius: 0.15,
283
+ presets: { lr: 0.05, mom: 0.1 } // Slow & Steady
284
+ },
285
+ {
286
+ id: 2,
287
+ name: 'The Ellipse',
288
+ difficulty: 'easy',
289
+ loss: (x, y) => x*x + 10*y*y,
290
+ min: {x: 0, y: 0},
291
+ winRadius: 0.2,
292
+ presets: { lr: 0.05, mom: 0.1 }
293
+ },
294
+ {
295
+ id: 3,
296
+ name: 'Rosenbrock Valley',
297
+ difficulty: 'medium',
298
+ loss: (x, y) => Math.pow(1-x, 2) + 100*Math.pow(y-x*x, 2),
299
+ min: {x: 1, y: 1},
300
+ winRadius: 0.25,
301
+ presets: { lr: 0.002, mom: 0.85 } // Needs momentum to traverse the valley
302
+ },
303
+ {
304
+ id: 4,
305
+ name: 'Beale Function',
306
+ difficulty: 'medium',
307
+ loss: (x, y) => Math.pow(1.5-x+x*y, 2) + Math.pow(2.25-x+x*y*y, 2) + Math.pow(2.625-x+x*y*y*y, 2),
308
+ min: {x: 3, y: 0.5},
309
+ winRadius: 0.25,
310
+ presets: { lr: 0.01, mom: 0.5 }
311
+ },
312
+ {
313
+ id: 5,
314
+ name: 'Rastrigin',
315
+ difficulty: 'hard',
316
+ loss: (x, y) => 20 + x*x - 10*Math.cos(2*Math.PI*x) + y*y - 10*Math.cos(2*Math.PI*y),
317
+ min: {x: 0, y: 0},
318
+ winRadius: 0.3,
319
+ presets: { lr: 0.005, mom: 0.95 } // HIGH MOMENTUM to escape local minima
320
+ }
321
+ ];
322
+
323
+ // Constants for 3D Visuals
324
+ const SURFACE_OFFSET = -0.5;
325
+ const Z_SCALE = 0.8;
326
+ const BALL_FLOAT = 0.1;
327
+
328
+ // --- APP STATE ---
329
+ let currentLevel = LEVELS[0];
330
+ let pos = { x: 3, y: 3 };
331
+ let velocity = { x: 0, y: 0 };
332
+ let path = [];
333
+ let learningRate = 0.05;
334
+ let momentum = 0.1; // Default starting momentum
335
+ let isAdaptive = false;
336
+ let isRunning = false;
337
+ let steps = 0;
338
+ let loopId = null;
339
+ let prevLoss = Infinity;
340
+
341
+ // --- THREE.JS SETUP ---
342
+ const container = document.getElementById('container');
343
+ const scene = new THREE.Scene();
344
+ scene.background = new THREE.Color(0x05080a);
345
+
346
+ const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000);
347
+ camera.position.set(8, 8, 8);
348
+ camera.lookAt(0, 0, 0);
349
+
350
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
351
+ renderer.setSize(container.clientWidth, container.clientHeight);
352
+ renderer.setPixelRatio(window.devicePixelRatio);
353
+ document.getElementById('three-canvas').appendChild(renderer.domElement);
354
+
355
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
356
+ scene.add(ambientLight);
357
+ const pointLight = new THREE.PointLight(0x00ffff, 1);
358
+ pointLight.position.set(10, 10, 10);
359
+ scene.add(pointLight);
360
+
361
+ function calculateVizHeight(x, y) {
362
+ let z = currentLevel.loss(x, y);
363
+ z = Math.min(z, 50);
364
+ return (Math.log(z + 1) * Z_SCALE) + SURFACE_OFFSET;
365
+ }
366
+
367
+ // Surface
368
+ let surfaceMesh;
369
+ function updateSurface() {
370
+ if (surfaceMesh) scene.remove(surfaceMesh);
371
+
372
+ const size = 64;
373
+ const geo = new THREE.PlaneBufferGeometry(10, 10, size, size);
374
+ const posAttr = geo.attributes.position;
375
+ const colorAttr = new THREE.BufferAttribute(new Float32Array(posAttr.count * 3), 3);
376
+
377
+ let minVal = Infinity, maxVal = -Infinity;
378
+ const vals = [];
379
+
380
+ for (let i = 0; i < posAttr.count; i++) {
381
+ const x = posAttr.getX(i);
382
+ const y = posAttr.getY(i);
383
+ let z = currentLevel.loss(x, y);
384
+ z = Math.min(z, 50);
385
+ z = Math.log(z + 1) * Z_SCALE;
386
+ vals.push(z);
387
+ minVal = Math.min(minVal, z);
388
+ maxVal = Math.max(maxVal, z);
389
+ }
390
+
391
+ for (let i = 0; i < posAttr.count; i++) {
392
+ const z = vals[i];
393
+ posAttr.setZ(i, z);
394
+ const t = (z - minVal) / (maxVal - minVal || 1);
395
+ colorAttr.setXYZ(i, 0.1 + t * 0.8, 0.8 - t * 0.6, 0.8 + t * 0.2);
396
+ }
397
+
398
+ geo.setAttribute('color', colorAttr);
399
+ geo.computeVertexNormals();
400
+
401
+ const mat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, transparent: true, opacity: 0.8, roughness: 0.5 });
402
+ surfaceMesh = new THREE.Mesh(geo, mat);
403
+ surfaceMesh.rotation.x = -Math.PI / 2;
404
+ surfaceMesh.position.y = SURFACE_OFFSET;
405
+ scene.add(surfaceMesh);
406
+ }
407
+
408
+ const ball = new THREE.Mesh(
409
+ new THREE.SphereGeometry(0.18, 24, 24),
410
+ new THREE.MeshStandardMaterial({ color: 0xff66cc, emissive: 0xff0088, emissiveIntensity: 0.5 })
411
+ );
412
+ scene.add(ball);
413
+
414
+ let pathLine;
415
+ function updatePath() {
416
+ if (pathLine) scene.remove(pathLine);
417
+ if (path.length < 2) return;
418
+
419
+ const points = path.map(p => new THREE.Vector3(p.x, calculateVizHeight(p.x, p.y) + 0.05, p.y));
420
+ const geo = new THREE.BufferGeometry().setFromPoints(points);
421
+ const mat = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8 });
422
+ pathLine = new THREE.Line(geo, mat);
423
+ scene.add(pathLine);
424
+ }
425
+
426
+ const goalMarker = new THREE.Group();
427
+ const torus = new THREE.Mesh(new THREE.TorusGeometry(0.3, 0.02, 16, 32), new THREE.MeshBasicMaterial({ color: 0x00ff66, transparent: true, opacity: 0.5 }));
428
+ torus.rotation.x = Math.PI/2;
429
+ const star = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), new THREE.MeshBasicMaterial({ color: 0x00ff66 }));
430
+ goalMarker.add(torus);
431
+ goalMarker.add(star);
432
+ scene.add(goalMarker);
433
+
434
+ function updateGoalMarker() {
435
+ const gx = currentLevel.min.x;
436
+ const gy = currentLevel.min.y;
437
+ const gz = calculateVizHeight(gx, gy);
438
+ goalMarker.position.set(gx, gz + 0.05, gy);
439
+ document.getElementById('goal-coords').innerText = `[${gx}, ${gy}]`;
440
+ }
441
+
442
+ scene.add(new THREE.GridHelper(10, 20, 0x1a3a4a, 0x0a1a2a));
443
+
444
+ // --- OPTIMIZER LOGIC ---
445
+ function calculateGradient(x, y) {
446
+ const h = 0.0001;
447
+ const dx = (currentLevel.loss(x + h, y) - currentLevel.loss(x - h, y)) / (2 * h);
448
+ const dy = (currentLevel.loss(x, y + h) - currentLevel.loss(x, y - h)) / (2 * h);
449
+
450
+ const magnitude = Math.sqrt(dx*dx + dy*dy);
451
+ const limit = 50;
452
+ if (magnitude > limit) {
453
+ return { x: (dx / magnitude) * limit, y: (dy / magnitude) * limit, magnitude: limit };
454
+ }
455
+ return { x: dx, y: dy, magnitude };
456
+ }
457
+
458
+ function step() {
459
+ const currentLoss = currentLevel.loss(pos.x, pos.y);
460
+ const grad = calculateGradient(pos.x, pos.y);
461
+
462
+ // --- ADAPTIVE LOGIC ---
463
+ if (isAdaptive) {
464
+ if (currentLoss > prevLoss * 1.05) {
465
+ learningRate *= 0.5;
466
+ }
467
+ else if (Math.abs(currentLoss - prevLoss) < 0.00001 && grad.magnitude < 0.01) {
468
+ learningRate *= 1.2;
469
+ }
470
+
471
+ learningRate = Math.max(0.001, Math.min(0.5, learningRate));
472
+ updateLRElement();
473
+ }
474
+
475
+ // Momentum Logic
476
+ velocity.x = momentum * velocity.x - learningRate * grad.x;
477
+ velocity.y = momentum * velocity.y - learningRate * grad.y;
478
+
479
+ pos.x = Math.max(-5, Math.min(5, pos.x + velocity.x));
480
+ pos.y = Math.max(-5, Math.min(5, pos.y + velocity.y));
481
+
482
+ path.push({...pos});
483
+ steps++;
484
+ prevLoss = currentLoss;
485
+ updateUI();
486
+
487
+ const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2));
488
+ if (dist < currentLevel.winRadius) {
489
+ stopDescent();
490
+ showVictory();
491
+ } else if (steps > 3000) {
492
+ stopDescent();
493
+ }
494
+ }
495
+
496
+ function updateLRElement() {
497
+ document.getElementById('lr-slider').value = learningRate;
498
+ document.getElementById('lr-value').innerText = learningRate.toFixed(3);
499
+ }
500
+
501
+ function updateUI() {
502
+ document.getElementById('pos-display').innerText = `X: ${pos.x.toFixed(2)} Y: ${pos.y.toFixed(2)}`;
503
+ document.getElementById('steps-val').innerText = steps;
504
+ const lossVal = currentLevel.loss(pos.x, pos.y);
505
+ document.getElementById('loss-val').innerText = lossVal.toFixed(2);
506
+
507
+ const dist = Math.sqrt(Math.pow(pos.x - currentLevel.min.x, 2) + Math.pow(pos.y - currentLevel.min.y, 2));
508
+ document.getElementById('dist-val').innerText = dist.toFixed(3);
509
+ document.getElementById('progress-bar').style.width = `${Math.max(5, Math.min(100, (1 - dist / 7) * 100))}%`;
510
+
511
+ const ballZ = calculateVizHeight(pos.x, pos.y);
512
+ ball.position.set(pos.x, ballZ + BALL_FLOAT, pos.y);
513
+ updatePath();
514
+ }
515
+
516
+ function toggleAdaptive() {
517
+ isAdaptive = !isAdaptive;
518
+ const bg = document.getElementById('adaptive-toggle-bg');
519
+ const dot = document.getElementById('adaptive-toggle-dot');
520
+ if (isAdaptive) {
521
+ bg.classList.replace('bg-slate-700', 'bg-cyan-400');
522
+ dot.style.left = '22px';
523
+ } else {
524
+ bg.classList.replace('bg-cyan-400', 'bg-slate-700');
525
+ dot.style.left = '4px';
526
+ }
527
+ }
528
+
529
+ function startDescent() {
530
+ isRunning = true;
531
+ document.getElementById('toggle-text').innerText = 'Pause';
532
+ document.getElementById('status-text').innerText = 'Descending...';
533
+ document.getElementById('status-text').className = 'text-xs font-mono text-yellow-400 mt-1 uppercase';
534
+ prevLoss = currentLevel.loss(pos.x, pos.y);
535
+ loopId = setInterval(step, 50);
536
+ }
537
+
538
+ function stopDescent() {
539
+ isRunning = false;
540
+ document.getElementById('toggle-text').innerText = 'Start';
541
+ document.getElementById('status-text').innerText = 'Paused';
542
+ document.getElementById('status-text').className = 'text-xs font-mono text-cyan-400 mt-1 uppercase';
543
+ clearInterval(loopId);
544
+ }
545
+
546
+ function showVictory() {
547
+ document.getElementById('modal-level-name').innerText = currentLevel.name;
548
+ document.getElementById('modal-steps').innerText = steps;
549
+ document.getElementById('victory-modal').classList.remove('hidden');
550
+ }
551
+
552
+ function reset() {
553
+ stopDescent();
554
+ if(currentLevel.id === 3) pos = { x: -3, y: -3 };
555
+ else if(currentLevel.id === 4) pos = { x: 1, y: 1 };
556
+ else pos = { x: (Math.random() - 0.5) * 8, y: (Math.random() - 0.5) * 8 };
557
+
558
+ velocity = { x: 0, y: 0 };
559
+ path = [{...pos}];
560
+ steps = 0;
561
+ updateUI();
562
+ }
563
+
564
+ // --- EVENT HANDLERS ---
565
+ document.getElementById('btn-toggle').onclick = () => isRunning ? stopDescent() : startDescent();
566
+ document.getElementById('btn-step').onclick = step;
567
+ document.getElementById('btn-reset').onclick = reset;
568
+ document.getElementById('lr-slider').oninput = (e) => {
569
+ learningRate = parseFloat(e.target.value);
570
+ document.getElementById('lr-value').innerText = learningRate.toFixed(3);
571
+ };
572
+ document.getElementById('mom-slider').oninput = (e) => {
573
+ momentum = parseFloat(e.target.value);
574
+ document.getElementById('mom-value').innerText = momentum.toFixed(2);
575
+ };
576
+
577
+ function toggleInfo() {
578
+ const content = document.getElementById('info-content');
579
+ content.classList.toggle('hidden');
580
+ }
581
+
582
+ function closeModal() {
583
+ document.getElementById('victory-modal').classList.add('hidden');
584
+ reset();
585
+ }
586
+
587
+ function nextLevel() {
588
+ const idx = LEVELS.findIndex(l => l.id === currentLevel.id);
589
+ const next = LEVELS[(idx + 1) % LEVELS.length];
590
+ selectLevel(next.id);
591
+ document.getElementById('victory-modal').classList.add('hidden');
592
+ }
593
+
594
+ function selectLevel(id) {
595
+ currentLevel = LEVELS.find(l => l.id === id);
596
+
597
+ // Apply Presets if they exist
598
+ if (currentLevel.presets) {
599
+ learningRate = currentLevel.presets.lr;
600
+ momentum = currentLevel.presets.mom;
601
+
602
+ // Update UI Controls
603
+ document.getElementById('lr-slider').value = learningRate;
604
+ document.getElementById('lr-value').innerText = learningRate.toFixed(3);
605
+ document.getElementById('mom-slider').value = momentum;
606
+ document.getElementById('mom-value').innerText = momentum.toFixed(2);
607
+ }
608
+
609
+ renderLevels();
610
+ updateSurface();
611
+ updateGoalMarker();
612
+ reset();
613
+ }
614
+
615
+ function renderLevels() {
616
+ const list = document.getElementById('level-list');
617
+ list.innerHTML = LEVELS.map(l => `
618
+ <button onclick="selectLevel(${l.id})" class="w-full text-left p-3 rounded-lg transition-all border ${currentLevel.id === l.id ? 'bg-cyan-400/10 border-cyan-400/50' : 'border-transparent hover:bg-slate-800'}">
619
+ <div class="flex justify-between items-center mb-1">
620
+ <span class="font-bold text-sm ${currentLevel.id === l.id ? 'text-cyan-400' : ''}">${l.name}</span>
621
+ <span class="text-[9px] px-1.5 py-0.5 rounded border ${l.difficulty === 'easy' ? 'border-green-500/30 text-green-400' : l.difficulty === 'medium' ? 'border-yellow-500/30 text-yellow-400' : 'border-red-500/30 text-red-400'} uppercase">${l.difficulty}</span>
622
+ </div>
623
+ </button>
624
+ `).join('');
625
+ }
626
+
627
+ function animate() {
628
+ requestAnimationFrame(animate);
629
+ if (isRunning) {
630
+ const s = 1 + Math.sin(Date.now() * 0.01) * 0.15;
631
+ ball.scale.setScalar(s);
632
+ }
633
+ renderer.render(scene, camera);
634
+ }
635
+
636
+ window.addEventListener('resize', () => {
637
+ camera.aspect = container.clientWidth / container.clientHeight;
638
+ camera.updateProjectionMatrix();
639
+ renderer.setSize(container.clientWidth, container.clientHeight);
640
+ });
641
+
642
+ renderLevels();
643
+ updateSurface();
644
+ updateGoalMarker();
645
+ updateUI();
646
+ animate();
647
+
648
+ // --- MOUSE & TOUCH CONTROLS ---
649
+ let isMouseDown = false;
650
+ let prevMouse = { x: 0, y: 0 };
651
+
652
+ // Mouse Events
653
+ container.addEventListener('mousedown', (e) => {
654
+ isMouseDown = true;
655
+ prevMouse = { x: e.clientX, y: e.clientY };
656
+ });
657
+ window.addEventListener('mouseup', () => isMouseDown = false);
658
+ window.addEventListener('mousemove', (e) => handleCameraRotate(e.clientX, e.clientY));
659
+
660
+ // Touch Events (Mobile)
661
+ container.addEventListener('touchstart', (e) => {
662
+ if(e.touches.length === 1) {
663
+ isMouseDown = true;
664
+ prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
665
+ }
666
+ }, { passive: false });
667
+
668
+ window.addEventListener('touchend', () => isMouseDown = false);
669
+
670
+ window.addEventListener('touchmove', (e) => {
671
+ if(e.touches.length === 1 && isMouseDown) {
672
+ handleCameraRotate(e.touches[0].clientX, e.touches[0].clientY);
673
+ }
674
+ }, { passive: false });
675
+
676
+ // Common Rotation Logic
677
+ function handleCameraRotate(clientX, clientY) {
678
+ if (!isMouseDown) return;
679
+ const dx = (clientX - prevMouse.x) * 0.005;
680
+ const dy = (clientY - prevMouse.y) * 0.005;
681
+ const radius = camera.position.length();
682
+ let phi = Math.atan2(camera.position.x, camera.position.z);
683
+ let theta = Math.acos(camera.position.y / radius);
684
+ phi -= dx;
685
+ theta = Math.max(0.1, Math.min(Math.PI / 2.1, theta - dy));
686
+ camera.position.x = radius * Math.sin(theta) * Math.sin(phi);
687
+ camera.position.y = radius * Math.cos(theta);
688
+ camera.position.z = radius * Math.sin(theta) * Math.cos(phi);
689
+ camera.lookAt(0, 0, 0);
690
+ prevMouse = { x: clientX, y: clientY };
691
+ }
692
+
693
+ </script>
694
+ </body>
695
+ </html>
templates/ica-threejs.html ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>ICA Simulator - Independent Component Analysis</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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
16
+
17
+ <!-- Import Map -->
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
+ }
24
+ }
25
+ </script>
26
+
27
+ {% raw %}
28
+ <style>
29
+ :root {
30
+ --background: 40 30% 98%;
31
+ --foreground: 220 20% 20%;
32
+ --card: 0 0% 100%;
33
+ --card-foreground: 220 20% 20%;
34
+ --popover: 0 0% 100%;
35
+ --popover-foreground: 220 20% 20%;
36
+ --primary: 12 80% 60%;
37
+ --primary-foreground: 0 0% 100%;
38
+ --secondary: 180 50% 45%;
39
+ --secondary-foreground: 0 0% 100%;
40
+ --muted: 40 20% 92%;
41
+ --muted-foreground: 220 10% 45%;
42
+ --accent: 260 60% 60%;
43
+ --accent-foreground: 0 0% 100%;
44
+ --destructive: 0 84.2% 60.2%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 40 20% 88%;
47
+ --input: 40 20% 88%;
48
+ --ring: 12 80% 60%;
49
+ --radius: 1rem;
50
+
51
+ /* Signal Colors */
52
+ --signal-1: 12 85% 62%;
53
+ --signal-2: 175 65% 45%;
54
+ --signal-mixed: 280 60% 55%;
55
+
56
+ --shadow-soft: 0 4px 20px -4px hsl(220 20% 20% / 0.1);
57
+ --shadow-glow: 0 0 30px hsl(12 80% 60% / 0.2);
58
+ }
59
+
60
+ body {
61
+ background-color: hsl(var(--background));
62
+ color: hsl(var(--foreground));
63
+ font-family: 'Space Grotesk', sans-serif;
64
+ margin: 0;
65
+ overflow-x: hidden;
66
+ }
67
+
68
+ code, .mono { font-family: 'JetBrains Mono', monospace; }
69
+
70
+ .text-gradient {
71
+ background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
72
+ -webkit-background-clip: text;
73
+ -webkit-text-fill-color: transparent;
74
+ }
75
+
76
+ .shadow-soft { box-shadow: var(--shadow-soft); }
77
+ .shadow-glow { box-shadow: var(--shadow-glow); }
78
+
79
+ @keyframes float {
80
+ 0%, 100% { transform: translateY(0); }
81
+ 50% { transform: translateY(-10px); }
82
+ }
83
+ .animate-float { animation: float 3s ease-in-out infinite; }
84
+
85
+ @keyframes fadeIn {
86
+ from { opacity: 0; transform: translateY(10px); }
87
+ to { opacity: 1; transform: translateY(0); }
88
+ }
89
+ .animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
90
+
91
+ .glass-card {
92
+ background: rgba(255, 255, 255, 0.7);
93
+ backdrop-filter: blur(10px);
94
+ border: 1px solid hsl(var(--border));
95
+ }
96
+
97
+ input[type=range] {
98
+ accent-color: hsl(var(--primary));
99
+ }
100
+
101
+ canvas {
102
+ width: 100% !important;
103
+ height: auto !important;
104
+ display: block;
105
+ }
106
+ </style>
107
+ {% endraw %}
108
+ </head>
109
+ <body>
110
+ <div id="root"></div>
111
+
112
+ {% raw %}
113
+ <script type="text/babel" data-type="module">
114
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
115
+ import { createRoot } from 'react-dom/client';
116
+
117
+ /** --- Icons --- */
118
+ const Icons = {
119
+ Play: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>,
120
+ RotateCcw: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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>,
121
+ Sparkles: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/></svg>,
122
+ Music: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>,
123
+ Radio: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 1 0-8.48 0"/><path d="M19.07 4.93a10 10 0 1 0-14.14 0"/></svg>,
124
+ Zap: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
125
+ ArrowRight: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>,
126
+ Lightbulb: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18h6m-3 4h.01M12 2a7 7 0 0 0-7 7c0 2.32 1.35 4.31 3.31 5.31l.69 1.69h6l.69-1.69C17.65 13.31 19 11.32 19 9a7 7 0 0 0-7-7z"/></svg>,
127
+ PartyPopper: (p) => <svg {...p} width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5.8 11.3 2 22l10.7-3.8"/><path d="M4 3h.01"/><path d="M22 8h.01"/><path d="M15 2h.01"/><path d="M22 20h.01"/><path d="m22 2-2.24.75a2.9 2.9 0 0 0-1.96 2.4l-.15 1.41a2.9 2.9 0 0 1-2.4 2.6l-1.4.15a2.9 2.9 0 0 0-2.4 1.96L11 13"/><path d="m18 13 2.24-.75a2.9 2.9 0 0 0 1.96-2.4l.15-1.41a2.9 2.9 0 0 1 2.4-2.6l1.4-.15a2.9 2.9 0 0 0 2.4-1.96L29 1.5"/></svg>,
128
+ };
129
+
130
+ /** --- Math Utility --- */
131
+ const generateSignal = (type, frequency, samples) => {
132
+ const signal = [];
133
+ for (let i = 0; i < samples; i++) {
134
+ const t = (i / samples) * 4 * Math.PI * frequency;
135
+ switch (type) {
136
+ case "sine": signal.push(Math.sin(t)); break;
137
+ case "square": signal.push(Math.sin(t) > 0 ? 0.8 : -0.8); break;
138
+ case "sawtooth": signal.push(((t % (2 * Math.PI)) / Math.PI - 1) * 0.8); break;
139
+ }
140
+ }
141
+ return signal;
142
+ };
143
+
144
+ /** --- UI Sub-Components --- */
145
+ const Button = ({ children, className = '', variant = 'default', size = 'default', ...props }) => {
146
+ const variants = {
147
+ default: "bg-primary text-primary-foreground hover:opacity-90 shadow-soft",
148
+ outline: "border border-input bg-background hover:bg-slate-50",
149
+ ghost: "hover:bg-slate-100",
150
+ };
151
+ const sizes = { default: "h-10 px-4 py-2", lg: "h-12 px-6 text-lg", sm: "h-9 rounded-md px-3" };
152
+ return <button className={`inline-flex items-center justify-center rounded-xl font-medium transition-all active:scale-95 disabled:opacity-50 whitespace-nowrap ${variants[variant] || variants.default} ${sizes[size] || sizes.default} ${className}`} {...props}>{children}</button>;
153
+ };
154
+
155
+ const WaveCanvas = ({ signals, colors, width = 400, height = 120, animated = true, showGrid = true }) => {
156
+ const canvasRef = useRef(null);
157
+ const offsetRef = useRef(0);
158
+
159
+ useEffect(() => {
160
+ const canvas = canvasRef.current; if (!canvas) return;
161
+ const ctx = canvas.getContext("2d"); if (!ctx) return;
162
+
163
+ const dpr = window.devicePixelRatio || 1;
164
+ canvas.width = width * dpr; canvas.height = height * dpr;
165
+ ctx.scale(dpr, dpr);
166
+
167
+ let req;
168
+ const draw = () => {
169
+ ctx.clearRect(0, 0, width, height);
170
+ if (showGrid) {
171
+ ctx.strokeStyle = "hsl(220 15% 88% / 0.5)"; ctx.lineWidth = 1;
172
+ ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke();
173
+ }
174
+ signals.forEach((signal, idx) => {
175
+ ctx.strokeStyle = colors[idx] || "#666"; ctx.lineWidth = 2.5; ctx.lineCap = "round";
176
+ ctx.beginPath();
177
+ const step = width / (signal.length - 1);
178
+ signal.forEach((val, i) => {
179
+ const x = i * step;
180
+ const drift = animated ? Math.sin((i * 0.05) + offsetRef.current * 0.02) * 0.1 : 0;
181
+ const y = height / 2 - (val + drift) * (height / 2 * 0.8);
182
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
183
+ });
184
+ ctx.stroke();
185
+ });
186
+ if (animated) { offsetRef.current += 1; req = requestAnimationFrame(draw); }
187
+ };
188
+ draw();
189
+ return () => cancelAnimationFrame(req);
190
+ }, [signals, colors, width, height, animated, showGrid]);
191
+
192
+ return <canvas ref={canvasRef} className="rounded-lg w-full h-auto" />;
193
+ };
194
+
195
+ const MixingSlider = ({ label, value, onChange, color }) => (
196
+ <div className="space-y-2">
197
+ <div className="flex items-center justify-between">
198
+ <span className="text-sm font-medium text-foreground">{label}</span>
199
+ <span className="text-sm font-mono px-2 py-0.5 rounded bg-slate-100" style={{ color }}>{value.toFixed(2)}</span>
200
+ </div>
201
+ <input
202
+ type="range" min="0" max="1" step="0.01" value={value}
203
+ onChange={(e) => onChange(parseFloat(e.target.value))}
204
+ className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer"
205
+ />
206
+ </div>
207
+ );
208
+
209
+ const SignalCard = ({ title, description, signals, colors, icon, badge, badgeColor = "bg-primary" }) => (
210
+ <div className="glass-card rounded-xl p-5 shadow-soft hover:shadow-lg transition-all group">
211
+ <div className="flex items-start justify-between mb-3">
212
+ <div className="flex items-center gap-3">
213
+ {icon && <div className="w-10 h-10 rounded-lg bg-slate-100 flex items-center justify-center text-slate-600 group-hover:scale-110 transition-transform">{icon}</div>}
214
+ <div><h3 className="font-semibold text-foreground text-sm sm:text-base">{title}</h3>{description && <p className="text-xs text-muted-foreground">{description}</p>}</div>
215
+ </div>
216
+ {badge && <span className={`text-[10px] font-bold px-2 py-1 rounded-full ${badgeColor} text-white uppercase`}>{badge}</span>}
217
+ </div>
218
+ <div className="bg-slate-50/50 rounded-lg p-3 overflow-hidden">
219
+ <WaveCanvas signals={signals} colors={colors} width={400} height={100} />
220
+ </div>
221
+ </div>
222
+ );
223
+
224
+ const StepIndicator = ({ currentStep, totalSteps, labels }) => (
225
+ <div className="flex items-center justify-center gap-2 sm:gap-4 mb-8 sm:mb-12 overflow-x-auto pb-2">
226
+ {labels.map((label, i) => (
227
+ <React.Fragment key={i}>
228
+ <div className="flex flex-col items-center flex-shrink-0">
229
+ <div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center font-bold text-xs sm:text-sm transition-all ${i < currentStep ? "bg-secondary text-white" : i === currentStep ? "bg-primary text-white shadow-glow" : "bg-slate-200 text-slate-400"}`}>{i + 1}</div>
230
+ <span className={`mt-2 text-[8px] sm:text-[10px] font-bold uppercase tracking-wider ${i <= currentStep ? "text-foreground" : "text-slate-400"}`}>{label}</span>
231
+ </div>
232
+ {i < totalSteps - 1 && <div className={`w-8 sm:w-12 h-0.5 mt-[-16px] sm:mt-[-20px] ${i < currentStep ? "bg-secondary" : "bg-slate-200"}`} />}
233
+ </React.Fragment>
234
+ ))}
235
+ </div>
236
+ );
237
+
238
+ /** --- Main ICA Simulator --- */
239
+ const App = () => {
240
+ const [step, setStep] = useState(0);
241
+ const [mixingMatrix, setMixingMatrix] = useState({ a11: 0.7, a12: 0.3, a21: 0.4, a22: 0.6 });
242
+ const [isUnmixing, setIsUnmixing] = useState(false);
243
+ const [unmixProgress, setUnmixProgress] = useState(0);
244
+
245
+ const SAMPLES = 200;
246
+ const originalSignals = useMemo(() => ({
247
+ signal1: generateSignal("sine", 1, SAMPLES),
248
+ signal2: generateSignal("square", 2, SAMPLES),
249
+ }), []);
250
+
251
+ const mixedSignals = useMemo(() => {
252
+ const { a11, a12, a21, a22 } = mixingMatrix;
253
+ const mixed1 = originalSignals.signal1.map((s1, i) => a11 * s1 + a12 * originalSignals.signal2[i]);
254
+ const mixed2 = originalSignals.signal1.map((s1, i) => a21 * s1 + a22 * originalSignals.signal2[i]);
255
+ return { mixed1, mixed2 };
256
+ }, [originalSignals, mixingMatrix]);
257
+
258
+ const recoveredSignals = useMemo(() => {
259
+ const { a11, a12, a21, a22 } = mixingMatrix;
260
+ const det = a11 * a22 - a12 * a21;
261
+ if (Math.abs(det) < 0.01) return { recovered1: mixedSignals.mixed1, recovered2: mixedSignals.mixed2 };
262
+ const invA11 = a22 / det; const invA12 = -a12 / det; const invA21 = -a21 / det; const invA22 = a11 / det;
263
+ const progress = unmixProgress / 100;
264
+ const recovered1 = mixedSignals.mixed1.map((m1, i) => {
265
+ const target = invA11 * m1 + invA12 * mixedSignals.mixed2[i];
266
+ return m1 + (target - m1) * progress;
267
+ });
268
+ const recovered2 = mixedSignals.mixed1.map((m1, i) => {
269
+ const target = invA21 * m1 + invA22 * mixedSignals.mixed2[i];
270
+ return mixedSignals.mixed2[i] + (target - mixedSignals.mixed2[i]) * progress;
271
+ });
272
+ return { recovered1, recovered2 };
273
+ }, [mixedSignals, mixingMatrix, unmixProgress]);
274
+
275
+ const runICA = useCallback(() => {
276
+ setIsUnmixing(true); setUnmixProgress(0);
277
+ const interval = setInterval(() => {
278
+ setUnmixProgress(prev => {
279
+ if (prev >= 100) { clearInterval(interval); setIsUnmixing(false); return 100; }
280
+ return prev + 2;
281
+ });
282
+ }, 30);
283
+ }, []);
284
+
285
+ const reset = () => { setStep(0); setUnmixProgress(0); setIsUnmixing(false); setMixingMatrix({ a11: 0.7, a12: 0.3, a21: 0.4, a22: 0.6 }); };
286
+
287
+ const signalColors = { signal1: "hsl(12 85% 62%)", signal2: "hsl(175 65% 45%)", mixed: "hsl(280 60% 55%)" };
288
+
289
+ return (
290
+ <div className="min-h-screen bg-background pb-12">
291
+ <header className="relative overflow-hidden pt-12 pb-6 sm:pt-16 sm:pb-8">
292
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-accent/5 to-secondary/5" />
293
+ <div className="max-w-4xl mx-auto px-6 relative text-center">
294
+ <div className="inline-flex items-center gap-2 bg-primary/10 text-primary px-4 py-2 rounded-full text-[10px] sm:text-xs font-bold uppercase tracking-wider mb-6 animate-fade-in">
295
+ <Icons.Sparkles className="w-3 h-3 sm:w-4 sm:h-4" /> Interactive Lab
296
+ </div>
297
+ <h1 className="text-3xl md:text-6xl font-bold tracking-tight text-foreground mb-4 animate-fade-in">Understanding <span className="text-gradient">ICA</span></h1>
298
+ <p className="text-muted-foreground text-sm sm:text-lg max-w-2xl mx-auto animate-fade-in leading-relaxed">Independent Component Analysis made simple. Learn to untangle mixed signals like conversations at a party!</p>
299
+ </div>
300
+ </header>
301
+
302
+ <main className="max-w-6xl mx-auto px-4 sm:px-6">
303
+ <StepIndicator currentStep={step} totalSteps={4} labels={["Sources", "Mix", "Observe", "Separate"]} />
304
+
305
+ {step === 0 && (
306
+ <div className="animate-fade-in space-y-6">
307
+ <div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft">
308
+ <div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-accent/10 flex items-center justify-center shrink-0"><Icons.Lightbulb className="w-6 h-6 sm:w-8 sm:h-8 text-accent" /></div>
309
+ <div><h2 className="text-xl sm:text-2xl font-bold mb-2">Step 1: The Original Sources</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">Imagine two people talking. Each voice is an <strong>independent source</strong>. In real-world data science, these could be audio recordings, brain waves, or even economic trends.</p></div>
310
+ </div>
311
+ <div className="grid md:grid-cols-2 gap-4 sm:gap-6">
312
+ <SignalCard title="Voice 1 (Sine Wave)" signals={[originalSignals.signal1]} colors={[signalColors.signal1]} icon={<Icons.Music className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Source A" />
313
+ <SignalCard title="Voice 2 (Square Wave)" signals={[originalSignals.signal2]} colors={[signalColors.signal2]} icon={<Icons.Radio className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Source B" />
314
+ </div>
315
+ <div className="flex justify-center"><Button size="lg" onClick={() => setStep(1)} className="gap-2">Next: Mix the Signals <Icons.ArrowRight className="w-4 h-4"/></Button></div>
316
+ </div>
317
+ )}
318
+
319
+ {step === 1 && (
320
+ <div className="animate-fade-in space-y-6">
321
+ <div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft">
322
+ <div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-secondary/10 flex items-center justify-center shrink-0"><Icons.PartyPopper className="w-6 h-6 sm:w-8 sm:h-8 text-secondary" /></div>
323
+ <div><h2 className="text-xl sm:text-2xl font-bold mb-2">Step 2: Mixing the Signals</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">At a party, microphones don't record one voice perfectly. They pick up a <strong>blend</strong> of everyone. Adjust the matrix sliders below to change how the signals are mixed!</p></div>
324
+ </div>
325
+ <div className="grid lg:grid-cols-2 gap-6">
326
+ <div className="glass-card rounded-2xl p-6 sm:p-8 space-y-6">
327
+ <h3 className="font-bold text-base sm:text-lg">Mixing Parameters</h3>
328
+ <div className="space-y-4 pt-4 border-t border-slate-100">
329
+ <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Mic 1 Weights</p>
330
+ <MixingSlider label="Voice A" value={mixingMatrix.a11} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a11: v })} color={signalColors.signal1} />
331
+ <MixingSlider label="Voice B" value={mixingMatrix.a12} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a12: v })} color={signalColors.signal2} />
332
+ </div>
333
+ <div className="space-y-4 pt-4 border-t border-slate-100">
334
+ <p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Mic 2 Weights</p>
335
+ <MixingSlider label="Voice A" value={mixingMatrix.a21} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a21: v })} color={signalColors.signal1} />
336
+ <MixingSlider label="Voice B" value={mixingMatrix.a22} onChange={(v) => setMixingMatrix({ ...mixingMatrix, a22: v })} color={signalColors.signal2} />
337
+ </div>
338
+ </div>
339
+ <div className="space-y-4">
340
+ <SignalCard title="Microphone 1 Input" signals={[mixedSignals.mixed1]} colors={[signalColors.mixed]} icon={<Icons.Zap className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Mixed" />
341
+ <SignalCard title="Microphone 2 Input" signals={[mixedSignals.mixed2]} colors={[signalColors.mixed]} icon={<Icons.Zap className="w-4 h-4 sm:w-5 sm:h-5"/>} badge="Mixed" />
342
+ </div>
343
+ </div>
344
+ <div className="flex flex-col sm:flex-row justify-center gap-4">
345
+ <Button variant="outline" onClick={() => setStep(0)}>Back</Button>
346
+ <Button size="lg" onClick={() => setStep(2)}>Next: Observe Challenge <Icons.ArrowRight className="ml-2 w-4 h-4"/></Button>
347
+ </div>
348
+ </div>
349
+ )}
350
+
351
+ {step === 2 && (
352
+ <div className="animate-fade-in space-y-6">
353
+ <div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft border-destructive/20">
354
+ <div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-destructive/10 flex items-center justify-center shrink-0"><Icons.Zap className="w-6 h-6 sm:w-8 sm:h-8 text-destructive" /></div>
355
+ <div><h2 className="text-xl sm:text-2xl font-bold mb-2 text-destructive">Step 3: The ICA Challenge</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">We only have the <strong>mixed recordings</strong>. We don't know the original voices or how they were combined. This is a classic Blind Source Separation problem.</p></div>
356
+ </div>
357
+ <div className="glass-card rounded-2xl p-4 sm:p-8 space-y-6">
358
+ <h3 className="font-bold text-lg text-center">Visualizing the Entanglement</h3>
359
+ <div className="bg-white rounded-xl p-3 sm:p-6 border border-slate-100 overflow-hidden">
360
+ <WaveCanvas
361
+ signals={[originalSignals.signal1, originalSignals.signal2, mixedSignals.mixed1, mixedSignals.mixed2]}
362
+ colors={[signalColors.signal1, signalColors.signal2, signalColors.mixed, "hsl(280 40% 45%)"]}
363
+ width={600}
364
+ height={200}
365
+ />
366
+ </div>
367
+ <div className="flex flex-wrap justify-center gap-4 sm:gap-6 text-[8px] sm:text-[10px] font-bold uppercase tracking-widest text-slate-400 pt-2 sm:pt-4">
368
+ <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.signal1 }} /> Original A</div>
369
+ <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.signal2 }} /> Original B</div>
370
+ <div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{ backgroundColor: signalColors.mixed }} /> Mixed Result</div>
371
+ </div>
372
+ </div>
373
+ <div className="flex flex-col sm:flex-row justify-center gap-4 px-4">
374
+ <Button variant="outline" onClick={() => setStep(1)} className="w-full sm:w-auto">Back</Button>
375
+ <Button
376
+ size="lg"
377
+ onClick={() => setStep(3)}
378
+ className="w-full sm:w-auto gap-2 text-sm sm:text-lg"
379
+ >
380
+ Next: Separate with ICA <Icons.ArrowRight className="w-4 h-4"/>
381
+ </Button>
382
+ </div>
383
+ </div>
384
+ )}
385
+
386
+ {step === 3 && (
387
+ <div className="animate-fade-in space-y-6">
388
+ <div className="glass-card rounded-2xl p-6 sm:p-8 flex flex-col md:flex-row gap-6 items-center shadow-soft border-secondary/20">
389
+ <div className="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-secondary/10 flex items-center justify-center shrink-0 animate-float"><Icons.Sparkles className="w-6 h-6 sm:w-8 sm:h-8 text-secondary" /></div>
390
+ <div><h2 className="text-xl sm:text-2xl font-bold mb-2 text-secondary">Step 4: ICA Magic</h2><p className="text-sm sm:text-base text-muted-foreground leading-relaxed">ICA assumes the sources are independent and non-Gaussian. By maximizing statistical independence, it finds the inverse mixing matrix automatically.</p></div>
391
+ </div>
392
+ <div className="grid lg:grid-cols-2 gap-6 sm:gap-8">
393
+ <div className="space-y-4">
394
+ <h3 className="font-bold text-center text-slate-400 text-sm sm:text-base">Input: Mixed recordings</h3>
395
+ <SignalCard title="Recorded Mic 1" signals={[mixedSignals.mixed1]} colors={[signalColors.mixed]} />
396
+ <SignalCard title="Recorded Mic 2" signals={[mixedSignals.mixed2]} colors={[signalColors.mixed]} />
397
+ </div>
398
+ <div className="space-y-4">
399
+ <h3 className="font-bold text-center text-secondary text-sm sm:text-base">Output: Recovered Sources</h3>
400
+ <SignalCard title="Recovered A" signals={[recoveredSignals.recovered1]} colors={[signalColors.signal1]} badge={unmixProgress === 100 ? "Success" : `${unmixProgress}%`} badgeColor="bg-secondary" />
401
+ <SignalCard title="Recovered B" signals={[recoveredSignals.recovered2]} colors={[signalColors.signal2]} badge={unmixProgress === 100 ? "Success" : `${unmixProgress}%`} badgeColor="bg-secondary" />
402
+ </div>
403
+ </div>
404
+ <div className="flex flex-col items-center space-y-6">
405
+ {unmixProgress === 0 && <Button size="lg" onClick={runICA} className="bg-secondary hover:bg-secondary/90 gap-2"><Icons.Play className="w-4 h-4" /> Run ICA Algorithm</Button>}
406
+ {isUnmixing && <div className="w-full max-w-md px-4"><div className="h-2 bg-slate-100 rounded-full overflow-hidden"><div className="h-full bg-secondary transition-all" style={{ width: `${unmixProgress}%` }} /></div><p className="text-center text-[10px] font-bold mt-2 text-secondary uppercase tracking-widest">Optimizing Independence...</p></div>}
407
+ {unmixProgress === 100 && <div className="text-center space-y-4 animate-scale-in"><div className="bg-secondary/10 text-secondary px-6 sm:px-8 py-3 sm:py-4 rounded-2xl border border-secondary/20 inline-block font-bold text-sm sm:text-base">🎉 Signals successfully separated!</div></div>}
408
+ </div>
409
+ <div className="flex flex-col sm:flex-row justify-center gap-4 px-4 mt-6">
410
+ <Button variant="outline" onClick={() => setStep(2)} className="w-full sm:w-auto">Back</Button>
411
+ <Button variant="outline" onClick={reset} className="w-full sm:w-auto"><Icons.RotateCcw className="w-4 h-4 mr-2" /> Start Over</Button>
412
+ </div>
413
+ </div>
414
+ )}
415
+ </main>
416
+
417
+ {/* Info Footer Section */}
418
+ <section className="mt-12 sm:mt-16 max-w-3xl mx-auto px-4">
419
+ <div className="glass-card p-6 sm:p-8 bg-gradient-to-br from-primary/5 to-accent/5 border-primary/10 rounded-3xl">
420
+ <h2 className="text-xl sm:text-2xl font-bold text-foreground mb-6 text-center">How does ICA work? 🧠</h2>
421
+ <div className="grid md:grid-cols-3 gap-8 text-center">
422
+ <div>
423
+ <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-primary/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">1️⃣</div>
424
+ <h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Assumption</h3>
425
+ <p className="text-xs sm:text-sm text-muted-foreground">Original sources are statistically independent.</p>
426
+ </div>
427
+ <div>
428
+ <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-accent/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">2️⃣</div>
429
+ <h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Goal</h3>
430
+ <p className="text-xs sm:text-sm text-muted-foreground">Find a transformation that maximizes output independence.</p>
431
+ </div>
432
+ <div>
433
+ <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-secondary/10 flex items-center justify-center mx-auto mb-3 text-lg sm:text-xl">3️⃣</div>
434
+ <h3 className="font-semibold text-foreground mb-1 text-sm sm:text-base">Result</h3>
435
+ <p className="text-xs sm:text-sm text-muted-foreground">Recover original hidden sources from mixed records.</p>
436
+ </div>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="absolute left-1/2 -translate-x-1/2 flex items-center">
441
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
442
+ <a href="/ica" 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">
443
+ Back to Core
444
+ </a>
445
+ </div>
446
+ </section>
447
+ </div>
448
+ );
449
+ };
450
+
451
+ const root = createRoot(document.getElementById('root'));
452
+ root.render(<App />);
453
+ </script>
454
+ {% endraw %}
455
+ </body>
456
+ </html>
templates/lda-three.html ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>LDA Simulator</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ border: "hsl(var(--border))",
16
+ input: "hsl(var(--input))",
17
+ ring: "hsl(var(--ring))",
18
+ background: "hsl(var(--background))",
19
+ foreground: "hsl(var(--foreground))",
20
+ primary: {
21
+ DEFAULT: "hsl(var(--primary))",
22
+ foreground: "hsl(var(--primary-foreground))",
23
+ },
24
+ secondary: {
25
+ DEFAULT: "hsl(var(--secondary))",
26
+ foreground: "hsl(var(--secondary-foreground))",
27
+ },
28
+ destructive: {
29
+ DEFAULT: "hsl(var(--destructive))",
30
+ foreground: "hsl(var(--destructive-foreground))",
31
+ },
32
+ muted: {
33
+ DEFAULT: "hsl(var(--muted))",
34
+ foreground: "hsl(var(--muted-foreground))",
35
+ },
36
+ accent: {
37
+ DEFAULT: "hsl(var(--accent))",
38
+ foreground: "hsl(var(--accent-foreground))",
39
+ },
40
+ card: {
41
+ DEFAULT: "hsl(var(--card))",
42
+ foreground: "hsl(var(--card-foreground))",
43
+ },
44
+ // Custom LDA Class Colors
45
+ 'class-a': '#06b6d4', // Cyan-500
46
+ 'class-b': '#d946ef', // Fuchsia-500
47
+ 'centroid': '#f59e0b', // Amber-500
48
+ },
49
+ animation: {
50
+ 'fade-in': 'fadeIn 0.5s ease-out',
51
+ },
52
+ keyframes: {
53
+ fadeIn: {
54
+ '0%': { opacity: '0', transform: 'translateY(10px)' },
55
+ '100%': { opacity: '1', transform: 'translateY(0)' },
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ </script>
62
+
63
+ <!-- React & ReactDOM -->
64
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
65
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
66
+
67
+ <!-- Babel for JSX -->
68
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
69
+
70
+ <style>
71
+ :root {
72
+ --background: 222.2 84% 4.9%;
73
+ --foreground: 210 40% 98%;
74
+ --card: 222.2 84% 4.9%;
75
+ --card-foreground: 210 40% 98%;
76
+ --popover: 222.2 84% 4.9%;
77
+ --popover-foreground: 210 40% 98%;
78
+ --primary: 217.2 91.2% 59.8%;
79
+ --primary-foreground: 222.2 47.4% 11.2%;
80
+ --secondary: 217.2 32.6% 17.5%;
81
+ --secondary-foreground: 210 40% 98%;
82
+ --muted: 217.2 32.6% 17.5%;
83
+ --muted-foreground: 215 20.2% 65.1%;
84
+ --accent: 217.2 32.6% 17.5%;
85
+ --accent-foreground: 210 40% 98%;
86
+ --destructive: 0 62.8% 30.6%;
87
+ --destructive-foreground: 210 40% 98%;
88
+ --border: 217.2 32.6% 17.5%;
89
+ --input: 217.2 32.6% 17.5%;
90
+ --ring: 212.7 26.8% 83.9%;
91
+ }
92
+
93
+ body {
94
+ background-color: hsl(222.2, 84%, 4.9%);
95
+ color: hsl(210, 40%, 98%);
96
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
97
+ }
98
+
99
+ .glow-primary { box-shadow: 0 0 15px hsl(217.2, 91.2%, 59.8%, 0.3); }
100
+ .glow-accent { box-shadow: 0 0 15px hsl(189, 94%, 43%, 0.3); }
101
+ .glow-class-b { box-shadow: 0 0 15px hsl(300, 76%, 60%, 0.3); }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div id="root"></div>
106
+
107
+ <script type="text/babel">
108
+ const { useState, useEffect, useRef, useCallback, useMemo } = React;
109
+
110
+ // --- UTILS ---
111
+ function cn(...classes) {
112
+ return classes.filter(Boolean).join(' ');
113
+ }
114
+
115
+ // --- MATH ENGINE (LDA) ---
116
+ // Simplified Linear Discriminant Analysis for 2D data
117
+ const calculateLDA = (points, canvasWidth, canvasHeight) => {
118
+ const classA = points.filter(p => p.classLabel === 'A');
119
+ const classB = points.filter(p => p.classLabel === 'B');
120
+
121
+ if (classA.length < 2 || classB.length < 2) return null;
122
+
123
+ // 1. Calculate Mean Vectors (Centroids)
124
+ const getMean = (pts) => {
125
+ const sum = pts.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
126
+ return { x: sum.x / pts.length, y: sum.y / pts.length };
127
+ };
128
+
129
+ const meanA = getMean(classA);
130
+ const meanB = getMean(classB);
131
+
132
+ // 2. Calculate Within-Class Scatter Matrix (Sw)
133
+ // Sw = sum((x - mu)(x - mu)T) for all classes
134
+ let swxx = 0, swxy = 0, swyy = 0;
135
+
136
+ const addToScatter = (pts, mean) => {
137
+ pts.forEach(p => {
138
+ const dx = p.x - mean.x;
139
+ const dy = p.y - mean.y;
140
+ swxx += dx * dx;
141
+ swxy += dx * dy;
142
+ swyy += dy * dy;
143
+ });
144
+ };
145
+
146
+ addToScatter(classA, meanA);
147
+ addToScatter(classB, meanB);
148
+
149
+ // 3. Calculate Inverse of Sw (Sw^-1)
150
+ const det = swxx * swyy - swxy * swxy;
151
+ if (Math.abs(det) < 1e-10) return null; // Singular matrix
152
+
153
+ const invSwxx = swyy / det;
154
+ const invSwxy = -swxy / det;
155
+ const invSwyy = swxx / det;
156
+
157
+ // 4. Calculate Vector w = Sw^-1 * (meanB - meanA)
158
+ // We want direction that separates means
159
+ const diffMeanX = meanB.x - meanA.x;
160
+ const diffMeanY = meanB.y - meanA.y;
161
+
162
+ const wx = invSwxx * diffMeanX + invSwxy * diffMeanY;
163
+ const wy = invSwxy * diffMeanX + invSwyy * diffMeanY;
164
+
165
+ // Normalize w
166
+ const mag = Math.sqrt(wx * wx + wy * wy);
167
+ const w = { x: wx / mag, y: wy / mag };
168
+
169
+ // 5. Project Points onto Line defined by w passing through Global Mean
170
+ const allPoints = [...classA, ...classB];
171
+ const globalMean = getMean(allPoints);
172
+
173
+ const projectedPoints = allPoints.map(p => {
174
+ // Vector from mean to point
175
+ const vmx = p.x - globalMean.x;
176
+ const vmy = p.y - globalMean.y;
177
+
178
+ // Scalar projection length
179
+ const dot = vmx * w.x + vmy * w.y;
180
+
181
+ // Projected point coordinates
182
+ return {
183
+ x: globalMean.x + dot * w.x,
184
+ y: globalMean.y + dot * w.y,
185
+ classLabel: p.classLabel
186
+ };
187
+ });
188
+
189
+ return {
190
+ projectionVector: w,
191
+ centroidA: meanA,
192
+ centroidB: meanB,
193
+ projectedPoints
194
+ };
195
+ };
196
+
197
+
198
+ // --- UI COMPONENTS ---
199
+
200
+ const Button = ({ children, className, variant = "default", ...props }) => {
201
+ const base = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2";
202
+ const variants = {
203
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
204
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
205
+ ghost: "hover:bg-accent hover:text-accent-foreground"
206
+ };
207
+ return (
208
+ <button className={cn(base, variants[variant], className)} {...props}>
209
+ {children}
210
+ </button>
211
+ );
212
+ };
213
+
214
+ const Toast = ({ message, type, onClose }) => {
215
+ useEffect(() => {
216
+ const timer = setTimeout(onClose, 3000);
217
+ return () => clearTimeout(timer);
218
+ }, [onClose]);
219
+
220
+ return (
221
+ <div className={cn(
222
+ "fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white font-medium animate-fade-in z-50",
223
+ type === 'error' ? "bg-red-500" : "bg-emerald-600"
224
+ )}>
225
+ {message}
226
+ </div>
227
+ );
228
+ };
229
+
230
+ // --- ICONS ---
231
+ const IconWrapper = ({ children, className }) => (
232
+ <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}>
233
+ {children}
234
+ </svg>
235
+ );
236
+
237
+ const Icons = {
238
+ CircleDot: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></IconWrapper>,
239
+ Trash2: (props) => <IconWrapper {...props}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></IconWrapper>,
240
+ Play: (props) => <IconWrapper {...props}><polygon points="5 3 19 12 5 21 5 3"/></IconWrapper>,
241
+ RotateCcw: (props) => <IconWrapper {...props}><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></IconWrapper>,
242
+ Sparkles: (props) => <IconWrapper {...props}><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M9 3v4"/><path d="M3 9h4"/><path d="M3 5h4"/></IconWrapper>,
243
+ Info: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></IconWrapper>,
244
+ Target: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></IconWrapper>,
245
+ Layers: (props) => <IconWrapper {...props}><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"/></IconWrapper>,
246
+ TrendingUp: (props) => <IconWrapper {...props}><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></IconWrapper>,
247
+ BookOpen: (props) => <IconWrapper {...props}><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></IconWrapper>,
248
+ Check: (props) => <IconWrapper {...props}><polyline points="20 6 9 17 4 12"/></IconWrapper>,
249
+ Circle: (props) => <IconWrapper {...props}><circle cx="12" cy="12" r="10"/></IconWrapper>,
250
+ ArrowRight: (props) => <IconWrapper {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></IconWrapper>,
251
+ };
252
+
253
+ // --- SUB-COMPONENTS ---
254
+
255
+ const LDACanvas = ({ points, onAddPoint, selectedClass, showProjection, ldaResult }) => {
256
+ const canvasRef = useRef(null);
257
+ const containerRef = useRef(null);
258
+ const [canvasSize, setCanvasSize] = useState({ width: 600, height: 400 });
259
+
260
+ // Handle adding points
261
+ const handleCanvasClick = useCallback((e) => {
262
+ const canvas = canvasRef.current;
263
+ if (!canvas) return;
264
+ const rect = canvas.getBoundingClientRect();
265
+ const x = (e.clientX - rect.left) * (canvas.width / rect.width);
266
+ const y = (e.clientY - rect.top) * (canvas.height / rect.height);
267
+ onAddPoint({ x, y, classLabel: selectedClass });
268
+ }, [onAddPoint, selectedClass]);
269
+
270
+ // Resize observer
271
+ useEffect(() => {
272
+ const handleResize = () => {
273
+ const container = containerRef.current;
274
+ if (container) {
275
+ setCanvasSize({
276
+ width: container.clientWidth,
277
+ height: Math.min(400, container.clientWidth * 0.75)
278
+ });
279
+ }
280
+ };
281
+ window.addEventListener('resize', handleResize);
282
+ handleResize();
283
+ return () => window.removeEventListener('resize', handleResize);
284
+ }, []);
285
+
286
+ // Draw Loop
287
+ useEffect(() => {
288
+ const canvas = canvasRef.current;
289
+ if (!canvas) return;
290
+ const ctx = canvas.getContext("2d");
291
+
292
+ // Clear
293
+ ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
294
+
295
+ // Grid
296
+ ctx.strokeStyle = "rgba(100, 116, 139, 0.1)";
297
+ ctx.lineWidth = 1;
298
+ const gridSize = 30;
299
+ for (let x = 0; x < canvasSize.width; x += gridSize) {
300
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvasSize.height); ctx.stroke();
301
+ }
302
+ for (let y = 0; y < canvasSize.height; y += gridSize) {
303
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvasSize.width, y); ctx.stroke();
304
+ }
305
+
306
+ // Projection
307
+ if (showProjection && ldaResult) {
308
+ const { projectionVector, centroidA, centroidB, projectedPoints } = ldaResult;
309
+ const allPts = [...points];
310
+ // Global Mean approx
311
+ const gMean = {
312
+ x: allPts.reduce((s, p) => s + p.x, 0) / allPts.length,
313
+ y: allPts.reduce((s, p) => s + p.y, 0) / allPts.length
314
+ };
315
+
316
+ // Draw Line
317
+ const lineLen = 1000;
318
+ ctx.beginPath();
319
+ ctx.strokeStyle = "hsl(45, 93%, 58%)"; // Amber
320
+ ctx.lineWidth = 2;
321
+ ctx.setLineDash([5, 5]);
322
+ ctx.moveTo(gMean.x - projectionVector.x * lineLen, gMean.y - projectionVector.y * lineLen);
323
+ ctx.lineTo(gMean.x + projectionVector.x * lineLen, gMean.y + projectionVector.y * lineLen);
324
+ ctx.stroke();
325
+ ctx.setLineDash([]);
326
+
327
+ // Draw Projected Points
328
+ projectedPoints.forEach(pp => {
329
+ ctx.beginPath();
330
+ ctx.fillStyle = pp.classLabel === 'A' ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
331
+ ctx.globalAlpha = 0.5;
332
+ ctx.arc(pp.x, pp.y, 4, 0, Math.PI * 2);
333
+ ctx.fill();
334
+
335
+ // Connecting line (optional, purely for visual connection)
336
+ // This would need original point ref, skipping for simplicity
337
+ });
338
+ ctx.globalAlpha = 1;
339
+
340
+ // Centroids
341
+ [centroidA, centroidB].forEach((c, idx) => {
342
+ ctx.beginPath();
343
+ ctx.strokeStyle = idx === 0 ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
344
+ ctx.lineWidth = 2;
345
+ ctx.arc(c.x, c.y, 12, 0, Math.PI * 2);
346
+ ctx.stroke();
347
+ // Cross
348
+ ctx.beginPath();
349
+ ctx.moveTo(c.x - 5, c.y); ctx.lineTo(c.x + 5, c.y);
350
+ ctx.moveTo(c.x, c.y - 5); ctx.lineTo(c.x, c.y + 5);
351
+ ctx.stroke();
352
+ });
353
+ }
354
+
355
+ // Points
356
+ points.forEach(p => {
357
+ ctx.beginPath();
358
+ const color = p.classLabel === 'A' ? "hsl(189, 94%, 43%)" : "hsl(300, 76%, 60%)";
359
+
360
+ // Glow
361
+ const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 15);
362
+ grad.addColorStop(0, color);
363
+ grad.addColorStop(1, "transparent");
364
+ ctx.fillStyle = grad;
365
+ ctx.globalAlpha = 0.3;
366
+ ctx.arc(p.x, p.y, 15, 0, Math.PI * 2);
367
+ ctx.fill();
368
+
369
+ // Core
370
+ ctx.globalAlpha = 1;
371
+ ctx.fillStyle = color;
372
+ ctx.beginPath();
373
+ ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
374
+ ctx.fill();
375
+ ctx.strokeStyle = "white";
376
+ ctx.lineWidth = 1.5;
377
+ ctx.stroke();
378
+ });
379
+
380
+ }, [points, canvasSize, showProjection, ldaResult]);
381
+
382
+ return (
383
+ <div ref={containerRef} className="relative w-full rounded-xl overflow-hidden border border-border bg-card/50 backdrop-blur-sm shadow-sm">
384
+ <canvas
385
+ ref={canvasRef}
386
+ width={canvasSize.width}
387
+ height={canvasSize.height}
388
+ onClick={handleCanvasClick}
389
+ className="cursor-crosshair block w-full h-auto"
390
+ />
391
+ <div className="absolute bottom-3 left-3 flex gap-2 text-[10px] sm:text-xs font-mono">
392
+ <span className="px-2 py-1 rounded bg-secondary/90 text-class-a border border-class-a/20">
393
+ Class A: {points.filter((p) => p.classLabel === "A").length}
394
+ </span>
395
+ <span className="px-2 py-1 rounded bg-secondary/90 text-class-b border border-class-b/20">
396
+ Class B: {points.filter((p) => p.classLabel === "B").length}
397
+ </span>
398
+ </div>
399
+ </div>
400
+ );
401
+ };
402
+
403
+ const ControlPanel = ({ selectedClass, onSelectClass, onClear, onRunLDA, onLoadExample, canRunLDA, showProjection }) => (
404
+ <div className="flex flex-col sm:flex-row flex-wrap gap-3 items-center justify-between p-4 rounded-xl bg-card border border-border shadow-sm">
405
+ <div className="flex gap-2 w-full sm:w-auto justify-center">
406
+ <Button
407
+ variant={selectedClass === "A" ? "default" : "outline"}
408
+ onClick={() => onSelectClass("A")}
409
+ className={`gap-2 flex-1 sm:flex-none ${selectedClass === "A" ? "bg-class-a hover:bg-class-a/90 text-white glow-accent" : "border-class-a/50 text-class-a hover:bg-class-a/10"}`}
410
+ >
411
+ <Icons.CircleDot className="w-4 h-4" /> Class A
412
+ </Button>
413
+ <Button
414
+ variant={selectedClass === "B" ? "default" : "outline"}
415
+ onClick={() => onSelectClass("B")}
416
+ className={`gap-2 flex-1 sm:flex-none ${selectedClass === "B" ? "bg-class-b hover:bg-class-b/90 text-white glow-class-b" : "border-class-b/50 text-class-b hover:bg-class-b/10"}`}
417
+ >
418
+ <Icons.CircleDot className="w-4 h-4" /> Class B
419
+ </Button>
420
+ </div>
421
+
422
+ <div className="flex gap-2 w-full sm:w-auto justify-center">
423
+ <Button variant="outline" onClick={onLoadExample} className="gap-2 text-xs sm:text-sm">
424
+ <Icons.Sparkles className="w-4 h-4" /> Example
425
+ </Button>
426
+ <Button variant="outline" onClick={onClear} className="gap-2 border-destructive/50 text-destructive hover:bg-destructive/10 text-xs sm:text-sm">
427
+ <Icons.Trash2 className="w-4 h-4" /> Clear
428
+ </Button>
429
+ <Button
430
+ onClick={onRunLDA}
431
+ disabled={!canRunLDA}
432
+ className={`gap-2 flex-1 sm:flex-none min-w-[120px] ${showProjection ? "bg-primary/80" : "glow-primary"}`}
433
+ >
434
+ {showProjection ? <><Icons.RotateCcw className="w-4 h-4" /> Reset View</> : <><Icons.Play className="w-4 h-4" /> Run LDA</>}
435
+ </Button>
436
+ </div>
437
+ </div>
438
+ );
439
+
440
+ const StepIndicator = ({ currentStep, showProjection }) => {
441
+ const steps = ["Add A", "Add B", "Run LDA", "Project"];
442
+ const activeStep = showProjection ? 4 : currentStep;
443
+
444
+ return (
445
+ <div className="flex items-center justify-between sm:justify-center gap-2 sm:gap-4 p-4 rounded-xl bg-card/50 border border-border/50 overflow-x-auto">
446
+ {steps.map((label, index) => {
447
+ const stepNum = index + 1;
448
+ const isCompleted = stepNum < activeStep;
449
+ const isActive = stepNum === activeStep;
450
+
451
+ return (
452
+ <div key={index} className="flex items-center gap-2 min-w-fit">
453
+ <div className={cn("w-6 h-6 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs sm:text-sm font-mono transition-all",
454
+ isCompleted ? "bg-accent text-white" : isActive ? "bg-primary text-primary-foreground glow-primary" : "bg-secondary text-muted-foreground"
455
+ )}>
456
+ {isCompleted ? <Icons.Check className="w-3 h-3 sm:w-4 sm:h-4" /> : isActive ? <Icons.CircleDot className="w-3 h-3 sm:w-4 sm:h-4" /> : stepNum}
457
+ </div>
458
+ <span className={cn("text-xs hidden sm:block", isActive ? "text-foreground font-medium" : "text-muted-foreground")}>{label}</span>
459
+ {index < steps.length - 1 && <Icons.ArrowRight className="w-3 h-3 text-muted-foreground/30" />}
460
+ </div>
461
+ )
462
+ })}
463
+ </div>
464
+ );
465
+ };
466
+
467
+ const ExplanationPanel = ({ step, showProjection }) => {
468
+ const currentStep = showProjection ? "result" : step;
469
+ const content = {
470
+ intro: { icon: Icons.Info, title: "Welcome!", desc: "LDA finds the best way to separate groups.", details: ["Click canvas to add points", "Switch classes with buttons"] },
471
+ addPoints: { icon: Icons.Target, title: "Add Points", desc: "Create two groups of data.", details: ["Add at least 3 points per class", "Try overlapping groups to see LDA magic"] },
472
+ centroids: { icon: Icons.Layers, title: "Ready to Run", desc: "We have enough data.", details: ["LDA will calculate centroids (averages)", "It finds a line to separate them"] },
473
+ projection: { icon: Icons.TrendingUp, title: "Projection", desc: "LDA finds the optimal separation line.", details: ["Golden line = Projection direction", "Notice how classes separate on the line"] },
474
+ result: { icon: Icons.TrendingUp, title: "Result", desc: "Data reduced from 2D to 1D.", details: ["Maximizes between-class distance", "Minimizes within-class scatter"] }
475
+ }[currentStep];
476
+
477
+ const Icon = content.icon;
478
+
479
+ return (
480
+ <div className="p-5 rounded-xl bg-card border border-border animate-fade-in shadow-sm">
481
+ <div className="flex items-start gap-3 mb-4">
482
+ <div className="p-2 rounded-lg bg-primary/20 text-primary"><Icon className="w-5 h-5" /></div>
483
+ <div>
484
+ <h3 className="font-semibold text-lg text-foreground">{content.title}</h3>
485
+ <p className="text-muted-foreground text-sm mt-1">{content.desc}</p>
486
+ </div>
487
+ </div>
488
+ <ul className="space-y-2 ml-10">
489
+ {content.details.map((d, i) => (
490
+ <li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
491
+ <span className="text-primary mt-1">•</span> {d}
492
+ </li>
493
+ ))}
494
+ </ul>
495
+ </div>
496
+ );
497
+ };
498
+
499
+ const FormulaCard = ({ showProjection }) => (
500
+ <div className="p-5 rounded-xl bg-card border border-border shadow-sm mt-4 lg:mt-0">
501
+ <div className="flex items-center gap-2 mb-4">
502
+ <Icons.BookOpen className="w-5 h-5 text-primary" />
503
+ <h3 className="font-semibold text-foreground">The Math</h3>
504
+ </div>
505
+ <div className="space-y-4 text-sm">
506
+ <div className="p-3 rounded-lg bg-secondary/50 font-mono text-center text-xs sm:text-base">
507
+ <span className="text-primary">w</span>
508
+ <span className="text-muted-foreground"> = </span>
509
+ <span className="text-accent">S<sub>w</sub><sup>-1</sup></span>
510
+ <span className="text-muted-foreground"> (</span>
511
+ <span className="text-class-b">μ<sub>B</sub></span>
512
+ <span className="text-muted-foreground"> - </span>
513
+ <span className="text-class-a">μ<sub>A</sub></span>
514
+ <span className="text-muted-foreground">)</span>
515
+ </div>
516
+ <p className="text-xs text-muted-foreground text-center">
517
+ We inverse the Scatter Matrix (<span className="text-accent">S<sub>w</sub></span>) and multiply by the difference of Means.
518
+ </p>
519
+ </div>
520
+ </div>
521
+ );
522
+
523
+ const TheorySection = () => (
524
+ <div className="mt-8 grid md:grid-cols-2 gap-6 animate-fade-in">
525
+ <div className="p-6 rounded-xl bg-card border border-border">
526
+ <div className="flex items-center gap-2 mb-4">
527
+ <Icons.BookOpen className="w-5 h-5 text-primary" />
528
+ <h2 className="text-xl font-bold">The Intuition</h2>
529
+ </div>
530
+ <p className="text-muted-foreground mb-4">
531
+ Imagine you have two bags of mixed candy (classes) spilled on a table. You want to take a photo (project to 2D -> 1D) such that the two types of candy look as separate as possible in the picture.
532
+ </p>
533
+ <p className="text-muted-foreground">
534
+ LDA tries to find the camera angle that:
535
+ </p>
536
+ <ul className="list-disc ml-5 mt-2 space-y-1 text-muted-foreground">
537
+ <li>Keeps candies of the same type close together (<strong>Minimize Within-Class Variance</strong>)</li>
538
+ <li>Pushes the centers of the two groups far apart (<strong>Maximize Between-Class Variance</strong>)</li>
539
+ </ul>
540
+ </div>
541
+
542
+ <div className="p-6 rounded-xl bg-card border border-border">
543
+ <div className="flex items-center gap-2 mb-4">
544
+ <Icons.TrendingUp className="w-5 h-5 text-accent" />
545
+ <h2 className="text-xl font-bold">Fisher's Criterion</h2>
546
+ </div>
547
+ <p className="text-muted-foreground mb-4">
548
+ Mathematically, LDA solves for a vector <span className="font-mono text-primary">w</span> that maximizes the ratio <span className="font-mono">J(w)</span>:
549
+ </p>
550
+ <div className="p-4 bg-secondary/30 rounded-lg text-center font-mono my-4">
551
+ J(w) = <span className="text-accent">Between-Class Variance</span> / <span className="text-primary">Within-Class Variance</span>
552
+ </div>
553
+ <p className="text-sm text-muted-foreground">
554
+ If we just maximized the distance between means, we might pick an angle where the classes are spread out and overlap. By dividing by the variance (spread), we ensure the clusters are tight <i>and</i> separated.
555
+ </p>
556
+ </div>
557
+ </div>
558
+ );
559
+
560
+ const App = () => {
561
+ const [points, setPoints] = useState([]);
562
+ const [selectedClass, setSelectedClass] = useState("A");
563
+ const [showProjection, setShowProjection] = useState(false);
564
+ const [ldaResult, setLdaResult] = useState(null);
565
+ const [toastMsg, setToastMsg] = useState(null);
566
+
567
+ const showToast = (msg, type = 'info') => setToastMsg({ msg, type, id: Date.now() });
568
+
569
+ // Example Data
570
+ const handleLoadExample = () => {
571
+ setPoints([
572
+ { x: 120, y: 150, classLabel: "A" }, { x: 150, y: 180, classLabel: "A" },
573
+ { x: 130, y: 200, classLabel: "A" }, { x: 160, y: 160, classLabel: "A" },
574
+ { x: 140, y: 220, classLabel: "A" }, { x: 180, y: 190, classLabel: "A" },
575
+ { x: 450, y: 200, classLabel: "B" }, { x: 480, y: 230, classLabel: "B" },
576
+ { x: 460, y: 260, classLabel: "B" }, { x: 490, y: 180, classLabel: "B" },
577
+ { x: 520, y: 220, classLabel: "B" }, { x: 470, y: 280, classLabel: "B" },
578
+ ]);
579
+ setShowProjection(false);
580
+ setLdaResult(null);
581
+ showToast("Example data loaded!");
582
+ };
583
+
584
+ const handleAddPoint = (point) => {
585
+ if (showProjection) return;
586
+ setPoints(prev => [...prev, point]);
587
+ };
588
+
589
+ const handleClear = () => {
590
+ setPoints([]);
591
+ setShowProjection(false);
592
+ setLdaResult(null);
593
+ showToast("Canvas cleared");
594
+ };
595
+
596
+ const handleRunLDA = () => {
597
+ if (showProjection) {
598
+ setShowProjection(false);
599
+ setLdaResult(null);
600
+ return;
601
+ }
602
+ const result = calculateLDA(points);
603
+ if (result) {
604
+ setLdaResult(result);
605
+ setShowProjection(true);
606
+ showToast("LDA Projection calculated!", "success");
607
+ } else {
608
+ showToast("Need more points!", "error");
609
+ }
610
+ };
611
+
612
+ const classACount = points.filter(p => p.classLabel === 'A').length;
613
+ const classBCount = points.filter(p => p.classLabel === 'B').length;
614
+ const canRunLDA = classACount >= 2 && classBCount >= 2;
615
+
616
+ const getCurrentStep = () => {
617
+ if (classACount === 0) return 1;
618
+ if (classBCount === 0) return 2;
619
+ if (!showProjection) return 3;
620
+ return 4;
621
+ };
622
+
623
+ const getExplanationStep = () => {
624
+ if (points.length === 0) return "intro";
625
+ if (classACount < 2 || classBCount < 2) return "addPoints";
626
+ if (!showProjection) return "centroids";
627
+ return "projection";
628
+ };
629
+
630
+ return (
631
+ <div className="min-h-screen pb-10">
632
+ <header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
633
+ <div className="container mx-auto px-4 py-4 flex items-center justify-between">
634
+ <div>
635
+ <h1 className="text-xl sm:text-2xl font-bold text-foreground">
636
+ LDA <span className="text-primary">Simulator</span>
637
+ </h1>
638
+ <p className="text-xs sm:text-sm text-muted-foreground hidden sm:block">Linear Discriminant Analysis — Interactive Learning</p>
639
+ </div>
640
+ </div>
641
+ </header>
642
+
643
+ <div class="absolute left-1/2 -translate-x-1/2 flex items-center">
644
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
645
+ <a href="/lda" 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">
646
+ Back to Core
647
+ </a>
648
+ </div>
649
+ <main className="container mx-auto px-4 py-6 space-y-6">
650
+ <StepIndicator currentStep={getCurrentStep()} showProjection={showProjection} />
651
+
652
+ <div className="grid lg:grid-cols-3 gap-6">
653
+ <div className="lg:col-span-2 space-y-4">
654
+ <ControlPanel
655
+ selectedClass={selectedClass} onSelectClass={setSelectedClass}
656
+ onClear={handleClear} onRunLDA={handleRunLDA} onLoadExample={handleLoadExample}
657
+ canRunLDA={canRunLDA} showProjection={showProjection}
658
+ />
659
+ <LDACanvas
660
+ points={points} onAddPoint={handleAddPoint}
661
+ selectedClass={selectedClass} showProjection={showProjection} ldaResult={ldaResult}
662
+ />
663
+ <div className="flex flex-wrap gap-2 text-[10px] sm:text-xs">
664
+ <span className="px-3 py-1.5 rounded-full bg-class-a/20 text-class-a border border-class-a/30">● Class A (Cyan)</span>
665
+ <span className="px-3 py-1.5 rounded-full bg-class-b/20 text-class-b border border-class-b/30">● Class B (Magenta)</span>
666
+ {showProjection && <span className="px-3 py-1.5 rounded-full bg-primary/20 text-primary border border-primary/30">─ ─ Projection</span>}
667
+ </div>
668
+ </div>
669
+
670
+ <div className="space-y-4">
671
+ <ExplanationPanel step={getExplanationStep()} showProjection={showProjection} />
672
+ <FormulaCard showProjection={showProjection} />
673
+ </div>
674
+ </div>
675
+
676
+ <TheorySection />
677
+ </main>
678
+
679
+ {toastMsg && <Toast message={toastMsg.msg} type={toastMsg.type} onClose={() => setToastMsg(null)} />}
680
+ </div>
681
+ );
682
+ };
683
+
684
+ const root = ReactDOM.createRoot(document.getElementById('root'));
685
+ root.render(<App />);
686
+ </script>
687
+ </body>
688
+ </html>
templates/pca-threejs.html ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>PCA Playground - Interactive Tool</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=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
16
+
17
+ <!-- Import Map -->
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
+ }
24
+ }
25
+ </script>
26
+
27
+ <style>
28
+ :root {
29
+ --background: 40 30% 98%;
30
+ --foreground: 220 20% 20%;
31
+ --card: 0 0% 100%;
32
+ --card-foreground: 220 20% 20%;
33
+ --popover: 0 0% 100%;
34
+ --popover-foreground: 220 20% 20%;
35
+ --primary: 12 80% 60%;
36
+ --primary-foreground: 0 0% 100%;
37
+ --secondary: 180 50% 45%;
38
+ --secondary-foreground: 0 0% 100%;
39
+ --muted: 40 20% 92%;
40
+ --muted-foreground: 220 10% 45%;
41
+ --accent: 260 60% 60%;
42
+ --accent-foreground: 0 0% 100%;
43
+ --destructive: 0 84.2% 60.2%;
44
+ --destructive-foreground: 210 40% 98%;
45
+ --border: 40 20% 88%;
46
+ --input: 40 20% 88%;
47
+ --ring: 12 80% 60%;
48
+ --radius: 1rem;
49
+
50
+ --pc1-color: 180 70% 45%;
51
+ --pc2-color: 260 60% 60%;
52
+ --point-color: 12 80% 60%;
53
+ --grid-color: 220 20% 90%;
54
+
55
+ --gradient-warm: linear-gradient(135deg, hsl(12 80% 60%) 0%, hsl(30 90% 65%) 100%);
56
+ --gradient-cool: linear-gradient(135deg, hsl(180 50% 45%) 0%, hsl(200 60% 50%) 100%);
57
+ --gradient-accent: linear-gradient(135deg, hsl(260 60% 60%) 0%, hsl(280 70% 65%) 100%);
58
+ --gradient-bg: linear-gradient(180deg, hsl(40 30% 98%) 0%, hsl(40 25% 95%) 100%);
59
+
60
+ --shadow-soft: 0 4px 20px -4px hsl(220 20% 20% / 0.1);
61
+ --shadow-glow: 0 0 30px hsl(12 80% 60% / 0.2);
62
+ }
63
+
64
+ body {
65
+ background-color: hsl(var(--background));
66
+ color: hsl(var(--foreground));
67
+ font-family: 'Space Grotesk', sans-serif;
68
+ margin: 0;
69
+ overflow-x: hidden;
70
+ -webkit-tap-highlight-color: transparent;
71
+ }
72
+
73
+ code, .mono { font-family: 'JetBrains Mono', monospace; }
74
+ .shadow-soft { box-shadow: var(--shadow-soft); }
75
+ .text-pc1 { color: hsl(var(--pc1-color)); }
76
+ .bg-pc1 { background-color: hsl(var(--pc1-color)); }
77
+ .text-pc2 { color: hsl(var(--pc2-color)); }
78
+ .bg-pc2 { background-color: hsl(var(--pc2-color)); }
79
+
80
+ @keyframes fadeIn {
81
+ from { opacity: 0; transform: translateY(10px); }
82
+ to { opacity: 1; transform: translateY(0); }
83
+ }
84
+ .animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
85
+
86
+ canvas { touch-action: none; }
87
+
88
+ .glass-btn {
89
+ background: white;
90
+ border: 1px solid hsl(var(--border));
91
+ transition: all 0.2s;
92
+ }
93
+ .glass-btn:hover { background: hsl(var(--muted)); }
94
+
95
+ .glass-btn-active {
96
+ background: hsl(var(--primary));
97
+ color: white;
98
+ border-color: hsl(var(--primary));
99
+ }
100
+ </style>
101
+ </head>
102
+ <body>
103
+ <div id="root"></div>
104
+
105
+ <script type="text/babel" data-type="module">
106
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
107
+ import { createRoot } from 'react-dom/client';
108
+
109
+ /** --- Utils: Icons --- */
110
+ const Icons = {
111
+ Trash: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2m-6 5v6m4-6v6"/></svg>,
112
+ Eye: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><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>,
113
+ EyeOff: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24M10.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.68M6.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" y1="2" x2="22" y2="22"/></svg>,
114
+ Tags: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 5H2v7l6.29 6.29c.94.94 2.48.94 3.42 0l3.58-3.58M13 13l3.58 3.58a2.41 2.41 0 0 0 3.42 0l3.58-3.58a2.41 2.41 0 0 0 0-3.42L13 2H6"/></svg>,
115
+ Tag: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>,
116
+ Sparkles: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/></svg>,
117
+ ChevronDown: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>,
118
+ ChevronUp: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg>,
119
+ Camera: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="13" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><circle cx="12" cy="13" r="3"/></svg>,
120
+ Chart: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>,
121
+ Dna: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m8 18.8 8.9-8.9M12 12l3.5-3.5M15.5 15.4 12 12M19 11.9l-3.5-3.5M19 19 10.1 10.1M5 12.1 8.4 8.7M8.5 15.4 12 12M5 5l8.9 8.9"/></svg>,
122
+ Music: (p) => <svg {...p} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>,
123
+ };
124
+
125
+ /** --- Math Logic --- */
126
+ const calculatePCA = (points) => {
127
+ if (points.length < 2) return null;
128
+ const n = points.length;
129
+ const mean = { x: points.reduce((s, p) => s + p.x, 0) / n, y: points.reduce((s, p) => s + p.y, 0) / n };
130
+
131
+ let covXX = 0, covYY = 0, covXY = 0;
132
+ points.forEach(p => {
133
+ const dx = p.x - mean.x;
134
+ const dy = p.y - mean.y;
135
+ covXX += dx * dx; covYY += dy * dy; covXY += dx * dy;
136
+ });
137
+ covXX /= (n - 1); covYY /= (n - 1); covXY /= (n - 1);
138
+
139
+ const trace = covXX + covYY;
140
+ const det = covXX * covYY - covXY * covXY;
141
+ const gap = Math.sqrt(Math.max(0, trace * trace - 4 * det));
142
+ const lambda1 = (trace + gap) / 2;
143
+ const lambda2 = (trace - gap) / 2;
144
+
145
+ const getEigenvector = (lambda) => {
146
+ if (Math.abs(covXY) > 1e-9) {
147
+ const x = covXY, y = lambda - covXX;
148
+ const mag = Math.sqrt(x*x + y*y);
149
+ return { x: x/mag, y: y/mag };
150
+ } else {
151
+ return covXX >= covYY
152
+ ? (lambda === lambda1 ? { x: 1, y: 0 } : { x: 0, y: 1 })
153
+ : (lambda === lambda1 ? { x: 0, y: 1 } : { x: 1, y: 0 });
154
+ }
155
+ };
156
+
157
+ const pc1 = getEigenvector(lambda1);
158
+ const pc2 = getEigenvector(lambda2);
159
+ const v1 = (lambda1 + lambda2 === 0) ? 50 : (lambda1 / (lambda1 + lambda2)) * 100;
160
+ const v2 = (lambda1 + lambda2 === 0) ? 50 : (lambda2 / (lambda1 + lambda2)) * 100;
161
+
162
+ const projections = points.map(p => {
163
+ const dot = (p.x - mean.x) * pc1.x + (p.y - mean.y) * pc1.y;
164
+ return { x: mean.x + pc1.x * dot, y: mean.y + pc1.y * dot };
165
+ });
166
+
167
+ return { mean, pc1, pc2, variance1: v1, variance2: v2, projections };
168
+ };
169
+
170
+ const generateSamplePoints = (pattern) => {
171
+ const pts = [];
172
+ const cx = 250, cy = 200;
173
+ for (let i = 0; i < 8; i++) {
174
+ let x, y;
175
+ if (pattern === 'diagonal') { x = cx + (i-4)*40 + (Math.random()-0.5)*20; y = cy + (i-4)*40 + (Math.random()-0.5)*20; }
176
+ else if (pattern === 'cluster') { x = cx + (Math.random()-0.5)*100; y = cy + (Math.random()-0.5)*100; }
177
+ else { x = Math.random()*400+50; y = Math.random()*300+50; }
178
+ pts.push({ x, y, id: `p_${Date.now()}_${i}` });
179
+ }
180
+ return pts;
181
+ };
182
+
183
+ /** --- Sub-Components --- */
184
+ const Button = ({ children, className = '', variant = 'default', size = 'default', ...props }) => {
185
+ const variants = {
186
+ default: "bg-primary text-primary-foreground hover:opacity-90 shadow-soft",
187
+ outline: "border border-input bg-background hover:bg-slate-50",
188
+ ghost: "hover:bg-slate-100",
189
+ glass: "glass-btn",
190
+ secondary: "bg-secondary text-secondary-foreground hover:opacity-90 shadow-soft",
191
+ pc2: "bg-[hsl(var(--pc2-color))] text-white shadow-soft",
192
+ };
193
+ const sizes = { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", icon: "h-10 w-10" };
194
+ const vClass = variants[variant] || variants.default;
195
+ const sClass = sizes[size] || sizes.default;
196
+ return <button className={`inline-flex items-center justify-center rounded-xl text-sm font-medium transition-all focus:outline-none active:scale-95 disabled:opacity-50 ${vClass} ${sClass} ${className}`} {...props}>{children}</button>;
197
+ };
198
+
199
+ const ExplanationPanel = () => {
200
+ const [expanded, setExpanded] = useState(0);
201
+ const steps = [
202
+ { title: "What is PCA?", emoji: "🎯", content: "PCA finds the directions where your data varies the most. Think of it like finding the 'spine' of your data cloud!" },
203
+ { title: "Step 1: Find the Center", emoji: "⭕", content: "First, we find the average position of all points. This is shown as the red hollow circle. Everything rotates around this center." },
204
+ { title: "Step 2: Find PC1", emoji: "📏", content: "The teal arrow (PC1) points in the direction of most spread. Points would be as far apart as possible!" },
205
+ { title: "Step 3: Find PC2", emoji: "↔️", content: "The purple arrow (PC2) is always perpendicular to PC1. It captures the remaining variation." },
206
+ { title: "Projections", emoji: "📐", content: "The dotted lines show how each point 'projects' onto PC1. This is how PCA compresses 2D data into 1D!" },
207
+ ];
208
+ return (
209
+ <div className="space-y-2">
210
+ {steps.map((s, i) => (
211
+ <div key={i} className="border border-border bg-card rounded-xl overflow-hidden shadow-soft transition-all duration-300">
212
+ <button className="w-full flex items-center justify-between p-3 transition-colors hover:bg-muted/50" onClick={() => setExpanded(expanded === i ? -1 : i)}>
213
+ <span className="text-sm font-medium flex items-center gap-2"><span>{s.emoji}</span> {s.title}</span>
214
+ {expanded === i ? <Icons.ChevronUp /> : <Icons.ChevronDown />}
215
+ </button>
216
+ {expanded === i && <p className="px-3 pb-3 text-xs text-muted-foreground leading-relaxed pl-9 animate-fade-in">{s.content}</p>}
217
+ </div>
218
+ ))}
219
+ </div>
220
+ );
221
+ };
222
+
223
+ const RealWorldExamples = () => (
224
+ <div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-3">
225
+ <h3 className="text-sm font-semibold flex items-center gap-2">🌍 Real-World Uses of PCA</h3>
226
+ {[
227
+ { Icon: Icons.Camera, title: "Face Recognition", desc: "Compress pixels into key features" },
228
+ { Icon: Icons.Chart, title: "Stock Analysis", desc: "Find patterns in market movements" },
229
+ { Icon: Icons.Dna, title: "Genetics", desc: "Visualize population spread" },
230
+ { Icon: Icons.Music, title: "Recommendation", desc: "Group songs by audio vibes" }
231
+ ].map((e, i) => (
232
+ <div key={i} className="flex gap-3 items-center group cursor-default">
233
+ <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors">
234
+ <e.Icon className="text-primary" />
235
+ </div>
236
+ <div>
237
+ <p className="text-xs font-semibold">{e.title}</p>
238
+ <p className="text-[10px] text-muted-foreground leading-tight">{e.desc}</p>
239
+ </div>
240
+ </div>
241
+ ))}
242
+ </div>
243
+ );
244
+
245
+ const PCACanvas = ({ points, onAddPoint, onMovePoint, showProjections, showPC2, showLabels }) => {
246
+ const canvasRef = useRef(null);
247
+ const [dragging, setDragging] = useState(null);
248
+ const [hovered, setHovered] = useState(null);
249
+ const pca = useMemo(() => calculatePCA(points), [points]);
250
+
251
+ const draw = useCallback(() => {
252
+ const canvas = canvasRef.current; if (!canvas) return;
253
+ const ctx = canvas.getContext('2d');
254
+ const { width, height } = canvas;
255
+
256
+ ctx.fillStyle = 'hsl(40, 30%, 98%)'; ctx.fillRect(0, 0, width, height);
257
+ ctx.strokeStyle = 'hsl(220, 20%, 90%)'; ctx.lineWidth = 1;
258
+ for (let x = 0; x <= width; x += 50) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); }
259
+ for (let y = 0; y <= height; y += 50) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); }
260
+
261
+ if (pca && points.length >= 2) {
262
+ const scale = 150;
263
+ if (showProjections) {
264
+ ctx.strokeStyle = 'rgba(12, 80, 60, 0.2)'; ctx.setLineDash([5, 5]);
265
+ points.forEach((p, i) => {
266
+ const proj = pca.projections[i];
267
+ ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(proj.x, proj.y); ctx.stroke();
268
+ });
269
+ ctx.setLineDash([]); ctx.fillStyle = 'hsl(var(--pc1-color))';
270
+ pca.projections.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2); ctx.fill(); });
271
+ }
272
+ if (showPC2) {
273
+ ctx.strokeStyle = 'hsl(var(--pc2-color))'; ctx.lineWidth = 3; ctx.beginPath();
274
+ ctx.moveTo(pca.mean.x - pca.pc2.x * scale, pca.mean.y - pca.pc2.y * scale);
275
+ ctx.lineTo(pca.mean.x + pca.pc2.x * scale, pca.mean.y + pca.pc2.y * scale); ctx.stroke();
276
+ }
277
+ ctx.strokeStyle = 'hsl(var(--pc1-color))'; ctx.lineWidth = 4; ctx.beginPath();
278
+ ctx.moveTo(pca.mean.x - pca.pc1.x * scale, pca.mean.y - pca.pc1.y * scale);
279
+ ctx.lineTo(pca.mean.x + pca.pc1.x * scale, pca.mean.y + pca.pc1.y * scale); ctx.stroke();
280
+
281
+ // --- BOLD RED MEAN POINT ---
282
+ ctx.strokeStyle = '#000000'; ctx.lineWidth = 4;
283
+ ctx.beginPath(); ctx.arc(pca.mean.x, pca.mean.y, 11, 0, Math.PI * 2); ctx.stroke();
284
+ ctx.fillStyle = '#ff3b30'; // Bold Red
285
+ ctx.beginPath(); ctx.arc(pca.mean.x, pca.mean.y, 7, 0, Math.PI * 2); ctx.fill();
286
+
287
+ if (showLabels) {
288
+ ctx.fillStyle = '#ff3b30';
289
+ ctx.font = 'bold 15px "Space Grotesk"';
290
+ ctx.textAlign = 'left';
291
+ ctx.fillText('MEAN', pca.mean.x + 18, pca.mean.y - 4);
292
+ ctx.font = 'bold 10px "Space Grotesk"';
293
+ ctx.fillStyle = '#000000';
294
+ ctx.fillText('(center)', pca.mean.x + 18, pca.mean.y + 10);
295
+ }
296
+ }
297
+
298
+ // --- BOLD DATA POINTS (3D Look) ---
299
+ points.forEach((p, i) => {
300
+ const isH = hovered === p.id;
301
+ const radius = isH ? 14 : 11; // Smaller radius
302
+ const hue = (i * 137.5) % 360;
303
+
304
+ // 3D Gradient
305
+ const grad = ctx.createRadialGradient(
306
+ p.x - radius/3, p.y - radius/3, radius/5,
307
+ p.x, p.y, radius
308
+ );
309
+ grad.addColorStop(0, `hsl(${hue}, 100%, 80%)`); // Highlight
310
+ grad.addColorStop(0.4, `hsl(${hue}, 85%, 55%)`); // Body
311
+ grad.addColorStop(1, `hsl(${hue}, 90%, 30%)`); // Shadow
312
+
313
+ ctx.shadowBlur = isH ? 8 : 4;
314
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
315
+ ctx.fillStyle = grad;
316
+
317
+ ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.fill();
318
+
319
+ // Subtle border
320
+ ctx.strokeStyle = 'rgba(255,255,255,0.6)';
321
+ ctx.lineWidth = 1;
322
+ ctx.stroke();
323
+
324
+ // Reset shadow for text
325
+ ctx.shadowBlur = 0;
326
+ ctx.fillStyle = 'white';
327
+ ctx.font = 'bold 10px "Space Grotesk"';
328
+ ctx.textAlign = 'center';
329
+ ctx.textBaseline = 'middle';
330
+ ctx.fillText(i + 1, p.x, p.y);
331
+
332
+ if (showLabels) {
333
+ ctx.textBaseline = 'alphabetic';
334
+ ctx.fillStyle = `hsl(${hue}, 85%, 45%)`;
335
+ ctx.font = 'bold 11px "Space Grotesk"';
336
+ ctx.textAlign = 'center';
337
+ ctx.fillText(`Point ${i + 1}`, p.x, p.y + (isH ? 30 : 26));
338
+ }
339
+ });
340
+
341
+ if (points.length === 0) {
342
+ ctx.fillStyle = 'hsl(var(--muted-foreground))';
343
+ ctx.font = 'bold 16px "Space Grotesk"';
344
+ ctx.textAlign = 'center';
345
+ ctx.fillText('Click/Touch to add your first data point!', width/2, height/2);
346
+ }
347
+ }, [points, pca, hovered, dragging, showProjections, showPC2, showLabels]);
348
+
349
+ useEffect(() => draw(), [draw]);
350
+
351
+ // --- Mobile Touch & Mouse Unified Handler ---
352
+ const getPos = (e) => {
353
+ const canvas = canvasRef.current;
354
+ const rect = canvas.getBoundingClientRect();
355
+
356
+ // Handle both touch and mouse events
357
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
358
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
359
+
360
+ const scaleX = canvas.width / rect.width;
361
+ const scaleY = canvas.height / rect.height;
362
+ return {
363
+ x: (clientX - rect.left) * scaleX,
364
+ y: (clientY - rect.top) * scaleY
365
+ };
366
+ };
367
+
368
+ const handleStart = (e) => {
369
+ e.preventDefault(); // Prevent scrolling on touch
370
+ const { x, y } = getPos(e);
371
+ const target = points.find(p => Math.sqrt((p.x - x)**2 + (p.y - y)**2) < 25);
372
+ if (target) setDragging(target.id);
373
+ else onAddPoint({ x, y, id: `p_${Date.now()}` });
374
+ };
375
+
376
+ const handleMove = (e) => {
377
+ e.preventDefault();
378
+ const { x, y } = getPos(e);
379
+ if (dragging) {
380
+ onMovePoint(dragging, x, y);
381
+ } else {
382
+ // Only hover effects for mouse, not touch (usually)
383
+ if (!e.touches) {
384
+ setHovered(points.find(p => Math.sqrt((p.x - x)**2 + (p.y - y)**2) < 25)?.id || null);
385
+ }
386
+ }
387
+ };
388
+
389
+ const handleEnd = (e) => {
390
+ // For touchEnd, we don't need to preventDefault usually, but it's good practice for games
391
+ if(e.cancelable) e.preventDefault();
392
+ setDragging(null);
393
+ };
394
+
395
+ return (
396
+ <canvas
397
+ ref={canvasRef} width={500} height={400}
398
+ className="w-full h-auto bg-white rounded-xl border-2 border-border cursor-crosshair shadow-soft"
399
+
400
+ // Mouse Events
401
+ onMouseDown={handleStart}
402
+ onMouseMove={handleMove}
403
+ onMouseUp={handleEnd}
404
+ onMouseLeave={() => { setDragging(null); setHovered(null); }}
405
+
406
+ // Touch Events (Mobile)
407
+ onTouchStart={handleStart}
408
+ onTouchMove={handleMove}
409
+ onTouchEnd={handleEnd}
410
+ onTouchCancel={handleEnd}
411
+ />
412
+ );
413
+ };
414
+
415
+ const ControlPanel = ({ onClear, onGenerate, showProjections, onToggleProjections, showPC2, onTogglePC2, showLabels, onToggleLabels, count }) => (
416
+ <div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-4">
417
+ <h3 className="text-sm font-semibold">Toggle Features</h3>
418
+ <div className="grid grid-cols-1 gap-2">
419
+ <Button variant={showProjections ? "default" : "glass"} size="sm" onClick={onToggleProjections} className={`justify-start gap-2 ${showProjections ? 'glass-btn-active' : ''}`}>
420
+ {showProjections ? <Icons.EyeOff /> : <Icons.Eye />} Projections
421
+ </Button>
422
+ <Button variant={showPC2 ? "pc2" : "glass"} size="sm" onClick={onTogglePC2} className={`justify-start gap-2 ${showPC2 ? 'glass-btn-active' : ''}`}>
423
+ {showPC2 ? <Icons.EyeOff /> : <Icons.Eye />} PC2 Arrow
424
+ </Button>
425
+ <Button variant={showLabels ? "secondary" : "glass"} size="sm" onClick={onToggleLabels} className={`justify-start gap-2 ${showLabels ? 'glass-btn-active' : ''}`}>
426
+ {showLabels ? <Icons.Tags /> : <Icons.Tag />} Labels
427
+ </Button>
428
+ </div>
429
+ <div className="pt-2 border-t border-border">
430
+ <h4 className="text-xs font-semibold mb-2">Try Patterns</h4>
431
+ <div className="grid grid-cols-3 gap-2">
432
+ <Button variant="outline" size="sm" onClick={() => onGenerate('diagonal')}>Linear</Button>
433
+ <Button variant="outline" size="sm" onClick={() => onGenerate('cluster')}>Cluster</Button>
434
+ <Button variant="outline" size="sm" onClick={() => onGenerate('spread')}>Random</Button>
435
+ </div>
436
+ </div>
437
+ <div className="flex items-center justify-between pt-2 border-t border-border">
438
+ <span className="text-xs text-muted-foreground font-medium">{count} points added</span>
439
+ <Button variant="ghost" size="sm" onClick={onClear} className="text-destructive h-auto p-1 px-2"><Icons.Trash /> Clear All</Button>
440
+ </div>
441
+ </div>
442
+ );
443
+
444
+ const App = () => {
445
+ const [points, setPoints] = useState([]);
446
+ const [showProjections, setShowProjections] = useState(false);
447
+ const [showPC2, setShowPC2] = useState(false);
448
+ const [showLabels, setShowLabels] = useState(true);
449
+ const [variance, setVariance] = useState({ v1: 50, v2: 50 });
450
+
451
+ useEffect(() => {
452
+ const res = calculatePCA(points);
453
+ setVariance(res ? { v1: res.variance1, v2: res.variance2 } : { v1: 50, v2: 50 });
454
+ }, [points]);
455
+
456
+ // Styles defined here to avoid Jinja2 conflict with double curly braces
457
+ const pc1BarStyle = { width: `${variance.v1}%` };
458
+ const pc2BarStyle = { width: `${variance.v2}%` };
459
+
460
+ return (
461
+ <div className="min-h-screen bg-background p-4 md:p-8 animate-fade-in">
462
+ <div className="max-w-6xl mx-auto space-y-8">
463
+ <header className="text-center space-y-3">
464
+ <div className="inline-flex items-center gap-2 bg-primary/10 text-primary px-4 py-2 rounded-full text-xs font-semibold tracking-wide uppercase"><Icons.Sparkles /> Interactive Lab</div>
465
+ <h1 className="text-4xl md:text-5xl font-bold tracking-tight text-foreground">PCA Playground</h1>
466
+ <p className="text-muted-foreground max-w-xl mx-auto text-base">Explore Principal Component Analysis (PCA) by adding points, dragging them, and discovering hidden patterns.</p>
467
+ </header>
468
+
469
+ <div className="grid lg:grid-cols-[1fr_340px] gap-8">
470
+ <div className="space-y-6">
471
+ <PCACanvas points={points} onAddPoint={p => setPoints(prev => [...prev, p])} onMovePoint={(id, x, y) => setPoints(prev => prev.map(p => p.id === id ? {...p, x, y} : p))} showProjections={showProjections} showPC2={showPC2} showLabels={showLabels} />
472
+ <div className="flex flex-wrap gap-6 justify-center bg-card p-3 rounded-xl border border-border text-[11px] font-bold text-muted-foreground shadow-sm uppercase tracking-wider">
473
+ <div className="flex items-center gap-2"><div className="w-4 h-4 rounded-full bg-point"/> Data Balls</div>
474
+ <div className="flex items-center gap-2"><div className="w-5 h-1.5 bg-pc1 rounded"/> PC1 (Main)</div>
475
+ <div className="flex items-center gap-2"><div className="w-5 h-1.5 bg-pc2 rounded"/> PC2 (Secondary)</div>
476
+ <div className="flex items-center gap-2"><div className="w-5 h-5 rounded-full border-2 border-foreground bg-[#ff3b30]"/> Mean Center</div>
477
+ </div>
478
+ </div>
479
+ <div className="space-y-4">
480
+ <ControlPanel onClear={() => { setPoints([]); setShowProjections(false); setShowPC2(false); }} onGenerate={pattern => setPoints(generateSamplePoints(pattern))} showProjections={showProjections} onToggleProjections={() => setShowProjections(prev => !prev)} showPC2={showPC2} onTogglePC2={() => setShowPC2(prev => !prev)} showLabels={showLabels} onToggleLabels={() => setShowLabels(prev => !prev)} count={points.length} />
481
+ {points.length >= 2 && (
482
+ <div className="bg-card rounded-xl p-4 border border-border shadow-soft space-y-3">
483
+ <h3 className="text-sm font-semibold">Variance Explained</h3>
484
+ <div className="space-y-4">
485
+ <div>
486
+ <div className="flex justify-between text-xs font-medium mb-1 text-pc1"><span>PC1</span><span>{variance.v1.toFixed(1)}%</span></div>
487
+ <div className="h-3 bg-muted rounded-full overflow-hidden"><div className="h-full bg-pc1 transition-all duration-500" style={pc1BarStyle} /></div>
488
+ </div>
489
+ <div>
490
+ <div className="flex justify-between text-xs font-medium mb-1 text-pc2"><span>PC2</span><span>{variance.v2.toFixed(1)}%</span></div>
491
+ <div className="h-3 bg-muted rounded-full overflow-hidden"><div className="h-full bg-pc2 transition-all duration-500" style={pc2BarStyle} /></div>
492
+ </div>
493
+ </div>
494
+ </div>
495
+ )}
496
+ <ExplanationPanel />
497
+ <RealWorldExamples />
498
+ </div>
499
+ </div>
500
+
501
+ {/* Centered Back Button */}
502
+ <div className="mt-12 flex justify-center pb-8 relative">
503
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
504
+ <a
505
+ href="/hierarchical-clustering"
506
+ onClick={(e) => {
507
+ const audio = document.getElementById("clickSound");
508
+ if(audio) audio.play().catch(err => console.log(err));
509
+ }}
510
+ className="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"
511
+ >
512
+ Back to Core
513
+ </a>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ );
518
+ };
519
+
520
+ const root = createRoot(document.getElementById('root'));
521
+ root.render(<App />);
522
+ </script>
523
+ </body>
524
+ </html>
templates/xboost-tree-three.html ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>XGBoost 3D Simulator</title>
7
+ <!-- Fonts -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <!-- Tailwind CSS -->
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <!-- Lucide Icons -->
12
+ <script src="https://unpkg.com/lucide@latest"></script>
13
+ <!-- Three.js -->
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
15
+ <!-- OrbitControls -->
16
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
17
+
18
+ <script>
19
+ tailwind.config = {
20
+ theme: {
21
+ extend: {
22
+ colors: {
23
+ background: 'hsl(222 47% 6%)',
24
+ foreground: 'hsl(210 40% 98%)',
25
+ primary: 'hsl(187 100% 50%)',
26
+ secondary: 'hsl(32 100% 55%)',
27
+ accent: 'hsl(270 80% 60%)',
28
+ success: 'hsl(142 76% 50%)',
29
+ muted: 'hsl(222 30% 15%)',
30
+ 'muted-foreground': 'hsl(215 20% 65%)',
31
+ border: 'hsl(222 30% 18%)',
32
+ },
33
+ fontFamily: {
34
+ sans: ['Outfit', 'sans-serif'],
35
+ mono: ['JetBrains Mono', 'monospace'],
36
+ }
37
+ }
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <style type="text/css">
43
+ body {
44
+ background-color: hsl(222 47% 6%);
45
+ color: hsl(210 40% 98%);
46
+ font-family: 'Outfit', sans-serif;
47
+ margin: 0;
48
+ overflow-x: hidden;
49
+ }
50
+
51
+ .gradient-hero {
52
+ background: linear-gradient(180deg, hsl(222 47% 8%) 0%, hsl(222 47% 4%) 100%);
53
+ }
54
+
55
+ .gradient-card {
56
+ background: linear-gradient(145deg, hsl(222 47% 10%) 0%, hsl(222 47% 6%) 100%);
57
+ }
58
+
59
+ .text-gradient-primary {
60
+ background: linear-gradient(135deg, hsl(187 100% 50%) 0%, hsl(200 100% 60%) 100%);
61
+ -webkit-background-clip: text;
62
+ -webkit-text-fill-color: transparent;
63
+ }
64
+
65
+ .glow-primary {
66
+ box-shadow: 0 0 30px hsla(187, 100%, 50%, 0.3);
67
+ }
68
+
69
+ canvas {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ touch-action: none; /* Prevents scrolling while rotating model */
74
+ }
75
+
76
+ .glass-btn {
77
+ background: rgba(255, 255, 255, 0.05);
78
+ backdrop-filter: blur(8px);
79
+ border: 1px solid rgba(255, 255, 255, 0.1);
80
+ transition: all 0.2s ease;
81
+ }
82
+
83
+ .glass-btn:hover:not(:disabled) {
84
+ background: rgba(255, 255, 255, 0.1);
85
+ border-color: rgba(255, 255, 255, 0.2);
86
+ }
87
+
88
+ .primary-btn {
89
+ background: linear-gradient(135deg, hsl(187 100% 50%) 0%, hsl(200 100% 60%) 100%);
90
+ color: hsl(222 47% 6%);
91
+ font-weight: 600;
92
+ transition: all 0.2s ease;
93
+ }
94
+
95
+ .primary-btn:hover {
96
+ transform: translateY(-1px);
97
+ box-shadow: 0 4px 20px hsla(187, 100%, 50%, 0.4);
98
+ }
99
+
100
+ input[type=range] {
101
+ -webkit-appearance: none;
102
+ width: 100%;
103
+ background: transparent;
104
+ }
105
+
106
+ input[type=range]:focus { outline: none; }
107
+ input[type=range]::-webkit-slider-runnable-track {
108
+ width: 100%;
109
+ height: 6px;
110
+ cursor: pointer;
111
+ background: hsl(222 30% 18%);
112
+ border-radius: 3px;
113
+ }
114
+
115
+ input[type=range]::-webkit-slider-thumb {
116
+ height: 18px;
117
+ width: 18px;
118
+ border-radius: 50%;
119
+ background: hsl(187 100% 50%);
120
+ cursor: pointer;
121
+ -webkit-appearance: none;
122
+ margin-top: -6px;
123
+ box-shadow: 0 0 10px rgba(0,0,0,0.5);
124
+ }
125
+
126
+ .chart-path {
127
+ stroke-dasharray: 1000;
128
+ stroke-dashoffset: 1000;
129
+ animation: dash 2s ease-out forwards;
130
+ }
131
+
132
+ @keyframes dash { to { stroke-dashoffset: 0; } }
133
+
134
+ select {
135
+ background: hsl(222 30% 12%);
136
+ border: 1px solid hsl(222 30% 20%);
137
+ color: white;
138
+ padding: 4px 8px;
139
+ border-radius: 4px;
140
+ font-size: 14px;
141
+ }
142
+
143
+ .logic-step {
144
+ position: relative;
145
+ padding-left: 2rem;
146
+ }
147
+ .logic-step::before {
148
+ content: '';
149
+ position: absolute;
150
+ left: 0.75rem;
151
+ top: 0;
152
+ bottom: 0;
153
+ width: 2px;
154
+ background: hsl(222 30% 20%);
155
+ }
156
+ .logic-step:last-child::before {
157
+ display: none;
158
+ }
159
+ </style>
160
+ </head>
161
+ <body class="gradient-hero min-h-screen">
162
+
163
+ <!-- Header -->
164
+ <!-- Changed: Added flex-col for mobile, relative positioning -->
165
+ <header class="border-b border-border/50 backdrop-blur-sm sticky top-0 z-50">
166
+ <div class="container mx-auto px-4 md:px-6 py-4 flex flex-col md:flex-row items-center justify-between gap-4 md:gap-0 relative">
167
+
168
+ <!-- Logo Area -->
169
+ <div class="flex items-center gap-4 w-full md:w-auto justify-start">
170
+ <div class="p-2 rounded-lg bg-primary/20 glow-primary shrink-0">
171
+ <i data-lucide="trees" class="h-6 w-6 text-primary"></i>
172
+ </div>
173
+ <div>
174
+ <h1 class="text-xl md:text-2xl font-bold text-gradient-primary leading-tight">XGBoost Simulator</h1>
175
+ <p class="text-xs md:text-sm text-muted-foreground hidden sm:block">Interactive 3D Visualization</p>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Centered Button -->
180
+ <!-- Changed: Relative on mobile (order-3), Absolute centered on desktop -->
181
+ <div class="order-3 md:order-none w-full md:w-auto md:absolute md:left-1/2 md:-translate-x-1/2 flex items-center justify-center">
182
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
183
+ <a href="/xgboost-regression" 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 w-full md:w-auto">
184
+ Back to Core
185
+ </a>
186
+ </div>
187
+
188
+ <!-- Dataset Selector -->
189
+ <!-- Changed: Full width on mobile -->
190
+ <div class="flex items-center justify-between md:justify-start gap-3 glass-btn px-4 py-2 rounded-xl w-full md:w-auto">
191
+ <span class="text-xs text-muted-foreground font-medium uppercase tracking-wider text-white">Dataset:</span>
192
+ <select id="dataset-selector" class="bg-transparent border-none outline-none text-right md:text-left">
193
+ <option value="sine">Sine Wave</option>
194
+ <option value="step">Step Function</option>
195
+ <option value="linear">Linear Trend</option>
196
+ <option value="random">Random Noise</option>
197
+ </select>
198
+ </div>
199
+ </div>
200
+ </header>
201
+
202
+ <main class="container mx-auto px-4 md:px-6 py-4 md:py-8 space-y-6 md:space-y-8">
203
+ <!-- Stats Section -->
204
+ <div id="stats-container" class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4"></div>
205
+
206
+ <!-- Simulator Layout -->
207
+ <!-- Changed: Stacked columns on mobile -->
208
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
209
+ <div class="lg:col-span-2 space-y-6">
210
+ <!-- 3D Scene -->
211
+ <div class="relative aspect-[16/12] md:aspect-[16/10] rounded-xl border border-border overflow-hidden gradient-card shadow-2xl touch-none">
212
+ <div id="canvas-container" class="w-full h-full"></div>
213
+ <div id="labels-container" class="absolute inset-0 pointer-events-none"></div>
214
+ </div>
215
+
216
+ <!-- Dual Charts Row -->
217
+ <!-- Changed: Stacked on small mobile, grid on md -->
218
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
219
+ <!-- Training Progress -->
220
+ <div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
221
+ <div class="flex items-center justify-between mb-4">
222
+ <div class="flex items-center gap-2">
223
+ <i data-lucide="line-chart" class="h-5 w-5 text-success"></i>
224
+ <h3 class="text-lg font-semibold text-foreground">Loss Decay</h3>
225
+ </div>
226
+ </div>
227
+ <div class="h-28 w-full relative">
228
+ <svg id="progress-chart" viewBox="0 0 400 100" class="w-full h-full overflow-visible">
229
+ <path id="loss-path" d="" fill="none" stroke="hsl(187 100% 50%)" stroke-width="2" class="chart-path" />
230
+ <path id="loss-area" d="" fill="url(#chart-gradient)" opacity="0.1" />
231
+ <g id="chart-dots"></g>
232
+ <defs>
233
+ <linearGradient id="chart-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
234
+ <stop offset="0%" style="stop-color:hsl(187 100% 50%);stop-opacity:1" />
235
+ <stop offset="100%" style="stop-color:hsl(187 100% 50%);stop-opacity:0" />
236
+ </linearGradient>
237
+ </defs>
238
+ </svg>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Feature Importance -->
243
+ <div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
244
+ <div class="flex items-center justify-between mb-4">
245
+ <div class="flex items-center gap-2">
246
+ <i data-lucide="bar-chart-3" class="h-5 w-5 text-accent"></i>
247
+ <h3 class="text-lg font-semibold text-foreground">Feature Importance</h3>
248
+ </div>
249
+ </div>
250
+ <div id="importance-container" class="space-y-3 pt-2">
251
+ <!-- Bars injected by JS -->
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ <!-- Decision Path Explorer -->
257
+ <div class="gradient-card border border-border rounded-xl p-4 md:p-6 shadow-lg">
258
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
259
+ <div class="flex items-center gap-2">
260
+ <i data-lucide="route" class="h-5 w-5 text-primary"></i>
261
+ <h3 class="text-lg font-semibold text-foreground">Decision Path Explorer</h3>
262
+ </div>
263
+ <div class="flex items-center justify-between sm:justify-end gap-2 w-full sm:w-auto bg-black/20 p-2 rounded-lg sm:bg-transparent sm:p-0">
264
+ <span class="text-[10px] text-muted-foreground uppercase font-mono tracking-widest">Tracking Point:</span>
265
+ <select id="point-selector" class="text-[10px] py-1 max-w-[120px]">
266
+ <!-- Options injected by JS -->
267
+ </select>
268
+ </div>
269
+ </div>
270
+ <div id="decision-path-container" class="space-y-4">
271
+ <!-- Iteration steps injected here -->
272
+ </div>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Side Panels -->
277
+ <div class="space-y-6">
278
+ <!-- Configuration Panel -->
279
+ <div class="gradient-card border border-border rounded-xl p-4 md:p-6 space-y-6 shadow-lg">
280
+ <div class="flex items-center justify-between">
281
+ <h3 class="text-lg font-semibold text-foreground">Configuration</h3>
282
+ <span id="iteration-count" class="text-sm text-muted-foreground font-mono">Iter 0/5</span>
283
+ </div>
284
+
285
+ <!-- Playback Controls -->
286
+ <div class="flex items-center justify-between sm:justify-center gap-2">
287
+ <button id="prev-btn" class="glass-btn p-2 rounded-lg disabled:opacity-30 flex-1 sm:flex-none justify-center flex"><i data-lucide="chevron-left" class="h-5 w-5"></i></button>
288
+ <button id="play-pause-btn" class="primary-btn px-4 sm:px-6 py-2 rounded-lg flex items-center gap-2 min-w-[100px] sm:min-w-[120px] justify-center flex-2">
289
+ <i id="play-icon" data-lucide="play" class="h-5 w-5"></i>
290
+ <span id="play-text">Play</span>
291
+ </button>
292
+ <button id="next-btn" class="glass-btn p-2 rounded-lg disabled:opacity-30 flex-1 sm:flex-none justify-center flex"><i data-lucide="chevron-right" class="h-5 w-5"></i></button>
293
+ <button id="reset-btn" class="glass-btn p-2 rounded-lg flex-1 sm:flex-none justify-center flex"><i data-lucide="rotate-ccw" class="h-5 w-5"></i></button>
294
+ </div>
295
+
296
+ <div class="space-y-4 pt-4 border-t border-border/50">
297
+ <div class="space-y-2">
298
+ <div class="flex justify-between text-xs text-muted-foreground"><span>Iteration</span><span id="boost-val">0</span></div>
299
+ <input id="iteration-slider" type="range" min="0" max="5" value="0" step="1">
300
+ </div>
301
+ <div class="space-y-2">
302
+ <div class="flex justify-between text-xs text-muted-foreground"><span>Learning Rate (η)</span><span id="lr-value" class="text-primary font-mono">0.30</span></div>
303
+ <input id="lr-slider" type="range" min="1" max="100" value="30">
304
+ </div>
305
+ <div class="space-y-2">
306
+ <div class="flex justify-between text-xs text-muted-foreground"><span>Max Tree Depth</span><span id="depth-val" class="text-secondary font-mono">2</span></div>
307
+ <input id="depth-slider" type="range" min="1" max="4" value="2">
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Explanation Panel -->
313
+ <div id="explanation-panel" class="gradient-card border border-border rounded-xl p-4 md:p-6 space-y-4 shadow-lg">
314
+ <div class="flex items-center gap-3">
315
+ <div class="p-2 rounded-lg bg-primary/20"><i data-lucide="book-open" class="h-5 w-5 text-primary"></i></div>
316
+ <h3 id="exp-title" class="text-lg font-semibold text-foreground">Status</h3>
317
+ </div>
318
+ <p id="exp-desc" class="text-muted-foreground text-sm leading-relaxed">Starting simulation...</p>
319
+ <div class="bg-muted/50 rounded-lg p-3 border border-border/50 overflow-x-auto">
320
+ <code id="exp-formula" class="text-sm font-mono text-gradient-primary whitespace-nowrap"></code>
321
+ </div>
322
+ <div class="pt-4 mt-4 border-t border-border/50 space-y-3">
323
+ <h4 class="text-xs font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-2">
324
+ <i data-lucide="brain-circuit" class="h-3 w-3"></i> The Objective Function
325
+ </h4>
326
+ <p class="text-[10px] text-muted-foreground leading-relaxed">
327
+ XGBoost minimizes $L(\phi) = \sum l(y_i, \hat{y}_i) + \sum \Omega(f_k)$.
328
+ It uses a 2nd order Taylor expansion for fast optimization.
329
+ </p>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </main>
335
+
336
+ <script>
337
+ // --- STATE & CONSTANTS ---
338
+ let currentIteration = 0, maxIterations = 5, learningRate = 0.3, maxDepth = 2, datasetType = 'sine', selectedPointIdx = 0;
339
+ let isPlaying = false, playInterval = null;
340
+ let scene, camera, renderer, controls, treeGroup, pointsGroup, initialGroup;
341
+ let dataPoints = [], iterationsData = [];
342
+
343
+ // --- SIMULATION LOGIC ---
344
+ function generateData(type) {
345
+ const points = [];
346
+ for (let i = 0; i < 12; i++) {
347
+ const x = (i / 11) * 10;
348
+ let actual = 0;
349
+ if (type === 'sine') actual = Math.sin(x * 0.6) * 2;
350
+ else if (type === 'step') actual = x < 5 ? -1.5 : 1.5;
351
+ else if (type === 'linear') actual = (x - 5) * 0.4;
352
+ else actual = (Math.random() - 0.5) * 4;
353
+ points.push({ id: i, x, actual: actual + (Math.random() * 0.2) });
354
+ }
355
+ return points;
356
+ }
357
+
358
+ function createTree(depthLimit, iteration, currentDepth = 0, x = 0, y = 0, z = 0, width = 4) {
359
+ const isLeaf = currentDepth >= depthLimit;
360
+ const threshold = Math.random() * 10;
361
+ const node = {
362
+ id: Math.random().toString(36),
363
+ feature: "X",
364
+ threshold: threshold,
365
+ isLeaf,
366
+ value: isLeaf ? (Math.random() * 1.5 - 0.75) : undefined,
367
+ position: [x, y, z]
368
+ };
369
+ if (!isLeaf) {
370
+ const nextWidth = width / 2;
371
+ node.left = createTree(depthLimit, iteration, currentDepth + 1, x - nextWidth, y - 1.5, z, nextWidth);
372
+ node.right = createTree(depthLimit, iteration, currentDepth + 1, x + nextWidth, y - 1.5, z, nextWidth);
373
+ }
374
+ return node;
375
+ }
376
+
377
+ function simulate(points, treeCount, lr, depth) {
378
+ const results = [];
379
+ let currentPreds = Array(points.length).fill(points.reduce((a,b)=>a+b.actual,0)/points.length);
380
+ for (let i = 1; i <= treeCount; i++) {
381
+ const tree = createTree(depth, i);
382
+ const residuals = points.map((p, idx) => p.actual - currentPreds[idx]);
383
+
384
+ // Track path for each point
385
+ const treeOutputs = points.map(p => {
386
+ let curr = tree;
387
+ const path = [];
388
+ while(!curr.isLeaf) {
389
+ const direction = p.x < curr.threshold ? "left" : "right";
390
+ path.push({ node: curr, direction });
391
+ curr = curr[direction];
392
+ }
393
+ path.push({ node: curr, direction: "leaf" });
394
+ return { value: curr.value, path };
395
+ });
396
+
397
+ currentPreds = currentPreds.map((p, idx) => p + lr * treeOutputs[idx].value);
398
+ results.push({
399
+ iteration: i,
400
+ tree,
401
+ predictions: [...currentPreds],
402
+ residuals: points.map((p, idx) => p.actual - currentPreds[idx]),
403
+ outputs: treeOutputs
404
+ });
405
+ }
406
+ return results;
407
+ }
408
+
409
+ // --- RENDERING ---
410
+ function initThree() {
411
+ const canvasContainer = document.getElementById('canvas-container');
412
+ scene = new THREE.Scene();
413
+ scene.fog = new THREE.Fog(0x0a0f1a, 10, 40);
414
+ camera = new THREE.PerspectiveCamera(50, canvasContainer.clientWidth/canvasContainer.clientHeight, 0.1, 1000);
415
+ camera.position.set(0, 2, 12);
416
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
417
+ renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
418
+ renderer.setPixelRatio(window.devicePixelRatio);
419
+ canvasContainer.appendChild(renderer.domElement);
420
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
421
+ controls.enableDamping = true;
422
+
423
+ scene.add(new THREE.AmbientLight(0xffffff, 0.4));
424
+ const p1 = new THREE.PointLight(0x00d4ff, 1); p1.position.set(10,10,10); scene.add(p1);
425
+ const grid = new THREE.GridHelper(40, 40, 0x1e3a5f, 0x0f172a); grid.position.y = -5; scene.add(grid);
426
+
427
+ treeGroup = new THREE.Group(); pointsGroup = new THREE.Group(); initialGroup = new THREE.Group();
428
+ scene.add(treeGroup); scene.add(pointsGroup); scene.add(initialGroup);
429
+
430
+ window.addEventListener('resize', () => {
431
+ camera.aspect = canvasContainer.clientWidth/canvasContainer.clientHeight; camera.updateProjectionMatrix();
432
+ renderer.setSize(canvasContainer.clientWidth, canvasContainer.clientHeight);
433
+ });
434
+
435
+ const animate = () => { requestAnimationFrame(animate); controls.update(); updateLabels(); renderer.render(scene, camera); };
436
+ animate();
437
+ }
438
+
439
+ function drawNode(node, parentPos = null) {
440
+ const group = new THREE.Group(); group.position.set(...node.position);
441
+ const nodeColor = node.isLeaf ? 0x22c55e : 0x00d4ff;
442
+ if (parentPos) {
443
+ const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0), new THREE.Vector3(parentPos[0]-node.position[0], parentPos[1]-node.position[1], parentPos[2]-node.position[2])]), new THREE.LineBasicMaterial({ color: 0x334155, transparent: true, opacity: 0.6 }));
444
+ group.add(line);
445
+ }
446
+ const mesh = new THREE.Mesh(new THREE.SphereGeometry(node.isLeaf?0.25:0.35, 32, 32), new THREE.MeshStandardMaterial({ color: nodeColor, emissive: nodeColor, emissiveIntensity: 0.4 }));
447
+ mesh.userData = { label: node.isLeaf ? node.value.toFixed(2) : `X < ${node.threshold.toFixed(1)}`, isNode: true };
448
+ group.add(mesh); treeGroup.add(group);
449
+ if (node.left) drawNode(node.left, node.position);
450
+ if (node.right) drawNode(node.right, node.position);
451
+ }
452
+
453
+ function drawPoints(data) {
454
+ pointsGroup.clear();
455
+ const startX = -((data.predictions.length-1) * 0.8) / 2;
456
+ data.predictions.forEach((pred, i) => {
457
+ const g = new THREE.Group(); g.position.set(startX + i * 0.8, -4, 0);
458
+ const barH = Math.max(0.1, Math.abs(pred)*1.5);
459
+ const bar = new THREE.Mesh(new THREE.BoxGeometry(0.3, barH, 0.3), new THREE.MeshStandardMaterial({ color: i === selectedPointIdx ? 0xffff00 : 0x00d4ff, emissive: i === selectedPointIdx ? 0xffff00 : 0x000000, emissiveIntensity: 0.5 }));
460
+ bar.position.y = barH/2; g.add(bar);
461
+ const res = new THREE.Mesh(new THREE.SphereGeometry(0.15), new THREE.MeshStandardMaterial({ color: data.residuals[i]>0?0x22c55e:0xef4444 }));
462
+ res.position.y = -0.5; g.add(res); pointsGroup.add(g);
463
+ });
464
+ }
465
+
466
+ function updateLabels() {
467
+ const labelsContainer = document.getElementById('labels-container');
468
+ if (!labelsContainer) return;
469
+ labelsContainer.innerHTML = '';
470
+ if (currentIteration === 0) {
471
+ createLabel("Base Prediction: Mean(y)", new THREE.Vector3(0,2,0), "text-white font-semibold text-xs");
472
+ } else {
473
+ treeGroup.children.forEach(g => {
474
+ const m = g.children.find(c=>c.userData.isNode);
475
+ if (m) {
476
+ const v = new THREE.Vector3(); m.getWorldPosition(v);
477
+ createLabel(m.userData.label, v.add(new THREE.Vector3(0,0.6,0)), "text-[9px] text-muted-foreground bg-background/80 px-1 rounded shadow-sm");
478
+ }
479
+ });
480
+ }
481
+ }
482
+
483
+ function createLabel(txt, pos, cls) {
484
+ const v = pos.project(camera); if (v.z > 1) return;
485
+ const x = (v.x*0.5+0.5)*renderer.domElement.clientWidth, y = (v.y*-0.5+0.5)*renderer.domElement.clientHeight;
486
+ const d = document.createElement('div'); d.className = `absolute transform -translate-x-1/2 pointer-events-none ${cls}`;
487
+ d.style.left = `${x}px`; d.style.top = `${y}px`; d.textContent = txt;
488
+ const labelsContainer = document.getElementById('labels-container');
489
+ if (labelsContainer) labelsContainer.appendChild(d);
490
+ }
491
+
492
+ // --- UI UPDATES ---
493
+ function refresh() {
494
+ dataPoints = generateData(datasetType);
495
+ iterationsData = simulate(dataPoints, maxIterations, learningRate, maxDepth);
496
+
497
+ // Re-populate point selector
498
+ const sel = document.getElementById('point-selector');
499
+ sel.innerHTML = '';
500
+ dataPoints.forEach((p, i) => {
501
+ const opt = document.createElement('option');
502
+ opt.value = i;
503
+ opt.textContent = `Point #${i} (X: ${p.x.toFixed(1)})`;
504
+ sel.appendChild(opt);
505
+ });
506
+ sel.value = selectedPointIdx;
507
+
508
+ render();
509
+ }
510
+
511
+ function render() {
512
+ document.getElementById('iteration-count').textContent = `Iter ${currentIteration}/${maxIterations}`;
513
+ document.getElementById('boost-val').textContent = currentIteration;
514
+ document.getElementById('depth-val').textContent = maxDepth;
515
+ document.getElementById('iteration-slider').value = currentIteration;
516
+
517
+ const baseMse = 1.0;
518
+ const currentMse = currentIteration === 0 ? baseMse : (iterationsData[currentIteration-1].residuals.reduce((a,b)=>a+b*b,0)/dataPoints.length);
519
+
520
+ updateStats(currentMse);
521
+ updateCharts(currentMse);
522
+ updateDecisionPath();
523
+
524
+ treeGroup.clear(); pointsGroup.clear(); initialGroup.clear();
525
+ if (currentIteration === 0) {
526
+ const g = new THREE.SphereGeometry(1,32,32);
527
+ initialGroup.add(new THREE.Mesh(g, new THREE.MeshStandardMaterial({color:0x00d4ff, transparent:true, opacity:0.8})));
528
+ document.getElementById('exp-title').textContent = "Initialization";
529
+ document.getElementById('exp-desc').textContent = "Model starts with the global mean. Initial residuals are high.";
530
+ document.getElementById('exp-formula').textContent = "F₀(x) = average(targets)";
531
+ } else {
532
+ const d = iterationsData[currentIteration-1];
533
+ drawNode(d.tree); drawPoints(d);
534
+ document.getElementById('exp-title').textContent = `Tree ${currentIteration}`;
535
+ document.getElementById('exp-desc').textContent = `Building weak learner ${currentIteration} to minimize residuals using the learning rate.`;
536
+ document.getElementById('exp-formula').textContent = `Fₘ = Fₘ₋₁ + η · Treeₘ(x)`;
537
+ }
538
+ }
539
+
540
+ function updateStats(mse) {
541
+ const container = document.getElementById('stats-container'); container.innerHTML = '';
542
+ const stats = [
543
+ { l: "MSE Loss", v: mse.toFixed(4), i: "target", c: "text-secondary" },
544
+ { l: "Complexity", v: currentIteration*maxDepth, i: "layers", c: "text-primary" },
545
+ { l: "Learning Rate", v: learningRate.toFixed(2), i: "zap", c: "text-accent" },
546
+ { l: "Progress", v: ((currentIteration/maxIterations)*100).toFixed(0)+"%", i: "trending-down", c: "text-success" }
547
+ ];
548
+ stats.forEach(s => {
549
+ const d = document.createElement('div'); d.className="gradient-card border border-border rounded-xl p-4";
550
+ d.innerHTML = `<div class='flex gap-2 mb-1'><i data-lucide='${s.i}' class='h-4 w-4 ${s.c}'></i><span class='text-xs text-muted-foreground'>${s.l}</span></div><div class='text-xl font-mono font-bold ${s.c}'>${s.v}</div>`;
551
+ container.appendChild(d);
552
+ });
553
+ lucide.createIcons();
554
+ }
555
+
556
+ function updateCharts(mse) {
557
+ const path = document.getElementById('loss-path'), area = document.getElementById('loss-area'), dots = document.getElementById('chart-dots');
558
+ const pts = [{x:0, y:1}];
559
+ iterationsData.slice(0, currentIteration).forEach((it, i) => {
560
+ const m = it.residuals.reduce((a,b)=>a+b*b,0)/dataPoints.length;
561
+ pts.push({x:(i+1)*(400/maxIterations), y:m});
562
+ });
563
+ let d = `M ${pts[0].x} ${100-pts[0].y*100}`;
564
+ let dotH = `<circle cx="${pts[0].x}" cy="${100-pts[0].y*100}" r="3" fill="hsl(187 100% 50%)" />`;
565
+ for(let i=1;i<pts.length;i++){ d += ` L ${pts[i].x} ${100-pts[i].y*100}`; dotH += `<circle cx="${pts[i].x}" cy="${100-pts[i].y*100}" r="3" fill="hsl(187 100% 50%)" />`; }
566
+ path.setAttribute('d', d); area.setAttribute('d', `${d} L ${pts[pts.length-1].x} 100 L 0 100 Z`); dots.innerHTML = dotH;
567
+
568
+ const impC = document.getElementById('importance-container'); impC.innerHTML = '';
569
+ const feats = [ {n: "Feature X", v: Math.min(100, currentIteration*18 + 5)}, {n: "Interaction", v: Math.min(100, currentIteration*8)}, {n: "Bias", v: 10} ];
570
+ feats.forEach(f => {
571
+ const row = document.createElement('div'); row.className = "space-y-1";
572
+ row.innerHTML = `<div class='flex justify-between text-[10px] uppercase font-bold text-muted-foreground'><span>${f.n}</span><span>${f.v}%</span></div><div class='h-1.5 w-full bg-border rounded-full overflow-hidden'><div class='h-full bg-accent transition-all duration-500' style='width:${f.v}%'></div></div>`;
573
+ impC.appendChild(row);
574
+ });
575
+ }
576
+
577
+ function updateDecisionPath() {
578
+ const container = document.getElementById('decision-path-container');
579
+ container.innerHTML = '';
580
+
581
+ if (currentIteration === 0) {
582
+ container.innerHTML = '<div class="text-center py-8 text-muted-foreground text-xs italic">Step into the boosting process to see point #0 trace through the ensemble.</div>';
583
+ return;
584
+ }
585
+
586
+ const p = dataPoints[selectedPointIdx];
587
+
588
+ iterationsData.slice(0, currentIteration).forEach((iter, idx) => {
589
+ const output = iter.outputs[selectedPointIdx];
590
+ const card = document.createElement('div');
591
+ card.className = "p-4 border border-border/50 rounded-lg bg-white/5 space-y-3";
592
+
593
+ let stepsHtml = '';
594
+ output.path.forEach((step, sIdx) => {
595
+ const isLast = sIdx === output.path.length - 1;
596
+ const nodeTxt = isLast ? `Leaf Weight: ${step.node.value.toFixed(3)}` : `Split: X (${p.x.toFixed(1)}) < ${step.node.threshold.toFixed(1)}?`;
597
+ const resTxt = isLast ? '' : `<span class="px-2 py-0.5 rounded text-[10px] font-bold ${step.direction === 'left' ? 'bg-success/20 text-success' : 'bg-secondary/20 text-secondary'}">${step.direction === 'left' ? 'YES' : 'NO'}</span>`;
598
+
599
+ stepsHtml += `
600
+ <div class="logic-step flex items-center justify-between text-[11px]">
601
+ <div class="flex items-center gap-2">
602
+ <div class="w-1.5 h-1.5 rounded-full ${isLast ? 'bg-success' : 'bg-primary'}"></div>
603
+ <span class="${isLast ? 'text-white font-bold' : 'text-muted-foreground'}">${nodeTxt}</span>
604
+ </div>
605
+ ${resTxt}
606
+ </div>
607
+ `;
608
+ });
609
+
610
+ card.innerHTML = `
611
+ <div class="flex items-center justify-between">
612
+ <span class="text-[10px] font-bold text-primary uppercase">Iteration ${iter.iteration}</span>
613
+ <span class="text-[10px] font-mono text-muted-foreground">contrib: η × ${output.value.toFixed(2)}</span>
614
+ </div>
615
+ <div class="space-y-2 relative">${stepsHtml}</div>
616
+ `;
617
+ container.appendChild(card);
618
+ });
619
+ }
620
+
621
+ window.onload = () => {
622
+ initThree(); refresh();
623
+ document.getElementById('dataset-selector').onchange = (e) => { datasetType = e.target.value; currentIteration = 0; refresh(); };
624
+ document.getElementById('depth-slider').oninput = (e) => { maxDepth = parseInt(e.target.value); refresh(); };
625
+ document.getElementById('lr-slider').oninput = (e) => { learningRate = parseInt(e.target.value)/100; document.getElementById('lr-value').textContent = learningRate.toFixed(2); refresh(); };
626
+ document.getElementById('iteration-slider').oninput = (e) => { currentIteration = parseInt(e.target.value); render(); };
627
+ document.getElementById('point-selector').onchange = (e) => { selectedPointIdx = parseInt(e.target.value); render(); };
628
+
629
+ document.getElementById('play-pause-btn').onclick = () => {
630
+ isPlaying = !isPlaying;
631
+ if(isPlaying) {
632
+ document.getElementById('play-text').textContent = "Pause";
633
+ document.getElementById('play-icon').setAttribute('data-lucide', 'pause');
634
+ playInterval = setInterval(() => { if(currentIteration < maxIterations){ currentIteration++; render(); } else { isPlaying=false; clearInterval(playInterval); document.getElementById('play-text').textContent = "Play"; document.getElementById('play-icon').setAttribute('data-lucide', 'play'); } }, 1500);
635
+ } else { clearInterval(playInterval); document.getElementById('play-text').textContent = "Play"; document.getElementById('play-icon').setAttribute('data-lucide', 'play'); }
636
+ lucide.createIcons();
637
+ };
638
+ document.getElementById('next-btn').onclick = () => { if(currentIteration < maxIterations) { currentIteration++; render(); } };
639
+ document.getElementById('prev-btn').onclick = () => { if(currentIteration > 0) { currentIteration--; render(); } };
640
+ document.getElementById('reset-btn').onclick = () => { currentIteration = 0; render(); };
641
+ lucide.createIcons();
642
+ };
643
+ </script>
644
+ </body>
645
+ </html>
templates/xbost-graph-three.html ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>XGB-CORE // Gradient Boosting Simulator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <!-- Added OrbitControls for user interaction -->
10
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
11
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+ <style>
13
+ :root {
14
+ --bg-color: #05080a;
15
+ --accent-cyan: #00f3ff;
16
+ --accent-pink: #ff00ff;
17
+ --accent-purple: #7000ff;
18
+ --glass: rgba(10, 15, 20, 0.7);
19
+ }
20
+
21
+ body {
22
+ background-color: var(--bg-color);
23
+ color: #e2e8f0;
24
+ font-family: 'Space Grotesk', sans-serif;
25
+ overflow: hidden; /* Main body hidden, scroll happens in panels */
26
+ margin: 0;
27
+ }
28
+
29
+ .glass-panel {
30
+ background: var(--glass);
31
+ backdrop-filter: blur(12px);
32
+ border: 1px solid rgba(255, 255, 255, 0.1);
33
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.8);
34
+ }
35
+
36
+ .glow-cyan { text-shadow: 0 0 10px var(--accent-cyan); }
37
+ .glow-pink { text-shadow: 0 0 10px var(--accent-pink); }
38
+
39
+ .border-glow-cyan { border-color: var(--accent-cyan); box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); }
40
+
41
+ input[type="range"] {
42
+ -webkit-appearance: none;
43
+ background: rgba(255, 255, 255, 0.1);
44
+ height: 4px;
45
+ border-radius: 2px;
46
+ }
47
+
48
+ input[type="range"]::-webkit-slider-thumb {
49
+ -webkit-appearance: none;
50
+ width: 14px;
51
+ height: 14px;
52
+ background: var(--accent-cyan);
53
+ border-radius: 50%;
54
+ cursor: pointer;
55
+ box-shadow: 0 0 10px var(--accent-cyan);
56
+ }
57
+
58
+ .gradient-text {
59
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple), var(--accent-pink));
60
+ -webkit-background-clip: text;
61
+ -webkit-text-fill-color: transparent;
62
+ }
63
+
64
+ .btn-action {
65
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
66
+ position: relative;
67
+ overflow: hidden;
68
+ }
69
+
70
+ .btn-action:hover {
71
+ transform: translateY(-2px);
72
+ filter: brightness(1.2);
73
+ }
74
+
75
+ canvas { display: block; touch-action: none; }
76
+
77
+ @keyframes pulse {
78
+ 0%, 100% { opacity: 1; }
79
+ 50% { opacity: 0.5; }
80
+ }
81
+
82
+ .status-pulse { animation: pulse 2s infinite; }
83
+
84
+ /* Scrollbar */
85
+ ::-webkit-scrollbar { width: 5px; }
86
+ ::-webkit-scrollbar-track { background: transparent; }
87
+ ::-webkit-scrollbar-thumb { background: #1a1a1a; border-radius: 10px; }
88
+ </style>
89
+ </head>
90
+ <body class="h-screen w-screen flex flex-col supports-[height:100dvh]:h-[100dvh]">
91
+
92
+ <!-- Header -->
93
+ <header class="h-16 shrink-0 flex items-center justify-between px-4 lg:px-8 border-b border-white/10 z-50 glass-panel relative">
94
+ <div class="flex items-center gap-2 lg:gap-4 z-10">
95
+ <div class="w-8 h-8 bg-gradient-to-br from-cyan-400 to-purple-600 rounded-lg flex items-center justify-center font-bold text-black shadow-lg shadow-cyan-500/20 shrink-0">X</div>
96
+ <div>
97
+ <h1 class="text-lg lg:text-xl font-bold gradient-text tracking-tighter leading-none">XGB-CORE <span class="hidden sm:inline">//</span> <span class="text-white/80 block sm:inline text-xs sm:text-lg">REGR-01</span></h1>
98
+ <p class="hidden sm:block text-[10px] text-white/40 uppercase tracking-[0.2em]">Gradient Boosting Machine Visualizer</p>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Centered Button - Position adjusted for mobile -->
103
+ <div class="absolute left-1/2 -translate-x-1/2 flex items-center top-1/2 -translate-y-1/2">
104
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
105
+ <a href="/xgboost-regression" 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-1.5 px-4 lg:py-2 lg:px-6 rounded-xl text-xs lg:text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider whitespace-nowrap">
106
+ Back to Core
107
+ </a>
108
+ </div>
109
+
110
+ <div class="hidden md:flex items-center gap-6 text-xs uppercase tracking-widest text-white/60">
111
+ <div class="flex gap-2 items-center">
112
+ <span class="w-2 h-2 rounded-full bg-cyan-500 status-pulse"></span>
113
+ <span>System Ready</span>
114
+ </div>
115
+ <div id="clock">00:00:00</div>
116
+ </div>
117
+ <!-- Mobile Menu Placeholder / Status dot for mobile -->
118
+ <div class="md:hidden">
119
+ <span class="w-2 h-2 rounded-full bg-cyan-500 status-pulse block"></span>
120
+ </div>
121
+ </header>
122
+
123
+ <main class="flex-1 flex flex-col lg:flex-row overflow-hidden relative">
124
+ <!-- Left Column: 3D Viewport -->
125
+ <!-- Mobile: Fixed height (45% of viewport), Desktop: Flex grow -->
126
+ <div class="relative w-full h-[40vh] lg:h-full lg:flex-1 order-1 shrink-0">
127
+ <div id="canvas-container" class="w-full h-full bg-[#030507]"></div>
128
+
129
+ <!-- HUD Overlays - Adjusted positioning for mobile -->
130
+ <div class="absolute top-2 left-2 lg:top-6 lg:left-6 pointer-events-none z-10">
131
+ <div class="glass-panel p-2 lg:p-4 rounded-xl border-l-4 border-l-cyan-500 min-w-[140px] lg:min-w-[200px]">
132
+ <h3 class="text-white/40 text-[8px] lg:text-[10px] uppercase mb-1">Training Status</h3>
133
+ <div id="iteration-display" class="text-xl lg:text-3xl font-bold font-mono">TREE: 00</div>
134
+ <div id="loss-display" class="text-cyan-400 text-[10px] lg:text-xs mt-1">MSE: 0.0000</div>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="absolute bottom-2 left-2 lg:bottom-6 lg:left-6 pointer-events-none hidden sm:block">
139
+ <div class="glass-panel p-2 lg:p-3 rounded-xl text-[10px] text-white/40 uppercase leading-relaxed">
140
+ <p>Vector Map: <span class="text-white">Active</span></p>
141
+ <p>Subsampling: <span class="text-white">1.0 (Exact)</span></p>
142
+ <p>View: <span class="text-white">User Controlled</span></p>
143
+ </div>
144
+ </div>
145
+
146
+ <div id="coord-hud" class="absolute top-2 right-2 lg:top-6 lg:right-6 pointer-events-none glass-panel p-2 lg:p-3 rounded-xl text-right z-10">
147
+ <div class="text-[8px] lg:text-[10px] text-white/40 mb-1">CURSOR DATA</div>
148
+ <div id="coord-readout" class="font-mono text-[10px] lg:text-xs text-cyan-500">X: 0.00 | Y: 0.00 | Z: 0.00</div>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Right Column: Sidebar -->
153
+ <!-- Mobile: Scrollable remaining space, Desktop: Fixed width sidebar -->
154
+ <aside class="w-full lg:w-[400px] bg-[var(--bg-color)] lg:bg-transparent glass-panel border-t lg:border-t-0 lg:border-l border-white/10 p-4 lg:p-6 flex flex-col gap-4 lg:gap-6 overflow-y-auto flex-1 lg:flex-none order-2 pb-20 lg:pb-6">
155
+
156
+ <!-- Controls Section -->
157
+ <section>
158
+ <h2 class="text-white/40 text-[10px] font-bold uppercase tracking-widest mb-4">Hyper-Parameters</h2>
159
+ <div class="flex flex-col gap-5">
160
+ <!-- Added Speed Controller -->
161
+ <div class="space-y-2">
162
+ <div class="flex justify-between text-xs">
163
+ <span class="text-white/80">Training Delay (ms)</span>
164
+ <span id="speed-val" class="text-cyan-400 font-mono">100</span>
165
+ </div>
166
+ <input type="range" id="speed-input" min="10" max="1000" step="10" value="100" class="w-full">
167
+ </div>
168
+
169
+ <div class="space-y-2">
170
+ <div class="flex justify-between text-xs">
171
+ <span class="text-white/80">Learning Rate (η)</span>
172
+ <span id="lr-val" class="text-cyan-400 font-mono">0.1</span>
173
+ </div>
174
+ <input type="range" id="lr-input" min="0.01" max="1.0" step="0.01" value="0.1" class="w-full">
175
+ </div>
176
+
177
+ <div class="space-y-2">
178
+ <div class="flex justify-between text-xs">
179
+ <span class="text-white/80">Max Tree Depth</span>
180
+ <span id="depth-val" class="text-cyan-400 font-mono">3</span>
181
+ </div>
182
+ <input type="range" id="depth-input" min="1" max="6" step="1" value="3" class="w-full">
183
+ </div>
184
+
185
+ <div class="space-y-2">
186
+ <div class="flex justify-between text-xs">
187
+ <span class="text-white/80">Regularization (λ)</span>
188
+ <span id="lambda-val" class="text-cyan-400 font-mono">1.0</span>
189
+ </div>
190
+ <input type="range" id="lambda-input" min="0" max="10" step="0.5" value="1.0" class="w-full">
191
+ </div>
192
+ </div>
193
+ </section>
194
+
195
+ <!-- Playback -->
196
+ <section class="bg-white/5 rounded-xl p-4 border border-white/10">
197
+ <div class="grid grid-cols-2 gap-3">
198
+ <button id="start-btn" class="btn-action bg-cyan-600 hover:bg-cyan-500 text-black font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 text-sm lg:text-base">
199
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M4.5 3.5v13l11-6.5-11-6.5z"/></svg>
200
+ TRAIN
201
+ </button>
202
+ <button id="step-btn" class="btn-action bg-white/10 hover:bg-white/20 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 text-sm lg:text-base">
203
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>
204
+ STEP
205
+ </button>
206
+ <button id="reset-btn" class="btn-action col-span-2 bg-pink-600/20 hover:bg-pink-600 text-pink-500 hover:text-white border border-pink-500/50 py-2 rounded-lg mt-1 font-bold text-sm lg:text-base">
207
+ RESET KERNEL
208
+ </button>
209
+ </div>
210
+ </section>
211
+
212
+ <!-- Scenario Selector -->
213
+ <section>
214
+ <h2 class="text-white/40 text-[10px] font-bold uppercase tracking-widest mb-4">Manifold Scenario</h2>
215
+ <div class="grid grid-cols-1 gap-2">
216
+ <button class="scenario-btn active border-glow-cyan bg-cyan-500/10 text-cyan-400 p-3 rounded-lg text-left text-xs transition-all" data-scene="sine">
217
+ <div class="font-bold">Interlocking Sine Wave</div>
218
+ <div class="opacity-60">High frequency non-linearity</div>
219
+ </button>
220
+ <button class="scenario-btn border border-white/5 hover:border-white/20 bg-white/5 p-3 rounded-lg text-left text-xs transition-all" data-scene="saddle">
221
+ <div class="font-bold">Hyperbolic Paraboloid</div>
222
+ <div class="opacity-60">Saddle-point gradient challenge</div>
223
+ </button>
224
+ <button class="scenario-btn border border-white/5 hover:border-white/20 bg-white/5 p-3 rounded-lg text-left text-xs transition-all" data-scene="stairs">
225
+ <div class="font-bold">Step Discontinuity</div>
226
+ <div class="opacity-60">Sharp edges, axis-aligned splits</div>
227
+ </button>
228
+ </div>
229
+ </section>
230
+
231
+ <!-- Real-time Stats -->
232
+ <section class="mt-auto pb-4 lg:pb-0">
233
+ <div class="grid grid-cols-2 gap-2 text-[10px] uppercase font-mono">
234
+ <div class="p-2 bg-white/5 rounded border border-white/10">
235
+ <div class="text-white/40">Trees Fit</div>
236
+ <div id="stat-trees" class="text-white text-lg font-bold">0</div>
237
+ </div>
238
+ <div class="p-2 bg-white/5 rounded border border-white/10">
239
+ <div class="text-white/40">Convergence</div>
240
+ <div id="stat-conv" class="text-white text-lg font-bold">---</div>
241
+ </div>
242
+ </div>
243
+ </section>
244
+ </aside>
245
+ </main>
246
+
247
+ <!-- Bottom Panel - Modified to stack on mobile, hidden details on tiny screens -->
248
+ <footer class="glass-panel border-t border-white/10 p-4 lg:px-8 flex flex-col lg:flex-row items-center justify-between text-xs z-50 shrink-0 gap-4 lg:gap-0">
249
+ <div class="flex flex-wrap justify-center lg:justify-start gap-4 lg:gap-12 w-full lg:w-auto">
250
+ <div class="flex flex-col items-center lg:items-start">
251
+ <span class="text-white/40 uppercase text-[9px] mb-1">Mechanism</span>
252
+ <span class="text-cyan-400">Additive Basis Expansion</span>
253
+ </div>
254
+ <div class="flex flex-col items-center lg:items-start">
255
+ <span class="text-white/40 uppercase text-[9px] mb-1">Loss Function</span>
256
+ <span class="text-purple-400">Sum of Squares (L2)</span>
257
+ </div>
258
+ <div class="flex flex-col items-center lg:items-start">
259
+ <span class="text-white/40 uppercase text-[9px] mb-1">Regularizer</span>
260
+ <span class="text-pink-400">Shrinkage + Complexity Penalties</span>
261
+ </div>
262
+ </div>
263
+ <div class="max-w-md text-white/50 italic text-[11px] leading-tight text-center lg:text-right hidden sm:block">
264
+ <strong class="text-white not-italic uppercase text-[9px]">Pro Tip:</strong>
265
+ Adjust **Training Delay** to slow down or speed up the recursive gradient minimization process.
266
+ </div>
267
+ </footer>
268
+
269
+ <!-- Victory Modal -->
270
+ <div id="victory-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md z-[100] hidden items-center justify-center p-6">
271
+ <div class="glass-panel max-w-lg w-full rounded-2xl border-2 border-cyan-500 p-8 text-center relative">
272
+ <button id="close-modal" class="absolute top-4 right-4 text-white/40 hover:text-white">
273
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
274
+ </button>
275
+ <div class="w-16 h-16 bg-cyan-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
276
+ <svg class="w-8 h-8 text-cyan-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5z"/></svg>
277
+ </div>
278
+ <h2 class="text-2xl lg:text-3xl font-bold mb-2 tracking-tighter italic">OPTIMIZATION COMPLETE</h2>
279
+ <p class="text-white/60 mb-8 text-sm lg:text-base">The XGB-CORE engine has successfully minimized residuals for the current manifold.</p>
280
+ <div class="grid grid-cols-3 gap-4 mb-8">
281
+ <div class="p-3 bg-white/5 rounded-xl border border-white/10">
282
+ <div class="text-[9px] text-white/40 uppercase">Total Estimators</div>
283
+ <div id="final-trees" class="text-xl font-bold text-cyan-400">50</div>
284
+ </div>
285
+ <div id="final-loss-container" class="p-3 bg-white/5 rounded-xl border border-white/10">
286
+ <div class="text-[9px] text-white/40 uppercase">Global Loss</div>
287
+ <div id="final-loss" class="text-xl font-bold text-pink-400">0.0021</div>
288
+ </div>
289
+ <div class="p-3 bg-white/5 rounded-xl border border-white/10">
290
+ <div class="text-[9px] text-white/40 uppercase">Score</div>
291
+ <div class="text-xl font-bold text-white">99.8%</div>
292
+ </div>
293
+ </div>
294
+ <button onclick="document.getElementById('victory-modal').style.display='none'" class="w-full bg-cyan-600 hover:bg-cyan-500 text-black font-bold py-4 rounded-xl shadow-lg shadow-cyan-500/20 transition-all">
295
+ ACKNOWLEDGE
296
+ </button>
297
+ </div>
298
+ </div>
299
+
300
+ <script>
301
+ /**
302
+ * XGBOOST REGRESSION CORE LOGIC
303
+ */
304
+ class DecisionTree {
305
+ constructor(maxDepth = 3) {
306
+ this.maxDepth = maxDepth;
307
+ this.tree = null;
308
+ }
309
+
310
+ fit(X, y) {
311
+ this.tree = this.buildTree(X, y, 0);
312
+ }
313
+
314
+ buildTree(X, y, depth) {
315
+ if (depth >= this.maxDepth || X.length <= 2) {
316
+ return { leaf: true, value: y.reduce((a, b) => a + b, 0) / y.length };
317
+ }
318
+
319
+ let bestSplit = null;
320
+ let minMse = Infinity;
321
+
322
+ for (let featureIdx of [0, 1]) {
323
+ const featureValues = X.map(p => p[featureIdx]);
324
+ const uniqueValues = [...new Set(featureValues)].sort((a,b) => a-b);
325
+ const candidates = uniqueValues.filter((_, i) => i % Math.max(1, Math.floor(uniqueValues.length / 10)) === 0);
326
+
327
+ for (let threshold of candidates) {
328
+ const leftIndices = X.map((p, i) => p[featureIdx] < threshold ? i : null).filter(i => i !== null);
329
+ const rightIndices = X.map((p, i) => p[featureIdx] >= threshold ? i : null).filter(i => i !== null);
330
+
331
+ if (leftIndices.length === 0 || rightIndices.length === 0) continue;
332
+
333
+ const leftY = leftIndices.map(i => y[i]);
334
+ const rightY = rightIndices.map(i => y[i]);
335
+ const mse = this.calculateMSE(leftY) * leftY.length + this.calculateMSE(rightY) * rightY.length;
336
+
337
+ if (mse < minMse) {
338
+ minMse = mse;
339
+ bestSplit = { featureIdx, threshold, leftIndices, rightIndices };
340
+ }
341
+ }
342
+ }
343
+
344
+ if (!bestSplit) return { leaf: true, value: y.reduce((a, b) => a + b, 0) / y.length };
345
+
346
+ return {
347
+ leaf: false,
348
+ featureIdx: bestSplit.featureIdx,
349
+ threshold: bestSplit.threshold,
350
+ left: this.buildTree(bestSplit.leftIndices.map(i => X[i]), bestSplit.leftIndices.map(i => y[i]), depth + 1),
351
+ right: this.buildTree(bestSplit.rightIndices.map(i => X[i]), bestSplit.rightIndices.map(i => y[i]), depth + 1)
352
+ };
353
+ }
354
+
355
+ calculateMSE(y) {
356
+ if (y.length === 0) return 0;
357
+ const mean = y.reduce((a, b) => a + b, 0) / y.length;
358
+ return y.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / y.length;
359
+ }
360
+
361
+ predictSingle(p) {
362
+ let node = this.tree;
363
+ while (!node.leaf) {
364
+ if (p[node.featureIdx] < node.threshold) node = node.left;
365
+ else node = node.right;
366
+ }
367
+ return node.value;
368
+ }
369
+ }
370
+
371
+ class XGBoost {
372
+ constructor() {
373
+ this.trees = [];
374
+ this.learningRate = 0.1;
375
+ this.maxDepth = 3;
376
+ this.basePrediction = 0;
377
+ }
378
+
379
+ init(y) {
380
+ this.trees = [];
381
+ this.basePrediction = y.reduce((a, b) => a + b, 0) / y.length;
382
+ }
383
+
384
+ trainStep(X, y) {
385
+ const currentPredictions = this.predictAll(X);
386
+ const residuals = y.map((val, i) => val - currentPredictions[i]);
387
+ const tree = new DecisionTree(this.maxDepth);
388
+ tree.fit(X, residuals);
389
+ this.trees.push(tree);
390
+ return this.calculateMSE(y, this.predictAll(X));
391
+ }
392
+
393
+ predict(p) {
394
+ let pred = this.basePrediction;
395
+ for (let tree of this.trees) {
396
+ pred += this.learningRate * tree.predictSingle(p);
397
+ }
398
+ return pred;
399
+ }
400
+
401
+ predictAll(X) {
402
+ return X.map(p => this.predict(p));
403
+ }
404
+
405
+ calculateMSE(yTrue, yPred) {
406
+ return yTrue.reduce((acc, val, i) => acc + Math.pow(val - yPred[i], 2), 0) / yTrue.length;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * THREE.JS VISUALIZATION ENGINE
412
+ */
413
+ const SCENE_CONFIG = {
414
+ sine: (x, z) => Math.sin(x * 2) * Math.cos(z * 2) * 0.5,
415
+ saddle: (x, z) => (x*x - z*z) * 0.4,
416
+ stairs: (x, z) => {
417
+ const stepX = Math.floor(x * 3) / 3;
418
+ const stepZ = Math.floor(z * 3) / 3;
419
+ return (stepX + stepZ) * 0.3;
420
+ }
421
+ };
422
+
423
+ let scene, camera, renderer, surface, dataPoints, pointsGroup, controls;
424
+ let container = document.getElementById('canvas-container');
425
+ let xgb = new XGBoost();
426
+ let trainingData = { X: [], y: [] };
427
+ let isTraining = false;
428
+ let currentScene = 'sine';
429
+ let trainingDelay = 100; // Delay between steps
430
+
431
+ function initThree() {
432
+ scene = new THREE.Scene();
433
+ camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
434
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
435
+ renderer.setSize(container.clientWidth, container.clientHeight);
436
+ renderer.setPixelRatio(window.devicePixelRatio);
437
+ container.appendChild(renderer.domElement);
438
+
439
+ // Added OrbitControls
440
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
441
+ controls.enableDamping = true;
442
+ controls.dampingFactor = 0.05;
443
+
444
+ const ambientLight = new THREE.AmbientLight(0x404040, 2);
445
+ scene.add(ambientLight);
446
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
447
+ directionalLight.position.set(5, 10, 7.5);
448
+ scene.add(directionalLight);
449
+
450
+ const grid = new THREE.GridHelper(4, 20, 0x1a1a1a, 0x1a1a1a);
451
+ grid.position.y = -0.5;
452
+ scene.add(grid);
453
+
454
+ setupData();
455
+ createSurface();
456
+
457
+ camera.position.set(3, 2.5, 3);
458
+ camera.lookAt(0, 0, 0);
459
+
460
+ animate();
461
+ }
462
+
463
+ function setupData() {
464
+ if (pointsGroup) scene.remove(pointsGroup);
465
+ pointsGroup = new THREE.Group();
466
+
467
+ trainingData.X = [];
468
+ trainingData.y = [];
469
+
470
+ const count = 400;
471
+ const geom = new THREE.BufferGeometry();
472
+ const pos = new Float32Array(count * 3);
473
+ const colors = new Float32Array(count * 3);
474
+
475
+ for (let i = 0; i < count; i++) {
476
+ const x = (Math.random() - 0.5) * 4;
477
+ const z = (Math.random() - 0.5) * 4;
478
+ const y = SCENE_CONFIG[currentScene](x, z) + (Math.random() - 0.5) * 0.1;
479
+
480
+ trainingData.X.push([x, z]);
481
+ trainingData.y.push(y);
482
+
483
+ pos[i * 3] = x;
484
+ pos[i * 3 + 1] = y;
485
+ pos[i * 3 + 2] = z;
486
+
487
+ colors[i * 3] = 1;
488
+ colors[i * 3 + 1] = 0.5 + y;
489
+ colors[i * 3 + 2] = 1;
490
+ }
491
+
492
+ geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
493
+ geom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
494
+
495
+ const mat = new THREE.PointsMaterial({ size: 0.05, vertexColors: true, transparent: true, opacity: 0.8 });
496
+ dataPoints = new THREE.Points(geom, mat);
497
+ pointsGroup.add(dataPoints);
498
+ scene.add(pointsGroup);
499
+
500
+ xgb.init(trainingData.y);
501
+ }
502
+
503
+ function createSurface() {
504
+ if (surface) scene.remove(surface);
505
+
506
+ const segments = 40;
507
+ const geom = new THREE.PlaneGeometry(4, 4, segments, segments);
508
+ geom.rotateX(-Math.PI / 2);
509
+
510
+ const mat = new THREE.MeshPhongMaterial({
511
+ color: 0x00f3ff,
512
+ wireframe: true,
513
+ transparent: true,
514
+ opacity: 0.3,
515
+ side: THREE.DoubleSide
516
+ });
517
+
518
+ surface = new THREE.Mesh(geom, mat);
519
+ scene.add(surface);
520
+ updateSurface();
521
+ }
522
+
523
+ function updateSurface() {
524
+ const positions = surface.geometry.attributes.position.array;
525
+ for (let i = 0; i < positions.length; i += 3) {
526
+ const x = positions[i];
527
+ const z = positions[i + 2];
528
+ positions[i + 1] = xgb.predict([x, z]);
529
+ }
530
+ surface.geometry.attributes.position.needsUpdate = true;
531
+ }
532
+
533
+ function animate() {
534
+ requestAnimationFrame(animate);
535
+ controls.update(); // Required for damping
536
+ renderer.render(scene, camera);
537
+ }
538
+
539
+ /**
540
+ * APP UI & STATE
541
+ */
542
+ const ui = {
543
+ speedInput: document.getElementById('speed-input'),
544
+ speedVal: document.getElementById('speed-val'),
545
+ lrInput: document.getElementById('lr-input'),
546
+ lrVal: document.getElementById('lr-val'),
547
+ depthInput: document.getElementById('depth-input'),
548
+ depthVal: document.getElementById('depth-val'),
549
+ lambdaInput: document.getElementById('lambda-input'),
550
+ lambdaVal: document.getElementById('lambda-val'),
551
+ startBtn: document.getElementById('start-btn'),
552
+ stepBtn: document.getElementById('step-btn'),
553
+ resetBtn: document.getElementById('reset-btn'),
554
+ iterDisplay: document.getElementById('iteration-display'),
555
+ lossDisplay: document.getElementById('loss-display'),
556
+ statTrees: document.getElementById('stat-trees'),
557
+ statConv: document.getElementById('stat-conv'),
558
+ finalTrees: document.getElementById('final-trees'),
559
+ finalLoss: document.getElementById('final-loss'),
560
+ victoryModal: document.getElementById('victory-modal'),
561
+ scenarioBtns: document.querySelectorAll('.scenario-btn'),
562
+ coordReadout: document.getElementById('coord-readout'),
563
+ clock: document.getElementById('clock')
564
+ };
565
+
566
+ // Speed Control logic
567
+ ui.speedInput.addEventListener('input', (e) => {
568
+ trainingDelay = parseInt(e.target.value);
569
+ ui.speedVal.innerText = e.target.value;
570
+ if (isTraining) {
571
+ // Restart interval with new speed
572
+ clearInterval(trainInterval);
573
+ trainInterval = setInterval(performStep, trainingDelay);
574
+ }
575
+ });
576
+
577
+ ui.lrInput.addEventListener('input', (e) => {
578
+ xgb.learningRate = parseFloat(e.target.value);
579
+ ui.lrVal.innerText = e.target.value;
580
+ });
581
+
582
+ ui.depthInput.addEventListener('input', (e) => {
583
+ xgb.maxDepth = parseInt(e.target.value);
584
+ ui.depthVal.innerText = e.target.value;
585
+ });
586
+
587
+ ui.lambdaInput.addEventListener('input', (e) => {
588
+ ui.lambdaVal.innerText = e.target.value;
589
+ });
590
+
591
+ ui.scenarioBtns.forEach(btn => {
592
+ btn.addEventListener('click', () => {
593
+ ui.scenarioBtns.forEach(b => {
594
+ b.classList.remove('active', 'border-glow-cyan', 'text-cyan-400');
595
+ b.classList.add('border-white/5', 'bg-white/5');
596
+ });
597
+ btn.classList.add('active', 'border-glow-cyan', 'text-cyan-400');
598
+ btn.classList.remove('border-white/5', 'bg-white/5');
599
+
600
+ currentScene = btn.dataset.scene;
601
+ resetSimulation();
602
+ });
603
+ });
604
+
605
+ function performStep() {
606
+ if (xgb.trees.length >= 60) {
607
+ stopTraining();
608
+ showVictory();
609
+ return;
610
+ }
611
+
612
+ const loss = xgb.trainStep(trainingData.X, trainingData.y);
613
+ updateSurface();
614
+
615
+ ui.iterDisplay.innerText = `TREE: ${xgb.trees.length.toString().padStart(2, '0')}`;
616
+ ui.lossDisplay.innerText = `MSE: ${loss.toFixed(6)}`;
617
+ ui.statTrees.innerText = xgb.trees.length;
618
+
619
+ const conv = (1 / (1 + loss)).toFixed(2);
620
+ ui.statConv.innerText = `${(conv * 100).toFixed(0)}%`;
621
+
622
+ if (loss < 0.0005) {
623
+ stopTraining();
624
+ showVictory();
625
+ }
626
+ }
627
+
628
+ let trainInterval;
629
+ function startTraining() {
630
+ if (isTraining) {
631
+ stopTraining();
632
+ } else {
633
+ isTraining = true;
634
+ ui.startBtn.innerHTML = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> PAUSE`;
635
+ ui.startBtn.classList.replace('bg-cyan-600', 'bg-purple-600');
636
+ trainInterval = setInterval(performStep, trainingDelay);
637
+ }
638
+ }
639
+
640
+ function stopTraining() {
641
+ isTraining = false;
642
+ ui.startBtn.innerHTML = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M4.5 3.5v13l11-6.5-11-6.5z"/></svg> TRAIN`;
643
+ ui.startBtn.classList.replace('bg-purple-600', 'bg-cyan-600');
644
+ clearInterval(trainInterval);
645
+ }
646
+
647
+ function resetSimulation() {
648
+ stopTraining();
649
+ setupData();
650
+ updateSurface();
651
+ ui.iterDisplay.innerText = `TREE: 00`;
652
+ ui.lossDisplay.innerText = `MSE: 0.0000`;
653
+ ui.statTrees.innerText = '0';
654
+ ui.statConv.innerText = '---';
655
+ }
656
+
657
+ function showVictory() {
658
+ const lastLoss = ui.lossDisplay.innerText.split(': ')[1];
659
+ ui.finalTrees.innerText = xgb.trees.length;
660
+ ui.finalLoss.innerText = lastLoss;
661
+ ui.victoryModal.style.display = 'flex';
662
+ }
663
+
664
+ ui.startBtn.addEventListener('click', startTraining);
665
+ ui.stepBtn.addEventListener('click', performStep);
666
+ ui.resetBtn.addEventListener('click', resetSimulation);
667
+ document.getElementById('close-modal').addEventListener('click', () => {
668
+ ui.victoryModal.style.display = 'none';
669
+ });
670
+
671
+ window.addEventListener('resize', () => {
672
+ camera.aspect = container.clientWidth / container.clientHeight;
673
+ camera.updateProjectionMatrix();
674
+ renderer.setSize(container.clientWidth, container.clientHeight);
675
+ });
676
+
677
+ container.addEventListener('mousemove', (e) => {
678
+ const rect = container.getBoundingClientRect();
679
+ const x = ((e.clientX - rect.left) / container.clientWidth) * 4 - 2;
680
+ const z = ((e.clientY - rect.top) / container.clientHeight) * 4 - 2;
681
+ const y = xgb.predict([x, z]);
682
+ ui.coordReadout.innerText = `X: ${x.toFixed(2)} | Y: ${y.toFixed(2)} | Z: ${z.toFixed(2)}`;
683
+ });
684
+
685
+ setInterval(() => {
686
+ const now = new Date();
687
+ ui.clock.innerText = now.toTimeString().split(' ')[0];
688
+ }, 1000);
689
+
690
+ window.onload = initThree;
691
+ </script>
692
+ </body>
693
+ </html>