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

); } return ( <> {/* Back Button + Header */} {/* Header */}

{experiment.name}

{experiment.description && (

{experiment.description}

)}
{experiment.status === 'planned' && ( )} {experiment.status === 'active' && ( )}
{/* Verdict Card (if results exist) */} {experiment.verdict && primaryResult && (
Primary Metric Verdict
{getVerdictLabel(experiment.verdict)}
{METRIC_LABELS[experiment.primary_metric] || experiment.primary_metric}
Lift
= 0 ? 'text-emerald-400' : 'text-red-400'}`}> {primaryResult.lift_percent >= 0 ? '+' : ''}{primaryResult.lift_percent}%
Confidence
{Math.round((experiment.verdict_probability || 0) * 100)}%
DiD Effect
{primaryResult.did_estimate >= 0 ? '+' : ''}{formatMetricValue(primaryResult.did_estimate, experiment.primary_metric)}
{/* Confidence bar */}
)} {/* Info Grid */}
{/* Hypothesis */}

{experiment.hypothesis || 'No hypothesis defined'}

{/* Timeline */}
{experiment.baseline_start_date && experiment.baseline_end_date && (
Baseline Period {new Date(experiment.baseline_start_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })} {' — '} {new Date(experiment.baseline_end_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })} Custom Seasonal Comparison
)}
Start {new Date(experiment.start_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}
{experiment.end_date && (
End {new Date(experiment.end_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}
)}
Primary Metric {METRIC_LABELS[experiment.primary_metric] || experiment.primary_metric}
{experiment.owner && (
Owner {experiment.owner}
)}
Marketplace {experiment.marketplace || 'All'}
{/* Change Annotations */} {experiment.changes && experiment.changes.length > 0 && (
{experiment.changes.map((c, i) => (
{c.field} {c.before_value} {c.after_value}
))}
)} {/* ASINs */}
{(experiment.asins || []).map(a => ( {a} ))} {(!experiment.asins || experiment.asins.length === 0) && ( No ASINs defined )}
{(experiment.control_asins || []).map(a => ( {a} ))} {(!experiment.control_asins || experiment.control_asins.length === 0) && ( No control group — using simple before/after comparison )}
{/* Trend Chart */} {trendData.length > 2 && experiment.did_results && (
w.split('-')[1] || w} stroke="#475569" /> { if (!active || !payload?.length) return null; const actual = payload.find(p => p.dataKey === 'actual'); const counter = payload.find(p => p.dataKey === 'counterfactual'); return (
{label}
Actual: {formatMetricValue(Number(actual?.value || 0), experiment.primary_metric)}
Counterfactual: {formatMetricValue(Number(counter?.value || 0), experiment.primary_metric)}
); }} /> {experiment.start_date && ( { const sd = new Date(experiment.start_date); const week = Math.ceil((sd.getTime() - new Date(sd.getFullYear(), 0, 1).getTime()) / (7 * 86400000)); return `${sd.getFullYear()}-W${String(week).padStart(2, '0')}`; })()} stroke="#f59e0b" strokeDasharray="4 4" label={{ value: 'Start', position: 'top', fill: '#f59e0b', fontSize: 11 }} /> )}
Actual performance
Counterfactual (without change)
)} {/* DiD Metric Cards */} {didResults && (
{(Object.entries(didResults.metrics) as [string, DiDMetricResult][]).map(([metric, result]) => { const isPrimary = metric === experiment.primary_metric; return (
{METRIC_LABELS[metric] || metric} {isPrimary && ( PRIMARY )}
= 0 ? 'text-emerald-400' : 'text-red-400'}`}> {result.lift_percent >= 0 ? '+' : ''}{result.lift_percent}%
DiD: {result.did_estimate >= 0 ? '+' : ''}{formatMetricValue(result.did_estimate, metric)}
= 0.9 ? 'bg-emerald-400' : result.posterior_prob_positive <= 0.1 ? 'bg-red-400' : 'bg-slate-400' }`} style={{ width: `${Math.round(result.posterior_prob_positive * 100)}%` }} />
{Math.round(result.posterior_prob_positive * 100)}%
); })}
)} {/* Before / After Summary Table */} {didResults && (
{(Object.entries(didResults.metrics) as [string, DiDMetricResult][]).map(([metric, r]) => ( ))}
Metric Treatment Before Treatment After Control Before Control After DiD Effect Lift
{METRIC_LABELS[metric] || metric} {formatMetricValue(r.treatment_before, metric)} {formatMetricValue(r.treatment_after, metric)} {typeof r.control_before === 'number' ? formatMetricValue(r.control_before, metric) : '—'} {typeof r.control_after === 'number' ? formatMetricValue(r.control_after, metric) : '—'} = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {r.did_estimate >= 0 ? '+' : ''}{formatMetricValue(r.did_estimate, metric)} = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {r.lift_percent >= 0 ? '+' : ''}{r.lift_percent}%
)} {/* Learnings */} {editing ? (