Sanjana Vekhande commited on
Commit
ea7fb35
·
1 Parent(s): d94bead

redesign: 2x2 single-page layout, copyright top, 16px font, FEA→MeMo

Browse files
Files changed (1) hide show
  1. frontend/index.html +231 -380
frontend/index.html CHANGED
@@ -7,169 +7,171 @@
7
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"/>
8
  <style>
9
  :root {
10
- --bg: #EEF1F8;
11
- --surface: #FFFFFF;
12
- --border: #D8DDED;
13
- --ink: #0A0E1F;
14
- --ink2: #4A5270;
15
- --ink3: #8A91AD;
16
- --teal: #4F46E5;
17
- --teal-l: #EFEEFD;
18
- --teal-d: #3730A3;
19
- --coral: #7C3AED;
20
- --coral-l: #F5F3FF;
21
- --r: 10px;
22
- --shadow: 0 1px 4px rgba(10,14,31,0.07);
23
- --shadow-md:0 4px 16px rgba(10,14,31,0.12);
24
  }
25
-
26
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
-
28
  body {
29
  background: var(--bg);
30
  color: var(--ink);
31
  font-family: 'DM Sans', -apple-system, sans-serif;
32
  font-size: 16px;
33
- line-height: 1.65;
34
- min-height: 100vh;
35
  }
36
 
37
- /* ── Page ───────────────────────────── */
38
  .page {
39
- max-width: 700px;
40
  margin: 0 auto;
41
- padding: 4rem 1.5rem 6rem;
 
 
 
42
  }
43
 
44
- /* ── Header ─────────────────────────── */
45
- header {
46
- margin-bottom: 3rem;
47
- position: relative;
 
 
 
48
  }
 
 
49
 
 
 
 
 
 
 
 
 
 
50
  .header-tag {
51
- display: inline-block;
52
- font-family: 'DM Sans', sans-serif;
53
  font-size: 11px;
54
  font-weight: 600;
55
  letter-spacing: 0.12em;
56
  text-transform: uppercase;
57
  color: var(--coral);
58
- margin-bottom: 0.75rem;
 
59
  }
60
-
61
  header h1 {
62
  font-family: 'Playfair Display', Georgia, serif;
63
- font-size: 3rem;
64
  font-weight: 700;
65
- line-height: 1.1;
66
- letter-spacing: -0.01em;
67
  color: var(--ink);
68
- margin-bottom: 0.3rem;
69
  }
70
-
71
  header h1 em {
72
  font-style: italic;
73
  font-weight: 400;
74
  color: var(--teal);
75
  }
76
-
 
 
 
 
 
77
  .header-sub {
78
- font-size: 14px;
79
  color: var(--ink2);
80
- line-height: 1.7;
81
- max-width: 480px;
82
- margin-top: 0.75rem;
83
  }
84
 
85
- .header-rule {
86
- width: 48px;
87
- height: 3px;
88
- background: var(--coral);
89
- border-radius: 2px;
90
- margin-top: 1.5rem;
91
  }
92
 
93
- /* ── Section title ───────────────────── */
94
- .section-label {
95
- font-size: 10px;
 
 
 
 
 
 
 
96
  font-weight: 600;
97
- letter-spacing: 0.14em;
98
  text-transform: uppercase;
99
  color: var(--ink3);
100
- margin-bottom: 1.5rem;
101
- margin-top: 2.5rem;
102
  display: flex;
103
  align-items: center;
104
- gap: 10px;
105
  }
106
-
107
- .section-label::after {
108
  content: '';
109
  flex: 1;
110
  height: 1px;
111
  background: var(--border);
112
  }
113
 
114
- /* ── Optional block ──────────────────── */
115
- .opt-block {
116
- background: var(--surface);
117
- border: 1px solid var(--border);
118
- border-radius: var(--r);
119
- padding: 1.25rem 1.5rem;
120
- box-shadow: var(--shadow);
121
- margin-bottom: 2.5rem;
122
- display: flex;
123
- gap: 1.25rem;
124
- align-items: flex-start;
125
- }
126
-
127
  .opt-tag {
128
  background: var(--coral-l);
129
  color: var(--coral);
130
  border: 1px solid #E8C4B3;
131
  font-size: 9px;
132
- font-weight: 700;
133
  letter-spacing: 0.1em;
134
  text-transform: uppercase;
135
  padding: 3px 7px;
136
  border-radius: 4px;
137
- margin-top: 3px;
138
- white-space: nowrap;
139
  }
140
-
141
- .opt-inner { flex: 1; }
142
-
143
  .opt-title {
144
  font-size: 16px;
145
  font-weight: 600;
146
  color: var(--ink);
147
- margin-bottom: 0.15rem;
148
  }
149
-
150
  .opt-desc {
151
- font-size: 14px;
152
  color: var(--ink2);
153
  margin-bottom: 0.85rem;
 
154
  }
155
 
156
- /* ── Slider ──────────────────────────── */
157
  .slider-row {
158
  display: flex;
159
  align-items: center;
160
  gap: 1rem;
161
  }
162
-
163
  .slider-val {
164
  font-family: 'Playfair Display', serif;
165
- font-size: 1.5rem;
166
  font-weight: 700;
167
  color: var(--teal);
168
  min-width: 58px;
169
  text-align: right;
170
  letter-spacing: -0.02em;
171
  }
172
-
173
  input[type="range"] {
174
  flex: 1;
175
  -webkit-appearance: none;
@@ -179,7 +181,6 @@ input[type="range"] {
179
  outline: none;
180
  cursor: pointer;
181
  }
182
-
183
  input[type="range"]::-webkit-slider-thumb {
184
  -webkit-appearance: none;
185
  width: 18px; height: 18px;
@@ -188,157 +189,89 @@ input[type="range"]::-webkit-slider-thumb {
188
  cursor: pointer;
189
  border: 3px solid var(--surface);
190
  box-shadow: 0 0 0 2px var(--teal);
191
- transition: box-shadow 0.15s;
192
- }
193
-
194
- input[type="range"]::-webkit-slider-thumb:hover {
195
- box-shadow: 0 0 0 4px rgba(45,106,106,0.2);
196
  }
197
-
198
  .slider-labels {
199
  display: flex;
200
  justify-content: space-between;
201
- font-size: 11px;
202
  color: var(--ink3);
203
  margin-top: 0.4rem;
204
  }
205
-
206
  .slider-note {
207
- font-size: 11px;
208
  color: var(--ink3);
209
  margin-top: 0.5rem;
210
  }
211
 
212
- /* ── Step row ────────────────────────── */
213
- .step {
214
- display: flex;
215
- gap: 1.25rem;
216
- margin-bottom: 1.25rem;
217
- animation: fadeUp 0.35s ease both;
218
- }
219
-
220
- .step:nth-child(1) { animation-delay: 0.04s; }
221
- .step:nth-child(2) { animation-delay: 0.08s; }
222
- .step:nth-child(3) { animation-delay: 0.12s; }
223
- .step:nth-child(4) { animation-delay: 0.16s; }
224
-
225
  .step-num {
226
- width: 30px; height: 30px;
227
- min-width: 30px;
228
  border-radius: 7px;
229
  background: transparent;
230
  border: 2px solid var(--ink);
231
  color: var(--ink);
232
  font-size: 13px;
233
  font-weight: 700;
234
- display: flex;
235
- align-items: center;
236
- justify-content: center;
237
- margin-top: 2px;
238
  font-family: 'DM Sans', sans-serif;
 
239
  transition: all 0.2s;
240
  }
241
-
242
  .step-num.done {
243
  background: var(--teal);
244
  border-color: var(--teal);
245
  color: #fff;
246
  }
247
-
248
- .step-content { flex: 1; }
249
-
250
  .step-title {
251
  font-size: 16px;
252
  font-weight: 600;
253
  color: var(--ink);
254
  margin-bottom: 0.2rem;
255
  }
256
-
257
  .step-desc {
258
- font-size: 14px;
259
  color: var(--ink2);
260
  margin-bottom: 0.75rem;
261
  line-height: 1.5;
262
  }
263
 
264
- /* ── Card ────────────────────────────── */
265
- .card {
266
- background: var(--surface);
267
- border: 1px solid var(--border);
268
- border-radius: var(--r);
269
- padding: 1.1rem 1.25rem;
270
- box-shadow: var(--shadow);
271
- }
272
-
273
- /* ── Drop zone ───────────────────────── */
274
  .drop-zone {
275
  border: 1.5px dashed var(--border);
276
  border-radius: 8px;
277
- padding: 2rem 1.5rem;
278
  text-align: center;
279
  cursor: pointer;
280
  position: relative;
281
  transition: all 0.15s;
282
  background: var(--bg);
283
  }
284
-
285
  .drop-zone:hover {
286
  border-color: var(--teal);
287
  background: var(--teal-l);
288
  }
289
-
290
  .drop-zone input[type="file"] {
291
  position: absolute; inset: 0;
292
  opacity: 0; cursor: pointer;
293
  width: 100%; height: 100%;
294
  }
 
 
 
 
295
 
296
- .drop-icon {
297
- font-size: 1.5rem;
298
- margin-bottom: 0.4rem;
299
- display: block;
300
- }
301
-
302
- .drop-text {
303
- font-size: 15px;
304
- color: var(--ink2);
305
- line-height: 1.6;
306
- }
307
-
308
- .drop-text strong {
309
- color: var(--ink);
310
- display: block;
311
- font-weight: 600;
312
- }
313
-
314
- .drop-text small {
315
- color: var(--ink3);
316
- font-size: 11px;
317
- margin-top: 2px;
318
- display: block;
319
- }
320
-
321
- /* ── File chips ──────────────────────── */
322
- .file-chips {
323
- display: flex;
324
- flex-wrap: wrap;
325
- gap: 0.4rem;
326
- margin-top: 0.65rem;
327
- }
328
-
329
  .chip {
330
- display: inline-flex;
331
- align-items: center;
332
- gap: 5px;
333
  background: var(--teal-l);
334
  border: 1px solid #B2D4D4;
335
  color: var(--teal-d);
336
  padding: 3px 10px;
337
  border-radius: 100px;
338
- font-size: 12px;
339
  font-weight: 500;
340
  }
341
-
342
  .mismatch-warning {
343
  display: flex;
344
  align-items: flex-start;
@@ -348,19 +281,27 @@ input[type="range"]::-webkit-slider-thumb:hover {
348
  border-radius: 8px;
349
  padding: 0.75rem 1rem;
350
  margin-top: 0.75rem;
351
- font-size: 13px;
352
  color: #9A3412;
353
  line-height: 1.5;
354
  }
355
 
356
- /* ── Run section ─────────────────────��─ */
357
- .run-section {
358
- margin-top: 2.5rem;
 
 
 
 
 
 
 
 
 
359
  }
360
-
361
  .btn-run {
362
- width: 100%;
363
- padding: 1rem 1.25rem;
364
  background: var(--ink);
365
  color: var(--bg);
366
  border: none;
@@ -370,44 +311,33 @@ input[type="range"]::-webkit-slider-thumb:hover {
370
  font-weight: 600;
371
  cursor: pointer;
372
  transition: all 0.15s;
373
- letter-spacing: 0.01em;
374
- display: flex;
375
- align-items: center;
376
- justify-content: center;
377
- gap: 8px;
378
  }
379
-
380
  .btn-run:hover:not(:disabled) {
381
  background: var(--teal);
382
  box-shadow: var(--shadow-md);
383
  transform: translateY(-1px);
384
  }
 
385
 
386
- .btn-run:disabled {
387
- opacity: 0.4;
388
- cursor: not-allowed;
389
- transform: none;
 
 
 
 
 
 
390
  }
391
-
392
- /* ── Progress ────────────────────────── */
393
  .progress-header {
394
- display: flex;
395
- justify-content: space-between;
396
- align-items: center;
397
- font-size: 13px;
398
- margin-bottom: 0.5rem;
399
  }
400
-
401
  .progress-label { font-weight: 600; color: var(--ink); }
402
  .progress-pct { color: var(--ink2); font-variant-numeric: tabular-nums; }
403
-
404
- .progress-track {
405
- height: 5px;
406
- background: var(--border);
407
- border-radius: 3px;
408
- overflow: hidden;
409
- }
410
-
411
  .progress-fill {
412
  height: 100%;
413
  background: linear-gradient(90deg, var(--teal), #4A9F9F);
@@ -415,89 +345,66 @@ input[type="range"]::-webkit-slider-thumb:hover {
415
  transition: width 0.45s ease;
416
  width: 0%;
417
  }
418
-
419
  .log-box { display: none; }
420
 
421
- /* ── Results ─────────────────────────── */
422
- .stats-row {
423
- display: grid;
424
- grid-template-columns: 1fr 1fr;
425
- gap: 0.65rem;
426
- margin-bottom: 1rem;
 
 
 
 
 
427
  }
428
-
429
  .stat-box {
430
  background: var(--bg);
431
  border: 1px solid var(--border);
432
  border-radius: 8px;
433
- padding: 1rem 1.1rem;
434
  }
435
-
436
  .stat-val {
437
  font-family: 'Playfair Display', serif;
438
- font-size: 2rem;
439
  font-weight: 700;
440
  color: var(--ink);
441
  line-height: 1;
442
- margin-bottom: 0.25rem;
443
  letter-spacing: -0.02em;
444
  }
445
-
446
  .stat-label {
447
- font-size: 11px;
448
  color: var(--ink2);
449
  font-weight: 500;
450
  text-transform: uppercase;
451
- letter-spacing: 0.07em;
452
  }
453
-
454
  .btn-download {
455
- display: flex;
456
- align-items: center;
457
- justify-content: center;
458
- gap: 8px;
459
- width: 100%;
460
- padding: 0.9rem 1.25rem;
461
  background: var(--teal-l);
462
  color: var(--teal-d);
463
  border: 1.5px solid var(--teal);
464
  border-radius: var(--r);
465
  font-family: 'DM Sans', sans-serif;
466
- font-size: 14px;
467
- font-weight: 600;
468
- cursor: pointer;
469
- text-decoration: none;
470
  transition: all 0.15s;
471
  }
472
-
473
  .btn-download:hover:not(:disabled) {
474
  background: var(--teal);
475
  color: white;
476
  transform: translateY(-1px);
477
  box-shadow: var(--shadow-md);
478
  }
 
479
 
480
- .btn-download:disabled {
481
- opacity: 0.7;
482
- cursor: not-allowed;
483
- transform: none;
484
- }
485
-
486
- /* Download progress bar */
487
- .download-progress-wrap {
488
- margin-top: 0.65rem;
489
- display: none;
490
- }
491
-
492
  .download-progress-wrap.show { display: block; }
493
-
494
- .download-progress-track {
495
- height: 4px;
496
- background: var(--border);
497
- border-radius: 2px;
498
- overflow: hidden;
499
- }
500
-
501
  .download-progress-fill {
502
  height: 100%;
503
  background: linear-gradient(90deg, var(--teal), #6B8FCF);
@@ -505,47 +412,19 @@ input[type="range"]::-webkit-slider-thumb:hover {
505
  width: 0%;
506
  transition: width 0.3s ease;
507
  }
508
-
509
- .download-progress-label {
510
- font-size: 12px;
511
- color: var(--ink2);
512
- margin-top: 0.3rem;
513
- text-align: center;
514
- }
515
-
516
  @keyframes indeterminate {
517
  0% { left: -40%; width: 40%; }
518
  60% { left: 100%; width: 40%; }
519
  100% { left: 100%; width: 40%; }
520
  }
521
-
522
  .download-progress-fill.indeterminate {
523
  position: relative;
524
  width: 40%;
525
  animation: indeterminate 1.4s ease infinite;
526
  }
527
 
528
- /* ── Footer ──────────────────────────── */
529
- footer {
530
- margin-top: 4rem;
531
- padding-top: 1.5rem;
532
- border-top: 1px solid var(--border);
533
- font-size: 11px;
534
- color: var(--ink3);
535
- line-height: 1.9;
536
- }
537
-
538
- footer a { color: var(--ink2); text-decoration: none; }
539
- footer a:hover { color: var(--ink); }
540
-
541
- /* ── Animations ──────────────────────── */
542
- @keyframes fadeUp {
543
- from { opacity: 0; transform: translateY(10px); }
544
- to { opacity: 1; transform: translateY(0); }
545
- }
546
-
547
  .hidden { display: none !important; }
548
-
549
  ::-webkit-scrollbar { width: 5px; }
550
  ::-webkit-scrollbar-track { background: transparent; }
551
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
@@ -554,18 +433,30 @@ footer a:hover { color: var(--ink); }
554
  <body>
555
  <div class="page">
556
 
 
 
 
 
 
 
 
557
  <!-- Header -->
558
  <header>
559
- <div class="header-tag">MeMo Modelling · A*STAR</div>
560
- <h1>Gene Expression<br><em>Bootstrapper</em></h1>
561
- <div class="header-rule"></div>
562
- <p class="header-sub">Fills in missing expression values for unmapped genes by sampling from the same metabolic subsystem. Output serves as input for FEA.</p>
 
 
563
  </header>
564
 
565
- <!-- Optional: batch count -->
566
- <div class="opt-block">
567
- <div class="opt-tag">Optional</div>
568
- <div class="opt-inner">
 
 
 
569
  <div class="opt-title">Bootstrap iterations</div>
570
  <div class="opt-desc">Number of output CSV files to generate. Default of 1,000 is recommended.</div>
571
  <div class="slider-row">
@@ -576,86 +467,71 @@ footer a:hover { color: var(--ink); }
576
  <div class="slider-labels"><span>100</span><span>5,000</span></div>
577
  <div class="slider-note">Higher values = more robust results, longer processing time.</div>
578
  </div>
579
- </div>
580
 
581
- <!-- Upload steps -->
582
- <div class="section-label">Upload your files</div>
583
-
584
- <!-- Step 1 -->
585
- <div class="step">
586
- <div class="step-num" id="num1">1</div>
587
- <div class="step-content">
588
  <div class="step-title">Prefixed model files</div>
589
  <div class="step-desc">One .xlsx per species — output from Step 4 of the Metabolic Community Pipeline.</div>
590
- <div class="card">
591
- <div class="drop-zone" id="dropModel">
592
- <input type="file" id="fileModel" accept=".xlsx" multiple onchange="handleFiles('model', this.files)"/>
593
- <span class="drop-icon">📁</span>
594
- <div class="drop-text">
595
- <strong>Drop model file here</strong>
596
- or click to browse
597
- <small>.xlsx only · one file per species</small>
598
- </div>
599
  </div>
600
- <div class="file-chips" id="chipsModel"></div>
601
  </div>
 
602
  </div>
603
- </div>
604
 
605
- <!-- Step 2 -->
606
- <div class="step">
607
- <div class="step-num" id="num2">2</div>
608
- <div class="step-content">
609
  <div class="step-title">Identifier mapping files</div>
610
  <div class="step-desc">One .xlsx per species, in the same order as model files — output from Step 3 of the Pipeline.</div>
611
- <div class="card">
612
- <div class="drop-zone" id="dropMapping">
613
- <input type="file" id="fileMapping" accept=".xlsx" multiple onchange="handleFiles('mapping', this.files)"/>
614
- <span class="drop-icon">📁</span>
615
- <div class="drop-text">
616
- <strong>Drop mapping file here</strong>
617
- or click to browse
618
- <small>.xlsx only · one file per species, same order as model</small>
619
- </div>
620
  </div>
621
- <div class="file-chips" id="chipsMapping"></div>
622
- <div class="mismatch-warning hidden" id="prefixWarning"></div>
623
  </div>
 
 
624
  </div>
625
- </div>
626
 
627
- <!-- Step 3 -->
628
- <div class="step">
629
- <div class="step-num" id="num3">3</div>
630
- <div class="step-content">
631
  <div class="step-title">Combined gene expression CSV</div>
632
  <div class="step-desc">Normalised gene expression counts for all species and samples from your metatranscriptomics data.</div>
633
- <div class="card">
634
- <div class="drop-zone" id="dropExpr">
635
- <input type="file" id="fileExpr" accept=".csv" onchange="handleFiles('expr', this.files)"/>
636
- <span class="drop-icon">📄</span>
637
- <div class="drop-text">
638
- <strong>Drop gene expression CSV here</strong>
639
- or click to browse
640
- <small>.csv only · single file</small>
641
- </div>
642
  </div>
643
- <div class="file-chips" id="chipsExpr"></div>
644
  </div>
 
645
  </div>
646
- </div>
647
 
648
- <!-- Run -->
649
- <div class="run-section">
650
- <button class="btn-run" id="runBtn" onclick="startJob()">
651
- Generate Bootstrapped Files
652
- </button>
653
- </div>
 
654
 
655
- <!-- Progress -->
656
- <div class="hidden" id="progressSection">
657
- <div class="section-label" style="margin-top:2.5rem">Processing</div>
658
- <div class="card">
659
  <div class="progress-header">
660
  <span class="progress-label" id="progressLabel">Starting…</span>
661
  <span class="progress-pct" id="progressPct">0%</span>
@@ -665,12 +541,8 @@ footer a:hover { color: var(--ink); }
665
  </div>
666
  <div class="log-box" id="logBox"></div>
667
  </div>
668
- </div>
669
 
670
- <!-- Results -->
671
- <div class="hidden" id="resultsSection">
672
- <div class="section-label" style="margin-top:2.5rem">Results</div>
673
- <div class="card">
674
  <div class="stats-row">
675
  <div class="stat-box">
676
  <div class="stat-val" id="statFiles">—</div>
@@ -693,14 +565,7 @@ footer a:hover { color: var(--ink); }
693
  </div>
694
  </div>
695
 
696
- <footer>
697
- © 2026 A*STAR Bioinformatics Institute &nbsp;·&nbsp;
698
- Free for academic research and education &nbsp;·&nbsp;
699
- Commercial licensing: <a href="mailto:yeohc@a-star.edu.sg">yeohc@a-star.edu.sg</a>
700
- </footer>
701
-
702
  </div>
703
-
704
  <script>
705
  const state = { model: [], mapping: [], expr: [] };
706
 
@@ -764,20 +629,19 @@ async function startJob() {
764
 
765
  document.getElementById('runBtn').disabled = true;
766
  document.getElementById('progressSection').classList.remove('hidden');
 
767
  document.getElementById('resultsSection').classList.add('hidden');
768
  document.getElementById('logBox').innerHTML = '';
769
  document.getElementById('progressBar').style.width = '0%';
770
  document.getElementById('progressPct').textContent = '0%';
771
  document.getElementById('progressLabel').textContent = 'Starting…';
772
 
773
- // Build form data
774
  const fd = new FormData();
775
  state.model.forEach(f => fd.append('model_files', f));
776
  state.mapping.forEach(f => fd.append('mapping_files', f));
777
  fd.append('expr_file', state.expr[0]);
778
  fd.append('batch_count', document.getElementById('batchSlider').value);
779
 
780
- // Submit job
781
  let jobId;
782
  try {
783
  const res = await fetch('/api/process', { method: 'POST', body: fd });
@@ -791,18 +655,14 @@ async function startJob() {
791
  return;
792
  }
793
 
794
- // Stream progress via SSE
795
  const sse = new EventSource(`/api/progress/${jobId}`);
796
 
797
  sse.onmessage = e => {
798
  const d = JSON.parse(e.data);
799
-
800
  if (d.message) appendLog(d.message, d.type || '');
801
-
802
  if (d.progress !== undefined) {
803
  document.getElementById('progressBar').style.width = d.progress + '%';
804
  document.getElementById('progressPct').textContent = d.progress + '%';
805
- // Show only clean status labels, never raw backend output
806
  const cleanLabels = ['Starting…', 'Files received. Starting bootstrap…', 'Writing output files…', 'Compressing output files…', 'Processing…'];
807
  if (d.label && cleanLabels.some(l => d.label.startsWith(l.split('…')[0]))) {
808
  document.getElementById('progressLabel').textContent = d.label;
@@ -810,20 +670,16 @@ async function startJob() {
810
  document.getElementById('progressLabel').textContent = 'Processing…';
811
  }
812
  }
813
-
814
  if (d.status === 'done') {
815
  sse.close();
816
- document.getElementById('statFiles').textContent = (d.file_count || '—').toLocaleString();
817
- document.getElementById('statSpecies').textContent = d.species_count || '—';
818
  document.getElementById('downloadBtn').dataset.jobId = jobId;
 
819
  document.getElementById('resultsSection').classList.remove('hidden');
 
820
  document.getElementById('runBtn').disabled = false;
821
- // Auto-scroll to download button
822
- setTimeout(() => {
823
- document.getElementById('downloadBtn').scrollIntoView({ behavior: 'smooth', block: 'center' });
824
- }, 300);
825
  }
826
-
827
  if (d.status === 'error') {
828
  sse.close();
829
  appendLog('Fatal error: ' + d.error, 'err');
@@ -842,10 +698,10 @@ async function triggerDownload() {
842
  const jobId = document.getElementById('downloadBtn').dataset.jobId;
843
  if (!jobId) return;
844
 
845
- const btn = document.getElementById('downloadBtn');
846
- const wrap = document.getElementById('dlProgressWrap');
847
- const fill = document.getElementById('dlProgressFill');
848
- const label = document.getElementById('dlProgressLabel');
849
 
850
  btn.disabled = true;
851
  btn.textContent = 'Preparing download…';
@@ -861,13 +717,11 @@ async function triggerDownload() {
861
  let blob;
862
 
863
  if (contentLength) {
864
- // Real progress from Content-Length
865
  const total = parseInt(contentLength);
866
  const chunks = [];
867
  let received = 0;
868
  const reader = res.body.getReader();
869
  fill.classList.remove('indeterminate');
870
-
871
  while (true) {
872
  const { done, value } = await reader.read();
873
  if (done) break;
@@ -879,7 +733,6 @@ async function triggerDownload() {
879
  }
880
  blob = new Blob(chunks, { type: 'application/zip' });
881
  } else {
882
- // No content-length — just show spinner until done
883
  label.textContent = 'Downloading…';
884
  blob = await res.blob();
885
  }
@@ -890,10 +743,8 @@ async function triggerDownload() {
890
 
891
  const url = URL.createObjectURL(blob);
892
  const a = document.createElement('a');
893
- a.href = url;
894
- a.download = 'geneExpr_bootstrapped.zip';
895
- document.body.appendChild(a);
896
- a.click();
897
  document.body.removeChild(a);
898
  URL.revokeObjectURL(url);
899
 
 
7
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"/>
8
  <style>
9
  :root {
10
+ --bg: #EEF1F8;
11
+ --surface: #FFFFFF;
12
+ --border: #D8DDED;
13
+ --ink: #0A0E1F;
14
+ --ink2: #4A5270;
15
+ --ink3: #8A91AD;
16
+ --teal: #4F46E5;
17
+ --teal-l: #EFEEFD;
18
+ --teal-d: #3730A3;
19
+ --coral: #7C3AED;
20
+ --coral-l: #F5F3FF;
21
+ --r: 10px;
22
+ --shadow: 0 1px 4px rgba(10,14,31,0.07);
23
+ --shadow-md: 0 4px 16px rgba(10,14,31,0.12);
24
  }
 
25
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
+ html, body { height: 100%; }
27
  body {
28
  background: var(--bg);
29
  color: var(--ink);
30
  font-family: 'DM Sans', -apple-system, sans-serif;
31
  font-size: 16px;
32
+ line-height: 1.6;
 
33
  }
34
 
 
35
  .page {
36
+ max-width: 1200px;
37
  margin: 0 auto;
38
+ padding: 1.25rem 2rem 1rem;
39
+ display: flex;
40
+ flex-direction: column;
41
+ min-height: 100vh;
42
  }
43
 
44
+ /* Copyright bar */
45
+ .copyright-bar {
46
+ font-size: 12px;
47
+ color: var(--ink3);
48
+ margin-bottom: 0.75rem;
49
+ padding-bottom: 0.75rem;
50
+ border-bottom: 1px solid var(--border);
51
  }
52
+ .copyright-bar a { color: var(--ink2); text-decoration: none; }
53
+ .copyright-bar a:hover { color: var(--ink); }
54
 
55
+ /* Header */
56
+ header {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 2rem;
60
+ margin-bottom: 1.25rem;
61
+ flex-wrap: wrap;
62
+ }
63
+ .header-left { flex: 0 0 auto; }
64
  .header-tag {
 
 
65
  font-size: 11px;
66
  font-weight: 600;
67
  letter-spacing: 0.12em;
68
  text-transform: uppercase;
69
  color: var(--coral);
70
+ margin-bottom: 0.25rem;
71
+ display: block;
72
  }
 
73
  header h1 {
74
  font-family: 'Playfair Display', Georgia, serif;
75
+ font-size: 2.2rem;
76
  font-weight: 700;
77
+ line-height: 1.15;
 
78
  color: var(--ink);
 
79
  }
 
80
  header h1 em {
81
  font-style: italic;
82
  font-weight: 400;
83
  color: var(--teal);
84
  }
85
+ .header-rule {
86
+ width: 32px; height: 3px;
87
+ background: var(--coral);
88
+ border-radius: 2px;
89
+ margin-top: 0.4rem;
90
+ }
91
  .header-sub {
92
+ font-size: 16px;
93
  color: var(--ink2);
94
+ line-height: 1.6;
95
+ flex: 1;
96
+ min-width: 180px;
97
  }
98
 
99
+ /* 2x2 grid */
100
+ .main-grid {
101
+ display: grid;
102
+ grid-template-columns: 1fr 1fr;
103
+ gap: 1.25rem;
104
+ flex: 1;
105
  }
106
 
107
+ /* Column cards */
108
+ .col-card {
109
+ background: var(--surface);
110
+ border: 1px solid var(--border);
111
+ border-radius: var(--r);
112
+ padding: 1.25rem;
113
+ box-shadow: var(--shadow);
114
+ }
115
+ .col-label {
116
+ font-size: 11px;
117
  font-weight: 600;
118
+ letter-spacing: 0.12em;
119
  text-transform: uppercase;
120
  color: var(--ink3);
121
+ margin-bottom: 0.85rem;
 
122
  display: flex;
123
  align-items: center;
124
+ gap: 8px;
125
  }
126
+ .col-label::after {
 
127
  content: '';
128
  flex: 1;
129
  height: 1px;
130
  background: var(--border);
131
  }
132
 
133
+ /* Optional block */
 
 
 
 
 
 
 
 
 
 
 
 
134
  .opt-tag {
135
  background: var(--coral-l);
136
  color: var(--coral);
137
  border: 1px solid #E8C4B3;
138
  font-size: 9px;
139
+ font-weight: 800;
140
  letter-spacing: 0.1em;
141
  text-transform: uppercase;
142
  padding: 3px 7px;
143
  border-radius: 4px;
144
+ display: inline-block;
145
+ margin-bottom: 0.5rem;
146
  }
 
 
 
147
  .opt-title {
148
  font-size: 16px;
149
  font-weight: 600;
150
  color: var(--ink);
151
+ margin-bottom: 0.25rem;
152
  }
 
153
  .opt-desc {
154
+ font-size: 16px;
155
  color: var(--ink2);
156
  margin-bottom: 0.85rem;
157
+ line-height: 1.5;
158
  }
159
 
160
+ /* Slider */
161
  .slider-row {
162
  display: flex;
163
  align-items: center;
164
  gap: 1rem;
165
  }
 
166
  .slider-val {
167
  font-family: 'Playfair Display', serif;
168
+ font-size: 1.4rem;
169
  font-weight: 700;
170
  color: var(--teal);
171
  min-width: 58px;
172
  text-align: right;
173
  letter-spacing: -0.02em;
174
  }
 
175
  input[type="range"] {
176
  flex: 1;
177
  -webkit-appearance: none;
 
181
  outline: none;
182
  cursor: pointer;
183
  }
 
184
  input[type="range"]::-webkit-slider-thumb {
185
  -webkit-appearance: none;
186
  width: 18px; height: 18px;
 
189
  cursor: pointer;
190
  border: 3px solid var(--surface);
191
  box-shadow: 0 0 0 2px var(--teal);
 
 
 
 
 
192
  }
 
193
  .slider-labels {
194
  display: flex;
195
  justify-content: space-between;
196
+ font-size: 12px;
197
  color: var(--ink3);
198
  margin-top: 0.4rem;
199
  }
 
200
  .slider-note {
201
+ font-size: 13px;
202
  color: var(--ink3);
203
  margin-top: 0.5rem;
204
  }
205
 
206
+ /* Steps */
 
 
 
 
 
 
 
 
 
 
 
 
207
  .step-num {
208
+ width: 28px; height: 28px;
 
209
  border-radius: 7px;
210
  background: transparent;
211
  border: 2px solid var(--ink);
212
  color: var(--ink);
213
  font-size: 13px;
214
  font-weight: 700;
215
+ display: inline-flex; align-items: center; justify-content: center;
 
 
 
216
  font-family: 'DM Sans', sans-serif;
217
+ margin-bottom: 0.5rem;
218
  transition: all 0.2s;
219
  }
 
220
  .step-num.done {
221
  background: var(--teal);
222
  border-color: var(--teal);
223
  color: #fff;
224
  }
 
 
 
225
  .step-title {
226
  font-size: 16px;
227
  font-weight: 600;
228
  color: var(--ink);
229
  margin-bottom: 0.2rem;
230
  }
 
231
  .step-desc {
232
+ font-size: 16px;
233
  color: var(--ink2);
234
  margin-bottom: 0.75rem;
235
  line-height: 1.5;
236
  }
237
 
238
+ /* Drop zone */
 
 
 
 
 
 
 
 
 
239
  .drop-zone {
240
  border: 1.5px dashed var(--border);
241
  border-radius: 8px;
242
+ padding: 1.5rem 1rem;
243
  text-align: center;
244
  cursor: pointer;
245
  position: relative;
246
  transition: all 0.15s;
247
  background: var(--bg);
248
  }
 
249
  .drop-zone:hover {
250
  border-color: var(--teal);
251
  background: var(--teal-l);
252
  }
 
253
  .drop-zone input[type="file"] {
254
  position: absolute; inset: 0;
255
  opacity: 0; cursor: pointer;
256
  width: 100%; height: 100%;
257
  }
258
+ .drop-icon { font-size: 1.4rem; margin-bottom: 0.3rem; display: block; }
259
+ .drop-text { font-size: 16px; color: var(--ink2); line-height: 1.5; }
260
+ .drop-text strong { color: var(--ink); display: block; font-weight: 600; }
261
+ .drop-text small { color: var(--ink3); font-size: 13px; margin-top: 2px; display: block; }
262
 
263
+ /* File chips */
264
+ .file-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  .chip {
266
+ display: inline-flex; align-items: center; gap: 5px;
 
 
267
  background: var(--teal-l);
268
  border: 1px solid #B2D4D4;
269
  color: var(--teal-d);
270
  padding: 3px 10px;
271
  border-radius: 100px;
272
+ font-size: 13px;
273
  font-weight: 500;
274
  }
 
275
  .mismatch-warning {
276
  display: flex;
277
  align-items: flex-start;
 
281
  border-radius: 8px;
282
  padding: 0.75rem 1rem;
283
  margin-top: 0.75rem;
284
+ font-size: 14px;
285
  color: #9A3412;
286
  line-height: 1.5;
287
  }
288
 
289
+ /* Bottom bar */
290
+ .bottom-bar {
291
+ margin-top: 1.25rem;
292
+ display: grid;
293
+ grid-template-columns: 1fr 1fr;
294
+ gap: 1.25rem;
295
+ align-items: center;
296
+ }
297
+ .run-col {
298
+ display: flex;
299
+ justify-content: center;
300
+ grid-column: 1 / -1;
301
  }
 
302
  .btn-run {
303
+ width: 40%;
304
+ padding: 0.65rem 1.25rem;
305
  background: var(--ink);
306
  color: var(--bg);
307
  border: none;
 
311
  font-weight: 600;
312
  cursor: pointer;
313
  transition: all 0.15s;
314
+ display: flex; align-items: center; justify-content: center; gap: 8px;
 
 
 
 
315
  }
 
316
  .btn-run:hover:not(:disabled) {
317
  background: var(--teal);
318
  box-shadow: var(--shadow-md);
319
  transform: translateY(-1px);
320
  }
321
+ .btn-run:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
322
 
323
+ /* Progress */
324
+ .progress-section {
325
+ display: none;
326
+ background: var(--surface);
327
+ border: 1px solid var(--border);
328
+ border-radius: var(--r);
329
+ padding: 0.9rem 1.25rem;
330
+ box-shadow: var(--shadow);
331
+ flex-direction: column;
332
+ grid-column: 1 / -1;
333
  }
 
 
334
  .progress-header {
335
+ display: flex; justify-content: space-between; align-items: center;
336
+ font-size: 16px; margin-bottom: 0.5rem;
 
 
 
337
  }
 
338
  .progress-label { font-weight: 600; color: var(--ink); }
339
  .progress-pct { color: var(--ink2); font-variant-numeric: tabular-nums; }
340
+ .progress-track { height: 5px; background: var(--border); border-radius: 3px; overflow: hidden; }
 
 
 
 
 
 
 
341
  .progress-fill {
342
  height: 100%;
343
  background: linear-gradient(90deg, var(--teal), #4A9F9F);
 
345
  transition: width 0.45s ease;
346
  width: 0%;
347
  }
 
348
  .log-box { display: none; }
349
 
350
+ /* Results */
351
+ .results-section {
352
+ display: none;
353
+ background: var(--surface);
354
+ border: 1px solid var(--border);
355
+ border-radius: var(--r);
356
+ padding: 0.9rem 1.25rem;
357
+ box-shadow: var(--shadow);
358
+ align-items: center;
359
+ gap: 1.25rem;
360
+ grid-column: 1 / -1;
361
  }
362
+ .stats-row { display: flex; gap: 1rem; }
363
  .stat-box {
364
  background: var(--bg);
365
  border: 1px solid var(--border);
366
  border-radius: 8px;
367
+ padding: 0.5rem 1rem;
368
  }
 
369
  .stat-val {
370
  font-family: 'Playfair Display', serif;
371
+ font-size: 1.6rem;
372
  font-weight: 700;
373
  color: var(--ink);
374
  line-height: 1;
375
+ margin-bottom: 0.15rem;
376
  letter-spacing: -0.02em;
377
  }
 
378
  .stat-label {
379
+ font-size: 12px;
380
  color: var(--ink2);
381
  font-weight: 500;
382
  text-transform: uppercase;
383
+ letter-spacing: 0.05em;
384
  }
 
385
  .btn-download {
386
+ display: flex; align-items: center; justify-content: center; gap: 8px;
387
+ padding: 0.7rem 1.25rem;
 
 
 
 
388
  background: var(--teal-l);
389
  color: var(--teal-d);
390
  border: 1.5px solid var(--teal);
391
  border-radius: var(--r);
392
  font-family: 'DM Sans', sans-serif;
393
+ font-size: 16px; font-weight: 600;
394
+ cursor: pointer; text-decoration: none; white-space: nowrap;
 
 
395
  transition: all 0.15s;
396
  }
 
397
  .btn-download:hover:not(:disabled) {
398
  background: var(--teal);
399
  color: white;
400
  transform: translateY(-1px);
401
  box-shadow: var(--shadow-md);
402
  }
403
+ .btn-download:disabled { opacity: 0.7; cursor: not-allowed; transform: none; }
404
 
405
+ .download-progress-wrap { margin-top: 0.65rem; display: none; }
 
 
 
 
 
 
 
 
 
 
 
406
  .download-progress-wrap.show { display: block; }
407
+ .download-progress-track { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
 
 
 
 
 
 
 
408
  .download-progress-fill {
409
  height: 100%;
410
  background: linear-gradient(90deg, var(--teal), #6B8FCF);
 
412
  width: 0%;
413
  transition: width 0.3s ease;
414
  }
415
+ .download-progress-label { font-size: 13px; color: var(--ink2); margin-top: 0.3rem; text-align: center; }
 
 
 
 
 
 
 
416
  @keyframes indeterminate {
417
  0% { left: -40%; width: 40%; }
418
  60% { left: 100%; width: 40%; }
419
  100% { left: 100%; width: 40%; }
420
  }
 
421
  .download-progress-fill.indeterminate {
422
  position: relative;
423
  width: 40%;
424
  animation: indeterminate 1.4s ease infinite;
425
  }
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  .hidden { display: none !important; }
 
428
  ::-webkit-scrollbar { width: 5px; }
429
  ::-webkit-scrollbar-track { background: transparent; }
430
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
 
433
  <body>
434
  <div class="page">
435
 
436
+ <!-- Copyright bar -->
437
+ <div class="copyright-bar">
438
+ © 2026 A*STAR Bioinformatics Institute &nbsp;·&nbsp;
439
+ Free for academic research and education &nbsp;·&nbsp;
440
+ Commercial licensing: <a href="mailto:yeohc@a-star.edu.sg">yeohc@a-star.edu.sg</a>
441
+ </div>
442
+
443
  <!-- Header -->
444
  <header>
445
+ <div class="header-left">
446
+ <span class="header-tag">MeMo Modelling · A*STAR</span>
447
+ <h1>Gene Expression <em>Bootstrapper</em></h1>
448
+ <div class="header-rule"></div>
449
+ </div>
450
+ <p class="header-sub">Fills in missing expression values for unmapped genes by sampling from the same metabolic subsystem. Output serves as input for MeMo.</p>
451
  </header>
452
 
453
+ <!-- 2x2 grid -->
454
+ <div class="main-grid">
455
+
456
+ <!-- Top-left: Optional -->
457
+ <div class="col-card">
458
+ <div class="col-label">Parameters</div>
459
+ <div class="opt-tag">Optional</div>
460
  <div class="opt-title">Bootstrap iterations</div>
461
  <div class="opt-desc">Number of output CSV files to generate. Default of 1,000 is recommended.</div>
462
  <div class="slider-row">
 
467
  <div class="slider-labels"><span>100</span><span>5,000</span></div>
468
  <div class="slider-note">Higher values = more robust results, longer processing time.</div>
469
  </div>
 
470
 
471
+ <!-- Top-right: Step 1 -->
472
+ <div class="col-card">
473
+ <div class="col-label">Step 1</div>
474
+ <div class="step-num" id="num1">1</div>
 
 
 
475
  <div class="step-title">Prefixed model files</div>
476
  <div class="step-desc">One .xlsx per species — output from Step 4 of the Metabolic Community Pipeline.</div>
477
+ <div class="drop-zone" id="dropModel">
478
+ <input type="file" id="fileModel" accept=".xlsx" multiple onchange="handleFiles('model', this.files)"/>
479
+ <span class="drop-icon">📄</span>
480
+ <div class="drop-text">
481
+ <strong>Drop model file here</strong>
482
+ or click to browse
483
+ <small>.xlsx only · one file per species</small>
 
 
484
  </div>
 
485
  </div>
486
+ <div class="file-chips" id="chipsModel"></div>
487
  </div>
 
488
 
489
+ <!-- Bottom-left: Step 2 -->
490
+ <div class="col-card">
491
+ <div class="col-label">Step 2</div>
492
+ <div class="step-num" id="num2">2</div>
493
  <div class="step-title">Identifier mapping files</div>
494
  <div class="step-desc">One .xlsx per species, in the same order as model files — output from Step 3 of the Pipeline.</div>
495
+ <div class="drop-zone" id="dropMapping">
496
+ <input type="file" id="fileMapping" accept=".xlsx" multiple onchange="handleFiles('mapping', this.files)"/>
497
+ <span class="drop-icon">📄</span>
498
+ <div class="drop-text">
499
+ <strong>Drop mapping file here</strong>
500
+ or click to browse
501
+ <small>.xlsx only · one file per species, same order as model</small>
 
 
502
  </div>
 
 
503
  </div>
504
+ <div class="file-chips" id="chipsMapping"></div>
505
+ <div class="mismatch-warning hidden" id="prefixWarning"></div>
506
  </div>
 
507
 
508
+ <!-- Bottom-right: Step 3 -->
509
+ <div class="col-card">
510
+ <div class="col-label">Step 3</div>
511
+ <div class="step-num" id="num3">3</div>
512
  <div class="step-title">Combined gene expression CSV</div>
513
  <div class="step-desc">Normalised gene expression counts for all species and samples from your metatranscriptomics data.</div>
514
+ <div class="drop-zone" id="dropExpr">
515
+ <input type="file" id="fileExpr" accept=".csv" onchange="handleFiles('expr', this.files)"/>
516
+ <span class="drop-icon">📄</span>
517
+ <div class="drop-text">
518
+ <strong>Drop gene expression CSV here</strong>
519
+ or click to browse
520
+ <small>.csv only · single file</small>
 
 
521
  </div>
 
522
  </div>
523
+ <div class="file-chips" id="chipsExpr"></div>
524
  </div>
 
525
 
526
+ </div><!-- /main-grid -->
527
+
528
+ <!-- Bottom bar -->
529
+ <div class="bottom-bar">
530
+ <div class="run-col">
531
+ <button class="btn-run" id="runBtn" onclick="startJob()">Generate Bootstrapped Files</button>
532
+ </div>
533
 
534
+ <div class="progress-section hidden" id="progressSection">
 
 
 
535
  <div class="progress-header">
536
  <span class="progress-label" id="progressLabel">Starting…</span>
537
  <span class="progress-pct" id="progressPct">0%</span>
 
541
  </div>
542
  <div class="log-box" id="logBox"></div>
543
  </div>
 
544
 
545
+ <div class="results-section hidden" id="resultsSection">
 
 
 
546
  <div class="stats-row">
547
  <div class="stat-box">
548
  <div class="stat-val" id="statFiles">—</div>
 
565
  </div>
566
  </div>
567
 
 
 
 
 
 
 
568
  </div>
 
569
  <script>
570
  const state = { model: [], mapping: [], expr: [] };
571
 
 
629
 
630
  document.getElementById('runBtn').disabled = true;
631
  document.getElementById('progressSection').classList.remove('hidden');
632
+ document.getElementById('progressSection').style.display = 'flex';
633
  document.getElementById('resultsSection').classList.add('hidden');
634
  document.getElementById('logBox').innerHTML = '';
635
  document.getElementById('progressBar').style.width = '0%';
636
  document.getElementById('progressPct').textContent = '0%';
637
  document.getElementById('progressLabel').textContent = 'Starting…';
638
 
 
639
  const fd = new FormData();
640
  state.model.forEach(f => fd.append('model_files', f));
641
  state.mapping.forEach(f => fd.append('mapping_files', f));
642
  fd.append('expr_file', state.expr[0]);
643
  fd.append('batch_count', document.getElementById('batchSlider').value);
644
 
 
645
  let jobId;
646
  try {
647
  const res = await fetch('/api/process', { method: 'POST', body: fd });
 
655
  return;
656
  }
657
 
 
658
  const sse = new EventSource(`/api/progress/${jobId}`);
659
 
660
  sse.onmessage = e => {
661
  const d = JSON.parse(e.data);
 
662
  if (d.message) appendLog(d.message, d.type || '');
 
663
  if (d.progress !== undefined) {
664
  document.getElementById('progressBar').style.width = d.progress + '%';
665
  document.getElementById('progressPct').textContent = d.progress + '%';
 
666
  const cleanLabels = ['Starting…', 'Files received. Starting bootstrap…', 'Writing output files…', 'Compressing output files…', 'Processing…'];
667
  if (d.label && cleanLabels.some(l => d.label.startsWith(l.split('…')[0]))) {
668
  document.getElementById('progressLabel').textContent = d.label;
 
670
  document.getElementById('progressLabel').textContent = 'Processing…';
671
  }
672
  }
 
673
  if (d.status === 'done') {
674
  sse.close();
675
+ document.getElementById('statFiles').textContent = (d.file_count || '—').toLocaleString();
676
+ document.getElementById('statSpecies').textContent = d.species_count || '—';
677
  document.getElementById('downloadBtn').dataset.jobId = jobId;
678
+ document.getElementById('progressSection').classList.add('hidden');
679
  document.getElementById('resultsSection').classList.remove('hidden');
680
+ document.getElementById('resultsSection').style.display = 'flex';
681
  document.getElementById('runBtn').disabled = false;
 
 
 
 
682
  }
 
683
  if (d.status === 'error') {
684
  sse.close();
685
  appendLog('Fatal error: ' + d.error, 'err');
 
698
  const jobId = document.getElementById('downloadBtn').dataset.jobId;
699
  if (!jobId) return;
700
 
701
+ const btn = document.getElementById('downloadBtn');
702
+ const wrap = document.getElementById('dlProgressWrap');
703
+ const fill = document.getElementById('dlProgressFill');
704
+ const label = document.getElementById('dlProgressLabel');
705
 
706
  btn.disabled = true;
707
  btn.textContent = 'Preparing download…';
 
717
  let blob;
718
 
719
  if (contentLength) {
 
720
  const total = parseInt(contentLength);
721
  const chunks = [];
722
  let received = 0;
723
  const reader = res.body.getReader();
724
  fill.classList.remove('indeterminate');
 
725
  while (true) {
726
  const { done, value } = await reader.read();
727
  if (done) break;
 
733
  }
734
  blob = new Blob(chunks, { type: 'application/zip' });
735
  } else {
 
736
  label.textContent = 'Downloading…';
737
  blob = await res.blob();
738
  }
 
743
 
744
  const url = URL.createObjectURL(blob);
745
  const a = document.createElement('a');
746
+ a.href = url; a.download = 'geneExpr_bootstrapped.zip';
747
+ document.body.appendChild(a); a.click();
 
 
748
  document.body.removeChild(a);
749
  URL.revokeObjectURL(url);
750