Upload 96 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +2 -0
- .gitignore +30 -0
- Face-Expresion.html +207 -0
- Lab-Selection.html +1227 -0
- Object-Detection.html +426 -0
- Pose.html +380 -0
- components.json +21 -0
- eslint.config.js +60 -0
- index.html +13 -0
- jsconfig.json +21 -0
- live-feed-object-detection.html +225 -0
- login.html +188 -0
- movement-detection.html +187 -0
- package-lock.json +0 -0
- package.json +100 -0
- postcss.config.js +6 -0
- src/App.jsx +88 -0
- src/Layout.jsx +127 -0
- src/api/base44Client.js +14 -0
- src/components/UserNotRegisteredError.jsx +31 -0
- src/components/lessons/AnimatedDiagram.jsx +215 -0
- src/components/lessons/Chapter1Slides.jsx +280 -0
- src/components/lessons/Chapter2Slides.jsx +396 -0
- src/components/lessons/Chapter3Slides.jsx +306 -0
- src/components/lessons/ClickReveal.jsx +51 -0
- src/components/lessons/KeyTermBadge.jsx +41 -0
- src/components/lessons/SlideContainer.jsx +21 -0
- src/components/lessons/SlideNavigation.jsx +75 -0
- src/components/ui/accordion.jsx +41 -0
- src/components/ui/alert-dialog.jsx +97 -0
- src/components/ui/alert.jsx +47 -0
- src/components/ui/aspect-ratio.jsx +5 -0
- src/components/ui/avatar.jsx +35 -0
- src/components/ui/badge.jsx +34 -0
- src/components/ui/breadcrumb.jsx +92 -0
- src/components/ui/button.jsx +48 -0
- src/components/ui/calendar.jsx +71 -0
- src/components/ui/card.jsx +50 -0
- src/components/ui/carousel.jsx +193 -0
- src/components/ui/chart.jsx +309 -0
- src/components/ui/checkbox.jsx +22 -0
- src/components/ui/collapsible.jsx +11 -0
- src/components/ui/command.jsx +116 -0
- src/components/ui/context-menu.jsx +156 -0
- src/components/ui/dialog.jsx +96 -0
- src/components/ui/drawer.jsx +92 -0
- src/components/ui/dropdown-menu.jsx +156 -0
- src/components/ui/form.jsx +134 -0
- src/components/ui/hover-card.jsx +25 -0
- 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 }
|