IDKHowToCodeFr/tinyml-backend
1
1"use client";2 3import { motion } from "framer-motion";4import Link from "next/link";5import { useTelemetry } from "@/context/TelemetryContext";6 7const easeFluid: [number, number, number, number] = [0.16, 1, 0.3, 1];8 9const staggerContainer = {10 hidden: { opacity: 0 },11 show: {12 opacity: 1,13 transition: { staggerChildren: 0.1, delayChildren: 0.1 }14 }15};16 17const fadeUp = {18 hidden: { opacity: 0, y: 20 },19 show: { opacity: 1, y: 0, transition: { duration: 0.8, ease: easeFluid } }20};21 22export default function Dashboard() {23 const { data, stream, status } = useTelemetry();24 25 return (26 <main className="w-full flex-1 bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">27 {/* Top Nav (Mechanical) */}28 29 <motion.div 30 variants={staggerContainer}31 initial="hidden"32 animate="show"33 className="max-w-350 mx-auto w-full px-8 py-16 flex-1 flex flex-col"34 >35 <motion.header variants={fadeUp} className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">36 <div>37 <div className="text-[12px] text-hazard font-bold tracking-widest mb-4 flex items-center gap-2">38 <motion.div 39 animate={{ opacity: [1, 0.2, 1] }} 40 transition={{ duration: 2, repeat: Infinity, ease: "linear" }}41 className="w-2 h-2 rounded-full bg-hazard"42 />43 {"/// TELEMETRY_STREAM_ACTIVE"}44 </div>45 <h1 className="text-[clamp(3rem,6vw,6rem)] leading-[0.9]">46 PATIENT<br />TELEMETRY47 </h1>48 </div>49 <div className="text-right hidden md:block">50 <div className={`text-[14px] font-bold uppercase tracking-wider border-2 border-ink px-4 py-2 text-canvas-cream ${status === 'reconnecting' ? 'bg-hazard animate-pulse' : 'bg-ink'}`}>51 STATUS: {status === 'reconnecting' ? "RECONNECTING..." : (data ? "CONNECTED" : "AWAITING SIGNAL")}52 </div>53 </div>54 </motion.header>55 56 {/* Dashboard Grid - Brutalist Tables */}57 <motion.div variants={staggerContainer} className="grid grid-cols-1 bg-ink border-2 border-ink" aria-live="polite">58 59 {/* Main Status Panel */}60 <motion.div variants={fadeUp} className="bg-canvas-cream flex flex-col">61 <div className="border-b-2 border-ink p-4 flex justify-between items-center bg-white">62 <h2 className="text-[14px] font-bold tracking-wider">{'< LIVE_VITALS >'}</h2>63 {data?.prediction ? (64 (() => {65 const getStatusStyles = (label: string) => {66 const normalized = (label || "").toLowerCase();67 if (normalized === 'healthy' || normalized === 'normal') return "bg-[#d1fae5] text-[#065f46] border-[#065f46]";68 if (normalized.includes('heart')) return "bg-[#fef08a] text-[#854d0e] border-[#854d0e]";69 if (normalized.includes('asthma')) return "bg-[#e0f2fe] text-[#075985] border-[#075985]";70 if (normalized.includes('hypertension')) return "bg-[#f3e8ff] text-[#6b21a8] border-[#6b21a8]";71 if (normalized.includes('diabetes')) return "bg-[#ffedd5] text-[#9a3412] border-[#9a3412]";72 return "bg-hazard text-white border-hazard"; 73 };74 return (75 <motion.div 76 key={data.prediction.label}77 initial={{ scale: 0.95, opacity: 0 }}78 animate={{ scale: 1, opacity: 1 }}79 transition={{ type: "spring", stiffness: 300, damping: 20 }}80 className={`px-4 py-2 text-[14px] font-bold uppercase tracking-wider border-2 ${getStatusStyles(data.prediction.label)}`}81 >82 {data.prediction.is_at_risk ? '[!]' : '[OK]'} {data.prediction.label} ({(data.prediction.confidence * 100).toFixed(1)}%)83 </motion.div>84 );85 })()86 ) : (87 <div className="px-4 py-2 text-[14px] font-bold tracking-wider border-2 border-ink text-slate animate-pulse">88 INITIALIZING...89 </div>90 )}91 </div>92 93 <motion.div variants={staggerContainer} className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-ink border-b-2 border-ink">94 <VitalCard label="HEART_RATE" value={data?.sensor_data?.Heart_Rate ? Math.round(data.sensor_data.Heart_Rate) : "---"} unit="BPM" />95 <VitalCard label="O2_SATURATION" value={data?.sensor_data?.SpO2_Level ? Math.round(data.sensor_data.SpO2_Level) : "---"} unit="%" />96 <VitalCard label="BLOOD_PRESSURE" value={data?.sensor_data ? `${Math.round(data.sensor_data.Systolic_BP)}/${Math.round(data.sensor_data.Diastolic_BP)}` : null} unit="MMHG" />97 <VitalCard label="CORE_TEMP" value={data?.sensor_data?.Body_Temp ? data.sensor_data.Body_Temp.toFixed(1) : "---"} unit="°C" />98 </motion.div>99 100 {/* 2-Column Mini Graphs */}101 <motion.div variants={fadeUp} className="grid grid-cols-1 lg:grid-cols-2 gap-px bg-ink border-b-2 border-ink h-40">102 <div className="bg-white p-3 flex flex-col group"><div className="text-[11px] font-bold text-slate tracking-widest mb-2 group-hover:text-hazard transition-colors">HR_STREAM</div><div className="flex-1 min-h-0 relative"><TelemetryGraph stream={stream.hr} min={60} max={150} color="var(--color-ink)" fill="rgba(5,5,5,0.05)" /></div></div>103 <div className="bg-white p-3 flex flex-col group"><div className="text-[11px] font-bold text-slate tracking-widest mb-2 group-hover:text-hazard transition-colors">TEMP_STREAM</div><div className="flex-1 min-h-0 relative"><TelemetryGraph stream={stream.temp} min={36.0} max={39.0} color="var(--color-ink)" fill="rgba(5,5,5,0.05)" /></div></div>104 </motion.div>105 106 {/* Diagnostic Probabilities */}107 <motion.div variants={fadeUp} className="bg-white flex flex-col min-h-70">108 <div className="border-b-2 border-ink p-3 px-6 flex justify-between items-center bg-canvas-cream">109 <h2 className="text-[12px] font-bold tracking-widest uppercase">{'< DIAGNOSTIC_PROBABILITIES >'}</h2>110 <div className="text-[11px] font-bold tracking-wider text-hazard uppercase">CLASS / LIKELIHOOD</div>111 </div>112 <div className="grow p-6 bg-white flex flex-col justify-center gap-6">113 {data?.prediction?.disease_probs ? (114 Object.entries(data.prediction.disease_probs)115 .sort((a, b) => (b[1] as number) - (a[1] as number))116 .map(([disease, prob], i) => {117 const pct = ((prob as number) * 100).toFixed(1);118 const isRisk = (prob as number) > 0.4 && disease.toLowerCase() !== 'normal' && disease.toLowerCase() !== 'healthy';119 return (120 <motion.div 121 key={disease} 122 initial={{ opacity: 0, x: -20 }}123 animate={{ opacity: 1, x: 0 }}124 transition={{ duration: 0.5, delay: i * 0.1, ease: easeFluid }}125 className="flex flex-col gap-2"126 >127 <div className="flex justify-between text-[12px] font-bold uppercase tracking-widest">128 <span className={isRisk ? "text-hazard drop-shadow-[0_0_2px_rgba(230,25,25,0.5)]" : "text-ink"}>{disease}</span>129 <span className="tabular-nums">{pct}%</span>130 </div>131 <div className="h-6 bg-canvas-cream border-2 border-ink overflow-hidden relative">132 {/* Blueprint tick marks */}133 <div className="absolute inset-0 z-0 opacity-20" style={{ backgroundImage: 'linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '10% 100%' }}></div>134 <motion.div 135 className={`h-full relative z-10 ${isRisk ? 'bg-hazard shadow-[0_0_8px_var(--hazard-red)]' : 'bg-ink'}`}136 initial={{ width: "0%" }}137 animate={{ width: `${pct}%` }}138 transition={{ duration: 0.8, ease: easeFluid }}139 />140 </div>141 </motion.div>142 )143 })144 ) : (145 <div className="h-full flex items-center justify-center text-[12px] font-bold text-slate tracking-widest animate-pulse">146 AWAITING CLASSIFICATION DATA...147 </div>148 )}149 </div>150 </motion.div>151 152 </motion.div>153 </motion.div>154 </motion.div>155 </main>156 );157}158 159function VitalCard({ label, value, unit }: { label: string, value: string | number | null | undefined, unit: string }) {160 return (161 <motion.div variants={fadeUp} className="bg-canvas-cream p-8 flex flex-col hover:bg-white transition-colors cursor-default overflow-hidden group">162 <div className="text-[11px] font-bold text-slate tracking-widest border-b border-ink/20 pb-2 mb-4 group-hover:text-hazard transition-colors">163 {label}164 </div>165 <div>166 <motion.div 167 key={String(value)}168 initial={{ opacity: 0.5, y: 5 }}169 animate={{ opacity: 1, y: 0 }}170 className="font-sans text-[clamp(40px,3.5vw,60px)] font-black tracking-tighter tabular-nums leading-none mb-1 break-all"171 >172 {value !== null && value !== undefined ? value : "---"}173 </motion.div>174 <div className="text-[12px] font-bold tracking-wider text-hazard">175 [{unit}]176 </div>177 </div>178 </motion.div>179 );180}181 182function TelemetryGraph({ stream, min, max, color, fill }: { stream: number[], min: number, max: number, color: string, fill: string }) {183 const range = max - min;184 185 const points = stream.map((val, i) => {186 const x = (i / (stream.length - 1)) * 100;187 const y = Math.max(0, Math.min(100, 100 - (((val - min) / range) * 100)));188 return `${x},${y}`;189 });190 191 const firstY = points[0] ? points[0].split(',')[1] : 100;192 const pathD = `M 0,100 L 0,${firstY} L ${points.join(" L ")} L 100,100 Z`;193 const lineD = `M 0,${firstY} L ${points.join(" L ")}`;194 const lastPoint = points[points.length - 1];195 const lastY = lastPoint ? lastPoint.split(',')[1] : 100;196 197 return (198 <div className="w-full h-full relative inset-0 border-2 border-ink overflow-hidden bg-white group ">199 {/* Blueprint Grid lines */}200 <div className="relative inset-0 z-0 opacity-[0.1]" style={{ backgroundImage: 'linear-gradient(var(--ink-black) 1px, transparent 1px), linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '20px 20px' }}></div>201 202 {/* Signal trace */}203 <div className="absolute inset-0 z-10 overflow-hidden">204 <svg className="w-full h-full block" viewBox="0 0 100 100" preserveAspectRatio="none">205 {/* Fill Area */}206 <path 207 d={pathD} 208 fill={fill} 209 className="transition-all duration-300 ease-linear"210 />211 {/* Stroke Line (Glow) */}212 <path 213 d={lineD} 214 fill="none" 215 stroke={color} 216 strokeWidth="1.5" 217 vectorEffect="non-scaling-stroke"218 className="transition-all duration-300 ease-linear"219 style={{ filter: `drop-shadow(0 0 4px ${color})` }}220 />221 {/* Leading Dot */}222 <circle 223 cx="100" 224 cy={lastY} 225 r="1.5" 226 fill={color} 227 className="transition-all duration-300 ease-linear"228 style={{ filter: `drop-shadow(0 0 4px ${color})` }}229 />230 </svg>231 </div>232 233 {/* Sweeping Radar Line */}234 <motion.div 235 className="relative top-0 bottom-0 w-0.5 z-20 opacity-40 pointer-events-none"236 style={{ backgroundColor: color, boxShadow: `0 0 12px 2px ${color}` }}237 initial={{ left: "0%" }}238 animate={{ left: "100%" }}239 transition={{ duration: 1.5, ease: "linear", repeat: Infinity }}240 />241 242 {/* Terminal Scanline Overlay */}243 <div className="relative inset-0 pointer-events-none z-30 opacity-40 bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.2)_50%)] bg-size-[100%_4px]"></div>244 </div>245 );246}247 