31puneet commited on
Commit
c8a3a1b
·
0 Parent(s):

Initial ChestXpert deployment

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ *.pt filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.env
7
+ .env
8
+ env/
9
+ venv/
10
+ uploads/
11
+ *.log
12
+ .DS_Store
13
+ .idea/
14
+ .vscode/
15
+ *.egg-info/
16
+ dist/
17
+ build/
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ RUN pip install --no-cache-dir --upgrade pip
10
+
11
+ COPY --chown=user requirements.txt requirements.txt
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ COPY --chown=user . /app
15
+
16
+ RUN mkdir -p /app/models /app/samples /app/uploads
17
+
18
+ EXPOSE 7860
19
+
20
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import json
4
+ import base64
5
+ import uuid
6
+ from datetime import datetime
7
+ import numpy as np
8
+ from PIL import Image
9
+ import torch
10
+ import torch.nn as nn
11
+ from torchvision import models
12
+ from transformers import AutoModel
13
+ import albumentations as A
14
+ from albumentations.pytorch import ToTensorV2
15
+ from flask import Flask, render_template, request, jsonify, send_from_directory
16
+
17
+ app = Flask(__name__)
18
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
19
+
20
+ DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
21
+ TARGET_LABELS = ["Atelectasis", "Cardiomegaly", "Consolidation", "Edema", "Pleural Effusion"]
22
+ LABEL_INFO = {
23
+ "Atelectasis": {
24
+ "description": "Partial or complete collapse of the lung or a section of the lung.",
25
+ "icon": ""
26
+ },
27
+ "Cardiomegaly": {
28
+ "description": "Enlargement of the heart, often indicating heart disease.",
29
+ "icon": ""
30
+ },
31
+ "Consolidation": {
32
+ "description": "Region of lung tissue filled with liquid instead of air.",
33
+ "icon": ""
34
+ },
35
+ "Edema": {
36
+ "description": "Excess fluid in the lungs, often due to heart failure.",
37
+ "icon": ""
38
+ },
39
+ "Pleural Effusion": {
40
+ "description": "Buildup of fluid between the lung and chest wall.",
41
+ "icon": ""
42
+ }
43
+ }
44
+ ENSEMBLE_WEIGHT_RD = 0.60
45
+ ENSEMBLE_WEIGHT_DN = 0.40
46
+ MODEL_DIR = os.path.join(os.path.dirname(__file__), 'models')
47
+ SAMPLES_DIR = os.path.join(os.path.dirname(__file__), 'samples')
48
+
49
+ # In-memory store for analysis results (for report generation)
50
+ analysis_store = {}
51
+
52
+
53
+ # --- Model Definitions ---
54
+ class RADDINOClassifier(nn.Module):
55
+ def __init__(self, num_classes=5, dropout=0.3):
56
+ super().__init__()
57
+ self.backbone = AutoModel.from_pretrained("microsoft/rad-dino")
58
+ self.hidden_dim = self.backbone.config.hidden_size
59
+ self.classifier = nn.Sequential(
60
+ nn.LayerNorm(self.hidden_dim),
61
+ nn.Dropout(dropout),
62
+ nn.Linear(self.hidden_dim, 256),
63
+ nn.GELU(),
64
+ nn.Dropout(dropout / 2),
65
+ nn.Linear(256, num_classes)
66
+ )
67
+
68
+ def forward(self, x):
69
+ features = self.backbone(x).last_hidden_state[:, 0]
70
+ return self.classifier(features)
71
+
72
+
73
+ class DenseNetClassifier(nn.Module):
74
+ def __init__(self, num_classes=5, dropout=0.4):
75
+ super().__init__()
76
+ self.backbone = models.densenet121(weights=None)
77
+ nf = self.backbone.classifier.in_features
78
+ self.backbone.classifier = nn.Sequential(
79
+ nn.Dropout(dropout),
80
+ nn.Linear(nf, 256),
81
+ nn.ReLU(),
82
+ nn.Dropout(dropout / 2),
83
+ nn.Linear(256, num_classes)
84
+ )
85
+
86
+ def forward(self, x):
87
+ return self.backbone(x)
88
+
89
+
90
+ # --- Grad-CAM ---
91
+ class GradCAM:
92
+ def __init__(self, model):
93
+ self.model = model
94
+ self.gradients = None
95
+ self.activations = None
96
+ target_layer = model.backbone.features.denseblock4
97
+ target_layer.register_forward_hook(self._forward_hook)
98
+ target_layer.register_full_backward_hook(self._backward_hook)
99
+
100
+ def _forward_hook(self, module, input, output):
101
+ self.activations = output.detach()
102
+
103
+ def _backward_hook(self, module, grad_input, grad_output):
104
+ self.gradients = grad_output[0].detach()
105
+
106
+ def generate(self, input_tensor, class_idx=None):
107
+ self.model.eval()
108
+ input_tensor.requires_grad_(True)
109
+ output = self.model(input_tensor)
110
+
111
+ if class_idx is None:
112
+ class_idx = output.sigmoid().mean(dim=0).argmax().item()
113
+
114
+ self.model.zero_grad()
115
+ target = output[0, class_idx]
116
+ target.backward()
117
+
118
+ gradients = self.gradients[0]
119
+ activations = self.activations[0]
120
+ weights = gradients.mean(dim=(1, 2), keepdim=True)
121
+ cam = (weights * activations).sum(dim=0)
122
+ cam = torch.relu(cam)
123
+ cam = cam - cam.min()
124
+ if cam.max() > 0:
125
+ cam = cam / cam.max()
126
+ return cam.cpu().numpy()
127
+
128
+
129
+ # --- Image Preprocessing ---
130
+ def get_transform(size):
131
+ return A.Compose([
132
+ A.Resize(size, size),
133
+ A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
134
+ ToTensorV2()
135
+ ])
136
+
137
+ transform_384 = get_transform(384)
138
+ transform_320 = get_transform(320)
139
+
140
+
141
+ def preprocess_image(image_bytes, transform):
142
+ img = Image.open(io.BytesIO(image_bytes)).convert('RGB')
143
+ img_np = np.array(img)
144
+ augmented = transform(image=img_np)
145
+ tensor = augmented['image'].unsqueeze(0).to(DEVICE)
146
+ return tensor, img_np
147
+
148
+
149
+ # --- DICOM Support ---
150
+ def read_dicom_as_bytes(file_bytes):
151
+ """Convert DICOM file bytes to standard image bytes."""
152
+ try:
153
+ import pydicom
154
+ ds = pydicom.dcmread(io.BytesIO(file_bytes))
155
+ pixel_array = ds.pixel_array
156
+
157
+ # Normalize to 0-255
158
+ arr = pixel_array.astype(float)
159
+ if arr.max() != arr.min():
160
+ arr = (arr - arr.min()) / (arr.max() - arr.min()) * 255
161
+ arr = arr.astype(np.uint8)
162
+
163
+ # Handle MONOCHROME1 (inverted)
164
+ if hasattr(ds, 'PhotometricInterpretation'):
165
+ if ds.PhotometricInterpretation == 'MONOCHROME1':
166
+ arr = 255 - arr
167
+
168
+ img = Image.fromarray(arr).convert('RGB')
169
+ buffer = io.BytesIO()
170
+ img.save(buffer, format='PNG')
171
+ return buffer.getvalue()
172
+ except Exception as e:
173
+ raise ValueError(f"Failed to read DICOM file: {str(e)}")
174
+
175
+
176
+ # --- Heatmap Generation ---
177
+ def create_heatmap_overlay(original_img, cam, alpha=0.4):
178
+ import cv2
179
+ h, w = original_img.shape[:2]
180
+ cam_resized = cv2.resize(cam, (w, h))
181
+ heatmap = cv2.applyColorMap(np.uint8(255 * cam_resized), cv2.COLORMAP_JET)
182
+ heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
183
+ overlay = np.float32(heatmap) * alpha + np.float32(original_img) * (1 - alpha)
184
+ overlay = np.clip(overlay, 0, 255).astype(np.uint8)
185
+ img_pil = Image.fromarray(overlay)
186
+ buffer = io.BytesIO()
187
+ img_pil.save(buffer, format='PNG')
188
+ return base64.b64encode(buffer.getvalue()).decode('utf-8')
189
+
190
+
191
+ def image_to_base64(img_np):
192
+ img_pil = Image.fromarray(img_np)
193
+ buffer = io.BytesIO()
194
+ img_pil.save(buffer, format='PNG')
195
+ return base64.b64encode(buffer.getvalue()).decode('utf-8')
196
+
197
+
198
+ # --- Load Models ---
199
+ rd_model = None
200
+ dn_model = None
201
+ grad_cam = None
202
+
203
+
204
+ def load_models():
205
+ global rd_model, dn_model, grad_cam
206
+
207
+ rd_path = os.path.join(MODEL_DIR, 'rad_dino_best.pth')
208
+ dn_path = os.path.join(MODEL_DIR, 'densenet_best.pth')
209
+
210
+ if os.path.exists(dn_path):
211
+ print("Loading DenseNet121...")
212
+ dn_model = DenseNetClassifier(num_classes=5, dropout=0.4)
213
+ state = torch.load(dn_path, map_location='cpu', weights_only=True)
214
+ dn_model.load_state_dict(state)
215
+ dn_model.to(DEVICE).eval()
216
+ grad_cam = GradCAM(dn_model)
217
+ print("[OK] DenseNet121 loaded")
218
+ else:
219
+ print(f"[WARN] DenseNet weights not found at {dn_path}")
220
+
221
+ if os.path.exists(rd_path):
222
+ print("Loading RAD-DINO...")
223
+ rd_model = RADDINOClassifier(num_classes=5, dropout=0.3)
224
+ state = torch.load(rd_path, map_location='cpu', weights_only=True)
225
+ rd_model.load_state_dict(state)
226
+ rd_model.to(DEVICE).eval()
227
+ print("[OK] RAD-DINO loaded")
228
+ else:
229
+ print(f"[WARN] RAD-DINO weights not found at {rd_path}")
230
+
231
+
232
+ # --- Core prediction logic ---
233
+ def run_prediction(image_bytes):
234
+ """Run ensemble prediction and return results dict."""
235
+ heatmaps = {}
236
+
237
+ # DenseNet prediction + Grad-CAM
238
+ dn_probs = None
239
+ if dn_model is not None:
240
+ tensor_320, img_np = preprocess_image(image_bytes, transform_320)
241
+ with torch.no_grad():
242
+ logits = dn_model(tensor_320)
243
+ dn_probs = torch.sigmoid(logits).cpu().numpy()[0]
244
+
245
+ for i, label in enumerate(TARGET_LABELS):
246
+ tensor_for_cam, _ = preprocess_image(image_bytes, transform_320)
247
+ cam = grad_cam.generate(tensor_for_cam, class_idx=i)
248
+ heatmaps[label] = create_heatmap_overlay(img_np, cam, alpha=0.45)
249
+
250
+ # RAD-DINO prediction
251
+ rd_probs = None
252
+ if rd_model is not None:
253
+ tensor_384, img_np = preprocess_image(image_bytes, transform_384)
254
+ with torch.no_grad():
255
+ logits = rd_model(tensor_384)
256
+ rd_probs = torch.sigmoid(logits).cpu().numpy()[0]
257
+
258
+ # Ensemble
259
+ if rd_probs is not None and dn_probs is not None:
260
+ ensemble_probs = ENSEMBLE_WEIGHT_RD * rd_probs + ENSEMBLE_WEIGHT_DN * dn_probs
261
+ elif rd_probs is not None:
262
+ ensemble_probs = rd_probs
263
+ elif dn_probs is not None:
264
+ ensemble_probs = dn_probs
265
+ else:
266
+ return None
267
+
268
+ original_b64 = image_to_base64(img_np)
269
+
270
+ results = []
271
+ for i, label in enumerate(TARGET_LABELS):
272
+ prob = float(ensemble_probs[i])
273
+ risk = 'high' if prob > 0.6 else ('medium' if prob > 0.3 else 'low')
274
+ results.append({
275
+ 'label': label,
276
+ 'probability': round(prob * 100, 1),
277
+ 'risk': risk,
278
+ 'description': LABEL_INFO[label]['description'],
279
+ 'icon': LABEL_INFO[label]['icon'],
280
+ 'heatmap': heatmaps.get(label, ''),
281
+ 'rd_prob': round(float(rd_probs[i]) * 100, 1) if rd_probs is not None else None,
282
+ 'dn_prob': round(float(dn_probs[i]) * 100, 1) if dn_probs is not None else None,
283
+ })
284
+
285
+ results.sort(key=lambda x: x['probability'], reverse=True)
286
+
287
+ return {
288
+ 'success': True,
289
+ 'results': results,
290
+ 'original_image': original_b64,
291
+ 'models_used': {
292
+ 'rad_dino': rd_probs is not None,
293
+ 'densenet': dn_probs is not None,
294
+ 'ensemble': rd_probs is not None and dn_probs is not None,
295
+ }
296
+ }
297
+
298
+
299
+ # --- Routes ---
300
+ @app.route('/')
301
+ def index():
302
+ return render_template('index.html')
303
+
304
+
305
+ @app.route('/analyze')
306
+ def analyze():
307
+ return render_template('analyze.html')
308
+
309
+
310
+ @app.route('/login')
311
+ def login():
312
+ return render_template('login.html')
313
+
314
+ @app.route('/register')
315
+ def register():
316
+ return render_template('register.html')
317
+
318
+ @app.route('/about')
319
+ def about():
320
+ return render_template('about.html')
321
+
322
+
323
+ @app.route('/history')
324
+ def history():
325
+ return render_template('history.html')
326
+
327
+
328
+ @app.route('/compare')
329
+ def compare():
330
+ return render_template('compare.html')
331
+
332
+
333
+ @app.route('/report/<analysis_id>')
334
+ def report(analysis_id):
335
+ data = analysis_store.get(analysis_id)
336
+ if not data:
337
+ return render_template('report.html', error=True)
338
+ return render_template('report.html', error=False, data=json.dumps(data))
339
+
340
+
341
+ @app.route('/samples')
342
+ def samples_page():
343
+ return render_template('analyze.html', show_samples=True)
344
+
345
+
346
+ # --- API Endpoints ---
347
+ @app.route('/predict', methods=['POST'])
348
+ def predict():
349
+ if 'file' not in request.files:
350
+ return jsonify({'error': 'No file uploaded'}), 400
351
+
352
+ file = request.files['file']
353
+ if file.filename == '':
354
+ return jsonify({'error': 'No file selected'}), 400
355
+
356
+ allowed = {'png', 'jpg', 'jpeg', 'bmp', 'dcm', 'dicom'}
357
+ ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else ''
358
+ if ext not in allowed:
359
+ return jsonify({'error': f'File type .{ext} not supported'}), 400
360
+
361
+ image_bytes = file.read()
362
+
363
+ # DICOM handling
364
+ if ext in ('dcm', 'dicom'):
365
+ try:
366
+ image_bytes = read_dicom_as_bytes(image_bytes)
367
+ except ValueError as e:
368
+ return jsonify({'error': str(e)}), 400
369
+
370
+ result = run_prediction(image_bytes)
371
+ if result is None:
372
+ return jsonify({'error': 'No models loaded'}), 500
373
+
374
+ # Store result for report generation
375
+ analysis_id = str(uuid.uuid4())[:8]
376
+ result['analysis_id'] = analysis_id
377
+ result['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
378
+ result['filename'] = file.filename
379
+ analysis_store[analysis_id] = result
380
+
381
+ # Keep only last 50 analyses in memory
382
+ if len(analysis_store) > 50:
383
+ oldest_key = next(iter(analysis_store))
384
+ del analysis_store[oldest_key]
385
+
386
+ return jsonify(result)
387
+
388
+
389
+ @app.route('/api/samples')
390
+ def api_samples():
391
+ """List available sample X-ray images."""
392
+ samples = []
393
+ if os.path.exists(SAMPLES_DIR):
394
+ for f in os.listdir(SAMPLES_DIR):
395
+ if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')):
396
+ name = os.path.splitext(f)[0].replace('_', ' ').replace('-', ' ').title()
397
+ samples.append({
398
+ 'filename': f,
399
+ 'name': name,
400
+ 'url': f'/samples/{f}'
401
+ })
402
+ return jsonify(samples)
403
+
404
+
405
+ @app.route('/samples/<path:filename>')
406
+ def serve_sample(filename):
407
+ return send_from_directory(SAMPLES_DIR, filename)
408
+
409
+
410
+ @app.route('/health')
411
+ def health():
412
+ return jsonify({
413
+ 'status': 'ok',
414
+ 'models': {
415
+ 'rad_dino': rd_model is not None,
416
+ 'densenet': dn_model is not None,
417
+ },
418
+ 'device': str(DEVICE)
419
+ })
420
+
421
+
422
+ if __name__ == '__main__':
423
+ os.makedirs(MODEL_DIR, exist_ok=True)
424
+ os.makedirs(SAMPLES_DIR, exist_ok=True)
425
+ os.makedirs('uploads', exist_ok=True)
426
+ load_models()
427
+ app.run(debug=False, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))
app.py_patch ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @app.route('/login')
2
+ def login():
3
+ return render_template('login.html')
4
+
5
+ @app.route('/register')
6
+ def register():
7
+ return render_template('register.html')
8
+
9
+ @app.route('/about')
models/README.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Place your model files here:
2
+ - rad_dino_best.pth (download from Kaggle notebook output)
3
+ - densenet_best.pth (download from Kaggle notebook output)
models/densenet_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a81fdeedb10246eff1943edf356791dbc052813c9b5d0a39f5590f03e5fbb75
3
+ size 29504839
models/rad_dino_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0b9ff91b504c2fe6034104bd1ea14ee87e94799585e08f37668713acd71c02b
3
+ size 347217911
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.1.0
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+ transformers>=4.30.0
5
+ Pillow>=10.0.0
6
+ albumentations>=1.3.0
7
+ numpy>=1.24.0
8
+ opencv-python>=4.8.0
9
+ pydicom>=2.4.0
10
+ gunicorn>=21.2.0
samples/README.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Place sample chest X-ray images here.
2
+ Files should be in PNG, JPG, or BMP format.
3
+ They will automatically appear in the sample gallery on the Analyze page.
4
+
5
+ Suggested naming convention:
6
+ - normal_chest.png
7
+ - pleural_effusion.png
8
+ - cardiomegaly.png
9
+ - edema.png
10
+ - consolidation.png
static/app.js ADDED
@@ -0,0 +1,903 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // ========== Auth Check ==========
3
+ initAuth();
4
+
5
+ // ========== Common: Health Check ==========
6
+ fetch('/health').then(r => r.json()).then(data => {
7
+ const dot = document.getElementById('status-dot');
8
+ const txt = document.getElementById('status-text');
9
+ if (!dot || !txt) return;
10
+ const both = data.models.rad_dino && data.models.densenet;
11
+ const any = data.models.rad_dino || data.models.densenet;
12
+ dot.className = 'status-dot ' + (both ? 'online' : (any ? 'partial' : 'offline'));
13
+ txt.textContent = both ? 'Models Ready' : (any ? 'Partial' : 'No Models');
14
+ }).catch(() => { });
15
+
16
+ // ========== Analyze Page ==========
17
+ initAnalyzePage();
18
+
19
+ // ========== History Page ==========
20
+ initHistoryPage();
21
+
22
+ // ========== Compare Page ==========
23
+ initComparePage();
24
+
25
+ // ========== Load Samples ==========
26
+ loadSamples();
27
+ });
28
+
29
+ // ---------- AUTHENTICATION ----------
30
+ function initAuth() {
31
+ const userJson = localStorage.getItem('cx_user');
32
+ const user = userJson ? JSON.parse(userJson) : null;
33
+
34
+ const navAuth = document.getElementById('nav-auth');
35
+ const navProfile = document.getElementById('nav-profile');
36
+
37
+ if (user && navProfile) {
38
+ if (navAuth) navAuth.style.display = 'none';
39
+ navProfile.style.display = 'flex';
40
+
41
+ const navName = document.getElementById('nav-user-name');
42
+ const dropName = document.getElementById('dropdown-name');
43
+ const dropEmail = document.getElementById('dropdown-email');
44
+
45
+ if (navName) navName.textContent = user.name.split(' ')[0];
46
+ if (dropName) dropName.textContent = user.name;
47
+ if (dropEmail) dropEmail.textContent = user.email;
48
+
49
+ // Dropdown toggle
50
+ const trigger = document.getElementById('profile-trigger');
51
+ const dropdown = document.getElementById('profile-dropdown');
52
+ if (trigger && dropdown) {
53
+ trigger.addEventListener('click', (e) => {
54
+ e.stopPropagation();
55
+ dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
56
+ });
57
+ document.addEventListener('click', (e) => {
58
+ if (!trigger.contains(e.target) && !dropdown.contains(e.target)) {
59
+ dropdown.style.display = 'none';
60
+ }
61
+ });
62
+ }
63
+
64
+ // Logout
65
+ const logoutBtn = document.getElementById('logout-btn');
66
+ if (logoutBtn) {
67
+ logoutBtn.addEventListener('click', () => {
68
+ localStorage.removeItem('cx_user');
69
+ window.location.href = '/';
70
+ });
71
+ }
72
+ } else if (navAuth) {
73
+ navAuth.style.display = 'flex';
74
+ if (navProfile) navProfile.style.display = 'none';
75
+ }
76
+
77
+ // Login Form
78
+ const loginForm = document.getElementById('login-form');
79
+ if (loginForm) {
80
+ loginForm.addEventListener('submit', (e) => {
81
+ e.preventDefault();
82
+ const email = document.getElementById('email').value;
83
+ // Mock login
84
+ const mockName = email.split('@')[0].charAt(0).toUpperCase() + email.split('@')[0].slice(1);
85
+ localStorage.setItem('cx_user', JSON.stringify({ name: mockName, email: email }));
86
+ window.location.href = '/history';
87
+ });
88
+ }
89
+
90
+ // Register Form
91
+ const registerForm = document.getElementById('register-form');
92
+ if (registerForm) {
93
+ registerForm.addEventListener('submit', (e) => {
94
+ e.preventDefault();
95
+ const name = document.getElementById('name').value;
96
+ const email = document.getElementById('email').value;
97
+ localStorage.setItem('cx_user', JSON.stringify({ name: name, email: email }));
98
+ window.location.href = '/history';
99
+ });
100
+ }
101
+ }
102
+
103
+
104
+ // ---------- HISTORY HELPERS ----------
105
+ function saveToHistory(data) {
106
+ try {
107
+ let history = JSON.parse(localStorage.getItem('chestxpert_history') || '[]');
108
+ const entry = {
109
+ id: data.analysis_id,
110
+ timestamp: data.timestamp,
111
+ filename: data.filename,
112
+ results: data.results.map(r => ({ label: r.label, probability: r.probability, risk: r.risk })),
113
+ thumbnail: data.original_image ? data.original_image.substring(0, 200) : ''
114
+ };
115
+ history.unshift(entry);
116
+ if (history.length > 30) history = history.slice(0, 30);
117
+ localStorage.setItem('chestxpert_history', JSON.stringify(history));
118
+ } catch (e) { console.warn('Could not save history', e); }
119
+ }
120
+
121
+
122
+ // ---------- ANALYZE PAGE ----------
123
+ function initAnalyzePage() {
124
+ const uploadZone = document.getElementById('upload-zone');
125
+ if (!uploadZone) return;
126
+
127
+ const fileInput = document.getElementById('file-input');
128
+ const previewCard = document.getElementById('preview-card');
129
+ const previewImg = document.getElementById('preview-img');
130
+ const fileInfo = document.getElementById('file-info');
131
+ const btnChange = document.getElementById('btn-change');
132
+ const btnAnalyze = document.getElementById('btn-analyze');
133
+ const btnNew = document.getElementById('btn-new');
134
+ const btnPdf = document.getElementById('btn-pdf');
135
+ const btnReport = document.getElementById('btn-report-link');
136
+ const btnExportJson = document.getElementById('btn-export-json');
137
+ const uploadSection = document.getElementById('upload-section');
138
+ const loadingSection = document.getElementById('loading-section');
139
+ const resultsSection = document.getElementById('results-section');
140
+ const samplesSection = document.getElementById('samples-section');
141
+
142
+ let selectedFile = null;
143
+ let currentData = null;
144
+
145
+ uploadZone.addEventListener('click', () => fileInput.click());
146
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
147
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
148
+ uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); });
149
+ fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) handleFile(e.target.files[0]); });
150
+
151
+ btnChange.addEventListener('click', resetUpload);
152
+ btnAnalyze.addEventListener('click', () => { if (selectedFile) analyzeImage(selectedFile); });
153
+ btnNew.addEventListener('click', resetAll);
154
+
155
+ if (btnPdf) btnPdf.addEventListener('click', () => { if (currentData) generatePDF(currentData); });
156
+ if (btnExportJson) btnExportJson.addEventListener('click', () => { if (currentData) exportJSON(currentData); });
157
+
158
+ function handleFile(file) {
159
+ const ext = file.name.split('.').pop().toLowerCase();
160
+ const validExts = ['png', 'jpg', 'jpeg', 'bmp', 'dcm', 'dicom'];
161
+ if (!file.type.startsWith('image/') && !validExts.includes(ext)) {
162
+ showToast('Please upload an image or DICOM file', 'error');
163
+ return;
164
+ }
165
+ selectedFile = file;
166
+
167
+ if (ext === 'dcm' || ext === 'dicom') {
168
+ previewImg.src = '';
169
+ previewImg.alt = 'DICOM file - preview after analysis';
170
+ uploadZone.style.display = 'none';
171
+ previewCard.style.display = 'block';
172
+ fileInfo.textContent = `${file.name} (DICOM, ${(file.size / 1024).toFixed(0)} KB)`;
173
+ } else {
174
+ const reader = new FileReader();
175
+ reader.onload = (e) => {
176
+ previewImg.src = e.target.result;
177
+ uploadZone.style.display = 'none';
178
+ previewCard.style.display = 'block';
179
+ fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(0)} KB)`;
180
+ };
181
+ reader.readAsDataURL(file);
182
+ }
183
+ }
184
+
185
+ function resetUpload() {
186
+ selectedFile = null;
187
+ fileInput.value = '';
188
+ uploadZone.style.display = '';
189
+ previewCard.style.display = 'none';
190
+ }
191
+
192
+ function resetAll() {
193
+ resetUpload();
194
+ resultsSection.style.display = 'none';
195
+ loadingSection.style.display = 'none';
196
+ uploadSection.style.display = '';
197
+ if (samplesSection) samplesSection.style.display = '';
198
+ currentData = null;
199
+ }
200
+
201
+ function showLoading() {
202
+ uploadSection.style.display = 'none';
203
+ loadingSection.style.display = '';
204
+ resultsSection.style.display = 'none';
205
+ if (samplesSection) samplesSection.style.display = 'none';
206
+ animateSteps();
207
+ }
208
+
209
+ function animateSteps() {
210
+ const steps = ['step-1', 'step-2', 'step-3', 'step-4', 'step-5'];
211
+ let current = 0;
212
+ const bar = document.getElementById('loading-bar-fill');
213
+ function next() {
214
+ if (current > 0) { const prev = document.getElementById(steps[current - 1]); prev.classList.remove('active'); prev.classList.add('done'); }
215
+ if (current < steps.length) {
216
+ document.getElementById(steps[current]).classList.add('active');
217
+ bar.style.width = ((current + 1) / steps.length * 100) + '%';
218
+ current++;
219
+ setTimeout(next, 600 + Math.random() * 400);
220
+ }
221
+ }
222
+ next();
223
+ }
224
+
225
+ async function analyzeImage(file) {
226
+ showLoading();
227
+ const formData = new FormData();
228
+ formData.append('file', file);
229
+ try {
230
+ const res = await fetch('/predict', { method: 'POST', body: formData });
231
+ const data = await res.json();
232
+ if (data.error) { showToast(data.error, 'error'); resetAll(); return; }
233
+ currentData = data;
234
+ saveToHistory(data);
235
+ setTimeout(() => showResults(data), 2000);
236
+ } catch (err) {
237
+ showToast('Analysis failed. Check if the server is running.', 'error');
238
+ resetAll();
239
+ }
240
+ }
241
+
242
+ // Expose for sample clicks
243
+ window._analyzeFile = analyzeImage;
244
+ window._handleFile = handleFile;
245
+ }
246
+
247
+ function showResults(data) {
248
+ const loadingSection = document.getElementById('loading-section');
249
+ const resultsSection = document.getElementById('results-section');
250
+ loadingSection.style.display = 'none';
251
+ resultsSection.style.display = '';
252
+
253
+ document.getElementById('result-original').src = 'data:image/png;base64,' + data.original_image;
254
+
255
+ const reportLink = document.getElementById('btn-report-link');
256
+ if (reportLink && data.analysis_id) {
257
+ reportLink.href = '/report/' + data.analysis_id;
258
+ }
259
+
260
+ const list = document.getElementById('predictions-list');
261
+ const pills = document.getElementById('heatmap-pills');
262
+ list.innerHTML = '';
263
+ pills.innerHTML = '';
264
+
265
+ const pillElements = [];
266
+ const predElements = [];
267
+
268
+ data.results.forEach((r, i) => {
269
+ const el = document.createElement('div');
270
+ el.className = 'pred-item p-3 rounded-sm border-l-4 border-transparent cursor-pointer hover:bg-slate-50 transition-colors';
271
+ el.innerHTML = `
272
+ <div class="pred-top">
273
+ <span class="pred-name">${r.label}</span>
274
+ <div class="pred-right">
275
+ <span class="risk-tag ${r.risk}">${r.risk.toUpperCase()}</span>
276
+ <span class="pred-pct ${r.risk}">${r.probability}%</span>
277
+ </div>
278
+ </div>
279
+ <div class="pred-bar-bg"><div class="pred-bar ${r.risk}" id="pred-bar-${i}"></div></div>
280
+ <p class="pred-desc">${r.description}</p>
281
+ ${r.rd_prob !== null && r.dn_prob !== null ? `<div class="pred-models text-slate-700"><span>RAD-DINO: ${r.rd_prob}%</span><span>DenseNet: ${r.dn_prob}%</span></div>` : ''}
282
+ `;
283
+ list.appendChild(el);
284
+ predElements.push(el);
285
+
286
+ setTimeout(() => { document.getElementById('pred-bar-' + i).style.width = r.probability + '%'; }, 150 + i * 120);
287
+
288
+ if (r.heatmap) {
289
+ const pill = document.createElement('button');
290
+ pill.className = 'pill';
291
+ pill.textContent = r.label;
292
+
293
+ const activate = () => {
294
+ pills.querySelectorAll('.pill').forEach(p => p.classList.remove('active'));
295
+ pill.classList.add('active');
296
+ document.getElementById('result-heatmap').src = 'data:image/png;base64,' + r.heatmap;
297
+
298
+ predElements.forEach(item => {
299
+ item.classList.remove('bg-blue-50/50', 'border-blue-900');
300
+ item.classList.add('border-transparent');
301
+ });
302
+ el.classList.remove('border-transparent');
303
+ el.classList.add('bg-blue-50/50', 'border-blue-900');
304
+
305
+ document.getElementById('result-heatmap').style.display = '';
306
+ document.getElementById('result-original').style.display = 'none';
307
+ document.querySelectorAll('.card-tab').forEach(t => t.classList.remove('active'));
308
+ const heatmapTab = document.querySelector('[data-tab="heatmap"]');
309
+ if (heatmapTab) heatmapTab.classList.add('active');
310
+ };
311
+
312
+ pill.addEventListener('click', activate);
313
+ el.addEventListener('click', activate);
314
+
315
+ pills.appendChild(pill);
316
+ pillElements.push({ pill, activate });
317
+ }
318
+ });
319
+
320
+ if (pillElements.length > 0) {
321
+ pillElements[0].activate();
322
+ }
323
+
324
+ document.querySelectorAll('.card-tab').forEach(tab => {
325
+ tab.addEventListener('click', () => {
326
+ document.querySelectorAll('.card-tab').forEach(t => t.classList.remove('active'));
327
+ tab.classList.add('active');
328
+ const t = tab.dataset.tab;
329
+ document.getElementById('result-original').style.display = t === 'original' ? '' : 'none';
330
+ document.getElementById('result-heatmap').style.display = t === 'heatmap' ? '' : 'none';
331
+ document.getElementById('heatmap-selector').style.display = t === 'heatmap' ? '' : 'none';
332
+ });
333
+ });
334
+
335
+ const tags = document.getElementById('model-tags');
336
+ tags.innerHTML = '';
337
+ const ms = data.models_used;
338
+ if (ms.rad_dino) tags.innerHTML += `<span class="model-tag on">RAD-DINO</span>`;
339
+ if (ms.densenet) tags.innerHTML += `<span class="model-tag on">DenseNet121</span>`;
340
+ if (ms.ensemble) tags.innerHTML += `<span class="model-tag on">Ensemble</span>`;
341
+
342
+ const high = data.results.filter(r => r.risk === 'high');
343
+ const med = data.results.filter(r => r.risk === 'medium');
344
+ const title = document.getElementById('summary-title');
345
+ const text = document.getElementById('summary-text');
346
+
347
+ if (high.length > 0) {
348
+ title.textContent = 'Findings Detected';
349
+ text.textContent = `High probability: ${high.map(r => r.label).join(', ')}.${med.length > 0 ? ' Moderate: ' + med.map(r => r.label).join(', ') + '.' : ''} Consult a radiologist.`;
350
+ } else if (med.length > 0) {
351
+ title.textContent = 'Possible Findings';
352
+ text.textContent = `Moderate probability: ${med.map(r => r.label).join(', ')}. Clinical correlation recommended.`;
353
+ } else {
354
+ title.textContent = 'No Significant Findings';
355
+ text.textContent = 'All conditions show low probability. This does not rule out other pathologies.';
356
+ }
357
+ }
358
+
359
+
360
+ // ---------- PDF GENERATION ----------
361
+ function generatePDF(data) {
362
+ try {
363
+ const { jsPDF } = window.jspdf;
364
+ const doc = new jsPDF();
365
+
366
+ doc.setFont('helvetica', 'bold');
367
+ doc.setFontSize(18);
368
+ doc.text('ChestXpert Analysis Report', 20, 20);
369
+
370
+ doc.setFont('helvetica', 'normal');
371
+ doc.setFontSize(10);
372
+ doc.text(`Date: ${data.timestamp || new Date().toLocaleString()}`, 20, 30);
373
+ doc.text(`File: ${data.filename || 'Unknown'}`, 20, 36);
374
+ doc.text(`Report ID: ${data.analysis_id || '-'}`, 20, 42);
375
+
376
+ // Original image
377
+ if (data.original_image) {
378
+ try {
379
+ doc.addImage('data:image/png;base64,' + data.original_image, 'PNG', 20, 50, 70, 70);
380
+ } catch (e) { /* skip image if it fails */ }
381
+ }
382
+
383
+ // Top heatmap
384
+ if (data.results[0] && data.results[0].heatmap) {
385
+ try {
386
+ doc.addImage('data:image/png;base64,' + data.results[0].heatmap, 'PNG', 110, 50, 70, 70);
387
+ doc.setFontSize(8);
388
+ doc.text('Grad-CAM: ' + data.results[0].label, 110, 48);
389
+ } catch (e) { /* skip */ }
390
+ }
391
+
392
+ // Findings table
393
+ let y = 130;
394
+ doc.setFont('helvetica', 'bold');
395
+ doc.setFontSize(14);
396
+ doc.text('Findings', 20, y);
397
+ y += 10;
398
+
399
+ doc.setFontSize(9);
400
+ doc.setFont('helvetica', 'bold');
401
+ doc.text('Condition', 20, y);
402
+ doc.text('Probability', 90, y);
403
+ doc.text('Risk', 120, y);
404
+ doc.text('RAD-DINO', 145, y);
405
+ doc.text('DenseNet', 170, y);
406
+ y += 2;
407
+ doc.line(20, y, 190, y);
408
+ y += 6;
409
+
410
+ doc.setFont('helvetica', 'normal');
411
+ data.results.forEach(r => {
412
+ doc.text(r.label, 20, y);
413
+ doc.text(r.probability + '%', 90, y);
414
+ doc.text(r.risk.toUpperCase(), 120, y);
415
+ doc.text(r.rd_prob !== null ? r.rd_prob + '%' : 'N/A', 145, y);
416
+ doc.text(r.dn_prob !== null ? r.dn_prob + '%' : 'N/A', 170, y);
417
+ y += 7;
418
+ });
419
+
420
+ y += 10;
421
+ doc.setFontSize(8);
422
+ doc.setFont('helvetica', 'italic');
423
+ doc.text('Disclaimer: This report is for educational and research purposes only.', 20, y);
424
+ doc.text('It is not a substitute for professional medical diagnosis.', 20, y + 5);
425
+
426
+ doc.save(`chestxpert_report_${data.analysis_id || 'analysis'}.pdf`);
427
+ showToast('PDF downloaded', 'info');
428
+ } catch (e) {
429
+ showToast('PDF generation failed: ' + e.message, 'error');
430
+ }
431
+ }
432
+
433
+
434
+ // ---------- JSON EXPORT ----------
435
+ function exportJSON(data) {
436
+ const exportData = {
437
+ analysis_id: data.analysis_id,
438
+ timestamp: data.timestamp,
439
+ filename: data.filename,
440
+ models_used: data.models_used,
441
+ results: data.results.map(r => ({
442
+ label: r.label,
443
+ probability: r.probability,
444
+ risk: r.risk,
445
+ rad_dino_prob: r.rd_prob,
446
+ densenet_prob: r.dn_prob
447
+ }))
448
+ };
449
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
450
+ const url = URL.createObjectURL(blob);
451
+ const a = document.createElement('a');
452
+ a.href = url;
453
+ a.download = `chestxpert_${data.analysis_id || 'results'}.json`;
454
+ a.click();
455
+ URL.revokeObjectURL(url);
456
+ showToast('JSON exported', 'info');
457
+ }
458
+
459
+
460
+ function initHistoryPage() {
461
+ const historyList = document.getElementById('history-list');
462
+ const tableContainer = document.getElementById('history-table');
463
+ if (!historyList) return;
464
+
465
+ const emptyState = document.getElementById('history-empty');
466
+ const emptyTitle = document.getElementById('empty-title');
467
+ const emptyDesc = document.getElementById('empty-desc');
468
+ const emptyAction = document.getElementById('empty-action');
469
+ const btnClear = document.getElementById('btn-clear-history');
470
+
471
+ function renderRow(entry, index) {
472
+ const topResult = entry.results[0];
473
+ let statusHtml = '';
474
+ let statusClass = 'bg-slate-100 text-slate-700 border-slate-200';
475
+
476
+ if (entry.results.some(r => r.risk === 'high')) {
477
+ statusHtml = `<span class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>High Risk</span>`;
478
+ statusClass = 'bg-red-50 text-red-700 border-red-200';
479
+ } else if (entry.results.some(r => r.risk === 'medium')) {
480
+ statusHtml = `<span class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>Moderate Risk</span>`;
481
+ statusClass = 'bg-amber-50 text-amber-700 border-amber-200';
482
+ } else {
483
+ statusHtml = `<span class="flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span>Low Risk</span>`;
484
+ }
485
+
486
+ const dateStr = entry.timestamp ? entry.timestamp.substring(0, 16).replace('T', ' ') : 'N/A';
487
+ const studyId = entry.id ? 'CX-' + entry.id.substring(0, 6).toUpperCase() : (entry.filename || 'UNKNOWN');
488
+
489
+ const tr = document.createElement('tr');
490
+ tr.className = 'bg-white hover:bg-slate-50 border-b border-slate-100 transition-colors cursor-pointer group';
491
+ tr.innerHTML = `
492
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-medium text-slate-700">${dateStr}</td>
493
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-slate-500 uppercase">${studyId}</td>
494
+ <td class="px-6 py-4 whitespace-nowrap text-xs font-bold text-slate-900">${topResult.label}</td>
495
+ <td class="px-6 py-4 whitespace-nowrap">
496
+ <div class="inline-flex items-center px-2 py-0.5 rounded-sm border ${statusClass} text-[10px] uppercase font-bold tracking-wider">
497
+ ${statusHtml}
498
+ </div>
499
+ </td>
500
+ <td class="px-6 py-4 whitespace-nowrap">
501
+ <div class="flex items-center gap-3">
502
+ <div class="w-24 h-1.5 bg-slate-100 rounded-none overflow-hidden border border-slate-200">
503
+ <div class="h-full bg-blue-900 transition-all" style="width: ${topResult.probability}%"></div>
504
+ </div>
505
+ <span class="text-xs font-bold text-slate-700 w-8 text-right">${topResult.probability}%</span>
506
+ </div>
507
+ </td>
508
+ <td class="px-6 py-4 whitespace-nowrap text-right text-xs font-medium">
509
+ <a href="/report/${entry.id}" target="_blank" class="text-blue-900 hover:text-blue-700 font-bold uppercase tracking-widest text-[10px] border border-transparent hover:border-blue-200 px-3 py-1.5 rounded-sm transition-colors" onclick="event.stopPropagation()">View Report</a>
510
+ </td>
511
+ `;
512
+
513
+ const detailsTr = document.createElement('tr');
514
+ detailsTr.className = 'bg-slate-50 border-b border-slate-200';
515
+ detailsTr.style.display = 'none';
516
+
517
+ let detailsHtml = '<td colspan="6" class="px-6 py-4"><div class="grid grid-cols-1 md:grid-cols-5 gap-6">';
518
+ entry.results.forEach(r => {
519
+ let barColor = 'bg-slate-400';
520
+ if (r.risk === 'high') barColor = 'bg-red-500';
521
+ else if (r.risk === 'medium') barColor = 'bg-amber-500';
522
+
523
+ detailsHtml += `
524
+ <div class="flex flex-col gap-1">
525
+ <div class="flex justify-between items-center">
526
+ <span class="text-[10px] font-bold uppercase tracking-widest text-slate-500">${r.label}</span>
527
+ <span class="text-[10px] font-bold text-slate-700">${r.probability}%</span>
528
+ </div>
529
+ <div class="w-full h-1 bg-slate-200 rounded-none overflow-hidden">
530
+ <div class="h-full ${barColor}" style="width: ${r.probability}%"></div>
531
+ </div>
532
+ </div>
533
+ `;
534
+ });
535
+ detailsHtml += '</div></td>';
536
+ detailsTr.innerHTML = detailsHtml;
537
+
538
+ tr.addEventListener('click', () => {
539
+ const isVisible = detailsTr.style.display !== 'none';
540
+ document.querySelectorAll('#history-list tr:nth-child(even)').forEach(row => row.style.display = 'none');
541
+ document.querySelectorAll('#history-list tr:nth-child(odd)').forEach(row => row.classList.remove('bg-slate-100'));
542
+
543
+ if (!isVisible) {
544
+ detailsTr.style.display = '';
545
+ tr.classList.add('bg-slate-100');
546
+ }
547
+ });
548
+
549
+ historyList.appendChild(tr);
550
+ historyList.appendChild(detailsTr);
551
+ }
552
+
553
+ function render() {
554
+ const history = JSON.parse(localStorage.getItem('chestxpert_history') || '[]');
555
+ historyList.innerHTML = '';
556
+
557
+ if (!localStorage.getItem('cx_user')) {
558
+ tableContainer.style.display = 'none';
559
+ emptyState.style.display = 'flex';
560
+ emptyTitle.textContent = 'Sign In Required';
561
+ emptyDesc.textContent = 'Clinical history access requires active authentication.';
562
+ emptyAction.innerHTML = '<a href="/login" class="bg-slate-800 hover:bg-slate-700 text-white text-[10px] font-bold py-2 px-6 rounded-sm uppercase tracking-widest shadow-sm transition-colors inline-block">Secure Login</a>';
563
+ if (btnClear) btnClear.style.display = 'none';
564
+ return;
565
+ }
566
+
567
+ if (history.length === 0) {
568
+ tableContainer.style.display = 'none';
569
+ emptyState.style.display = 'flex';
570
+ emptyTitle.textContent = 'Archive Empty';
571
+ emptyDesc.textContent = 'No clinical studies are currently stored in the local registry.';
572
+ emptyAction.innerHTML = '<a href="/analyze" class="bg-blue-900 hover:bg-blue-800 text-white text-[10px] font-bold py-2 px-6 rounded-sm uppercase tracking-widest shadow-sm transition-colors border border-blue-900 inline-block">Initialize New Study</a>';
573
+ return;
574
+ }
575
+
576
+ tableContainer.style.display = 'table';
577
+ emptyState.style.display = 'none';
578
+ if (btnClear) btnClear.style.display = 'block';
579
+
580
+ history.forEach((entry, idx) => renderRow(entry, idx));
581
+
582
+ const searchInput = document.querySelector('input[placeholder="Search by ID or Date..."]');
583
+ if (searchInput) {
584
+ searchInput.addEventListener('input', (e) => {
585
+ const term = e.target.value.toLowerCase();
586
+ const rows = historyList.querySelectorAll('tr:nth-child(odd)');
587
+ const details = historyList.querySelectorAll('tr:nth-child(even)');
588
+ rows.forEach((row, i) => {
589
+ const text = row.textContent.toLowerCase();
590
+ if (text.includes(term)) {
591
+ row.style.display = '';
592
+ } else {
593
+ row.style.display = 'none';
594
+ details[i].style.display = 'none';
595
+ }
596
+ });
597
+ });
598
+ }
599
+ }
600
+
601
+ btnClear.addEventListener('click', () => {
602
+ localStorage.removeItem('chestxpert_history');
603
+ render();
604
+ });
605
+
606
+ render();
607
+ }
608
+
609
+
610
+ function initComparePage() {
611
+ const zoneA = document.getElementById('upload-zone-a');
612
+ if (!zoneA) return;
613
+
614
+ const zoneB = document.getElementById('upload-zone-b');
615
+ const inputA = document.getElementById('file-input-a');
616
+ const inputB = document.getElementById('file-input-b');
617
+ const previewA = document.getElementById('preview-a');
618
+ const previewB = document.getElementById('preview-b');
619
+ const imgA = document.getElementById('preview-img-a');
620
+ const imgB = document.getElementById('preview-img-b');
621
+ const btnChangeA = document.getElementById('btn-change-a');
622
+ const btnChangeB = document.getElementById('btn-change-b');
623
+ const btnCompare = document.getElementById('btn-compare');
624
+ const loadingDiv = document.getElementById('compare-loading');
625
+ const resultsDiv = document.getElementById('compare-results');
626
+ const btnNew = document.getElementById('btn-compare-new');
627
+
628
+ const predsEmptyA = document.getElementById('compare-preds-a-empty');
629
+ const predsA = document.getElementById('compare-preds-a');
630
+ const predsEmptyB = document.getElementById('compare-preds-b-empty');
631
+ const predsB = document.getElementById('compare-preds-b');
632
+ const diffContainer = document.getElementById('compare-diff');
633
+
634
+ let fileA = null, fileB = null;
635
+
636
+ if (zoneA) zoneA.addEventListener('click', () => inputA && inputA.click());
637
+ if (zoneB) zoneB.addEventListener('click', () => inputB && inputB.click());
638
+
639
+ inputA.addEventListener('change', (e) => { if (e.target.files[0]) setFile('a', e.target.files[0]); });
640
+ inputB.addEventListener('change', (e) => { if (e.target.files[0]) setFile('b', e.target.files[0]); });
641
+
642
+ btnChangeA.addEventListener('click', (e) => { e.stopPropagation(); clearFile('a'); });
643
+ btnChangeB.addEventListener('click', (e) => { e.stopPropagation(); clearFile('b'); });
644
+ btnCompare.addEventListener('click', runComparison);
645
+ btnNew.addEventListener('click', () => {
646
+ clearFile('a'); clearFile('b');
647
+ resetDiffs();
648
+ });
649
+
650
+ function setupToolbar(zoneId, imgId) {
651
+ const zone = document.getElementById(zoneId);
652
+ const img = document.getElementById(imgId);
653
+ if (!zone || !img) return;
654
+
655
+ const btns = Array.from(zone.querySelectorAll('button'));
656
+ const invertBtn = btns.find(b => b.textContent.trim() === 'INVERT');
657
+ const resetBtn = btns.find(b => b.textContent.trim() === 'RESET');
658
+ const gradcamBtn = btns.find(b => b.textContent.trim() === 'GRAD-CAM');
659
+
660
+ let inverted = false;
661
+
662
+ if (invertBtn) {
663
+ invertBtn.addEventListener('click', () => {
664
+ inverted = !inverted;
665
+ img.style.filter = inverted ? 'invert(1)' : 'none';
666
+ });
667
+ }
668
+
669
+ if (resetBtn) {
670
+ resetBtn.addEventListener('click', () => {
671
+ inverted = false;
672
+ img.style.filter = 'none';
673
+ if (img.dataset.original) img.src = img.dataset.original;
674
+ });
675
+ }
676
+
677
+ if (gradcamBtn) {
678
+ gradcamBtn.addEventListener('click', () => {
679
+ if (img.dataset.heatmap) {
680
+ img.src = img.src === img.dataset.heatmap ? (img.dataset.original || img.src) : img.dataset.heatmap;
681
+ } else {
682
+ showToast('Compute Differential first to generate Grad-CAM.', 'info');
683
+ }
684
+ });
685
+ }
686
+ }
687
+
688
+ setupToolbar('compare-zone-1', 'preview-img-a');
689
+ setupToolbar('compare-zone-2', 'preview-img-b');
690
+
691
+ function setFile(side, file) {
692
+ if (side === 'a') { fileA = file; } else { fileB = file; }
693
+ const zone = side === 'a' ? zoneA : zoneB;
694
+ const preview = side === 'a' ? previewA : previewB;
695
+ const img = side === 'a' ? imgA : imgB;
696
+
697
+ const reader = new FileReader();
698
+ reader.onload = (e) => {
699
+ img.src = e.target.result;
700
+ zone.style.display = 'none';
701
+ preview.style.display = 'flex';
702
+ };
703
+ reader.readAsDataURL(file);
704
+ }
705
+
706
+ function clearFile(side) {
707
+ if (side === 'a') { fileA = null; inputA.value = ''; } else { fileB = null; inputB.value = ''; }
708
+ const zone = side === 'a' ? zoneA : zoneB;
709
+ const preview = side === 'a' ? previewA : previewB;
710
+ zone.style.display = 'flex';
711
+ preview.style.display = 'none';
712
+
713
+ if (side === 'a') {
714
+ predsA.style.display = 'none';
715
+ predsEmptyA.style.display = 'flex';
716
+ predsA.innerHTML = '';
717
+ delete imgA.dataset.original;
718
+ delete imgA.dataset.heatmap;
719
+ } else {
720
+ predsB.style.display = 'none';
721
+ predsEmptyB.style.display = 'flex';
722
+ predsB.innerHTML = '';
723
+ delete imgB.dataset.original;
724
+ delete imgB.dataset.heatmap;
725
+ }
726
+ }
727
+
728
+ function resetDiffs() {
729
+ if (!diffContainer) return;
730
+ diffContainer.innerHTML = `
731
+ <div class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
732
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Atelectasis</span>
733
+ <span class="text-lg font-bold text-slate-400">---</span>
734
+ </div>
735
+ <div class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
736
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Cardiomegaly</span>
737
+ <span class="text-lg font-bold text-slate-400">---</span>
738
+ </div>
739
+ <div class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
740
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Consolidation</span>
741
+ <span class="text-lg font-bold text-slate-400">---</span>
742
+ </div>
743
+ <div class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
744
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Edema</span>
745
+ <span class="text-lg font-bold text-slate-400">---</span>
746
+ </div>
747
+ <div class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
748
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Pleural Effusion</span>
749
+ <span class="text-lg font-bold text-slate-400">---</span>
750
+ </div>
751
+ `;
752
+ }
753
+
754
+ async function runComparison() {
755
+ if (!fileA || !fileB) {
756
+ showToast('Please upload both Baseline and Follow-up studies.', 'error');
757
+ return;
758
+ }
759
+
760
+ loadingDiv.style.display = 'flex';
761
+
762
+ let bar = document.getElementById('compare-loading-bar');
763
+ if (bar) {
764
+ bar.style.width = '0%';
765
+ setTimeout(() => { bar.style.width = '85%'; }, 100);
766
+ }
767
+
768
+ try {
769
+ const [resA, resB] = await Promise.all([
770
+ analyzeFile(fileA),
771
+ analyzeFile(fileB)
772
+ ]);
773
+
774
+ loadingDiv.style.display = 'none';
775
+
776
+ if (resA.error || resB.error) {
777
+ showToast('Analysis failed: ' + (resA.error || resB.error), 'error');
778
+ return;
779
+ }
780
+
781
+ showCompareResults(resA, resB);
782
+ } catch (e) {
783
+ showToast('Comparison failed', 'error');
784
+ loadingDiv.style.display = 'none';
785
+ }
786
+ }
787
+
788
+ async function analyzeFile(file) {
789
+ const formData = new FormData();
790
+ formData.append('file', file);
791
+ const res = await fetch('/predict', { method: 'POST', body: formData });
792
+ return res.json();
793
+ }
794
+
795
+ function showCompareResults(dataA, dataB) {
796
+ predsEmptyA.style.display = 'none';
797
+ predsEmptyB.style.display = 'none';
798
+ predsA.style.display = 'flex';
799
+ predsB.style.display = 'flex';
800
+
801
+ imgA.dataset.original = 'data:image/png;base64,' + dataA.original_image;
802
+ if (dataA.results[0] && dataA.results[0].heatmap) imgA.dataset.heatmap = 'data:image/png;base64,' + dataA.results[0].heatmap;
803
+
804
+ imgB.dataset.original = 'data:image/png;base64,' + dataB.original_image;
805
+ if (dataB.results[0] && dataB.results[0].heatmap) imgB.dataset.heatmap = 'data:image/png;base64,' + dataB.results[0].heatmap;
806
+
807
+ renderComparePreds(predsA, dataA.results);
808
+ renderComparePreds(predsB, dataB.results);
809
+ renderDiff(dataA.results, dataB.results);
810
+ }
811
+
812
+ function renderComparePreds(el, results) {
813
+ el.innerHTML = '<div class="flex flex-col gap-3">';
814
+ results.forEach(r => {
815
+ let color = 'bg-slate-400';
816
+ if (r.risk === 'high') color = 'bg-red-500';
817
+ else if (r.risk === 'medium') color = 'bg-amber-500';
818
+
819
+ el.innerHTML += `
820
+ <div class="flex items-center justify-between text-xs">
821
+ <span class="font-bold text-slate-700 uppercase tracking-wide">${r.label}</span>
822
+ <div class="flex items-center gap-3 w-3/5">
823
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden border border-slate-300">
824
+ <div class="h-full ${color}" style="width: ${r.probability}%"></div>
825
+ </div>
826
+ <span class="text-[10px] font-bold text-slate-700 w-8 text-right">${r.probability}%</span>
827
+ </div>
828
+ </div>
829
+ `;
830
+ });
831
+ el.innerHTML += '</div>';
832
+ }
833
+
834
+ function renderDiff(resultsA, resultsB) {
835
+ if (!diffContainer) return;
836
+ diffContainer.innerHTML = '';
837
+
838
+ const mapA = {};
839
+ resultsA.forEach(r => { mapA[r.label] = r.probability; });
840
+
841
+ resultsB.forEach(r => {
842
+ const probA = mapA[r.label] || 0;
843
+ const diff = (r.probability - probA).toFixed(1);
844
+
845
+ let colorClass = 'text-slate-600 bg-slate-100 border-slate-200';
846
+ let sign = '';
847
+
848
+ if (diff > 5) {
849
+ colorClass = 'text-red-700 bg-red-50 border-red-200';
850
+ sign = '+';
851
+ } else if (diff < -5) {
852
+ colorClass = 'text-emerald-700 bg-emerald-50 border-emerald-200';
853
+ }
854
+
855
+ diffContainer.innerHTML += `
856
+ <div class="${colorClass} border rounded-sm p-3 flex flex-col items-center justify-center h-20 transition-colors">
857
+ <span class="text-[9px] font-bold uppercase tracking-widest opacity-80 mb-1">${r.label}</span>
858
+ <span class="text-lg font-bold">${sign}${diff}%</span>
859
+ </div>
860
+ `;
861
+ });
862
+ }
863
+ }
864
+
865
+
866
+ // ---------- SAMPLE GALLERY ----------
867
+ function loadSamples() {
868
+ const grid = document.getElementById('samples-grid');
869
+ if (!grid) return;
870
+
871
+ fetch('/api/samples').then(r => r.json()).then(samples => {
872
+ if (samples.length === 0) {
873
+ grid.closest('.samples-section').style.display = 'none';
874
+ return;
875
+ }
876
+
877
+ samples.forEach(s => {
878
+ const card = document.createElement('div');
879
+ card.className = 'sample-card';
880
+ card.innerHTML = `<img src="${s.url}" alt="${s.name}"><p>${s.name}</p>`;
881
+ card.addEventListener('click', async () => {
882
+ const res = await fetch(s.url);
883
+ const blob = await res.blob();
884
+ const file = new File([blob], s.filename, { type: blob.type });
885
+ if (window._handleFile) window._handleFile(file);
886
+ });
887
+ grid.appendChild(card);
888
+ });
889
+ }).catch(() => {
890
+ const section = grid.closest('.samples-section');
891
+ if (section) section.style.display = 'none';
892
+ });
893
+ }
894
+
895
+
896
+ // ---------- TOAST ----------
897
+ function showToast(msg, type) {
898
+ const t = document.createElement('div');
899
+ t.className = 'toast ' + type;
900
+ t.textContent = msg;
901
+ document.body.appendChild(t);
902
+ setTimeout(() => t.remove(), 3500);
903
+ }
static/default-avatar.svg ADDED
static/style.css ADDED
@@ -0,0 +1,1879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── ChestXpert — Professional Light Medical Theme ──────────────────── */
2
+ :root {
3
+ --bg: #f8fafc;
4
+ --bg-white: #ffffff;
5
+ --bg-subtle: #f1f5f9;
6
+ --border: #e2e8f0;
7
+ --border-focus: #2563eb;
8
+ --text: #0f172a;
9
+ --text-secondary: #475569;
10
+ --text-muted: #94a3b8;
11
+ --primary: #2563eb;
12
+ --primary-light: #eff6ff;
13
+ --primary-hover: #1d4ed8;
14
+ --green: #16a34a;
15
+ --green-bg: #f0fdf4;
16
+ --yellow: #ca8a04;
17
+ --yellow-bg: #fefce8;
18
+ --red: #dc2626;
19
+ --red-bg: #fef2f2;
20
+ --radius: 12px;
21
+ --radius-sm: 8px;
22
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
23
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
24
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.04);
25
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
26
+ }
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ body {
35
+ font-family: var(--font);
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ line-height: 1.6;
39
+ -webkit-font-smoothing: antialiased;
40
+ }
41
+
42
+ .container {
43
+ max-width: 1120px;
44
+ margin: 0 auto;
45
+ padding: 0 24px;
46
+ }
47
+
48
+ a {
49
+ color: var(--primary);
50
+ text-decoration: none;
51
+ }
52
+
53
+ /* ── Navigation ─────────────────────────────────────────────────────── */
54
+ .navbar {
55
+ background: var(--bg-white);
56
+ border-bottom: 1px solid var(--border);
57
+ position: sticky;
58
+ top: 0;
59
+ z-index: 100;
60
+ }
61
+
62
+ .nav-container {
63
+ max-width: 1120px;
64
+ margin: 0 auto;
65
+ padding: 0 24px;
66
+ height: 60px;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+ }
71
+
72
+ .nav-brand {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 10px;
76
+ font-weight: 700;
77
+ font-size: 1.1rem;
78
+ color: var(--text);
79
+ }
80
+
81
+ .nav-logo {
82
+ width: 32px;
83
+ height: 32px;
84
+ }
85
+
86
+ .brand-mark {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 32px;
91
+ height: 32px;
92
+ border-radius: 8px;
93
+ background: var(--primary);
94
+ color: white;
95
+ font-size: 0.72rem;
96
+ font-weight: 700;
97
+ }
98
+
99
+ .feature-num {
100
+ width: 40px;
101
+ height: 40px;
102
+ border-radius: 50%;
103
+ background: var(--primary-light);
104
+ color: var(--primary);
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ font-size: 1rem;
109
+ font-weight: 700;
110
+ margin: 0 auto 14px;
111
+ }
112
+
113
+ .hero-tag {
114
+ font-size: 0.82rem;
115
+ color: var(--primary);
116
+ font-weight: 600;
117
+ margin-bottom: 10px;
118
+ }
119
+
120
+ .nav-links {
121
+ display: flex;
122
+ gap: 4px;
123
+ }
124
+
125
+ .nav-link {
126
+ padding: 8px 16px;
127
+ border-radius: var(--radius-sm);
128
+ font-size: 0.88rem;
129
+ font-weight: 500;
130
+ color: var(--text-secondary);
131
+ transition: all 0.2s;
132
+ }
133
+
134
+ .nav-link:hover {
135
+ color: var(--text);
136
+ background: var(--bg-subtle);
137
+ }
138
+
139
+ .nav-link.active {
140
+ color: var(--primary);
141
+ background: var(--primary-light);
142
+ }
143
+
144
+ .nav-status {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 6px;
148
+ font-size: 0.78rem;
149
+ color: var(--text-muted);
150
+ }
151
+
152
+ .status-dot {
153
+ width: 8px;
154
+ height: 8px;
155
+ border-radius: 50%;
156
+ background: var(--text-muted);
157
+ }
158
+
159
+ .status-dot.online {
160
+ background: var(--green);
161
+ box-shadow: 0 0 4px var(--green);
162
+ }
163
+
164
+ .status-dot.partial {
165
+ background: var(--yellow);
166
+ }
167
+
168
+ .status-dot.offline {
169
+ background: var(--red);
170
+ }
171
+
172
+ /* ── Buttons ────────────────────────────────────────────────────────── */
173
+ .btn {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 8px;
177
+ padding: 9px 18px;
178
+ border-radius: var(--radius-sm);
179
+ border: none;
180
+ font-family: var(--font);
181
+ font-weight: 600;
182
+ font-size: 0.88rem;
183
+ cursor: pointer;
184
+ transition: all 0.2s;
185
+ line-height: 1.4;
186
+ }
187
+
188
+ .btn-primary {
189
+ background: var(--primary);
190
+ color: white;
191
+ }
192
+
193
+ .btn-primary:hover {
194
+ background: var(--primary-hover);
195
+ }
196
+
197
+ .btn-lg {
198
+ padding: 12px 28px;
199
+ font-size: 0.95rem;
200
+ }
201
+
202
+ .btn-outline {
203
+ background: var(--bg-white);
204
+ color: var(--text-secondary);
205
+ border: 1px solid var(--border);
206
+ }
207
+
208
+ .btn-outline:hover {
209
+ border-color: var(--primary);
210
+ color: var(--primary);
211
+ }
212
+
213
+ .btn-text {
214
+ background: none;
215
+ color: var(--primary);
216
+ padding: 4px 8px;
217
+ }
218
+
219
+ /* ── Cards ──────────────────────────────────────────────────────────── */
220
+ .card {
221
+ background: var(--bg-white);
222
+ border: 1px solid var(--border);
223
+ border-radius: var(--radius);
224
+ padding: 24px;
225
+ }
226
+
227
+ /* ── Hero Section ───────────────────────────────────���───────────────── */
228
+ .hero {
229
+ padding: 60px 0 80px;
230
+ background: linear-gradient(180deg, var(--primary-light) 0%, var(--bg) 100%);
231
+ }
232
+
233
+ .hero-content {
234
+ display: grid;
235
+ grid-template-columns: 1fr 1fr;
236
+ gap: 60px;
237
+ align-items: center;
238
+ }
239
+
240
+ .hero-badge {
241
+ display: inline-block;
242
+ padding: 4px 14px;
243
+ background: var(--primary-light);
244
+ color: var(--primary);
245
+ font-size: 0.78rem;
246
+ font-weight: 600;
247
+ border-radius: 20px;
248
+ border: 1px solid rgba(37, 99, 235, 0.2);
249
+ margin-bottom: 16px;
250
+ }
251
+
252
+ .hero h1 {
253
+ font-size: 2.6rem;
254
+ font-weight: 800;
255
+ line-height: 1.15;
256
+ letter-spacing: -0.03em;
257
+ color: var(--text);
258
+ margin-bottom: 16px;
259
+ }
260
+
261
+ .hero-desc {
262
+ font-size: 1.02rem;
263
+ color: var(--text-secondary);
264
+ line-height: 1.7;
265
+ margin-bottom: 28px;
266
+ }
267
+
268
+ .hero-stats {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 24px;
272
+ margin-bottom: 32px;
273
+ }
274
+
275
+ .stat-value {
276
+ display: block;
277
+ font-size: 1.5rem;
278
+ font-weight: 700;
279
+ color: var(--primary);
280
+ }
281
+
282
+ .stat-label {
283
+ font-size: 0.78rem;
284
+ color: var(--text-muted);
285
+ font-weight: 500;
286
+ }
287
+
288
+ .stat-divider {
289
+ width: 1px;
290
+ height: 36px;
291
+ background: var(--border);
292
+ }
293
+
294
+ /* Hero Visual */
295
+ .hero-card {
296
+ background: var(--bg-white);
297
+ border-radius: var(--radius);
298
+ border: 1px solid var(--border);
299
+ box-shadow: var(--shadow-md);
300
+ overflow: hidden;
301
+ }
302
+
303
+ .hero-card-header {
304
+ padding: 10px 14px;
305
+ display: flex;
306
+ gap: 6px;
307
+ border-bottom: 1px solid var(--border);
308
+ background: var(--bg-subtle);
309
+ }
310
+
311
+ .card-dot {
312
+ width: 10px;
313
+ height: 10px;
314
+ border-radius: 50%;
315
+ }
316
+
317
+ .card-dot.red {
318
+ background: #fca5a5;
319
+ }
320
+
321
+ .card-dot.yellow {
322
+ background: #fde68a;
323
+ }
324
+
325
+ .card-dot.green {
326
+ background: #86efac;
327
+ }
328
+
329
+ .hero-preview {
330
+ height: 200px;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ background: #f9fafb;
335
+ }
336
+
337
+ .preview-placeholder {
338
+ text-align: center;
339
+ color: var(--text-muted);
340
+ }
341
+
342
+ .preview-placeholder p {
343
+ margin-top: 8px;
344
+ font-size: 0.82rem;
345
+ }
346
+
347
+ .hero-bars {
348
+ padding: 16px;
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 10px;
352
+ }
353
+
354
+ .hero-bar-item {
355
+ display: grid;
356
+ grid-template-columns: 120px 1fr 40px;
357
+ align-items: center;
358
+ gap: 10px;
359
+ font-size: 0.82rem;
360
+ color: var(--text-secondary);
361
+ }
362
+
363
+ .hero-bar-track {
364
+ height: 6px;
365
+ border-radius: 3px;
366
+ background: var(--bg-subtle);
367
+ overflow: hidden;
368
+ }
369
+
370
+ .hero-bar-fill {
371
+ height: 100%;
372
+ border-radius: 3px;
373
+ background: var(--red);
374
+ transition: width 1s ease;
375
+ }
376
+
377
+ .hero-bar-fill.medium {
378
+ background: var(--yellow);
379
+ }
380
+
381
+ .hero-bar-fill.low {
382
+ background: var(--green);
383
+ }
384
+
385
+ .hero-bar-pct {
386
+ text-align: right;
387
+ font-weight: 600;
388
+ font-size: 0.78rem;
389
+ }
390
+
391
+ /* ── Features ───────────────────────────────────────────────────────── */
392
+ .features {
393
+ padding: 80px 0;
394
+ }
395
+
396
+ .section-title {
397
+ text-align: center;
398
+ font-size: 1.6rem;
399
+ font-weight: 700;
400
+ margin-bottom: 8px;
401
+ }
402
+
403
+ .section-desc {
404
+ text-align: center;
405
+ color: var(--text-secondary);
406
+ margin-bottom: 40px;
407
+ font-size: 0.95rem;
408
+ }
409
+
410
+ .features-grid {
411
+ display: grid;
412
+ grid-template-columns: repeat(3, 1fr);
413
+ gap: 20px;
414
+ }
415
+
416
+ .feature-card {
417
+ background: var(--bg-white);
418
+ border: 1px solid var(--border);
419
+ border-radius: var(--radius);
420
+ padding: 28px 24px;
421
+ text-align: center;
422
+ transition: box-shadow 0.2s;
423
+ }
424
+
425
+ .feature-card:hover {
426
+ box-shadow: var(--shadow-md);
427
+ }
428
+
429
+ .feature-icon {
430
+ width: 56px;
431
+ height: 56px;
432
+ border-radius: 14px;
433
+ background: var(--primary-light);
434
+ display: inline-flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ margin-bottom: 16px;
438
+ }
439
+
440
+ .feature-card h3 {
441
+ font-size: 1rem;
442
+ font-weight: 700;
443
+ margin-bottom: 8px;
444
+ }
445
+
446
+ .feature-card p {
447
+ font-size: 0.88rem;
448
+ color: var(--text-secondary);
449
+ line-height: 1.5;
450
+ }
451
+
452
+ /* ── Conditions ─────────────────────────────────────────────────────── */
453
+ .conditions {
454
+ padding: 60px 0 80px;
455
+ background: var(--bg-white);
456
+ }
457
+
458
+ .conditions-grid {
459
+ display: grid;
460
+ grid-template-columns: repeat(5, 1fr);
461
+ gap: 16px;
462
+ }
463
+
464
+ .condition-card {
465
+ text-align: center;
466
+ padding: 24px 16px;
467
+ border: 1px solid var(--border);
468
+ border-radius: var(--radius);
469
+ transition: box-shadow 0.2s;
470
+ }
471
+
472
+ .condition-card:hover {
473
+ box-shadow: var(--shadow-md);
474
+ }
475
+
476
+ .condition-icon {
477
+ font-size: 2rem;
478
+ margin-bottom: 12px;
479
+ }
480
+
481
+ .condition-card h4 {
482
+ font-size: 0.88rem;
483
+ font-weight: 700;
484
+ margin-bottom: 6px;
485
+ }
486
+
487
+ .condition-card p {
488
+ font-size: 0.78rem;
489
+ color: var(--text-secondary);
490
+ line-height: 1.5;
491
+ }
492
+
493
+ /* ── CTA ────────────────────────────────────────────────────────────── */
494
+ .cta {
495
+ padding: 60px 0;
496
+ }
497
+
498
+ .cta-card {
499
+ background: var(--primary);
500
+ color: white;
501
+ border-radius: var(--radius);
502
+ text-align: center;
503
+ padding: 48px 40px;
504
+ }
505
+
506
+ .cta-card h2 {
507
+ font-size: 1.5rem;
508
+ margin-bottom: 8px;
509
+ }
510
+
511
+ .cta-card p {
512
+ opacity: 0.85;
513
+ margin-bottom: 24px;
514
+ }
515
+
516
+ .cta-card .btn {
517
+ background: white;
518
+ color: var(--primary);
519
+ }
520
+
521
+ .cta-card .btn:hover {
522
+ background: #f0f0f0;
523
+ }
524
+
525
+ /* ── Page Content ───────────────────────────────────────────────────── */
526
+ .page-content {
527
+ padding: 32px 0 60px;
528
+ min-height: calc(100vh - 140px);
529
+ }
530
+
531
+ .page-header {
532
+ margin-bottom: 32px;
533
+ }
534
+
535
+ .page-header h1 {
536
+ font-size: 1.6rem;
537
+ font-weight: 700;
538
+ margin-bottom: 4px;
539
+ }
540
+
541
+ .page-header p {
542
+ color: var(--text-secondary);
543
+ font-size: 0.95rem;
544
+ }
545
+
546
+ /* ── Upload ─────────────────────────────────────────────────────────── */
547
+ .upload-card {
548
+ border: 2px dashed var(--border);
549
+ border-radius: var(--radius);
550
+ padding: 60px 40px;
551
+ text-align: center;
552
+ cursor: pointer;
553
+ transition: all 0.2s;
554
+ background: var(--bg-white);
555
+ }
556
+
557
+ .upload-card:hover,
558
+ .upload-card.dragover {
559
+ border-color: var(--primary);
560
+ background: var(--primary-light);
561
+ }
562
+
563
+ .upload-icon-wrap {
564
+ width: 72px;
565
+ height: 72px;
566
+ border-radius: 50%;
567
+ background: var(--primary-light);
568
+ display: inline-flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ margin-bottom: 16px;
572
+ }
573
+
574
+ .upload-card h3 {
575
+ font-size: 1.15rem;
576
+ font-weight: 700;
577
+ margin-bottom: 6px;
578
+ }
579
+
580
+ .upload-card p {
581
+ color: var(--text-secondary);
582
+ font-size: 0.92rem;
583
+ margin-bottom: 8px;
584
+ }
585
+
586
+ .upload-hint {
587
+ font-size: 0.78rem;
588
+ color: var(--text-muted);
589
+ }
590
+
591
+ /* Preview Card */
592
+ .preview-card {
593
+ background: var(--bg-white);
594
+ border: 1px solid var(--border);
595
+ border-radius: var(--radius);
596
+ overflow: hidden;
597
+ max-width: 560px;
598
+ margin: 0 auto;
599
+ }
600
+
601
+ .preview-header {
602
+ padding: 14px 20px;
603
+ border-bottom: 1px solid var(--border);
604
+ display: flex;
605
+ justify-content: space-between;
606
+ align-items: center;
607
+ }
608
+
609
+ .preview-header h3 {
610
+ font-size: 0.92rem;
611
+ font-weight: 600;
612
+ }
613
+
614
+ .preview-body {
615
+ background: #f9fafb;
616
+ display: flex;
617
+ align-items: center;
618
+ justify-content: center;
619
+ max-height: 380px;
620
+ overflow: hidden;
621
+ }
622
+
623
+ .preview-body img {
624
+ width: 100%;
625
+ max-height: 380px;
626
+ object-fit: contain;
627
+ }
628
+
629
+ .preview-footer {
630
+ padding: 14px 20px;
631
+ border-top: 1px solid var(--border);
632
+ display: flex;
633
+ justify-content: space-between;
634
+ align-items: center;
635
+ }
636
+
637
+ .file-info {
638
+ font-size: 0.82rem;
639
+ color: var(--text-muted);
640
+ }
641
+
642
+ /* ── Loading ────────────────────────────────────────────────────────── */
643
+ .loading-card {
644
+ max-width: 420px;
645
+ margin: 40px auto;
646
+ background: var(--bg-white);
647
+ border: 1px solid var(--border);
648
+ border-radius: var(--radius);
649
+ padding: 36px 32px;
650
+ text-align: center;
651
+ }
652
+
653
+ .loading-bar {
654
+ height: 4px;
655
+ background: var(--bg-subtle);
656
+ border-radius: 2px;
657
+ overflow: hidden;
658
+ margin-bottom: 24px;
659
+ }
660
+
661
+ .loading-bar-fill {
662
+ width: 0%;
663
+ height: 100%;
664
+ background: var(--primary);
665
+ border-radius: 2px;
666
+ transition: width 0.4s;
667
+ animation: loading-pulse 2s ease-in-out infinite;
668
+ }
669
+
670
+ @keyframes loading-pulse {
671
+
672
+ 0%,
673
+ 100% {
674
+ opacity: 1;
675
+ }
676
+
677
+ 50% {
678
+ opacity: 0.6;
679
+ }
680
+ }
681
+
682
+ .loading-card h3 {
683
+ font-size: 1.05rem;
684
+ font-weight: 700;
685
+ margin-bottom: 4px;
686
+ }
687
+
688
+ .loading-card p {
689
+ font-size: 0.85rem;
690
+ color: var(--text-secondary);
691
+ margin-bottom: 20px;
692
+ }
693
+
694
+ .loading-steps-list {
695
+ text-align: left;
696
+ }
697
+
698
+ .step-item {
699
+ display: flex;
700
+ align-items: center;
701
+ gap: 10px;
702
+ padding: 6px 0;
703
+ font-size: 0.84rem;
704
+ color: var(--text-muted);
705
+ transition: color 0.2s;
706
+ }
707
+
708
+ .step-item.active {
709
+ color: var(--primary);
710
+ font-weight: 500;
711
+ }
712
+
713
+ .step-item.done {
714
+ color: var(--green);
715
+ }
716
+
717
+ .step-num {
718
+ width: 22px;
719
+ height: 22px;
720
+ border-radius: 50%;
721
+ background: var(--bg-subtle);
722
+ display: inline-flex;
723
+ align-items: center;
724
+ justify-content: center;
725
+ font-size: 0.72rem;
726
+ font-weight: 700;
727
+ }
728
+
729
+ .step-item.active .step-num {
730
+ background: var(--primary-light);
731
+ color: var(--primary);
732
+ }
733
+
734
+ .step-item.done .step-num {
735
+ background: var(--green-bg);
736
+ color: var(--green);
737
+ }
738
+
739
+ /* ── Results ────────────────────────────────────────────────────────── */
740
+ .results-bar {
741
+ display: flex;
742
+ justify-content: space-between;
743
+ align-items: center;
744
+ margin-bottom: 20px;
745
+ }
746
+
747
+ .results-bar h2 {
748
+ font-size: 1.2rem;
749
+ font-weight: 700;
750
+ }
751
+
752
+ .results-layout {
753
+ display: grid;
754
+ grid-template-columns: 1fr 1fr;
755
+ gap: 20px;
756
+ align-items: start;
757
+ }
758
+
759
+ /* Image Column */
760
+ .card-tabs {
761
+ display: flex;
762
+ border-bottom: 1px solid var(--border);
763
+ margin: -24px -24px 0;
764
+ padding: 0 24px;
765
+ }
766
+
767
+ .card-tab {
768
+ padding: 12px 16px;
769
+ background: none;
770
+ border: none;
771
+ border-bottom: 2px solid transparent;
772
+ font-family: var(--font);
773
+ font-size: 0.85rem;
774
+ font-weight: 600;
775
+ color: var(--text-muted);
776
+ cursor: pointer;
777
+ transition: all 0.2s;
778
+ }
779
+
780
+ .card-tab.active {
781
+ color: var(--primary);
782
+ border-bottom-color: var(--primary);
783
+ }
784
+
785
+ .card-tab:hover:not(.active) {
786
+ color: var(--text);
787
+ }
788
+
789
+ .card-image-wrap {
790
+ margin: 16px -24px;
791
+ background: #f9fafb;
792
+ display: flex;
793
+ align-items: center;
794
+ justify-content: center;
795
+ min-height: 300px;
796
+ }
797
+
798
+ .card-image {
799
+ width: 100%;
800
+ max-height: 400px;
801
+ object-fit: contain;
802
+ }
803
+
804
+ .heatmap-selector {
805
+ border-top: 1px solid var(--border);
806
+ margin: 0 -24px -24px;
807
+ padding: 14px 24px;
808
+ }
809
+
810
+ .heatmap-selector label {
811
+ font-size: 0.78rem;
812
+ color: var(--text-muted);
813
+ font-weight: 600;
814
+ text-transform: uppercase;
815
+ letter-spacing: 0.04em;
816
+ display: block;
817
+ margin-bottom: 8px;
818
+ }
819
+
820
+ .pill-group {
821
+ display: flex;
822
+ flex-wrap: wrap;
823
+ gap: 6px;
824
+ }
825
+
826
+ .pill {
827
+ padding: 4px 12px;
828
+ border-radius: 20px;
829
+ border: 1px solid var(--border);
830
+ background: var(--bg-white);
831
+ font-family: var(--font);
832
+ font-size: 0.78rem;
833
+ font-weight: 500;
834
+ color: var(--text-secondary);
835
+ cursor: pointer;
836
+ transition: all 0.15s;
837
+ }
838
+
839
+ .pill:hover {
840
+ border-color: var(--primary);
841
+ color: var(--primary);
842
+ }
843
+
844
+ .pill.active {
845
+ background: var(--primary-light);
846
+ border-color: var(--primary);
847
+ color: var(--primary);
848
+ }
849
+
850
+ .model-card {
851
+ margin-top: 16px;
852
+ }
853
+
854
+ .model-card h4 {
855
+ font-size: 0.88rem;
856
+ font-weight: 600;
857
+ margin-bottom: 10px;
858
+ }
859
+
860
+ .model-tags {
861
+ display: flex;
862
+ flex-wrap: wrap;
863
+ gap: 6px;
864
+ }
865
+
866
+ .model-tag {
867
+ padding: 5px 12px;
868
+ border-radius: 20px;
869
+ font-size: 0.76rem;
870
+ font-weight: 600;
871
+ }
872
+
873
+ .model-tag.on {
874
+ background: var(--green-bg);
875
+ color: var(--green);
876
+ border: 1px solid rgba(22, 163, 74, 0.2);
877
+ }
878
+
879
+ .model-tag.off {
880
+ background: var(--bg-subtle);
881
+ color: var(--text-muted);
882
+ border: 1px solid var(--border);
883
+ }
884
+
885
+ /* Predictions */
886
+ .card-title {
887
+ font-size: 1rem;
888
+ font-weight: 700;
889
+ margin-bottom: 16px;
890
+ }
891
+
892
+ .pred-item {
893
+ padding: 14px 16px;
894
+ border: 1px solid var(--border);
895
+ border-radius: var(--radius-sm);
896
+ margin-bottom: 10px;
897
+ transition: box-shadow 0.2s;
898
+ }
899
+
900
+ .pred-item:hover {
901
+ box-shadow: var(--shadow);
902
+ }
903
+
904
+ .pred-top {
905
+ display: flex;
906
+ justify-content: space-between;
907
+ align-items: center;
908
+ margin-bottom: 8px;
909
+ }
910
+
911
+ .pred-name {
912
+ font-weight: 600;
913
+ font-size: 0.9rem;
914
+ }
915
+
916
+ .pred-name span {
917
+ margin-right: 6px;
918
+ }
919
+
920
+ .pred-right {
921
+ display: flex;
922
+ align-items: center;
923
+ gap: 8px;
924
+ }
925
+
926
+ .pred-pct {
927
+ font-weight: 700;
928
+ font-size: 1rem;
929
+ font-variant-numeric: tabular-nums;
930
+ }
931
+
932
+ .pred-pct.low {
933
+ color: var(--green);
934
+ }
935
+
936
+ .pred-pct.medium {
937
+ color: var(--yellow);
938
+ }
939
+
940
+ .pred-pct.high {
941
+ color: var(--red);
942
+ }
943
+
944
+ .risk-tag {
945
+ padding: 2px 8px;
946
+ border-radius: 10px;
947
+ font-size: 0.68rem;
948
+ font-weight: 700;
949
+ text-transform: uppercase;
950
+ letter-spacing: 0.03em;
951
+ }
952
+
953
+ .risk-tag.low {
954
+ background: var(--green-bg);
955
+ color: var(--green);
956
+ }
957
+
958
+ .risk-tag.medium {
959
+ background: var(--yellow-bg);
960
+ color: var(--yellow);
961
+ }
962
+
963
+ .risk-tag.high {
964
+ background: var(--red-bg);
965
+ color: var(--red);
966
+ }
967
+
968
+ .pred-bar-bg {
969
+ height: 6px;
970
+ border-radius: 3px;
971
+ background: var(--bg-subtle);
972
+ overflow: hidden;
973
+ margin-bottom: 8px;
974
+ }
975
+
976
+ .pred-bar {
977
+ height: 100%;
978
+ border-radius: 3px;
979
+ width: 0%;
980
+ transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
981
+ }
982
+
983
+ .pred-bar.low {
984
+ background: var(--green);
985
+ }
986
+
987
+ .pred-bar.medium {
988
+ background: var(--yellow);
989
+ }
990
+
991
+ .pred-bar.high {
992
+ background: var(--red);
993
+ }
994
+
995
+ .pred-desc {
996
+ font-size: 0.78rem;
997
+ color: var(--text-muted);
998
+ line-height: 1.4;
999
+ }
1000
+
1001
+ .pred-models {
1002
+ font-size: 0.72rem;
1003
+ color: var(--text-muted);
1004
+ margin-top: 4px;
1005
+ display: flex;
1006
+ gap: 12px;
1007
+ }
1008
+
1009
+ /* Summary */
1010
+ .summary-card {
1011
+ margin-top: 16px;
1012
+ }
1013
+
1014
+ .summary-top {
1015
+ display: flex;
1016
+ align-items: center;
1017
+ gap: 10px;
1018
+ margin-bottom: 8px;
1019
+ }
1020
+
1021
+ .summary-badge {
1022
+ width: 32px;
1023
+ height: 32px;
1024
+ border-radius: 50%;
1025
+ display: flex;
1026
+ align-items: center;
1027
+ justify-content: center;
1028
+ font-size: 1rem;
1029
+ }
1030
+
1031
+ .summary-top h4 {
1032
+ font-size: 0.95rem;
1033
+ font-weight: 700;
1034
+ }
1035
+
1036
+ .summary-card>p {
1037
+ font-size: 0.88rem;
1038
+ color: var(--text-secondary);
1039
+ line-height: 1.6;
1040
+ }
1041
+
1042
+ /* Disclaimer */
1043
+ .disclaimer {
1044
+ margin-top: 16px;
1045
+ padding: 14px 18px;
1046
+ background: var(--yellow-bg);
1047
+ border: 1px solid rgba(202, 138, 4, 0.15);
1048
+ border-radius: var(--radius-sm);
1049
+ font-size: 0.8rem;
1050
+ color: var(--text-secondary);
1051
+ line-height: 1.5;
1052
+ }
1053
+
1054
+ .disclaimer.lg {
1055
+ padding: 24px;
1056
+ font-size: 0.88rem;
1057
+ }
1058
+
1059
+ .disclaimer.lg h4 {
1060
+ margin-bottom: 8px;
1061
+ color: var(--text);
1062
+ }
1063
+
1064
+ /* ── About Page ─────────────────────────────────────────────────────── */
1065
+ .about-section {
1066
+ margin-bottom: 40px;
1067
+ }
1068
+
1069
+ .about-section h2 {
1070
+ font-size: 1.2rem;
1071
+ font-weight: 700;
1072
+ margin-bottom: 16px;
1073
+ }
1074
+
1075
+ .about-grid {
1076
+ display: grid;
1077
+ grid-template-columns: repeat(3, 1fr);
1078
+ gap: 16px;
1079
+ }
1080
+
1081
+ .about-grid.two-col {
1082
+ grid-template-columns: repeat(2, 1fr);
1083
+ }
1084
+
1085
+ .about-card {
1086
+ text-align: center;
1087
+ }
1088
+
1089
+ .about-card-icon {
1090
+ font-size: 2rem;
1091
+ margin-bottom: 12px;
1092
+ }
1093
+
1094
+ .about-card h3 {
1095
+ font-size: 1rem;
1096
+ font-weight: 700;
1097
+ margin-bottom: 4px;
1098
+ }
1099
+
1100
+ .about-card-sub {
1101
+ font-size: 0.8rem;
1102
+ color: var(--text-muted);
1103
+ margin-bottom: 14px;
1104
+ }
1105
+
1106
+ .about-card ul {
1107
+ list-style: none;
1108
+ text-align: left;
1109
+ font-size: 0.84rem;
1110
+ color: var(--text-secondary);
1111
+ }
1112
+
1113
+ .about-card ul li {
1114
+ padding: 4px 0;
1115
+ padding-left: 16px;
1116
+ position: relative;
1117
+ }
1118
+
1119
+ .about-card ul li::before {
1120
+ content: '•';
1121
+ position: absolute;
1122
+ left: 0;
1123
+ color: var(--primary);
1124
+ }
1125
+
1126
+ .about-card-badge {
1127
+ display: inline-block;
1128
+ margin-top: 14px;
1129
+ padding: 4px 14px;
1130
+ border-radius: 20px;
1131
+ background: var(--bg-subtle);
1132
+ font-size: 0.82rem;
1133
+ font-weight: 700;
1134
+ color: var(--text-secondary);
1135
+ }
1136
+
1137
+ .about-card-badge.highlight {
1138
+ background: var(--primary-light);
1139
+ color: var(--primary);
1140
+ }
1141
+
1142
+ /* Performance Table */
1143
+ .perf-table {
1144
+ width: 100%;
1145
+ border-collapse: collapse;
1146
+ font-size: 0.88rem;
1147
+ }
1148
+
1149
+ .perf-table th {
1150
+ text-align: left;
1151
+ padding: 10px 14px;
1152
+ border-bottom: 2px solid var(--border);
1153
+ font-weight: 600;
1154
+ color: var(--text-secondary);
1155
+ font-size: 0.82rem;
1156
+ text-transform: uppercase;
1157
+ letter-spacing: 0.04em;
1158
+ }
1159
+
1160
+ .perf-table td {
1161
+ padding: 10px 14px;
1162
+ border-bottom: 1px solid var(--border);
1163
+ }
1164
+
1165
+ .perf-table tfoot td {
1166
+ font-weight: 700;
1167
+ border-top: 2px solid var(--border);
1168
+ border-bottom: none;
1169
+ }
1170
+
1171
+ .perf-badge {
1172
+ padding: 2px 10px;
1173
+ border-radius: 10px;
1174
+ font-size: 0.72rem;
1175
+ font-weight: 700;
1176
+ }
1177
+
1178
+ .perf-badge.excellent {
1179
+ background: var(--green-bg);
1180
+ color: var(--green);
1181
+ }
1182
+
1183
+ .perf-badge.good {
1184
+ background: var(--primary-light);
1185
+ color: var(--primary);
1186
+ }
1187
+
1188
+ .perf-badge.fair {
1189
+ background: var(--yellow-bg);
1190
+ color: var(--yellow);
1191
+ }
1192
+
1193
+ /* Dataset */
1194
+ .dataset-info {
1195
+ display: grid;
1196
+ grid-template-columns: 1fr 1fr;
1197
+ gap: 24px;
1198
+ align-items: center;
1199
+ }
1200
+
1201
+ .dataset-item h4 {
1202
+ font-size: 1rem;
1203
+ font-weight: 700;
1204
+ margin-bottom: 6px;
1205
+ }
1206
+
1207
+ .dataset-item p {
1208
+ font-size: 0.88rem;
1209
+ color: var(--text-secondary);
1210
+ }
1211
+
1212
+ .dataset-stats {
1213
+ display: flex;
1214
+ gap: 24px;
1215
+ justify-content: flex-end;
1216
+ }
1217
+
1218
+ .ds-stat {
1219
+ text-align: center;
1220
+ }
1221
+
1222
+ .ds-num {
1223
+ display: block;
1224
+ font-size: 1.5rem;
1225
+ font-weight: 700;
1226
+ color: var(--primary);
1227
+ }
1228
+
1229
+ .ds-label {
1230
+ font-size: 0.72rem;
1231
+ color: var(--text-muted);
1232
+ font-weight: 500;
1233
+ }
1234
+
1235
+ /* Detail Card */
1236
+ .detail-card h4 {
1237
+ font-size: 0.95rem;
1238
+ font-weight: 700;
1239
+ margin-bottom: 14px;
1240
+ }
1241
+
1242
+ .detail-card dl {
1243
+ display: grid;
1244
+ grid-template-columns: 120px 1fr;
1245
+ gap: 8px 12px;
1246
+ font-size: 0.84rem;
1247
+ }
1248
+
1249
+ .detail-card dt {
1250
+ font-weight: 600;
1251
+ color: var(--text-secondary);
1252
+ }
1253
+
1254
+ .detail-card dd {
1255
+ color: var(--text);
1256
+ }
1257
+
1258
+ /* ── Footer ─────────────────────────────────────────────────────────── */
1259
+ .footer {
1260
+ background: var(--bg-white);
1261
+ border-top: 1px solid var(--border);
1262
+ padding: 40px 0;
1263
+ margin-top: 40px;
1264
+ }
1265
+
1266
+ .footer-grid {
1267
+ display: grid;
1268
+ grid-template-columns: 2fr 1fr 1fr;
1269
+ gap: 40px;
1270
+ margin-bottom: 24px;
1271
+ }
1272
+
1273
+ .footer h4 {
1274
+ font-size: 0.88rem;
1275
+ font-weight: 700;
1276
+ margin-bottom: 12px;
1277
+ }
1278
+
1279
+ .footer p {
1280
+ font-size: 0.82rem;
1281
+ color: var(--text-secondary);
1282
+ line-height: 1.5;
1283
+ }
1284
+
1285
+ .footer a {
1286
+ display: block;
1287
+ font-size: 0.82rem;
1288
+ color: var(--text-secondary);
1289
+ padding: 3px 0;
1290
+ }
1291
+
1292
+ .footer a:hover {
1293
+ color: var(--primary);
1294
+ }
1295
+
1296
+ .footer-bottom {
1297
+ border-top: 1px solid var(--border);
1298
+ padding-top: 20px;
1299
+ text-align: center;
1300
+ }
1301
+
1302
+ .footer-bottom p {
1303
+ font-size: 0.78rem;
1304
+ color: var(--text-muted);
1305
+ }
1306
+
1307
+ /* ── Responsive ─────────────────────────────────────────────────────── */
1308
+ @media (max-width: 768px) {
1309
+ .hero-content {
1310
+ grid-template-columns: 1fr;
1311
+ gap: 32px;
1312
+ }
1313
+
1314
+ .hero-visual {
1315
+ display: none;
1316
+ }
1317
+
1318
+ .features-grid {
1319
+ grid-template-columns: 1fr;
1320
+ }
1321
+
1322
+ .conditions-grid {
1323
+ grid-template-columns: repeat(2, 1fr);
1324
+ }
1325
+
1326
+ .results-layout {
1327
+ grid-template-columns: 1fr;
1328
+ }
1329
+
1330
+ .about-grid {
1331
+ grid-template-columns: 1fr;
1332
+ }
1333
+
1334
+ .about-grid.two-col {
1335
+ grid-template-columns: 1fr;
1336
+ }
1337
+
1338
+ .dataset-info {
1339
+ grid-template-columns: 1fr;
1340
+ }
1341
+
1342
+ .footer-grid {
1343
+ grid-template-columns: 1fr;
1344
+ }
1345
+ }
1346
+
1347
+ /* ── Animations ─────────────────────────────────────────────────────── */
1348
+ @keyframes fadeIn {
1349
+ from {
1350
+ opacity: 0;
1351
+ transform: translateY(8px);
1352
+ }
1353
+
1354
+ to {
1355
+ opacity: 1;
1356
+ transform: translateY(0);
1357
+ }
1358
+ }
1359
+
1360
+ .results-section {
1361
+ animation: fadeIn 0.4s ease;
1362
+ }
1363
+
1364
+ .pred-item {
1365
+ animation: fadeIn 0.35s ease backwards;
1366
+ }
1367
+
1368
+ .pred-item:nth-child(1) {
1369
+ animation-delay: 0.05s;
1370
+ }
1371
+
1372
+ .pred-item:nth-child(2) {
1373
+ animation-delay: 0.1s;
1374
+ }
1375
+
1376
+ .pred-item:nth-child(3) {
1377
+ animation-delay: 0.15s;
1378
+ }
1379
+
1380
+ .pred-item:nth-child(4) {
1381
+ animation-delay: 0.2s;
1382
+ }
1383
+
1384
+ .pred-item:nth-child(5) {
1385
+ animation-delay: 0.25s;
1386
+ }
1387
+
1388
+ /* Toast */
1389
+ .toast {
1390
+ position: fixed;
1391
+ bottom: 24px;
1392
+ right: 24px;
1393
+ padding: 12px 20px;
1394
+ border-radius: var(--radius-sm);
1395
+ font-family: var(--font);
1396
+ font-size: 0.85rem;
1397
+ font-weight: 600;
1398
+ z-index: 9999;
1399
+ box-shadow: var(--shadow-md);
1400
+ animation: fadeIn 0.3s ease;
1401
+ }
1402
+
1403
+ .toast.error {
1404
+ background: var(--red);
1405
+ color: white;
1406
+ }
1407
+
1408
+ .toast.info {
1409
+ background: var(--primary);
1410
+ color: white;
1411
+ }
1412
+
1413
+ /* ── Hero Actions ───────────────────────────────────────────────────── */
1414
+ .hero-actions {
1415
+ display: flex;
1416
+ gap: 12px;
1417
+ }
1418
+
1419
+ /* ── Features Overview (4-col) ──────────────────────────────────────── */
1420
+ .features-overview {
1421
+ padding: 60px 0;
1422
+ background: var(--bg-white);
1423
+ }
1424
+
1425
+ .four-col {
1426
+ grid-template-columns: repeat(4, 1fr);
1427
+ }
1428
+
1429
+ /* ── Page Header Row ────────────────────────────────────────────────── */
1430
+ .page-header-row {
1431
+ display: flex;
1432
+ justify-content: space-between;
1433
+ align-items: flex-start;
1434
+ }
1435
+
1436
+ /* ── Results Actions ────────────────────────────────────────────────── */
1437
+ .results-actions {
1438
+ display: flex;
1439
+ gap: 8px;
1440
+ flex-wrap: wrap;
1441
+ }
1442
+
1443
+ /* ── Sample Gallery ─────────────────────────────────────────────────── */
1444
+ .samples-section {
1445
+ margin-bottom: 24px;
1446
+ }
1447
+
1448
+ .samples-section h3 {
1449
+ font-size: 1rem;
1450
+ font-weight: 600;
1451
+ margin-bottom: 4px;
1452
+ }
1453
+
1454
+ .samples-hint {
1455
+ font-size: 0.82rem;
1456
+ color: var(--text-muted);
1457
+ margin-bottom: 12px;
1458
+ }
1459
+
1460
+ .samples-grid {
1461
+ display: flex;
1462
+ gap: 12px;
1463
+ overflow-x: auto;
1464
+ padding-bottom: 8px;
1465
+ }
1466
+
1467
+ .sample-card {
1468
+ min-width: 120px;
1469
+ max-width: 140px;
1470
+ background: var(--bg-white);
1471
+ border: 1px solid var(--border);
1472
+ border-radius: var(--radius-sm);
1473
+ overflow: hidden;
1474
+ cursor: pointer;
1475
+ transition: all 0.2s;
1476
+ flex-shrink: 0;
1477
+ }
1478
+
1479
+ .sample-card:hover {
1480
+ border-color: var(--primary);
1481
+ box-shadow: var(--shadow);
1482
+ }
1483
+
1484
+ .sample-card img {
1485
+ width: 100%;
1486
+ height: 100px;
1487
+ object-fit: cover;
1488
+ }
1489
+
1490
+ .sample-card p {
1491
+ padding: 6px 10px;
1492
+ font-size: 0.72rem;
1493
+ font-weight: 500;
1494
+ text-align: center;
1495
+ color: var(--text-secondary);
1496
+ }
1497
+
1498
+ /* ── History Page ───────────────────────────────────────────────────── */
1499
+ .empty-state {
1500
+ text-align: center;
1501
+ padding: 60px 20px;
1502
+ }
1503
+
1504
+ .empty-state h3 {
1505
+ font-size: 1.1rem;
1506
+ margin-bottom: 8px;
1507
+ }
1508
+
1509
+ .empty-state p {
1510
+ color: var(--text-secondary);
1511
+ margin-bottom: 20px;
1512
+ }
1513
+
1514
+ .history-list {
1515
+ display: flex;
1516
+ flex-direction: column;
1517
+ gap: 12px;
1518
+ }
1519
+
1520
+ .history-card {
1521
+ background: var(--bg-white);
1522
+ border: 1px solid var(--border);
1523
+ border-radius: var(--radius);
1524
+ padding: 18px 20px;
1525
+ transition: box-shadow 0.2s;
1526
+ }
1527
+
1528
+ .history-card:hover {
1529
+ box-shadow: var(--shadow);
1530
+ }
1531
+
1532
+ .history-card-main {
1533
+ display: flex;
1534
+ justify-content: space-between;
1535
+ align-items: flex-start;
1536
+ margin-bottom: 12px;
1537
+ }
1538
+
1539
+ .history-info h4 {
1540
+ font-size: 0.92rem;
1541
+ font-weight: 600;
1542
+ margin-bottom: 2px;
1543
+ }
1544
+
1545
+ .history-date {
1546
+ font-size: 0.78rem;
1547
+ color: var(--text-muted);
1548
+ }
1549
+
1550
+ .history-id {
1551
+ font-size: 0.72rem;
1552
+ color: var(--text-muted);
1553
+ font-family: monospace;
1554
+ }
1555
+
1556
+ .history-preds {
1557
+ display: flex;
1558
+ flex-direction: column;
1559
+ gap: 6px;
1560
+ }
1561
+
1562
+ .history-pred {
1563
+ display: grid;
1564
+ grid-template-columns: 130px 1fr 50px;
1565
+ gap: 8px;
1566
+ align-items: center;
1567
+ font-size: 0.82rem;
1568
+ }
1569
+
1570
+ .history-bar-bg {
1571
+ height: 4px;
1572
+ border-radius: 2px;
1573
+ background: var(--bg-subtle);
1574
+ overflow: hidden;
1575
+ }
1576
+
1577
+ .history-bar {
1578
+ height: 100%;
1579
+ border-radius: 2px;
1580
+ }
1581
+
1582
+ .history-bar.low {
1583
+ background: var(--green);
1584
+ }
1585
+
1586
+ .history-bar.medium {
1587
+ background: var(--yellow);
1588
+ }
1589
+
1590
+ .history-bar.high {
1591
+ background: var(--red);
1592
+ }
1593
+
1594
+ .history-pct {
1595
+ text-align: right;
1596
+ font-weight: 600;
1597
+ font-size: 0.78rem;
1598
+ color: var(--text-secondary);
1599
+ }
1600
+
1601
+ .history-actions {
1602
+ margin-top: 10px;
1603
+ padding-top: 10px;
1604
+ border-top: 1px solid var(--border);
1605
+ }
1606
+
1607
+ /* ── Compare Page ───────────────────────────────────────────────────── */
1608
+ .compare-uploads {
1609
+ display: grid;
1610
+ grid-template-columns: 1fr 1fr;
1611
+ gap: 20px;
1612
+ margin-bottom: 20px;
1613
+ }
1614
+
1615
+ .compare-upload-card {
1616
+ position: relative;
1617
+ }
1618
+
1619
+ .compare-label {
1620
+ font-size: 0.82rem;
1621
+ font-weight: 700;
1622
+ color: var(--text-secondary);
1623
+ text-transform: uppercase;
1624
+ letter-spacing: 0.04em;
1625
+ margin-bottom: 8px;
1626
+ }
1627
+
1628
+ .upload-card.compact {
1629
+ padding: 30px 20px;
1630
+ }
1631
+
1632
+ .upload-card.compact p {
1633
+ font-size: 0.85rem;
1634
+ margin-bottom: 0;
1635
+ }
1636
+
1637
+ .compare-preview {
1638
+ background: var(--bg-white);
1639
+ border: 1px solid var(--border);
1640
+ border-radius: var(--radius);
1641
+ overflow: hidden;
1642
+ text-align: center;
1643
+ }
1644
+
1645
+ .compare-preview img {
1646
+ width: 100%;
1647
+ max-height: 250px;
1648
+ object-fit: contain;
1649
+ background: #f9fafb;
1650
+ }
1651
+
1652
+ .compare-preview .btn {
1653
+ margin: 8px;
1654
+ }
1655
+
1656
+ .compare-actions {
1657
+ text-align: center;
1658
+ margin-bottom: 24px;
1659
+ }
1660
+
1661
+ .compare-results-grid {
1662
+ display: grid;
1663
+ grid-template-columns: 1fr auto 1fr;
1664
+ gap: 16px;
1665
+ align-items: start;
1666
+ }
1667
+
1668
+ .compare-col-title {
1669
+ font-size: 0.88rem;
1670
+ font-weight: 700;
1671
+ text-transform: uppercase;
1672
+ letter-spacing: 0.04em;
1673
+ color: var(--text-secondary);
1674
+ margin-bottom: 10px;
1675
+ }
1676
+
1677
+ .card-image-wrap.compact {
1678
+ min-height: 180px;
1679
+ }
1680
+
1681
+ .compare-pred-row {
1682
+ display: grid;
1683
+ grid-template-columns: 110px 1fr 50px;
1684
+ gap: 8px;
1685
+ align-items: center;
1686
+ padding: 6px 0;
1687
+ font-size: 0.82rem;
1688
+ }
1689
+
1690
+ .compare-pred-label {
1691
+ font-weight: 500;
1692
+ }
1693
+
1694
+ .compare-diff-col {
1695
+ min-width: 160px;
1696
+ }
1697
+
1698
+ .diff-row {
1699
+ display: flex;
1700
+ justify-content: space-between;
1701
+ padding: 8px 0;
1702
+ border-bottom: 1px solid var(--border);
1703
+ font-size: 0.84rem;
1704
+ }
1705
+
1706
+ .diff-row:last-child {
1707
+ border-bottom: none;
1708
+ }
1709
+
1710
+ .diff-label {
1711
+ font-weight: 500;
1712
+ }
1713
+
1714
+ .diff-value {
1715
+ font-weight: 700;
1716
+ font-variant-numeric: tabular-nums;
1717
+ }
1718
+
1719
+ .diff-up {
1720
+ color: var(--red);
1721
+ }
1722
+
1723
+ .diff-down {
1724
+ color: var(--green);
1725
+ }
1726
+
1727
+ .diff-neutral {
1728
+ color: var(--text-muted);
1729
+ }
1730
+
1731
+ /* ── Report Page ────────────────────────────────────────────────────── */
1732
+ .report-container {
1733
+ max-width: 900px;
1734
+ }
1735
+
1736
+ .report-header {
1737
+ display: flex;
1738
+ gap: 8px;
1739
+ justify-content: flex-end;
1740
+ margin-bottom: 20px;
1741
+ }
1742
+
1743
+ .report-page {
1744
+ background: var(--bg-white);
1745
+ border: 1px solid var(--border);
1746
+ border-radius: var(--radius);
1747
+ overflow: hidden;
1748
+ }
1749
+
1750
+ .report-title-bar {
1751
+ display: flex;
1752
+ justify-content: space-between;
1753
+ align-items: flex-start;
1754
+ padding: 28px 32px;
1755
+ border-bottom: 2px solid var(--primary);
1756
+ }
1757
+
1758
+ .report-title-bar h1 {
1759
+ font-size: 1.3rem;
1760
+ font-weight: 700;
1761
+ }
1762
+
1763
+ .report-subtitle {
1764
+ font-size: 0.82rem;
1765
+ color: var(--text-secondary);
1766
+ }
1767
+
1768
+ .report-meta {
1769
+ text-align: right;
1770
+ font-size: 0.78rem;
1771
+ color: var(--text-secondary);
1772
+ }
1773
+
1774
+ .report-meta p {
1775
+ margin-bottom: 2px;
1776
+ }
1777
+
1778
+ .report-body {
1779
+ padding: 28px 32px;
1780
+ }
1781
+
1782
+ .report-body h3 {
1783
+ font-size: 1rem;
1784
+ font-weight: 700;
1785
+ margin: 20px 0 12px;
1786
+ }
1787
+
1788
+ .report-grid {
1789
+ display: grid;
1790
+ grid-template-columns: 1fr 1fr;
1791
+ gap: 20px;
1792
+ }
1793
+
1794
+ .report-image-box {
1795
+ background: #f9fafb;
1796
+ border: 1px solid var(--border);
1797
+ border-radius: var(--radius-sm);
1798
+ overflow: hidden;
1799
+ }
1800
+
1801
+ .report-image-box img {
1802
+ width: 100%;
1803
+ max-height: 300px;
1804
+ object-fit: contain;
1805
+ }
1806
+
1807
+ .report-heatmap-label {
1808
+ font-size: 0.78rem;
1809
+ color: var(--text-muted);
1810
+ margin-top: 6px;
1811
+ }
1812
+
1813
+ .report-table {
1814
+ width: 100%;
1815
+ border-collapse: collapse;
1816
+ font-size: 0.88rem;
1817
+ margin-bottom: 20px;
1818
+ }
1819
+
1820
+ .report-table th {
1821
+ text-align: left;
1822
+ padding: 8px 12px;
1823
+ border-bottom: 2px solid var(--border);
1824
+ font-size: 0.78rem;
1825
+ font-weight: 600;
1826
+ color: var(--text-secondary);
1827
+ text-transform: uppercase;
1828
+ }
1829
+
1830
+ .report-table td {
1831
+ padding: 8px 12px;
1832
+ border-bottom: 1px solid var(--border);
1833
+ }
1834
+
1835
+ .report-disclaimer {
1836
+ margin-top: 24px;
1837
+ padding: 16px;
1838
+ background: var(--yellow-bg);
1839
+ border: 1px solid rgba(202, 138, 4, 0.15);
1840
+ border-radius: var(--radius-sm);
1841
+ font-size: 0.82rem;
1842
+ color: var(--text-secondary);
1843
+ }
1844
+
1845
+ /* ── Responsive (Additional) ────────────────────────────────────────── */
1846
+ @media (max-width: 768px) {
1847
+ .compare-uploads {
1848
+ grid-template-columns: 1fr;
1849
+ }
1850
+
1851
+ .compare-results-grid {
1852
+ grid-template-columns: 1fr;
1853
+ }
1854
+
1855
+ .report-grid {
1856
+ grid-template-columns: 1fr;
1857
+ }
1858
+
1859
+ .four-col {
1860
+ grid-template-columns: repeat(2, 1fr);
1861
+ }
1862
+
1863
+ .hero-actions {
1864
+ flex-direction: column;
1865
+ }
1866
+
1867
+ .results-actions {
1868
+ flex-direction: column;
1869
+ }
1870
+
1871
+ .history-pred {
1872
+ grid-template-columns: 100px 1fr 40px;
1873
+ }
1874
+ }
1875
+
1876
+ .btn-sm {
1877
+ font-size: 0.78rem;
1878
+ padding: 4px 10px;
1879
+ }
templates/about.html ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>System Specifications & Methodology | ChestXpert</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ letter-spacing: -0.015em;
14
+ }
15
+ </style>
16
+ </head>
17
+
18
+ <body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
19
+ <nav class="bg-white border-b border-slate-200 h-14 flex items-center px-6 justify-between shrink-0">
20
+ <div class="flex items-center gap-2">
21
+ <svg class="text-blue-900 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
23
+ </svg>
24
+ <span class="text-blue-900 font-bold tracking-tight text-lg">ChestXpert</span>
25
+ </div>
26
+ <div class="hidden md:flex items-center gap-6 text-sm text-slate-600 font-medium">
27
+ <a href="/" class="hover:text-slate-900 transition-colors">Home</a>
28
+ <a href="/analyze" class="hover:text-slate-900 transition-colors">Analyze</a>
29
+ <a href="/compare" class="hover:text-slate-900 transition-colors">Compare</a>
30
+ <a href="/history" class="hover:text-slate-900 transition-colors">History</a>
31
+ <a href="/about" class="text-slate-900 font-semibold transition-colors">About</a>
32
+ </div>
33
+ <div class="flex items-center gap-4 text-sm font-medium">
34
+ <div
35
+ class="flex items-center gap-2 px-2.5 py-1 rounded-sm bg-slate-50 border border-slate-200 text-slate-700 text-[10px] uppercase font-bold tracking-wider">
36
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" id="status-dot"></span>
37
+ <span id="status-text">Models Ready</span>
38
+ </div>
39
+ <div id="nav-auth" class="flex items-center gap-2" style="display: none;">
40
+ <a href="/login" class="text-blue-900 hover:underline transition-colors px-2">Login</a>
41
+ </div>
42
+ <div id="nav-profile" class="flex items-center relative" style="display: none;">
43
+ <div id="profile-trigger"
44
+ class="flex items-center gap-2 cursor-pointer border border-slate-200 px-2 py-0.5 rounded-sm bg-slate-50 hover:bg-slate-100">
45
+ <img src="/static/default-avatar.svg" alt="Profile"
46
+ class="w-5 h-5 rounded-full border border-slate-200 bg-white">
47
+ <span id="nav-user-name" class="text-slate-700 text-xs font-bold">User</span>
48
+ </div>
49
+ <div id="profile-dropdown"
50
+ class="absolute top-8 right-0 bg-white border border-slate-200 rounded-sm shadow-sm min-w-[200px] z-50 text-left flex flex-col"
51
+ style="display:none;">
52
+ <div class="p-3 border-b border-slate-200 bg-slate-50">
53
+ <strong id="dropdown-name" class="block text-slate-800 text-sm">User</strong>
54
+ <span id="dropdown-email"
55
+ class="text-slate-500 text-[10px] uppercase tracking-wider block mt-0.5 truncate">user@example.com</span>
56
+ </div>
57
+ <div class="p-1">
58
+ <a href="/history"
59
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">My
60
+ Analyses</a>
61
+ <a href="#"
62
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">Account
63
+ Settings</a>
64
+ <div class="h-px bg-slate-200 my-1"></div>
65
+ <button id="logout-btn"
66
+ class="w-full text-left px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-sm">Sign
67
+ Out</button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </nav>
73
+
74
+ <div class="bg-white border-b border-slate-200 py-4 px-6 shrink-0">
75
+ <h1 class="text-xl font-bold text-slate-800 tracking-tight">System Specifications & Methodology</h1>
76
+ <p class="text-sm text-slate-600 mt-1">Technical documentation for the ChestXpert diagnostic ensemble.</p>
77
+ </div>
78
+
79
+ <main class="w-full flex-1 p-6 max-w-7xl mx-auto flex flex-col gap-6">
80
+
81
+ <div class="bg-white border border-slate-300 rounded-sm">
82
+ <div class="px-6 py-3 border-b border-slate-300">
83
+ <h2 class="text-sm font-bold text-slate-800 uppercase tracking-widest">Ensemble Architecture</h2>
84
+ </div>
85
+ <div class="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-slate-200">
86
+ <div class="p-6 flex flex-col h-full">
87
+ <div class="mb-4">
88
+ <h3 class="font-bold text-slate-900 text-base">RAD-DINO</h3>
89
+ <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Vision Transformer
90
+ (ViT-B/14)</span>
91
+ </div>
92
+ <ul class="list-disc pl-4 text-sm text-slate-600 space-y-1.5 flex-1 mb-6">
93
+ <li>Pre-trained by Microsoft on radiology images</li>
94
+ <li>Fine-tuned on CheXpert</li>
95
+ <li>Input resolution: 384x384</li>
96
+ <li>~86M parameters</li>
97
+ </ul>
98
+ <div class="mt-auto">
99
+ <span
100
+ class="inline-block bg-slate-100 border border-slate-200 text-slate-700 px-2 py-1 text-xs font-mono font-bold rounded-sm tracking-wider">AUC:
101
+ 0.8380</span>
102
+ </div>
103
+ </div>
104
+ <div class="p-6 flex flex-col h-full">
105
+ <div class="mb-4">
106
+ <h3 class="font-bold text-slate-900 text-base">DenseNet121</h3>
107
+ <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Convolutional Neural
108
+ Network</span>
109
+ </div>
110
+ <ul class="list-disc pl-4 text-sm text-slate-600 space-y-1.5 flex-1 mb-6">
111
+ <li>Pre-trained on ImageNet</li>
112
+ <li>Fine-tuned on CheXpert</li>
113
+ <li>Input resolution: 320x320</li>
114
+ <li>~7M parameters</li>
115
+ </ul>
116
+ <div class="mt-auto">
117
+ <span
118
+ class="inline-block bg-slate-100 border border-slate-200 text-slate-700 px-2 py-1 text-xs font-mono font-bold rounded-sm tracking-wider">AUC:
119
+ 0.8470</span>
120
+ </div>
121
+ </div>
122
+ <div class="p-6 flex flex-col h-full bg-slate-50/50">
123
+ <div class="mb-4">
124
+ <h3 class="font-bold text-slate-900 text-base">Ensemble Integration</h3>
125
+ <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Weighted Average
126
+ Mechanism</span>
127
+ </div>
128
+ <ul class="list-disc pl-4 text-sm text-slate-600 space-y-1.5 flex-1 mb-6">
129
+ <li>RAD-DINO weight constraint: 60%</li>
130
+ <li>DenseNet121 weight constraint: 40%</li>
131
+ <li>Optimized entirely on validation set distribution</li>
132
+ <li>Leverages complementary inductive biases</li>
133
+ </ul>
134
+ <div class="mt-auto">
135
+ <span
136
+ class="inline-block bg-blue-50 border border-blue-200 text-blue-800 px-2 py-1 text-xs font-mono font-bold rounded-sm tracking-wider">AUC:
137
+ 0.8523</span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <div class="bg-white border border-slate-300 rounded-sm">
144
+ <div class="px-6 py-3 border-b border-slate-300">
145
+ <h2 class="text-sm font-bold text-slate-800 uppercase tracking-widest">Validation Performance</h2>
146
+ </div>
147
+ <div class="overflow-x-auto">
148
+ <table class="w-full text-left text-sm whitespace-nowrap">
149
+ <thead
150
+ class="bg-slate-100 text-slate-700 font-bold uppercase text-xs tracking-widest border-b border-slate-300">
151
+ <tr>
152
+ <th scope="col" class="px-6 py-3">Condition</th>
153
+ <th scope="col" class="px-6 py-3">RAD-DINO</th>
154
+ <th scope="col" class="px-6 py-3">DenseNet121</th>
155
+ <th scope="col" class="px-6 py-3 text-slate-900">Ensemble</th>
156
+ <th scope="col" class="px-6 py-3">Rating</th>
157
+ </tr>
158
+ </thead>
159
+ <tbody class="text-slate-700 font-medium">
160
+ <tr class="bg-white border-b border-slate-100">
161
+ <td class="px-6 py-3 font-bold text-slate-900">Pleural Effusion</td>
162
+ <td class="px-6 py-3 font-mono">0.892</td>
163
+ <td class="px-6 py-3 font-mono">0.901</td>
164
+ <td class="px-6 py-3 font-mono font-bold text-slate-900">0.908</td>
165
+ <td class="px-6 py-3"><span
166
+ class="inline-block bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-sm">Excellent</span>
167
+ </td>
168
+ </tr>
169
+ <tr class="bg-slate-50 border-b border-slate-100">
170
+ <td class="px-6 py-3 font-bold text-slate-900">Cardiomegaly</td>
171
+ <td class="px-6 py-3 font-mono">0.878</td>
172
+ <td class="px-6 py-3 font-mono">0.885</td>
173
+ <td class="px-6 py-3 font-mono font-bold text-slate-900">0.892</td>
174
+ <td class="px-6 py-3"><span
175
+ class="inline-block bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-sm">Excellent</span>
176
+ </td>
177
+ </tr>
178
+ <tr class="bg-white border-b border-slate-100">
179
+ <td class="px-6 py-3 font-bold text-slate-900">Edema</td>
180
+ <td class="px-6 py-3 font-mono">0.810</td>
181
+ <td class="px-6 py-3 font-mono">0.840</td>
182
+ <td class="px-6 py-3 font-mono font-bold text-slate-900">0.845</td>
183
+ <td class="px-6 py-3"><span
184
+ class="inline-block bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-sm">Excellent</span>
185
+ </td>
186
+ </tr>
187
+ <tr class="bg-slate-50 border-b border-slate-100">
188
+ <td class="px-6 py-3 font-bold text-slate-900">Atelectasis</td>
189
+ <td class="px-6 py-3 font-mono">0.805</td>
190
+ <td class="px-6 py-3 font-mono">0.810</td>
191
+ <td class="px-6 py-3 font-mono font-bold text-slate-900">0.815</td>
192
+ <td class="px-6 py-3"><span
193
+ class="inline-block bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-sm">Excellent</span>
194
+ </td>
195
+ </tr>
196
+ <tr class="bg-white border-b border-slate-200">
197
+ <td class="px-6 py-3 font-bold text-slate-900">Consolidation</td>
198
+ <td class="px-6 py-3 font-mono">0.805</td>
199
+ <td class="px-6 py-3 font-mono">0.799</td>
200
+ <td class="px-6 py-3 font-mono font-bold text-slate-900">0.801</td>
201
+ <td class="px-6 py-3"><span
202
+ class="inline-block bg-green-50 text-green-700 border border-green-200 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded-sm">Excellent</span>
203
+ </td>
204
+ </tr>
205
+ <tr class="bg-slate-100/80">
206
+ <td class="px-6 py-3 font-bold text-slate-900">Mean AUC</td>
207
+ <td class="px-6 py-3 font-mono font-bold">0.838</td>
208
+ <td class="px-6 py-3 font-mono font-bold">0.847</td>
209
+ <td class="px-6 py-3 font-mono font-bold text-slate-900 text-base">0.8523</td>
210
+ <td class="px-6 py-3"></td>
211
+ </tr>
212
+ </tbody>
213
+ </table>
214
+ </div>
215
+ </div>
216
+
217
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
218
+
219
+ <div class="bg-white border border-slate-300 rounded-sm">
220
+ <div class="px-6 py-3 border-b border-slate-300">
221
+ <h2 class="text-sm font-bold text-slate-800 uppercase tracking-widest">Training & Interpretability
222
+ </h2>
223
+ </div>
224
+ <div class="p-6 flex flex-col gap-3">
225
+ <div class="flex items-center gap-4 py-1 border-b border-slate-100">
226
+ <span
227
+ class="w-24 shrink-0 text-xs font-bold text-slate-500 uppercase tracking-widest">Approach</span>
228
+ <span class="text-sm text-slate-800 font-medium">Progressive unfreezing</span>
229
+ </div>
230
+ <div class="flex items-center gap-4 py-1 border-b border-slate-100">
231
+ <span
232
+ class="w-24 shrink-0 text-xs font-bold text-slate-500 uppercase tracking-widest">Optimizer</span>
233
+ <span class="text-sm text-slate-800 font-medium">AdamW</span>
234
+ </div>
235
+ <div class="flex items-center gap-4 py-1 border-b border-slate-100">
236
+ <span
237
+ class="w-24 shrink-0 text-xs font-bold text-slate-500 uppercase tracking-widest">Loss</span>
238
+ <span class="text-sm text-slate-800 font-medium">BCE with class weights</span>
239
+ </div>
240
+ <div class="flex items-center gap-4 py-1 border-b border-slate-100">
241
+ <span
242
+ class="w-24 shrink-0 text-xs font-bold text-slate-500 uppercase tracking-widest">Method</span>
243
+ <span class="text-sm text-slate-800 font-medium">Grad-CAM (Gradient-weighted Class Activation
244
+ Mapping)</span>
245
+ </div>
246
+ <div class="flex items-center gap-4 py-1">
247
+ <span class="w-24 shrink-0 text-xs font-bold text-slate-500 uppercase tracking-widest">Target
248
+ Layer</span>
249
+ <span class="text-sm text-slate-800 font-medium">DenseNet DenseBlock4 spatial
250
+ convolutions</span>
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="bg-white border border-slate-300 rounded-sm">
256
+ <div class="px-6 py-3 border-b border-slate-300">
257
+ <h2 class="text-sm font-bold text-slate-800 uppercase tracking-widest">Dataset Specifications</h2>
258
+ </div>
259
+ <div class="p-6">
260
+ <h3 class="font-bold text-slate-900 mb-4 whitespace-nowrap overflow-hidden text-ellipsis">CheXpert
261
+ v1.0 Small <span class="text-slate-500 font-normal ml-2">Stanford ML Group</span></h3>
262
+ <div
263
+ class="bg-slate-50 border border-slate-200 rounded-sm p-4 text-sm font-mono text-slate-700 leading-relaxed shadow-inner">
264
+ Total Radiographs: 224,316<br>
265
+ Unique Patients: 65,240<br>
266
+ <div class="h-px bg-slate-200 my-2"></div>
267
+ Training Split: 153,000<br>
268
+ Validation Set: 18,900<br>
269
+ Target Labels: 5 (Diagnostic)
270
+ </div>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <div class="bg-amber-50 border border-amber-200 rounded-sm p-4 flex items-start gap-3 mt-4">
276
+ <svg class="w-5 h-5 text-amber-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
278
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
279
+ </path>
280
+ </svg>
281
+ <div>
282
+ <span class="font-bold text-amber-800 text-sm tracking-wide">REGULATORY NOTICE:</span>
283
+ <span class="text-sm text-amber-800 ml-1">ChestXpert is for educational and research use only. Not
284
+ intended or cleared for clinical diagnostics. Consult a qualified radiologist or physician for
285
+ medical decisions.</span>
286
+ </div>
287
+ </div>
288
+
289
+ </main>
290
+
291
+ <footer class="w-full bg-slate-50 py-6 px-4 shrink-0 text-center border-t border-slate-200">
292
+ <p class="text-[10px] uppercase tracking-widest text-slate-400 font-bold">&copy; 2026 ChestXpert &mdash; For
293
+ Research & Educational Use Only.</p>
294
+ </footer>
295
+
296
+ <script src="/static/app.js"></script>
297
+ </body>
298
+
299
+ </html>
templates/analyze.html ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Analyze | ChestXpert</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ letter-spacing: -0.015em;
15
+ }
16
+ </style>
17
+ </head>
18
+
19
+ <body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
20
+ <nav class="bg-white border-b border-slate-200 h-14 flex items-center px-6 justify-between shrink-0">
21
+ <div class="flex items-center gap-2">
22
+ <svg class="text-blue-900 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
23
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
24
+ </svg>
25
+ <span class="text-blue-900 font-bold tracking-tight text-lg">ChestXpert</span>
26
+ </div>
27
+ <div class="hidden md:flex items-center gap-6 text-sm text-slate-600 font-medium">
28
+ <a href="/" class="hover:text-slate-900 transition-colors">Home</a>
29
+ <a href="/analyze" class="text-slate-900 font-semibold transition-colors">Analyze</a>
30
+ <a href="/compare" class="hover:text-slate-900 transition-colors">Compare</a>
31
+ <a href="/history" class="hover:text-slate-900 transition-colors">History</a>
32
+ <a href="/about" class="hover:text-slate-900 transition-colors">About</a>
33
+ </div>
34
+ <div class="flex items-center gap-4 text-sm font-medium">
35
+ <div
36
+ class="flex items-center gap-2 px-2.5 py-1 rounded-sm bg-slate-50 border border-slate-200 text-slate-700 text-[10px] uppercase font-bold tracking-wider">
37
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" id="status-dot"></span>
38
+ <span id="status-text">Models Ready</span>
39
+ </div>
40
+ <div id="nav-auth" class="flex items-center gap-2" style="display: none;">
41
+ <a href="/login" class="text-blue-900 hover:underline transition-colors px-2">Login</a>
42
+ </div>
43
+ <div id="nav-profile" class="flex items-center relative" style="display: none;">
44
+ <div id="profile-trigger"
45
+ class="flex items-center gap-2 cursor-pointer border border-slate-200 px-2 py-0.5 rounded-sm bg-slate-50 hover:bg-slate-100">
46
+ <img src="/static/default-avatar.svg" alt="Profile"
47
+ class="w-5 h-5 rounded-full border border-slate-200 bg-white">
48
+ <span id="nav-user-name" class="text-slate-700 text-xs font-bold">User</span>
49
+ </div>
50
+ <div id="profile-dropdown"
51
+ class="absolute top-8 right-0 bg-white border border-slate-200 rounded-sm shadow-sm min-w-[200px] z-50 text-left flex flex-col"
52
+ style="display:none;">
53
+ <div class="p-3 border-b border-slate-200 bg-slate-50">
54
+ <strong id="dropdown-name" class="block text-slate-800 text-sm">User</strong>
55
+ <span id="dropdown-email"
56
+ class="text-slate-500 text-[10px] uppercase tracking-wider block mt-0.5 truncate">user@example.com</span>
57
+ </div>
58
+ <div class="p-1">
59
+ <a href="/history"
60
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">My
61
+ Analyses</a>
62
+ <a href="#"
63
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">Account
64
+ Settings</a>
65
+ <div class="h-px bg-slate-200 my-1"></div>
66
+ <button id="logout-btn"
67
+ class="w-full text-left px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-sm">Sign
68
+ Out</button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </nav>
74
+
75
+ <main class="max-w-7xl mx-auto w-full px-4 py-6 flex-1 flex flex-col gap-6" id="upload-section">
76
+ <div class="flex flex-col md:flex-row gap-6 lg:h-[600px]">
77
+ <div class="w-full md:w-[30%] bg-white border border-slate-200 rounded-sm flex flex-col">
78
+ <div class="border-b border-slate-200 p-3 bg-slate-50/50">
79
+ <h2 class="text-xs font-bold uppercase tracking-wider text-slate-500">Image Acquisition</h2>
80
+ </div>
81
+ <div class="p-4 flex-1 flex flex-col gap-4">
82
+ <input type="file" id="file-input" class="hidden" accept=".png,.jpg,.jpeg,.bmp,.dcm,.dicom">
83
+ <div id="upload-zone"
84
+ class="border-2 border-dashed border-slate-300 bg-slate-50 rounded-sm p-6 flex flex-col items-center justify-center text-center cursor-pointer hover:bg-slate-100 transition-colors shrink-0 min-h-[160px]">
85
+ <svg class="w-8 h-8 text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
86
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
87
+ d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
88
+ </path>
89
+ </svg>
90
+ <span class="text-xs font-semibold text-slate-500 leading-relaxed">Drag & Drop Chest
91
+ X-Ray<br>Supports DICOM, PNG, JPG</span>
92
+ </div>
93
+ <div id="preview-card" style="display:none;"
94
+ class="border border-slate-200 rounded-sm overflow-hidden h-40 flex flex-col relative bg-slate-900 shrink-0">
95
+ <img id="preview-img" class="w-full h-full object-contain opacity-50" src="">
96
+ <div class="absolute inset-0 flex flex-col items-center justify-center p-2 text-center">
97
+ <span id="file-info"
98
+ class="text-white text-[10px] font-mono tracking-wide bg-slate-900/80 px-2 py-1 rounded-sm truncate w-full mb-3 border border-slate-700"></span>
99
+ <button id="btn-change"
100
+ class="text-xs bg-white text-slate-900 px-3 py-1.5 rounded-sm font-bold shadow hover:bg-slate-200 uppercase tracking-widest">Change</button>
101
+ </div>
102
+ </div>
103
+ <div class="flex-1 mt-4">
104
+ <h3
105
+ class="text-[10px] font-bold text-slate-400 mb-2 uppercase tracking-widest border-b border-slate-100 pb-2">
106
+ Active Ensemble</h3>
107
+ <div class="flex flex-col gap-2">
108
+ <div
109
+ class="flex items-center justify-between text-xs p-2.5 bg-slate-50 border border-slate-200 rounded-sm text-slate-700">
110
+ <span class="font-bold text-slate-600 tracking-wide font-mono">RAD-DINO</span>
111
+ <span class="w-1.5 h-1.5 rounded-full bg-blue-600"></span>
112
+ </div>
113
+ <div
114
+ class="flex items-center justify-between text-xs p-2.5 bg-slate-50 border border-slate-200 rounded-sm text-slate-700">
115
+ <span class="font-bold text-slate-600 tracking-wide font-mono">DenseNet121</span>
116
+ <span class="w-1.5 h-1.5 rounded-full bg-blue-600"></span>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ <button id="btn-analyze"
121
+ class="w-full bg-blue-900 hover:bg-blue-800 text-white text-sm font-bold py-3.5 rounded-sm transition-colors mt-auto flex justify-center items-center gap-2 uppercase tracking-widest shadow-sm">
122
+ Run Analysis
123
+ </button>
124
+ </div>
125
+ </div>
126
+ <div
127
+ class="w-full md:w-[70%] bg-slate-900 flex flex-col relative overflow-hidden ring-1 ring-slate-300 rounded-sm">
128
+ <div class="h-10 border-b border-slate-700 flex items-center px-4 gap-4 bg-[#0a0f1c] shrink-0">
129
+ <svg class="w-4 h-4 text-slate-500 hover:text-white cursor-pointer" fill="none"
130
+ stroke="currentColor" viewBox="0 0 24 24">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
132
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path>
133
+ </svg>
134
+ <svg class="w-4 h-4 text-slate-500 hover:text-white cursor-pointer" fill="none"
135
+ stroke="currentColor" viewBox="0 0 24 24">
136
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
137
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path>
138
+ </svg>
139
+ <div class="w-px h-4 bg-slate-700 mx-1"></div>
140
+ <svg class="w-4 h-4 text-slate-500 hover:text-white cursor-pointer" fill="none"
141
+ stroke="currentColor" viewBox="0 0 24 24">
142
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
143
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5">
144
+ </path>
145
+ </svg>
146
+ <div class="w-px h-4 bg-slate-700 mx-1"></div>
147
+ <span
148
+ class="text-slate-400 text-[9px] font-bold tracking-widest px-2 py-0.5 border border-slate-600 rounded-sm cursor-pointer hover:text-white hover:border-slate-400">INVERT</span>
149
+ <span
150
+ class="text-slate-400 text-[9px] font-bold tracking-widest px-2 py-0.5 border border-slate-600 rounded-sm cursor-pointer hover:text-white hover:border-slate-400">RESET</span>
151
+ </div>
152
+ <div class="flex-1 flex items-center justify-center p-4 relative" id="viewer-container">
153
+ <p id="viewer-placeholder"
154
+ class="text-slate-500 text-xs tracking-widest font-mono uppercase text-center opacity-50">
155
+ Awaiting image input.<br>Grad-CAM visualization will render here.</p>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
160
+ <div class="bg-white border border-slate-200 rounded-sm p-5 shadow-sm">
161
+ <h3
162
+ class="text-[10px] font-bold uppercase tracking-widest text-slate-400 mb-3 border-b border-slate-100 pb-2">
163
+ Model Performance</h3>
164
+ <div class="flex items-center gap-4 mt-4">
165
+ <div class="text-slate-900 font-bold text-2xl tracking-tight">85%</div>
166
+ <div class="flex flex-col">
167
+ <span class="text-xs font-bold text-slate-700">Val AUC</span>
168
+ <span class="text-[10px] text-slate-500 uppercase tracking-wider">Verified Ensemble</span>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ <div class="bg-white border border-slate-200 rounded-sm p-5 shadow-sm">
173
+ <h3
174
+ class="text-[10px] font-bold uppercase tracking-widest text-slate-400 mb-3 border-b border-slate-100 pb-2">
175
+ Diagnostic Labels</h3>
176
+ <div class="flex flex-wrap gap-2 mt-3">
177
+ <span
178
+ class="px-2 py-1 bg-slate-50 border border-slate-200 rounded-sm text-[10px] uppercase font-bold text-slate-600 tracking-wider">Atelectasis</span>
179
+ <span
180
+ class="px-2 py-1 bg-slate-50 border border-slate-200 rounded-sm text-[10px] uppercase font-bold text-slate-600 tracking-wider">Cardiomegaly</span>
181
+ <span
182
+ class="px-2 py-1 bg-slate-50 border border-slate-200 rounded-sm text-[10px] uppercase font-bold text-slate-600 tracking-wider">Consolidation</span>
183
+ <span
184
+ class="px-2 py-1 bg-slate-50 border border-slate-200 rounded-sm text-[10px] uppercase font-bold text-slate-600 tracking-wider">Edema</span>
185
+ <span
186
+ class="px-2 py-1 bg-slate-50 border border-slate-200 rounded-sm text-[10px] uppercase font-bold text-slate-600 tracking-wider">Pleural
187
+ Effusion</span>
188
+ </div>
189
+ </div>
190
+ <div class="bg-white border border-slate-200 rounded-sm p-5 shadow-sm">
191
+ <h3
192
+ class="text-[10px] font-bold uppercase tracking-widest text-slate-400 mb-2 border-b border-slate-100 pb-2">
193
+ System Notice</h3>
194
+ <p class="text-[11px] text-slate-600 leading-relaxed font-medium mt-3 uppercase tracking-wide">For
195
+ Research & Educational Use Only. All image processing is executed locally to ensure complete data
196
+ privacy.</p>
197
+ </div>
198
+ </div>
199
+ </main>
200
+ <section class="samples-section max-w-7xl mx-auto w-full px-4 mb-6" id="samples-section">
201
+ <div class="bg-white border border-slate-200 rounded-sm p-4 shadow-sm">
202
+ <h3
203
+ class="text-[10px] font-bold uppercase tracking-widest text-slate-400 border-b border-slate-100 pb-2 mb-3">
204
+ Sample Library</h3>
205
+ <div id="samples-grid" class="flex gap-4 overflow-x-auto pb-2"></div>
206
+ </div>
207
+ <style>
208
+ .sample-card {
209
+ border: 1px solid #e2e8f0;
210
+ border-radius: 2px;
211
+ cursor: pointer;
212
+ padding: 0.2rem;
213
+ min-width: 100px;
214
+ background: #f8fafc;
215
+ transition: border-color 0.2s;
216
+ }
217
+
218
+ .sample-card img {
219
+ width: 100px;
220
+ height: 100px;
221
+ object-fit: cover;
222
+ }
223
+
224
+ .sample-card p {
225
+ font-size: 0.6rem;
226
+ text-align: center;
227
+ margin-top: 0.3rem;
228
+ color: #475569;
229
+ font-weight: 600;
230
+ text-transform: uppercase;
231
+ letter-spacing: 0.05em;
232
+ text-overflow: ellipsis;
233
+ overflow: hidden;
234
+ white-space: nowrap;
235
+ }
236
+
237
+ .sample-card:hover {
238
+ border-color: #64748b;
239
+ }
240
+ </style>
241
+ </section>
242
+
243
+ <main class="max-w-7xl mx-auto w-full px-4 py-12 flex-1 flex-col justify-center items-center" id="loading-section"
244
+ style="display:none;">
245
+ <div class="w-full max-w-md bg-white border border-slate-200 p-8 rounded-sm text-center shadow-sm mx-auto">
246
+ <h2 class="text-sm font-bold text-slate-800 mb-1 tracking-widest uppercase">Processing Image</h2>
247
+ <p class="text-xs text-slate-500 mb-6 font-medium">Running ensemble inference...</p>
248
+ <div class="w-full bg-slate-100 h-1.5 rounded-sm overflow-hidden mb-6 border border-slate-200">
249
+ <div class="bg-blue-900 h-full w-0 transition-all duration-300" id="loading-bar-fill"></div>
250
+ </div>
251
+ <div class="flex flex-col gap-3 text-left text-[10px] font-bold uppercase tracking-wider text-slate-400">
252
+ <div id="step-1" class="flex gap-3 items-center"><span
253
+ class="w-2.5 h-2.5 rounded-full border border-slate-300"></span> Preprocessing</div>
254
+ <div id="step-2" class="flex gap-3 items-center"><span
255
+ class="w-2.5 h-2.5 rounded-full border border-slate-300"></span> RAD-DINO Analysis</div>
256
+ <div id="step-3" class="flex gap-3 items-center"><span
257
+ class="w-2.5 h-2.5 rounded-full border border-slate-300"></span> DenseNet Feature Extraction
258
+ </div>
259
+ <div id="step-4" class="flex gap-3 items-center"><span
260
+ class="w-2.5 h-2.5 rounded-full border border-slate-300"></span> Grad-CAM Generation</div>
261
+ <div id="step-5" class="flex gap-3 items-center"><span
262
+ class="w-2.5 h-2.5 rounded-full border border-slate-300"></span> Finalizing Report</div>
263
+ </div>
264
+ <style>
265
+ .active {
266
+ color: #1e293b;
267
+ }
268
+
269
+ .active span {
270
+ border-color: #1e3a8a;
271
+ border-left-color: transparent;
272
+ border-bottom-color: transparent;
273
+ animation: spin 1s linear infinite;
274
+ }
275
+
276
+ .done {
277
+ color: #10b981;
278
+ }
279
+
280
+ .done span {
281
+ background: #10b981;
282
+ border-color: #10b981;
283
+ }
284
+
285
+ @keyframes spin {
286
+ 100% {
287
+ transform: rotate(360deg);
288
+ }
289
+ }
290
+ </style>
291
+ </div>
292
+ </main>
293
+
294
+ <main class="max-w-7xl mx-auto w-full px-4 py-6 flex-1 flex flex-col gap-6" id="results-section"
295
+ style="display:none;">
296
+ <div class="flex justify-between items-center bg-white border border-slate-200 p-3 rounded-sm shadow-sm">
297
+ <div>
298
+ <h2 class="text-sm font-bold text-slate-800 tracking-widest uppercase" id="summary-title">Analysis
299
+ Complete</h2>
300
+ <p class="text-xs text-slate-500 font-medium tracking-wide mt-1" id="summary-text">Review the findings
301
+ below.</p>
302
+ </div>
303
+ <div class="flex gap-2">
304
+ <button id="btn-pdf"
305
+ class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest bg-white border border-slate-300 text-slate-700 hover:bg-slate-50 rounded-sm">PDF
306
+ Report</button>
307
+ <button id="btn-export-json"
308
+ class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest bg-white border border-slate-300 text-slate-700 hover:bg-slate-50 rounded-sm">JSON</button>
309
+ <a id="btn-report-link" href="#" target="_blank"
310
+ class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-widest bg-white border border-slate-300 text-slate-700 hover:bg-slate-50 rounded-sm flex items-center justify-center">Printable
311
+ Report</a>
312
+ <button id="btn-new"
313
+ class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest bg-slate-800 border border-slate-800 text-white hover:bg-slate-700 rounded-sm ml-2">New
314
+ Analysis</button>
315
+ </div>
316
+ </div>
317
+ <div class="flex flex-col md:flex-row gap-6">
318
+ <div class="w-full md:w-[30%] bg-white border border-slate-200 rounded-sm flex flex-col lg:max-h-[600px]">
319
+ <div class="border-b border-slate-200 p-3 bg-slate-50/50">
320
+ <h2 class="text-[10px] font-bold uppercase tracking-widest text-slate-500">Predicted Findings</h2>
321
+ </div>
322
+ <div class="p-4 flex flex-col gap-5 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-300"
323
+ id="predictions-list">
324
+ </div>
325
+ <div class="border-t border-slate-200 p-3 mt-auto flex flex-col gap-2 shrink-0 bg-slate-50">
326
+ <h3 class="text-[10px] font-bold uppercase tracking-widest text-slate-400 mb-1">Models Extracted
327
+ </h3>
328
+ <div id="model-tags" class="flex gap-1.5 flex-wrap"></div>
329
+ </div>
330
+ <style>
331
+ .risk-tag {
332
+ font-size: 0.55rem;
333
+ padding: 0.15rem 0.3rem;
334
+ border-radius: 2px;
335
+ font-weight: 800;
336
+ letter-spacing: 0.05em;
337
+ text-transform: uppercase;
338
+ border: 1px solid currentColor;
339
+ }
340
+
341
+ .risk-tag.high {
342
+ color: #dc2626;
343
+ background: #fef2f2;
344
+ }
345
+
346
+ .risk-tag.medium {
347
+ color: #d97706;
348
+ background: #fffbeb;
349
+ }
350
+
351
+ .risk-tag.low {
352
+ color: #059669;
353
+ background: #ecfdf5;
354
+ }
355
+
356
+ .pred-pct {
357
+ font-size: 0.8rem;
358
+ font-weight: 800;
359
+ }
360
+
361
+ .pred-pct.high {
362
+ color: #dc2626;
363
+ }
364
+
365
+ .pred-pct.medium {
366
+ color: #d97706;
367
+ }
368
+
369
+ .pred-pct.low {
370
+ color: #059669;
371
+ }
372
+
373
+ .pred-bar-bg {
374
+ width: 100%;
375
+ height: 4px;
376
+ background: #f1f5f9;
377
+ border-radius: 0px;
378
+ overflow: hidden;
379
+ margin: 0.4rem 0;
380
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
381
+ }
382
+
383
+ .pred-bar {
384
+ height: 100%;
385
+ transition: width 0.8s ease-out;
386
+ }
387
+
388
+ .pred-bar.high {
389
+ background: #dc2626;
390
+ }
391
+
392
+ .pred-bar.medium {
393
+ background: #f59e0b;
394
+ }
395
+
396
+ .pred-bar.low {
397
+ background: #10b981;
398
+ }
399
+
400
+ .pred-name {
401
+ font-size: 0.75rem;
402
+ font-weight: 700;
403
+ color: #1e293b;
404
+ text-transform: uppercase;
405
+ letter-spacing: 0.05em;
406
+ }
407
+
408
+ .pred-desc {
409
+ font-size: 0.65rem;
410
+ color: #64748b;
411
+ margin-top: 0.2rem;
412
+ font-weight: 500;
413
+ }
414
+
415
+ .pred-models {
416
+ font-size: 0.6rem;
417
+ color: #94a3b8;
418
+ display: flex;
419
+ justify-content: space-between;
420
+ margin-top: 0.4rem;
421
+ font-family: monospace;
422
+ font-weight: 600;
423
+ }
424
+
425
+ .model-tag.on {
426
+ background: #fff;
427
+ border: 1px solid #cbd5e1;
428
+ font-size: 0.6rem;
429
+ padding: 0.1rem 0.3rem;
430
+ font-weight: 700;
431
+ border-radius: 2px;
432
+ color: #334155;
433
+ text-transform: uppercase;
434
+ letter-spacing: 0.05em;
435
+ }
436
+
437
+ .scrollbar-thin::-webkit-scrollbar {
438
+ width: 4px;
439
+ }
440
+
441
+ .scrollbar-thin::-webkit-scrollbar-track {
442
+ background: transparent;
443
+ }
444
+
445
+ .scrollbar-thin::-webkit-scrollbar-thumb {
446
+ background-color: #cbd5e1;
447
+ border-radius: 20px;
448
+ }
449
+ </style>
450
+ </div>
451
+ <div
452
+ class="w-full md:w-[70%] bg-slate-900 flex flex-col relative h-[600px] overflow-hidden rounded-sm ring-1 ring-slate-300">
453
+ <div
454
+ class="h-10 border-b border-slate-700 flex justify-between items-center px-4 bg-[#0a0f1c] shrink-0">
455
+ <div class="flex items-center gap-1">
456
+ <button
457
+ class="card-tab active px-3 py-1 text-[9px] font-bold tracking-widest border border-slate-600 text-white bg-slate-700 rounded-sm uppercase"
458
+ data-tab="heatmap">Heatmap</button>
459
+ <button
460
+ class="card-tab px-3 py-1 text-[9px] font-bold tracking-widest border border-slate-600 text-slate-400 hover:text-white rounded-sm uppercase"
461
+ data-tab="original">Original</button>
462
+ </div>
463
+ <div class="flex items-center gap-2" id="heatmap-selector">
464
+ <span class="text-[9px] uppercase font-bold text-slate-500 tracking-widest">Target:</span>
465
+ <div id="heatmap-pills" class="flex gap-1.5"></div>
466
+ <style>
467
+ .pill {
468
+ background: #1e293b;
469
+ border: 1px solid #334155;
470
+ color: #94a3b8;
471
+ font-size: 0.6rem;
472
+ padding: 0.15rem 0.4rem;
473
+ font-weight: 700;
474
+ cursor: pointer;
475
+ border-radius: 2px;
476
+ text-transform: uppercase;
477
+ letter-spacing: 0.1em;
478
+ }
479
+
480
+ .pill:hover {
481
+ background: #334155;
482
+ color: white;
483
+ }
484
+
485
+ .pill.active {
486
+ background: #1e3a8a;
487
+ border-color: #3b82f6;
488
+ color: white;
489
+ }
490
+
491
+ .card-tab.active {
492
+ background: #1e293b;
493
+ color: white;
494
+ border-color: #475569;
495
+ }
496
+ </style>
497
+ </div>
498
+ </div>
499
+ <div class="flex-1 flex items-center justify-center p-4 relative h-full" id="viewer-container">
500
+ <div class="absolute top-3 left-4 text-[10px] text-white/70 font-mono z-10 opacity-70">ID: CX-99281
501
+ | Sex: M | Age: 45</div>
502
+ <div class="absolute top-3 right-4 text-[10px] text-white/70 font-mono z-10 text-right opacity-70">
503
+ View: AP | Study: Thorax</div>
504
+ <img id="result-heatmap" src="" class="max-w-full max-h-full object-contain">
505
+ <img id="result-original" src="" class="max-w-full max-h-full object-contain" style="display:none;">
506
+ </div>
507
+ </div>
508
+ </div>
509
+ </main>
510
+ <script src="/static/app.js"></script>
511
+ </body>
512
+
513
+ </html>
templates/compare.html ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Compare | ChestXpert</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ letter-spacing: -0.015em;
14
+ }
15
+ </style>
16
+ </head>
17
+
18
+ <body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
19
+ <nav class="bg-white border-b border-slate-200 h-14 flex items-center px-6 justify-between shrink-0">
20
+ <div class="flex items-center gap-2">
21
+ <svg class="text-blue-900 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
23
+ </svg>
24
+ <span class="text-blue-900 font-bold tracking-tight text-lg">ChestXpert</span>
25
+ </div>
26
+ <div class="hidden md:flex items-center gap-6 text-sm text-slate-600 font-medium">
27
+ <a href="/" class="hover:text-slate-900 transition-colors">Home</a>
28
+ <a href="/analyze" class="hover:text-slate-900 transition-colors">Analyze</a>
29
+ <a href="/compare" class="text-slate-900 font-semibold transition-colors">Compare</a>
30
+ <a href="/history" class="hover:text-slate-900 transition-colors">History</a>
31
+ <a href="/about" class="hover:text-slate-900 transition-colors">About</a>
32
+ </div>
33
+ <div class="flex items-center gap-4 text-sm font-medium">
34
+ <div
35
+ class="flex items-center gap-2 px-2.5 py-1 rounded-sm bg-slate-50 border border-slate-200 text-slate-700 text-[10px] uppercase font-bold tracking-wider">
36
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" id="status-dot"></span>
37
+ <span id="status-text">Models Ready</span>
38
+ </div>
39
+ <div id="nav-auth" class="flex items-center gap-2" style="display: none;">
40
+ <a href="/login" class="text-blue-900 hover:underline transition-colors px-2">Login</a>
41
+ </div>
42
+ <div id="nav-profile" class="flex items-center relative" style="display: none;">
43
+ <div id="profile-trigger"
44
+ class="flex items-center gap-2 cursor-pointer border border-slate-200 px-2 py-0.5 rounded-sm bg-slate-50 hover:bg-slate-100">
45
+ <img src="/static/default-avatar.svg" alt="Profile"
46
+ class="w-5 h-5 rounded-full border border-slate-200 bg-white">
47
+ <span id="nav-user-name" class="text-slate-700 text-xs font-bold">User</span>
48
+ </div>
49
+ <div id="profile-dropdown"
50
+ class="absolute top-8 right-0 bg-white border border-slate-200 rounded-sm shadow-sm min-w-[200px] z-50 text-left flex flex-col"
51
+ style="display:none;">
52
+ <div class="p-3 border-b border-slate-200 bg-slate-50">
53
+ <strong id="dropdown-name" class="block text-slate-800 text-sm">User</strong>
54
+ <span id="dropdown-email"
55
+ class="text-slate-500 text-[10px] uppercase tracking-wider block mt-0.5 truncate">user@example.com</span>
56
+ </div>
57
+ <div class="p-1">
58
+ <a href="/history"
59
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">My
60
+ Analyses</a>
61
+ <a href="#"
62
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">Account
63
+ Settings</a>
64
+ <div class="h-px bg-slate-200 my-1"></div>
65
+ <button id="logout-btn"
66
+ class="w-full text-left px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-sm">Sign
67
+ Out</button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </nav>
73
+
74
+ <div class="bg-white border-b border-slate-200 py-3 px-6 flex justify-between items-center w-full shrink-0">
75
+ <h1 class="text-lg font-bold text-slate-800 tracking-tight">Comparative Analysis: Baseline vs Follow-up</h1>
76
+ <div class="flex items-center gap-4">
77
+ <div class="flex items-center gap-2">
78
+ <span class="text-xs font-bold uppercase tracking-widest text-slate-500">Sync Views</span>
79
+ <button class="w-8 h-4 bg-slate-200 rounded-full relative cursor-pointer outline-none">
80
+ <div class="w-3 h-3 bg-white rounded-full absolute left-0.5 top-0.5 shadow-sm transition-transform">
81
+ </div>
82
+ </button>
83
+ </div>
84
+ <div class="h-4 w-px bg-slate-300"></div>
85
+ <button id="btn-compare-new"
86
+ class="text-[10px] uppercase tracking-widest font-bold text-slate-600 hover:text-slate-900 border border-slate-300 rounded-sm px-4 py-1.5 transition-colors">Clear
87
+ Studies</button>
88
+ <button id="btn-compare"
89
+ class="text-[10px] uppercase tracking-widest font-bold text-white bg-blue-900 hover:bg-blue-800 rounded-sm px-4 py-1.5 transition-colors shadow-sm ml-2">Compute
90
+ Differential</button>
91
+ </div>
92
+ </div>
93
+
94
+ <main class="w-full flex-1 p-6" id="compare-uploads">
95
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 h-full max-w-[1600px] mx-auto">
96
+ <div class="bg-white border border-slate-300 rounded-sm flex flex-col h-full shadow-sm" id="compare-zone-1">
97
+ <div class="grid grid-cols-2 bg-slate-50 border-b border-slate-300 py-2 px-3 gap-y-1">
98
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1">PATIENT ID:
99
+ <span class="text-slate-800">CX-0091</span></p>
100
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1 text-right">DATE:
101
+ <span class="text-slate-800">2026-01-15</span></p>
102
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1">MODALITY: <span
103
+ class="text-slate-800">CR</span></p>
104
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1 text-right">VIEW:
105
+ <span class="text-slate-800">PA</span></p>
106
+ </div>
107
+ <div class="h-8 bg-slate-100 border-b border-slate-300 flex items-center px-2 gap-1 shrink-0">
108
+ <button
109
+ class="w-6 h-6 flex items-center justify-center hover:bg-slate-200 rounded-sm text-slate-500"><svg
110
+ class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
111
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
112
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path>
113
+ </svg></button>
114
+ <button
115
+ class="w-6 h-6 flex items-center justify-center hover:bg-slate-200 rounded-sm text-slate-500"><svg
116
+ class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
117
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
118
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5">
119
+ </path>
120
+ </svg></button>
121
+ <div class="w-px h-4 bg-slate-300 mx-1"></div>
122
+ <button
123
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">INVERT</button>
124
+ <button
125
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">RESET</button>
126
+ <div class="w-px h-4 bg-slate-300 mx-1"></div>
127
+ <button
128
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">GRAD-CAM</button>
129
+ </div>
130
+ <div class="bg-slate-900 aspect-[4/3] flex items-center justify-center relative p-4 group cursor-pointer"
131
+ id="upload-zone-a" onclick="document.getElementById('file-input-a').click()">
132
+ <div
133
+ class="absolute inset-4 border-2 border-dashed border-slate-700 rounded-sm flex flex-col items-center justify-center transition-colors group-hover:border-slate-500 group-hover:bg-slate-800/50">
134
+ <svg class="w-6 h-6 text-slate-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
136
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
137
+ </svg>
138
+ <span class="text-[10px] uppercase font-bold tracking-widest text-slate-500">Drop Study A X-Ray
139
+ Here</span>
140
+ </div>
141
+ <input type="file" id="file-input-a" accept="image/*,.dcm,.dicom" class="hidden">
142
+ </div>
143
+ <div class="bg-slate-900 aspect-[4/3] relative flex items-center justify-center" id="preview-a"
144
+ style="display:none">
145
+ <img id="preview-img-a" src="" class="max-w-full max-h-full object-contain">
146
+ <div
147
+ class="absolute inset-0 bg-slate-900/50 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
148
+ <button class="bg-white text-slate-900 text-xs font-bold py-1.5 px-4 rounded-sm shadow-sm"
149
+ id="btn-change-a">Change Image</button>
150
+ </div>
151
+ </div>
152
+ <div class="p-4 flex-1 flex flex-col border-t border-slate-300 min-h-0 bg-slate-50"
153
+ id="compare-preds-a-empty">
154
+ <div class="flex flex-col gap-3 opacity-40 grayscale pointer-events-none">
155
+ <div class="flex items-center justify-between text-xs">
156
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Atelectasis</span>
157
+ <div class="flex items-center gap-3 w-3/5">
158
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
159
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
160
+ </div>
161
+ </div>
162
+ <div class="flex items-center justify-between text-xs">
163
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Cardiomegaly</span>
164
+ <div class="flex items-center gap-3 w-3/5">
165
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
166
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
167
+ </div>
168
+ </div>
169
+ <div class="flex items-center justify-between text-xs">
170
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Consolidation</span>
171
+ <div class="flex items-center gap-3 w-3/5">
172
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
173
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
174
+ </div>
175
+ </div>
176
+ <div class="flex items-center justify-between text-xs">
177
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Edema</span>
178
+ <div class="flex items-center gap-3 w-3/5">
179
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
180
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
181
+ </div>
182
+ </div>
183
+ <div class="flex items-center justify-between text-xs">
184
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Pleural Effusion</span>
185
+ <div class="flex items-center gap-3 w-3/5">
186
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
187
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ <div class="p-4 flex-1 flex flex-col border-t border-slate-300 min-h-0 bg-slate-50 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-300"
193
+ id="compare-preds-a" style="display:none"></div>
194
+ </div>
195
+ <div class="bg-white border border-slate-300 rounded-sm flex flex-col h-full shadow-sm" id="compare-zone-2">
196
+ <div class="grid grid-cols-2 bg-slate-50 border-b border-slate-300 py-2 px-3 gap-y-1">
197
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1">PATIENT ID:
198
+ <span class="text-slate-800">CX-0091</span></p>
199
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1 text-right">DATE:
200
+ <span class="text-slate-800">2026-03-01</span></p>
201
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1">MODALITY: <span
202
+ class="text-slate-800">CR</span></p>
203
+ <p class="text-[9px] uppercase tracking-widest text-slate-500 font-bold col-span-1 text-right">VIEW:
204
+ <span class="text-slate-800">PA</span></p>
205
+ </div>
206
+ <div class="h-8 bg-slate-100 border-b border-slate-300 flex items-center px-2 gap-1 shrink-0">
207
+ <button
208
+ class="w-6 h-6 flex items-center justify-center hover:bg-slate-200 rounded-sm text-slate-500"><svg
209
+ class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
210
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
211
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path>
212
+ </svg></button>
213
+ <button
214
+ class="w-6 h-6 flex items-center justify-center hover:bg-slate-200 rounded-sm text-slate-500"><svg
215
+ class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
216
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
217
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5">
218
+ </path>
219
+ </svg></button>
220
+ <div class="w-px h-4 bg-slate-300 mx-1"></div>
221
+ <button
222
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">INVERT</button>
223
+ <button
224
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">RESET</button>
225
+ <div class="w-px h-4 bg-slate-300 mx-1"></div>
226
+ <button
227
+ class="text-[9px] uppercase tracking-widest font-bold text-slate-500 hover:text-slate-800 px-1">GRAD-CAM</button>
228
+ </div>
229
+ <div class="bg-slate-900 aspect-[4/3] flex items-center justify-center relative p-4 group cursor-pointer"
230
+ id="upload-zone-b" onclick="document.getElementById('file-input-b').click()">
231
+ <div
232
+ class="absolute inset-4 border-2 border-dashed border-slate-700 rounded-sm flex flex-col items-center justify-center transition-colors group-hover:border-slate-500 group-hover:bg-slate-800/50">
233
+ <svg class="w-6 h-6 text-slate-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
234
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
235
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
236
+ </svg>
237
+ <span class="text-[10px] uppercase font-bold tracking-widest text-slate-500">Drop Study B X-Ray
238
+ Here</span>
239
+ </div>
240
+ <input type="file" id="file-input-b" accept="image/*,.dcm,.dicom" class="hidden">
241
+ </div>
242
+ <div class="bg-slate-900 aspect-[4/3] relative flex items-center justify-center" id="preview-b"
243
+ style="display:none">
244
+ <img id="preview-img-b" src="" class="max-w-full max-h-full object-contain">
245
+ <div
246
+ class="absolute inset-0 bg-slate-900/50 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
247
+ <button class="bg-white text-slate-900 text-xs font-bold py-1.5 px-4 rounded-sm shadow-sm"
248
+ id="btn-change-b">Change Image</button>
249
+ </div>
250
+ </div>
251
+ <div class="p-4 flex-1 flex flex-col border-t border-slate-300 min-h-0 bg-slate-50"
252
+ id="compare-preds-b-empty">
253
+ <div class="flex flex-col gap-3 opacity-40 grayscale pointer-events-none">
254
+ <div class="flex items-center justify-between text-xs">
255
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Atelectasis</span>
256
+ <div class="flex items-center gap-3 w-3/5">
257
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
258
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
259
+ </div>
260
+ </div>
261
+ <div class="flex items-center justify-between text-xs">
262
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Cardiomegaly</span>
263
+ <div class="flex items-center gap-3 w-3/5">
264
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
265
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
266
+ </div>
267
+ </div>
268
+ <div class="flex items-center justify-between text-xs">
269
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Consolidation</span>
270
+ <div class="flex items-center gap-3 w-3/5">
271
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
272
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
273
+ </div>
274
+ </div>
275
+ <div class="flex items-center justify-between text-xs">
276
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Edema</span>
277
+ <div class="flex items-center gap-3 w-3/5">
278
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
279
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
280
+ </div>
281
+ </div>
282
+ <div class="flex items-center justify-between text-xs">
283
+ <span class="font-bold text-slate-700 uppercase tracking-wide">Pleural Effusion</span>
284
+ <div class="flex items-center gap-3 w-3/5">
285
+ <div class="h-1.5 rounded-none bg-slate-200 w-full overflow-hidden"></div>
286
+ <span class="text-[10px] font-bold text-slate-500 w-8 text-right">--%</span>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ <div class="p-4 flex-1 flex flex-col border-t border-slate-300 min-h-0 bg-slate-50 overflow-y-auto scrollbar-thin scrollbar-thumb-slate-300"
292
+ id="compare-preds-b" style="display:none"></div>
293
+ </div>
294
+ </div>
295
+ </main>
296
+
297
+ <section class="max-w-[1600px] mx-auto w-full px-6 pb-6">
298
+ <div class="bg-white border text-center border-slate-300 rounded-sm shadow-sm pt-4 pb-5 px-6"
299
+ id="compare-results">
300
+ <h2
301
+ class="text-sm font-bold uppercase tracking-wider text-slate-500 mb-6 text-left border-b border-slate-100 pb-2">
302
+ Automated Differential Insights</h2>
303
+ <div class="grid grid-cols-1 sm:grid-cols-5 gap-3" id="compare-diff">
304
+ <div
305
+ class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
306
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Atelectasis</span>
307
+ <span class="text-lg font-bold text-slate-400">---</span>
308
+ </div>
309
+ <div
310
+ class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
311
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Cardiomegaly</span>
312
+ <span class="text-lg font-bold text-slate-400">---</span>
313
+ </div>
314
+ <div
315
+ class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
316
+ <span
317
+ class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Consolidation</span>
318
+ <span class="text-lg font-bold text-slate-400">---</span>
319
+ </div>
320
+ <div
321
+ class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
322
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Edema</span>
323
+ <span class="text-lg font-bold text-slate-400">---</span>
324
+ </div>
325
+ <div
326
+ class="bg-slate-100 border border-slate-200 rounded-sm p-3 flex flex-col items-center justify-center h-20">
327
+ <span class="text-[9px] font-bold uppercase tracking-widest text-slate-500 mb-1">Pleural
328
+ Effusion</span>
329
+ <span class="text-lg font-bold text-slate-400">---</span>
330
+ </div>
331
+ </div>
332
+ <p class="text-[9px] uppercase tracking-widest text-slate-400 mt-4 text-right pr-1">Variance computed using
333
+ RAD-DINO and DenseNet121 ensemble probability deltas.</p>
334
+ </div>
335
+ </section>
336
+
337
+ <div id="compare-loading"
338
+ class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-50 flex items-center justify-center"
339
+ style="display:none">
340
+ <div class="bg-white p-8 rounded-sm shadow-xl max-w-sm w-full text-center border border-slate-200">
341
+ <h3 class="text-sm font-bold uppercase tracking-widest text-slate-800 mb-2">Analyzing Studies</h3>
342
+ <p class="text-[10px] text-slate-500 font-bold tracking-wider mb-6">Computing comparative differential
343
+ metrics...</p>
344
+ <div class="w-full h-1.5 bg-slate-100 rounded-none overflow-hidden border border-slate-200">
345
+ <div id="compare-loading-bar" class="h-full bg-blue-900 w-0 transition-all duration-300"></div>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <style>
351
+ .scrollbar-thin::-webkit-scrollbar {
352
+ width: 4px;
353
+ }
354
+
355
+ .scrollbar-thin::-webkit-scrollbar-track {
356
+ background: transparent;
357
+ }
358
+
359
+ .scrollbar-thin::-webkit-scrollbar-thumb {
360
+ background-color: #cbd5e1;
361
+ border-radius: 20px;
362
+ }
363
+ </style>
364
+ <script src="/static/app.js"></script>
365
+ </body>
366
+
367
+ </html>
templates/history.html ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Study Archive | ChestXpert</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ letter-spacing: -0.015em;
14
+ }
15
+ </style>
16
+ </head>
17
+
18
+ <body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
19
+ <nav class="bg-white border-b border-slate-200 h-14 flex items-center px-6 justify-between shrink-0">
20
+ <div class="flex items-center gap-2">
21
+ <svg class="text-blue-900 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
23
+ </svg>
24
+ <span class="text-blue-900 font-bold tracking-tight text-lg">ChestXpert</span>
25
+ </div>
26
+ <div class="hidden md:flex items-center gap-6 text-sm text-slate-600 font-medium">
27
+ <a href="/" class="hover:text-slate-900 transition-colors">Home</a>
28
+ <a href="/analyze" class="hover:text-slate-900 transition-colors">Analyze</a>
29
+ <a href="/compare" class="hover:text-slate-900 transition-colors">Compare</a>
30
+ <a href="/history" class="text-slate-900 font-semibold transition-colors">History</a>
31
+ <a href="/about" class="hover:text-slate-900 transition-colors">About</a>
32
+ </div>
33
+ <div class="flex items-center gap-4 text-sm font-medium">
34
+ <div
35
+ class="flex items-center gap-2 px-2.5 py-1 rounded-sm bg-slate-50 border border-slate-200 text-slate-700 text-[10px] uppercase font-bold tracking-wider">
36
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" id="status-dot"></span>
37
+ <span id="status-text">Models Ready</span>
38
+ </div>
39
+ <div id="nav-auth" class="flex items-center gap-2" style="display: none;">
40
+ <a href="/login" class="text-blue-900 hover:underline transition-colors px-2">Login</a>
41
+ </div>
42
+ <div id="nav-profile" class="flex items-center relative" style="display: none;">
43
+ <div id="profile-trigger"
44
+ class="flex items-center gap-2 cursor-pointer border border-slate-200 px-2 py-0.5 rounded-sm bg-slate-50 hover:bg-slate-100">
45
+ <img src="/static/default-avatar.svg" alt="Profile"
46
+ class="w-5 h-5 rounded-full border border-slate-200 bg-white">
47
+ <span id="nav-user-name" class="text-slate-700 text-xs font-bold">User</span>
48
+ </div>
49
+ <div id="profile-dropdown"
50
+ class="absolute top-8 right-0 bg-white border border-slate-200 rounded-sm shadow-sm min-w-[200px] z-50 text-left flex flex-col"
51
+ style="display:none;">
52
+ <div class="p-3 border-b border-slate-200 bg-slate-50">
53
+ <strong id="dropdown-name" class="block text-slate-800 text-sm">User</strong>
54
+ <span id="dropdown-email"
55
+ class="text-slate-500 text-[10px] uppercase tracking-wider block mt-0.5 truncate">user@example.com</span>
56
+ </div>
57
+ <div class="p-1">
58
+ <a href="/history"
59
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">My
60
+ Analyses</a>
61
+ <a href="#"
62
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">Account
63
+ Settings</a>
64
+ <div class="h-px bg-slate-200 my-1"></div>
65
+ <button id="logout-btn"
66
+ class="w-full text-left px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-sm">Sign
67
+ Out</button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </nav>
73
+
74
+ <div class="bg-white border-b border-slate-200 py-3 px-6 flex justify-between items-center w-full shrink-0">
75
+ <h1 class="text-lg font-bold text-slate-800 tracking-wider uppercase">Study Archive</h1>
76
+ <div class="flex items-center gap-4">
77
+ <div class="relative">
78
+ <svg class="w-4 h-4 text-slate-400 absolute left-2.5 top-1.5" fill="none" stroke="currentColor"
79
+ viewBox="0 0 24 24">
80
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
81
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
82
+ </svg>
83
+ <input type="text" placeholder="Search by ID or Date..."
84
+ class="pl-8 pr-3 py-1.5 text-xs text-slate-800 border border-slate-300 outline-none focus:border-blue-900 rounded-sm w-64 shadow-sm bg-slate-50 focus:bg-white transition-colors">
85
+ </div>
86
+ <button id="btn-clear-history"
87
+ class="text-[10px] uppercase tracking-widest font-bold text-slate-600 hover:text-red-700 hover:bg-red-50 border border-slate-300 hover:border-red-200 rounded-sm px-4 py-1.5 transition-colors bg-white shadow-sm">Clear
88
+ Registry</button>
89
+ </div>
90
+ </div>
91
+
92
+ <main class="w-full flex-1 p-6 max-w-7xl mx-auto flex flex-col gap-6">
93
+ <div class="bg-white border border-slate-200 rounded-sm shadow-sm flex flex-col min-h-[500px]">
94
+ <div class="w-full overflow-x-auto text-left text-sm text-slate-500">
95
+ <table class="w-full min-w-[800px]" id="history-table">
96
+ <thead
97
+ class="text-xs text-slate-700 uppercase bg-slate-100 border-b border-slate-200 font-bold tracking-widest sticky top-0">
98
+ <tr>
99
+ <th scope="col" class="px-6 py-3 whitespace-nowrap">Date & Time</th>
100
+ <th scope="col" class="px-6 py-3 whitespace-nowrap">Study ID</th>
101
+ <th scope="col" class="px-6 py-3 whitespace-nowrap">Primary Finding</th>
102
+ <th scope="col" class="px-6 py-3 whitespace-nowrap">Risk Level</th>
103
+ <th scope="col" class="px-6 py-3 whitespace-nowrap">Model Confidence</th>
104
+ <th scope="col" class="px-6 py-3 whitespace-nowrap text-right">Actions</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody id="history-list">
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+
112
+ <div id="history-empty" class="flex-1 flex flex-col items-center justify-center p-12 text-center"
113
+ style="display:none">
114
+ <div
115
+ class="w-16 h-16 bg-slate-100 border border-slate-200 rounded-full flex items-center justify-center mb-4">
116
+ <svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
117
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
118
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10">
119
+ </path>
120
+ </svg>
121
+ </div>
122
+ <h3 class="text-sm font-bold text-slate-800 uppercase tracking-widest mb-2" id="empty-title">Archive
123
+ Empty</h3>
124
+ <p class="text-xs text-slate-500 font-medium tracking-wide mb-6" id="empty-desc">No clinical studies are
125
+ currently stored in the local registry.</p>
126
+ <div id="empty-action">
127
+ <a href="/analyze"
128
+ class="bg-blue-900 hover:bg-blue-800 text-white text-[10px] font-bold py-2 px-6 rounded-sm uppercase tracking-widest shadow-sm transition-colors border border-blue-900 inline-block">Initialize
129
+ New Study</a>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </main>
134
+
135
+ <footer class="w-full bg-slate-50 py-6 px-4 shrink-0 text-center border-t border-slate-200">
136
+ <p class="text-[10px] uppercase tracking-widest text-slate-400 font-bold">&copy; 2026 ChestXpert &mdash; For
137
+ Research & Educational Use Only.</p>
138
+ </footer>
139
+
140
+ <script src="/static/app.js"></script>
141
+ </body>
142
+
143
+ </html>
templates/index.html ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>ChestXpert | Clinical Radiology Analysis</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ letter-spacing: -0.015em;
14
+ }
15
+ </style>
16
+ </head>
17
+
18
+ <body class="bg-slate-50 text-slate-900 min-h-screen flex flex-col">
19
+ <nav class="bg-white border-b border-slate-200 h-14 flex items-center px-6 justify-between shrink-0">
20
+ <div class="flex items-center gap-2">
21
+ <svg class="text-blue-900 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
23
+ </svg>
24
+ <span class="text-blue-900 font-bold tracking-tight text-lg">ChestXpert</span>
25
+ </div>
26
+ <div class="hidden md:flex items-center gap-6 text-sm text-slate-600 font-medium">
27
+ <a href="/" class="text-slate-900 font-semibold transition-colors">Home</a>
28
+ <a href="/analyze" class="hover:text-slate-900 transition-colors">Analyze</a>
29
+ <a href="/compare" class="hover:text-slate-900 transition-colors">Compare</a>
30
+ <a href="/history" class="hover:text-slate-900 transition-colors">History</a>
31
+ <a href="/about" class="hover:text-slate-900 transition-colors">About</a>
32
+ </div>
33
+ <div class="flex items-center gap-4 text-sm font-medium">
34
+ <div
35
+ class="flex items-center gap-2 px-2.5 py-1 rounded-sm bg-slate-50 border border-slate-200 text-slate-700 text-[10px] uppercase font-bold tracking-wider">
36
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" id="status-dot"></span>
37
+ <span id="status-text">Models Ready</span>
38
+ </div>
39
+ <div id="nav-auth" class="flex items-center gap-2" style="display: none;">
40
+ <a href="/login" class="text-blue-900 hover:underline transition-colors px-2">Login</a>
41
+ </div>
42
+ <div id="nav-profile" class="flex items-center relative" style="display: none;">
43
+ <div id="profile-trigger"
44
+ class="flex items-center gap-2 cursor-pointer border border-slate-200 px-2 py-0.5 rounded-sm bg-slate-50 hover:bg-slate-100">
45
+ <img src="/static/default-avatar.svg" alt="Profile"
46
+ class="w-5 h-5 rounded-full border border-slate-200 bg-white">
47
+ <span id="nav-user-name" class="text-slate-700 text-xs font-bold">User</span>
48
+ </div>
49
+ <div id="profile-dropdown"
50
+ class="absolute top-8 right-0 bg-white border border-slate-200 rounded-sm shadow-sm min-w-[200px] z-50 text-left flex flex-col"
51
+ style="display:none;">
52
+ <div class="p-3 border-b border-slate-200 bg-slate-50">
53
+ <strong id="dropdown-name" class="block text-slate-800 text-sm">User</strong>
54
+ <span id="dropdown-email"
55
+ class="text-slate-500 text-[10px] uppercase tracking-wider block mt-0.5 truncate">user@example.com</span>
56
+ </div>
57
+ <div class="p-1">
58
+ <a href="/history"
59
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">My
60
+ Analyses</a>
61
+ <a href="#"
62
+ class="block px-3 py-2 text-xs font-semibold text-slate-700 hover:bg-slate-100 rounded-sm">Account
63
+ Settings</a>
64
+ <div class="h-px bg-slate-200 my-1"></div>
65
+ <button id="logout-btn"
66
+ class="w-full text-left px-3 py-2 text-xs font-bold text-red-600 hover:bg-red-50 rounded-sm">Sign
67
+ Out</button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </nav>
73
+
74
+ <main class="max-w-7xl mx-auto w-full px-4 py-12 flex-1 flex flex-col gap-16">
75
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
76
+ <div class="flex flex-col items-start text-left">
77
+ <h1 class="text-4xl md:text-5xl font-bold tracking-tight text-slate-900 leading-tight">ChestXpert
78
+ Radiology Analysis</h1>
79
+ <p class="mt-4 text-base text-slate-600 leading-relaxed font-medium">AI-powered thoracic screening
80
+ utilizing an optimized RAD-DINO and DenseNet121 ensemble architecture.</p>
81
+ <div class="mt-6 mb-8 px-3 py-2 bg-white border border-slate-200 rounded-sm inline-flex">
82
+ <p class="text-[11px] font-bold text-slate-700 uppercase tracking-widest">85% Mean AUC <span
83
+ class="text-slate-300 mx-2">|</span> 5 Conditions <span class="text-slate-300 mx-2">|</span>
84
+ Grad-CAM Visualization</p>
85
+ </div>
86
+ <div class="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
87
+ <a href="/analyze"
88
+ class="bg-blue-900 hover:bg-blue-800 text-white text-xs font-bold py-3 px-6 rounded-sm uppercase tracking-widest text-center shadow-sm transition-colors border border-blue-900">Launch
89
+ Analysis Workspace</a>
90
+ <a href="/compare"
91
+ class="bg-white hover:bg-slate-50 text-slate-900 text-xs font-bold py-3 px-6 rounded-sm uppercase tracking-widest text-center shadow-sm transition-colors border border-slate-200">Compare
92
+ Studies</a>
93
+ </div>
94
+ </div>
95
+ <div
96
+ class="bg-slate-900 rounded-sm border-slate-700 border shadow-md h-64 md:h-96 w-full flex items-center justify-center relative overflow-hidden ring-1 ring-slate-800">
97
+ <div
98
+ class="absolute top-0 w-full h-8 bg-[#0a0f1c] border-b border-slate-700 flex items-center px-4 gap-2">
99
+ <div class="w-2 h-2 rounded-sm bg-slate-600"></div>
100
+ <div class="w-2 h-2 rounded-sm bg-slate-600"></div>
101
+ </div>
102
+ <div
103
+ class="absolute inset-0 top-8 bg-slate-900 opacity-50 bg-[radial-gradient(#1e293b_1px,transparent_1px)] [background-size:16px_16px]">
104
+ </div>
105
+ <span class="text-slate-600 text-[10px] font-mono tracking-widest uppercase z-10">[Dashboard Preview
106
+ Mockup]</span>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="flex flex-col">
111
+ <h2 class="text-xs font-bold uppercase tracking-wider text-slate-500 border-b border-slate-200 pb-3 mb-6">
112
+ System Capabilities</h2>
113
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
114
+ <div class="bg-white border border-slate-200 rounded-sm p-6 shadow-sm flex flex-col gap-2">
115
+ <div
116
+ class="w-8 h-8 rounded-sm bg-slate-50 border border-slate-100 flex items-center justify-center mb-2">
117
+ <svg class="w-4 h-4 text-blue-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
118
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
119
+ d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2">
120
+ </path>
121
+ </svg>
122
+ </div>
123
+ <h3 class="text-sm font-bold text-slate-900">Diagnostic Scope</h3>
124
+ <p class="text-xs text-slate-600 leading-relaxed font-medium">Multi-label classification evaluating
125
+ frontal chest radiographs across 5 key pathologies simultaneously.</p>
126
+ </div>
127
+ <div class="bg-white border border-slate-200 rounded-sm p-6 shadow-sm flex flex-col gap-2">
128
+ <div
129
+ class="w-8 h-8 rounded-sm bg-slate-50 border border-slate-100 flex items-center justify-center mb-2">
130
+ <svg class="w-4 h-4 text-blue-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
132
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 002-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10">
133
+ </path>
134
+ </svg>
135
+ </div>
136
+ <h3 class="text-sm font-bold text-slate-900">Model Architecture</h3>
137
+ <p class="text-xs text-slate-600 leading-relaxed font-medium">State-of-the-art dual ensemble
138
+ consisting of a Vision Transformer (RAD-DINO) and a robust CNN (DenseNet121).</p>
139
+ </div>
140
+ <div class="bg-white border border-slate-200 rounded-sm p-6 shadow-sm flex flex-col gap-2">
141
+ <div
142
+ class="w-8 h-8 rounded-sm bg-slate-50 border border-slate-100 flex items-center justify-center mb-2">
143
+ <svg class="w-4 h-4 text-blue-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
144
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
145
+ d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
146
+ </path>
147
+ </svg>
148
+ </div>
149
+ <h3 class="text-sm font-bold text-slate-900">Clinical Output</h3>
150
+ <p class="text-xs text-slate-600 leading-relaxed font-medium">Native DICOM & PNG support,
151
+ comprehensive Grad-CAM heatmaps, with structured JSON and PDF report exports.</p>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </main>
156
+
157
+ <section class="w-full bg-white border-y border-slate-200 py-12">
158
+ <div class="max-w-7xl mx-auto px-4">
159
+ <h2 class="text-xs font-bold uppercase tracking-wider text-slate-500 border-b border-slate-100 pb-3 mb-0">
160
+ Target Pathologies</h2>
161
+ <div class="flex flex-col border border-t-0 border-slate-200 bg-white shadow-sm rounded-b-sm">
162
+ <div
163
+ class="flex flex-col sm:flex-row items-start sm:items-center py-4 px-6 bg-white border-b border-slate-100">
164
+ <span class="w-48 text-sm font-bold text-slate-900 shrink-0 mb-1 sm:mb-0">Atelectasis</span>
165
+ <span class="text-xs text-slate-600 leading-relaxed font-medium">Complete or partial collapse of the
166
+ lung or lobe, occurring when alveoli become deflated or filled with fluid. Radiographically
167
+ presents as increased opacification with volume loss and potential mediastinal shift. It is a
168
+ common post-operative respiratory complication or consequence of airway obstruction.</span>
169
+ </div>
170
+ <div
171
+ class="flex flex-col sm:flex-row items-start sm:items-center py-4 px-6 bg-slate-50 border-b border-slate-100">
172
+ <span class="w-48 text-sm font-bold text-slate-900 shrink-0 mb-1 sm:mb-0">Cardiomegaly</span>
173
+ <span class="text-xs text-slate-600 leading-relaxed font-medium">Radiographic enlargement of the
174
+ cardiac silhouette, typically defined by a cardiothoracic ratio &gt;0.5 on a PA chest
175
+ radiograph. It serves as a critical indicator of underlying cardiovascular pathology, including
176
+ congestive heart failure, valvular disease, or ventricular hypertrophy.</span>
177
+ </div>
178
+ <div
179
+ class="flex flex-col sm:flex-row items-start sm:items-center py-4 px-6 bg-white border-b border-slate-100">
180
+ <span class="w-48 text-sm font-bold text-slate-900 shrink-0 mb-1 sm:mb-0">Consolidation</span>
181
+ <span class="text-xs text-slate-600 leading-relaxed font-medium">Region of normally compressible
182
+ lung tissue that has filled with liquid, cellular debris, or other exudate. Classic radiologic
183
+ presentation includes air bronchograms and dense opacification without volume loss, frequently
184
+ indicative of pneumonia, pulmonary hemorrhage, or malignancy.</span>
185
+ </div>
186
+ <div
187
+ class="flex flex-col sm:flex-row items-start sm:items-center py-4 px-6 bg-slate-50 border-b border-slate-100">
188
+ <span class="w-48 text-sm font-bold text-slate-900 shrink-0 mb-1 sm:mb-0">Edema</span>
189
+ <span class="text-xs text-slate-600 leading-relaxed font-medium">Accumulation of excess fluid within
190
+ the pulmonary interstitium and alveolar spaces, impairing optimal gas exchange. Clinically
191
+ correlates with heart failure (cardiogenic) or acute respiratory distress syndrome
192
+ (non-cardiogenic), presenting with prominent vascular markings, Kerley B lines, and perihilar
193
+ haze.</span>
194
+ </div>
195
+ <div class="flex flex-col sm:flex-row items-start sm:items-center py-4 px-6 bg-white rounded-b-sm">
196
+ <span class="w-48 text-sm font-bold text-slate-900 shrink-0 mb-1 sm:mb-0">Pleural Effusion</span>
197
+ <span class="text-xs text-slate-600 leading-relaxed font-medium">Pathological accumulation of fluid
198
+ in the pleural cavity between the parietal and visceral pleura. Radiographically characterized
199
+ by blunting of the costophrenic angles and a distinct meniscus sign, commonly associated with
200
+ heart failure, pneumonia, pulmonary embolism, or advanced malignancy.</span>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </section>
205
+
206
+ <section class="w-full bg-slate-100 border-y border-slate-200 py-6 mt-12 shrink-0">
207
+ <div class="max-w-7xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4">
208
+ <span class="text-sm font-bold text-slate-900 uppercase tracking-widest">Initialize Diagnostic
209
+ Session</span>
210
+ <a href="/analyze"
211
+ class="bg-blue-900 hover:bg-blue-800 text-white text-[11px] font-bold py-3 px-8 rounded-sm uppercase tracking-widest text-center shadow-sm transition-colors border border-blue-900">Go
212
+ to Analysis</a>
213
+ </div>
214
+ </section>
215
+
216
+ <footer class="w-full bg-slate-50 py-6 px-4 shrink-0 text-center">
217
+ <p class="text-[10px] uppercase tracking-widest text-slate-400 font-bold">&copy; 2026 ChestXpert &mdash; For
218
+ Research & Educational Use Only. Not for primary diagnostic use.</p>
219
+ </footer>
220
+
221
+ <script src="/static/app.js"></script>
222
+ </body>
223
+
224
+ </html>
templates/login.html ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Sign In &mdash; ChestXpert</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/style.css">
11
+ <style>
12
+ .auth-container {
13
+ max-width: 400px;
14
+ margin: 4rem auto;
15
+ padding: 2rem;
16
+ }
17
+
18
+ .auth-header {
19
+ text-align: center;
20
+ margin-bottom: 2rem;
21
+ }
22
+
23
+ .auth-header h1 {
24
+ font-size: 1.5rem;
25
+ margin-bottom: 0.5rem;
26
+ color: var(--text-dark);
27
+ }
28
+
29
+ .auth-header p {
30
+ color: var(--text-light);
31
+ }
32
+
33
+ .form-group {
34
+ margin-bottom: 1.5rem;
35
+ }
36
+
37
+ .form-group label {
38
+ display: block;
39
+ margin-bottom: 0.5rem;
40
+ font-weight: 500;
41
+ color: var(--text-color);
42
+ }
43
+
44
+ .form-control {
45
+ width: 100%;
46
+ padding: 0.75rem 1rem;
47
+ border: 1px solid var(--border-color);
48
+ border-radius: 6px;
49
+ font-family: inherit;
50
+ font-size: 1rem;
51
+ transition: all 0.2s;
52
+ }
53
+
54
+ .form-control:focus {
55
+ outline: none;
56
+ border-color: var(--primary-color);
57
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
58
+ }
59
+
60
+ .auth-btn {
61
+ width: 100%;
62
+ padding: 0.75rem;
63
+ font-size: 1rem;
64
+ }
65
+
66
+ .auth-footer {
67
+ margin-top: 1.5rem;
68
+ text-align: center;
69
+ font-size: 0.9rem;
70
+ color: var(--text-light);
71
+ }
72
+ </style>
73
+ </head>
74
+
75
+ <body>
76
+ <nav class="navbar">
77
+ <div class="nav-container">
78
+ <a href="/" class="nav-brand"><span class="brand-mark">CX</span><span>ChestXpert</span></a>
79
+ <div class="nav-links">
80
+ <a href="/" class="nav-link">Home</a>
81
+ <a href="/analyze" class="nav-link">Analyze</a>
82
+ </div>
83
+ <div class="nav-auth" id="nav-auth"
84
+ style="display: none; align-items: center; gap: 0.5rem; margin-left: 1rem;">
85
+ <a href="/login" class="btn btn-outline" style="padding: 0.4rem 1rem;">Sign In</a>
86
+ <a href="/register" class="btn btn-primary" style="padding: 0.4rem 1rem;">Sign Up</a>
87
+ </div>
88
+ <div class="nav-profile" id="nav-profile"
89
+ style="display: none; align-items: center; margin-left: 1rem; position: relative;">
90
+ <div class="profile-trigger" id="profile-trigger"
91
+ style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem;">
92
+ <img src="/static/default-avatar.svg" alt="Profile"
93
+ style="width: 32px; height: 32px; border-radius: 50%; border: 2px solid var(--border-color); background: #f1f5f9;">
94
+ <span id="nav-user-name" style="font-weight: 500; font-size: 0.9rem;">User</span>
95
+ </div>
96
+ <div class="profile-dropdown" id="profile-dropdown"
97
+ style="display: none; position: absolute; top: 120%; right: 0; background: white; border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 220px; z-index: 100;">
98
+ <div style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
99
+ <strong id="dropdown-name"
100
+ style="display: block; color: var(--text-dark); margin-bottom: 0.25rem;">User</strong>
101
+ <span id="dropdown-email"
102
+ style="font-size: 0.8rem; color: var(--text-light); word-break: break-all;">user@example.com</span>
103
+ </div>
104
+ <div style="padding: 0.5rem;">
105
+ <a href="/history"
106
+ style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
107
+ onmouseover="this.style.background='var(--bg-light)'"
108
+ onmouseout="this.style.background='transparent'">My Analyses</a>
109
+ <a href="#"
110
+ style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
111
+ onmouseover="this.style.background='var(--bg-light)'"
112
+ onmouseout="this.style.background='transparent'">Account Settings</a>
113
+ <div style="height: 1px; background: var(--border-color); margin: 0.5rem 0;"></div>
114
+ <button id="logout-btn"
115
+ style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem; color: #ef4444; border: none; background: transparent; cursor: pointer; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
116
+ onmouseover="this.style.background='#fef2f2'"
117
+ onmouseout="this.style.background='transparent'">Sign Out</button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </nav>
123
+
124
+ <main class="page-content">
125
+ <div class="container">
126
+ <div class="card auth-container">
127
+ <div class="auth-header">
128
+ <h1>Sign In</h1>
129
+ <p>Welcome back to ChestXpert</p>
130
+ </div>
131
+ <form id="login-form">
132
+ <div class="form-group">
133
+ <label for="email">Email Address</label>
134
+ <input type="email" id="email" class="form-control" required placeholder="you@example.com">
135
+ </div>
136
+ <div class="form-group">
137
+ <label for="password">Password</label>
138
+ <input type="password" id="password" class="form-control" required placeholder="••••••••">
139
+ </div>
140
+ <button type="submit" class="btn btn-primary auth-btn">Sign In</button>
141
+ <div class="auth-footer">
142
+ Don't have an account? <a href="/register">Sign Up</a>
143
+ </div>
144
+ </form>
145
+ </div>
146
+ </div>
147
+ </main>
148
+
149
+ <script src="/static/app.js"></script>
150
+ </body>
151
+
152
+ </html>
templates/register.html ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Sign Up &mdash; ChestXpert</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/style.css">
11
+ <style>
12
+ .auth-container {
13
+ max-width: 400px;
14
+ margin: 4rem auto;
15
+ padding: 2rem;
16
+ }
17
+
18
+ .auth-header {
19
+ text-align: center;
20
+ margin-bottom: 2rem;
21
+ }
22
+
23
+ .auth-header h1 {
24
+ font-size: 1.5rem;
25
+ margin-bottom: 0.5rem;
26
+ color: var(--text-dark);
27
+ }
28
+
29
+ .auth-header p {
30
+ color: var(--text-light);
31
+ }
32
+
33
+ .form-group {
34
+ margin-bottom: 1.5rem;
35
+ }
36
+
37
+ .form-group label {
38
+ display: block;
39
+ margin-bottom: 0.5rem;
40
+ font-weight: 500;
41
+ color: var(--text-color);
42
+ }
43
+
44
+ .form-control {
45
+ width: 100%;
46
+ padding: 0.75rem 1rem;
47
+ border: 1px solid var(--border-color);
48
+ border-radius: 6px;
49
+ font-family: inherit;
50
+ font-size: 1rem;
51
+ transition: all 0.2s;
52
+ }
53
+
54
+ .form-control:focus {
55
+ outline: none;
56
+ border-color: var(--primary-color);
57
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
58
+ }
59
+
60
+ .auth-btn {
61
+ width: 100%;
62
+ padding: 0.75rem;
63
+ font-size: 1rem;
64
+ }
65
+
66
+ .auth-footer {
67
+ margin-top: 1.5rem;
68
+ text-align: center;
69
+ font-size: 0.9rem;
70
+ color: var(--text-light);
71
+ }
72
+ </style>
73
+ </head>
74
+
75
+ <body>
76
+ <nav class="navbar">
77
+ <div class="nav-container">
78
+ <a href="/" class="nav-brand"><span class="brand-mark">CX</span><span>ChestXpert</span></a>
79
+ <div class="nav-links">
80
+ <a href="/" class="nav-link">Home</a>
81
+ <a href="/analyze" class="nav-link">Analyze</a>
82
+ </div>
83
+ <div class="nav-auth" id="nav-auth"
84
+ style="display: none; align-items: center; gap: 0.5rem; margin-left: 1rem;">
85
+ <a href="/login" class="btn btn-outline" style="padding: 0.4rem 1rem;">Sign In</a>
86
+ <a href="/register" class="btn btn-primary" style="padding: 0.4rem 1rem;">Sign Up</a>
87
+ </div>
88
+ <div class="nav-profile" id="nav-profile"
89
+ style="display: none; align-items: center; margin-left: 1rem; position: relative;">
90
+ <div class="profile-trigger" id="profile-trigger"
91
+ style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem;">
92
+ <img src="/static/default-avatar.svg" alt="Profile"
93
+ style="width: 32px; height: 32px; border-radius: 50%; border: 2px solid var(--border-color); background: #f1f5f9;">
94
+ <span id="nav-user-name" style="font-weight: 500; font-size: 0.9rem;">User</span>
95
+ </div>
96
+ <div class="profile-dropdown" id="profile-dropdown"
97
+ style="display: none; position: absolute; top: 120%; right: 0; background: white; border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 220px; z-index: 100;">
98
+ <div style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
99
+ <strong id="dropdown-name"
100
+ style="display: block; color: var(--text-dark); margin-bottom: 0.25rem;">User</strong>
101
+ <span id="dropdown-email"
102
+ style="font-size: 0.8rem; color: var(--text-light); word-break: break-all;">user@example.com</span>
103
+ </div>
104
+ <div style="padding: 0.5rem;">
105
+ <a href="/history"
106
+ style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
107
+ onmouseover="this.style.background='var(--bg-light)'"
108
+ onmouseout="this.style.background='transparent'">My Analyses</a>
109
+ <a href="#"
110
+ style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
111
+ onmouseover="this.style.background='var(--bg-light)'"
112
+ onmouseout="this.style.background='transparent'">Account Settings</a>
113
+ <div style="height: 1px; background: var(--border-color); margin: 0.5rem 0;"></div>
114
+ <button id="logout-btn"
115
+ style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem; color: #ef4444; border: none; background: transparent; cursor: pointer; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;"
116
+ onmouseover="this.style.background='#fef2f2'"
117
+ onmouseout="this.style.background='transparent'">Sign Out</button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </nav>
123
+
124
+ <main class="page-content">
125
+ <div class="container">
126
+ <div class="card auth-container">
127
+ <div class="auth-header">
128
+ <h1>Create an Account</h1>
129
+ <p>Join ChestXpert to save your analyses</p>
130
+ </div>
131
+ <form id="register-form">
132
+ <div class="form-group">
133
+ <label for="name">Full Name</label>
134
+ <input type="text" id="name" class="form-control" required placeholder="Dr. Jane Doe">
135
+ </div>
136
+ <div class="form-group">
137
+ <label for="email">Email Address</label>
138
+ <input type="email" id="email" class="form-control" required placeholder="you@example.com">
139
+ </div>
140
+ <div class="form-group">
141
+ <label for="password">Password</label>
142
+ <input type="password" id="password" class="form-control" required placeholder="••••••••"
143
+ minlength="6">
144
+ </div>
145
+ <button type="submit" class="btn btn-primary auth-btn">Sign Up</button>
146
+ <div class="auth-footer">
147
+ Already have an account? <a href="/login">Sign In</a>
148
+ </div>
149
+ </form>
150
+ </div>
151
+ </div>
152
+ </main>
153
+
154
+ <script src="/static/app.js"></script>
155
+ </body>
156
+
157
+ </html>
templates/report.html ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Report — ChestXpert</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="/static/style.css">
11
+ <style>
12
+ @media print {
13
+
14
+ .navbar,
15
+ .no-print {
16
+ display: none !important;
17
+ }
18
+
19
+ body {
20
+ background: white;
21
+ }
22
+
23
+ .report-container {
24
+ max-width: 100%;
25
+ padding: 0;
26
+ }
27
+
28
+ .card {
29
+ border: 1px solid #ddd;
30
+ box-shadow: none;
31
+ }
32
+ }
33
+ </style>
34
+ </head>
35
+
36
+ <body>
37
+ <nav class="navbar no-print">
38
+ <div class="nav-container">
39
+ <a href="/" class="nav-brand"><span class="brand-mark">CX</span><span>ChestXpert</span></a>
40
+ <div class="nav-links">
41
+ <a href="/" class="nav-link">Home</a>
42
+ <a href="/analyze" class="nav-link">Analyze</a>
43
+ <a href="/compare" class="nav-link">Compare</a>
44
+ <a href="/history" class="nav-link">History</a>
45
+ <a href="/about" class="nav-link">About</a>
46
+ </div>
47
+ </div>
48
+ </nav>
49
+
50
+ <main class="page-content">
51
+ <div class="container report-container">
52
+ {% if error %}
53
+ <div class="empty-state">
54
+ <h3>Report Not Found</h3>
55
+ <p>This analysis report has expired or does not exist.</p>
56
+ <a href="/analyze" class="btn btn-primary">New Analysis</a>
57
+ </div>
58
+ {% else %}
59
+ <div class="report-header no-print">
60
+ <button class="btn btn-outline" onclick="window.print()">Print Report</button>
61
+ <button class="btn btn-outline" onclick="window.close()">Close</button>
62
+ </div>
63
+
64
+ <div class="report-page" id="report-page">
65
+ <div class="report-title-bar">
66
+ <div>
67
+ <h1>ChestXpert Analysis Report</h1>
68
+ <p class="report-subtitle">AI-Powered Chest X-Ray Analysis</p>
69
+ </div>
70
+ <div class="report-meta">
71
+ <p><strong>Report ID:</strong> <span id="report-id"></span></p>
72
+ <p><strong>Date:</strong> <span id="report-date"></span></p>
73
+ <p><strong>File:</strong> <span id="report-file"></span></p>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="report-body">
78
+ <div class="report-grid">
79
+ <div>
80
+ <h3>Original Image</h3>
81
+ <div class="report-image-box">
82
+ <img id="report-original" src="" alt="X-Ray">
83
+ </div>
84
+ </div>
85
+ <div>
86
+ <h3>Grad-CAM Heatmap</h3>
87
+ <div class="report-image-box">
88
+ <img id="report-heatmap" src="" alt="Heatmap">
89
+ </div>
90
+ <p class="report-heatmap-label" id="report-heatmap-label"></p>
91
+ </div>
92
+ </div>
93
+
94
+ <h3>Findings</h3>
95
+ <table class="report-table">
96
+ <thead>
97
+ <tr>
98
+ <th>Condition</th>
99
+ <th>Probability</th>
100
+ <th>Risk Level</th>
101
+ <th>RAD-DINO</th>
102
+ <th>DenseNet</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody id="report-findings"></tbody>
106
+ </table>
107
+
108
+ <h3>Models Used</h3>
109
+ <p id="report-models"></p>
110
+
111
+ <div class="report-disclaimer">
112
+ <strong>Disclaimer:</strong> This report is generated by an AI system for educational and
113
+ research purposes only. It is not a substitute for professional medical diagnosis. Always
114
+ consult a qualified healthcare provider for clinical decisions.
115
+ </div>
116
+ </div>
117
+ </div>
118
+ {% endif %}
119
+ </div>
120
+ </main>
121
+
122
+ {% if not error %}
123
+ <script>
124
+ const reportData = {{ data | safe }};
125
+ document.getElementById('report-id').textContent = reportData.analysis_id || '-';
126
+ document.getElementById('report-date').textContent = reportData.timestamp || new Date().toLocaleString();
127
+ document.getElementById('report-file').textContent = reportData.filename || '-';
128
+ document.getElementById('report-original').src = 'data:image/png;base64,' + reportData.original_image;
129
+
130
+ // Show heatmap of highest probability condition
131
+ const topResult = reportData.results[0];
132
+ if (topResult && topResult.heatmap) {
133
+ document.getElementById('report-heatmap').src = 'data:image/png;base64,' + topResult.heatmap;
134
+ document.getElementById('report-heatmap-label').textContent = 'Showing: ' + topResult.label;
135
+ }
136
+
137
+ // Findings table
138
+ const tbody = document.getElementById('report-findings');
139
+ reportData.results.forEach(r => {
140
+ const tr = document.createElement('tr');
141
+ tr.innerHTML = `
142
+ <td><strong>${r.label}</strong></td>
143
+ <td>${r.probability}%</td>
144
+ <td><span class="risk-tag ${r.risk}">${r.risk.toUpperCase()}</span></td>
145
+ <td>${r.rd_prob !== null ? r.rd_prob + '%' : 'N/A'}</td>
146
+ <td>${r.dn_prob !== null ? r.dn_prob + '%' : 'N/A'}</td>
147
+ `;
148
+ tbody.appendChild(tr);
149
+ });
150
+
151
+ // Models
152
+ const m = reportData.models_used;
153
+ const parts = [];
154
+ if (m.rad_dino) parts.push('RAD-DINO (ViT-B/14)');
155
+ if (m.densenet) parts.push('DenseNet121');
156
+ if (m.ensemble) parts.push('Weighted Ensemble (60/40)');
157
+ document.getElementById('report-models').textContent = parts.join(', ');
158
+ </script>
159
+ {% endif %}
160
+ <script src="/static/app.js"></script>
161
+ </body>
162
+
163
+ </html>
update_nav.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+
4
+ nav_extension = """
5
+ <div class="nav-auth" id="nav-auth" style="display: none; align-items: center; gap: 0.5rem; margin-left: 1rem;">
6
+ <a href="/login" class="btn btn-outline" style="padding: 0.4rem 1rem;">Sign In</a>
7
+ <a href="/register" class="btn btn-primary" style="padding: 0.4rem 1rem;">Sign Up</a>
8
+ </div>
9
+ <div class="nav-profile" id="nav-profile" style="display: none; align-items: center; margin-left: 1rem; position: relative;">
10
+ <div class="profile-trigger" id="profile-trigger" style="cursor: pointer; display: flex; align-items: center; gap: 0.5rem;">
11
+ <img src="/static/default-avatar.svg" alt="Profile" style="width: 32px; height: 32px; border-radius: 50%; border: 2px solid var(--border-color); background: #f1f5f9;">
12
+ <span id="nav-user-name" style="font-weight: 500; font-size: 0.9rem;">User</span>
13
+ </div>
14
+ <div class="profile-dropdown" id="profile-dropdown" style="display: none; position: absolute; top: 120%; right: 0; background: white; border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 220px; z-index: 100;">
15
+ <div style="padding: 1rem; border-bottom: 1px solid var(--border-color);">
16
+ <strong id="dropdown-name" style="display: block; color: var(--text-dark); margin-bottom: 0.25rem;">User</strong>
17
+ <span id="dropdown-email" style="font-size: 0.8rem; color: var(--text-light); word-break: break-all;">user@example.com</span>
18
+ </div>
19
+ <div style="padding: 0.5rem;">
20
+ <a href="/history" style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;" onmouseover="this.style.background='var(--bg-light)'" onmouseout="this.style.background='transparent'">My Analyses</a>
21
+ <a href="#" style="display: block; padding: 0.5rem 1rem; color: var(--text-color); text-decoration: none; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;" onmouseover="this.style.background='var(--bg-light)'" onmouseout="this.style.background='transparent'">Account Settings</a>
22
+ <div style="height: 1px; background: var(--border-color); margin: 0.5rem 0;"></div>
23
+ <button id="logout-btn" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem; color: #ef4444; border: none; background: transparent; cursor: pointer; border-radius: 4px; font-size: 0.9rem; transition: background 0.2s;" onmouseover="this.style.background='#fef2f2'" onmouseout="this.style.background='transparent'">Sign Out</button>
24
+ </div>
25
+ </div>
26
+ </div>"""
27
+
28
+ for f in glob.glob('templates/*.html'):
29
+ if 'login.html' in f or 'register.html' in f:
30
+ continue
31
+ with open(f, 'r', encoding='utf-8') as file:
32
+ content = file.read()
33
+ if 'id="nav-auth"' not in content:
34
+ content = content.replace('</div>\\n <div class="nav-status">', '</div>\\n' + nav_extension + '\\n <div class="nav-status">')
35
+ with open(f, 'w', encoding='utf-8') as file:
36
+ file.write(content)
37
+ print(f"Updated {f}")