const { useState: useARPState, useEffect: useARPEffect, useRef: useARPRef } = React; function _arStatusText(stage) { if (!stage) return 'Starting auto-remodel...'; switch (stage) { case 'queued': return 'Starting auto-remodel...'; case 'crawling': return 'Crawling and scoring...'; case 'picked': return 'Top picks selected...'; case 'filtering': return 'Filtering picked ads...'; case 'filter_done': return 'Filter audit complete...'; case 'remodeling': return 'Generating remodeled scripts...'; case 'done': return 'Auto-remodel complete'; case 'cancelled': return 'Auto-remodel stopped'; case 'failed': return 'Auto-remodel failed'; default: return stage; } } function _sumProgressValue(ev, key) { const progress = ev?.platform_progress || {}; const counts = ev?.platform_counts || {}; return Object.keys({ ...progress, ...counts }).reduce((total, platform) => { const progressValue = Number(progress[platform]?.[key] || 0); const countValue = Number(counts[platform]?.[key] || 0); return total + Math.max(progressValue, countValue); }, 0); } function _sumFetched(ev) { if (typeof ev?.ads_fetched_total === 'number') return ev.ads_fetched_total; const fetched = ev?.platform_ads_fetched || {}; const progressChecked = _sumProgressValue(ev, 'ads_checked'); const fetchedTotal = Object.values(fetched).reduce((total, value) => total + Number(value || 0), 0); return Math.max(progressChecked, fetchedTotal); } function _arProgressMetrics(stage, adsFound, targetAds, picked, varsDone, varsTotal, ev) { const checked = _sumFetched(ev); const scored = Math.max( Number(ev?.total_ads_scored || 0), _sumProgressValue(ev, 'scored'), ); const pickedCount = Math.max( Number(picked || 0), Number(ev?.picked_count || ev?.picked || 0), Array.isArray(ev?.picked_ad_ids) ? ev.picked_ad_ids.length : 0, ); const foundCount = Math.max(Number(adsFound || 0), pickedCount); const target = Math.max(Number(targetAds || 0), Number(ev?.target_ads || 0), foundCount); const remodelDone = Math.max( Number(varsDone || 0), Number(ev?.completed || 0) + Number(ev?.failed || 0), _sumProgressValue(ev, 'remodel_done') + _sumProgressValue(ev, 'remodel_failed'), ); const remodelTotal = Math.max(Number(varsTotal || 0), Number(ev?.jobs_started || ev?.jobs_created || 0), remodelDone); const directionsDone = Number(ev?.directions_done || 0); const directionsTotal = Number(ev?.directions_total || 0); let pct = 0; if (stage === 'done') { pct = 100; } else if (stage === 'failed') { pct = 8; } else { const crawlRatio = directionsTotal > 0 ? Math.min(directionsDone / directionsTotal, 1) : (checked > 0 ? 0.5 : 0); const scoreRatio = checked > 0 ? Math.min(scored / Math.max(checked, 1), 1) : 0; const pickRatio = target > 0 ? Math.min(foundCount / target, 1) : 0; const remodelRatio = remodelTotal > 0 ? Math.min(remodelDone / remodelTotal, 1) : 0; pct = 8 + (crawlRatio * 22) + (scoreRatio * 20) + (pickRatio * 25) + (remodelRatio * 25); if (stage === 'cancelled') pct = Math.max(8, pct); } return { checked, scored, pickedCount, target, remodelDone, remodelTotal, pct: Math.max(0, Math.min(100, pct)), }; } function _arMetaText(stage, adsFound, targetAds, picked, varsDone, varsTotal, ev) { if (stage === 'failed' && ev?.error) return ev.error; const metrics = _arProgressMetrics(stage, adsFound, targetAds, picked, varsDone, varsTotal, ev); const bits = []; const directionProgress = Object.values(ev?.direction_progress || {}).filter(Boolean); const coverageTotal = Number(ev?.coverage_total || 0) || directionProgress.length; const coverageTouched = Number(ev?.coverage_touched || 0) || directionProgress.filter(row => row.touched).length; if (coverageTotal) bits.push(`${coverageTouched}/${coverageTotal} searches touched`); bits.push(`${metrics.checked} crawled`); bits.push(`${metrics.scored} scored`); const pickTarget = metrics.target || targetAds || 0; bits.push(pickTarget > 0 ? `${metrics.pickedCount}/${pickTarget} picked` : `${metrics.pickedCount} picked`); if (metrics.remodelTotal > 0) bits.push(`${metrics.remodelDone}/${metrics.remodelTotal} remodels done`); if (ev?.overrides) bits.push(`${ev.overrides} filter override${ev.overrides === 1 ? '' : 's'}`); return bits.join(' · '); } function _directionStatusText(row) { const raw = row?.message || row?.last_message || row?.stop_reason || row?.status || ''; const text = String(raw).replace(/_/g, ' ').trim(); if (!text || text === 'running') return row?.touched ? 'touched' : 'pending'; if (text === 'target met') return 'run target reached'; return text; } function _directionProgressGroups(ev) { const rows = Object.values(ev?.direction_progress || {}) .filter(Boolean) .sort((a, b) => { const left = `${a.direction_name || a.direction_id || ''}:${a.source || ''}`; const right = `${b.direction_name || b.direction_id || ''}:${b.source || ''}`; return left.localeCompare(right); }); const groups = rows.reduce((acc, row) => { const key = row.direction_id || row.direction_name || 'unknown'; if (!acc[key]) { acc[key] = { key, name: row.direction_name || row.direction_id || 'Unknown direction', rows: [], }; } acc[key].rows.push(row); return acc; }, {}); return Object.values(groups); } function _formatDirectionProgressRow(row) { const label = (PLATFORMS[row.source]?.label || row.source || 'source').toUpperCase(); const ads = Number(row.ads_checked || 0); const pages = Number(row.pages_fetched || row.internal_pages_fetched || 0); const scored = Number(row.scored || 0); const winners = Number(row.winners || 0); const picked = Number(row.picked || 0); return `${label}: ${ads} ads fetched · ${pages} page${pages === 1 ? '' : 's'} · ${scored} scored · ${winners} winners · ${picked} picked · ${_directionStatusText(row)}`; } function _platformProgressRows(ev, sources = {}) { const fetched = ev?.platform_ads_fetched || {}; const counts = ev?.platform_counts || {}; const progress = ev?.platform_progress || {}; const selected = Object.keys(sources || {}).filter(key => sources[key]); const platforms = [...new Set([...selected, ...Object.keys(fetched), ...Object.keys(counts), ...Object.keys(progress)])]; if (!platforms.length) return []; return platforms .sort() .map((platform) => { const entry = progress[platform] || {}; const scored = Number(entry.scored ?? counts[platform]?.scored ?? 0); const winners = Number(entry.winners ?? counts[platform]?.winners ?? 0); const checked = Number(entry.ads_checked ?? fetched[platform] ?? 0); const picked = Number(entry.picked || 0); const skippedLow = Number(entry.skipped_low_score || 0); const skippedNoInput = Number(entry.skipped_no_input || 0); const duplicates = Number(entry.duplicates_hidden || 0); const remodelDone = Number(entry.remodel_done || 0); const remodelStarted = Number(entry.remodel_started || 0); const remodelFailed = Number(entry.remodel_failed || 0); const remodelReused = Number(entry.remodel_reused || 0); const threshold = Number(entry.score_threshold || 0); const label = (PLATFORMS[platform]?.label || platform).toUpperCase(); const bits = [ `${label}: ${checked} ads checked`, `${scored} scored`, `${winners} winners`, `${picked} picked`, ]; if (skippedLow) bits.push(`${skippedLow} skipped low score${threshold ? ` < ${threshold}` : ''}`); if (skippedNoInput) bits.push(`${skippedNoInput} skipped no input`); if (duplicates) bits.push(`${duplicates} duplicates hidden`); if (remodelStarted || remodelDone || remodelFailed || remodelReused) { bits.push(`${remodelDone}/${remodelStarted || remodelDone} remodels done`); } if (remodelFailed) bits.push(`${remodelFailed} remodel failed`); if (remodelReused) bits.push(`${remodelReused} reused`); return { platform, text: bits.join(' · ') }; }); } window.AutoRemodelPanel = function AutoRemodelPanel({ open, onClose, directionOptions = [], running, stage, lastEvent, adsFound = 0, targetAds = 0, variantsDone = 0, variantsTotal = 0, sources = { foreplay: true, adspy: true, brandsearch: true }, onToggleSource, onSelectAllSources, onRun, onStop, onRetryFailed, starting = false, strategy = '', onStrategyChange, }) { const [count, setCount] = useARPState('5'); const [maxAdsPerDirection, setMaxAdsPerDirection] = useARPState(''); const [maxAdsPerPlatform, setMaxAdsPerPlatform] = useARPState(''); const [selectedDirectionIds, setSelectedDirectionIds] = useARPState(new Set()); const [pickerOpen, setPickerOpen] = useARPState(false); const [estimate, setEstimate] = useARPState(null); const [estimating, setEstimating] = useARPState(false); const [estimateError, setEstimateError] = useARPState(''); const [estimateDirty, setEstimateDirty] = useARPState(false); const pickerRef = useARPRef(null); const defaultDirectionsLoadedRef = useARPRef(false); const directions = Array.isArray(directionOptions) ? directionOptions : []; const selectedIds = [...selectedDirectionIds]; const countMax = window.RESILIA_LIMITS?.autoRemodelCountMax || 10000; const adsPerDirectionMax = window.RESILIA_LIMITS?.searchAdsPerDirectionMax || 10000; const adsPerPlatformMax = window.RESILIA_LIMITS?.searchAdsPerPlatformMax || 100000; const capNum = Math.max(1, Math.min(countMax, parseInt(count, 10) || 5)); const variants = 1; const directionBudget = maxAdsPerDirection === '' ? null : Math.max(1, Math.min(adsPerDirectionMax, parseInt(maxAdsPerDirection, 10) || 1)); const platformBudget = maxAdsPerPlatform === '' ? null : Math.max(1, Math.min(adsPerPlatformMax, parseInt(maxAdsPerPlatform, 10) || 1)); const randomPoolSize = directions.length; const pickerLabel = selectedIds.length ? `${selectedIds.length} direction${selectedIds.length === 1 ? '' : 's'} selected` : `Random ${Math.min(capNum, randomPoolSize)} direction${Math.min(capNum, randomPoolSize) === 1 ? '' : 's'}`; const labelFor = (d) => d.display_name || d.brand_name || d.name || d.id; const typeLabel = (d) => d.type === 'competitor' ? 'Competitor' : 'Keyword'; const selectedDirectionPreview = selectedIds.length ? directions.filter(d => selectedDirectionIds.has(d.id)) : []; const liveKeys = Object.keys(PLATFORMS).filter(k => k === 'foreplay' || k === 'adspy' || k === 'brandsearch'); const selectedSources = liveKeys.filter(k => sources[k]); const busy = running || starting; useARPEffect(() => { if (running && targetAds > 0) setCount(String(targetAds)); }, [running, targetAds]); useARPEffect(() => { if (!directions.length || defaultDirectionsLoadedRef.current) return; setSelectedDirectionIds(new Set(directions.map(d => d.id).filter(Boolean))); defaultDirectionsLoadedRef.current = true; }, [directions.map(d => d.id).join('|')]); useARPEffect(() => { if (!open || !estimateDirty) return; let cancelled = false; if (selectedSources.length === 0) { setEstimate(null); setEstimating(false); setEstimateError('Pick at least one source'); return () => { cancelled = true; }; } setEstimating(true); setEstimateError(''); const timer = setTimeout(async () => { try { const out = await RESILIA_API.estimateAutoRemodel({ mode: 'auto_search', directionId: null, directionIds: selectedIds, count: capNum, nVariants: variants, platforms: selectedSources, }); if (!cancelled) setEstimate(out); } catch (e) { if (!cancelled) setEstimateError(e.message || 'Estimate unavailable'); } finally { if (!cancelled) setEstimating(false); } }, 750); return () => { cancelled = true; clearTimeout(timer); }; }, [open, estimateDirty, count, selectedIds.join('|'), selectedSources.join('|')]); useARPEffect(() => { 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]); if (!open && !running && stage !== 'done' && stage !== 'failed' && stage !== 'cancelled') return null; const pickedCount = lastEvent?.picked_count ?? (Array.isArray(lastEvent?.picked_ad_ids) ? lastEvent.picked_ad_ids.length : null) ?? lastEvent?.picked; const isComplete = stage === 'done'; const progressMetrics = _arProgressMetrics(stage, adsFound, targetAds, pickedCount, variantsDone, variantsTotal, lastEvent); const pct = progressMetrics.pct; const meta = _arMetaText(stage, adsFound, targetAds, pickedCount, variantsDone, variantsTotal, lastEvent); const platformProgressRows = _platformProgressRows(lastEvent, sources); const directionGroups = _directionProgressGroups(lastEvent); const directionRowsCount = directionGroups.reduce((total, group) => total + group.rows.length, 0); const coverageTotal = Number(lastEvent?.coverage_total || 0) || directionRowsCount; const coverageTouched = Number(lastEvent?.coverage_touched || 0) || directionGroups.reduce((total, group) => total + group.rows.filter(row => row.touched).length, 0); const toggleDirection = (id) => { setEstimateDirty(true); setSelectedDirectionIds(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; const selectAllDirections = () => { setEstimateDirty(true); setSelectedDirectionIds(new Set(directions.map(d => d.id))); }; const allDirectionsPicked = directions.length > 0 && selectedDirectionIds.size >= directions.length; const run = () => { onRun({ mode: 'auto_search', directionId: null, directionIds: selectedIds, count: capNum, nVariants: variants, platforms: selectedSources, maxAdsPerDirection: directionBudget, maxAdsPerPlatform: platformBudget, strategy: String(strategy || '').trim(), }); }; return (