mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 12:45:23 +02:00
feat: replace old detail view entirely with premium dashboard and add safe metric fallbacks
This commit is contained in:
+48
-404
@@ -474,7 +474,6 @@ const ExperimentDetailView: React.FC<{
|
||||
);
|
||||
}
|
||||
|
||||
if (experiment.verdict || didResults) {
|
||||
const prob = Math.round((experiment.verdict_probability || 0) * 100);
|
||||
const radius = 60;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
@@ -483,13 +482,14 @@ const ExperimentDetailView: React.FC<{
|
||||
return (
|
||||
<div className="bg-[#0b1120] text-slate-200 min-h-screen -mx-4 md:-mx-6 -mt-4 md:-mt-6 p-6 md:p-8 font-sans">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row items-start justify-between gap-4 mb-8">
|
||||
<div className="flex flex-col xl:flex-row items-start justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded">
|
||||
EXPERIMENT RESULT
|
||||
</span>
|
||||
<span className="text-slate-400 text-sm font-medium">ASIN: {experiment.asins?.[0] || 'Multiple'}</span>
|
||||
<StatusBadge status={experiment.status} />
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-3xl lg:text-4xl font-extrabold text-white mb-3 tracking-tight">
|
||||
{experiment.name}
|
||||
@@ -506,16 +506,38 @@ const ExperimentDetailView: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={onBack} className="p-2.5 px-4 bg-transparent hover:bg-white/5 border border-white/10 rounded-lg transition-colors text-slate-300 font-semibold text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{experiment.status === 'planned' && (
|
||||
<button onClick={() => handleUpdateStatus('active')} className="px-4 py-2.5 bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/30 rounded-lg transition-colors text-emerald-400 font-semibold text-sm">
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
{experiment.status === 'active' && (
|
||||
<button onClick={() => handleUpdateStatus('completed')} className="px-4 py-2.5 bg-indigo-600/20 hover:bg-indigo-600/30 border border-indigo-500/30 rounded-lg transition-colors text-indigo-400 font-semibold text-sm">
|
||||
Complete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={analyzing || salesData.length === 0}
|
||||
className="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors text-white font-semibold text-sm flex items-center gap-2 shadow-[0_0_15px_rgba(79,70,229,0.3)]"
|
||||
>
|
||||
{analyzing ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
)}
|
||||
Run Analysis
|
||||
</button>
|
||||
<button onClick={onEdit} className="p-2.5 bg-transparent hover:bg-white/5 border border-white/10 rounded-lg transition-colors text-slate-300">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
|
||||
</button>
|
||||
<button onClick={handleDelete} className="p-2.5 bg-transparent hover:bg-red-500/10 border border-white/10 hover:border-red-500/30 rounded-lg transition-colors text-slate-300 hover:text-red-400">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
<button onClick={onBack} className="p-2.5 px-4 bg-transparent hover:bg-white/5 border border-white/10 rounded-lg transition-colors text-slate-300 font-semibold text-sm ml-2">
|
||||
Back
|
||||
</button>
|
||||
<button className="p-2.5 bg-transparent hover:bg-white/5 border border-white/10 rounded-lg transition-colors text-slate-300">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
||||
</button>
|
||||
<button className="p-2.5 bg-transparent hover:bg-white/5 border border-white/10 rounded-lg transition-colors text-slate-300">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -541,10 +563,24 @@ const ExperimentDetailView: React.FC<{
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{/* Render specific metrics: Units, Revenue, CVR, BSR/ACOS (if available in didResults) */}
|
||||
{/* 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 null;
|
||||
if (!res) {
|
||||
return (
|
||||
<tr key={metric} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-4 text-[13px] font-bold text-slate-200">
|
||||
{METRIC_LABELS[metric] || metric}
|
||||
{metric === experiment.primary_metric && (
|
||||
<span className="ml-2 bg-indigo-500/20 text-indigo-400 border border-indigo-500/20 text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded">PRIMARY</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-4 text-[14px] text-[#64748b] font-medium">—</td>
|
||||
<td className="py-4 text-[14px] text-white font-bold">—</td>
|
||||
<td className="py-4 text-sm font-bold text-slate-500">—</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
const isPositiveGood = !['acos', 'bsr'].includes(metric);
|
||||
const isGood = isPositiveGood ? res.lift_percent >= 0 : res.lift_percent <= 0;
|
||||
return (
|
||||
@@ -719,400 +755,8 @@ const ExperimentDetailView: React.FC<{
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Back Button + Header */}
|
||||
<button onClick={onBack} className="flex items-center gap-1.5 text-sm text-slate-400 hover:text-white transition-colors mb-6">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
Back to experiments
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight">{experiment.name}</h1>
|
||||
<StatusBadge status={experiment.status} />
|
||||
<TypeBadge type={experiment.type} />
|
||||
</div>
|
||||
{experiment.description && (
|
||||
<p className="text-sm text-slate-400 max-w-2xl">{experiment.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{experiment.status === 'planned' && (
|
||||
<button onClick={() => handleUpdateStatus('active')} className="px-3 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
Start Experiment
|
||||
</button>
|
||||
)}
|
||||
{experiment.status === 'active' && (
|
||||
<button onClick={() => handleUpdateStatus('completed')} className="px-3 py-2 bg-slate-600 hover:bg-slate-500 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
Complete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={analyzing || salesData.length === 0}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
{analyzing ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
||||
</svg>
|
||||
)}
|
||||
Run Analysis
|
||||
</button>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-2 text-slate-400 hover:text-indigo-400 hover:bg-indigo-500/10 rounded-lg transition-colors"
|
||||
title="Edit experiment"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="p-2 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
||||
title="Delete experiment"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verdict Card (if results exist) */}
|
||||
{experiment.verdict && primaryResult && (
|
||||
<div className={`mb-8 p-6 rounded-2xl border ${getVerdictColor(experiment.verdict)} relative overflow-hidden`}>
|
||||
<div className="relative z-10 flex flex-col md:flex-row items-center gap-6">
|
||||
<div className="text-center md:text-left">
|
||||
<div className="text-xs uppercase tracking-widest opacity-60 mb-1">Primary Metric Verdict</div>
|
||||
<div className="text-3xl font-bold">
|
||||
{getVerdictLabel(experiment.verdict)}
|
||||
</div>
|
||||
<div className="text-sm opacity-80 mt-1">
|
||||
{METRIC_LABELS[experiment.primary_metric] || experiment.primary_metric}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-slate-400 mb-1">Lift</div>
|
||||
<div className={`text-2xl font-bold ${primaryResult.lift_percent >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{primaryResult.lift_percent >= 0 ? '+' : ''}{primaryResult.lift_percent}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-slate-400 mb-1">Confidence</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{Math.round((experiment.verdict_probability || 0) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-slate-400 mb-1">DiD Effect</div>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{primaryResult.did_estimate >= 0 ? '+' : ''}{formatMetricValue(primaryResult.did_estimate, experiment.primary_metric)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Confidence bar */}
|
||||
<div className="mt-4 h-1.5 bg-slate-700/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${experiment.verdict === 'winner' ? 'bg-emerald-400' :
|
||||
experiment.verdict === 'loser' ? 'bg-red-400' : 'bg-slate-400'
|
||||
}`}
|
||||
style={{ width: `${Math.round((experiment.verdict_probability || 0) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
{/* Hypothesis */}
|
||||
<Card title="Hypothesis" className="lg:col-span-2">
|
||||
<p className="text-sm text-slate-300 leading-relaxed">
|
||||
{experiment.hypothesis || 'No hypothesis defined'}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Timeline */}
|
||||
<Card title="Timeline">
|
||||
<div className="space-y-3">
|
||||
{experiment.baseline_start_date && experiment.baseline_end_date && (
|
||||
<div className="flex flex-col mb-3 pb-3 border-b border-slate-700/50">
|
||||
<span className="text-xs font-semibold text-indigo-400 mb-1 uppercase tracking-wider">Baseline Period</span>
|
||||
<span className="text-white text-sm">
|
||||
{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' })}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 mt-0.5">Custom Seasonal Comparison</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">Start</span>
|
||||
<span className="text-white font-medium">{new Date(experiment.start_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</span>
|
||||
</div>
|
||||
{experiment.end_date && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">End</span>
|
||||
<span className="text-white font-medium">{new Date(experiment.end_date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">Primary Metric</span>
|
||||
<span className="text-indigo-400 font-medium">{METRIC_LABELS[experiment.primary_metric] || experiment.primary_metric}</span>
|
||||
</div>
|
||||
{experiment.owner && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">Owner</span>
|
||||
<span className="text-white">{experiment.owner}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-400">Marketplace</span>
|
||||
<span className="text-white">{experiment.marketplace || 'All'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Change Annotations */}
|
||||
{experiment.changes && experiment.changes.length > 0 && (
|
||||
<Card title="Changes Made" className="mb-8">
|
||||
<div className="space-y-2">
|
||||
{experiment.changes.map((c, i) => (
|
||||
<div key={i} className="flex items-center gap-3 py-2 px-3 bg-slate-800/50 rounded-lg">
|
||||
<span className="text-xs font-medium text-slate-400 uppercase tracking-wide w-24 shrink-0">{c.field}</span>
|
||||
<code className="text-sm text-red-400/80 bg-red-500/10 px-2 py-0.5 rounded">{c.before_value}</code>
|
||||
<svg className="w-4 h-4 text-slate-500 shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
<code className="text-sm text-emerald-400/80 bg-emerald-500/10 px-2 py-0.5 rounded">{c.after_value}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ASINs */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<Card title={`Treatment ASINs (${experiment.asins?.length || 0})`}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(experiment.asins || []).map(a => (
|
||||
<span key={a} className="text-xs font-mono bg-indigo-500/10 text-indigo-300 px-2 py-1 rounded border border-indigo-500/20">{a}</span>
|
||||
))}
|
||||
{(!experiment.asins || experiment.asins.length === 0) && (
|
||||
<span className="text-sm text-slate-500">No ASINs defined</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={`Control Group (${experiment.control_asins?.length || 0})`}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(experiment.control_asins || []).map(a => (
|
||||
<span key={a} className="text-xs font-mono bg-slate-500/10 text-slate-300 px-2 py-1 rounded border border-slate-500/20">{a}</span>
|
||||
))}
|
||||
{(!experiment.control_asins || experiment.control_asins.length === 0) && (
|
||||
<span className="text-sm text-slate-500">No control group — using simple before/after comparison</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Trend Chart */}
|
||||
{trendData.length > 2 && experiment.did_results && (
|
||||
<Card title="Performance Trend: Actual vs Counterfactual" className="mb-8">
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendData} margin={{ top: 10, right: 20, left: 10, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis
|
||||
dataKey="week"
|
||||
tick={{ fill: '#94a3b8', fontSize: 11 }}
|
||||
tickFormatter={w => w.split('-')[1] || w}
|
||||
stroke="#475569"
|
||||
/>
|
||||
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} stroke="#475569" />
|
||||
<RechartsTooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
const actual = payload.find(p => p.dataKey === 'actual');
|
||||
const counter = payload.find(p => p.dataKey === 'counterfactual');
|
||||
return (
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-3 shadow-xl">
|
||||
<div className="text-xs text-slate-400 mb-2">{label}</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="w-2 h-2 rounded-full bg-indigo-500" />
|
||||
<span className="text-slate-300">Actual:</span>
|
||||
<span className="text-white font-medium">{formatMetricValue(Number(actual?.value || 0), experiment.primary_metric)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm mt-1">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-400" />
|
||||
<span className="text-slate-300">Counterfactual:</span>
|
||||
<span className="text-white font-medium">{formatMetricValue(Number(counter?.value || 0), experiment.primary_metric)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{experiment.start_date && (
|
||||
<ReferenceLine
|
||||
x={(() => {
|
||||
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 }}
|
||||
/>
|
||||
)}
|
||||
<Line type="monotone" dataKey="actual" stroke="#6366f1" strokeWidth={2.5} dot={false} name="Actual" />
|
||||
<Line type="monotone" dataKey="counterfactual" stroke="#64748b" strokeWidth={2} strokeDasharray="6 3" dot={false} name="Counterfactual" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-6 mt-3">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<div className="w-6 h-0.5 bg-indigo-500 rounded" />
|
||||
Actual performance
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<div className="w-6 h-0.5 bg-slate-500 rounded" style={{ backgroundImage: 'repeating-linear-gradient(90deg, #64748b 0, #64748b 4px, transparent 4px, transparent 7px)' }} />
|
||||
Counterfactual (without change)
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* DiD Metric Cards */}
|
||||
{didResults && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-8">
|
||||
{(Object.entries(didResults.metrics) as [string, DiDMetricResult][]).map(([metric, result]) => {
|
||||
const isPrimary = metric === experiment.primary_metric;
|
||||
return (
|
||||
<div
|
||||
key={metric}
|
||||
className={`p-4 rounded-xl border transition-colors ${isPrimary ? 'border-indigo-500/40 bg-indigo-500/5' : 'border-slate-800 bg-slate-900/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-medium text-slate-400 uppercase tracking-wide">
|
||||
{METRIC_LABELS[metric] || metric}
|
||||
</span>
|
||||
{isPrimary && (
|
||||
<span className="text-[10px] bg-indigo-500/20 text-indigo-400 px-1.5 py-0.5 rounded font-medium">PRIMARY</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`text-xl font-bold ${result.lift_percent >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{result.lift_percent >= 0 ? '+' : ''}{result.lift_percent}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
DiD: {result.did_estimate >= 0 ? '+' : ''}{formatMetricValue(result.did_estimate, metric)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 h-1 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${result.posterior_prob_positive >= 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)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">{Math.round(result.posterior_prob_positive * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Before / After Summary Table */}
|
||||
{didResults && (
|
||||
<Card title="Before vs After Comparison" className="mb-8">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800">
|
||||
<Th>Metric</Th>
|
||||
<Th align="right">Treatment Before</Th>
|
||||
<Th align="right">Treatment After</Th>
|
||||
<Th align="right">Control Before</Th>
|
||||
<Th align="right">Control After</Th>
|
||||
<Th align="right">DiD Effect</Th>
|
||||
<Th align="right">Lift</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{(Object.entries(didResults.metrics) as [string, DiDMetricResult][]).map(([metric, r]) => (
|
||||
<tr key={metric} className={metric === experiment.primary_metric ? 'bg-indigo-500/5' : ''}>
|
||||
<td className="px-4 py-2.5 text-slate-300 font-medium">{METRIC_LABELS[metric] || metric}</td>
|
||||
<td className="px-4 py-2.5 text-right text-slate-400">{formatMetricValue(r.treatment_before, metric)}</td>
|
||||
<td className="px-4 py-2.5 text-right text-white font-medium">{formatMetricValue(r.treatment_after, metric)}</td>
|
||||
<td className="px-4 py-2.5 text-right text-slate-400">{typeof r.control_before === 'number' ? formatMetricValue(r.control_before, metric) : '—'}</td>
|
||||
<td className="px-4 py-2.5 text-right text-slate-400">{typeof r.control_after === 'number' ? formatMetricValue(r.control_after, metric) : '—'}</td>
|
||||
<td className={`px-4 py-2.5 text-right font-medium ${r.did_estimate >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{r.did_estimate >= 0 ? '+' : ''}{formatMetricValue(r.did_estimate, metric)}
|
||||
</td>
|
||||
<td className={`px-4 py-2.5 text-right font-bold ${r.lift_percent >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{r.lift_percent >= 0 ? '+' : ''}{r.lift_percent}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Learnings */}
|
||||
<Card title="Learnings & Conclusions" className="mb-8">
|
||||
{editing ? (
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={editLearnings}
|
||||
onChange={e => setEditLearnings(e.target.value)}
|
||||
rows={5}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-y"
|
||||
placeholder="What did you learn from this experiment?"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleSaveLearnings} className="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded-lg transition-colors">
|
||||
Save
|
||||
</button>
|
||||
<button onClick={() => { setEditing(false); setEditLearnings(experiment.learnings || ''); }} className="px-3 py-1.5 text-slate-400 hover:text-white text-sm transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setEditing(true)}
|
||||
className="cursor-pointer hover:bg-slate-800/30 rounded-lg p-2 -m-2 transition-colors"
|
||||
>
|
||||
{experiment.learnings ? (
|
||||
<p className="text-sm text-slate-300 leading-relaxed whitespace-pre-wrap">{experiment.learnings}</p>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 italic">Click to add learnings...</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ====================================================================
|
||||
|
||||
Reference in New Issue
Block a user