/* global React, PR */ const { useState, useEffect, useCallback } = React; const { I } = PR; function CreativeThumb({ thumb, type, status, primaryMetric }) { const gid = `cvg_${(thumb.from + thumb.to).replace(/[^a-z0-9]/gi, "")}`; const playIcon = (() => { if (type === "playable") return ( ); if (type === "static") return ( ); return ; })(); return (
{type === "video" && } {type === "playable" && } {type === "static" && } {type} {status === "live" && } {status}
{playIcon}
{primaryMetric && (
{primaryMetric.label} {primaryMetric.value}
)}
); } function CreativeCard({ c }) { const targetCPI = c.target?.cpi; const cpiOk = c.metrics.cpi != null && targetCPI != null && c.metrics.cpi <= targetCPI; const cpiBad = c.metrics.cpi != null && targetCPI != null && c.metrics.cpi > targetCPI; const fmt = (v, prefix = "", suf = "") => v == null ? "—" : prefix + (typeof v === "number" ? v.toFixed(1) : v) + suf; const primary = c.metrics.cpi != null ? { label: "CPI", value: "$" + c.metrics.cpi.toFixed(2), cls: cpiOk ? "good" : cpiBad ? "bad" : "" } : null; return (
{c.title}
{c.network && {c.network}}
CTR{fmt(c.metrics.ctr, "", "%")}
Hook{c.metrics.hookRate == null ? "—" : c.metrics.hookRate + "%"}
CR{fmt(c.metrics.cr, "", "%")}
IPM{fmt(c.metrics.ipm)}
{c.tracker ? ( e.preventDefault()}> {c.tracker} ) : } {c.metrics.impressions ? (c.metrics.impressions / 1000).toFixed(0) + "K imp" : "no spend"}
); } function AddCreativeModal({ hypothesisId, onClose, onAdded }) { const [form, setForm] = useState({ type: "video", title: "", network: "", status: "draft", tracker: "", target_cpi: "", thumb_from: "#3a2a55", thumb_to: "#7c5fc4" }); const [saving, setSaving] = useState(false); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); const handleSubmit = async (e) => { e.preventDefault(); setSaving(true); try { const payload = { ...form, target_cpi: form.target_cpi ? parseFloat(form.target_cpi) : null, }; await fetch(`/api/hypotheses/${hypothesisId}/creatives`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); onAdded(); onClose(); } finally { setSaving(false); } }; return (
e.stopPropagation()} style={{ maxWidth: 460 }}>
Add creative test
set("title", e.target.value)} placeholder="e.g. Hero loop · 15s" />
set("target_cpi", e.target.value)} placeholder="e.g. 1.50" />
set("tracker", e.target.value)} placeholder="e.g. JIRA-1234" />
set("thumb_from", e.target.value)} style={{ width: 36, height: 28, border: "1px solid var(--border)", borderRadius: 4, cursor: "pointer" }} /> set("thumb_to", e.target.value)} style={{ width: 36, height: 28, border: "1px solid var(--border)", borderRadius: 4, cursor: "pointer" }} />
); } function CreativesPanel({ data, hypothesisId }) { const [creatives, setCreatives] = useState([]); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const load = useCallback(async () => { const resp = await fetch(`/api/hypotheses/${hypothesisId}/creatives`); if (resp.ok) setCreatives(await resp.json()); setLoading(false); }, [hypothesisId]); useEffect(() => { load(); }, [load]); if (loading) return
Loading creatives…
; const all = creatives; if (all.length === 0) { return (
No creative tests yet
Creative testing runs in parallel to the main pipeline. Add a video, playable, or static creative to start measuring CPI / Hook Rate / CTR.
{showAdd && setShowAdd(false)} onAdded={load} />}
); } const live = all.filter(c => c.status === "live").length; const cpis = all.filter(c => c.metrics.cpi != null && c.status !== "draft").map(c => c.metrics.cpi); const bestCPI = cpis.length ? Math.min(...cpis) : null; const totalSpend = all.reduce((s, c) => s + (c.metrics.spend || 0), 0); const totalImp = all.reduce((s, c) => s + (c.metrics.impressions || 0), 0); const targetCPI = all.find(c => c.target?.cpi)?.target?.cpi; const cpiOk = bestCPI != null && targetCPI != null && bestCPI <= targetCPI; return (
Best CPI {bestCPI != null ? "$" + bestCPI.toFixed(2) : "—"}
{targetCPI && (
Target ≤ ${targetCPI.toFixed(2)}
)}
Live {live}/{all.length}
Spend {totalSpend >= 1000 ? "$" + (totalSpend / 1000).toFixed(1) + "K" : "$" + totalSpend}
Impressions {totalImp >= 1000000 ? (totalImp / 1000000).toFixed(2) + "M" : (totalImp / 1000).toFixed(0) + "K"}
{all.map(c => )}
{showAdd && setShowAdd(false)} onAdded={load} />}
); } window.PRCreatives = { CreativesPanel, CreativeThumb };