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> &mdash; including MONET
            feature extraction, ConvNeXt classification, Grad-CAM attention maps, and
            clinical guideline retrieval &mdash; 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>
  );
}