/** * Create Benchmark Page (Story 6.2) * * Route: /benchmarks/new * * Features: * - Form: Name, Description, Agent dropdown, Min sessions * - Variant editor: starts with 2 rows, add/remove (min 2, max 10) * - Metric selector: checkboxes for 8 metrics * - Time range: optional date pickers * - Validation, submit, error display * - Help text explaining tagging workflow */ import React, { useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useApi } from '../hooks/useApi'; import { getAgents, createBenchmark } from '../api/client'; // ─── Constants ────────────────────────────────────────────────────── const ALL_METRICS = [ { id: 'cost_per_session', label: 'Cost per Session' }, { id: 'avg_latency', label: 'Average Latency' }, { id: 'error_rate', label: 'Error Rate' }, { id: 'tool_call_count', label: 'Tool Call Count' }, { id: 'tokens_per_session', label: 'Tokens per Session' }, { id: 'session_duration', label: 'Session Duration' }, { id: 'task_completion', label: 'Task Completion' }, { id: 'user_satisfaction', label: 'User Satisfaction' }, ]; const MAX_VARIANTS = 10; const MIN_VARIANTS = 2; interface VariantRow { key: number; name: string; tag: string; description: string; } interface FormErrors { name?: string; variants?: string; metrics?: string; api?: string; } function emptyVariant(key: number): VariantRow { return { key, name: '', tag: '', description: '' }; } // ─── Component ────────────────────────────────────────────────────── export function BenchmarkNew(): React.ReactElement { const navigate = useNavigate(); // Agents for dropdown const { data: agents } = useApi(() => getAgents(), []); // Form state const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [agentId, setAgentId] = useState(''); const [minSessions, setMinSessions] = useState(30); const [variants, setVariants] = useState([emptyVariant(1), emptyVariant(2)]); const [metrics, setMetrics] = useState>(new Set(ALL_METRICS.map((m) => m.id))); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [errors, setErrors] = useState({}); const [submitting, setSubmitting] = useState(false); // Variant key counter const [nextKey, setNextKey] = useState(3); // ─── Variant Handlers ─────────────────────────────────────────── const updateVariant = useCallback((key: number, field: keyof VariantRow, value: string) => { setVariants((prev) => prev.map((v) => (v.key === key ? { ...v, [field]: value } : v)), ); }, []); const addVariant = useCallback(() => { setVariants((prev) => { if (prev.length >= MAX_VARIANTS) return prev; return [...prev, emptyVariant(nextKey)]; }); setNextKey((k) => k + 1); }, [nextKey]); const removeVariant = useCallback((key: number) => { setVariants((prev) => { if (prev.length <= MIN_VARIANTS) return prev; return prev.filter((v) => v.key !== key); }); }, []); // ─── Metric Toggle ───────────────────────────────────────────── const toggleMetric = useCallback((id: string) => { setMetrics((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); // ─── Validate ─────────────────────────────────────────────────── const validate = useCallback((): FormErrors => { const errs: FormErrors = {}; if (!name.trim()) { errs.name = 'Name is required.'; } const validVariants = variants.filter((v) => v.name.trim() && v.tag.trim()); if (validVariants.length < MIN_VARIANTS) { errs.variants = `At least ${MIN_VARIANTS} variants are required, each with a name and tag.`; } if (metrics.size === 0) { errs.metrics = 'At least one metric must be selected.'; } return errs; }, [name, variants, metrics]); // ─── Submit ───────────────────────────────────────────────────── const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); const errs = validate(); setErrors(errs); if (Object.keys(errs).length > 0) return; setSubmitting(true); try { const result = await createBenchmark({ name: name.trim(), description: description.trim() || undefined, agentId: agentId || undefined, minSessions, variants: variants .filter((v) => v.name.trim() && v.tag.trim()) .map((v) => ({ name: v.name.trim(), tag: v.tag.trim(), description: v.description.trim() || undefined, })), metrics: Array.from(metrics), startDate: startDate || undefined, endDate: endDate || undefined, }); navigate(`/benchmarks/${result.id}`); } catch (err) { setErrors({ api: err instanceof Error ? err.message : 'Failed to create benchmark.', }); } finally { setSubmitting(false); } }, [name, description, agentId, minSessions, variants, metrics, startDate, endDate, validate, navigate], ); return (
{/* Header */}

New Benchmark

Set up an A/B test to compare agent configurations

{/* API error */} {errors.api && (
{errors.api}
)}
{/* ─── Basic Info ────────────────────────────────────────── */}

Basic Information

{/* Name */}
setName(e.target.value)} placeholder="e.g., GPT-4o vs Claude Sonnet" className={`mt-1 block w-full rounded-md border px-3 py-2 text-sm shadow-sm focus:ring-2 focus:ring-brand-500 focus:border-brand-500 ${ errors.name ? 'border-red-300' : 'border-gray-300' }`} /> {errors.name &&

{errors.name}

}
{/* Description */}