From 3a65307fd0cbee4a0be612762bdf96dae7ac747c Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Sun, 2 Nov 2025 17:44:37 +0300 Subject: [PATCH] init brojs --- .prettierignore | 7 + .prettierrc.json | 7 + Jenkinsfile | 57 + bro.config.js | 23 + eslint.config.mjs | 57 + .../frontend/CHALLENGE_ANALYTICS_SUMMARY.md | 635 + .../frontend/CHALLENGE_FRONTEND_GUIDE.md | 1125 ++ .../frontend/CHALLENGE_REACT_EXAMPLE.md | 1060 ++ memory bank/frontend/README.md | 178 + memory bank/frontend/TEACHER_GUIDE.md | 439 + package-lock.json | 11642 ++++++++++++++++ package.json | 40 + src/__data__/urls.ts | 15 + src/app.tsx | 17 + src/dashboard.tsx | 24 + src/index.tsx | 28 + src/pages/index.ts | 3 + src/pages/main/index.ts | 3 + src/pages/main/main.tsx | 28 + src/theme.tsx | 33 + stubs/api/index.js | 8 + tsconfig.json | 25 + types.d.ts | 6 + 23 files changed, 15460 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 Jenkinsfile create mode 100644 bro.config.js create mode 100644 eslint.config.mjs create mode 100644 memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md create mode 100644 memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md create mode 100644 memory bank/frontend/CHALLENGE_REACT_EXAMPLE.md create mode 100644 memory bank/frontend/README.md create mode 100644 memory bank/frontend/TEACHER_GUIDE.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/__data__/urls.ts create mode 100644 src/app.tsx create mode 100644 src/dashboard.tsx create mode 100644 src/index.tsx create mode 100644 src/pages/index.ts create mode 100644 src/pages/main/index.ts create mode 100644 src/pages/main/main.tsx create mode 100644 src/theme.tsx create mode 100644 stubs/api/index.js create mode 100644 tsconfig.json create mode 100644 types.d.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fc5ced8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +# Ignore artifacts: +build +dist +coverage +stubs +logs +d-scripts \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bcf40f7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "jsxSingleQuote": false +} diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..d235032 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,57 @@ +pipeline { + agent { + docker { + image 'node:20' + } + } + + stages { + stage('install') { + steps { + sh 'node -v' + sh 'npm -v' + script { + String tag = sh(returnStdout: true, script: 'git tag --contains').trim() + String branchName = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() + String commit = sh(returnStdout: true, script: 'git log -1 --oneline').trim() + String commitMsg = commit.substring(commit.indexOf(' ')).trim() + + if (tag) { + currentBuild.displayName = "#${BUILD_NUMBER}, tag ${tag}" + } else { + currentBuild.displayName = "#${BUILD_NUMBER}, branch ${branchName}" + } + + String author = sh(returnStdout: true, script: "git log -1 --pretty=format:'%an'").trim() + currentBuild.description = "${author}
${commitMsg}" + echo 'starting installing' + sh 'npm ci' + } + } + } + + stage('checks') { + parallel { + stage('eslint') { + steps { + sh 'npm run eslint' + } + } + + stage('build') { + steps { + sh 'npm run build' + } + } + } + } + + stage('clean-all') { + steps { + sh 'rm -rf .[!.]*' + sh 'rm -rf ./*' + sh 'ls -a' + } + } + } +} diff --git a/bro.config.js b/bro.config.js new file mode 100644 index 0000000..80a9b85 --- /dev/null +++ b/bro.config.js @@ -0,0 +1,23 @@ +const pkg = require('./package') + +module.exports = { + apiPath: 'stubs/api', + webpackConfig: { + output: { + publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/` + } + }, + /* use https://admin.bro-js.ru/ to create config, navigations and features */ + navigations: { + 'challenge-pl.main': '/challenge-pl', + 'link.challenge-pl.auth': '/auth' + }, + features: { + 'challenge-pl': { + // add your features here in the format [featureName]: { value: string } + }, + }, + config: { + 'challenge-pl.api': '/api' + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..e7bf561 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,57 @@ +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' +import pluginReact from 'eslint-plugin-react' +import stylistic from '@stylistic/eslint-plugin' + +export default [ + { + settings: { + react: { + version: 'detect', + }, + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + }, + { files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] }, + { languageOptions: { globals: globals.browser } }, + { + ignores: ['stubs/', 'bro.config.js'], + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReact.configs.flat.recommended, + { + plugins: { + '@stylistic': stylistic, + }, + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', // or "error" + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'sort-imports': [ + 'error', + { + ignoreCase: false, + ignoreDeclarationSort: true, + ignoreMemberSort: true, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + allowSeparatedGroups: true, + }, + ], + semi: ['error', 'never'], + '@stylistic/indent': ['error', 2], + }, + }, +] diff --git a/memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md b/memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md new file mode 100644 index 0000000..d4058e4 --- /dev/null +++ b/memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md @@ -0,0 +1,635 @@ +# Challenge Service - Аналитика и метрики для фронтенда + +Краткое руководство по ключевым метрикам и аналитике для интеграции на фронтенде. + +## 📊 Ключевые метрики для отслеживания + +### 1. Метрики производительности + +```typescript +// Метрики для мониторинга +interface PerformanceMetrics { + // Время от отправки до получения результата + timeToFeedback: number // миллисекунды + + // Время ожидания в очереди + queueWaitTime: number // миллисекунды + + // Время непосредственной проверки + checkTime: number // миллисекунды + + // Позиция в очереди при добавлении + initialQueuePosition: number + + // Количество проверок статуса до завершения + pollsBeforeComplete: number +} + +// Пример сбора метрик +class MetricsCollector { + private startTime: number = 0 + private pollCount: number = 0 + + startTracking() { + this.startTime = Date.now() + this.pollCount = 0 + } + + incrementPoll() { + this.pollCount++ + } + + getMetrics(submission: ChallengeSubmission): PerformanceMetrics { + return { + timeToFeedback: Date.now() - this.startTime, + queueWaitTime: submission.checkedAt + ? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime() + : 0, + checkTime: submission.checkedAt + ? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime() + : 0, + initialQueuePosition: 0, // Сохранить из первого ответа + pollsBeforeComplete: this.pollCount + } + } +} +``` + +### 2. Метрики пользовательского поведения + +```typescript +interface UserBehaviorMetrics { + // Время, проведенное на задании + timeSpentOnTask: number // секунды + + // Количество символов в решении + solutionLength: number + + // Количество редактирований текста + editCount: number + + // Использовал ли черновик + usedDraft: boolean + + // Время от загрузки до отправки + timeToSubmit: number // секунды +} + +// Трекинг поведения +class BehaviorTracker { + private taskStartTime: number = Date.now() + private editCount: number = 0 + private lastValue: string = '' + + onTextChange(newValue: string) { + if (newValue !== this.lastValue) { + this.editCount++ + this.lastValue = newValue + } + } + + getMetrics(result: string, usedDraft: boolean): UserBehaviorMetrics { + return { + timeSpentOnTask: Math.floor((Date.now() - this.taskStartTime) / 1000), + solutionLength: result.length, + editCount: this.editCount, + usedDraft, + timeToSubmit: Math.floor((Date.now() - this.taskStartTime) / 1000) + } + } +} +``` + +### 3. Метрики успешности + +```typescript +interface SuccessMetrics { + // Процент принятых заданий с первой попытки + firstAttemptSuccessRate: number // 0-100 + + // Среднее количество попыток до успеха + averageAttemptsToSuccess: number + + // Процент завершенных цепочек + chainCompletionRate: number // 0-100 + + // Время до первого успешного задания + timeToFirstSuccess: number // минуты +} + +// Расчет метрик успешности +function calculateSuccessMetrics(stats: UserStats): SuccessMetrics { + const taskStats = stats.taskStats + + const firstAttemptSuccess = taskStats.filter( + t => t.status === 'completed' && t.totalAttempts === 1 + ).length + + const completedTasks = taskStats.filter(t => t.status === 'completed') + const totalAttempts = completedTasks.reduce((sum, t) => sum + t.totalAttempts, 0) + + return { + firstAttemptSuccessRate: (firstAttemptSuccess / taskStats.length) * 100, + averageAttemptsToSuccess: completedTasks.length > 0 + ? totalAttempts / completedTasks.length + : 0, + chainCompletionRate: (stats.chainStats.filter(c => c.progress === 100).length / stats.chainStats.length) * 100, + timeToFirstSuccess: 0 // Требует дополнительных данных + } +} +``` + +## 📈 Дашборды для фронтенда + +### 1. Personal Dashboard (для студента) + +```typescript +interface PersonalDashboard { + // Общий прогресс + overview: { + tasksCompleted: number + totalTasks: number + completionPercentage: number + currentStreak: number // дней подряд + } + + // Текущие цепочки + activeChains: Array<{ + chainId: string + name: string + progress: number + nextTask: ChallengeTask | null + estimatedTimeToComplete: number // минуты + }> + + // Последние достижения + recentAchievements: Array<{ + type: 'task_completed' | 'chain_completed' | 'first_try_success' + taskTitle: string + timestamp: string + }> + + // Статистика по попыткам + attemptsStats: { + totalAttempts: number + successfulAttempts: number + successRate: number + } + + // Рекомендации + recommendations: Array<{ + type: 'retry' | 'continue' | 'new_chain' + message: string + actionLink: string + }> +} + +// Генерация dashboard +async function generatePersonalDashboard(userId: string): Promise { + const stats = await challengeAPI.getUserStats(userId) + const chains = await challengeAPI.getChains() + + return { + overview: { + tasksCompleted: stats.completedTasks, + totalTasks: stats.totalTasksAttempted, + completionPercentage: (stats.completedTasks / stats.totalTasksAttempted) * 100, + currentStreak: 0 // Требует дополнительной логики + }, + activeChains: stats.chainStats + .filter(c => c.progress > 0 && c.progress < 100) + .map(c => { + const chain = chains.find(ch => ch.id === c.chainId) + const completedCount = c.completedTasks + const nextTask = chain?.tasks[completedCount] || null + + return { + chainId: c.chainId, + name: c.chainName, + progress: c.progress, + nextTask, + estimatedTimeToComplete: (c.totalTasks - c.completedTasks) * 10 // 10 мин на задание + } + }), + recentAchievements: [], // Требует истории + attemptsStats: { + totalAttempts: stats.totalSubmissions, + successfulAttempts: stats.completedTasks, + successRate: (stats.completedTasks / stats.totalSubmissions) * 100 + }, + recommendations: generateRecommendations(stats) + } +} + +function generateRecommendations(stats: UserStats): Array<{type: string, message: string, actionLink: string}> { + const recommendations = [] + + // Если есть задания требующие доработки + if (stats.needsRevisionTasks > 0) { + recommendations.push({ + type: 'retry', + message: `У вас ${stats.needsRevisionTasks} заданий требуют доработки`, + actionLink: '/tasks?status=needs_revision' + }) + } + + // Если есть начатые цепочки + const inProgressChains = stats.chainStats.filter(c => c.progress > 0 && c.progress < 100) + if (inProgressChains.length > 0) { + recommendations.push({ + type: 'continue', + message: `Продолжите цепочку "${inProgressChains[0].chainName}"`, + actionLink: `/chain/${inProgressChains[0].chainId}` + }) + } + + return recommendations +} +``` + +### 2. Admin Dashboard (для преподавателя) + +```typescript +interface AdminDashboard { + // Системные метрики + system: { + totalUsers: number + activeUsers24h: number + totalTasks: number + totalChains: number + queueStatus: { + length: number + processing: number + avgWaitTime: number + } + } + + // Метрики заданий + taskMetrics: Array<{ + taskId: string + title: string + attemptsCount: number + successRate: number + avgAttempts: number + avgTimeToComplete: number + difficulty: 'easy' | 'medium' | 'hard' // на основе метрик + }> + + // Активность пользователей + userActivity: { + registrationsToday: number + submissionsToday: number + peakHours: Array<{ hour: number, count: number }> + } + + // Проблемные области + issues: Array<{ + type: 'low_success_rate' | 'high_attempts' | 'long_queue' + severity: 'low' | 'medium' | 'high' + message: string + affectedEntity: string + }> +} + +// Анализ сложности задания +function analyzeDifficulty( + successRate: number, + avgAttempts: number +): 'easy' | 'medium' | 'hard' { + if (successRate > 70 && avgAttempts < 2) return 'easy' + if (successRate > 40 && avgAttempts < 3) return 'medium' + return 'hard' +} + +// Определение проблем +function detectIssues(stats: SystemStats): Array { + const issues = [] + + // Длинная очередь + if (stats.queue.queueLength > 50) { + issues.push({ + type: 'long_queue', + severity: 'high', + message: `Очередь содержит ${stats.queue.queueLength} заданий`, + affectedEntity: 'system' + }) + } + + // Низкий success rate системы + const systemSuccessRate = (stats.submissions.accepted / stats.submissions.total) * 100 + if (systemSuccessRate < 30) { + issues.push({ + type: 'low_success_rate', + severity: 'medium', + message: `Общий процент принятых заданий всего ${systemSuccessRate.toFixed(1)}%`, + affectedEntity: 'system' + }) + } + + return issues +} +``` + +## 🎯 Визуализация метрик + +### 1. Progress Chart (круговая диаграмма) + +```typescript +interface ProgressChartData { + completed: number + inProgress: number + needsRevision: number + notStarted: number +} + +// Компонент для отображения (концепт) +function ProgressChart({ data }: { data: ProgressChartData }) { + const total = Object.values(data).reduce((a, b) => a + b, 0) + + return ( +
+ + {/* Реализация круговой диаграммы */} + +
+
✅ Завершено: {data.completed}
+
🔄 В процессе: {data.inProgress}
+
❌ Доработка: {data.needsRevision}
+
⚪ Не начато: {data.notStarted}
+
+
+ ) +} +``` + +### 2. Timeline Chart (время проверки) + +```typescript +interface TimelineData { + submissions: Array<{ + timestamp: string + checkTime: number + status: 'accepted' | 'needs_revision' + }> +} + +// График времени проверки по времени суток +function TimelineChart({ data }: { data: TimelineData }) { + const hourlyData = new Array(24).fill(0).map((_, hour) => { + const submissions = data.submissions.filter(s => + new Date(s.timestamp).getHours() === hour + ) + + return { + hour, + count: submissions.length, + avgCheckTime: submissions.length > 0 + ? submissions.reduce((sum, s) => sum + s.checkTime, 0) / submissions.length + : 0 + } + }) + + return ( +
+ {/* Реализация bar chart */} +
+ ) +} +``` + +### 3. Heatmap (активность по дням) + +```typescript +interface HeatmapData { + dates: Array<{ + date: string // YYYY-MM-DD + submissions: number + successRate: number + }> +} + +// Визуализация активности пользователя +function ActivityHeatmap({ data }: { data: HeatmapData }) { + return ( +
+ {data.dates.map(day => ( +
50 ? 'green' : 'red' + }} + title={`${day.date}: ${day.submissions} попыток, ${day.successRate}% успех`} + /> + ))} +
+ ) +} +``` + +## 🔔 Real-time уведомления + +### События для отслеживания + +```typescript +enum ChallengeEventType { + SUBMISSION_QUEUED = 'submission_queued', + SUBMISSION_CHECKING = 'submission_checking', + SUBMISSION_COMPLETED = 'submission_completed', + TASK_COMPLETED = 'task_completed', + CHAIN_COMPLETED = 'chain_completed', + ACHIEVEMENT_UNLOCKED = 'achievement_unlocked' +} + +interface ChallengeEvent { + type: ChallengeEventType + timestamp: string + userId: string + data: any +} + +// Event emitter для уведомлений +class ChallengeEventEmitter { + private listeners: Map void>> = new Map() + + on(type: ChallengeEventType, callback: (event: ChallengeEvent) => void) { + if (!this.listeners.has(type)) { + this.listeners.set(type, []) + } + this.listeners.get(type)!.push(callback) + } + + emit(event: ChallengeEvent) { + const callbacks = this.listeners.get(event.type) || [] + callbacks.forEach(cb => cb(event)) + } +} + +// Использование +const events = new ChallengeEventEmitter() + +events.on(ChallengeEventType.TASK_COMPLETED, (event) => { + // Показать toast уведомление + showNotification('✅ Задание выполнено!', 'success') + + // Обновить статистику + refreshStats() + + // Отправить аналитику + analytics.track('task_completed', event.data) +}) +``` + +## 📱 Адаптивная аналитика + +### Мобильная версия дашборда + +```typescript +interface MobileDashboard { + // Упрощенные метрики для мобильных + quickStats: { + completedToday: number + currentStreak: number + nextTask: string + } + + // Минимальные графики + weekProgress: number[] // 7 последних дней + + // Быстрые действия + quickActions: Array<{ + label: string + action: () => void + icon: string + }> +} +``` + +## 🎨 UI Components для метрик + +### Stat Card Component + +```typescript +interface StatCardProps { + title: string + value: number | string + change?: number // % изменение + trend?: 'up' | 'down' + icon?: string +} + +function StatCard({ title, value, change, trend, icon }: StatCardProps) { + return ( +
+
+ {icon} + {title} +
+
{value}
+ {change && ( +
+ {trend === 'up' ? '↑' : '↓'} {Math.abs(change)}% +
+ )} +
+ ) +} + +// Использование + +``` + +## 🔍 A/B Testing + +### Метрики для тестирования + +```typescript +interface ABTestMetrics { + variant: 'A' | 'B' + + // Конверсионные метрики + submissionRate: number // % пользователей, отправивших хотя бы одно задание + completionRate: number // % завершенных заданий + retryRate: number // % повторных попыток + + // Временные метрики + timeToFirstSubmission: number + sessionDuration: number + + // Качественные метрики + satisfactionScore?: number // если есть опрос +} + +// Сравнение вариантов +function compareVariants(variantA: ABTestMetrics, variantB: ABTestMetrics) { + return { + submissionRateDiff: ((variantB.submissionRate - variantA.submissionRate) / variantA.submissionRate) * 100, + completionRateDiff: ((variantB.completionRate - variantA.completionRate) / variantA.completionRate) * 100, + winner: variantB.completionRate > variantA.completionRate ? 'B' : 'A' + } +} +``` + +## 📊 Экспорт данных + +### CSV Export + +```typescript +async function exportUserProgress(userId: string): Promise { + const stats = await challengeAPI.getUserStats(userId) + const submissions = await challengeAPI.getSubmissions(userId) + + let csv = 'Task,Status,Attempts,Last Attempt,Feedback\n' + + stats.taskStats.forEach(task => { + csv += `"${task.taskTitle}","${task.status}",${task.totalAttempts},"${task.lastAttemptAt || 'N/A'}",""\n` + }) + + return csv +} + +// Скачивание файла +function downloadCSV(csv: string, filename: string) { + const blob = new Blob([csv], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + link.click() + URL.revokeObjectURL(url) +} +``` + +--- + +## ✅ Чек-лист для фронтенд-разработчика + +- [ ] Интегрировать API клиент +- [ ] Настроить Context для state management +- [ ] Реализовать polling механизм +- [ ] Добавить Personal Dashboard +- [ ] Создать визуализации прогресса +- [ ] Настроить event tracking +- [ ] Добавить offline support +- [ ] Реализовать экспорт данных +- [ ] Добавить A/B тестирование +- [ ] Настроить мониторинг ошибок +- [ ] Оптимизировать для мобильных +- [ ] Добавить accessibility features + +## 📚 Полезные ресурсы + +- **API документация**: `CHALLENGE_API_README.md` +- **Архитектура**: `CHALLENGE_ARCHITECTURE.md` +- **React пример**: `CHALLENGE_REACT_EXAMPLE.md` +- **Быстрый старт**: `CHALLENGE_QUICK_START.md` + +Используйте эти метрики и компоненты для создания информативного и user-friendly интерфейса! + diff --git a/memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md b/memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md new file mode 100644 index 0000000..22d8bfb --- /dev/null +++ b/memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md @@ -0,0 +1,1125 @@ +# Challenge Service - Руководство для фронтенда + +Руководство по интеграции сервиса проверки заданий через LLM в пользовательский интерфейс. + +## Содержание + +1. [Основные сценарии использования](#основные-сценарии-использования) +2. [Структура данных](#структура-данных) +3. [API взаимодействие](#api-взаимодействие) +4. [Компоненты UI](#компоненты-ui) +5. [State Management](#state-management) +6. [Best Practices](#best-practices) + +--- + +## Основные сценарии использования + +### 1. Сценарий для студента + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Вход/Регистрация (nickname) │ +│ POST /api/challenge/auth │ +│ → Сохранить userId в localStorage/state │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 2. Просмотр доступных цепочек заданий │ +│ GET /api/challenge/chains │ +│ → Отобразить список с прогрессом │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 3. Выбор задания из цепочки │ +│ → Отобразить описание (Markdown) │ +│ → Показать историю своих попыток │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 4. Написание решения │ +│ → Текстовое поле / Code Editor │ +│ → Кнопка "Отправить на проверку" │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 5. Отправка на проверку │ +│ POST /api/challenge/submit │ +│ → Получить queueId │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 6. Ожидание результата (polling) │ +│ GET /api/challenge/check-status/:queueId │ +│ → Показать индикатор загрузки + позицию в очереди │ +│ → Повторять каждые 2-3 секунды │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 7. Получение результата │ +│ ✅ ПРИНЯТО → Поздравление + переход к следующему │ +│ ❌ ДОРАБОТКА → Feedback от LLM + возможность повторить │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2. Сценарий для администратора + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Создание заданий │ +│ POST /api/challenge/task │ +│ → Markdown редактор для описания │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 2. Создание цепочек │ +│ POST /api/challenge/chain │ +│ → Выбор заданий + порядок │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 3. Мониторинг статистики │ +│ GET /api/challenge/stats │ +│ → Dashboard с метриками │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Структура данных + +### User (Пользователь) + +```typescript +interface ChallengeUser { + _id: string + id: string + nickname: string + createdAt: string // ISO 8601 +} +``` + +### Task (Задание) + +```typescript +interface ChallengeTask { + _id: string + id: string + title: string + description: string // Markdown (видно всем) + hiddenInstructions?: string // Скрытые инструкции для LLM (только для преподавателей) + creator?: object // Данные создателя (только для преподавателей) + createdAt: string + updatedAt: string +} +``` + +**Важно:** Поля `hiddenInstructions` и `creator` доступны только пользователям с ролью `teacher` в Keycloak. При запросе заданий обычными студентами эти поля будут отфильтрованы на сервере. + +### Chain (Цепочка заданий) + +```typescript +interface ChallengeChain { + _id: string + id: string + name: string + tasks: ChallengeTask[] // Populated + createdAt: string + updatedAt: string +} +``` + +### Submission (Попытка) + +```typescript +type SubmissionStatus = 'pending' | 'in_progress' | 'accepted' | 'needs_revision' + +interface ChallengeSubmission { + _id: string + id: string + user: ChallengeUser | string // Populated или ID + task: ChallengeTask | string // Populated или ID + result: string // Результат пользователя + status: SubmissionStatus + queueId?: string + feedback?: string // Комментарий от LLM + submittedAt: string + checkedAt?: string + attemptNumber: number +} +``` + +### Queue Status (Статус проверки) + +```typescript +type QueueStatusType = 'waiting' | 'in_progress' | 'completed' | 'error' | 'not_found' + +interface QueueStatus { + status: QueueStatusType + submission?: ChallengeSubmission + error?: string + position?: number // Позиция в очереди (если waiting) +} +``` + +### User Stats (Статистика пользователя) + +```typescript +interface TaskStats { + taskId: string + taskTitle: string + attempts: Array<{ + attemptNumber: number + status: SubmissionStatus + submittedAt: string + checkedAt?: string + feedback?: string + }> + totalAttempts: number + status: 'not_attempted' | 'pending' | 'in_progress' | 'completed' | 'needs_revision' + lastAttemptAt: string | null +} + +interface ChainStats { + chainId: string + chainName: string + totalTasks: number + completedTasks: number + progress: number // 0-100 +} + +interface UserStats { + totalTasksAttempted: number + completedTasks: number + inProgressTasks: number + needsRevisionTasks: number + totalSubmissions: number + averageCheckTimeMs: number + taskStats: TaskStats[] + chainStats: ChainStats[] +} +``` + +### System Stats (Общая статистика) + +```typescript +interface SystemStats { + users: number + tasks: number + chains: number + submissions: { + total: number + accepted: number + rejected: number + pending: number + inProgress: number + } + averageCheckTimeMs: number + queue: { + queueLength: number + waiting: number + inProgress: number + maxConcurrency: number + currentlyProcessing: number + } +} +``` + +--- + +## API взаимодействие + +### Базовая настройка + +```typescript +const API_BASE_URL = 'http://localhost:8082/api/challenge' + +// Универсальная функция для запросов +async function apiRequest( + endpoint: string, + options: RequestInit = {} +): Promise<{ error: any; data: T }> { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }) + + return response.json() +} +``` + +### Примеры использования + +#### 1. Аутентификация + +```typescript +async function authenticateUser(nickname: string): Promise { + const { data, error } = await apiRequest<{ ok: boolean; userId: string }>( + '/auth', + { + method: 'POST', + body: JSON.stringify({ nickname }), + } + ) + + if (error) throw error + + // Сохраняем userId + localStorage.setItem('challengeUserId', data.userId) + + return data.userId +} +``` + +#### 2. Получение цепочек + +```typescript +async function getChains(): Promise { + const { data, error } = await apiRequest('/chains') + + if (error) throw error + + return data +} +``` + +#### 3. Отправка решения + +```typescript +async function submitSolution( + userId: string, + taskId: string, + result: string +): Promise<{ queueId: string; submissionId: string }> { + const { data, error } = await apiRequest<{ + queueId: string + submissionId: string + }>('/submit', { + method: 'POST', + body: JSON.stringify({ userId, taskId, result }), + }) + + if (error) throw error + + return data +} +``` + +#### 4. Polling проверки + +```typescript +async function pollCheckStatus( + queueId: string, + onUpdate: (status: QueueStatus) => void, + interval: number = 2000 +): Promise { + return new Promise((resolve, reject) => { + const checkStatus = async () => { + try { + const { data, error } = await apiRequest( + `/check-status/${queueId}` + ) + + if (error) { + reject(error) + return + } + + onUpdate(data) + + if (data.status === 'completed' && data.submission) { + resolve(data.submission) + } else if (data.status === 'error') { + reject(new Error(data.error || 'Check failed')) + } else { + // Продолжаем polling + setTimeout(checkStatus, interval) + } + } catch (err) { + reject(err) + } + } + + checkStatus() + }) +} +``` + +#### 5. Получение статистики пользователя + +```typescript +async function getUserStats(userId: string): Promise { + const { data, error } = await apiRequest(`/user/${userId}/stats`) + + if (error) throw error + + return data +} +``` + +--- + +## Компоненты UI + +### 1. AuthForm - Форма входа + +```typescript +// AuthForm.tsx +import { useState } from 'react' + +interface AuthFormProps { + onAuth: (userId: string, nickname: string) => void +} + +export function AuthForm({ onAuth }: AuthFormProps) { + const [nickname, setNickname] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + try { + const userId = await authenticateUser(nickname) + onAuth(userId, nickname) + } catch (err) { + setError('Ошибка входа. Попробуйте другой nickname.') + } finally { + setLoading(false) + } + } + + return ( +
+ setNickname(e.target.value)} + minLength={3} + maxLength={50} + required + /> + + {error &&
{error}
} +
+ ) +} +``` + +### 2. ChainList - Список цепочек + +```typescript +// ChainList.tsx +import { useState, useEffect } from 'react' + +interface ChainListProps { + userId: string + onSelectChain: (chain: ChallengeChain) => void +} + +export function ChainList({ userId, onSelectChain }: ChainListProps) { + const [chains, setChains] = useState([]) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + loadData() + }, [userId]) + + const loadData = async () => { + try { + const [chainsData, statsData] = await Promise.all([ + getChains(), + getUserStats(userId) + ]) + setChains(chainsData) + setStats(statsData) + } catch (err) { + console.error(err) + } finally { + setLoading(false) + } + } + + const getChainProgress = (chainId: string) => { + return stats?.chainStats.find(cs => cs.chainId === chainId) + } + + if (loading) return
Загрузка...
+ + return ( +
+ {chains.map(chain => { + const progress = getChainProgress(chain.id) + + return ( +
onSelectChain(chain)}> +

{chain.name}

+

{chain.tasks.length} заданий

+ + {progress && ( +
+
+ {progress.completedTasks} / {progress.totalTasks} +
+ )} +
+ ) + })} +
+ ) +} +``` + +### 3. TaskView - Просмотр и решение задания + +```typescript +// TaskView.tsx +import { useState } from 'react' +import ReactMarkdown from 'react-markdown' + +interface TaskViewProps { + task: ChallengeTask + userId: string + onComplete: () => void +} + +export function TaskView({ task, userId, onComplete }: TaskViewProps) { + const [result, setResult] = useState('') + const [submitting, setSubmitting] = useState(false) + + const handleSubmit = async () => { + setSubmitting(true) + + try { + const { queueId } = await submitSolution(userId, task.id, result) + // Переходим к экрану проверки + // (см. CheckStatusView) + } catch (err) { + alert('Ошибка отправки') + } finally { + setSubmitting(false) + } + } + + return ( +
+

{task.title}

+ +
+ {task.description} +
+ +
+

Ваше решение:

+