(function () { 'use strict'; const STORAGE_KEYS = { selectedDocIds: 'pluto.selectedDocIds', detailLevel: 'pluto.detailLevel', }; const RUN_TIMEOUT_MS = 600000; const BENCH_TIMEOUT_MS = 600000; const stages = ['route', 'extract', 'merge', 'evidence_check']; const stageEls = {}; const statusEls = {}; const connectors = document.querySelectorAll('.stage-rail__connector'); const queryInput = document.getElementById('queryInput'); const runBtn = document.getElementById('runBtn'); const benchBtn = document.getElementById('benchBtn'); const detailModeSelect = document.getElementById('detailModeSelect'); const queryScopeLabel = document.getElementById('queryScopeLabel'); const answerBody = document.getElementById('answerBody'); const evidenceBody = document.getElementById('evidenceBody'); const traceBody = document.getElementById('traceBody'); const busBody = document.getElementById('busBody'); const confRing = document.getElementById('confRing'); const confValue = document.getElementById('confValue'); const benchPanel = document.getElementById('benchPanel'); const benchBody = document.getElementById('benchBody'); const dropArea = document.getElementById('dropArea'); const fileInput = document.getElementById('fileInput'); const uploadStatus = document.getElementById('uploadStatus'); const corpusDocs = document.getElementById('corpusDocs'); const refreshCorpus = document.getElementById('refreshCorpus'); const corpusSelectionSummary = document.getElementById('corpusSelectionSummary'); let uploadProcessingActive = false; let pipelineRunning = false; let activeEventSource = null; let activeSessionId = null; let previousQuery = ''; let previousQueryTimestamp = null; let previousSessionId = null; let latestCorpusDocs = []; let pendingCorpusDocIds = []; let selectedDocIds = loadStoredDocIds(); let detailLevel = loadStoredDetailLevel(); let corpusRefreshTimer = null; stages.forEach((stageName) => { stageEls[stageName] = document.getElementById(`stage-${stageName}`); statusEls[stageName] = document.getElementById(`status-${stageName}`); }); init(); function init() { detailModeSelect.value = detailLevel; detailModeSelect.addEventListener('change', () => { detailLevel = normalizeDetailLevel(detailModeSelect.value); detailModeSelect.value = detailLevel; localStorage.setItem(STORAGE_KEYS.detailLevel, detailLevel); updateSelectionSummary(); }); runBtn.addEventListener('click', runPipeline); benchBtn.addEventListener('click', runBenchmark); queryInput.addEventListener('keydown', (event) => { if (event.key === 'Enter' && !queryInput.disabled) { runBtn.click(); } }); queryInput.addEventListener('input', syncControls); ['dragenter', 'dragover'].forEach((eventName) => { dropArea.addEventListener(eventName, (event) => { event.preventDefault(); dropArea.classList.add('dragover'); }); }); ['dragleave', 'drop'].forEach((eventName) => { dropArea.addEventListener(eventName, (event) => { event.preventDefault(); dropArea.classList.remove('dragover'); }); }); dropArea.addEventListener('drop', (event) => { const files = event.dataTransfer.files; if (files && files.length) { uploadFiles(files); } }); dropArea.addEventListener('click', () => { if (!uploadProcessingActive && !pipelineRunning) { fileInput.click(); } }); fileInput.addEventListener('change', () => { if (fileInput.files && fileInput.files.length) { uploadFiles(fileInput.files); } fileInput.value = ''; }); refreshCorpus.addEventListener('click', loadCorpus); loadCorpus(); syncControls(); } function syncControls() { const hasText = queryInput.value.trim().length > 0; const controlsLocked = pipelineRunning || hasBlockingPendingDocs(); queryInput.disabled = controlsLocked; queryInput.style.opacity = controlsLocked ? '0.7' : '1'; queryInput.style.cursor = controlsLocked ? 'not-allowed' : ''; detailModeSelect.disabled = controlsLocked; runBtn.disabled = controlsLocked || !hasText; runBtn.style.opacity = runBtn.disabled ? '0.5' : '1'; runBtn.style.cursor = runBtn.disabled ? 'not-allowed' : ''; benchBtn.disabled = controlsLocked || !hasText; benchBtn.style.opacity = benchBtn.disabled ? '0.5' : '1'; benchBtn.style.cursor = benchBtn.disabled ? 'not-allowed' : ''; refreshCorpus.disabled = controlsLocked; refreshCorpus.style.opacity = refreshCorpus.disabled ? '0.5' : '1'; refreshCorpus.style.cursor = refreshCorpus.disabled ? 'not-allowed' : ''; dropArea.style.pointerEvents = controlsLocked ? 'none' : ''; dropArea.style.opacity = controlsLocked ? '0.7' : '1'; } function hasBlockingPendingDocs() { if (uploadProcessingActive) { return true; } if (!pendingCorpusDocIds.length) { return false; } if (!selectedDocIds.length) { return true; } return selectedDocIds.some((docId) => pendingCorpusDocIds.includes(docId)); } async function runPipeline() { const query = queryInput.value.trim(); if (!query || pipelineRunning || hasBlockingPendingDocs()) { return; } pipelineRunning = true; const sessionId = createSessionId(); const queryTimestamp = Date.now(); try { activeSessionId = sessionId; syncControls(); runBtn.innerHTML = ' Running...'; resetUI(); listenSSE(sessionId); const response = await fetchWithTimeout('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildQueryPayload(query, sessionId, queryTimestamp)), }, RUN_TIMEOUT_MS, 'Pipeline request timed out. The server may still be working; try again or refresh the app.'); const data = await parseJsonResponse(response, 'Server returned an invalid response'); activeSessionId = data.session_id || sessionId; if (!response.ok || data.error) { throw new Error(data.error || `Server error: ${response.status}`); } renderResult(data); previousQuery = query; previousQueryTimestamp = queryTimestamp; previousSessionId = data.session_id || sessionId; } catch (error) { answerBody.innerHTML = renderErrorCard('Pipeline Error', error.message); console.error(error); } finally { closeActiveStream(); pipelineRunning = false; activeSessionId = null; runBtn.innerHTML = ' Run Pipeline'; syncControls(); } } async function runBenchmark() { const query = queryInput.value.trim(); if (!query || pipelineRunning || hasBlockingPendingDocs()) { return; } pipelineRunning = true; syncControls(); benchBtn.innerHTML = ' Benchmarking...'; benchPanel.hidden = false; benchBody.innerHTML = '
Running side-by-side comparison...
'; try { const response = await fetchWithTimeout('/api/compare', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildQueryPayload(query)), }, BENCH_TIMEOUT_MS, 'Benchmark request timed out. Try again after the server finishes the current work.'); const data = await parseJsonResponse(response, 'Benchmark returned an invalid response'); if (!response.ok || data.error) { throw new Error(data.error || `Benchmark error: ${response.status}`); } renderBenchmark(data); } catch (error) { benchBody.innerHTML = `

Error: ${esc(error.message)}

`; } finally { pipelineRunning = false; benchBtn.innerHTML = ' Benchmark'; syncControls(); } } function buildQueryPayload(query, sessionId = activeSessionId, queryTimestamp = Date.now()) { return { query, session_id: sessionId, query_timestamp: queryTimestamp, prev_query: previousQuery, prev_query_timestamp: previousQueryTimestamp, prev_session_id: previousSessionId, selected_doc_ids: [...selectedDocIds], detail_level: detailLevel, }; } function listenSSE(sessionId) { closeActiveStream(); const eventSource = new EventSource(`/api/stream?session_id=${encodeURIComponent(sessionId)}`); activeEventSource = eventSource; eventSource.onmessage = (event) => { try { const payload = JSON.parse(event.data); handleProgress(payload); if (payload.stage === 'done' || payload.stage === 'error') { eventSource.close(); if (activeEventSource === eventSource) { activeEventSource = null; } } } catch (error) { console.error('Failed to parse stream event', error); } }; eventSource.onerror = (error) => { console.error('Progress stream disconnected', error); if (activeEventSource === eventSource) { eventSource.close(); activeEventSource = null; } }; } function closeActiveStream() { if (activeEventSource) { activeEventSource.close(); activeEventSource = null; } } function clearCorpusAutoRefresh() { if (corpusRefreshTimer) { window.clearTimeout(corpusRefreshTimer); corpusRefreshTimer = null; } } function scheduleCorpusAutoRefresh() { clearCorpusAutoRefresh(); if (!pendingCorpusDocIds.length) { return; } corpusRefreshTimer = window.setTimeout(() => { loadCorpus(); }, 2500); } function handleProgress(data) { const stage = data.stage; if (stage === 'bus') { appendBusMessage({ sender: data.sender, type: data.type, payload: data.payload, }); return; } if (stage === 'error') { answerBody.innerHTML = renderErrorCard('Pipeline Error', data.detail || 'Unknown error'); return; } if (stage === 'finish' || stage === 'done') { markAllStagesComplete(); return; } if (stage === 'connected' || stage === 'heartbeat' || !stages.includes(stage)) { return; } const status = data.status; const index = stages.indexOf(stage); if (status === 'running') { stageEls[stage].classList.add('active'); stageEls[stage].classList.remove('complete'); statusEls[stage].innerHTML = 'running'; answerBody.innerHTML = `

${stage.toUpperCase()}: processing...

`; return; } if (status === 'complete') { stageEls[stage].classList.remove('active'); stageEls[stage].classList.add('complete'); let info = 'done'; if (stage === 'route' && data.chunks) { info = `done (${data.chunks} chunks)`; } else if (stage === 'extract' && data.extractions) { info = `done (${data.extractions} facts)`; } else if (stage === 'merge' && data.key_claims) { info = `done (${data.key_claims} claims)`; } else if (stage === 'evidence_check' && data.checked) { info = `done (${data.checked} checked)`; } statusEls[stage].innerHTML = `${esc(info)}`; if (index < connectors.length) { connectors[index].classList.add('active'); } } } function renderResult(data) { if (data.error) { answerBody.innerHTML = `
${esc(data.error)}
`; return; } markAllStagesComplete(); const finalAnswer = data.final_answer || {}; let html = ''; if (Array.isArray(finalAnswer.sections) && finalAnswer.sections.length) { html = finalAnswer.sections.map((section) => `
${esc(section.title)}
${esc(section.content)}
`).join(''); } else if (finalAnswer.response) { html = `
${esc(finalAnswer.response)}
`; } const gaps = Array.isArray(data.missing_info) ? data.missing_info : []; const nextActions = Array.isArray(data.next_actions) ? data.next_actions : []; if (gaps.length) { const gapTitle = nextActions.length ? 'Evidence Check / Coverage Gaps Found' : 'Coverage Gaps Noted'; const gapIntro = nextActions.length ? 'Some answer points could not be fully supported from the extracted evidence.' : 'The detailed answer asked for coverage beyond what the document clearly supports in the selected scope.'; const gapPrefix = nextActions.length ? 'Need support:' : 'Not clearly covered:'; html += `
${esc(gapTitle)}
${esc(gapIntro)}
${gaps.map((gap) => `
${esc(gapPrefix)} ${esc(typeof gap === 'string' ? gap : JSON.stringify(gap))}
`).join('')}
`; } answerBody.innerHTML = html || '

No answer generated

'; const evidence = Array.isArray(data.evidence) ? data.evidence : []; if (evidence.length) { evidenceBody.innerHTML = evidence.map((item) => `
${esc(item.doc_id)} / ${esc(item.chunk_id)} - ${esc(item.where)}
"${esc(item.quote)}"
Supports: ${esc(item.supports)}
`).join(''); } else { evidenceBody.innerHTML = '

No evidence found

'; } const trace = data.trace_summary || {}; traceBody.innerHTML = `
Real Switching ${trace.real_switching ? 'Yes' : 'No'}
Chunks Processed ${trace.chunks_processed || 0}
Models Used ${esc((trace.models_used || []).join(', ') || '-')}
${renderModeCounts(trace.modes_used_counts || {})}
Docs Opened ${esc((trace.docs_opened || []).join(', ') || '-')}
Budget ${esc(trace.budget_notes || '-')}
Cache Hits ${data.cache_hits || 0}
Cache Misses ${data.cache_misses || 0}
`; const busMessages = Array.isArray(data.bus_messages) ? data.bus_messages : []; if (busMessages.length) { busBody.innerHTML = ''; busMessages.forEach((message) => appendBusMessage(message)); } else { busBody.innerHTML = '

No agent messages were emitted for this run

'; } setConfidence(data.confidence || 0); } function renderModeCounts(counts) { return Object.entries(counts).map(([mode, count]) => { const cls = mode.includes('QUICK') ? 'quick' : mode.includes('VISION') ? 'vision' : 'reasoning'; return `
${esc(mode)} ${count} calls
`; }).join(''); } function appendBusMessage(message) { if (busBody.querySelector('.panel__placeholder')) { busBody.innerHTML = ''; } const element = document.createElement('div'); element.className = 'bus-message animate-in'; element.innerHTML = `
${esc(message.sender || '')}
${esc(message.type || '')}
${esc(describeBusPayload(message.payload || {}))}
`; busBody.appendChild(element); busBody.scrollTop = busBody.scrollHeight; } function describeBusPayload(payload) { if (!payload || typeof payload !== 'object') { return String(payload || ''); } if (typeof payload.synthesis === 'string' && payload.synthesis.trim()) { return payload.synthesis; } if (Array.isArray(payload.flaws) && payload.flaws.length) { return payload.flaws.join(' | '); } if (Array.isArray(payload.gaps) && payload.gaps.length) { return payload.gaps.join(' | '); } if (payload.status) { return `status=${payload.status}`; } if (Array.isArray(payload.plan)) { return `Planned ${payload.plan.length} chunk(s)`; } if (Array.isArray(payload.supplement) && payload.supplement.length) { return `Suggested ${payload.supplement.length} supplementary item(s)`; } return JSON.stringify(payload); } function renderBenchmark(data) { const pluto = data.pluto || {}; const baseline = data.baseline || {}; const winner = data.winner || 'Unavailable'; const yesNoClass = (value) => value ? 'bench-stat__value bench-stat__value--good' : 'bench-stat__value bench-stat__value--bad'; const createColumn = (title, stats, isWinner) => `

${isWinner ? 'Winner' : 'Runner-up'} ${esc(title)}

Latency
${stats.latency_s || 0}s
Real Model Switching
${stats.real_switching ? 'Yes' : 'No'}
Evidence Check
${stats.evidence_checked ? 'Enabled' : 'Disabled'}
Evidence Count
${stats.evidence_count || 0}
Chunks Scanned
${stats.chunks_processed || 0}
Models Used
${esc((stats.models_used || []).join(', ') || '-')}
Answer Preview
${esc(stats.answer_preview || stats.error || 'No preview available')}
`; benchBody.innerHTML = ` ${createColumn('Pluto', pluto, winner === 'Pluto')}
VS
${createColumn('Single Model Baseline', baseline, winner === 'Baseline')} `; } function markAllStagesComplete() { stages.forEach((stageName, index) => { stageEls[stageName].classList.remove('active'); stageEls[stageName].classList.add('complete'); statusEls[stageName].textContent = 'done'; if (connectors[index]) { connectors[index].classList.add('active'); } }); } function resetUI() { stages.forEach((stageName) => { stageEls[stageName].classList.remove('active', 'complete'); statusEls[stageName].textContent = 'idle'; }); connectors.forEach((connector) => connector.classList.remove('active')); benchPanel.hidden = true; answerBody.innerHTML = '

Processing...

'; evidenceBody.innerHTML = '

Waiting...

'; traceBody.innerHTML = '

Waiting...

'; busBody.innerHTML = '

Agent activity will stream here...

'; confRing.style.strokeDashoffset = '327'; confValue.textContent = '-'; } function setConfidence(value) { const circumference = 2 * Math.PI * 52; const offset = circumference - (value * circumference); confRing.style.strokeDashoffset = String(offset); confValue.textContent = `${Math.round(value * 100)}%`; } async function uploadFiles(fileList) { if (uploadProcessingActive || pipelineRunning) { return; } uploadProcessingActive = true; uploadStatus.innerHTML = ''; syncControls(); const steps = [ { id: 'upload', label: 'Uploading file to server' }, { id: 'convert', label: 'Converting to text' }, { id: 'chunk', label: 'Splitting into chunks' }, { id: 'understand', label: 'AI reading and understanding document' }, { id: 'ready', label: 'Ready for questions!' }, ]; const filenames = Array.from(fileList).map((file) => file.name).join(', '); uploadStatus.innerHTML = `
Processing: ${esc(filenames)}
${steps.map((step, index) => `
${index === 0 ? '' : 'o'} ${esc(step.label)}
`).join('')}
`; const timers = [ window.setTimeout(() => activateUploadStep(steps, 1), 1200), window.setTimeout(() => activateUploadStep(steps, 2), 2400), window.setTimeout(() => activateUploadStep(steps, 3), 4000), ]; const formData = new FormData(); Array.from(fileList).forEach((file) => formData.append('files', file)); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await parseJsonResponse(response, 'Upload returned an invalid response'); timers.forEach((timerId) => window.clearTimeout(timerId)); ['upload', 'convert', 'chunk'].forEach(completeUploadStep); (data.uploaded || []).forEach((item) => { addStatusItem(item.filename, `Indexed as "${item.doc_id}" (${item.chunks} chunks)`, 'success'); }); (data.errors || []).forEach((item) => { addStatusItem(item.filename, item.error, 'error'); }); await loadCorpus(); const docsToWatch = (data.uploaded || []).filter((item) => item.understanding === 'in_progress'); if (!docsToWatch.length) { completeUploadStep('understand'); completeUploadStep('ready'); uploadProcessingActive = false; syncControls(); return; } activateUploadStep(steps, 3); await pollDocumentReadiness(docsToWatch); completeUploadStep('understand'); completeUploadStep('ready'); } catch (error) { timers.forEach((timerId) => window.clearTimeout(timerId)); uploadStatus.innerHTML = ''; addStatusItem('Upload', error.message, 'error'); } finally { uploadProcessingActive = false; syncControls(); await loadCorpus(); } } async function pollDocumentReadiness(docsToWatch) { const pendingIds = new Set(docsToWatch.map((doc) => doc.doc_id)); while (pendingIds.size > 0) { await delay(2000); for (const docId of Array.from(pendingIds)) { const response = await fetch(`/api/doc-status/${encodeURIComponent(docId)}`, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' }, }); const data = await parseJsonResponse(response, 'Document status returned an invalid response'); if (data.status === 'failed') { throw new Error(data.error || `Document understanding failed for ${docId}`); } if (data.status === 'ready') { pendingIds.delete(docId); } } } } function activateUploadStep(steps, index) { if (index > 0) { completeUploadStep(steps[index - 1].id); } const currentStep = steps[index]; if (!currentStep) { return; } const icon = document.getElementById(`upload-icon-${currentStep.id}`); const row = document.getElementById(`upload-step-${currentStep.id}`); if (icon) { icon.innerHTML = ''; } if (row) { row.classList.add('upload-step--active'); row.classList.remove('upload-step--complete'); } } function completeUploadStep(stepId) { const icon = document.getElementById(`upload-icon-${stepId}`); const row = document.getElementById(`upload-step-${stepId}`); if (icon) { icon.innerHTML = '✓'; } if (row) { row.classList.remove('upload-step--active'); row.classList.add('upload-step--complete'); } } function addStatusItem(name, message, type) { const element = document.createElement('div'); element.className = `upload-status-item upload-status-item--${type}`; const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : '...'; element.innerHTML = `${icon} ${esc(name)} - ${esc(message)}`; uploadStatus.appendChild(element); } async function loadCorpus() { try { clearCorpusAutoRefresh(); const response = await fetch('/api/corpus', { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' }, }); const data = await parseJsonResponse(response, 'Corpus response was invalid'); latestCorpusDocs = Array.isArray(data.documents) ? data.documents : []; pendingCorpusDocIds = latestCorpusDocs .filter((doc) => doc.processing_status === 'understanding') .map((doc) => doc.doc_id); const validDocIds = new Set(latestCorpusDocs.map((doc) => doc.doc_id)); const prunedSelection = selectedDocIds.filter((docId) => validDocIds.has(docId)); if (prunedSelection.length !== selectedDocIds.length) { selectedDocIds = prunedSelection; persistSelectedDocIds(); } renderCorpusDocs(); updateSelectionSummary(); syncControls(); scheduleCorpusAutoRefresh(); } catch (error) { scheduleCorpusAutoRefresh(); corpusDocs.innerHTML = 'Failed to load'; } } function renderCorpusDocs() { if (!latestCorpusDocs.length) { corpusDocs.innerHTML = 'No documents in corpus'; return; } corpusDocs.innerHTML = latestCorpusDocs.map((doc) => { const isSelected = selectedDocIds.includes(doc.doc_id); const isReady = doc.processing_status === 'ready' || doc.is_processed === true; const stateLabel = isReady ? 'Ready' : doc.processing_status === 'failed' ? 'Failed' : 'Understanding'; const chipClasses = [ 'corpus-doc-chip', isSelected ? 'corpus-doc-chip--selected' : '', !isReady ? 'corpus-doc-chip--muted' : '', ].filter(Boolean).join(' '); return `
${esc(doc.filename)} ${formatSize(doc.size)} ${esc(stateLabel)}
`; }).join(''); corpusDocs.querySelectorAll('.corpus-doc-chip').forEach((chip) => { chip.addEventListener('click', () => { if (chip.dataset.selectable !== 'true' || uploadProcessingActive || pipelineRunning) { return; } toggleDocSelection(chip.dataset.docId || ''); }); }); corpusDocs.querySelectorAll('.corpus-doc-chip__delete').forEach((button) => { button.addEventListener('click', (event) => { event.stopPropagation(); deleteDoc(button.dataset.deleteDoc || ''); }); }); } function createSessionId() { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return window.crypto.randomUUID(); } return `session-${Date.now()}-${Math.random().toString(16).slice(2)}`; } function toggleDocSelection(docId) { if (!docId) { return; } if (selectedDocIds.includes(docId)) { selectedDocIds = selectedDocIds.filter((item) => item !== docId); } else { selectedDocIds = [...selectedDocIds, docId]; } persistSelectedDocIds(); renderCorpusDocs(); updateSelectionSummary(); } async function deleteDoc(docId) { if (!docId || !window.confirm(`Remove "${docId}" from corpus?`)) { return; } try { await fetch(`/api/corpus/${encodeURIComponent(docId)}`, { method: 'DELETE' }); selectedDocIds = selectedDocIds.filter((item) => item !== docId); persistSelectedDocIds(); await loadCorpus(); } catch (error) { console.error('Failed to delete document', error); } } function updateSelectionSummary() { const selectedDocs = latestCorpusDocs.filter((doc) => selectedDocIds.includes(doc.doc_id)); let scopeText = 'Scope: entire corpus'; let helpText = 'Click a ready corpus document to limit the next query to that document.'; if (selectedDocs.length === 1) { scopeText = `Scope: ${selectedDocs[0].filename}`; helpText = 'This query will only use the selected document. Click it again to clear the filter.'; } else if (selectedDocs.length > 1) { scopeText = `Scope: ${selectedDocs.length} selected documents`; helpText = 'This query will only use the selected documents. Click a selected chip again to clear it.'; } const detailText = detailLevel === 'detailed' ? 'Detailed answer' : 'Standard answer'; queryScopeLabel.textContent = `${scopeText} | ${detailText}`; corpusSelectionSummary.textContent = helpText; } function persistSelectedDocIds() { localStorage.setItem(STORAGE_KEYS.selectedDocIds, JSON.stringify(selectedDocIds)); } function loadStoredDocIds() { try { const parsed = JSON.parse(localStorage.getItem(STORAGE_KEYS.selectedDocIds) || '[]'); return Array.isArray(parsed) ? parsed.map((value) => String(value || '').trim()).filter(Boolean) : []; } catch (_) { return []; } } function loadStoredDetailLevel() { return normalizeDetailLevel(localStorage.getItem(STORAGE_KEYS.detailLevel)); } function normalizeDetailLevel(value) { return String(value || '').toLowerCase() === 'detailed' ? 'detailed' : 'standard'; } async function parseJsonResponse(response, errorPrefix) { try { return await response.json(); } catch (_) { throw new Error(`${errorPrefix} (${response.status} ${response.statusText})`); } } async function fetchWithTimeout(url, options = {}, timeoutMs = 120000, timeoutMessage = 'Request timed out') { const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { ...options, signal: controller.signal }); } catch (error) { if (error && error.name === 'AbortError') { throw new Error(timeoutMessage); } throw error; } finally { window.clearTimeout(timeoutId); } } function formatSize(bytes) { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function renderErrorCard(title, message) { return `
${esc(title)}: ${esc(message)}
`; } function esc(value) { const div = document.createElement('div'); div.textContent = String(value == null ? '' : value); return div.innerHTML; } function delay(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } })();