diff --git a/docs/CHALLENGE_ANALYTICS_SUMMARY.md b/docs/CHALLENGE_ANALYTICS_SUMMARY.md deleted file mode 100644 index d4058e4..0000000 --- a/docs/CHALLENGE_ANALYTICS_SUMMARY.md +++ /dev/null @@ -1,635 +0,0 @@ -# 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/docs/CHALLENGE_API_README.md b/docs/CHALLENGE_API_README.md new file mode 100644 index 0000000..1c68ecd --- /dev/null +++ b/docs/CHALLENGE_API_README.md @@ -0,0 +1,544 @@ +# Challenge Service API Documentation + +Сервис для проверки заданий в реальном времени через LLM (GigaChat). + +## Описание + +Система позволяет: +- Создавать задания с описанием в Markdown +- Объединять задания в цепочки +- Пользователям проходить задания и отправлять результаты +- Автоматически проверять результаты через GigaChat с ограничением потоков +- Отслеживать статистику по пользователям и системе + +## Конфигурация + +В `.env` файле добавьте: + +```env +CHALLENGE_LLM_THREADS=2 # Количество одновременных проверок через LLM +``` + +## API Endpoints + +Все endpoints начинаются с префикса `/api/challenge` + +### Аутентификация + +#### POST `/auth` + +Регистрация/авторизация пользователя по nickname. + +**Request:** +```json +{ + "nickname": "user123" +} +``` + +**Валидация:** +- `nickname`: обязательное поле, от 3 до 50 символов + +**Response:** +```json +{ + "error": null, + "data": { + "ok": true, + "userId": "507f1f77bcf86cd799439011" + } +} +``` + +### Управление заданиями + +**Важно:** Все операции создания/обновления/удаления заданий требуют авторизации через Keycloak с ролью `teacher` или `challenge-author`. + +#### POST `/task` + +Создание нового задания (требует роль `teacher` или `challenge-author`). + +**Headers:** +``` +Authorization: Bearer +``` + +**Request:** +```json +{ + "title": "Написать функцию сортировки", + "description": "# Задание\n\nНапишите функцию сортировки массива чисел...", + "hiddenInstructions": "Проверь сложность алгоритма. Должна быть O(n log n), не принимай bubble sort" +} +``` + +**Валидация:** +- `title`: обязательное поле, максимум 255 символов +- `description`: обязательное поле +- `hiddenInstructions`: опциональное поле + +**Response:** +```json +{ + "error": null, + "data": { + "_id": "507f1f77bcf86cd799439011", + "title": "Написать функцию сортировки", + "description": "# Задание\n\n...", + "hiddenInstructions": "Проверь сложность алгоритма...", + "creator": { "sub": "...", "preferred_username": "teacher1" }, + "createdAt": "2023-10-29T12:00:00.000Z", + "updatedAt": "2023-10-29T12:00:00.000Z" + } +} +``` + +**Примечание:** Поле `hiddenInstructions` опционально. Эти инструкции будут переданы в LLM при проверке, но скрыты от студентов. + +#### GET `/task/:taskId` + +Получение задания по ID. + +**Важно:** Поля `hiddenInstructions` и `creator` возвращаются только для пользователей с ролью `teacher` или `challenge-author`. Обычные студенты их не увидят. + +#### GET `/tasks` + +Получение всех заданий. + +**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты. + +#### PUT `/task/:taskId` + +Обновление задания (требует роль `teacher` или `challenge-author`). + +**Headers:** +``` +Authorization: Bearer +``` + +**Request:** +```json +{ + "title": "Новый заголовок", + "description": "Новое описание", + "hiddenInstructions": "Обновленные инструкции для LLM" +} +``` + +**Валидация:** +- `title`: опциональное поле, максимум 255 символов +- `description`: опциональное поле +- `hiddenInstructions`: опциональное поле + +#### DELETE `/task/:taskId` + +Удаление задания (требует роль `teacher` или `challenge-author`). Также удаляется из всех цепочек. + +**Headers:** +``` +Authorization: Bearer +``` + +### Управление цепочками заданий + +**Важно:** Все операции создания/обновления/удаления цепочек требуют авторизации через Keycloak с ролью `teacher` или `challenge-author`. + +#### POST `/chain` + +Создание цепочки заданий (требует роль `teacher` или `challenge-author`). + +**Request:** +```json +{ + "name": "Основы программирования", + "taskIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"] +} +``` + +**Валидация:** +- `name`: обязательное поле, максимум 255 символов +- `taskIds`: обязательное поле, массив строк + +#### GET `/chains` + +Получение всех цепочек с заданиями. + +**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты в заданиях внутри цепочек. + +#### GET `/chain/:chainId` + +Получение цепочки по ID. + +**Примечание:** Для не-преподавателей поля `hiddenInstructions` и `creator` скрыты в заданиях внутри цепочки. + +#### PUT `/chain/:chainId` + +Обновление цепочки (требует роль `teacher` или `challenge-author`). + +**Headers:** +``` +Authorization: Bearer +``` + +**Request:** +```json +{ + "name": "Новое название цепочки", + "taskIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"] +} +``` + +**Валидация:** +- `name`: опциональное поле, максимум 255 символов +- `taskIds`: опциональное поле, массив строк + +#### DELETE `/chain/:chainId` + +Удаление цепочки (требует роль `teacher` или `challenge-author`). + +**Headers:** +``` +Authorization: Bearer +``` + +### Отправка и проверка заданий + +#### POST `/submit` + +Отправка результата выполнения задания на проверку. + +**Request:** +```json +{ + "userId": "507f1f77bcf86cd799439011", + "taskId": "507f1f77bcf86cd799439012", + "result": "function sort(arr) { return arr.sort((a, b) => a - b); }" +} +``` + +**Response:** +```json +{ + "error": null, + "data": { + "queueId": "550e8400-e29b-41d4-a716-446655440000", + "submissionId": "507f1f77bcf86cd799439013" + } +} +``` + +**Валидация:** +- `userId`: обязательное поле +- `taskId`: обязательное поле +- `result`: обязательное поле + +#### GET `/check-status/:queueId` + +Polling endpoint для проверки статуса проверки. + +**Response (в процессе):** +```json +{ + "error": null, + "data": { + "status": "waiting", + "position": 3 + } +} +``` + +**Response (завершено):** +```json +{ + "error": null, + "data": { + "status": "completed", + "submission": { + "_id": "507f1f77bcf86cd799439013", + "user": {...}, + "task": {...}, + "result": "...", + "status": "accepted", + "feedback": "Отлично! Задание выполнено правильно.", + "attemptNumber": 1, + "submittedAt": "2023-10-29T12:00:00.000Z", + "checkedAt": "2023-10-29T12:00:05.000Z" + } + } +} +``` + +Статусы проверки: +- `waiting` - ожидает в очереди +- `in_progress` - проверяется +- `completed` - проверка завершена +- `error` - ошибка при проверке +- `not_found` - не найдено + +Статусы submission: +- `pending` - ожидает проверки +- `in_progress` - проверяется +- `accepted` - принято +- `needs_revision` - требует доработки + +### Просмотр попыток + +#### GET `/user/:userId/submissions?taskId=...` + +Получение всех попыток пользователя (опционально для конкретного задания). + +**Response:** +```json +{ + "error": null, + "data": [ + { + "_id": "507f1f77bcf86cd799439013", + "task": {...}, + "result": "...", + "status": "accepted", + "feedback": "...", + "attemptNumber": 2, + "submittedAt": "2023-10-29T12:00:00.000Z", + "checkedAt": "2023-10-29T12:00:05.000Z" + } + ] +} +``` + +### Статистика + +#### GET `/user/:userId/stats` + +Детальная статистика пользователя. + +**Response:** +```json +{ + "error": null, + "data": { + "totalTasksAttempted": 5, + "completedTasks": 3, + "inProgressTasks": 1, + "needsRevisionTasks": 1, + "totalSubmissions": 8, + "averageCheckTimeMs": 5234, + "taskStats": [ + { + "taskId": "507f1f77bcf86cd799439012", + "taskTitle": "Написать функцию сортировки", + "attempts": [...], + "totalAttempts": 2, + "status": "completed", + "lastAttemptAt": "2023-10-29T12:00:00.000Z" + } + ], + "chainStats": [ + { + "chainId": "507f1f77bcf86cd799439014", + "chainName": "Основы программирования", + "totalTasks": 5, + "completedTasks": 3, + "progress": 60 + } + ] + } +} +``` + +**Примечание:** Статусы заданий в `taskStats.status`: +- `not_attempted` - задание не начато +- `pending` - ожидает проверки +- `in_progress` - проверяется +- `needs_revision` - требует доработки +- `completed` - выполнено + +#### GET `/stats` + +Общая статистика системы. + +**Response:** +```json +{ + "error": null, + "data": { + "users": 150, + "tasks": 25, + "chains": 5, + "submissions": { + "total": 850, + "accepted": 420, + "rejected": 380, + "pending": 30, + "inProgress": 20 + }, + "averageCheckTimeMs": 5500, + "queue": { + "queueLength": 12, + "waiting": 10, + "inProgress": 2, + "maxConcurrency": 2, + "currentlyProcessing": 2 + } + } +} +``` + +#### GET `/stats/v2` + +Расширенная статистика системы с детальными данными для таблиц и прогресс-баров. + +**Query параметры:** +- `chainId` (опционально): фильтрация статистики по конкретной цепочке + +**Response:** +```json +{ + "error": null, + "data": { + "users": 150, + "tasks": 25, + "chains": 5, + "submissions": { + "total": 850, + "accepted": 420, + "rejected": 380, + "pending": 30, + "inProgress": 20 + }, + "averageCheckTimeMs": 5500, + "queue": { + "queueLength": 12, + "waiting": 10, + "inProgress": 2, + "maxConcurrency": 2, + "currentlyProcessing": 2 + }, + "tasksTable": [ + { + "taskId": "507f1f77bcf86cd799439012", + "title": "Написать функцию сортировки", + "totalAttempts": 45, + "uniqueUsers": 20, + "acceptedCount": 18, + "successRate": 90, + "averageAttemptsToSuccess": 1.5 + } + ], + "activeParticipants": [ + { + "userId": "507f1f77bcf86cd799439011", + "nickname": "user123", + "totalSubmissions": 10, + "completedTasks": 5, + "chainProgress": [ + { + "chainId": "507f1f77bcf86cd799439014", + "chainName": "Основы программирования", + "totalTasks": 10, + "completedTasks": 5, + "progressPercent": 50 + } + ] + } + ], + "chainsDetailed": [ + { + "chainId": "507f1f77bcf86cd799439014", + "name": "Основы программирования", + "totalTasks": 10, + "tasks": [ + { + "taskId": "507f1f77bcf86cd799439012", + "title": "Написать функцию сортировки", + "description": "# Задание\n\n..." + } + ], + "participantProgress": [ + { + "userId": "507f1f77bcf86cd799439011", + "nickname": "user123", + "taskProgress": [ + { + "taskId": "507f1f77bcf86cd799439012", + "taskTitle": "Написать функцию сортировки", + "status": "completed" + } + ], + "completedCount": 5, + "progressPercent": 50 + } + ] + } + ] + } +} +``` + +**Примечание:** Статусы задач в `taskProgress`: +- `not_started` - задание не начато +- `pending` - ожидает проверки +- `in_progress` - проверяется +- `needs_revision` - требует доработки +- `completed` - выполнено + +## Архитектура + +### Модели данных + +- **ChallengeUser** - пользователи сервиса +- **ChallengeTask** - отдельные задания +- **ChallengeChain** - цепочки заданий +- **ChallengeSubmission** - результаты выполнения заданий + +### Сервисы + +- **challenge-checker.ts** - проверка заданий через GigaChat +- **ChallengeCheckQueue.ts** - управление очередью проверок +- **challengeQueueInstance.ts** - singleton экземпляр очереди + +### Очередь проверки + +Очередь работает in-memory с ограничением на количество одновременных проверок. + +- Элементы добавляются в очередь при отправке задания +- Обработка происходит автоматически каждую секунду +- Количество параллельных проверок ограничено `CHALLENGE_LLM_THREADS` +- После завершения проверки результаты сохраняются в БД +- Записи в очереди удаляются через 5 минут после завершения + +### Проверка через LLM + +Для проверки используется GigaChat с промптом, который: +- Получает описание задания +- Получает результат пользователя +- Возвращает статус (ПРИНЯТО/ДОРАБОТКА) и feedback + +Ответ парсится и сохраняется в submission. + +## Примеры использования + +### Типичный flow пользователя + +1. Авторизация: `POST /api/challenge/auth` +2. Получение цепочек: `GET /api/challenge/chains` +3. Получение заданий цепочки: `GET /api/challenge/chain/:chainId` +4. Отправка результата: `POST /api/challenge/submit` +5. Polling проверки: `GET /api/challenge/check-status/:queueId` (каждые 2-3 секунды) +6. Просмотр статистики: `GET /api/challenge/user/:userId/stats` + +### Типичный flow администратора + +1. Создание заданий: `POST /api/challenge/task` +2. Создание цепочки: `POST /api/challenge/chain` +3. Просмотр общей статистики: `GET /api/challenge/stats` +4. Просмотр расширенной статистики: `GET /api/challenge/stats/v2` (опционально с `?chainId=...`) + +## Ограничения и особенности + +- Очередь работает in-memory, при перезапуске сервера незавершенные проверки вернутся в статус `pending` +- История всех попыток сохраняется полностью +- Markdown описания заданий хранятся как простой текст в БД +- Аутентификация простая, без паролей (только nickname) +- Nickname должен быть уникальным в системе + diff --git a/docs/CHALLENGE_FRONTEND_GUIDE.md b/docs/CHALLENGE_FRONTEND_GUIDE.md deleted file mode 100644 index 22d8bfb..0000000 --- a/docs/CHALLENGE_FRONTEND_GUIDE.md +++ /dev/null @@ -1,1125 +0,0 @@ -# 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} -
- -
-

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

-