Spaces:
Sleeping
Sleeping
Commit ·
c8a3a1b
0
Parent(s):
Initial ChestXpert deployment
Browse files- .gitattributes +3 -0
- .gitignore +17 -0
- Dockerfile +20 -0
- app.py +427 -0
- app.py_patch +9 -0
- models/README.txt +3 -0
- models/densenet_best.pth +3 -0
- models/rad_dino_best.pth +3 -0
- requirements.txt +10 -0
- samples/README.txt +10 -0
- static/app.js +903 -0
- static/default-avatar.svg +4 -0
- static/style.css +1879 -0
- templates/about.html +299 -0
- templates/analyze.html +513 -0
- templates/compare.html +367 -0
- templates/history.html +143 -0
- templates/index.html +224 -0
- templates/login.html +152 -0
- templates/register.html +157 -0
- templates/report.html +163 -0
- update_nav.py +37 -0
.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">© 2026 ChestXpert — 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">© 2026 ChestXpert — 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 >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">© 2026 ChestXpert — 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 — 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 — 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}")
|