diff --git a/App.tsx b/App.tsx index 031bb56..9d22f6c 100644 --- a/App.tsx +++ b/App.tsx @@ -23,8 +23,7 @@ const AdsPerformance = lazy(() => import('./components/AdsPerformance')); const ForecastView = lazy(() => import('./components/ForecastView')); const VendorDataView = lazy(() => import('./components/VendorDataView')); const ExperimentsView = lazy(() => import('./components/ExperimentsView')); -const ExperimentDetail = lazy(() => import('./components/ExperimentDetail')); -const ExperimentForm = lazy(() => import('./components/ExperimentForm')); +// ExperimentDetail and ExperimentForm are now integrated into ExperimentsView // Loading fallback component const LoadingSpinner = () => ( @@ -64,17 +63,7 @@ const App: React.FC = () => { // Experiments state const [experimentMap, setExperimentMap] = useState>(new Map()); - const [selectedExperimentId, setSelectedExperimentId] = useState(null); - const [showCreateExperiment, setShowCreateExperiment] = useState(false); - const [preselectedAsins, setPreselectedAsins] = useState([]); - const [preselectedMarketplace, setPreselectedMarketplace] = useState(''); - const [preselectedLine, setPreselectedLine] = useState(''); - - // Available product lines (for experiment form) - const availableProductLines = useMemo(() => { - const lines = new Set(rawData.map(r => r.line).filter(Boolean)); - return Array.from(lines).sort(); - }, [rawData]); + // Experiment modal state removed โ€” detail/create are now inline in ExperimentsView // Modal State const [isDataModalOpen, setIsDataModalOpen] = useState(false); @@ -349,13 +338,10 @@ const App: React.FC = () => { setView('ads'); }, []); - // Create experiment for a specific line - const handleCreateExperimentForLine = useCallback((line: string) => { - if (!line) return; - setPreselectedLine(line); - setPreselectedMarketplace(filters.customer[0] || ''); - setShowCreateExperiment(true); - }, [filters.customer]); + // Create experiment for a specific line โ€” now handled inline by ExperimentsView + const handleCreateExperimentForLine = useCallback((_line: string) => { + setView('experiments'); + }, []); // 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL useEffect(() => { @@ -902,7 +888,7 @@ const App: React.FC = () => { velocityMap={velocityMap} buyBoxLostMap={buyBoxLostMap} experimentMap={experimentMap} - onOpenExperiment={(id) => setSelectedExperimentId(id)} + onOpenExperiment={() => setView('experiments')} /> @@ -962,13 +948,8 @@ const App: React.FC = () => { }>
setSelectedExperimentId(id)} - onOpenCreate={() => { - setPreselectedAsins([]); - setPreselectedMarketplace(filters.customer[0] || ''); - setShowCreateExperiment(true); - }} salesData={unfilteredCombinedData} + onExperimentsFetch={handleExperimentsFetch} />
@@ -1036,33 +1017,7 @@ const App: React.FC = () => { ) } - {/* Experiment Detail Modal */} - {selectedExperimentId && ( - setSelectedExperimentId(null)} - salesData={unfilteredCombinedData} - /> - )} - - {/* Create Experiment Modal */} - {showCreateExperiment && ( - { - setShowCreateExperiment(false); - setPreselectedAsins([]); - setPreselectedMarketplace(''); - setPreselectedLine(''); - }} - onSuccess={() => { - handleExperimentsFetch(); - }} - initialAsins={preselectedAsins} - initialMarketplace={preselectedMarketplace} - initialLine={preselectedLine} - availableLines={availableProductLines} - /> - )} + {/* Experiment modals removed โ€” detail/create are now inline in ExperimentsView */} ); diff --git a/components/ExperimentBadge.tsx b/components/ExperimentBadge.tsx index 6a5c858..c9a3dac 100644 --- a/components/ExperimentBadge.tsx +++ b/components/ExperimentBadge.tsx @@ -98,11 +98,12 @@ export const ExperimentTypeBadge: React.FC = ({ type, lg: 'px-3 py-1.5 text-sm', }; - const typeLabels = { + const typeLabels: Record = { pricing: 'Pricing', advertising: 'Advertising', content: 'Content', promotion: 'Promotion', + seo: 'SEO', }; return ( diff --git a/components/ExperimentsView.tsx b/components/ExperimentsView.tsx index 202a117..781ce7c 100644 --- a/components/ExperimentsView.tsx +++ b/components/ExperimentsView.tsx @@ -1,106 +1,135 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { ExperimentListItem, ExperimentType, ExperimentStatus, CombinedKPIs } from '../types'; -import { listExperiments, deleteExperiment, getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon, getExperimentAsins } from '../services/experiments'; +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 { ExperimentBadge, ExperimentStatusBadge, ExperimentTypeBadge } from './ExperimentBadge'; -import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip as RechartsTooltip, + ResponsiveContainer, + ReferenceLine, +} from 'recharts'; -const getChartMetricColor = (metric: string) => { - switch (metric) { - case 'cvr': return '#34d399'; // emerald - case 'gv': return '#a78bfa'; // purple - case 'acos': return '#fb7185'; // rose - case 'revenue': return '#fbbf24'; // amber - default: return '#818cf8'; // indigo for units - } -}; +// ============ Sub-view type ============ +type SubView = 'list' | 'detail' | 'create'; -const ExperimentSparkline: React.FC<{ experiment: ExperimentListItem; salesData: CombinedKPIs[] }> = ({ experiment, salesData }) => { - const chartData = useMemo(() => { - if (!experiment?.asins || !salesData?.length) return []; +// ============ 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' }, +]; - const asinSet = getExperimentAsins(experiment.asins, salesData); - const weeklyMap = new Map(); +// ============ Main Component ============ +interface ExperimentsViewProps { + salesData?: CombinedKPIs[]; + onExperimentsFetch?: () => void; +} - salesData.forEach(r => { - const asin = (r.asin || '').toUpperCase(); - if (!asinSet.has(asin)) return; +const ExperimentsView: React.FC = ({ salesData = [], onExperimentsFetch }) => { + const [subView, setSubView] = useState('list'); + const [selectedId, setSelectedId] = useState(null); - const weekNum = r.week || 1; - const yearStr = r.year || new Date().getFullYear(); - const weekStr = `${yearStr}-W${String(weekNum).padStart(2, '0')}`; + const navigateToDetail = useCallback((id: string) => { + setSelectedId(id); + setSubView('detail'); + }, []); - if (!weeklyMap.has(weekStr)) { - const d = new Date(yearStr, 0, 1 + (weekNum - 1) * 7); - weeklyMap.set(weekStr, { - name: weekStr, - timestamp: d.getTime(), - units: 0, - revenue: 0, - gv: 0, - spend: 0, - }); - } + const navigateToCreate = useCallback(() => { + setSubView('create'); + }, []); - const w = weeklyMap.get(weekStr); - w.units += r.unitsTotal || 0; - w.revenue += r.salesTotal || 0; - w.gv += r.glanceViews || 0; - w.spend += r.cost || 0; - }); + const navigateToList = useCallback(() => { + setSelectedId(null); + setSubView('list'); + }, []); - const arr = Array.from(weeklyMap.values()).map(w => ({ - ...w, - cvr: w.gv > 0 ? (w.units / w.gv) * 100 : 0, - acos: w.revenue > 0 ? (w.spend / w.revenue) * 100 : 0 - })).sort((a, b) => a.timestamp - b.timestamp); - - return arr.slice(-12); - }, [experiment, salesData]); - - if (chartData.length < 2) return
; - - const metric = experiment.primary_metric || 'units'; + const handleCreated = useCallback((id: string) => { + setSelectedId(id); + setSubView('detail'); + onExperimentsFetch?.(); + }, [onExperimentsFetch]); return ( -
- - - - - - +
+ {subView === 'list' && ( + + )} + {subView === 'detail' && selectedId && ( + + )} + {subView === 'create' && ( + + )}
); }; -interface ExperimentsViewProps { - onOpenDetail: (experimentId: string) => void; - onOpenCreate: () => void; - salesData?: CombinedKPIs[]; -} +// ==================================================================== +// LIST VIEW +// ==================================================================== -const ExperimentsView: React.FC = ({ onOpenDetail, onOpenCreate, salesData = [] }) => { +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); - - // Filters const [statusFilter, setStatusFilter] = useState([]); const [typeFilter, setTypeFilter] = useState([]); const [marketplaceFilter, setMarketplaceFilter] = useState([]); const [searchQuery, setSearchQuery] = useState(''); - const statusOptions = ['planned', 'active', 'completed', 'paused']; - const typeOptions = ['pricing', 'advertising', 'content', 'promotion']; - const marketplaceOptions = ['DE', 'UK', 'FR', 'IT', 'ES']; - const loadExperiments = useCallback(async () => { setLoading(true); setError(null); @@ -118,174 +147,141 @@ const ExperimentsView: React.FC = ({ onOpenDetail, onOpenC } }, [statusFilter, typeFilter, marketplaceFilter]); - useEffect(() => { - loadExperiments(); - }, [loadExperiments]); + useEffect(() => { loadExperiments(); }, [loadExperiments]); - const filteredExperiments = useMemo(() => { + const filtered = useMemo(() => { if (!searchQuery) return experiments; - const query = searchQuery.toLowerCase(); - return experiments.filter(exp => - exp.name.toLowerCase().includes(query) || - exp.owner?.toLowerCase().includes(query) + 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('Are you sure you want to delete this experiment?')) return; - + if (!confirm('Delete this experiment? This action cannot be undone.')) return; try { await deleteExperiment(id); loadExperiments(); } catch (e: any) { - alert(`Error deleting experiment: ${e.message}`); + alert(`Error: ${e.message}`); } }; - const handleDuplicate = async (experiment: ExperimentListItem, e: React.MouseEvent) => { - e.stopPropagation(); - onOpenDetail(experiment.id); - // Duplicate functionality will be in detail view - }; - - // Stats - const stats = useMemo(() => ({ - total: experiments.length, - active: experiments.filter(e => e.status === 'active').length, - planned: experiments.filter(e => e.status === 'planned').length, - avgLift: experiments.filter(e => e.actual_lift_percent !== undefined) - .reduce((sum, e) => sum + (e.actual_lift_percent || 0), 0) / - Math.max(1, experiments.filter(e => e.actual_lift_percent !== undefined).length), - }), [experiments]); - return ( -
+ <> {/* Header */} -
+
-

๐Ÿงช Experiments

+

Experiments

- Track and analyze your product experiments + Measure the impact of your changes with scientific rigor

- {/* Stats Cards */} + {/* Stat Cards */}
- - - - + + + +
{/* Filters */} -
- - - - setSearchQuery(e.target.value)} - className="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 flex-1 min-w-[200px]" - /> +
+ + + +
+ + + + 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 State */} + {/* Error */} {error && ( -
- Error: {error} +
+ {error}
)} {/* Table */}
{loading ? ( -
-
-
- ) : filteredExperiments.length === 0 ? ( -
-
๐Ÿงช
-

No experiments yet

-

- Create your first experiment to start tracking performance -

- +
+
+ ) : filtered.length === 0 ? ( + ) : (
- - - - - - - - - - - - + + + + + + + + + + + + - - {filteredExperiments.map((exp) => ( + + {filtered.map(exp => ( onOpenDetail(exp.id)} - className="hover:bg-slate-800/30 cursor-pointer transition-colors" + className="hover:bg-slate-800/30 cursor-pointer transition-colors group" > - + - -
StatusTypeNameMarketplaceASINsDurationProgressImpactOwnerActions
StatusTypeNameMarketASINsControlDurationVerdictOwnerActions
- + - + {exp.name} @@ -293,56 +289,36 @@ const ExperimentsView: React.FC = ({ onOpenDetail, onOpenC {exp.marketplace} - {exp.asin_count} ASINs + + {exp.asin_count} + + + {exp.control_asin_count > 0 ? exp.control_asin_count : 'โ€”'} + -
-
{new Date(exp.start_date).toLocaleDateString()}
+
+ {new Date(exp.start_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })} {exp.end_date && ( -
- โ†’ {new Date(exp.end_date).toLocaleDateString()} -
+ + {' โ†’ '}{new Date(exp.end_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' })} + )}
-
-
-
-
- {exp.progress_percent}% -
-
-
- {salesData.length > 0 && } -
- {exp.actual_lift_percent !== undefined ? ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {exp.actual_lift_percent >= 0 ? '+' : ''}{exp.actual_lift_percent}% - - ) : ( - โ€” - )} -
- {exp.primary_metric || 'units'} -
-
-
+
{exp.owner || 'โ€”'} -
+
+
)} - + ); }; -// Stat Card Component -const StatCard: React.FC<{ label: string; value: string | number; color: string }> = ({ label, value, color }) => { - const colorClasses: Record = { - indigo: 'bg-indigo-500/10 border-indigo-500/30 text-indigo-400', - emerald: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400', - amber: 'bg-amber-500/10 border-amber-500/30 text-amber-400', - purple: 'bg-purple-500/10 border-purple-500/30 text-purple-400', +// ==================================================================== +// DETAIL VIEW +// ==================================================================== + +const ExperimentDetailView: React.FC<{ + experimentId: string; + salesData: CombinedKPIs[]; + onBack: () => void; + onExperimentsFetch?: () => void; +}> = ({ experimentId, salesData, onBack, 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 */} + +
+
+ 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]) => ( + + + + + + + + + + ))} + +
MetricTreatment BeforeTreatment AfterControl BeforeControl AfterDiD EffectLift
{METRIC_LABELS[metric] || metric}{formatMetricValue(r.treatment_before, metric)}{formatMetricValue(r.treatment_after, metric)}{r.control_before ? formatMetricValue(r.control_before, metric) : 'โ€”'}{r.control_after ? 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 ? ( +
+