Spaces:
Running
Running
File size: 15,090 Bytes
47cf216 699b218 47cf216 699b218 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 84e44c2 47cf216 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
"use client"
import { useState, useEffect, useRef } from "react"
import MetricsPanel from "@/components/neurolink/metrics-panel"
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Play, Square, RotateCcw, Zap, User, Fingerprint, Shield, Target } from "lucide-react"
import { io, Socket } from "socket.io-client"
import Link from "next/link"
// --- CORRECTION ICI : ON UTILISE L'ADRESSE DU CLOUD ---
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"
interface MetricData { timestamp: number; value: number; engagement: number; satisfaction: number; trust: number; }
interface UserInfo { firstName: string; lastName: string; clientId: string }
export default function Dashboard() {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [socket, setSocket] = useState<Socket | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [isRecording, setIsRecording] = useState(false)
const [sessionTime, setSessionTime] = useState(0)
const [formData, setFormData] = useState({ firstName: "", lastName: "", clientId: "" })
const [currentMetrics, setCurrentMetrics] = useState({ engagement: 0, satisfaction: 50, trust: 50, loyalty: 50, opinion: 50, emotion: "neutral" })
const [history, setHistory] = useState<MetricData[]>([])
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const [faceCoords, setFaceCoords] = useState<any>(null)
const [cameraActive, setCameraActive] = useState(false)
// 1. Démarrer Webcam Locale
useEffect(() => {
if (userInfo && !cameraActive) {
navigator.mediaDevices.getUserMedia({ video: { width: 480, height: 360 } })
.then(stream => { if (videoRef.current) { videoRef.current.srcObject = stream; setCameraActive(true) } })
.catch(err => console.error("Erreur Webcam:", err))
}
}, [userInfo, cameraActive])
// 2. Logique Socket & Envoi d'images
useEffect(() => {
if (!userInfo) return;
// --- CORRECTION ICI : CONNEXION AU CLOUD ---
const newSocket = io(API_URL, { transports: ["websocket", "polling"] })
newSocket.on("connect", () => setIsConnected(true))
newSocket.on("disconnect", () => setIsConnected(false))
newSocket.on("metrics_update", (data: any) => {
setSessionTime(data.session_time); setIsRecording(data.is_recording)
setFaceCoords(data.face_coords)
const newMetrics = {
emotion: data.emotion,
engagement: data.metrics.engagement, satisfaction: data.metrics.satisfaction,
trust: data.metrics.trust, loyalty: data.metrics.loyalty, opinion: data.metrics.opinion
}
setCurrentMetrics(newMetrics)
if (data.is_recording) {
setHistory(prev => [...prev.slice(-29), {
timestamp: data.session_time, value: newMetrics.engagement, engagement: newMetrics.engagement,
satisfaction: newMetrics.satisfaction, trust: newMetrics.trust
}])
}
})
setSocket(newSocket)
// Envoi 5 fois par seconde
const interval = setInterval(() => {
if (videoRef.current && canvasRef.current && newSocket.connected) {
const ctx = canvasRef.current.getContext('2d')
if (ctx) {
ctx.drawImage(videoRef.current, 0, 0, 480, 360)
const dataUrl = canvasRef.current.toDataURL('image/jpeg', 0.5)
newSocket.emit('process_frame', dataUrl)
}
}
}, 200)
return () => { clearInterval(interval); newSocket.close() }
}, [userInfo])
const handleLogin = (e: React.FormEvent) => { e.preventDefault(); if (formData.firstName && formData.lastName) setUserInfo(formData) }
const handleStartStop = () => { if (socket && userInfo) { isRecording ? socket.emit("stop_session") : (sessionTime === 0 && setHistory([]), socket.emit("start_session", userInfo)) } }
const handleReset = () => { if (socket) socket.emit("stop_session"); setHistory([]); setSessionTime(0); setCurrentMetrics(prev => ({ ...prev, engagement: 0, emotion: "neutral" })) }
const handleLogout = () => { setUserInfo(null); setHistory([]); setSessionTime(0); setCameraActive(false); if(socket) socket.disconnect() }
const formatTime = (s: number) => `${Math.floor(s/60).toString().padStart(2,'0')}:${(s%60).toString().padStart(2,'0')}`
const getEmotionDisplay = (e: string) => { const map: any = { happy: "😄 JOIE", sad: "😢 TRISTESSE", angry: "😠 COLÈRE", surprise: "😲 SURPRISE", fear: "😨 PEUR", neutral: "😐 NEUTRE" }; return map[e] || e.toUpperCase() }
if (!userInfo) {
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4 relative overflow-hidden text-slate-900">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-green-100 via-slate-50 to-white opacity-80"></div>
<Card className="w-full max-w-md border-slate-200 bg-white shadow-2xl relative z-10">
<CardHeader className="text-center space-y-4">
<div className="mx-auto w-20 h-20 rounded-full bg-green-50 flex items-center justify-center border border-green-100 shadow-sm"><Fingerprint className="w-10 h-10 text-green-600" /></div>
<div><CardTitle className="text-3xl font-bold tracking-tight text-slate-900">STARTECH <span className="text-green-600">ID</span></CardTitle><CardDescription className="text-slate-500">Identification biométrique du sujet</CardDescription></div>
</CardHeader>
<form onSubmit={handleLogin}>
<CardContent className="space-y-4">
<div className="space-y-2"><Label htmlFor="firstName" className="text-xs uppercase tracking-widest text-slate-500">Prénom</Label><Input id="firstName" placeholder="Ex: Jean" className="bg-slate-50 border-slate-200 text-slate-900 focus:border-green-500 h-11" value={formData.firstName} onChange={e => setFormData({...formData, firstName: e.target.value})} required /></div>
<div className="space-y-2"><Label htmlFor="lastName" className="text-xs uppercase tracking-widest text-slate-500">Nom</Label><Input id="lastName" placeholder="Ex: Dupont" className="bg-slate-50 border-slate-200 text-slate-900 focus:border-green-500 h-11" value={formData.lastName} onChange={e => setFormData({...formData, lastName: e.target.value})} required /></div>
<div className="space-y-2"><Label htmlFor="clientId" className="text-xs uppercase tracking-widest text-slate-500">Code Projet</Label><Input id="clientId" placeholder="Ex: PROJET-A12" className="bg-slate-50 border-slate-200 text-slate-900 focus:border-green-500 h-11" value={formData.clientId} onChange={e => setFormData({...formData, clientId: e.target.value})} /></div>
</CardContent>
<CardFooter><Button type="submit" className="w-full bg-green-600 hover:bg-green-700 text-white h-12 text-lg font-bold shadow-lg shadow-green-200">INITIALISER SESSION</Button></CardFooter>
</form>
</Card>
<div className="absolute bottom-6 right-6 z-20"><Link href="/admin"><Button variant="ghost" className="text-slate-500 hover:text-slate-900 hover:bg-slate-200 text-xs gap-2"><Shield className="w-3 h-3" /> Accès Admin</Button></Link></div>
</div>
)
}
return (
<div className="min-h-screen bg-slate-50 text-slate-900 flex flex-col font-sans">
<header className="border-b border-slate-200 bg-white/80 backdrop-blur-md sticky top-0 z-50 shadow-sm">
<div className="flex h-16 items-center px-6 justify-between">
<div className="flex items-center gap-3 font-bold text-xl tracking-tight text-slate-900"><div className="w-3 h-3 rounded-full bg-green-500 shadow-[0_0_15px_#22c55e] animate-pulse" />STARTECH <span className="text-slate-400 font-normal">VISION</span></div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 px-4 py-1.5 bg-slate-100 rounded-full border border-slate-200">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-green-500 to-emerald-700 flex items-center justify-center text-xs font-bold text-white">{userInfo.firstName.charAt(0)}{userInfo.lastName.charAt(0)}</div>
<div className="flex flex-col"><span className="text-sm font-bold text-slate-900 leading-none">{userInfo.firstName} {userInfo.lastName}</span><span className="text-[10px] text-slate-500 leading-none mt-1">{userInfo.clientId || "ID: GUEST"}</span></div>
</div>
<Button variant="ghost" size="sm" onClick={handleLogout} className="text-xs text-slate-500 hover:text-red-600 hover:bg-red-50">Sortir</Button>
</div>
</div>
</header>
<main className="flex-1 p-4 md:p-6 lg:p-8 overflow-hidden flex flex-col gap-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full min-h-[600px]">
<div className="lg:col-span-7 flex flex-col h-full">
<Card className="border-slate-300 bg-white shadow-xl relative overflow-hidden transition-all duration-500 flex-1 flex flex-col group">
<div className="absolute top-4 left-4 w-16 h-16 border-l-4 border-t-4 border-green-500 z-20 rounded-tl-lg opacity-80" />
<div className="absolute top-4 right-4 w-16 h-16 border-r-4 border-t-4 border-green-500 z-20 rounded-tr-lg opacity-80" />
<div className="absolute bottom-4 left-4 w-16 h-16 border-l-4 border-b-4 border-green-500 z-20 rounded-bl-lg opacity-80" />
<div className="absolute bottom-4 right-4 w-16 h-16 border-r-4 border-b-4 border-green-500 z-20 rounded-br-lg opacity-80" />
{isRecording && <div className="absolute inset-x-0 h-0.5 bg-green-500 shadow-[0_0_20px_#22c55e] z-10 animate-[scan_3s_ease-in-out_infinite]" style={{ top: '0%' }} />}
{isRecording && <div className="absolute top-0 w-full h-1 bg-red-500 animate-pulse z-30" />}
<CardContent className="p-0 flex-1 relative flex flex-col items-center justify-center bg-black overflow-hidden rounded-md m-1">
<canvas ref={canvasRef} width="480" height="360" className="hidden" />
<div className="absolute inset-0 w-full h-full relative">
<video ref={videoRef} autoPlay playsInline muted className="w-full h-full object-cover transform scale-x-[-1]" />
{faceCoords && (
<div className="absolute border-2 border-green-500 z-50 transition-all duration-100 ease-linear shadow-[0_0_15px_#22c55e]" style={{ left: `${(faceCoords.x / 480) * 100}%`, top: `${(faceCoords.y / 360) * 100}%`, width: `${(faceCoords.w / 480) * 100}%`, height: `${(faceCoords.h / 360) * 100}%`, transform: 'scaleX(-1)' }}>
<div className="absolute -top-6 left-0 bg-green-500 text-black text-[10px] font-bold px-1 scale-x-[-1]">TARGET LOCKED</div>
</div>
)}
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0)_50%,rgba(0,0,0,0.1)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] z-10 bg-[length:100%_4px,6px_100%] pointer-events-none" />
</div>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10 opacity-30"><Target className="w-64 h-64 text-white stroke-1" /></div>
<div className="z-20 w-full px-8 pb-8 mt-auto absolute bottom-0">
<div className="flex justify-between items-end mb-8">
<div>
<div className="flex items-center gap-2 mb-2"><Badge variant="outline" className={`px-3 py-1 border-none backdrop-blur-md ${isRecording ? "bg-red-600 text-white animate-pulse" : "bg-white/20 text-white"}`}><div className={`w-2 h-2 rounded-full mr-2 ${isRecording ? "bg-white" : "bg-slate-300"}`} />{isRecording ? "ENREGISTREMENT" : "PRÊT"}</Badge></div>
<div className="text-7xl font-mono font-bold text-white tabular-nums tracking-tighter drop-shadow-lg">{formatTime(sessionTime)}</div>
</div>
<div className="text-right">
<div className="bg-white/90 backdrop-blur-xl px-6 py-4 rounded-xl border border-white shadow-2xl">
<span className="block text-[10px] text-slate-500 uppercase tracking-widest mb-1 font-bold">Emotion Dominante</span>
<span className="text-3xl font-bold text-slate-900 flex items-center justify-end gap-3">{getEmotionDisplay(currentMetrics.emotion)}</span>
</div>
</div>
</div>
<div className="flex items-center justify-center gap-8 pt-6 border-t border-white/20">
<Button size="icon" variant="outline" onClick={handleReset} className="h-14 w-14 rounded-full border border-white/20 bg-white/10 text-white hover:bg-white hover:text-black backdrop-blur-md transition-all"><RotateCcw className="h-5 w-5" /></Button>
{!isRecording ? (<Button onClick={handleStartStop} className="bg-green-600 hover:bg-green-500 text-white px-10 h-14 text-lg font-bold rounded-full shadow-[0_0_20px_rgba(34,197,94,0.4)] transition-all hover:scale-105"><Play className="mr-2 h-5 w-5 fill-current" /> DÉMARRER</Button>) : (<Button onClick={handleStartStop} variant="destructive" className="px-10 h-14 text-lg font-bold rounded-full shadow-[0_0_20px_rgba(239,68,68,0.4)] transition-all hover:scale-105"><Square className="mr-2 h-5 w-5 fill-current" /> STOP</Button>)}
</div>
</div>
</CardContent>
</Card>
</div>
<div className="lg:col-span-5 flex flex-col h-full gap-4">
<MetricsPanel metrics={currentMetrics} />
<div className="p-4 rounded-xl bg-white border border-slate-200 shadow-sm flex justify-between items-center text-xs font-mono text-slate-500 mt-auto">
<div className="flex items-center gap-2"><Zap className={`w-3 h-3 ${isConnected ? "text-green-500" : "text-red-500"}`} />SERVEUR STARTECH</div>
<span className={isConnected ? "text-green-600 font-bold bg-green-50 px-2 py-1 rounded" : "text-red-500 bg-red-50 px-2 py-1 rounded"}>{isConnected ? "ONLINE" : "OFFLINE"}</span>
</div>
</div>
</div>
</main>
<style jsx global>{` @keyframes scan { 0% { top: 0%; opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { top: 100%; opacity: 0; } } @keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .animate-spin-slow { animation: spin-slow 10s linear infinite; } `}</style>
</div>
)
} |