fix api calls

This commit is contained in:
2025-12-09 12:25:29 +03:00
parent fd55d5a214
commit 2a08d9df35
10 changed files with 674 additions and 4196 deletions

View File

@@ -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<PersonalDashboard> {
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<any> {
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 (
<div className="progress-chart">
<svg viewBox="0 0 100 100">
{/* Реализация круговой диаграммы */}
</svg>
<div className="legend">
<div> Завершено: {data.completed}</div>
<div>🔄 В процессе: {data.inProgress}</div>
<div> Доработка: {data.needsRevision}</div>
<div> Не начато: {data.notStarted}</div>
</div>
</div>
)
}
```
### 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 (
<div className="timeline-chart">
{/* Реализация bar chart */}
</div>
)
}
```
### 3. Heatmap (активность по дням)
```typescript
interface HeatmapData {
dates: Array<{
date: string // YYYY-MM-DD
submissions: number
successRate: number
}>
}
// Визуализация активности пользователя
function ActivityHeatmap({ data }: { data: HeatmapData }) {
return (
<div className="activity-heatmap">
{data.dates.map(day => (
<div
key={day.date}
className="heatmap-cell"
style={{
opacity: day.submissions / 10, // Интенсивность цвета
backgroundColor: day.successRate > 50 ? 'green' : 'red'
}}
title={`${day.date}: ${day.submissions} попыток, ${day.successRate}% успех`}
/>
))}
</div>
)
}
```
## 🔔 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<ChallengeEventType, Array<(event: ChallengeEvent) => 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 (
<div className="stat-card">
<div className="stat-header">
<span className="stat-icon">{icon}</span>
<span className="stat-title">{title}</span>
</div>
<div className="stat-value">{value}</div>
{change && (
<div className={`stat-change ${trend}`}>
{trend === 'up' ? '↑' : '↓'} {Math.abs(change)}%
</div>
)}
</div>
)
}
// Использование
<StatCard
title="Задания завершено"
value={42}
change={15}
trend="up"
icon="✅"
/>
```
## 🔍 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<string> {
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 интерфейса!

View File

@@ -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 <keycloak_token>
```
**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 <keycloak_token>
```
**Request:**
```json
{
"title": "Новый заголовок",
"description": "Новое описание",
"hiddenInstructions": "Обновленные инструкции для LLM"
}
```
**Валидация:**
- `title`: опциональное поле, максимум 255 символов
- `description`: опциональное поле
- `hiddenInstructions`: опциональное поле
#### DELETE `/task/:taskId`
Удаление задания (требует роль `teacher` или `challenge-author`). Также удаляется из всех цепочек.
**Headers:**
```
Authorization: Bearer <keycloak_token>
```
### Управление цепочками заданий
**Важно:** Все операции создания/обновления/удаления цепочек требуют авторизации через 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 <keycloak_token>
```
**Request:**
```json
{
"name": "Новое название цепочки",
"taskIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
}
```
**Валидация:**
- `name`: опциональное поле, максимум 255 символов
- `taskIds`: опциональное поле, массив строк
#### DELETE `/chain/:chainId`
Удаление цепочки (требует роль `teacher` или `challenge-author`).
**Headers:**
```
Authorization: Bearer <keycloak_token>
```
### Отправка и проверка заданий
#### 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 должен быть уникальным в системе

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,178 +0,0 @@
# Challenge Service - Frontend Documentation
Документация для фронтенд-разработчиков и преподавателей.
## 📚 Документы
### Для всех фронтенд-разработчиков
1. **[CHALLENGE_FRONTEND_GUIDE.md](./CHALLENGE_FRONTEND_GUIDE.md)**
- Основное руководство по интеграции
- Сценарии использования
- Структуры данных (TypeScript)
- API взаимодействие
- Готовые компоненты React
- Best practices
2. **[CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md)**
- Полный пример React + TypeScript приложения
- Готовый код компонентов
- Custom hooks
- State management
- Стили
3. **[CHALLENGE_ANALYTICS_SUMMARY.md](./CHALLENGE_ANALYTICS_SUMMARY.md)**
- Метрики для отслеживания
- Дашборды (Personal, Admin)
- Визуализация данных
- Real-time уведомления
- A/B тестирование
### Для преподавателей 🔒
4. **[TEACHER_GUIDE.md](./TEACHER_GUIDE.md)**
- Работа со скрытыми инструкциями для LLM
- Настройка Keycloak
- Создание и редактирование заданий
- UI компоненты для преподавателей
- Best practices
- Примеры реальных сценариев
## 🚀 Быстрый старт
### Для студентов (обычные пользователи)
1. Прочитайте [CHALLENGE_FRONTEND_GUIDE.md](./CHALLENGE_FRONTEND_GUIDE.md)
2. Посмотрите примеры в [CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md)
3. Используйте готовые компоненты как основу
### Для преподавателей
1. Прочитайте [TEACHER_GUIDE.md](./TEACHER_GUIDE.md) 🔒
2. Настройте Keycloak (роль `teacher`)
3. Используйте скрытые инструкции для улучшения проверки
## 🎯 Ключевые особенности
### Для студентов
- ✅ Простая аутентификация (nickname)
- ✅ Просмотр цепочек с прогрессом
- ✅ Отправка решений
- ✅ Real-time отслеживание проверки
- ✅ Персональная статистика
- ✅ Feedback от AI
### Для преподавателей 🔒
- ✅ Создание заданий через Keycloak
- ✅ Скрытые инструкции для LLM
- ✅ Управление цепочками
- ✅ Просмотр статистики
- ✅ Контроль качества проверок
## 📊 Структура данных
### Task (с учетом ролей)
```typescript
// Для студента
interface ChallengeTask {
_id: string
title: string
description: string // Markdown
createdAt: string
}
// Для преподавателя (teacher)
interface ChallengeTask {
_id: string
title: string
description: string // Markdown
hiddenInstructions: string // 🔒 Только для преподавателей
creator: object // 🔒 Только для преподавателей
createdAt: string
}
```
## 🔐 Авторизация
### Студенты
```typescript
// Простая регистрация по nickname
POST /api/challenge/auth
{ "nickname": "student123" }
```
### Преподаватели
```typescript
// Запросы с токеном Keycloak
headers: {
'Authorization': 'Bearer <keycloak_token>'
}
// Требуется роль 'teacher' в клиенте 'journal'
```
## 🎨 UI Components
### Для студентов
- `AuthForm` - вход по nickname
- `ChainList` - список цепочек
- `TaskView` - просмотр и решение
- `CheckStatus` - отслеживание проверки
- `ResultView` - результат
- `UserStats` - статистика
### Для преподавателей 🔒
- `TeacherTaskForm` - создание с hiddenInstructions
- `TaskCard` - с индикацией скрытых инструкций
- `AdminDashboard` - полная статистика
## 📖 Примеры использования
### Создание задания (преподаватель)
```typescript
const task = await createTask({
title: "Реализовать сортировку",
description: "# Задание\n\nНапишите функцию...",
hiddenInstructions: "Проверь сложность алгоритма O(n log n)" // 🔒
})
```
### Отправка решения (студент)
```typescript
const { queueId } = await submitSolution(userId, taskId, result)
// Polling
const submission = await pollCheckStatus(queueId, (status) => {
console.log('Status:', status.status)
})
```
## 🛠️ Технологии
- React + TypeScript
- Keycloak для авторизации преподавателей
- Context API / Redux для state
- React Markdown
- Fetch API
## 📚 Дополнительные ресурсы
- [API документация](../CHALLENGE_API_README.md)
- [Архитектура системы](../CHALLENGE_ARCHITECTURE.md)
- [Быстрый старт](../CHALLENGE_QUICK_START.md)
---
**Версия:** 1.0.0
**Дата:** 29 октября 2025
**Статус:** ✅ Production Ready

View File

@@ -1,439 +0,0 @@
# Challenge Service - Руководство для преподавателей
Специальное руководство для пользователей с ролью `teacher` в Keycloak.
## Требования
Для создания и редактирования заданий и цепочек необходимо:
1. Быть авторизованным через Keycloak
2. Иметь роль `teacher` в клиенте `journal`
## Особенности для преподавателей
### 1. Скрытые инструкции для LLM
При создании заданий вы можете добавить **скрытые инструкции** (`hiddenInstructions`), которые:
- ✅ Видны только преподавателям
- ✅ Передаются в LLM при проверке
-Не видны студентам
-Не отображаются в интерфейсе студента
#### Примеры использования
**Пример 1: Контроль сложности**
```json
{
"title": "Реализовать сортировку",
"description": "Напишите функцию для сортировки массива чисел",
"hiddenInstructions": "Проверь, чтобы сложность алгоритма была не хуже O(n log n). Не принимай bubble sort или простые O(n²) решения."
}
```
**Пример 2: Специфичные требования**
```json
{
"title": "REST API endpoint",
"description": "Создайте endpoint для получения списка пользователей",
"hiddenInstructions": "Обязательно должна быть пагинация, обработка ошибок и валидация параметров. Если чего-то не хватает - укажи в feedback."
}
```
**Пример 3: Стиль кода**
```json
{
"title": "Компонент React",
"description": "Создайте компонент для отображения карточки товара",
"hiddenInstructions": "Проверь использование TypeScript, правильное применение хуков, и соблюдение best practices React. Код должен быть чистым и читаемым."
}
```
**Пример 4: Тонкая настройка проверки**
```json
{
"title": "SQL запрос",
"description": "Напишите запрос для выборки активных пользователей",
"hiddenInstructions": "Даже если запрос работает, но неоптимален (например, использует SELECT *), укажи на это в feedback и попроси оптимизировать."
}
```
### 2. Создание задания через API
#### С помощью Keycloak токена
```typescript
// Получение токена (пример для frontend)
const keycloakToken = keycloak.token // из keycloak-js
// Создание задания
async function createTask(title: string, description: string, hiddenInstructions: string) {
const response = await fetch('http://localhost:8082/api/challenge/task', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${keycloakToken}`
},
body: JSON.stringify({
title,
description,
hiddenInstructions
})
})
return response.json()
}
```
#### С помощью curl
```bash
# Получить токен от Keycloak
TOKEN="your_keycloak_token"
# Создать задание
curl -X POST http://localhost:8082/api/challenge/task \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"title": "Написать функцию",
"description": "# Задание\n\nНапишите функцию для...",
"hiddenInstructions": "Проверь производительность и обработку ошибок"
}'
```
### 3. UI компоненты для преподавателей
#### TaskForm с скрытыми инструкциями
```typescript
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
interface TaskFormProps {
onSubmit: (task: { title: string; description: string; hiddenInstructions: string }) => void
}
export function TeacherTaskForm({ onSubmit }: TaskFormProps) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [hiddenInstructions, setHiddenInstructions] = useState('')
const [showPreview, setShowPreview] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit({ title, description, hiddenInstructions })
}
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Заголовок задания</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
maxLength={255}
/>
</div>
<div className="form-group">
<label>Описание (Markdown)</label>
<div className="tabs">
<button type="button" onClick={() => setShowPreview(false)}>Редактор</button>
<button type="button" onClick={() => setShowPreview(true)}>Превью</button>
</div>
{showPreview ? (
<div className="markdown-preview">
<ReactMarkdown>{description}</ReactMarkdown>
</div>
) : (
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
required
rows={15}
placeholder="# Заголовок\n\nОписание задания..."
/>
)}
</div>
<div className="form-group highlight">
<label>
🔒 Скрытые инструкции для LLM
<span className="info-tooltip">
Эти инструкции увидит только LLM при проверке.
Студенты их не увидят.
</span>
</label>
<textarea
value={hiddenInstructions}
onChange={(e) => setHiddenInstructions(e.target.value)}
rows={5}
placeholder="Дополнительные требования к проверке..."
/>
</div>
<button type="submit">Создать задание</button>
</form>
)
}
```
#### TaskCard с индикацией скрытых инструкций
```typescript
interface TaskCardProps {
task: ChallengeTask
isTeacher: boolean
}
export function TaskCard({ task, isTeacher }: TaskCardProps) {
return (
<div className="task-card">
<h3>{task.title}</h3>
<div className="task-description">
<ReactMarkdown>{task.description}</ReactMarkdown>
</div>
{isTeacher && task.hiddenInstructions && (
<div className="hidden-instructions-indicator">
<span className="lock-icon">🔒</span>
<span>Содержит скрытые инструкции для LLM</span>
</div>
)}
{isTeacher && task.creator && (
<div className="task-meta">
<span>Создал: {task.creator.preferred_username}</span>
</div>
)}
</div>
)
}
```
### 4. Настройка Keycloak
#### Добавление роли teacher
1. Войдите в админ панель Keycloak
2. Выберите realm (например, `bro-js` или `itpark`)
3. Перейдите в **Clients**`journal`
4. Перейдите на вкладку **Roles**
5. Добавьте роль `teacher`, если её нет
6. Назначьте роль нужным пользователям через **Users** → [пользователь] → **Role Mappings**
### 5. Best Practices
#### ✅ Хорошие скрытые инструкции
```
"Проверь, что функция обрабатывает edge cases: пустой массив,
один элемент, отрицательные числа. Если что-то упущено - укажи."
```
```
"Код должен следовать принципу DRY. Если есть дублирование -
отправь на доработку с рекомендацией."
```
```
"Обязательна обработка ошибок. Если try-catch отсутствует или
неполный - укажи в feedback."
```
#### ❌ Плохие скрытые инструкции
```
"Проверь" // Слишком общее
```
```
"Это задание должно быть правильным" // Бессмысленное
```
```
"Не принимай, если не идеально" // Слишком строгое, непонятное
```
### 6. Просмотр скрытых инструкций
Скрытые инструкции доступны только при запросе с токеном `teacher`:
```typescript
// Получить задание (с токеном teacher)
async function getTaskAsTeacher(taskId: string) {
const response = await fetch(`http://localhost:8082/api/challenge/task/${taskId}`, {
headers: {
'Authorization': `Bearer ${keycloakToken}`
}
})
const { data } = await response.json()
// data.hiddenInstructions будет доступно
console.log('Hidden instructions:', data.hiddenInstructions)
}
// Получить задание (без токена или с обычным пользователем)
async function getTaskAsStudent(taskId: string) {
const response = await fetch(`http://localhost:8082/api/challenge/task/${taskId}`)
const { data } = await response.json()
// data.hiddenInstructions будет undefined
console.log('Hidden instructions:', data.hiddenInstructions) // undefined
}
```
### 7. Редактирование существующих заданий
```typescript
async function updateTask(
taskId: string,
updates: {
title?: string
description?: string
hiddenInstructions?: string
}
) {
const response = await fetch(`http://localhost:8082/api/challenge/task/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${keycloakToken}`
},
body: JSON.stringify(updates)
})
return response.json()
}
// Пример использования
updateTask('507f1f77bcf86cd799439011', {
hiddenInstructions: 'Обновленные требования к проверке'
})
```
### 8. Мониторинг эффективности инструкций
Отслеживайте, как скрытые инструкции влияют на результаты:
```typescript
interface InstructionEffectiveness {
taskId: string
taskTitle: string
hasHiddenInstructions: boolean
acceptanceRate: number // % принятых с первой попытки
averageFeedbackQuality: number // оценка качества feedback
}
// Анализ эффективности
async function analyzeInstructionsEffectiveness() {
const tasks = await fetchAllTasks()
const stats = await fetchSystemStats()
return tasks.map(task => ({
taskId: task.id,
taskTitle: task.title,
hasHiddenInstructions: !!task.hiddenInstructions,
acceptanceRate: calculateAcceptanceRate(task.id, stats),
averageFeedbackQuality: calculateFeedbackQuality(task.id, stats)
}))
}
```
### 9. Шаблоны скрытых инструкций
#### Для программирования
```
Проверь:
1. Корректность алгоритма
2. Обработку edge cases
3. Сложность алгоритма (должна быть оптимальной)
4. Читаемость кода
5. Наличие комментариев в сложных местах
```
#### Для веб-разработки
```
Проверь:
1. Соответствие HTML семантике
2. Доступность (accessibility)
3. Responsive design
4. Производительность
5. Best practices для используемого фреймворка
```
#### Для баз данных
```
Проверь:
1. Правильность SQL синтаксиса
2. Оптимальность запроса
3. Использование индексов
4. Защиту от SQL injection
5. Читаемость запроса
```
### 10. FAQ
**Q: Что если я не добавлю скрытые инструкции?**
A: Задание будет работать нормально. LLM проверит решение на основе только видимого описания.
**Q: Могут ли студенты как-то увидеть скрытые инструкции?**
A: Нет, сервер автоматически фильтрует это поле при запросах без роли teacher.
**Q: Можно ли изменить скрытые инструкции после создания?**
A: Да, используйте PUT /api/challenge/task/:taskId с новым значением hiddenInstructions.
**Q: Влияют ли скрытые инструкции на все проверки?**
A: Да, каждая проверка использует актуальные hiddenInstructions из задания.
**Q: Можно ли использовать Markdown в скрытых инструкциях?**
A: Можно, но это обычный текст. Markdown не рендерится, так как инструкции идут прямо в LLM.
---
## Примеры реальных сценариев
### Сценарий 1: Курс по алгоритмам
```json
{
"title": "Реализовать бинарный поиск",
"description": "Напишите функцию binarySearch(arr, target), которая ищет элемент в отсортированном массиве",
"hiddenInstructions": "Проверь сложность - должна быть O(log n). Если используется линейный поиск или неоптимальный алгоритм - отклони с объяснением. Также проверь обработку случаев, когда элемент не найден."
}
```
### Сценарий 2: Курс по React
```json
{
"title": "Форма регистрации",
"description": "Создайте компонент формы регистрации с полями email и пароль",
"hiddenInstructions": "Обязательна валидация на стороне клиента, использование controlled components, и правильное управление state. Если используются uncontrolled components или нет валидации - отправь на доработку."
}
```
### Сценарий 3: Курс по безопасности
```json
{
"title": "Безопасный API endpoint",
"description": "Создайте endpoint для аутентификации пользователя",
"hiddenInstructions": "Критически важно: пароли должны хешироваться, должна быть защита от SQL injection, rate limiting. Если что-то из этого отсутствует - обязательно отклони и подробно объясни риски безопасности."
}
```
---
Используйте скрытые инструкции разумно для повышения качества автоматической проверки! 🎓

View File

@@ -1,693 +0,0 @@
# API Статистики v2 - Документация для Frontend
## Обзор
Эндпоинт `/challenge/stats/v2` предоставляет расширенную статистику системы проверки заданий с детальными данными для построения таблиц и прогресс-баров.
## Эндпоинт
```
GET /challenge/stats/v2
```
### Аутентификация
Не требуется.
### Параметры запроса
| Параметр | Тип | Обязательный | Описание |
|----------|-----|--------------|----------|
| `chainId` | string | Нет | ID цепочки для фильтрации статистики. Если указан, возвращается статистика только по заданиям из этой цепочки |
#### Примеры использования
Получить полную статистику:
```
GET /challenge/stats/v2
```
Получить статистику только по одной цепочке:
```
GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439031
```
## Структура ответа
```typescript
{
success: true,
body: {
// ========== БАЗОВАЯ СТАТИСТИКА (из v1) ==========
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
},
// ========== НОВЫЕ ДАННЫЕ V2 ==========
// Таблица заданий с детальной статистикой
tasksTable: Array<{
taskId: string,
title: string,
totalAttempts: number, // Всего попыток по всем пользователям
uniqueUsers: number, // Количество уникальных пользователей
acceptedCount: number, // Количество успешных прохождений
successRate: number, // Процент успешного прохождения (0-100)
averageAttemptsToSuccess: number // Среднее количество попыток до успеха
}>,
// Активные участники с прогрессом
activeParticipants: Array<{
userId: string,
nickname: string,
totalSubmissions: number, // Всего попыток пользователя
completedTasks: number, // Завершенных заданий
chainProgress: Array<{ // Прогресс по каждой цепочке
chainId: string,
chainName: string,
totalTasks: number, // Всего заданий в цепочке
completedTasks: number, // Завершено заданий
progressPercent: number // Процент прохождения (0-100)
}>
}>,
// Детальная информация по цепочкам
chainsDetailed: Array<{
chainId: string,
name: string,
totalTasks: number,
tasks: Array<{ // Список заданий в цепочке
taskId: string,
title: string,
description: string
}>,
participantProgress: Array<{ // Прогресс каждого участника
userId: string,
nickname: string,
taskProgress: Array<{ // Статус по каждому заданию
taskId: string,
taskTitle: string,
status: 'not_started' | 'pending' | 'in_progress' | 'needs_revision' | 'completed'
}>,
completedCount: number, // Завершено заданий
progressPercent: number // Процент прохождения (0-100)
}>
}>
}
}
```
## Пример ответа
```json
{
"success": true,
"body": {
"users": 34,
"tasks": 22,
"chains": 2,
"submissions": {
"total": 39,
"accepted": 16,
"rejected": 23,
"pending": 0,
"inProgress": 0
},
"averageCheckTimeMs": 2043,
"queue": {
"queueLength": 0,
"waiting": 0,
"inProgress": 0,
"maxConcurrency": 1,
"currentlyProcessing": 0
},
"tasksTable": [
{
"taskId": "507f1f77bcf86cd799439011",
"title": "Создание REST API",
"totalAttempts": 45,
"uniqueUsers": 12,
"acceptedCount": 8,
"successRate": 67,
"averageAttemptsToSuccess": 2.3
},
{
"taskId": "507f1f77bcf86cd799439012",
"title": "Работа с базой данных",
"totalAttempts": 38,
"uniqueUsers": 10,
"acceptedCount": 6,
"successRate": 60,
"averageAttemptsToSuccess": 3.1
}
],
"activeParticipants": [
{
"userId": "507f1f77bcf86cd799439021",
"nickname": "student1",
"totalSubmissions": 15,
"completedTasks": 5,
"chainProgress": [
{
"chainId": "507f1f77bcf86cd799439031",
"chainName": "Основы Backend",
"totalTasks": 10,
"completedTasks": 5,
"progressPercent": 50
},
{
"chainId": "507f1f77bcf86cd799439032",
"chainName": "Frontend разработка",
"totalTasks": 8,
"completedTasks": 0,
"progressPercent": 0
}
]
}
],
"chainsDetailed": [
{
"chainId": "507f1f77bcf86cd799439031",
"name": "Основы Backend",
"totalTasks": 10,
"tasks": [
{
"taskId": "507f1f77bcf86cd799439011",
"title": "Создание REST API",
"description": "Создайте простой REST API с использованием Express.js"
},
{
"taskId": "507f1f77bcf86cd799439012",
"title": "Работа с базой данных",
"description": "Интегрируйте MongoDB в ваше приложение"
}
],
"participantProgress": [
{
"userId": "507f1f77bcf86cd799439021",
"nickname": "student1",
"taskProgress": [
{
"taskId": "507f1f77bcf86cd799439011",
"taskTitle": "Создание REST API",
"status": "completed"
},
{
"taskId": "507f1f77bcf86cd799439012",
"taskTitle": "Работа с базой данных",
"status": "needs_revision"
}
],
"completedCount": 1,
"progressPercent": 10
}
]
}
]
}
}
```
## Фильтрация по цепочке
При передаче параметра `chainId`:
1. **tasksTable** - содержит только задания из указанной цепочки
2. **activeParticipants** - включает только участников, которые делали попытки по заданиям этой цепочки. В `chainProgress` будет информация только об указанной цепочке
3. **chainsDetailed** - содержит информацию только об указанной цепочке
### Пример фильтрованного ответа
```bash
curl http://localhost:3000/challenge/stats/v2?chainId=507f1f77bcf86cd799439031
```
```json
{
"success": true,
"body": {
"users": 34,
"tasks": 22,
"chains": 2,
"submissions": {
"total": 39,
"accepted": 16,
"rejected": 23,
"pending": 0,
"inProgress": 0
},
"averageCheckTimeMs": 2043,
"queue": { ... },
"tasksTable": [
// Только задания из цепочки 507f1f77bcf86cd799439031
{
"taskId": "507f1f77bcf86cd799439011",
"title": "Создание REST API",
"totalAttempts": 45,
"uniqueUsers": 12,
"acceptedCount": 8,
"successRate": 67,
"averageAttemptsToSuccess": 2.3
}
],
"activeParticipants": [
// Только участники с попытками в этой цепочке
{
"userId": "507f1f77bcf86cd799439021",
"nickname": "student1",
"totalSubmissions": 10,
"completedTasks": 3,
"chainProgress": [
// Только одна цепочка
{
"chainId": "507f1f77bcf86cd799439031",
"chainName": "Основы Backend",
"totalTasks": 10,
"completedTasks": 3,
"progressPercent": 30
}
]
}
],
"chainsDetailed": [
// Только указанная цепочка
{
"chainId": "507f1f77bcf86cd799439031",
"name": "Основы Backend",
"totalTasks": 10,
"tasks": [...],
"participantProgress": [...]
}
]
}
}
```
## Использование в UI компонентах
### 1. Таблица заданий
Используйте `tasksTable` для отображения статистики по каждому заданию:
```tsx
// React пример
function TasksTable({ tasksTable }) {
return (
<table>
<thead>
<tr>
<th>Название задания</th>
<th>Попыток</th>
<th>Уникальных пользователей</th>
<th>Успешно завершено</th>
<th>% успеха</th>
<th>Среднее попыток до успеха</th>
</tr>
</thead>
<tbody>
{tasksTable.map(task => (
<tr key={task.taskId}>
<td>{task.title}</td>
<td>{task.totalAttempts}</td>
<td>{task.uniqueUsers}</td>
<td>{task.acceptedCount}</td>
<td>{task.successRate}%</td>
<td>{task.averageAttemptsToSuccess.toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
);
}
```
### 2. Прогресс-бары участников
Используйте `activeParticipants` для отображения прогресса каждого участника:
```tsx
// React пример
function ParticipantProgress({ activeParticipants }) {
return (
<div>
{activeParticipants.map(participant => (
<div key={participant.userId} className="participant-card">
<h3>{participant.nickname}</h3>
<p>Завершено заданий: {participant.completedTasks}</p>
<p>Всего попыток: {participant.totalSubmissions}</p>
<div className="chain-progress">
{participant.chainProgress.map(chain => (
<div key={chain.chainId} className="chain-item">
<div className="chain-header">
<span>{chain.chainName}</span>
<span>{chain.completedTasks}/{chain.totalTasks}</span>
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${chain.progressPercent}%` }}
/>
</div>
<span className="progress-text">{chain.progressPercent}%</span>
</div>
))}
</div>
</div>
))}
</div>
);
}
```
### 3. Детальный прогресс по цепочкам
Используйте `chainsDetailed` для отображения детального прогресса всех участников в рамках каждой цепочки:
```tsx
// React пример
function ChainDetailedView({ chainsDetailed }) {
return (
<div>
{chainsDetailed.map(chain => (
<div key={chain.chainId} className="chain-detailed">
<h2>{chain.name}</h2>
<p>Заданий в цепочке: {chain.totalTasks}</p>
{/* Список заданий */}
<div className="tasks-list">
<h3>Задания:</h3>
{chain.tasks.map(task => (
<div key={task.taskId} className="task-item">
<h4>{task.title}</h4>
<p>{task.description}</p>
</div>
))}
</div>
{/* Прогресс участников */}
<div className="participants-progress">
<h3>Прогресс участников:</h3>
<table>
<thead>
<tr>
<th>Участник</th>
{chain.tasks.map(task => (
<th key={task.taskId}>{task.title}</th>
))}
<th>Прогресс</th>
</tr>
</thead>
<tbody>
{chain.participantProgress.map(participant => (
<tr key={participant.userId}>
<td>{participant.nickname}</td>
{participant.taskProgress.map(taskProg => (
<td key={taskProg.taskId}>
<StatusBadge status={taskProg.status} />
</td>
))}
<td>{participant.progressPercent}%</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
);
}
// Компонент для отображения статуса
function StatusBadge({ status }) {
const statusConfig = {
'not_started': { label: 'Не начато', color: 'gray' },
'pending': { label: 'Ожидает', color: 'yellow' },
'in_progress': { label: 'В процессе', color: 'blue' },
'needs_revision': { label: 'Доработка', color: 'orange' },
'completed': { label: 'Завершено', color: 'green' }
};
const config = statusConfig[status];
return (
<span className={`badge badge-${config.color}`}>
{config.label}
</span>
);
}
```
## Рекомендации по использованию
### Производительность
- Эндпоинт выполняет множество агрегаций, поэтому может работать медленно при большом количестве данных
- Рекомендуется кэшировать результат на клиенте на 30-60 секунд
- Используйте loading индикаторы во время загрузки данных
```tsx
// Пример с кэшированием
function useStatsV2(chainId?: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const lastFetch = useRef(0);
const fetchStats = async (force = false) => {
const now = Date.now();
// Кэш на 60 секунд
if (!force && data && (now - lastFetch.current) < 60000) {
return data;
}
setLoading(true);
try {
const url = chainId
? `/challenge/stats/v2?chainId=${chainId}`
: '/challenge/stats/v2';
const response = await fetch(url);
const result = await response.json();
if (result.success) {
setData(result.body);
lastFetch.current = now;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
}, [chainId]); // Перезагружаем при смене цепочки
return { data, loading, error, refetch: () => fetchStats(true) };
}
// Использование
function ChainStatistics() {
const [selectedChainId, setSelectedChainId] = useState<string | undefined>();
const { data, loading } = useStatsV2(selectedChainId);
if (loading) return <div>Загрузка...</div>;
return (
<div>
<select
value={selectedChainId || ''}
onChange={(e) => setSelectedChainId(e.target.value || undefined)}
>
<option value="">Все цепочки</option>
{data?.chainsDetailed.map(chain => (
<option key={chain.chainId} value={chain.chainId}>
{chain.name}
</option>
))}
</select>
<TasksTable tasksTable={data?.tasksTable} />
<ParticipantProgress activeParticipants={data?.activeParticipants} />
</div>
);
}
```
### Фильтрация и сортировка
Все массивы данных можно фильтровать и сортировать на клиенте:
```tsx
// Сортировка таблицы заданий по проценту успеха
const sortedTasks = [...tasksTable].sort((a, b) => b.successRate - a.successRate);
// Фильтрация активных участников с прогрессом > 50%
const activeStudents = activeParticipants.filter(p =>
p.chainProgress.some(c => c.progressPercent > 50)
);
// Поиск участника по имени
const searchParticipant = (query) =>
activeParticipants.filter(p =>
p.nickname.toLowerCase().includes(query.toLowerCase())
);
```
### Визуализация данных
Для построения графиков и диаграмм используйте библиотеки типа:
- **Chart.js** / **Recharts** - для графиков прогресса
- **AG Grid** / **TanStack Table** - для таблиц с сортировкой
- **React Progress Bar** - для прогресс-баров
```tsx
// Пример с Chart.js
import { Bar } from 'react-chartjs-2';
function TasksSuccessChart({ tasksTable }) {
const data = {
labels: tasksTable.map(t => t.title),
datasets: [{
label: 'Процент успеха',
data: tasksTable.map(t => t.successRate),
backgroundColor: 'rgba(75, 192, 192, 0.6)',
}]
};
return <Bar data={data} />;
}
```
## Когда использовать фильтрацию по цепочке
### Используйте `chainId` когда:
1. **Страница отдельной цепочки** - пользователь просматривает детали конкретной цепочки
```tsx
function ChainDetailsPage({ chainId }) {
const { data } = useStatsV2(chainId);
return <ChainDashboard data={data} />;
}
```
2. **Улучшение производительности** - когда нужны данные только по одной цепочке, фильтрация на сервере работает быстрее
3. **Фокус на конкретной программе** - преподаватель хочет видеть прогресс по конкретному курсу/модулю
### НЕ используйте `chainId` когда:
1. **Общий дашборд** - нужна статистика по всем цепочкам
2. **Сравнение цепочек** - нужно показать метрики по всем цепочкам одновременно
3. **Общая аналитика** - нужны агрегированные данные по всей системе
## Различия между v1 и v2
| Параметр | v1 (/stats) | v2 (/stats/v2) | v2 с chainId |
|----------|-------------|----------------|--------------|
| Базовая статистика | ✅ | ✅ | ✅ |
| Таблица заданий | ❌ | ✅ (все) | ✅ (фильтр) |
| Прогресс участников | ❌ | ✅ (все) | ✅ (фильтр) |
| Детальный прогресс по цепочкам | ❌ | ✅ (все) | ✅ (одна) |
| Статистика по попыткам | Общая | Детальная | Детальная (фильтр) |
| Скорость работы | Быстро | Медленнее | Средняя |
## Обработка ошибок
```tsx
async function fetchStatsV2(chainId?: string) {
try {
const url = chainId
? `/challenge/stats/v2?chainId=${chainId}`
: '/challenge/stats/v2';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error?.message || 'Unknown error');
}
return data.body;
} catch (error) {
console.error('Failed to fetch stats v2:', error);
// Специальная обработка для неверного chainId
if (error.message.includes('Chain not found')) {
showNotification('Цепочка не найдена', 'error');
// Перенаправить на страницу всех цепочек
window.location.href = '/chains';
} else {
showNotification('Не удалось загрузить статистику', 'error');
}
}
}
```
### Типичные ошибки
| Ошибка | Причина | Решение |
|--------|---------|---------|
| `Chain not found` | Передан несуществующий `chainId` | Проверить ID цепочки, показать ошибку пользователю |
| `500 Internal Server Error` | Проблема на сервере | Показать общее сообщение об ошибке, повторить запрос |
| Timeout | Слишком много данных | Использовать фильтрацию по `chainId` для уменьшения объема данных |
## Дополнительные возможности
### Экспорт данных
Данные можно экспортировать в CSV или Excel для анализа:
```typescript
function exportToCSV(tasksTable) {
const headers = ['Название', 'Попыток', 'Пользователей', 'Успешно', '% успеха', 'Средние попытки'];
const rows = tasksTable.map(task => [
task.title,
task.totalAttempts,
task.uniqueUsers,
task.acceptedCount,
task.successRate,
task.averageAttemptsToSuccess
]);
const csv = [headers, ...rows]
.map(row => row.join(','))
.join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tasks-statistics.csv';
a.click();
}
```
## Заключение
Эндпоинт `/stats/v2` предоставляет все необходимые данные для построения информативных дашбордов с таблицами заданий и прогресс-барами участников. Комбинируйте различные части данных для создания удобных UI компонентов.
Для дополнительной информации см. также:
- [CHALLENGE_FRONTEND_GUIDE.md](./CHALLENGE_FRONTEND_GUIDE.md) - общее руководство по работе с Challenge API
- [CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md) - примеры React компонентов

View File

@@ -5,7 +5,6 @@ import { keycloak } from '../kc'
import type {
ChallengeTask,
ChallengeChain,
ChallengeUser,
ChallengeSubmission,
SystemStats,
SystemStatsV2,
@@ -113,13 +112,6 @@ export const api = createApi({
invalidatesTags: ['Chain'],
}),
// Users
getUsers: builder.query<ChallengeUser[], void>({
query: () => '/challenge/users',
transformResponse: (response: { body: ChallengeUser[] }) => response.body,
providesTags: ['User'],
}),
// Statistics
getSystemStats: builder.query<SystemStats, void>({
query: () => '/challenge/stats',
@@ -149,11 +141,6 @@ export const api = createApi({
transformResponse: (response: { body: ChallengeSubmission[] }) => response.body,
providesTags: ['Submission'],
}),
getAllSubmissions: builder.query<ChallengeSubmission[], void>({
query: () => '/challenge/submissions',
transformResponse: (response: { body: ChallengeSubmission[] }) => response.body,
providesTags: ['Submission'],
}),
}),
})
@@ -168,11 +155,9 @@ export const {
useCreateChainMutation,
useUpdateChainMutation,
useDeleteChainMutation,
useGetUsersQuery,
useGetSystemStatsQuery,
useGetSystemStatsV2Query,
useGetUserStatsQuery,
useGetUserSubmissionsQuery,
useGetAllSubmissionsQuery,
} = api

View File

@@ -20,30 +20,61 @@ import {
createListCollection,
} from '@chakra-ui/react'
import ReactMarkdown from 'react-markdown'
import { useGetAllSubmissionsQuery } from '../../__data__/api/api'
import { useGetSystemStatsV2Query, useGetUserSubmissionsQuery } from '../../__data__/api/api'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert'
import { EmptyState } from '../../components/EmptyState'
import { StatusBadge } from '../../components/StatusBadge'
import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge'
import type {
ActiveParticipant,
ChallengeSubmission,
SubmissionStatus,
ChallengeTask,
ChallengeUser,
} from '../../types/challenge'
export const SubmissionsPage: React.FC = () => {
const { t } = useTranslation()
const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery()
const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats } =
useGetSystemStatsV2Query(undefined)
const [searchQuery, setSearchQuery] = useState('')
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
const [selectedSubmission, setSelectedSubmission] = useState<ChallengeSubmission | null>(null)
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const {
data: submissions,
isLoading: isSubmissionsLoading,
error: submissionsError,
refetch: refetchSubmissions,
} = useGetUserSubmissionsQuery(
{ userId: selectedUserId!, taskId: undefined },
{ skip: !selectedUserId }
)
const isLoading = isStatsLoading || (selectedUserId && isSubmissionsLoading)
const error = statsError || submissionsError
const handleRetry = () => {
refetchStats()
if (selectedUserId) {
refetchSubmissions()
}
}
if (isLoading) {
return <LoadingSpinner message={t('challenge.admin.submissions.loading')} />
}
if (error || !submissions) {
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={refetch} />
if (error || !stats) {
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={handleRetry} />
}
const filteredSubmissions = submissions.filter((submission) => {
const participants: ActiveParticipant[] = stats.activeParticipants || []
const submissionsList: ChallengeSubmission[] = submissions || []
const filteredSubmissions = submissionsList.filter((submission) => {
const user = submission.user as ChallengeUser
const task = submission.task as ChallengeTask
@@ -84,13 +115,44 @@ export const SubmissionsPage: React.FC = () => {
],
})
const userOptions = createListCollection({
items: participants.map((participant) => ({
label: `${participant.nickname} (${participant.userId})`,
value: participant.userId,
})),
})
const hasParticipants = participants.length > 0
const hasSelectedUser = !!selectedUserId
return (
<Box>
<Heading mb={6}>{t('challenge.admin.submissions.title')}</Heading>
{/* Filters */}
{submissions.length > 0 && (
<HStack mb={4} gap={4}>
{hasParticipants && (
<VStack mb={4} gap={3} align="stretch">
<HStack gap={4}>
<Select.Root
collection={userOptions}
value={selectedUserId ? [selectedUserId] : []}
onValueChange={(e) => setSelectedUserId(e.value[0] ?? null)}
maxW="300px"
>
<Select.Trigger>
<Select.ValueText placeholder={t('challenge.admin.submissions.filter.user')} />
</Select.Trigger>
<Select.Content>
{userOptions.items.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
{submissionsList.length > 0 && (
<>
<Input
placeholder={t('challenge.admin.submissions.search.placeholder')}
value={searchQuery}
@@ -114,13 +176,27 @@ export const SubmissionsPage: React.FC = () => {
))}
</Select.Content>
</Select.Root>
</>
)}
</HStack>
</VStack>
)}
{filteredSubmissions.length === 0 && submissions.length === 0 ? (
<EmptyState title={t('challenge.admin.submissions.empty.title')} description={t('challenge.admin.submissions.empty.description')} />
{!hasParticipants ? (
<EmptyState
title={t('challenge.admin.submissions.empty.title')}
description={t('challenge.admin.submissions.empty.description')}
/>
) : !hasSelectedUser ? (
<EmptyState
title={t('challenge.admin.submissions.empty.title')}
description={t('challenge.admin.submissions.filter.user')}
/>
) : filteredSubmissions.length === 0 ? (
<EmptyState title={t('challenge.admin.submissions.search.empty.title')} description={t('challenge.admin.submissions.search.empty.description')} />
<EmptyState
title={t('challenge.admin.submissions.search.empty.title')}
description={t('challenge.admin.submissions.search.empty.description')}
/>
) : (
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
<Table.Root size="sm">

View File

@@ -20,14 +20,17 @@ import {
Badge,
Progress,
} from '@chakra-ui/react'
import { useGetUsersQuery, useGetUserStatsQuery } from '../../__data__/api/api'
import { useGetSystemStatsV2Query, useGetUserStatsQuery } from '../../__data__/api/api'
import type { ActiveParticipant } from '../../types/challenge'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert'
import { EmptyState } from '../../components/EmptyState'
export const UsersPage: React.FC = () => {
const { t } = useTranslation()
const { data: users, isLoading, error, refetch } = useGetUsersQuery()
const { data: stats, isLoading, error, refetch } = useGetSystemStatsV2Query(undefined, {
pollingInterval: 10000,
})
const [searchQuery, setSearchQuery] = useState('')
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
@@ -35,22 +38,16 @@ export const UsersPage: React.FC = () => {
return <LoadingSpinner message={t('challenge.admin.users.loading')} />
}
if (error || !users) {
if (error || !stats) {
return <ErrorAlert message={t('challenge.admin.users.load.error')} onRetry={refetch} />
}
const users: ActiveParticipant[] = stats.activeParticipants || []
const filteredUsers = users.filter((user) =>
user.nickname.toLowerCase().includes(searchQuery.toLowerCase())
)
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
return (
<Box>
<Heading mb={6}>{t('challenge.admin.users.title')}</Heading>
@@ -80,22 +77,28 @@ export const UsersPage: React.FC = () => {
<Table.Row>
<Table.ColumnHeader>{t('challenge.admin.users.table.nickname')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.id')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.registered')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.total.submissions')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.completed')}</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">{t('challenge.admin.users.table.actions')}</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{filteredUsers.map((user) => (
<Table.Row key={user.id}>
<Table.Row key={user.userId}>
<Table.Cell fontWeight="medium">{user.nickname}</Table.Cell>
<Table.Cell>
<Text fontSize="xs" fontFamily="monospace" color="gray.600">
{user.id}
{user.userId}
</Text>
</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{formatDate(user.createdAt)}
{user.totalSubmissions}
</Text>
</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{user.completedTasks}
</Text>
</Table.Cell>
<Table.Cell textAlign="right">
@@ -103,7 +106,7 @@ export const UsersPage: React.FC = () => {
size="sm"
variant="ghost"
colorPalette="teal"
onClick={() => setSelectedUserId(user.id)}
onClick={() => setSelectedUserId(user.userId)}
>
{t('challenge.admin.users.button.stats')}
</Button>