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 'failed': return 'Auto-remodel failed'; default: return stage; } } function _arMetaText(stage, adsFound, targetAds, picked, varsDone, varsTotal, ev) { if (stage === 'failed' && ev?.error) return ev.error; const bits = []; if (targetAds > 0 && (stage === 'crawling' || stage === 'queued')) { bits.push(`${adsFound}/${targetAds} ads found`); } if (picked != null && picked >= 0 && ['picked', 'filtering', 'filter_done', 'remodeling', 'done'].includes(stage)) { bits.push(`${picked} pick${picked === 1 ? '' : 's'}`); } if (ev?.overrides) bits.push(`${ev.overrides} filter override${ev.overrides === 1 ? '' : 's'}`); if (varsTotal > 0 && (stage === 'remodeling' || stage === 'done')) { bits.push(`${varsDone}/${varsTotal} variants`); } return bits.join(' · '); } window.AutoRemodelPanel = function AutoRemodelPanel({ open, onClose, directionOptions = [], running, stage, lastEvent, adsFound = 0, targetAds = 0, variantsDone = 0, variantsTotal = 0, sources = { foreplay: true, adspy: false, brandsearch: true }, onToggleSource, onSelectAllSources, onRun, }) { const [count, setCount] = useARPState('5'); const [nVariants, setNVariants] = useARPState('1'); 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 pickerRef = useARPRef(null); const directions = Array.isArray(directionOptions) ? directionOptions : []; const selectedIds = [...selectedDirectionIds]; const capNum = Math.max(1, Math.min(20, parseInt(count, 10) || 5)); const variants = Math.max(1, Math.min(5, parseInt(nVariants, 10) || 1)); const randomPoolSize = sources.brandsearch ? directions.length : directions.filter(d => d.type !== 'competitor').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.description || d.brand_name || d.name || d.id; const typeLabel = (d) => d.type === 'competitor' ? 'Competitor' : 'Keyword'; const liveKeys = Object.keys(PLATFORMS).filter(k => k === 'foreplay' || k === 'adspy' || k === 'brandsearch'); const selectedSources = liveKeys.filter(k => sources[k]); const allSourcesOn = liveKeys.length > 0 && liveKeys.every(k => sources[k]); useARPEffect(() => { if (running && targetAds > 0) setCount(String(targetAds)); }, [running, targetAds]); useARPEffect(() => { if (!open) 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); } }, 250); return () => { cancelled = true; clearTimeout(timer); }; }, [open, count, nVariants, 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') return null; const pickedCount = lastEvent?.picked_count ?? (Array.isArray(lastEvent?.picked_ad_ids) ? lastEvent.picked_ad_ids.length : null) ?? lastEvent?.picked; let pct = 0; if (stage === 'crawling' || stage === 'queued') pct = targetAds > 0 ? Math.min(adsFound / targetAds, 1) * 35 : 8; else if (stage === 'picked') pct = 40; else if (stage === 'filtering') pct = 55; else if (stage === 'filter_done') pct = 65; else if (stage === 'remodeling') pct = 65 + (variantsTotal > 0 ? Math.min(variantsDone / variantsTotal, 1) * 35 : 0); else if (stage === 'done') pct = 100; else if (stage === 'failed') pct = 8; const meta = _arMetaText(stage, adsFound, targetAds, pickedCount, variantsDone, variantsTotal, lastEvent); const toggleDirection = (id) => { setSelectedDirectionIds(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }; const run = () => { onRun({ mode: 'auto_search', directionId: null, directionIds: selectedIds, count: capNum, nVariants: variants, platforms: selectedSources, }); }; return (