mobadara commited on
Commit
e78cf24
·
1 Parent(s): ed483bc

implemented the dashboard

Browse files
frontend/index.html CHANGED
@@ -1,13 +1,55 @@
1
  <!doctype html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>frontend</title>
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
  <div id="root"></div>
11
  <script type="module" src="/src/main.jsx"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  </body>
13
  </html>
 
1
  <!doctype html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
7
+ <meta name="format-detection" content="telephone=no" />
8
+ <meta name="description" content="Pneumonia Vision AI — diagnostic assistance using explainable AI for chest X-rays." />
9
+ <meta name="robots" content="index,follow" />
10
+
11
+ <!-- Mobile / PWA -->
12
+ <meta name="mobile-web-app-capable" content="yes" />
13
+ <meta name="apple-mobile-web-app-capable" content="yes" />
14
+ <meta name="apple-mobile-web-app-status-bar-style" content="default" />
15
+ <meta name="apple-mobile-web-app-title" content="Pneumonia Vision AI" />
16
+ <meta name="theme-color" content="#0b5cff" />
17
+ <link rel="manifest" href="/manifest.json" />
18
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
19
+ <link rel="apple-touch-icon" href="/apple-touch-icon.svg" sizes="180x180" />
20
+ <link rel="icon" href="/apple-touch-icon.svg" sizes="180x180" />
21
+
22
+ <!-- Social / SEO -->
23
+ <meta property="og:type" content="website" />
24
+ <meta property="og:title" content="Pneumonia Vision AI" />
25
+ <meta property="og:description" content="Explainable AI for chest X-ray diagnosis and triage." />
26
+ <meta property="og:url" content="/" />
27
+ <meta property="og:image" content="/social-preview.png" />
28
+ <meta name="twitter:card" content="summary_large_image" />
29
+ <meta name="twitter:title" content="Pneumonia Vision AI" />
30
+ <meta name="twitter:description" content="Explainable AI for chest X-ray diagnosis and triage." />
31
+
32
+ <title>Pneumonia Vision AI</title>
33
  </head>
34
  <body>
35
  <div id="root"></div>
36
  <script type="module" src="/src/main.jsx"></script>
37
+ <script>
38
+ if ('serviceWorker' in navigator) {
39
+ window.addEventListener('load', () => {
40
+ navigator.serviceWorker.register('/sw.js')
41
+ .then(reg => console.log('Service worker registered:', reg))
42
+ .catch(err => console.warn('SW registration failed:', err));
43
+ });
44
+ }
45
+
46
+ // Capture install prompt for later use
47
+ window.deferredPrompt = null;
48
+ window.addEventListener('beforeinstallprompt', (e) => {
49
+ e.preventDefault();
50
+ window.deferredPrompt = e;
51
+ console.log('beforeinstallprompt captured; call window.deferredPrompt.prompt() to show install dialog.');
52
+ });
53
+ </script>
54
  </body>
55
  </html>
frontend/public/apple-touch-icon.svg ADDED
frontend/public/manifest.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Pneumonia Vision AI",
3
+ "short_name": "PneumoniaAI",
4
+ "start_url": ".",
5
+ "display": "standalone",
6
+ "background_color": "#ffffff",
7
+ "theme_color": "#0b5cff",
8
+ "description": "Explainable AI-assisted chest X-ray triage and diagnostics.",
9
+ "icons": [
10
+ {
11
+ "src": "/apple-touch-icon.svg",
12
+ "sizes": "180x180",
13
+ "type": "image/svg+xml",
14
+ "purpose": "any maskable"
15
+ },
16
+ {
17
+ "src": "/social-preview.png",
18
+ "sizes": "512x512",
19
+ "type": "image/png",
20
+ "purpose": "any maskable"
21
+ }
22
+ ]
23
+ }
frontend/public/social-preview.png ADDED
frontend/public/sw.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CACHE_NAME = 'pneumonia-ai-v1';
2
+ const PRECACHE_URLS = [
3
+ '/',
4
+ '/index.html',
5
+ '/manifest.json',
6
+ '/apple-touch-icon.svg',
7
+ '/favicon.svg',
8
+ '/social-preview.png'
9
+ ];
10
+
11
+ self.addEventListener('install', event => {
12
+ event.waitUntil(
13
+ caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
14
+ );
15
+ self.skipWaiting();
16
+ });
17
+
18
+ self.addEventListener('activate', event => {
19
+ event.waitUntil(
20
+ caches.keys().then(keys => Promise.all(
21
+ keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
22
+ ))
23
+ );
24
+ self.clients.claim();
25
+ });
26
+
27
+ self.addEventListener('fetch', event => {
28
+ if (event.request.method !== 'GET') return;
29
+ event.respondWith(
30
+ caches.match(event.request).then(response => {
31
+ if (response) return response;
32
+ return fetch(event.request).then(networkRes => {
33
+ return caches.open(CACHE_NAME).then(cache => {
34
+ cache.put(event.request, networkRes.clone());
35
+ return networkRes;
36
+ });
37
+ }).catch(() => caches.match('/index.html'));
38
+ })
39
+ );
40
+ });
frontend/src/Dashboard.jsx CHANGED
@@ -1,34 +1,279 @@
1
- import React from 'react';
 
2
  import { useAuth } from './useAuth';
3
- import { LogOut, Activity } from 'lucide-react';
4
 
5
  export default function Dashboard() {
6
  const { user, logout } = useAuth();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  return (
9
- <div className="min-h-screen bg-gray-50">
10
- {/* Top Navigation Bar */}
11
  <nav className="bg-navy text-white p-4 shadow-md flex justify-between items-center">
12
  <div className="flex items-center gap-3">
13
  <Activity className="w-6 h-6 text-trust-cyan" />
14
- <span className="font-bold tracking-wide">DEBUTRON LAB</span>
15
  </div>
16
  <div className="flex items-center gap-6">
17
- <span className="text-sm text-gray-200">Welcome, {user?.name}</span>
18
- <button
19
- onClick={logout}
20
- className="flex items-center gap-2 hover:text-red-300 transition-colors text-sm font-medium"
21
- >
22
  <LogOut className="w-4 h-4" /> Logout
23
  </button>
24
  </div>
25
  </nav>
26
 
27
- {/* Main Content Area */}
28
- <main className="max-w-7xl mx-auto p-6 mt-8">
29
- <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-12 text-center">
30
- <h2 className="text-2xl font-semibold text-navy mb-2">Diagnostic Engine Active</h2>
31
- <p className="text-gray-500">The main X-Ray upload interface will go here.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
33
  </main>
34
  </div>
 
1
+ import React, { useState, useRef } from 'react';
2
+ import axios from 'axios';
3
  import { useAuth } from './useAuth';
4
+ import { LogOut, Activity, UploadCloud, XCircle, CheckCircle2, AlertTriangle, Loader2, ThumbsUp, ThumbsDown } from 'lucide-react';
5
 
6
  export default function Dashboard() {
7
  const { user, logout } = useAuth();
8
+
9
+ // UI State
10
+ const [dragActive, setDragActive] = useState(false);
11
+ const [selectedFile, setSelectedFile] = useState(null);
12
+ const [previewUrl, setPreviewUrl] = useState(null);
13
+
14
+ // API State
15
+ const [loading, setLoading] = useState(false);
16
+ const [result, setResult] = useState(null);
17
+ const [error, setError] = useState(null);
18
+
19
+ // Feedback State
20
+ const [feedbackStatus, setFeedbackStatus] = useState(null); // null, 'loading', 'success', 'error'
21
+
22
+ const inputRef = useRef(null);
23
+
24
+ // --- Drag and Drop Handlers ---
25
+ const handleDrag = (e) => {
26
+ e.preventDefault();
27
+ e.stopPropagation();
28
+ if (e.type === "dragenter" || e.type === "dragover") {
29
+ setDragActive(true);
30
+ } else if (e.type === "dragleave") {
31
+ setDragActive(false);
32
+ }
33
+ };
34
+
35
+ const handleDrop = (e) => {
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ setDragActive(false);
39
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
40
+ processFile(e.dataTransfer.files[0]);
41
+ }
42
+ };
43
+
44
+ const handleChange = (e) => {
45
+ e.preventDefault();
46
+ if (e.target.files && e.target.files[0]) {
47
+ processFile(e.target.files[0]);
48
+ }
49
+ };
50
+
51
+ const processFile = (file) => {
52
+ if (!file.type.startsWith("image/")) {
53
+ setError("Please upload a valid image file (JPEG/PNG).");
54
+ return;
55
+ }
56
+ setError(null);
57
+ setResult(null);
58
+ setFeedbackStatus(null);
59
+ setSelectedFile(file);
60
+ setPreviewUrl(URL.createObjectURL(file));
61
+ };
62
+
63
+ const clearSelection = () => {
64
+ setSelectedFile(null);
65
+ setPreviewUrl(null);
66
+ setResult(null);
67
+ setError(null);
68
+ setFeedbackStatus(null);
69
+ if (inputRef.current) inputRef.current.value = "";
70
+ };
71
+
72
+ // --- API Integration ---
73
+ const analyzeXRay = async () => {
74
+ if (!selectedFile) return;
75
+
76
+ setLoading(true);
77
+ setError(null);
78
+ setFeedbackStatus(null);
79
+
80
+ const formData = new FormData();
81
+ formData.append("file", selectedFile);
82
+
83
+ try {
84
+ const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
85
+ const response = await axios.post(`${API_URL}/api/v1/predict`, formData, {
86
+ headers: { "Content-Type": "multipart/form-data" }
87
+ });
88
+ setResult(response.data);
89
+ } catch (err) {
90
+ console.error("API Error:", err);
91
+ setError("Failed to connect to the diagnostic engine. Is the Hugging Face space running?");
92
+ } finally {
93
+ setLoading(false);
94
+ }
95
+ };
96
+
97
+ // --- A/B Testing Feedback Loop ---
98
+ const submitFeedback = async (isAgree) => {
99
+ if (!result?.record_id) return;
100
+ setFeedbackStatus('loading');
101
+
102
+ try {
103
+ const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
104
+
105
+ // If the doctor agrees, override is FALSE. If they disagree, override is TRUE.
106
+ const payload = {
107
+ physician_override: !isAgree,
108
+ notes: isAgree ? "Agreed with AI" : "Physician disagreed with AI diagnosis"
109
+ };
110
+
111
+ await axios.put(`${API_URL}/api/v1/override/${result.record_id}`, payload);
112
+ setFeedbackStatus('success');
113
+ } catch (err) {
114
+ console.error("Feedback Error:", err);
115
+ setFeedbackStatus('error');
116
+ }
117
+ };
118
 
119
  return (
120
+ <div className="min-h-screen bg-gray-50 font-sans">
 
121
  <nav className="bg-navy text-white p-4 shadow-md flex justify-between items-center">
122
  <div className="flex items-center gap-3">
123
  <Activity className="w-6 h-6 text-trust-cyan" />
124
+ <span className="font-bold tracking-wide">PNEUMONIA VISION AI</span>
125
  </div>
126
  <div className="flex items-center gap-6">
127
+ <span className="text-sm text-gray-200">Dr. {user?.name.replace('Dr. ', '')}</span>
128
+ <button onClick={logout} className="flex items-center gap-2 hover:text-red-300 transition-colors text-sm font-medium">
 
 
 
129
  <LogOut className="w-4 h-4" /> Logout
130
  </button>
131
  </div>
132
  </nav>
133
 
134
+ <main className="max-w-5xl mx-auto p-6 mt-8">
135
+ <div className="mb-8">
136
+ <h1 className="text-3xl font-bold text-navy">Diagnostic Workbench</h1>
137
+ <p className="text-gray-500 mt-2">Upload a pediatric chest X-Ray for automated pneumonia screening.</p>
138
+ </div>
139
+
140
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
141
+ {/* Left Column: Upload & Preview */}
142
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
143
+ <h2 className="text-lg font-semibold text-gray-800 mb-4">Patient Scan</h2>
144
+
145
+ {!previewUrl ? (
146
+ <div
147
+ className={`flex-1 relative border-2 border-dashed rounded-xl p-12 flex flex-col items-center justify-center transition-all ${dragActive ? 'border-trust-cyan bg-blue-50' : 'border-gray-300 bg-gray-50 hover:bg-gray-100'}`}
148
+ onDragEnter={handleDrag}
149
+ onDragLeave={handleDrag}
150
+ onDragOver={handleDrag}
151
+ onDrop={handleDrop}
152
+ >
153
+ <input
154
+ ref={inputRef}
155
+ type="file"
156
+ accept="image/*"
157
+ onChange={handleChange}
158
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
159
+ />
160
+ <UploadCloud className="w-12 h-12 text-gray-400 mx-auto mb-4" />
161
+ <p className="text-gray-600 font-medium">Drag & Drop X-Ray image here</p>
162
+ <p className="text-gray-400 text-sm mt-1">or click to browse files</p>
163
+ </div>
164
+ ) : (
165
+ <div className="flex-1 relative rounded-xl overflow-hidden border border-gray-200 bg-black flex justify-center items-center">
166
+ <img src={previewUrl} alt="X-Ray Preview" className="max-h-full max-w-full object-contain" />
167
+ <button
168
+ onClick={clearSelection}
169
+ className="absolute top-2 right-2 p-1.5 bg-black/50 text-white rounded-full hover:bg-red-500 transition-colors"
170
+ >
171
+ <XCircle className="w-6 h-6" />
172
+ </button>
173
+ </div>
174
+ )}
175
+
176
+ {error && (
177
+ <div className="mt-4 p-3 bg-red-50 text-critical-red flex items-center gap-2 rounded-md border border-red-100 text-sm">
178
+ <AlertTriangle className="w-5 h-5 flex-shrink-0" />
179
+ <span>{error}</span>
180
+ </div>
181
+ )}
182
+
183
+ <button
184
+ onClick={analyzeXRay}
185
+ disabled={!selectedFile || loading}
186
+ className={`w-full mt-6 py-3 rounded-lg font-semibold text-white transition-all flex justify-center items-center gap-2 ${!selectedFile || loading ? 'bg-gray-300 cursor-not-allowed' : 'bg-clinical-teal hover:bg-teal-800 shadow-md'}`}
187
+ >
188
+ {loading ? (
189
+ <><Loader2 className="w-5 h-5 animate-spin" /> Analyzing Scan...</>
190
+ ) : (
191
+ "Run AI Diagnostics"
192
+ )}
193
+ </button>
194
+ </div>
195
+
196
+ {/* Right Column: AI Results */}
197
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex flex-col h-full">
198
+ <h2 className="text-lg font-semibold text-gray-800 mb-4">AI Analysis Report</h2>
199
+
200
+ {!result && !loading ? (
201
+ <div className="flex-1 flex flex-col items-center justify-center text-gray-400">
202
+ <Activity className="w-16 h-16 mb-4 opacity-20" />
203
+ <p>Awaiting scan data...</p>
204
+ </div>
205
+ ) : loading ? (
206
+ <div className="flex-1 flex flex-col items-center justify-center text-clinical-teal">
207
+ <div className="w-16 h-16 border-4 border-teal-100 border-t-clinical-teal rounded-full animate-spin mb-4"></div>
208
+ <p className="font-medium animate-pulse">Running DenseNet121 Inference...</p>
209
+ </div>
210
+ ) : result && (
211
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-500 flex flex-col h-full">
212
+ {/* Status Badge */}
213
+ <div className={`flex items-center gap-3 p-4 rounded-lg mb-6 ${result.prediction === 'PNEUMONIA' ? 'bg-red-50 border border-red-100' : 'bg-green-50 border border-green-100'}`}>
214
+ {result.prediction === 'PNEUMONIA' ? (
215
+ <AlertTriangle className="w-8 h-8 text-critical-red" />
216
+ ) : (
217
+ <CheckCircle2 className="w-8 h-8 text-surgical-mint" />
218
+ )}
219
+ <div>
220
+ <p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Detection Result</p>
221
+ <h3 className={`text-2xl font-bold ${result.prediction === 'PNEUMONIA' ? 'text-critical-red' : 'text-surgical-mint'}`}>
222
+ {result.prediction}
223
+ </h3>
224
+ </div>
225
+ <div className="ml-auto text-right">
226
+ <p className="text-sm text-gray-500">Confidence</p>
227
+ <p className="text-xl font-bold text-gray-800">{(result.confidence * 100).toFixed(1)}%</p>
228
+ </div>
229
+ </div>
230
+
231
+ {/* Heatmap Image */}
232
+ <div className="mb-6">
233
+ <p className="text-sm font-medium text-gray-700 mb-2">Grad-CAM Explainability Heatmap</p>
234
+ <div className="rounded-xl overflow-hidden border border-gray-200 bg-black flex justify-center items-center h-56">
235
+ <img src={result.heatmap_url} alt="AI Heatmap" className="max-h-full max-w-full object-contain" />
236
+ </div>
237
+ </div>
238
+
239
+ {/* The New Feedback Loop */}
240
+ <div className="mt-auto border-t border-gray-100 pt-4">
241
+ <p className="text-sm font-medium text-gray-700 mb-3 text-center">Do you agree with this AI diagnosis?</p>
242
+
243
+ {feedbackStatus === 'success' ? (
244
+ <div className="flex items-center justify-center gap-2 py-2 px-4 bg-green-50 text-surgical-mint rounded-lg border border-green-100">
245
+ <CheckCircle2 className="w-5 h-5" />
246
+ <span className="font-medium">Feedback recorded. Thank you.</span>
247
+ </div>
248
+ ) : (
249
+ <div className="flex gap-4 justify-center">
250
+ <button
251
+ onClick={() => submitFeedback(true)}
252
+ disabled={feedbackStatus === 'loading'}
253
+ className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 border border-green-200 bg-green-50 text-green-700 rounded-lg hover:bg-green-100 transition-colors font-semibold disabled:opacity-50"
254
+ >
255
+ <ThumbsUp className="w-4 h-4" /> Agree
256
+ </button>
257
+ <button
258
+ onClick={() => submitFeedback(false)}
259
+ disabled={feedbackStatus === 'loading'}
260
+ className="flex-1 flex items-center justify-center gap-2 py-2.5 px-4 border border-red-200 bg-red-50 text-red-700 rounded-lg hover:bg-red-100 transition-colors font-semibold disabled:opacity-50"
261
+ >
262
+ <ThumbsDown className="w-4 h-4" /> Disagree
263
+ </button>
264
+ </div>
265
+ )}
266
+ {feedbackStatus === 'error' && (
267
+ <p className="text-xs text-critical-red mt-2 text-center">Failed to connect to database.</p>
268
+ )}
269
+ </div>
270
+
271
+ <div className="mt-4 text-xs text-gray-400 font-mono text-center">
272
+ Record ID: {result.record_id}
273
+ </div>
274
+ </div>
275
+ )}
276
+ </div>
277
  </div>
278
  </main>
279
  </div>
frontend/src/LoginScreen.jsx CHANGED
@@ -21,7 +21,7 @@ export default function LoginScreen() {
21
  <div className="bg-navy p-3 rounded-full mb-4">
22
  <Activity className="text-white w-8 h-8" />
23
  </div>
24
- <h2 className="text-2xl font-bold text-navy">DEBUTRON LAB</h2>
25
  <p className="text-gray-500 text-sm mt-1">Precision Diagnostics Portal</p>
26
  </div>
27
 
 
21
  <div className="bg-navy p-3 rounded-full mb-4">
22
  <Activity className="text-white w-8 h-8" />
23
  </div>
24
+ <h2 className="text-2xl font-bold text-navy">PNEUMONIA VISION AI</h2>
25
  <p className="text-gray-500 text-sm mt-1">Precision Diagnostics Portal</p>
26
  </div>
27