init brojs
All checks were successful
platform/bro-js/challenge-pl/pipeline/head This commit looks good
All checks were successful
platform/bro-js/challenge-pl/pipeline/head This commit looks good
This commit is contained in:
parent
bc77227aeb
commit
3a65307fd0
7
.prettierignore
Normal file
7
.prettierignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
stubs
|
||||||
|
logs
|
||||||
|
d-scripts
|
||||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"jsxSingleQuote": false
|
||||||
|
}
|
||||||
57
Jenkinsfile
vendored
Normal file
57
Jenkinsfile
vendored
Normal 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
23
bro.config.js
Normal 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
57
eslint.config.mjs
Normal 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],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
635
memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md
Normal file
635
memory bank/frontend/CHALLENGE_ANALYTICS_SUMMARY.md
Normal 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 интерфейса!
|
||||||
|
|
||||||
1125
memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md
Normal file
1125
memory bank/frontend/CHALLENGE_FRONTEND_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
1060
memory bank/frontend/CHALLENGE_REACT_EXAMPLE.md
Normal file
1060
memory bank/frontend/CHALLENGE_REACT_EXAMPLE.md
Normal file
File diff suppressed because it is too large
Load Diff
178
memory bank/frontend/README.md
Normal file
178
memory bank/frontend/README.md
Normal 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
|
||||||
|
|
||||||
439
memory bank/frontend/TEACHER_GUIDE.md
Normal file
439
memory bank/frontend/TEACHER_GUIDE.md
Normal 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
11642
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal 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
15
src/__data__/urls.ts
Normal 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
17
src/app.tsx
Normal 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
24
src/dashboard.tsx
Normal 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
28
src/index.tsx
Normal 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
3
src/pages/index.ts
Normal 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
3
src/pages/main/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { MainPage } from './main'
|
||||||
|
|
||||||
|
export default MainPage
|
||||||
28
src/pages/main/main.tsx
Normal file
28
src/pages/main/main.tsx
Normal 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
33
src/theme.tsx
Normal 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
8
stubs/api/index.js
Normal 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
25
tsconfig.json
Normal 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
6
types.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
declare module '*.svg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const __webpack_public_path__: string
|
||||||
Loading…
x
Reference in New Issue
Block a user