Добавлены новые функции генерации уроков с использованием ИИ в компонент LessonList и соответствующие изменения в форме создания урока. Обновлены локализации для поддержки новых функций. Реализован API для генерации уроков и добавлены тестовые данные для имитации ответов сервера.
This commit is contained in:
parent
b00fd32042
commit
e178ce5cd6
@ -11,7 +11,6 @@
|
||||
"journal.pl.common.edit": "Edit",
|
||||
"journal.pl.common.delete": "Delete",
|
||||
"journal.pl.common.save": "Save",
|
||||
"journal.pl.common.cancel": "Cancel",
|
||||
"journal.pl.common.students": "students",
|
||||
"journal.pl.common.teachers": "teachers",
|
||||
"journal.pl.common.date": "Date",
|
||||
@ -31,7 +30,7 @@
|
||||
"journal.pl.common.cancel": "Cancel",
|
||||
"journal.pl.common.journal": "Journal",
|
||||
"journal.pl.common.course": "Course",
|
||||
"journal.pl.common.lesson": "Lesson",
|
||||
"journal.pl.common.lesson": "Lessons",
|
||||
"journal.pl.common.marked": "Marked",
|
||||
"journal.pl.common.people": "people",
|
||||
"journal.pl.common.success": "Success",
|
||||
@ -75,6 +74,11 @@
|
||||
"journal.pl.lesson.action": "Actions",
|
||||
"journal.pl.lesson.expand": "Expand lesson list",
|
||||
"journal.pl.lesson.collapse": "Collapse lesson list",
|
||||
"journal.pl.lesson.aiSuggested": "AI Suggested Lessons",
|
||||
"journal.pl.lesson.aiSuggestedDescription": "These lessons were automatically generated based on your course schedule",
|
||||
"journal.pl.lesson.createFromSuggestion": "Create",
|
||||
"journal.pl.lesson.aiGenerated": "AI generated content",
|
||||
"journal.pl.lesson.generatingAiSuggestions": "Generating AI lesson suggestions...",
|
||||
|
||||
"journal.pl.exam.title": "Exam",
|
||||
"journal.pl.exam.startExam": "Start exam",
|
||||
|
@ -27,7 +27,7 @@
|
||||
"journal.pl.common.cancel": "Отмена",
|
||||
"journal.pl.common.journal": "Журнал",
|
||||
"journal.pl.common.course": "Курс",
|
||||
"journal.pl.common.lesson": "Лекция",
|
||||
"journal.pl.common.lesson": "Лекций",
|
||||
"journal.pl.common.marked": "Отмечено",
|
||||
"journal.pl.common.people": "человек",
|
||||
"journal.pl.common.success": "Успешно",
|
||||
@ -71,6 +71,11 @@
|
||||
"journal.pl.lesson.action": "Действия",
|
||||
"journal.pl.lesson.expand": "Развернуть список занятий",
|
||||
"journal.pl.lesson.collapse": "Свернуть список занятий",
|
||||
"journal.pl.lesson.aiSuggested": "Рекомендации ИИ",
|
||||
"journal.pl.lesson.aiSuggestedDescription": "Эти занятия были автоматически сгенерированы на основе вашего расписания курса",
|
||||
"journal.pl.lesson.createFromSuggestion": "Создать",
|
||||
"journal.pl.lesson.aiGenerated": "Сгенерировано ИИ",
|
||||
"journal.pl.lesson.generatingAiSuggestions": "Генерация рекомендаций ИИ...",
|
||||
|
||||
"journal.pl.exam.title": "Экзамен",
|
||||
"journal.pl.exam.startExam": "Начать экзамен",
|
||||
|
@ -68,6 +68,11 @@ export const api = createApi({
|
||||
query: (courseId) => `/lesson/list/${courseId}`,
|
||||
providesTags: ['LessonList'],
|
||||
}),
|
||||
|
||||
generateLessons: builder.mutation<BaseResponse<{ date: string; name: string }[]>, string>({
|
||||
query: (courseId) => `/lesson/${courseId}/ai/generate-lessons`,
|
||||
}),
|
||||
|
||||
createLesson: builder.mutation<
|
||||
BaseResponse<Lesson>,
|
||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||
|
@ -6,18 +6,22 @@ import {
|
||||
useColorMode
|
||||
} from '@chakra-ui/react'
|
||||
|
||||
export const XlSpinner = () => {
|
||||
interface XlSpinnerProps {
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const XlSpinner: React.FC<XlSpinnerProps> = ({ size = 'xl' }) => {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl">
|
||||
<Center h="300px">
|
||||
<Center h={size === 'sm' ? 'auto' : '300px'}>
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor={colorMode === 'light' ? 'gray.200' : 'gray.600'}
|
||||
color={colorMode === 'light' ? 'blue.500' : 'blue.300'}
|
||||
size="xl"
|
||||
size={size}
|
||||
/>
|
||||
</Center>
|
||||
</Container>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import {
|
||||
Box,
|
||||
@ -14,9 +14,22 @@ import {
|
||||
FormHelperText,
|
||||
FormErrorMessage,
|
||||
Input,
|
||||
Flex,
|
||||
Icon,
|
||||
Text,
|
||||
Badge,
|
||||
useColorModeValue,
|
||||
HStack,
|
||||
Divider,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
useStyleConfig
|
||||
} from '@chakra-ui/react'
|
||||
import { AddIcon } from '@chakra-ui/icons'
|
||||
import { AddIcon, CheckIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FaRobot } from 'react-icons/fa'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { dateToCalendarFormat } from '../../../utils/time'
|
||||
import { Lesson } from '../../../__data__/model'
|
||||
@ -28,13 +41,17 @@ interface NewLessonForm {
|
||||
}
|
||||
|
||||
interface LessonFormProps {
|
||||
lesson?: Partial<Lesson>
|
||||
lesson?: Partial<Lesson> | any // Разрешаем передавать как Lesson, так и AI-сгенерированный урок
|
||||
isLoading: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (lesson: Lesson) => void
|
||||
error?: string
|
||||
title: string
|
||||
nameButton: string
|
||||
aiSuggestions?: any[] // Список предложений от ИИ
|
||||
isLoadingAiSuggestions?: boolean // Индикатор загрузки предложений
|
||||
onSelectAiSuggestion?: (suggestion: any) => void // Обработчик выбора предложения
|
||||
selectedAiSuggestion?: any // Выбранное предложение
|
||||
}
|
||||
|
||||
export const LessonForm = ({
|
||||
@ -45,8 +62,18 @@ export const LessonForm = ({
|
||||
error,
|
||||
title,
|
||||
nameButton,
|
||||
aiSuggestions = [],
|
||||
isLoadingAiSuggestions = false,
|
||||
onSelectAiSuggestion = () => {},
|
||||
selectedAiSuggestion
|
||||
}: LessonFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isAiSuggested = lesson && !lesson._id && !lesson.id
|
||||
const aiHighlightColor = useColorModeValue('blue.100', 'blue.800')
|
||||
const suggestionBgColor = useColorModeValue('blue.50', 'blue.900')
|
||||
const suggestionHoverBgColor = useColorModeValue('blue.100', 'blue.800')
|
||||
const borderColor = useColorModeValue('blue.200', 'blue.700')
|
||||
const textSecondaryColor = useColorModeValue('gray.600', 'gray.400')
|
||||
|
||||
const getNearestTimeSlot = () => {
|
||||
const now = new Date();
|
||||
@ -68,6 +95,7 @@ export const LessonForm = ({
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<NewLessonForm>({
|
||||
defaultValues: (lesson && {
|
||||
@ -79,12 +107,41 @@ export const LessonForm = ({
|
||||
},
|
||||
})
|
||||
|
||||
// Рендерим скелетон для предложений ИИ
|
||||
const renderSkeletons = () => {
|
||||
return (
|
||||
<VStack spacing={3} align="stretch" mt={4}>
|
||||
<Skeleton height="20px" width="70%" />
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3}>
|
||||
{[1, 2, 3, 4, 5].map(idx => (
|
||||
<Skeleton key={idx} height="60px" borderRadius="md" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
// Применяем выбранное предложение к форме
|
||||
const handleSelectSuggestion = (suggestion) => {
|
||||
setValue('name', suggestion.name)
|
||||
setValue('date', dateToCalendarFormat(suggestion.date))
|
||||
onSelectAiSuggestion(suggestion)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card align="left">
|
||||
<Card align="left" bg={isAiSuggested ? aiHighlightColor : undefined}>
|
||||
<CardHeader display="flex">
|
||||
<Heading as="h2" mt="0">
|
||||
{title}
|
||||
</Heading>
|
||||
<Flex align="center">
|
||||
<Heading as="h2" mt="0">
|
||||
{title}
|
||||
</Heading>
|
||||
{isAiSuggested && (
|
||||
<Badge colorScheme="blue" ml={2} display="flex" alignItems="center">
|
||||
<Icon as={FaRobot} mr={1} />
|
||||
<Text>{t('journal.pl.lesson.aiGenerated')}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<CloseButton
|
||||
ml="auto"
|
||||
onClick={() => {
|
||||
@ -142,8 +199,8 @@ export const LessonForm = ({
|
||||
<Button
|
||||
size="lg"
|
||||
type="submit"
|
||||
leftIcon={<AddIcon />}
|
||||
colorScheme="blue"
|
||||
leftIcon={isAiSuggested ? <Icon as={FaRobot} /> : <AddIcon />}
|
||||
colorScheme={isAiSuggested ? "blue" : "blue"}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{nameButton}
|
||||
@ -153,6 +210,73 @@ export const LessonForm = ({
|
||||
|
||||
{error && <ErrorSpan>{error}</ErrorSpan>}
|
||||
</form>
|
||||
|
||||
{/* Блок с предложениями ИИ */}
|
||||
{(aiSuggestions.length > 0 || isLoadingAiSuggestions) && (
|
||||
<>
|
||||
<Divider my={6} />
|
||||
|
||||
<Flex align="center" mb={4}>
|
||||
<Icon as={FaRobot} color="blue.500" mr={2} />
|
||||
<Heading size="sm" color="blue.500">
|
||||
{t('journal.pl.lesson.aiSuggested')}
|
||||
</Heading>
|
||||
{!isLoadingAiSuggestions && (
|
||||
<Badge colorScheme="blue" ml={2}>
|
||||
{aiSuggestions.length} {t('journal.pl.common.lesson').toLowerCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isLoadingAiSuggestions ? (
|
||||
renderSkeletons()
|
||||
) : (
|
||||
<>
|
||||
<Text fontSize="sm" color={textSecondaryColor} mb={4}>
|
||||
{t('journal.pl.lesson.aiSuggestedDescription')}
|
||||
</Text>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{aiSuggestions.map((suggestion, index) => {
|
||||
const isSelected = selectedAiSuggestion &&
|
||||
selectedAiSuggestion.name === suggestion.name &&
|
||||
selectedAiSuggestion.date === suggestion.date;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`ai-suggestion-${index}`}
|
||||
bg={isSelected ? suggestionHoverBgColor : suggestionBgColor}
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
borderLeft="3px solid"
|
||||
borderLeftColor={isSelected ? "green.400" : borderColor}
|
||||
cursor="pointer"
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: suggestionHoverBgColor,
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 'sm'
|
||||
}}
|
||||
>
|
||||
<Flex justify="space-between" align="center" mb={1}>
|
||||
<HStack>
|
||||
<Icon as={FaRobot} color="blue.500" boxSize="3" />
|
||||
<Text fontWeight="bold">{suggestion.name}</Text>
|
||||
</HStack>
|
||||
{isSelected && <CheckIcon color="green.400" />}
|
||||
</Flex>
|
||||
<Text fontSize="sm" color={textSecondaryColor}>
|
||||
{dayjs(suggestion.date).format('DD.MM.YYYY HH:mm')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
Tr,
|
||||
Th,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
@ -48,6 +49,13 @@ const LessonList = () => {
|
||||
const { courseId } = useParams()
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading, error, isSuccess } = api.useLessonListQuery(courseId)
|
||||
const [generateLessonsMutation, {
|
||||
data: generateLessons,
|
||||
isLoading: isLoadingGenerateLessons,
|
||||
error: errorGenerateLessons,
|
||||
isSuccess: isSuccessGenerateLessons
|
||||
}, ] = api.useGenerateLessonsMutation()
|
||||
|
||||
const [createLesson, crLQuery] = api.useCreateLessonMutation()
|
||||
const [deleteLesson, deletingRqst] = api.useDeleteLessonMutation()
|
||||
const [updateLesson, updateLessonRqst] = api.useUpdateLessonMutation()
|
||||
@ -58,6 +66,7 @@ const LessonList = () => {
|
||||
const toastRef = useRef(null)
|
||||
const createdLessonRef = useRef(null)
|
||||
const [editLesson, setEditLesson] = useState<Lesson>(null)
|
||||
const [suggestedLessonToCreate, setSuggestedLessonToCreate] = useState(null)
|
||||
const { t } = useTranslation()
|
||||
const sorted = useMemo(
|
||||
() => [...(data?.body || [])]?.sort((a, b) => (a.date > b.date ? 1 : -1)),
|
||||
@ -93,6 +102,18 @@ const LessonList = () => {
|
||||
return lessonsData.sort((a, b) => (a.date < b.date ? 1 : -1))
|
||||
}, [groupByDate, isSuccess, sorted])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTeacher(user) && !isSuccessGenerateLessons) {
|
||||
generateLessonsMutation(courseId)
|
||||
}
|
||||
}, [isSuccessGenerateLessons, user, courseId, generateLessonsMutation])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessGenerateLessons) {
|
||||
console.log(generateLessons)
|
||||
}
|
||||
}, [isSuccessGenerateLessons])
|
||||
|
||||
const onSubmit = (lessonData) => {
|
||||
toastRef.current = toast({
|
||||
title: t('journal.pl.common.sending'),
|
||||
@ -174,6 +195,18 @@ const LessonList = () => {
|
||||
}
|
||||
}, [updateLessonRqst.isSuccess])
|
||||
|
||||
// Обработчик выбора предложения ИИ в форме
|
||||
const handleSelectAiSuggestion = (suggestion) => {
|
||||
setSuggestedLessonToCreate(suggestion)
|
||||
}
|
||||
|
||||
// Очищаем выбранную сгенерированную лекцию при закрытии формы
|
||||
const handleCancelForm = () => {
|
||||
setShowForm(false)
|
||||
setEditLesson(null)
|
||||
setSuggestedLessonToCreate(null)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <XlSpinner />
|
||||
}
|
||||
@ -234,17 +267,18 @@ const LessonList = () => {
|
||||
<Box mt="15" mb="15">
|
||||
{showForm ? (
|
||||
<LessonForm
|
||||
key={editLesson?.id}
|
||||
key={editLesson?.id || 'new-lesson'}
|
||||
isLoading={crLQuery.isLoading}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {
|
||||
setShowForm(false)
|
||||
setEditLesson(null)
|
||||
}}
|
||||
onCancel={handleCancelForm}
|
||||
error={(crLQuery.error as any)?.error}
|
||||
lesson={editLesson}
|
||||
lesson={editLesson || suggestedLessonToCreate || undefined}
|
||||
title={editLesson ? t('journal.pl.lesson.editTitle') : t('journal.pl.lesson.createTitle')}
|
||||
nameButton={editLesson ? t('journal.pl.edit') : t('journal.pl.common.create')}
|
||||
aiSuggestions={generateLessons?.body || []}
|
||||
isLoadingAiSuggestions={isLoadingGenerateLessons}
|
||||
onSelectAiSuggestion={handleSelectAiSuggestion}
|
||||
selectedAiSuggestion={suggestedLessonToCreate}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
|
@ -45,6 +45,12 @@ router.get('/lesson/list/:courseId', (req, res) => {
|
||||
res.send(require('../mocks/lessons/list/success.json'))
|
||||
})
|
||||
|
||||
|
||||
// https://platform.bro-js.ru/jrnl-bh/api/lesson/67cf0c9f2f4241c6fc29f464/ai/generate-lessons
|
||||
router.get('/lesson/:courseId/ai/generate-lessons', timer(3000), (req, res) => {
|
||||
res.send(require('../mocks/lessons/generate/success.json'))
|
||||
})
|
||||
|
||||
router.post('/lesson', (req, res) => {
|
||||
res.send(require('../mocks/lessons/create/success.json'))
|
||||
})
|
||||
|
25
stubs/mocks/lessons/generate/success.json
Normal file
25
stubs/mocks/lessons/generate/success.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"success": true,
|
||||
"body": [
|
||||
{
|
||||
"date": "2025-03-26",
|
||||
"name": "Создание первого агента"
|
||||
},
|
||||
{
|
||||
"date": "2025-03-31",
|
||||
"name": "Работа с памятью агентов"
|
||||
},
|
||||
{
|
||||
"date": "2025-04-02",
|
||||
"name": "Интеграция инструментов в агентов"
|
||||
},
|
||||
{
|
||||
"date": "2025-04-07",
|
||||
"name": "Управление цепочками рассуждений"
|
||||
},
|
||||
{
|
||||
"date": "2025-04-09",
|
||||
"name": "Оптимизация производительности агентов"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user