Files
challenge-pl/src/components/admin/ABTestPanel.tsx
T

201 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useState } from 'react'
import {
Box,
Button,
Grid,
GridItem,
Heading,
NumberInput,
NumberInputInput,
Stat,
StatHelpText,
StatLabel,
StatValueText,
Text,
VStack,
} from '@chakra-ui/react'
import type { ABTestMetrics } from '../../__data__/types'
import { compareVariants } from '../../utils/analytics'
interface VariantFormState {
submissionRate: number
completionRate: number
retryRate: number
timeToFirstSubmission: number
sessionDuration: number
satisfactionScore?: number
}
const createVariantState = (): VariantFormState => ({
submissionRate: 0,
completionRate: 0,
retryRate: 0,
timeToFirstSubmission: 0,
sessionDuration: 0,
satisfactionScore: undefined,
})
const buildMetrics = (variant: 'A' | 'B', state: VariantFormState): ABTestMetrics => ({
variant,
submissionRate: state.submissionRate,
completionRate: state.completionRate,
retryRate: state.retryRate,
timeToFirstSubmission: state.timeToFirstSubmission,
sessionDuration: state.sessionDuration,
satisfactionScore: state.satisfactionScore,
})
const MetricInput = ({
label,
value,
onChange,
suffix,
}: {
label: string
value: number
onChange: (value: number) => void
suffix?: string
}) => (
<Box>
<Text fontSize="sm" fontWeight="medium" mb={1}>
{label}
</Text>
<NumberInput value={value} min={0} onChange={(_, val) => onChange(Number.isNaN(val) ? 0 : val)}>
<NumberInputInput />
</NumberInput>
{suffix && (
<StatHelpText fontSize="xs" color="gray.500">
{suffix}
</StatHelpText>
)}
</Box>
)
export const ABTestPanel = () => {
const [variantA, setVariantA] = useState<VariantFormState>(createVariantState)
const [variantB, setVariantB] = useState<VariantFormState>(createVariantState)
const [comparison, setComparison] = useState<ReturnType<typeof compareVariants> | null>(null)
const handleCompare = () => {
const metricsA = buildMetrics('A', variantA)
const metricsB = buildMetrics('B', variantB)
setComparison(compareVariants(metricsA, metricsB))
}
const hasData = useMemo(
() =>
Object.values(variantA).some((value) => value !== 0) ||
Object.values(variantB).some((value) => value !== 0),
[variantA, variantB],
)
return (
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={4}>
<Heading size="sm" mb={4}>
A/B тест: сравнение вариантов
</Heading>
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4} mb={4}>
<GridItem>
<Heading size="xs" mb={2}>
Вариант A
</Heading>
<VStack spacing={3} align="stretch">
<MetricInput
label="Submission Rate (%)"
value={variantA.submissionRate}
onChange={(value) => setVariantA((prev) => ({ ...prev, submissionRate: value }))}
suffix="Процент пользователей, отправивших хотя бы одно решение"
/>
<MetricInput
label="Completion Rate (%)"
value={variantA.completionRate}
onChange={(value) => setVariantA((prev) => ({ ...prev, completionRate: value }))}
/>
<MetricInput
label="Retry Rate (%)"
value={variantA.retryRate}
onChange={(value) => setVariantA((prev) => ({ ...prev, retryRate: value }))}
/>
<MetricInput
label="Time to First Submission (мин)"
value={variantA.timeToFirstSubmission}
onChange={(value) => setVariantA((prev) => ({ ...prev, timeToFirstSubmission: value }))}
/>
<MetricInput
label="Session Duration (мин)"
value={variantA.sessionDuration}
onChange={(value) => setVariantA((prev) => ({ ...prev, sessionDuration: value }))}
/>
</VStack>
</GridItem>
<GridItem>
<Heading size="xs" mb={2}>
Вариант B
</Heading>
<VStack spacing={3} align="stretch">
<MetricInput
label="Submission Rate (%)"
value={variantB.submissionRate}
onChange={(value) => setVariantB((prev) => ({ ...prev, submissionRate: value }))}
/>
<MetricInput
label="Completion Rate (%)"
value={variantB.completionRate}
onChange={(value) => setVariantB((prev) => ({ ...prev, completionRate: value }))}
/>
<MetricInput
label="Retry Rate (%)"
value={variantB.retryRate}
onChange={(value) => setVariantB((prev) => ({ ...prev, retryRate: value }))}
/>
<MetricInput
label="Time to First Submission (мин)"
value={variantB.timeToFirstSubmission}
onChange={(value) => setVariantB((prev) => ({ ...prev, timeToFirstSubmission: value }))}
/>
<MetricInput
label="Session Duration (мин)"
value={variantB.sessionDuration}
onChange={(value) => setVariantB((prev) => ({ ...prev, sessionDuration: value }))}
/>
</VStack>
</GridItem>
</Grid>
<Button onClick={handleCompare} colorScheme="teal" isDisabled={!hasData}>
Сравнить варианты
</Button>
{comparison && (
<Box mt={4} borderWidth="1px" borderRadius="md" borderColor="teal.200" bg="teal.50" p={4}>
<Heading size="xs" mb={2}>
Результат сравнения
</Heading>
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
<Stat>
<StatLabel>Δ Submission Rate</StatLabel>
<StatValueText>{comparison.submissionRateDiff.toFixed(1)}%</StatValueText>
<StatHelpText>Положительное значение рост у варианта B</StatHelpText>
</Stat>
<Stat>
<StatLabel>Δ Completion Rate</StatLabel>
<StatValueText>{comparison.completionRateDiff.toFixed(1)}%</StatValueText>
<StatHelpText>Положительное значение рост у варианта B</StatHelpText>
</Stat>
</Grid>
<Stat mt={4}>
<StatLabel>Победитель</StatLabel>
<StatValueText>Вариант {comparison.winner}</StatValueText>
<StatHelpText>Основано на сравнении коэффициента завершения</StatHelpText>
</Stat>
</Box>
)}
</Box>
)
}