YOUSEF2434 commited on
Commit
a566fb0
·
verified ·
1 Parent(s): 6f06b5f

Upload 96 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +2 -0
  2. .gitignore +30 -0
  3. Face-Expresion.html +207 -0
  4. Lab-Selection.html +1227 -0
  5. Object-Detection.html +426 -0
  6. Pose.html +380 -0
  7. components.json +21 -0
  8. eslint.config.js +60 -0
  9. index.html +13 -0
  10. jsconfig.json +21 -0
  11. live-feed-object-detection.html +225 -0
  12. login.html +188 -0
  13. movement-detection.html +187 -0
  14. package-lock.json +0 -0
  15. package.json +100 -0
  16. postcss.config.js +6 -0
  17. src/App.jsx +88 -0
  18. src/Layout.jsx +127 -0
  19. src/api/base44Client.js +14 -0
  20. src/components/UserNotRegisteredError.jsx +31 -0
  21. src/components/lessons/AnimatedDiagram.jsx +215 -0
  22. src/components/lessons/Chapter1Slides.jsx +280 -0
  23. src/components/lessons/Chapter2Slides.jsx +396 -0
  24. src/components/lessons/Chapter3Slides.jsx +306 -0
  25. src/components/lessons/ClickReveal.jsx +51 -0
  26. src/components/lessons/KeyTermBadge.jsx +41 -0
  27. src/components/lessons/SlideContainer.jsx +21 -0
  28. src/components/lessons/SlideNavigation.jsx +75 -0
  29. src/components/ui/accordion.jsx +41 -0
  30. src/components/ui/alert-dialog.jsx +97 -0
  31. src/components/ui/alert.jsx +47 -0
  32. src/components/ui/aspect-ratio.jsx +5 -0
  33. src/components/ui/avatar.jsx +35 -0
  34. src/components/ui/badge.jsx +34 -0
  35. src/components/ui/breadcrumb.jsx +92 -0
  36. src/components/ui/button.jsx +48 -0
  37. src/components/ui/calendar.jsx +71 -0
  38. src/components/ui/card.jsx +50 -0
  39. src/components/ui/carousel.jsx +193 -0
  40. src/components/ui/chart.jsx +309 -0
  41. src/components/ui/checkbox.jsx +22 -0
  42. src/components/ui/collapsible.jsx +11 -0
  43. src/components/ui/command.jsx +116 -0
  44. src/components/ui/context-menu.jsx +156 -0
  45. src/components/ui/dialog.jsx +96 -0
  46. src/components/ui/drawer.jsx +92 -0
  47. src/components/ui/dropdown-menu.jsx +156 -0
  48. src/components/ui/form.jsx +134 -0
  49. src/components/ui/hover-card.jsx +25 -0
  50. src/components/ui/input-otp.jsx +53 -0
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ VITE_BASE44_APP_ID=YOUR_REAL_APP_ID
2
+ VITE_BASE44_APP_BASE_URL=YOUR_REAL_BASE44_URL
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #env
2
+ .env
3
+ .env.*
4
+
5
+ # Logs
6
+ logs
7
+ *.log
8
+ npm-debug.log*
9
+ yarn-debug.log*
10
+ yarn-error.log*
11
+ pnpm-debug.log*
12
+ lerna-debug.log*
13
+
14
+ node_modules
15
+ dist
16
+ dist-ssr
17
+ *.local
18
+
19
+ # Editor directories and files
20
+ .vscode/*
21
+ !.vscode/extensions.json
22
+ .idea
23
+ .DS_Store
24
+ *.suo
25
+ *.ntvs*
26
+ *.njsproj
27
+ *.sln
28
+ *.sw?
29
+
30
+ .env
Face-Expresion.html ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Expression Recognition</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: center;
14
+ justify-content: center;
15
+ height: 100vh;
16
+ background-color: #1a1a1a;
17
+ font-family: 'Segoe UI', sans-serif;
18
+ color: white;
19
+ }
20
+
21
+ .video-container {
22
+ position: relative;
23
+ width: 640px;
24
+ height: 480px;
25
+ background: #000;
26
+ border-radius: 12px;
27
+ overflow: hidden;
28
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
29
+ }
30
+
31
+ video {
32
+ width: 100%;
33
+ height: 100%;
34
+ object-fit: cover;
35
+ /* Visual mirror only */
36
+ transform: scaleX(-1);
37
+ }
38
+
39
+ canvas {
40
+ position: absolute;
41
+ top: 0;
42
+ left: 0;
43
+ /* Canvas stays normal */
44
+ }
45
+
46
+ #loader {
47
+ position: absolute;
48
+ top: 0;
49
+ left: 0;
50
+ width: 100%;
51
+ height: 100%;
52
+ background: rgba(0,0,0,0.9);
53
+ display: flex;
54
+ flex-direction: column;
55
+ align-items: center;
56
+ justify-content: center;
57
+ z-index: 10;
58
+ }
59
+
60
+ .spinner {
61
+ width: 40px;
62
+ height: 40px;
63
+ border: 4px solid #f3f3f3;
64
+ border-top: 4px solid #9b59b6; /* Purple for expressions */
65
+ border-radius: 50%;
66
+ animation: spin 1s linear infinite;
67
+ margin-bottom: 15px;
68
+ }
69
+
70
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
71
+
72
+ .controls { margin-top: 20px; display: flex; gap: 15px; }
73
+ button { padding: 12px 30px; font-size: 16px; border: none; border-radius: 50px; cursor: pointer; font-weight: 600; transition: transform 0.1s; }
74
+ button:active { transform: scale(0.95); }
75
+
76
+ #btnCapture { background: linear-gradient(135deg, #28a745, #218838); color: white; }
77
+ #btnCapture:disabled { background: #555; cursor: not-allowed; }
78
+ #btnRetake { background: linear-gradient(135deg, #dc3545, #c82333); color: white; display: none; }
79
+ #status { margin-top: 15px; color: #ccc; font-size: 14px; }
80
+ </style>
81
+ <script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
82
+ </head>
83
+ <body>
84
+
85
+ <div class="video-container">
86
+ <video id="video" autoplay muted playsinline></video>
87
+ <canvas id="canvas"></canvas>
88
+ <div id="loader">
89
+ <div class="spinner"></div>
90
+ <div id="loadingText">Loading Expression Models...</div>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="controls">
95
+ <button id="btnCapture" disabled>Wait...</button>
96
+ <button id="btnRetake">Retake</button>
97
+ </div>
98
+
99
+ <div id="status">Initializing system...</div>
100
+
101
+ <script>
102
+ const video = document.getElementById('video');
103
+ const canvas = document.getElementById('canvas');
104
+ const btnCapture = document.getElementById('btnCapture');
105
+ const btnRetake = document.getElementById('btnRetake');
106
+ const statusText = document.getElementById('status');
107
+ const loader = document.getElementById('loader');
108
+
109
+ // Use the same reliable CDN
110
+ const MODEL_URL = 'https://cdn.jsdelivr.net/gh/cgarciagl/face-api.js@0.22.2/weights/';
111
+
112
+ async function init() {
113
+ try {
114
+ // Load SSD MobileNet (High Accuracy Detector)
115
+ await faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL);
116
+
117
+ // Load Face Expression Model
118
+ await faceapi.nets.faceExpressionNet.loadFromUri(MODEL_URL);
119
+
120
+ startCamera();
121
+ } catch (error) {
122
+ alert("Error loading models: " + error);
123
+ }
124
+ }
125
+
126
+ function startCamera() {
127
+ navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } })
128
+ .then(stream => { video.srcObject = stream; })
129
+ .catch(err => { console.error(err); });
130
+ }
131
+
132
+ video.addEventListener('play', () => {
133
+ const displaySize = { width: video.videoWidth, height: video.videoHeight };
134
+ faceapi.matchDimensions(canvas, displaySize);
135
+ loader.style.display = 'none';
136
+ btnCapture.disabled = false;
137
+ btnCapture.innerText = "Capture Expression";
138
+ statusText.innerText = "Ready. Show me an emotion!";
139
+ });
140
+
141
+ btnCapture.addEventListener('click', async () => {
142
+ if (video.paused) return;
143
+
144
+ video.pause();
145
+ btnCapture.style.display = 'none';
146
+ btnRetake.style.display = 'inline-block';
147
+ statusText.innerText = "Analyzing Expressions...";
148
+
149
+ const displaySize = { width: video.videoWidth, height: video.videoHeight };
150
+ faceapi.matchDimensions(canvas, displaySize);
151
+
152
+ // Detect Faces + Expressions
153
+ const detections = await faceapi.detectAllFaces(video, new faceapi.SsdMobilenetv1Options({ minConfidence: 0.5 }))
154
+ .withFaceExpressions();
155
+
156
+ const resizedDetections = faceapi.resizeResults(detections, displaySize);
157
+ const ctx = canvas.getContext('2d');
158
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
159
+
160
+ // 1. Draw Boxes (Mirrored Context)
161
+ ctx.save();
162
+ ctx.scale(-1, 1);
163
+ ctx.translate(-canvas.width, 0);
164
+ faceapi.draw.drawDetections(canvas, resizedDetections);
165
+ ctx.restore();
166
+
167
+ // 2. Draw Expressions (Normal Context)
168
+ resizedDetections.forEach(result => {
169
+ const expressions = result.expressions;
170
+
171
+ // Sort expressions to find the top one
172
+ const sorted = Object.entries(expressions).sort((a, b) => b[1] - a[1]);
173
+ const topEmotion = sorted[0]; // [emotion, score]
174
+
175
+ // Calculate mirrored position for the text/bars
176
+ const box = result.detection.box;
177
+ const mirroredX = canvas.width - box.x - box.width;
178
+ const mirroredPos = { x: mirroredX, y: box.bottomLeft.y };
179
+
180
+ // Draw the top emotion text
181
+ new faceapi.draw.DrawTextField(
182
+ [`${topEmotion[0]} (${Math.round(topEmotion[1] * 100)}%)`],
183
+ mirroredPos
184
+ ).draw(canvas);
185
+
186
+ // OPTIONAL: Draw the full expression bar chart
187
+ // We offset it slightly so it doesn't overlap the box too much
188
+ const minConfidence = 0.1; // Only show emotions above 10%
189
+ faceapi.draw.drawFaceExpressions(canvas, resizedDetections, minConfidence, mirroredPos);
190
+ });
191
+
192
+ if (detections.length === 0) statusText.innerText = "No face detected.";
193
+ else statusText.innerText = `Analysis Done. Found ${detections.length} face(s).`;
194
+ });
195
+
196
+ btnRetake.addEventListener('click', () => {
197
+ canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
198
+ video.play();
199
+ btnCapture.style.display = 'inline-block';
200
+ btnRetake.style.display = 'none';
201
+ statusText.innerText = "Ready.";
202
+ });
203
+
204
+ init();
205
+ </script>
206
+ </body>
207
+ </html>
Lab-Selection.html ADDED
@@ -0,0 +1,1227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>AI STEM Lab</title>
7
+ <style>
8
+ :root {
9
+ /* Dark Mode Variables */
10
+ --bg-color: #121212;
11
+ --card-bg: #1e1e1e;
12
+ --accent-color: #0a84ff; /* Apple-style vibrant blue */
13
+ --accent-hover: #409cff;
14
+ --text-main: #f5f5f7;
15
+ --text-secondary: #a1a1a6;
16
+
17
+ /* Window Colors */
18
+ --mac-window-bg: #2c2c2e;
19
+ --mac-header-bg: #3a3a3c;
20
+ --mac-border: rgba(0, 0, 0, 0.5);
21
+ }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
25
+ background-color: var(--bg-color);
26
+ color: var(--text-main);
27
+ margin: 0;
28
+ display: flex;
29
+ flex-direction: column;
30
+ align-items: center;
31
+ justify-content: center;
32
+ min-height: 100vh;
33
+ padding: 20px;
34
+ }
35
+
36
+ /* Playground Header */
37
+ header {
38
+ margin-bottom: 50px;
39
+ text-align: center;
40
+ max-width: 800px;
41
+ }
42
+
43
+ h1 {
44
+ color: var(--text-main);
45
+ font-weight: 800;
46
+ font-size: 3rem;
47
+ margin: 0 0 15px 0;
48
+ letter-spacing: -0.03em;
49
+ background: linear-gradient(90deg, #fff, #a1a1a6);
50
+ -webkit-background-clip: text;
51
+ -webkit-text-fill-color: transparent;
52
+ }
53
+
54
+ p.subtitle {
55
+ color: var(--text-secondary);
56
+ font-size: 1.3rem;
57
+ line-height: 1.5;
58
+ }
59
+
60
+ /* Card Container */
61
+ .playground-grid {
62
+ display: grid;
63
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
64
+ gap: 30px;
65
+ width: 95%;
66
+ max-width: 1400px;
67
+ }
68
+
69
+ /* Lab Cards */
70
+ .card {
71
+ background: var(--card-bg);
72
+ border-radius: 24px;
73
+ padding: 40px;
74
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
75
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.3s ease;
76
+ display: flex;
77
+ flex-direction: column;
78
+ align-items: flex-start;
79
+ border: 1px solid rgba(255,255,255,0.08);
80
+ position: relative;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .card:hover {
85
+ transform: translateY(-8px);
86
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
87
+ border-color: rgba(255,255,255,0.2);
88
+ }
89
+
90
+ .card::before {
91
+ content: '';
92
+ position: absolute;
93
+ top: 0;
94
+ left: 0;
95
+ width: 100%;
96
+ height: 6px;
97
+ background: linear-gradient(90deg, var(--accent-color), #bc52ff);
98
+ opacity: 0.7;
99
+ }
100
+
101
+ .icon-box {
102
+ width: 64px;
103
+ height: 64px;
104
+ background: rgba(255, 255, 255, 0.1);
105
+ border-radius: 16px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ font-size: 32px;
110
+ margin-bottom: 25px;
111
+ }
112
+
113
+ .card h2 {
114
+ margin: 0 0 15px 0;
115
+ font-size: 1.8rem;
116
+ font-weight: 700;
117
+ color: var(--text-main);
118
+ }
119
+
120
+ .card p {
121
+ margin: 0 0 30px 0;
122
+ color: var(--text-secondary);
123
+ line-height: 1.6;
124
+ font-size: 1.1rem;
125
+ flex-grow: 1;
126
+ }
127
+
128
+ .card strong {
129
+ color: #fff;
130
+ font-weight: 600;
131
+ }
132
+
133
+ .launch-btn {
134
+ background-color: var(--accent-color);
135
+ color: white;
136
+ border: none;
137
+ padding: 14px 28px;
138
+ border-radius: 30px;
139
+ font-weight: 600;
140
+ font-size: 1.1rem;
141
+ cursor: pointer;
142
+ transition: all 0.2s ease;
143
+ width: 100%;
144
+ text-align: center;
145
+ }
146
+
147
+ .launch-btn:hover {
148
+ background-color: var(--accent-hover);
149
+ transform: scale(1.02);
150
+ }
151
+
152
+ /* Modal Overlay */
153
+ .modal-overlay {
154
+ position: fixed;
155
+ top: 0;
156
+ left: 0;
157
+ width: 100%;
158
+ height: 100%;
159
+ background: rgba(0, 0, 0, 0.75);
160
+ backdrop-filter: blur(8px);
161
+ z-index: 1000;
162
+ display: none; /* Changed from visibility hidden to display none for stricter reset */
163
+ align-items: center;
164
+ justify-content: center;
165
+ opacity: 0;
166
+ transition: opacity 0.3s ease;
167
+ }
168
+
169
+ .modal-overlay.active {
170
+ display: flex;
171
+ opacity: 1;
172
+ }
173
+
174
+ /* macOS Window Styling */
175
+ .mac-window {
176
+ width: 85%;
177
+ max-width: 1000px;
178
+ height: 85vh;
179
+ background: var(--mac-window-bg);
180
+ border-radius: 12px;
181
+ box-shadow: 0 50px 100px rgba(0, 0, 0, 0.6);
182
+ display: flex;
183
+ flex-direction: column;
184
+ overflow: hidden;
185
+ border: 1px solid rgba(255,255,255,0.1);
186
+ transform: scale(0.95);
187
+ transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1);
188
+ position: relative;
189
+ }
190
+
191
+ .modal-overlay.active .mac-window {
192
+ transform: scale(1);
193
+ }
194
+
195
+ /* Window Header */
196
+ .window-header {
197
+ height: 44px;
198
+ background: var(--mac-header-bg);
199
+ border-bottom: 1px solid #1c1c1e;
200
+ display: flex;
201
+ align-items: center;
202
+ padding-left: 16px;
203
+ cursor: default;
204
+ }
205
+
206
+ /* The Red Button */
207
+ .traffic-light {
208
+ width: 14px;
209
+ height: 14px;
210
+ border-radius: 50%;
211
+ background-color: #ff453a;
212
+ border: none;
213
+ cursor: pointer;
214
+ position: relative;
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: center;
218
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
219
+ }
220
+
221
+ .traffic-light:hover::after {
222
+ content: "×";
223
+ color: #3b0000;
224
+ font-size: 11px;
225
+ font-weight: 900;
226
+ margin-top: -1px;
227
+ }
228
+
229
+ .window-title {
230
+ position: absolute;
231
+ left: 50%;
232
+ transform: translateX(-50%);
233
+ font-size: 14px;
234
+ color: #cfcfcf;
235
+ font-weight: 500;
236
+ }
237
+
238
+ /* Window Content */
239
+ .window-content {
240
+ flex: 1;
241
+ background: #000;
242
+ position: relative;
243
+ }
244
+
245
+ iframe {
246
+ width: 100%;
247
+ height: 100%;
248
+ border: none;
249
+ background: #121212;
250
+ }
251
+
252
+ </style>
253
+ </head>
254
+ <body>
255
+
256
+ <header>
257
+ <h1>AI Model Laboratory</h1>
258
+ <p class="subtitle">Welcome to the hands-on section! Based on the slideshow we just watched, choose an experiment below to test how computers "see" the world.</p>
259
+ </header>
260
+
261
+ <div class="playground-grid">
262
+ <!-- Option 1: Pose Estimation -->
263
+ <div class="card">
264
+ <div class="icon-box">🧘</div>
265
+ <h2>Pose Estimation</h2>
266
+ <p>
267
+ <strong>Remember the slide about skeletons?</strong><br><br>
268
+ In this experiment, the AI will try to find your joints (like elbows, knees, and shoulders) and connect them with lines.
269
+ <br><br>
270
+ <em>Try this: Try to do some movements with yout hands and face. Does the AI follow your movement?</em>
271
+ </p>
272
+ <button class="launch-btn" onclick="openWindow('Pose Estimation', 'pose.html')">Launch Experiment</button>
273
+ </div>
274
+
275
+ <!-- Option 2: Object Recognition -->
276
+ <div class="card">
277
+ <div class="icon-box">🔍</div>
278
+ <h2>Object Recognition</h2>
279
+ <p>
280
+ <strong>How does a self-driving car see?</strong><br><br>
281
+ This model is trained to recognize thousands of everyday items. It draws a box around what it sees and gives it a label.
282
+ <br><br>
283
+ <em>Try this: Hold up a pen, a water bottle, or your shoe. Can it guess them all correctly?</em>
284
+ </p>
285
+ <center>
286
+ <button id="dev-trigger-btn">Wondering what objects this model can detect? Click here to find out!</button>
287
+ </center>
288
+ <br>
289
+ <button class="launch-btn" onclick="openWindow('Object Recognition', 'Object-Detection.html')">Launch Experiment</button>
290
+ </div>
291
+
292
+ <!-- Option 3: Face Expression -->
293
+ <div class="card">
294
+ <div class="icon-box">😊</div>
295
+ <h2>Face Expression</h2>
296
+ <p>
297
+ <strong>Can computers understand feelings?</strong><br><br>
298
+ This AI looks at the geometry of your face—how much your mouth curves or your eyebrows lift—to guess your emotion.
299
+ <br><br>
300
+ <em>Try this: Make a super happy face, then a sad face. See if the "Confidence Score" changes!</em>
301
+ </p>
302
+ <button class="launch-btn" onclick="openWindow('Face Expression', 'Face-Expresion.html')">Launch Experiment</button>
303
+ </div>
304
+ </div>
305
+
306
+ <h1 style="text-align:center; margin:60px 0 30px;">
307
+ Here are some extra ones that you might like....
308
+ </h1>
309
+
310
+ <div class="playground-grid">
311
+ <!-- Extra Option 1 -->
312
+ <div class="card">
313
+ <div class="icon-box">🏃</div>
314
+ <h2>JS WebCam Motion Detection</h2>
315
+ <p>
316
+ <strong>Can a computer actually “see” movement?</strong><br><br>
317
+ This project watches your webcam and compares each frame to the one before it.
318
+ When something moves—even a tiny bit—the computer highlights the change so you can
319
+ spot motion glowing on the screen.<br><br>
320
+ <em>Try this: Wave your hand slowly, then quickly. Notice how the motion pattern changes!</em>
321
+ </p>
322
+ <button class="launch-btn" onclick="openWindow('Movement Detection', 'movement-detection.html')">Launch Experiment</button>
323
+ </div>
324
+
325
+ <!-- Extra Option 2 -->
326
+ <div class="card">
327
+ <div class="icon-box">👁️</div>
328
+ <h2>Object Detection with TensorFlow.js</h2>
329
+ <p>
330
+ <strong>How can a computer tell what's in a picture?</strong><br><br>
331
+ This project uses a pre‑trained AI model that can look at an image—or even your
332
+ webcam—and point out objects it recognizes. It doesn’t just guess the object,
333
+ it also shows *where* it is in the picture by drawing a box around it.<br><br>
334
+ <em>Try this: Hold up different objects (like a pencil, cup, or book) and see
335
+ which ones the AI can detect!</em>
336
+ </p>
337
+ <button class="launch-btn" onclick="openWindow('Object Detection with TensorFlow.js', 'live-feed-object-detection.html')">Launch Experiment</button>
338
+ </div>
339
+ </div>
340
+
341
+ <!-- Modal Overlay -->
342
+ <div class="modal-overlay" id="modalOverlay">
343
+ <div class="mac-window" id="macWindow">
344
+ <div class="window-header">
345
+ <div class="traffic-light" id="closeBtn"></div>
346
+ <div class="window-title" id="windowTitle">Terminal</div>
347
+ </div>
348
+ <div class="window-content">
349
+ <iframe id="labFrame" src=""></iframe>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+
355
+ <script>
356
+ // Wait for DOM to load to prevent "not defined" errors
357
+ document.addEventListener('DOMContentLoaded', function() {
358
+
359
+ const overlay = document.getElementById('modalOverlay');
360
+ const macWindow = document.getElementById('macWindow');
361
+ const labFrame = document.getElementById('labFrame');
362
+ const windowTitle = document.getElementById('windowTitle');
363
+ const closeBtn = document.getElementById('closeBtn');
364
+
365
+ // Global function to be called by buttons
366
+ window.openWindow = function(title, fileName) {
367
+ if (windowTitle) windowTitle.textContent = "Running: " + title;
368
+ if (overlay) overlay.classList.add('active');
369
+
370
+ // Set the iframe SRC to the actual file link
371
+ if (labFrame) labFrame.src = fileName;
372
+ };
373
+
374
+ function closeWindow() {
375
+ if (overlay) overlay.classList.remove('active');
376
+ // Clear iframe src to stop the page from running in background
377
+ setTimeout(() => {
378
+ if (labFrame) labFrame.src = "";
379
+ }, 300);
380
+ }
381
+
382
+ // Close button listener
383
+ if (closeBtn) {
384
+ closeBtn.addEventListener('click', closeWindow);
385
+ }
386
+
387
+ // Click outside to close Logic
388
+ if (overlay) {
389
+ overlay.addEventListener('click', function(event) {
390
+ if (event.target === overlay) {
391
+ closeWindow();
392
+ }
393
+ });
394
+ }
395
+
396
+ // Prevent clicks inside the window from closing it
397
+ if (macWindow) {
398
+ macWindow.addEventListener('click', function(event) {
399
+ event.stopPropagation();
400
+ });
401
+ }
402
+ });
403
+ </script>
404
+
405
+ </body>
406
+ </html>
407
+
408
+
409
+
410
+
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+
425
+
426
+
427
+
428
+
429
+
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+
439
+
440
+
441
+
442
+
443
+
444
+
445
+
446
+
447
+
448
+
449
+
450
+
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
+
461
+
462
+
463
+
464
+
465
+
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
475
+
476
+
477
+
478
+
479
+
480
+
481
+
482
+
483
+
484
+
485
+
486
+
487
+
488
+
489
+
490
+
491
+
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+
533
+
534
+
535
+
536
+
537
+
538
+
539
+
540
+
541
+
542
+
543
+
544
+
545
+
546
+
547
+
548
+
549
+
550
+
551
+
552
+
553
+
554
+
555
+
556
+
557
+
558
+
559
+
560
+
561
+
562
+
563
+
564
+
565
+
566
+
567
+
568
+
569
+
570
+
571
+
572
+
573
+
574
+
575
+
576
+
577
+
578
+
579
+
580
+
581
+
582
+
583
+
584
+
585
+
586
+
587
+
588
+
589
+
590
+
591
+
592
+
593
+
594
+
595
+
596
+
597
+
598
+
599
+
600
+
601
+
602
+
603
+
604
+
605
+
606
+
607
+
608
+
609
+
610
+
611
+
612
+
613
+
614
+
615
+
616
+
617
+
618
+
619
+
620
+
621
+
622
+
623
+
624
+
625
+
626
+
627
+
628
+
629
+
630
+
631
+
632
+
633
+
634
+
635
+
636
+
637
+
638
+
639
+
640
+
641
+
642
+
643
+
644
+
645
+
646
+
647
+
648
+
649
+
650
+
651
+
652
+
653
+
654
+
655
+
656
+
657
+
658
+
659
+
660
+
661
+
662
+
663
+
664
+
665
+
666
+
667
+
668
+
669
+
670
+
671
+
672
+
673
+
674
+
675
+
676
+
677
+
678
+
679
+
680
+
681
+
682
+
683
+
684
+
685
+
686
+
687
+
688
+
689
+
690
+
691
+
692
+
693
+
694
+
695
+
696
+
697
+
698
+
699
+
700
+
701
+
702
+
703
+
704
+
705
+
706
+
707
+
708
+
709
+
710
+
711
+
712
+
713
+
714
+
715
+
716
+
717
+
718
+
719
+
720
+
721
+
722
+
723
+
724
+
725
+
726
+
727
+
728
+
729
+
730
+
731
+
732
+
733
+
734
+
735
+
736
+
737
+
738
+
739
+
740
+
741
+
742
+
743
+
744
+
745
+
746
+
747
+
748
+
749
+
750
+
751
+
752
+
753
+
754
+
755
+
756
+
757
+
758
+
759
+
760
+
761
+
762
+
763
+
764
+
765
+
766
+
767
+
768
+
769
+
770
+
771
+
772
+
773
+
774
+
775
+
776
+
777
+
778
+
779
+
780
+
781
+
782
+
783
+
784
+
785
+
786
+
787
+
788
+
789
+
790
+
791
+
792
+
793
+
794
+
795
+
796
+
797
+
798
+
799
+
800
+
801
+
802
+
803
+
804
+
805
+
806
+
807
+
808
+
809
+
810
+
811
+
812
+
813
+
814
+
815
+
816
+
817
+
818
+
819
+
820
+
821
+
822
+
823
+
824
+
825
+
826
+
827
+
828
+
829
+
830
+
831
+
832
+
833
+
834
+
835
+
836
+
837
+
838
+
839
+
840
+
841
+
842
+
843
+
844
+
845
+
846
+
847
+
848
+
849
+
850
+
851
+
852
+
853
+
854
+
855
+
856
+
857
+
858
+
859
+
860
+
861
+
862
+
863
+
864
+
865
+
866
+
867
+
868
+
869
+
870
+
871
+
872
+
873
+
874
+
875
+
876
+
877
+
878
+
879
+
880
+
881
+
882
+
883
+
884
+
885
+
886
+
887
+
888
+
889
+
890
+
891
+
892
+
893
+
894
+
895
+
896
+
897
+
898
+
899
+
900
+
901
+
902
+
903
+
904
+ <!DOCTYPE html>
905
+ <html lang="en">
906
+ <head>
907
+ <meta charset="UTF-8">
908
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
909
+ <style>
910
+
911
+
912
+ /* The Trigger Button */
913
+ #dev-trigger-btn {
914
+ background-color: #007AFF;
915
+ color: white;
916
+ border: none;
917
+ padding: 12px 24px;
918
+ font-size: 16px;
919
+ font-weight: 600;
920
+ border-radius: 8px;
921
+ cursor: pointer;
922
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
923
+ transition: transform 0.1s ease;
924
+ }
925
+
926
+ #dev-trigger-btn:active {
927
+ transform: scale(0.96);
928
+ }
929
+
930
+ /* Modal Overlay (Background) */
931
+ #modal-overlay {
932
+ display: none; /* Hidden by default */
933
+ position: fixed;
934
+ top: 0;
935
+ left: 0;
936
+ width: 100%;
937
+ height: 100%;
938
+ background: rgba(0, 0, 0, 0.4);
939
+ backdrop-filter: blur(5px);
940
+ z-index: 1000;
941
+ justify-content: center;
942
+ align-items: center;
943
+ opacity: 0;
944
+ transition: opacity 0.3s ease;
945
+ }
946
+
947
+ /* The Dark MacOS Window */
948
+ #macos-window {
949
+ width: 80%;
950
+ max-width: 900px;
951
+ height: 70%;
952
+ background-color: #1e1e1e; /* Dark Mode Background */
953
+ border-radius: 12px;
954
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.1);
955
+ display: flex;
956
+ flex-direction: column;
957
+ overflow: hidden;
958
+ transform: scale(0.95);
959
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
960
+ }
961
+
962
+ /* Window Title Bar */
963
+ .window-header {
964
+ height: 38px;
965
+ background: linear-gradient(to bottom, #3a3a3a, #2b2b2b);
966
+ border-bottom: 1px solid #000;
967
+ display: flex;
968
+ align-items: center;
969
+ padding: 0 16px;
970
+ flex-shrink: 0;
971
+ }
972
+
973
+ /* Traffic Light Buttons */
974
+ .window-controls {
975
+ display: flex;
976
+ gap: 8px;
977
+ }
978
+
979
+ .control-dot {
980
+ width: 12px;
981
+ height: 12px;
982
+ border-radius: 50%;
983
+ border: 1px solid rgba(0,0,0,0.2);
984
+ }
985
+
986
+ .close-dot { background-color: #ff5f56; cursor: pointer; }
987
+ .close-dot:hover { background-color: #ff3b30; } /* Brightens on hover */
988
+ .minimize-dot { background-color: #ffbd2e; }
989
+ .expand-dot { background-color: #27c93f; }
990
+
991
+ .window-title {
992
+ color: #d1d1d1;
993
+ font-size: 13px;
994
+ font-weight: 500;
995
+ margin-left: 20px;
996
+ flex-grow: 1;
997
+ text-align: center;
998
+ padding-right: 60px; /* Balance the title */
999
+ }
1000
+
1001
+ /* Content Area */
1002
+ .window-content {
1003
+ padding: 20px;
1004
+ overflow-y: auto;
1005
+ color: #fff;
1006
+ }
1007
+
1008
+ /* Grid for Tiles */
1009
+ .tiles-grid {
1010
+ display: grid;
1011
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1012
+ gap: 16px;
1013
+ }
1014
+
1015
+ /* Individual Tile Style */
1016
+ .tile {
1017
+ background-color: #2c2c2e;
1018
+ border: 1px solid #3a3a3c;
1019
+ border-radius: 8px;
1020
+ padding: 16px;
1021
+ display: flex;
1022
+ flex-direction: column;
1023
+ gap: 8px;
1024
+ transition: background-color 0.2s;
1025
+ }
1026
+
1027
+ .tile:hover {
1028
+ background-color: #3a3a3c;
1029
+ }
1030
+
1031
+ .tile-icon {
1032
+ font-size: 32px;
1033
+ }
1034
+
1035
+ .tile-name {
1036
+ font-size: 16px;
1037
+ font-weight: 700;
1038
+ color: #fff;
1039
+ text-transform: capitalize;
1040
+ }
1041
+
1042
+ .tile-desc {
1043
+ font-size: 13px;
1044
+ line-height: 1.4;
1045
+ color: #a1a1a6;
1046
+ }
1047
+
1048
+ /* Scrollbar Styling */
1049
+ .window-content::-webkit-scrollbar {
1050
+ width: 8px;
1051
+ }
1052
+ .window-content::-webkit-scrollbar-track {
1053
+ background: #1e1e1e;
1054
+ }
1055
+ .window-content::-webkit-scrollbar-thumb {
1056
+ background: #48484a;
1057
+ border-radius: 4px;
1058
+ }
1059
+
1060
+ /* Animation States */
1061
+ #modal-overlay.active {
1062
+ display: flex;
1063
+ opacity: 1;
1064
+ }
1065
+
1066
+ #modal-overlay.active #macos-window {
1067
+ transform: scale(1);
1068
+ }
1069
+ </style>
1070
+ </head>
1071
+ <body>
1072
+
1073
+ <!-- 1. The Trigger Button -->
1074
+
1075
+ <!-- 2. The Modal Structure -->
1076
+ <div id="modal-overlay">
1077
+ <div id="macos-window">
1078
+ <!-- Header with Red Button -->
1079
+ <div class="window-header">
1080
+ <div class="window-controls">
1081
+ <div class="control-dot close-dot" id="close-btn" title="Close"></div>
1082
+ </div>
1083
+
1084
+ <div class="window-title">COCO Dataset Classes (80 Objects)</div>
1085
+ </div>
1086
+
1087
+ <!-- Content -->
1088
+ <div class="window-content">
1089
+ <div style="margin-bottom: 20px; color: #a1a1a6; font-size: 14px;">
1090
+ This AI model is trained to see the world like you do. Below are the 80 different things it can instantly recognize in a photo.
1091
+ </div>
1092
+ <div class="tiles-grid" id="tiles-container">
1093
+ <!-- Tiles injected via JS -->
1094
+ </div>
1095
+ </div>
1096
+ </div>
1097
+ </div>
1098
+
1099
+ <script>
1100
+ // Data: 80 COCO Classes with Emojis and Kid-Friendly Explanations
1101
+ const cocoData = [
1102
+ { name: "person", emoji: "👤", desc: "A human being, like you, your friends, or your teacher." },
1103
+ { name: "bicycle", emoji: "🚲", desc: "A two-wheeled vehicle that you pedal to ride." },
1104
+ { name: "car", emoji: "🚗", desc: "A vehicle with four wheels used to drive on roads." },
1105
+ { name: "motorcycle", emoji: "🏍️", desc: "Like a bicycle, but with a fast engine and heavier wheels." },
1106
+ { name: "airplane", emoji: "✈️", desc: "A flying machine with wings that carries passengers across the sky." },
1107
+ { name: "bus", emoji: "🚌", desc: "A large vehicle that carries many people along a set route." },
1108
+ { name: "train", emoji: "🚆", desc: "A series of connected cars that run on a metal track." },
1109
+ { name: "truck", emoji: "🚚", desc: "A large, heavy vehicle used for carrying cargo or big loads." },
1110
+ { name: "boat", emoji: "⛵", desc: "A vehicle designed to float and travel on water." },
1111
+ { name: "traffic light", emoji: "🚦", desc: "A signal with red, yellow, and green lights to control cars." },
1112
+ { name: "fire hydrant", emoji: "🚒", desc: "A pipe on the street that firefighters use to get water." },
1113
+ { name: "stop sign", emoji: "🛑", desc: "A red octagon sign that tells drivers to stop moving." },
1114
+ { name: "parking meter", emoji: "🅿️", desc: "A machine where you pay money to park your car on the street." },
1115
+ { name: "bench", emoji: "🪑", desc: "A long seat for several people, usually found in parks." },
1116
+ { name: "bird", emoji: "🐦", desc: "An animal with feathers and wings that can usually fly." },
1117
+ { name: "cat", emoji: "🐈", desc: "A small, furry pet that purrs and catches mice." },
1118
+ { name: "dog", emoji: "🐕", desc: "A loyal pet with four legs that barks and loves to play." },
1119
+ { name: "horse", emoji: "🐎", desc: "A large, strong animal that people can ride." },
1120
+ { name: "sheep", emoji: "🐑", desc: "A fluffy farm animal that gives us wool for clothes." },
1121
+ { name: "cow", emoji: "🐄", desc: "A large farm animal that gives us milk." },
1122
+ { name: "elephant", emoji: "🐘", desc: "A huge animal with big ears and a long trunk." },
1123
+ { name: "bear", emoji: "🐻", desc: "A large, strong wild animal with thick fur." },
1124
+ { name: "zebra", emoji: "🦓", desc: "A wild horse-like animal with black and white stripes." },
1125
+ { name: "giraffe", emoji: "🦒", desc: "The tallest animal, with a very long neck to eat tree leaves." },
1126
+ { name: "backpack", emoji: "🎒", desc: "A bag you carry on your back to hold books and supplies." },
1127
+ { name: "umbrella", emoji: "☂️", desc: "A tool you hold over your head to stay dry in the rain." },
1128
+ { name: "handbag", emoji: "👜", desc: "A small bag used to carry personal items like a wallet." },
1129
+ { name: "tie", emoji: "👔", desc: "A long piece of cloth worn around the neck with a suit." },
1130
+ { name: "suitcase", emoji: "🧳", desc: "A large bag used for packing clothes when traveling." },
1131
+ { name: "frisbee", emoji: "🥏", desc: "A plastic disc you throw to a friend or dog." },
1132
+ { name: "skis", emoji: "⛷️", desc: "Long, flat runners you attach to boots to glide on snow." },
1133
+ { name: "snowboard", emoji: "🏂", desc: "A single wide board you stand on to surf down snowy hills." },
1134
+ { name: "sports ball", emoji: "⚽", desc: "Any round object used in games like soccer, basketball, or tennis." },
1135
+ { name: "kite", emoji: "🪁", desc: "A light frame with paper or cloth that flies in the wind." },
1136
+ { name: "baseball bat", emoji: "⚾", desc: "A smooth wooden or metal stick used to hit a baseball." },
1137
+ { name: "baseball glove", emoji: "🧤", desc: "A leather mitt worn to catch a baseball safely." },
1138
+ { name: "skateboard", emoji: "🛹", desc: "A short board with wheels that you stand on and ride." },
1139
+ { name: "surfboard", emoji: "🏄", desc: "A long board used to ride ocean waves." },
1140
+ { name: "tennis racket", emoji: "🎾", desc: "A bat with a net mesh used to hit a tennis ball." },
1141
+ { name: "bottle", emoji: "🍾", desc: "A container with a narrow neck for holding liquids." },
1142
+ { name: "wine glass", emoji: "🍷", desc: "A fancy glass with a stem, used for special drinks." },
1143
+ { name: "cup", emoji: "☕", desc: "A small open container for drinking tea or coffee." },
1144
+ { name: "fork", emoji: "🍴", desc: "A tool with prongs used to pick up food." },
1145
+ { name: "knife", emoji: "🔪", desc: "A tool with a sharp edge used for cutting food." },
1146
+ { name: "spoon", emoji: "🥄", desc: "A tool with a small scoop for eating soup or cereal." },
1147
+ { name: "bowl", emoji: "🥣", desc: "A round, deep dish used for soup or cereal." },
1148
+ { name: "banana", emoji: "🍌", desc: "A long, curved yellow fruit that you peel to eat." },
1149
+ { name: "apple", emoji: "🍎", desc: "A round red or green fruit that is crunchy and sweet." },
1150
+ { name: "sandwich", emoji: "🥪", desc: "Two slices of bread with meat, cheese, or veggies inside." },
1151
+ { name: "orange", emoji: "🍊", desc: "A round citrus fruit with a thick skin and juicy inside." },
1152
+ { name: "broccoli", emoji: "🥦", desc: "A green vegetable that looks like a little tree." },
1153
+ { name: "carrot", emoji: "🥕", desc: "A crunchy orange vegetable that grows underground." },
1154
+ { name: "hot dog", emoji: "🌭", desc: "A cooked sausage served in a long, soft bun." },
1155
+ { name: "pizza", emoji: "🍕", desc: "A round dough base topped with tomato sauce and cheese." },
1156
+ { name: "donut", emoji: "🍩", desc: "A sweet, fried ring of dough, often with frosting." },
1157
+ { name: "cake", emoji: "🎂", desc: "A sweet baked dessert often eaten at birthday parties." },
1158
+ { name: "chair", emoji: "🪑", desc: "A piece of furniture designed for one person to sit on." },
1159
+ { name: "couch", emoji: "🛋️", desc: "A soft, long seat where multiple people can relax." },
1160
+ { name: "potted plant", emoji: "🪴", desc: "A plant grown inside a container for decoration." },
1161
+ { name: "bed", emoji: "🛏️", desc: "A piece of furniture with a mattress for sleeping." },
1162
+ { name: "dining table", emoji: "🍽️", desc: "A large table where families sit to eat meals." },
1163
+ { name: "toilet", emoji: "🚽", desc: "A bathroom fixture used for getting rid of waste." },
1164
+ { name: "tv", emoji: "📺", desc: "An electronic screen for watching shows and movies." },
1165
+ { name: "laptop", emoji: "💻", desc: "A portable computer that you can use on your lap." },
1166
+ { name: "mouse", emoji: "🖱️", desc: "A handheld device used to move the cursor on a computer." },
1167
+ { name: "remote", emoji: "📡", desc: "A controller used to change channels on a TV from afar." },
1168
+ { name: "keyboard", emoji: "⌨️", desc: "A board with buttons for typing letters into a computer." },
1169
+ { name: "cell phone", emoji: "📱", desc: "A small phone you can carry in your pocket." },
1170
+ { name: "microwave", emoji: "​​​​​​⏲️", desc: "An oven that heats food very quickly." },
1171
+ { name: "oven", emoji: "🥘", desc: "A kitchen appliance used for baking and roasting food." },
1172
+ { name: "toaster", emoji: "🍞", desc: "A machine that browns bread slices." },
1173
+ { name: "sink", emoji: "🚰", desc: "A basin with a faucet for washing hands or dishes." },
1174
+ { name: "refrigerator", emoji: "❄️", desc: "A cold closet that keeps food fresh." },
1175
+ { name: "book", emoji: "📖", desc: "A set of pages with writing or pictures to read." },
1176
+ { name: "clock", emoji: "⏰", desc: "A device that tells you what time it is." },
1177
+ { name: "vase", emoji: "🏺", desc: "A decorative container used for holding flowers." },
1178
+ { name: "scissors", emoji: "✂️", desc: "A tool with two blades used for cutting paper." },
1179
+ { name: "teddy bear", emoji: "🧸", desc: "A soft toy bear that is cuddly and cute." },
1180
+ { name: "hair drier", emoji: "💨", desc: "A machine that blows hot air to dry wet hair." },
1181
+ { name: "toothbrush", emoji: "🪥", desc: "A small brush used to clean your teeth." }
1182
+ ];
1183
+
1184
+ // DOM Elements
1185
+ const triggerBtn = document.getElementById('dev-trigger-btn');
1186
+ const modal = document.getElementById('modal-overlay');
1187
+ const closeBtn = document.getElementById('close-btn');
1188
+ const tilesContainer = document.getElementById('tiles-container');
1189
+ const macosWindow = document.getElementById('macos-window');
1190
+
1191
+ // 1. Populate the Grid
1192
+ function renderTiles() {
1193
+ tilesContainer.innerHTML = cocoData.map(item => `
1194
+ <div class="tile">
1195
+ <div class="tile-icon">${item.emoji}</div>
1196
+ <div class="tile-name">${item.name}</div>
1197
+ <div class="tile-desc">${item.desc}</div>
1198
+ </div>
1199
+ `).join('');
1200
+ }
1201
+
1202
+ // 2. Open Modal Function
1203
+ triggerBtn.addEventListener('click', () => {
1204
+ renderTiles(); // Render content on open
1205
+ modal.classList.add('active'); // Show modal
1206
+ });
1207
+
1208
+ // 3. Close Modal Function
1209
+ function closeModal() {
1210
+ modal.classList.remove('active');
1211
+ }
1212
+
1213
+ // Event: Click the Red Dot
1214
+ closeBtn.addEventListener('click', closeModal);
1215
+
1216
+ // Event: Click Outside the Window
1217
+ modal.addEventListener('click', (e) => {
1218
+ // If the user clicks the dark background (modal), close it.
1219
+ // If they click INSIDE the window, do nothing.
1220
+ if (e.target === modal) {
1221
+ closeModal();
1222
+ }
1223
+ });
1224
+
1225
+ </script>
1226
+ </body>
1227
+ </html>
Object-Detection.html ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Enhanced Object Detection</title>
7
+ <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
8
+ <style>
9
+ body { font-family: 'Roboto', sans-serif; margin: 2em; color: #3d3d3d; background: #f0f2f5; }
10
+ h1 { color: #007f8b; text-align: center; }
11
+ .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
12
+
13
+ /* Controls Section */
14
+ .controls { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; padding: 15px; background: #e6fcfd; border-radius: 8px; align-items: center; }
15
+ .control-group { display: flex; flex-direction: column; min-width: 200px; }
16
+ label { font-weight: bold; font-size: 0.9em; margin-bottom: 5px; color: #007f8b; }
17
+ input[type=range] { width: 100%; }
18
+ .upload-btn { background: #007f8b; color: white; padding: 10px 20px; border-radius: 25px; cursor: pointer; display: inline-block; font-weight: bold; text-align: center; }
19
+ .upload-btn:hover { background: #006069; }
20
+ input[type="file"] { display: none; }
21
+
22
+ /* Image Grid */
23
+ /* Replace the existing .image-grid and .detect-card styles */
24
+
25
+ .image-grid {
26
+ /* Disable Grid, use Columns instead */
27
+ display: block;
28
+ column-count: 3; /* Creates 3 columns like Pinterest */
29
+ column-gap: 20px;
30
+ }
31
+
32
+ /* Responsive: 2 columns on smaller screens */
33
+ @media (max-width: 900px) {
34
+ .image-grid { column-count: 2; }
35
+ }
36
+ @media (max-width: 600px) {
37
+ .image-grid { column-count: 1; }
38
+ }
39
+
40
+ .detect-card {
41
+ position: relative;
42
+ border-radius: 8px;
43
+ overflow: hidden;
44
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
45
+ background: #000;
46
+ cursor: pointer;
47
+ transition: transform 0.2s;
48
+ z-index: 1;
49
+
50
+ /* NEW: Prevents card from splitting across columns */
51
+ break-inside: avoid-column;
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ .detect-card:hover { transform: scale(1.01); }
56
+ .detect-card img { display: block; width: 100%; height: auto; transition: opacity 0.3s; }
57
+
58
+ /* Processing State */
59
+ .detect-card.processing { pointer-events: none; } /* Prevent double clicks */
60
+ .detect-card.processing img { opacity: 0.6; filter: grayscale(50%); }
61
+
62
+ /* --- NEW: Inference Loading Bar --- */
63
+ .inference-panel {
64
+ position: absolute;
65
+ bottom: 0;
66
+ left: 0;
67
+ width: 100%;
68
+ background: rgba(255, 255, 255, 0.95);
69
+ padding: 15px;
70
+ box-sizing: border-box;
71
+ transform: translateY(100%); /* Hidden by default */
72
+ transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
73
+ z-index: 50;
74
+ border-top: 3px solid #007f8b;
75
+ }
76
+
77
+ /* Show panel when processing */
78
+ .detect-card.processing .inference-panel {
79
+ transform: translateY(0);
80
+ }
81
+
82
+ .inference-status {
83
+ display: flex;
84
+ justify-content: space-between;
85
+ font-weight: bold;
86
+ color: #007f8b;
87
+ margin-bottom: 8px;
88
+ font-size: 0.9rem;
89
+ }
90
+
91
+ .progress-track {
92
+ width: 100%;
93
+ height: 6px;
94
+ background: #e0e0e0;
95
+ border-radius: 3px;
96
+ overflow: hidden;
97
+ }
98
+
99
+ .progress-bar {
100
+ height: 100%;
101
+ background: #007f8b;
102
+ width: 30%;
103
+ border-radius: 3px;
104
+ animation: loading 1.5s infinite ease-in-out;
105
+ }
106
+
107
+ @keyframes loading {
108
+ 0% { transform: translateX(-100%); }
109
+ 100% { transform: translateX(400%); }
110
+ }
111
+ /* ---------------------------------- */
112
+
113
+ /* Bounding Boxes */
114
+ .highlighter { position: absolute; border: 2px solid; border-radius: 4px; z-index: 10; pointer-events: none; }
115
+ .label-tag { position: absolute; padding: 2px 6px; color: white; font-size: 11px; font-weight: bold; border-radius: 4px; pointer-events: none; z-index: 11; white-space: nowrap; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
116
+
117
+ /* Loading Spinner (Initial Model Load) */
118
+ #loader { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px; background: white; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.2); display: none; z-index: 1000; font-weight: bold; }
119
+ </style>
120
+ </head>
121
+ <body>
122
+
123
+ <div class="container">
124
+ <h1>Smart Object Recognition</h1>
125
+
126
+ <div class="controls">
127
+ <div class="control-group">
128
+ <label for="imageUpload" class="upload-btn">📂 Upload Image</label>
129
+ <input type="file" id="imageUpload" accept="image/*">
130
+ </div>
131
+
132
+ <div class="control-group">
133
+ <label>Confidence Threshold: <span id="confValue">50</span>%</label>
134
+ <input type="range" id="confidenceSlider" min="10" max="90" value="50">
135
+ <small>Increase to remove weak guesses.</small>
136
+ </div>
137
+
138
+ <div class="control-group">
139
+ <label>Overlap Fix (NMS): <span id="overlapValue">30</span>%</label>
140
+ <input type="range" id="overlapSlider" min="0" max="100" value="30">
141
+ <small>Lower value = Fewer overlapping boxes.</small>
142
+ </div>
143
+ </div>
144
+
145
+ <div id="loader">Loading AI Model...</div>
146
+
147
+ <div class="image-grid" id="imageContainer">
148
+ <!-- Image 1 -->
149
+ <div class="detect-card">
150
+ <img src="https://assets.codepen.io/9177687/coupledog.jpeg" crossorigin="anonymous" />
151
+ </div>
152
+
153
+ <!-- Image 2 -->
154
+ <div class="detect-card">
155
+ <img src="https://assets.codepen.io/9177687/doggo.jpeg" crossorigin="anonymous" />
156
+ </div>
157
+
158
+ <!-- Image 3 (FIXED: Added wrapper div) -->
159
+ <div class="detect-card">
160
+ <img src="https://tse3.mm.bing.net/th/id/OIP.mIJJ36cXpVujF1wnZnd4VQHaE8?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
161
+ </div>
162
+
163
+ <!-- Image 4 -->
164
+ <div class="detect-card">
165
+ <img src="https://images.pexels.com/photos/23409055/pexels-photo-23409055/free-photo-of-cars-on-street-in-town.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1" crossorigin="anonymous" />
166
+ </div>
167
+
168
+ <!-- Image 5 -->
169
+ <div class="detect-card">
170
+ <img src="https://tse4.mm.bing.net/th/id/OIP.bWwaHeR-aoBb3esBRaAEEgHaE8?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
171
+ </div>
172
+
173
+ <!-- Image 6 -->
174
+ <div class="detect-card">
175
+ <img src="https://tse4.mm.bing.net/th/id/OIP.vs_d1C-7n4PoNv0GVlaVDwHaFj?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
176
+ </div>
177
+
178
+ <!-- Image 7 -->
179
+ <div class="detect-card">
180
+ <img src="https://tse4.mm.bing.net/th/id/OIP.V1zVa5IUI22o0i6gG4or2QHaLH?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
181
+ </div>
182
+
183
+ <!-- Image 8 -->
184
+ <div class="detect-card">
185
+ <img src="https://th.bing.com/th/id/R.2be55af1ab4a38df1a9b54bf6b68a8bd?rik=f7wiKdc8N42mgA&pid=ImgRaw&r=0" crossorigin="anonymous" />
186
+ </div>
187
+
188
+ <!-- Image 9 -->
189
+ <div class="detect-card">
190
+ <img src="https://images.pexels.com/photos/20625972/pexels-photo-20625972.jpeg?cs=srgb&dl=pexels-saturnus99-20625972.jpg&fm=jpg" crossorigin="anonymous" />
191
+ </div>
192
+
193
+ <!-- Image 10 -->
194
+ <div class="detect-card">
195
+ <img src="https://tse4.mm.bing.net/th/id/OIP.ZMnNqw1GVTa9HpHvsTYcjQAAAA?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
196
+ </div>
197
+
198
+ <!-- Image 11 -->
199
+ <div class="detect-card">
200
+ <img src="https://bestbackpacklab.com/wp-content/uploads/2021/05/children-1536x864.jpg" crossorigin="anonymous" />
201
+ </div>
202
+
203
+ <!-- Image 12 -->
204
+ <div class="detect-card">
205
+ <img src="https://tse1.explicit.bing.net/th/id/OIP.za2l0WGKXbR4Qkj8phu2UwHaE8?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
206
+ </div>
207
+
208
+ <!-- Image 13 -->
209
+ <div class="detect-card">
210
+ <img src="https://tse2.mm.bing.net/th/id/OIP.bcOP7ZTpLAyyl3tKpdk5gAHaFB?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
211
+ </div>
212
+
213
+ <!-- Image 14 -->
214
+ <div class="detect-card">
215
+ <img src="https://tse3.mm.bing.net/th/id/OIP.PC6Fr2mEuGUsEiaNfCSOaAHaE7?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
216
+ </div>
217
+
218
+ <!-- Image 15 -->
219
+ <div class="detect-card">
220
+ <img src="https://tse3.mm.bing.net/th/id/OIF.EABwKojMHBX0uEfpxor95w?rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
221
+ </div>
222
+
223
+ <!-- Image 16 -->
224
+ <div class="detect-card">
225
+ <img src="https://tse4.mm.bing.net/th/id/OIP.qliYrfiREN-ydW4DxWYfSgHaE7?w=626&h=417&rs=1&pid=ImgDetMain&o=7&rm=3" crossorigin="anonymous" />
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+ <script type="module">
231
+ import { ObjectDetector, FilesetResolver } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2";
232
+
233
+ const loader = document.getElementById("loader");
234
+ let objectDetector;
235
+ let runningMode = "IMAGE";
236
+
237
+ // SETTINGS
238
+ let confidenceThreshold = 0.5;
239
+ let overlapThreshold = 0.3;
240
+
241
+ // 1. Initialize MediaPipe
242
+ const initializeObjectDetector = async () => {
243
+ loader.style.display = "block";
244
+ const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm");
245
+
246
+ objectDetector = await ObjectDetector.createFromOptions(vision, {
247
+ baseOptions: {
248
+ modelAssetPath: `https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite2/float16/1/efficientdet_lite2.tflite`,
249
+ delegate: "GPU"
250
+ },
251
+ scoreThreshold: 0.2,
252
+ runningMode: runningMode
253
+ });
254
+
255
+ loader.style.display = "none";
256
+ console.log("Model Loaded: EfficientDet-Lite2");
257
+ };
258
+
259
+ initializeObjectDetector();
260
+
261
+ // 2. NMS Filter Function
262
+ function filterDetections(detections, iouLimit) {
263
+ detections.sort((a, b) => b.categories[0].score - a.categories[0].score);
264
+
265
+ const selected = [];
266
+ const active = new Array(detections.length).fill(true);
267
+
268
+ for (let i = 0; i < detections.length; i++) {
269
+ if (!active[i]) continue;
270
+ if (detections[i].categories[0].score < confidenceThreshold) continue;
271
+
272
+ selected.push(detections[i]);
273
+ const boxA = detections[i].boundingBox;
274
+
275
+ for (let j = i + 1; j < detections.length; j++) {
276
+ if (!active[j]) continue;
277
+
278
+ const boxB = detections[j].boundingBox;
279
+
280
+ const x1 = Math.max(boxA.originX, boxB.originX);
281
+ const y1 = Math.max(boxA.originY, boxB.originY);
282
+ const x2 = Math.min(boxA.originX + boxA.width, boxB.originX + boxB.width);
283
+ const y2 = Math.min(boxA.originY + boxA.height, boxB.originY + boxB.height);
284
+
285
+ if (x2 < x1 || y2 < y1) continue;
286
+
287
+ const intersection = (x2 - x1) * (y2 - y1);
288
+ const areaA = boxA.width * boxA.height;
289
+ const areaB = boxB.width * boxB.height;
290
+ const union = areaA + areaB - intersection;
291
+
292
+ const iou = intersection / union;
293
+
294
+ if (iou > iouLimit) {
295
+ active[j] = false;
296
+ }
297
+ }
298
+ }
299
+ return selected;
300
+ }
301
+
302
+ // 3. Handle Clicks & Draw
303
+ async function handleClick(event) {
304
+ if (!objectDetector) return;
305
+
306
+ const img = event.target;
307
+ const card = img.parentNode;
308
+
309
+ // -- NEW: Inject/Show Inference Bar --
310
+ let infoPanel = card.querySelector('.inference-panel');
311
+ if (!infoPanel) {
312
+ infoPanel = document.createElement('div');
313
+ infoPanel.className = 'inference-panel';
314
+ infoPanel.innerHTML = `
315
+ <div class="inference-status">
316
+ <span>Running Inference...</span>
317
+ <span>Please wait</span>
318
+ </div>
319
+ <div class="progress-track">
320
+ <div class="progress-bar"></div>
321
+ </div>
322
+ `;
323
+ card.appendChild(infoPanel);
324
+ }
325
+
326
+ // 1. Show Loading State
327
+ card.classList.add('processing');
328
+ // Clear old boxes immediately so the user sees a "reset"
329
+ card.querySelectorAll('.highlighter, .label-tag').forEach(el => el.remove());
330
+
331
+ // 2. Force a tiny delay so the browser renders the loading bar
332
+ // before the heavy synchronous AI detection freezes the thread.
333
+ await new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 50)));
334
+
335
+ try {
336
+ // 3. Run Detection
337
+ const predictions = objectDetector.detect(img);
338
+ const filteredDetections = filterDetections(predictions.detections, overlapThreshold);
339
+ displayDetections(filteredDetections, img);
340
+ } catch(e) {
341
+ console.error(e);
342
+ alert("Error running model");
343
+ } finally {
344
+ // 4. Hide Loading State
345
+ card.classList.remove('processing');
346
+ }
347
+ }
348
+
349
+ function displayDetections(detections, img) {
350
+ const ratioX = img.width / img.naturalWidth;
351
+ const ratioY = img.height / img.naturalHeight;
352
+
353
+ detections.forEach(detection => {
354
+ const box = detection.boundingBox;
355
+ const category = detection.categories[0];
356
+ const score = Math.round(category.score * 100);
357
+ const color = getColorForLabel(category.categoryName);
358
+
359
+ const highlighter = document.createElement("div");
360
+ highlighter.className = "highlighter";
361
+ highlighter.style.left = `${box.originX * ratioX}px`;
362
+ highlighter.style.top = `${box.originY * ratioY}px`;
363
+ highlighter.style.width = `${box.width * ratioX}px`;
364
+ highlighter.style.height = `${box.height * ratioY}px`;
365
+ highlighter.style.borderColor = color;
366
+ highlighter.style.backgroundColor = color + "20";
367
+
368
+ const label = document.createElement("div");
369
+ label.className = "label-tag";
370
+ label.innerText = `${category.categoryName} ${score}%`;
371
+ label.style.backgroundColor = color;
372
+
373
+ const topPos = (box.originY * ratioY) - 25;
374
+ label.style.left = `${box.originX * ratioX}px`;
375
+ label.style.top = `${topPos > 0 ? topPos : (box.originY * ratioY)}px`;
376
+
377
+ img.parentNode.appendChild(highlighter);
378
+ img.parentNode.appendChild(label);
379
+ });
380
+ }
381
+
382
+ function getColorForLabel(label) {
383
+ let hash = 0;
384
+ for (let i = 0; i < label.length; i++) {
385
+ hash = label.charCodeAt(i) + ((hash << 5) - hash);
386
+ }
387
+ const c = (hash & 0x00FFFFFF).toString(16).toUpperCase();
388
+ return "#" + "00000".substring(0, 6 - c.length) + c;
389
+ }
390
+
391
+ // 4. Initialization & Event Listeners
392
+ const imageContainer = document.getElementById("imageContainer");
393
+
394
+ imageContainer.addEventListener('click', (e) => {
395
+ if (e.target.tagName === 'IMG') handleClick(e);
396
+ });
397
+
398
+ document.getElementById('imageUpload').addEventListener('change', (e) => {
399
+ const file = e.target.files[0];
400
+ if (!file) return;
401
+
402
+ const reader = new FileReader();
403
+ reader.onload = (event) => {
404
+ const div = document.createElement('div');
405
+ div.className = 'detect-card';
406
+ const img = document.createElement('img');
407
+ img.src = event.target.result;
408
+ div.appendChild(img);
409
+ imageContainer.insertBefore(div, imageContainer.firstChild);
410
+ };
411
+ reader.readAsDataURL(file);
412
+ });
413
+
414
+ document.getElementById('confidenceSlider').addEventListener('input', (e) => {
415
+ confidenceThreshold = e.target.value / 100;
416
+ document.getElementById('confValue').innerText = e.target.value;
417
+ });
418
+
419
+ document.getElementById('overlapSlider').addEventListener('input', (e) => {
420
+ overlapThreshold = e.target.value / 100;
421
+ document.getElementById('overlapValue').innerText = e.target.value;
422
+ });
423
+ </script>
424
+
425
+ </body>
426
+ </html>
Pose.html ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" />
6
+ <title>MediaPipe Hands + FaceMesh (Bigger + Better)</title>
7
+
8
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils@0.3.1675466862/camera_utils.js" crossorigin="anonymous"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/hands.js" crossorigin="anonymous"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/face_mesh.js" crossorigin="anonymous"></script>
11
+
12
+ <style>
13
+ :root { color-scheme: dark; }
14
+ html, body { margin: 0; width: 100%; height: 100%; background: #000; overflow: hidden; }
15
+ #video { position: absolute; left: 0; top: 0; width: 2px; height: 2px; opacity: 0; pointer-events: none; z-index: -1; }
16
+ #canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; background: #000; }
17
+
18
+ #hud{
19
+ position:fixed; left:14px; top:14px; z-index:10; display:none;
20
+ padding:10px 12px; border-radius:10px;
21
+ background:rgba(18,18,18,0.72); border:1px solid rgba(255,255,255,0.10);
22
+ backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
23
+ box-shadow:0 10px 24px rgba(0,0,0,0.45);
24
+ font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
25
+ line-height:1.15; user-select:none; min-width: 190px;
26
+ }
27
+ .row{ display:flex; justify-content:space-between; gap:10px; }
28
+ .k{ color:rgba(255,255,255,0.65); font-size:11px; letter-spacing:0.12em; text-transform:uppercase; }
29
+ .v{ font-size:18px; font-weight:800; }
30
+
31
+ #start{
32
+ position:fixed; inset:0; margin:auto; z-index:20;
33
+ width:min(380px, calc(100vw - 36px)); height:56px;
34
+ border:0; border-radius:999px; cursor:pointer;
35
+ font-weight:800; letter-spacing:0.08em; text-transform:uppercase;
36
+ color:#001114;
37
+ background:linear-gradient(135deg, #00ffff 0%, #00d4ff 45%, #00ff9a 100%);
38
+ box-shadow:0 0 0 1px rgba(0,255,255,0.18), 0 18px 44px rgba(0,255,255,0.18);
39
+ }
40
+ </style>
41
+ </head>
42
+
43
+ <body>
44
+ <video id="video" playsinline muted></video>
45
+ <canvas id="canvas"></canvas>
46
+
47
+ <div id="hud">
48
+ <div class="row"><div class="k">Render</div><div id="fpsR" class="v">0</div></div>
49
+ <div class="row"><div class="k">Hands</div><div id="fpsH" class="v">0</div></div>
50
+ <div class="row"><div class="k">Face</div><div id="fpsF" class="v">0</div></div>
51
+ <div class="row"><div class="k">Face</div><div id="faceOn" class="v" style="font-size:14px;">OFF</div></div>
52
+ </div>
53
+
54
+ <button id="start">Start</button>
55
+
56
+ <script>
57
+ const HANDS_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/hands@0.4.1675469240/";
58
+ const FACE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/";
59
+
60
+ // Camera input (keep low for speed)
61
+ const CAM_W = 320, CAM_H = 240;
62
+
63
+ // Make face/hands BIGGER (visual-only scaling around their own centers)
64
+ // This does NOT change tracking, only how we draw it.
65
+ const HAND_SCALE = 1.35;
66
+ const FACE_SCALE = 1.25;
67
+
68
+ // Visual thickness/size (also makes them look bigger/better)
69
+ const HAND_LINE_W = 3.5;
70
+ const HAND_POINT = 4;
71
+ const FACE_POINT = 1.4;
72
+
73
+ // Scheduler rates
74
+ const HANDS_HZ = 24;
75
+ const FACE_HZ = 10;
76
+
77
+ // Model options
78
+ const MAX_HANDS = 2;
79
+ const HANDS_COMPLEXITY = 0;
80
+ const FACE_REFINE = false; // set true for nicer eyes/lips (slower)
81
+
82
+ const videoEl = document.getElementById('video');
83
+ const canvasEl = document.getElementById('canvas');
84
+ const ctx = canvasEl.getContext('2d', { alpha: false, desynchronized: true });
85
+
86
+ function resizeCanvas() {
87
+ const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
88
+ canvasEl.width = Math.floor(window.innerWidth * dpr);
89
+ canvasEl.height = Math.floor(window.innerHeight * dpr);
90
+ canvasEl.style.width = window.innerWidth + 'px';
91
+ canvasEl.style.height = window.innerHeight + 'px';
92
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
93
+ clearBlack();
94
+ }
95
+ function clearBlack() {
96
+ ctx.fillStyle = '#000000';
97
+ ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
98
+ }
99
+ window.addEventListener('resize', resizeCanvas, { passive: true });
100
+ resizeCanvas();
101
+
102
+ // Contain mapping (camera aspect to screen)
103
+ function getViewRect() {
104
+ const cw = window.innerWidth, ch = window.innerHeight;
105
+ const camAR = CAM_W / CAM_H;
106
+ const canvasAR = cw / ch;
107
+ let vw, vh, vx, vy;
108
+ if (canvasAR > camAR) { vh = ch; vw = vh * camAR; vx = (cw - vw) * 0.5; vy = 0; }
109
+ else { vw = cw; vh = vw / camAR; vx = 0; vy = (ch - vh) * 0.5; }
110
+ return { x: vx, y: vy, w: vw, h: vh };
111
+ }
112
+ function mapLM(lm, view) {
113
+ return { x: view.x + lm.x * view.w, y: view.y + lm.y * view.h };
114
+ }
115
+
116
+ // Compute centroid in pixel space (for visual scaling)
117
+ function centroidPx(landmarks, view) {
118
+ let sx = 0, sy = 0;
119
+ const n = landmarks.length;
120
+ for (let i = 0; i < n; i++) {
121
+ const p = mapLM(landmarks[i], view);
122
+ sx += p.x; sy += p.y;
123
+ }
124
+ return { x: sx / n, y: sy / n };
125
+ }
126
+
127
+ function drawPointsScaled(landmarks, color, sizePx, view, scale) {
128
+ if (!landmarks || !landmarks.length) return;
129
+ const c = centroidPx(landmarks, view);
130
+ ctx.fillStyle = color;
131
+ ctx.beginPath();
132
+ for (let i = 0, n = landmarks.length; i < n; i++) {
133
+ const p = mapLM(landmarks[i], view);
134
+ const x = c.x + (p.x - c.x) * scale;
135
+ const y = c.y + (p.y - c.y) * scale;
136
+ ctx.rect(x, y, sizePx, sizePx);
137
+ }
138
+ ctx.fill();
139
+ }
140
+
141
+ function drawEdgesScaled(landmarks, edges, color, lineWidth, view, scale) {
142
+ if (!landmarks || !landmarks.length) return;
143
+ const c = centroidPx(landmarks, view);
144
+ ctx.strokeStyle = color;
145
+ ctx.lineWidth = lineWidth;
146
+ ctx.beginPath();
147
+ for (let i = 0; i < edges.length; i++) {
148
+ const a = edges[i][0], b = edges[i][1];
149
+ const p0 = mapLM(landmarks[a], view);
150
+ const p1 = mapLM(landmarks[b], view);
151
+ const x0 = c.x + (p0.x - c.x) * scale;
152
+ const y0 = c.y + (p0.y - c.y) * scale;
153
+ const x1 = c.x + (p1.x - c.x) * scale;
154
+ const y1 = c.y + (p1.y - c.y) * scale;
155
+ ctx.moveTo(x0, y0);
156
+ ctx.lineTo(x1, y1);
157
+ }
158
+ ctx.stroke();
159
+ }
160
+
161
+ // Hands skeleton (21 landmarks)
162
+ const HAND_EDGES = [
163
+ [0,1],[1,2],[2,3],[3,4],
164
+ [0,5],[5,6],[6,7],[7,8],
165
+ [0,9],[9,10],[10,11],[11,12],
166
+ [0,13],[13,14],[14,15],[15,16],
167
+ [0,17],[17,18],[18,19],[19,20],
168
+ [5,9],[9,13],[13,17]
169
+ ];
170
+
171
+ // ===== Smoothing + interpolation (normalized coords) =====
172
+ const EMA_HANDS = 0.65;
173
+ const EMA_FACE = 0.45;
174
+
175
+ function cloneLms(lms) {
176
+ if (!lms) return null;
177
+ const out = new Array(lms.length);
178
+ for (let i = 0; i < lms.length; i++) out[i] = { x: lms[i].x, y: lms[i].y, z: lms[i].z };
179
+ return out;
180
+ }
181
+ function emaUpdate(prev, next, alpha) {
182
+ if (!next) return null;
183
+ if (!prev || prev.length !== next.length) return cloneLms(next);
184
+ const out = new Array(next.length);
185
+ for (let i = 0; i < next.length; i++) {
186
+ const p = prev[i], n = next[i];
187
+ out[i] = { x: p.x + (n.x - p.x) * alpha, y: p.y + (n.y - p.y) * alpha, z: 0 };
188
+ }
189
+ return out;
190
+ }
191
+ function lerpFrame(a, b, t) {
192
+ if (!a) return null;
193
+ if (!b || a.length !== b.length) return a;
194
+ const out = new Array(a.length);
195
+ for (let i = 0; i < a.length; i++) {
196
+ out[i] = { x: a[i].x + (b[i].x - a[i].x) * t, y: a[i].y + (b[i].y - a[i].y) * t, z: 0 };
197
+ }
198
+ return out;
199
+ }
200
+
201
+ let prevLH=null, currLH=null, prevRH=null, currRH=null;
202
+ let prevFace=null, currFace=null;
203
+
204
+ let prevHandsTs=0, lastHandsTs=0;
205
+ let prevFaceTs=0, lastFaceTs=0;
206
+
207
+ // HUD FPS
208
+ const hudEl = document.getElementById('hud');
209
+ const fpsREl = document.getElementById('fpsR');
210
+ const fpsHEl = document.getElementById('fpsH');
211
+ const fpsFEl = document.getElementById('fpsF');
212
+ const faceOnEl = document.getElementById('faceOn');
213
+
214
+ let rFrames = 0, rLastT = performance.now();
215
+ function tickRenderFps() {
216
+ rFrames++;
217
+ const now = performance.now();
218
+ const dt = now - rLastT;
219
+ if (dt >= 500) {
220
+ fpsREl.textContent = String(Math.round((rFrames * 1000) / dt));
221
+ rLastT = now; rFrames = 0;
222
+ }
223
+ }
224
+ let hFrames = 0, hLastT = performance.now();
225
+ function tickHandsFps() {
226
+ hFrames++;
227
+ const now = performance.now();
228
+ const dt = now - hLastT;
229
+ if (dt >= 900) {
230
+ fpsHEl.textContent = String(Math.round((hFrames * 1000) / dt));
231
+ hLastT = now; hFrames = 0;
232
+ }
233
+ }
234
+ let fFrames = 0, fLastT = performance.now();
235
+ function tickFaceFps() {
236
+ fFrames++;
237
+ const now = performance.now();
238
+ const dt = now - fLastT;
239
+ if (dt >= 1100) {
240
+ fpsFEl.textContent = String(Math.round((fFrames * 1000) / dt));
241
+ fLastT = now; fFrames = 0;
242
+ }
243
+ }
244
+
245
+ function renderLoop() {
246
+ clearBlack();
247
+ const view = getViewRect();
248
+ const now = performance.now();
249
+
250
+ const hdt = Math.max(1, lastHandsTs - prevHandsTs);
251
+ const ht = Math.min(1, Math.max(0, (now - lastHandsTs) / hdt));
252
+ const drawLH = lerpFrame(prevLH, currLH, ht);
253
+ const drawRH = lerpFrame(prevRH, currRH, ht);
254
+
255
+ const fdt = Math.max(1, lastFaceTs - prevFaceTs);
256
+ const ft = Math.min(1, Math.max(0, (now - lastFaceTs) / fdt));
257
+ const drawFace = lerpFrame(prevFace, currFace, ft);
258
+
259
+ if (drawLH) {
260
+ drawEdgesScaled(drawLH, HAND_EDGES, '#00ff00', HAND_LINE_W, view, HAND_SCALE);
261
+ drawPointsScaled(drawLH, '#00ff00', HAND_POINT, view, HAND_SCALE);
262
+ }
263
+ if (drawRH) {
264
+ drawEdgesScaled(drawRH, HAND_EDGES, '#ffff00', HAND_LINE_W, view, HAND_SCALE);
265
+ drawPointsScaled(drawRH, '#ffff00', HAND_POINT, view, HAND_SCALE);
266
+ }
267
+
268
+ if (drawFace) {
269
+ drawPointsScaled(drawFace, '#ff00ff', FACE_POINT, view, FACE_SCALE);
270
+ }
271
+
272
+ tickRenderFps();
273
+ requestAnimationFrame(renderLoop);
274
+ }
275
+
276
+ // ===== Models =====
277
+ const hands = new Hands({ locateFile: (f) => HANDS_BASE + f });
278
+ hands.setOptions({
279
+ maxNumHands: MAX_HANDS,
280
+ modelComplexity: HANDS_COMPLEXITY,
281
+ minDetectionConfidence: 0.5,
282
+ minTrackingConfidence: 0.5,
283
+ });
284
+ hands.onResults((res) => {
285
+ prevHandsTs = lastHandsTs;
286
+ lastHandsTs = performance.now();
287
+ tickHandsFps();
288
+
289
+ let left = null, right = null;
290
+ const lms = res.multiHandLandmarks || [];
291
+ const hd = res.multiHandedness || [];
292
+ for (let i = 0; i < lms.length; i++) {
293
+ const label = hd[i]?.label;
294
+ if (label === 'Left') left = lms[i];
295
+ else if (label === 'Right') right = lms[i];
296
+ }
297
+
298
+ prevLH = currLH; currLH = emaUpdate(currLH, left, EMA_HANDS);
299
+ prevRH = currRH; currRH = emaUpdate(currRH, right, EMA_HANDS);
300
+ });
301
+
302
+ const faceMesh = new FaceMesh({ locateFile: (f) => FACE_BASE + f });
303
+ faceMesh.setOptions({
304
+ maxNumFaces: 1,
305
+ refineLandmarks: FACE_REFINE,
306
+ minDetectionConfidence: 0.5,
307
+ minTrackingConfidence: 0.5,
308
+ });
309
+ faceMesh.onResults((res) => {
310
+ prevFaceTs = lastFaceTs;
311
+ lastFaceTs = performance.now();
312
+ tickFaceFps();
313
+
314
+ const face = res.multiFaceLandmarks?.[0] || null;
315
+ prevFace = currFace;
316
+ currFace = emaUpdate(currFace, face, EMA_FACE);
317
+
318
+ faceOnEl.textContent = face ? "ON" : "OFF";
319
+ faceOnEl.style.color = face ? "#00ff80" : "#ff3b3b";
320
+ });
321
+
322
+ // ===== Deterministic scheduler =====
323
+ let busy = false;
324
+ let lastHandsRun = 0, lastFaceRun = 0;
325
+ let preferFaceNext = true;
326
+
327
+ function due(now, lastT, hz) { return (now - lastT) >= (1000 / hz); }
328
+
329
+ async function runScheduled(now) {
330
+ if (busy) return;
331
+
332
+ const handsDue = due(now, lastHandsRun, HANDS_HZ);
333
+ const faceDue = due(now, lastFaceRun, FACE_HZ);
334
+
335
+ let run = null;
336
+ if (handsDue && !faceDue) run = 'hands';
337
+ else if (!handsDue && faceDue) run = 'face';
338
+ else if (handsDue && faceDue) run = (preferFaceNext ? 'face' : 'hands');
339
+ else return;
340
+
341
+ busy = true;
342
+ try {
343
+ if (run === 'hands') {
344
+ lastHandsRun = now;
345
+ preferFaceNext = true;
346
+ await hands.send({ image: videoEl });
347
+ } else {
348
+ lastFaceRun = now;
349
+ preferFaceNext = false;
350
+ await faceMesh.send({ image: videoEl });
351
+ }
352
+ } finally {
353
+ busy = false;
354
+ }
355
+ }
356
+
357
+ const camera = new Camera(videoEl, {
358
+ width: CAM_W,
359
+ height: CAM_H,
360
+ onFrame: async () => { await runScheduled(performance.now()); }
361
+ });
362
+
363
+ document.getElementById('start').addEventListener('click', async () => {
364
+ document.getElementById('start').style.display = 'none';
365
+ hudEl.style.display = 'block';
366
+ clearBlack();
367
+
368
+ const t = performance.now();
369
+ prevHandsTs = lastHandsTs = t;
370
+ prevFaceTs = lastFaceTs = t;
371
+
372
+ lastHandsRun = t - 1000;
373
+ lastFaceRun = t - 1000;
374
+
375
+ await camera.start();
376
+ requestAnimationFrame(renderLoop);
377
+ });
378
+ </script>
379
+ </body>
380
+ </html>
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": false,
6
+ "tailwind": {
7
+ "config": "tailwind.config.js",
8
+ "css": "src/index.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
eslint.config.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+ import pluginReact from "eslint-plugin-react";
4
+ import pluginReactHooks from "eslint-plugin-react-hooks";
5
+ import pluginUnusedImports from "eslint-plugin-unused-imports";
6
+
7
+ export default [
8
+ {
9
+ files: [
10
+ "src/components/**/*.{js,mjs,cjs,jsx}",
11
+ "src/pages/**/*.{js,mjs,cjs,jsx}",
12
+ "src/Layout.jsx",
13
+ ],
14
+ ignores: ["src/lib/**/*", "src/components/ui/**/*"],
15
+ ...pluginJs.configs.recommended,
16
+ ...pluginReact.configs.flat.recommended,
17
+ languageOptions: {
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 2022,
21
+ sourceType: "module",
22
+ ecmaFeatures: {
23
+ jsx: true,
24
+ },
25
+ },
26
+ },
27
+ settings: {
28
+ react: {
29
+ version: "detect",
30
+ },
31
+ },
32
+ plugins: {
33
+ react: pluginReact,
34
+ "react-hooks": pluginReactHooks,
35
+ "unused-imports": pluginUnusedImports,
36
+ },
37
+ rules: {
38
+ "no-unused-vars": "off",
39
+ "react/jsx-uses-vars": "error",
40
+ "react/jsx-uses-react": "error",
41
+ "unused-imports/no-unused-imports": "error",
42
+ "unused-imports/no-unused-vars": [
43
+ "warn",
44
+ {
45
+ vars: "all",
46
+ varsIgnorePattern: "^_",
47
+ args: "after-used",
48
+ argsIgnorePattern: "^_",
49
+ },
50
+ ],
51
+ "react/prop-types": "off",
52
+ "react/react-in-jsx-scope": "off",
53
+ "react/no-unknown-property": [
54
+ "error",
55
+ { ignore: ["cmdk-input-wrapper", "toast-close"] },
56
+ ],
57
+ "react-hooks/rules-of-hooks": "error",
58
+ },
59
+ },
60
+ ];
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="https://base44.com/logo_v2.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="manifest" href="/manifest.json" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
jsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ },
7
+ "jsx": "react-jsx",
8
+ "module": "esnext",
9
+ "moduleResolution": "bundler",
10
+ "lib": ["esnext", "dom"],
11
+ "target": "esnext",
12
+ "checkJs": true,
13
+ "skipLibCheck": true,
14
+ "allowSyntheticDefaultImports": true,
15
+ "esModuleInterop": true,
16
+ "resolveJsonModule": true,
17
+ "types": []
18
+ },
19
+ "include": ["src/components/**/*.js", "src/pages/**/*.jsx", "src/Layout.jsx"],
20
+ "exclude": ["node_modules", "dist", "src/vite-plugins", "src/components/ui", "src/api", "src/lib"]
21
+ }
live-feed-object-detection.html ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>🤖 AI Object Scanner</title>
7
+
8
+ <!-- These are the Brains (TensorFlow.js) -->
9
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd"></script>
11
+
12
+ <style>
13
+ /* CSS: The Future Lab Look */
14
+ body {
15
+ background-color: #0d1117; /* Deep space dark */
16
+ color: #00f3ff; /* Cyber blue */
17
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ min-height: 100vh;
22
+ margin: 0;
23
+ padding: 20px;
24
+ }
25
+
26
+ h1 {
27
+ text-shadow: 0 0 15px #00f3ff;
28
+ letter-spacing: 2px;
29
+ margin-bottom: 5px;
30
+ }
31
+
32
+ p {
33
+ color: #ccc;
34
+ margin-bottom: 20px;
35
+ }
36
+
37
+ /* The container keeps the video and the boxes aligned */
38
+ .cam-container {
39
+ position: relative; /* This is crucial for positioning boxes */
40
+ border: 3px solid #333;
41
+ border-radius: 10px;
42
+ overflow: hidden;
43
+ box-shadow: 0 0 30px rgba(0, 243, 255, 0.2);
44
+ background: #000;
45
+ min-width: 640px;
46
+ min-height: 480px;
47
+ display: flex;
48
+ justify-content: center;
49
+ align-items: center;
50
+ }
51
+
52
+ video {
53
+ display: block; /* Removes weird gaps */
54
+ width: 640px;
55
+ height: 480px;
56
+ }
57
+
58
+ /* The layer where we draw the boxes */
59
+ #box-overlay {
60
+ position: absolute;
61
+ top: 0;
62
+ left: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ pointer-events: none; /* Let clicks pass through */
66
+ }
67
+
68
+ /* The style of the detection boxes */
69
+ .detection-box {
70
+ position: absolute;
71
+ border: 2px solid #00f3ff;
72
+ background-color: rgba(0, 243, 255, 0.1); /* See-through blue */
73
+ z-index: 10;
74
+ }
75
+
76
+ .detection-label {
77
+ position: absolute;
78
+ top: -25px;
79
+ left: 0;
80
+ background-color: #00f3ff;
81
+ color: #000;
82
+ padding: 2px 8px;
83
+ font-size: 14px;
84
+ font-weight: bold;
85
+ }
86
+
87
+ button {
88
+ background-color: #00f3ff;
89
+ color: #000;
90
+ border: none;
91
+ padding: 15px 40px;
92
+ font-size: 1.2rem;
93
+ font-weight: bold;
94
+ cursor: pointer;
95
+ border-radius: 50px;
96
+ margin-top: 20px;
97
+ transition: transform 0.2s;
98
+ }
99
+
100
+ button:hover {
101
+ transform: scale(1.05);
102
+ box-shadow: 0 0 20px #00f3ff;
103
+ }
104
+
105
+ button:disabled {
106
+ background-color: #555;
107
+ color: #888;
108
+ cursor: wait;
109
+ transform: none;
110
+ box-shadow: none;
111
+ }
112
+
113
+ .status {
114
+ font-family: monospace;
115
+ font-size: 1.2rem;
116
+ margin-top: 10px;
117
+ }
118
+ </style>
119
+ </head>
120
+ <body>
121
+
122
+ <h1>🤖 AI Vision Lab</h1>
123
+ <p>Hold objects up to the camera (cellphone, cup, book, person) to scan them.</p>
124
+
125
+ <div class="cam-container">
126
+ <!-- The video plays here -->
127
+ <video id="webcam" autoplay muted playsinline></video>
128
+ <!-- The colored boxes appear here -->
129
+ <div id="box-overlay"></div>
130
+ </div>
131
+
132
+ <div class="status" id="statusText">⏳ Initializing Systems...</div>
133
+
134
+ <button id="startBtn" onclick="enableCam()" disabled>Please Wait...</button>
135
+
136
+ <script>
137
+ // JAVASCRIPT: Where the AI lives
138
+
139
+ const video = document.getElementById('webcam');
140
+ const overlay = document.getElementById('box-overlay');
141
+ const startBtn = document.getElementById('startBtn');
142
+ const statusText = document.getElementById('statusText');
143
+
144
+ let model = undefined;
145
+
146
+ // 1. LOAD THE AI MODEL
147
+ // We do this immediately so it's ready when the user clicks start
148
+ cocoSsd.load().then(function (loadedModel) {
149
+ model = loadedModel;
150
+ statusText.innerText = "✅ System Ready";
151
+ startBtn.disabled = false;
152
+ startBtn.innerText = "🔴 Activate Scanner";
153
+ });
154
+
155
+ // 2. ENABLE WEBCAM
156
+ function enableCam() {
157
+ if (!model) {
158
+ return; // Model isn't ready yet
159
+ }
160
+
161
+ // Hide the button after clicking
162
+ startBtn.style.display = 'none';
163
+ statusText.innerText = "👀 Scanning...";
164
+
165
+ // Ask for camera permission
166
+ const constraints = {
167
+ video: { width: 640, height: 480 }
168
+ };
169
+
170
+ navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
171
+ video.srcObject = stream;
172
+ // When the video actually has data, start the prediction loop
173
+ video.addEventListener('loadeddata', predictWebcam);
174
+ });
175
+ }
176
+
177
+ // 3. THE PREDICTION LOOP
178
+ function predictWebcam() {
179
+ // Ask the model to look at the video frame
180
+ model.detect(video).then(function (predictions) {
181
+
182
+ // Clear the old boxes from the last frame
183
+ overlay.innerHTML = '';
184
+
185
+ // Loop through every object the AI found
186
+ for (let n = 0; n < predictions.length; n++) {
187
+
188
+ // Only show things if the AI is more than 66% sure
189
+ if (predictions[n].score > 0.66) {
190
+
191
+ // Create the box formatting
192
+ const p = document.createElement('div');
193
+ p.classList.add('detection-box');
194
+
195
+ // We need the coordinates: [x, y, width, height]
196
+ // These numbers come from the AI
197
+ const x = predictions[n].bbox[0];
198
+ const y = predictions[n].bbox[1];
199
+ const width = predictions[n].bbox[2];
200
+ const height = predictions[n].bbox[3];
201
+
202
+ // Apply the math to the CSS
203
+ p.style.left = x + 'px';
204
+ p.style.top = y + 'px';
205
+ p.style.width = width + 'px';
206
+ p.style.height = height + 'px';
207
+
208
+ // Create the text label (e.g., "cup 90%")
209
+ const label = document.createElement('span');
210
+ label.classList.add('detection-label');
211
+ label.innerText = predictions[n].class.toUpperCase() + ' ' + Math.round(parseFloat(predictions[n].score) * 100) + '%';
212
+
213
+ // Add the label to the box, and the box to the screen
214
+ p.appendChild(label);
215
+ overlay.appendChild(p);
216
+ }
217
+ }
218
+
219
+ // Call this function again immediately to create a video loop
220
+ window.requestAnimationFrame(predictWebcam);
221
+ });
222
+ }
223
+ </script>
224
+ </body>
225
+ </html>
login.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="https://base44.com/logo_v2.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="manifest" href="/manifest.json" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
14
+
15
+
16
+
17
+
18
+
19
+ <!doctype html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="UTF-8" />
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
+ <title>Class Entry</title>
25
+ <style>
26
+ :root{
27
+ --bg1:#0f172a; /* slate-900 */
28
+ --bg2:#2e1065; /* purple-900-ish */
29
+ --card:#0b1220cc;
30
+ --border:#ffffff1a;
31
+ --text:#e5e7eb;
32
+ --muted:#cbd5e180;
33
+ --cyan:#22d3ee;
34
+ --purple:#a855f7;
35
+ --danger:#fb7185;
36
+ }
37
+ *{box-sizing:border-box}
38
+ html,body{height:100%; margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;}
39
+ body{
40
+ color:var(--text);
41
+ background: radial-gradient(900px 500px at 20% 20%, rgba(59,130,246,.22), transparent 60%),
42
+ radial-gradient(900px 500px at 80% 80%, rgba(168,85,247,.22), transparent 60%),
43
+ linear-gradient(135deg, var(--bg1), var(--bg2), var(--bg1));
44
+ display:grid;
45
+ place-items:center;
46
+ padding:24px;
47
+ overflow-x:hidden;
48
+ }
49
+ .card{
50
+ width:min(520px, 100%);
51
+ background: linear-gradient(180deg, rgba(15,23,42,.75), rgba(15,23,42,.45));
52
+ border:1px solid var(--border);
53
+ border-radius:24px;
54
+ padding:28px;
55
+ backdrop-filter: blur(14px);
56
+ box-shadow: 0 25px 60px rgba(0,0,0,.45);
57
+ position:relative;
58
+ overflow:hidden;
59
+ }
60
+ .glow{
61
+ position:absolute; inset:-80px;
62
+ background: radial-gradient(circle at 30% 20%, rgba(34,211,238,.16), transparent 55%),
63
+ radial-gradient(circle at 70% 80%, rgba(168,85,247,.16), transparent 55%);
64
+ pointer-events:none;
65
+ }
66
+ h1{margin:0 0 8px; font-size:28px; letter-spacing:.2px;}
67
+ .sub{margin:0 0 18px; color:var(--muted); line-height:1.4;}
68
+ label{display:block; font-size:14px; color:#e5e7ebcc; margin:14px 0 8px;}
69
+ input{
70
+ width:100%;
71
+ padding:14px 14px;
72
+ border-radius:14px;
73
+ border:1px solid rgba(255,255,255,.15);
74
+ background: rgba(2,6,23,.55);
75
+ color:var(--text);
76
+ outline:none;
77
+ font-size:16px;
78
+ }
79
+ input:focus{
80
+ border-color: rgba(34,211,238,.55);
81
+ box-shadow: 0 0 0 4px rgba(34,211,238,.12);
82
+ }
83
+ .row{display:flex; gap:12px; margin-top:16px; align-items:center; flex-wrap:wrap;}
84
+ button{
85
+ border:0;
86
+ border-radius:16px;
87
+ padding:12px 16px;
88
+ font-size:16px;
89
+ font-weight:700;
90
+ color:white;
91
+ cursor:pointer;
92
+ background: linear-gradient(90deg, rgba(59,130,246,1), rgba(168,85,247,1));
93
+ box-shadow: 0 16px 30px rgba(168,85,247,.22);
94
+ }
95
+ button:active{transform: translateY(1px);}
96
+ .hint{
97
+ font-size:13px;
98
+ color:var(--muted);
99
+ flex: 1 1 auto;
100
+ }
101
+ .error{
102
+ margin-top:12px;
103
+ color: var(--danger);
104
+ font-size:14px;
105
+ display:none;
106
+ }
107
+ .ok{
108
+ margin-top:12px;
109
+ color: rgba(34,211,238,.95);
110
+ font-size:14px;
111
+ display:none;
112
+ }
113
+ .tiny{
114
+ margin-top:18px;
115
+ border-top:1px solid var(--border);
116
+ padding-top:12px;
117
+ color:#ffffff66;
118
+ font-size:12px;
119
+ text-align:center;
120
+ }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <div class="card">
125
+ <div class="glow"></div>
126
+
127
+ <h1>Computer Vision Lab</h1>
128
+ <p class="sub">Enter your class username to continue.</p>
129
+
130
+ <form id="entryForm" autocomplete="off">
131
+ <label for="username">Username</label>
132
+ <input id="username" name="username" placeholder="e.g., cv-student" required />
133
+
134
+ <div class="row">
135
+ <button type="submit">Continue</button>
136
+ <div class="hint">Tip: usernames are case-insensitive in this form.</div>
137
+ </div>
138
+
139
+ <div id="ok" class="ok">Accepted. Redirecting…</div>
140
+ <div id="error" class="error">That username isn’t allowed.</div>
141
+ </form>
142
+
143
+ <div class="tiny">Class access check (client-side)</div>
144
+ </div>
145
+
146
+ <script>
147
+ // ---- Configure these two values ----
148
+ const ONLY_ALLOWED_USERNAME = "GLIS26STEM"; // the one username you want to allow
149
+ const REDIRECT_TO = "./Dashboard.html"; // where to go after success (change this)
150
+ // -----------------------------------
151
+
152
+ const form = document.getElementById("entryForm");
153
+ const input = document.getElementById("username");
154
+ const error = document.getElementById("error");
155
+ const ok = document.getElementById("ok");
156
+
157
+ const normalize = (s) => (s || "").trim().toLowerCase();
158
+
159
+ form.addEventListener("submit", (e) => {
160
+ e.preventDefault();
161
+
162
+ const entered = normalize(input.value);
163
+ const allowed = normalize(ONLY_ALLOWED_USERNAME);
164
+
165
+ error.style.display = "none";
166
+ ok.style.display = "none";
167
+
168
+ if (!entered) return;
169
+
170
+ if (entered === allowed) {
171
+ ok.style.display = "block";
172
+
173
+ // Remember they passed (optional)
174
+ localStorage.setItem("class_username", entered);
175
+ localStorage.setItem("class_access_granted", "true");
176
+
177
+ setTimeout(() => {
178
+ window.location.href = REDIRECT_TO;
179
+ }, 450);
180
+ } else {
181
+ error.style.display = "block";
182
+ input.focus();
183
+ input.select();
184
+ }
185
+ });
186
+ </script>
187
+ </body>
188
+ </html>
movement-detection.html ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>🕵️‍♂️ Secret Agent Motion Detector</title>
7
+ <style>
8
+ /* CSS: Making it look cool */
9
+ body {
10
+ background-color: #1a1a1a; /* Dark spy background */
11
+ color: #00ff41; /* Hacker green text */
12
+ font-family: 'Courier New', Courier, monospace; /* Typewriter font */
13
+ display: flex;
14
+ flex-direction: column;
15
+ align-items: center;
16
+ min-height: 100vh;
17
+ margin: 0;
18
+ padding: 20px;
19
+ }
20
+
21
+ h1 {
22
+ text-shadow: 0 0 10px #00ff41;
23
+ margin-bottom: 10px;
24
+ }
25
+
26
+ .instructions {
27
+ background: #222;
28
+ padding: 15px;
29
+ border-radius: 10px;
30
+ border: 1px solid #444;
31
+ max-width: 600px;
32
+ text-align: center;
33
+ margin-bottom: 20px;
34
+ color: #ddd;
35
+ }
36
+
37
+ /* The canvas where the magic happens */
38
+ #canvasFinal {
39
+ border: 4px solid #00ff41;
40
+ border-radius: 8px;
41
+ box-shadow: 0 0 20px rgba(0, 255, 65, 0.3);
42
+ background-color: #000;
43
+ max-width: 100%;
44
+ display: none; /* Hidden until camera starts */
45
+ }
46
+
47
+ /* Hide the raw video stream and processing canvas */
48
+ #camStream, #canvasHidden {
49
+ display: none;
50
+ }
51
+
52
+ button {
53
+ background-color: #00ff41;
54
+ color: #000;
55
+ border: none;
56
+ padding: 15px 30px;
57
+ font-size: 1.2rem;
58
+ font-weight: bold;
59
+ font-family: inherit;
60
+ cursor: pointer;
61
+ border-radius: 5px;
62
+ margin-bottom: 20px;
63
+ transition: all 0.3s;
64
+ }
65
+
66
+ button:hover {
67
+ background-color: #fff;
68
+ box-shadow: 0 0 15px #fff;
69
+ }
70
+
71
+ .status {
72
+ margin-top: 10px;
73
+ font-size: 0.9rem;
74
+ color: #888;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body>
79
+
80
+ <button id="startBtn" onclick="startSpyCamera()">🚀 Activate Movement Detection Mode</button>
81
+
82
+ <p class="status" id="statusText">Waiting for activation...</p>
83
+
84
+ <!-- These are the HTML elements that handle the video -->
85
+ <video id="camStream" playsinline autoplay></video>
86
+ <canvas id="canvasHidden"></canvas>
87
+ <canvas id="canvasFinal"></canvas>
88
+
89
+ <script>
90
+ // JAVASCRIPT: The Brains of the Operation
91
+
92
+ // 1. Grab our HTML elements so we can control them
93
+ const video = document.getElementById('camStream');
94
+ const hiddenCanvas = document.getElementById('canvasHidden');
95
+ const finalCanvas = document.getElementById('canvasFinal');
96
+ const startBtn = document.getElementById('startBtn');
97
+ const statusText = document.getElementById('statusText');
98
+
99
+ // Contexts are like the "paintbrushes" for the canvas
100
+ const hiddenCtx = hiddenCanvas.getContext('2d');
101
+ const finalCtx = finalCanvas.getContext('2d');
102
+
103
+ // We need a place to store the "previous" frame to compare against
104
+ let previousFrameData = null;
105
+
106
+ async function startSpyCamera() {
107
+ try {
108
+ // 2. Ask the browser for permission to use the camera
109
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
110
+
111
+ // Connect the camera stream to our hidden video element
112
+ video.srcObject = stream;
113
+
114
+ // Update the UI
115
+ statusText.innerText = "System Active. Scanning for movement...";
116
+ startBtn.style.display = "none"; // Hide button
117
+ finalCanvas.style.display = "block"; // Show screen
118
+
119
+ // Start the loop!
120
+ requestAnimationFrame(processVideo);
121
+
122
+ } catch (err) {
123
+ // If they say no or don't have a camera
124
+ statusText.innerText = "⚠️ Error: Could not access camera. Please allow permission.";
125
+ console.error(err);
126
+ }
127
+ }
128
+
129
+ function processVideo() {
130
+ // Check if the video is ready and playing
131
+ if (video.readyState === video.HAVE_ENOUGH_DATA) {
132
+
133
+ // Set canvas sizes to match the video size
134
+ const width = video.videoWidth;
135
+ const height = video.videoHeight;
136
+ hiddenCanvas.width = width;
137
+ hiddenCanvas.height = height;
138
+ finalCanvas.width = width;
139
+ finalCanvas.height = height;
140
+
141
+ // 3. Draw the current video frame onto the hidden canvas
142
+ hiddenCtx.drawImage(video, 0, 0, width, height);
143
+
144
+ // 4. Get the pixel data (the raw numbers for every color)
145
+ const currentFrame = hiddenCtx.getImageData(0, 0, width, height);
146
+ const currentData = currentFrame.data;
147
+
148
+ // Create a new image for the result
149
+ const outputImage = finalCtx.createImageData(width, height);
150
+ const outputData = outputImage.data;
151
+
152
+ // 5. THE ALGORITHM: Compare this frame to the previous one
153
+ if (previousFrameData) {
154
+ const prevData = previousFrameData.data;
155
+
156
+ // Loop through every pixel (pixels are groups of 4: Red, Green, Blue, Alpha)
157
+ for (let i = 0; i < currentData.length; i += 4) {
158
+
159
+ // This math calculates the difference between old and new
160
+ // If pixels are the same, it turns GREY.
161
+ // If they are different, it creates high contrast colors.
162
+
163
+ outputData[i] = 0.5 * (255 - currentData[i]) + 0.5 * prevData[i]; // Red
164
+ outputData[i+1] = 0.5 * (255 - currentData[i+1]) + 0.5 * prevData[i+1]; // Green
165
+ outputData[i+2] = 0.5 * (255 - currentData[i+2]) + 0.5 * prevData[i+2]; // Blue
166
+ outputData[i+3] = 255; // Alpha (Opacity) is always 100%
167
+ }
168
+
169
+ // Draw the result to the main screen
170
+ finalCtx.putImageData(outputImage, 0, 0);
171
+ }
172
+
173
+ // 6. Save the current frame to be the "previous" frame for next time
174
+ // We clone it so we don't overwrite it immediately
175
+ previousFrameData = new ImageData(
176
+ new Uint8ClampedArray(currentFrame.data),
177
+ currentFrame.width,
178
+ currentFrame.height
179
+ );
180
+ }
181
+
182
+ // Repeat this function as fast as the screen can refresh
183
+ requestAnimationFrame(processVideo);
184
+ }
185
+ </script>
186
+ </body>
187
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "base44-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint . --quiet",
10
+ "lint:fix": "eslint . --fix",
11
+ "typecheck": "tsc -p ./jsconfig.json",
12
+ "preview": "vite preview"
13
+ },
14
+ "dependencies": {
15
+ "@base44/sdk": "^0.8.3",
16
+ "@base44/vite-plugin": "^0.2.15",
17
+ "@hello-pangea/dnd": "^17.0.0",
18
+ "@hookform/resolvers": "^4.1.2",
19
+ "@radix-ui/react-accordion": "^1.2.3",
20
+ "@radix-ui/react-alert-dialog": "^1.1.6",
21
+ "@radix-ui/react-aspect-ratio": "^1.1.2",
22
+ "@radix-ui/react-avatar": "^1.1.3",
23
+ "@radix-ui/react-checkbox": "^1.1.4",
24
+ "@radix-ui/react-collapsible": "^1.1.3",
25
+ "@radix-ui/react-context-menu": "^2.2.6",
26
+ "@radix-ui/react-dialog": "^1.1.6",
27
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
28
+ "@radix-ui/react-hover-card": "^1.1.6",
29
+ "@radix-ui/react-label": "^2.1.2",
30
+ "@radix-ui/react-menubar": "^1.1.6",
31
+ "@radix-ui/react-navigation-menu": "^1.2.5",
32
+ "@radix-ui/react-popover": "^1.1.6",
33
+ "@radix-ui/react-progress": "^1.1.2",
34
+ "@radix-ui/react-radio-group": "^1.2.3",
35
+ "@radix-ui/react-scroll-area": "^1.2.3",
36
+ "@radix-ui/react-select": "^2.1.6",
37
+ "@radix-ui/react-separator": "^1.1.2",
38
+ "@radix-ui/react-slider": "^1.2.3",
39
+ "@radix-ui/react-slot": "^1.1.2",
40
+ "@radix-ui/react-switch": "^1.1.3",
41
+ "@radix-ui/react-tabs": "^1.1.3",
42
+ "@radix-ui/react-toast": "^1.2.2",
43
+ "@radix-ui/react-toggle": "^1.1.2",
44
+ "@radix-ui/react-toggle-group": "^1.1.2",
45
+ "@radix-ui/react-tooltip": "^1.1.8",
46
+ "@stripe/react-stripe-js": "^3.0.0",
47
+ "@stripe/stripe-js": "^5.2.0",
48
+ "@tanstack/react-query": "^5.84.1",
49
+ "canvas-confetti": "^1.9.4",
50
+ "class-variance-authority": "^0.7.1",
51
+ "clsx": "^2.1.1",
52
+ "cmdk": "^1.0.0",
53
+ "date-fns": "^3.6.0",
54
+ "embla-carousel-react": "^8.5.2",
55
+ "framer-motion": "^11.16.4",
56
+ "html2canvas": "^1.4.1",
57
+ "input-otp": "^1.4.2",
58
+ "jspdf": "^4.0.0",
59
+ "lodash": "^4.17.21",
60
+ "lucide-react": "^0.475.0",
61
+ "moment": "^2.30.1",
62
+ "next-themes": "^0.4.4",
63
+ "react": "^18.2.0",
64
+ "react-day-picker": "^8.10.1",
65
+ "react-dom": "^18.2.0",
66
+ "react-hook-form": "^7.54.2",
67
+ "react-hot-toast": "^2.6.0",
68
+ "react-leaflet": "^4.2.1",
69
+ "react-markdown": "^9.0.1",
70
+ "react-quill": "^0.0.2",
71
+ "react-resizable-panels": "^2.1.7",
72
+ "react-router-dom": "^6.26.0",
73
+ "recharts": "^2.15.4",
74
+ "sonner": "^2.0.1",
75
+ "tailwind-merge": "^3.0.2",
76
+ "tailwindcss-animate": "^1.0.7",
77
+ "three": "^0.171.0",
78
+ "vaul": "^1.1.2",
79
+ "zod": "^3.24.2"
80
+ },
81
+ "devDependencies": {
82
+ "@eslint/js": "^9.19.0",
83
+ "@types/node": "^22.13.5",
84
+ "@types/react": "^18.2.66",
85
+ "@types/react-dom": "^18.2.22",
86
+ "@vitejs/plugin-react": "^4.3.4",
87
+ "autoprefixer": "^10.4.20",
88
+ "baseline-browser-mapping": "^2.8.32",
89
+ "eslint": "^9.19.0",
90
+ "eslint-plugin-react": "^7.37.4",
91
+ "eslint-plugin-react-hooks": "^5.0.0",
92
+ "eslint-plugin-react-refresh": "^0.4.18",
93
+ "eslint-plugin-unused-imports": "^4.3.0",
94
+ "globals": "^15.14.0",
95
+ "postcss": "^8.5.3",
96
+ "tailwindcss": "^3.4.17",
97
+ "typescript": "^5.8.2",
98
+ "vite": "^6.4.1"
99
+ }
100
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
src/App.jsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import LabSelection from "./login.jsx";
3
+ import { QueryClientProvider } from '@tanstack/react-query'
4
+ import { queryClientInstance } from '@/lib/query-client'
5
+ import NavigationTracker from '@/lib/NavigationTracker'
6
+ import { pagesConfig } from './pages.config'
7
+ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
8
+ import PageNotFound from './lib/PageNotFound';
9
+ import { AuthProvider, useAuth } from '@/lib/AuthContext';
10
+ import UserNotRegisteredError from '@/components/UserNotRegisteredError';
11
+
12
+
13
+ const { Pages, Layout, mainPage } = pagesConfig;
14
+ const mainPageKey = mainPage ?? Object.keys(Pages)[0];
15
+ const MainPage = mainPageKey ? Pages[mainPageKey] : <></>;
16
+
17
+ const LayoutWrapper = ({ children, currentPageName }) => Layout ?
18
+ <Layout currentPageName={currentPageName}>{children}</Layout>
19
+ : <>{children}</>;
20
+
21
+ const AuthenticatedApp = () => {
22
+ const { isLoadingAuth, isLoadingPublicSettings, authError, navigateToLogin } = useAuth();
23
+
24
+ // Show loading spinner while checking app public settings or auth
25
+ if (isLoadingPublicSettings || isLoadingAuth) {
26
+ return (
27
+ <div className="fixed inset-0 flex items-center justify-center">
28
+ <div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ // Handle authentication errors
34
+ if (authError) {
35
+ if (authError.type === 'user_not_registered') {
36
+ return <UserNotRegisteredError />;
37
+ } else if (authError.type === 'auth_required') {
38
+ // Redirect to login automatically
39
+ navigateToLogin();
40
+ return null;
41
+ }
42
+ }
43
+
44
+ // Render the main app
45
+ return (
46
+ <Routes>
47
+ <Route path="/" element={
48
+ <LayoutWrapper currentPageName={mainPageKey}>
49
+ <MainPage />
50
+ </LayoutWrapper>
51
+ } />
52
+ {Object.entries(Pages).map(([path, Page]) => (
53
+ <Route
54
+ key={path}
55
+ path={`/${path}`}
56
+ element={
57
+ <LayoutWrapper currentPageName={path}>
58
+ <Page />
59
+ </LayoutWrapper>
60
+ }
61
+ />
62
+ ))}
63
+ <Route path="*" element={<PageNotFound />} />
64
+ </Routes>
65
+
66
+ );
67
+ };
68
+
69
+
70
+ function App() {
71
+ const [labApproved, setLabApproved] = useState(false);
72
+
73
+ if (!labApproved) {
74
+ return <LabSelection onApproved={() => setLabApproved(true)} />;
75
+ }
76
+
77
+ return (
78
+ <AuthProvider>
79
+ <QueryClientProvider client={queryClientInstance}>
80
+ <Router>
81
+ <NavigationTracker />
82
+ <AuthenticatedApp />
83
+ </Router>
84
+ </QueryClientProvider>
85
+ </AuthProvider>
86
+ );
87
+ }
88
+ export default App;
src/Layout.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { createPageUrl } from '@/utils';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { Brain, Home, BookOpen, Cpu, Target, Menu, X, Sparkles } from 'lucide-react';
6
+ import { Button } from '@/components/ui/button';
7
+
8
+ const navItems = [
9
+ { name: 'Home', icon: Home, page: 'Home' },
10
+ { name: 'Lessons', icon: BookOpen, page: 'Lessons' },
11
+ { name: 'Quiz', icon: Target, page: 'Quiz' }
12
+ ];
13
+
14
+ export default function Layout({ children, currentPageName }) {
15
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
16
+ const location = useLocation();
17
+
18
+ useEffect(() => {
19
+ window.scrollTo(0, 0);
20
+ }, [location.pathname]);
21
+
22
+ return (
23
+ <div className="min-h-screen bg-slate-900">
24
+ {/* Navigation */}
25
+ <nav className="fixed top-0 left-0 right-0 z-50 bg-slate-900/80 backdrop-blur-xl border-b border-white/10">
26
+ <div className="max-w-6xl mx-auto px-6">
27
+ <div className="flex items-center justify-between h-16">
28
+ {/* Logo */}
29
+ <Link to={createPageUrl('Home')} className="flex items-center gap-3">
30
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
31
+ <Brain className="w-6 h-6 text-white" />
32
+ </div>
33
+ <span className="font-bold text-white text-lg hidden sm:block">Computer Vision Lab</span>
34
+ </Link>
35
+
36
+ {/* Desktop Navigation */}
37
+ <div className="hidden md:flex items-center gap-1">
38
+ {navItems.map((item) => {
39
+ const isActive = currentPageName === item.page;
40
+ return (
41
+ <Link key={item.page} to={createPageUrl(item.page)}>
42
+ <Button
43
+ variant="ghost"
44
+ className={`relative rounded-xl px-4 ${
45
+ isActive
46
+ ? 'text-white bg-white/10'
47
+ : 'text-white/70 hover:text-white hover:bg-white/5'
48
+ }`}
49
+ >
50
+ <item.icon className="w-4 h-4 mr-2" />
51
+ {item.name}
52
+ {isActive && (
53
+ <motion.div
54
+ layoutId="activeTab"
55
+ className="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-r from-blue-500 to-purple-500"
56
+ />
57
+ )}
58
+ </Button>
59
+ </Link>
60
+ );
61
+ })}
62
+ </div>
63
+
64
+ {/* Mobile Menu Button */}
65
+ <Button
66
+ size="icon"
67
+ className="md:hidden text-white bg-transparent hover:bg-white/10 border-0"
68
+ onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
69
+ >
70
+ {mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
71
+ </Button>
72
+ </div>
73
+ </div>
74
+
75
+ {/* Mobile Menu */}
76
+ <AnimatePresence>
77
+ {mobileMenuOpen && (
78
+ <motion.div
79
+ initial={{ opacity: 0, height: 0 }}
80
+ animate={{ opacity: 1, height: 'auto' }}
81
+ exit={{ opacity: 0, height: 0 }}
82
+ className="md:hidden bg-slate-900/95 backdrop-blur-xl border-b border-white/10"
83
+ >
84
+ <div className="px-6 py-4 space-y-2">
85
+ {navItems.map((item) => {
86
+ const isActive = currentPageName === item.page;
87
+ return (
88
+ <Link
89
+ key={item.page}
90
+ to={createPageUrl(item.page)}
91
+ onClick={() => setMobileMenuOpen(false)}
92
+ >
93
+ <div className={`flex items-center gap-3 p-3 rounded-xl ${
94
+ isActive
95
+ ? 'bg-white/10 text-white'
96
+ : 'text-white/70 hover:bg-white/5 hover:text-white'
97
+ }`}>
98
+ <item.icon className="w-5 h-5" />
99
+ <span className="font-medium">{item.name}</span>
100
+ </div>
101
+ </Link>
102
+ );
103
+ })}
104
+ </div>
105
+ </motion.div>
106
+ )}
107
+ </AnimatePresence>
108
+ </nav>
109
+
110
+ {/* Main Content */}
111
+ <main>
112
+ {children}
113
+ </main>
114
+
115
+ {/* Footer */}
116
+ <footer className="bg-slate-900 border-t border-white/10 py-9 px-6">
117
+ <div className="max-w-6xl mx-auto">
118
+ <div className="pt-3 text-center text-white/40 text-sm">
119
+ <p>© 2026 Computer Vision Lab</p>
120
+ </div>
121
+ </div>
122
+ </footer>
123
+
124
+ </div>
125
+
126
+ );
127
+ }
src/api/base44Client.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@base44/sdk';
2
+ import { appParams } from '@/lib/app-params';
3
+
4
+ const { appId, token, functionsVersion, appBaseUrl } = appParams;
5
+
6
+ //Create a client with authentication required
7
+ export const base44 = createClient({
8
+ appId,
9
+ token,
10
+ functionsVersion,
11
+ serverUrl: '',
12
+ requiresAuth: false,
13
+ appBaseUrl
14
+ });
src/components/UserNotRegisteredError.jsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const UserNotRegisteredError = () => {
4
+ return (
5
+ <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-white to-slate-50">
6
+ <div className="max-w-md w-full p-8 bg-white rounded-lg shadow-lg border border-slate-100">
7
+ <div className="text-center">
8
+ <div className="inline-flex items-center justify-center w-16 h-16 mb-6 rounded-full bg-orange-100">
9
+ <svg className="w-8 h-8 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
10
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
11
+ </svg>
12
+ </div>
13
+ <h1 className="text-3xl font-bold text-slate-900 mb-4">Access Restricted</h1>
14
+ <p className="text-slate-600 mb-8">
15
+ You are not registered to use this application. Please contact the app administrator to request access.
16
+ </p>
17
+ <div className="p-4 bg-slate-50 rounded-md text-sm text-slate-600">
18
+ <p>If you believe this is an error, you can:</p>
19
+ <ul className="list-disc list-inside mt-2 space-y-1">
20
+ <li>Verify you are logged in with the correct account</li>
21
+ <li>Contact the app administrator for access</li>
22
+ <li>Try logging out and back in again</li>
23
+ </ul>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default UserNotRegisteredError;
src/components/lessons/AnimatedDiagram.jsx ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ export function BoundingBoxDemo() {
5
+ return (
6
+ <div className="relative w-full max-w-md mx-auto aspect-video bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl overflow-hidden border border-white/10">
7
+ {/* Simulated Image */}
8
+ <div className="absolute inset-0 flex items-center justify-center">
9
+ <div className="text-6xl">🚗</div>
10
+ </div>
11
+
12
+ {/* Animated Bounding Box */}
13
+ <motion.div
14
+ className="absolute border-4 border-cyan-400 rounded-lg"
15
+ initial={{ opacity: 0, scale: 0.8 }}
16
+ animate={{
17
+ opacity: [0, 1, 1, 1],
18
+ scale: [0.8, 1, 1, 1],
19
+ }}
20
+ transition={{ duration: 2, repeat: Infinity, repeatDelay: 1 }}
21
+ style={{
22
+ top: '25%',
23
+ left: '30%',
24
+ width: '40%',
25
+ height: '50%'
26
+ }}
27
+ >
28
+ <motion.div
29
+ className="absolute -top-8 left-0 bg-cyan-400 text-slate-900 px-3 py-1 rounded-lg text-sm font-bold"
30
+ initial={{ opacity: 0, y: 10 }}
31
+ animate={{ opacity: [0, 1, 1, 0], y: [10, 0, 0, -10] }}
32
+ transition={{ duration: 2, repeat: Infinity, repeatDelay: 1, delay: 0.3 }}
33
+ >
34
+ Car 98%
35
+ </motion.div>
36
+
37
+ {/* Corner markers */}
38
+ {[
39
+ { top: -4, left: -4 },
40
+ { top: -4, right: -4 },
41
+ { bottom: -4, left: -4 },
42
+ { bottom: -4, right: -4 }
43
+ ].map((pos, i) => (
44
+ <motion.div
45
+ key={i}
46
+ className="absolute w-3 h-3 bg-cyan-400 rounded-full"
47
+ style={pos}
48
+ animate={{ scale: [1, 1.3, 1] }}
49
+ transition={{ duration: 1, repeat: Infinity, delay: i * 0.1 }}
50
+ />
51
+ ))}
52
+ </motion.div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function PoseSkeletonDemo() {
58
+ const keypoints = [
59
+ { id: 'head', x: 50, y: 10, label: 'Head' },
60
+ { id: 'neck', x: 50, y: 22 },
61
+ { id: 'lshoulder', x: 35, y: 25 },
62
+ { id: 'rshoulder', x: 65, y: 25 },
63
+ { id: 'lelbow', x: 25, y: 40 },
64
+ { id: 'relbow', x: 75, y: 40 },
65
+ { id: 'lwrist', x: 20, y: 55 },
66
+ { id: 'rwrist', x: 80, y: 55 },
67
+ { id: 'hip', x: 50, y: 50 },
68
+ { id: 'lhip', x: 40, y: 52 },
69
+ { id: 'rhip', x: 60, y: 52 },
70
+ { id: 'lknee', x: 38, y: 70 },
71
+ { id: 'rknee', x: 62, y: 70 },
72
+ { id: 'lankle', x: 35, y: 90 },
73
+ { id: 'rankle', x: 65, y: 90 },
74
+ ];
75
+
76
+ const bones = [
77
+ ['head', 'neck'],
78
+ ['neck', 'lshoulder'],
79
+ ['neck', 'rshoulder'],
80
+ ['lshoulder', 'lelbow'],
81
+ ['rshoulder', 'relbow'],
82
+ ['lelbow', 'lwrist'],
83
+ ['relbow', 'rwrist'],
84
+ ['neck', 'hip'],
85
+ ['hip', 'lhip'],
86
+ ['hip', 'rhip'],
87
+ ['lhip', 'lknee'],
88
+ ['rhip', 'rknee'],
89
+ ['lknee', 'lankle'],
90
+ ['rknee', 'rankle'],
91
+ ];
92
+
93
+ const getPoint = (id) => keypoints.find(k => k.id === id);
94
+
95
+ return (
96
+ <div className="relative w-full max-w-sm mx-auto aspect-[3/4] bg-gradient-to-br from-purple-900/50 to-pink-900/50 rounded-2xl overflow-hidden border border-white/10">
97
+ <svg className="w-full h-full" viewBox="0 0 100 100">
98
+ {/* Bones */}
99
+ {bones.map(([from, to], i) => {
100
+ const p1 = getPoint(from);
101
+ const p2 = getPoint(to);
102
+ return (
103
+ <motion.line
104
+ key={i}
105
+ x1={p1.x}
106
+ y1={p1.y}
107
+ x2={p2.x}
108
+ y2={p2.y}
109
+ stroke="url(#boneGradient)"
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ initial={{ pathLength: 0, opacity: 0 }}
113
+ animate={{ pathLength: 1, opacity: 1 }}
114
+ transition={{ duration: 0.5, delay: i * 0.05 }}
115
+ />
116
+ );
117
+ })}
118
+
119
+ {/* Keypoints */}
120
+ {keypoints.map((point, i) => (
121
+ <motion.circle
122
+ key={point.id}
123
+ cx={point.x}
124
+ cy={point.y}
125
+ r="3"
126
+ fill="#EC4899"
127
+ initial={{ scale: 0 }}
128
+ animate={{ scale: [1, 1.3, 1] }}
129
+ transition={{
130
+ scale: { duration: 1, repeat: Infinity, delay: i * 0.1 },
131
+ default: { delay: i * 0.05 }
132
+ }}
133
+ />
134
+ ))}
135
+
136
+ <defs>
137
+ <linearGradient id="boneGradient" x1="0%" y1="0%" x2="100%" y2="100%">
138
+ <stop offset="0%" stopColor="#8B5CF6" />
139
+ <stop offset="100%" stopColor="#EC4899" />
140
+ </linearGradient>
141
+ </defs>
142
+ </svg>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ export function EmotionFaceDemo() {
148
+ const emotions = [
149
+ { emoji: '😊', label: 'Happy', color: 'from-yellow-400 to-orange-400' },
150
+ { emoji: '😢', label: 'Sad', color: 'from-blue-400 to-indigo-400' },
151
+ { emoji: '😠', label: 'Angry', color: 'from-red-400 to-rose-400' },
152
+ { emoji: '😲', label: 'Surprise', color: 'from-purple-400 to-pink-400' },
153
+ { emoji: '😐', label: 'Neutral', color: 'from-gray-400 to-slate-400' },
154
+ ];
155
+
156
+ const [currentEmotion, setCurrentEmotion] = React.useState(0);
157
+
158
+ React.useEffect(() => {
159
+ const interval = setInterval(() => {
160
+ setCurrentEmotion((prev) => (prev + 1) % emotions.length);
161
+ }, 2000);
162
+ return () => clearInterval(interval);
163
+ }, []);
164
+
165
+ const emotion = emotions[currentEmotion];
166
+
167
+ return (
168
+ <div className="relative w-full max-w-sm mx-auto">
169
+ <motion.div
170
+ key={currentEmotion}
171
+ initial={{ scale: 0.8, opacity: 0 }}
172
+ animate={{ scale: 1, opacity: 1 }}
173
+ exit={{ scale: 0.8, opacity: 0 }}
174
+ className="aspect-square bg-gradient-to-br from-slate-800 to-slate-900 rounded-3xl border border-white/10 flex flex-col items-center justify-center"
175
+ >
176
+ <motion.div
177
+ className="text-8xl mb-4"
178
+ animate={{ scale: [1, 1.1, 1] }}
179
+ transition={{ duration: 1, repeat: Infinity }}
180
+ >
181
+ {emotion.emoji}
182
+ </motion.div>
183
+
184
+ <motion.div
185
+ className={`px-6 py-2 rounded-full bg-gradient-to-r ${emotion.color} text-white font-bold text-lg`}
186
+ initial={{ y: 20, opacity: 0 }}
187
+ animate={{ y: 0, opacity: 1 }}
188
+ transition={{ delay: 0.2 }}
189
+ >
190
+ {emotion.label}
191
+ </motion.div>
192
+ </motion.div>
193
+
194
+ {/* Confidence Bars */}
195
+ <div className="mt-6 space-y-2">
196
+ {emotions.map((e, i) => (
197
+ <div key={i} className="flex items-center gap-3">
198
+ <span className="text-xl w-8">{e.emoji}</span>
199
+ <div className="flex-1 h-3 bg-white/10 rounded-full overflow-hidden">
200
+ <motion.div
201
+ className={`h-full bg-gradient-to-r ${e.color}`}
202
+ initial={{ width: 0 }}
203
+ animate={{ width: i === currentEmotion ? '95%' : `${Math.random() * 30 + 5}%` }}
204
+ transition={{ duration: 0.5 }}
205
+ />
206
+ </div>
207
+ <span className="text-white/60 text-sm w-12 text-right">
208
+ {i === currentEmotion ? '95%' : `${Math.floor(Math.random() * 20 + 5)}%`}
209
+ </span>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ );
215
+ }
src/components/lessons/Chapter1Slides.jsx ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { motion } from 'framer-motion';
4
+ import { Eye, Car, Bot, Camera, Zap, Target, CheckCircle2 } from 'lucide-react';
5
+ import ClickReveal from './ClickReveal';
6
+ import KeyTermBadge from './KeyTermBadge';
7
+ import { BoundingBoxDemo } from './AnimatedDiagram';
8
+
9
+ export const chapter1Slides = [
10
+ // Slide 1: What is Object Detection?
11
+ {
12
+ content: (
13
+ <div className="space-y-10">
14
+ <motion.div
15
+ initial={{ scale: 0 }}
16
+ animate={{ scale: 1 }}
17
+ transition={{ type: "spring", delay: 0.2 }}
18
+ className="w-28 h-28 mx-auto rounded-3xl bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center shadow-2xl shadow-blue-500/30"
19
+ >
20
+ <Eye className="w-14 h-14 text-white" />
21
+ </motion.div>
22
+
23
+ <div className="text-center mb-8">
24
+ <h1 className="text-5xl md:text-6xl font-black text-white mb-6 leading-tight">
25
+ What is Object Detection?
26
+ </h1>
27
+ <p className="text-xl text-white/70">Chapter 1 • Slide 1 of 5</p>
28
+ </div>
29
+
30
+ <motion.div
31
+ initial={{ opacity: 0, y: 20 }}
32
+ animate={{ opacity: 1, y: 0 }}
33
+ transition={{ delay: 0.4 }}
34
+ className="bg-white/5 backdrop-blur-xl rounded-3xl p-10 border border-white/10"
35
+ >
36
+ <p className="text-2xl text-white/90 leading-relaxed mb-8">
37
+ <KeyTermBadge term="Object Detection" definition="A computer vision technique that identifies and locates objects in images or videos" color="cyan" /> is like giving a computer the power to <span className="text-cyan-400 font-semibold">see and identify things</span> in pictures and videos — just like you can!
38
+ </p>
39
+
40
+ <div className="flex items-center justify-center gap-8 my-8">
41
+ <div className="text-7xl">👁️</div>
42
+ <div className="text-5xl">➡️</div>
43
+ <div className="text-7xl">🤖</div>
44
+ </div>
45
+
46
+ <p className="text-xl text-white/70 leading-relaxed">
47
+ When you look at a photo, you instantly know "that's a dog" or "that's a car." Object detection teaches computers to do the same thing automatically!
48
+ </p>
49
+ </motion.div>
50
+
51
+ <ClickReveal title="🤔 Fun Fact!" color="blue">
52
+ <p className="text-lg">Your brain processes images in just 13 milliseconds — but modern AI can now detect objects almost as fast!</p>
53
+ </ClickReveal>
54
+ </div>
55
+ )
56
+ },
57
+
58
+ // Slide 2: How Computers See Objects
59
+ {
60
+ content: (
61
+ <div className="space-y-10">
62
+ <div className="text-center mb-8">
63
+ <h1 className="text-5xl md:text-6xl font-black text-white mb-6">
64
+ How Computers See Objects
65
+ </h1>
66
+ <p className="text-2xl text-white/70">Bounding Boxes & Labels</p>
67
+ </div>
68
+
69
+ <motion.div
70
+ initial={{ opacity: 0, y: 20 }}
71
+ animate={{ opacity: 1, y: 0 }}
72
+ transition={{ delay: 0.3 }}
73
+ className="mb-10"
74
+ >
75
+ <BoundingBoxDemo />
76
+ </motion.div>
77
+
78
+ <div className="grid md:grid-cols-2 gap-6">
79
+ <motion.div
80
+ initial={{ opacity: 0, x: -20 }}
81
+ animate={{ opacity: 1, x: 0 }}
82
+ transition={{ delay: 0.5 }}
83
+ className="bg-white/5 backdrop-blur-xl rounded-3xl p-8 border border-white/10"
84
+ >
85
+ <div className="text-5xl mb-4">📦</div>
86
+ <h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-2">
87
+ <Target className="w-7 h-7 text-cyan-400" />
88
+ Bounding Boxes
89
+ </h3>
90
+ <p className="text-white/80 leading-relaxed text-lg">
91
+ A <KeyTermBadge term="Bounding Box" definition="A rectangle drawn around an object to show where it is in the image" color="cyan" /> is a rectangle that the computer draws around each object it finds. It's like drawing a box around something to say "Look here!"
92
+ </p>
93
+ </motion.div>
94
+
95
+ <motion.div
96
+ initial={{ opacity: 0, x: 20 }}
97
+ animate={{ opacity: 1, x: 0 }}
98
+ transition={{ delay: 0.6 }}
99
+ className="bg-white/5 backdrop-blur-xl rounded-3xl p-8 border border-white/10"
100
+ >
101
+ <div className="text-5xl mb-4">⚡</div>
102
+ <h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-2">
103
+ <Zap className="w-7 h-7 text-yellow-400" />
104
+ Confidence Score
105
+ </h3>
106
+ <p className="text-white/80 leading-relaxed text-lg">
107
+ The computer also gives a <KeyTermBadge term="Confidence Score" definition="A percentage showing how sure the AI is about its prediction" color="yellow" /> — like saying "I'm 98% sure this is a car!"
108
+ </p>
109
+ </motion.div>
110
+ </div>
111
+ </div>
112
+ )
113
+ },
114
+
115
+ // Slide 3: Real-World Examples
116
+ {
117
+ content: (
118
+ <div className="space-y-8">
119
+ <div className="text-center">
120
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
121
+ Real-World Examples
122
+ </h1>
123
+ <p className="text-xl text-white/70">Object detection is everywhere!</p>
124
+ </div>
125
+
126
+ <div className="grid gap-6">
127
+ {[
128
+ {
129
+ icon: Car,
130
+ title: "Self-Driving Cars",
131
+ description: "Cars use object detection to spot other vehicles, pedestrians, traffic signs, and road markings to drive safely.",
132
+ color: "from-blue-500 to-cyan-500"
133
+ },
134
+ {
135
+ icon: Bot,
136
+ title: "Robots & Automation",
137
+ description: "Factory robots detect products on assembly lines to pick, sort, and package items automatically.",
138
+ color: "from-purple-500 to-pink-500"
139
+ },
140
+ {
141
+ icon: Camera,
142
+ title: "Phone Cameras",
143
+ description: "Your phone's camera detects faces to focus properly and apply cool filters!",
144
+ color: "from-emerald-500 to-teal-500"
145
+ }
146
+ ].map((item, i) => (
147
+ <motion.div
148
+ key={i}
149
+ initial={{ opacity: 0, x: -30 }}
150
+ animate={{ opacity: 1, x: 0 }}
151
+ transition={{ delay: 0.3 + i * 0.15 }}
152
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-6 border border-white/10 flex items-start gap-4"
153
+ >
154
+ <div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${item.color} flex items-center justify-center flex-shrink-0`}>
155
+ <item.icon className="w-7 h-7 text-white" />
156
+ </div>
157
+ <div>
158
+ <h3 className="text-xl font-bold text-white mb-2">{item.title}</h3>
159
+ <p className="text-white/70">{item.description}</p>
160
+ </div>
161
+ </motion.div>
162
+ ))}
163
+ </div>
164
+
165
+ <ClickReveal title="🚗 Did you know?" color="blue">
166
+ <p>A single self-driving car processes about 1 terabyte of data per day — that's like watching 500 hours of HD video!</p>
167
+ </ClickReveal>
168
+ </div>
169
+ )
170
+ },
171
+
172
+ // Slide 4: How Detection Works
173
+ {
174
+ content: (
175
+ <div className="space-y-8">
176
+ <div className="text-center">
177
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
178
+ How Detection Works
179
+ </h1>
180
+ <p className="text-xl text-white/70">The magic behind the scenes</p>
181
+ </div>
182
+
183
+ <div className="relative">
184
+ {/* Process Steps */}
185
+ <div className="space-y-6">
186
+ {[
187
+ { step: 1, title: "Input Image", desc: "The computer receives a photo or video frame", icon: "📷" },
188
+ { step: 2, title: "Grid Division", desc: "The image is split into a grid of small cells", icon: "🔲" },
189
+ { step: 3, title: "Feature Extraction", desc: "AI looks for patterns, edges, and shapes", icon: "🔍" },
190
+ { step: 4, title: "Predictions", desc: "Each cell predicts what objects might be there", icon: "🎯" },
191
+ { step: 5, title: "Output", desc: "Final boxes and labels are drawn on the image", icon: "✅" }
192
+ ].map((item, i) => (
193
+ <motion.div
194
+ key={i}
195
+ initial={{ opacity: 0, x: -30 }}
196
+ animate={{ opacity: 1, x: 0 }}
197
+ transition={{ delay: 0.2 + i * 0.1 }}
198
+ className="flex items-center gap-4"
199
+ >
200
+ <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-cyan-500/20 to-blue-500/20 border border-cyan-500/30 flex items-center justify-center text-2xl">
201
+ {item.icon}
202
+ </div>
203
+ <div className="flex-1 bg-white/5 rounded-2xl p-4 border border-white/10">
204
+ <div className="flex items-center gap-2 mb-1">
205
+ <span className="w-6 h-6 rounded-full bg-cyan-500 text-white text-sm font-bold flex items-center justify-center">{item.step}</span>
206
+ <h4 className="font-bold text-white">{item.title}</h4>
207
+ </div>
208
+ <p className="text-white/70 text-sm">{item.desc}</p>
209
+ </div>
210
+ </motion.div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+
215
+ <ClickReveal title="⚡ Speed Matters!" color="yellow">
216
+ <p>YOLO (You Only Look Once) can process 45 frames per second — faster than your eyes can blink!</p>
217
+ </ClickReveal>
218
+ </div>
219
+ )
220
+ },
221
+
222
+ // Slide 5: Chapter Summary
223
+ {
224
+ content: (
225
+ <div className="space-y-10">
226
+ <div className="text-center mb-8">
227
+ <div className="text-7xl mb-6">🎉</div>
228
+ <h1 className="text-5xl md:text-6xl font-black text-white mb-6">
229
+ Chapter Complete!
230
+ </h1>
231
+ <p className="text-2xl text-white/70">You've mastered Object Detection</p>
232
+ </div>
233
+
234
+ <div className="bg-gradient-to-br from-blue-500/20 to-cyan-500/20 rounded-3xl p-10 border border-cyan-500/30">
235
+ <h3 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
236
+ <CheckCircle2 className="w-8 h-8 text-cyan-400" />
237
+ What You Learned
238
+ </h3>
239
+ <div className="grid gap-4">
240
+ {[
241
+ { icon: "👁️", text: "Object detection finds and identifies things in images" },
242
+ { icon: "📦", text: "Bounding boxes show where objects are located" },
243
+ { icon: "💯", text: "Confidence scores tell us how sure the AI is" },
244
+ { icon: "🚗", text: "This technology powers cars, robots, and phones!" }
245
+ ].map((item, i) => (
246
+ <motion.div
247
+ key={i}
248
+ initial={{ opacity: 0, x: -20 }}
249
+ animate={{ opacity: 1, x: 0 }}
250
+ transition={{ delay: 0.3 + i * 0.1 }}
251
+ className="flex items-center gap-4 bg-white/5 rounded-2xl p-5"
252
+ >
253
+ <div className="text-4xl">{item.icon}</div>
254
+ <p className="text-white/90 text-lg flex-1">{item.text}</p>
255
+ </motion.div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+
260
+ <div className="grid md:grid-cols-3 gap-6">
261
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
262
+ <div className="text-4xl mb-3">🎯</div>
263
+ <div className="text-3xl font-bold text-cyan-400">5</div>
264
+ <div className="text-white/60">Slides Completed</div>
265
+ </div>
266
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
267
+ <div className="text-4xl mb-3">🧠</div>
268
+ <div className="text-3xl font-bold text-blue-400">80+</div>
269
+ <div className="text-white/60">Object Classes</div>
270
+ </div>
271
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
272
+ <div className="text-4xl mb-3">⚡</div>
273
+ <div className="text-3xl font-bold text-purple-400">45</div>
274
+ <div className="text-white/60">FPS Speed</div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ )
279
+ }
280
+ ];
src/components/lessons/Chapter2Slides.jsx ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Activity, Dumbbell, Gamepad2, Stethoscope, Play, CheckCircle2 } from 'lucide-react';
4
+ import ClickReveal from './ClickReveal';
5
+ import KeyTermBadge from './KeyTermBadge';
6
+ import { PoseSkeletonDemo } from './AnimatedDiagram';
7
+
8
+ export const chapter2Slides = [
9
+ // Slide 1: What is Pose Estimation?
10
+ {
11
+ content: (
12
+ <div className="space-y-8">
13
+ <motion.div
14
+ initial={{ scale: 0 }}
15
+ animate={{ scale: 1 }}
16
+ transition={{ type: "spring", delay: 0.2 }}
17
+ className="w-24 h-24 mx-auto rounded-3xl bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-2xl shadow-purple-500/30"
18
+ >
19
+ <Activity className="w-12 h-12 text-white" />
20
+ </motion.div>
21
+
22
+ <div className="text-center">
23
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
24
+ What is Pose Estimation?
25
+ </h1>
26
+ <p className="text-xl text-white/70">Chapter 2 • Slide 1 of 5</p>
27
+ </div>
28
+
29
+ <motion.div
30
+ initial={{ opacity: 0, y: 20 }}
31
+ animate={{ opacity: 1, y: 0 }}
32
+ transition={{ delay: 0.4 }}
33
+ className="bg-white/5 backdrop-blur-xl rounded-3xl p-8 border border-white/10"
34
+ >
35
+ <p className="text-xl text-white/90 leading-relaxed mb-6">
36
+ <KeyTermBadge term="Pose Estimation" definition="Technology that detects human body positions by identifying key body points" color="purple" /> is like teaching a computer to understand <span className="text-purple-400 font-semibold">body language</span>!
37
+ </p>
38
+
39
+ <p className="text-lg text-white/70 leading-relaxed">
40
+ It finds important points on your body — like your nose, shoulders, elbows, and knees — and connects them to create a "skeleton" that shows how you're moving.
41
+ </p>
42
+ </motion.div>
43
+
44
+ <ClickReveal title="🎮 Cool Connection!" color="purple">
45
+ <p>Video games like Just Dance use pose estimation to track your dance moves in real-time!</p>
46
+ </ClickReveal>
47
+ </div>
48
+ )
49
+ },
50
+
51
+ // Slide 2: Keypoints and Skeleton Lines
52
+ {
53
+ content: (
54
+ <div className="space-y-8">
55
+ <div className="text-center">
56
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
57
+ Keypoints & Skeleton
58
+ </h1>
59
+ <p className="text-xl text-white/70">Building a digital body map</p>
60
+ </div>
61
+
62
+ <div className="grid md:grid-cols-2 gap-6">
63
+ <motion.div
64
+ initial={{ opacity: 0, y: 20 }}
65
+ animate={{ opacity: 1, y: 0 }}
66
+ transition={{ delay: 0.3 }}
67
+ >
68
+ <PoseSkeletonDemo />
69
+ </motion.div>
70
+
71
+ <div className="space-y-4">
72
+ <motion.div
73
+ initial={{ opacity: 0, x: 20 }}
74
+ animate={{ opacity: 1, x: 0 }}
75
+ transition={{ delay: 0.4 }}
76
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-5 border border-white/10"
77
+ >
78
+ <h3 className="text-xl font-bold text-white mb-3 flex items-center gap-2">
79
+ <span className="w-3 h-3 rounded-full bg-pink-500" />
80
+ Keypoints
81
+ </h3>
82
+ <p className="text-white/80">
83
+ <KeyTermBadge term="Keypoints" definition="Specific body locations like joints that the AI tracks" color="pink" /> are the important spots on your body that the AI tracks — typically 17 points including your nose, eyes, shoulders, elbows, wrists, hips, knees, and ankles.
84
+ </p>
85
+ </motion.div>
86
+
87
+ <motion.div
88
+ initial={{ opacity: 0, x: 20 }}
89
+ animate={{ opacity: 1, x: 0 }}
90
+ transition={{ delay: 0.5 }}
91
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-5 border border-white/10"
92
+ >
93
+ <h3 className="text-xl font-bold text-white mb-3 flex items-center gap-2">
94
+ <div className="w-8 h-0.5 bg-gradient-to-r from-purple-500 to-pink-500" />
95
+ Skeleton Lines
96
+ </h3>
97
+ <p className="text-white/80">
98
+ Lines connect the keypoints to show your body structure — like a stick figure that moves exactly like you!
99
+ </p>
100
+ </motion.div>
101
+
102
+ <motion.div
103
+ initial={{ opacity: 0, x: 20 }}
104
+ animate={{ opacity: 1, x: 0 }}
105
+ transition={{ delay: 0.6 }}
106
+ className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-2xl p-5 border border-purple-500/30"
107
+ >
108
+ <p className="text-white/90 font-medium">
109
+ 💡 Each keypoint has X, Y coordinates (position) and sometimes a confidence score!
110
+ </p>
111
+ </motion.div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ )
116
+ },
117
+
118
+ // Slide 3: Real-World Uses
119
+ {
120
+ content: (
121
+ <div className="space-y-8">
122
+ <div className="text-center">
123
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
124
+ Real-World Uses
125
+ </h1>
126
+ <p className="text-xl text-white/70">Pose estimation helps everywhere!</p>
127
+ </div>
128
+
129
+ <div className="grid gap-6">
130
+ {[
131
+ {
132
+ icon: Dumbbell,
133
+ title: "Sports & Fitness",
134
+ description: "Coaches analyze athlete movements to improve technique. Fitness apps check if you're doing exercises correctly!",
135
+ color: "from-purple-500 to-pink-500"
136
+ },
137
+ {
138
+ icon: Gamepad2,
139
+ title: "Gaming & VR",
140
+ description: "Motion-controlled games track your body to make you the controller. Dance games know exactly how you move!",
141
+ color: "from-blue-500 to-purple-500"
142
+ },
143
+ {
144
+ icon: Stethoscope,
145
+ title: "Healthcare",
146
+ description: "Physical therapists use it to track patient recovery. It can even help detect early signs of movement disorders.",
147
+ color: "from-emerald-500 to-teal-500"
148
+ }
149
+ ].map((item, i) => (
150
+ <motion.div
151
+ key={i}
152
+ initial={{ opacity: 0, x: -30 }}
153
+ animate={{ opacity: 1, x: 0 }}
154
+ transition={{ delay: 0.3 + i * 0.15 }}
155
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-6 border border-white/10 flex items-start gap-4"
156
+ >
157
+ <div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${item.color} flex items-center justify-center flex-shrink-0`}>
158
+ <item.icon className="w-7 h-7 text-white" />
159
+ </div>
160
+ <div>
161
+ <h3 className="text-xl font-bold text-white mb-2">{item.title}</h3>
162
+ <p className="text-white/70">{item.description}</p>
163
+ </div>
164
+ </motion.div>
165
+ ))}
166
+ </div>
167
+
168
+ <ClickReveal title="🏃 Amazing Fact!" color="purple">
169
+ <p>Olympic coaches use pose estimation to analyze athletes frame-by-frame, finding improvements invisible to the human eye!</p>
170
+ </ClickReveal>
171
+ </div>
172
+ )
173
+ },
174
+
175
+ // Slide 4: Animated Stick Figure
176
+ {
177
+ content: (
178
+ <div className="space-y-8">
179
+ <div className="text-center">
180
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
181
+ See It Move!
182
+ </h1>
183
+ <p className="text-xl text-white/70">Watch pose estimation in action</p>
184
+ </div>
185
+
186
+ <AnimatedStickFigure />
187
+
188
+ <div className="grid md:grid-cols-3 gap-4">
189
+ {[
190
+ { label: "Real-time Tracking", icon: "⚡" },
191
+ { label: "17 Keypoints", icon: "📍" },
192
+ { label: "Smooth Motion", icon: "🌊" }
193
+ ].map((item, i) => (
194
+ <motion.div
195
+ key={i}
196
+ initial={{ opacity: 0, y: 20 }}
197
+ animate={{ opacity: 1, y: 0 }}
198
+ transition={{ delay: 0.5 + i * 0.1 }}
199
+ className="bg-white/5 rounded-xl p-4 border border-white/10 text-center"
200
+ >
201
+ <span className="text-2xl">{item.icon}</span>
202
+ <p className="text-white/80 mt-2 font-medium">{item.label}</p>
203
+ </motion.div>
204
+ ))}
205
+ </div>
206
+ </div>
207
+ )
208
+ },
209
+
210
+ // Slide 5: Chapter Summary
211
+ {
212
+ content: (
213
+ <div className="space-y-10">
214
+ <div className="text-center mb-8">
215
+ <div className="text-7xl mb-6">🎊</div>
216
+ <h1 className="text-5xl md:text-6xl font-black text-white mb-6">
217
+ Chapter Complete!
218
+ </h1>
219
+ <p className="text-2xl text-white/70">You've mastered Pose Estimation</p>
220
+ </div>
221
+
222
+ <div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-3xl p-10 border border-purple-500/30">
223
+ <h3 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
224
+ <CheckCircle2 className="w-8 h-8 text-purple-400" />
225
+ Key Takeaways
226
+ </h3>
227
+ <div className="grid gap-4">
228
+ {[
229
+ { icon: "🎯", text: "Pose estimation tracks body movements using keypoints" },
230
+ { icon: "🦴", text: "17 keypoints create a full body skeleton" },
231
+ { icon: "⚽", text: "Used in sports, gaming, healthcare, and more" },
232
+ { icon: "⚡", text: "Works in real-time for interactive applications" }
233
+ ].map((item, i) => (
234
+ <motion.div
235
+ key={i}
236
+ initial={{ opacity: 0, x: -20 }}
237
+ animate={{ opacity: 1, x: 0 }}
238
+ transition={{ delay: 0.3 + i * 0.1 }}
239
+ className="flex items-center gap-4 bg-white/5 rounded-2xl p-5"
240
+ >
241
+ <div className="text-4xl">{item.icon}</div>
242
+ <p className="text-white/90 text-lg flex-1">{item.text}</p>
243
+ </motion.div>
244
+ ))}
245
+ </div>
246
+ </div>
247
+
248
+ <div className="grid md:grid-cols-3 gap-6">
249
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
250
+ <div className="text-4xl mb-3">🎯</div>
251
+ <div className="text-3xl font-bold text-purple-400">5</div>
252
+ <div className="text-white/60">Slides Completed</div>
253
+ </div>
254
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
255
+ <div className="text-4xl mb-3">📍</div>
256
+ <div className="text-3xl font-bold text-pink-400">17</div>
257
+ <div className="text-white/60">Body Keypoints</div>
258
+ </div>
259
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
260
+ <div className="text-4xl mb-3">👥</div>
261
+ <div className="text-3xl font-bold text-violet-400">Multi</div>
262
+ <div className="text-white/60">Person Tracking</div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ )
267
+ }
268
+ ];
269
+
270
+ function AnimatedStickFigure() {
271
+ const [pose, setPose] = React.useState(0);
272
+ const poses = ['standing', 'waving'];
273
+
274
+ React.useEffect(() => {
275
+ const interval = setInterval(() => {
276
+ setPose(p => (p + 1) % poses.length);
277
+ }, 2000);
278
+ return () => clearInterval(interval);
279
+ }, []);
280
+
281
+ const poseConfigs = {
282
+ standing: {
283
+ head: { x: 50, y: 15 },
284
+ neck: { x: 50, y: 22 },
285
+ lshoulder: { x: 38, y: 25 },
286
+ rshoulder: { x: 62, y: 25 },
287
+ lelbow: { x: 32, y: 38 },
288
+ relbow: { x: 68, y: 38 },
289
+ lwrist: { x: 30, y: 50 },
290
+ rwrist: { x: 70, y: 50 },
291
+ hip: { x: 50, y: 50 },
292
+ lhip: { x: 42, y: 52 },
293
+ rhip: { x: 58, y: 52 },
294
+ lknee: { x: 42, y: 70 },
295
+ rknee: { x: 58, y: 70 },
296
+ lankle: { x: 42, y: 88 },
297
+ rankle: { x: 58, y: 88 }
298
+ },
299
+ waving: {
300
+ head: { x: 50, y: 15 },
301
+ neck: { x: 50, y: 22 },
302
+ lshoulder: { x: 38, y: 25 },
303
+ rshoulder: { x: 62, y: 25 },
304
+ lelbow: { x: 32, y: 38 },
305
+ relbow: { x: 75, y: 15 },
306
+ lwrist: { x: 30, y: 50 },
307
+ rwrist: { x: 85, y: 8 },
308
+ hip: { x: 50, y: 50 },
309
+ lhip: { x: 42, y: 52 },
310
+ rhip: { x: 58, y: 52 },
311
+ lknee: { x: 42, y: 70 },
312
+ rknee: { x: 58, y: 70 },
313
+ lankle: { x: 42, y: 88 },
314
+ rankle: { x: 58, y: 88 }
315
+ }
316
+ };
317
+
318
+ const currentPose = poseConfigs[poses[pose]];
319
+ const bones = [
320
+ ['head', 'neck'],
321
+ ['neck', 'lshoulder'],
322
+ ['neck', 'rshoulder'],
323
+ ['lshoulder', 'lelbow'],
324
+ ['rshoulder', 'relbow'],
325
+ ['lelbow', 'lwrist'],
326
+ ['relbow', 'rwrist'],
327
+ ['neck', 'hip'],
328
+ ['hip', 'lhip'],
329
+ ['hip', 'rhip'],
330
+ ['lhip', 'lknee'],
331
+ ['rhip', 'rknee'],
332
+ ['lknee', 'lankle'],
333
+ ['rknee', 'rankle'],
334
+ ];
335
+
336
+ return (
337
+ <div className="relative max-w-md mx-auto">
338
+ <div className="aspect-[3/4] bg-gradient-to-br from-purple-900/50 to-pink-900/50 rounded-3xl overflow-hidden border border-white/10">
339
+ <svg className="w-full h-full" viewBox="0 0 100 100">
340
+ {/* Bones */}
341
+ {bones.map(([from, to], i) => (
342
+ <motion.line
343
+ key={i}
344
+ x1={currentPose[from].x}
345
+ y1={currentPose[from].y}
346
+ x2={currentPose[to].x}
347
+ y2={currentPose[to].y}
348
+ stroke="url(#poseGradient)"
349
+ strokeWidth="3"
350
+ strokeLinecap="round"
351
+ initial={false}
352
+ animate={{
353
+ x1: currentPose[from].x,
354
+ y1: currentPose[from].y,
355
+ x2: currentPose[to].x,
356
+ y2: currentPose[to].y
357
+ }}
358
+ transition={{ type: "spring", stiffness: 100, damping: 15 }}
359
+ />
360
+ ))}
361
+
362
+ {/* Keypoints */}
363
+ {Object.entries(currentPose).map(([key, pos], i) => (
364
+ <motion.circle
365
+ key={key}
366
+ r="4"
367
+ fill="#EC4899"
368
+ initial={false}
369
+ animate={{ cx: pos.x, cy: pos.y }}
370
+ transition={{ type: "spring", stiffness: 100, damping: 15 }}
371
+ />
372
+ ))}
373
+
374
+ <defs>
375
+ <linearGradient id="poseGradient" x1="0%" y1="0%" x2="100%" y2="100%">
376
+ <stop offset="0%" stopColor="#8B5CF6" />
377
+ <stop offset="100%" stopColor="#EC4899" />
378
+ </linearGradient>
379
+ </defs>
380
+ </svg>
381
+ </div>
382
+
383
+ {/* Pose Label */}
384
+ <motion.div
385
+ key={pose}
386
+ initial={{ opacity: 0, y: 10 }}
387
+ animate={{ opacity: 1, y: 0 }}
388
+ className="mt-4 text-center"
389
+ >
390
+ <span className="px-6 py-2 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 text-white font-bold text-lg">
391
+ {poses[pose].charAt(0).toUpperCase() + poses[pose].slice(1)}
392
+ </span>
393
+ </motion.div>
394
+ </div>
395
+ );
396
+ }
src/components/lessons/Chapter3Slides.jsx ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { motion } from 'framer-motion';
4
+ import { Smile, Brain, AlertTriangle, Gamepad2, Accessibility, Heart, Shield, CheckCircle2 } from 'lucide-react';
5
+ import ClickReveal from './ClickReveal';
6
+ import KeyTermBadge from './KeyTermBadge';
7
+ import { EmotionFaceDemo } from './AnimatedDiagram';
8
+
9
+ export const chapter3Slides = [
10
+ // Slide 1: What is Emotion Recognition?
11
+ {
12
+ content: (
13
+ <div className="space-y-8">
14
+ <motion.div
15
+ initial={{ scale: 0 }}
16
+ animate={{ scale: 1 }}
17
+ transition={{ type: "spring", delay: 0.2 }}
18
+ className="w-24 h-24 mx-auto rounded-3xl bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-2xl shadow-emerald-500/30"
19
+ >
20
+ <Smile className="w-12 h-12 text-white" />
21
+ </motion.div>
22
+
23
+ <div className="text-center">
24
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
25
+ What is Emotion Recognition?
26
+ </h1>
27
+ <p className="text-xl text-white/70">Chapter 3 • Slide 1 of 5</p>
28
+ </div>
29
+
30
+ <motion.div
31
+ initial={{ opacity: 0, y: 20 }}
32
+ animate={{ opacity: 1, y: 0 }}
33
+ transition={{ delay: 0.4 }}
34
+ className="bg-white/5 backdrop-blur-xl rounded-3xl p-8 border border-white/10"
35
+ >
36
+ <p className="text-xl text-white/90 leading-relaxed mb-6">
37
+ <KeyTermBadge term="Emotion Recognition" definition="AI technology that identifies human emotions from facial expressions" color="green" /> teaches computers to understand how people are feeling by looking at their faces!
38
+ </p>
39
+
40
+ <p className="text-lg text-white/70 leading-relaxed">
41
+ Just like you can tell when a friend is happy, sad, or surprised, AI can learn to read these same expressions — and it's getting really good at it!
42
+ </p>
43
+ </motion.div>
44
+
45
+ <ClickReveal title="🧠 Mind-Blowing Fact!" color="green">
46
+ <p>Humans can recognize emotions in just 100 milliseconds — that's faster than the blink of an eye! AI is learning to match this speed.</p>
47
+ </ClickReveal>
48
+ </div>
49
+ )
50
+ },
51
+
52
+ // Slide 2: The 7 Emotions
53
+ {
54
+ content: (
55
+ <div className="space-y-8">
56
+ <div className="text-center">
57
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
58
+ The 7 Basic Emotions
59
+ </h1>
60
+ <p className="text-xl text-white/70">What AI learns to detect</p>
61
+ </div>
62
+
63
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
64
+ {[
65
+ { emoji: "😊", label: "Happy", color: "from-yellow-400 to-orange-400" },
66
+ { emoji: "😢", label: "Sad", color: "from-blue-400 to-indigo-400" },
67
+ { emoji: "😠", label: "Angry", color: "from-red-400 to-rose-400" },
68
+ { emoji: "😨", label: "Fear", color: "from-purple-400 to-violet-400" },
69
+ { emoji: "😲", label: "Surprise", color: "from-pink-400 to-fuchsia-400" },
70
+ { emoji: "🤢", label: "Disgust", color: "from-green-400 to-lime-400" },
71
+ { emoji: "😐", label: "Neutral", color: "from-gray-400 to-slate-400" }
72
+ ].map((emotion, i) => (
73
+ <motion.div
74
+ key={i}
75
+ initial={{ opacity: 0, scale: 0.8 }}
76
+ animate={{ opacity: 1, scale: 1 }}
77
+ transition={{ delay: 0.2 + i * 0.08 }}
78
+ whileHover={{ scale: 1.05, y: -5 }}
79
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-4 border border-white/10 text-center cursor-pointer"
80
+ >
81
+ <motion.span
82
+ className="text-5xl block mb-2"
83
+ animate={{ rotate: [0, -5, 5, 0] }}
84
+ transition={{ duration: 2, repeat: Infinity, delay: i * 0.2 }}
85
+ >
86
+ {emotion.emoji}
87
+ </motion.span>
88
+ <span className={`inline-block px-3 py-1 rounded-full bg-gradient-to-r ${emotion.color} text-white font-bold text-sm`}>
89
+ {emotion.label}
90
+ </span>
91
+ </motion.div>
92
+ ))}
93
+ <motion.div
94
+ initial={{ opacity: 0, scale: 0.8 }}
95
+ animate={{ opacity: 1, scale: 1 }}
96
+ transition={{ delay: 0.8 }}
97
+ className="bg-gradient-to-br from-emerald-500/20 to-teal-500/20 backdrop-blur-xl rounded-2xl p-4 border border-emerald-500/30 flex items-center justify-center"
98
+ >
99
+ <p className="text-white/80 text-sm font-medium text-center">
100
+ These 7 emotions are called <KeyTermBadge term="FER" definition="Facial Expression Recognition - the standard set of 7 emotions used in AI" color="green" /> emotions!
101
+ </p>
102
+ </motion.div>
103
+ </div>
104
+
105
+ <ClickReveal title="🌍 Universal Expressions" color="green">
106
+ <p>These 7 emotions are recognized across all cultures! A smile means happiness whether you're in Tokyo, New York, or Cairo.</p>
107
+ </ClickReveal>
108
+ </div>
109
+ )
110
+ },
111
+
112
+ // Slide 3: How the Model Reads Faces
113
+ {
114
+ content: (
115
+ <div className="space-y-8">
116
+ <div className="text-center">
117
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
118
+ How AI Reads Faces
119
+ </h1>
120
+ <p className="text-xl text-white/70">The technical magic explained</p>
121
+ </div>
122
+
123
+ <div className="grid md:grid-cols-2 gap-8">
124
+ <motion.div
125
+ initial={{ opacity: 0, y: 20 }}
126
+ animate={{ opacity: 1, y: 0 }}
127
+ transition={{ delay: 0.3 }}
128
+ >
129
+ <EmotionFaceDemo />
130
+ </motion.div>
131
+
132
+ <div className="space-y-4">
133
+ <motion.div
134
+ initial={{ opacity: 0, x: 20 }}
135
+ animate={{ opacity: 1, x: 0 }}
136
+ transition={{ delay: 0.4 }}
137
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-5 border border-white/10"
138
+ >
139
+ <h3 className="text-xl font-bold text-white mb-3">48×48 Pixels</h3>
140
+ <p className="text-white/80">
141
+ The AI looks at tiny <KeyTermBadge term="Grayscale" definition="Black and white image without color - makes processing faster" color="green" /> images — just 48 by 48 pixels! That's smaller than most emojis.
142
+ </p>
143
+ </motion.div>
144
+
145
+ <motion.div
146
+ initial={{ opacity: 0, x: 20 }}
147
+ animate={{ opacity: 1, x: 0 }}
148
+ transition={{ delay: 0.5 }}
149
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-5 border border-white/10"
150
+ >
151
+ <h3 className="text-xl font-bold text-white mb-3">Pattern Recognition</h3>
152
+ <p className="text-white/80">
153
+ The model looks for patterns: raised eyebrows = surprise, frown + lowered brows = anger, upturned mouth = happy!
154
+ </p>
155
+ </motion.div>
156
+
157
+ <motion.div
158
+ initial={{ opacity: 0, x: 20 }}
159
+ animate={{ opacity: 1, x: 0 }}
160
+ transition={{ delay: 0.6 }}
161
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-5 border border-white/10"
162
+ >
163
+ <h3 className="text-xl font-bold text-white mb-3">Confidence Scores</h3>
164
+ <p className="text-white/80">
165
+ The AI gives a <KeyTermBadge term="Classification" definition="Putting something into a category - like sorting emotions" color="green" /> score for each emotion, like "85% happy, 10% neutral, 5% surprise."
166
+ </p>
167
+ </motion.div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+ },
173
+
174
+ // Slide 4: Real-World Uses
175
+ {
176
+ content: (
177
+ <div className="space-y-8">
178
+ <div className="text-center">
179
+ <h1 className="text-4xl md:text-5xl font-black text-white mb-4">
180
+ Real-World Applications
181
+ </h1>
182
+ <p className="text-xl text-white/70">Where emotion AI helps</p>
183
+ </div>
184
+
185
+ <div className="grid gap-6">
186
+ {[
187
+ {
188
+ icon: Gamepad2,
189
+ title: "Video Games",
190
+ description: "Games can adapt to your mood! If you look frustrated, the game might offer hints. If you're having fun, it might increase the challenge.",
191
+ color: "from-purple-500 to-pink-500"
192
+ },
193
+ {
194
+ icon: Heart,
195
+ title: "Smart Assistants",
196
+ description: "Virtual assistants might change their tone based on your mood — being more upbeat when you're sad or calmer when you're stressed.",
197
+ color: "from-red-500 to-rose-500"
198
+ },
199
+ {
200
+ icon: Accessibility,
201
+ title: "Accessibility",
202
+ description: "Helps people with autism or other conditions understand facial expressions better through real-time feedback.",
203
+ color: "from-emerald-500 to-teal-500"
204
+ }
205
+ ].map((item, i) => (
206
+ <motion.div
207
+ key={i}
208
+ initial={{ opacity: 0, x: -30 }}
209
+ animate={{ opacity: 1, x: 0 }}
210
+ transition={{ delay: 0.3 + i * 0.15 }}
211
+ className="bg-white/5 backdrop-blur-xl rounded-2xl p-6 border border-white/10 flex items-start gap-4"
212
+ >
213
+ <div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${item.color} flex items-center justify-center flex-shrink-0`}>
214
+ <item.icon className="w-7 h-7 text-white" />
215
+ </div>
216
+ <div>
217
+ <h3 className="text-xl font-bold text-white mb-2">{item.title}</h3>
218
+ <p className="text-white/70">{item.description}</p>
219
+ </div>
220
+ </motion.div>
221
+ ))}
222
+ </div>
223
+
224
+ <ClickReveal title="🎬 In Movies!" color="green">
225
+ <p>Film studios use emotion recognition to test audience reactions to trailers and scenes before movies are released!</p>
226
+ </ClickReveal>
227
+ </div>
228
+ )
229
+ },
230
+
231
+ // Slide 5: Chapter Summary
232
+ {
233
+ content: (
234
+ <div className="space-y-10">
235
+ <div className="text-center mb-8">
236
+ <div className="text-7xl mb-6">🌟</div>
237
+ <h1 className="text-5xl md:text-6xl font-black text-white mb-6">
238
+ Chapter Complete!
239
+ </h1>
240
+ <p className="text-2xl text-white/70">You've mastered Emotion Recognition</p>
241
+ </div>
242
+
243
+ <motion.div
244
+ initial={{ opacity: 0, y: 20 }}
245
+ animate={{ opacity: 1, y: 0 }}
246
+ transition={{ delay: 0.3 }}
247
+ className="bg-gradient-to-br from-amber-500/20 to-orange-500/20 rounded-3xl p-8 border border-amber-500/30"
248
+ >
249
+ <div className="flex items-start gap-4">
250
+ <AlertTriangle className="w-8 h-8 text-amber-400 flex-shrink-0" />
251
+ <div>
252
+ <h3 className="text-xl font-bold text-white mb-2">Remember</h3>
253
+ <p className="text-white/80 text-lg">
254
+ Just because AI <em>can</em> do something doesn't mean it <em>should</em>. Always use emotion recognition responsibly!
255
+ </p>
256
+ </div>
257
+ </div>
258
+ </motion.div>
259
+
260
+ <div className="bg-gradient-to-br from-emerald-500/20 to-teal-500/20 rounded-3xl p-10 border border-emerald-500/30">
261
+ <h3 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
262
+ <CheckCircle2 className="w-8 h-8 text-emerald-400" />
263
+ What You Learned
264
+ </h3>
265
+ <div className="grid gap-4">
266
+ {[
267
+ { icon: "😊", text: "Emotion AI detects 7 basic facial expressions" },
268
+ { icon: "🖼️", text: "It uses small grayscale images (48×48 pixels)" },
269
+ { icon: "🎮", text: "Applications include games, assistants, and accessibility" },
270
+ { icon: "⚖️", text: "Ethics and privacy are crucial considerations" }
271
+ ].map((item, i) => (
272
+ <motion.div
273
+ key={i}
274
+ initial={{ opacity: 0, x: -20 }}
275
+ animate={{ opacity: 1, x: 0 }}
276
+ transition={{ delay: 0.5 + i * 0.1 }}
277
+ className="flex items-center gap-4 bg-white/5 rounded-2xl p-5"
278
+ >
279
+ <div className="text-4xl">{item.icon}</div>
280
+ <p className="text-white/90 text-lg flex-1">{item.text}</p>
281
+ </motion.div>
282
+ ))}
283
+ </div>
284
+ </div>
285
+
286
+ <div className="grid md:grid-cols-3 gap-6">
287
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
288
+ <div className="text-4xl mb-3">🎯</div>
289
+ <div className="text-3xl font-bold text-emerald-400">5</div>
290
+ <div className="text-white/60">Slides Completed</div>
291
+ </div>
292
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
293
+ <div className="text-4xl mb-3">😊</div>
294
+ <div className="text-3xl font-bold text-teal-400">7</div>
295
+ <div className="text-white/60">Emotions Tracked</div>
296
+ </div>
297
+ <div className="bg-white/5 rounded-2xl p-6 text-center border border-white/10">
298
+ <div className="text-4xl mb-3">🖼️</div>
299
+ <div className="text-3xl font-bold text-cyan-400">48×48</div>
300
+ <div className="text-white/60">Pixel Size</div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ )
305
+ }
306
+ ];
src/components/lessons/ClickReveal.jsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Sparkles, ChevronDown } from 'lucide-react';
4
+
5
+ export default function ClickReveal({ title, children, color = "blue" }) {
6
+ const [isRevealed, setIsRevealed] = useState(false);
7
+
8
+ const colors = {
9
+ blue: "from-blue-500/20 to-cyan-500/20 border-blue-500/30 hover:border-blue-400/50",
10
+ purple: "from-purple-500/20 to-pink-500/20 border-purple-500/30 hover:border-purple-400/50",
11
+ green: "from-emerald-500/20 to-teal-500/20 border-emerald-500/30 hover:border-emerald-400/50",
12
+ yellow: "from-yellow-500/20 to-orange-500/20 border-yellow-500/30 hover:border-yellow-400/50"
13
+ };
14
+
15
+ return (
16
+ <motion.div
17
+ className={`rounded-2xl bg-gradient-to-br ${colors[color]} backdrop-blur-sm border-2 overflow-hidden cursor-pointer transition-all duration-300`}
18
+ onClick={() => setIsRevealed(!isRevealed)}
19
+ whileHover={{ scale: 1.01 }}
20
+ whileTap={{ scale: 0.99 }}
21
+ >
22
+ <div className="p-5 flex items-center justify-between">
23
+ <div className="flex items-center gap-3">
24
+ <Sparkles className="w-5 h-5 text-yellow-400" />
25
+ <span className="font-semibold text-white">{title}</span>
26
+ </div>
27
+ <motion.div
28
+ animate={{ rotate: isRevealed ? 180 : 0 }}
29
+ transition={{ duration: 0.3 }}
30
+ >
31
+ <ChevronDown className="w-5 h-5 text-white/60" />
32
+ </motion.div>
33
+ </div>
34
+
35
+ <AnimatePresence>
36
+ {isRevealed && (
37
+ <motion.div
38
+ initial={{ height: 0, opacity: 0 }}
39
+ animate={{ height: "auto", opacity: 1 }}
40
+ exit={{ height: 0, opacity: 0 }}
41
+ transition={{ duration: 0.3 }}
42
+ >
43
+ <div className="px-5 pb-5 text-white/80 border-t border-white/10 pt-4">
44
+ {children}
45
+ </div>
46
+ </motion.div>
47
+ )}
48
+ </AnimatePresence>
49
+ </motion.div>
50
+ );
51
+ }
src/components/lessons/KeyTermBadge.jsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+
4
+ export default function KeyTermBadge({ term, definition, color = "cyan" }) {
5
+ const [showDefinition, setShowDefinition] = useState(false);
6
+
7
+ const colors = {
8
+ cyan: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30 hover:bg-cyan-500/30",
9
+ purple: "bg-purple-500/20 text-purple-300 border-purple-500/30 hover:bg-purple-500/30",
10
+ pink: "bg-pink-500/20 text-pink-300 border-pink-500/30 hover:bg-pink-500/30",
11
+ green: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30 hover:bg-emerald-500/30",
12
+ yellow: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30 hover:bg-yellow-500/30"
13
+ };
14
+
15
+ return (
16
+ <span className="relative inline-block">
17
+ <motion.button
18
+ className={`px-3 py-1 rounded-lg border ${colors[color]} font-semibold text-sm transition-colors cursor-pointer`}
19
+ onClick={() => setShowDefinition(!showDefinition)}
20
+ whileHover={{ scale: 1.05 }}
21
+ whileTap={{ scale: 0.95 }}
22
+ >
23
+ {term}
24
+ </motion.button>
25
+
26
+ <AnimatePresence>
27
+ {showDefinition && (
28
+ <motion.div
29
+ initial={{ opacity: 0, y: 10, scale: 0.9 }}
30
+ animate={{ opacity: 1, y: 0, scale: 1 }}
31
+ exit={{ opacity: 0, y: 10, scale: 0.9 }}
32
+ className="absolute z-50 left-0 top-full mt-2 p-3 rounded-xl bg-slate-800 border border-white/20 shadow-xl min-w-[200px] max-w-[300px]"
33
+ >
34
+ <p className="text-white/90 text-sm">{definition}</p>
35
+ <div className="absolute -top-2 left-4 w-4 h-4 bg-slate-800 border-l border-t border-white/20 transform rotate-45" />
36
+ </motion.div>
37
+ )}
38
+ </AnimatePresence>
39
+ </span>
40
+ );
41
+ }
src/components/lessons/SlideContainer.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+
4
+ export default function SlideContainer({ children, slideKey }) {
5
+ return (
6
+ <AnimatePresence mode="wait">
7
+ <motion.div
8
+ key={slideKey}
9
+ initial={{ opacity: 0, x: 50 }}
10
+ animate={{ opacity: 1, x: 0 }}
11
+ exit={{ opacity: 0, x: -50 }}
12
+ transition={{ duration: 0.4, ease: "easeOut" }}
13
+ className="min-h-screen pt-24 pb-32 px-6"
14
+ >
15
+ <div className="max-w-4xl mx-auto">
16
+ {children}
17
+ </div>
18
+ </motion.div>
19
+ </AnimatePresence>
20
+ );
21
+ }
src/components/lessons/SlideNavigation.jsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Button } from '@/components/ui/button';
4
+ import { ChevronLeft, ChevronRight, Home } from 'lucide-react';
5
+
6
+ export default function SlideNavigation({
7
+ currentSlide,
8
+ totalSlides,
9
+ onPrev,
10
+ onNext,
11
+ onHome,
12
+ chapterTitle
13
+ }) {
14
+ return (
15
+ <div className="fixed bottom-0 left-0 right-0 z-50">
16
+ <div className="bg-slate-900/90 backdrop-blur-xl border-t border-white/10">
17
+ <div className="max-w-4xl mx-auto px-6 py-4">
18
+ <div className="flex items-center justify-between gap-4">
19
+ {/* Progress Dots */}
20
+ <div className="hidden sm:flex items-center gap-2">
21
+ {Array.from({ length: totalSlides }).map((_, i) => (
22
+ <motion.div
23
+ key={i}
24
+ className={`h-2 rounded-full transition-all duration-300 ${
25
+ i === currentSlide
26
+ ? 'w-8 bg-gradient-to-r from-cyan-400 to-purple-400'
27
+ : i < currentSlide
28
+ ? 'w-2 bg-white/50'
29
+ : 'w-2 bg-white/20'
30
+ }`}
31
+ initial={false}
32
+ animate={{ scale: i === currentSlide ? 1.1 : 1 }}
33
+ />
34
+ ))}
35
+ </div>
36
+
37
+ {/* Slide Counter (Mobile) */}
38
+ <div className="sm:hidden text-white/70 font-medium">
39
+ {currentSlide + 1} / {totalSlides}
40
+ </div>
41
+
42
+ {/* Navigation Buttons */}
43
+ <div className="flex items-center gap-3">
44
+ <Button
45
+ size="icon"
46
+ onClick={onHome}
47
+ className="text-white/70 hover:text-white bg-white/5 hover:bg-white/10 rounded-xl border-0"
48
+ >
49
+ <Home className="w-5 h-5" />
50
+ </Button>
51
+
52
+ <Button
53
+ onClick={onPrev}
54
+ disabled={currentSlide === 0}
55
+ className="border-2 border-white/20 text-white bg-white/5 hover:bg-white/10 rounded-xl disabled:opacity-30 disabled:cursor-not-allowed font-semibold"
56
+ >
57
+ <ChevronLeft className="w-5 h-5 mr-1" />
58
+ Back
59
+ </Button>
60
+
61
+ <Button
62
+ onClick={onNext}
63
+ disabled={currentSlide === totalSlides - 1}
64
+ className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 rounded-xl disabled:opacity-30 disabled:cursor-not-allowed text-white font-bold border-0 shadow-lg"
65
+ >
66
+ Next
67
+ <ChevronRight className="w-5 h-5 ml-1" />
68
+ </Button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
src/components/ui/accordion.jsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDown } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Accordion = AccordionPrimitive.Root
8
+
9
+ const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
10
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
11
+ ))
12
+ AccordionItem.displayName = "AccordionItem"
13
+
14
+ const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
15
+ <AccordionPrimitive.Header className="flex">
16
+ <AccordionPrimitive.Trigger
17
+ ref={ref}
18
+ className={cn(
19
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
20
+ className
21
+ )}
22
+ {...props}>
23
+ {children}
24
+ <ChevronDown
25
+ className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
26
+ </AccordionPrimitive.Trigger>
27
+ </AccordionPrimitive.Header>
28
+ ))
29
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
30
+
31
+ const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
32
+ <AccordionPrimitive.Content
33
+ ref={ref}
34
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
35
+ {...props}>
36
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
37
+ </AccordionPrimitive.Content>
38
+ ))
39
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
40
+
41
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
src/components/ui/alert-dialog.jsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
14
+ <AlertDialogPrimitive.Overlay
15
+ className={cn(
16
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
17
+ className
18
+ )}
19
+ {...props}
20
+ ref={ref} />
21
+ ))
22
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
23
+
24
+ const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
25
+ <AlertDialogPortal>
26
+ <AlertDialogOverlay />
27
+ <AlertDialogPrimitive.Content
28
+ ref={ref}
29
+ className={cn(
30
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
31
+ className
32
+ )}
33
+ {...props} />
34
+ </AlertDialogPortal>
35
+ ))
36
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
37
+
38
+ const AlertDialogHeader = ({
39
+ className,
40
+ ...props
41
+ }) => (
42
+ <div
43
+ className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
44
+ {...props} />
45
+ )
46
+ AlertDialogHeader.displayName = "AlertDialogHeader"
47
+
48
+ const AlertDialogFooter = ({
49
+ className,
50
+ ...props
51
+ }) => (
52
+ <div
53
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
54
+ {...props} />
55
+ )
56
+ AlertDialogFooter.displayName = "AlertDialogFooter"
57
+
58
+ const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
59
+ <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
60
+ ))
61
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
62
+
63
+ const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
64
+ <AlertDialogPrimitive.Description
65
+ ref={ref}
66
+ className={cn("text-sm text-muted-foreground", className)}
67
+ {...props} />
68
+ ))
69
+ AlertDialogDescription.displayName =
70
+ AlertDialogPrimitive.Description.displayName
71
+
72
+ const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
73
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
74
+ ))
75
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
76
+
77
+ const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Cancel
79
+ ref={ref}
80
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
81
+ {...props} />
82
+ ))
83
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
84
+
85
+ export {
86
+ AlertDialog,
87
+ AlertDialogPortal,
88
+ AlertDialogOverlay,
89
+ AlertDialogTrigger,
90
+ AlertDialogContent,
91
+ AlertDialogHeader,
92
+ AlertDialogFooter,
93
+ AlertDialogTitle,
94
+ AlertDialogDescription,
95
+ AlertDialogAction,
96
+ AlertDialogCancel,
97
+ }
src/components/ui/alert.jsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
23
+ <div
24
+ ref={ref}
25
+ role="alert"
26
+ className={cn(alertVariants({ variant }), className)}
27
+ {...props} />
28
+ ))
29
+ Alert.displayName = "Alert"
30
+
31
+ const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
32
+ <h5
33
+ ref={ref}
34
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
35
+ {...props} />
36
+ ))
37
+ AlertTitle.displayName = "AlertTitle"
38
+
39
+ const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
40
+ <div
41
+ ref={ref}
42
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
43
+ {...props} />
44
+ ))
45
+ AlertDescription.displayName = "AlertDescription"
46
+
47
+ export { Alert, AlertTitle, AlertDescription }
src/components/ui/aspect-ratio.jsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root
4
+
5
+ export { AspectRatio }
src/components/ui/avatar.jsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef(({ className, ...props }, ref) => (
9
+ <AvatarPrimitive.Root
10
+ ref={ref}
11
+ className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
12
+ {...props} />
13
+ ))
14
+ Avatar.displayName = AvatarPrimitive.Root.displayName
15
+
16
+ const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
17
+ <AvatarPrimitive.Image
18
+ ref={ref}
19
+ className={cn("aspect-square h-full w-full", className)}
20
+ {...props} />
21
+ ))
22
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
23
+
24
+ const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
25
+ <AvatarPrimitive.Fallback
26
+ ref={ref}
27
+ className={cn(
28
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
29
+ className
30
+ )}
31
+ {...props} />
32
+ ))
33
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
34
+
35
+ export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/badge.jsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ function Badge({
27
+ className,
28
+ variant,
29
+ ...props
30
+ }) {
31
+ return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
32
+ }
33
+
34
+ export { Badge, badgeVariants }
src/components/ui/breadcrumb.jsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Breadcrumb = React.forwardRef(
8
+ ({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
9
+ )
10
+ Breadcrumb.displayName = "Breadcrumb"
11
+
12
+ const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
13
+ <ol
14
+ ref={ref}
15
+ className={cn(
16
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
17
+ className
18
+ )}
19
+ {...props} />
20
+ ))
21
+ BreadcrumbList.displayName = "BreadcrumbList"
22
+
23
+ const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
24
+ <li
25
+ ref={ref}
26
+ className={cn("inline-flex items-center gap-1.5", className)}
27
+ {...props} />
28
+ ))
29
+ BreadcrumbItem.displayName = "BreadcrumbItem"
30
+
31
+ const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
32
+ const Comp = asChild ? Slot : "a"
33
+
34
+ return (
35
+ (<Comp
36
+ ref={ref}
37
+ className={cn("transition-colors hover:text-foreground", className)}
38
+ {...props} />)
39
+ );
40
+ })
41
+ BreadcrumbLink.displayName = "BreadcrumbLink"
42
+
43
+ const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
44
+ <span
45
+ ref={ref}
46
+ role="link"
47
+ aria-disabled="true"
48
+ aria-current="page"
49
+ className={cn("font-normal text-foreground", className)}
50
+ {...props} />
51
+ ))
52
+ BreadcrumbPage.displayName = "BreadcrumbPage"
53
+
54
+ const BreadcrumbSeparator = ({
55
+ children,
56
+ className,
57
+ ...props
58
+ }) => (
59
+ <li
60
+ role="presentation"
61
+ aria-hidden="true"
62
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
63
+ {...props}>
64
+ {children ?? <ChevronRight />}
65
+ </li>
66
+ )
67
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
68
+
69
+ const BreadcrumbEllipsis = ({
70
+ className,
71
+ ...props
72
+ }) => (
73
+ <span
74
+ role="presentation"
75
+ aria-hidden="true"
76
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
77
+ {...props}>
78
+ <MoreHorizontal className="h-4 w-4" />
79
+ <span className="sr-only">More</span>
80
+ </span>
81
+ )
82
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
83
+
84
+ export {
85
+ Breadcrumb,
86
+ BreadcrumbList,
87
+ BreadcrumbItem,
88
+ BreadcrumbLink,
89
+ BreadcrumbPage,
90
+ BreadcrumbSeparator,
91
+ BreadcrumbEllipsis,
92
+ }
src/components/ui/button.jsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16
+ outline:
17
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2",
25
+ sm: "h-8 rounded-md px-3 text-xs",
26
+ lg: "h-10 rounded-md px-8",
27
+ icon: "h-9 w-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
38
+ const Comp = asChild ? Slot : "button"
39
+ return (
40
+ (<Comp
41
+ className={cn(buttonVariants({ variant, size, className }))}
42
+ ref={ref}
43
+ {...props} />)
44
+ );
45
+ })
46
+ Button.displayName = "Button"
47
+
48
+ export { Button, buttonVariants }
src/components/ui/calendar.jsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import { DayPicker } from "react-day-picker"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { buttonVariants } from "@/components/ui/button"
7
+
8
+ function Calendar({
9
+ className,
10
+ classNames,
11
+ showOutsideDays = true,
12
+ ...props
13
+ }) {
14
+ return (
15
+ (<DayPicker
16
+ showOutsideDays={showOutsideDays}
17
+ className={cn("p-3", className)}
18
+ classNames={{
19
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
20
+ month: "space-y-4",
21
+ caption: "flex justify-center pt-1 relative items-center",
22
+ caption_label: "text-sm font-medium",
23
+ nav: "space-x-1 flex items-center",
24
+ nav_button: cn(
25
+ buttonVariants({ variant: "outline" }),
26
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
27
+ ),
28
+ nav_button_previous: "absolute left-1",
29
+ nav_button_next: "absolute right-1",
30
+ table: "w-full border-collapse space-y-1",
31
+ head_row: "flex",
32
+ head_cell:
33
+ "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
34
+ row: "flex w-full mt-2",
35
+ cell: cn(
36
+ "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
37
+ props.mode === "range"
38
+ ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
39
+ : "[&:has([aria-selected])]:rounded-md"
40
+ ),
41
+ day: cn(
42
+ buttonVariants({ variant: "ghost" }),
43
+ "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
44
+ ),
45
+ day_range_start: "day-range-start",
46
+ day_range_end: "day-range-end",
47
+ day_selected:
48
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
49
+ day_today: "bg-accent text-accent-foreground",
50
+ day_outside:
51
+ "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
52
+ day_disabled: "text-muted-foreground opacity-50",
53
+ day_range_middle:
54
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
55
+ day_hidden: "invisible",
56
+ ...classNames,
57
+ }}
58
+ components={{
59
+ IconLeft: ({ className, ...props }) => (
60
+ <ChevronLeft className={cn("h-4 w-4", className)} {...props} />
61
+ ),
62
+ IconRight: ({ className, ...props }) => (
63
+ <ChevronRight className={cn("h-4 w-4", className)} {...props} />
64
+ ),
65
+ }}
66
+ {...props} />)
67
+ );
68
+ }
69
+ Calendar.displayName = "Calendar"
70
+
71
+ export { Calendar }
src/components/ui/card.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef(({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
9
+ {...props} />
10
+ ))
11
+ Card.displayName = "Card"
12
+
13
+ const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
14
+ <div
15
+ ref={ref}
16
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
17
+ {...props} />
18
+ ))
19
+ CardHeader.displayName = "CardHeader"
20
+
21
+ const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
22
+ <div
23
+ ref={ref}
24
+ className={cn("font-semibold leading-none tracking-tight", className)}
25
+ {...props} />
26
+ ))
27
+ CardTitle.displayName = "CardTitle"
28
+
29
+ const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
30
+ <div
31
+ ref={ref}
32
+ className={cn("text-sm text-muted-foreground", className)}
33
+ {...props} />
34
+ ))
35
+ CardDescription.displayName = "CardDescription"
36
+
37
+ const CardContent = React.forwardRef(({ className, ...props }, ref) => (
38
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
39
+ ))
40
+ CardContent.displayName = "CardContent"
41
+
42
+ const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
43
+ <div
44
+ ref={ref}
45
+ className={cn("flex items-center p-6 pt-0", className)}
46
+ {...props} />
47
+ ))
48
+ CardFooter.displayName = "CardFooter"
49
+
50
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/carousel.jsx ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import useEmblaCarousel from "embla-carousel-react";
3
+ import { ArrowLeft, ArrowRight } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { Button } from "@/components/ui/button"
7
+
8
+ const CarouselContext = React.createContext(null)
9
+
10
+ function useCarousel() {
11
+ const context = React.useContext(CarouselContext)
12
+
13
+ if (!context) {
14
+ throw new Error("useCarousel must be used within a <Carousel />")
15
+ }
16
+
17
+ return context
18
+ }
19
+
20
+ const Carousel = React.forwardRef((
21
+ {
22
+ orientation = "horizontal",
23
+ opts,
24
+ setApi,
25
+ plugins,
26
+ className,
27
+ children,
28
+ ...props
29
+ },
30
+ ref
31
+ ) => {
32
+ const [carouselRef, api] = useEmblaCarousel({
33
+ ...opts,
34
+ axis: orientation === "horizontal" ? "x" : "y",
35
+ }, plugins)
36
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
37
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
38
+
39
+ const onSelect = React.useCallback((api) => {
40
+ if (!api) {
41
+ return
42
+ }
43
+
44
+ setCanScrollPrev(api.canScrollPrev())
45
+ setCanScrollNext(api.canScrollNext())
46
+ }, [])
47
+
48
+ const scrollPrev = React.useCallback(() => {
49
+ api?.scrollPrev()
50
+ }, [api])
51
+
52
+ const scrollNext = React.useCallback(() => {
53
+ api?.scrollNext()
54
+ }, [api])
55
+
56
+ const handleKeyDown = React.useCallback((event) => {
57
+ if (event.key === "ArrowLeft") {
58
+ event.preventDefault()
59
+ scrollPrev()
60
+ } else if (event.key === "ArrowRight") {
61
+ event.preventDefault()
62
+ scrollNext()
63
+ }
64
+ }, [scrollPrev, scrollNext])
65
+
66
+ React.useEffect(() => {
67
+ if (!api || !setApi) {
68
+ return
69
+ }
70
+
71
+ setApi(api)
72
+ }, [api, setApi])
73
+
74
+ React.useEffect(() => {
75
+ if (!api) {
76
+ return
77
+ }
78
+
79
+ onSelect(api)
80
+ api.on("reInit", onSelect)
81
+ api.on("select", onSelect)
82
+
83
+ return () => {
84
+ api?.off("select", onSelect)
85
+ };
86
+ }, [api, onSelect])
87
+
88
+ return (
89
+ (<CarouselContext.Provider
90
+ value={{
91
+ carouselRef,
92
+ api: api,
93
+ opts,
94
+ orientation:
95
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
96
+ scrollPrev,
97
+ scrollNext,
98
+ canScrollPrev,
99
+ canScrollNext,
100
+ }}>
101
+ <div
102
+ ref={ref}
103
+ onKeyDownCapture={handleKeyDown}
104
+ className={cn("relative", className)}
105
+ role="region"
106
+ aria-roledescription="carousel"
107
+ {...props}>
108
+ {children}
109
+ </div>
110
+ </CarouselContext.Provider>)
111
+ );
112
+ })
113
+ Carousel.displayName = "Carousel"
114
+
115
+ const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
116
+ const { carouselRef, orientation } = useCarousel()
117
+
118
+ return (
119
+ (<div ref={carouselRef} className="overflow-hidden">
120
+ <div
121
+ ref={ref}
122
+ className={cn(
123
+ "flex",
124
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
125
+ className
126
+ )}
127
+ {...props} />
128
+ </div>)
129
+ );
130
+ })
131
+ CarouselContent.displayName = "CarouselContent"
132
+
133
+ const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
134
+ const { orientation } = useCarousel()
135
+
136
+ return (
137
+ (<div
138
+ ref={ref}
139
+ role="group"
140
+ aria-roledescription="slide"
141
+ className={cn(
142
+ "min-w-0 shrink-0 grow-0 basis-full",
143
+ orientation === "horizontal" ? "pl-4" : "pt-4",
144
+ className
145
+ )}
146
+ {...props} />)
147
+ );
148
+ })
149
+ CarouselItem.displayName = "CarouselItem"
150
+
151
+ const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
152
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
153
+
154
+ return (
155
+ (<Button
156
+ ref={ref}
157
+ variant={variant}
158
+ size={size}
159
+ className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
160
+ ? "-left-12 top-1/2 -translate-y-1/2"
161
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
162
+ disabled={!canScrollPrev}
163
+ onClick={scrollPrev}
164
+ {...props}>
165
+ <ArrowLeft className="h-4 w-4" />
166
+ <span className="sr-only">Previous slide</span>
167
+ </Button>)
168
+ );
169
+ })
170
+ CarouselPrevious.displayName = "CarouselPrevious"
171
+
172
+ const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
173
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
174
+
175
+ return (
176
+ (<Button
177
+ ref={ref}
178
+ variant={variant}
179
+ size={size}
180
+ className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
181
+ ? "-right-12 top-1/2 -translate-y-1/2"
182
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
183
+ disabled={!canScrollNext}
184
+ onClick={scrollNext}
185
+ {...props}>
186
+ <ArrowRight className="h-4 w-4" />
187
+ <span className="sr-only">Next slide</span>
188
+ </Button>)
189
+ );
190
+ })
191
+ CarouselNext.displayName = "CarouselNext"
192
+
193
+ export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
src/components/ui/chart.jsx ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as RechartsPrimitive from "recharts"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ // Format: { THEME_NAME: CSS_SELECTOR }
8
+ const THEMES = {
9
+ light: "",
10
+ dark: ".dark"
11
+ }
12
+
13
+ const ChartContext = React.createContext(null)
14
+
15
+ function useChart() {
16
+ const context = React.useContext(ChartContext)
17
+
18
+ if (!context) {
19
+ throw new Error("useChart must be used within a <ChartContainer />")
20
+ }
21
+
22
+ return context
23
+ }
24
+
25
+ const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
26
+ const uniqueId = React.useId()
27
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
28
+
29
+ return (
30
+ (<ChartContext.Provider value={{ config }}>
31
+ <div
32
+ data-chart={chartId}
33
+ ref={ref}
34
+ className={cn(
35
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
36
+ className
37
+ )}
38
+ {...props}>
39
+ <ChartStyle id={chartId} config={config} />
40
+ <RechartsPrimitive.ResponsiveContainer>
41
+ {children}
42
+ </RechartsPrimitive.ResponsiveContainer>
43
+ </div>
44
+ </ChartContext.Provider>)
45
+ );
46
+ })
47
+ ChartContainer.displayName = "Chart"
48
+
49
+ const ChartStyle = ({
50
+ id,
51
+ config
52
+ }) => {
53
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
54
+
55
+ if (!colorConfig.length) {
56
+ return null
57
+ }
58
+
59
+ return (
60
+ (<style
61
+ dangerouslySetInnerHTML={{
62
+ __html: Object.entries(THEMES)
63
+ .map(([theme, prefix]) => `
64
+ ${prefix} [data-chart=${id}] {
65
+ ${colorConfig
66
+ .map(([key, itemConfig]) => {
67
+ const color =
68
+ itemConfig.theme?.[theme] ||
69
+ itemConfig.color
70
+ return color ? ` --color-${key}: ${color};` : null
71
+ })
72
+ .join("\n")}
73
+ }
74
+ `)
75
+ .join("\n"),
76
+ }} />)
77
+ );
78
+ }
79
+
80
+ const ChartTooltip = RechartsPrimitive.Tooltip
81
+
82
+ const ChartTooltipContent = React.forwardRef((
83
+ {
84
+ active,
85
+ payload,
86
+ className,
87
+ indicator = "dot",
88
+ hideLabel = false,
89
+ hideIndicator = false,
90
+ label,
91
+ labelFormatter,
92
+ labelClassName,
93
+ formatter,
94
+ color,
95
+ nameKey,
96
+ labelKey,
97
+ },
98
+ ref
99
+ ) => {
100
+ const { config } = useChart()
101
+
102
+ const tooltipLabel = React.useMemo(() => {
103
+ if (hideLabel || !payload?.length) {
104
+ return null
105
+ }
106
+
107
+ const [item] = payload
108
+ const key = `${labelKey || item.dataKey || item.name || "value"}`
109
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
110
+ const value =
111
+ !labelKey && typeof label === "string"
112
+ ? config[label]?.label || label
113
+ : itemConfig?.label
114
+
115
+ if (labelFormatter) {
116
+ return (
117
+ (<div className={cn("font-medium", labelClassName)}>
118
+ {labelFormatter(value, payload)}
119
+ </div>)
120
+ );
121
+ }
122
+
123
+ if (!value) {
124
+ return null
125
+ }
126
+
127
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
128
+ }, [
129
+ label,
130
+ labelFormatter,
131
+ payload,
132
+ hideLabel,
133
+ labelClassName,
134
+ config,
135
+ labelKey,
136
+ ])
137
+
138
+ if (!active || !payload?.length) {
139
+ return null
140
+ }
141
+
142
+ const nestLabel = payload.length === 1 && indicator !== "dot"
143
+
144
+ return (
145
+ (<div
146
+ ref={ref}
147
+ className={cn(
148
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
149
+ className
150
+ )}>
151
+ {!nestLabel ? tooltipLabel : null}
152
+ <div className="grid gap-1.5">
153
+ {payload.map((item, index) => {
154
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
155
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
156
+ const indicatorColor = color || item.payload.fill || item.color
157
+
158
+ return (
159
+ (<div
160
+ key={item.dataKey}
161
+ className={cn(
162
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
163
+ indicator === "dot" && "items-center"
164
+ )}>
165
+ {formatter && item?.value !== undefined && item.name ? (
166
+ formatter(item.value, item.name, item, index, item.payload)
167
+ ) : (
168
+ <>
169
+ {itemConfig?.icon ? (
170
+ <itemConfig.icon />
171
+ ) : (
172
+ !hideIndicator && (
173
+ <div
174
+ className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
175
+ "h-2.5 w-2.5": indicator === "dot",
176
+ "w-1": indicator === "line",
177
+ "w-0 border-[1.5px] border-dashed bg-transparent":
178
+ indicator === "dashed",
179
+ "my-0.5": nestLabel && indicator === "dashed",
180
+ })}
181
+ style={
182
+ {
183
+ "--color-bg": indicatorColor,
184
+ "--color-border": indicatorColor
185
+ }
186
+ } />
187
+ )
188
+ )}
189
+ <div
190
+ className={cn(
191
+ "flex flex-1 justify-between leading-none",
192
+ nestLabel ? "items-end" : "items-center"
193
+ )}>
194
+ <div className="grid gap-1.5">
195
+ {nestLabel ? tooltipLabel : null}
196
+ <span className="text-muted-foreground">
197
+ {itemConfig?.label || item.name}
198
+ </span>
199
+ </div>
200
+ {item.value && (
201
+ <span className="font-mono font-medium tabular-nums text-foreground">
202
+ {item.value.toLocaleString()}
203
+ </span>
204
+ )}
205
+ </div>
206
+ </>
207
+ )}
208
+ </div>)
209
+ );
210
+ })}
211
+ </div>
212
+ </div>)
213
+ );
214
+ })
215
+ ChartTooltipContent.displayName = "ChartTooltip"
216
+
217
+ const ChartLegend = RechartsPrimitive.Legend
218
+
219
+ const ChartLegendContent = React.forwardRef((
220
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
221
+ ref
222
+ ) => {
223
+ const { config } = useChart()
224
+
225
+ if (!payload?.length) {
226
+ return null
227
+ }
228
+
229
+ return (
230
+ (<div
231
+ ref={ref}
232
+ className={cn(
233
+ "flex items-center justify-center gap-4",
234
+ verticalAlign === "top" ? "pb-3" : "pt-3",
235
+ className
236
+ )}>
237
+ {payload.map((item) => {
238
+ const key = `${nameKey || item.dataKey || "value"}`
239
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
240
+
241
+ return (
242
+ (<div
243
+ key={item.value}
244
+ className={cn(
245
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
246
+ )}>
247
+ {itemConfig?.icon && !hideIcon ? (
248
+ <itemConfig.icon />
249
+ ) : (
250
+ <div
251
+ className="h-2 w-2 shrink-0 rounded-[2px]"
252
+ style={{
253
+ backgroundColor: item.color,
254
+ }} />
255
+ )}
256
+ {itemConfig?.label}
257
+ </div>)
258
+ );
259
+ })}
260
+ </div>)
261
+ );
262
+ })
263
+ ChartLegendContent.displayName = "ChartLegend"
264
+
265
+ // Helper to extract item config from a payload.
266
+ function getPayloadConfigFromPayload(
267
+ config,
268
+ payload,
269
+ key
270
+ ) {
271
+ if (typeof payload !== "object" || payload === null) {
272
+ return undefined
273
+ }
274
+
275
+ const payloadPayload =
276
+ "payload" in payload &&
277
+ typeof payload.payload === "object" &&
278
+ payload.payload !== null
279
+ ? payload.payload
280
+ : undefined
281
+
282
+ let configLabelKey = key
283
+
284
+ if (
285
+ key in payload &&
286
+ typeof payload[key] === "string"
287
+ ) {
288
+ configLabelKey = payload[key]
289
+ } else if (
290
+ payloadPayload &&
291
+ key in payloadPayload &&
292
+ typeof payloadPayload[key] === "string"
293
+ ) {
294
+ configLabelKey = payloadPayload[key]
295
+ }
296
+
297
+ return configLabelKey in config
298
+ ? config[configLabelKey]
299
+ : config[key];
300
+ }
301
+
302
+ export {
303
+ ChartContainer,
304
+ ChartTooltip,
305
+ ChartTooltipContent,
306
+ ChartLegend,
307
+ ChartLegendContent,
308
+ ChartStyle,
309
+ }
src/components/ui/checkbox.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
8
+ <CheckboxPrimitive.Root
9
+ ref={ref}
10
+ className={cn(
11
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
12
+ className
13
+ )}
14
+ {...props}>
15
+ <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
16
+ <Check className="h-4 w-4" />
17
+ </CheckboxPrimitive.Indicator>
18
+ </CheckboxPrimitive.Root>
19
+ ))
20
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
21
+
22
+ export { Checkbox }
src/components/ui/collapsible.jsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/command.jsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Command as CommandPrimitive } from "cmdk"
3
+ import { Search } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
7
+
8
+ const Command = React.forwardRef(({ className, ...props }, ref) => (
9
+ <CommandPrimitive
10
+ ref={ref}
11
+ className={cn(
12
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
13
+ className
14
+ )}
15
+ {...props} />
16
+ ))
17
+ Command.displayName = CommandPrimitive.displayName
18
+
19
+ const CommandDialog = ({
20
+ children,
21
+ ...props
22
+ }) => {
23
+ return (
24
+ (<Dialog {...props}>
25
+ <DialogContent className="overflow-hidden p-0">
26
+ <Command
27
+ className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
28
+ {children}
29
+ </Command>
30
+ </DialogContent>
31
+ </Dialog>)
32
+ );
33
+ }
34
+
35
+ const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
36
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
37
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
38
+ <CommandPrimitive.Input
39
+ ref={ref}
40
+ className={cn(
41
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
42
+ className
43
+ )}
44
+ {...props} />
45
+ </div>
46
+ ))
47
+
48
+ CommandInput.displayName = CommandPrimitive.Input.displayName
49
+
50
+ const CommandList = React.forwardRef(({ className, ...props }, ref) => (
51
+ <CommandPrimitive.List
52
+ ref={ref}
53
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
54
+ {...props} />
55
+ ))
56
+
57
+ CommandList.displayName = CommandPrimitive.List.displayName
58
+
59
+ const CommandEmpty = React.forwardRef((props, ref) => (
60
+ <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
61
+ ))
62
+
63
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
64
+
65
+ const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (
66
+ <CommandPrimitive.Group
67
+ ref={ref}
68
+ className={cn(
69
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
70
+ className
71
+ )}
72
+ {...props} />
73
+ ))
74
+
75
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
76
+
77
+ const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (
78
+ <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
79
+ ))
80
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
81
+
82
+ const CommandItem = React.forwardRef(({ className, ...props }, ref) => (
83
+ <CommandPrimitive.Item
84
+ ref={ref}
85
+ className={cn(
86
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
87
+ className
88
+ )}
89
+ {...props} />
90
+ ))
91
+
92
+ CommandItem.displayName = CommandPrimitive.Item.displayName
93
+
94
+ const CommandShortcut = ({
95
+ className,
96
+ ...props
97
+ }) => {
98
+ return (
99
+ (<span
100
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
101
+ {...props} />)
102
+ );
103
+ }
104
+ CommandShortcut.displayName = "CommandShortcut"
105
+
106
+ export {
107
+ Command,
108
+ CommandDialog,
109
+ CommandInput,
110
+ CommandList,
111
+ CommandEmpty,
112
+ CommandGroup,
113
+ CommandItem,
114
+ CommandShortcut,
115
+ CommandSeparator,
116
+ }
src/components/ui/context-menu.jsx ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
20
+ <ContextMenuPrimitive.SubTrigger
21
+ ref={ref}
22
+ className={cn(
23
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
24
+ inset && "pl-8",
25
+ className
26
+ )}
27
+ {...props}>
28
+ {children}
29
+ <ChevronRight className="ml-auto h-4 w-4" />
30
+ </ContextMenuPrimitive.SubTrigger>
31
+ ))
32
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
33
+
34
+ const ContextMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
35
+ <ContextMenuPrimitive.SubContent
36
+ ref={ref}
37
+ className={cn(
38
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
39
+ className
40
+ )}
41
+ {...props} />
42
+ ))
43
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
44
+
45
+ const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
46
+ <ContextMenuPrimitive.Portal>
47
+ <ContextMenuPrimitive.Content
48
+ ref={ref}
49
+ className={cn(
50
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ className
52
+ )}
53
+ {...props} />
54
+ </ContextMenuPrimitive.Portal>
55
+ ))
56
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
57
+
58
+ const ContextMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Item
60
+ ref={ref}
61
+ className={cn(
62
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
63
+ inset && "pl-8",
64
+ className
65
+ )}
66
+ {...props} />
67
+ ))
68
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
69
+
70
+ const ContextMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
71
+ <ContextMenuPrimitive.CheckboxItem
72
+ ref={ref}
73
+ className={cn(
74
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
75
+ className
76
+ )}
77
+ checked={checked}
78
+ {...props}>
79
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
80
+ <ContextMenuPrimitive.ItemIndicator>
81
+ <Check className="h-4 w-4" />
82
+ </ContextMenuPrimitive.ItemIndicator>
83
+ </span>
84
+ {children}
85
+ </ContextMenuPrimitive.CheckboxItem>
86
+ ))
87
+ ContextMenuCheckboxItem.displayName =
88
+ ContextMenuPrimitive.CheckboxItem.displayName
89
+
90
+ const ContextMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
91
+ <ContextMenuPrimitive.RadioItem
92
+ ref={ref}
93
+ className={cn(
94
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
95
+ className
96
+ )}
97
+ {...props}>
98
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
99
+ <ContextMenuPrimitive.ItemIndicator>
100
+ <Circle className="h-4 w-4 fill-current" />
101
+ </ContextMenuPrimitive.ItemIndicator>
102
+ </span>
103
+ {children}
104
+ </ContextMenuPrimitive.RadioItem>
105
+ ))
106
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
107
+
108
+ const ContextMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
109
+ <ContextMenuPrimitive.Label
110
+ ref={ref}
111
+ className={cn(
112
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
113
+ inset && "pl-8",
114
+ className
115
+ )}
116
+ {...props} />
117
+ ))
118
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
119
+
120
+ const ContextMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
121
+ <ContextMenuPrimitive.Separator
122
+ ref={ref}
123
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
124
+ {...props} />
125
+ ))
126
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
127
+
128
+ const ContextMenuShortcut = ({
129
+ className,
130
+ ...props
131
+ }) => {
132
+ return (
133
+ (<span
134
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
135
+ {...props} />)
136
+ );
137
+ }
138
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
139
+
140
+ export {
141
+ ContextMenu,
142
+ ContextMenuTrigger,
143
+ ContextMenuContent,
144
+ ContextMenuItem,
145
+ ContextMenuCheckboxItem,
146
+ ContextMenuRadioItem,
147
+ ContextMenuLabel,
148
+ ContextMenuSeparator,
149
+ ContextMenuShortcut,
150
+ ContextMenuGroup,
151
+ ContextMenuPortal,
152
+ ContextMenuSub,
153
+ ContextMenuSubContent,
154
+ ContextMenuSubTrigger,
155
+ ContextMenuRadioGroup,
156
+ }
src/components/ui/dialog.jsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
18
+ <DialogPrimitive.Overlay
19
+ ref={ref}
20
+ className={cn(
21
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className
23
+ )}
24
+ {...props} />
25
+ ))
26
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
27
+
28
+ const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
29
+ <DialogPortal>
30
+ <DialogOverlay />
31
+ <DialogPrimitive.Content
32
+ ref={ref}
33
+ className={cn(
34
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
35
+ className
36
+ )}
37
+ {...props}>
38
+ {children}
39
+ <DialogPrimitive.Close
40
+ className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
41
+ <X className="h-4 w-4" />
42
+ <span className="sr-only">Close</span>
43
+ </DialogPrimitive.Close>
44
+ </DialogPrimitive.Content>
45
+ </DialogPortal>
46
+ ))
47
+ DialogContent.displayName = DialogPrimitive.Content.displayName
48
+
49
+ const DialogHeader = ({
50
+ className,
51
+ ...props
52
+ }) => (
53
+ <div
54
+ className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
55
+ {...props} />
56
+ )
57
+ DialogHeader.displayName = "DialogHeader"
58
+
59
+ const DialogFooter = ({
60
+ className,
61
+ ...props
62
+ }) => (
63
+ <div
64
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
65
+ {...props} />
66
+ )
67
+ DialogFooter.displayName = "DialogFooter"
68
+
69
+ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
70
+ <DialogPrimitive.Title
71
+ ref={ref}
72
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
73
+ {...props} />
74
+ ))
75
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
76
+
77
+ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
78
+ <DialogPrimitive.Description
79
+ ref={ref}
80
+ className={cn("text-sm text-muted-foreground", className)}
81
+ {...props} />
82
+ ))
83
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
84
+
85
+ export {
86
+ Dialog,
87
+ DialogPortal,
88
+ DialogOverlay,
89
+ DialogTrigger,
90
+ DialogClose,
91
+ DialogContent,
92
+ DialogHeader,
93
+ DialogFooter,
94
+ DialogTitle,
95
+ DialogDescription,
96
+ }
src/components/ui/drawer.jsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Drawer as DrawerPrimitive } from "vaul"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Drawer = ({
9
+ shouldScaleBackground = true,
10
+ ...props
11
+ }) => (
12
+ <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
13
+ )
14
+ Drawer.displayName = "Drawer"
15
+
16
+ const DrawerTrigger = DrawerPrimitive.Trigger
17
+
18
+ const DrawerPortal = DrawerPrimitive.Portal
19
+
20
+ const DrawerClose = DrawerPrimitive.Close
21
+
22
+ const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
23
+ <DrawerPrimitive.Overlay
24
+ ref={ref}
25
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
26
+ {...props} />
27
+ ))
28
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
29
+
30
+ const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
31
+ <DrawerPortal>
32
+ <DrawerOverlay />
33
+ <DrawerPrimitive.Content
34
+ ref={ref}
35
+ className={cn(
36
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
37
+ className
38
+ )}
39
+ {...props}>
40
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
41
+ {children}
42
+ </DrawerPrimitive.Content>
43
+ </DrawerPortal>
44
+ ))
45
+ DrawerContent.displayName = "DrawerContent"
46
+
47
+ const DrawerHeader = ({
48
+ className,
49
+ ...props
50
+ }) => (
51
+ <div
52
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
53
+ {...props} />
54
+ )
55
+ DrawerHeader.displayName = "DrawerHeader"
56
+
57
+ const DrawerFooter = ({
58
+ className,
59
+ ...props
60
+ }) => (
61
+ <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
62
+ )
63
+ DrawerFooter.displayName = "DrawerFooter"
64
+
65
+ const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
66
+ <DrawerPrimitive.Title
67
+ ref={ref}
68
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
69
+ {...props} />
70
+ ))
71
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName
72
+
73
+ const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
74
+ <DrawerPrimitive.Description
75
+ ref={ref}
76
+ className={cn("text-sm text-muted-foreground", className)}
77
+ {...props} />
78
+ ))
79
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName
80
+
81
+ export {
82
+ Drawer,
83
+ DrawerPortal,
84
+ DrawerOverlay,
85
+ DrawerTrigger,
86
+ DrawerClose,
87
+ DrawerContent,
88
+ DrawerHeader,
89
+ DrawerFooter,
90
+ DrawerTitle,
91
+ DrawerDescription,
92
+ }
src/components/ui/dropdown-menu.jsx ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root
8
+
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10
+
11
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
12
+
13
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14
+
15
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
16
+
17
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18
+
19
+ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
20
+ <DropdownMenuPrimitive.SubTrigger
21
+ ref={ref}
22
+ className={cn(
23
+ "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
24
+ inset && "pl-8",
25
+ className
26
+ )}
27
+ {...props}>
28
+ {children}
29
+ <ChevronRight className="ml-auto" />
30
+ </DropdownMenuPrimitive.SubTrigger>
31
+ ))
32
+ DropdownMenuSubTrigger.displayName =
33
+ DropdownMenuPrimitive.SubTrigger.displayName
34
+
35
+ const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
36
+ <DropdownMenuPrimitive.SubContent
37
+ ref={ref}
38
+ className={cn(
39
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
40
+ className
41
+ )}
42
+ {...props} />
43
+ ))
44
+ DropdownMenuSubContent.displayName =
45
+ DropdownMenuPrimitive.SubContent.displayName
46
+
47
+ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
48
+ <DropdownMenuPrimitive.Portal>
49
+ <DropdownMenuPrimitive.Content
50
+ ref={ref}
51
+ sideOffset={sideOffset}
52
+ className={cn(
53
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
54
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
55
+ className
56
+ )}
57
+ {...props} />
58
+ </DropdownMenuPrimitive.Portal>
59
+ ))
60
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
61
+
62
+ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
63
+ <DropdownMenuPrimitive.Item
64
+ ref={ref}
65
+ className={cn(
66
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
67
+ inset && "pl-8",
68
+ className
69
+ )}
70
+ {...props} />
71
+ ))
72
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
73
+
74
+ const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
75
+ <DropdownMenuPrimitive.CheckboxItem
76
+ ref={ref}
77
+ className={cn(
78
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
79
+ className
80
+ )}
81
+ checked={checked}
82
+ {...props}>
83
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
84
+ <DropdownMenuPrimitive.ItemIndicator>
85
+ <Check className="h-4 w-4" />
86
+ </DropdownMenuPrimitive.ItemIndicator>
87
+ </span>
88
+ {children}
89
+ </DropdownMenuPrimitive.CheckboxItem>
90
+ ))
91
+ DropdownMenuCheckboxItem.displayName =
92
+ DropdownMenuPrimitive.CheckboxItem.displayName
93
+
94
+ const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
95
+ <DropdownMenuPrimitive.RadioItem
96
+ ref={ref}
97
+ className={cn(
98
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
99
+ className
100
+ )}
101
+ {...props}>
102
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
103
+ <DropdownMenuPrimitive.ItemIndicator>
104
+ <Circle className="h-2 w-2 fill-current" />
105
+ </DropdownMenuPrimitive.ItemIndicator>
106
+ </span>
107
+ {children}
108
+ </DropdownMenuPrimitive.RadioItem>
109
+ ))
110
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
111
+
112
+ const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
113
+ <DropdownMenuPrimitive.Label
114
+ ref={ref}
115
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
116
+ {...props} />
117
+ ))
118
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
119
+
120
+ const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.Separator
122
+ ref={ref}
123
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
124
+ {...props} />
125
+ ))
126
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
127
+
128
+ const DropdownMenuShortcut = ({
129
+ className,
130
+ ...props
131
+ }) => {
132
+ return (
133
+ (<span
134
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
135
+ {...props} />)
136
+ );
137
+ }
138
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
139
+
140
+ export {
141
+ DropdownMenu,
142
+ DropdownMenuTrigger,
143
+ DropdownMenuContent,
144
+ DropdownMenuItem,
145
+ DropdownMenuCheckboxItem,
146
+ DropdownMenuRadioItem,
147
+ DropdownMenuLabel,
148
+ DropdownMenuSeparator,
149
+ DropdownMenuShortcut,
150
+ DropdownMenuGroup,
151
+ DropdownMenuPortal,
152
+ DropdownMenuSub,
153
+ DropdownMenuSubContent,
154
+ DropdownMenuSubTrigger,
155
+ DropdownMenuRadioGroup,
156
+ }
src/components/ui/form.jsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import * as React from "react"
3
+ import { Slot } from "@radix-ui/react-slot"
4
+ import { Controller, FormProvider, useFormContext } from "react-hook-form";
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Label } from "@/components/ui/label"
8
+
9
+ const Form = FormProvider
10
+
11
+ const FormFieldContext = React.createContext({})
12
+
13
+ const FormField = (
14
+ {
15
+ ...props
16
+ }
17
+ ) => {
18
+ return (
19
+ (<FormFieldContext.Provider value={{ name: props.name }}>
20
+ <Controller {...props} />
21
+ </FormFieldContext.Provider>)
22
+ );
23
+ }
24
+
25
+ const useFormField = () => {
26
+ const fieldContext = React.useContext(FormFieldContext)
27
+ const itemContext = React.useContext(FormItemContext)
28
+ const { getFieldState, formState } = useFormContext()
29
+
30
+ const fieldState = getFieldState(fieldContext.name, formState)
31
+
32
+ if (!fieldContext) {
33
+ throw new Error("useFormField should be used within <FormField>")
34
+ }
35
+
36
+ const { id } = itemContext
37
+
38
+ return {
39
+ id,
40
+ name: fieldContext.name,
41
+ formItemId: `${id}-form-item`,
42
+ formDescriptionId: `${id}-form-item-description`,
43
+ formMessageId: `${id}-form-item-message`,
44
+ ...fieldState,
45
+ }
46
+ }
47
+
48
+ const FormItemContext = React.createContext({})
49
+
50
+ const FormItem = React.forwardRef(({ className, ...props }, ref) => {
51
+ const id = React.useId()
52
+
53
+ return (
54
+ (<FormItemContext.Provider value={{ id }}>
55
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
56
+ </FormItemContext.Provider>)
57
+ );
58
+ })
59
+ FormItem.displayName = "FormItem"
60
+
61
+ const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
62
+ const { error, formItemId } = useFormField()
63
+
64
+ return (
65
+ (<Label
66
+ ref={ref}
67
+ className={cn(error && "text-destructive", className)}
68
+ htmlFor={formItemId}
69
+ {...props} />)
70
+ );
71
+ })
72
+ FormLabel.displayName = "FormLabel"
73
+
74
+ const FormControl = React.forwardRef(({ ...props }, ref) => {
75
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
76
+
77
+ return (
78
+ (<Slot
79
+ ref={ref}
80
+ id={formItemId}
81
+ aria-describedby={
82
+ !error
83
+ ? `${formDescriptionId}`
84
+ : `${formDescriptionId} ${formMessageId}`
85
+ }
86
+ aria-invalid={!!error}
87
+ {...props} />)
88
+ );
89
+ })
90
+ FormControl.displayName = "FormControl"
91
+
92
+ const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
93
+ const { formDescriptionId } = useFormField()
94
+
95
+ return (
96
+ (<p
97
+ ref={ref}
98
+ id={formDescriptionId}
99
+ className={cn("text-[0.8rem] text-muted-foreground", className)}
100
+ {...props} />)
101
+ );
102
+ })
103
+ FormDescription.displayName = "FormDescription"
104
+
105
+ const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
106
+ const { error, formMessageId } = useFormField()
107
+ const body = error ? String(error?.message) : children
108
+
109
+ if (!body) {
110
+ return null
111
+ }
112
+
113
+ return (
114
+ (<p
115
+ ref={ref}
116
+ id={formMessageId}
117
+ className={cn("text-[0.8rem] font-medium text-destructive", className)}
118
+ {...props}>
119
+ {body}
120
+ </p>)
121
+ );
122
+ })
123
+ FormMessage.displayName = "FormMessage"
124
+
125
+ export {
126
+ useFormField,
127
+ Form,
128
+ FormItem,
129
+ FormLabel,
130
+ FormControl,
131
+ FormDescription,
132
+ FormMessage,
133
+ FormField,
134
+ }
src/components/ui/hover-card.jsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const HoverCard = HoverCardPrimitive.Root
9
+
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger
11
+
12
+ const HoverCardContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
13
+ <HoverCardPrimitive.Content
14
+ ref={ref}
15
+ align={align}
16
+ sideOffset={sideOffset}
17
+ className={cn(
18
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
19
+ className
20
+ )}
21
+ {...props} />
22
+ ))
23
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
24
+
25
+ export { HoverCard, HoverCardTrigger, HoverCardContent }
src/components/ui/input-otp.jsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { OTPInput, OTPInputContext } from "input-otp"
3
+ import { Minus } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
8
+ <OTPInput
9
+ ref={ref}
10
+ containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
11
+ className={cn("disabled:cursor-not-allowed", className)}
12
+ {...props} />
13
+ ))
14
+ InputOTP.displayName = "InputOTP"
15
+
16
+ const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
17
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
18
+ ))
19
+ InputOTPGroup.displayName = "InputOTPGroup"
20
+
21
+ const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
22
+ const inputOTPContext = React.useContext(OTPInputContext)
23
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
24
+
25
+ return (
26
+ (<div
27
+ ref={ref}
28
+ className={cn(
29
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
30
+ isActive && "z-10 ring-1 ring-ring",
31
+ className
32
+ )}
33
+ {...props}>
34
+ {char}
35
+ {hasFakeCaret && (
36
+ <div
37
+ className="pointer-events-none absolute inset-0 flex items-center justify-center">
38
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
39
+ </div>
40
+ )}
41
+ </div>)
42
+ );
43
+ })
44
+ InputOTPSlot.displayName = "InputOTPSlot"
45
+
46
+ const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
47
+ <div ref={ref} role="separator" {...props}>
48
+ <Minus />
49
+ </div>
50
+ ))
51
+ InputOTPSeparator.displayName = "InputOTPSeparator"
52
+
53
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }