cgoodmaker Claude Opus 4.6 commited on
Commit
58a4476
·
1 Parent(s): 4cbee96

Redesign chat UI and fix MedGemma generation config issues

Browse files

- Landing page: sunrise orb with glassmorphism, about section, disclaimer
- Chat page: remove card wrappers from AI responses, plain text rendering
- Tool calls shown as inline status lines with dots/spinners (not cards)
- Thinking segments show animated spinner while active, done dot when complete
- Fix max_new_tokens vs max_length conflict in generation_config
- Fix Gemma3ImageProcessor warning by explicitly loading fast processor
- Add chat route import to backend routes __init__

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

backend/routes/__init__.py CHANGED
@@ -1 +1 @@
1
- from . import patients, lesions, analysis
 
1
+ from . import patients, lesions, analysis, chat
models/medgemma_agent.py CHANGED
@@ -205,7 +205,7 @@ class MedGemmaAgent:
205
 
206
  import os
207
  import torch
208
- from transformers import pipeline
209
 
210
  # Authenticate with HF Hub (required for gated models like MedGemma)
211
  hf_token = os.environ.get("HF_TOKEN")
@@ -234,13 +234,23 @@ class MedGemmaAgent:
234
  )
235
 
236
  start = time.time()
 
 
 
237
  self.pipe = pipeline(
238
  "image-text-to-text",
239
  model=self.model_id,
240
  model_kwargs=model_kwargs,
241
  token=hf_token, # pass explicitly in addition to login()
 
 
242
  )
243
 
 
 
 
 
 
244
  self._print(f"Model loaded in {time.time() - start:.1f}s")
245
  self.loaded = True
246
 
 
205
 
206
  import os
207
  import torch
208
+ from transformers import pipeline, AutoProcessor
209
 
210
  # Authenticate with HF Hub (required for gated models like MedGemma)
211
  hf_token = os.environ.get("HF_TOKEN")
 
234
  )
235
 
236
  start = time.time()
237
+ processor = AutoProcessor.from_pretrained(
238
+ self.model_id, use_fast=True, token=hf_token,
239
+ )
240
  self.pipe = pipeline(
241
  "image-text-to-text",
242
  model=self.model_id,
243
  model_kwargs=model_kwargs,
244
  token=hf_token, # pass explicitly in addition to login()
245
+ image_processor=processor.image_processor,
246
+ tokenizer=processor.tokenizer,
247
  )
248
 
249
+ # Clear default max_length from generation_config to avoid conflict
250
+ # with max_new_tokens passed at inference time
251
+ if hasattr(self.pipe.model, "generation_config"):
252
+ self.pipe.model.generation_config.max_length = None
253
+
254
  self._print(f"Model loaded in {time.time() - start:.1f}s")
255
  self.loaded = True
256
 
web/src/components/MessageContent.css CHANGED
@@ -5,19 +5,19 @@
5
  gap: 6px;
6
  font-size: 0.9375rem;
7
  line-height: 1.6;
8
- color: var(--gray-800);
9
  width: 100%;
10
  }
11
 
12
  /* ─── Stage header ───────────────────────────────────────────────────────── */
13
  .mc-stage {
14
- font-size: 0.75rem;
15
  font-weight: 600;
16
- color: var(--primary);
17
  text-transform: uppercase;
18
  letter-spacing: 0.06em;
19
  padding: 8px 0 2px;
20
- border-top: 1px solid var(--gray-100);
21
  margin-top: 4px;
22
  }
23
 
@@ -29,14 +29,39 @@
29
 
30
  /* ─── Thinking text ──────────────────────────────────────────────────────── */
31
  .mc-thinking {
 
 
 
32
  font-size: 0.8125rem;
33
- color: var(--gray-500);
34
  font-style: italic;
35
  }
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  /* ─── Response block (markdown) ──────────────────────────────────────────── */
38
  .mc-response {
39
- color: var(--gray-800);
40
  }
41
 
42
  .mc-response p {
@@ -49,7 +74,7 @@
49
 
50
  .mc-response strong {
51
  font-weight: 600;
52
- color: var(--gray-900);
53
  }
54
 
55
  .mc-response em {
@@ -72,29 +97,29 @@
72
  .mc-response h4 {
73
  font-size: 0.9375rem;
74
  font-weight: 600;
75
- color: var(--gray-900);
76
  margin: 10px 0 4px;
77
  }
78
 
79
  .mc-response code {
80
  font-family: monospace;
81
  font-size: 0.875em;
82
- background: var(--gray-100);
83
  padding: 1px 5px;
84
  border-radius: 4px;
85
  }
86
 
87
  /* ─── Tool output (monospace block) ─────────────────────────────────────── */
88
  .mc-tool-output {
89
- background: var(--gray-900);
90
- border-radius: 8px;
91
  overflow: hidden;
92
  }
93
 
94
  .mc-tool-output-label {
95
  font-size: 0.6875rem;
96
  font-weight: 600;
97
- color: var(--gray-400);
98
  text-transform: uppercase;
99
  letter-spacing: 0.05em;
100
  padding: 6px 12px 4px;
@@ -108,7 +133,7 @@
108
  font-family: 'SF Mono', 'Fira Code', monospace;
109
  font-size: 0.8rem;
110
  line-height: 1.5;
111
- color: #e2e8f0;
112
  white-space: pre;
113
  overflow-x: auto;
114
  }
@@ -119,9 +144,9 @@
119
  }
120
 
121
  .mc-image-label {
122
- font-size: 0.75rem;
123
  font-weight: 600;
124
- color: var(--gray-500);
125
  text-transform: uppercase;
126
  letter-spacing: 0.05em;
127
  margin-bottom: 6px;
@@ -130,16 +155,16 @@
130
  .mc-gradcam-img {
131
  width: 100%;
132
  max-width: 380px;
133
- border-radius: 10px;
134
- border: 1px solid var(--gray-200);
135
  display: block;
136
  }
137
 
138
  .mc-comparison-img {
139
  width: 100%;
140
  max-width: 560px;
141
- border-radius: 10px;
142
- border: 1px solid var(--gray-200);
143
  display: block;
144
  }
145
 
@@ -160,22 +185,22 @@
160
  .mc-gradcam-compare-title {
161
  font-size: 0.75rem;
162
  font-weight: 600;
163
- color: var(--gray-600);
164
  text-align: center;
165
  }
166
 
167
  .mc-gradcam-compare-img {
168
  width: 100%;
169
- border-radius: 8px;
170
- border: 1px solid var(--gray-200);
171
  display: block;
172
  }
173
 
174
  /* ─── Result / error / complete / observation ───────────────────────────── */
175
  .mc-result {
176
- background: linear-gradient(135deg, #f0fdf4, #dcfce7);
177
- border: 1px solid #86efac;
178
- border-radius: 8px;
179
  padding: 8px 12px;
180
  font-size: 0.875rem;
181
  font-weight: 500;
@@ -183,9 +208,9 @@
183
  }
184
 
185
  .mc-error {
186
- background: #fef2f2;
187
- border: 1px solid #fca5a5;
188
- border-radius: 8px;
189
  padding: 8px 12px;
190
  font-size: 0.875rem;
191
  color: #dc2626;
@@ -193,13 +218,13 @@
193
 
194
  .mc-complete {
195
  font-size: 0.8rem;
196
- color: var(--gray-400);
197
  text-align: right;
198
  }
199
 
200
  .mc-observation {
201
  font-size: 0.875rem;
202
- color: var(--gray-600);
203
  font-style: italic;
204
  }
205
 
@@ -207,21 +232,21 @@
207
  .mc-text {
208
  white-space: pre-wrap;
209
  word-break: break-word;
210
- color: var(--gray-700);
211
  font-size: 0.875rem;
212
  }
213
 
214
  /* ─── References ─────────────────────────────────────────────────────────── */
215
  .mc-references {
216
- border-top: 1px solid var(--gray-100);
217
  padding-top: 8px;
218
  margin-top: 4px;
219
  }
220
 
221
  .mc-references-title {
222
- font-size: 0.75rem;
223
  font-weight: 600;
224
- color: var(--gray-500);
225
  text-transform: uppercase;
226
  letter-spacing: 0.05em;
227
  margin-bottom: 4px;
@@ -229,7 +254,7 @@
229
 
230
  .mc-ref-item {
231
  font-size: 0.8125rem;
232
- color: var(--gray-600);
233
  line-height: 1.5;
234
  }
235
 
@@ -237,7 +262,7 @@
237
  font-size: 0.6875rem;
238
  vertical-align: super;
239
  margin-right: 4px;
240
- color: var(--primary);
241
  font-weight: 600;
242
  }
243
 
@@ -246,5 +271,5 @@
246
  }
247
 
248
  .mc-ref-page {
249
- color: var(--gray-400);
250
  }
 
5
  gap: 6px;
6
  font-size: 0.9375rem;
7
  line-height: 1.6;
8
+ color: #333333;
9
  width: 100%;
10
  }
11
 
12
  /* ─── Stage header ───────────────────────────────────────────────────────── */
13
  .mc-stage {
14
+ font-size: 0.7rem;
15
  font-weight: 600;
16
+ color: #999999;
17
  text-transform: uppercase;
18
  letter-spacing: 0.06em;
19
  padding: 8px 0 2px;
20
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
21
  margin-top: 4px;
22
  }
23
 
 
29
 
30
  /* ─── Thinking text ──────────────────────────────────────────────────────── */
31
  .mc-thinking {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 8px;
35
  font-size: 0.8125rem;
36
+ color: #999999;
37
  font-style: italic;
38
  }
39
 
40
+ .mc-thinking-spinner {
41
+ width: 12px;
42
+ height: 12px;
43
+ border: 2px solid rgba(0, 0, 0, 0.08);
44
+ border-top-color: #999999;
45
+ border-radius: 50%;
46
+ animation: mc-spin 0.8s linear infinite;
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .mc-thinking-done {
51
+ width: 6px;
52
+ height: 6px;
53
+ background: #bbbbbb;
54
+ border-radius: 50%;
55
+ flex-shrink: 0;
56
+ }
57
+
58
+ @keyframes mc-spin {
59
+ to { transform: rotate(360deg); }
60
+ }
61
+
62
  /* ─── Response block (markdown) ──────────────────────────────────────────── */
63
  .mc-response {
64
+ color: #333333;
65
  }
66
 
67
  .mc-response p {
 
74
 
75
  .mc-response strong {
76
  font-weight: 600;
77
+ color: #1a1a1a;
78
  }
79
 
80
  .mc-response em {
 
97
  .mc-response h4 {
98
  font-size: 0.9375rem;
99
  font-weight: 600;
100
+ color: #111111;
101
  margin: 10px 0 4px;
102
  }
103
 
104
  .mc-response code {
105
  font-family: monospace;
106
  font-size: 0.875em;
107
+ background: rgba(0, 0, 0, 0.05);
108
  padding: 1px 5px;
109
  border-radius: 4px;
110
  }
111
 
112
  /* ─── Tool output (monospace block) ─────────────────────────────────────── */
113
  .mc-tool-output {
114
+ background: #1a1a1a;
115
+ border-radius: 10px;
116
  overflow: hidden;
117
  }
118
 
119
  .mc-tool-output-label {
120
  font-size: 0.6875rem;
121
  font-weight: 600;
122
+ color: #777777;
123
  text-transform: uppercase;
124
  letter-spacing: 0.05em;
125
  padding: 6px 12px 4px;
 
133
  font-family: 'SF Mono', 'Fira Code', monospace;
134
  font-size: 0.8rem;
135
  line-height: 1.5;
136
+ color: #e0e0e0;
137
  white-space: pre;
138
  overflow-x: auto;
139
  }
 
144
  }
145
 
146
  .mc-image-label {
147
+ font-size: 0.7rem;
148
  font-weight: 600;
149
+ color: #999999;
150
  text-transform: uppercase;
151
  letter-spacing: 0.05em;
152
  margin-bottom: 6px;
 
155
  .mc-gradcam-img {
156
  width: 100%;
157
  max-width: 380px;
158
+ border-radius: 12px;
159
+ border: 1px solid rgba(0, 0, 0, 0.06);
160
  display: block;
161
  }
162
 
163
  .mc-comparison-img {
164
  width: 100%;
165
  max-width: 560px;
166
+ border-radius: 12px;
167
+ border: 1px solid rgba(0, 0, 0, 0.06);
168
  display: block;
169
  }
170
 
 
185
  .mc-gradcam-compare-title {
186
  font-size: 0.75rem;
187
  font-weight: 600;
188
+ color: #666666;
189
  text-align: center;
190
  }
191
 
192
  .mc-gradcam-compare-img {
193
  width: 100%;
194
+ border-radius: 10px;
195
+ border: 1px solid rgba(0, 0, 0, 0.06);
196
  display: block;
197
  }
198
 
199
  /* ─── Result / error / complete / observation ───────────────────────────── */
200
  .mc-result {
201
+ background: rgba(22, 163, 74, 0.08);
202
+ border: 1px solid rgba(22, 163, 74, 0.15);
203
+ border-radius: 10px;
204
  padding: 8px 12px;
205
  font-size: 0.875rem;
206
  font-weight: 500;
 
208
  }
209
 
210
  .mc-error {
211
+ background: rgba(239, 68, 68, 0.06);
212
+ border: 1px solid rgba(239, 68, 68, 0.15);
213
+ border-radius: 10px;
214
  padding: 8px 12px;
215
  font-size: 0.875rem;
216
  color: #dc2626;
 
218
 
219
  .mc-complete {
220
  font-size: 0.8rem;
221
+ color: #bbbbbb;
222
  text-align: right;
223
  }
224
 
225
  .mc-observation {
226
  font-size: 0.875rem;
227
+ color: #666666;
228
  font-style: italic;
229
  }
230
 
 
232
  .mc-text {
233
  white-space: pre-wrap;
234
  word-break: break-word;
235
+ color: #444444;
236
  font-size: 0.875rem;
237
  }
238
 
239
  /* ─── References ─────────────────────────────────────────────────────────── */
240
  .mc-references {
241
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
242
  padding-top: 8px;
243
  margin-top: 4px;
244
  }
245
 
246
  .mc-references-title {
247
+ font-size: 0.7rem;
248
  font-weight: 600;
249
+ color: #999999;
250
  text-transform: uppercase;
251
  letter-spacing: 0.05em;
252
  margin-bottom: 4px;
 
254
 
255
  .mc-ref-item {
256
  font-size: 0.8125rem;
257
+ color: #666666;
258
  line-height: 1.5;
259
  }
260
 
 
262
  font-size: 0.6875rem;
263
  vertical-align: super;
264
  margin-right: 4px;
265
+ color: #555555;
266
  font-weight: 600;
267
  }
268
 
 
271
  }
272
 
273
  .mc-ref-page {
274
+ color: #aaaaaa;
275
  }
web/src/components/MessageContent.tsx CHANGED
@@ -158,8 +158,18 @@ export function MessageContent({ text }: { text: string }) {
158
  case 'stage':
159
  return <div key={i} className="mc-stage">{seg.label}</div>;
160
 
161
- case 'thinking':
162
- return <div key={i} className="mc-thinking">{seg.content}</div>;
 
 
 
 
 
 
 
 
 
 
163
 
164
  case 'response':
165
  return (
 
158
  case 'stage':
159
  return <div key={i} className="mc-stage">{seg.label}</div>;
160
 
161
+ case 'thinking': {
162
+ // Spinner only on the last thinking segment (earlier ones are done)
163
+ const isLast = !segments.slice(i + 1).some(s => s.type !== 'text' || s.content.trim());
164
+ return (
165
+ <div key={i} className="mc-thinking">
166
+ {isLast
167
+ ? <span className="mc-thinking-spinner" />
168
+ : <span className="mc-thinking-done" />}
169
+ {seg.content}
170
+ </div>
171
+ );
172
+ }
173
 
174
  case 'response':
175
  return (
web/src/components/ToolCallCard.css CHANGED
@@ -1,338 +1,59 @@
1
- /* ─── Card container ─────────────────────────────────────────────────────── */
2
- .tool-card {
3
- border: 1px solid var(--gray-200);
4
- border-left: 3px solid var(--primary);
5
- border-radius: 10px;
6
- overflow: hidden;
7
- background: var(--gray-50);
8
- margin-top: 8px;
9
- }
10
-
11
- .tool-card.loading {
12
- border-left-color: var(--gray-400);
13
- }
14
-
15
- .tool-card.error {
16
- border-left-color: #ef4444;
17
- }
18
-
19
- /* ─── Header (collapsed row) ─────────────────────────────────────────────── */
20
- .tool-card-header {
21
- width: 100%;
22
  display: flex;
23
  align-items: center;
24
  gap: 8px;
25
- padding: 10px 14px;
26
- background: transparent;
27
- border: none;
28
- cursor: pointer;
29
- text-align: left;
30
- transition: background 0.15s;
31
- }
32
-
33
- .tool-card-header:hover:not(:disabled) {
34
- background: var(--gray-100);
35
- }
36
-
37
- .tool-card-header:disabled {
38
- cursor: default;
39
  }
40
 
41
- .tool-icon {
42
- font-size: 1rem;
43
  flex-shrink: 0;
44
- }
45
-
46
- .tool-label {
47
- flex: 1;
48
- font-size: 0.875rem;
49
- font-weight: 500;
50
- color: var(--gray-700);
51
- text-transform: capitalize;
52
- }
53
-
54
- .tool-status {
55
- font-size: 0.8125rem;
56
- flex-shrink: 0;
57
- }
58
-
59
- .tool-status.done {
60
- color: var(--success, #22c55e);
61
- font-weight: 600;
62
- }
63
-
64
- .tool-status.calling {
65
- color: var(--gray-500);
66
  display: flex;
67
  align-items: center;
68
- gap: 5px;
69
  }
70
 
71
- .tool-status.error-text {
72
- color: #ef4444;
73
- }
74
-
75
- .tool-header-summary {
76
- font-size: 0.8125rem;
77
- color: var(--gray-500);
78
- font-weight: 400;
79
- white-space: nowrap;
80
- overflow: hidden;
81
- text-overflow: ellipsis;
82
- max-width: 200px;
83
  }
84
 
85
- .tool-chevron {
86
- font-size: 0.625rem;
87
- color: var(--gray-400);
88
- margin-left: 2px;
89
- flex-shrink: 0;
90
- }
91
 
92
- /* ─── Spinner ────────────────────────────────────────────────────────────── */
93
- .spinner {
94
  display: inline-block;
95
- width: 12px;
96
- height: 12px;
97
- border: 2px solid var(--gray-300);
98
- border-top-color: var(--gray-600);
99
  border-radius: 50%;
100
- animation: spin 0.8s linear infinite;
101
  }
102
 
103
- @keyframes spin {
104
  to { transform: rotate(360deg); }
105
  }
106
 
107
- /* ─── Card body ──────────────────────────────────────────────────────────── */
108
- .tool-card-body {
109
- padding: 14px;
110
- border-top: 1px solid var(--gray-200);
111
- background: white;
112
- }
113
-
114
- /* ─── analyze_image ──────────────────────────────────────────────────────── */
115
- .analyze-result {
116
- display: flex;
117
- flex-direction: column;
118
- gap: 12px;
119
- }
120
-
121
- .analyze-top {
122
- display: flex;
123
- gap: 14px;
124
- align-items: flex-start;
125
- }
126
-
127
- .analyze-thumb {
128
- width: 72px;
129
- height: 72px;
130
- object-fit: cover;
131
- border-radius: 8px;
132
- border: 1px solid var(--gray-200);
133
- flex-shrink: 0;
134
- }
135
-
136
- .analyze-info {
137
- flex: 1;
138
- min-width: 0;
139
- }
140
-
141
- .diagnosis-name {
142
- font-size: 0.9375rem;
143
- font-weight: 600;
144
- color: var(--gray-900);
145
- margin: 0 0 4px;
146
- line-height: 1.3;
147
- }
148
-
149
- .confidence-label {
150
- font-size: 0.8125rem;
151
- font-weight: 500;
152
- margin: 0 0 6px;
153
- }
154
-
155
- .confidence-bar-track {
156
- height: 6px;
157
- background: var(--gray-200);
158
- border-radius: 999px;
159
- overflow: hidden;
160
- }
161
-
162
- .confidence-bar-fill {
163
- height: 100%;
164
- border-radius: 999px;
165
- transition: width 0.3s ease;
166
- }
167
-
168
- .analyze-summary {
169
- font-size: 0.875rem;
170
- color: var(--gray-700);
171
- line-height: 1.6;
172
- margin: 0;
173
- border-top: 1px solid var(--gray-100);
174
- padding-top: 10px;
175
- white-space: pre-wrap;
176
- }
177
-
178
- .other-predictions {
179
- list-style: none;
180
- padding: 0;
181
- margin: 0;
182
- display: flex;
183
- flex-direction: column;
184
- gap: 6px;
185
- border-top: 1px solid var(--gray-100);
186
- padding-top: 10px;
187
- }
188
-
189
- .prediction-row {
190
- display: flex;
191
- justify-content: space-between;
192
  font-size: 0.8125rem;
 
193
  }
194
 
195
- .pred-name {
196
- color: var(--gray-600);
197
- }
198
-
199
- .pred-pct {
200
- color: var(--gray-500);
201
- font-variant-numeric: tabular-nums;
202
- }
203
-
204
- /* ─── compare_images ─────────────────────────────────────────────────────── */
205
- .compare-result {
206
- display: flex;
207
- flex-direction: column;
208
- gap: 12px;
209
- }
210
-
211
- .carousel {
212
- position: relative;
213
- display: flex;
214
- align-items: center;
215
- justify-content: center;
216
- gap: 8px;
217
- }
218
-
219
- .carousel-image-wrap {
220
- position: relative;
221
- display: inline-block;
222
- }
223
-
224
- .carousel-image {
225
- width: 200px;
226
- height: 160px;
227
- object-fit: cover;
228
- border-radius: 10px;
229
- border: 1px solid var(--gray-200);
230
- display: block;
231
- }
232
-
233
- .carousel-label {
234
- position: absolute;
235
- bottom: 8px;
236
- left: 50%;
237
- transform: translateX(-50%);
238
- background: rgba(0, 0, 0, 0.55);
239
- color: white;
240
- font-size: 0.75rem;
241
- font-weight: 600;
242
- padding: 3px 10px;
243
- border-radius: 999px;
244
- white-space: nowrap;
245
- }
246
-
247
- .carousel-btn {
248
- background: white;
249
- border: 1px solid var(--gray-300);
250
- border-radius: 6px;
251
- width: 28px;
252
- height: 28px;
253
- cursor: pointer;
254
  font-size: 0.75rem;
255
- color: var(--gray-600);
256
- display: flex;
257
- align-items: center;
258
- justify-content: center;
259
- flex-shrink: 0;
260
- }
261
-
262
- .carousel-btn:hover {
263
- background: var(--gray-100);
264
- }
265
-
266
- .carousel-dots {
267
- position: absolute;
268
- bottom: -18px;
269
- left: 50%;
270
- transform: translateX(-50%);
271
- display: flex;
272
- gap: 5px;
273
- }
274
-
275
- .carousel-dot {
276
- width: 6px;
277
- height: 6px;
278
- border-radius: 50%;
279
- background: var(--gray-300);
280
- cursor: pointer;
281
- transition: background 0.15s;
282
- }
283
-
284
- .carousel-dot.active {
285
- background: var(--primary);
286
  }
287
 
288
- .compare-status {
289
- font-size: 0.9375rem;
290
- margin-top: 6px;
291
- }
292
-
293
- .feature-changes {
294
- list-style: none;
295
- padding: 0;
296
- margin: 0;
297
- display: flex;
298
- flex-direction: column;
299
- gap: 6px;
300
- }
301
-
302
- .feature-row {
303
- display: flex;
304
- justify-content: space-between;
305
  font-size: 0.8125rem;
306
- }
307
-
308
- .feature-name {
309
- color: var(--gray-600);
310
- text-transform: capitalize;
311
- }
312
-
313
- .feature-delta {
314
- font-variant-numeric: tabular-nums;
315
  font-weight: 500;
316
  }
317
 
318
- .compare-summary {
319
- font-size: 0.875rem;
320
- color: var(--gray-600);
321
- line-height: 1.5;
322
- margin: 0;
323
- border-top: 1px solid var(--gray-100);
324
- padding-top: 10px;
325
- }
326
-
327
- /* ─── Generic fallback ───────────────────────────────────────────────────── */
328
- .generic-result {
329
  font-size: 0.75rem;
330
- background: var(--gray-50);
331
- border-radius: 6px;
332
- padding: 10px;
333
- overflow-x: auto;
334
- color: var(--gray-700);
335
- margin: 0;
336
- white-space: pre-wrap;
337
- word-break: break-all;
338
  }
 
1
+ /* ─── Tool status line ────────────────────────────────────────────────────── */
2
+ .tool-line {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  display: flex;
4
  align-items: center;
5
  gap: 8px;
6
+ padding: 4px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  }
8
 
9
+ .tool-line-indicator {
 
10
  flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  display: flex;
12
  align-items: center;
 
13
  }
14
 
15
+ .tool-dot {
16
+ width: 6px;
17
+ height: 6px;
18
+ border-radius: 50%;
 
 
 
 
 
 
 
 
19
  }
20
 
21
+ .dot-green { background: #22c55e; }
22
+ .dot-amber { background: #f59e0b; }
23
+ .dot-red { background: #ef4444; }
24
+ .dot-blue { background: #3b82f6; }
 
 
25
 
26
+ .tool-spinner {
 
27
  display: inline-block;
28
+ width: 10px;
29
+ height: 10px;
30
+ border: 1.5px solid #dddddd;
31
+ border-top-color: #888888;
32
  border-radius: 50%;
33
+ animation: tool-spin 0.8s linear infinite;
34
  }
35
 
36
+ @keyframes tool-spin {
37
  to { transform: rotate(360deg); }
38
  }
39
 
40
+ .tool-line-label {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  font-size: 0.8125rem;
42
+ color: #888888;
43
  }
44
 
45
+ .tool-line-status {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  font-size: 0.75rem;
47
+ color: #bbbbbb;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
+ .tool-line-summary {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  font-size: 0.8125rem;
52
+ color: #555555;
 
 
 
 
 
 
 
 
53
  font-weight: 500;
54
  }
55
 
56
+ .tool-line-error {
 
 
 
 
 
 
 
 
 
 
57
  font-size: 0.75rem;
58
+ color: #ef4444;
 
 
 
 
 
 
 
59
  }
web/src/components/ToolCallCard.tsx CHANGED
@@ -1,4 +1,3 @@
1
- import { useEffect, useState } from 'react';
2
  import { ToolCall } from '../types';
3
  import './ToolCallCard.css';
4
 
@@ -6,202 +5,56 @@ interface ToolCallCardProps {
6
  toolCall: ToolCall;
7
  }
8
 
9
- /** One-line summary shown in the collapsed header so results are visible at a glance */
10
- function CollapsedSummary({ toolCall }: { toolCall: ToolCall }) {
11
- const r = toolCall.result;
12
- if (!r) return null;
13
-
14
- if (toolCall.tool === 'analyze_image') {
15
- const name = r.full_name ?? r.diagnosis;
16
- const pct = r.confidence != null ? `${Math.round(r.confidence * 100)}%` : null;
17
- if (name) return (
18
- <span className="tool-header-summary">
19
- {name}{pct ? ` — ${pct}` : ''}
20
- </span>
21
- );
22
- }
23
-
24
- if (toolCall.tool === 'compare_images') {
25
- const key = r.status_label ?? 'STABLE';
26
- const cfg = STATUS_CONFIG[key] ?? { emoji: '⚪', label: key };
27
- return (
28
- <span className="tool-header-summary">
29
- {cfg.emoji} {cfg.label}
30
- </span>
31
- );
32
- }
33
 
34
- return null;
35
- }
 
 
 
 
36
 
37
  export function ToolCallCard({ toolCall }: ToolCallCardProps) {
38
- // Auto-expand when the tool completes so results are immediately visible.
39
- // User can collapse manually afterwards.
40
- const [expanded, setExpanded] = useState(false);
41
-
42
- useEffect(() => {
43
- if (toolCall.status === 'complete') setExpanded(true);
44
- }, [toolCall.status]);
45
-
46
  const isLoading = toolCall.status === 'calling';
47
  const isError = toolCall.status === 'error';
48
-
49
- const icon = toolCall.tool === 'compare_images' ? '🔄' : '🔬';
50
- const label = toolCall.tool.replace(/_/g, ' ');
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  return (
53
- <div className={`tool-card ${isLoading ? 'loading' : ''} ${isError ? 'error' : ''}`}>
54
- <button
55
- className="tool-card-header"
56
- onClick={() => !isLoading && setExpanded(e => !e)}
57
- disabled={isLoading}
58
- >
59
- <span className="tool-icon">{icon}</span>
60
- <span className="tool-label">{label}</span>
61
  {isLoading ? (
62
- <span className="tool-status calling">
63
- <span className="spinner" /> running…
64
- </span>
65
  ) : isError ? (
66
- <span className="tool-status error-text">error</span>
67
  ) : (
68
- <>
69
- <span className="tool-status done">✓</span>
70
- {!expanded && <CollapsedSummary toolCall={toolCall} />}
71
- </>
72
- )}
73
- {!isLoading && (
74
- <span className="tool-chevron">{expanded ? '▲' : '▼'}</span>
75
  )}
76
- </button>
77
-
78
- {expanded && !isLoading && toolCall.result && (
79
- <div className="tool-card-body">
80
- {toolCall.tool === 'analyze_image' && (
81
- <AnalyzeImageResult result={toolCall.result} />
82
- )}
83
- {toolCall.tool === 'compare_images' && (
84
- <CompareImagesResult result={toolCall.result} />
85
- )}
86
- {toolCall.tool !== 'analyze_image' && toolCall.tool !== 'compare_images' && (
87
- <GenericResult result={toolCall.result} />
88
- )}
89
- </div>
90
- )}
91
- </div>
92
- );
93
- }
94
-
95
- /* ─── analyze_image renderer ─────────────────────────────────────────────── */
96
-
97
- function AnalyzeImageResult({ result }: { result: ToolCall['result'] }) {
98
- if (!result) return null;
99
-
100
- const hasClassifier = result.diagnosis != null;
101
- const topPrediction = result.all_predictions?.[0];
102
- const otherPredictions = result.all_predictions?.slice(1) ?? [];
103
- const confidence = result.confidence ?? topPrediction?.probability ?? 0;
104
- const pct = Math.round(confidence * 100);
105
- const statusColor = pct >= 70 ? '#ef4444' : pct >= 40 ? '#f59e0b' : '#22c55e';
106
-
107
- return (
108
- <div className="analyze-result">
109
- <div className="analyze-top">
110
- {result.image_url && (
111
- <img
112
- src={result.image_url}
113
- alt="Analyzed lesion"
114
- className="analyze-thumb"
115
- />
116
- )}
117
- <div className="analyze-info">
118
- {hasClassifier ? (
119
- <>
120
- <p className="diagnosis-name">{result.full_name ?? result.diagnosis}</p>
121
- <p className="confidence-label" style={{ color: statusColor }}>
122
- Confidence: {pct}%
123
- </p>
124
- <div className="confidence-bar-track">
125
- <div
126
- className="confidence-bar-fill"
127
- style={{ width: `${pct}%`, background: statusColor }}
128
- />
129
- </div>
130
- </>
131
- ) : (
132
- <p className="diagnosis-name" style={{ color: 'var(--gray-500)', fontWeight: 400, fontSize: '0.875rem' }}>
133
- Visual assessment complete — classifier unavailable
134
- </p>
135
- )}
136
- </div>
137
- </div>
138
-
139
- {hasClassifier && otherPredictions.length > 0 && (
140
- <ul className="other-predictions">
141
- {otherPredictions.map(p => (
142
- <li key={p.class} className="prediction-row">
143
- <span className="pred-name">{p.full_name ?? p.class}</span>
144
- <span className="pred-pct">{Math.round(p.probability * 100)}%</span>
145
- </li>
146
- ))}
147
- </ul>
148
- )}
149
- </div>
150
- );
151
- }
152
-
153
- /* ─── compare_images renderer ────────────────────────────────────────────── */
154
-
155
- const STATUS_CONFIG: Record<string, { label: string; color: string; emoji: string }> = {
156
- STABLE: { label: 'Stable', color: '#22c55e', emoji: '🟢' },
157
- MINOR_CHANGE: { label: 'Minor Change', color: '#f59e0b', emoji: '🟡' },
158
- SIGNIFICANT_CHANGE: { label: 'Significant Change', color: '#ef4444', emoji: '🔴' },
159
- IMPROVED: { label: 'Improved', color: '#3b82f6', emoji: '🔵' },
160
- };
161
-
162
- function CompareImagesResult({ result }: { result: ToolCall['result'] }) {
163
- if (!result) return null;
164
-
165
- const statusKey = result.status_label ?? 'STABLE';
166
- const status = STATUS_CONFIG[statusKey] ?? { label: statusKey, color: '#6b7280', emoji: '⚪' };
167
- const featureChanges = Object.entries(result.feature_changes ?? {});
168
-
169
- return (
170
- <div className="compare-result">
171
- <div className="compare-status" style={{ color: status.color }}>
172
- <strong>Status: {status.label} {status.emoji}</strong>
173
- </div>
174
-
175
- {featureChanges.length > 0 && (
176
- <ul className="feature-changes">
177
- {featureChanges.map(([name, vals]) => {
178
- const delta = vals.curr - vals.prev;
179
- const sign = delta > 0 ? '+' : '';
180
- return (
181
- <li key={name} className="feature-row">
182
- <span className="feature-name">{name}</span>
183
- <span className="feature-delta" style={{ color: Math.abs(delta) > 0.1 ? '#f59e0b' : '#6b7280' }}>
184
- {sign}{(delta * 100).toFixed(1)}%
185
- </span>
186
- </li>
187
- );
188
- })}
189
- </ul>
190
- )}
191
-
192
- {result.summary && (
193
- <p className="compare-summary">{result.summary}</p>
194
- )}
195
  </div>
196
  );
197
  }
198
-
199
- /* ─── Generic (unknown tool) renderer ───────────────────────────────────── */
200
-
201
- function GenericResult({ result }: { result: ToolCall['result'] }) {
202
- return (
203
- <pre className="generic-result">
204
- {JSON.stringify(result, null, 2)}
205
- </pre>
206
- );
207
- }
 
 
1
  import { ToolCall } from '../types';
2
  import './ToolCallCard.css';
3
 
 
5
  toolCall: ToolCall;
6
  }
7
 
8
+ const TOOL_LABELS: Record<string, string> = {
9
+ analyze_image: 'Analyse image',
10
+ compare_images: 'Compare images',
11
+ load_model: 'Loading analysis model',
12
+ visual_exam: 'MedGemma visual examination',
13
+ classify: 'Running classifier',
14
+ gradcam: 'Generating attention map',
15
+ guidelines: 'Searching clinical guidelines',
16
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ const STATUS_CONFIG: Record<string, { label: string; dot: string }> = {
19
+ STABLE: { label: 'Stable', dot: 'dot-green' },
20
+ MINOR_CHANGE: { label: 'Minor Change', dot: 'dot-amber' },
21
+ SIGNIFICANT_CHANGE: { label: 'Significant Change', dot: 'dot-red' },
22
+ IMPROVED: { label: 'Improved', dot: 'dot-blue' },
23
+ };
24
 
25
  export function ToolCallCard({ toolCall }: ToolCallCardProps) {
 
 
 
 
 
 
 
 
26
  const isLoading = toolCall.status === 'calling';
27
  const isError = toolCall.status === 'error';
28
+ const label = TOOL_LABELS[toolCall.tool] ?? toolCall.tool.replace(/_/g, ' ');
29
+
30
+ // Build summary text for completed tools
31
+ let summary = '';
32
+ if (toolCall.status === 'complete' && toolCall.result) {
33
+ const r = toolCall.result;
34
+ if (toolCall.tool === 'analyze_image' && (r.full_name || r.diagnosis)) {
35
+ const pct = r.confidence != null ? ` (${Math.round(r.confidence * 100)}%)` : '';
36
+ summary = `${r.full_name ?? r.diagnosis}${pct}`;
37
+ } else if (toolCall.tool === 'compare_images' && r.status_label) {
38
+ const cfg = STATUS_CONFIG[r.status_label];
39
+ summary = cfg?.label ?? r.status_label;
40
+ }
41
+ }
42
 
43
  return (
44
+ <div className="tool-line">
45
+ <span className="tool-line-indicator">
 
 
 
 
 
 
46
  {isLoading ? (
47
+ <span className="tool-spinner" />
 
 
48
  ) : isError ? (
49
+ <span className="tool-dot dot-red" />
50
  ) : (
51
+ <span className="tool-dot dot-green" />
 
 
 
 
 
 
52
  )}
53
+ </span>
54
+ <span className="tool-line-label">{label}</span>
55
+ {isLoading && <span className="tool-line-status">running</span>}
56
+ {summary && <span className="tool-line-summary">{summary}</span>}
57
+ {isError && <span className="tool-line-error">failed</span>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  </div>
59
  );
60
  }
 
 
 
 
 
 
 
 
 
 
web/src/pages/ChatPage.css CHANGED
@@ -3,7 +3,7 @@
3
  display: flex;
4
  flex-direction: column;
5
  height: 100vh;
6
- background: var(--gray-50);
7
  overflow: hidden;
8
  }
9
 
@@ -14,8 +14,10 @@
14
  gap: 12px;
15
  padding: 0 16px;
16
  height: 56px;
17
- background: white;
18
- border-bottom: 1px solid var(--gray-200);
 
 
19
  flex-shrink: 0;
20
  z-index: 10;
21
  }
@@ -29,14 +31,14 @@
29
  border: none;
30
  background: transparent;
31
  cursor: pointer;
32
- color: var(--gray-600);
33
  border-radius: 8px;
34
  transition: background 0.15s;
35
  flex-shrink: 0;
36
  }
37
 
38
  .header-back-btn:hover {
39
- background: var(--gray-100);
40
  }
41
 
42
  .header-back-btn svg {
@@ -54,7 +56,7 @@
54
  .header-app-name {
55
  font-size: 0.7rem;
56
  font-weight: 600;
57
- color: var(--primary);
58
  text-transform: uppercase;
59
  letter-spacing: 0.05em;
60
  line-height: 1;
@@ -63,7 +65,7 @@
63
  .header-patient-name {
64
  font-size: 1rem;
65
  font-weight: 600;
66
- color: var(--gray-900);
67
  white-space: nowrap;
68
  overflow: hidden;
69
  text-overflow: ellipsis;
@@ -71,30 +73,33 @@
71
  }
72
 
73
  .header-clear-btn {
74
- border: 1px solid var(--gray-300);
75
  background: transparent;
76
  border-radius: 8px;
77
  padding: 6px 14px;
78
  font-size: 0.8125rem;
79
- color: var(--gray-600);
80
  cursor: pointer;
81
  transition: all 0.15s;
82
  flex-shrink: 0;
83
  }
84
 
85
  .header-clear-btn:hover {
86
- background: var(--gray-100);
87
- border-color: var(--gray-400);
88
  }
89
 
90
  /* ─── Messages ──────────────────────────────────────────────────────────── */
91
  .chat-messages {
92
  flex: 1;
93
  overflow-y: auto;
94
- padding: 20px 16px;
95
  display: flex;
96
  flex-direction: column;
97
- gap: 12px;
 
 
 
98
  }
99
 
100
  .chat-empty {
@@ -103,7 +108,7 @@
103
  flex-direction: column;
104
  align-items: center;
105
  justify-content: center;
106
- color: var(--gray-400);
107
  text-align: center;
108
  gap: 12px;
109
  margin: auto;
@@ -112,7 +117,7 @@
112
  .chat-empty-icon svg {
113
  width: 40px;
114
  height: 40px;
115
- color: var(--gray-300);
116
  }
117
 
118
  .chat-empty p {
@@ -123,42 +128,35 @@
123
 
124
  .message-row {
125
  display: flex;
126
- max-width: 720px;
127
  width: 100%;
128
  }
129
 
130
  .message-row.user {
131
- align-self: flex-end;
132
  justify-content: flex-end;
133
  }
134
 
135
  .message-row.assistant {
136
- align-self: flex-start;
137
  justify-content: flex-start;
138
  }
139
 
140
- /* ─── Bubbles ────────────────────────────────────────────────────────────── */
141
- .bubble {
142
- max-width: 85%;
143
- border-radius: 16px;
144
- padding: 12px 16px;
145
  display: flex;
146
  flex-direction: column;
147
- gap: 8px;
 
 
148
  }
149
 
150
  .user-bubble {
151
- background: var(--primary);
152
- color: white;
 
153
  border-bottom-right-radius: 4px;
154
- }
155
-
156
- .assistant-bubble {
157
- background: white;
158
- border: 1px solid var(--gray-200);
159
- border-bottom-left-radius: 4px;
160
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
161
- max-width: 90%;
162
  }
163
 
164
  .bubble-text {
@@ -167,35 +165,48 @@
167
  white-space: pre-wrap;
168
  word-break: break-word;
169
  margin: 0;
 
170
  }
171
 
172
- .user-bubble .bubble-text {
173
- color: white;
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
 
176
  .assistant-text {
177
- color: var(--gray-800);
 
 
178
  }
179
 
180
- /* Image in user bubble */
181
- .message-image {
182
- width: 100%;
183
- max-width: 260px;
184
- border-radius: 10px;
185
- display: block;
186
  }
187
 
188
  /* ─── Thinking indicator ──────────────────────────────────────────────��──── */
189
  .thinking {
190
  display: flex;
191
  gap: 4px;
192
- padding: 4px 0;
193
  }
194
 
195
  .dot {
196
  width: 7px;
197
  height: 7px;
198
- background: var(--gray-400);
199
  border-radius: 50%;
200
  animation: bounce 1.2s infinite;
201
  }
@@ -210,8 +221,10 @@
210
 
211
  /* ─── Input bar ──────────────────────────────────────────────────────────── */
212
  .chat-input-bar {
213
- background: white;
214
- border-top: 1px solid var(--gray-200);
 
 
215
  padding: 12px 16px;
216
  flex-shrink: 0;
217
  }
@@ -226,8 +239,8 @@
226
  width: 72px;
227
  height: 72px;
228
  object-fit: cover;
229
- border-radius: 10px;
230
- border: 2px solid var(--gray-200);
231
  display: block;
232
  }
233
 
@@ -237,7 +250,7 @@
237
  right: -8px;
238
  width: 22px;
239
  height: 22px;
240
- background: var(--gray-700);
241
  color: white;
242
  border: none;
243
  border-radius: 50%;
@@ -253,16 +266,19 @@
253
  display: flex;
254
  align-items: flex-end;
255
  gap: 8px;
 
 
 
256
  }
257
 
258
  .attach-btn {
259
  width: 38px;
260
  height: 38px;
261
- border: 1px solid var(--gray-300);
262
  background: transparent;
263
- border-radius: 10px;
264
  cursor: pointer;
265
- color: var(--gray-500);
266
  display: flex;
267
  align-items: center;
268
  justify-content: center;
@@ -271,9 +287,9 @@
271
  }
272
 
273
  .attach-btn:hover:not(:disabled) {
274
- background: var(--gray-100);
275
- border-color: var(--gray-400);
276
- color: var(--gray-700);
277
  }
278
 
279
  .attach-btn:disabled {
@@ -288,8 +304,8 @@
288
 
289
  .chat-input {
290
  flex: 1;
291
- border: 1px solid var(--gray-300);
292
- border-radius: 10px;
293
  padding: 9px 14px;
294
  font-size: 0.9375rem;
295
  font-family: inherit;
@@ -298,25 +314,26 @@
298
  max-height: 160px;
299
  overflow-y: auto;
300
  transition: border-color 0.15s;
 
301
  }
302
 
303
  .chat-input:focus {
304
  outline: none;
305
- border-color: var(--primary);
306
  }
307
 
308
  .chat-input:disabled {
309
- background: var(--gray-50);
310
- color: var(--gray-400);
311
  }
312
 
313
  .send-btn {
314
  width: 38px;
315
  height: 38px;
316
- background: var(--primary);
317
  color: white;
318
  border: none;
319
- border-radius: 10px;
320
  cursor: pointer;
321
  display: flex;
322
  align-items: center;
@@ -326,11 +343,11 @@
326
  }
327
 
328
  .send-btn:hover:not(:disabled) {
329
- background: var(--primary-hover);
330
  }
331
 
332
  .send-btn:disabled {
333
- background: var(--gray-300);
334
  cursor: not-allowed;
335
  }
336
 
 
3
  display: flex;
4
  flex-direction: column;
5
  height: 100vh;
6
+ background: #fafafa;
7
  overflow: hidden;
8
  }
9
 
 
14
  gap: 12px;
15
  padding: 0 16px;
16
  height: 56px;
17
+ background: rgba(255, 255, 255, 0.7);
18
+ backdrop-filter: blur(20px);
19
+ -webkit-backdrop-filter: blur(20px);
20
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
21
  flex-shrink: 0;
22
  z-index: 10;
23
  }
 
31
  border: none;
32
  background: transparent;
33
  cursor: pointer;
34
+ color: #555555;
35
  border-radius: 8px;
36
  transition: background 0.15s;
37
  flex-shrink: 0;
38
  }
39
 
40
  .header-back-btn:hover {
41
+ background: rgba(0, 0, 0, 0.05);
42
  }
43
 
44
  .header-back-btn svg {
 
56
  .header-app-name {
57
  font-size: 0.7rem;
58
  font-weight: 600;
59
+ color: #999999;
60
  text-transform: uppercase;
61
  letter-spacing: 0.05em;
62
  line-height: 1;
 
65
  .header-patient-name {
66
  font-size: 1rem;
67
  font-weight: 600;
68
+ color: #111111;
69
  white-space: nowrap;
70
  overflow: hidden;
71
  text-overflow: ellipsis;
 
73
  }
74
 
75
  .header-clear-btn {
76
+ border: 1px solid rgba(0, 0, 0, 0.1);
77
  background: transparent;
78
  border-radius: 8px;
79
  padding: 6px 14px;
80
  font-size: 0.8125rem;
81
+ color: #777777;
82
  cursor: pointer;
83
  transition: all 0.15s;
84
  flex-shrink: 0;
85
  }
86
 
87
  .header-clear-btn:hover {
88
+ background: rgba(0, 0, 0, 0.04);
89
+ border-color: rgba(0, 0, 0, 0.15);
90
  }
91
 
92
  /* ─── Messages ──────────────────────────────────────────────────────────── */
93
  .chat-messages {
94
  flex: 1;
95
  overflow-y: auto;
96
+ padding: 28px 24px;
97
  display: flex;
98
  flex-direction: column;
99
+ gap: 24px;
100
+ max-width: 760px;
101
+ width: 100%;
102
+ margin: 0 auto;
103
  }
104
 
105
  .chat-empty {
 
108
  flex-direction: column;
109
  align-items: center;
110
  justify-content: center;
111
+ color: #aaaaaa;
112
  text-align: center;
113
  gap: 12px;
114
  margin: auto;
 
117
  .chat-empty-icon svg {
118
  width: 40px;
119
  height: 40px;
120
+ color: #cccccc;
121
  }
122
 
123
  .chat-empty p {
 
128
 
129
  .message-row {
130
  display: flex;
 
131
  width: 100%;
132
  }
133
 
134
  .message-row.user {
 
135
  justify-content: flex-end;
136
  }
137
 
138
  .message-row.assistant {
 
139
  justify-content: flex-start;
140
  }
141
 
142
+ /* ─── User message ───────────────────────────────────────────────────────── */
143
+ .user-message {
 
 
 
144
  display: flex;
145
  flex-direction: column;
146
+ align-items: flex-end;
147
+ gap: 4px;
148
+ max-width: 75%;
149
  }
150
 
151
  .user-bubble {
152
+ background: #111111;
153
+ color: #ffffff;
154
+ border-radius: 18px;
155
  border-bottom-right-radius: 4px;
156
+ padding: 12px 16px;
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: 8px;
 
 
 
 
160
  }
161
 
162
  .bubble-text {
 
165
  white-space: pre-wrap;
166
  word-break: break-word;
167
  margin: 0;
168
+ color: #ffffff;
169
  }
170
 
171
+ .message-image {
172
+ width: 100%;
173
+ max-width: 260px;
174
+ border-radius: 12px;
175
+ display: block;
176
+ }
177
+
178
+ /* ─── Assistant message - no card, just text ──────────────────────────────── */
179
+ .assistant-message {
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 6px;
183
+ max-width: 90%;
184
  }
185
 
186
  .assistant-text {
187
+ color: #333333;
188
+ font-size: 0.9375rem;
189
+ line-height: 1.65;
190
  }
191
 
192
+ /* ─── Timestamps ─────────────────────────────────────────────────────────── */
193
+ .msg-time {
194
+ font-size: 0.6875rem;
195
+ color: #bbbbbb;
196
+ padding: 0 4px;
 
197
  }
198
 
199
  /* ─── Thinking indicator ──────────────────────────────────────────────��──── */
200
  .thinking {
201
  display: flex;
202
  gap: 4px;
203
+ padding: 8px 0;
204
  }
205
 
206
  .dot {
207
  width: 7px;
208
  height: 7px;
209
+ background: #bbbbbb;
210
  border-radius: 50%;
211
  animation: bounce 1.2s infinite;
212
  }
 
221
 
222
  /* ─── Input bar ──────────────────────────────────────────────────────────── */
223
  .chat-input-bar {
224
+ background: rgba(255, 255, 255, 0.8);
225
+ backdrop-filter: blur(20px);
226
+ -webkit-backdrop-filter: blur(20px);
227
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
228
  padding: 12px 16px;
229
  flex-shrink: 0;
230
  }
 
239
  width: 72px;
240
  height: 72px;
241
  object-fit: cover;
242
+ border-radius: 12px;
243
+ border: 1px solid rgba(0, 0, 0, 0.08);
244
  display: block;
245
  }
246
 
 
250
  right: -8px;
251
  width: 22px;
252
  height: 22px;
253
+ background: #333333;
254
  color: white;
255
  border: none;
256
  border-radius: 50%;
 
266
  display: flex;
267
  align-items: flex-end;
268
  gap: 8px;
269
+ max-width: 760px;
270
+ margin: 0 auto;
271
+ width: 100%;
272
  }
273
 
274
  .attach-btn {
275
  width: 38px;
276
  height: 38px;
277
+ border: 1px solid rgba(0, 0, 0, 0.1);
278
  background: transparent;
279
+ border-radius: 12px;
280
  cursor: pointer;
281
+ color: #777777;
282
  display: flex;
283
  align-items: center;
284
  justify-content: center;
 
287
  }
288
 
289
  .attach-btn:hover:not(:disabled) {
290
+ background: rgba(0, 0, 0, 0.04);
291
+ border-color: rgba(0, 0, 0, 0.15);
292
+ color: #444444;
293
  }
294
 
295
  .attach-btn:disabled {
 
304
 
305
  .chat-input {
306
  flex: 1;
307
+ border: 1px solid rgba(0, 0, 0, 0.1);
308
+ border-radius: 12px;
309
  padding: 9px 14px;
310
  font-size: 0.9375rem;
311
  font-family: inherit;
 
314
  max-height: 160px;
315
  overflow-y: auto;
316
  transition: border-color 0.15s;
317
+ background: rgba(255, 255, 255, 0.6);
318
  }
319
 
320
  .chat-input:focus {
321
  outline: none;
322
+ border-color: rgba(0, 0, 0, 0.25);
323
  }
324
 
325
  .chat-input:disabled {
326
+ background: rgba(0, 0, 0, 0.03);
327
+ color: #aaaaaa;
328
  }
329
 
330
  .send-btn {
331
  width: 38px;
332
  height: 38px;
333
+ background: #111111;
334
  color: white;
335
  border: none;
336
+ border-radius: 12px;
337
  cursor: pointer;
338
  display: flex;
339
  align-items: center;
 
343
  }
344
 
345
  .send-btn:hover:not(:disabled) {
346
+ background: #333333;
347
  }
348
 
349
  .send-btn:disabled {
350
+ background: #cccccc;
351
  cursor: not-allowed;
352
  }
353
 
web/src/pages/ChatPage.tsx CHANGED
@@ -7,6 +7,13 @@ import { MessageContent } from '../components/MessageContent';
7
  import { Patient, ChatMessage, ToolCall } from '../types';
8
  import './ChatPage.css';
9
 
 
 
 
 
 
 
 
10
  export function ChatPage() {
11
  const { patientId } = useParams<{ patientId: string }>();
12
  const navigate = useNavigate();
@@ -176,16 +183,27 @@ export function ChatPage() {
176
  {messages.map(msg => (
177
  <div key={msg.id} className={`message-row ${msg.role}`}>
178
  {msg.role === 'user' ? (
179
- <div className="bubble user-bubble">
180
- {msg.image_url && (
181
- <img src={msg.image_url} className="message-image" alt="Attached image" />
182
- )}
183
- {msg.content && <p className="bubble-text">{msg.content}</p>}
 
 
 
184
  </div>
185
  ) : (
186
- <div className="bubble assistant-bubble">
 
 
 
 
 
 
187
  {msg.content ? (
188
- <MessageContent text={msg.content} />
 
 
189
  ) : (!msg.tool_calls || msg.tool_calls.length === 0) && isStreaming ? (
190
  <div className="thinking">
191
  <span className="dot" />
@@ -193,9 +211,10 @@ export function ChatPage() {
193
  <span className="dot" />
194
  </div>
195
  ) : null}
196
- {(msg.tool_calls ?? []).map(tc => (
197
- <ToolCallCard key={tc.id} toolCall={tc} />
198
- ))}
 
199
  </div>
200
  )}
201
  </div>
 
7
  import { Patient, ChatMessage, ToolCall } from '../types';
8
  import './ChatPage.css';
9
 
10
+ function formatTime(ts: string) {
11
+ return new Date(ts).toLocaleTimeString('en-US', {
12
+ hour: 'numeric',
13
+ minute: '2-digit',
14
+ });
15
+ }
16
+
17
  export function ChatPage() {
18
  const { patientId } = useParams<{ patientId: string }>();
19
  const navigate = useNavigate();
 
183
  {messages.map(msg => (
184
  <div key={msg.id} className={`message-row ${msg.role}`}>
185
  {msg.role === 'user' ? (
186
+ <div className="user-message">
187
+ <div className="user-bubble">
188
+ {msg.image_url && (
189
+ <img src={msg.image_url} className="message-image" alt="Attached" />
190
+ )}
191
+ {msg.content && <p className="bubble-text">{msg.content}</p>}
192
+ </div>
193
+ <span className="msg-time">{formatTime(msg.timestamp)}</span>
194
  </div>
195
  ) : (
196
+ <div className="assistant-message">
197
+ {/* Tool call status lines */}
198
+ {(msg.tool_calls ?? []).map(tc => (
199
+ <ToolCallCard key={tc.id} toolCall={tc} />
200
+ ))}
201
+
202
+ {/* Text content */}
203
  {msg.content ? (
204
+ <div className="assistant-text">
205
+ <MessageContent text={msg.content} />
206
+ </div>
207
  ) : (!msg.tool_calls || msg.tool_calls.length === 0) && isStreaming ? (
208
  <div className="thinking">
209
  <span className="dot" />
 
211
  <span className="dot" />
212
  </div>
213
  ) : null}
214
+
215
+ {(msg.content || (msg.tool_calls && msg.tool_calls.length > 0)) && (
216
+ <span className="msg-time">{formatTime(msg.timestamp)}</span>
217
+ )}
218
  </div>
219
  )}
220
  </div>
web/src/pages/PatientsPage.css CHANGED
@@ -3,110 +3,204 @@
3
  display: flex;
4
  flex-direction: column;
5
  align-items: center;
6
- padding: 60px 20px;
7
- background: linear-gradient(180deg, var(--gray-50) 0%, white 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  }
9
 
10
- .patients-page .title {
11
- font-size: 2.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
12
  font-weight: 700;
13
- color: var(--gray-900);
14
- margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
15
  }
16
 
17
- .patients-page .subtitle {
 
 
 
 
 
 
 
 
18
  font-size: 1rem;
19
- color: var(--gray-500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  margin-bottom: 48px;
21
  }
22
 
23
- .patients-page .loading {
24
- color: var(--gray-500);
 
 
 
 
 
25
  }
26
 
 
27
  .patients-grid {
28
  display: grid;
29
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
30
- gap: 20px;
31
- max-width: 900px;
32
- width: 100%;
33
  }
34
 
35
  .patient-card {
36
- background: white;
37
- border: 2px solid var(--gray-200);
38
- border-radius: 16px;
39
- padding: 28px 24px;
 
 
40
  cursor: pointer;
41
- transition: all 0.2s ease;
42
  display: flex;
43
  flex-direction: column;
44
  align-items: center;
45
- gap: 12px;
46
  position: relative;
 
 
 
 
47
  }
48
 
49
  .patient-card:hover {
50
- border-color: var(--primary);
51
- box-shadow: 0 8px 25px rgba(99, 102, 241, 0.15);
52
- transform: translateY(-2px);
53
- }
54
-
55
- .patient-card.new-patient {
56
- border-style: dashed;
57
- background: var(--gray-50);
58
- }
59
-
60
- .patient-card.new-patient:hover {
61
- background: white;
62
  }
63
 
64
  .patient-icon {
65
- width: 48px;
66
- height: 48px;
67
- background: var(--gray-100);
68
  border-radius: 50%;
69
  display: flex;
70
  align-items: center;
71
  justify-content: center;
72
- color: var(--gray-500);
73
  }
74
 
75
  .patient-icon svg {
76
- width: 28px;
77
- height: 28px;
78
- }
79
-
80
- .add-icon {
81
- width: 48px;
82
- height: 48px;
83
- background: var(--primary);
84
- color: white;
85
- border-radius: 50%;
86
- display: flex;
87
- align-items: center;
88
- justify-content: center;
89
- font-size: 2rem;
90
- font-weight: 300;
91
  }
92
 
93
  .patient-name {
94
  font-weight: 600;
95
- color: var(--gray-800);
96
- font-size: 1rem;
97
  text-align: center;
98
  }
99
 
100
-
101
  .delete-btn {
102
  position: absolute;
103
- top: 12px;
104
- right: 12px;
105
  background: transparent;
106
  border: none;
107
  padding: 6px;
108
  cursor: pointer;
109
- color: var(--gray-400);
110
  opacity: 0;
111
  transition: all 0.2s;
112
  border-radius: 6px;
@@ -118,19 +212,138 @@
118
 
119
  .delete-btn:hover {
120
  color: #ef4444;
121
- background: #fef2f2;
122
  }
123
 
124
  .delete-btn svg {
125
- width: 18px;
126
- height: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
128
 
129
- /* Modal */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  .modal-overlay {
131
  position: fixed;
132
  inset: 0;
133
- background: rgba(0, 0, 0, 0.4);
 
 
134
  display: flex;
135
  align-items: center;
136
  justify-content: center;
@@ -138,33 +351,39 @@
138
  }
139
 
140
  .modal {
141
- background: white;
142
- border-radius: 16px;
143
- padding: 28px;
 
 
 
144
  width: 100%;
145
  max-width: 400px;
146
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
 
 
147
  }
148
 
149
  .modal h2 {
150
  font-size: 1.25rem;
151
  font-weight: 600;
152
- color: var(--gray-800);
153
  margin-bottom: 20px;
154
  }
155
 
156
  .modal input {
157
  width: 100%;
158
  padding: 12px 16px;
159
- border: 1px solid var(--gray-300);
160
- border-radius: 10px;
161
  font-size: 1rem;
162
  margin-bottom: 20px;
 
163
  }
164
 
165
  .modal input:focus {
166
  outline: none;
167
- border-color: var(--primary);
168
  }
169
 
170
  .modal-buttons {
@@ -175,34 +394,51 @@
175
 
176
  .cancel-btn {
177
  background: transparent;
178
- border: 1px solid var(--gray-300);
179
- border-radius: 8px;
180
  padding: 10px 20px;
181
  font-size: 0.875rem;
182
- color: var(--gray-600);
183
  cursor: pointer;
 
184
  }
185
 
186
  .cancel-btn:hover {
187
- background: var(--gray-50);
188
  }
189
 
190
  .create-btn {
191
- background: var(--primary);
192
  color: white;
193
  border: none;
194
- border-radius: 8px;
195
  padding: 10px 24px;
196
  font-size: 0.875rem;
197
  font-weight: 500;
198
  cursor: pointer;
 
199
  }
200
 
201
  .create-btn:hover {
202
- background: var(--primary-hover);
203
  }
204
 
205
  .create-btn:disabled {
206
- background: var(--gray-300);
207
  cursor: not-allowed;
208
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  display: flex;
4
  flex-direction: column;
5
  align-items: center;
6
+ background: #fafafa;
7
+ position: relative;
8
+ overflow: hidden;
9
+ }
10
+
11
+ /* ── Sunrise orb ── */
12
+ .patients-page::before {
13
+ content: '';
14
+ position: absolute;
15
+ top: -120px;
16
+ left: 50%;
17
+ transform: translateX(-50%);
18
+ width: 600px;
19
+ height: 600px;
20
+ border-radius: 50%;
21
+ background: radial-gradient(
22
+ circle at 50% 50%,
23
+ rgba(255, 183, 77, 0.45) 0%,
24
+ rgba(255, 152, 67, 0.30) 25%,
25
+ rgba(255, 120, 80, 0.18) 45%,
26
+ rgba(255, 100, 100, 0.08) 65%,
27
+ transparent 80%
28
+ );
29
+ filter: blur(40px);
30
+ pointer-events: none;
31
+ z-index: 0;
32
+ }
33
+
34
+ .patients-page::after {
35
+ content: '';
36
+ position: absolute;
37
+ top: -60px;
38
+ left: 50%;
39
+ transform: translateX(-50%);
40
+ width: 320px;
41
+ height: 320px;
42
+ border-radius: 50%;
43
+ background: radial-gradient(
44
+ circle,
45
+ rgba(255, 200, 100, 0.50) 0%,
46
+ rgba(255, 170, 70, 0.25) 40%,
47
+ transparent 70%
48
+ );
49
+ filter: blur(25px);
50
+ pointer-events: none;
51
+ z-index: 0;
52
  }
53
 
54
+ /* ── Hero ── */
55
+ .hero {
56
+ position: relative;
57
+ z-index: 1;
58
+ display: flex;
59
+ flex-direction: column;
60
+ align-items: center;
61
+ padding: 80px 24px 48px;
62
+ text-align: center;
63
+ max-width: 600px;
64
+ }
65
+
66
+ .title {
67
+ font-size: 3rem;
68
  font-weight: 700;
69
+ color: #111111;
70
+ letter-spacing: -0.04em;
71
+ margin-bottom: 12px;
72
+ }
73
+
74
+ .tagline {
75
+ font-size: 1.05rem;
76
+ line-height: 1.65;
77
+ color: #555555;
78
+ margin-bottom: 36px;
79
+ max-width: 480px;
80
  }
81
 
82
+ .cta-btn {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ gap: 10px;
86
+ background: #111111;
87
+ color: #ffffff;
88
+ border: none;
89
+ border-radius: 14px;
90
+ padding: 16px 36px;
91
  font-size: 1rem;
92
+ font-weight: 600;
93
+ cursor: pointer;
94
+ transition: all 0.25s ease;
95
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
96
+ }
97
+
98
+ .cta-btn:hover {
99
+ background: #333333;
100
+ transform: translateY(-2px);
101
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
102
+ }
103
+
104
+ .cta-btn svg {
105
+ width: 20px;
106
+ height: 20px;
107
+ }
108
+
109
+ .loading {
110
+ color: #6b6b6b;
111
+ padding: 120px 0;
112
+ }
113
+
114
+ /* ── Section shared ── */
115
+ .patients-section,
116
+ .how-section,
117
+ .about-section {
118
+ position: relative;
119
+ z-index: 1;
120
+ width: 100%;
121
+ max-width: 900px;
122
+ padding: 0 24px;
123
  margin-bottom: 48px;
124
  }
125
 
126
+ .section-label {
127
+ font-size: 0.75rem;
128
+ font-weight: 600;
129
+ text-transform: uppercase;
130
+ letter-spacing: 0.08em;
131
+ color: #999999;
132
+ margin-bottom: 16px;
133
  }
134
 
135
+ /* ── Patient cards ── */
136
  .patients-grid {
137
  display: grid;
138
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
139
+ gap: 16px;
 
 
140
  }
141
 
142
  .patient-card {
143
+ background: rgba(255, 255, 255, 0.55);
144
+ backdrop-filter: blur(20px);
145
+ -webkit-backdrop-filter: blur(20px);
146
+ border: 1px solid rgba(255, 255, 255, 0.7);
147
+ border-radius: 18px;
148
+ padding: 28px 20px;
149
  cursor: pointer;
150
+ transition: all 0.3s ease;
151
  display: flex;
152
  flex-direction: column;
153
  align-items: center;
154
+ gap: 10px;
155
  position: relative;
156
+ box-shadow:
157
+ 0 4px 24px rgba(0, 0, 0, 0.04),
158
+ 0 1px 2px rgba(0, 0, 0, 0.03),
159
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
160
  }
161
 
162
  .patient-card:hover {
163
+ background: rgba(255, 255, 255, 0.75);
164
+ border-color: rgba(255, 255, 255, 0.9);
165
+ box-shadow:
166
+ 0 12px 40px rgba(0, 0, 0, 0.08),
167
+ 0 2px 8px rgba(0, 0, 0, 0.04),
168
+ inset 0 1px 0 rgba(255, 255, 255, 0.9);
169
+ transform: translateY(-3px);
 
 
 
 
 
170
  }
171
 
172
  .patient-icon {
173
+ width: 44px;
174
+ height: 44px;
175
+ background: rgba(0, 0, 0, 0.06);
176
  border-radius: 50%;
177
  display: flex;
178
  align-items: center;
179
  justify-content: center;
180
+ color: #555555;
181
  }
182
 
183
  .patient-icon svg {
184
+ width: 24px;
185
+ height: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
  .patient-name {
189
  font-weight: 600;
190
+ color: #1a1a1a;
191
+ font-size: 0.95rem;
192
  text-align: center;
193
  }
194
 
 
195
  .delete-btn {
196
  position: absolute;
197
+ top: 10px;
198
+ right: 10px;
199
  background: transparent;
200
  border: none;
201
  padding: 6px;
202
  cursor: pointer;
203
+ color: #aaaaaa;
204
  opacity: 0;
205
  transition: all 0.2s;
206
  border-radius: 6px;
 
212
 
213
  .delete-btn:hover {
214
  color: #ef4444;
215
+ background: rgba(239, 68, 68, 0.08);
216
  }
217
 
218
  .delete-btn svg {
219
+ width: 16px;
220
+ height: 16px;
221
+ }
222
+
223
+ /* ── How It Works ── */
224
+ .steps-row {
225
+ display: grid;
226
+ grid-template-columns: repeat(3, 1fr);
227
+ gap: 16px;
228
+ }
229
+
230
+ .step-card {
231
+ background: rgba(255, 255, 255, 0.55);
232
+ backdrop-filter: blur(20px);
233
+ -webkit-backdrop-filter: blur(20px);
234
+ border: 1px solid rgba(255, 255, 255, 0.7);
235
+ border-radius: 18px;
236
+ padding: 28px 24px;
237
+ box-shadow:
238
+ 0 4px 24px rgba(0, 0, 0, 0.04),
239
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
240
+ }
241
+
242
+ .step-num {
243
+ width: 32px;
244
+ height: 32px;
245
+ border-radius: 50%;
246
+ background: #111111;
247
+ color: white;
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ font-size: 0.8rem;
252
+ font-weight: 700;
253
+ margin-bottom: 14px;
254
+ }
255
+
256
+ .step-card h3 {
257
+ font-size: 1rem;
258
+ font-weight: 600;
259
+ color: #1a1a1a;
260
+ margin-bottom: 6px;
261
+ }
262
+
263
+ .step-card p {
264
+ font-size: 0.875rem;
265
+ line-height: 1.55;
266
+ color: #666666;
267
+ }
268
+
269
+ /* ── About ── */
270
+ .about-card {
271
+ background: rgba(255, 255, 255, 0.55);
272
+ backdrop-filter: blur(20px);
273
+ -webkit-backdrop-filter: blur(20px);
274
+ border: 1px solid rgba(255, 255, 255, 0.7);
275
+ border-radius: 20px;
276
+ padding: 36px 32px;
277
+ box-shadow:
278
+ 0 4px 24px rgba(0, 0, 0, 0.04),
279
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
280
+ }
281
+
282
+ .about-card h3 {
283
+ font-size: 1.1rem;
284
+ font-weight: 600;
285
+ color: #111111;
286
+ margin-bottom: 14px;
287
+ }
288
+
289
+ .about-card p {
290
+ font-size: 0.9rem;
291
+ line-height: 1.7;
292
+ color: #444444;
293
+ margin-bottom: 14px;
294
  }
295
 
296
+ .about-card p strong {
297
+ color: #1a1a1a;
298
+ }
299
+
300
+ .tech-pills {
301
+ display: flex;
302
+ flex-wrap: wrap;
303
+ gap: 8px;
304
+ margin-top: 8px;
305
+ }
306
+
307
+ .pill {
308
+ background: rgba(0, 0, 0, 0.06);
309
+ color: #333333;
310
+ padding: 6px 14px;
311
+ border-radius: 100px;
312
+ font-size: 0.75rem;
313
+ font-weight: 600;
314
+ letter-spacing: 0.01em;
315
+ }
316
+
317
+ /* ── Disclaimer ── */
318
+ .disclaimer {
319
+ position: relative;
320
+ z-index: 1;
321
+ width: 100%;
322
+ max-width: 900px;
323
+ padding: 0 24px;
324
+ margin-bottom: 60px;
325
+ }
326
+
327
+ .disclaimer p {
328
+ font-size: 0.8rem;
329
+ line-height: 1.6;
330
+ color: #888888;
331
+ text-align: center;
332
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
333
+ padding-top: 24px;
334
+ }
335
+
336
+ .disclaimer strong {
337
+ color: #666666;
338
+ }
339
+
340
+ /* ── Modal (glassmorphic) ── */
341
  .modal-overlay {
342
  position: fixed;
343
  inset: 0;
344
+ background: rgba(0, 0, 0, 0.35);
345
+ backdrop-filter: blur(8px);
346
+ -webkit-backdrop-filter: blur(8px);
347
  display: flex;
348
  align-items: center;
349
  justify-content: center;
 
351
  }
352
 
353
  .modal {
354
+ background: rgba(255, 255, 255, 0.85);
355
+ backdrop-filter: blur(24px);
356
+ -webkit-backdrop-filter: blur(24px);
357
+ border: 1px solid rgba(255, 255, 255, 0.8);
358
+ border-radius: 20px;
359
+ padding: 32px;
360
  width: 100%;
361
  max-width: 400px;
362
+ box-shadow:
363
+ 0 24px 48px rgba(0, 0, 0, 0.12),
364
+ 0 8px 16px rgba(0, 0, 0, 0.06);
365
  }
366
 
367
  .modal h2 {
368
  font-size: 1.25rem;
369
  font-weight: 600;
370
+ color: #111111;
371
  margin-bottom: 20px;
372
  }
373
 
374
  .modal input {
375
  width: 100%;
376
  padding: 12px 16px;
377
+ border: 1px solid rgba(0, 0, 0, 0.12);
378
+ border-radius: 12px;
379
  font-size: 1rem;
380
  margin-bottom: 20px;
381
+ background: rgba(255, 255, 255, 0.6);
382
  }
383
 
384
  .modal input:focus {
385
  outline: none;
386
+ border-color: rgba(0, 0, 0, 0.3);
387
  }
388
 
389
  .modal-buttons {
 
394
 
395
  .cancel-btn {
396
  background: transparent;
397
+ border: 1px solid rgba(0, 0, 0, 0.12);
398
+ border-radius: 10px;
399
  padding: 10px 20px;
400
  font-size: 0.875rem;
401
+ color: #555555;
402
  cursor: pointer;
403
+ transition: all 0.2s;
404
  }
405
 
406
  .cancel-btn:hover {
407
+ background: rgba(0, 0, 0, 0.04);
408
  }
409
 
410
  .create-btn {
411
+ background: #111111;
412
  color: white;
413
  border: none;
414
+ border-radius: 10px;
415
  padding: 10px 24px;
416
  font-size: 0.875rem;
417
  font-weight: 500;
418
  cursor: pointer;
419
+ transition: all 0.2s;
420
  }
421
 
422
  .create-btn:hover {
423
+ background: #333333;
424
  }
425
 
426
  .create-btn:disabled {
427
+ background: #cccccc;
428
  cursor: not-allowed;
429
  }
430
+
431
+ /* ── Responsive ── */
432
+ @media (max-width: 640px) {
433
+ .title {
434
+ font-size: 2.25rem;
435
+ }
436
+
437
+ .steps-row {
438
+ grid-template-columns: 1fr;
439
+ }
440
+
441
+ .patients-grid {
442
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
443
+ }
444
+ }
web/src/pages/PatientsPage.tsx CHANGED
@@ -49,43 +49,117 @@ export function PatientsPage() {
49
 
50
  return (
51
  <div className="patients-page">
52
- <h1 className="title">SkinProAI</h1>
53
- <p className="subtitle">Dermatological AI Assistant</p>
 
 
 
 
 
54
 
55
- <div className="patients-grid">
56
- {patients.map(patient => (
57
- <div
58
- key={patient.id}
59
- className="patient-card"
60
- onClick={() => navigate(`/chat/${patient.id}`)}
61
- >
62
- <div className="patient-icon">
63
- <svg viewBox="0 0 24 24" fill="currentColor">
64
- <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"/>
65
- </svg>
66
- </div>
67
- <span className="patient-name">{patient.name}</span>
68
- <button
69
- className="delete-btn"
70
- onClick={(e) => handleDeletePatient(e, patient.id)}
71
- title="Delete patient"
72
- >
73
- <svg viewBox="0 0 24 24" fill="currentColor">
74
- <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
75
- </svg>
76
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  </div>
78
- ))}
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- <div
81
- className="patient-card new-patient"
82
- onClick={() => setShowNewPatient(true)}
83
- >
84
- <div className="add-icon">+</div>
85
- <span className="patient-name">New Patient</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
- </div>
 
 
 
 
 
 
 
 
 
 
88
 
 
89
  {showNewPatient && (
90
  <div className="modal-overlay" onClick={() => setShowNewPatient(false)}>
91
  <div className="modal" onClick={e => e.stopPropagation()}>
 
49
 
50
  return (
51
  <div className="patients-page">
52
+ {/* Hero Section */}
53
+ <section className="hero">
54
+ <h1 className="title">SkinProAI</h1>
55
+ <p className="tagline">
56
+ Multimodal dermatological analysis powered by MedGemma
57
+ and intelligent tool orchestration.
58
+ </p>
59
 
60
+ <button className="cta-btn" onClick={() => setShowNewPatient(true)}>
61
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
62
+ <line x1="12" y1="5" x2="12" y2="19" />
63
+ <line x1="5" y1="12" x2="19" y2="12" />
64
+ </svg>
65
+ New Patient
66
+ </button>
67
+ </section>
68
+
69
+ {/* Existing patients */}
70
+ {patients.length > 0 && (
71
+ <section className="patients-section">
72
+ <p className="section-label">Recent Patients</p>
73
+ <div className="patients-grid">
74
+ {patients.map(patient => (
75
+ <div
76
+ key={patient.id}
77
+ className="patient-card"
78
+ onClick={() => navigate(`/chat/${patient.id}`)}
79
+ >
80
+ <div className="patient-icon">
81
+ <svg viewBox="0 0 24 24" fill="currentColor">
82
+ <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"/>
83
+ </svg>
84
+ </div>
85
+ <span className="patient-name">{patient.name}</span>
86
+ <button
87
+ className="delete-btn"
88
+ onClick={(e) => handleDeletePatient(e, patient.id)}
89
+ title="Delete patient"
90
+ >
91
+ <svg viewBox="0 0 24 24" fill="currentColor">
92
+ <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
93
+ </svg>
94
+ </button>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </section>
99
+ )}
100
+
101
+ {/* How It Works */}
102
+ <section className="how-section">
103
+ <p className="section-label">How It Works</p>
104
+ <div className="steps-row">
105
+ <div className="step-card">
106
+ <div className="step-num">1</div>
107
+ <h3>Upload</h3>
108
+ <p>Capture or upload a dermatoscopic or clinical image of the lesion.</p>
109
  </div>
110
+ <div className="step-card">
111
+ <div className="step-num">2</div>
112
+ <h3>Analyze</h3>
113
+ <p>MedGemma examines the image and coordinates specialist tools for deeper insight.</p>
114
+ </div>
115
+ <div className="step-card">
116
+ <div className="step-num">3</div>
117
+ <h3>Track</h3>
118
+ <p>Monitor lesions over time with side-by-side comparison and change detection.</p>
119
+ </div>
120
+ </div>
121
+ </section>
122
 
123
+ {/* About */}
124
+ <section className="about-section">
125
+ <div className="about-card">
126
+ <h3>About SkinProAI</h3>
127
+ <p>
128
+ Built for the <strong>Kaggle MedGemma Multimodal Medical AI Competition</strong>,
129
+ SkinProAI explores how a foundation medical vision-language model can be
130
+ augmented with specialised tools to deliver richer clinical insight.
131
+ </p>
132
+ <p>
133
+ At its core sits Google's <strong>MedGemma 4B</strong>, a multimodal model
134
+ fine-tuned for medical image understanding. Rather than relying on the model
135
+ alone, SkinProAI connects it to a suite of external tools via
136
+ the <strong>Model Context Protocol (MCP)</strong> &mdash; including MONET
137
+ feature extraction, ConvNeXt classification, Grad-CAM attention maps, and
138
+ clinical guideline retrieval &mdash; letting the model reason across multiple
139
+ sources before presenting a synthesised assessment.
140
+ </p>
141
+ <div className="tech-pills">
142
+ <span className="pill">MedGemma 4B</span>
143
+ <span className="pill">MCP Tools</span>
144
+ <span className="pill">MONET</span>
145
+ <span className="pill">ConvNeXt</span>
146
+ <span className="pill">Grad-CAM</span>
147
+ <span className="pill">RAG Guidelines</span>
148
+ </div>
149
  </div>
150
+ </section>
151
+
152
+ {/* Disclaimer */}
153
+ <footer className="disclaimer">
154
+ <p>
155
+ <strong>Research prototype only.</strong> SkinProAI is an educational project and
156
+ competition entry. It is not a medical device and must not be used for clinical
157
+ decision-making. Always consult a qualified healthcare professional for diagnosis
158
+ and treatment.
159
+ </p>
160
+ </footer>
161
 
162
+ {/* New Patient Modal */}
163
  {showNewPatient && (
164
  <div className="modal-overlay" onClick={() => setShowNewPatient(false)}>
165
  <div className="modal" onClick={e => e.stopPropagation()}>