init brojs
All checks were successful
platform/bro-js/challenge-pl/pipeline/head This commit looks good

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-11-02 17:44:37 +03:00
parent bc77227aeb
commit 3a65307fd0
23 changed files with 15460 additions and 0 deletions

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
# Ignore artifacts:
build
dist
coverage
stubs
logs
d-scripts

7
.prettierrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"jsxSingleQuote": false
}

57
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,57 @@
pipeline {
agent {
docker {
image 'node:20'
}
}
stages {
stage('install') {
steps {
sh 'node -v'
sh 'npm -v'
script {
String tag = sh(returnStdout: true, script: 'git tag --contains').trim()
String branchName = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
String commit = sh(returnStdout: true, script: 'git log -1 --oneline').trim()
String commitMsg = commit.substring(commit.indexOf(' ')).trim()
if (tag) {
currentBuild.displayName = "#${BUILD_NUMBER}, tag ${tag}"
} else {
currentBuild.displayName = "#${BUILD_NUMBER}, branch ${branchName}"
}
String author = sh(returnStdout: true, script: "git log -1 --pretty=format:'%an'").trim()
currentBuild.description = "${author}<br />${commitMsg}"
echo 'starting installing'
sh 'npm ci'
}
}
}
stage('checks') {
parallel {
stage('eslint') {
steps {
sh 'npm run eslint'
}
}
stage('build') {
steps {
sh 'npm run build'
}
}
}
}
stage('clean-all') {
steps {
sh 'rm -rf .[!.]*'
sh 'rm -rf ./*'
sh 'ls -a'
}
}
}
}

23
bro.config.js Normal file
View File

@ -0,0 +1,23 @@
const pkg = require('./package')
module.exports = {
apiPath: 'stubs/api',
webpackConfig: {
output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`
}
},
/* use https://admin.bro-js.ru/ to create config, navigations and features */
navigations: {
'challenge-pl.main': '/challenge-pl',
'link.challenge-pl.auth': '/auth'
},
features: {
'challenge-pl': {
// add your features here in the format [featureName]: { value: string }
},
},
config: {
'challenge-pl.api': '/api'
}
}

57
eslint.config.mjs Normal file
View File

@ -0,0 +1,57 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginReact from 'eslint-plugin-react'
import stylistic from '@stylistic/eslint-plugin'
export default [
{
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
},
{ files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] },
{ languageOptions: { globals: globals.browser } },
{
ignores: ['stubs/', 'bro.config.js'],
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
plugins: {
'@stylistic': stylistic,
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn', // or "error"
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'sort-imports': [
'error',
{
ignoreCase: false,
ignoreDeclarationSort: true,
ignoreMemberSort: true,
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
allowSeparatedGroups: true,
},
],
semi: ['error', 'never'],
'@stylistic/indent': ['error', 2],
},
},
]

View File

@ -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<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 интерфейса!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,178 @@
# 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

@ -0,0 +1,439 @@
# 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. Если что-то из этого отсутствует - обязательно отклони и подробно объясни риски безопасности."
}
```
---
Используйте скрытые инструкции разумно для повышения качества автоматической проверки! 🎓

11642
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "challenge-pl",
"version": "0.0.0",
"description": "",
"main": "./src/index.tsx",
"scripts": {
"test": "exit 0",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist",
"eslint": "npx eslint ./src/**/*",
"eslint:fix": "npx eslint ./src/**/* --fix"
},
"repository": {
"type": "git",
"url": "ssh://git@85.143.175.152:222/bro-js/challenge-pl.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@brojs/cli": "^1.9.4",
"@chakra-ui/react": "^3.2.0",
"@emotion/react": "^11.13.5",
"@eslint/js": "^9.11.0",
"@stylistic/eslint-plugin": "^2.8.0",
"@types/node": "^22.18.13",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"eslint": "^9.11.0",
"eslint-plugin-react": "^7.36.1",
"express": "^4.19.2",
"globals": "^15.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"typescript-eslint": "^8.6.0"
}
}

15
src/__data__/urls.ts Normal file
View File

@ -0,0 +1,15 @@
import { getNavigation, getNavigationValue } from '@brojs/cli'
import pkg from '../../package.json'
const baseUrl = getNavigationValue(`${pkg.name}.main`)
const navs = getNavigation()
const makeUrl = (url) => baseUrl + url
export const URLs = {
baseUrl,
auth: {
url: makeUrl(navs[`link.${pkg.name}.auth`]),
isOn: Boolean(navs[`link.${pkg.name}.auth`])
},
}

17
src/app.tsx Normal file
View File

@ -0,0 +1,17 @@
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import { Dashboard } from './dashboard'
import { Provider } from './theme'
const App = () => {
return (
<BrowserRouter>
<Provider>
<Dashboard />
</Provider>
</BrowserRouter>
)
}
export default App

24
src/dashboard.tsx Normal file
View File

@ -0,0 +1,24 @@
import React, { Suspense } from 'react'
import { Route, Routes } from 'react-router-dom'
import { URLs } from './__data__/urls'
import { MainPage } from './pages'
const PageWrapper = ({ children }: React.PropsWithChildren) => (
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
)
export const Dashboard = () => {
return (
<Routes>
<Route
path={URLs.baseUrl}
element={
<PageWrapper>
<MainPage />
</PageWrapper>
}
/>
</Routes>
)
}

28
src/index.tsx Normal file
View File

@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/display-name */
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './app'
export default () => <App/>
let rootElement: ReactDOM.Root
export const mount = (Component, element = document.getElementById('app')) => {
rootElement = ReactDOM.createRoot(element)
rootElement.render(<Component/>)
// @ts-ignore
if(module.hot) {
// @ts-ignore
module.hot.accept('./app', ()=> {
rootElement.render(<Component/>)
})
}
}
export const unmount = () => {
rootElement.unmount()
}

3
src/pages/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { lazy } from 'react'
export const MainPage = lazy(() => import(/* webpackChunkName: 'main' */'./main'))

3
src/pages/main/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { MainPage } from './main'
export default MainPage

28
src/pages/main/main.tsx Normal file
View File

@ -0,0 +1,28 @@
import React from 'react'
import { Grid, GridItem } from '@chakra-ui/react'
export const MainPage = () => {
return (
<Grid
h="100%"
bgColor="gray.300"
templateAreas={{
md: `"header header"
"aside main"
"footer footer"`,
sm: `"header"
"main"
"aside"
"footer"`,
}}
gridTemplateRows={{ sm: '1fr', md: '50px 1fr 30px' }}
gridTemplateColumns={{ sm: '1fr', md: '150px 1fr' }}
gap={4}
>
<GridItem bgColor="green.100" gridArea="header">header</GridItem>
<GridItem bgColor="green.300" gridArea="aside">aside</GridItem>
<GridItem bgColor="green.600" gridArea="main" h="100vh">main</GridItem>
<GridItem bgColor="green.300" gridArea="footer">footer</GridItem>
</Grid>
)
}

33
src/theme.tsx Normal file
View File

@ -0,0 +1,33 @@
import React from 'react'
import { ChakraProvider as ChacraProv, createSystem, defaultConfig } from '@chakra-ui/react'
import type { PropsWithChildren } from 'react'
const ChacraProvider: React.ElementType = ChacraProv
const system = createSystem(defaultConfig, {
globalCss: {
body: {
colorPalette: 'teal',
},
},
theme: {
tokens: {
fonts: {
body: { value: 'var(--font-outfit)' },
},
},
semanticTokens: {
radii: {
l1: { value: '0.5rem' },
l2: { value: '0.75rem' },
l3: { value: '1rem' },
},
},
},
})
export const Provider = (props: PropsWithChildren) => (
<ChacraProvider value={system}>
{props.children}
</ChacraProvider>
)

8
stubs/api/index.js Normal file
View File

@ -0,0 +1,8 @@
const router = require('express').Router();
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
router.use(timer());
module.exports = router;

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"lib": [
"dom",
"es2017"
],
"outDir": "./dist/",
"sourceMap": true,
"esModuleInterop": true,
"noImplicitAny": false,
"module": "esnext",
"moduleResolution": "node",
"target": "es6",
"jsx": "react",
"typeRoots": ["node_modules/@types", "src/typings"],
"types" : ["webpack-env", "node"],
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"**/*.test.ts",
"**/*.test.tsx",
"node_modules/@types/jest"
]
}

6
types.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module '*.svg' {
const src: string
export default src
}
declare const __webpack_public_path__: string