| 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> | |
| ); | |
| } | |