|
|
|
|
|
import React, { useState, useCallback, useMemo } from 'react'; |
|
|
|
|
|
|
|
|
const IconUpload = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>; |
|
|
const IconCpu = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M20 15h2"/></svg>; |
|
|
const IconCheckCircle = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>; |
|
|
const IconXCircle = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>; |
|
|
const IconLoader = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.8-2.8"/><path d="M18 12h4"/><path d="m19.72 19.72-2.8-2.8"/><path d="M12 18v4"/><path d="m4.22 19.78 2.8-2.8"/><path d="M2 12h4"/><path d="m4.22 4.22 2.8 2.8"/></svg>; |
|
|
const IconBrain = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a2 2 0 0 0-2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 0-2-2"/><path d="M20 2a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2"/></svg>; |
|
|
const IconAlertTriangle = (props) => <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><line x1="12" x2="12" y1="9" y2="13"/><line x1="12" x2="12.01" y1="17" y2="17"/></svg>; |
|
|
|
|
|
const App = () => { |
|
|
const [selectedFile, setSelectedFile] = useState(null); |
|
|
const [previewUrl, setPreviewUrl] = useState(''); |
|
|
const [prediction, setPrediction] = useState(null); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [error, setError] = useState(''); |
|
|
|
|
|
|
|
|
const API_URL = ''; |
|
|
|
|
|
const CLASS_INFO = useMemo(() => ({ |
|
|
glioma: { name: "Glioma Tumor (Malignant)", color: "#dc2626", bg: "#fef2f2", icon: <IconAlertTriangle className="icon-sm" /> }, |
|
|
meningioma: { name: "Meningioma Tumor (Benign)", color: "#ea580c", bg: "#fff7ed", icon: <IconAlertTriangle className="icon-sm" /> }, |
|
|
pituitary: { name: "Pituitary Tumor (Benign)", color: "#ca8a04", bg: "#fefce8", icon: <IconAlertTriangle className="icon-sm" /> }, |
|
|
notumor: { name: "No Tumor Detected", color: "#16a34a", bg: "#f0fdf4", icon: <IconCheckCircle className="icon-sm" /> }, |
|
|
}), []); |
|
|
|
|
|
const handleFileChange = useCallback((event) => { |
|
|
const file = event.target.files[0]; |
|
|
if (file) { |
|
|
setSelectedFile(file); |
|
|
setPreviewUrl(URL.createObjectURL(file)); |
|
|
setPrediction(null); |
|
|
setError(''); |
|
|
} |
|
|
}, []); |
|
|
|
|
|
const handleSubmit = async (event) => { |
|
|
event.preventDefault(); |
|
|
|
|
|
if (!selectedFile) { |
|
|
setError('Please select an MRI image file.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
setLoading(true); |
|
|
setPrediction(null); |
|
|
setError(''); |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('mriImage', selectedFile); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${API_URL}/api/predict`, { |
|
|
method: 'POST', |
|
|
body: formData, |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(data.detail || data.error || 'Unknown server error.'); |
|
|
} |
|
|
|
|
|
setPrediction(data); |
|
|
} catch (err) { |
|
|
console.error('Prediction failed:', err); |
|
|
setError(`Prediction failed: ${err.message}`); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const ResultDisplay = ({ result }) => { |
|
|
const info = CLASS_INFO[result.predicted_class.toLowerCase()] || CLASS_INFO.notumor; |
|
|
const confidencePercent = (result.confidence * 100).toFixed(2); |
|
|
|
|
|
return ( |
|
|
<div |
|
|
className="result-card" |
|
|
style={{ |
|
|
backgroundColor: info.bg, |
|
|
borderLeftColor: info.color |
|
|
}} |
|
|
> |
|
|
<h3 className="result-title"> |
|
|
{info.icon} |
|
|
<span className="result-title-text">Diagnosis Result</span> |
|
|
</h3> |
|
|
<div className="result-details"> |
|
|
<div className="result-row result-row-border"> |
|
|
<span className="result-label">Predicted Class:</span> |
|
|
<span className="result-value" style={{ color: info.color }}>{info.name}</span> |
|
|
</div> |
|
|
<div className="result-row"> |
|
|
<span className="result-label">Confidence Score:</span> |
|
|
<span className="result-value result-value-indigo">{confidencePercent}%</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="confidence-bar-container"> |
|
|
<div |
|
|
className="confidence-bar-fill" |
|
|
style={{ |
|
|
width: `${confidencePercent}%`, |
|
|
backgroundColor: info.color |
|
|
}} |
|
|
></div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="app-container"> |
|
|
<style>{` |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); |
|
|
|
|
|
:root { |
|
|
--color-indigo-600: #4f46e5; |
|
|
--color-indigo-700: #4338ca; |
|
|
--color-gray-900: #111827; |
|
|
--color-gray-500: #6b7280; |
|
|
--color-gray-300: #d1d5db; |
|
|
--color-white: #ffffff; |
|
|
--color-gray-100: #f3f4f6; |
|
|
--color-gray-50: #f9fafb; |
|
|
--color-red-700: #b91c1c; |
|
|
--color-red-100: #fee2e2; |
|
|
} |
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
|
|
|
|
|
.app-container { |
|
|
min-height: 100vh; |
|
|
background-color: var(--color-gray-100); |
|
|
padding: 1rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-family: 'Inter', sans-serif; |
|
|
} |
|
|
|
|
|
.main-card { |
|
|
width: 100%; |
|
|
max-width: 900px; |
|
|
background-color: var(--color-white); |
|
|
border-radius: 1.5rem; |
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); |
|
|
padding: 2.5rem; |
|
|
border-top: 8px solid var(--color-indigo-600); |
|
|
} |
|
|
@media (max-width: 768px) { .main-card { padding: 1.5rem; } } |
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 2.5rem; |
|
|
} |
|
|
.header-icon { |
|
|
color: var(--color-indigo-600); |
|
|
width: 3rem; |
|
|
height: 3rem; |
|
|
margin: 0 auto 0.75rem; |
|
|
stroke-width: 2.5; |
|
|
} |
|
|
.header-title { |
|
|
font-size: 2.25rem; |
|
|
font-weight: 800; |
|
|
color: var(--color-gray-900); |
|
|
letter-spacing: -0.025em; |
|
|
} |
|
|
.header-subtitle { |
|
|
margin-top: 0.5rem; |
|
|
font-size: 1.125rem; |
|
|
color: var(--color-gray-500); |
|
|
} |
|
|
|
|
|
.form-grid { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr; |
|
|
gap: 2rem; |
|
|
} |
|
|
@media (min-width: 768px) { |
|
|
.form-grid { grid-template-columns: repeat(2, 1fr); } |
|
|
} |
|
|
|
|
|
.section-upload { |
|
|
padding: 1rem; |
|
|
border: 2px dashed var(--color-gray-300); |
|
|
border-radius: 0.75rem; |
|
|
transition: border-color 200ms ease-in-out; |
|
|
} |
|
|
.section-upload:hover { border-color: #6366f1; } |
|
|
.section-results { |
|
|
padding: 1rem; |
|
|
background-color: var(--color-gray-50); |
|
|
border-radius: 0.75rem; |
|
|
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
|
|
|
.section-title { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 600; |
|
|
color: var(--color-indigo-700); |
|
|
margin-bottom: 1rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
} |
|
|
.section-title-results { color: var(--color-gray-800); } |
|
|
.section-title-results > svg { color: var(--color-indigo-700); } |
|
|
.section-title > svg { |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
margin-right: 0.5rem; |
|
|
} |
|
|
|
|
|
.upload-label { |
|
|
cursor: pointer; |
|
|
display: block; |
|
|
width: 100%; |
|
|
text-align: center; |
|
|
padding: 2.5rem 0; |
|
|
background-color: var(--color-gray-50); |
|
|
border-radius: 0.5rem; |
|
|
transition: background-color 200ms ease-in-out; |
|
|
} |
|
|
.upload-label:hover { background-color: var(--color-gray-100); } |
|
|
.upload-text-strong { |
|
|
font-weight: 500; |
|
|
color: #4b5563; |
|
|
} |
|
|
.upload-text-file { |
|
|
color: var(--color-indigo-600); |
|
|
font-weight: 700; |
|
|
} |
|
|
.upload-text-muted { |
|
|
color: #9ca3af; |
|
|
font-size: 0.875rem; |
|
|
margin-top: 0.25rem; |
|
|
} |
|
|
.upload-icon { |
|
|
width: 2rem; |
|
|
height: 2rem; |
|
|
margin: 0 auto 0.5rem; |
|
|
color: #9ca3af; |
|
|
} |
|
|
|
|
|
.submit-button { |
|
|
width: 100%; |
|
|
margin-top: 1rem; |
|
|
padding: 0.75rem 0; |
|
|
border-radius: 0.75rem; |
|
|
font-weight: 700; |
|
|
font-size: 1.125rem; |
|
|
transition: all 300ms ease-in-out; |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.06); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
color: var(--color-white); |
|
|
background-color: var(--color-indigo-600); |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
} |
|
|
.submit-button:hover:not(:disabled) { |
|
|
background-color: var(--color-indigo-700); |
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
.submit-button:disabled { |
|
|
background-color: #a5b4fc; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
.submit-button > svg { |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
margin-right: 0.5rem; |
|
|
} |
|
|
|
|
|
.preview-container { margin-bottom: 1rem; } |
|
|
.preview-label { |
|
|
font-size: 1rem; |
|
|
font-weight: 500; |
|
|
color: #4b5563; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
.preview-box { |
|
|
width: 100%; |
|
|
height: 12rem; |
|
|
background-color: #e5e7eb; |
|
|
border-radius: 0.5rem; |
|
|
overflow: hidden; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border: 1px solid var(--color-gray-300); |
|
|
} |
|
|
.preview-placeholder { color: #9ca3af; } |
|
|
.preview-image { |
|
|
object-fit: cover; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
} |
|
|
|
|
|
.status-message { |
|
|
padding: 0.75rem; |
|
|
border-radius: 0.5rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
font-weight: 500; |
|
|
} |
|
|
.status-message > svg { |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
margin-right: 0.5rem; |
|
|
} |
|
|
.status-error { |
|
|
background-color: var(--color-red-100); |
|
|
color: var(--color-red-700); |
|
|
} |
|
|
.status-info { |
|
|
background-color: #e0e7ff; |
|
|
color: var(--color-indigo-700); |
|
|
} |
|
|
|
|
|
.result-card { |
|
|
margin-top: 2rem; |
|
|
padding: 1.5rem; |
|
|
border-radius: 0.75rem; |
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); |
|
|
transition: all 300ms ease-in-out; |
|
|
border-left-width: 8px; |
|
|
border-style: solid; |
|
|
} |
|
|
.result-title { |
|
|
font-size: 1.5rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 1rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
} |
|
|
.result-title-text { |
|
|
margin-left: 0.75rem; |
|
|
color: var(--color-gray-800); |
|
|
} |
|
|
.result-details { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
.result-row { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
padding: 0.5rem 0; |
|
|
} |
|
|
.result-row-border { border-bottom: 1px solid var(--color-gray-300); } |
|
|
.result-label { |
|
|
font-size: 1.125rem; |
|
|
font-weight: 600; |
|
|
color: #4b5563; |
|
|
} |
|
|
.result-value { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 800; |
|
|
} |
|
|
.result-value-indigo { |
|
|
font-weight: 700; |
|
|
color: var(--color-indigo-700); |
|
|
} |
|
|
|
|
|
.confidence-bar-container { |
|
|
width: 100%; |
|
|
height: 0.75rem; |
|
|
margin-top: 1rem; |
|
|
border-radius: 9999px; |
|
|
background-color: #e5e7eb; |
|
|
overflow: hidden; |
|
|
} |
|
|
.confidence-bar-fill { |
|
|
height: 100%; |
|
|
transition: width 500ms ease-out; |
|
|
} |
|
|
|
|
|
.loader-spin { |
|
|
animation: spin 1s linear infinite; |
|
|
} |
|
|
@keyframes spin { |
|
|
from { transform: rotate(0deg); } |
|
|
to { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
.icon-sm { |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
} |
|
|
`}</style> |
|
|
|
|
|
<div className="main-card"> |
|
|
<header className="header"> |
|
|
<div className="header-icon mx-auto mb-3"> |
|
|
<IconBrain className="header-icon" strokeWidth={2.5} /> |
|
|
</div> |
|
|
<h1 className="header-title"> |
|
|
NeuroGuard |
|
|
</h1> |
|
|
<p className="header-subtitle"> |
|
|
Upload an MRI image for automated Brain tumor detection and classification. |
|
|
</p> |
|
|
</header> |
|
|
|
|
|
<form onSubmit={handleSubmit} className="form-grid"> |
|
|
<section className="section-upload"> |
|
|
<h2 className="section-title"> |
|
|
<IconUpload className="icon-sm mr-2" /> |
|
|
1. Upload MRI Scan |
|
|
</h2> |
|
|
|
|
|
<label htmlFor="file-upload" className="upload-label"> |
|
|
{selectedFile ? ( |
|
|
<p className="upload-text-strong">File Selected: <span className="upload-text-file">{selectedFile.name}</span></p> |
|
|
) : ( |
|
|
<> |
|
|
<IconUpload className="upload-icon" /> |
|
|
<p className="upload-text-strong" style={{ color: '#4b5563' }}>Click to upload or drag & drop</p> |
|
|
<p className="upload-text-muted">PNG, JPG, or JPEG (Max 10MB)</p> |
|
|
</> |
|
|
)} |
|
|
</label> |
|
|
<input |
|
|
id="file-upload" |
|
|
type="file" |
|
|
onChange={handleFileChange} |
|
|
accept="image/*" |
|
|
style={{ display: 'none' }} |
|
|
/> |
|
|
|
|
|
<button type="submit" disabled={loading || !selectedFile} className="submit-button"> |
|
|
{loading ? ( |
|
|
<> |
|
|
<IconLoader className="icon-sm mr-2 loader-spin" /> |
|
|
Analyzing... |
|
|
</> |
|
|
) : ( |
|
|
<> |
|
|
<IconCpu className="icon-sm mr-2" /> |
|
|
Run Diagnosis |
|
|
</> |
|
|
)} |
|
|
</button> |
|
|
</section> |
|
|
|
|
|
<section className="section-results"> |
|
|
<h2 className="section-title section-title-results"> |
|
|
<IconCheckCircle className="icon-sm mr-2" /> |
|
|
2. Review & Diagnosis |
|
|
</h2> |
|
|
|
|
|
<div className="preview-container"> |
|
|
<h3 className="preview-label">Image Preview:</h3> |
|
|
<div className="preview-box"> |
|
|
{previewUrl ? ( |
|
|
<img src={previewUrl} alt="MRI Preview" className="preview-image" /> |
|
|
) : ( |
|
|
<p className="preview-placeholder">Preview Area</p> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{error && ( |
|
|
<div className="status-message status-error"> |
|
|
<IconXCircle className="icon-sm mr-2" /> |
|
|
<span style={{ fontWeight: 700 }}>Error:</span> {error} |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{prediction && <ResultDisplay result={prediction} />} |
|
|
|
|
|
{!loading && !prediction && !error && ( |
|
|
<div className="status-message status-info"> |
|
|
<IconBrain className="icon-sm mr-2" /> |
|
|
Upload an image and click "Run Diagnosis" to begin analysis. |
|
|
</div> |
|
|
)} |
|
|
</section> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default App; |