| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Prediction Results</title> |
| <style> |
| :root { |
| color-scheme: light; |
| --bg: #f4f0e8; |
| --panel: rgba(255, 255, 255, 0.9); |
| --panel-border: rgba(33, 37, 41, 0.12); |
| --text: #1f2937; |
| --muted: #5b6472; |
| --accent: #2563a8; |
| --shadow: 0 18px 50px rgba(32, 41, 48, 0.12); |
| } |
| body { |
| margin: 0; |
| font-family: "Segoe UI", Tahoma, sans-serif; |
| color: var(--text); |
| background: |
| radial-gradient(circle at top left, rgba(37, 99, 168, 0.14), transparent 28%), |
| linear-gradient(180deg, #faf7f1 0%, var(--bg) 100%); |
| } |
| .wrap { max-width: 1180px; margin: 0 auto; padding: 32px 20px 56px; } |
| .card { border: 1px solid var(--panel-border); border-radius: 18px; padding: 18px; background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(10px); margin-top: 16px; } |
| .topline { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; } |
| .pill { display: inline-flex; align-items: center; gap: 8px; border-radius: 999px; padding: 8px 12px; background: rgba(37, 99, 168, 0.1); color: var(--accent); font-weight: 700; } |
| .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } |
| .stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } |
| .stat { padding: 12px 14px; border-radius: 14px; background: rgba(37, 99, 168, 0.06); border: 1px solid rgba(37, 99, 168, 0.12); } |
| .stat .k { font-size: 12px; color: var(--muted); margin-bottom: 6px; } |
| .stat .v { font-size: 18px; font-weight: 700; } |
| .meta { color: var(--muted); font-size: 14px; line-height: 1.6; } |
| table { width: 100%; border-collapse: collapse; overflow: hidden; } |
| th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.08); font-size: 14px; } |
| th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; } |
| a { color: var(--accent); text-decoration: none; } |
| a:hover { text-decoration: underline; } |
| pre { white-space: pre-wrap; word-wrap: break-word; background: #f9fafb; padding: 14px; border-radius: 12px; overflow-x: auto; } |
| .file-field { display: grid; gap: 8px; } |
| .file-control { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| width: 100%; |
| min-width: 0; |
| padding: 10px 12px; |
| border: 1px solid #cdd5df; |
| border-radius: 10px; |
| background: #fff; |
| box-sizing: border-box; |
| overflow: hidden; |
| } |
| .file-control button { |
| width: 110px; |
| flex: 0 0 110px; |
| white-space: nowrap; |
| padding: 8px 12px; |
| border-radius: 8px; |
| } |
| .file-name { |
| color: var(--muted); |
| font-size: 13px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| min-width: 0; |
| flex: 1 1 auto; |
| } |
| .file-control input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } |
| .preview-img { |
| width: 100%; |
| height: auto; |
| border-radius: 12px; |
| border: 1px solid rgba(0,0,0,0.08); |
| background: white; |
| } |
| .top5-grid { |
| display: grid; |
| gap: 8px; |
| } |
| .top5-item { |
| padding: 10px 12px; |
| border-radius: 12px; |
| background: rgba(37, 99, 168, 0.06); |
| border: 1px solid rgba(37, 99, 168, 0.12); |
| font-size: 13px; |
| line-height: 1.6; |
| } |
| .helper-note { |
| color: var(--muted); |
| font-size: 13px; |
| line-height: 1.6; |
| margin-top: 8px; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
| <div class="topline"> |
| <div> |
| <h2>Prediction Results: {{ prediction_id }}</h2> |
| <div class="meta">The model checkpoint has been loaded and predictions have been generated.</div> |
| </div> |
| <div class="pill">DONE</div> |
| </div> |
|
|
| <div class="card"> |
| <form action="/predict" method="post" enctype="multipart/form-data" class="grid" style="margin-bottom: 16px;"> |
| <div class="file-field"> |
| <label>Saved Model (.pth)</label> |
| <div class="file-control"> |
| <button type="button" data-file-target="predict_model_file">Choose file</button> |
| <span class="file-name" data-file-name="predict_model_file">No file selected</span> |
| <input type="file" id="predict_model_file" name="model_file" accept=".pth" required> |
| </div> |
| </div> |
| <div class="file-field"> |
| <label>True Label Mapping File (optional, .json/.txt)</label> |
| <div class="file-control"> |
| <button type="button" data-file-target="predict_label_mapping_file">Choose file</button> |
| <span class="file-name" data-file-name="predict_label_mapping_file">No file selected</span> |
| <input type="file" id="predict_label_mapping_file" name="label_mapping_file" accept=".json,.txt"> |
| </div> |
| </div> |
| <div class="file-field"> |
| <label>Spectral (.npy/.txt/.csv)</label> |
| <div class="file-control"> |
| <button type="button" data-file-target="predict_spectral_file">Choose file</button> |
| <span class="file-name" data-file-name="predict_spectral_file">No file selected</span> |
| <input type="file" id="predict_spectral_file" name="spectral_file" accept=".npy,.txt,.csv" required> |
| </div> |
| </div> |
| <div class="file-field"> |
| <label>Wavelengths / Wavenumbers (optional, .npy/.txt)</label> |
| <div class="file-control"> |
| <button type="button" data-file-target="predict_wavenumbers_file">Choose file</button> |
| <span class="file-name" data-file-name="predict_wavenumbers_file">No file selected</span> |
| <input type="file" id="predict_wavenumbers_file" name="wavenumbers_file" accept=".npy,.txt,.csv"> |
| </div> |
| </div> |
| <div class="file-field"> |
| <label>Manual wavelength range fallback (optional)</label> |
| <div class="grid" style="grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;"> |
| <div> |
| <label style="margin-bottom: 4px;">Low value</label> |
| <input type="number" step="0.0001" name="manual_low_cm" placeholder="e.g. 0"> |
| </div> |
| <div> |
| <label style="margin-bottom: 4px;">High value</label> |
| <input type="number" step="0.0001" name="manual_high_cm" placeholder="e.g. 3500"> |
| </div> |
| </div> |
| </div> |
| <div style="grid-column: 1 / -1;"> |
| <button type="submit" style="width: 100%;">Run Prediction</button> |
| </div> |
| </form> |
|
|
| <div class="helper-note"> |
| If your spectrum file already contains two columns (wavelength and intensity), you can leave the wavelength file empty. |
| If the spectrum file contains only intensity values, upload a wavelength file or fill in the manual range. |
| </div> |
|
|
| <div class="stats"> |
| <div class="stat"><div class="k">Samples</div><div class="v">{{ summary.num_samples }}</div></div> |
| <div class="stat"><div class="k">Classes</div><div class="v">{{ summary.class_names|length }}</div></div> |
| <div class="stat"><div class="k">Download CSV</div><div class="v"><a href="{{ download_csv }}">CSV</a></div></div> |
| </div> |
| <div class="meta" style="margin-top: 12px;">If you need the same checkpoint for another dataset, keep the exported pth file and upload it here again.</div> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <h3>Per-sample Predictions</h3> |
| <table> |
| <thead> |
| <tr> |
| <th>Sample</th> |
| <th>Predicted class</th> |
| <th>Confidence</th> |
| </tr> |
| </thead> |
| <tbody> |
| {% for row in rows %} |
| <tr> |
| <td>{{ row.sample_index }}</td> |
| <td>{{ row.pred_label }}</td> |
| <td>{{ "%.4f"|format(row.confidence) }}</td> |
| </tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
|
|
| <div class="card"> |
| <h3>Top-5 Logits</h3> |
| <div class="top5-grid"> |
| {% for row in top5_rows %} |
| <div class="top5-item"> |
| <strong>Sample {{ row.sample_index }}</strong> |
| <div> |
| {% for item in row.top5 %} |
| <div>Top{{ item.rank }}: {{ item.class_name }} - {{ "%.6f"|format(item.logit) }}</div> |
| {% endfor %} |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| </div> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <h3>Input Spectra Preview</h3> |
| <img class="preview-img" src="{{ preview_image }}" alt="Input spectra preview"> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h3>Model Notes</h3> |
| <div class="meta">The model checkpoint was loaded with its saved architecture and preprocessing config, so the same pth file can be reused later for inference on new spectra.</div> |
| {% if summary.spectrum_source or summary.wavenumber_source %} |
| <div class="helper-note">Spectrum source: {{ summary.spectrum_source }} | Wavelength source: {{ summary.wavenumber_source }}</div> |
| {% endif %} |
| {% if summary.label_mapping_source %} |
| <div class="helper-note">Label mapping source: {{ summary.label_mapping_source }}</div> |
| {% endif %} |
| </div> |
|
|
| <p style="margin-top: 12px;"><a href="/">Back</a></p> |
| </div> |
|
|
| <script> |
| document.querySelectorAll('[data-file-target]').forEach((button) => { |
| button.addEventListener('click', () => { |
| const targetId = button.getAttribute('data-file-target'); |
| const input = document.getElementById(targetId); |
| if (input) { |
| input.click(); |
| } |
| }); |
| }); |
| |
| document.querySelectorAll('input[type="file"]').forEach((input) => { |
| input.addEventListener('change', () => { |
| const nameNode = document.querySelector(`[data-file-name="${input.id}"]`); |
| if (nameNode) { |
| const fileName = input.files.length ? input.files[0].name : 'No file selected'; |
| nameNode.textContent = fileName; |
| nameNode.title = fileName; |
| } |
| }); |
| }); |
| |
| const syncHoverText = () => { |
| document.querySelectorAll('input:not([type="file"]):not([type="hidden"])').forEach((input) => { |
| const hoverText = input.value || input.placeholder || input.getAttribute('aria-label') || input.name || ''; |
| input.title = hoverText; |
| }); |
| |
| document.querySelectorAll('.file-name').forEach((node) => { |
| node.title = node.textContent.trim(); |
| }); |
| }; |
| |
| document.querySelectorAll('input:not([type="file"]):not([type="hidden"])').forEach((input) => { |
| input.addEventListener('input', syncHoverText); |
| input.addEventListener('change', syncHoverText); |
| }); |
| |
| syncHoverText(); |
| </script> |
| </body> |
| </html> |
|
|