Harshit Ghosh commited on
Commit
5d2b6e2
Β·
1 Parent(s): dc8b958

Landing page init

Browse files
app_new.py CHANGED
@@ -958,9 +958,9 @@ def _log_response(response): # pyright: ignore[reportUnusedFunction]
958
 
959
  @app.route("/")
960
  def home():
961
- """Home page"""
962
  if not current_user.is_authenticated:
963
- return redirect(url_for("auth.login"))
964
 
965
  cases = _load_user_cases(current_user.id)
966
  stats = compute_stats(cases)
 
958
 
959
  @app.route("/")
960
  def home():
961
+ """Home page β€” shows landing page for guests, dashboard for logged-in users."""
962
  if not current_user.is_authenticated:
963
+ return render_template("landing.html")
964
 
965
  cases = _load_user_cases(current_user.id)
966
  stats = compute_stats(cases)
static/css/auth.css CHANGED
@@ -383,102 +383,76 @@
383
  .profile-avatar-wrapper {
384
  position: relative;
385
  display: inline-block;
386
- margin-right: 20px;
387
  }
388
  .profile-avatar-img {
389
- width: 72px;
390
- height: 72px;
391
- border-radius: 50%;
392
  object-fit: cover;
393
- border: 2px solid #334155;
394
- background: #0f172a;
395
  }
396
  .avatar-upload-btn {
397
- position: absolute;
398
- bottom: 0;
399
- right: -4px;
400
- width: 28px;
401
- height: 28px;
402
- border-radius: 50%;
403
- background: #3b82f6;
404
- border: 2px solid #1e293b;
405
  color: #fff;
406
- display: flex;
407
- align-items: center;
408
- justify-content: center;
409
- cursor: pointer;
410
- transition: all 0.2s;
411
- }
412
- .avatar-upload-btn:hover {
413
- background: #2563eb;
414
- transform: scale(1.1);
415
  }
 
416
 
417
  .btn-inline-edit {
418
- background: transparent;
419
- border: 1px solid #334155;
420
- color: #94a3b8;
421
- padding: 4px 12px;
422
- border-radius: 4px;
423
- font-size: 13px;
424
- font-weight: 500;
425
  cursor: pointer;
426
  transition: all 0.2s;
427
  margin-left: auto;
 
 
428
  }
429
  .btn-inline-edit:hover {
430
- background: #1e293b;
431
- color: #f8fafc;
432
- border-color: #475569;
433
  }
434
 
435
  .profile-row {
436
  display: flex;
437
  align-items: center;
438
  padding: 12px 0;
439
- border-bottom: 1px solid #1e293b;
440
- }
441
- .profile-row:last-child {
442
- border-bottom: none;
443
- }
444
- .pr-label {
445
- width: 140px;
446
- color: #64748b;
447
- font-size: 14px;
448
- }
449
- .pr-value {
450
- color: #f8fafc;
451
- font-size: 15px;
452
- font-weight: 500;
453
  }
 
 
 
454
 
455
  .modal-overlay {
456
- position: fixed;
457
- top: 0; left: 0; right: 0; bottom: 0;
458
- background: rgba(2, 6, 23, 0.8);
459
- backdrop-filter: blur(4px);
460
  z-index: 1000;
461
- display: flex;
462
- align-items: center;
463
- justify-content: center;
464
  padding: 20px;
465
  }
466
  .profile-modal-content {
467
- width: 100%;
468
- max-width: 400px;
469
- background-color: #111c33 !important;
470
- color: #f8fafc !important;
471
- border: 1px solid #1e293b !important;
472
  animation: modal-pop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
473
  }
474
- .profile-modal-content h2, .profile-modal-content label {
475
- color: #f8fafc;
476
- }
477
  @keyframes modal-pop {
478
- 0% { transform: scale(0.95) translateY(10px); opacity: 0; }
479
  100% { transform: scale(1) translateY(0); opacity: 1; }
480
  }
481
-
482
- .profile-message-container {
483
- margin-bottom: 24px;
484
- }
 
383
  .profile-avatar-wrapper {
384
  position: relative;
385
  display: inline-block;
386
+ flex-shrink: 0;
387
  }
388
  .profile-avatar-img {
389
+ width: 72px; height: 72px; border-radius: 50%;
 
 
390
  object-fit: cover;
391
+ border: 2px solid rgba(110,168,254,0.35);
392
+ box-shadow: 0 0 0 4px rgba(110,168,254,0.12);
393
  }
394
  .avatar-upload-btn {
395
+ position: absolute; bottom: 0; right: -4px;
396
+ width: 28px; height: 28px; border-radius: 50%;
397
+ background: linear-gradient(135deg, #6ea8fe, #6366f1);
398
+ border: 2px solid #0c1427;
 
 
 
 
399
  color: #fff;
400
+ display: flex; align-items: center; justify-content: center;
401
+ cursor: pointer; transition: all 0.2s;
 
 
 
 
 
 
 
402
  }
403
+ .avatar-upload-btn:hover { transform: scale(1.12); box-shadow: 0 4px 14px rgba(110,168,254,0.4); }
404
 
405
  .btn-inline-edit {
406
+ background: rgba(110,168,254,0.07);
407
+ border: 1px solid rgba(110,168,254,0.25);
408
+ color: #6ea8fe;
409
+ padding: 5px 14px;
410
+ border-radius: 8px;
411
+ font-size: 0.82rem;
412
+ font-weight: 600;
413
  cursor: pointer;
414
  transition: all 0.2s;
415
  margin-left: auto;
416
+ font-family: inherit;
417
+ flex-shrink: 0;
418
  }
419
  .btn-inline-edit:hover {
420
+ background: rgba(110,168,254,0.15);
421
+ border-color: #6ea8fe;
422
+ color: #a8c9ff;
423
  }
424
 
425
  .profile-row {
426
  display: flex;
427
  align-items: center;
428
  padding: 12px 0;
429
+ border-bottom: 1px solid rgba(36,51,86,0.5);
430
+ gap: 12px;
431
+ font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
+ .profile-row:last-child { border-bottom: none; }
434
+ .pr-label { color: #8ba0c4; font-weight: 500; width: 140px; flex-shrink: 0; }
435
+ .pr-value { color: #e8ecf6; font-weight: 600; flex: 1; }
436
 
437
  .modal-overlay {
438
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
439
+ background: rgba(7,13,26,0.82);
440
+ backdrop-filter: blur(6px);
 
441
  z-index: 1000;
442
+ display: flex; align-items: center; justify-content: center;
 
 
443
  padding: 20px;
444
  }
445
  .profile-modal-content {
446
+ width: 100%; max-width: 420px;
447
+ background: linear-gradient(160deg, #162244, #111c33) !important;
448
+ border: 1px solid #243356 !important;
449
+ border-radius: 20px !important;
450
+ color: #e8ecf6 !important;
451
  animation: modal-pop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
452
  }
453
+ .profile-modal-content h2, .profile-modal-content label { color: #e8ecf6; }
 
 
454
  @keyframes modal-pop {
455
+ 0% { transform: scale(0.95) translateY(10px); opacity: 0; }
456
  100% { transform: scale(1) translateY(0); opacity: 1; }
457
  }
458
+ .profile-message-container { margin-bottom: 16px; }
 
 
 
static/css/landing.css ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ ICH Pipeline β€” Landing Page Stylesheet
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ :root {
6
+ --bg: #070d1a;
7
+ --bg2: #0c1427;
8
+ --panel: #111c33;
9
+ --panel2: #162244;
10
+ --line: #243356;
11
+ --accent: #6ea8fe;
12
+ --accent2: #818cf8;
13
+ --text: #e8ecf6;
14
+ --muted: #8ba0c4;
15
+ --green: #34d399;
16
+ --red: #fb7185;
17
+ }
18
+
19
+ /* ── Reset ─────────────────────────────────────────────────── */
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+ html { scroll-behavior: smooth; }
22
+ body {
23
+ font-family: "Inter", system-ui, sans-serif;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ line-height: 1.6;
27
+ min-height: 100vh;
28
+ overflow-x: hidden;
29
+ }
30
+
31
+ /* ── Topbar ─────────────────────────────────────────────────── */
32
+ .topbar {
33
+ position: fixed; top: 0; left: 0; right: 0; z-index: 100;
34
+ padding: 16px 32px;
35
+ display: flex; align-items: center; justify-content: space-between;
36
+ background: rgba(7,13,26,0.85);
37
+ backdrop-filter: blur(14px);
38
+ border-bottom: 1px solid rgba(36,51,86,0.5);
39
+ }
40
+ .brand { display: flex; align-items: center; gap: 10px; text-decoration: none; }
41
+ .brand-icon {
42
+ width: 36px; height: 36px; border-radius: 10px;
43
+ background: linear-gradient(135deg, rgba(110,168,254,0.13), rgba(129,140,248,0.13));
44
+ border: 1px solid rgba(110,168,254,0.3);
45
+ display: flex; align-items: center; justify-content: center;
46
+ color: var(--accent);
47
+ }
48
+ .brand-name { font-weight: 800; font-size: 1rem; color: var(--text); }
49
+ .brand-name span { color: var(--accent); }
50
+ .nav-actions { display: flex; gap: 12px; align-items: center; }
51
+
52
+ .btn-ghost {
53
+ padding: 8px 20px; border-radius: 10px; font-weight: 600;
54
+ font-size: 0.9rem; text-decoration: none; color: var(--muted);
55
+ transition: color 0.15s;
56
+ }
57
+ .btn-ghost:hover { color: var(--text); }
58
+
59
+ .btn-primary {
60
+ padding: 9px 22px; border-radius: 10px; font-weight: 700;
61
+ font-size: 0.9rem; text-decoration: none;
62
+ background: linear-gradient(135deg, #6ea8fe, #818cf8);
63
+ color: #070d1a;
64
+ transition: opacity 0.15s, transform 0.15s;
65
+ display: inline-flex; align-items: center; gap: 7px;
66
+ }
67
+ .btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
68
+
69
+ /* ── Hero ───────────────────────────────────────────────────── */
70
+ .hero {
71
+ min-height: 100vh;
72
+ display: flex; align-items: center; justify-content: center;
73
+ text-align: center; padding: 120px 24px 80px;
74
+ position: relative; overflow: hidden;
75
+ }
76
+ .hero::before {
77
+ content: '';
78
+ position: absolute; inset: 0;
79
+ background:
80
+ radial-gradient(ellipse 900px 600px at 50% 0%, rgba(110,168,254,0.12) 0%, transparent 70%),
81
+ radial-gradient(ellipse 600px 400px at 80% 80%, rgba(129,140,248,0.10) 0%, transparent 65%),
82
+ radial-gradient(ellipse 500px 400px at 10% 70%, rgba(52,211,153,0.06) 0%, transparent 60%);
83
+ pointer-events: none;
84
+ }
85
+ .hero-inner { position: relative; z-index: 1; max-width: 820px; width: 100%; }
86
+
87
+ .hero-badge {
88
+ display: inline-flex; align-items: center; gap: 8px;
89
+ padding: 7px 16px; border-radius: 100px;
90
+ background: rgba(110,168,254,0.1);
91
+ border: 1px solid rgba(110,168,254,0.25);
92
+ font-size: 0.82rem; font-weight: 600;
93
+ color: var(--accent); margin-bottom: 32px;
94
+ animation: fadeUp 0.6s ease both;
95
+ }
96
+ .badge-dot {
97
+ width: 7px; height: 7px; border-radius: 50%;
98
+ background: var(--accent);
99
+ box-shadow: 0 0 8px var(--accent);
100
+ animation: pulse 2s ease-in-out infinite;
101
+ }
102
+
103
+ .hero h1 {
104
+ font-size: clamp(2.6rem, 6vw, 4.5rem);
105
+ font-weight: 900; line-height: 1.1;
106
+ letter-spacing: -0.03em;
107
+ animation: fadeUp 0.6s 0.1s ease both;
108
+ }
109
+ .hero h1 .grad {
110
+ background: linear-gradient(135deg, #6ea8fe 0%, #818cf8 50%, #34d399 100%);
111
+ -webkit-background-clip: text;
112
+ -webkit-text-fill-color: transparent;
113
+ background-clip: text;
114
+ }
115
+ .hero-sub {
116
+ max-width: 600px; margin: 24px auto 0;
117
+ font-size: 1.15rem; color: var(--muted); line-height: 1.7;
118
+ animation: fadeUp 0.6s 0.2s ease both;
119
+ }
120
+ .hero-ctas {
121
+ display: flex; gap: 14px; justify-content: center; flex-wrap: wrap;
122
+ margin-top: 44px;
123
+ animation: fadeUp 0.6s 0.3s ease both;
124
+ }
125
+
126
+ .btn-hero {
127
+ padding: 14px 32px; border-radius: 12px; font-weight: 700;
128
+ font-size: 1rem; text-decoration: none;
129
+ display: inline-flex; align-items: center; gap: 9px;
130
+ transition: all 0.2s;
131
+ }
132
+ .btn-hero-primary {
133
+ background: linear-gradient(135deg, #6ea8fe, #818cf8);
134
+ color: #070d1a;
135
+ box-shadow: 0 4px 32px rgba(110,168,254,0.35);
136
+ }
137
+ .btn-hero-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 40px rgba(110,168,254,0.45); }
138
+ .btn-hero-secondary {
139
+ background: rgba(255,255,255,0.05);
140
+ border: 1px solid var(--line);
141
+ color: var(--text);
142
+ }
143
+ .btn-hero-secondary:hover { background: rgba(255,255,255,0.09); transform: translateY(-2px); }
144
+
145
+ /* ── Trust bar ──────────────────────────────────────────────── */
146
+ .trust-bar {
147
+ display: flex; gap: 32px; justify-content: center; flex-wrap: wrap;
148
+ margin-top: 60px; padding-top: 40px;
149
+ border-top: 1px solid rgba(36,51,86,0.5);
150
+ animation: fadeUp 0.6s 0.4s ease both;
151
+ }
152
+ .trust-item { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.88rem; font-weight: 500; }
153
+ .trust-item svg { color: var(--accent); flex-shrink: 0; }
154
+
155
+ /* ── Stats strip ────────────────────────────────────────────── */
156
+ .stats-strip {
157
+ background: var(--panel);
158
+ border-top: 1px solid var(--line);
159
+ border-bottom: 1px solid var(--line);
160
+ padding: 40px 24px;
161
+ }
162
+ .stats-inner {
163
+ max-width: 900px; margin: 0 auto;
164
+ display: grid; grid-template-columns: repeat(3, 1fr);
165
+ }
166
+ .stat-item { text-align: center; padding: 8px 24px; }
167
+ .stat-item + .stat-item { border-left: 1px solid var(--line); }
168
+ .stat-num { font-size: 2.5rem; font-weight: 900; color: var(--accent); letter-spacing: -0.03em; }
169
+ .stat-desc { color: var(--muted); font-size: 0.9rem; margin-top: 4px; }
170
+
171
+ /* ── Features ───────────────────────────────────────────────── */
172
+ .features { padding: 100px 24px; }
173
+
174
+ .section-label {
175
+ text-align: center; font-size: 0.82rem; font-weight: 700;
176
+ letter-spacing: 0.1em; text-transform: uppercase;
177
+ color: var(--accent); margin-bottom: 16px;
178
+ }
179
+ .section-title {
180
+ text-align: center; font-size: clamp(1.8rem, 4vw, 2.8rem);
181
+ font-weight: 800; margin-bottom: 14px;
182
+ }
183
+ .section-sub {
184
+ text-align: center; color: var(--muted); max-width: 540px;
185
+ margin: 0 auto 60px; font-size: 1rem;
186
+ }
187
+
188
+ .features-grid {
189
+ max-width: 1100px; margin: 0 auto;
190
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
191
+ }
192
+ .feat-card {
193
+ background: linear-gradient(160deg, var(--panel2), var(--panel));
194
+ border: 1px solid var(--line);
195
+ border-radius: 18px; padding: 32px 28px;
196
+ transition: transform 0.2s, box-shadow 0.2s;
197
+ }
198
+ .feat-card:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
199
+ .feat-icon {
200
+ width: 52px; height: 52px; border-radius: 14px;
201
+ background: rgba(110,168,254,0.1);
202
+ border: 1px solid rgba(110,168,254,0.2);
203
+ display: flex; align-items: center; justify-content: center;
204
+ color: var(--accent); margin-bottom: 20px;
205
+ }
206
+ .feat-card h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: 10px; }
207
+ .feat-card p { color: var(--muted); font-size: 0.92rem; line-height: 1.65; }
208
+
209
+ /* ── How it works ───────────────────────────────────────────── */
210
+ .how {
211
+ padding: 80px 24px 100px;
212
+ background: var(--panel);
213
+ border-top: 1px solid var(--line);
214
+ border-bottom: 1px solid var(--line);
215
+ }
216
+ .steps {
217
+ max-width: 860px; margin: 0 auto;
218
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;
219
+ }
220
+ .step { text-align: center; }
221
+ .step-num {
222
+ width: 52px; height: 52px; border-radius: 50%;
223
+ background: linear-gradient(135deg, rgba(110,168,254,0.2), rgba(129,140,248,0.2));
224
+ border: 1px solid rgba(110,168,254,0.35);
225
+ display: flex; align-items: center; justify-content: center;
226
+ font-size: 1.25rem; font-weight: 900; color: var(--accent);
227
+ margin: 0 auto 16px;
228
+ }
229
+ .step h4 { font-weight: 700; font-size: 0.95rem; margin-bottom: 8px; }
230
+ .step p { color: var(--muted); font-size: 0.85rem; line-height: 1.6; }
231
+
232
+ /* ── CTA section ────────────────────────────────────────────── */
233
+ .cta-section {
234
+ padding: 100px 24px; text-align: center;
235
+ background: radial-gradient(ellipse 800px 400px at 50% 50%, rgba(110,168,254,0.08) 0%, transparent 70%);
236
+ }
237
+ .cta-section h2 { font-size: clamp(1.8rem, 4vw, 2.8rem); font-weight: 900; margin-bottom: 14px; }
238
+ .cta-section p { color: var(--muted); font-size: 1rem; margin-bottom: 40px; }
239
+
240
+ /* ── Disclaimer ─────────────────────────────────────────────── */
241
+ .disclaimer {
242
+ max-width: 680px; margin: 0 auto 60px;
243
+ padding: 16px 20px; border-radius: 12px;
244
+ background: rgba(251,113,133,0.07);
245
+ border: 1px solid rgba(251,113,133,0.2);
246
+ color: #fca5a5; font-size: 0.85rem; line-height: 1.6;
247
+ display: flex; gap: 12px; align-items: flex-start;
248
+ }
249
+ .disclaimer svg { flex-shrink: 0; margin-top: 2px; }
250
+
251
+ /* ── Footer ─────────────────────────────────────────────────── */
252
+ footer {
253
+ padding: 32px 24px; text-align: center;
254
+ border-top: 1px solid var(--line);
255
+ color: var(--muted); font-size: 0.85rem;
256
+ }
257
+
258
+ /* ── Animations ─────────────────────────────────────────────── */
259
+ @keyframes fadeUp {
260
+ from { opacity: 0; transform: translateY(20px); }
261
+ to { opacity: 1; transform: translateY(0); }
262
+ }
263
+ @keyframes pulse {
264
+ 0%, 100% { opacity: 1; }
265
+ 50% { opacity: 0.4; }
266
+ }
267
+
268
+ /* ── Responsive ─────────────────────────────────────────────── */
269
+ @media (max-width: 768px) {
270
+ .features-grid, .steps { grid-template-columns: 1fr 1fr; }
271
+ .stats-inner { grid-template-columns: 1fr; }
272
+ .stat-item + .stat-item { border-left: none; border-top: 1px solid var(--line); }
273
+ .topbar { padding: 14px 20px; }
274
+ }
275
+ @media (max-width: 480px) {
276
+ .features-grid, .steps { grid-template-columns: 1fr; }
277
+ .nav-actions .btn-ghost { display: none; }
278
+ }
static/js/landing.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ ICH Pipeline β€” Landing Page JS
3
+ ═══════════════════════════════════════════════════════════════ */
4
+
5
+ (function () {
6
+ /* ── Scroll-triggered navbar shadow ── */
7
+ var topbar = document.querySelector('.topbar');
8
+ if (topbar) {
9
+ window.addEventListener('scroll', function () {
10
+ if (window.scrollY > 20) {
11
+ topbar.style.boxShadow = '0 4px 32px rgba(0,0,0,0.4)';
12
+ } else {
13
+ topbar.style.boxShadow = 'none';
14
+ }
15
+ }, { passive: true });
16
+ }
17
+
18
+ /* ── Intersection Observer: fade-in feature cards on scroll ── */
19
+ if ('IntersectionObserver' in window) {
20
+ var cards = document.querySelectorAll('.feat-card, .step, .stat-item');
21
+ var observer = new IntersectionObserver(function (entries) {
22
+ entries.forEach(function (entry) {
23
+ if (entry.isIntersecting) {
24
+ entry.target.style.opacity = '1';
25
+ entry.target.style.transform = 'translateY(0)';
26
+ observer.unobserve(entry.target);
27
+ }
28
+ });
29
+ }, { threshold: 0.12 });
30
+
31
+ cards.forEach(function (card) {
32
+ card.style.opacity = '0';
33
+ card.style.transform = 'translateY(24px)';
34
+ card.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
35
+ observer.observe(card);
36
+ });
37
+ }
38
+ })();
static/js/upload.js CHANGED
@@ -88,6 +88,50 @@
88
  }
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  function initUploadPage() {
92
  var tabs = document.querySelectorAll('.upload-tab');
93
  var panels = document.querySelectorAll('.tab-panel');
@@ -98,53 +142,36 @@
98
 
99
  tabs.forEach(function (tab) {
100
  tab.addEventListener('click', function () {
101
- tabs.forEach(function (item) {
102
- item.classList.remove('active');
103
- });
104
- panels.forEach(function (panel) {
105
- panel.classList.remove('active');
106
- });
107
-
108
  tab.classList.add('active');
109
  var target = document.getElementById('tab-' + tab.dataset.tab);
110
- if (target) {
111
- target.classList.add('active');
112
- }
113
  });
114
  });
115
 
116
  wireDropzone({
117
- zoneId: 'dropzoneSingle',
118
- inputId: 'singleInput',
119
- infoId: 'singleInfo',
120
- labelId: 'singleFileName',
121
- clearSel: '.js-clear-single',
122
- submitId: 'singleSubmit',
123
- formId: 'singleForm',
124
- overlayId: 'singleOverlay',
125
  multi: false
126
  });
127
 
128
  wireDropzone({
129
- zoneId: 'dropzoneMulti',
130
- inputId: 'multiInput',
131
- infoId: 'multiInfo',
132
- labelId: 'multiFileName',
133
- clearSel: '.js-clear-multi',
134
- submitId: 'multiSubmit',
135
- formId: 'multiForm',
136
- overlayId: 'multiOverlay',
137
  multi: true
138
  });
139
 
 
 
 
140
  var dirInput = document.getElementById('dirPath');
141
  var dirSubmit = document.getElementById('dirSubmit');
142
-
143
  if (dirInput && dirSubmit) {
144
- function checkDir() {
145
- dirSubmit.disabled = !dirInput.value.trim();
146
- }
147
-
148
  dirInput.addEventListener('input', checkDir);
149
  checkDir();
150
  }
 
88
  }
89
  }
90
 
91
+ // ── File size guard ──────────────────────────────────────────────────────
92
+ var MAX_MB = 50;
93
+
94
+ function removeSizeWarning() {
95
+ var w = document.getElementById('uploadSizeWarning');
96
+ if (w) w.remove();
97
+ }
98
+
99
+ function showSizeWarning(sizeMB) {
100
+ removeSizeWarning();
101
+ var el = document.createElement('div');
102
+ el.id = 'uploadSizeWarning';
103
+ el.style.cssText = 'margin-top:14px;padding:14px 18px;border-radius:12px;background:rgba(251,191,36,0.08);border:1px solid rgba(251,191,36,0.35);color:#fde68a;font-size:0.88rem;line-height:1.6;display:flex;align-items:flex-start;gap:12px;';
104
+ el.innerHTML =
105
+ '<span style="font-size:1.5rem;flex-shrink:0;line-height:1.2">πŸ˜…</span>' +
106
+ '<div><strong>File too large (' + sizeMB.toFixed(1) + ' MB)</strong><br>' +
107
+ 'Because of free-tier limitations, we can\'t go further than <strong>' + MAX_MB + ' MB</strong> per upload. ' +
108
+ 'Please split your scan into smaller batches and try again.</div>';
109
+ var active = document.querySelector('.tab-panel.active');
110
+ if (active && active.parentNode) {
111
+ active.parentNode.insertBefore(el, active.nextSibling);
112
+ }
113
+ }
114
+
115
+ function attachSizeCheck(inputId, submitId, formId, overlayId) {
116
+ var inp = document.getElementById(inputId);
117
+ var sub = document.getElementById(submitId);
118
+ var frm = document.getElementById(formId);
119
+ var ov = document.getElementById(overlayId);
120
+ if (!inp || !frm) return;
121
+ frm.addEventListener('submit', function (e) {
122
+ var totalMB = 0;
123
+ for (var i = 0; i < inp.files.length; i++) { totalMB += inp.files[i].size / (1024 * 1024); }
124
+ if (totalMB > MAX_MB) {
125
+ e.preventDefault();
126
+ if (sub) sub.disabled = false;
127
+ if (ov) ov.style.display = 'none';
128
+ showSizeWarning(totalMB);
129
+ } else {
130
+ removeSizeWarning();
131
+ }
132
+ });
133
+ }
134
+
135
  function initUploadPage() {
136
  var tabs = document.querySelectorAll('.upload-tab');
137
  var panels = document.querySelectorAll('.tab-panel');
 
142
 
143
  tabs.forEach(function (tab) {
144
  tab.addEventListener('click', function () {
145
+ tabs.forEach(function (item) { item.classList.remove('active'); });
146
+ panels.forEach(function (panel) { panel.classList.remove('active'); });
 
 
 
 
 
147
  tab.classList.add('active');
148
  var target = document.getElementById('tab-' + tab.dataset.tab);
149
+ if (target) target.classList.add('active');
150
+ removeSizeWarning();
 
151
  });
152
  });
153
 
154
  wireDropzone({
155
+ zoneId: 'dropzoneSingle', inputId: 'singleInput', infoId: 'singleInfo',
156
+ labelId: 'singleFileName', clearSel: '.js-clear-single',
157
+ submitId: 'singleSubmit', formId: 'singleForm', overlayId: 'singleOverlay',
 
 
 
 
 
158
  multi: false
159
  });
160
 
161
  wireDropzone({
162
+ zoneId: 'dropzoneMulti', inputId: 'multiInput', infoId: 'multiInfo',
163
+ labelId: 'multiFileName', clearSel: '.js-clear-multi',
164
+ submitId: 'multiSubmit', formId: 'multiForm', overlayId: 'multiOverlay',
 
 
 
 
 
165
  multi: true
166
  });
167
 
168
+ attachSizeCheck('singleInput', 'singleSubmit', 'singleForm', 'singleOverlay');
169
+ attachSizeCheck('multiInput', 'multiSubmit', 'multiForm', 'multiOverlay');
170
+
171
  var dirInput = document.getElementById('dirPath');
172
  var dirSubmit = document.getElementById('dirSubmit');
 
173
  if (dirInput && dirSubmit) {
174
+ function checkDir() { dirSubmit.disabled = !dirInput.value.trim(); }
 
 
 
175
  dirInput.addEventListener('input', checkDir);
176
  checkDir();
177
  }
templates/landing.html ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" />
7
+ <title>ICH Detection Pipeline β€” AI-Powered CT Brain Scan Analysis</title>
8
+ <meta name="description"
9
+ content="Clinical-grade intracranial hemorrhage detection using deep learning. Upload DICOM CT scans and get instant AI-powered screening with Grad-CAM heatmaps and triage reports." />
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
13
+ rel="stylesheet" />
14
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/landing.css') }}" />
15
+ </head>
16
+
17
+ <body>
18
+
19
+ <!-- ── Topbar ────────────────────────────────────────────────── -->
20
+ <header class="topbar">
21
+ <a class="brand" href="#">
22
+ <div class="brand-icon">
23
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
24
+ stroke-linecap="round">
25
+ <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
26
+ </svg>
27
+ </div>
28
+ <span class="brand-name">ICH <span>Pipeline</span></span>
29
+ </a>
30
+ <nav class="nav-actions">
31
+ <a href="{{ url_for('auth.login') }}" class="btn-ghost">Sign In</a>
32
+ <a href="{{ url_for('auth.register') }}" class="btn-primary">
33
+ Get Started
34
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
35
+ <line x1="5" y1="12" x2="19" y2="12" />
36
+ <polyline points="12 5 19 12 12 19" />
37
+ </svg>
38
+ </a>
39
+ </nav>
40
+ </header>
41
+
42
+ <!-- ── Hero ──────────────────────────────────────────────────── -->
43
+ <section class="hero">
44
+ <div class="hero-inner">
45
+ <div class="hero-badge">
46
+ <div class="badge-dot"></div>
47
+ AI-Powered Medical Screening
48
+ </div>
49
+
50
+ <h1>
51
+ Detect Brain Hemorrhage<br>
52
+ <span class="grad">In Seconds, Not Hours</span>
53
+ </h1>
54
+
55
+ <p class="hero-sub">
56
+ Upload a DICOM CT brain scan and get an AI-powered hemorrhage probability score,
57
+ Grad-CAM heatmap visualization, and automated clinical triage report β€” instantly.
58
+ </p>
59
+
60
+ <div class="hero-ctas">
61
+ <a href="{{ url_for('auth.register') }}" class="btn-hero btn-hero-primary">
62
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
63
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
64
+ <polyline points="17 8 12 3 7 8" />
65
+ <line x1="12" y1="3" x2="12" y2="15" />
66
+ </svg>
67
+ Start Screening Free
68
+ </a>
69
+ <a href="{{ url_for('auth.login') }}" class="btn-hero btn-hero-secondary">
70
+ Sign In to Dashboard
71
+ </a>
72
+ </div>
73
+
74
+ <div class="trust-bar">
75
+ <span class="trust-item">
76
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
77
+ <polyline points="20 6 9 17 4 12" />
78
+ </svg>
79
+ EfficientNet-B4 Model
80
+ </span>
81
+ <span class="trust-item">
82
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
83
+ <polyline points="20 6 9 17 4 12" />
84
+ </svg>
85
+ Grad-CAM Heatmaps
86
+ </span>
87
+ <span class="trust-item">
88
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
89
+ <polyline points="20 6 9 17 4 12" />
90
+ </svg>
91
+ Calibrated Confidence Scores
92
+ </span>
93
+ <span class="trust-item">
94
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
95
+ <polyline points="20 6 9 17 4 12" />
96
+ </svg>
97
+ Automated Triage
98
+ </span>
99
+ </div>
100
+ </div>
101
+ </section>
102
+
103
+ <!-- ── Stats ─────────────────────────────────────────────────── -->
104
+ <div class="stats-strip">
105
+ <div class="stats-inner">
106
+ <div class="stat-item">
107
+ <div class="stat-num">~90%</div>
108
+ <div class="stat-desc">Sensitivity on ICH-positive slices</div>
109
+ </div>
110
+ <div class="stat-item">
111
+ <div class="stat-num">&lt; 30s</div>
112
+ <div class="stat-desc">Time to first result per scan</div>
113
+ </div>
114
+ <div class="stat-item">
115
+ <div class="stat-num">DICOM</div>
116
+ <div class="stat-desc">Native .dcm &amp; .zip batch support</div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- ── Features ────────────��─────────────────────────────────── -->
122
+ <section class="features">
123
+ <div class="section-label">Core Capabilities</div>
124
+ <h2 class="section-title">Everything a radiologist needs β€” fast</h2>
125
+ <p class="section-sub">From raw DICOM upload to clinical-grade report in under a minute.</p>
126
+
127
+ <div class="features-grid">
128
+
129
+ <div class="feat-card">
130
+ <div class="feat-icon">
131
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
132
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
133
+ <polyline points="17 8 12 3 7 8" />
134
+ <line x1="12" y1="3" x2="12" y2="15" />
135
+ </svg>
136
+ </div>
137
+ <h3>Batch DICOM Processing</h3>
138
+ <p>Upload a .dcm file or a .zip archive of hundreds of slices. The pipeline handles CT windowing, preprocessing,
139
+ and inference on every slice automatically.</p>
140
+ </div>
141
+
142
+ <div class="feat-card">
143
+ <div class="feat-icon" style="background:rgba(52,211,153,0.1);border-color:rgba(52,211,153,0.2);color:#34d399;">
144
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
145
+ <circle cx="12" cy="12" r="10" />
146
+ <path d="M12 8v4l3 3" />
147
+ </svg>
148
+ </div>
149
+ <h3>Calibrated AI Confidence</h3>
150
+ <p>Our EfficientNet-B4 model outputs probability scores calibrated with temperature scaling, so a 90% score
151
+ actually means 90%.</p>
152
+ </div>
153
+
154
+ <div class="feat-card">
155
+ <div class="feat-icon"
156
+ style="background:rgba(251,113,133,0.1);border-color:rgba(251,113,133,0.2);color:#fb7185;">
157
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
158
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
159
+ <circle cx="12" cy="12" r="3" />
160
+ </svg>
161
+ </div>
162
+ <h3>Grad-CAM Heatmaps</h3>
163
+ <p>Gradient-weighted class activation maps overlay on every scan, highlighting the exact regions that drove the
164
+ model's hemorrhage prediction.</p>
165
+ </div>
166
+
167
+ <div class="feat-card">
168
+ <div class="feat-icon"
169
+ style="background:rgba(129,140,248,0.1);border-color:rgba(129,140,248,0.2);color:#818cf8;">
170
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
171
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
172
+ <polyline points="14 2 14 8 20 8" />
173
+ </svg>
174
+ </div>
175
+ <h3>LLM Clinical Summary</h3>
176
+ <p>Each scan triggers a Groq-powered LLM that generates a human-readable clinical narrative with triage action
177
+ and urgency classification.</p>
178
+ </div>
179
+
180
+ <div class="feat-card">
181
+ <div class="feat-icon" style="background:rgba(251,191,36,0.1);border-color:rgba(251,191,36,0.2);color:#fbbf24;">
182
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
183
+ <line x1="18" y1="20" x2="18" y2="10" />
184
+ <line x1="12" y1="20" x2="12" y2="4" />
185
+ <line x1="6" y1="20" x2="6" y2="14" />
186
+ </svg>
187
+ </div>
188
+ <h3>Model Evaluation Metrics</h3>
189
+ <p>Built-in evaluation dashboard with ROC curves, calibration plots, and confidence band analysis to track model
190
+ performance over time.</p>
191
+ </div>
192
+
193
+ <div class="feat-card">
194
+ <div class="feat-icon" style="background:rgba(52,211,153,0.1);border-color:rgba(52,211,153,0.2);color:#34d399;">
195
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
196
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
197
+ <polyline points="22 4 12 14.01 9 11.01" />
198
+ </svg>
199
+ </div>
200
+ <h3>Secure &amp; Isolated</h3>
201
+ <p>Every user gets their own isolated data directory. All uploads are user-scoped and access-controlled with
202
+ rate limiting and audit logs.</p>
203
+ </div>
204
+
205
+ </div>
206
+ </section>
207
+
208
+ <!-- ── How it works ───────────────────────────────────────────── -->
209
+ <section class="how">
210
+ <div class="section-label">Workflow</div>
211
+ <h2 class="section-title">Four steps to a clinical report</h2>
212
+ <p class="section-sub" style="margin-bottom:48px;">The entire pipeline runs in the background β€” you just upload and
213
+ wait.</p>
214
+
215
+ <div class="steps">
216
+ <div class="step">
217
+ <div class="step-num">1</div>
218
+ <h4>Upload DICOM</h4>
219
+ <p>Upload a .dcm slice or a .zip batch. Single exams or full series are both supported.</p>
220
+ </div>
221
+ <div class="step">
222
+ <div class="step-num">2</div>
223
+ <h4>AI Inference</h4>
224
+ <p>EfficientNet-B4 scores each slice for ICH probability with calibrated confidence.</p>
225
+ </div>
226
+ <div class="step">
227
+ <div class="step-num">3</div>
228
+ <h4>Grad-CAM</h4>
229
+ <p>Gradient-weighted heatmaps highlight regions driving the hemorrhage prediction.</p>
230
+ </div>
231
+ <div class="step">
232
+ <div class="step-num">4</div>
233
+ <h4>Clinical Report</h4>
234
+ <p>Auto-generated PDF report with findings, confidence bands, and triage action.</p>
235
+ </div>
236
+ </div>
237
+ </section>
238
+
239
+ <!-- ── CTA ───────────────────────────────────────────────────── -->
240
+ <section class="cta-section">
241
+ <h2>Ready to try it?</h2>
242
+ <p>Create a free account and upload your first scan in under a minute.</p>
243
+
244
+ <div class="disclaimer">
245
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
246
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
247
+ <line x1="12" y1="9" x2="12" y2="13" />
248
+ <line x1="12" y1="17" x2="12.01" y2="17" />
249
+ </svg>
250
+ <span>
251
+ <strong>Medical Disclaimer:</strong> This is an AI-assisted screening tool and does <strong>not</strong>
252
+ constitute a medical diagnosis. All findings must be reviewed by a qualified medical professional
253
+ before any clinical action is taken.
254
+ </span>
255
+ </div>
256
+
257
+ <div style="display:flex;gap:14px;justify-content:center;flex-wrap:wrap;">
258
+ <a href="{{ url_for('auth.register') }}" class="btn-hero btn-hero-primary">
259
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
260
+ <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
261
+ <circle cx="8.5" cy="7" r="4" />
262
+ <line x1="20" y1="8" x2="20" y2="14" />
263
+ <line x1="23" y1="11" x2="17" y2="11" />
264
+ </svg>
265
+ Create Free Account
266
+ </a>
267
+ <a href="{{ url_for('auth.login') }}" class="btn-hero btn-hero-secondary">Sign In</a>
268
+ </div>
269
+ </section>
270
+
271
+ <!-- ── Footer ────────────────────────────────────────────────── -->
272
+ <footer>
273
+ <p>ICH Detection Pipeline &mdash; AI screening tool, not a diagnostic device.</p>
274
+ <p style="margin-top:6px;font-size:0.78rem;opacity:0.6;">All findings must be reviewed by a qualified medical
275
+ professional.</p>
276
+ </footer>
277
+
278
+ <script src="{{ url_for('static', filename='js/landing.js') }}" defer></script>
279
+ </body>
280
+
281
+ </html>
templates/upload.html CHANGED
@@ -30,10 +30,10 @@
30
 
31
  <!-- ── Tab navigation ──────────────────────────────────────────────── -->
32
  <div class="upload-tabs" role="tablist">
33
- <button class="upload-tab active" data-tab="single" role="tab">Single File</button>
34
- <button class="upload-tab" data-tab="multi" role="tab">Multi-File / ZIP</button>
35
  {% if local_mode %}
36
- <button class="upload-tab" data-tab="dirscan" role="tab">Scan Directory</button>
37
  {% endif %}
38
  </div>
39
 
@@ -41,13 +41,11 @@
41
  <!-- TAB 1 β€” Single .dcm file -->
42
  <!-- ════════════════════════════════════════════════════════════════════ -->
43
  <section class="panel upload-panel tab-panel active" id="tab-single">
44
- <form method="post" action="{{ url_for('analyze') }}"
45
- enctype="multipart/form-data" id="singleForm">
46
 
47
  <div class="dropzone" id="dropzoneSingle">
48
- <svg width="56" height="56" viewBox="0 0 24 24" fill="none"
49
- stroke="currentColor" stroke-width="1.5"
50
- stroke-linecap="round" stroke-linejoin="round">
51
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
52
  <polyline points="17 8 12 3 7 8" />
53
  <line x1="12" y1="3" x2="12" y2="15" />
@@ -58,8 +56,7 @@
58
  </div>
59
 
60
  <div class="file-info" id="singleInfo" style="display: none">
61
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none"
62
- stroke="currentColor" stroke-width="2">
63
  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
64
  <polyline points="14 2 14 8 20 8" />
65
  </svg>
@@ -68,8 +65,7 @@
68
  </div>
69
 
70
  <button type="submit" class="btn btn-primary" id="singleSubmit" disabled>
71
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
72
- stroke="currentColor" stroke-width="2">
73
  <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
74
  </svg>
75
  Analyze Scan
@@ -87,25 +83,21 @@
87
  <!-- TAB 2 β€” Multi-file / ZIP upload -->
88
  <!-- ════════════════════════════════════════════════════════════════════ -->
89
  <section class="panel upload-panel tab-panel" id="tab-multi">
90
- <form method="post" action="{{ url_for('analyze') }}"
91
- enctype="multipart/form-data" id="multiForm">
92
 
93
  <div class="dropzone" id="dropzoneMulti">
94
- <svg width="56" height="56" viewBox="0 0 24 24" fill="none"
95
- stroke="currentColor" stroke-width="1.5"
96
- stroke-linecap="round" stroke-linejoin="round">
97
  <rect x="2" y="7" width="20" height="14" rx="2" />
98
  <path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
99
  </svg>
100
  <p class="dropzone-text">Drag &amp; drop .dcm files or a .zip archive</p>
101
  <p class="muted small">Select multiple files, or a single .zip containing DICOM slices</p>
102
- <input type="file" name="file" id="multiInput"
103
- accept=".dcm,.zip" multiple hidden />
104
  </div>
105
 
106
  <div class="file-info" id="multiInfo" style="display: none">
107
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none"
108
- stroke="currentColor" stroke-width="2">
109
  <rect x="2" y="7" width="20" height="14" rx="2" />
110
  <path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
111
  </svg>
@@ -114,8 +106,7 @@
114
  </div>
115
 
116
  <button type="submit" class="btn btn-primary" id="multiSubmit" disabled>
117
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
118
- stroke="currentColor" stroke-width="2">
119
  <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
120
  </svg>
121
  Analyze Batch
@@ -139,12 +130,10 @@
139
  Server-side directory containing .dcm files
140
  </label>
141
  <div class="dir-input-row">
142
- <input type="text" name="dir_path" id="dirPath" class="input"
143
- placeholder="D:\scans\patient_001"
144
- spellcheck="false" autocomplete="off" />
145
  <button type="submit" class="btn btn-primary" id="dirSubmit">
146
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
147
- stroke="currentColor" stroke-width="2">
148
  <circle cx="11" cy="11" r="8" />
149
  <line x1="21" y1="21" x2="16.65" y2="16.65" />
150
  </svg>
@@ -182,7 +171,7 @@
182
  <div class="step-num">3</div>
183
  <div class="step-text">
184
  <strong>Analyze</strong>
185
- <p class="muted small">EfficientNet-B0 model with calibrated scoring</p>
186
  </div>
187
  </div>
188
  <div class="step">
@@ -198,4 +187,4 @@
198
 
199
  {% block scripts %}
200
  <script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
201
- {% endblock %}
 
30
 
31
  <!-- ── Tab navigation ──────────────────────────────────────────────── -->
32
  <div class="upload-tabs" role="tablist">
33
+ <button class="upload-tab active" data-tab="single" role="tab">Single File</button>
34
+ <button class="upload-tab" data-tab="multi" role="tab">Multi-File / ZIP</button>
35
  {% if local_mode %}
36
+ <button class="upload-tab" data-tab="dirscan" role="tab">Scan Directory</button>
37
  {% endif %}
38
  </div>
39
 
 
41
  <!-- TAB 1 β€” Single .dcm file -->
42
  <!-- ════════════════════════════════════════════════════════════════════ -->
43
  <section class="panel upload-panel tab-panel active" id="tab-single">
44
+ <form method="post" action="{{ url_for('analyze') }}" enctype="multipart/form-data" id="singleForm">
 
45
 
46
  <div class="dropzone" id="dropzoneSingle">
47
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
48
+ stroke-linecap="round" stroke-linejoin="round">
 
49
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
50
  <polyline points="17 8 12 3 7 8" />
51
  <line x1="12" y1="3" x2="12" y2="15" />
 
56
  </div>
57
 
58
  <div class="file-info" id="singleInfo" style="display: none">
59
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
60
  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
61
  <polyline points="14 2 14 8 20 8" />
62
  </svg>
 
65
  </div>
66
 
67
  <button type="submit" class="btn btn-primary" id="singleSubmit" disabled>
68
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
69
  <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
70
  </svg>
71
  Analyze Scan
 
83
  <!-- TAB 2 β€” Multi-file / ZIP upload -->
84
  <!-- ════════════════════════════════════════════════════════════════════ -->
85
  <section class="panel upload-panel tab-panel" id="tab-multi">
86
+ <form method="post" action="{{ url_for('analyze') }}" enctype="multipart/form-data" id="multiForm">
 
87
 
88
  <div class="dropzone" id="dropzoneMulti">
89
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
90
+ stroke-linecap="round" stroke-linejoin="round">
 
91
  <rect x="2" y="7" width="20" height="14" rx="2" />
92
  <path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
93
  </svg>
94
  <p class="dropzone-text">Drag &amp; drop .dcm files or a .zip archive</p>
95
  <p class="muted small">Select multiple files, or a single .zip containing DICOM slices</p>
96
+ <input type="file" name="file" id="multiInput" accept=".dcm,.zip" multiple hidden />
 
97
  </div>
98
 
99
  <div class="file-info" id="multiInfo" style="display: none">
100
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
101
  <rect x="2" y="7" width="20" height="14" rx="2" />
102
  <path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
103
  </svg>
 
106
  </div>
107
 
108
  <button type="submit" class="btn btn-primary" id="multiSubmit" disabled>
109
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
110
  <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
111
  </svg>
112
  Analyze Batch
 
130
  Server-side directory containing .dcm files
131
  </label>
132
  <div class="dir-input-row">
133
+ <input type="text" name="dir_path" id="dirPath" class="input" placeholder="D:\scans\patient_001"
134
+ spellcheck="false" autocomplete="off" />
 
135
  <button type="submit" class="btn btn-primary" id="dirSubmit">
136
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
137
  <circle cx="11" cy="11" r="8" />
138
  <line x1="21" y1="21" x2="16.65" y2="16.65" />
139
  </svg>
 
171
  <div class="step-num">3</div>
172
  <div class="step-text">
173
  <strong>Analyze</strong>
174
+ <p class="muted small">EfficientNet-B4 model with calibrated scoring</p>
175
  </div>
176
  </div>
177
  <div class="step">
 
187
 
188
  {% block scripts %}
189
  <script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
190
+ {% endblock %}