Refactor project structure and integrate Redux for state management. Update configuration files for new project name and add Webpack plugins for environment variables. Implement user authentication with Keycloak and create a context for challenge management. Add various components for user interaction, including dashboards and task workspaces. Enhance API integration and add error handling utilities. Introduce analytics and polling mechanisms for improved user experience.
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user