Spaces:
Runtime error
Runtime error
| /** | |
| * Flow Agent β Side Panel | |
| * Displays live connection status, metrics, and request log. | |
| */ | |
| // ββ Type label map βββββββββββββββββββββββββββββββββββββββββββ | |
| const TYPE_LABELS = { | |
| // Worker request types | |
| GENERATE_IMAGE: 'GEN IMAGE', | |
| REGENERATE_IMAGE: 'REGEN IMAGE', | |
| EDIT_IMAGE: 'EDIT IMAGE', | |
| GENERATE_CHARACTER_IMAGE: 'GEN REF', | |
| REGENERATE_CHARACTER_IMAGE: 'REGEN REF', | |
| EDIT_CHARACTER_IMAGE: 'EDIT REF', | |
| GENERATE_VIDEO: 'GEN VIDEO', | |
| GENERATE_VIDEO_REFS: 'GEN VIDEO FROM REFS', | |
| UPSCALE_VIDEO: 'UPSCALE VIDEO', | |
| // Captcha action types | |
| IMAGE_GENERATION: 'GEN IMAGE', | |
| VIDEO_GENERATION: 'GEN VIDEO', | |
| // Extension-classified API types | |
| GEN_IMG: 'GEN IMAGE', | |
| GEN_VID: 'GEN VIDEO', | |
| GEN_VID_REF: 'GEN VIDEO FROM REFS', | |
| UPSCALE: 'UPSCALE VIDEO', | |
| UPS_IMG: 'UPSCALE IMAGE', | |
| POLL: 'CHECK GEN VIDEO', | |
| CREDITS: 'CHECK CREDIT', | |
| CREATE_PROJECT: 'CREATE PROJECT', | |
| UPLOAD: 'UPLOAD IMAGE', | |
| MEDIA: 'READ MEDIA', | |
| TRACKING: 'GOOGLE FLOW TRACK', | |
| URL_REFRESH: 'URL REFRESH', | |
| TRPC: 'TRPC', | |
| API: 'API', | |
| }; | |
| function formatType(type) { | |
| if (!type) return 'β'; | |
| return TYPE_LABELS[type] || type.slice(0, 5).toUpperCase(); | |
| } | |
| // ββ Time formatting ββββββββββββββββββββββββββββββββββββββββββ | |
| function formatTime(iso) { | |
| if (!iso) return 'β'; | |
| try { | |
| const d = new Date(iso); | |
| const hh = String(d.getHours()).padStart(2, '0'); | |
| const mm = String(d.getMinutes()).padStart(2, '0'); | |
| const ss = String(d.getSeconds()).padStart(2, '0'); | |
| return `${hh}:${mm}:${ss}`; | |
| } catch { | |
| return 'β'; | |
| } | |
| } | |
| // ββ Status update ββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateStatus(data) { | |
| if (!data) return; | |
| // Connection dot | |
| const dot = document.getElementById('conn-dot'); | |
| const connected = data.agentConnected; | |
| dot.className = connected ? 'on' : ''; | |
| // Toggle state | |
| const toggle = document.getElementById('main-toggle'); | |
| const toggleLabel = document.getElementById('toggle-label'); | |
| const isOn = data.state !== 'off'; | |
| toggle.checked = isOn; | |
| toggleLabel.textContent = isOn ? 'ON' : 'OFF'; | |
| // State badge | |
| const stateBadge = document.getElementById('state-badge'); | |
| const st = data.state || 'off'; | |
| stateBadge.textContent = st; | |
| stateBadge.className = st; // idle | running | off | |
| // Token status | |
| const tokenEl = document.getElementById('token-status'); | |
| if (data.flowKeyPresent) { | |
| const ageMs = data.tokenAge || 0; | |
| const ageMin = Math.round(ageMs / 60000); | |
| if (ageMs > 3600000) { | |
| tokenEl.textContent = `token expired β open Flow to refresh`; | |
| tokenEl.className = 'warn'; | |
| } else { | |
| tokenEl.textContent = `token synced ${ageMin}m`; | |
| tokenEl.className = 'ok'; | |
| } | |
| // Auto-refresh when token age > 55 min and connected | |
| if (ageMs > 3300000 && data.agentConnected) { | |
| chrome.runtime.sendMessage({ type: 'REFRESH_TOKEN' }); | |
| } | |
| } else { | |
| tokenEl.textContent = 'no token'; | |
| tokenEl.className = 'bad'; | |
| } | |
| // Metrics | |
| const m = data.metrics || {}; | |
| document.getElementById('m-total').textContent = m.requestCount || 0; | |
| document.getElementById('m-success').textContent = m.successCount || 0; | |
| document.getElementById('m-failed').textContent = m.failedCount || 0; | |
| } | |
| // ββ Request log ββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateRequestLog(entries) { | |
| const tbody = document.getElementById('log-body'); | |
| const countEl = document.getElementById('log-count'); | |
| if (!entries || entries.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="5" class="log-empty">No requests yet</td></tr>'; | |
| countEl.textContent = '0'; | |
| return; | |
| } | |
| countEl.textContent = entries.length; | |
| _logEntries = entries; | |
| // Render newest first (entries already sorted DESC by background.js) | |
| const rows = entries.map((entry) => { | |
| const shortId = entry.id ? String(entry.id).slice(0, 8) : 'β'; | |
| const type = formatType(entry.type || entry.method); | |
| const time = formatTime(entry.time || entry.timestamp || entry.createdAt); | |
| const status = entry.status || entry.state || 'pending'; | |
| const error = entry.error || ''; | |
| let badgeHtml; | |
| if (status === 'COMPLETED' || status === 'success') { | |
| badgeHtml = '<span class="badge badge-ok">✓ done</span>'; | |
| } else if (status === 'FAILED' || status === 'failed' || (typeof status === 'number' && status >= 400)) { | |
| badgeHtml = '<span class="badge badge-fail">✗ fail</span>'; | |
| } else if (status === 'PROCESSING') { | |
| badgeHtml = '<span class="badge badge-proc">⏳ gen...</span>'; | |
| } else if (status === 200 || status === 'processing') { | |
| badgeHtml = '<span class="badge badge-proc">⏳ sent</span>'; | |
| } else { | |
| badgeHtml = '<span class="badge badge-proc">⏳ sent</span>'; | |
| } | |
| const errorDisplay = error | |
| ? `<td class="td-error" title="${escHtml(error)}">${escHtml(truncate(error, 28))}</td>` | |
| : `<td class="td-error empty">β</td>`; | |
| return `<tr> | |
| <td class="td-id" data-request-id="${escHtml(entry.id || '')}">${escHtml(shortId)}</td> | |
| <td class="td-type">${escHtml(type)}</td> | |
| <td class="td-time">${escHtml(time)}</td> | |
| <td>${badgeHtml}</td> | |
| ${errorDisplay} | |
| </tr>`; | |
| }); | |
| tbody.innerHTML = rows.join(''); | |
| // Attach click handlers to ID cells | |
| tbody.querySelectorAll('.td-id[data-request-id]').forEach(td => { | |
| td.addEventListener('click', () => { | |
| const reqId = td.getAttribute('data-request-id'); | |
| if (reqId) showRequestDetail(reqId); | |
| }); | |
| }); | |
| } | |
| function escHtml(str) { | |
| return String(str) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| function truncate(str, len) { | |
| if (!str || str.length <= len) return str; | |
| return str.slice(0, len) + 'β¦'; | |
| } | |
| // ββ Request detail modal ββββββββββββββββββββββββββββββββββββ | |
| let _logEntries = []; | |
| function showRequestDetail(reqId) { | |
| const entry = _logEntries.find(e => e.id === reqId); | |
| if (!entry) return; | |
| const overlay = document.getElementById('detail-overlay'); | |
| const title = document.getElementById('detail-title'); | |
| const body = document.getElementById('detail-body'); | |
| title.textContent = `Request ${String(reqId).slice(0, 12)}`; | |
| const fields = [ | |
| ['ID', entry.id], | |
| ['Type', formatType(entry.type || entry.method)], | |
| ['Time', formatTime(entry.time || entry.timestamp || entry.createdAt)], | |
| ['Status', entry.status || entry.state || 'pending'], | |
| ['HTTP', entry.httpStatus || 'β'], | |
| ['URL', entry.url || 'β'], | |
| ['Payload', entry.payloadSummary || 'β'], | |
| ['Response', entry.responseSummary || 'β'], | |
| ['Error', entry.error || 'β'], | |
| ]; | |
| body.innerHTML = fields.map(([label, value]) => { | |
| let cls = 'detail-value'; | |
| if (label === 'Error' && value && value !== 'β') cls += ' error'; | |
| if (label === 'Status' && (value === 'COMPLETED' || value === 'success')) cls += ' ok'; | |
| return `<div class="detail-row"> | |
| <div class="detail-label">${escHtml(label)}</div> | |
| <div class="${cls}">${escHtml(String(value || 'β'))}</div> | |
| </div>`; | |
| }).join(''); | |
| overlay.classList.add('open'); | |
| } | |
| document.getElementById('detail-close').addEventListener('click', () => { | |
| document.getElementById('detail-overlay').classList.remove('open'); | |
| }); | |
| document.getElementById('detail-overlay').addEventListener('click', (e) => { | |
| if (e.target === e.currentTarget) { | |
| e.currentTarget.classList.remove('open'); | |
| } | |
| }); | |
| // ββ Initial data fetch βββββββββββββββββββββββββββββββββββββββ | |
| function fetchStatus() { | |
| chrome.runtime.sendMessage({ type: 'STATUS' }, (data) => { | |
| if (chrome.runtime.lastError) return; | |
| updateStatus(data); | |
| }); | |
| } | |
| function fetchLog() { | |
| chrome.runtime.sendMessage({ type: 'REQUEST_LOG' }, (data) => { | |
| if (chrome.runtime.lastError) return; | |
| if (data && data.log) updateRequestLog(data.log); | |
| }); | |
| } | |
| // ββ Message listener (push updates) βββββββββββββββββββββββββ | |
| chrome.runtime.onMessage.addListener((msg) => { | |
| if (msg.type === 'STATUS_PUSH') { | |
| fetchStatus(); | |
| } | |
| if (msg.type === 'REQUEST_LOG_UPDATE') { | |
| if (msg.log) updateRequestLog(msg.log); | |
| } | |
| }); | |
| // ββ Toggle (connect / disconnect) βββββββββββββββββββββββββββ | |
| document.getElementById('main-toggle').addEventListener('change', (e) => { | |
| const msgType = e.target.checked ? 'RECONNECT' : 'DISCONNECT'; | |
| chrome.runtime.sendMessage({ type: msgType }, () => { | |
| if (chrome.runtime.lastError) return; | |
| setTimeout(fetchStatus, 400); | |
| }); | |
| }); | |
| // ββ Action buttons βββββββββββββββββββββββββββββββββββββββββββ | |
| document.getElementById('btn-flow').addEventListener('click', () => { | |
| chrome.runtime.sendMessage({ type: 'OPEN_FLOW_TAB' }, () => { | |
| if (chrome.runtime.lastError) return; | |
| }); | |
| }); | |
| document.getElementById('btn-token').addEventListener('click', () => { | |
| const btn = document.getElementById('btn-token'); | |
| btn.textContent = 'Opening...'; | |
| btn.disabled = true; | |
| chrome.runtime.sendMessage({ type: 'REFRESH_TOKEN' }, () => { | |
| if (chrome.runtime.lastError) { /* ignore */ } | |
| btn.textContent = 'Refresh Token'; | |
| btn.disabled = false; | |
| }); | |
| }); | |
| // ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| fetchStatus(); | |
| fetchLog(); | |
| }); | |