Files
CrazeAnalytix/components/ExperimentBadge.tsx
T
Christian Vidal WolfandClaude Opus 4.6 24df5935cc feat: rebuild Experiments tab with Difference-in-Differences analysis and Bayesian verdicts
Replace the basic CRUD experiment tracker with a scientifically rigorous A/B testing system:

- Add DiD analysis engine (services/experimentAnalysis.ts) that computes treatment vs control
  group comparisons across 7 metrics (units, sessions, CVR, CTR, ROAS, revenue, ACOS)
- Implement Bayesian verdict system (Winner/Loser/Inconclusive) using posterior probability
  with normal CDF approximation (Abramowitz & Stegun erf, no external deps)
- Build counterfactual time series for trend charts (actual vs estimated without change)
- Rewrite ExperimentsView as single component with 3 inline sub-views (list, detail, create)
  replacing the previous modal-based ExperimentDetail and ExperimentForm
- Add control group support, change annotations (before→after diffs), and SEO experiment type
- New types: ExperimentChangeAnnotation, DiDMetricResult, DifferenceInDifferencesResult,
  ExperimentVerdict
- Simplify App.tsx by removing experiment modal state (5 useState hooks eliminated)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:42:40 +01:00

116 lines
3.5 KiB
TypeScript

import React from 'react';
import { ActiveExperiment, ExperimentStatus } from '../types';
import { getExperimentStatusColor, getExperimentTypeColor, getExperimentIcon } from '../services/experiments';
interface ExperimentBadgeProps {
experiments: ActiveExperiment[];
onClick?: (experimentId: string) => void;
}
const getStatusBadge = (status: ExperimentStatus) => {
switch (status) {
case 'active': return '🟢';
case 'planned': return '🟡';
case 'completed': return '⚪';
case 'paused': return '⏸️';
}
};
export const ExperimentBadge: React.FC<ExperimentBadgeProps> = ({ experiments, onClick }) => {
if (!experiments || experiments.length === 0) return null;
const activeExp = experiments.find(e => e.status === 'active');
const plannedExp = experiments.find(e => e.status === 'planned');
// Priority: active > planned > past
const displayExp = activeExp || plannedExp || experiments[0];
const daysText = displayExp.days_remaining !== undefined
? displayExp.days_remaining > 0
? `${displayExp.days_remaining}d left`
: 'Ending soon'
: '';
return (
<div className="inline-flex items-center gap-1">
{experiments.length > 1 && (
<span className="text-[9px] text-slate-500 mr-1">+{experiments.length}</span>
)}
<button
onClick={(e) => {
e.stopPropagation();
onClick?.(displayExp.experiment_id);
}}
className={`
inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-medium
border ${getExperimentStatusColor(displayExp.status)}
hover:opacity-80 transition-opacity cursor-pointer
`}
title={`${displayExp.experiment_name}
Type: ${displayExp.type}
Start: ${displayExp.start_date}
End: ${displayExp.end_date || 'Ongoing'}
${daysText}`}
>
<span>{getExperimentIcon(displayExp.type)}</span>
<span className="hidden xl:inline truncate max-w-[80px]">{displayExp.experiment_name}</span>
{daysText && <span className="opacity-60">{daysText}</span>}
</button>
</div>
);
};
interface ExperimentStatusBadgeProps {
status: ExperimentStatus;
size?: 'sm' | 'md' | 'lg';
}
export const ExperimentStatusBadge: React.FC<ExperimentStatusBadgeProps> = ({ status, size = 'md' }) => {
const sizeClasses = {
sm: 'px-1.5 py-0.5 text-[9px]',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
};
const statusLabels = {
active: 'Active',
planned: 'Planned',
completed: 'Completed',
paused: 'Paused',
};
return (
<span className={`inline-flex items-center gap-1 rounded-full font-medium border ${getExperimentStatusColor(status)} ${sizeClasses[size]}`}>
{getStatusBadge(status)} {statusLabels[status]}
</span>
);
};
interface ExperimentTypeBadgeProps {
type: string;
size?: 'sm' | 'md' | 'lg';
}
export const ExperimentTypeBadge: React.FC<ExperimentTypeBadgeProps> = ({ type, size = 'md' }) => {
const sizeClasses = {
sm: 'px-1.5 py-0.5 text-[9px]',
md: 'px-2 py-1 text-xs',
lg: 'px-3 py-1.5 text-sm',
};
const typeLabels: Record<string, string> = {
pricing: 'Pricing',
advertising: 'Advertising',
content: 'Content',
promotion: 'Promotion',
seo: 'SEO',
};
return (
<span className={`inline-flex items-center gap-1 rounded-full font-medium border ${getExperimentTypeColor(type as any)} ${sizeClasses[size]}`}>
<span>{getExperimentIcon(type as any)}</span>
{typeLabels[type as keyof typeof typeLabels] || type}
</span>
);
};