// 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 — max current-run winning ads to output onPerDirectionTarget, // (value) => void maxAdsPerDirection, // string | number — optional crawler stop cap onMaxAdsPerDirection, // (value) => void maxAdsPerPlatform, // string | number — optional crawler stop cap onMaxAdsPerPlatform, // (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 onSelectAllPickedDirections, // () => void onRun, // () => void onStop, // () => void running, // boolean progress, // { crawlState, totalScored, winners, directionsSatisfied, directionsTotal, creditsRemaining } | null }) { const liveKeys = Object.keys(PLATFORMS).filter(k => k === 'foreplay' || k === 'adspy' || k === 'brandsearch'); 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 directionPool = dirs; const allPicked = dirs.length > 0 && picked.size >= dirs.length; const labelFor = (d) => d.display_name || d.brand_name || d.id; const targetMax = window.RESILIA_LIMITS?.autoresearchPerDirectionMax || 1000; const adsPerDirectionMax = window.RESILIA_LIMITS?.searchAdsPerDirectionMax || 10000; const adsPerPlatformMax = window.RESILIA_LIMITS?.searchAdsPerPlatformMax || 100000; const pickerLabel = picked.size > 0 ? `${picked.size} direction${picked.size === 1 ? '' : 's'} selected` : `All ${directionPool.length} direction${directionPool.length === 1 ? '' : 's'}`; const fetchedLabel = Object.entries(progress?.platformAdsFetched || {}) .map(([source, count]) => `${source}: ${count}`) .join(' · '); 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)} current-run scored · {(progress?.winners || 0)} winning ads loaded · {(progress?.directionsSatisfied || 0)}/{(progress?.directionsTotal || 0)} direction checks done {fetchedLabel ? ` · fetched ${fetchedLabel}` : ''} {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 → all ${directionPool.length} directions will run, including competitors.` : `${picked.size} picked — only these will run.`}
{!allPicked && ( )} {picked.size > 0 && ( )}
{dirs.map((d) => { const on = picked.has(d.id); return ( ); })}
)}
)}
); };