Spaces:
Sleeping
Sleeping
implemented the dashboard
Browse files- frontend/index.html +45 -3
- frontend/public/apple-touch-icon.svg +6 -0
- frontend/public/manifest.json +23 -0
- frontend/public/social-preview.png +0 -0
- frontend/public/sw.js +40 -0
- frontend/src/Dashboard.jsx +260 -15
- frontend/src/LoginScreen.jsx +1 -1
frontend/index.html
CHANGED
|
@@ -1,13 +1,55 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
-
<
|
| 7 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 15 |
</div>
|
| 16 |
<div className="flex items-center gap-6">
|
| 17 |
-
<span className="text-sm text-gray-200">
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
<
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 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 |
|