// Autoresearch sub-section of the Research tab. // A single Run button kicks off the crawler against every direction in // search_directions.json. Source toggles mirror the Search card. const { useState: useARState, useEffect: useAREffect, useRef: useARRef } = React; window.AutoresearchPanel = function AutoresearchPanel({ sources, // { foreplay: true, adspy: false, ... } onToggleSource, // (key) => void onSelectAllSources, // () => void — enables every *live* source in one click perDirectionTarget, // string | number — how many winning ads to find per direction onPerDirectionTarget, // (value) => void numDirections, // string | number — how many directions to crawl (cap) onNumDirections, // (value) => void directions, // [{ id, display_name, brand_name, type }] pickedDirections, // Set — explicit direction ids (empty = auto-pick by numDirections) onTogglePickedDirection, // (id) => void onClearPickedDirections, // () => void onRun, // () => void onStop, // () => void running, // boolean progress, // { crawlState, totalScored, winners, directionsSatisfied, directionsTotal, creditsRemaining } | null disabled, // boolean }) { const liveKeys = Object.keys(PLATFORMS).filter(k => k === 'foreplay' || k === 'adspy' || k === 'brandsearch'); const allOn = liveKeys.length > 0 && liveKeys.every(k => sources[k]); const sourceLabel = liveKeys.filter(k => sources[k]).map(k => PLATFORMS[k].label).join(' + ') || 'sources'; const showProgress = running || (progress && progress.crawlState && progress.crawlState !== 'idle'); const dirPct = progress && progress.directionsTotal ? Math.min(100, Math.round((progress.directionsSatisfied || 0) * 100 / progress.directionsTotal)) : 0; const picked = pickedDirections || new Set(); const dirs = Array.isArray(directions) ? directions : []; const randomDirs = sources?.brandsearch ? dirs : dirs.filter(d => d.type !== 'competitor'); const labelFor = (d) => d.display_name || d.brand_name || d.id; const capNum = Math.max(1, parseInt(numDirections, 10) || 5); const pickerLabel = picked.size > 0 ? `${picked.size} direction${picked.size === 1 ? '' : 's'} selected` : `Random ${Math.min(capNum, randomDirs.length)} of ${randomDirs.length}`; const [pickerOpen, setPickerOpen] = useARState(false); const pickerRef = useARRef(null); useAREffect(() => { if (!pickerOpen) return; const onClick = (e) => { if (pickerRef.current && !pickerRef.current.contains(e.target)) setPickerOpen(false); }; const onKey = (e) => { if (e.key === 'Escape') setPickerOpen(false); }; document.addEventListener('mousedown', onClick); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); }; }, [pickerOpen]); return (
Autoresearch
{running ? ( ) : ( )}
{showProgress && (
{progress?.crawlState === 'starting' || !progress?.crawlState ? 'Starting autoresearch…' : progress?.crawlState === 'done' ? 'Autoresearch complete' : `Scanning ${sourceLabel}…`} {(progress?.totalScored || 0)} scored · {(progress?.winners || 0)} winners · {(progress?.directionsSatisfied || 0)}/{(progress?.directionsTotal || 0)} directions {typeof progress?.creditsRemaining === 'number' ? ` · ${progress.creditsRemaining} credits left` : ''}
)}
{Object.entries(PLATFORMS).map(([key, p]) => { const on = !!sources[key]; const live = key === 'foreplay' || key === 'adspy' || key === 'brandsearch'; return ( ); })}
{dirs.length > 0 && (
{pickerOpen && (
{picked.size === 0 ? `No picks → run will randomly sample ${Math.min(capNum, randomDirs.length)} of ${randomDirs.length}. BrandSearch includes competitor brand lookups.` : `${picked.size} picked — only these will run.`} {picked.size > 0 && ( )}
{dirs.map((d) => { const on = picked.has(d.id); return ( ); })}
)}
)}
); };