Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>XmLLM β Document Structure Engine</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Chicago&display=swap'); | |
| :root { | |
| --bg: #a8a8a8; | |
| --window-bg: #ffffff; | |
| --titlebar: #000000; | |
| --titlebar-text: #ffffff; | |
| --border: #000000; | |
| --text: #000000; | |
| --button-bg: #ffffff; | |
| --button-shadow: #555555; | |
| --select-bg: #000000; | |
| --select-text: #ffffff; | |
| --disabled: #888888; | |
| --success: #006600; | |
| --error: #cc0000; | |
| --accent: #000000; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: "Geneva", "Chicago", "Charcoal", "Lucida Grande", "Helvetica", monospace; | |
| font-size: 12px; | |
| background: var(--bg); | |
| background-image: | |
| linear-gradient(45deg, #9e9e9e 25%, transparent 25%), | |
| linear-gradient(-45deg, #9e9e9e 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #9e9e9e 75%), | |
| linear-gradient(-45deg, transparent 75%, #9e9e9e 75%); | |
| background-size: 4px 4px; | |
| background-position: 0 0, 0 2px, 2px -2px, -2px 0px; | |
| color: var(--text); | |
| min-height: 100vh; | |
| padding: 8px; | |
| } | |
| /* ββ Desktop icons βββββββββββββββββββββββββββββββββββ */ | |
| .desktop { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .desktop-icon { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 4px; | |
| width: 80px; | |
| padding: 6px 4px; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| user-select: none; | |
| } | |
| .desktop-icon:hover { border: 2px dotted var(--border); } | |
| .desktop-icon.active { | |
| background: var(--select-bg); | |
| color: var(--select-text); | |
| border: 2px solid var(--border); | |
| } | |
| .desktop-icon .icon-img { | |
| width: 48px; | |
| height: 48px; | |
| border: 2px solid var(--border); | |
| background: var(--window-bg); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 22px; | |
| } | |
| .desktop-icon.active .icon-img { | |
| background: var(--select-bg); | |
| color: var(--select-text); | |
| border-color: var(--select-text); | |
| } | |
| .desktop-icon .icon-label { | |
| font-size: 10px; | |
| text-align: center; | |
| word-break: break-word; | |
| } | |
| /* ββ Window chrome βββββββββββββββββββββββββββββββββββ */ | |
| .window { | |
| background: var(--window-bg); | |
| border: 2px solid var(--border); | |
| box-shadow: 3px 3px 0 var(--border); | |
| max-width: 760px; | |
| margin: 0 auto; | |
| } | |
| .titlebar { | |
| background: var(--titlebar); | |
| color: var(--titlebar-text); | |
| padding: 3px 8px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: default; | |
| user-select: none; | |
| } | |
| .titlebar .close-box { | |
| width: 14px; | |
| height: 14px; | |
| border: 1px solid var(--titlebar-text); | |
| display: inline-block; | |
| cursor: pointer; | |
| } | |
| .titlebar .title-text { flex: 1; text-align: center; } | |
| .titlebar .stripes { | |
| flex: 0 0 40px; | |
| height: 10px; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| var(--titlebar-text) 0px, var(--titlebar-text) 1px, | |
| var(--titlebar) 1px, var(--titlebar) 3px | |
| ); | |
| } | |
| .window-body { padding: 16px; } | |
| /* ββ Menu bar ββββββββββββββββββββββββββββββββββββββββ */ | |
| .menubar { | |
| background: var(--window-bg); | |
| border-bottom: 2px solid var(--border); | |
| padding: 2px 8px; | |
| font-size: 11px; | |
| display: flex; | |
| gap: 16px; | |
| } | |
| .menubar span { cursor: default; font-weight: bold; } | |
| /* ββ Buttons βββββββββββββββββββββββββββββββββββββββββ */ | |
| button, .btn { | |
| font-family: inherit; | |
| font-size: 12px; | |
| background: var(--button-bg); | |
| border: 2px solid var(--border); | |
| padding: 4px 16px; | |
| cursor: pointer; | |
| box-shadow: 2px 2px 0 var(--button-shadow); | |
| user-select: none; | |
| } | |
| button:active, .btn:active { | |
| box-shadow: none; | |
| transform: translate(2px, 2px); | |
| } | |
| button:disabled { | |
| color: var(--disabled); | |
| border-color: var(--disabled); | |
| box-shadow: 1px 1px 0 var(--disabled); | |
| cursor: not-allowed; | |
| } | |
| button.primary { | |
| background: var(--accent); | |
| color: var(--titlebar-text); | |
| font-weight: bold; | |
| border-radius: 6px; | |
| padding: 6px 24px; | |
| } | |
| button.primary:active { | |
| background: #333; | |
| } | |
| /* ββ Form elements βββββββββββββββββββββββββββββββββββ */ | |
| label { | |
| font-size: 11px; | |
| font-weight: bold; | |
| display: block; | |
| margin-bottom: 2px; | |
| } | |
| input[type="file"] { | |
| font-family: inherit; | |
| font-size: 11px; | |
| } | |
| /* ββ Drop zone βββββββββββββββββββββββββββββββββββββββ */ | |
| .drop-zone { | |
| border: 3px dashed var(--border); | |
| padding: 32px 16px; | |
| text-align: center; | |
| cursor: pointer; | |
| margin-bottom: 12px; | |
| transition: background 0.15s; | |
| } | |
| .drop-zone:hover, .drop-zone.dragover { | |
| background: #e0e0e0; | |
| } | |
| .drop-zone .icon { font-size: 36px; margin-bottom: 8px; } | |
| .drop-zone .hint { font-size: 11px; color: var(--disabled); } | |
| .drop-zone .filename { | |
| font-weight: bold; | |
| margin-top: 8px; | |
| word-break: break-all; | |
| } | |
| /* ββ Status / progress βββββββββββββββββββββββββββββββ */ | |
| .status-bar { | |
| background: var(--window-bg); | |
| border-top: 2px solid var(--border); | |
| padding: 3px 8px; | |
| font-size: 10px; | |
| color: var(--disabled); | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .progress-text { | |
| font-weight: bold; | |
| padding: 8px; | |
| text-align: center; | |
| font-size: 11px; | |
| } | |
| /* ββ Result card βββββββββββββββββββββββββββββββββββββ */ | |
| .result-area { | |
| border: 2px solid var(--border); | |
| padding: 12px; | |
| margin-top: 12px; | |
| background: #f5f5f5; | |
| } | |
| .result-area h3 { | |
| font-size: 12px; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 4px; | |
| margin-bottom: 8px; | |
| } | |
| .result-row { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| align-items: center; | |
| } | |
| .result-label { font-weight: bold; min-width: 80px; font-size: 11px; } | |
| .result-value { font-size: 11px; } | |
| .tag { | |
| display: inline-block; | |
| border: 1px solid var(--border); | |
| padding: 1px 6px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| } | |
| .tag.ok { background: #c6f5c6; } | |
| .tag.no { background: #f0f0f0; color: var(--disabled); } | |
| .download-btn { | |
| display: inline-block; | |
| font-family: inherit; | |
| font-size: 11px; | |
| background: var(--window-bg); | |
| border: 2px solid var(--border); | |
| padding: 3px 12px; | |
| cursor: pointer; | |
| text-decoration: none; | |
| color: var(--text); | |
| box-shadow: 2px 2px 0 var(--button-shadow); | |
| margin: 2px; | |
| } | |
| .download-btn:active { | |
| box-shadow: none; | |
| transform: translate(2px, 2px); | |
| } | |
| /* ββ Event log βββββββββββββββββββββββββββββββββββββββ */ | |
| .event-log { | |
| font-family: "Monaco", "Courier New", monospace; | |
| font-size: 10px; | |
| background: var(--window-bg); | |
| border: 2px inset var(--border); | |
| padding: 6px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| white-space: pre; | |
| margin-top: 8px; | |
| } | |
| /* ββ Jobs table ββββββββββββββββββββββββββββββββββββββ */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 11px; | |
| } | |
| th { | |
| text-align: left; | |
| font-weight: bold; | |
| border-bottom: 2px solid var(--border); | |
| padding: 4px 6px; | |
| background: #e8e8e8; | |
| } | |
| td { | |
| padding: 3px 6px; | |
| border-bottom: 1px solid #ccc; | |
| } | |
| tr:hover td { background: #e8e8f0; } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 1px 6px; | |
| font-size: 10px; | |
| font-weight: bold; | |
| border: 1px solid; | |
| } | |
| .status-badge.succeeded { background: #c6f5c6; border-color: var(--success); } | |
| .status-badge.failed { background: #f5c6c6; border-color: var(--error); } | |
| .status-badge.partial_success { background: #f5eec6; border-color: #886600; } | |
| .status-badge.running { background: #c6e0f5; border-color: #004488; } | |
| .status-badge.queued { background: #e8e8e8; border-color: var(--disabled); } | |
| .hidden { display: none; } | |
| /* ββ Responsive ββββββββββββββββββββββββββββββββββββββ */ | |
| @media (max-width: 600px) { | |
| .desktop { justify-content: center; } | |
| .window { margin: 0 4px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- βββ Desktop Icons βββ --> | |
| <div class="desktop"> | |
| <div class="desktop-icon active" onclick="showPage('scan')"> | |
| <div class="icon-img">📄</div> | |
| <div class="icon-label">Scan Document</div> | |
| </div> | |
| <div class="desktop-icon" onclick="showPage('jobs')"> | |
| <div class="icon-img">📁</div> | |
| <div class="icon-label">Documents</div> | |
| </div> | |
| <div class="desktop-icon" onclick="showPage('advanced')"> | |
| <div class="icon-img">⚙</div> | |
| <div class="icon-label">Advanced</div> | |
| </div> | |
| </div> | |
| <!-- βββ SCAN WINDOW βββ --> | |
| <div id="page-scan" class="window"> | |
| <div class="titlebar"> | |
| <span class="close-box"></span> | |
| <span class="stripes"></span> | |
| <span class="title-text">Scan Document</span> | |
| <span class="stripes"></span> | |
| </div> | |
| <div class="menubar"> | |
| <span>File</span> | |
| <span>Edit</span> | |
| <span>View</span> | |
| </div> | |
| <div class="window-body"> | |
| <div class="drop-zone" id="drop-zone" onclick="document.getElementById('image-input').click()"> | |
| <div class="icon">🖼</div> | |
| <div><b>Drop an image here</b></div> | |
| <div class="hint">or click to browse — PNG, JPEG, TIFF, WebP</div> | |
| <div class="filename hidden" id="file-label"></div> | |
| <input type="file" id="image-input" accept=".png,.jpg,.jpeg,.tiff,.tif,.webp,.bmp" style="display:none"> | |
| </div> | |
| <div style="text-align:center;"> | |
| <button class="primary" id="btn-scan" onclick="runScan()" disabled> | |
| ▶ Run OCR Pipeline | |
| </button> | |
| </div> | |
| <div class="progress-text hidden" id="progress">Processing…</div> | |
| <div class="result-area hidden" id="result-area"> | |
| <h3>☑ Result</h3> | |
| <div class="result-row"> | |
| <span class="result-label">Status</span> | |
| <span class="result-value" id="res-status"></span> | |
| </div> | |
| <div class="result-row"> | |
| <span class="result-label">Duration</span> | |
| <span class="result-value" id="res-duration"></span> | |
| </div> | |
| <div class="result-row"> | |
| <span class="result-label">Exports</span> | |
| <span class="result-value" id="res-exports"></span> | |
| </div> | |
| <div class="result-row" id="res-downloads-row"> | |
| <span class="result-label">Download</span> | |
| <span class="result-value" id="res-downloads"></span> | |
| </div> | |
| <div class="event-log hidden" id="event-log"></div> | |
| <div style="margin-top:6px;text-align:right;"> | |
| <button onclick="toggleLog()">Show Log</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status-bar"> | |
| <span id="statusbar-left">Ready</span> | |
| <span>XmLLM v0.1.0</span> | |
| </div> | |
| </div> | |
| <!-- βββ JOBS WINDOW βββ --> | |
| <div id="page-jobs" class="window hidden"> | |
| <div class="titlebar"> | |
| <span class="close-box"></span> | |
| <span class="stripes"></span> | |
| <span class="title-text">Documents</span> | |
| <span class="stripes"></span> | |
| </div> | |
| <div class="menubar"> | |
| <span>File</span> | |
| <span>View</span> | |
| </div> | |
| <div class="window-body"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>Status</th> | |
| <th>Source</th> | |
| <th>ALTO</th> | |
| <th>PAGE</th> | |
| <th>Duration</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="jobs-table"> | |
| <tr><td colspan="7" style="text-align:center;color:#888;">Loading…</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="status-bar"> | |
| <span id="jobs-count">-</span> | |
| <span>XmLLM v0.1.0</span> | |
| </div> | |
| </div> | |
| <!-- βββ ADVANCED WINDOW (raw payload) βββ --> | |
| <div id="page-advanced" class="window hidden"> | |
| <div class="titlebar"> | |
| <span class="close-box"></span> | |
| <span class="stripes"></span> | |
| <span class="title-text">Advanced — Raw Payload</span> | |
| <span class="stripes"></span> | |
| </div> | |
| <div class="menubar"> | |
| <span>File</span> | |
| </div> | |
| <div class="window-body"> | |
| <p style="margin-bottom:12px;font-size:11px;color:#555;"> | |
| Upload a raw OCR JSON payload manually (for providers other than PaddleOCR). | |
| </p> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;"> | |
| <div> | |
| <label>Raw Payload JSON</label> | |
| <input type="file" id="payload-file" accept=".json"> | |
| </div> | |
| <div> | |
| <label>Provider Family</label> | |
| <select id="provider-family" style="font-family:inherit;font-size:11px;padding:2px;width:100%;border:2px solid var(--border);"> | |
| <option value="word_box_json">word_box_json (PaddleOCR)</option> | |
| <option value="line_box_json">line_box_json</option> | |
| <option value="text_only">text_only (mLLM)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:12px;"> | |
| <div> | |
| <label>Provider ID</label> | |
| <input type="text" id="provider-id" value="paddleocr" style="font-family:inherit;font-size:11px;padding:2px;width:100%;border:2px solid var(--border);"> | |
| </div> | |
| <div> | |
| <label>Image Width (px)</label> | |
| <input type="number" id="img-width" value="2480" style="font-family:inherit;font-size:11px;padding:2px;width:100%;border:2px solid var(--border);"> | |
| </div> | |
| <div> | |
| <label>Image Height (px)</label> | |
| <input type="number" id="img-height" value="3508" style="font-family:inherit;font-size:11px;padding:2px;width:100%;border:2px solid var(--border);"> | |
| </div> | |
| </div> | |
| <button class="primary" id="btn-run-advanced" onclick="runAdvanced()">Run Pipeline</button> | |
| <span id="adv-status" style="margin-left:8px;font-size:11px;color:#888;"></span> | |
| </div> | |
| <div class="status-bar"> | |
| <span>Manual mode</span> | |
| <span>XmLLM v0.1.0</span> | |
| </div> | |
| </div> | |
| <script> | |
| const API = ''; | |
| /* ββ Page switching ββ */ | |
| function showPage(name) { | |
| ['scan','jobs','advanced'].forEach(p => { | |
| document.getElementById('page-' + p).classList.add('hidden'); | |
| }); | |
| document.getElementById('page-' + name).classList.remove('hidden'); | |
| document.querySelectorAll('.desktop-icon').forEach(d => d.classList.remove('active')); | |
| event.currentTarget.classList.add('active'); | |
| if (name === 'jobs') loadJobs(); | |
| } | |
| /* ββ File drop zone ββ */ | |
| const dropZone = document.getElementById('drop-zone'); | |
| const imageInput = document.getElementById('image-input'); | |
| ['dragenter','dragover'].forEach(e => { | |
| dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.add('dragover'); }); | |
| }); | |
| ['dragleave','drop'].forEach(e => { | |
| dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.remove('dragover'); }); | |
| }); | |
| dropZone.addEventListener('drop', ev => { | |
| if (ev.dataTransfer.files.length) { | |
| imageInput.files = ev.dataTransfer.files; | |
| onFileSelected(); | |
| } | |
| }); | |
| imageInput.addEventListener('change', onFileSelected); | |
| function onFileSelected() { | |
| const f = imageInput.files[0]; | |
| if (!f) return; | |
| const label = document.getElementById('file-label'); | |
| label.textContent = f.name + ' (' + (f.size / 1024).toFixed(0) + ' KB)'; | |
| label.classList.remove('hidden'); | |
| document.getElementById('btn-scan').disabled = false; | |
| document.getElementById('statusbar-left').textContent = 'Image loaded: ' + f.name; | |
| } | |
| /* ββ Main scan (image β OCR β XML) ββ */ | |
| async function runScan() { | |
| const f = imageInput.files[0]; | |
| if (!f) return; | |
| const btn = document.getElementById('btn-scan'); | |
| const progress = document.getElementById('progress'); | |
| const result = document.getElementById('result-area'); | |
| btn.disabled = true; | |
| progress.classList.remove('hidden'); | |
| result.classList.add('hidden'); | |
| document.getElementById('statusbar-left').textContent = 'Running OCR pipeline\u2026'; | |
| const fd = new FormData(); | |
| fd.append('image', f); | |
| try { | |
| const r = await fetch(API + '/ocr', { method: 'POST', body: fd }); | |
| const data = await r.json(); | |
| if (!r.ok) { | |
| progress.textContent = 'Error: ' + (data.detail || r.statusText); | |
| progress.style.color = 'var(--error)'; | |
| document.getElementById('statusbar-left').textContent = 'Error'; | |
| btn.disabled = false; | |
| return; | |
| } | |
| progress.classList.add('hidden'); | |
| progress.style.color = ''; | |
| progress.textContent = 'Processing\u2026'; | |
| btn.disabled = false; | |
| showResult(data); | |
| document.getElementById('statusbar-left').textContent = 'Done \u2014 ' + data.status; | |
| } catch(e) { | |
| progress.textContent = 'Network error: ' + e.message; | |
| progress.style.color = 'var(--error)'; | |
| btn.disabled = false; | |
| } | |
| } | |
| function showResult(data) { | |
| const area = document.getElementById('result-area'); | |
| area.classList.remove('hidden'); | |
| document.getElementById('res-status').innerHTML = | |
| '<span class="status-badge ' + data.status + '">' + data.status.toUpperCase() + '</span>'; | |
| document.getElementById('res-duration').textContent = | |
| data.duration_ms ? Math.round(data.duration_ms) + ' ms' : '\u2014'; | |
| document.getElementById('res-exports').innerHTML = | |
| '<span class="tag ' + (data.has_alto ? 'ok' : 'no') + '">ALTO ' + (data.has_alto ? '\u2713' : '\u2717') + '</span> ' + | |
| '<span class="tag ' + (data.has_page_xml ? 'ok' : 'no') + '">PAGE ' + (data.has_page_xml ? '\u2713' : '\u2717') + '</span>'; | |
| const dl = document.getElementById('res-downloads'); | |
| const id = data.job_id; | |
| dl.innerHTML = ''; | |
| if (data.has_alto) | |
| dl.innerHTML += '<a class="download-btn" href="' + API + '/jobs/' + id + '/alto">📄 ALTO XML</a>'; | |
| if (data.has_page_xml) | |
| dl.innerHTML += '<a class="download-btn" href="' + API + '/jobs/' + id + '/pagexml">📄 PAGE XML</a>'; | |
| dl.innerHTML += '<a class="download-btn" href="' + API + '/jobs/' + id + '/canonical" target="_blank">📋 Canonical JSON</a>'; | |
| // Load event log | |
| fetch(API + '/jobs/' + id + '/logs').then(r => r.json()).then(events => { | |
| document.getElementById('event-log').textContent = events.map(e => | |
| '[' + e.status.padEnd(9) + '] ' + e.step.padEnd(20) + ' ' + | |
| (e.duration_ms ? Math.round(e.duration_ms) + 'ms' : e.message || '') | |
| ).join('\n'); | |
| }).catch(() => {}); | |
| } | |
| function toggleLog() { | |
| const log = document.getElementById('event-log'); | |
| log.classList.toggle('hidden'); | |
| } | |
| /* ββ Jobs list ββ */ | |
| async function loadJobs() { | |
| try { | |
| const r = await fetch(API + '/jobs'); | |
| const jobs = await r.json(); | |
| const tbody = document.getElementById('jobs-table'); | |
| if (!jobs.length) { | |
| tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#888;">No documents yet</td></tr>'; | |
| document.getElementById('jobs-count').textContent = '0 documents'; | |
| return; | |
| } | |
| tbody.innerHTML = jobs.map(j => `<tr> | |
| <td style="font-family:monospace;font-size:10px;">${j.job_id.slice(-8)}</td> | |
| <td><span class="status-badge ${j.status}">${j.status}</span></td> | |
| <td>${j.source_filename || '\u2014'}</td> | |
| <td><span class="tag ${j.has_alto ? 'ok' : 'no'}">${j.has_alto ? '\u2713' : '\u2717'}</span></td> | |
| <td><span class="tag ${j.has_page_xml ? 'ok' : 'no'}">${j.has_page_xml ? '\u2713' : '\u2717'}</span></td> | |
| <td>${j.duration_ms ? Math.round(j.duration_ms) + 'ms' : '\u2014'}</td> | |
| <td> | |
| ${j.has_alto ? `<a class="download-btn" href="${API}/jobs/${j.job_id}/alto" style="font-size:10px;">ALTO</a>` : ''} | |
| ${j.has_page_xml ? `<a class="download-btn" href="${API}/jobs/${j.job_id}/pagexml" style="font-size:10px;">PAGE</a>` : ''} | |
| </td> | |
| </tr>`).join(''); | |
| document.getElementById('jobs-count').textContent = jobs.length + ' document' + (jobs.length > 1 ? 's' : ''); | |
| } catch(e) { | |
| document.getElementById('jobs-table').innerHTML = | |
| '<tr><td colspan="7" style="color:var(--error);">Failed to load</td></tr>'; | |
| } | |
| } | |
| /* ββ Advanced (raw payload) ββ */ | |
| async function runAdvanced() { | |
| const fileInput = document.getElementById('payload-file'); | |
| if (!fileInput.files.length) { alert('Select a payload JSON file'); return; } | |
| const btn = document.getElementById('btn-run-advanced'); | |
| const status = document.getElementById('adv-status'); | |
| btn.disabled = true; | |
| status.textContent = 'Running\u2026'; | |
| const fd = new FormData(); | |
| fd.append('raw_payload_file', fileInput.files[0]); | |
| const params = new URLSearchParams({ | |
| provider_id: document.getElementById('provider-id').value, | |
| provider_family: document.getElementById('provider-family').value, | |
| image_width: document.getElementById('img-width').value, | |
| image_height: document.getElementById('img-height').value, | |
| }); | |
| try { | |
| const r = await fetch(API + '/jobs?' + params, { method: 'POST', body: fd }); | |
| const data = await r.json(); | |
| btn.disabled = false; | |
| status.textContent = r.ok | |
| ? '\u2713 ' + data.status + (data.duration_ms ? ' (' + Math.round(data.duration_ms) + 'ms)' : '') | |
| : 'Error: ' + (data.detail || r.statusText); | |
| } catch(e) { | |
| btn.disabled = false; | |
| status.textContent = 'Error: ' + e.message; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |