// Static Ads — image-ad generation surface. Standalone product: it drives the // existing static_ads backend (POC orchestrator -> RabbitMQ lane -> S3) and only // renders the run form, live progress, and the variants gallery/history. No // pipeline logic lives here. Mirrors AutoRemodelPanel.jsx conventions. const { useState: useSAState, useEffect: useSAEffect } = React; const STATIC_ADS_VARIANTS_MAX = 10; const STATIC_ADS_VARIANTS_DEFAULT = 4; // Backend only emits coarse status + a 0.0–1.0 fraction (0.05 the whole time a // run is "running"), so the named sub-steps from the ticket are not authoritative. const SA_STRICTNESS_OPTIONS = ['strict', 'medium', 'loose']; function _saStatusText(status) { switch (status) { case 'queued': return 'Queued…'; case 'running': return 'Generating variants…'; case 'done': return 'Complete'; case 'failed': return 'Failed'; case 'cancelled': return 'Stopped'; default: return status || 'Idle'; } } function _saStatusClass(status) { if (status === 'done') return 'ok'; if (status === 'failed') return 'err'; if (status === 'cancelled') return 'warn'; return 'run'; } function _saMoney(n) { const v = Number(n); if (!Number.isFinite(v)) return '$0.00'; return `$${v.toFixed(v < 1 ? 4 : 2)}`; } function _saAge(iso) { if (!iso) return ''; const t = Date.parse(iso); if (!Number.isFinite(t)) return ''; return window.formatRelativeTime ? window.formatRelativeTime(Date.now() - t) : ''; } // Canonical per-step sequence (#221) — must match STATIC_ADS_STEPS in // static_ads_service.py, in real execution order (validation runs early). const SA_STEPS = [ { key: 'research', label: 'Researching competitor ads' }, { key: 'validation', label: 'Scoring & winning-ad benchmark' }, { key: 'prompt_writing', label: 'Writing the image prompt' }, { key: 'banana_generation', label: 'Generating images' }, { key: 'compliance', label: 'Finalizing' }, { key: 'done', label: 'Complete' }, ]; const _saStepIndex = (key) => SA_STEPS.findIndex(s => s.key === key); // -1 if unknown // Friendly failure text (backend classifies into state.error_message, #221); // falls back gracefully for older rows that predate classification. function _saFriendlyError(run) { return (run && run.state && run.state.error_message) || 'Generation failed — please try again.'; } // Per-step progress stepper. Completed steps = check, current = spinner, // the failed step = ✕, the rest = dim. Driven by the live polled run state. function StaticAdsStepper({ currentStep, failedStep, status }) { const done = status === 'done'; const failed = status === 'failed'; const curIdx = done ? SA_STEPS.length - 1 : failed ? _saStepIndex(failedStep) : _saStepIndex(currentStep); return (
    {SA_STEPS.map((s, i) => { let cls = 'pending'; if (done || (curIdx >= 0 && i < curIdx)) cls = 'done'; else if (curIdx >= 0 && i === curIdx) cls = failed ? 'failed' : 'active'; return (
  1. {cls === 'done' ? : cls === 'failed' ? : cls === 'active' ? {s.label}
  2. ); })}
); } // One variant tile. Image link is short-lived (15-min S3 presign); on load // error we show a graceful "expired" placeholder rather than a broken image. // Per-variant Save / Regenerate / Delete wired in #221 (run-scoped via runId). function StaticAdsVariantCard({ variant, runId, onOpen, onSave, onRegenerate, onDelete, busy }) { const [errored, setErrored] = useSAState(false); const [regenOpen, setRegenOpen] = useSAState(false); const [regenPrompt, setRegenPrompt] = useSAState(''); const idx = variant?.variant_idx; const url = variant?.image_url; const saved = !!variant?.saved; const actionsEnabled = !!runId && !busy; const openRegen = () => { setRegenPrompt(variant?.prompt || ''); setRegenOpen(true); }; const confirmRegen = () => { const override = (regenPrompt || '').trim(); onRegenerate && onRegenerate(runId, idx, override && override !== variant?.prompt ? override : null); setRegenOpen(false); }; return (
{url && !errored ? ( {`Variant setErrored(true)} onClick={() => onOpen && onOpen(url)} /> ) : (
{errored ? 'Image link expired' : 'No image'}
)} {saved && Keeper}
#{idx} {_saMoney(variant?.cost_usd)} Compliance: {variant?.compliance_score == null ? '—' : variant.compliance_score}
{variant?.prompt && (
Prompt

{variant.prompt}

)} {regenOpen && (