deedrop1140 commited on
Commit
c61ce8c
ยท
verified ยท
1 Parent(s): 5afe614

Upload 16 files

Browse files
templates/Apriori-Algorithm.html CHANGED
@@ -192,6 +192,53 @@
192
  <div class="container">
193
  <h1>๐Ÿ›’ Study Guide: The Apriori Algorithm</h1>
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <h2>๐Ÿ”น Core Concepts</h2>
196
  <div class="story-apriori">
197
  <p><strong>Story-style intuition: The Supermarket Detective</strong></p>
 
192
  <div class="container">
193
  <h1>๐Ÿ›’ Study Guide: The Apriori Algorithm</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="/apriori-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-apriori">
244
  <p><strong>Story-style intuition: The Supermarket Detective</strong></p>
templates/Eclat-Algorithm.html CHANGED
@@ -192,6 +192,53 @@
192
  <div class="container">
193
  <h1>โšก Study Guide: The Eclat Algorithm</h1>
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <h2>๐Ÿ”น Core Concepts</h2>
196
  <div class="story-eclat">
197
  <p><strong>Story-style intuition: The Efficient Librarian</strong></p>
 
192
  <div class="container">
193
  <h1>โšก Study Guide: The Eclat Algorithm</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="/eclat-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-eclat">
244
  <p><strong>Story-style intuition: The Efficient Librarian</strong></p>
templates/Gradient-Boosting.html CHANGED
@@ -151,6 +151,53 @@
151
  <div class="container">
152
  <h1>๐Ÿ“˜ Study Guide: Gradient Boosting Regression (GBR)</h1>
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  <h2>๐Ÿ”น Core Concepts</h2>
155
  <div class="story">
156
  <p><strong>Story-style intuition:</strong></p>
 
151
  <div class="container">
152
  <h1>๐Ÿ“˜ Study Guide: Gradient Boosting Regression (GBR)</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="/gradient-boosting-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
  <h2>๐Ÿ”น Core Concepts</h2>
202
  <div class="story">
203
  <p><strong>Story-style intuition:</strong></p>
templates/Hierarchical-Clustering.html CHANGED
@@ -174,6 +174,53 @@
174
  <div class="container">
175
  <h1>๐ŸŒณ Study Guide: Hierarchical Clustering</h1>
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  <h2>๐Ÿ”น Core Concepts</h2>
178
  <div class="story">
179
  <p><strong>Story-style intuition: Organizing a Family Reunion</strong></p>
 
174
  <div class="container">
175
  <h1>๐ŸŒณ Study Guide: Hierarchical Clustering</h1>
176
 
177
+
178
+ <!-- button -->
179
+ <div>
180
+ <!-- Audio Element -->
181
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
182
+ but since this is triggered by a click, it should work fine. -->
183
+
184
+
185
+ <a
186
+ href="/hierarchical-three"
187
+ target="_blank"
188
+ onclick="playSound()"
189
+ class="
190
+ cursor-pointer
191
+ inline-block
192
+ relative
193
+ bg-blue-500
194
+ text-white
195
+ font-bold
196
+ py-4 px-8
197
+ rounded-xl
198
+ text-2xl
199
+ transition-all
200
+ duration-150
201
+
202
+ /* 3D Effect (Hard Shadow) */
203
+ shadow-[0_8px_0_rgb(29,78,216)]
204
+
205
+ /* Pressed State (Move down & remove shadow) */
206
+ active:shadow-none
207
+ active:translate-y-[8px]
208
+ ">
209
+ Tap Me!
210
+ </a>
211
+ </div>
212
+
213
+ <script>
214
+ function playSound() {
215
+ const audio = document.getElementById("clickSound");
216
+ if (audio) {
217
+ audio.currentTime = 0;
218
+ audio.play().catch(e => console.log("Audio play failed:", e));
219
+ }
220
+ }
221
+ </script>
222
+ <!-- button -->
223
+
224
  <h2>๐Ÿ”น Core Concepts</h2>
225
  <div class="story">
226
  <p><strong>Story-style intuition: Organizing a Family Reunion</strong></p>
templates/Hierarchical-three.html ADDED
@@ -0,0 +1,896 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Hierarchical Clustering 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
+ muted: {
30
+ DEFAULT: "hsl(var(--muted))",
31
+ foreground: "hsl(var(--muted-foreground))",
32
+ },
33
+ accent: {
34
+ DEFAULT: "hsl(var(--accent))",
35
+ foreground: "hsl(var(--accent-foreground))",
36
+ },
37
+ canvas: {
38
+ DEFAULT: "hsl(var(--canvas))",
39
+ grid: "hsl(var(--canvas-grid))",
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <!-- React & Libraries -->
48
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
49
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
50
+ <script src="https://unpkg.com/framer-motion@10.16.4/dist/framer-motion.js"></script>
51
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
52
+ <!-- Lucide Icons -->
53
+ <script src="https://unpkg.com/lucide@latest"></script>
54
+
55
+ <style>
56
+ :root {
57
+ --background: 224 71% 4%;
58
+ --foreground: 213 31% 91%;
59
+ --primary: 263.4 70% 50.4%;
60
+ --primary-foreground: 210 40% 98%;
61
+ --secondary: 222.2 47.4% 11.2%;
62
+ --secondary-foreground: 210 40% 98%;
63
+ --muted: 217.2 32.6% 17.5%;
64
+ --muted-foreground: 215 20.2% 65.1%;
65
+ --accent: 330 80% 60%;
66
+ --accent-foreground: 210 40% 98%;
67
+ --border: 217.2 32.6% 17.5%;
68
+ --canvas: 224 71% 5%;
69
+ --canvas-grid: 217.2 32.6% 17.5%;
70
+ }
71
+
72
+ body {
73
+ background-color: hsl(var(--background));
74
+ color: hsl(var(--foreground));
75
+ font-family: system-ui, -apple-system, sans-serif;
76
+ }
77
+
78
+ .glass-card {
79
+ background: rgba(30, 41, 59, 0.5);
80
+ backdrop-filter: blur(12px);
81
+ border: 1px solid hsl(var(--border));
82
+ border-radius: 1rem;
83
+ }
84
+
85
+ .gradient-text {
86
+ background: linear-gradient(to right, hsl(var(--primary)), hsl(var(--accent)));
87
+ -webkit-background-clip: text;
88
+ -webkit-text-fill-color: transparent;
89
+ }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <div id="root"></div>
94
+
95
+ <script type="text/babel">
96
+ const { useState, useEffect, useCallback, useMemo } = React;
97
+ const { motion, AnimatePresence } = window.Motion;
98
+
99
+ // --- UTILS ---
100
+ function cn(...classes) {
101
+ return classes.filter(Boolean).join(' ');
102
+ }
103
+
104
+ // --- ICONS ---
105
+ // Using Lucide React icons manually since we're in a standalone environment
106
+ const Icon = ({ name, size = 24, className, ...props }) => {
107
+ const LucideIcon = window.lucide && window.lucide.icons ? window.lucide.icons[name] : null;
108
+ if (!LucideIcon) return null;
109
+
110
+ // Convert the lucide icon object to an SVG string/element
111
+ // Since lucide.icons[name] gives us the JS representation, we render an svg manually for React
112
+ // Actually, simplest way in this specific standalone babel setup without full build chain:
113
+ // We'll just SVG paths mapped manually for stability, or rely on lucide global if available.
114
+ // For robustness, I'll use the SVG paths provided in your original code + a few more,
115
+ // wrapped in a clean component.
116
+ return null;
117
+ };
118
+
119
+ // Defining Icons manually to ensure 100% reliability without external dep failures
120
+ const Icons = {
121
+ Play: (props) => <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" {...props}><polygon points="5 3 19 12 5 21 5 3"/></svg>,
122
+ Pause: (props) => <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" {...props}><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>,
123
+ SkipForward: (props) => <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" {...props}><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" x2="19" y1="5" y2="19"/></svg>,
124
+ SkipBack: (props) => <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" {...props}><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" x2="5" y1="19" y2="5"/></svg>,
125
+ RotateCcw: (props) => <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" {...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"/></svg>,
126
+ Shuffle: (props) => <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" {...props}><path d="M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l14.2-12.6"/><path d="M22 6h-1.4c-1.3 0-2.5.6-3.3 1.7L3.1 20.3"/><path d="M2 6h1.4c1.3 0 2.5.6 3.3 1.7l2.5 2.3"/><path d="M22 18h-1.4c-1.3 0-2.5-.6-3.3-1.7l-2.5-2.3"/></svg>,
127
+ GitMerge: (props) => <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" {...props}><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></svg>,
128
+ Scissors: (props) => <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" {...props}><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>,
129
+ Info: (props) => <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" {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>,
130
+ ArrowRight: (props) => <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" {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>,
131
+ CheckCircle: (props) => <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" {...props}><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>,
132
+ Target: (props) => <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" {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>,
133
+ Layers: (props) => <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" {...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"/></svg>,
134
+ BookOpen: (props) => <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" {...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"/></svg>,
135
+ SplitSquareVertical: (props) => <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" {...props}><path d="M5 8V5c0-1 1-2 2-2h10c1 0 2 1 2 2v3"/><path d="M19 16v3c0 1-1 2-2 2H7c-1 0-2-1-2-2v-3"/><line x1="4" x2="20" y1="12" y2="12"/></svg>,
136
+ CircleDot: (props) => <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" {...props}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>,
137
+ };
138
+
139
+ const CLUSTER_COLORS = [
140
+ "hsl(340, 80%, 55%)", "hsl(200, 85%, 50%)", "hsl(45, 95%, 50%)",
141
+ "hsl(160, 70%, 45%)", "hsl(280, 75%, 55%)", "hsl(20, 90%, 55%)",
142
+ "hsl(100, 70%, 45%)", "hsl(320, 75%, 50%)",
143
+ ];
144
+
145
+ // --- ALGORITHMS ---
146
+ function generateRandomPoints(count, width, height) {
147
+ const padding = 60;
148
+ return Array.from({ length: count }, (_, i) => ({
149
+ id: i,
150
+ x: padding + Math.random() * (width - padding * 2),
151
+ y: padding + Math.random() * (height - padding * 2),
152
+ clusterId: i,
153
+ }));
154
+ }
155
+
156
+ function calculateDistance(p1, p2) {
157
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
158
+ }
159
+
160
+ function getCentroid(points, pointIds) {
161
+ const clusterPoints = points.filter(p => pointIds.includes(p.id));
162
+ if (clusterPoints.length === 0) return { x: 0, y: 0 };
163
+ const sumX = clusterPoints.reduce((sum, p) => sum + p.x, 0);
164
+ const sumY = clusterPoints.reduce((sum, p) => sum + p.y, 0);
165
+ return {
166
+ x: sumX / clusterPoints.length,
167
+ y: sumY / clusterPoints.length,
168
+ };
169
+ }
170
+
171
+ // --- AGGLOMERATIVE LOGIC ---
172
+ function initializeClusters(points) {
173
+ return points.map((p, i) => ({
174
+ id: p.id,
175
+ points: [p.id],
176
+ color: CLUSTER_COLORS[i % CLUSTER_COLORS.length],
177
+ centroid: { x: p.x, y: p.y },
178
+ }));
179
+ }
180
+
181
+ function findClosestClusters(clusters, points) {
182
+ if (clusters.length < 2) return null;
183
+ let minDistance = Infinity;
184
+ let closest = null;
185
+
186
+ for (let i = 0; i < clusters.length; i++) {
187
+ for (let j = i + 1; j < clusters.length; j++) {
188
+ const centroid1 = getCentroid(points, clusters[i].points);
189
+ const centroid2 = getCentroid(points, clusters[j].points);
190
+ const distance = calculateDistance(centroid1, centroid2);
191
+
192
+ if (distance < minDistance) {
193
+ minDistance = distance;
194
+ closest = { cluster1: clusters[i], cluster2: clusters[j], distance };
195
+ }
196
+ }
197
+ }
198
+ return closest;
199
+ }
200
+
201
+ function mergeClusters(clusters, cluster1Id, cluster2Id, newClusterId, points) {
202
+ const cluster1 = clusters.find(c => c.id === cluster1Id);
203
+ const cluster2 = clusters.find(c => c.id === cluster2Id);
204
+ if (!cluster1 || !cluster2) return clusters;
205
+
206
+ const mergedPoints = [...cluster1.points, ...cluster2.points];
207
+ const newCentroid = getCentroid(points, mergedPoints);
208
+
209
+ const newCluster = {
210
+ id: newClusterId,
211
+ points: mergedPoints,
212
+ color: cluster1.color,
213
+ centroid: newCentroid,
214
+ };
215
+
216
+ return clusters.filter(c => c.id !== cluster1Id && c.id !== cluster2Id).concat(newCluster);
217
+ }
218
+
219
+ function generateAllMergeSteps(points) {
220
+ const steps = [];
221
+ let clusters = initializeClusters(points);
222
+ let nextClusterId = points.length;
223
+
224
+ while (clusters.length > 1) {
225
+ const closest = findClosestClusters(clusters, points);
226
+ if (!closest) break;
227
+
228
+ const { cluster1, cluster2, distance } = closest;
229
+ const step = {
230
+ cluster1Id: cluster1.id,
231
+ cluster2Id: cluster2.id,
232
+ newClusterId: nextClusterId,
233
+ distance,
234
+ explanation: `Merging clusters with ${cluster1.points.length} and ${cluster2.points.length} points (distance: ${distance.toFixed(1)}px)`,
235
+ };
236
+
237
+ steps.push(step);
238
+ clusters = mergeClusters(clusters, cluster1.id, cluster2.id, nextClusterId, points);
239
+ nextClusterId++;
240
+ }
241
+ return steps;
242
+ }
243
+
244
+ function buildDendrogram(points, steps) {
245
+ const nodes = new Map();
246
+ points.forEach(p => {
247
+ nodes.set(p.id, { id: p.id, height: 0, points: [p.id] });
248
+ });
249
+
250
+ steps.forEach((step, index) => {
251
+ const left = nodes.get(step.cluster1Id);
252
+ const right = nodes.get(step.cluster2Id);
253
+ const newNode = {
254
+ id: step.newClusterId,
255
+ left, right,
256
+ height: index + 1,
257
+ points: [...left.points, ...right.points],
258
+ };
259
+ nodes.set(step.newClusterId, newNode);
260
+ });
261
+
262
+ if (steps.length > 0) return nodes.get(steps[steps.length - 1].newClusterId);
263
+ return nodes.get(0);
264
+ }
265
+
266
+ // --- DIVISIVE LOGIC ---
267
+ function initializeSingleCluster(points) {
268
+ if (points.length === 0) return [];
269
+ const allPointIds = points.map(p => p.id);
270
+ return [{
271
+ id: 0,
272
+ points: allPointIds,
273
+ color: CLUSTER_COLORS[0],
274
+ centroid: getCentroid(points, allPointIds),
275
+ }];
276
+ }
277
+
278
+ function splitCluster(cluster, points, newId1, newId2, colorIndex) {
279
+ const clusterPoints = points.filter(p => cluster.points.includes(p.id));
280
+
281
+ // Find 2 most distant points as seeds
282
+ let maxDistance = -1;
283
+ let p1 = clusterPoints[0];
284
+ let p2 = clusterPoints[1];
285
+
286
+ for (let i = 0; i < clusterPoints.length; i++) {
287
+ for (let j = i + 1; j < clusterPoints.length; j++) {
288
+ const dist = calculateDistance(clusterPoints[i], clusterPoints[j]);
289
+ if (dist > maxDistance) {
290
+ maxDistance = dist;
291
+ p1 = clusterPoints[i];
292
+ p2 = clusterPoints[j];
293
+ }
294
+ }
295
+ }
296
+
297
+ const group1 = [];
298
+ const group2 = [];
299
+
300
+ for (const p of clusterPoints) {
301
+ const dist1 = calculateDistance(p, p1);
302
+ const dist2 = calculateDistance(p, p2);
303
+ if (dist1 <= dist2) group1.push(p.id);
304
+ else group2.push(p.id);
305
+ }
306
+
307
+ return {
308
+ cluster1: {
309
+ id: newId1, points: group1, color: cluster.color, centroid: getCentroid(points, group1)
310
+ },
311
+ cluster2: {
312
+ id: newId2, points: group2, color: CLUSTER_COLORS[colorIndex % CLUSTER_COLORS.length], centroid: getCentroid(points, group2)
313
+ }
314
+ };
315
+ }
316
+
317
+ function findClusterWithHighestVariance(clusters, points) {
318
+ const splittable = clusters.filter(c => c.points.length > 1);
319
+ if (splittable.length === 0) return null;
320
+
321
+ let maxVar = -1;
322
+ let target = null;
323
+
324
+ for (const c of splittable) {
325
+ const pts = points.filter(p => c.points.includes(p.id));
326
+ const centroid = getCentroid(points, c.points);
327
+ // Using distance variance as metric
328
+ const variance = pts.reduce((sum, p) => sum + Math.pow(calculateDistance(p, centroid), 2), 0) / pts.length;
329
+
330
+ if (variance > maxVar) {
331
+ maxVar = variance;
332
+ target = c;
333
+ }
334
+ }
335
+ return target;
336
+ }
337
+
338
+ function applySplit(clusters, parentId, child1Id, child2Id, points, colorIndex) {
339
+ const parent = clusters.find(c => c.id === parentId);
340
+ if (!parent || parent.points.length < 2) return clusters;
341
+ const { cluster1, cluster2 } = splitCluster(parent, points, child1Id, child2Id, colorIndex);
342
+ return clusters.filter(c => c.id !== parentId).concat([cluster1, cluster2]);
343
+ }
344
+
345
+ function generateAllSplitSteps(points) {
346
+ if (points.length <= 1) return [];
347
+ const steps = [];
348
+ let clusters = initializeSingleCluster(points);
349
+ let nextClusterId = 1;
350
+ let colorIndex = 1;
351
+
352
+ while (true) {
353
+ const target = findClusterWithHighestVariance(clusters, points);
354
+ if (!target) break;
355
+
356
+ const child1Id = nextClusterId++;
357
+ const child2Id = nextClusterId++;
358
+ steps.push({
359
+ parentClusterId: target.id,
360
+ child1Id, child2Id,
361
+ explanation: `Splitting cluster with ${target.points.length} points into two smaller clusters`,
362
+ });
363
+ clusters = applySplit(clusters, target.id, child1Id, child2Id, points, colorIndex++);
364
+ }
365
+ return steps;
366
+ }
367
+
368
+ function buildDivisiveDendrogram(points, steps) {
369
+ if (points.length === 0) return null;
370
+ if (points.length === 1) return { id: points[0].id, height: 0, points: [points[0].id] };
371
+
372
+ const nodes = new Map();
373
+ const totalSteps = steps.length;
374
+
375
+ const root = { id: 0, height: totalSteps, points: points.map(p => p.id) };
376
+ nodes.set(0, root);
377
+
378
+ steps.forEach((step, index) => {
379
+ const parent = nodes.get(step.parentClusterId);
380
+ if (!parent) return;
381
+
382
+ // We need to re-calculate splitting to know which points go where for the tree structure
383
+ // This is a bit inefficient but accurate for visualization
384
+ let tempClusters = initializeSingleCluster(points);
385
+ for(let i=0; i<=index; i++) {
386
+ const s = steps[i];
387
+ tempClusters = applySplit(tempClusters, s.parentClusterId, s.child1Id, s.child2Id, points, i+1);
388
+ }
389
+ const c1 = tempClusters.find(c => c.id === step.child1Id);
390
+ const c2 = tempClusters.find(c => c.id === step.child2Id);
391
+
392
+ if (c1 && c2) {
393
+ const node1 = { id: step.child1Id, height: totalSteps - index - 1, points: c1.points };
394
+ const node2 = { id: step.child2Id, height: totalSteps - index - 1, points: c2.points };
395
+ parent.left = node1;
396
+ parent.right = node2;
397
+ nodes.set(step.child1Id, node1);
398
+ nodes.set(step.child2Id, node2);
399
+ }
400
+ });
401
+ return root;
402
+ }
403
+
404
+ // --- COMPONENTS ---
405
+
406
+ const Button = ({ children, onClick, disabled, variant = "primary", size = "default", className = "" }) => {
407
+ const baseStyles = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
408
+ const variants = {
409
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
410
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
411
+ outline: "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
412
+ icon: "hover:bg-accent hover:text-accent-foreground",
413
+ };
414
+ const sizes = {
415
+ default: "h-9 px-4 py-2 text-sm",
416
+ sm: "h-8 rounded-md px-3 text-xs",
417
+ icon: "h-9 w-9",
418
+ };
419
+ return (
420
+ <button
421
+ onClick={onClick}
422
+ disabled={disabled}
423
+ className={cn(baseStyles, variants[variant], sizes[size], className)}
424
+ >
425
+ {children}
426
+ </button>
427
+ );
428
+ };
429
+
430
+ const Dendrogram = ({ root, currentStep, totalSteps }) => {
431
+ if (!root) return null;
432
+ const width = 300;
433
+ const height = 280;
434
+ const padding = { top: 30, bottom: 40, left: 30, right: 30 };
435
+
436
+ const getLeafCount = (node) => {
437
+ if (!node.left && !node.right) return 1;
438
+ return (node.left ? getLeafCount(node.left) : 0) + (node.right ? getLeafCount(node.right) : 0);
439
+ };
440
+
441
+ const leafCount = getLeafCount(root);
442
+ const leafWidth = (width - padding.left - padding.right) / (leafCount || 1);
443
+ const heightScale = (height - padding.top - padding.bottom) / (totalSteps || 1);
444
+ let leafIndex = 0;
445
+
446
+ const renderNode = (node) => {
447
+ const y = height - padding.bottom - node.height * heightScale;
448
+ const isVisible = node.height <= currentStep;
449
+
450
+ if (!node.left && !node.right) {
451
+ const x = padding.left + leafIndex * leafWidth + leafWidth / 2;
452
+ leafIndex++;
453
+ return {
454
+ x,
455
+ element: (
456
+ <g key={`leaf-${node.id}`}>
457
+ <motion.circle cx={x} cy={height - padding.bottom} r={8} fill="hsl(var(--primary))" initial={ {scale:0} } animate={ {scale:1} } />
458
+ <text x={x} y={height - padding.bottom + 25} textAnchor="middle" className="text-xs fill-white font-medium">P{node.id + 1}</text>
459
+ </g>
460
+ )
461
+ };
462
+ }
463
+
464
+ const leftRes = node.left ? renderNode(node.left) : null;
465
+ const rightRes = node.right ? renderNode(node.right) : null;
466
+
467
+ const x = (leftRes.x + rightRes.x) / 2;
468
+ const leftY = node.left ? height - padding.bottom - node.left.height * heightScale : height - padding.bottom;
469
+ const rightY = node.right ? height - padding.bottom - node.right.height * heightScale : height - padding.bottom;
470
+
471
+ return {
472
+ x,
473
+ element: (
474
+ <g key={`node-${node.id}`}>
475
+ {leftRes?.element} {rightRes?.element}
476
+ {isVisible && (
477
+ <>
478
+ <motion.line x1={leftRes.x} y1={leftY} x2={leftRes.x} y2={y} stroke="hsl(var(--primary))" strokeWidth={2} initial={ {pathLength:0} } animate={ {pathLength:1} } />
479
+ <motion.line x1={rightRes.x} y1={rightY} x2={rightRes.x} y2={y} stroke="hsl(var(--primary))" strokeWidth={2} initial={ {pathLength:0} } animate={ {pathLength:1} } />
480
+ <motion.line x1={leftRes.x} y1={y} x2={rightRes.x} y2={y} stroke="hsl(var(--primary))" strokeWidth={2} initial={ {pathLength:0} } animate={ {pathLength:1} } />
481
+ <motion.circle cx={x} cy={y} r={5} fill="hsl(var(--accent))" initial={ {scale:0} } animate={ {scale:1} } />
482
+ </>
483
+ )}
484
+ </g>
485
+ )
486
+ };
487
+ };
488
+
489
+ leafIndex = 0;
490
+ const tree = renderNode(root);
491
+
492
+ return (
493
+ <div className="glass-card p-4">
494
+ <h3 className="text-sm font-semibold mb-3">Dendrogram</h3>
495
+ <svg width="100%" height={height} viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="xMidYMid meet">
496
+ {tree.element}
497
+ </svg>
498
+ </div>
499
+ );
500
+ };
501
+
502
+ const ClusteringCanvas = ({ points, clusters, currentStep, steps, highlightedClusters = [] }) => {
503
+ const getPointCluster = (id) => clusters.find(c => c.points.includes(id));
504
+ const currentMerge = currentStep > 0 && currentStep <= steps.length ? steps[currentStep - 1] : null;
505
+
506
+ return (
507
+ <div className="relative w-full h-[400px] md:h-[500px] rounded-2xl overflow-hidden bg-canvas border border-border/50">
508
+ {/* Grid */}
509
+ <div className="absolute inset-0 opacity-20"
510
+ style={ {backgroundImage: 'radial-gradient(hsl(var(--canvas-grid)) 1px, transparent 1px)', backgroundSize: '30px 30px'} }>
511
+ </div>
512
+
513
+ <svg className="absolute inset-0 w-full h-full pointer-events-none">
514
+ <AnimatePresence>
515
+ {/* Merge Lines */}
516
+ {currentMerge && clusters.map(c1 =>
517
+ clusters.filter(c2 => c2.id > c1.id).map(c2 => {
518
+ const isHighlighted = (c1.id === currentMerge.cluster1Id && c2.id === currentMerge.cluster2Id) || (c1.id === currentMerge.cluster2Id && c2.id === currentMerge.cluster1Id);
519
+ if(!isHighlighted) return null;
520
+ const cent1 = getCentroid(points, c1.points);
521
+ const cent2 = getCentroid(points, c2.points);
522
+ return (
523
+ <motion.line key={`merge-${c1.id}-${c2.id}`} x1={cent1.x} y1={cent1.y} x2={cent2.x} y2={cent2.y}
524
+ stroke="hsl(var(--primary))" strokeWidth="3" strokeDasharray="8,4"
525
+ initial={ {opacity:0, pathLength:0} } animate={ {opacity:1, pathLength:1} } exit={ {opacity:0} } />
526
+ )
527
+ })
528
+ )}
529
+
530
+ {/* Hulls */}
531
+ {clusters.filter(c => c.points.length > 1).map(c => {
532
+ const pts = points.filter(p => c.points.includes(p.id));
533
+ const cent = getCentroid(points, c.points);
534
+ const maxDist = Math.max(...pts.map(p => calculateDistance(p, cent)));
535
+ const isHigh = highlightedClusters.includes(c.id);
536
+ return (
537
+ <motion.circle key={`hull-${c.id}`} cx={cent.x} cy={cent.y} r={maxDist + 30}
538
+ fill={c.color} fillOpacity={isHigh ? 0.2 : 0.1}
539
+ stroke={c.color} strokeWidth={isHigh ? 3 : 2} strokeOpacity={isHigh ? 0.8 : 0.4}
540
+ initial={ {scale:0, opacity:0} } animate={ {scale:1, opacity:1} } exit={ {scale:0, opacity:0} }
541
+ />
542
+ );
543
+ })}
544
+ </AnimatePresence>
545
+ </svg>
546
+
547
+ <AnimatePresence>
548
+ {points.map(p => {
549
+ const cluster = getPointCluster(p.id);
550
+ const isHigh = cluster && highlightedClusters.includes(cluster.id);
551
+ return (
552
+ <motion.div key={p.id} className="absolute rounded-full flex items-center justify-center font-bold text-[10px]"
553
+ style={ {
554
+ left: p.x - 12, top: p.y - 12, width: 24, height: 24,
555
+ backgroundColor: cluster?.color || "hsl(var(--primary))",
556
+ boxShadow: isHigh ? `0 0 20px ${cluster?.color}` : 'none'
557
+ } }
558
+ initial={ {scale:0} } animate={ {scale: isHigh ? 1.3 : 1, opacity: 1} } whileHover={ {scale: 1.2} }>
559
+ {p.id + 1}
560
+ </motion.div>
561
+ )
562
+ })}
563
+ </AnimatePresence>
564
+
565
+ <div className="absolute bottom-4 left-4 glass-card px-4 py-2 text-sm font-medium text-muted-foreground">
566
+ Step {currentStep} of {steps.length}
567
+ </div>
568
+ </div>
569
+ );
570
+ };
571
+
572
+ const DivisiveCanvas = ({ points, clusters, currentStep, steps, highlightedCluster }) => {
573
+ const currentSplit = currentStep > 0 && currentStep <= steps.length ? steps[currentStep - 1] : null;
574
+
575
+ return (
576
+ <div className="relative w-full h-[400px] md:h-[500px] rounded-2xl overflow-hidden bg-canvas border border-border/50">
577
+ <div className="absolute inset-0 opacity-20"
578
+ style={ {backgroundImage: 'radial-gradient(hsl(var(--canvas-grid)) 1px, transparent 1px)', backgroundSize: '30px 30px'} }>
579
+ </div>
580
+
581
+ <svg className="absolute inset-0 w-full h-full pointer-events-none">
582
+ <AnimatePresence>
583
+ {/* Split Visual */}
584
+ {currentSplit && (() => {
585
+ const c1 = clusters.find(c => c.id === currentSplit.child1Id);
586
+ const c2 = clusters.find(c => c.id === currentSplit.child2Id);
587
+ if (!c1 || !c2) return null;
588
+ const cent1 = getCentroid(points, c1.points);
589
+ const cent2 = getCentroid(points, c2.points);
590
+ const midX = (cent1.x + cent2.x) / 2;
591
+ const midY = (cent1.y + cent2.y) / 2;
592
+ const dx = cent2.x - cent1.x;
593
+ const dy = cent2.y - cent1.y;
594
+ const len = Math.sqrt(dx*dx + dy*dy);
595
+ const px = -dy/len * 100;
596
+ const py = dx/len * 100;
597
+
598
+ return (
599
+ <g key={`split-${currentStep}`}>
600
+ <motion.line x1={midX - px} y1={midY - py} x2={midX + px} y2={midY + py}
601
+ stroke="hsl(var(--accent))" strokeWidth="3" strokeDasharray="8,4"
602
+ initial={ {opacity:0, pathLength:0} } animate={ {opacity:1, pathLength:1} } exit={ {opacity:0} } />
603
+ </g>
604
+ );
605
+ })()}
606
+
607
+ {/* Hulls */}
608
+ {clusters.map(c => {
609
+ const pts = points.filter(p => c.points.includes(p.id));
610
+ const cent = getCentroid(points, c.points);
611
+ const maxDist = pts.length > 1 ? Math.max(...pts.map(p => calculateDistance(p, cent))) : 0;
612
+ const isHigh = c.id === highlightedCluster;
613
+ const isSingle = c.points.length === 1;
614
+ return (
615
+ <motion.circle key={`hull-${c.id}`} cx={cent.x} cy={cent.y} r={isSingle ? 25 : maxDist + 30}
616
+ fill={c.color} fillOpacity={isHigh ? 0.25 : 0.1}
617
+ stroke={c.color} strokeWidth={isHigh ? 3 : 2} strokeOpacity={isHigh ? 0.9 : 0.4} strokeDasharray={isHigh ? "8,4" : "none"}
618
+ initial={ {scale:0, opacity:0} } animate={ {scale:1, opacity:1} } exit={ {scale:0, opacity:0} }
619
+ />
620
+ );
621
+ })}
622
+ </AnimatePresence>
623
+ </svg>
624
+
625
+ <AnimatePresence>
626
+ {points.map(p => {
627
+ const c = clusters.find(c => c.points.includes(p.id));
628
+ const isHigh = c && c.id === highlightedCluster;
629
+ return (
630
+ <motion.div key={p.id} className="absolute rounded-full flex items-center justify-center font-bold text-[10px]"
631
+ style={ {
632
+ left: p.x - 12, top: p.y - 12, width: 24, height: 24,
633
+ backgroundColor: c?.color || "hsl(var(--primary))",
634
+ boxShadow: isHigh ? `0 0 20px ${c?.color}` : 'none'
635
+ } }
636
+ initial={ {scale:0} } animate={ {scale: isHigh ? 1.3 : 1, opacity: 1} }>
637
+ {p.id + 1}
638
+ </motion.div>
639
+ )
640
+ })}
641
+ </AnimatePresence>
642
+ <div className="absolute top-4 left-4 glass-card px-3 py-1.5 flex items-center gap-2 text-xs font-medium text-accent">
643
+ <Icons.Scissors className="w-4 h-4" /> Divisive Mode
644
+ </div>
645
+ </div>
646
+ )
647
+ }
648
+
649
+ const ControlPanel = ({ isPlaying, currentStep, totalSteps, onPlay, onPause, onNext, onPrev, onReset, onRandomize }) => (
650
+ <motion.div className="glass-card p-4 flex flex-col gap-4" initial={ {opacity:0, y:20} } animate={ {opacity:1, y:0} }>
651
+ <h3 className="text-sm font-semibold">Controls</h3>
652
+ <div className="flex items-center justify-center gap-2">
653
+ <Button variant="outline" size="icon" onClick={onPrev} disabled={currentStep === 0}><Icons.SkipBack className="h-4 w-4" /></Button>
654
+ <Button size="icon" className="h-12 w-12 rounded-full" onClick={isPlaying ? onPause : onPlay} disabled={currentStep >= totalSteps}>
655
+ {isPlaying ? <Icons.Pause className="h-5 w-5" /> : <Icons.Play className="h-5 w-5 ml-0.5" />}
656
+ </Button>
657
+ <Button variant="outline" size="icon" onClick={onNext} disabled={currentStep >= totalSteps}><Icons.SkipForward className="h-4 w-4" /></Button>
658
+ </div>
659
+ <div className="w-full h-2 bg-muted rounded-full overflow-hidden">
660
+ <motion.div className="h-full bg-primary" initial={ {width:0} } animate={ {width: `${(currentStep/totalSteps)*100}%`} } />
661
+ </div>
662
+ <div className="flex gap-2">
663
+ <Button variant="secondary" size="sm" onClick={onReset} className="flex-1"><Icons.RotateCcw className="h-4 w-4 mr-1"/> Reset</Button>
664
+ <Button variant="secondary" size="sm" onClick={onRandomize} className="flex-1"><Icons.Shuffle className="h-4 w-4 mr-1"/> New Data</Button>
665
+ </div>
666
+ </motion.div>
667
+ );
668
+
669
+ const ExplanationPanel = ({ currentStep, steps, totalClusters, mode }) => {
670
+ const currentOp = currentStep > 0 && currentStep <= steps.length ? steps[currentStep - 1] : null;
671
+ const isComplete = currentStep >= steps.length && steps.length > 0;
672
+
673
+ return (
674
+ <motion.div className="glass-card p-5" initial={ {opacity:0, y:20} } animate={ {opacity:1, y:0} } transition={ {delay:0.2} }>
675
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
676
+ <Icons.Info className="h-4 w-4 text-accent"/> What's Happening?
677
+ </h3>
678
+ <AnimatePresence mode="wait">
679
+ {currentStep === 0 ? (
680
+ <motion.div key="start" initial={ {opacity:0, x:-10} } animate={ {opacity:1, x:0} } exit={ {opacity:0, x:10} }>
681
+ <p className="text-sm font-medium mb-1">Starting Point</p>
682
+ <p className="text-sm text-muted-foreground">
683
+ {mode === 'agglomerative'
684
+ ? "Each point is its own cluster. We will merge the closest ones."
685
+ : "All points start in one big cluster. We will split the widest spread cluster."}
686
+ </p>
687
+ </motion.div>
688
+ ) : isComplete ? (
689
+ <motion.div key="done" initial={ {opacity:0, x:-10} } animate={ {opacity:1, x:0} } exit={ {opacity:0, x:10} }>
690
+ <p className="text-sm font-medium text-green-400 flex items-center gap-2"><Icons.CheckCircle className="h-4 w-4"/> Complete!</p>
691
+ <p className="text-sm text-muted-foreground">The hierarchy is fully formed.</p>
692
+ </motion.div>
693
+ ) : currentOp ? (
694
+ <motion.div key={`step-${currentStep}`} initial={ {opacity:0, x:-10} } animate={ {opacity:1, x:0} } exit={ {opacity:0, x:10} }>
695
+ <div className="flex items-center gap-2 mb-2">
696
+ <span className="px-2 py-1 bg-primary/20 rounded text-xs font-mono text-primary">Step {currentStep}</span>
697
+ <span className="text-sm font-medium">{mode === 'agglomerative' ? 'Merging' : 'Splitting'}</span>
698
+ </div>
699
+ <p className="text-sm text-muted-foreground mb-3">{currentOp.explanation}</p>
700
+ <div className="flex gap-4 p-3 bg-muted/50 rounded-lg text-center">
701
+ <div className="flex-1">
702
+ <span className="text-lg font-bold">{totalClusters}</span>
703
+ <p className="text-xs text-muted-foreground">Clusters</p>
704
+ </div>
705
+ <div className="w-px bg-border"></div>
706
+ <div className="flex-1">
707
+ <span className="text-lg font-bold">{steps.length - currentStep}</span>
708
+ <p className="text-xs text-muted-foreground">Steps Left</p>
709
+ </div>
710
+ </div>
711
+ </motion.div>
712
+ ) : null}
713
+ </AnimatePresence>
714
+ </motion.div>
715
+ );
716
+ }
717
+
718
+ const AlgorithmInfo = () => (
719
+ <div className="glass-card p-5 mt-6 grid md:grid-cols-2 gap-6">
720
+ <div>
721
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"><Icons.GitMerge className="h-4 w-4 text-primary"/> Agglomerative (Bottom-Up)</h3>
722
+ <ul className="text-xs text-muted-foreground space-y-2">
723
+ <li className="flex gap-2"><div className="p-1 bg-primary/10 rounded"><Icons.Target className="h-3 w-3"/></div> Start: Each point is a cluster</li>
724
+ <li className="flex gap-2"><div className="p-1 bg-primary/10 rounded"><Icons.Layers className="h-3 w-3"/></div> Find closest pair</li>
725
+ <li className="flex gap-2"><div className="p-1 bg-primary/10 rounded"><Icons.GitMerge className="h-3 w-3"/></div> Merge them</li>
726
+ <li className="flex gap-2"><div className="p-1 bg-primary/10 rounded"><Icons.BookOpen className="h-3 w-3"/></div> Repeat until one cluster remains</li>
727
+ </ul>
728
+ </div>
729
+ <div>
730
+ <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"><Icons.Scissors className="h-4 w-4 text-accent"/> Divisive (Top-Down)</h3>
731
+ <ul className="text-xs text-muted-foreground space-y-2">
732
+ <li className="flex gap-2"><div className="p-1 bg-accent/10 rounded"><Icons.CircleDot className="h-3 w-3"/></div> Start: One cluster contains all</li>
733
+ <li className="flex gap-2"><div className="p-1 bg-accent/10 rounded"><Icons.SplitSquareVertical className="h-3 w-3"/></div> Find cluster with max variance</li>
734
+ <li className="flex gap-2"><div className="p-1 bg-accent/10 rounded"><Icons.Scissors className="h-3 w-3"/></div> Split into two</li>
735
+ <li className="flex gap-2"><div className="p-1 bg-accent/10 rounded"><Icons.BookOpen className="h-3 w-3"/></div> Repeat until single points remain</li>
736
+ </ul>
737
+ </div>
738
+ </div>
739
+ );
740
+
741
+ // --- MAIN APP ---
742
+
743
+ const POINT_COUNT = 6;
744
+ const CANVAS_WIDTH = 600;
745
+ const CANVAS_HEIGHT = 400;
746
+
747
+ const App = () => {
748
+ const [mode, setMode] = useState("agglomerative");
749
+ const [points, setPoints] = useState([]);
750
+ const [clusters, setClusters] = useState([]);
751
+ const [steps, setSteps] = useState([]);
752
+ const [currentStep, setCurrentStep] = useState(0);
753
+ const [isPlaying, setIsPlaying] = useState(false);
754
+ const [dendrogram, setDendrogram] = useState(null);
755
+
756
+ const initSim = useCallback(() => {
757
+ const newPoints = generateRandomPoints(POINT_COUNT, CANVAS_WIDTH, CANVAS_HEIGHT);
758
+
759
+ if (mode === 'agglomerative') {
760
+ const newClusters = initializeClusters(newPoints);
761
+ const newSteps = generateAllMergeSteps(newPoints);
762
+ const newDendrogram = buildDendrogram(newPoints, newSteps);
763
+ setClusters(newClusters);
764
+ setSteps(newSteps);
765
+ setDendrogram(newDendrogram);
766
+ } else {
767
+ const newClusters = initializeSingleCluster(newPoints);
768
+ const newSteps = generateAllSplitSteps(newPoints);
769
+ const newDendrogram = buildDivisiveDendrogram(newPoints, newSteps);
770
+ setClusters(newClusters);
771
+ setSteps(newSteps);
772
+ setDendrogram(newDendrogram);
773
+ }
774
+
775
+ setPoints(newPoints);
776
+ setCurrentStep(0);
777
+ setIsPlaying(false);
778
+ }, [mode]);
779
+
780
+ useEffect(() => { initSim(); }, [initSim]);
781
+
782
+ const applyStep = useCallback((stepIdx) => {
783
+ if (stepIdx <= 0) {
784
+ setClusters(mode === 'agglomerative' ? initializeClusters(points) : initializeSingleCluster(points));
785
+ return;
786
+ }
787
+
788
+ if (mode === 'agglomerative') {
789
+ let curr = initializeClusters(points);
790
+ for(let i=0; i<stepIdx; i++) {
791
+ const s = steps[i];
792
+ curr = mergeClusters(curr, s.cluster1Id, s.cluster2Id, s.newClusterId, points);
793
+ }
794
+ setClusters(curr);
795
+ } else {
796
+ let curr = initializeSingleCluster(points);
797
+ for(let i=0; i<stepIdx; i++) {
798
+ const s = steps[i];
799
+ curr = applySplit(curr, s.parentClusterId, s.child1Id, s.child2Id, points, i+1);
800
+ }
801
+ setClusters(curr);
802
+ }
803
+ }, [mode, points, steps]);
804
+
805
+ const handleNext = useCallback(() => {
806
+ if (currentStep < steps.length) {
807
+ const next = currentStep + 1;
808
+ setCurrentStep(next);
809
+ applyStep(next);
810
+ }
811
+ }, [currentStep, steps, applyStep]);
812
+
813
+ const handlePrev = useCallback(() => {
814
+ if (currentStep > 0) {
815
+ const prev = currentStep - 1;
816
+ setCurrentStep(prev);
817
+ applyStep(prev);
818
+ }
819
+ }, [currentStep, applyStep]);
820
+
821
+ useEffect(() => {
822
+ if(!isPlaying) return;
823
+ const timer = setInterval(() => {
824
+ if(currentStep >= steps.length) setIsPlaying(false);
825
+ else handleNext();
826
+ }, 1500);
827
+ return () => clearInterval(timer);
828
+ }, [isPlaying, currentStep, steps, handleNext]);
829
+
830
+ const highlighted = useMemo(() => {
831
+ if (currentStep === 0 || currentStep > steps.length) return mode === 'agglomerative' ? [] : null;
832
+ const s = steps[currentStep - 1];
833
+ return mode === 'agglomerative' ? [s.cluster1Id, s.cluster2Id] : s.parentClusterId;
834
+ }, [currentStep, steps, mode]);
835
+
836
+ return (
837
+ <div className="min-h-screen p-4 md:p-8">
838
+ <div className="max-w-7xl mx-auto">
839
+ <header className="text-center mb-8">
840
+ <h1 className="text-3xl md:text-5xl font-bold mb-3 gradient-text">Hierarchical Clustering</h1>
841
+ <p className="text-lg text-muted-foreground max-w-2xl mx-auto">Visualize how data grouping works in machine learning.</p>
842
+ </header>
843
+
844
+ <div className="flex justify-center mb-8">
845
+ <div className="glass-card p-1.5 flex gap-2">
846
+ <button onClick={() => setMode("agglomerative")} className={cn("flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-all", mode === "agglomerative" ? "bg-primary text-primary-foreground shadow-lg" : "text-muted-foreground hover:bg-muted/50")}>
847
+ <Icons.GitMerge className="w-4 h-4" /> Agglomerative
848
+ </button>
849
+ <button onClick={() => setMode("divisive")} className={cn("flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-all", mode === "divisive" ? "bg-accent text-accent-foreground shadow-lg" : "text-muted-foreground hover:bg-muted/50")}>
850
+ <Icons.Scissors className="w-4 h-4" /> Divisive
851
+ </button>
852
+ </div>
853
+ </div>
854
+
855
+ <div className="grid lg:grid-cols-[1fr_320px] gap-6">
856
+ <motion.div initial={ {opacity:0, scale:0.95} } animate={ {opacity:1, scale:1} }>
857
+ {mode === 'agglomerative'
858
+ ? <ClusteringCanvas points={points} clusters={clusters} currentStep={currentStep} steps={steps} highlightedClusters={highlighted} />
859
+ : <DivisiveCanvas points={points} clusters={clusters} currentStep={currentStep} steps={steps} highlightedCluster={highlighted} />
860
+ }
861
+ <AlgorithmInfo />
862
+ </motion.div>
863
+
864
+ <div className="space-y-4">
865
+ <ControlPanel isPlaying={isPlaying} currentStep={currentStep} totalSteps={steps.length}
866
+ onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)}
867
+ onNext={handleNext} onPrev={handlePrev} onReset={() => {setCurrentStep(0); applyStep(0); setIsPlaying(false);}} onRandomize={initSim}
868
+ />
869
+ <ExplanationPanel currentStep={currentStep} steps={steps} totalClusters={clusters.length} mode={mode} />
870
+ <Dendrogram root={dendrogram} currentStep={currentStep} totalSteps={steps.length} />
871
+ </div>
872
+ </div>
873
+ </div>
874
+ {/* Centered Back Button */}
875
+ <div className="mt-12 flex justify-center pb-8 relative">
876
+ <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
877
+ <a
878
+ href="/hierarchical-clustering"
879
+ onClick={(e) => {
880
+ const audio = document.getElementById("clickSound");
881
+ if(audio) audio.play().catch(err => console.log(err));
882
+ }}
883
+ 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"
884
+ >
885
+ Back to Core
886
+ </a>
887
+ </div>
888
+ </div>
889
+ );
890
+ };
891
+
892
+ const root = ReactDOM.createRoot(document.getElementById('root'));
893
+ root.render(<App />);
894
+ </script>
895
+ </body>
896
+ </html>
templates/Principal-Component-Analysis.html CHANGED
@@ -177,6 +177,54 @@
177
  <div class="container">
178
  <h1>๐Ÿ“‰ Study Guide: Principal Component Analysis (PCA)</h1>
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <h2>๐Ÿ”น Core Concepts</h2>
181
  <div class="story-pca">
182
  <p><strong>Story-style intuition: The Shadow Puppet Master</strong></p>
 
177
  <div class="container">
178
  <h1>๐Ÿ“‰ Study Guide: Principal Component Analysis (PCA)</h1>
179
 
180
+
181
+
182
+ <!-- button -->
183
+ <div>
184
+ <!-- Audio Element -->
185
+ <!-- Note: Browsers may block audio autoplay if the user hasn't interacted with the document first,
186
+ but since this is triggered by a click, it should work fine. -->
187
+
188
+
189
+ <a
190
+ href="/pca-three"
191
+ target="_blank"
192
+ onclick="playSound()"
193
+ class="
194
+ cursor-pointer
195
+ inline-block
196
+ relative
197
+ bg-blue-500
198
+ text-white
199
+ font-bold
200
+ py-4 px-8
201
+ rounded-xl
202
+ text-2xl
203
+ transition-all
204
+ duration-150
205
+
206
+ /* 3D Effect (Hard Shadow) */
207
+ shadow-[0_8px_0_rgb(29,78,216)]
208
+
209
+ /* Pressed State (Move down & remove shadow) */
210
+ active:shadow-none
211
+ active:translate-y-[8px]
212
+ ">
213
+ Tap Me!
214
+ </a>
215
+ </div>
216
+
217
+ <script>
218
+ function playSound() {
219
+ const audio = document.getElementById("clickSound");
220
+ if (audio) {
221
+ audio.currentTime = 0;
222
+ audio.play().catch(e => console.log("Audio play failed:", e));
223
+ }
224
+ }
225
+ </script>
226
+ <!-- button -->
227
+
228
  <h2>๐Ÿ”น Core Concepts</h2>
229
  <div class="story-pca">
230
  <p><strong>Story-style intuition: The Shadow Puppet Master</strong></p>