WebashalarForML commited on
Commit
623a404
·
verified ·
1 Parent(s): 52af245

Upload 5 files

Browse files
Files changed (4) hide show
  1. app.py +31 -1
  2. static/css/style.css +253 -13
  3. templates/index.html +7 -7
  4. templates/report.html +145 -128
app.py CHANGED
@@ -1,6 +1,8 @@
1
  import os
 
2
  import zipfile
3
  import pandas as pd
 
4
  from flask import Flask, request, redirect, url_for, send_from_directory, flash, render_template
5
  from werkzeug.utils import secure_filename
6
  from tqdm import tqdm
@@ -73,6 +75,24 @@ def get_inference_engine():
73
  print(f"[WARNING] Could not download from HF: {e}. Expecting local files.")
74
 
75
  infer_engine = DiamondInference(model_path, encoder_dir, MODEL_ID)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  return infer_engine
77
 
78
  @app.route('/flush', methods=['POST'])
@@ -157,11 +177,21 @@ def upload_files():
157
  y_pred = []
158
 
159
  print(f"[INFO] Initializing Inference Pipeline for {len(df)} stones...")
160
- for index, row in tqdm(df.iterrows(), total=len(df), desc="Inference Progress"):
 
 
 
 
 
161
  l_code = str(row.get('L_Code', '')).split('.')[0]
162
  sr_no = str(row.get('SrNo', '')).split('.')[0]
163
  stone_id = str(row.get('Stone_Id', ''))
164
 
 
 
 
 
 
165
  img_path = None
166
  for full_path in all_extracted_files:
167
  fname = os.path.basename(full_path)
 
1
  import os
2
+ import sys
3
  import zipfile
4
  import pandas as pd
5
+ import numpy as np
6
  from flask import Flask, request, redirect, url_for, send_from_directory, flash, render_template
7
  from werkzeug.utils import secure_filename
8
  from tqdm import tqdm
 
75
  print(f"[WARNING] Could not download from HF: {e}. Expecting local files.")
76
 
77
  infer_engine = DiamondInference(model_path, encoder_dir, MODEL_ID)
78
+
79
+ # Warmup prediction to initialize TF graph and prevent "stuck" feeling on first stone
80
+ print("[INFO] Warming up Inference Engine...")
81
+ try:
82
+ # Create a dummy row and zero patches for warmup
83
+ dummy_row = {"StoneType": "NATURAL", "Color": "D", "Brown": "N", "BlueUv": "N", "GrdType": "GIA", "Carat": 1.0, "Result": "D"}
84
+ # We don't need a real image for warmup, just a pass through predict
85
+ # We'll mock process_image to return zeros
86
+ orig_process = infer_engine.process_image
87
+ try:
88
+ infer_engine.process_image = lambda path, tta_transform=None: np.zeros(infer_engine.hp["flat_patches_shape"], dtype=np.float32)
89
+ infer_engine.predict(dummy_row, "warmup.jpg", use_tta=False)
90
+ finally:
91
+ infer_engine.process_image = orig_process
92
+ print("[INFO] Warmup complete.")
93
+ except Exception as e:
94
+ print(f"[WARNING] Warmup failed: {e}")
95
+
96
  return infer_engine
97
 
98
  @app.route('/flush', methods=['POST'])
 
177
  y_pred = []
178
 
179
  print(f"[INFO] Initializing Inference Pipeline for {len(df)} stones...")
180
+ sys.stdout.flush()
181
+
182
+ # Progress bar with direct stdout for Gunicorn visibility
183
+ pbar = tqdm(df.iterrows(), total=len(df), desc="Inference Progress", file=sys.stdout)
184
+
185
+ for index, row in pbar:
186
  l_code = str(row.get('L_Code', '')).split('.')[0]
187
  sr_no = str(row.get('SrNo', '')).split('.')[0]
188
  stone_id = str(row.get('Stone_Id', ''))
189
 
190
+ # Log currently processing stone for "aliveness" verification
191
+ if index % 5 == 0:
192
+ print(f"[PROC] Stone {index+1}/{len(df)}: {l_code}")
193
+ sys.stdout.flush()
194
+
195
  img_path = None
196
  for full_path in all_extracted_files:
197
  fname = os.path.basename(full_path)
static/css/style.css CHANGED
@@ -51,12 +51,12 @@ canvas#canvas3d {
51
  position: relative;
52
  z-index: 20;
53
  width: 100%;
54
- max-width: 1400px;
55
  margin: 0 auto;
56
- padding: 0.5rem 1.5rem;
57
  display: flex;
58
  flex-direction: column;
59
  min-height: 100vh;
 
60
  }
61
 
62
  nav {
@@ -596,29 +596,269 @@ td {
596
  }
597
  }
598
 
599
- /* Dashboard Layout */
600
- .report-grid {
601
  display: grid;
602
  grid-template-columns: 1fr;
603
- gap: 2rem;
 
604
  }
605
 
606
  @media (min-width: 1100px) {
607
- .report-grid {
608
- grid-template-columns: 1fr 380px;
 
609
  }
 
610
 
611
- .sticky-side {
612
- position: sticky;
613
- top: 1rem;
614
- height: min-content;
615
- }
616
  }
617
 
618
- .scroll-card {
 
 
 
619
  height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  display: flex;
621
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  }
623
 
624
  .flush-section {
 
51
  position: relative;
52
  z-index: 20;
53
  width: 100%;
 
54
  margin: 0 auto;
55
+ padding: 0.5rem 1rem;
56
  display: flex;
57
  flex-direction: column;
58
  min-height: 100vh;
59
+ box-sizing: border-box;
60
  }
61
 
62
  nav {
 
596
  }
597
  }
598
 
599
+ /* Ultra-Dense Dashboard Grid */
600
+ .dashboard-grid {
601
  display: grid;
602
  grid-template-columns: 1fr;
603
+ gap: 1rem;
604
+ margin: 0.5rem 0;
605
  }
606
 
607
  @media (min-width: 1100px) {
608
+ .dashboard-grid {
609
+ grid-template-columns: 1fr 1fr;
610
+ gap: 1.5rem;
611
  }
612
+ }
613
 
614
+ .dashboard-main {
615
+ display: flex;
616
+ flex-direction: column;
617
+ gap: 1rem;
 
618
  }
619
 
620
+ .dashboard-sidebar {
621
+ display: flex;
622
+ flex-direction: column;
623
+ gap: 1rem;
624
  height: 100%;
625
+ }
626
+
627
+ /* Compact KPI Row */
628
+ .kpi-row {
629
+ display: grid;
630
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
631
+ gap: 0.8rem;
632
+ }
633
+
634
+ .kpi-card {
635
+ background: rgba(255, 255, 255, 0.05);
636
+ border: 1px solid rgba(255, 255, 255, 0.1);
637
+ padding: 1rem 1.2rem;
638
+ text-align: center;
639
+ transition: all 0.3s;
640
+ }
641
+
642
+ .kpi-card:hover {
643
+ background: rgba(255, 255, 255, 0.08);
644
+ border-color: var(--secondary-color);
645
+ }
646
+
647
+ .kpi-value {
648
+ display: block;
649
+ font-size: 1.8rem;
650
+ font-weight: 900;
651
+ color: white;
652
+ line-height: 1;
653
+ margin-bottom: 0.3rem;
654
+ }
655
+
656
+ .kpi-label {
657
+ display: block;
658
+ font-size: 9px;
659
+ letter-spacing: 0.15em;
660
+ text-transform: uppercase;
661
+ opacity: 0.7;
662
+ font-weight: 700;
663
+ }
664
+
665
+ /* Section Labels */
666
+ .section-label {
667
+ font-size: 8px;
668
+ letter-spacing: 0.2em;
669
+ text-transform: uppercase;
670
+ opacity: 0.5;
671
+ font-weight: 700;
672
+ margin-bottom: 0.5rem;
673
+ }
674
+
675
+ /* Compact Table Section */
676
+ .compact-table-section {
677
+ background: rgba(255, 255, 255, 0.03);
678
+ border: 1px solid rgba(255, 255, 255, 0.05);
679
+ padding: 1rem;
680
+ }
681
+
682
+ .compact-table-wrapper {
683
+ max-height: 280px;
684
+ overflow-y: auto;
685
+ scrollbar-width: thin;
686
+ scrollbar-color: var(--secondary-color) transparent;
687
+ }
688
+
689
+ .compact-table-wrapper::-webkit-scrollbar {
690
+ width: 3px;
691
+ }
692
+
693
+ .compact-table-wrapper::-webkit-scrollbar-thumb {
694
+ background: var(--secondary-color);
695
+ border-radius: 10px;
696
+ }
697
+
698
+ .compact-table {
699
+ width: 100%;
700
+ border-collapse: collapse;
701
+ font-size: 11px;
702
+ }
703
+
704
+ .compact-table th {
705
+ text-align: left;
706
+ padding: 0.4rem;
707
+ font-size: 8px;
708
+ letter-spacing: 0.1em;
709
+ text-transform: uppercase;
710
+ opacity: 0.5;
711
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
712
+ position: sticky;
713
+ top: 0;
714
+ background: rgba(5, 5, 5, 0.95);
715
+ }
716
+
717
+ .compact-table td {
718
+ padding: 0.4rem;
719
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
720
+ }
721
+
722
+ .compact-table .grade-col {
723
+ font-weight: 700;
724
+ color: var(--secondary-color);
725
+ }
726
+
727
+ .compact-table .sup-col {
728
+ opacity: 0.4;
729
+ }
730
+
731
+ /* Compact Confusion Matrix */
732
+ .cm-compact {
733
+ background: rgba(255, 255, 255, 0.03);
734
+ border: 1px solid rgba(255, 255, 255, 0.05);
735
+ padding: 1rem;
736
  display: flex;
737
  flex-direction: column;
738
+ height: 100%;
739
+ }
740
+
741
+ .cm-compact-table {
742
+ width: 100%;
743
+ border-collapse: collapse;
744
+ font-size: 8px;
745
+ table-layout: auto;
746
+ }
747
+
748
+ .cm-compact-table th,
749
+ .cm-compact-table td {
750
+ padding: 0.3rem 0.35rem;
751
+ text-align: center;
752
+ border: 1px solid rgba(255, 255, 255, 0.05);
753
+ font-size: 8px;
754
+ }
755
+
756
+ .cm-compact-table th {
757
+ font-size: 7px;
758
+ letter-spacing: 0.03em;
759
+ background: rgba(255, 255, 255, 0.02);
760
+ color: var(--secondary-color);
761
+ font-weight: 600;
762
+ }
763
+
764
+ .cm-corner {
765
+ width: 30px;
766
+ }
767
+
768
+ .cm-row-label {
769
+ background: rgba(255, 255, 255, 0.02);
770
+ font-weight: 700;
771
+ color: var(--secondary-color);
772
+ }
773
+
774
+ .cm-cell {
775
+ font-weight: 600;
776
+ }
777
+
778
+ .cm-diag {
779
+ background: rgba(168, 85, 247, 0.15);
780
+ color: white;
781
+ }
782
+
783
+ .cm-zero {
784
+ opacity: 0.2;
785
+ }
786
+
787
+ /* Summary Mini Cards */
788
+ .summary-mini {
789
+ display: grid;
790
+ grid-template-columns: 1fr 1fr;
791
+ gap: 0.6rem;
792
+ }
793
+
794
+ .summary-mini-card {
795
+ background: rgba(255, 255, 255, 0.03);
796
+ border: 1px solid rgba(255, 255, 255, 0.08);
797
+ padding: 0.6rem;
798
+ text-align: center;
799
+ }
800
+
801
+ .summary-mini-label {
802
+ display: block;
803
+ font-size: 7px;
804
+ letter-spacing: 0.1em;
805
+ text-transform: uppercase;
806
+ opacity: 0.6;
807
+ margin-bottom: 0.2rem;
808
+ }
809
+
810
+ .summary-mini-value {
811
+ display: block;
812
+ font-size: 1.2rem;
813
+ font-weight: 900;
814
+ color: var(--secondary-color);
815
+ }
816
+
817
+ /* Manifest Section (Collapsible) */
818
+ .manifest-section {
819
+ margin-top: 1rem;
820
+ background: rgba(255, 255, 255, 0.03);
821
+ border: 1px solid rgba(255, 255, 255, 0.05);
822
+ padding: 0.8rem;
823
+ }
824
+
825
+ .manifest-header {
826
+ display: flex;
827
+ justify-content: space-between;
828
+ align-items: center;
829
+ cursor: pointer;
830
+ user-select: none;
831
+ }
832
+
833
+ .toggle-icon {
834
+ font-size: 10px;
835
+ opacity: 0.5;
836
+ transition: transform 0.3s;
837
+ }
838
+
839
+ .manifest-content {
840
+ transition: all 0.3s;
841
+ }
842
+
843
+ .stone-id-col {
844
+ font-weight: 700;
845
+ color: white;
846
+ font-size: 11px;
847
+ }
848
+
849
+ .result-col {
850
+ font-weight: 700;
851
+ color: var(--secondary-color);
852
+ font-size: 11px;
853
+ }
854
+
855
+ /* Sticky header row in manifest table */
856
+ .manifest-content table thead th {
857
+ position: sticky;
858
+ top: 0;
859
+ background: rgba(5, 5, 5, 0.98);
860
+ z-index: 10;
861
+ border-bottom: 2px solid rgba(168, 85, 247, 0.3);
862
  }
863
 
864
  .flush-section {
templates/index.html CHANGED
@@ -132,21 +132,21 @@
132
  const loaderText = document.querySelector('.loader-text');
133
  let p = 0;
134
  const itv = setInterval(() => {
135
- if (p < 99) {
136
- // Logic to simulate real work: slow down as it gets closer to 99
137
- const increment = (99 - p) * 0.03 + 0.1;
138
  p += increment;
139
  }
140
  const displayP = Math.floor(p);
141
  loaderBar.style.width = displayP + '%';
142
 
143
  let phase = "ANALYZING";
144
- if (displayP > 30) phase = "INFERENCING";
145
- if (displayP > 70) phase = "GRADATING";
146
- if (displayP > 90) phase = "COMPILING REPORT";
147
 
148
  loaderText.innerText = `PROCS / ${phase} DATA [${displayP}%]`;
149
- }, 80);
150
  });
151
 
152
  function confirmFlush() {
 
132
  const loaderText = document.querySelector('.loader-text');
133
  let p = 0;
134
  const itv = setInterval(() => {
135
+ if (p < 92) {
136
+ // Logic to simulate real work: slow down as it gets closer to 92
137
+ const increment = (92 - p) * 0.03 + 0.1;
138
  p += increment;
139
  }
140
  const displayP = Math.floor(p);
141
  loaderBar.style.width = displayP + '%';
142
 
143
  let phase = "ANALYZING";
144
+ if (displayP > 25) phase = "INFERENCING";
145
+ if (displayP > 60) phase = "GRADATING";
146
+ if (displayP > 85) phase = "COMPILING REPORT";
147
 
148
  loaderText.innerText = `PROCS / ${phase} DATA [${displayP}%]`;
149
+ }, 100);
150
  });
151
 
152
  function confirmFlush() {
templates/report.html CHANGED
@@ -13,176 +13,182 @@
13
  <div class="atmospheric-grain"></div>
14
 
15
  <div class="container animate-up">
16
- <nav>
17
  <div class="nav-logo">RESULTS / DASHBOARD</div>
18
  <div class="nav-meta">
19
  <span>STATUS: COMPLETE</span>
20
  </div>
21
  </nav>
22
 
23
- <header style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 1rem;">
24
  <div>
25
  <p class="label-mini">Batch Processing Overview</p>
26
- <h1 style="font-size: 3rem;">ANALYTICS <span class="italic">REPORT</span></h1>
 
 
 
 
 
 
 
27
  </div>
28
- <a href="/download/{{ report_file }}" class="btn-launch"
29
- style="text-decoration: none; padding: 0.8rem 1.5rem; font-size: 10px;">Export Result Dataset</a>
30
  </header>
31
 
32
  {% if metrics %}
33
- <div class="report-grid">
34
- <div class="scroll-card">
35
- <section class="upload-card" style="margin: 0; padding: 2rem;">
36
- <p class="label-mini">Performance Summary</p>
37
- <div class="metrics-grid">
38
- <div class="metric-item">
39
- <span class="metric-value">{{ (metrics.accuracy * 100)|round(2) }}%</span>
40
- <span class="metric-label">ACCURACY</span>
41
- <p class="metric-note">Overall correctness.</p>
42
- </div>
43
- <div class="metric-item">
44
- <span class="metric-value">{{ metrics.f1|round(4) }}</span>
45
- <span class="metric-label">WEIGHTED F1</span>
46
- <p class="metric-note">Balanced score (weighted).</p>
47
- </div>
48
- <div class="metric-item">
49
- <span class="metric-value">{{ metrics.macro_f1|round(4) }}</span>
50
- <span class="metric-label">MACRO F1</span>
51
- <p class="metric-note">Unweighted average score.</p>
52
- </div>
53
  </div>
 
 
 
 
 
54
 
55
- <p class="label-mini" style="margin-top: 2rem;">Class-Wise Decomposition</p>
56
- <div class="table-wrapper" style="max-height: 400px;">
57
- <table>
 
 
58
  <thead>
59
  <tr>
60
- <th>Grade</th>
61
- <th>Prec.</th>
62
- <th>Recall</th>
63
  <th>F1</th>
64
- <th>Sup.</th>
65
  </tr>
66
  </thead>
67
  <tbody>
68
  {% for item in metrics.class_metrics %}
69
  <tr>
70
- <td style="font-weight: 700; color: var(--secondary-color);">{{ item.label }}</td>
71
  <td>{{ item.precision }}</td>
72
  <td>{{ item.recall }}</td>
73
  <td>{{ item.f1 }}</td>
74
- <td style="opacity: 0.5;">{{ item.support }}</td>
75
  </tr>
76
  {% endfor %}
77
  </tbody>
78
  </table>
79
  </div>
80
- </section>
81
  </div>
82
 
83
- <div class="sticky-side">
84
- <section class="upload-card" style="margin: 0; padding: 2rem;">
85
- <p class="label-mini">Confusion Matrix</p>
86
- <div class="cm-container" style="margin-top: 1rem;">
87
- <table class="cm-table" style="width: 100%;">
88
- <thead>
89
- <tr class="cm-label-row">
90
- <th>A\P</th>
91
- {% for label in metrics.confusion_matrix.labels %}
92
- <th style="min-width: 30px; padding: 0.5rem;">{{ label }}</th>
93
- {% endfor %}
94
- </tr>
95
- </thead>
96
- <tbody>
97
- {% for row_idx in range(metrics.confusion_matrix.matrix|length) %}
98
- <tr>
99
- <td
100
- style="background: rgba(255,255,255,0.03); font-weight: 700; color: var(--secondary-color); padding: 0.5rem;">
101
- {{ metrics.confusion_matrix.labels[row_idx] }}
102
- </td>
103
- {% for col_idx in range(metrics.confusion_matrix.matrix[row_idx]|length) %}
104
- {% set val = metrics.confusion_matrix.matrix[row_idx][col_idx] %}
105
- <td class="cm-value {% if row_idx == col_idx %}cm-match{% endif %} {% if val == 0 %}cm-dimmed{% endif %}"
106
- style="padding: 0.5rem;">
107
- {{ val }}
108
- </td>
109
- {% endfor %}
110
- </tr>
111
  {% endfor %}
112
- </tbody>
113
- </table>
114
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- <div class="summary-metrics"
117
- style="margin-top: 2rem; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
118
- <div class="summary-card" style="padding: 1rem;">
119
- <span class="lbl">Macro F1</span>
120
- <span class="val" style="font-size: 1.5rem;">{{ metrics.macro_f1|round(4) }}</span>
121
- </div>
122
- <div class="summary-card" style="padding: 1rem;">
123
- <span class="lbl">Accuracy</span>
124
- <span class="val" style="font-size: 1.5rem;">{{ (metrics.accuracy * 100)|round(0) }}%</span>
125
- </div>
126
  </div>
127
- </section>
128
  </div>
129
  </div>
130
  {% endif %}
131
 
132
- <section class="upload-card" style="margin-top: 1rem; padding: 1.5rem; border-radius: 8px;">
133
- <p class="label-mini">Inference Manifest</p>
134
- <div class="table-wrapper">
135
- <table>
136
- <thead>
137
- <tr>
138
- <th>Stone Identifier</th>
139
- {% for feature in model_features %}
140
- <th>{{ feature }}</th>
141
- {% endfor %}
142
- <th>AI Result</th>
143
- {% for col in out_of_box_cols %}
144
- <th>{{ col }}</th>
145
- {% endfor %}
146
- <th>Preview</th>
147
- </tr>
148
- </thead>
149
- <tbody>
150
- {% for row in report_data %}
151
- <tr>
152
- <td style="font-size: 14px; font-weight: 900; color: white;">{{ row.L_Code }}</td>
153
- {% for feature in model_features %}
154
- <td style="opacity: 0.7;">{{ row[feature] }}</td>
155
- {% endfor %}
156
- <td style="color: var(--secondary-color); font-size: 14px; font-weight: 900;">{{
157
- row.Predicted_FGrdCol }}</td>
158
- {% for col in out_of_box_cols %}
159
- <td>
160
- {% if row[col] and row[col]|string != 'nan' %}
161
- <span class="badge-oob">{{ row[col] }}</span>
162
- {% else %}
163
- <span style="opacity: 0.2;">-</span>
164
- {% endif %}
165
- </td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  {% endfor %}
167
- <td>
168
- {% if row.Image_Path != 'N/A' %}
169
- <img src="/image/{{ row.Image_Path }}" class="img-thumb" onclick="openModal(this.src)"
170
- alt="Diamond Preview">
171
- {% else %}
172
- <span style="font-size: 10px; opacity: 0.3;">NO IMAGE</span>
173
- {% endif %}
174
- </td>
175
- </tr>
176
- {% endfor %}
177
- </tbody>
178
- </table>
179
  </div>
180
  </section>
181
 
182
- <footer style="margin-top: 4rem; padding-bottom: 2rem;">
183
  <a href="/"
184
- style="font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: white; opacity: 0.5; text-decoration: none;">←
185
- New Batch</a>
186
  </footer>
187
  </div>
188
 
@@ -194,6 +200,18 @@
194
 
195
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
196
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
197
  function openModal(src) {
198
  const modal = document.getElementById('image-modal');
199
  const modalImg = document.getElementById('modal-img');
@@ -206,12 +224,11 @@
206
  modal.classList.remove('active');
207
  }
208
 
209
- // Close modal on background click
210
  document.getElementById('image-modal').onclick = function (e) {
211
  if (e.target === this) closeModal();
212
  };
213
 
214
- // Shader from index.html repeated for consistency
215
  const vertexShader = `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
216
  const fragmentShader = `
217
  uniform float uTime;
 
13
  <div class="atmospheric-grain"></div>
14
 
15
  <div class="container animate-up">
16
+ <nav style="padding: 0.5rem 0;">
17
  <div class="nav-logo">RESULTS / DASHBOARD</div>
18
  <div class="nav-meta">
19
  <span>STATUS: COMPLETE</span>
20
  </div>
21
  </nav>
22
 
23
+ <header style="display: flex; justify-content: space-between; align-items: center; margin: 0.5rem 0;">
24
  <div>
25
  <p class="label-mini">Batch Processing Overview</p>
26
+ <h1 style="font-size: 2.5rem; line-height: 1;">ANALYTICS <span class="italic">REPORT</span></h1>
27
+ </div>
28
+ <div style="display: flex; gap: 0.8rem;">
29
+ <a href="/" class="btn-launch"
30
+ style="text-decoration: none; padding: 0.6rem 1.2rem; font-size: 9px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.5); color: white; opacity: 1;">←
31
+ NEW BATCH</a>
32
+ <a href="/download/{{ report_file }}" class="btn-launch"
33
+ style="text-decoration: none; padding: 0.6rem 1.2rem; font-size: 9px;">EXPORT DATASET</a>
34
  </div>
 
 
35
  </header>
36
 
37
  {% if metrics %}
38
+ <!-- Single-Screen Dashboard Grid -->
39
+ <div class="dashboard-grid">
40
+ <!-- Left Column: KPIs + Class Decomposition -->
41
+ <div class="dashboard-main">
42
+ <!-- KPI Row -->
43
+ <div class="kpi-row">
44
+ <div class="kpi-card">
45
+ <span class="kpi-value">{{ (metrics.accuracy * 100)|round(1) }}%</span>
46
+ <span class="kpi-label">ACCURACY</span>
47
+ </div>
48
+ <div class="kpi-card">
49
+ <span class="kpi-value">{{ metrics.f1|round(3) }}</span>
50
+ <span class="kpi-label">WEIGHTED F1</span>
 
 
 
 
 
 
 
51
  </div>
52
+ <div class="kpi-card">
53
+ <span class="kpi-value">{{ metrics.macro_f1|round(3) }}</span>
54
+ <span class="kpi-label">MACRO F1</span>
55
+ </div>
56
+ </div>
57
 
58
+ <!-- Class-Wise Table (Compact) -->
59
+ <div class="compact-table-section">
60
+ <p class="section-label">CLASS-WISE METRICS</p>
61
+ <div class="compact-table-wrapper">
62
+ <table class="compact-table">
63
  <thead>
64
  <tr>
65
+ <th>GRADE</th>
66
+ <th>PREC</th>
67
+ <th>RECALL</th>
68
  <th>F1</th>
69
+ <th>SUP</th>
70
  </tr>
71
  </thead>
72
  <tbody>
73
  {% for item in metrics.class_metrics %}
74
  <tr>
75
+ <td class="grade-col">{{ item.label }}</td>
76
  <td>{{ item.precision }}</td>
77
  <td>{{ item.recall }}</td>
78
  <td>{{ item.f1 }}</td>
79
+ <td class="sup-col">{{ item.support }}</td>
80
  </tr>
81
  {% endfor %}
82
  </tbody>
83
  </table>
84
  </div>
85
+ </div>
86
  </div>
87
 
88
+ <!-- Right Column: Confusion Matrix -->
89
+ <div class="dashboard-sidebar">
90
+ <p class="section-label">CONFUSION MATRIX</p>
91
+ <div class="cm-compact">
92
+ <table class="cm-compact-table">
93
+ <thead>
94
+ <tr>
95
+ <th class="cm-corner">A\P</th>
96
+ {% for label in metrics.confusion_matrix.labels %}
97
+ <th>{{ label }}</th>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  {% endfor %}
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ {% for row_idx in range(metrics.confusion_matrix.matrix|length) %}
103
+ <tr>
104
+ <td class="cm-row-label">{{ metrics.confusion_matrix.labels[row_idx] }}</td>
105
+ {% for col_idx in range(metrics.confusion_matrix.matrix[row_idx]|length) %}
106
+ {% set val = metrics.confusion_matrix.matrix[row_idx][col_idx] %}
107
+ <td
108
+ class="cm-cell {% if row_idx == col_idx %}cm-diag{% endif %} {% if val == 0 %}cm-zero{% endif %}">
109
+ {{ val }}
110
+ </td>
111
+ {% endfor %}
112
+ </tr>
113
+ {% endfor %}
114
+ </tbody>
115
+ </table>
116
+ </div>
117
 
118
+ <!-- Summary Cards -->
119
+ <div class="summary-mini">
120
+ <div class="summary-mini-card">
121
+ <span class="summary-mini-label">MACRO F1</span>
122
+ <span class="summary-mini-value">{{ metrics.macro_f1|round(3) }}</span>
123
+ </div>
124
+ <div class="summary-mini-card">
125
+ <span class="summary-mini-label">ACCURACY</span>
126
+ <span class="summary-mini-value">{{ (metrics.accuracy * 100)|round(1) }}%</span>
 
127
  </div>
128
+ </div>
129
  </div>
130
  </div>
131
  {% endif %}
132
 
133
+ <!-- Inference Manifest (Expanded by default for immediate access) -->
134
+ <section class="manifest-section">
135
+ <div class="manifest-header" onclick="toggleManifest()">
136
+ <p class="section-label" style="margin: 0;">INFERENCE MANIFEST</p>
137
+ <span class="toggle-icon" id="manifest-toggle">▼</span>
138
+ </div>
139
+ <div class="manifest-content" id="manifest-content" style="display: block;">
140
+ <div class="table-wrapper" style="max-height: 350px; margin-top: 0.5rem;">
141
+ <table>
142
+ <thead>
143
+ <tr>
144
+ <th>STONE ID</th>
145
+ {% for feature in model_features %}
146
+ <th>{{ feature }}</th>
147
+ {% endfor %}
148
+ <th>AI RESULT</th>
149
+ {% for col in out_of_box_cols %}
150
+ <th>{{ col }}</th>
151
+ {% endfor %}
152
+ <th>IMG</th>
153
+ </tr>
154
+ </thead>
155
+ <tbody>
156
+ {% for row in report_data %}
157
+ <tr>
158
+ <td class="stone-id-col">{{ row.L_Code }}</td>
159
+ {% for feature in model_features %}
160
+ <td style="opacity: 0.7; font-size: 11px;">{{ row[feature] }}</td>
161
+ {% endfor %}
162
+ <td class="result-col">{{ row.Predicted_FGrdCol }}</td>
163
+ {% for col in out_of_box_cols %}
164
+ <td style="font-size: 10px;">
165
+ {% if row[col] and row[col]|string != 'nan' %}
166
+ <span class="badge-oob">{{ row[col] }}</span>
167
+ {% else %}
168
+ <span style="opacity: 0.2;">-</span>
169
+ {% endif %}
170
+ </td>
171
+ {% endfor %}
172
+ <td>
173
+ {% if row.Image_Path != 'N/A' %}
174
+ <img src="/image/{{ row.Image_Path }}" class="img-thumb"
175
+ onclick="openModal(this.src)" alt="Diamond">
176
+ {% else %}
177
+ <span style="font-size: 8px; opacity: 0.3;">N/A</span>
178
+ {% endif %}
179
+ </td>
180
+ </tr>
181
  {% endfor %}
182
+ </tbody>
183
+ </table>
184
+ </div>
 
 
 
 
 
 
 
 
 
185
  </div>
186
  </section>
187
 
188
+ <footer style="margin-top: 1rem; padding-bottom: 1rem; text-align: center;">
189
  <a href="/"
190
+ style="font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: white; opacity: 0.5; text-decoration: none;">←
191
+ NEW BATCH</a>
192
  </footer>
193
  </div>
194
 
 
200
 
201
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
202
  <script>
203
+ function toggleManifest() {
204
+ const content = document.getElementById('manifest-content');
205
+ const toggle = document.getElementById('manifest-toggle');
206
+ if (content.style.display === 'none') {
207
+ content.style.display = 'block';
208
+ toggle.textContent = '▼';
209
+ } else {
210
+ content.style.display = 'none';
211
+ toggle.textContent = '▶';
212
+ }
213
+ }
214
+
215
  function openModal(src) {
216
  const modal = document.getElementById('image-modal');
217
  const modalImg = document.getElementById('modal-img');
 
224
  modal.classList.remove('active');
225
  }
226
 
 
227
  document.getElementById('image-modal').onclick = function (e) {
228
  if (e.target === this) closeModal();
229
  };
230
 
231
+ // Background shader
232
  const vertexShader = `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
233
  const fragmentShader = `
234
  uniform float uTime;