From e777b57991b7e71937e672c816ccf7d91025f746 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 3 Nov 2025 17:59:08 +0300 Subject: [PATCH] init + api use --- .gitignore | 132 + @types/index.d.ts | 25 + README.md | 258 + bro.config.js | 31 + docs/CHALLENGE_ANALYTICS_SUMMARY.md | 635 + docs/CHALLENGE_FRONTEND_GUIDE.md | 1125 ++ docs/CHALLENGE_REACT_EXAMPLE.md | 1060 ++ docs/README.md | 178 + docs/TEACHER_GUIDE.md | 439 + eslint.config.mjs | 58 + locales/en.json | 3 + locales/ru.json | 3 + package-lock.json | 12927 ++++++++++++++++++++ package.json | 44 + src/__data__/api/api.ts | 168 + src/__data__/kc.ts | 8 + src/__data__/slices/user.ts | 10 + src/__data__/store.ts | 24 + src/__data__/urls.ts | 36 + src/app.tsx | 26 + src/components/ConfirmDialog.tsx | 61 + src/components/EmptyState.tsx | 46 + src/components/ErrorAlert.tsx | 33 + src/components/Layout.tsx | 103 + src/components/LoadingSpinner.tsx | 18 + src/components/StatCard.tsx | 37 + src/components/StatusBadge.tsx | 46 + src/components/ui/toaster.tsx | 12 + src/dashboard.tsx | 107 + src/index.tsx | 68 + src/pages/chains/ChainFormPage.tsx | 320 + src/pages/chains/ChainsListPage.tsx | 166 + src/pages/dashboard/DashboardPage.tsx | 162 + src/pages/index.ts | 4 + src/pages/main/index.ts | 2 + src/pages/main/main.tsx | 11 + src/pages/submissions/SubmissionsPage.tsx | 340 + src/pages/tasks/TaskFormPage.tsx | 244 + src/pages/tasks/TasksListPage.tsx | 180 + src/pages/users/UsersPage.tsx | 281 + src/theme.tsx | 79 + src/types/challenge.ts | 141 + src/utils/authLoopGuard.ts | 59 + stubs/api/README.md | 173 + stubs/api/data/chains.json | 70 + stubs/api/data/stats.json | 21 + stubs/api/data/submissions.json | 203 + stubs/api/data/tasks.json | 72 + stubs/api/data/users.json | 51 + stubs/api/index.js | 391 + tsconfig.json | 27 + types.d.ts | 7 + 52 files changed, 20725 insertions(+) create mode 100644 .gitignore create mode 100644 @types/index.d.ts create mode 100644 README.md create mode 100644 bro.config.js create mode 100644 docs/CHALLENGE_ANALYTICS_SUMMARY.md create mode 100644 docs/CHALLENGE_FRONTEND_GUIDE.md create mode 100644 docs/CHALLENGE_REACT_EXAMPLE.md create mode 100644 docs/README.md create mode 100644 docs/TEACHER_GUIDE.md create mode 100644 eslint.config.mjs create mode 100644 locales/en.json create mode 100644 locales/ru.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/__data__/api/api.ts create mode 100644 src/__data__/kc.ts create mode 100644 src/__data__/slices/user.ts create mode 100644 src/__data__/store.ts create mode 100644 src/__data__/urls.ts create mode 100644 src/app.tsx create mode 100644 src/components/ConfirmDialog.tsx create mode 100644 src/components/EmptyState.tsx create mode 100644 src/components/ErrorAlert.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/LoadingSpinner.tsx create mode 100644 src/components/StatCard.tsx create mode 100644 src/components/StatusBadge.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/dashboard.tsx create mode 100644 src/index.tsx create mode 100644 src/pages/chains/ChainFormPage.tsx create mode 100644 src/pages/chains/ChainsListPage.tsx create mode 100644 src/pages/dashboard/DashboardPage.tsx create mode 100644 src/pages/index.ts create mode 100644 src/pages/main/index.ts create mode 100644 src/pages/main/main.tsx create mode 100644 src/pages/submissions/SubmissionsPage.tsx create mode 100644 src/pages/tasks/TaskFormPage.tsx create mode 100644 src/pages/tasks/TasksListPage.tsx create mode 100644 src/pages/users/UsersPage.tsx create mode 100644 src/theme.tsx create mode 100644 src/types/challenge.ts create mode 100644 src/utils/authLoopGuard.ts create mode 100644 stubs/api/README.md create mode 100644 stubs/api/data/chains.json create mode 100644 stubs/api/data/stats.json create mode 100644 stubs/api/data/submissions.json create mode 100644 stubs/api/data/tasks.json create mode 100644 stubs/api/data/users.json create mode 100644 stubs/api/index.js create mode 100644 tsconfig.json create mode 100644 types.d.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceaea36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + diff --git a/@types/index.d.ts b/@types/index.d.ts new file mode 100644 index 0000000..d3d1942 --- /dev/null +++ b/@types/index.d.ts @@ -0,0 +1,25 @@ +declare const IS_PROD: string +declare const KC_URL: string +declare const KC_REALM: string +declare const KC_CLIENT_ID: string + +declare module '*.svg' { + const svg_path: string + + export default svg_path +} + +declare module '*.jpg' { + const jpg_path: string + + export default jpg_path +} + +declare module '*.png' { + const png_path: string + + export default png_path +} + +declare const __webpack_public_path__: string + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbb44ba --- /dev/null +++ b/README.md @@ -0,0 +1,258 @@ +# Challenge Admin Panel + +Админская панель для управления Challenge Service - системой автоматической проверки заданий с помощью LLM. + +## 🎯 Реализованная функциональность + +### ✅ Dashboard (Главная страница) +- **Основные метрики:** + - Количество пользователей, заданий, цепочек + - Общее количество проверок + - Статус очереди с загруженностью + - Среднее время проверки +- **Статистика проверок:** + - Принятые/отклоненные/ожидающие + - Распределение по статусам +- **Real-time обновление:** автоматическое обновление данных каждые 10 секунд + +### ✅ CRUD для Tasks (Заданий) +- **Список заданий:** + - Таблица с названием, автором, датой создания + - Индикатор наличия скрытых инструкций (🔒) + - Поиск по названию + - Действия: редактировать, удалить +- **Создание/редактирование задания:** + - Название (обязательное, до 255 символов) + - Описание в формате Markdown с вкладками "Редактор" и "Превью" + - **Скрытые инструкции для LLM** (🔒 только для преподавателей): + - Специальное поле с визуальным выделением + - Инструкции передаются LLM при проверке, но не видны студентам + - Примеры использования в подсказках + - Отображение метаданных (автор, даты создания/обновления) + - Валидация и обработка ошибок + +### ✅ CRUD для Chains (Цепочек) +- **Список цепочек:** + - Таблица с названием, количеством заданий, датой создания + - Поиск по названию + - Действия: редактировать, удалить +- **Создание/редактирование цепочки:** + - Название цепочки (обязательное) + - Выбор заданий из существующих + - Управление порядком заданий (кнопки вверх/вниз) + - Добавление/удаление заданий в цепочку + - Поиск доступных заданий + +### ✅ Users (Пользователи) +- **Список пользователей:** + - Таблица с nickname, ID, датой регистрации + - Поиск по nickname + - Кнопка просмотра статистики +- **Детальная статистика пользователя (модальное окно):** + - Общие метрики: выполнено, всего попыток, в процессе, требует доработки + - Прогресс по цепочкам с визуальными прогресс-барами + - Детали по заданиям со статусами + - Среднее время проверки + +### ✅ Submissions (Попытки) +- **Список попыток:** + - Таблица с пользователем, заданием, статусом, попыткой + - Дата отправки и время проверки + - **Фильтрация:** + - Поиск по пользователю или заданию + - Фильтр по статусу (все/принято/доработка/проверяется/ожидает) + - Цветные бейджи статусов +- **Детали попытки (модальное окно):** + - Метаданные: пользователь, статус, даты + - Описание задания (Markdown) + - Решение пользователя + - Обратная связь от LLM + - Время проверки + +## 🛠 Технологический стек + +- **React 18** + **TypeScript** +- **React Router** для навигации +- **Redux Toolkit** + **RTK Query** для state management и API +- **Chakra UI v3** для UI компонентов +- **react-markdown** для отображения Markdown +- **Keycloak** для авторизации преподавателей + +## 📁 Структура проекта + +``` +src/ +├── __data__/ +│ ├── api/ +│ │ └── api.ts # RTK Query API endpoints +│ ├── kc.ts # Keycloak конфигурация +│ ├── store.ts # Redux store +│ └── urls.ts # URL константы +├── components/ +│ ├── ui/ +│ │ └── toaster.tsx # Toast уведомления +│ ├── ConfirmDialog.tsx # Диалог подтверждения +│ ├── EmptyState.tsx # Пустое состояние +│ ├── ErrorAlert.tsx # Отображение ошибок +│ ├── Layout.tsx # Общий layout с навигацией +│ ├── LoadingSpinner.tsx # Индикатор загрузки +│ ├── StatCard.tsx # Карточка метрики +│ └── StatusBadge.tsx # Бейдж статуса +├── pages/ +│ ├── dashboard/ +│ │ └── DashboardPage.tsx # Главная страница +│ ├── tasks/ +│ │ ├── TasksListPage.tsx # Список заданий +│ │ └── TaskFormPage.tsx # Форма задания +│ ├── chains/ +│ │ ├── ChainsListPage.tsx # Список цепочек +│ │ └── ChainFormPage.tsx # Форма цепочки +│ ├── users/ +│ │ └── UsersPage.tsx # Список пользователей +│ └── submissions/ +│ └── SubmissionsPage.tsx # Список попыток +├── types/ +│ └── challenge.ts # TypeScript типы +├── app.tsx # Главный компонент +├── dashboard.tsx # Роутинг +├── index.tsx # Entry point +└── theme.tsx # Chakra UI theme +``` + +## 🔐 Авторизация + +Приложение требует авторизации через **Keycloak** с ролью **`teacher`** в клиенте **`journal`**. + +Конфигурация в `bro.config.js`: +```javascript +KC_URL: 'https://auth.brojs.ru' +KC_REALM: 'itpark' +KC_CLIENT_ID: 'journal' +``` + +Все API запросы автоматически отправляют Bearer токен через RTK Query middleware. + +## 🚀 Запуск проекта + +```bash +# Установка зависимостей +npm install + +# Запуск в режиме разработки (с стабами API) +npm start + +# Сборка для production +npm run build:prod +``` + +Приложение будет доступно по адресу: `http://localhost:8099` + +### 📡 Стабовый API сервер + +Проект включает полнофункциональный стабовый API сервер для разработки и тестирования без реального бэкенда. + +**Тестовые данные:** +- `stubs/api/data/tasks.json` - 5 заданий с hiddenInstructions +- `stubs/api/data/chains.json` - 3 цепочки заданий +- `stubs/api/data/users.json` - 8 пользователей +- `stubs/api/data/submissions.json` - 8 попыток с feedback от LLM +- `stubs/api/data/stats.json` - системная статистика + +**Возможности:** +- ✅ Полный CRUD для заданий и цепочек +- ✅ In-memory хранилище (изменения сбрасываются при перезапуске) +- ✅ Автоматическое обновление статистики +- ✅ Генерация статистики пользователей на лету +- ✅ Поддержка всех endpoints из документации + +**Примечание:** Все ответы возвращаются в формате `{ error: null, data: ... }` + +## 📡 API Endpoints + +Все endpoints используют базовый URL из конфига (`/api`). + +### Tasks +- `GET /challenge/tasks` - список заданий +- `GET /challenge/task/:id` - одно задание +- `POST /challenge/task` - создать задание +- `PUT /challenge/task/:id` - обновить задание +- `DELETE /challenge/task/:id` - удалить задание + +### Chains +- `GET /challenge/chains` - список цепочек +- `GET /challenge/chain/:id` - одна цепочка +- `POST /challenge/chain` - создать цепочку +- `PUT /challenge/chain/:id` - обновить цепочку +- `DELETE /challenge/chain/:id` - удалить цепочку + +### Users & Stats +- `GET /challenge/users` - список пользователей +- `GET /challenge/stats` - общая статистика системы +- `GET /challenge/user/:userId/stats` - статистика пользователя + +### Submissions +- `GET /challenge/submissions` - все попытки +- `GET /challenge/user/:userId/submissions` - попытки пользователя + +## ⚙️ Ключевые особенности реализации + +### RTK Query с кэшированием +Настроены `tagTypes` для автоматической инвалидации кэша: +- `Task` - для заданий +- `Chain` - для цепочек +- `User` - для пользователей +- `Submission` - для попыток +- `Stats` - для статистики + +### Real-time обновления +Dashboard использует `pollingInterval: 10000` для автоматического обновления статистики каждые 10 секунд. + +### hiddenInstructions (Скрытые инструкции) +Специальное поле только для преподавателей: +- Визуально выделено фиолетовым цветом +- Передаётся LLM при проверке решений +- Не видно студентам +- Позволяет тонко настроить проверку + +### UX оптимизации +- Loading состояния для всех запросов +- Toast уведомления для успеха/ошибок +- Confirm диалоги для опасных действий (удаление) +- Empty states для пустых списков +- Поиск и фильтрация на всех страницах +- Адаптивный дизайн + +## 🔗 Навигация + +Переход между проектами настроен через `bro.config.js`: +```javascript +navigations: { + 'challenge-admin-pl.main': '/challenge-admin-pl', + 'link.challenge': '/challenge', +} +``` + +В Layout есть кнопка "Открыть проигрыватель" для перехода к студенческому интерфейсу. + +## 📝 Документация + +Подробная документация по API и архитектуре системы находится в папке `docs/`: +- `CHALLENGE_FRONTEND_GUIDE.md` - руководство для фронтенд разработчиков +- `TEACHER_GUIDE.md` - руководство для преподавателей +- `CHALLENGE_ANALYTICS_SUMMARY.md` - аналитика и метрики +- `CHALLENGE_REACT_EXAMPLE.md` - примеры React компонентов + +## ✨ Дополнительно + +- Все компоненты типизированы с TypeScript +- Markdown поддержка с превью и стилизацией +- Обработка ошибок API с понятными сообщениями +- Валидация форм +- Responsive design + +--- + +**Версия:** 1.0.0 +**Дата:** Ноябрь 2025 +**Статус:** ✅ Production Ready + diff --git a/bro.config.js b/bro.config.js new file mode 100644 index 0000000..0025a67 --- /dev/null +++ b/bro.config.js @@ -0,0 +1,31 @@ +const pkg = require('./package') +const webpack = require('webpack') + +module.exports = { + apiPath: 'stubs/api', + webpackConfig: { + output: { + publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/` + }, + plugins: [ + new webpack.DefinePlugin({ + KC_URL: process.env.KC_URL || '"https://auth.brojs.ru"', + KC_REALM: process.env.KC_REALM || '"itpark"', + KC_CLIENT_ID: process.env.KC_CLIENT_ID || '"journal"', + }), + ], + }, + /* use https://admin.bro-js.ru/ to create config, navigations and features */ + navigations: { + 'challenge-admin.main': '/challenge-admin', + 'link.challenge': '/challenge', + }, + features: { + 'challenge-admin': { + // add your features here in the format [featureName]: { value: string } + }, + }, + config: { + 'challenge-admin.api': '/api' + } +} diff --git a/docs/CHALLENGE_ANALYTICS_SUMMARY.md b/docs/CHALLENGE_ANALYTICS_SUMMARY.md new file mode 100644 index 0000000..d4058e4 --- /dev/null +++ b/docs/CHALLENGE_ANALYTICS_SUMMARY.md @@ -0,0 +1,635 @@ +# Challenge Service - Аналитика и метрики для фронтенда + +Краткое руководство по ключевым метрикам и аналитике для интеграции на фронтенде. + +## 📊 Ключевые метрики для отслеживания + +### 1. Метрики производительности + +```typescript +// Метрики для мониторинга +interface PerformanceMetrics { + // Время от отправки до получения результата + timeToFeedback: number // миллисекунды + + // Время ожидания в очереди + queueWaitTime: number // миллисекунды + + // Время непосредственной проверки + checkTime: number // миллисекунды + + // Позиция в очереди при добавлении + initialQueuePosition: number + + // Количество проверок статуса до завершения + pollsBeforeComplete: number +} + +// Пример сбора метрик +class MetricsCollector { + private startTime: number = 0 + private pollCount: number = 0 + + startTracking() { + this.startTime = Date.now() + this.pollCount = 0 + } + + incrementPoll() { + this.pollCount++ + } + + getMetrics(submission: ChallengeSubmission): PerformanceMetrics { + return { + timeToFeedback: Date.now() - this.startTime, + queueWaitTime: submission.checkedAt + ? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime() + : 0, + checkTime: submission.checkedAt + ? new Date(submission.checkedAt).getTime() - new Date(submission.submittedAt).getTime() + : 0, + initialQueuePosition: 0, // Сохранить из первого ответа + pollsBeforeComplete: this.pollCount + } + } +} +``` + +### 2. Метрики пользовательского поведения + +```typescript +interface UserBehaviorMetrics { + // Время, проведенное на задании + timeSpentOnTask: number // секунды + + // Количество символов в решении + solutionLength: number + + // Количество редактирований текста + editCount: number + + // Использовал ли черновик + usedDraft: boolean + + // Время от загрузки до отправки + timeToSubmit: number // секунды +} + +// Трекинг поведения +class BehaviorTracker { + private taskStartTime: number = Date.now() + private editCount: number = 0 + private lastValue: string = '' + + onTextChange(newValue: string) { + if (newValue !== this.lastValue) { + this.editCount++ + this.lastValue = newValue + } + } + + getMetrics(result: string, usedDraft: boolean): UserBehaviorMetrics { + return { + timeSpentOnTask: Math.floor((Date.now() - this.taskStartTime) / 1000), + solutionLength: result.length, + editCount: this.editCount, + usedDraft, + timeToSubmit: Math.floor((Date.now() - this.taskStartTime) / 1000) + } + } +} +``` + +### 3. Метрики успешности + +```typescript +interface SuccessMetrics { + // Процент принятых заданий с первой попытки + firstAttemptSuccessRate: number // 0-100 + + // Среднее количество попыток до успеха + averageAttemptsToSuccess: number + + // Процент завершенных цепочек + chainCompletionRate: number // 0-100 + + // Время до первого успешного задания + timeToFirstSuccess: number // минуты +} + +// Расчет метрик успешности +function calculateSuccessMetrics(stats: UserStats): SuccessMetrics { + const taskStats = stats.taskStats + + const firstAttemptSuccess = taskStats.filter( + t => t.status === 'completed' && t.totalAttempts === 1 + ).length + + const completedTasks = taskStats.filter(t => t.status === 'completed') + const totalAttempts = completedTasks.reduce((sum, t) => sum + t.totalAttempts, 0) + + return { + firstAttemptSuccessRate: (firstAttemptSuccess / taskStats.length) * 100, + averageAttemptsToSuccess: completedTasks.length > 0 + ? totalAttempts / completedTasks.length + : 0, + chainCompletionRate: (stats.chainStats.filter(c => c.progress === 100).length / stats.chainStats.length) * 100, + timeToFirstSuccess: 0 // Требует дополнительных данных + } +} +``` + +## 📈 Дашборды для фронтенда + +### 1. Personal Dashboard (для студента) + +```typescript +interface PersonalDashboard { + // Общий прогресс + overview: { + tasksCompleted: number + totalTasks: number + completionPercentage: number + currentStreak: number // дней подряд + } + + // Текущие цепочки + activeChains: Array<{ + chainId: string + name: string + progress: number + nextTask: ChallengeTask | null + estimatedTimeToComplete: number // минуты + }> + + // Последние достижения + recentAchievements: Array<{ + type: 'task_completed' | 'chain_completed' | 'first_try_success' + taskTitle: string + timestamp: string + }> + + // Статистика по попыткам + attemptsStats: { + totalAttempts: number + successfulAttempts: number + successRate: number + } + + // Рекомендации + recommendations: Array<{ + type: 'retry' | 'continue' | 'new_chain' + message: string + actionLink: string + }> +} + +// Генерация dashboard +async function generatePersonalDashboard(userId: string): Promise { + const stats = await challengeAPI.getUserStats(userId) + const chains = await challengeAPI.getChains() + + return { + overview: { + tasksCompleted: stats.completedTasks, + totalTasks: stats.totalTasksAttempted, + completionPercentage: (stats.completedTasks / stats.totalTasksAttempted) * 100, + currentStreak: 0 // Требует дополнительной логики + }, + activeChains: stats.chainStats + .filter(c => c.progress > 0 && c.progress < 100) + .map(c => { + const chain = chains.find(ch => ch.id === c.chainId) + const completedCount = c.completedTasks + const nextTask = chain?.tasks[completedCount] || null + + return { + chainId: c.chainId, + name: c.chainName, + progress: c.progress, + nextTask, + estimatedTimeToComplete: (c.totalTasks - c.completedTasks) * 10 // 10 мин на задание + } + }), + recentAchievements: [], // Требует истории + attemptsStats: { + totalAttempts: stats.totalSubmissions, + successfulAttempts: stats.completedTasks, + successRate: (stats.completedTasks / stats.totalSubmissions) * 100 + }, + recommendations: generateRecommendations(stats) + } +} + +function generateRecommendations(stats: UserStats): Array<{type: string, message: string, actionLink: string}> { + const recommendations = [] + + // Если есть задания требующие доработки + if (stats.needsRevisionTasks > 0) { + recommendations.push({ + type: 'retry', + message: `У вас ${stats.needsRevisionTasks} заданий требуют доработки`, + actionLink: '/tasks?status=needs_revision' + }) + } + + // Если есть начатые цепочки + const inProgressChains = stats.chainStats.filter(c => c.progress > 0 && c.progress < 100) + if (inProgressChains.length > 0) { + recommendations.push({ + type: 'continue', + message: `Продолжите цепочку "${inProgressChains[0].chainName}"`, + actionLink: `/chain/${inProgressChains[0].chainId}` + }) + } + + return recommendations +} +``` + +### 2. Admin Dashboard (для преподавателя) + +```typescript +interface AdminDashboard { + // Системные метрики + system: { + totalUsers: number + activeUsers24h: number + totalTasks: number + totalChains: number + queueStatus: { + length: number + processing: number + avgWaitTime: number + } + } + + // Метрики заданий + taskMetrics: Array<{ + taskId: string + title: string + attemptsCount: number + successRate: number + avgAttempts: number + avgTimeToComplete: number + difficulty: 'easy' | 'medium' | 'hard' // на основе метрик + }> + + // Активность пользователей + userActivity: { + registrationsToday: number + submissionsToday: number + peakHours: Array<{ hour: number, count: number }> + } + + // Проблемные области + issues: Array<{ + type: 'low_success_rate' | 'high_attempts' | 'long_queue' + severity: 'low' | 'medium' | 'high' + message: string + affectedEntity: string + }> +} + +// Анализ сложности задания +function analyzeDifficulty( + successRate: number, + avgAttempts: number +): 'easy' | 'medium' | 'hard' { + if (successRate > 70 && avgAttempts < 2) return 'easy' + if (successRate > 40 && avgAttempts < 3) return 'medium' + return 'hard' +} + +// Определение проблем +function detectIssues(stats: SystemStats): Array { + const issues = [] + + // Длинная очередь + if (stats.queue.queueLength > 50) { + issues.push({ + type: 'long_queue', + severity: 'high', + message: `Очередь содержит ${stats.queue.queueLength} заданий`, + affectedEntity: 'system' + }) + } + + // Низкий success rate системы + const systemSuccessRate = (stats.submissions.accepted / stats.submissions.total) * 100 + if (systemSuccessRate < 30) { + issues.push({ + type: 'low_success_rate', + severity: 'medium', + message: `Общий процент принятых заданий всего ${systemSuccessRate.toFixed(1)}%`, + affectedEntity: 'system' + }) + } + + return issues +} +``` + +## 🎯 Визуализация метрик + +### 1. Progress Chart (круговая диаграмма) + +```typescript +interface ProgressChartData { + completed: number + inProgress: number + needsRevision: number + notStarted: number +} + +// Компонент для отображения (концепт) +function ProgressChart({ data }: { data: ProgressChartData }) { + const total = Object.values(data).reduce((a, b) => a + b, 0) + + return ( +
+ + {/* Реализация круговой диаграммы */} + +
+
✅ Завершено: {data.completed}
+
🔄 В процессе: {data.inProgress}
+
❌ Доработка: {data.needsRevision}
+
⚪ Не начато: {data.notStarted}
+
+
+ ) +} +``` + +### 2. Timeline Chart (время проверки) + +```typescript +interface TimelineData { + submissions: Array<{ + timestamp: string + checkTime: number + status: 'accepted' | 'needs_revision' + }> +} + +// График времени проверки по времени суток +function TimelineChart({ data }: { data: TimelineData }) { + const hourlyData = new Array(24).fill(0).map((_, hour) => { + const submissions = data.submissions.filter(s => + new Date(s.timestamp).getHours() === hour + ) + + return { + hour, + count: submissions.length, + avgCheckTime: submissions.length > 0 + ? submissions.reduce((sum, s) => sum + s.checkTime, 0) / submissions.length + : 0 + } + }) + + return ( +
+ {/* Реализация bar chart */} +
+ ) +} +``` + +### 3. Heatmap (активность по дням) + +```typescript +interface HeatmapData { + dates: Array<{ + date: string // YYYY-MM-DD + submissions: number + successRate: number + }> +} + +// Визуализация активности пользователя +function ActivityHeatmap({ data }: { data: HeatmapData }) { + return ( +
+ {data.dates.map(day => ( +
50 ? 'green' : 'red' + }} + title={`${day.date}: ${day.submissions} попыток, ${day.successRate}% успех`} + /> + ))} +
+ ) +} +``` + +## 🔔 Real-time уведомления + +### События для отслеживания + +```typescript +enum ChallengeEventType { + SUBMISSION_QUEUED = 'submission_queued', + SUBMISSION_CHECKING = 'submission_checking', + SUBMISSION_COMPLETED = 'submission_completed', + TASK_COMPLETED = 'task_completed', + CHAIN_COMPLETED = 'chain_completed', + ACHIEVEMENT_UNLOCKED = 'achievement_unlocked' +} + +interface ChallengeEvent { + type: ChallengeEventType + timestamp: string + userId: string + data: any +} + +// Event emitter для уведомлений +class ChallengeEventEmitter { + private listeners: Map void>> = new Map() + + on(type: ChallengeEventType, callback: (event: ChallengeEvent) => void) { + if (!this.listeners.has(type)) { + this.listeners.set(type, []) + } + this.listeners.get(type)!.push(callback) + } + + emit(event: ChallengeEvent) { + const callbacks = this.listeners.get(event.type) || [] + callbacks.forEach(cb => cb(event)) + } +} + +// Использование +const events = new ChallengeEventEmitter() + +events.on(ChallengeEventType.TASK_COMPLETED, (event) => { + // Показать toast уведомление + showNotification('✅ Задание выполнено!', 'success') + + // Обновить статистику + refreshStats() + + // Отправить аналитику + analytics.track('task_completed', event.data) +}) +``` + +## 📱 Адаптивная аналитика + +### Мобильная версия дашборда + +```typescript +interface MobileDashboard { + // Упрощенные метрики для мобильных + quickStats: { + completedToday: number + currentStreak: number + nextTask: string + } + + // Минимальные графики + weekProgress: number[] // 7 последних дней + + // Быстрые действия + quickActions: Array<{ + label: string + action: () => void + icon: string + }> +} +``` + +## 🎨 UI Components для метрик + +### Stat Card Component + +```typescript +interface StatCardProps { + title: string + value: number | string + change?: number // % изменение + trend?: 'up' | 'down' + icon?: string +} + +function StatCard({ title, value, change, trend, icon }: StatCardProps) { + return ( +
+
+ {icon} + {title} +
+
{value}
+ {change && ( +
+ {trend === 'up' ? '↑' : '↓'} {Math.abs(change)}% +
+ )} +
+ ) +} + +// Использование + +``` + +## 🔍 A/B Testing + +### Метрики для тестирования + +```typescript +interface ABTestMetrics { + variant: 'A' | 'B' + + // Конверсионные метрики + submissionRate: number // % пользователей, отправивших хотя бы одно задание + completionRate: number // % завершенных заданий + retryRate: number // % повторных попыток + + // Временные метрики + timeToFirstSubmission: number + sessionDuration: number + + // Качественные метрики + satisfactionScore?: number // если есть опрос +} + +// Сравнение вариантов +function compareVariants(variantA: ABTestMetrics, variantB: ABTestMetrics) { + return { + submissionRateDiff: ((variantB.submissionRate - variantA.submissionRate) / variantA.submissionRate) * 100, + completionRateDiff: ((variantB.completionRate - variantA.completionRate) / variantA.completionRate) * 100, + winner: variantB.completionRate > variantA.completionRate ? 'B' : 'A' + } +} +``` + +## 📊 Экспорт данных + +### CSV Export + +```typescript +async function exportUserProgress(userId: string): Promise { + const stats = await challengeAPI.getUserStats(userId) + const submissions = await challengeAPI.getSubmissions(userId) + + let csv = 'Task,Status,Attempts,Last Attempt,Feedback\n' + + stats.taskStats.forEach(task => { + csv += `"${task.taskTitle}","${task.status}",${task.totalAttempts},"${task.lastAttemptAt || 'N/A'}",""\n` + }) + + return csv +} + +// Скачивание файла +function downloadCSV(csv: string, filename: string) { + const blob = new Blob([csv], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + link.click() + URL.revokeObjectURL(url) +} +``` + +--- + +## ✅ Чек-лист для фронтенд-разработчика + +- [ ] Интегрировать API клиент +- [ ] Настроить Context для state management +- [ ] Реализовать polling механизм +- [ ] Добавить Personal Dashboard +- [ ] Создать визуализации прогресса +- [ ] Настроить event tracking +- [ ] Добавить offline support +- [ ] Реализовать экспорт данных +- [ ] Добавить A/B тестирование +- [ ] Настроить мониторинг ошибок +- [ ] Оптимизировать для мобильных +- [ ] Добавить accessibility features + +## 📚 Полезные ресурсы + +- **API документация**: `CHALLENGE_API_README.md` +- **Архитектура**: `CHALLENGE_ARCHITECTURE.md` +- **React пример**: `CHALLENGE_REACT_EXAMPLE.md` +- **Быстрый старт**: `CHALLENGE_QUICK_START.md` + +Используйте эти метрики и компоненты для создания информативного и user-friendly интерфейса! + diff --git a/docs/CHALLENGE_FRONTEND_GUIDE.md b/docs/CHALLENGE_FRONTEND_GUIDE.md new file mode 100644 index 0000000..22d8bfb --- /dev/null +++ b/docs/CHALLENGE_FRONTEND_GUIDE.md @@ -0,0 +1,1125 @@ +# Challenge Service - Руководство для фронтенда + +Руководство по интеграции сервиса проверки заданий через LLM в пользовательский интерфейс. + +## Содержание + +1. [Основные сценарии использования](#основные-сценарии-использования) +2. [Структура данных](#структура-данных) +3. [API взаимодействие](#api-взаимодействие) +4. [Компоненты UI](#компоненты-ui) +5. [State Management](#state-management) +6. [Best Practices](#best-practices) + +--- + +## Основные сценарии использования + +### 1. Сценарий для студента + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Вход/Регистрация (nickname) │ +│ POST /api/challenge/auth │ +│ → Сохранить userId в localStorage/state │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 2. Просмотр доступных цепочек заданий │ +│ GET /api/challenge/chains │ +│ → Отобразить список с прогрессом │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 3. Выбор задания из цепочки │ +│ → Отобразить описание (Markdown) │ +│ → Показать историю своих попыток │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 4. Написание решения │ +│ → Текстовое поле / Code Editor │ +│ → Кнопка "Отправить на проверку" │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 5. Отправка на проверку │ +│ POST /api/challenge/submit │ +│ → Получить queueId │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 6. Ожидание результата (polling) │ +│ GET /api/challenge/check-status/:queueId │ +│ → Показать индикатор загрузки + позицию в очереди │ +│ → Повторять каждые 2-3 секунды │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 7. Получение результата │ +│ ✅ ПРИНЯТО → Поздравление + переход к следующему │ +│ ❌ ДОРАБОТКА → Feedback от LLM + возможность повторить │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2. Сценарий для администратора + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. Создание заданий │ +│ POST /api/challenge/task │ +│ → Markdown редактор для описания │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 2. Создание цепочек │ +│ POST /api/challenge/chain │ +│ → Выбор заданий + порядок │ +└────────────────┬────────────────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────────────────┐ +│ 3. Мониторинг статистики │ +│ GET /api/challenge/stats │ +│ → Dashboard с метриками │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Структура данных + +### User (Пользователь) + +```typescript +interface ChallengeUser { + _id: string + id: string + nickname: string + createdAt: string // ISO 8601 +} +``` + +### Task (Задание) + +```typescript +interface ChallengeTask { + _id: string + id: string + title: string + description: string // Markdown (видно всем) + hiddenInstructions?: string // Скрытые инструкции для LLM (только для преподавателей) + creator?: object // Данные создателя (только для преподавателей) + createdAt: string + updatedAt: string +} +``` + +**Важно:** Поля `hiddenInstructions` и `creator` доступны только пользователям с ролью `teacher` в Keycloak. При запросе заданий обычными студентами эти поля будут отфильтрованы на сервере. + +### Chain (Цепочка заданий) + +```typescript +interface ChallengeChain { + _id: string + id: string + name: string + tasks: ChallengeTask[] // Populated + createdAt: string + updatedAt: string +} +``` + +### Submission (Попытка) + +```typescript +type SubmissionStatus = 'pending' | 'in_progress' | 'accepted' | 'needs_revision' + +interface ChallengeSubmission { + _id: string + id: string + user: ChallengeUser | string // Populated или ID + task: ChallengeTask | string // Populated или ID + result: string // Результат пользователя + status: SubmissionStatus + queueId?: string + feedback?: string // Комментарий от LLM + submittedAt: string + checkedAt?: string + attemptNumber: number +} +``` + +### Queue Status (Статус проверки) + +```typescript +type QueueStatusType = 'waiting' | 'in_progress' | 'completed' | 'error' | 'not_found' + +interface QueueStatus { + status: QueueStatusType + submission?: ChallengeSubmission + error?: string + position?: number // Позиция в очереди (если waiting) +} +``` + +### User Stats (Статистика пользователя) + +```typescript +interface TaskStats { + taskId: string + taskTitle: string + attempts: Array<{ + attemptNumber: number + status: SubmissionStatus + submittedAt: string + checkedAt?: string + feedback?: string + }> + totalAttempts: number + status: 'not_attempted' | 'pending' | 'in_progress' | 'completed' | 'needs_revision' + lastAttemptAt: string | null +} + +interface ChainStats { + chainId: string + chainName: string + totalTasks: number + completedTasks: number + progress: number // 0-100 +} + +interface UserStats { + totalTasksAttempted: number + completedTasks: number + inProgressTasks: number + needsRevisionTasks: number + totalSubmissions: number + averageCheckTimeMs: number + taskStats: TaskStats[] + chainStats: ChainStats[] +} +``` + +### System Stats (Общая статистика) + +```typescript +interface SystemStats { + users: number + tasks: number + chains: number + submissions: { + total: number + accepted: number + rejected: number + pending: number + inProgress: number + } + averageCheckTimeMs: number + queue: { + queueLength: number + waiting: number + inProgress: number + maxConcurrency: number + currentlyProcessing: number + } +} +``` + +--- + +## API взаимодействие + +### Базовая настройка + +```typescript +const API_BASE_URL = 'http://localhost:8082/api/challenge' + +// Универсальная функция для запросов +async function apiRequest( + endpoint: string, + options: RequestInit = {} +): Promise<{ error: any; data: T }> { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }) + + return response.json() +} +``` + +### Примеры использования + +#### 1. Аутентификация + +```typescript +async function authenticateUser(nickname: string): Promise { + const { data, error } = await apiRequest<{ ok: boolean; userId: string }>( + '/auth', + { + method: 'POST', + body: JSON.stringify({ nickname }), + } + ) + + if (error) throw error + + // Сохраняем userId + localStorage.setItem('challengeUserId', data.userId) + + return data.userId +} +``` + +#### 2. Получение цепочек + +```typescript +async function getChains(): Promise { + const { data, error } = await apiRequest('/chains') + + if (error) throw error + + return data +} +``` + +#### 3. Отправка решения + +```typescript +async function submitSolution( + userId: string, + taskId: string, + result: string +): Promise<{ queueId: string; submissionId: string }> { + const { data, error } = await apiRequest<{ + queueId: string + submissionId: string + }>('/submit', { + method: 'POST', + body: JSON.stringify({ userId, taskId, result }), + }) + + if (error) throw error + + return data +} +``` + +#### 4. Polling проверки + +```typescript +async function pollCheckStatus( + queueId: string, + onUpdate: (status: QueueStatus) => void, + interval: number = 2000 +): Promise { + return new Promise((resolve, reject) => { + const checkStatus = async () => { + try { + const { data, error } = await apiRequest( + `/check-status/${queueId}` + ) + + if (error) { + reject(error) + return + } + + onUpdate(data) + + if (data.status === 'completed' && data.submission) { + resolve(data.submission) + } else if (data.status === 'error') { + reject(new Error(data.error || 'Check failed')) + } else { + // Продолжаем polling + setTimeout(checkStatus, interval) + } + } catch (err) { + reject(err) + } + } + + checkStatus() + }) +} +``` + +#### 5. Получение статистики пользователя + +```typescript +async function getUserStats(userId: string): Promise { + const { data, error } = await apiRequest(`/user/${userId}/stats`) + + if (error) throw error + + return data +} +``` + +--- + +## Компоненты UI + +### 1. AuthForm - Форма входа + +```typescript +// AuthForm.tsx +import { useState } from 'react' + +interface AuthFormProps { + onAuth: (userId: string, nickname: string) => void +} + +export function AuthForm({ onAuth }: AuthFormProps) { + const [nickname, setNickname] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + try { + const userId = await authenticateUser(nickname) + onAuth(userId, nickname) + } catch (err) { + setError('Ошибка входа. Попробуйте другой nickname.') + } finally { + setLoading(false) + } + } + + return ( +
+ setNickname(e.target.value)} + minLength={3} + maxLength={50} + required + /> + + {error &&
{error}
} +
+ ) +} +``` + +### 2. ChainList - Список цепочек + +```typescript +// ChainList.tsx +import { useState, useEffect } from 'react' + +interface ChainListProps { + userId: string + onSelectChain: (chain: ChallengeChain) => void +} + +export function ChainList({ userId, onSelectChain }: ChainListProps) { + const [chains, setChains] = useState([]) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + loadData() + }, [userId]) + + const loadData = async () => { + try { + const [chainsData, statsData] = await Promise.all([ + getChains(), + getUserStats(userId) + ]) + setChains(chainsData) + setStats(statsData) + } catch (err) { + console.error(err) + } finally { + setLoading(false) + } + } + + const getChainProgress = (chainId: string) => { + return stats?.chainStats.find(cs => cs.chainId === chainId) + } + + if (loading) return
Загрузка...
+ + return ( +
+ {chains.map(chain => { + const progress = getChainProgress(chain.id) + + return ( +
onSelectChain(chain)}> +

{chain.name}

+

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

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

{task.title}

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

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

+