import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Experiment, ExperimentCreateInput, ExperimentListItem, ExperimentType, ExperimentStatus, ExperimentMetric, ExperimentVerdict, ExperimentChangeAnnotation, DiDMetricResult, CombinedKPIs, } from '../types'; import { listExperiments, getExperiment, createExperiment, updateExperiment, deleteExperiment, getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon, getVerdictColor, getVerdictLabel, METRIC_LABELS, formatMetricValue, } from '../services/experiments'; import { computeDiD, computeVerdict, buildCounterfactualSeries, TrendDataPoint } from '../services/experimentAnalysis'; import MultiSelectDropdown from './MultiSelectDropdown'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, ReferenceLine, } from 'recharts'; // ============ Sub-view type ============ type SubView = 'list' | 'detail' | 'create' | 'edit'; // ============ Constants ============ const STATUS_OPTIONS = ['planned', 'active', 'completed', 'paused']; const TYPE_OPTIONS: ExperimentType[] = ['content', 'pricing', 'advertising', 'seo', 'promotion']; const MARKETPLACE_OPTIONS = ['DE', 'UK', 'FR', 'IT', 'ES']; const METRIC_OPTIONS: { value: ExperimentMetric; label: string }[] = [ { value: 'units', label: 'Units Sold' }, { value: 'sessions', label: 'Sessions' }, { value: 'cvr', label: 'Conversion Rate (CVR)' }, { value: 'ctr', label: 'Click-Through Rate (CTR)' }, { value: 'roas', label: 'ROAS' }, { value: 'revenue', label: 'Revenue' }, { value: 'acos', label: 'ACOS' }, ]; // ============ Main Component ============ interface ExperimentsViewProps { salesData?: CombinedKPIs[]; onExperimentsFetch?: () => void; } const ExperimentsView: React.FC = ({ salesData = [], onExperimentsFetch }) => { const [subView, setSubView] = useState('list'); const [selectedId, setSelectedId] = useState(null); const navigateToDetail = useCallback((id: string) => { setSelectedId(id); setSubView('detail'); }, []); const navigateToCreate = useCallback(() => { setSelectedId(null); setSubView('create'); }, []); const navigateToEdit = useCallback((id: string) => { setSelectedId(id); setSubView('edit'); }, []); const navigateToList = useCallback(() => { setSelectedId(null); setSubView('list'); }, []); const handleSaved = useCallback((id: string) => { setSelectedId(id); setSubView('detail'); onExperimentsFetch?.(); }, [onExperimentsFetch]); return (
{subView === 'list' && ( )} {subView === 'detail' && selectedId && ( navigateToEdit(selectedId)} onExperimentsFetch={onExperimentsFetch} /> )} {subView === 'create' && ( )} {subView === 'edit' && selectedId && ( navigateToDetail(selectedId)} onSaved={handleSaved} /> )}
); }; // ==================================================================== // LIST VIEW // ==================================================================== const ExperimentListView: React.FC<{ salesData: CombinedKPIs[]; onOpenDetail: (id: string) => void; onOpenCreate: () => void; }> = ({ salesData, onOpenDetail, onOpenCreate }) => { const [experiments, setExperiments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [statusFilter, setStatusFilter] = useState([]); const [typeFilter, setTypeFilter] = useState([]); const [marketplaceFilter, setMarketplaceFilter] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const loadExperiments = useCallback(async () => { setLoading(true); setError(null); try { const data = await listExperiments({ status: statusFilter.length > 0 ? statusFilter as ExperimentStatus[] : undefined, type: typeFilter.length > 0 ? typeFilter as ExperimentType[] : undefined, marketplace: marketplaceFilter.length > 0 ? marketplaceFilter : undefined, }); setExperiments(data); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [statusFilter, typeFilter, marketplaceFilter]); useEffect(() => { loadExperiments(); }, [loadExperiments]); const filtered = useMemo(() => { if (!searchQuery) return experiments; const q = searchQuery.toLowerCase(); return experiments.filter(e => e.name.toLowerCase().includes(q) || e.owner?.toLowerCase().includes(q) ); }, [experiments, searchQuery]); const stats = useMemo(() => { const winners = experiments.filter(e => e.verdict === 'winner').length; const withLift = experiments.filter(e => e.verdict_probability != null); const avgProb = withLift.length > 0 ? withLift.reduce((s, e) => s + (e.verdict_probability || 0), 0) / withLift.length : 0; return { total: experiments.length, active: experiments.filter(e => e.status === 'active').length, winners, avgProb: Math.round(avgProb * 100), }; }, [experiments]); const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (!confirm('Delete this experiment? This action cannot be undone.')) return; try { await deleteExperiment(id); loadExperiments(); } catch (e: any) { alert(`Error: ${e.message}`); } }; return ( <> {/* Header */}

Experiments

Measure the impact of your changes with scientific rigor

{/* Stat Cards */}
{/* Filters */}
setSearchQuery(e.target.value)} className="w-full bg-slate-800/80 border border-slate-700 rounded-lg pl-9 pr-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500" />
{(statusFilter.length > 0 || typeFilter.length > 0 || marketplaceFilter.length > 0 || searchQuery) && ( )}
{/* Error */} {error && (
{error}
)} {/* Table */}
{loading ? (
) : filtered.length === 0 ? ( ) : (
{filtered.map(exp => ( onOpenDetail(exp.id)} className="hover:bg-slate-800/30 cursor-pointer transition-colors group" > ))}
Status Type Name Market ASINs Control Duration Verdict Owner Actions
{exp.name} {exp.marketplace} {exp.asin_count} {exp.control_asin_count > 0 ? exp.control_asin_count : '—'}
{new Date(exp.start_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })} {exp.end_date && ( {' → '}{new Date(exp.end_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })} )}
{exp.owner || '—'}
)}
); }; // ==================================================================== // DETAIL VIEW // ==================================================================== const ExperimentDetailView: React.FC<{ experimentId: string; salesData: CombinedKPIs[]; onBack: () => void; onEdit: () => void; onExperimentsFetch?: () => void; }> = ({ experimentId, salesData, onBack, onEdit, onExperimentsFetch }) => { const [experiment, setExperiment] = useState(null); const [loading, setLoading] = useState(true); const [analyzing, setAnalyzing] = useState(false); const [editing, setEditing] = useState(false); const [editLearnings, setEditLearnings] = useState(''); const loadExperiment = useCallback(async () => { setLoading(true); try { const data = await getExperiment(experimentId); setExperiment(data); if (data) setEditLearnings(data.learnings || ''); } catch (e: any) { console.error('Failed to load experiment:', e); } finally { setLoading(false); } }, [experimentId]); useEffect(() => { loadExperiment(); }, [loadExperiment]); const handleRunAnalysis = async () => { if (!experiment || salesData.length === 0) return; setAnalyzing(true); try { const didResults = computeDiD(experiment, salesData); const { verdict, probability } = computeVerdict(didResults, experiment.primary_metric); const updated = await updateExperiment(experiment.id, { did_results: didResults, verdict, verdict_probability: probability, }); setExperiment(updated); onExperimentsFetch?.(); } catch (e: any) { alert(`Analysis error: ${e.message}`); } finally { setAnalyzing(false); } }; const handleSaveLearnings = async () => { if (!experiment) return; try { const updated = await updateExperiment(experiment.id, { learnings: editLearnings }); setExperiment(updated); setEditing(false); } catch (e: any) { alert(`Error: ${e.message}`); } }; const handleUpdateStatus = async (status: ExperimentStatus) => { if (!experiment) return; try { const updated = await updateExperiment(experiment.id, { status }); setExperiment(updated); onExperimentsFetch?.(); } catch (e: any) { alert(`Error: ${e.message}`); } }; const handleDelete = async () => { if (!experiment) return; if (!confirm('Delete this experiment permanently?')) return; try { await deleteExperiment(experiment.id); onBack(); onExperimentsFetch?.(); } catch (e: any) { alert(`Error: ${e.message}`); } }; const trendData = useMemo(() => { if (!experiment || salesData.length === 0) return []; return buildCounterfactualSeries(experiment, salesData, experiment.primary_metric); }, [experiment, salesData]); const didResults = experiment?.did_results; const primaryResult = didResults?.metrics[experiment?.primary_metric || 'units']; if (loading) { return (
); } if (!experiment) { return (

Experiment not found

); } const prob = Math.round((experiment.verdict_probability || 0) * 100); const radius = 60; const circumference = 2 * Math.PI * radius; const offset = circumference - (prob / 100) * circumference; return (
{/* Header */}
EXPERIMENT RESULT ASIN: {experiment.asins?.[0] || 'Multiple'}

{experiment.name}

Test: {new Date(experiment.start_date).toLocaleDateString('en-GB', { month: 'short', day: '2-digit', year: 'numeric' })} - {experiment.end_date ? new Date(experiment.end_date).toLocaleDateString('en-GB', { month: 'short', day: '2-digit', year: 'numeric' }) : 'Ongoing'}
PRIMARY GOAL: {METRIC_LABELS[experiment.primary_metric] || experiment.primary_metric}
{experiment.status === 'planned' && ( )} {experiment.status === 'active' && ( )}
{/* Main Grid */}
{/* Left Column (Metrics + Content) */}
{/* Key Performance Metrics */}

Key Performance Metrics

{/* Render specific metrics: Units, Revenue, CVR, BSR/ACOS */} {['units', 'revenue', 'cvr', 'acos', 'sessions', 'roas', 'ctr'].map(metric => { const res = didResults?.metrics?.[metric]; if (!res) { return ( ); } const isPositiveGood = !['acos', 'bsr'].includes(metric); const isGood = isPositiveGood ? res.lift_percent >= 0 : res.lift_percent <= 0; return ( ); })}
KPI Metric Control (A) Treatment (B) Impact Delta
{METRIC_LABELS[metric] || metric} {metric === experiment.primary_metric && ( PRIMARY )}
{METRIC_LABELS[metric] || metric} {metric === experiment.primary_metric && ( PRIMARY )} {formatMetricValue(res.treatment_before, metric)} {formatMetricValue(res.treatment_after, metric)}
= 0 ? '' : 'rotate-180') : 'rotate-180'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}> {res.lift_percent >= 0 ? '+' : ''}{res.lift_percent.toFixed(1)}% DiD: {res.did_estimate >= 0 ? '+' : ''}{formatMetricValue(res.did_estimate, metric)}
{/* Content Comparison */}

Content Comparison

Original

{experiment.changes[0]?.before_value || 'Original content based on baseline metrics.'}

Variant B (Winner)

{experiment.changes[0]?.after_value || 'New optimized content tested in this experiment.'}

{/* Right Column (Widgets) */}
{/* Probability of Success */}

Probability of Success

{prob}%
Significant Confidence Reached
{/* Estimated Annual Impact */}

Estimated Annual Impact

Optimistic Forecast $65,000
Expected Gain $42,000
Conservative Baseline $18,000
); }; // ==================================================================== // FORM VIEW // ==================================================================== const ExperimentFormView: React.FC<{ salesData: CombinedKPIs[]; initialData?: Experiment; onBack: () => void; onSaved: (id: string) => void; }> = ({ salesData, initialData, onBack, onSaved }) => { const [name, setName] = useState(initialData?.name || ''); const [description, setDescription] = useState(initialData?.description || ''); const [type, setType] = useState(initialData?.type || 'content'); const [marketplace, setMarketplace] = useState(initialData?.marketplace || 'DE'); const [owner, setOwner] = useState(initialData?.owner || ''); const [hypothesis, setHypothesis] = useState(initialData?.hypothesis || ''); const [primaryMetric, setPrimaryMetric] = useState(initialData?.primary_metric || 'units'); const [startDate, setStartDate] = useState(initialData?.start_date?.split('T')[0] || new Date().toISOString().split('T')[0]); const [endDate, setEndDate] = useState(initialData?.end_date?.split('T')[0] || ''); const [enableCustomBaseline, setEnableCustomBaseline] = useState(!!(initialData?.baseline_start_date && initialData?.baseline_end_date)); const [baselineStartDate, setBaselineStartDate] = useState(initialData?.baseline_start_date?.split('T')[0] || ''); const [baselineEndDate, setBaselineEndDate] = useState(initialData?.baseline_end_date?.split('T')[0] || ''); const [treatmentAsins, setTreatmentAsins] = useState(initialData?.asins?.join(', ') || ''); const [controlAsins, setControlAsins] = useState(initialData?.control_asins?.join(', ') || ''); const [changes, setChanges] = useState( initialData?.changes?.length ? initialData.changes : [{ field: '', before_value: '', after_value: '' }] ); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const parseAsins = (raw: string): string[] => { return raw .split(/[,;\n]+/) .flatMap(chunk => { const trimmed = chunk.trim().toUpperCase(); if (trimmed.startsWith('LINE:')) return [trimmed]; return trimmed.split(/\s+/); }) .filter(s => s.length >= 5); }; const addChange = () => { setChanges([...changes, { field: '', before_value: '', after_value: '' }]); }; const updateChange = (index: number, key: keyof ExperimentChangeAnnotation, value: string) => { const updated = [...changes]; updated[index] = { ...updated[index], [key]: value }; setChanges(updated); }; const removeChange = (index: number) => { setChanges(changes.filter((_, i) => i !== index)); }; const handleSubmit = async () => { setError(''); if (!name.trim()) { setError('Name is required'); return; } const asins = parseAsins(treatmentAsins); if (asins.length === 0) { setError('At least one treatment ASIN is required'); return; } setSubmitting(true); try { const validChanges = changes.filter(c => c.field.trim() && (c.before_value.trim() || c.after_value.trim())); const input: ExperimentCreateInput = { name: name.trim(), description: description.trim() || undefined, type, asins, control_asins: parseAsins(controlAsins), marketplace, start_date: startDate, end_date: endDate || undefined, baseline_start_date: enableCustomBaseline && baselineStartDate ? baselineStartDate : undefined, baseline_end_date: enableCustomBaseline && baselineEndDate ? baselineEndDate : undefined, hypothesis: hypothesis.trim() || undefined, primary_metric: primaryMetric, changes: validChanges, owner: owner.trim() || undefined, }; if (initialData) { const updated = await updateExperiment(initialData.id, input); onSaved(updated.id); } else { const created = await createExperiment(input); onSaved(created.id); } } catch (e: any) { setError(e.message); } finally { setSubmitting(false); } }; return ( <>

{initialData ? "Edit Experiment" : "New Experiment"}

{/* Basic Info */}
setName(e.target.value)} placeholder="e.g. Price reduction test - Premium Line" className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500" />