flow-extension/popup.js
type-two 607b5ec2b5 fix(harvest): robust Download-row finder + two-step hover + Copy Log button + 300-line log
Reported symptom: harvest opens the More menu then moves on without ever hovering
Download. Cause: the size flyout's text can glue onto the Download row (textContent
"Download1K Original size2K Upscaled..."), so the old /^download\b/ match — which needs a
word boundary right after "download" — returned null and the whole size step was skipped.

Fix: findDownloadRow() matches "download" anywhere and takes the SHORTEST visible
candidate (the row itself, not a wrapper that swallowed the flyout). Stage-3 hover now
moves in two steps (approach from the left, then settle on centre) to fire a real
mouseenter that unfurls the submenu, and re-queries. Added per-stage logging (Download
row text+coords, chosen size option) so a copied log shows exactly where any future miss
happens. Popup gains a Copy button (whole activity log to clipboard) and log retention is
raised 100 -> 300 so a full harvest pass is capturable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:33:18 +10:00

198 lines
7.0 KiB
JavaScript

// Default Settings
const DEFAULT_SETTINGS = {
serverUrl: 'http://localhost:8017',
isRunning: false,
promptSel: '',
submitSel: '',
resultSel: '',
autoStart: false,
pollInterval: 4000,
logs: []
};
// UI Elements
const serverUrlInput = document.getElementById('server-url');
const toggleBtn = document.getElementById('toggle-btn');
const globalStatus = document.getElementById('global-status');
const statusLabel = globalStatus.querySelector('.status-label');
const logConsole = document.getElementById('log-console');
const clearLogsBtn = document.getElementById('clear-logs');
const autoStartInput = document.getElementById('auto-start');
// Advanced inputs
const selPromptInput = document.getElementById('sel-prompt');
const selGenerateInput = document.getElementById('sel-generate');
const selModelInput = document.getElementById('sel-model');
const pollIntervalInput = document.getElementById('poll-interval');
// Load settings on startup
chrome.storage.local.get(DEFAULT_SETTINGS, (settings) => {
serverUrlInput.value = settings.serverUrl;
autoStartInput.checked = !!settings.autoStart;
selPromptInput.value = settings.promptSel;
selGenerateInput.value = settings.submitSel;
selModelInput.value = settings.resultSel;
pollIntervalInput.value = settings.pollInterval;
updateToggleUI(settings.isRunning);
renderLogs(settings.logs);
updateStatusUI(settings.isRunning ? 'running' : 'idle');
});
// Helper: Append log to UI console
function appendLogToUI(log) {
const entry = document.createElement('div');
entry.className = `log-entry ${log.level}-log`;
const time = new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
entry.innerText = `[${time}] ${log.text}`;
logConsole.appendChild(entry);
logConsole.scrollTop = logConsole.scrollHeight;
}
// Render entire log list
function renderLogs(logs) {
logConsole.innerHTML = '';
if (!logs || logs.length === 0) {
const entry = document.createElement('div');
entry.className = 'log-entry system-log';
entry.innerText = '[System] No logs yet. Click Start to run.';
logConsole.appendChild(entry);
return;
}
logs.forEach(appendLogToUI);
}
// Update Toggle Button Style
function updateToggleUI(isRunning) {
if (isRunning) {
toggleBtn.textContent = '⏹ Stop Prompt Mode';
toggleBtn.className = 'btn btn-primary stop-state';
} else {
toggleBtn.textContent = '▶ Prompt Mode';
toggleBtn.className = 'btn btn-primary start-state';
}
}
// DL Mode — one-shot harvest of everything on screen (1K originals), click again to stop.
const dlBtn = document.getElementById('dl-btn');
function updateDlUI(active) {
dlBtn.textContent = active ? '⏹ Stop DL' : '⬇ DL Mode';
dlBtn.className = 'btn btn-primary ' + (active ? 'stop-state' : 'start-state');
}
dlBtn.addEventListener('click', () => {
chrome.storage.local.get({ harvestActive: false }, (r) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs[0]) return;
chrome.tabs.sendMessage(tabs[0].id, { type: r.harvestActive ? 'stopHarvest' : 'startHarvest' }, () => {
if (chrome.runtime.lastError) addSystemLog('DL mode: no Flow tab responding — open the Flow project tab first.');
});
});
});
});
chrome.storage.local.get({ harvestActive: false }, (r) => updateDlUI(r.harvestActive));
// Update Global Status indicator
function updateStatusUI(status) {
globalStatus.className = 'status-indicator';
if (status === 'running') {
globalStatus.classList.add('active');
statusLabel.textContent = 'Running';
} else if (status === 'idle') {
statusLabel.textContent = 'Idle';
} else if (status === 'error') {
globalStatus.classList.add('error');
statusLabel.textContent = 'Error';
} else if (status === 'working') {
globalStatus.classList.add('active');
statusLabel.textContent = 'Generating...';
}
}
// Input Change Handlers
function saveSetting(key, value) {
chrome.storage.local.set({ [key]: value });
}
serverUrlInput.addEventListener('change', (e) => saveSetting('serverUrl', e.target.value));
autoStartInput.addEventListener('change', (e) => saveSetting('autoStart', e.target.checked));
selPromptInput.addEventListener('change', (e) => saveSetting('promptSel', e.target.value));
selGenerateInput.addEventListener('change', (e) => saveSetting('submitSel', e.target.value));
selModelInput.addEventListener('change', (e) => saveSetting('resultSel', e.target.value));
pollIntervalInput.addEventListener('change', (e) => saveSetting('pollInterval', parseInt(e.target.value, 10) || 4000));
// Clear Logs
clearLogsBtn.addEventListener('click', () => {
chrome.storage.local.set({ logs: [] }, () => {
logConsole.innerHTML = '';
});
});
// Copy Logs — grab the whole activity log to the clipboard for pasting into chat.
const copyLogsBtn = document.getElementById('copy-logs');
copyLogsBtn.addEventListener('click', () => {
chrome.storage.local.get({ logs: [] }, (res) => {
const text = (res.logs || []).map(l =>
`[${new Date(l.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}] ${l.text}`
).join('\n');
const done = () => { copyLogsBtn.textContent = 'Copied!'; setTimeout(() => { copyLogsBtn.textContent = 'Copy'; }, 1200); };
navigator.clipboard.writeText(text).then(done).catch(() => {
const ta = document.createElement('textarea'); ta.value = text;
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); } catch (e) {}
ta.remove(); done();
});
});
});
// Toggle Run Loop
toggleBtn.addEventListener('click', () => {
chrome.storage.local.get('isRunning', (res) => {
const nextState = !res.isRunning;
chrome.storage.local.set({ isRunning: nextState }, () => {
updateToggleUI(nextState);
updateStatusUI(nextState ? 'running' : 'idle');
// Log event
const logMsg = nextState ? 'Automation started.' : 'Automation stopped.';
addSystemLog(logMsg);
});
});
});
// Add System Log Helper
function addSystemLog(text) {
chrome.storage.local.get({ logs: [] }, (res) => {
const newLog = {
level: 'system',
text: text,
timestamp: Date.now()
};
const updated = [...res.logs, newLog].slice(-300); // keep enough for a harvest debug
chrome.storage.local.set({ logs: updated }, () => {
appendLogToUI(newLog);
});
});
}
// Listen for storage changes to sync UI across multiple popup openings
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local') {
if (changes.isRunning) {
updateToggleUI(changes.isRunning.newValue);
updateStatusUI(changes.isRunning.newValue ? 'running' : 'idle');
}
if (changes.status) {
updateStatusUI(changes.status.newValue);
}
if (changes.harvestActive) {
updateDlUI(changes.harvestActive.newValue);
}
if (changes.logs) {
// Re-render logs if they changed (e.g. from background content script)
renderLogs(changes.logs.newValue);
}
}
});