'use client' import { useState, useEffect, useCallback } from 'react' interface Variant { id: string variant_name: string variant_key: string traffic_percent: number is_control: boolean banner_title: string | null banner_description: string | null position: string | null primary_color: string | null is_active: boolean } interface VariantStat { variant_id: string variant_key: string variant_name: string traffic_percent: number is_control: boolean total: number accepted: number opt_in_rate: number is_winner?: boolean significance?: number } const API = '/api/sdk/v1/compliance/banner/ab' export function ABTestPanel({ siteConfigId }: { siteConfigId?: string }) { const [variants, setVariants] = useState([]) const [stats, setStats] = useState([]) const [loading, setLoading] = useState(true) const [showCreate, setShowCreate] = useState(false) const [newVariant, setNewVariant] = useState({ variant_name: '', variant_key: 'B', traffic_percent: 50, banner_title: '', primary_color: '' }) const scid = siteConfigId || '' const loadData = useCallback(async () => { if (!scid) { setLoading(false); return } setLoading(true) try { const [v, s] = await Promise.all([ fetch(`${API}/${scid}/variants`).then(r => r.ok ? r.json() : []), fetch(`${API}/${scid}/stats`).then(r => r.ok ? r.json() : []), ]) setVariants(v) setStats(s) } catch { /* ignore */ } setLoading(false) }, [scid]) useEffect(() => { loadData() }, [loadData]) const handleCreate = async () => { if (!scid || !newVariant.variant_name) return await fetch(`${API}/${scid}/variants`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newVariant), }) setShowCreate(false) setNewVariant({ variant_name: '', variant_key: 'B', traffic_percent: 50, banner_title: '', primary_color: '' }) loadData() } const handleDelete = async (id: string) => { await fetch(`${API}/variants/${id}`, { method: 'DELETE' }) loadData() } const handleTrafficChange = async (id: string, pct: number) => { await fetch(`${API}/variants/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ traffic_percent: pct }), }) loadData() } if (!scid) { return
Bitte waehlen Sie zuerst eine Site aus.
} if (loading) return
Lade A/B-Test...
const maxRate = Math.max(...stats.map(s => s.opt_in_rate), 1) return (
{/* Header */}

A/B-Test Varianten

Testen Sie verschiedene Banner-Konfigurationen um die Opt-In-Rate zu optimieren.

{/* Create Form */} {showCreate && (
setNewVariant({ ...newVariant, variant_name: e.target.value })} placeholder="Name (z.B. Variante B)" className="px-3 py-2 text-sm border border-gray-200 rounded-lg" /> setNewVariant({ ...newVariant, variant_key: e.target.value })} placeholder="Key (z.B. B)" className="px-3 py-2 text-sm border border-gray-200 rounded-lg" /> setNewVariant({ ...newVariant, banner_title: e.target.value })} placeholder="Banner-Titel (Override)" className="px-3 py-2 text-sm border border-gray-200 rounded-lg" /> setNewVariant({ ...newVariant, primary_color: e.target.value })} placeholder="Farbe (z.B. #22c55e)" type="color" className="px-3 py-2 h-10 text-sm border border-gray-200 rounded-lg" />
setNewVariant({ ...newVariant, traffic_percent: parseInt(e.target.value) })} className="flex-1" /> {newVariant.traffic_percent}%
)} {/* Variants + Stats */} {variants.length === 0 ? (

Kein A/B-Test aktiv.

Erstellen Sie mindestens 2 Varianten um einen Test zu starten.

) : (
{/* Comparison Chart */} {stats.length > 0 && (

Opt-In-Rate Vergleich

{stats.map(s => (
{s.variant_name} {s.is_control && (Kontrolle)}
{s.opt_in_rate}% ({s.accepted}/{s.total})
{s.is_winner && ( Gewinner ({s.significance}%) )}
))}
)} {/* Variant Cards */}
{variants.map(v => (
{v.variant_name} {v.variant_key} {v.is_control && Kontrolle}
handleTrafficChange(v.id, parseInt(e.target.value))} className="flex-1 h-1" /> {v.traffic_percent}%
{v.banner_title &&
Titel: {v.banner_title}
} {v.primary_color && (
{v.primary_color}
)}
))}
)}
) }