201 lines
6.7 KiB
TypeScript
201 lines
6.7 KiB
TypeScript
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>
|
||
)
|
||
}
|
||
|