# 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}

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