File size: 7,108 Bytes
86f402d 58a4476 86f402d 58a4476 86f402d 58a4476 86f402d 58a4476 86f402d 58a4476 86f402d 58a4476 86f402d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../services/api';
import { Patient } from '../types';
import './PatientsPage.css';
export function PatientsPage() {
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(true);
const [showNewPatient, setShowNewPatient] = useState(false);
const [newPatientName, setNewPatientName] = useState('');
const navigate = useNavigate();
useEffect(() => {
loadPatients();
}, []);
const loadPatients = () => {
api.listPatients()
.then(res => setPatients(res.patients))
.finally(() => setLoading(false));
};
const handleCreatePatient = async () => {
if (!newPatientName.trim()) return;
const { patient } = await api.createPatient(newPatientName.trim());
setPatients(prev => [...prev, patient]);
setNewPatientName('');
setShowNewPatient(false);
navigate(`/chat/${patient.id}`);
};
const handleDeletePatient = async (e: React.MouseEvent, patientId: string) => {
e.stopPropagation();
if (!confirm('Delete this patient and all their data?')) return;
await api.deletePatient(patientId);
setPatients(prev => prev.filter(p => p.id !== patientId));
};
if (loading) {
return (
<div className="patients-page">
<div className="loading">Loading...</div>
</div>
);
}
return (
<div className="patients-page">
{/* Hero Section */}
<section className="hero">
<h1 className="title">SkinProAI</h1>
<p className="tagline">
Multimodal dermatological analysis powered by MedGemma
and intelligent tool orchestration.
</p>
<button className="cta-btn" onClick={() => setShowNewPatient(true)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
New Patient
</button>
</section>
{/* Existing patients */}
{patients.length > 0 && (
<section className="patients-section">
<p className="section-label">Recent Patients</p>
<div className="patients-grid">
{patients.map(patient => (
<div
key={patient.id}
className="patient-card"
onClick={() => navigate(`/chat/${patient.id}`)}
>
<div className="patient-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<span className="patient-name">{patient.name}</span>
<button
className="delete-btn"
onClick={(e) => handleDeletePatient(e, patient.id)}
title="Delete patient"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
</button>
</div>
))}
</div>
</section>
)}
{/* How It Works */}
<section className="how-section">
<p className="section-label">How It Works</p>
<div className="steps-row">
<div className="step-card">
<div className="step-num">1</div>
<h3>Upload</h3>
<p>Capture or upload a dermatoscopic or clinical image of the lesion.</p>
</div>
<div className="step-card">
<div className="step-num">2</div>
<h3>Analyze</h3>
<p>MedGemma examines the image and coordinates specialist tools for deeper insight.</p>
</div>
<div className="step-card">
<div className="step-num">3</div>
<h3>Track</h3>
<p>Monitor lesions over time with side-by-side comparison and change detection.</p>
</div>
</div>
</section>
{/* About */}
<section className="about-section">
<div className="about-card">
<h3>About SkinProAI</h3>
<p>
Built for the <strong>Kaggle MedGemma Multimodal Medical AI Competition</strong>,
SkinProAI explores how a foundation medical vision-language model can be
augmented with specialised tools to deliver richer clinical insight.
</p>
<p>
At its core sits Google's <strong>MedGemma 4B</strong>, a multimodal model
fine-tuned for medical image understanding. Rather than relying on the model
alone, SkinProAI connects it to a suite of external tools via
the <strong>Model Context Protocol (MCP)</strong> — including MONET
feature extraction, ConvNeXt classification, Grad-CAM attention maps, and
clinical guideline retrieval — letting the model reason across multiple
sources before presenting a synthesised assessment.
</p>
<div className="tech-pills">
<span className="pill">MedGemma 4B</span>
<span className="pill">MCP Tools</span>
<span className="pill">MONET</span>
<span className="pill">ConvNeXt</span>
<span className="pill">Grad-CAM</span>
<span className="pill">RAG Guidelines</span>
</div>
</div>
</section>
{/* Disclaimer */}
<footer className="disclaimer">
<p>
<strong>Research prototype only.</strong> SkinProAI is an educational project and
competition entry. It is not a medical device and must not be used for clinical
decision-making. Always consult a qualified healthcare professional for diagnosis
and treatment.
</p>
</footer>
{/* New Patient Modal */}
{showNewPatient && (
<div className="modal-overlay" onClick={() => setShowNewPatient(false)}>
<div className="modal" onClick={e => e.stopPropagation()}>
<h2>New Patient</h2>
<input
type="text"
placeholder="Patient name..."
value={newPatientName}
onChange={e => setNewPatientName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreatePatient()}
autoFocus
/>
<div className="modal-buttons">
<button className="cancel-btn" onClick={() => setShowNewPatient(false)}>
Cancel
</button>
<button
className="create-btn"
onClick={handleCreatePatient}
disabled={!newPatientName.trim()}
>
Create
</button>
</div>
</div>
</div>
)}
</div>
);
}
|