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>
  )
}