/** * 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 = 'No requests yet'; 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 = '✓ done'; } else if (status === 'FAILED' || status === 'failed' || (typeof status === 'number' && status >= 400)) { badgeHtml = '✗ fail'; } else if (status === 'PROCESSING') { badgeHtml = '⏳ gen...'; } else if (status === 200 || status === 'processing') { badgeHtml = '⏳ sent'; } else { badgeHtml = '⏳ sent'; } const errorDisplay = error ? `${escHtml(truncate(error, 28))}` : `—`; return ` ${escHtml(shortId)} ${escHtml(type)} ${escHtml(time)} ${badgeHtml} ${errorDisplay} `; }); 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, '"'); } 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 `
${escHtml(label)}
${escHtml(String(value || '—'))}
`; }).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(); });