Add optional learningMaterial field to ChallengeTask model and update API endpoints. Introduce LearningMaterialViewer component for displaying additional educational content in Markdown format. Enhance TaskWorkspace to conditionally render learning materials, improving user experience with task-related resources.
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
202
UPDATE.md
Normal file
202
UPDATE.md
Normal file
@@ -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 (
|
||||
<form>
|
||||
<input name="title" value={formData.title} />
|
||||
<textarea name="description" value={formData.description} />
|
||||
|
||||
{/* Новое поле для дополнительного материала */}
|
||||
<label>Дополнительный учебный материал (Markdown)</label>
|
||||
<textarea
|
||||
name="learningMaterial"
|
||||
value={formData.learningMaterial}
|
||||
placeholder="Дополнительные объяснения, ссылки, примеры..."
|
||||
/>
|
||||
|
||||
{/* Только для преподавателей */}
|
||||
<textarea name="hiddenInstructions" value={formData.hiddenInstructions} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Отображение заданий
|
||||
|
||||
При отображении задания студентам показывать `learningMaterial` как дополнительную информацию:
|
||||
|
||||
```typescript
|
||||
const TaskView = ({ task }: { task: ChallengeTask }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>{task.title}</h1>
|
||||
|
||||
{/* Основное описание */}
|
||||
<div dangerouslySetInnerHTML={{ __html: marked(task.description) }} />
|
||||
|
||||
{/* Дополнительный учебный материал */}
|
||||
{task.learningMaterial && (
|
||||
<div className="learning-material">
|
||||
<h2>Дополнительные материалы</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: marked(task.learningMaterial) }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Миграция данных
|
||||
|
||||
Поле `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 автоматически
|
||||
@@ -12,6 +12,7 @@ export interface ChallengeTask {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
learningMaterial?: string
|
||||
hiddenInstructions?: string
|
||||
creator?: Record<string, unknown>
|
||||
createdAt: string
|
||||
|
||||
271
src/components/personal/LearningMaterialViewer.tsx
Normal file
271
src/components/personal/LearningMaterialViewer.tsx
Normal file
@@ -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 (
|
||||
<Box
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
borderColor="blue.200"
|
||||
p={4}
|
||||
bg="blue.50"
|
||||
shadow="sm"
|
||||
>
|
||||
<VStack align="stretch" gap={3}>
|
||||
<HStack justify="space-between" align="center">
|
||||
<Text fontSize="lg" fontWeight="bold" color="blue.800">
|
||||
Дополнительные материалы
|
||||
</Text>
|
||||
{totalPages > 1 && (
|
||||
<Text fontSize="sm" color="blue.600">
|
||||
Страница {currentPage + 1} из {totalPages}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Box
|
||||
color="gray.700"
|
||||
fontSize="sm"
|
||||
lineHeight="1.7"
|
||||
css={{
|
||||
// Заголовки
|
||||
'& h1': {
|
||||
fontSize: '1.75em',
|
||||
fontWeight: '700',
|
||||
marginTop: '1.2em',
|
||||
marginBottom: '0.6em',
|
||||
color: '#2D3748',
|
||||
borderBottom: '2px solid #E2E8F0',
|
||||
paddingBottom: '0.3em'
|
||||
},
|
||||
'& h2': {
|
||||
fontSize: '1.5em',
|
||||
fontWeight: '600',
|
||||
marginTop: '1em',
|
||||
marginBottom: '0.5em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h3': {
|
||||
fontSize: '1.25em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.8em',
|
||||
marginBottom: '0.4em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h4': {
|
||||
fontSize: '1.1em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.6em',
|
||||
marginBottom: '0.3em',
|
||||
color: '#4A5568'
|
||||
},
|
||||
// Параграфы
|
||||
'& p': {
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
lineHeight: '1.8'
|
||||
},
|
||||
// Списки
|
||||
'& ul, & ol': {
|
||||
marginLeft: '1.5em',
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
paddingLeft: '0.5em'
|
||||
},
|
||||
'& li': {
|
||||
marginTop: '0.4em',
|
||||
marginBottom: '0.4em',
|
||||
lineHeight: '1.7'
|
||||
},
|
||||
'& li > 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'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{pages[currentPage]}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<HStack justify="center" gap={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={goToPrevious}
|
||||
// @ts-expect-error Chakra UI v2 uses isDisabled
|
||||
isDisabled={currentPage === 0}
|
||||
leftIcon={<Text>←</Text>}
|
||||
>
|
||||
Предыдущая
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={goToNext}
|
||||
// @ts-expect-error Chakra UI v2 uses isDisabled
|
||||
isDisabled={currentPage === totalPages - 1}
|
||||
rightIcon={<Text>→</Text>}
|
||||
>
|
||||
Следующая
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof finalSubmission>(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) => {
|
||||
<Text fontSize="lg" fontWeight="bold" mb={3} color="gray.800">
|
||||
{task.title}
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
color="gray.700"
|
||||
fontSize="sm"
|
||||
lineHeight="1.7"
|
||||
css={{
|
||||
// Заголовки
|
||||
'& h1': {
|
||||
fontSize: '1.75em',
|
||||
fontWeight: '700',
|
||||
marginTop: '1.2em',
|
||||
'& h1': {
|
||||
fontSize: '1.75em',
|
||||
fontWeight: '700',
|
||||
marginTop: '1.2em',
|
||||
marginBottom: '0.6em',
|
||||
color: '#2D3748',
|
||||
borderBottom: '2px solid #E2E8F0',
|
||||
paddingBottom: '0.3em'
|
||||
},
|
||||
'& h2': {
|
||||
fontSize: '1.5em',
|
||||
fontWeight: '600',
|
||||
marginTop: '1em',
|
||||
'& h2': {
|
||||
fontSize: '1.5em',
|
||||
fontWeight: '600',
|
||||
marginTop: '1em',
|
||||
marginBottom: '0.5em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h3': {
|
||||
fontSize: '1.25em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.8em',
|
||||
'& h3': {
|
||||
fontSize: '1.25em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.8em',
|
||||
marginBottom: '0.4em',
|
||||
color: '#2D3748'
|
||||
},
|
||||
'& h4': {
|
||||
fontSize: '1.1em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.6em',
|
||||
'& h4': {
|
||||
fontSize: '1.1em',
|
||||
fontWeight: '600',
|
||||
marginTop: '0.6em',
|
||||
marginBottom: '0.3em',
|
||||
color: '#4A5568'
|
||||
},
|
||||
// Параграфы
|
||||
'& p': {
|
||||
marginTop: '0.75em',
|
||||
'& p': {
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
lineHeight: '1.8'
|
||||
},
|
||||
// Списки
|
||||
'& ul, & ol': {
|
||||
marginLeft: '1.5em',
|
||||
marginTop: '0.75em',
|
||||
'& ul, & ol': {
|
||||
marginLeft: '1.5em',
|
||||
marginTop: '0.75em',
|
||||
marginBottom: '0.75em',
|
||||
paddingLeft: '0.5em'
|
||||
},
|
||||
'& li': {
|
||||
marginTop: '0.4em',
|
||||
'& li': {
|
||||
marginTop: '0.4em',
|
||||
marginBottom: '0.4em',
|
||||
lineHeight: '1.7'
|
||||
},
|
||||
@@ -135,41 +140,41 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
||||
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) => {
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{task.description}</ReactMarkdown>
|
||||
</Box>
|
||||
|
||||
{/* Кнопка для показа дополнительного материала */}
|
||||
{task.learningMaterial && !showLearningMaterial && (
|
||||
<HStack justify="center" mt={4}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={() => setShowLearningMaterial(true)}
|
||||
>
|
||||
📚 Прочитать доп. материал
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Дополнительные материалы - показываются отдельно */}
|
||||
{task.learningMaterial && showLearningMaterial && (
|
||||
<LearningMaterialViewer
|
||||
content={task.learningMaterial}
|
||||
linesPerPage={30}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* Статус проверки и результат - фиксированное место */}
|
||||
<Box minH="80px">
|
||||
{queueStatus && !finalSubmission ? (
|
||||
<Box
|
||||
borderWidth="2px"
|
||||
borderRadius="lg"
|
||||
borderColor="blue.300"
|
||||
bg="blue.50"
|
||||
<Box
|
||||
borderWidth="2px"
|
||||
borderRadius="lg"
|
||||
borderColor="blue.300"
|
||||
bg="blue.50"
|
||||
p={4}
|
||||
>
|
||||
<VStack gap={3} align="stretch">
|
||||
@@ -262,13 +291,13 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
|
||||
Проверяем решение...
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
|
||||
<Box>
|
||||
{/* Кастомный прогресс-бар */}
|
||||
<Box
|
||||
bg="blue.100"
|
||||
borderRadius="md"
|
||||
h="24px"
|
||||
<Box
|
||||
bg="blue.100"
|
||||
borderRadius="md"
|
||||
h="24px"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user