IDKHowToCodeFr/tinyml-backend
1
1"use client";2 3import { motion } from "framer-motion";4import Link from "next/link";5import { useState, useEffect } from "react";6import ShapChart from "@/components/ShapChart";7 8const easeFluid: [number, number, number, number] = [0.16, 1, 0.3, 1];9 10const staggerContainer = {11 hidden: { opacity: 0 },12 show: {13 opacity: 1,14 transition: { staggerChildren: 0.1, delayChildren: 0.1 }15 }16};17 18const fadeUp = {19 hidden: { opacity: 0, y: 20 },20 show: { opacity: 1, y: 0, transition: { duration: 0.8, ease: easeFluid } }21};22 23export default function Analysis() {24 const [formData, setFormData] = useState({25 Heart_Rate: 75,26 SpO2_Level: 98,27 Systolic_BP: 120,28 Diastolic_BP: 80,29 Body_Temp: 37.030 });31 32 useEffect(() => {33 setFormData({34 Heart_Rate: Math.floor(Math.random() * (140 - 60 + 1)) + 60,35 SpO2_Level: Math.floor(Math.random() * (100 - 90 + 1)) + 90,36 Systolic_BP: Math.floor(Math.random() * (160 - 90 + 1)) + 90,37 Diastolic_BP: Math.floor(Math.random() * (100 - 60 + 1)) + 60,38 Body_Temp: Number((Math.random() * (39.5 - 36.0) + 36.0).toFixed(1))39 });40 }, []);41 42 const [submittedData, setSubmittedData] = useState<any>(null);43 const [prediction, setPrediction] = useState<any>(null);44 const [loading, setLoading] = useState(false);45 46 const handlePredict = async () => {47 setLoading(true);48 setPrediction(null);49 setSubmittedData(null);50 51 try {52 const res = await fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/predict", {53 method: "POST",54 headers: { "Content-Type": "application/json" },55 body: JSON.stringify(formData)56 });57 const data = await res.json();58 setPrediction(data);59 setSubmittedData(formData);60 } catch (err) {61 console.error(err);62 } finally {63 setLoading(false);64 }65 };66 67 return (68 <main className="w-full flex-1 bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">69 {/* Top Nav (Mechanical) */}70 71 <motion.div72 variants={staggerContainer}73 initial="hidden"74 animate="show"75 className="max-w-350 mx-auto w-full px-8 py-16 flex-1 flex flex-col"76 >77 <motion.header variants={fadeUp} className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">78 <div>79 <div className="text-[12px] text-hazard font-bold tracking-widest mb-4">80 {"/// EXPLAINABLE_AI_MODULE"}81 </div>82 <h1 className="text-[clamp(3rem,6vw,6rem)]">83 MODEL<br />SIMULATOR84 </h1>85 </div>86 <div className="text-right hidden md:block max-w-sm">87 <div className="text-[14px] font-bold uppercase tracking-wider border-2 border-ink px-4 py-2 bg-ink text-canvas-cream text-left">88 MANUAL OVERRIDE. INPUT CUSTOM TELEMETRY TO TEST ENSEMBLE CLASSIFIER AND VIEW SHAPLEY FEATURE IMPORTANCE VECTORS.89 </div>90 </div>91 </motion.header>92 93 <motion.div variants={staggerContainer} className="grid grid-cols-1 lg:grid-cols-12 gap-px bg-ink border-2 border-ink">94 95 {/* Input Panel */}96 <motion.div variants={fadeUp} className="lg:col-span-5 bg-canvas-cream flex flex-col">97 <div className="border-b-2 border-ink p-4 bg-white flex justify-between items-center">98 <h2 className="text-[14px] font-bold tracking-wider">{'< MANUAL_INPUT_MATRIX >'}</h2>99 </div>100 101 <div className="p-8 flex flex-col gap-8 grow">102 <InputControl label="HEART_RATE (BPM)" value={formData.Heart_Rate} min={40} max={200} onChange={(v: number) => setFormData({ ...formData, Heart_Rate: v })} />103 <InputControl label="SPO2_LEVEL (%)" value={formData.SpO2_Level} min={70} max={100} onChange={(v: number) => setFormData({ ...formData, SpO2_Level: v })} />104 <InputControl label="SYSTOLIC_BP (MMHG)" value={formData.Systolic_BP} min={80} max={220} onChange={(v: number) => setFormData({ ...formData, Systolic_BP: v })} />105 <InputControl label="DIASTOLIC_BP (MMHG)" value={formData.Diastolic_BP} min={40} max={140} onChange={(v: number) => setFormData({ ...formData, Diastolic_BP: v })} />106 <InputControl label="BODY_TEMP (°C)" value={formData.Body_Temp} min={30} max={42} step={0.1} onChange={(v: number) => setFormData({ ...formData, Body_Temp: v })} />107 108 <motion.button109 onClick={handlePredict}110 disabled={loading}111 whileHover={{ y: 2, x: 2, boxShadow: "4px 4px 0px 0px rgba(0,0,0,1)" }}112 whileTap={{ y: 6, x: 6, boxShadow: "none" }}113 transition={{ duration: 0.1 }}114 className="mt-6 border-[3px] border-ink bg-hazard text-white py-6 font-bold text-[18px] tracking-[0.2em] shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] uppercase disabled:opacity-50 disabled:cursor-not-allowed"115 >116 {loading ? 'PROCESSING_REQUEST...' : '[ EXECUTE_INFERENCE ]'}117 </motion.button>118 119 {/* Terminal Readout */}120 <div className="mt-8 grow border-[3px] border-ink bg-ink text-[#4AF626] p-0 flex flex-col min-h-37.5 shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] relative overflow-hidden group">121 <div className="absolute inset-0 pointer-events-none opacity-10 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,255,255,0.1)_2px,rgba(255,255,255,0.1)_4px)] z-10" />122 <div className="text-[10px] font-bold tracking-[0.2em] text-ink bg-white border-b-[3px] border-ink px-4 py-3 flex justify-between uppercase">123 <span>RAW_PAYLOAD_PREVIEW</span>124 <span>{JSON.stringify(formData).length} BYTES {"// SYS_READY"}</span>125 </div>126 <div className="p-6 grow flex flex-col relative z-0">127 <pre className="text-[12px] font-mono whitespace-pre-wrap grow overflow-auto leading-relaxed">128 {`{129 "Heart_Rate": ${formData.Heart_Rate},130 "SpO2_Level": ${formData.SpO2_Level},131 "Systolic_BP": ${formData.Systolic_BP},132 "Diastolic_BP": ${formData.Diastolic_BP},133 "Body_Temp": ${formData.Body_Temp.toFixed(1)}134}`}135 <span className="inline-block w-2 h-4 bg-[#4AF626] animate-pulse ml-1 align-middle" />136 </pre>137 </div>138 </div>139 </div>140 </motion.div>141 142 {/* Results Panel */}143 <motion.div variants={fadeUp} className="lg:col-span-7 bg-canvas-cream flex flex-col overflow-hidden">144 <div className="border-b-2 border-ink p-4 bg-white flex justify-between items-center">145 <h2 className="text-[14px] font-bold tracking-wider">{'< INFERENCE_RESULTS_&_XAI >'}</h2>146 </div>147 148 <div className="grow flex flex-col relative bg-white">149 <div className="absolute inset-0 z-0 opacity-10 pointer-events-none" style={{ backgroundImage: 'linear-gradient(var(--ink-black) 1px, transparent 1px), linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '40px 40px' }}></div>150 151 <div className="z-10 p-8 flex flex-col h-full">152 {prediction ? (153 <motion.div154 initial={{ opacity: 0, scale: 0.95 }}155 animate={{ opacity: 1, scale: 1 }}156 transition={{ duration: 0.6, ease: easeFluid }}157 >158 <div className="mb-12">159 <div className="text-[11px] font-bold text-slate tracking-widest mb-4">MODEL_OUTPUT</div>160 161 {(() => {162 const getStatusStyles = (label: string) => {163 const normalized = (label || "").toLowerCase();164 if (normalized === 'healthy' || normalized === 'normal') return "bg-[#d1fae5] text-[#065f46] border-[#065f46]";165 if (normalized.includes('heart')) return "bg-[#fef08a] text-[#854d0e] border-[#854d0e]";166 if (normalized.includes('asthma')) return "bg-[#e0f2fe] text-[#075985] border-[#075985]";167 if (normalized.includes('hypertension')) return "bg-[#f3e8ff] text-[#6b21a8] border-[#6b21a8]";168 if (normalized.includes('diabetes')) return "bg-[#ffedd5] text-[#9a3412] border-[#9a3412]";169 return "bg-hazard text-white border-hazard";170 };171 172 return (173 <div className={`border-2 p-6 flex flex-col mb-4 ${getStatusStyles(prediction.prediction ? prediction.prediction_label : 'Healthy')}`}>174 <div className="font-sans text-[clamp(28px,3vw,56px)] font-black tracking-tighter leading-none mb-2 uppercase wrap-break-words">175 {prediction.prediction ? `[!]_${prediction.prediction_label.replace(" ", "_")}` : 'NORMAL'}176 </div>177 <div className="text-[14px] font-bold tracking-widest">178 CONFIDENCE: {(prediction.probability * 100).toFixed(1)}%179 </div>180 </div>181 );182 })()}183 184 {/* Ensemble Diagnostics */}185 {prediction.model_outputs && (186 <div className="border-2 border-ink bg-white flex flex-col text-[12px] font-bold uppercase tracking-wider">187 <div className="border-b-2 border-ink px-4 py-2 bg-ink text-canvas-cream flex justify-between">188 <span>{'< ENSEMBLE_STACK_DIAGNOSTICS >'}</span>189 <span className="text-hazard">WEIGHTED_VOTING</span>190 </div>191 <div className="divide-y-2 divide-ink">192 {Object.keys(prediction.model_outputs).map((modelName, index) => (193 <motion.div194 key={modelName}195 initial={{ opacity: 0, x: 20 }}196 animate={{ opacity: 1, x: 0 }}197 transition={{ duration: 0.4, delay: index * 0.1, ease: "easeOut" }}198 className="flex justify-between items-center p-3 hover:bg-canvas-cream transition-colors"199 >200 <div className="w-[20%] text-slate">[{modelName}]</div>201 <div className={`w-[40%] ${prediction.model_outputs[modelName] !== 'Healthy' ? 'text-hazard' : ''}`}>202 {prediction.model_outputs[modelName] !== 'Healthy' ? '[!] ' + prediction.model_outputs[modelName] : '[OK] HEALTHY'}203 </div>204 <div className="w-[20%] text-right">205 CONF: {(prediction.model_probs[modelName] * 100).toFixed(1)}%206 </div>207 <div className="w-[20%] text-right text-slate">208 W: {prediction.weights[modelName].toFixed(2)}209 </div>210 </motion.div>211 ))}212 </div>213 </div>214 )}215 </div>216 217 <div className="flex-1 border-2 border-ink bg-white p-8 flex flex-col min-h-75">218 <div className="text-[11px] font-bold text-slate tracking-widest mb-6 border-b-2 border-ink pb-2">SHAPLEY_ADDITIVE_EXPLANATIONS</div>219 <div className="flex-1">220 <ShapChart patientData={submittedData} />221 </div>222 </div>223 </motion.div>224 ) : (225 <div className="h-full flex items-center justify-center">226 <div className="text-slate text-[14px] font-bold tracking-widest uppercase border-2 border-slate px-6 py-4 animate-pulse">227 {loading ? 'CALCULATING_SHAP_VALUES...' : 'AWAITING_INPUT...'}228 </div>229 </div>230 )}231 </div>232 </div>233 </motion.div>234 235 </motion.div>236 </motion.div>237 </main>238 );239}240 241function InputControl({ label, value, min, max, step = 1, onChange }: any) {242 return (243 <div className="flex flex-col gap-3 group">244 <div className="flex justify-between items-end">245 <label className="text-[13px] font-bold text-slate tracking-widest group-hover:text-ink transition-colors">{label}</label>246 <motion.span247 key={value}248 initial={{ opacity: 0.5, y: -5 }}249 animate={{ opacity: 1, y: 0 }}250 className="font-sans text-[32px] font-black leading-none tabular-nums tracking-tighter"251 >252 {value}253 </motion.span>254 </div>255 <div className="relative border-2 border-ink p-1 bg-white">256 <input257 type="range"258 min={min}259 max={max}260 step={step}261 value={value}262 onChange={(e) => onChange(Number(e.target.value))}263 className="w-full appearance-none h-4 bg-ink/10 outline-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-8 [&::-webkit-slider-thumb]:bg-ink [&::-webkit-slider-thumb]:cursor-pointer relative z-10 block"264 />265 </div>266 </div>267 );268}269 