diff --git a/UPDATE.md b/UPDATE.md
new file mode 100644
index 0000000..2118bcb
--- /dev/null
+++ b/UPDATE.md
@@ -0,0 +1,202 @@
+# Добавление поля learningMaterial в задачу челленджа
+
+## Описание изменений
+
+В модель задачи челленджа (`ChallengeTask`) добавлено новое необязательное текстовое поле `learningMaterial` для хранения дополнительной обучающей информации в формате Markdown.
+
+## Структура данных
+
+### Модель ChallengeTask
+
+```typescript
+{
+ title: string, // Заголовок задания (обязательное)
+ description: string, // Основное описание в Markdown (обязательное, видно студентам)
+ learningMaterial: string, // Дополнительный учебный материал в Markdown (необязательное, видно студентам)
+ hiddenInstructions: string, // Скрытые инструкции для LLM (необязательное, только для преподавателей)
+ createdAt: Date, // Дата создания
+ updatedAt: Date, // Дата последнего обновления
+ creator: Object // Данные создателя из Keycloak
+}
+```
+
+## Изменения в API
+
+### 1. Создание задания (POST /challenge/task)
+
+**Добавлено поле в тело запроса:**
+```json
+{
+ "title": "Название задания",
+ "description": "Основное описание в Markdown",
+ "learningMaterial": "Дополнительный учебный материал в Markdown",
+ "hiddenInstructions": "Скрытые инструкции для преподавателей"
+}
+```
+
+**Пример запроса:**
+```bash
+POST /challenge/task
+Content-Type: application/json
+
+{
+ "title": "Реализация алгоритма сортировки",
+ "description": "Напишите функцию сортировки массива методом пузырька",
+ "learningMaterial": "## Теория\n\nМетод пузырьковой сортировки работает путем...\n\n## Полезные ссылки\n- [Википедия](https://ru.wikipedia.org/wiki/Сортировка_пузырьком)\n- [Видео объяснение](https://example.com/video)",
+ "hiddenInstructions": "Оценить эффективность алгоритма и стиль кода"
+}
+```
+
+### 2. Обновление задания (PUT /challenge/task/:taskId)
+
+**Добавлено поле в тело запроса:**
+```json
+{
+ "title": "Новое название",
+ "description": "Обновленное описание",
+ "learningMaterial": "Обновленный учебный материал",
+ "hiddenInstructions": "Обновленные инструкции"
+}
+```
+
+## Получение данных
+
+### Получение задания (GET /challenge/task/:taskId)
+
+**Ответ содержит новое поле:**
+```json
+{
+ "id": "task_id",
+ "title": "Название задания",
+ "description": "Основное описание в Markdown",
+ "learningMaterial": "Дополнительный учебный материал в Markdown",
+ "createdAt": "2025-01-15T10:00:00.000Z",
+ "updatedAt": "2025-01-15T10:30:00.000Z"
+}
+```
+
+**Важно:** Поле `learningMaterial` видно всем пользователям (студентам и преподавателям), в отличие от `hiddenInstructions`, которое скрывается от студентов.
+
+### Получение всех заданий (GET /challenge/tasks)
+
+Возвращает массив заданий с новым полем `learningMaterial`.
+
+### Получение цепочек (GET /challenge/chains, GET /challenge/chain/:chainId)
+
+При получении цепочек с populate заданий, поле `learningMaterial` будет доступно в каждом задании цепочки.
+
+## Frontend изменения
+
+### Интерфейсы TypeScript
+
+```typescript
+interface ChallengeTask {
+ id: string;
+ title: string;
+ description: string; // Markdown
+ learningMaterial?: string; // Новое поле - дополнительный материал в Markdown
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+### Формы создания/редактирования заданий
+
+В формах создания и редактирования заданий необходимо добавить поле для ввода `learningMaterial`:
+
+```typescript
+// Пример компонента формы
+const TaskForm = () => {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ learningMaterial: '', // Новое поле
+ hiddenInstructions: ''
+ });
+
+ // Визуальный редактор или textarea для learningMaterial
+ return (
+
+ );
+};
+```
+
+### Отображение заданий
+
+При отображении задания студентам показывать `learningMaterial` как дополнительную информацию:
+
+```typescript
+const TaskView = ({ task }: { task: ChallengeTask }) => {
+ return (
+
+
{task.title}
+
+ {/* Основное описание */}
+
+
+ {/* Дополнительный учебный материал */}
+ {task.learningMaterial && (
+
+
Дополнительные материалы
+
+
+ )}
+
+ );
+};
+```
+
+## Миграция данных
+
+Поле `learningMaterial` добавлено как необязательное с значением по умолчанию `''`, поэтому:
+- Существующие задания будут работать без изменений
+- Новое поле будет пустым для старых заданий
+- Можно постепенно добавлять учебный материал к существующим заданиям
+
+## Тестирование
+
+### Создание задания с учебным материалом
+```bash
+# Создать задание с дополнительным материалом
+POST /challenge/task
+{
+ "title": "Тестовое задание",
+ "description": "Основное задание",
+ "learningMaterial": "# Полезная информация\n\nЭто дополнительный материал для студентов"
+}
+```
+
+### Получение задания
+```bash
+GET /challenge/task/{taskId}
+# Проверить, что learningMaterial присутствует в ответе
+```
+
+### Обновление учебного материала
+```bash
+PUT /challenge/task/{taskId}
+{
+ "learningMaterial": "# Обновленная информация\n\nНовые полезные материалы..."
+}
+```
+
+## Влияние на существующий код
+
+- Все существующие эндпоинты получения данных автоматически возвращают новое поле
+- Создание заданий без указания `learningMaterial` работает как прежде
+- Фильтрация и валидация не затрагиваются
+- Поле индексируется MongoDB автоматически
\ No newline at end of file
diff --git a/src/__data__/types.ts b/src/__data__/types.ts
index c887635..96681c6 100644
--- a/src/__data__/types.ts
+++ b/src/__data__/types.ts
@@ -12,6 +12,7 @@ export interface ChallengeTask {
id: string
title: string
description: string
+ learningMaterial?: string
hiddenInstructions?: string
creator?: Record
createdAt: string
diff --git a/src/components/personal/LearningMaterialViewer.tsx b/src/components/personal/LearningMaterialViewer.tsx
new file mode 100644
index 0000000..200c217
--- /dev/null
+++ b/src/components/personal/LearningMaterialViewer.tsx
@@ -0,0 +1,271 @@
+import React, { useState, useMemo } from 'react'
+import {
+ Box,
+ Button,
+ HStack,
+ Text,
+ VStack,
+} from '@chakra-ui/react'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+
+interface LearningMaterialViewerProps {
+ content: string
+ linesPerPage?: number
+}
+
+export const LearningMaterialViewer = ({
+ content,
+ linesPerPage = 30
+}: LearningMaterialViewerProps) => {
+ const [currentPage, setCurrentPage] = useState(0)
+
+ // Разделяем контент на страницы по linesPerPage строк
+ const pages = useMemo(() => {
+ const lines = content.split('\n')
+ const pagesArray: string[] = []
+
+ for (let i = 0; i < lines.length; i += linesPerPage) {
+ const pageLines = lines.slice(i, i + linesPerPage)
+ pagesArray.push(pageLines.join('\n'))
+ }
+
+ return pagesArray
+ }, [content, linesPerPage])
+
+ const totalPages = pages.length
+
+ if (totalPages === 0) {
+ return null
+ }
+
+ const goToPrevious = () => {
+ setCurrentPage(prev => Math.max(0, prev - 1))
+ }
+
+ const goToNext = () => {
+ setCurrentPage(prev => Math.min(totalPages - 1, prev + 1))
+ }
+
+ return (
+
+
+
+
+ Дополнительные материалы
+
+ {totalPages > 1 && (
+
+ Страница {currentPage + 1} из {totalPages}
+
+ )}
+
+
+ p': {
+ marginTop: '0.25em',
+ marginBottom: '0.25em'
+ },
+ // Инлайн-код
+ '& code': {
+ backgroundColor: '#EDF2F7',
+ color: '#C53030',
+ padding: '0.15em 0.4em',
+ borderRadius: '4px',
+ fontSize: '0.9em',
+ fontFamily: 'Monaco, Consolas, "Courier New", monospace',
+ fontWeight: '500'
+ },
+ // Блоки кода
+ '& pre': {
+ backgroundColor: '#1A202C',
+ color: '#E2E8F0',
+ padding: '1em 1.2em',
+ borderRadius: '8px',
+ overflowX: 'auto',
+ marginTop: '1em',
+ marginBottom: '1em',
+ border: '1px solid #2D3748',
+ fontSize: '0.9em',
+ lineHeight: '1.6'
+ },
+ '& pre code': {
+ backgroundColor: 'transparent',
+ color: '#E2E8F0',
+ padding: '0',
+ fontFamily: 'Monaco, Consolas, "Courier New", monospace'
+ },
+ // Цитаты
+ '& blockquote': {
+ borderLeft: '4px solid #4299E1',
+ paddingLeft: '1em',
+ paddingTop: '0.5em',
+ paddingBottom: '0.5em',
+ marginLeft: '0',
+ marginTop: '1em',
+ marginBottom: '1em',
+ fontStyle: 'italic',
+ color: '#4A5568',
+ backgroundColor: '#EBF8FF',
+ borderRadius: '0 4px 4px 0'
+ },
+ '& blockquote p': {
+ marginTop: '0.25em',
+ marginBottom: '0.25em'
+ },
+ // Ссылки
+ '& a': {
+ color: '#3182CE',
+ textDecoration: 'underline',
+ fontWeight: '500',
+ transition: 'color 0.2s',
+ '&:hover': {
+ color: '#2C5282'
+ }
+ },
+ // Горизонтальная линия
+ '& hr': {
+ border: 'none',
+ borderTop: '2px solid #E2E8F0',
+ marginTop: '1.5em',
+ marginBottom: '1.5em'
+ },
+ // Таблицы
+ '& table': {
+ borderCollapse: 'collapse',
+ width: '100%',
+ marginTop: '1em',
+ marginBottom: '1em',
+ fontSize: '0.95em'
+ },
+ '& table thead': {
+ backgroundColor: '#F7FAFC'
+ },
+ '& table th': {
+ border: '1px solid #E2E8F0',
+ padding: '0.75em 1em',
+ textAlign: 'left',
+ fontWeight: '600',
+ color: '#2D3748'
+ },
+ '& table td': {
+ border: '1px solid #E2E8F0',
+ padding: '0.75em 1em',
+ textAlign: 'left'
+ },
+ '& table tr:nth-of-type(even)': {
+ backgroundColor: '#F7FAFC'
+ },
+ // Выделение (strong, em)
+ '& strong': {
+ fontWeight: '600',
+ color: '#2D3748'
+ },
+ '& em': {
+ fontStyle: 'italic'
+ },
+ // Изображения
+ '& img': {
+ maxWidth: '100%',
+ height: 'auto',
+ borderRadius: '8px',
+ marginTop: '1em',
+ marginBottom: '1em'
+ }
+ }}
+ >
+
+ {pages[currentPage]}
+
+
+
+ {totalPages > 1 && (
+
+ ←}
+ >
+ Предыдущая
+
+ →}
+ >
+ Следующая
+
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/personal/TaskWorkspace.tsx b/src/components/personal/TaskWorkspace.tsx
index 084e297..00203e4 100644
--- a/src/components/personal/TaskWorkspace.tsx
+++ b/src/components/personal/TaskWorkspace.tsx
@@ -13,6 +13,7 @@ import remarkGfm from 'remark-gfm'
import type { ChallengeTask } from '../../__data__/types'
import { useChallenge } from '../../context/ChallengeContext'
import { useSubmission } from '../../hooks/useSubmission'
+import { LearningMaterialViewer } from './LearningMaterialViewer'
interface TaskWorkspaceProps {
task: ChallengeTask
@@ -27,31 +28,34 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
// Сохраняем последний результат, чтобы блок не исчезал
const [lastResult, setLastResult] = useState(null)
+ // Состояние для показа дополнительного материала
+ const [showLearningMaterial, setShowLearningMaterial] = useState(false)
const isChecking = !!queueStatus || isSubmitting
const isAccepted = finalSubmission?.status === 'accepted'
const needsRevision = finalSubmission?.status === 'needs_revision'
-
+
// Вычисляем прогресс проверки (0-100%)
const checkingProgress = (() => {
if (!queueStatus) return 0
-
+
const initial = queueStatus.initialPosition || 3
const current = queueStatus.position || 0
-
+
if (queueStatus.status === 'in_progress') return 90 // Почти готово
if (current === 0) return 90
-
+
// От 0% до 80% по мере движения в очереди
const progress = ((initial - current) / initial) * 80
return Math.max(10, progress) // Минимум 10% чтобы было видно
})()
-
+
// Сбрасываем состояние при смене задания
useEffect(() => {
setLastResult(null)
+ setShowLearningMaterial(false)
}, [task.id])
-
+
// Обновляем сохраненный результат только когда получаем новый
useEffect(() => {
if (finalSubmission) {
@@ -64,7 +68,7 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
refreshStats()
}
}, [finalSubmission, refreshStats])
-
+
// Используем либо текущий результат, либо последний сохраненный
const displayedSubmission = finalSubmission || lastResult
const showAccepted = displayedSubmission?.status === 'accepted'
@@ -76,57 +80,58 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
{task.title}
+
{
marginBottom: '0.25em'
},
// Инлайн-код
- '& code': {
- backgroundColor: '#EDF2F7',
+ '& code': {
+ backgroundColor: '#EDF2F7',
color: '#C53030',
- padding: '0.15em 0.4em',
- borderRadius: '4px',
+ padding: '0.15em 0.4em',
+ borderRadius: '4px',
fontSize: '0.9em',
fontFamily: 'Monaco, Consolas, "Courier New", monospace',
fontWeight: '500'
},
// Блоки кода
- '& pre': {
- backgroundColor: '#1A202C',
+ '& pre': {
+ backgroundColor: '#1A202C',
color: '#E2E8F0',
- padding: '1em 1.2em',
- borderRadius: '8px',
- overflowX: 'auto',
- marginTop: '1em',
+ padding: '1em 1.2em',
+ borderRadius: '8px',
+ overflowX: 'auto',
+ marginTop: '1em',
marginBottom: '1em',
border: '1px solid #2D3748',
fontSize: '0.9em',
lineHeight: '1.6'
},
- '& pre code': {
- backgroundColor: 'transparent',
+ '& pre code': {
+ backgroundColor: 'transparent',
color: '#E2E8F0',
padding: '0',
fontFamily: 'Monaco, Consolas, "Courier New", monospace'
},
// Цитаты
- '& blockquote': {
- borderLeft: '4px solid #4299E1',
- paddingLeft: '1em',
+ '& blockquote': {
+ borderLeft: '4px solid #4299E1',
+ paddingLeft: '1em',
paddingTop: '0.5em',
paddingBottom: '0.5em',
- marginLeft: '0',
+ marginLeft: '0',
marginTop: '1em',
marginBottom: '1em',
fontStyle: 'italic',
@@ -182,8 +187,8 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
marginBottom: '0.25em'
},
// Ссылки
- '& a': {
- color: '#3182CE',
+ '& a': {
+ color: '#3182CE',
textDecoration: 'underline',
fontWeight: '500',
transition: 'color 0.2s',
@@ -244,16 +249,40 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
>
{task.description}
+
+ {/* Кнопка для показа дополнительного материала */}
+ {task.learningMaterial && !showLearningMaterial && (
+
+ setShowLearningMaterial(true)}
+ >
+ 📚 Прочитать доп. материал
+
+
+ )}
+ {/* Дополнительные материалы - показываются отдельно */}
+ {task.learningMaterial && showLearningMaterial && (
+
+ )}
+
+
+
{/* Статус проверки и результат - фиксированное место */}
{queueStatus && !finalSubmission ? (
-
@@ -262,13 +291,13 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
Проверяем решение...
-
+
{/* Кастомный прогресс-бар */}
-
diff --git a/stubs/api/data/chains.json b/stubs/api/data/chains.json
index 0d831fa..107739c 100644
--- a/stubs/api/data/chains.json
+++ b/stubs/api/data/chains.json
@@ -13,6 +13,7 @@
"_id": "task-html-intro",
"title": "HTML старт",
"description": "# HTML старт\n\nСоздайте базовую HTML-страницу с заголовком и абзацем.",
+ "learningMaterial": "# Дополнительные материалы: HTML основы\n\n## Что такое HTML?\n\n**HTML (HyperText Markup Language)** — это язык разметки гипертекста, который является основой всех веб-страниц. HTML позволяет создавать структуру документа и определять его содержимое.\n\n## История HTML\n\nHTML был создан Тимом Бернерс-Ли в 1989 году. Первая версия HTML появилась в 1991 году. С тех пор язык эволюционировал:\n\n- **HTML 2.0** (1995) — первая стандартизированная версия\n- **HTML 3.2** (1997) — добавлены таблицы и апплеты\n- **HTML 4.01** (1999) — последняя версия HTML 4\n- **HTML5** (2014) — современная версия с множеством новых возможностей\n\n## Базовая структура HTML-документа\n\nКаждый HTML-документ имеет стандартную структуру:\n\n```html\n\n\n\n \n \n Заголовок страницы \n\n\n \n\n\n```\n\n### Детальное объяснение структуры:\n\n- `` — сообщает браузеру, что это HTML5 документ\n- `` — корневой элемент с указанием языка\n- `` — содержит мета-информацию\n- ` ` — кодировка символов\n- ` ` — адаптивность для мобильных устройств\n- `` — заголовок вкладки браузера\n- ` ` — видимая часть страницы\n\n## Семантические элементы HTML5\n\nHTML5 ввел множество семантических элементов для лучшей структуры:\n\n```html\n