Spaces:
Sleeping
Sleeping
Antigravity AI commited on
Commit ·
8ceb96d
1
Parent(s): f178ad2
UI improvements and HUD refinements for deployment
Browse files- frontend/app/FaceVisionApp.tsx +168 -108
- frontend/app/components/EmotionDisplay.tsx +42 -9
- frontend/app/components/StatsPanel.tsx +64 -42
- frontend/app/globals.css +61 -56
frontend/app/FaceVisionApp.tsx
CHANGED
|
@@ -58,69 +58,75 @@ export default function FaceVisionApp() {
|
|
| 58 |
return (
|
| 59 |
<div className="min-h-screen animated-bg flex flex-col">
|
| 60 |
{/* Header */}
|
| 61 |
-
<header className="glass border-b border-white/5 px-
|
| 62 |
-
<div className="flex items-center gap-
|
| 63 |
-
<div className="
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
</div>
|
| 66 |
<div>
|
| 67 |
<h1
|
| 68 |
-
className="text-
|
| 69 |
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 70 |
>
|
| 71 |
FaceVision AI
|
| 72 |
</h1>
|
| 73 |
-
<
|
|
|
|
|
|
|
|
|
|
| 74 |
</div>
|
| 75 |
</div>
|
| 76 |
|
| 77 |
-
<div className="flex items-center gap-
|
| 78 |
{/* Model loading status */}
|
| 79 |
-
<div className="flex items-center gap-2 glass rounded-
|
| 80 |
{loadError ? (
|
| 81 |
<>
|
| 82 |
-
<div className="w-
|
| 83 |
-
<span className="text-
|
| 84 |
</>
|
| 85 |
) : isLoaded ? (
|
| 86 |
<>
|
| 87 |
-
<div className="w-
|
| 88 |
-
<span className="text-
|
| 89 |
</>
|
| 90 |
) : (
|
| 91 |
<>
|
| 92 |
-
<div className="w-
|
| 93 |
-
<span className="text-
|
| 94 |
</>
|
| 95 |
)}
|
| 96 |
</div>
|
| 97 |
|
| 98 |
{/* Backend status */}
|
| 99 |
{logError ? (
|
| 100 |
-
<div className="flex items-center gap-2 glass rounded-
|
| 101 |
-
<div className="w-
|
| 102 |
-
<span className="text-
|
| 103 |
</div>
|
| 104 |
) : lastLog ? (
|
| 105 |
-
<div className="flex items-center gap-2 glass rounded-
|
| 106 |
-
<div className="w-
|
| 107 |
-
<span className="text-
|
| 108 |
</div>
|
| 109 |
) : null}
|
| 110 |
</div>
|
| 111 |
</header>
|
| 112 |
|
| 113 |
{/* Main layout */}
|
| 114 |
-
<main className="flex-1 flex flex-col lg:flex-row gap-
|
| 115 |
{/* Left sidebar */}
|
| 116 |
-
<aside className="w-full lg:w-
|
| 117 |
{/* Emotion display */}
|
| 118 |
-
<div className="
|
| 119 |
<h2
|
| 120 |
-
className="text-
|
| 121 |
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 122 |
>
|
| 123 |
-
Current
|
| 124 |
</h2>
|
| 125 |
<EmotionDisplay emotion={currentEmotion} faceDetected={stats.faceDetected} />
|
| 126 |
</div>
|
|
@@ -134,32 +140,49 @@ export default function FaceVisionApp() {
|
|
| 134 |
|
| 135 |
{/* Last log entry */}
|
| 136 |
{lastLog && (
|
| 137 |
-
<div className="glass rounded-2xl p-
|
| 138 |
-
<
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
</
|
| 143 |
-
<p className="text-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</div>
|
| 147 |
)}
|
| 148 |
</aside>
|
| 149 |
|
| 150 |
{/* Video Feed */}
|
| 151 |
-
<section className="flex-1 flex flex-col gap-
|
| 152 |
-
<div className="relative flex-1 rounded-
|
| 153 |
{/* Camera error state */}
|
| 154 |
{cameraError ? (
|
| 155 |
-
<div className="absolute inset-0 flex flex-col items-center justify-center gap-
|
| 156 |
-
<div className="text-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
<button
|
| 159 |
onClick={() => window.location.reload()}
|
| 160 |
-
className="px-
|
| 161 |
>
|
| 162 |
-
|
| 163 |
</button>
|
| 164 |
</div>
|
| 165 |
) : (
|
|
@@ -172,113 +195,150 @@ export default function FaceVisionApp() {
|
|
| 172 |
onUserMedia={handleUserMedia}
|
| 173 |
onUserMediaError={handleUserMediaError}
|
| 174 |
videoConstraints={{
|
| 175 |
-
width: { ideal:
|
| 176 |
-
height: { ideal:
|
| 177 |
facingMode: "user",
|
| 178 |
-
frameRate: { ideal:
|
| 179 |
}}
|
| 180 |
-
className="w-full h-full object-cover"
|
| 181 |
style={{ display: isCameraReady ? "block" : "none" }}
|
| 182 |
/>
|
| 183 |
|
| 184 |
{/* Loading skeleton */}
|
| 185 |
{!isCameraReady && (
|
| 186 |
-
<div className="absolute inset-0 flex flex-col items-center justify-center gap-
|
| 187 |
-
<div className="
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
</div>
|
| 190 |
)}
|
| 191 |
|
| 192 |
-
{/*
|
| 193 |
-
{isCameraReady && isTracking && (
|
| 194 |
-
<div
|
| 195 |
-
className="absolute inset-0 pointer-events-none rounded-3xl transition-all duration-500"
|
| 196 |
-
style={{
|
| 197 |
-
boxShadow: `inset 0 0 60px ${glowColor}20, 0 0 40px ${glowColor}15`,
|
| 198 |
-
border: `1px solid ${glowColor}30`,
|
| 199 |
-
}}
|
| 200 |
-
/>
|
| 201 |
-
)}
|
| 202 |
-
|
| 203 |
-
{/* Corner brackets overlay */}
|
| 204 |
{isCameraReady && isTracking && (
|
| 205 |
<>
|
| 206 |
-
|
| 207 |
-
<div className="absolute top-4 right-4 w-8 h-8 border-t-2 border-r-2 border-blue-400/50 rounded-tr-lg" />
|
| 208 |
-
<div className="absolute bottom-4 left-4 w-8 h-8 border-b-2 border-l-2 border-blue-400/50 rounded-bl-lg" />
|
| 209 |
-
<div className="absolute bottom-4 right-4 w-8 h-8 border-b-2 border-r-2 border-blue-400/50 rounded-br-lg" />
|
| 210 |
-
</>
|
| 211 |
-
)}
|
| 212 |
-
|
| 213 |
-
{/* Emotion badge overlay */}
|
| 214 |
-
{isCameraReady && isTracking && stats.faceDetected && (
|
| 215 |
-
<div
|
| 216 |
-
key={currentEmotion}
|
| 217 |
-
className="absolute top-5 left-1/2 -translate-x-1/2 animate-emotion"
|
| 218 |
-
>
|
| 219 |
<div
|
| 220 |
-
className="
|
| 221 |
style={{
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
boxShadow: `0 0 20px ${glowColor}20`,
|
| 225 |
}}
|
| 226 |
-
>
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
| 233 |
</div>
|
| 234 |
-
</div>
|
| 235 |
-
)}
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
)}
|
| 244 |
</>
|
| 245 |
)}
|
| 246 |
</div>
|
| 247 |
|
| 248 |
{/* Controls */}
|
| 249 |
-
<div className="glass rounded-
|
| 250 |
-
<div className="
|
| 251 |
-
{
|
| 252 |
-
?
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
</div>
|
| 259 |
|
| 260 |
<button
|
| 261 |
id="tracking-toggle-btn"
|
| 262 |
onClick={() => setIsTracking((v) => !v)}
|
| 263 |
disabled={!isCameraReady || !isLoaded || !!loadError}
|
| 264 |
-
className={`px-
|
| 265 |
isTracking
|
| 266 |
-
? "bg-red-500/
|
| 267 |
-
: "bg-
|
| 268 |
}`}
|
| 269 |
>
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
</button>
|
| 272 |
</div>
|
| 273 |
</section>
|
| 274 |
</main>
|
| 275 |
|
| 276 |
{/* Footer */}
|
| 277 |
-
<footer className="glass border-t border-white/5 py-
|
| 278 |
-
<p className="text-
|
| 279 |
-
|
| 280 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
</footer>
|
|
|
|
| 282 |
</div>
|
| 283 |
);
|
| 284 |
}
|
|
|
|
| 58 |
return (
|
| 59 |
<div className="min-h-screen animated-bg flex flex-col">
|
| 60 |
{/* Header */}
|
| 61 |
+
<header className="glass border-b border-white/5 px-8 py-5 flex items-center justify-between sticky top-0 z-50">
|
| 62 |
+
<div className="flex items-center gap-4">
|
| 63 |
+
<div className="relative group">
|
| 64 |
+
<div className="absolute inset-0 bg-blue-500 blur-lg opacity-20 group-hover:opacity-40 transition-opacity" />
|
| 65 |
+
<div className="relative w-11 h-11 rounded-2xl bg-gradient-to-br from-blue-500 via-indigo-500 to-purple-600 flex items-center justify-center text-xl shadow-2xl transform group-hover:scale-105 transition-transform duration-300">
|
| 66 |
+
🧠
|
| 67 |
+
</div>
|
| 68 |
</div>
|
| 69 |
<div>
|
| 70 |
<h1
|
| 71 |
+
className="text-xl font-black gradient-text tracking-tighter"
|
| 72 |
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 73 |
>
|
| 74 |
FaceVision AI
|
| 75 |
</h1>
|
| 76 |
+
<div className="flex items-center gap-2">
|
| 77 |
+
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
|
| 78 |
+
<p className="text-[10px] text-white/30 font-bold uppercase tracking-[0.2em]">Neural Engine v1.0</p>
|
| 79 |
+
</div>
|
| 80 |
</div>
|
| 81 |
</div>
|
| 82 |
|
| 83 |
+
<div className="flex items-center gap-4">
|
| 84 |
{/* Model loading status */}
|
| 85 |
+
<div className="flex items-center gap-2.5 glass-strong rounded-xl px-4 py-2 border border-white/5">
|
| 86 |
{loadError ? (
|
| 87 |
<>
|
| 88 |
+
<div className="w-1.5 h-1.5 rounded-full bg-red-400 shadow-[0_0_8px_rgba(248,113,113,0.6)]" />
|
| 89 |
+
<span className="text-[10px] font-bold text-red-400 uppercase tracking-widest">Engine Error</span>
|
| 90 |
</>
|
| 91 |
) : isLoaded ? (
|
| 92 |
<>
|
| 93 |
+
<div className="w-1.5 h-1.5 rounded-full bg-green-400 shadow-[0_0_8px_rgba(74,222,128,0.6)]" />
|
| 94 |
+
<span className="text-[10px] font-bold text-green-400 uppercase tracking-widest">Engine Ready</span>
|
| 95 |
</>
|
| 96 |
) : (
|
| 97 |
<>
|
| 98 |
+
<div className="w-1.5 h-1.5 rounded-full bg-yellow-400 animate-blink shadow-[0_0_8px_rgba(250,204,21,0.6)]" />
|
| 99 |
+
<span className="text-[10px] font-bold text-yellow-400 uppercase tracking-widest">Initializing…</span>
|
| 100 |
</>
|
| 101 |
)}
|
| 102 |
</div>
|
| 103 |
|
| 104 |
{/* Backend status */}
|
| 105 |
{logError ? (
|
| 106 |
+
<div className="flex items-center gap-2.5 glass-strong rounded-xl px-4 py-2 border border-white/5">
|
| 107 |
+
<div className="w-1.5 h-1.5 rounded-full bg-orange-400/50" />
|
| 108 |
+
<span className="text-[10px] font-bold text-orange-400 uppercase tracking-widest">Offline</span>
|
| 109 |
</div>
|
| 110 |
) : lastLog ? (
|
| 111 |
+
<div className="flex items-center gap-2.5 glass-strong rounded-xl px-4 py-2 border border-white/5 animate-pulse-glow">
|
| 112 |
+
<div className="w-1.5 h-1.5 rounded-full bg-blue-400 shadow-[0_0_8px_rgba(96,165,250,0.6)]" />
|
| 113 |
+
<span className="text-[10px] font-bold text-blue-400 uppercase tracking-widest">Syncing</span>
|
| 114 |
</div>
|
| 115 |
) : null}
|
| 116 |
</div>
|
| 117 |
</header>
|
| 118 |
|
| 119 |
{/* Main layout */}
|
| 120 |
+
<main className="flex-1 flex flex-col lg:flex-row gap-8 p-8 max-w-[1600px] mx-auto w-full">
|
| 121 |
{/* Left sidebar */}
|
| 122 |
+
<aside className="w-full lg:w-80 flex flex-col gap-6 order-2 lg:order-1 animate-slide-in">
|
| 123 |
{/* Emotion display */}
|
| 124 |
+
<div className="space-y-3">
|
| 125 |
<h2
|
| 126 |
+
className="px-1 text-[10px] font-bold text-white/30 uppercase tracking-[0.3em]"
|
| 127 |
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 128 |
>
|
| 129 |
+
Current State
|
| 130 |
</h2>
|
| 131 |
<EmotionDisplay emotion={currentEmotion} faceDetected={stats.faceDetected} />
|
| 132 |
</div>
|
|
|
|
| 140 |
|
| 141 |
{/* Last log entry */}
|
| 142 |
{lastLog && (
|
| 143 |
+
<div className="glass rounded-2xl p-5 space-y-3 border-l-2 border-l-blue-500/50 relative overflow-hidden group hover:bg-white/[0.05] transition-colors">
|
| 144 |
+
<div className="absolute top-0 right-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
|
| 145 |
+
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 146 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 147 |
+
</svg>
|
| 148 |
+
</div>
|
| 149 |
+
<p className="text-[10px] text-blue-400 font-bold uppercase tracking-widest">Recent Activity</p>
|
| 150 |
+
<div>
|
| 151 |
+
<p className="text-lg font-bold text-white leading-tight">
|
| 152 |
+
{lastLog.dominant_emotion}
|
| 153 |
+
</p>
|
| 154 |
+
<p className="text-xs text-white/40 mt-1">
|
| 155 |
+
Sustained for {lastLog.duration_seconds}s
|
| 156 |
+
</p>
|
| 157 |
+
</div>
|
| 158 |
+
<div className="pt-2 flex items-center justify-between border-t border-white/5">
|
| 159 |
+
<span className="text-[10px] text-white/20 font-mono">
|
| 160 |
+
{new Date(lastLog.timestamp).toLocaleTimeString()}
|
| 161 |
+
</span>
|
| 162 |
+
<span className="text-[10px] text-green-500 font-bold uppercase tracking-tighter">Verified</span>
|
| 163 |
+
</div>
|
| 164 |
</div>
|
| 165 |
)}
|
| 166 |
</aside>
|
| 167 |
|
| 168 |
{/* Video Feed */}
|
| 169 |
+
<section className="flex-1 flex flex-col gap-6 order-1 lg:order-2 animate-scale-up">
|
| 170 |
+
<div className="relative flex-1 rounded-[2.5rem] overflow-hidden glass min-h-[500px] border border-white/10 shadow-2xl">
|
| 171 |
{/* Camera error state */}
|
| 172 |
{cameraError ? (
|
| 173 |
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6 p-12 text-center bg-black/40 backdrop-blur-md">
|
| 174 |
+
<div className="w-20 h-20 rounded-3xl bg-red-500/10 border border-red-500/20 flex items-center justify-center text-4xl">
|
| 175 |
+
⚠️
|
| 176 |
+
</div>
|
| 177 |
+
<div>
|
| 178 |
+
<h3 className="text-xl font-bold text-white mb-2">Camera Access Required</h3>
|
| 179 |
+
<p className="text-white/40 text-sm max-w-xs mx-auto leading-relaxed">{cameraError}</p>
|
| 180 |
+
</div>
|
| 181 |
<button
|
| 182 |
onClick={() => window.location.reload()}
|
| 183 |
+
className="px-8 py-3 rounded-2xl bg-white/5 hover:bg-white/10 text-sm font-bold text-white transition-all border border-white/10 hover:border-white/20"
|
| 184 |
>
|
| 185 |
+
Request Again
|
| 186 |
</button>
|
| 187 |
</div>
|
| 188 |
) : (
|
|
|
|
| 195 |
onUserMedia={handleUserMedia}
|
| 196 |
onUserMediaError={handleUserMediaError}
|
| 197 |
videoConstraints={{
|
| 198 |
+
width: { ideal: 1920 },
|
| 199 |
+
height: { ideal: 1080 },
|
| 200 |
facingMode: "user",
|
| 201 |
+
frameRate: { ideal: 60 },
|
| 202 |
}}
|
| 203 |
+
className="w-full h-full object-cover scale-[1.01]"
|
| 204 |
style={{ display: isCameraReady ? "block" : "none" }}
|
| 205 |
/>
|
| 206 |
|
| 207 |
{/* Loading skeleton */}
|
| 208 |
{!isCameraReady && (
|
| 209 |
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6 bg-[#070b14]">
|
| 210 |
+
<div className="relative">
|
| 211 |
+
<div className="w-20 h-20 rounded-[2rem] border-2 border-blue-500/20" />
|
| 212 |
+
<div className="absolute inset-0 w-20 h-20 rounded-[2rem] border-t-2 border-blue-500 animate-spin" />
|
| 213 |
+
</div>
|
| 214 |
+
<div className="text-center">
|
| 215 |
+
<p className="text-sm font-bold text-white/60 tracking-widest uppercase">Initializing Vision</p>
|
| 216 |
+
<p className="text-xs text-white/20 mt-2">Connecting to hardware layer…</p>
|
| 217 |
+
</div>
|
| 218 |
</div>
|
| 219 |
)}
|
| 220 |
|
| 221 |
+
{/* HUD Elements */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
{isCameraReady && isTracking && (
|
| 223 |
<>
|
| 224 |
+
{/* Emotion overlay glow border */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
<div
|
| 226 |
+
className="absolute inset-0 pointer-events-none transition-all duration-700"
|
| 227 |
style={{
|
| 228 |
+
boxShadow: `inset 0 0 100px ${glowColor}15, inset 0 0 40px ${glowColor}10`,
|
| 229 |
+
border: `2px solid ${glowColor}20`,
|
|
|
|
| 230 |
}}
|
| 231 |
+
/>
|
| 232 |
+
|
| 233 |
+
{/* Corner brackets overlay */}
|
| 234 |
+
<div className="absolute inset-8 pointer-events-none opacity-40">
|
| 235 |
+
<div className="absolute top-0 left-0 w-12 h-12 border-t-2 border-l-2 border-blue-400 rounded-tl-2xl" />
|
| 236 |
+
<div className="absolute top-0 right-0 w-12 h-12 border-t-2 border-r-2 border-blue-400 rounded-tr-2xl" />
|
| 237 |
+
<div className="absolute bottom-0 left-0 w-12 h-12 border-b-2 border-l-2 border-blue-400 rounded-bl-2xl" />
|
| 238 |
+
<div className="absolute bottom-0 right-0 w-12 h-12 border-b-2 border-r-2 border-blue-400 rounded-br-2xl" />
|
| 239 |
</div>
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
{/* Emotion badge overlay */}
|
| 242 |
+
{stats.faceDetected && (
|
| 243 |
+
<div
|
| 244 |
+
key={currentEmotion}
|
| 245 |
+
className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-emotion"
|
| 246 |
+
>
|
| 247 |
+
<div
|
| 248 |
+
className="px-8 py-3 rounded-2xl glass-strong border-2 backdrop-blur-3xl text-lg font-black transition-all duration-500 flex items-center gap-3 shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
|
| 249 |
+
style={{
|
| 250 |
+
borderColor: `${glowColor}30`,
|
| 251 |
+
color: glowColor,
|
| 252 |
+
boxShadow: `0 0 30px ${glowColor}15`,
|
| 253 |
+
}}
|
| 254 |
+
>
|
| 255 |
+
<span className="text-2xl filter drop-shadow-md">
|
| 256 |
+
{currentEmotion === "Happy" && "😊"}
|
| 257 |
+
{currentEmotion === "Sad" && "😢"}
|
| 258 |
+
{currentEmotion === "Angry" && "😠"}
|
| 259 |
+
{currentEmotion === "Surprised" && "😲"}
|
| 260 |
+
{currentEmotion === "Neutral" && "😐"}
|
| 261 |
+
</span>
|
| 262 |
+
<span className="tracking-tighter uppercase">{currentEmotion}</span>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
)}
|
| 266 |
+
|
| 267 |
+
{/* Scanning Grid Effect */}
|
| 268 |
+
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-20">
|
| 269 |
+
<div className="w-full h-full"
|
| 270 |
+
style={{
|
| 271 |
+
backgroundImage: 'radial-gradient(circle, white 1px, transparent 1px)',
|
| 272 |
+
backgroundSize: '40px 40px'
|
| 273 |
+
}}
|
| 274 |
+
/>
|
| 275 |
+
<div className="w-full h-1 bg-gradient-to-r from-transparent via-blue-400 to-transparent absolute shadow-[0_0_20px_rgba(96,165,250,0.5)]"
|
| 276 |
+
style={{ animation: "scan-line 6s ease-in-out infinite" }} />
|
| 277 |
+
</div>
|
| 278 |
+
</>
|
| 279 |
)}
|
| 280 |
</>
|
| 281 |
)}
|
| 282 |
</div>
|
| 283 |
|
| 284 |
{/* Controls */}
|
| 285 |
+
<div className="glass rounded-3xl p-5 flex items-center justify-between gap-6 border border-white/5 shadow-xl">
|
| 286 |
+
<div className="flex items-center gap-4">
|
| 287 |
+
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white/5 border border-white/5 transition-all duration-500 ${isTracking ? 'opacity-100' : 'opacity-40'}`}>
|
| 288 |
+
<div className={`w-2 h-2 rounded-full ${isTracking ? 'bg-blue-400 animate-pulse shadow-[0_0_8px_rgba(96,165,250,0.6)]' : 'bg-white/20'}`} />
|
| 289 |
+
<span className="text-[10px] font-bold text-white/50 uppercase tracking-widest">System Live</span>
|
| 290 |
+
</div>
|
| 291 |
+
<div className="h-4 w-px bg-white/10" />
|
| 292 |
+
<p className="text-xs text-white/30 font-medium tracking-tight">
|
| 293 |
+
{!isCameraReady
|
| 294 |
+
? "Waiting for sensor input…"
|
| 295 |
+
: !isLoaded
|
| 296 |
+
? "Loading neural weights…"
|
| 297 |
+
: isTracking
|
| 298 |
+
? `Processing window active • Log interval 10s`
|
| 299 |
+
: "Vision system standby"}
|
| 300 |
+
</p>
|
| 301 |
</div>
|
| 302 |
|
| 303 |
<button
|
| 304 |
id="tracking-toggle-btn"
|
| 305 |
onClick={() => setIsTracking((v) => !v)}
|
| 306 |
disabled={!isCameraReady || !isLoaded || !!loadError}
|
| 307 |
+
className={`relative group px-10 py-4 rounded-2xl text-sm font-black transition-all duration-300 disabled:opacity-20 disabled:cursor-not-allowed overflow-hidden ${
|
| 308 |
isTracking
|
| 309 |
+
? "bg-red-500/10 text-red-400 border border-red-500/30 hover:bg-red-500/20"
|
| 310 |
+
: "bg-white text-black hover:bg-white/90 shadow-[0_10px_20px_rgba(255,255,255,0.1)]"
|
| 311 |
}`}
|
| 312 |
>
|
| 313 |
+
<div className="relative z-10 flex items-center gap-2">
|
| 314 |
+
{isTracking ? (
|
| 315 |
+
<>
|
| 316 |
+
<span className="text-lg">⏹</span>
|
| 317 |
+
<span className="uppercase tracking-tighter">Disable</span>
|
| 318 |
+
</>
|
| 319 |
+
) : (
|
| 320 |
+
<>
|
| 321 |
+
<span className="text-lg">▶</span>
|
| 322 |
+
<span className="uppercase tracking-tighter">Initialize</span>
|
| 323 |
+
</>
|
| 324 |
+
)}
|
| 325 |
+
</div>
|
| 326 |
</button>
|
| 327 |
</div>
|
| 328 |
</section>
|
| 329 |
</main>
|
| 330 |
|
| 331 |
{/* Footer */}
|
| 332 |
+
<footer className="glass border-t border-white/5 py-4 px-8 flex items-center justify-between">
|
| 333 |
+
<p className="text-[10px] text-white/20 font-bold uppercase tracking-[0.2em]">
|
| 334 |
+
End-to-End Encryption • Local Edge Inference
|
| 335 |
</p>
|
| 336 |
+
<div className="flex items-center gap-6">
|
| 337 |
+
<span className="text-[10px] text-white/20 font-bold uppercase tracking-[0.2em]">MediaPipe AI</span>
|
| 338 |
+
<span className="text-[10px] text-white/20 font-bold uppercase tracking-[0.2em]">WASM Optimized</span>
|
| 339 |
+
</div>
|
| 340 |
</footer>
|
| 341 |
+
|
| 342 |
</div>
|
| 343 |
);
|
| 344 |
}
|
frontend/app/components/EmotionDisplay.tsx
CHANGED
|
@@ -53,26 +53,59 @@ export default function EmotionDisplay({ emotion, faceDetected }: EmotionDisplay
|
|
| 53 |
|
| 54 |
if (!faceDetected) {
|
| 55 |
return (
|
| 56 |
-
<div className="glass rounded-2xl p-
|
| 57 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
<div>
|
| 59 |
-
<p className="text-sm font-
|
| 60 |
-
<p className="text-
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
);
|
| 64 |
}
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return (
|
| 67 |
<div
|
| 68 |
key={emotion}
|
| 69 |
-
className={`glass rounded-2xl p-5 flex items-center gap-
|
|
|
|
|
|
|
|
|
|
| 70 |
>
|
| 71 |
-
|
| 72 |
-
<div
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
);
|
| 78 |
}
|
|
|
|
|
|
| 53 |
|
| 54 |
if (!faceDetected) {
|
| 55 |
return (
|
| 56 |
+
<div className="glass rounded-2xl p-6 flex items-center gap-5 border border-dashed border-white/10 animate-pulse-slow">
|
| 57 |
+
<div className="relative">
|
| 58 |
+
<div className="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center text-2xl grayscale opacity-30">
|
| 59 |
+
👤
|
| 60 |
+
</div>
|
| 61 |
+
<div className="absolute inset-0 rounded-full border border-white/10 animate-ping opacity-20" />
|
| 62 |
+
</div>
|
| 63 |
<div>
|
| 64 |
+
<p className="text-sm font-bold text-white/40 tracking-tight uppercase">Ready to Scan</p>
|
| 65 |
+
<p className="text-[11px] text-white/20 mt-0.5 leading-tight">Please position your face<br/>clearly in the camera</p>
|
| 66 |
</div>
|
| 67 |
</div>
|
| 68 |
);
|
| 69 |
}
|
| 70 |
|
| 71 |
+
const emotionColors: Record<EmotionType, string> = {
|
| 72 |
+
Happy: "rgba(104, 211, 145, 0.4)",
|
| 73 |
+
Sad: "rgba(118, 228, 247, 0.4)",
|
| 74 |
+
Angry: "rgba(252, 129, 129, 0.4)",
|
| 75 |
+
Surprised: "rgba(246, 173, 85, 0.4)",
|
| 76 |
+
Neutral: "rgba(160, 174, 192, 0.3)",
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
return (
|
| 80 |
<div
|
| 81 |
key={emotion}
|
| 82 |
+
className={`relative glass rounded-2xl p-5 flex items-center gap-5 border overflow-hidden transition-all duration-500 ${config.bgClass} animate-emotion`}
|
| 83 |
+
style={{
|
| 84 |
+
boxShadow: `0 10px 40px -10px ${emotionColors[emotion]}`,
|
| 85 |
+
}}
|
| 86 |
>
|
| 87 |
+
{/* Background Glow */}
|
| 88 |
+
<div
|
| 89 |
+
className="absolute -right-4 -top-4 w-24 h-24 blur-3xl rounded-full opacity-20 transition-all duration-700"
|
| 90 |
+
style={{ background: emotionColors[emotion] }}
|
| 91 |
+
/>
|
| 92 |
+
|
| 93 |
+
<div className="relative text-5xl animate-float filter drop-shadow-lg">
|
| 94 |
+
{config.emoji}
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div className="relative">
|
| 98 |
+
<div className="flex items-center gap-2 mb-0.5">
|
| 99 |
+
<p className={`text-2xl font-black tracking-tight ${config.colorClass}`}>
|
| 100 |
+
{config.label}
|
| 101 |
+
</p>
|
| 102 |
+
<div className={`w-1.5 h-1.5 rounded-full animate-blink ${config.bgClass}`} style={{ background: 'currentColor' }} />
|
| 103 |
+
</div>
|
| 104 |
+
<p className="text-xs text-white/40 font-medium">
|
| 105 |
+
{config.description}
|
| 106 |
+
</p>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
);
|
| 110 |
}
|
| 111 |
+
|
frontend/app/components/StatsPanel.tsx
CHANGED
|
@@ -8,20 +8,23 @@ interface BlendshapeBarProps {
|
|
| 8 |
|
| 9 |
function BlendshapeBar({ name, value, color = "#63b3ed" }: BlendshapeBarProps) {
|
| 10 |
return (
|
| 11 |
-
<div className="flex
|
| 12 |
-
<
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
<div
|
| 15 |
-
className="h-full rounded-full transition-all duration-
|
| 16 |
style={{
|
| 17 |
width: `${Math.round(value * 100)}%`,
|
| 18 |
-
background: color,
|
|
|
|
| 19 |
}}
|
| 20 |
/>
|
| 21 |
</div>
|
| 22 |
-
<span className="text-xs text-white/30 w-8 text-right flex-shrink-0">
|
| 23 |
-
{Math.round(value * 100)}
|
| 24 |
-
</span>
|
| 25 |
</div>
|
| 26 |
);
|
| 27 |
}
|
|
@@ -36,7 +39,7 @@ const KEY_BLENDSHAPES: Array<{ key: string; label: string; color: string }> = [
|
|
| 36 |
{ key: "mouthSmileLeft", label: "Smile L", color: "#68d391" },
|
| 37 |
{ key: "mouthSmileRight", label: "Smile R", color: "#68d391" },
|
| 38 |
{ key: "browInnerUp", label: "Brow Up", color: "#f6ad55" },
|
| 39 |
-
{ key: "jawOpen", label: "Jaw Open", color: "#
|
| 40 |
{ key: "browDownLeft", label: "Brow ↓L", color: "#fc8181" },
|
| 41 |
{ key: "browDownRight", label: "Brow ↓R", color: "#fc8181" },
|
| 42 |
{ key: "mouthFrownLeft", label: "Frown L", color: "#76e4f7" },
|
|
@@ -45,52 +48,71 @@ const KEY_BLENDSHAPES: Array<{ key: string; label: string; color: string }> = [
|
|
| 45 |
|
| 46 |
export default function StatsPanel({ fps, faceDetected, blendshapes }: StatsPanel) {
|
| 47 |
return (
|
| 48 |
-
<div className="glass rounded-2xl p-5 space-y-
|
| 49 |
<div className="flex items-center justify-between">
|
| 50 |
-
<h3
|
| 51 |
-
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 52 |
-
className="text-sm font-semibold text-white/60 uppercase tracking-widest"
|
| 53 |
-
>
|
| 54 |
-
Analytics
|
| 55 |
-
</h3>
|
| 56 |
<div className="flex items-center gap-2">
|
| 57 |
-
<div
|
| 58 |
-
className=
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
-
{/*
|
| 65 |
-
<div className="
|
| 66 |
-
<div className="glass-strong rounded-xl
|
| 67 |
-
<p className="text-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
-
<div className="glass-strong rounded-xl
|
| 73 |
-
<p className="text-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
| 77 |
</div>
|
| 78 |
</div>
|
| 79 |
|
| 80 |
{/* Blendshapes */}
|
| 81 |
{faceDetected && (
|
| 82 |
-
<div className="space-y-2">
|
| 83 |
-
<
|
| 84 |
-
|
| 85 |
-
<
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
)}
|
| 94 |
</div>
|
| 95 |
);
|
| 96 |
}
|
|
|
|
|
|
| 8 |
|
| 9 |
function BlendshapeBar({ name, value, color = "#63b3ed" }: BlendshapeBarProps) {
|
| 10 |
return (
|
| 11 |
+
<div className="flex flex-col gap-1.5">
|
| 12 |
+
<div className="flex items-center justify-between px-0.5">
|
| 13 |
+
<span className="text-[10px] text-white/40 uppercase tracking-wider font-medium">{name}</span>
|
| 14 |
+
<span className="text-[10px] text-white/30 font-mono">
|
| 15 |
+
{Math.round(value * 100)}%
|
| 16 |
+
</span>
|
| 17 |
+
</div>
|
| 18 |
+
<div className="h-1.5 bg-white/5 rounded-full overflow-hidden border border-white/5 shadow-inner">
|
| 19 |
<div
|
| 20 |
+
className="h-full rounded-full transition-all duration-300 ease-out"
|
| 21 |
style={{
|
| 22 |
width: `${Math.round(value * 100)}%`,
|
| 23 |
+
background: `linear-gradient(90deg, ${color}cc, ${color})`,
|
| 24 |
+
boxShadow: `0 0 10px ${color}40`,
|
| 25 |
}}
|
| 26 |
/>
|
| 27 |
</div>
|
|
|
|
|
|
|
|
|
|
| 28 |
</div>
|
| 29 |
);
|
| 30 |
}
|
|
|
|
| 39 |
{ key: "mouthSmileLeft", label: "Smile L", color: "#68d391" },
|
| 40 |
{ key: "mouthSmileRight", label: "Smile R", color: "#68d391" },
|
| 41 |
{ key: "browInnerUp", label: "Brow Up", color: "#f6ad55" },
|
| 42 |
+
{ key: "jawOpen", label: "Jaw Open", color: "#63b3ed" },
|
| 43 |
{ key: "browDownLeft", label: "Brow ↓L", color: "#fc8181" },
|
| 44 |
{ key: "browDownRight", label: "Brow ↓R", color: "#fc8181" },
|
| 45 |
{ key: "mouthFrownLeft", label: "Frown L", color: "#76e4f7" },
|
|
|
|
| 48 |
|
| 49 |
export default function StatsPanel({ fps, faceDetected, blendshapes }: StatsPanel) {
|
| 50 |
return (
|
| 51 |
+
<div className="glass rounded-2xl p-5 space-y-6 animate-scale-up">
|
| 52 |
<div className="flex items-center justify-between">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
<div className="flex items-center gap-2">
|
| 54 |
+
<div className="p-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
| 55 |
+
<svg className="w-3.5 h-3.5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 56 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
| 57 |
+
</svg>
|
| 58 |
+
</div>
|
| 59 |
+
<h3
|
| 60 |
+
style={{ fontFamily: "Outfit, sans-serif" }}
|
| 61 |
+
className="text-xs font-bold text-white/70 uppercase tracking-widest"
|
| 62 |
+
>
|
| 63 |
+
Analytics
|
| 64 |
+
</h3>
|
| 65 |
+
</div>
|
| 66 |
+
<div className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-tighter border transition-colors ${
|
| 67 |
+
faceDetected ? "bg-green-500/10 border-green-500/30 text-green-400" : "bg-red-500/10 border-red-500/30 text-red-400"
|
| 68 |
+
}`}>
|
| 69 |
+
{faceDetected ? "Active" : "Idle"}
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
+
{/* Stats Grid */}
|
| 74 |
+
<div className="grid grid-cols-2 gap-3">
|
| 75 |
+
<div className="glass-strong rounded-xl p-3 border border-white/5 group hover:border-white/10 transition-colors">
|
| 76 |
+
<p className="text-xs text-white/30 mb-1">Performance</p>
|
| 77 |
+
<div className="flex items-baseline gap-1">
|
| 78 |
+
<span className="text-2xl font-bold text-white tracking-tight" style={{ fontFamily: "Outfit, sans-serif" }}>
|
| 79 |
+
{fps}
|
| 80 |
+
</span>
|
| 81 |
+
<span className="text-[10px] text-white/20 font-bold uppercase">fps</span>
|
| 82 |
+
</div>
|
| 83 |
</div>
|
| 84 |
+
<div className="glass-strong rounded-xl p-3 border border-white/5 group hover:border-white/10 transition-colors">
|
| 85 |
+
<p className="text-xs text-white/30 mb-1">Detection</p>
|
| 86 |
+
<div className="flex items-baseline gap-1">
|
| 87 |
+
<span className="text-2xl font-bold text-white tracking-tight" style={{ fontFamily: "Outfit, sans-serif" }}>
|
| 88 |
+
{faceDetected ? "1" : "0"}
|
| 89 |
+
</span>
|
| 90 |
+
<span className="text-[10px] text-white/20 font-bold uppercase">face</span>
|
| 91 |
+
</div>
|
| 92 |
</div>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
{/* Blendshapes */}
|
| 96 |
{faceDetected && (
|
| 97 |
+
<div className="space-y-4 pt-2">
|
| 98 |
+
<div className="flex items-center gap-2">
|
| 99 |
+
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
| 100 |
+
<span className="text-[10px] text-white/20 uppercase tracking-widest font-bold">Signals</span>
|
| 101 |
+
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
| 102 |
+
</div>
|
| 103 |
+
<div className="grid grid-cols-1 gap-4">
|
| 104 |
+
{KEY_BLENDSHAPES.map(({ key, label, color }) => (
|
| 105 |
+
<BlendshapeBar
|
| 106 |
+
key={key}
|
| 107 |
+
name={label}
|
| 108 |
+
value={blendshapes[key] ?? 0}
|
| 109 |
+
color={color}
|
| 110 |
+
/>
|
| 111 |
+
))}
|
| 112 |
+
</div>
|
| 113 |
</div>
|
| 114 |
)}
|
| 115 |
</div>
|
| 116 |
);
|
| 117 |
}
|
| 118 |
+
|
frontend/app/globals.css
CHANGED
|
@@ -39,17 +39,19 @@ html, body {
|
|
| 39 |
|
| 40 |
/* Glassmorphism */
|
| 41 |
.glass {
|
| 42 |
-
background: rgba(255, 255, 255, 0.
|
| 43 |
-
backdrop-filter: blur(
|
| 44 |
-
-webkit-backdrop-filter: blur(
|
| 45 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
.glass-strong {
|
| 49 |
-
background: rgba(255, 255, 255, 0.
|
| 50 |
-
backdrop-filter: blur(
|
| 51 |
-
-webkit-backdrop-filter: blur(
|
| 52 |
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
/* Gradient text */
|
|
@@ -58,26 +60,44 @@ html, body {
|
|
| 58 |
-webkit-background-clip: text;
|
| 59 |
-webkit-text-fill-color: transparent;
|
| 60 |
background-clip: text;
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
-
/*
|
| 64 |
-
.
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
radial-gradient(ellipse at 80% 20%, rgba(159, 122, 234, 0.08) 0%, transparent 50%),
|
| 68 |
-
radial-gradient(ellipse at 60% 80%, rgba(104, 211, 145, 0.05) 0%, transparent 50%),
|
| 69 |
-
var(--bg-primary);
|
| 70 |
}
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
@keyframes scan-line {
|
|
@@ -90,11 +110,6 @@ html, body {
|
|
| 90 |
100% { opacity: 1; transform: scale(1) translateY(0px); }
|
| 91 |
}
|
| 92 |
|
| 93 |
-
@keyframes shimmer {
|
| 94 |
-
0% { background-position: -200% center; }
|
| 95 |
-
100% { background-position: 200% center; }
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
@keyframes blink {
|
| 99 |
0%, 100% { opacity: 1; }
|
| 100 |
50% { opacity: 0.3; }
|
|
@@ -110,8 +125,9 @@ html, body {
|
|
| 110 |
100% { opacity: 1; transform: translateY(0); }
|
| 111 |
}
|
| 112 |
|
| 113 |
-
.animate-
|
| 114 |
-
.animate-
|
|
|
|
| 115 |
.animate-emotion { animation: emotion-appear 0.3s ease-out forwards; }
|
| 116 |
.animate-fade-in-up { animation: fade-in-up 0.5s ease-out forwards; }
|
| 117 |
.animate-blink { animation: blink 1.5s ease-in-out infinite; }
|
|
@@ -124,38 +140,27 @@ html, body {
|
|
| 124 |
.emotion-surprised { color: #f6ad55; }
|
| 125 |
.emotion-neutral { color: #a0aec0; }
|
| 126 |
|
| 127 |
-
.emotion-bg-happy { background: rgba(104, 211, 145, 0.
|
| 128 |
-
.emotion-bg-sad { background: rgba(118, 228, 247, 0.
|
| 129 |
-
.emotion-bg-angry { background: rgba(252, 129, 129, 0.
|
| 130 |
-
.emotion-bg-surprised { background: rgba(246, 173, 85, 0.
|
| 131 |
-
.emotion-bg-neutral { background: rgba(160, 174, 192, 0.
|
| 132 |
|
| 133 |
-
/*
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
position: absolute;
|
| 137 |
-
width: 100%;
|
| 138 |
-
height: 2px;
|
| 139 |
-
background: linear-gradient(90deg, transparent, rgba(99, 179, 237, 0.6), transparent);
|
| 140 |
-
animation: scan-line 3s linear infinite;
|
| 141 |
-
pointer-events: none;
|
| 142 |
}
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
.corner-bracket::after {
|
| 147 |
-
content: '';
|
| 148 |
-
position: absolute;
|
| 149 |
-
width: 20px;
|
| 150 |
-
height: 20px;
|
| 151 |
-
border-color: rgba(99, 179, 237, 0.6);
|
| 152 |
-
border-style: solid;
|
| 153 |
}
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
| 157 |
}
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
}
|
|
|
|
|
|
| 39 |
|
| 40 |
/* Glassmorphism */
|
| 41 |
.glass {
|
| 42 |
+
background: rgba(255, 255, 255, 0.03);
|
| 43 |
+
backdrop-filter: blur(12px);
|
| 44 |
+
-webkit-backdrop-filter: blur(12px);
|
| 45 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 46 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 47 |
}
|
| 48 |
|
| 49 |
.glass-strong {
|
| 50 |
+
background: rgba(255, 255, 255, 0.06);
|
| 51 |
+
backdrop-filter: blur(24px);
|
| 52 |
+
-webkit-backdrop-filter: blur(24px);
|
| 53 |
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 54 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
|
| 55 |
}
|
| 56 |
|
| 57 |
/* Gradient text */
|
|
|
|
| 60 |
-webkit-background-clip: text;
|
| 61 |
-webkit-text-fill-color: transparent;
|
| 62 |
background-clip: text;
|
| 63 |
+
font-weight: 700;
|
| 64 |
}
|
| 65 |
|
| 66 |
+
/* High-Tech Borders */
|
| 67 |
+
.cyber-border {
|
| 68 |
+
position: relative;
|
| 69 |
+
border: 1px solid rgba(99, 179, 237, 0.2);
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
+
.cyber-border::before {
|
| 73 |
+
content: '';
|
| 74 |
+
position: absolute;
|
| 75 |
+
top: -1px; left: -1px; right: -1px; bottom: -1px;
|
| 76 |
+
background: linear-gradient(45deg, transparent, rgba(99, 179, 237, 0.3), transparent);
|
| 77 |
+
z-index: -1;
|
| 78 |
+
border-radius: inherit;
|
| 79 |
+
opacity: 0;
|
| 80 |
+
transition: opacity 0.3s;
|
| 81 |
}
|
| 82 |
|
| 83 |
+
.cyber-border:hover::before {
|
| 84 |
+
opacity: 1;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Animations */
|
| 88 |
+
@keyframes slide-in-right {
|
| 89 |
+
0% { opacity: 0; transform: translateX(30px); }
|
| 90 |
+
100% { opacity: 1; transform: translateX(0); }
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
@keyframes scale-up {
|
| 94 |
+
0% { opacity: 0; transform: scale(0.95); }
|
| 95 |
+
100% { opacity: 1; transform: scale(1); }
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
@keyframes glow-pulse {
|
| 99 |
+
0%, 100% { opacity: 0.5; }
|
| 100 |
+
50% { opacity: 1; }
|
| 101 |
}
|
| 102 |
|
| 103 |
@keyframes scan-line {
|
|
|
|
| 110 |
100% { opacity: 1; transform: scale(1) translateY(0px); }
|
| 111 |
}
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
@keyframes blink {
|
| 114 |
0%, 100% { opacity: 1; }
|
| 115 |
50% { opacity: 0.3; }
|
|
|
|
| 125 |
100% { opacity: 1; transform: translateY(0); }
|
| 126 |
}
|
| 127 |
|
| 128 |
+
.animate-slide-in { animation: slide-in-right 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
| 129 |
+
.animate-scale-up { animation: scale-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
| 130 |
+
.animate-glow { animation: glow-pulse 2s ease-in-out infinite; }
|
| 131 |
.animate-emotion { animation: emotion-appear 0.3s ease-out forwards; }
|
| 132 |
.animate-fade-in-up { animation: fade-in-up 0.5s ease-out forwards; }
|
| 133 |
.animate-blink { animation: blink 1.5s ease-in-out infinite; }
|
|
|
|
| 140 |
.emotion-surprised { color: #f6ad55; }
|
| 141 |
.emotion-neutral { color: #a0aec0; }
|
| 142 |
|
| 143 |
+
.emotion-bg-happy { background: rgba(104, 211, 145, 0.1); border-color: rgba(104, 211, 145, 0.2); }
|
| 144 |
+
.emotion-bg-sad { background: rgba(118, 228, 247, 0.1); border-color: rgba(118, 228, 247, 0.2); }
|
| 145 |
+
.emotion-bg-angry { background: rgba(252, 129, 129, 0.1); border-color: rgba(252, 129, 129, 0.2); }
|
| 146 |
+
.emotion-bg-surprised { background: rgba(246, 173, 85, 0.1); border-color: rgba(246, 173, 85, 0.2); }
|
| 147 |
+
.emotion-bg-neutral { background: rgba(160, 174, 192, 0.08); border-color: rgba(160, 174, 192, 0.15); }
|
| 148 |
|
| 149 |
+
/* Custom Scrollbar */
|
| 150 |
+
::-webkit-scrollbar {
|
| 151 |
+
width: 5px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
| 154 |
+
::-webkit-scrollbar-track {
|
| 155 |
+
background: transparent;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
+
|
| 158 |
+
::-webkit-scrollbar-thumb {
|
| 159 |
+
background: rgba(255, 255, 255, 0.1);
|
| 160 |
+
border-radius: 10px;
|
| 161 |
}
|
| 162 |
+
|
| 163 |
+
::-webkit-scrollbar-thumb:hover {
|
| 164 |
+
background: rgba(255, 255, 255, 0.2);
|
| 165 |
}
|
| 166 |
+
|