IDKHowToCodeFr commited on
Commit
1e77bf3
·
verified ·
1 Parent(s): aad3f84

Upload folder using huggingface_hub

Browse files
frontend/src/app/history/page.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import Link from "next/link";
4
- import { useEffect, useState, useCallback, useMemo } from "react";
5
  import {
6
  getSessionHistory,
7
  downloadSessionCSV,
@@ -144,6 +144,8 @@ export default function PatientHistory() {
144
  const [showReportModal, setShowReportModal] = useState(false);
145
  const [reportCopied, setReportCopied] = useState(false);
146
 
 
 
147
  const refreshSessionHistory = useCallback(() => {
148
  setRecords(getSessionHistory());
149
  setLoading(false);
@@ -209,6 +211,9 @@ export default function PatientHistory() {
209
  sessionStorage.setItem("heartflow_session_log_v2", JSON.stringify(SAMPLE_CLINICAL_RECORDS));
210
  }
211
  setRecords(SAMPLE_CLINICAL_RECORDS);
 
 
 
212
  };
213
 
214
  const handleOpenReport = () => {
@@ -441,7 +446,7 @@ ${metrics.narrative}
441
  </div>
442
 
443
  {/* Telemetry Log Table */}
444
- <div className="crosshair-corner border-2 border-ink bg-ink">
445
  <div className="overflow-x-auto bg-canvas-cream">
446
  <table className="w-full text-left">
447
  <thead className="bg-ink text-white font-bold text-[12px] tracking-widest uppercase">
 
1
  "use client";
2
 
3
  import Link from "next/link";
4
+ import { useEffect, useState, useCallback, useMemo, useRef } from "react";
5
  import {
6
  getSessionHistory,
7
  downloadSessionCSV,
 
144
  const [showReportModal, setShowReportModal] = useState(false);
145
  const [reportCopied, setReportCopied] = useState(false);
146
 
147
+ const tableSectionRef = useRef<HTMLDivElement>(null);
148
+
149
  const refreshSessionHistory = useCallback(() => {
150
  setRecords(getSessionHistory());
151
  setLoading(false);
 
211
  sessionStorage.setItem("heartflow_session_log_v2", JSON.stringify(SAMPLE_CLINICAL_RECORDS));
212
  }
213
  setRecords(SAMPLE_CLINICAL_RECORDS);
214
+ setTimeout(() => {
215
+ tableSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
216
+ }, 60);
217
  };
218
 
219
  const handleOpenReport = () => {
 
446
  </div>
447
 
448
  {/* Telemetry Log Table */}
449
+ <div ref={tableSectionRef} className="crosshair-corner border-2 border-ink bg-ink scroll-mt-24">
450
  <div className="overflow-x-auto bg-canvas-cream">
451
  <table className="w-full text-left">
452
  <thead className="bg-ink text-white font-bold text-[12px] tracking-widest uppercase">
frontend/src/app/mlops/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import { useState } from "react";
4
  import {
5
  Download,
6
  FileSpreadsheet,
@@ -99,6 +99,21 @@ export default function MLOps() {
99
  const [status, setStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
100
  const [message, setMessage] = useState("");
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  const parseAndValidateCSV = async (fileToValidate: File) => {
103
  try {
104
  const text = await fileToValidate.text();
@@ -201,6 +216,7 @@ export default function MLOps() {
201
  setFile(newFile);
202
  if (newFile) {
203
  parseAndValidateCSV(newFile);
 
204
  } else {
205
  setValidation(null);
206
  setStatus("idle");
@@ -229,6 +245,9 @@ export default function MLOps() {
229
  const handleUpload = async () => {
230
  if (!file) return;
231
  setStatus("uploading");
 
 
 
232
 
233
  const formData = new FormData();
234
  formData.append("file", file);
@@ -245,6 +264,9 @@ export default function MLOps() {
245
  if (data.status === "success" || !data.error) {
246
  setStatus("success");
247
  setMessage(data.message || "Dataset merged and ensemble models successfully retrained!");
 
 
 
248
  return;
249
  } else {
250
  throw new Error(data.error || "Retraining failed");
@@ -263,6 +285,9 @@ export default function MLOps() {
263
  setStatus("error");
264
  setMessage("Validation failed. Please verify that all 5 continuous physiological features are present.");
265
  }
 
 
 
266
  };
267
 
268
  return (
@@ -359,7 +384,11 @@ export default function MLOps() {
359
  </section>
360
 
361
  {/* Upload & Staging Workspace - Unified Monolithic Workbench */}
362
- <div className="border-2 border-ink bg-white grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x-2 divide-ink shadow-[4px_4px_0px_0px_#141413]">
 
 
 
 
363
  {/* Dropzone Panel */}
364
  <div className="lg:col-span-5 p-6 sm:p-8 flex flex-col justify-between">
365
  <div>
@@ -422,7 +451,8 @@ export default function MLOps() {
422
 
423
  {status !== "idle" && (
424
  <div
425
- className={`p-3 border-2 text-[12px] font-bold uppercase tracking-wider ${
 
426
  status === "success"
427
  ? "bg-[#d1fae5] border-[#065f46] text-[#065f46]"
428
  : status === "error"
 
1
  "use client";
2
 
3
+ import { useState, useRef } from "react";
4
  import {
5
  Download,
6
  FileSpreadsheet,
 
99
  const [status, setStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
100
  const [message, setMessage] = useState("");
101
 
102
+ const stagingWorkbenchRef = useRef<HTMLDivElement>(null);
103
+ const statusBannerRef = useRef<HTMLDivElement>(null);
104
+
105
+ const scrollToWorkbench = () => {
106
+ if (typeof window === "undefined") return;
107
+ requestAnimationFrame(() => {
108
+ setTimeout(() => {
109
+ stagingWorkbenchRef.current?.scrollIntoView({
110
+ behavior: "smooth",
111
+ block: "start",
112
+ });
113
+ }, 50);
114
+ });
115
+ };
116
+
117
  const parseAndValidateCSV = async (fileToValidate: File) => {
118
  try {
119
  const text = await fileToValidate.text();
 
216
  setFile(newFile);
217
  if (newFile) {
218
  parseAndValidateCSV(newFile);
219
+ scrollToWorkbench();
220
  } else {
221
  setValidation(null);
222
  setStatus("idle");
 
245
  const handleUpload = async () => {
246
  if (!file) return;
247
  setStatus("uploading");
248
+ setTimeout(() => {
249
+ statusBannerRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
250
+ }, 60);
251
 
252
  const formData = new FormData();
253
  formData.append("file", file);
 
264
  if (data.status === "success" || !data.error) {
265
  setStatus("success");
266
  setMessage(data.message || "Dataset merged and ensemble models successfully retrained!");
267
+ setTimeout(() => {
268
+ statusBannerRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
269
+ }, 80);
270
  return;
271
  } else {
272
  throw new Error(data.error || "Retraining failed");
 
285
  setStatus("error");
286
  setMessage("Validation failed. Please verify that all 5 continuous physiological features are present.");
287
  }
288
+ setTimeout(() => {
289
+ statusBannerRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
290
+ }, 80);
291
  };
292
 
293
  return (
 
384
  </section>
385
 
386
  {/* Upload & Staging Workspace - Unified Monolithic Workbench */}
387
+ <div
388
+ ref={stagingWorkbenchRef}
389
+ id="staging-workbench"
390
+ className="scroll-mt-24 border-2 border-ink bg-white grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x-2 divide-ink shadow-[4px_4px_0px_0px_#141413]"
391
+ >
392
  {/* Dropzone Panel */}
393
  <div className="lg:col-span-5 p-6 sm:p-8 flex flex-col justify-between">
394
  <div>
 
451
 
452
  {status !== "idle" && (
453
  <div
454
+ ref={statusBannerRef}
455
+ className={`p-3 border-2 text-[12px] font-bold uppercase tracking-wider scroll-mt-24 ${
456
  status === "success"
457
  ? "bg-[#d1fae5] border-[#065f46] text-[#065f46]"
458
  : status === "error"
frontend/src/app/simulator/page.tsx CHANGED
@@ -1,10 +1,11 @@
1
  "use client";
2
 
3
- import { useState, useEffect, useCallback } from "react";
4
- import { Sliders, Activity, Cpu, Shuffle, Play, Square, Sparkles } from "lucide-react";
5
  import ShapChart from "@/components/ShapChart";
6
  import { PatientVitals } from "@/lib/edge/types";
7
  import { evaluateEdge } from "@/lib/edge/inference";
 
8
 
9
  interface PredictionResult {
10
  prediction: boolean;
@@ -42,65 +43,59 @@ const DEFAULT_VITALS: PatientVitals = {
42
  Body_Temp: 37.0,
43
  };
44
 
45
- export default function Analysis() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  const [formData, setFormData] = useState<PatientVitals>(DEFAULT_VITALS);
47
- const [submittedData, setSubmittedData] = useState<PatientVitals | null>(DEFAULT_VITALS);
48
- const [prediction, setPrediction] = useState<PredictionResult | null>(() => {
49
- const edge = evaluateEdge(DEFAULT_VITALS);
50
- return {
51
- prediction: Boolean(edge.is_at_risk),
52
- prediction_label: edge.label,
53
- probability: edge.confidence,
54
- model_outputs: edge.model_outputs,
55
- model_probs: edge.model_probs,
56
- weights: edge.weights,
57
- };
58
- });
59
  const [loading, setLoading] = useState(false);
60
- const [autoStream, setAutoStream] = useState(false);
61
-
62
- // Core evaluation function
63
- const executeEvaluation = useCallback((data: PatientVitals) => {
64
- const edgeResult = evaluateEdge(data);
65
- setSubmittedData({ ...data });
66
- setPrediction({
67
- prediction: Boolean(edgeResult.is_at_risk),
68
- prediction_label: edgeResult.label,
69
- probability: edgeResult.confidence,
70
- model_outputs: edgeResult.model_outputs,
71
- model_probs: edgeResult.model_probs,
72
- weights: edgeResult.weights,
73
- });
74
- }, []);
75
-
76
- // Handler to throw randomized values for all features
77
- const handleThrowRandomValues = useCallback(() => {
78
  const randomVitals = generateRandomVitals();
79
  setFormData(randomVitals);
80
- executeEvaluation(randomVitals);
81
- }, [executeEvaluation]);
82
-
83
- // Throw initial random values on mount so the simulator is always dynamic
84
- useEffect(() => {
85
- const timer = setTimeout(() => {
86
- handleThrowRandomValues();
87
- }, 50);
88
- return () => clearTimeout(timer);
89
- }, [handleThrowRandomValues]);
90
-
91
- // Continuous auto-streaming simulation mode
92
- useEffect(() => {
93
- if (!autoStream) return;
94
- const interval = setInterval(() => {
95
- handleThrowRandomValues();
96
- }, 2400);
97
- return () => clearInterval(interval);
98
- }, [autoStream, handleThrowRandomValues]);
99
-
100
- const handlePredict = async (inputData?: PatientVitals) => {
101
- const targetData = inputData || formData;
102
  setLoading(true);
103
- setSubmittedData({ ...targetData });
104
 
105
  try {
106
  const apiUrl = process.env.NEXT_PUBLIC_API_URL;
@@ -113,174 +108,129 @@ export default function Analysis() {
113
  if (res.ok) {
114
  const data = await res.json();
115
  setPrediction(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  setLoading(false);
117
  return;
118
  }
119
  }
120
  } catch {
121
- // Cloud API unreachable, seamlessly evaluate on-chip
122
  }
123
 
124
- // Client-side Zero-Cloud TinyML Ensemble Inference
125
  try {
126
- executeEvaluation(targetData);
127
- } catch (e) {
128
- console.error("Local inference error", e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  } finally {
130
  setLoading(false);
131
  }
132
  };
133
 
134
- // Real-time reactive slider adjustment
135
- const handleSliderChange = (field: keyof PatientVitals, val: number) => {
136
- const updated = { ...formData, [field]: val };
137
- setFormData(updated);
138
- executeEvaluation(updated);
139
- };
140
-
141
- const handleSelectPreset = (presetData: PatientVitals) => {
142
- setFormData({ ...presetData });
143
- executeEvaluation(presetData);
144
- };
145
 
146
  return (
147
  <main className="min-h-dvh bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">
148
- <div className="max-w-350 mx-auto w-full px-4 sm:px-8 py-10 md:py-12 flex-1 flex flex-col">
149
- {/* Monolithic Header */}
150
  <header className="mb-8 border-b-4 border-ink pb-8 flex flex-col md:flex-row justify-between md:items-end gap-6">
151
  <div>
152
- <div className="text-[12px] text-hazard font-bold tracking-widest mb-4 flex items-center gap-2 uppercase">
153
  <Sliders size={14} />
154
- MODEL EXPLAINABILITY &amp; SIMULATION BENCH
155
  </div>
156
- <h1 className="text-[clamp(2.5rem,5vw,5.5rem)] leading-[0.9]">
157
  MODEL<br />SIMULATOR
158
  </h1>
159
  </div>
160
- <div className="flex flex-col items-start md:items-end gap-2 max-w-sm">
161
- <div className="text-[12px] font-bold uppercase tracking-wider border-2 border-ink px-4 py-2 bg-ink text-white">
162
- ENSEMBLE: 5 MODELS · SHAP EXPLAINABILITY
163
- </div>
164
- <div className="text-[11px] text-slate font-bold uppercase tracking-wider">
165
- Deterministic In-Browser Inference · Dual-Polarity SHAP Signs
166
  </div>
167
  </div>
168
  </header>
169
 
170
- {/* Global Control Bar for Randomization */}
171
- <div className="mb-6 border-2 border-ink bg-white p-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 shadow-[4px_4px_0px_0px_#141413]">
172
- <div className="flex items-center gap-2">
173
- <span className="w-2.5 h-2.5 rounded-full bg-hazard" />
174
- <span className="text-[12px] font-black uppercase tracking-wider text-ink">
175
- TELEMETRY GENERATOR
176
- </span>
177
- <span className="text-[10px] font-bold text-slate uppercase bg-canvas-cream border border-ink/30 px-2 py-0.5 hidden sm:inline">
178
- Random Physiologic Vector Sampling
179
- </span>
180
- </div>
181
-
182
- <div className="flex items-center gap-2 w-full sm:w-auto">
183
- {/* Throw Random Values Button */}
184
- <button
185
- type="button"
186
- onClick={handleThrowRandomValues}
187
- className="btn-tactile border-2 border-ink bg-hazard text-white hover:bg-black px-4 py-2 text-[12px] font-black uppercase tracking-wider flex items-center justify-center gap-2 transition-colors cursor-pointer flex-1 sm:flex-none"
188
- title="Throw randomized physiological vitals"
189
- >
190
- <Shuffle size={14} />
191
- <span>Throw Random Values</span>
192
- </button>
193
-
194
- {/* Auto Stream Toggle */}
195
- <button
196
- type="button"
197
- onClick={() => setAutoStream(!autoStream)}
198
- className={`border-2 border-ink px-3 py-2 text-[12px] font-black uppercase tracking-wider flex items-center gap-1.5 transition-colors cursor-pointer ${
199
- autoStream
200
- ? "bg-emerald-600 text-white border-emerald-700 animate-pulse"
201
- : "bg-white text-ink hover:bg-canvas-cream"
202
- }`}
203
- title="Continuously throw random vitals every 2.4 seconds"
204
- >
205
- {autoStream ? <Square size={13} /> : <Play size={13} />}
206
- <span>{autoStream ? "Streaming (Active)" : "Auto-Stream"}</span>
207
- </button>
208
- </div>
209
- </div>
210
-
211
  {/* Main Workbench Grid */}
212
  <div className="crosshair-corner grid grid-cols-1 lg:grid-cols-12 gap-px bg-ink border-2 border-ink items-stretch">
213
- {/* Input Panel - 6 Columns */}
214
- <div className="lg:col-span-6 bg-canvas-cream flex flex-col justify-between">
215
- <div>
216
- <div className="h-14 px-4 sm:px-6 border-b-2 border-ink bg-white flex justify-between items-center">
217
- <h2 className="text-[14px] font-bold tracking-wider uppercase flex items-center gap-2">
218
- <Activity size={16} />
219
- Patient Parameters
220
- </h2>
221
- <div className="flex items-center gap-2">
222
- <button
223
- type="button"
224
- onClick={handleThrowRandomValues}
225
- className="text-[10px] font-black uppercase tracking-wider text-hazard border border-hazard px-2 py-0.5 hover:bg-hazard hover:text-white transition-colors cursor-pointer flex items-center gap-1"
226
- >
227
- <Sparkles size={11} />
228
- <span>Randomize</span>
229
- </button>
230
- <span className="text-[10px] text-slate uppercase font-bold">5 Telemetry Inputs</span>
231
- </div>
232
- </div>
233
 
234
- {/* Quick Clinical Test Vector Presets */}
235
- <div className="p-5 sm:p-6 bg-canvas-cream border-b-2 border-ink flex flex-col gap-3">
236
- <div className="text-[11px] font-bold tracking-widest text-slate uppercase flex justify-between items-center">
237
- <span className="text-ink">Clinical Patient Profiles</span>
238
- <span className="text-hazard text-[10px]">Click to Load Preset</span>
239
- </div>
240
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
241
- {[
242
- {
243
- name: "Resting Normal",
244
- tag: "NORMAL",
245
- desc: "Normotensive baseline vitals",
246
- data: { Heart_Rate: 72, SpO2_Level: 98, Systolic_BP: 118, Diastolic_BP: 78, Body_Temp: 37.0 },
247
- },
248
- {
249
- name: "STEMI / Ischemia",
250
- tag: "CRITICAL",
251
- desc: "Acute ischemic anomaly",
252
- data: { Heart_Rate: 128, SpO2_Level: 91, Systolic_BP: 165, Diastolic_BP: 104, Body_Temp: 37.3 },
253
- },
254
- {
255
- name: "Hypoxic Bradycardia",
256
- tag: "WARNING",
257
- desc: "Hypoxemic rhythm depression",
258
- data: { Heart_Rate: 48, SpO2_Level: 82, Systolic_BP: 88, Diastolic_BP: 54, Body_Temp: 35.8 },
259
- },
260
- {
261
- name: "Hypertensive Crisis",
262
- tag: "WARNING",
263
- desc: "Acute arterial pressure surge",
264
- data: { Heart_Rate: 98, SpO2_Level: 96, Systolic_BP: 195, Diastolic_BP: 118, Body_Temp: 37.1 },
265
- },
266
- ].map((preset) => {
267
- const isActive =
268
- formData.Heart_Rate === preset.data.Heart_Rate &&
269
- formData.SpO2_Level === preset.data.SpO2_Level &&
270
- formData.Systolic_BP === preset.data.Systolic_BP &&
271
- formData.Diastolic_BP === preset.data.Diastolic_BP;
272
-
273
- return (
274
- <button
275
- key={preset.name}
276
- type="button"
277
- onClick={() => handleSelectPreset(preset.data)}
278
- className={`btn-tactile border-2 border-ink p-3.5 text-left transition-colors group flex flex-col justify-between cursor-pointer ${
279
- isActive
280
- ? "bg-ink text-white font-bold"
281
- : "bg-white text-ink hover:bg-canvas-cream"
282
- }`}
283
- >
284
  <div className="flex justify-between items-center text-[12px] font-black uppercase tracking-wider mb-1">
285
  <span>{preset.name}</span>
286
  <span
@@ -297,95 +247,101 @@ export default function Analysis() {
297
  {preset.tag}
298
  </span>
299
  </div>
300
- <div className={`text-[10px] mb-2 ${isActive ? "text-white/75" : "text-slate"}`}>
301
  {preset.desc}
302
  </div>
303
- <div className="text-[10px] font-mono pt-2 border-t border-ink/10 flex justify-between tabular-nums">
304
- <span>{preset.data.Heart_Rate} BPM · {preset.data.SpO2_Level}%</span>
305
- <span className="font-bold">{preset.data.Systolic_BP}/{preset.data.Diastolic_BP}</span>
306
- </div>
307
- </button>
308
- );
309
- })}
310
- </div>
311
  </div>
 
312
 
313
- {/* Slider Inputs with Live Scrubbing */}
314
- <div className="p-6 sm:p-8 flex flex-col gap-6">
315
- <InputControl
316
- label="Heart Rate"
317
- unit="BPM"
318
- value={formData.Heart_Rate}
319
- min={40}
320
- max={200}
321
- onChange={(v: number) => handleSliderChange("Heart_Rate", v)}
322
- />
323
- <InputControl
324
- label="SpO2 Level"
325
- unit="%"
326
- value={formData.SpO2_Level}
327
- min={70}
328
- max={100}
329
- onChange={(v: number) => handleSliderChange("SpO2_Level", v)}
330
- />
331
- <InputControl
332
- label="Systolic BP"
333
- unit="mmHg"
334
- value={formData.Systolic_BP}
335
- min={80}
336
- max={220}
337
- onChange={(v: number) => handleSliderChange("Systolic_BP", v)}
338
- />
339
- <InputControl
340
- label="Diastolic BP"
341
- unit="mmHg"
342
- value={formData.Diastolic_BP}
343
- min={40}
344
- max={140}
345
- onChange={(v: number) => handleSliderChange("Diastolic_BP", v)}
346
- />
347
- <InputControl
348
- label="Body Temperature"
349
- unit="°C"
350
- value={formData.Body_Temp}
351
- min={30}
352
- max={42}
353
- step={0.1}
354
- onChange={(v: number) => handleSliderChange("Body_Temp", v)}
355
- />
356
- </div>
357
  </div>
358
 
359
- <div className="p-6 sm:p-8 pt-0 flex gap-3">
 
360
  <button
361
  type="button"
362
- onClick={handleThrowRandomValues}
363
- className="btn-tactile py-4 px-5 border-2 border-ink bg-canvas-cream hover:bg-white text-ink font-bold text-[13px] tracking-wider uppercase flex items-center justify-center gap-2 cursor-pointer transition-colors"
364
- title="Throw random physiological values"
365
  >
366
  <Shuffle size={14} className="text-hazard" />
367
- <span>Randomize</span>
368
  </button>
369
 
370
  <button
371
  type="button"
372
- onClick={() => handlePredict()}
373
  disabled={loading}
374
- className="btn-tactile-hazard py-4 font-bold text-[14px] sm:text-[15px] tracking-[0.2em] uppercase flex items-center justify-center gap-2 flex-1 cursor-pointer disabled:cursor-not-allowed"
375
  >
376
- {loading ? "Evaluating Model..." : "Re-evaluate Model"}
377
  </button>
378
  </div>
379
  </div>
380
 
381
- {/* Results Panel - 6 Columns */}
382
- <div className="lg:col-span-6 bg-canvas-cream flex flex-col overflow-hidden">
383
  <div className="h-14 px-4 sm:px-6 border-b-2 border-ink bg-white flex justify-between items-center">
384
  <h2 className="text-[14px] font-bold tracking-wider uppercase flex items-center gap-2">
385
  <Cpu size={16} />
386
- Classification &amp; SHAP Attributions
387
  </h2>
388
- <span className="text-[10px] text-slate uppercase font-bold">Ensemble Explainability</span>
 
 
 
 
 
389
  </div>
390
 
391
  <div className="grow flex flex-col relative bg-white">
@@ -398,12 +354,12 @@ export default function Analysis() {
398
  }}
399
  />
400
 
401
- <div className="z-10 p-6 sm:p-8 flex flex-col h-full gap-8">
402
- {prediction ? (
403
  <>
404
  <div>
405
  <div className="text-[11px] font-bold text-slate tracking-widest mb-3 uppercase">
406
- Model Output &amp; Diagnosis
407
  </div>
408
 
409
  {(() => {
@@ -422,16 +378,20 @@ export default function Analysis() {
422
  return "bg-hazard text-white border-hazard";
423
  };
424
 
 
 
 
 
425
  return (
426
  <div
427
- className={`border-2 p-5 sm:p-6 flex flex-col mb-4 ${getStatusStyles(
428
  prediction.prediction ? prediction.prediction_label : "Healthy"
429
  )}`}
430
  >
431
- <div className="font-sans text-[clamp(28px,3vw,52px)] font-black tracking-tighter leading-none mb-2 uppercase wrap-break-words">
432
- {prediction.prediction ? prediction.prediction_label : "Normal"}
433
  </div>
434
- <div className="text-[13px] font-bold tracking-widest">
435
  CONFIDENCE: {(prediction.probability * 100).toFixed(1)}%
436
  </div>
437
  </div>
@@ -439,47 +399,44 @@ export default function Analysis() {
439
  })()}
440
 
441
  {/* Ensemble Diagnostics Breakdown */}
442
- {prediction.model_outputs && (() => {
443
- const outputs = prediction.model_outputs;
444
- return (
445
- <div className="border-2 border-ink bg-white flex flex-col text-[11px] font-bold uppercase tracking-wider">
446
- <div className="border-b-2 border-ink px-4 py-2 bg-ink text-canvas-cream flex justify-between">
447
- <span>Ensemble Stack Diagnostics</span>
448
- <span className="text-hazard">Weighted Voting</span>
449
- </div>
450
- <div className="divide-y-2 divide-ink">
451
- {Object.keys(outputs).map((modelName) => (
 
452
  <div
453
  key={modelName}
454
  className="flex justify-between items-center p-2.5 hover:bg-canvas-cream transition-colors"
455
  >
456
  <div className="w-[25%] text-slate">{modelName}</div>
457
- <div
458
- className={`w-[35%] ${
459
- outputs[modelName] !== "Healthy" ? "text-hazard font-black" : "text-emerald-700"
460
- }`}
461
- >
462
- {outputs[modelName]}
463
  </div>
464
  <div className="w-[20%] text-right tabular-nums">
465
- {(((prediction.model_probs?.[modelName] ?? prediction.probability ?? 0.95)) * 100).toFixed(1)}%
466
  </div>
467
  <div className="w-[20%] text-right text-slate tabular-nums">
468
  W: {(prediction.weights?.[modelName] ?? 0.25).toFixed(2)}
469
  </div>
470
  </div>
471
- ))}
472
- </div>
473
  </div>
474
- );
475
- })()}
476
  </div>
477
 
478
  {/* SHAP Feature Attributions Container */}
479
- <div className="border-2 border-ink bg-white p-5 sm:p-6 flex flex-col">
480
  <div className="text-[12px] font-black uppercase text-ink tracking-wider mb-4 border-b-2 border-ink pb-2 flex justify-between items-center">
481
- <span>SHAP Attributions &amp; Directional Signs</span>
482
- <span className="text-[10px] text-slate font-mono">Kernel / TreeSHAP ABI</span>
483
  </div>
484
  <div className="w-full">
485
  <ShapChart patientData={submittedData} />
@@ -487,9 +444,9 @@ export default function Analysis() {
487
  </div>
488
  </>
489
  ) : (
490
- <div className="h-full flex items-center justify-center">
491
- <div className="text-slate text-[14px] font-bold tracking-widest uppercase border-2 border-slate px-6 py-4">
492
- {loading ? "Evaluating Model..." : "Awaiting Input..."}
493
  </div>
494
  </div>
495
  )}
@@ -516,14 +473,14 @@ function InputControl({ label, value, min, max, step = 1, unit, onChange }: Inpu
516
  return (
517
  <div className="flex flex-col gap-2 group">
518
  <div className="flex justify-between items-baseline">
519
- <label className="text-[12px] font-bold text-slate tracking-widest group-hover:text-ink transition-colors uppercase">
520
  {label}
521
  </label>
522
  <div className="flex items-baseline gap-1.5">
523
  <span className="font-sans text-[26px] sm:text-[30px] font-black leading-none tabular-nums tracking-tighter">
524
  {value}
525
  </span>
526
- {unit && <span className="text-[11px] font-bold text-hazard">{unit}</span>}
527
  </div>
528
  </div>
529
 
@@ -537,9 +494,8 @@ function InputControl({ label, value, min, max, step = 1, unit, onChange }: Inpu
537
  onChange={(e) => onChange(Number(e.target.value))}
538
  className="fader-slider"
539
  />
540
- <div className="flex justify-between items-center text-[9px] font-mono font-bold text-slate tracking-widest mt-1">
541
  <span>{min}</span>
542
- <span className="text-ink/25 tracking-widest">| · · · · | · · · · |</span>
543
  <span>{max}</span>
544
  </div>
545
  </div>
 
1
  "use client";
2
 
3
+ import { useState } from "react";
4
+ import { Sliders, Activity, Cpu, Shuffle } from "lucide-react";
5
  import ShapChart from "@/components/ShapChart";
6
  import { PatientVitals } from "@/lib/edge/types";
7
  import { evaluateEdge } from "@/lib/edge/inference";
8
+ import { appendSessionHistory } from "@/lib/edge/history";
9
 
10
  interface PredictionResult {
11
  prediction: boolean;
 
43
  Body_Temp: 37.0,
44
  };
45
 
46
+ const PRESET_VECTORS = [
47
+ {
48
+ name: "BASELINE NORMAL",
49
+ tag: "NORMAL",
50
+ desc: "Normotensive resting telemetry",
51
+ data: { Heart_Rate: 72, SpO2_Level: 98, Systolic_BP: 118, Diastolic_BP: 78, Body_Temp: 37.0 },
52
+ },
53
+ {
54
+ name: "ISCHEMIC RISK",
55
+ tag: "CRITICAL",
56
+ desc: "Tachycardic desaturation event",
57
+ data: { Heart_Rate: 128, SpO2_Level: 91, Systolic_BP: 165, Diastolic_BP: 104, Body_Temp: 37.3 },
58
+ },
59
+ {
60
+ name: "HYPOXIC EVENT",
61
+ tag: "WARNING",
62
+ desc: "Hypoxemia with bradycardia depression",
63
+ data: { Heart_Rate: 48, SpO2_Level: 82, Systolic_BP: 88, Diastolic_BP: 54, Body_Temp: 35.8 },
64
+ },
65
+ {
66
+ name: "HYPERTENSIVE",
67
+ tag: "WARNING",
68
+ desc: "Arterial pressure elevation",
69
+ data: { Heart_Rate: 98, SpO2_Level: 96, Systolic_BP: 195, Diastolic_BP: 118, Body_Temp: 37.1 },
70
+ },
71
+ ];
72
+
73
+ export default function Simulator() {
74
  const [formData, setFormData] = useState<PatientVitals>(DEFAULT_VITALS);
75
+ const [submittedData, setSubmittedData] = useState<PatientVitals | null>(null);
76
+ const [prediction, setPrediction] = useState<PredictionResult | null>(null);
 
 
 
 
 
 
 
 
 
 
77
  const [loading, setLoading] = useState(false);
78
+
79
+ // Manual slider adjustment updates input parameters only
80
+ const handleSliderChange = (field: keyof PatientVitals, val: number) => {
81
+ setFormData((prev) => ({ ...prev, [field]: val }));
82
+ };
83
+
84
+ // Preset selection updates input parameters only
85
+ const handleSelectPreset = (presetData: PatientVitals) => {
86
+ setFormData({ ...presetData });
87
+ };
88
+
89
+ // Randomization updates input parameters only
90
+ const handleRandomize = () => {
 
 
 
 
 
91
  const randomVitals = generateRandomVitals();
92
  setFormData(randomVitals);
93
+ };
94
+
95
+ // Inference execution runs strictly when the user clicks the action button
96
+ const handlePredict = async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  setLoading(true);
98
+ const targetData = { ...formData };
99
 
100
  try {
101
  const apiUrl = process.env.NEXT_PUBLIC_API_URL;
 
108
  if (res.ok) {
109
  const data = await res.json();
110
  setPrediction(data);
111
+ setSubmittedData(targetData);
112
+
113
+ // Append execution record to session audit log
114
+ appendSessionHistory({
115
+ timestamp: new Date().toISOString().replace("T", " ").substring(0, 19),
116
+ heart_rate: targetData.Heart_Rate,
117
+ spo2: targetData.SpO2_Level,
118
+ sys_bp: targetData.Systolic_BP,
119
+ dia_bp: targetData.Diastolic_BP,
120
+ body_temp: targetData.Body_Temp,
121
+ prediction_label: data.prediction_label || (data.prediction ? "Anomaly" : "Normal"),
122
+ confidence: data.probability ?? 0.95,
123
+ is_at_risk: Boolean(data.prediction),
124
+ });
125
+
126
  setLoading(false);
127
  return;
128
  }
129
  }
130
  } catch {
131
+ // Cloud API unreachable, seamlessly fall back to local Edge inference
132
  }
133
 
134
+ // Zero-Cloud Local TinyML Ensemble Inference Execution
135
  try {
136
+ const edgeRes = evaluateEdge(targetData);
137
+ setPrediction({
138
+ prediction: edgeRes.is_at_risk === 1,
139
+ prediction_label: edgeRes.label,
140
+ probability: edgeRes.confidence,
141
+ model_outputs: edgeRes.model_outputs,
142
+ model_probs: edgeRes.model_probs,
143
+ weights: edgeRes.weights,
144
+ });
145
+ setSubmittedData(targetData);
146
+
147
+ // Append execution record to session audit log
148
+ appendSessionHistory({
149
+ timestamp: new Date().toISOString().replace("T", " ").substring(0, 19),
150
+ heart_rate: targetData.Heart_Rate,
151
+ spo2: targetData.SpO2_Level,
152
+ sys_bp: targetData.Systolic_BP,
153
+ dia_bp: targetData.Diastolic_BP,
154
+ body_temp: targetData.Body_Temp,
155
+ prediction_label: edgeRes.label,
156
+ confidence: edgeRes.confidence,
157
+ is_at_risk: edgeRes.is_at_risk === 1,
158
+ });
159
+ } catch (err) {
160
+ console.error("Local inference error", err);
161
  } finally {
162
  setLoading(false);
163
  }
164
  };
165
 
166
+ const isStagedDirty =
167
+ submittedData !== null &&
168
+ (formData.Heart_Rate !== submittedData.Heart_Rate ||
169
+ formData.SpO2_Level !== submittedData.SpO2_Level ||
170
+ formData.Systolic_BP !== submittedData.Systolic_BP ||
171
+ formData.Diastolic_BP !== submittedData.Diastolic_BP ||
172
+ formData.Body_Temp !== submittedData.Body_Temp);
 
 
 
 
173
 
174
  return (
175
  <main className="min-h-dvh bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">
176
+ <div className="max-w-350 mx-auto w-full px-4 sm:px-8 py-10 md:py-14 flex-1 flex flex-col">
177
+ {/* Header */}
178
  <header className="mb-8 border-b-4 border-ink pb-8 flex flex-col md:flex-row justify-between md:items-end gap-6">
179
  <div>
180
+ <div className="text-[12px] text-hazard font-bold tracking-widest mb-4 flex items-center gap-2">
181
  <Sliders size={14} />
182
+ <span>EXPLAINABLE AI MODULE</span>
183
  </div>
184
+ <h1 className="text-[clamp(2.75rem,5.5vw,5.5rem)] leading-[0.9]">
185
  MODEL<br />SIMULATOR
186
  </h1>
187
  </div>
188
+ <div className="text-left md:text-right max-w-md">
189
+ <div className="text-[13px] font-bold uppercase tracking-wider border-2 border-ink px-4 py-3 bg-ink text-canvas-cream text-left">
190
+ Manual override. Input custom telemetry to test ensemble classifier and view Shapley feature importance vectors.
 
 
 
191
  </div>
192
  </div>
193
  </header>
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  {/* Main Workbench Grid */}
196
  <div className="crosshair-corner grid grid-cols-1 lg:grid-cols-12 gap-px bg-ink border-2 border-ink items-stretch">
197
+ {/* Input Panel - 5 Columns */}
198
+ <div className="lg:col-span-5 bg-canvas-cream flex flex-col border-b-2 lg:border-b-0 border-ink">
199
+ <div className="h-14 px-4 sm:px-6 border-b-2 border-ink bg-white flex justify-between items-center">
200
+ <h2 className="text-[14px] font-bold tracking-wider uppercase flex items-center gap-2">
201
+ <Activity size={16} />
202
+ <span>MANUAL INPUT MATRIX</span>
203
+ </h2>
204
+ <span className="text-[10px] text-slate uppercase font-bold">5 Telemetry Inputs</span>
205
+ </div>
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ {/* Clinical Profile Presets - Enlarged & Proportional Presence */}
208
+ <div className="p-5 sm:p-6 bg-canvas-cream border-b-2 border-ink flex flex-col gap-3">
209
+ <div className="text-[11px] font-bold tracking-widest text-slate uppercase flex justify-between items-center">
210
+ <span className="text-ink">CLINICAL TEST PROFILES</span>
211
+ <span className="text-[10px] text-slate">SELECT TO LOAD</span>
212
+ </div>
213
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
214
+ {PRESET_VECTORS.map((preset) => {
215
+ const isActive =
216
+ formData.Heart_Rate === preset.data.Heart_Rate &&
217
+ formData.SpO2_Level === preset.data.SpO2_Level &&
218
+ formData.Systolic_BP === preset.data.Systolic_BP &&
219
+ formData.Diastolic_BP === preset.data.Diastolic_BP &&
220
+ formData.Body_Temp === preset.data.Body_Temp;
221
+
222
+ return (
223
+ <button
224
+ key={preset.name}
225
+ type="button"
226
+ onClick={() => handleSelectPreset(preset.data)}
227
+ className={`btn-tactile border-2 border-ink p-3.5 sm:p-4 text-left transition-colors cursor-pointer flex flex-col justify-between gap-2 min-h-[96px] ${
228
+ isActive
229
+ ? "bg-ink text-white font-bold"
230
+ : "bg-white text-ink hover:bg-canvas-cream"
231
+ }`}
232
+ >
233
+ <div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  <div className="flex justify-between items-center text-[12px] font-black uppercase tracking-wider mb-1">
235
  <span>{preset.name}</span>
236
  <span
 
247
  {preset.tag}
248
  </span>
249
  </div>
250
+ <div className={`text-[11px] leading-snug ${isActive ? "text-white/75" : "text-slate"}`}>
251
  {preset.desc}
252
  </div>
253
+ </div>
254
+ <div className={`text-[11px] font-mono pt-2 border-t border-ink/15 flex justify-between tabular-nums ${isActive ? "text-white/80" : "text-slate"}`}>
255
+ <span>{preset.data.Heart_Rate} BPM · {preset.data.SpO2_Level}%</span>
256
+ <span className="font-bold">{preset.data.Systolic_BP}/{preset.data.Diastolic_BP} mmHg</span>
257
+ </div>
258
+ </button>
259
+ );
260
+ })}
261
  </div>
262
+ </div>
263
 
264
+ {/* Slider Inputs - Dynamically Distributed to Match Right Panel Height */}
265
+ <div className="p-5 sm:p-6 flex-1 flex flex-col justify-between gap-4">
266
+ <InputControl
267
+ label="HEART RATE"
268
+ unit="BPM"
269
+ value={formData.Heart_Rate}
270
+ min={40}
271
+ max={200}
272
+ onChange={(v: number) => handleSliderChange("Heart_Rate", v)}
273
+ />
274
+ <InputControl
275
+ label="SPO2 LEVEL"
276
+ unit="%"
277
+ value={formData.SpO2_Level}
278
+ min={70}
279
+ max={100}
280
+ onChange={(v: number) => handleSliderChange("SpO2_Level", v)}
281
+ />
282
+ <InputControl
283
+ label="SYSTOLIC BP"
284
+ unit="mmHg"
285
+ value={formData.Systolic_BP}
286
+ min={80}
287
+ max={220}
288
+ onChange={(v: number) => handleSliderChange("Systolic_BP", v)}
289
+ />
290
+ <InputControl
291
+ label="DIASTOLIC BP"
292
+ unit="mmHg"
293
+ value={formData.Diastolic_BP}
294
+ min={40}
295
+ max={140}
296
+ onChange={(v: number) => handleSliderChange("Diastolic_BP", v)}
297
+ />
298
+ <InputControl
299
+ label="BODY TEMP"
300
+ unit="°C"
301
+ value={formData.Body_Temp}
302
+ min={30}
303
+ max={42}
304
+ step={0.1}
305
+ onChange={(v: number) => handleSliderChange("Body_Temp", v)}
306
+ />
 
307
  </div>
308
 
309
+ {/* Action Bar - Anchored to bottom with border */}
310
+ <div className="mt-auto border-t-2 border-ink bg-white p-4 sm:p-5 flex gap-3">
311
  <button
312
  type="button"
313
+ onClick={handleRandomize}
314
+ className="btn-tactile py-3.5 px-5 border-2 border-ink bg-white hover:bg-canvas-cream text-ink font-bold text-[12px] tracking-wider uppercase flex items-center justify-center gap-2 cursor-pointer transition-colors"
315
+ title="Generate random physiological vitals"
316
  >
317
  <Shuffle size={14} className="text-hazard" />
318
+ <span>RANDOMIZE</span>
319
  </button>
320
 
321
  <button
322
  type="button"
323
+ onClick={handlePredict}
324
  disabled={loading}
325
+ className="btn-tactile-hazard py-3.5 font-bold text-[13px] sm:text-[14px] tracking-[0.2em] uppercase flex items-center justify-center gap-2 flex-1 cursor-pointer disabled:cursor-not-allowed"
326
  >
327
+ {loading ? "PROCESSING REQUEST..." : "EXECUTE INFERENCE"}
328
  </button>
329
  </div>
330
  </div>
331
 
332
+ {/* Results Panel - 7 Columns */}
333
+ <div className="lg:col-span-7 bg-canvas-cream flex flex-col overflow-hidden">
334
  <div className="h-14 px-4 sm:px-6 border-b-2 border-ink bg-white flex justify-between items-center">
335
  <h2 className="text-[14px] font-bold tracking-wider uppercase flex items-center gap-2">
336
  <Cpu size={16} />
337
+ <span>INFERENCE RESULTS &amp; XAI</span>
338
  </h2>
339
+ {isStagedDirty && (
340
+ <span className="text-[10px] text-hazard font-bold uppercase tracking-wider flex items-center gap-1.5">
341
+ <span className="w-1.5 h-1.5 bg-hazard animate-pulse" />
342
+ INPUT MODIFIED · RE-RUN REQUIRED
343
+ </span>
344
+ )}
345
  </div>
346
 
347
  <div className="grow flex flex-col relative bg-white">
 
354
  }}
355
  />
356
 
357
+ <div className="z-10 p-5 sm:p-6 flex flex-col h-full gap-6">
358
+ {prediction && submittedData ? (
359
  <>
360
  <div>
361
  <div className="text-[11px] font-bold text-slate tracking-widest mb-3 uppercase">
362
+ MODEL OUTPUT
363
  </div>
364
 
365
  {(() => {
 
378
  return "bg-hazard text-white border-hazard";
379
  };
380
 
381
+ const diagnosisText = prediction.prediction
382
+ ? prediction.prediction_label.toUpperCase()
383
+ : "NORMAL";
384
+
385
  return (
386
  <div
387
+ className={`border-2 p-4 sm:p-5 flex flex-col mb-4 ${getStatusStyles(
388
  prediction.prediction ? prediction.prediction_label : "Healthy"
389
  )}`}
390
  >
391
+ <div className="font-sans text-[clamp(26px,2.5vw,48px)] font-black tracking-tighter leading-none mb-2 uppercase wrap-break-words">
392
+ {diagnosisText}
393
  </div>
394
+ <div className="text-[12px] font-bold tracking-widest uppercase">
395
  CONFIDENCE: {(prediction.probability * 100).toFixed(1)}%
396
  </div>
397
  </div>
 
399
  })()}
400
 
401
  {/* Ensemble Diagnostics Breakdown */}
402
+ {prediction.model_outputs && (
403
+ <div className="border-2 border-ink bg-white flex flex-col text-[11px] font-bold uppercase tracking-wider">
404
+ <div className="border-b-2 border-ink px-4 py-2 bg-ink text-canvas-cream flex justify-between">
405
+ <span>ENSEMBLE STACK DIAGNOSTICS</span>
406
+ <span className="text-hazard">WEIGHTED VOTING</span>
407
+ </div>
408
+ <div className="divide-y-2 divide-ink">
409
+ {Object.keys(prediction.model_outputs).map((modelName) => {
410
+ const outputLabel = prediction.model_outputs![modelName];
411
+ const isAnomaly = outputLabel !== "Healthy" && outputLabel !== "Normal";
412
+ return (
413
  <div
414
  key={modelName}
415
  className="flex justify-between items-center p-2.5 hover:bg-canvas-cream transition-colors"
416
  >
417
  <div className="w-[25%] text-slate">{modelName}</div>
418
+ <div className={`w-[35%] ${isAnomaly ? "text-hazard font-black" : "text-emerald-700"}`}>
419
+ {outputLabel}
 
 
 
 
420
  </div>
421
  <div className="w-[20%] text-right tabular-nums">
422
+ CONF: {(((prediction.model_probs?.[modelName] ?? prediction.probability ?? 0.95)) * 100).toFixed(1)}%
423
  </div>
424
  <div className="w-[20%] text-right text-slate tabular-nums">
425
  W: {(prediction.weights?.[modelName] ?? 0.25).toFixed(2)}
426
  </div>
427
  </div>
428
+ );
429
+ })}
430
  </div>
431
+ </div>
432
+ )}
433
  </div>
434
 
435
  {/* SHAP Feature Attributions Container */}
436
+ <div className="border-2 border-ink bg-white p-4 sm:p-5 flex flex-col">
437
  <div className="text-[12px] font-black uppercase text-ink tracking-wider mb-4 border-b-2 border-ink pb-2 flex justify-between items-center">
438
+ <span>SHAPLEY ADDITIVE EXPLANATIONS</span>
439
+ <span className="text-[10px] text-slate font-mono">DIRECTIONAL POLARITY</span>
440
  </div>
441
  <div className="w-full">
442
  <ShapChart patientData={submittedData} />
 
444
  </div>
445
  </>
446
  ) : (
447
+ <div className="h-full min-h-[380px] flex items-center justify-center">
448
+ <div className="text-slate text-[13px] font-bold tracking-widest uppercase border-2 border-slate px-6 py-4 animate-pulse text-center max-w-sm">
449
+ {loading ? "CALCULATING SHAP VALUES..." : "Awaiting Input · Click Execute Inference to Generate Attributions"}
450
  </div>
451
  </div>
452
  )}
 
473
  return (
474
  <div className="flex flex-col gap-2 group">
475
  <div className="flex justify-between items-baseline">
476
+ <label className="text-[13px] sm:text-[14px] font-black text-slate tracking-wider group-hover:text-ink transition-colors uppercase">
477
  {label}
478
  </label>
479
  <div className="flex items-baseline gap-1.5">
480
  <span className="font-sans text-[26px] sm:text-[30px] font-black leading-none tabular-nums tracking-tighter">
481
  {value}
482
  </span>
483
+ {unit && <span className="text-[12px] font-bold text-hazard">{unit}</span>}
484
  </div>
485
  </div>
486
 
 
494
  onChange={(e) => onChange(Number(e.target.value))}
495
  className="fader-slider"
496
  />
497
+ <div className="flex justify-between items-center text-[10px] sm:text-[11px] font-mono font-bold text-slate tracking-widest mt-1">
498
  <span>{min}</span>
 
499
  <span>{max}</span>
500
  </div>
501
  </div>
frontend/src/components/Navigation.tsx CHANGED
@@ -2,10 +2,10 @@
2
 
3
  import Link from "next/link";
4
  import { usePathname } from "next/navigation";
5
- import { useState, useEffect } from "react";
6
- import { Activity, Clock } from "lucide-react";
7
 
8
  const NAV_LINKS = [
 
9
  { href: "/monitor", label: "MONITOR" },
10
  { href: "/simulator", label: "SIMULATOR" },
11
  { href: "/history", label: "HISTORY" },
@@ -16,22 +16,8 @@ const NAV_LINKS = [
16
 
17
  export default function Navigation() {
18
  const pathname = usePathname();
19
- const [clock, setClock] = useState<string>("");
20
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
21
 
22
- useEffect(() => {
23
- const updateTime = () => {
24
- const now = new Date();
25
- const h = String(now.getHours()).padStart(2, "0");
26
- const m = String(now.getMinutes()).padStart(2, "0");
27
- const s = String(now.getSeconds()).padStart(2, "0");
28
- setClock(`${h}:${m}:${s}`);
29
- };
30
- updateTime();
31
- const interval = setInterval(updateTime, 1000);
32
- return () => clearInterval(interval);
33
- }, []);
34
-
35
  return (
36
  <nav className="w-full border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
37
  <div className="max-w-350 mx-auto px-4 sm:px-8 py-3 flex items-center justify-between gap-4">
@@ -44,17 +30,6 @@ export default function Navigation() {
44
  <span className="inline-block w-2.5 h-2.5 bg-hazard" />
45
  <span>HEARTFLOW_OS</span>
46
  </Link>
47
-
48
- {/* System Telemetry Pill (Desktop) */}
49
- <div className="hidden xl:flex items-center gap-2 px-2.5 py-1 bg-white border border-ink/40 text-[11px] font-mono font-bold tracking-wider uppercase text-slate">
50
- <Activity size={12} className="text-hazard" />
51
- <span>STREAM: ACTIVE</span>
52
- <span className="text-ink/30">|</span>
53
- <span className="flex items-center gap-1 text-ink tabular-nums">
54
- <Clock size={11} className="text-slate" />
55
- {clock || "READY"}
56
- </span>
57
- </div>
58
  </div>
59
 
60
  {/* Desktop Navigation Links */}
@@ -79,11 +54,6 @@ export default function Navigation() {
79
 
80
  {/* Mobile Menu Toggle Button */}
81
  <div className="flex md:hidden items-center gap-2">
82
- {clock && (
83
- <span className="text-[10px] font-mono font-bold tabular-nums text-slate bg-white px-2 py-1 border border-ink/40">
84
- {clock}
85
- </span>
86
- )}
87
  <button
88
  onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
89
  className="px-3 py-1 border-2 border-ink bg-white text-ink text-[11px] font-bold tracking-widest uppercase hover:bg-ink hover:text-white transition-colors"
 
2
 
3
  import Link from "next/link";
4
  import { usePathname } from "next/navigation";
5
+ import { useState } from "react";
 
6
 
7
  const NAV_LINKS = [
8
+ { href: "/", label: "OVERVIEW" },
9
  { href: "/monitor", label: "MONITOR" },
10
  { href: "/simulator", label: "SIMULATOR" },
11
  { href: "/history", label: "HISTORY" },
 
16
 
17
  export default function Navigation() {
18
  const pathname = usePathname();
 
19
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  return (
22
  <nav className="w-full border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
23
  <div className="max-w-350 mx-auto px-4 sm:px-8 py-3 flex items-center justify-between gap-4">
 
30
  <span className="inline-block w-2.5 h-2.5 bg-hazard" />
31
  <span>HEARTFLOW_OS</span>
32
  </Link>
 
 
 
 
 
 
 
 
 
 
 
33
  </div>
34
 
35
  {/* Desktop Navigation Links */}
 
54
 
55
  {/* Mobile Menu Toggle Button */}
56
  <div className="flex md:hidden items-center gap-2">
 
 
 
 
 
57
  <button
58
  onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
59
  className="px-3 py-1 border-2 border-ink bg-white text-ink text-[11px] font-bold tracking-widest uppercase hover:bg-ink hover:text-white transition-colors"
frontend/src/components/ShapChart.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import { useMemo } from "react";
4
  import { PatientVitals } from "@/lib/edge/types";
5
  import { computeEdgeShap } from "@/lib/edge/explain";
6
  import { TrendingUp, TrendingDown, ShieldCheck, AlertTriangle } from "lucide-react";
@@ -9,24 +9,57 @@ interface ShapChartProps {
9
  patientData: PatientVitals | null;
10
  }
11
 
 
 
 
 
 
 
12
  export default function ShapChart({ patientData }: ShapChartProps) {
13
- // Synchronous client-side edge SHAP computation - instant 0ms latency
14
- const shapData = useMemo(() => {
 
 
15
  if (!patientData) return null;
16
  return computeEdgeShap(patientData);
17
  }, [patientData]);
18
 
19
- if (!shapData || !patientData) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  return (
21
- <div className="h-full w-full flex flex-col items-center justify-center p-8 text-center text-slate">
22
  <span className="text-[12px] font-bold uppercase tracking-widest border border-ink/20 px-4 py-2 bg-canvas-cream">
23
- Awaiting Patient Telemetry...
24
  </span>
25
  </div>
26
  );
27
  }
28
 
29
- // Feature clinical units mapping
30
  const units: Record<string, string> = {
31
  Heart_Rate: "BPM",
32
  SpO2_Level: "%",
@@ -37,13 +70,12 @@ export default function ShapChart({ patientData }: ShapChartProps) {
37
 
38
  const featureLabels: Record<string, string> = {
39
  Heart_Rate: "HEART RATE",
40
- SpO2_Level: "SPO2 SATURATION",
41
  Systolic_BP: "SYSTOLIC BP",
42
  Diastolic_BP: "DIASTOLIC BP",
43
- Body_Temp: "BODY TEMPERATURE",
44
  };
45
 
46
- // Prepare sorted attribution items
47
  const items = shapData.feature_names
48
  .map((rawName, i) => {
49
  const val = shapData.shap_values[i];
@@ -67,54 +99,54 @@ export default function ShapChart({ patientData }: ShapChartProps) {
67
 
68
  return (
69
  <div className="flex flex-col gap-5 w-full h-full justify-between font-mono">
70
- {/* SHAP Sign & Polarity Summary Card */}
71
  <div className="border-2 border-ink bg-canvas-cream p-3 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
72
  <div className="flex items-center gap-2">
73
  {netShap > 0 ? (
74
- <span className="flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-black uppercase bg-hazard text-white border border-ink">
75
  <AlertTriangle size={13} />
76
- <span>[+] NET RISK IMPACT: +{netShap.toFixed(3)}</span>
77
  </span>
78
  ) : (
79
- <span className="flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-black uppercase bg-emerald-700 text-white border border-ink">
80
  <ShieldCheck size={13} />
81
- <span>[-] NET PROTECTIVE IMPACT: {netShap.toFixed(3)}</span>
82
  </span>
83
  )}
84
  </div>
85
 
86
- <div className="flex items-center gap-3 text-[10px] font-black uppercase tracking-wider">
87
  <span className="text-hazard flex items-center gap-1">
88
  <span className="inline-block w-2 h-2 bg-hazard" />
89
- <span>{riskCount} RISK SIGN (+)</span>
90
  </span>
91
- <span className="text-emerald-700 flex items-center gap-1">
92
- <span className="inline-block w-2 h-2 bg-emerald-600" />
93
- <span>{safeCount} PROTECTIVE SIGN (-)</span>
94
  </span>
95
  </div>
96
  </div>
97
 
98
  {/* Axis Polarity Legend */}
99
- <div className="grid grid-cols-2 text-[10px] font-black tracking-widest uppercase border-b-2 border-ink pb-2">
100
  <div className="flex items-center gap-1.5 text-emerald-800">
101
  <TrendingDown size={14} className="text-emerald-700" />
102
- <span>(-) PROTECTIVE EFFECT (SUPPRESSES RISK)</span>
103
  </div>
104
  <div className="flex items-center justify-end gap-1.5 text-hazard">
105
- <span>(+) RISK FACTOR (DRIVES ANOMALY)</span>
106
  <TrendingUp size={14} className="text-hazard" />
107
  </div>
108
  </div>
109
 
110
- {/* Feature Attributions with Prominent Signs */}
111
  <div className="flex flex-col gap-4">
112
  {items.map((item) => {
113
  const widthPercent = Math.min((item.abs / maxAbs) * 50, 48);
114
 
115
  return (
116
  <div key={item.key} className="flex flex-col gap-1.5">
117
- {/* Header row: Name + Telemetry + SHAP Sign Badge */}
118
  <div className="flex justify-between items-baseline text-[11px] font-bold">
119
  <div className="flex items-center gap-2">
120
  <span className="text-ink font-black">{item.name}</span>
@@ -123,18 +155,14 @@ export default function ShapChart({ patientData }: ShapChartProps) {
123
  </span>
124
  </div>
125
 
126
- {/* Prominent Sign Pill */}
127
  <div className="flex items-center gap-1">
128
  {item.isPositive ? (
129
- <span className="px-2 py-0.5 text-[10px] font-black bg-hazard text-white border border-ink flex items-center gap-1">
130
- <span>+</span>
131
- <span>{item.value.toFixed(3)}</span>
132
- <span>[RISK DRIVER ↑]</span>
133
  </span>
134
  ) : (
135
- <span className="px-2 py-0.5 text-[10px] font-black bg-emerald-100 text-emerald-900 border border-emerald-700 flex items-center gap-1">
136
- <span>{item.value.toFixed(3)}</span>
137
- <span>[PROTECTIVE ↓]</span>
138
  </span>
139
  )}
140
  </div>
@@ -142,7 +170,7 @@ export default function ShapChart({ patientData }: ShapChartProps) {
142
 
143
  {/* Dual-Polarity Divergent Chart Bar with Centered 0.00 Midline */}
144
  <div className="relative h-6 w-full bg-canvas-cream border-2 border-ink flex items-center overflow-hidden">
145
- {/* Background Grid Pattern */}
146
  <div
147
  className="absolute inset-0 opacity-15 pointer-events-none"
148
  style={{
@@ -158,44 +186,23 @@ export default function ShapChart({ patientData }: ShapChartProps) {
158
  {/* Midline (0.00 Zero-Axis) */}
159
  <div className="absolute left-1/2 -top-1 -bottom-1 w-0.5 bg-ink z-20" />
160
 
161
- {/* Center Zero Marker */}
162
- <span className="absolute left-1/2 -translate-x-1/2 text-[8px] font-black text-slate bg-white px-1 border border-ink/40 z-30 pointer-events-none">
163
- 0.00
164
- </span>
165
-
166
  {/* Responsive Dual-Polarity Bar */}
167
  {item.isPositive ? (
168
- // Positive (+) bar grows to the right
169
  <div
170
- className="absolute top-0 bottom-0 left-1/2 bg-hazard z-10 flex items-center justify-end pr-1 transition-all duration-150 border-r border-ink"
171
  style={{ width: `${widthPercent}%` }}
172
- >
173
- <span className="text-[9px] font-black text-white pl-1 select-none">
174
- +
175
- </span>
176
- </div>
177
  ) : (
178
- // Negative (-) bar grows to the left
179
  <div
180
- className="absolute top-0 bottom-0 right-1/2 bg-emerald-700 z-10 flex items-center justify-start pl-1 transition-all duration-150 border-l border-ink"
181
  style={{ width: `${widthPercent}%` }}
182
- >
183
- <span className="text-[9px] font-black text-white pr-1 select-none">
184
- -
185
- </span>
186
- </div>
187
  )}
188
  </div>
189
  </div>
190
  );
191
  })}
192
  </div>
193
-
194
- {/* Mathematical Reference Note */}
195
- <div className="pt-2 border-t border-ink/20 flex flex-wrap justify-between items-center text-[9px] text-slate font-bold uppercase gap-2">
196
- <span>Base Reference Value E[f(x)] = 0.50</span>
197
- <span>Local Linear Attribution Model: f(x) = E[f(x)] + ∑ φ_i</span>
198
- </div>
199
  </div>
200
  );
201
  }
 
1
  "use client";
2
 
3
+ import { useEffect, useState, useMemo } from "react";
4
  import { PatientVitals } from "@/lib/edge/types";
5
  import { computeEdgeShap } from "@/lib/edge/explain";
6
  import { TrendingUp, TrendingDown, ShieldCheck, AlertTriangle } from "lucide-react";
 
9
  patientData: PatientVitals | null;
10
  }
11
 
12
+ interface ShapData {
13
+ feature_names: string[];
14
+ shap_values: number[];
15
+ base_value?: number;
16
+ }
17
+
18
  export default function ShapChart({ patientData }: ShapChartProps) {
19
+ const [cloudShap, setCloudShap] = useState<ShapData | null>(null);
20
+
21
+ // Compute zero-cloud edge attributions synchronously whenever patientData changes
22
+ const localShap = useMemo(() => {
23
  if (!patientData) return null;
24
  return computeEdgeShap(patientData);
25
  }, [patientData]);
26
 
27
+ // Attempt backend TreeSHAP API enhancement if configured
28
+ useEffect(() => {
29
+ if (!patientData || !process.env.NEXT_PUBLIC_API_URL) return;
30
+
31
+ let isCancelled = false;
32
+
33
+ fetch(`${process.env.NEXT_PUBLIC_API_URL}/explain`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify(patientData),
37
+ })
38
+ .then((res) => (res.ok ? res.json() : null))
39
+ .then((data) => {
40
+ if (data?.shap_values && !isCancelled) {
41
+ setCloudShap(data);
42
+ }
43
+ })
44
+ .catch(() => {});
45
+
46
+ return () => {
47
+ isCancelled = true;
48
+ };
49
+ }, [patientData]);
50
+
51
+ const shapData = cloudShap || localShap;
52
+
53
+ if (!patientData || !shapData) {
54
  return (
55
+ <div className="h-full min-h-60 w-full flex flex-col items-center justify-center p-8 text-center text-slate">
56
  <span className="text-[12px] font-bold uppercase tracking-widest border border-ink/20 px-4 py-2 bg-canvas-cream">
57
+ Awaiting Input · Run Inference to Generate Attributions
58
  </span>
59
  </div>
60
  );
61
  }
62
 
 
63
  const units: Record<string, string> = {
64
  Heart_Rate: "BPM",
65
  SpO2_Level: "%",
 
70
 
71
  const featureLabels: Record<string, string> = {
72
  Heart_Rate: "HEART RATE",
73
+ SpO2_Level: "SPO2 LEVEL",
74
  Systolic_BP: "SYSTOLIC BP",
75
  Diastolic_BP: "DIASTOLIC BP",
76
+ Body_Temp: "BODY TEMP",
77
  };
78
 
 
79
  const items = shapData.feature_names
80
  .map((rawName, i) => {
81
  const val = shapData.shap_values[i];
 
99
 
100
  return (
101
  <div className="flex flex-col gap-5 w-full h-full justify-between font-mono">
102
+ {/* SHAP Sign & Polarity Summary */}
103
  <div className="border-2 border-ink bg-canvas-cream p-3 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
104
  <div className="flex items-center gap-2">
105
  {netShap > 0 ? (
106
+ <span className="flex items-center gap-1.5 px-2.5 py-1 text-[11px] font-black uppercase bg-hazard text-white border border-ink">
107
  <AlertTriangle size={13} />
108
+ <span>NET RISK ATTRIBUTION: +{netShap.toFixed(3)}</span>
109
  </span>
110
  ) : (
111
+ <span className="flex items-center gap-1.5 px-2.5 py-1 text-[11px] font-black uppercase bg-emerald-800 text-white border border-ink">
112
  <ShieldCheck size={13} />
113
+ <span>NET PROTECTIVE ATTRIBUTION: {netShap.toFixed(3)}</span>
114
  </span>
115
  )}
116
  </div>
117
 
118
+ <div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-wider">
119
  <span className="text-hazard flex items-center gap-1">
120
  <span className="inline-block w-2 h-2 bg-hazard" />
121
+ <span>{riskCount} RISK FACTORS</span>
122
  </span>
123
+ <span className="text-emerald-800 flex items-center gap-1">
124
+ <span className="inline-block w-2 h-2 bg-emerald-700" />
125
+ <span>{safeCount} PROTECTIVE</span>
126
  </span>
127
  </div>
128
  </div>
129
 
130
  {/* Axis Polarity Legend */}
131
+ <div className="grid grid-cols-2 text-[10px] font-bold tracking-widest uppercase border-b-2 border-ink pb-2">
132
  <div className="flex items-center gap-1.5 text-emerald-800">
133
  <TrendingDown size={14} className="text-emerald-700" />
134
+ <span>PROTECTIVE (SUPPRESSES RISK)</span>
135
  </div>
136
  <div className="flex items-center justify-end gap-1.5 text-hazard">
137
+ <span>RISK FACTOR (DRIVES ANOMALY)</span>
138
  <TrendingUp size={14} className="text-hazard" />
139
  </div>
140
  </div>
141
 
142
+ {/* Feature Attributions with Calibrated Midline Bars */}
143
  <div className="flex flex-col gap-4">
144
  {items.map((item) => {
145
  const widthPercent = Math.min((item.abs / maxAbs) * 50, 48);
146
 
147
  return (
148
  <div key={item.key} className="flex flex-col gap-1.5">
149
+ {/* Header row: Feature Name + Patient Vitals + Signed Attribution */}
150
  <div className="flex justify-between items-baseline text-[11px] font-bold">
151
  <div className="flex items-center gap-2">
152
  <span className="text-ink font-black">{item.name}</span>
 
155
  </span>
156
  </div>
157
 
 
158
  <div className="flex items-center gap-1">
159
  {item.isPositive ? (
160
+ <span className="px-2 py-0.5 text-[10px] font-black bg-hazard text-white border border-ink">
161
+ +{item.value.toFixed(3)}
 
 
162
  </span>
163
  ) : (
164
+ <span className="px-2 py-0.5 text-[10px] font-black bg-emerald-100 text-emerald-900 border border-emerald-700">
165
+ {item.value.toFixed(3)}
 
166
  </span>
167
  )}
168
  </div>
 
170
 
171
  {/* Dual-Polarity Divergent Chart Bar with Centered 0.00 Midline */}
172
  <div className="relative h-6 w-full bg-canvas-cream border-2 border-ink flex items-center overflow-hidden">
173
+ {/* Background Calibration Grid */}
174
  <div
175
  className="absolute inset-0 opacity-15 pointer-events-none"
176
  style={{
 
186
  {/* Midline (0.00 Zero-Axis) */}
187
  <div className="absolute left-1/2 -top-1 -bottom-1 w-0.5 bg-ink z-20" />
188
 
 
 
 
 
 
189
  {/* Responsive Dual-Polarity Bar */}
190
  {item.isPositive ? (
 
191
  <div
192
+ className="absolute top-0 bottom-0 left-1/2 bg-hazard z-10 h-full transition-all duration-150 border-r border-ink"
193
  style={{ width: `${widthPercent}%` }}
194
+ />
 
 
 
 
195
  ) : (
 
196
  <div
197
+ className="absolute top-0 bottom-0 right-1/2 bg-emerald-700 z-10 h-full transition-all duration-150 border-l border-ink"
198
  style={{ width: `${widthPercent}%` }}
199
+ />
 
 
 
 
200
  )}
201
  </div>
202
  </div>
203
  );
204
  })}
205
  </div>
 
 
 
 
 
 
206
  </div>
207
  );
208
  }