/**
* 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();
});