diff --git a/docs/CHALLENGE_LEARNING_MATERIAL.md b/docs/CHALLENGE_LEARNING_MATERIAL.md
new file mode 100644
index 0000000..2118bcb
--- /dev/null
+++ b/docs/CHALLENGE_LEARNING_MATERIAL.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/locales/en.json b/locales/en.json
index a1e1ebc..5b030d4 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -20,6 +20,9 @@
"challenge.admin.tasks.field.description": "Description (Markdown)",
"challenge.admin.tasks.field.description.placeholder": "# Task title\n\nTask description in Markdown format...",
"challenge.admin.tasks.field.description.helper": "Use Markdown to format text",
+ "challenge.admin.tasks.field.learning.material": "Additional Learning Material (Markdown)",
+ "challenge.admin.tasks.field.learning.material.placeholder": "# Additional Materials\n\nTheory, links, solution examples...",
+ "challenge.admin.tasks.field.learning.material.helper": "Materials for in-depth study. Displayed with scrolling like a book.",
"challenge.admin.tasks.tab.editor": "Editor",
"challenge.admin.tasks.tab.preview": "Preview",
"challenge.admin.tasks.preview.empty": "Preview will appear here...",
diff --git a/locales/ru.json b/locales/ru.json
index d3e9a39..9f5bccd 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -19,6 +19,9 @@
"challenge.admin.tasks.field.description": "Описание (Markdown)",
"challenge.admin.tasks.field.description.placeholder": "# Заголовок задания\n\nОписание задания в формате Markdown...",
"challenge.admin.tasks.field.description.helper": "Используйте Markdown для форматирования текста",
+ "challenge.admin.tasks.field.learning.material": "Дополнительный учебный материал (Markdown)",
+ "challenge.admin.tasks.field.learning.material.placeholder": "# Дополнительные материалы\n\nТеория, ссылки, примеры решений...",
+ "challenge.admin.tasks.field.learning.material.helper": "Материалы для углубленного изучения. Отображаются с прокруткой как книга.",
"challenge.admin.tasks.tab.editor": "Редактор",
"challenge.admin.tasks.tab.preview": "Превью",
"challenge.admin.tasks.preview.empty": "Предпросмотр появится здесь...",
diff --git a/src/__data__/kc.ts b/src/__data__/kc.ts
index 6aa5557..f52927e 100644
--- a/src/__data__/kc.ts
+++ b/src/__data__/kc.ts
@@ -3,6 +3,6 @@ import Keycloak from 'keycloak-js'
export const keycloak = new Keycloak({
url: KC_URL,
realm: KC_REALM,
- clientId: KC_CLIENT_ID,
-});
+ clientId: KC_CLIENT_ID
+})
diff --git a/src/pages/tasks/TaskFormPage.tsx b/src/pages/tasks/TaskFormPage.tsx
index 01b8c7b..83516bf 100644
--- a/src/pages/tasks/TaskFormPage.tsx
+++ b/src/pages/tasks/TaskFormPage.tsx
@@ -40,6 +40,7 @@ export const TaskFormPage: React.FC = () => {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
+ const [learningMaterial, setLearningMaterial] = useState('')
const [hiddenInstructions, setHiddenInstructions] = useState('')
const [showDescPreview, setShowDescPreview] = useState(false)
const [testAnswer, setTestAnswer] = useState('')
@@ -50,6 +51,7 @@ export const TaskFormPage: React.FC = () => {
if (task) {
setTitle(task.title)
setDescription(task.description)
+ setLearningMaterial(task.learningMaterial || '')
setHiddenInstructions(task.hiddenInstructions || '')
}
}, [task])
@@ -106,6 +108,7 @@ export const TaskFormPage: React.FC = () => {
data: {
title: title.trim(),
description: description.trim(),
+ learningMaterial: learningMaterial.trim() || undefined,
hiddenInstructions: hiddenInstructions.trim() || undefined,
},
}).unwrap()
@@ -118,6 +121,7 @@ export const TaskFormPage: React.FC = () => {
await createTask({
title: title.trim(),
description: description.trim(),
+ learningMaterial: learningMaterial.trim() || undefined,
hiddenInstructions: hiddenInstructions.trim() || undefined,
}).unwrap()
toaster.create({
@@ -355,6 +359,128 @@ export const TaskFormPage: React.FC = () => {
{t('challenge.admin.tasks.field.description.helper')}
+ {/* Learning Material */}
+
+ {t('challenge.admin.tasks.field.learning.material')}
+
+ {/* Табы для мобильных */}
+ setShowDescPreview(e.value === 'preview')}
+ >
+
+ {t('challenge.admin.tasks.tab.editor')}
+ {t('challenge.admin.tasks.tab.preview')}
+
+
+
+
+
+ {learningMaterial ? (
+
+ {learningMaterial}
+
+ ) : (
+
+ {t('challenge.admin.tasks.preview.empty')}
+
+ )}
+
+
+
+
+
+ {/* Две колонки для десктопа */}
+
+
+
+ {t('challenge.admin.tasks.tab.editor')}
+
+
+
+
+ {t('challenge.admin.tasks.tab.preview')}
+
+
+ {learningMaterial ? (
+
+ {learningMaterial}
+
+ ) : (
+
+ {t('challenge.admin.tasks.preview.empty')}
+
+ )}
+
+
+
+ {t('challenge.admin.tasks.field.learning.material.helper')}
+
+
{/* Hidden Instructions */}
diff --git a/src/types/challenge.ts b/src/types/challenge.ts
index 082ef73..1f23615 100644
--- a/src/types/challenge.ts
+++ b/src/types/challenge.ts
@@ -12,6 +12,7 @@ export interface ChallengeTask {
id: string
title: string
description: string // Markdown
+ learningMaterial?: string // Дополнительный учебный материал в Markdown
hiddenInstructions?: string // Только для преподавателей
creator?: {
sub: string
@@ -121,12 +122,14 @@ export interface APIResponse {
export interface CreateTaskRequest {
title: string
description: string
+ learningMaterial?: string
hiddenInstructions?: string
}
export interface UpdateTaskRequest {
title?: string
description?: string
+ learningMaterial?: string
hiddenInstructions?: string
}