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

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-03 12:55:34 +03:00
parent 3a65307fd0
commit 624280ab5e
47 changed files with 3465 additions and 67 deletions
+200
View File
@@ -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>
)
}