courceNameSuggestion feature
This commit is contained in:
parent
5debb1ebe9
commit
386d2b409d
@ -44,6 +44,11 @@ module.exports = {
|
|||||||
value: '',
|
value: '',
|
||||||
key: 'courses.statistics',
|
key: 'courses.statistics',
|
||||||
},
|
},
|
||||||
|
'courceNameSuggestion': {
|
||||||
|
on: true,
|
||||||
|
value: '',
|
||||||
|
key: 'courceNameSuggestion',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
|
@ -73,6 +73,14 @@ export const api = createApi({
|
|||||||
query: (courseId) => `/lesson/${courseId}/ai/generate-lessons`,
|
query: (courseId) => `/lesson/${courseId}/ai/generate-lessons`,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
generateLessonName: builder.mutation<BaseResponse<{ name: string }[]>, { courseId: string, name: string }>({
|
||||||
|
query: ({ courseId, name }) => ({
|
||||||
|
url: `/lesson/${courseId}/ai/generate-lesson-name`,
|
||||||
|
method: 'POST',
|
||||||
|
body: { name },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
createLesson: builder.mutation<
|
createLesson: builder.mutation<
|
||||||
BaseResponse<Lesson>,
|
BaseResponse<Lesson>,
|
||||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||||
|
@ -119,6 +119,7 @@ export const Item: React.FC<ItemProps> = ({
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setEdit(false)
|
setEdit(false)
|
||||||
}}
|
}}
|
||||||
|
courseId={courseId}
|
||||||
lesson={{ _id: id, id, name, date }}
|
lesson={{ _id: id, id, name, date }}
|
||||||
title={t('journal.pl.lesson.editTitle')}
|
title={t('journal.pl.lesson.editTitle')}
|
||||||
nameButton={t('journal.pl.save')}
|
nameButton={t('journal.pl.save')}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
|
import { getFeatures } from '@brojs/cli'
|
||||||
import { useForm, Controller } from 'react-hook-form'
|
import { useForm, Controller } from 'react-hook-form'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -29,7 +30,11 @@ import {
|
|||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
IconButton,
|
IconButton,
|
||||||
Center
|
Center,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
List,
|
||||||
|
ListItem
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { AddIcon, CheckIcon, WarningIcon, RepeatIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
|
import { AddIcon, CheckIcon, WarningIcon, RepeatIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -40,6 +45,9 @@ import { formatDate } from '../../../utils/dayjs-config'
|
|||||||
import { dateToCalendarFormat } from '../../../utils/time'
|
import { dateToCalendarFormat } from '../../../utils/time'
|
||||||
import { Lesson } from '../../../__data__/model'
|
import { Lesson } from '../../../__data__/model'
|
||||||
import { ErrorSpan } from '../style'
|
import { ErrorSpan } from '../style'
|
||||||
|
import { api } from '../../../__data__/api/api'
|
||||||
|
|
||||||
|
const courceNameSuggestion = getFeatures('journal')['courceNameSuggestion']
|
||||||
|
|
||||||
interface NewLessonForm {
|
interface NewLessonForm {
|
||||||
name: string
|
name: string
|
||||||
@ -49,6 +57,7 @@ interface NewLessonForm {
|
|||||||
|
|
||||||
interface LessonFormProps {
|
interface LessonFormProps {
|
||||||
lesson?: Partial<Lesson> | any // Разрешаем передавать как Lesson, так и AI-сгенерированный урок
|
lesson?: Partial<Lesson> | any // Разрешаем передавать как Lesson, так и AI-сгенерированный урок
|
||||||
|
courseId: string
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
onSubmit: (lesson: Lesson) => void
|
onSubmit: (lesson: Lesson) => void
|
||||||
@ -65,6 +74,7 @@ interface LessonFormProps {
|
|||||||
|
|
||||||
export const LessonForm = ({
|
export const LessonForm = ({
|
||||||
lesson,
|
lesson,
|
||||||
|
courseId,
|
||||||
isLoading,
|
isLoading,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -85,6 +95,72 @@ export const LessonForm = ({
|
|||||||
const suggestionHoverBgColor = useColorModeValue('blue.100', 'blue.800')
|
const suggestionHoverBgColor = useColorModeValue('blue.100', 'blue.800')
|
||||||
const borderColor = useColorModeValue('blue.200', 'blue.700')
|
const borderColor = useColorModeValue('blue.200', 'blue.700')
|
||||||
const textSecondaryColor = useColorModeValue('gray.600', 'gray.400')
|
const textSecondaryColor = useColorModeValue('gray.600', 'gray.400')
|
||||||
|
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const suggestionsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
const [generateLessonName, {
|
||||||
|
data: generateLessonNameData,
|
||||||
|
isLoading: isLoadingGenerateLessonName,
|
||||||
|
error: errorGenerateLessonName,
|
||||||
|
isSuccess: isSuccessGenerateLessonName
|
||||||
|
}] = api.useGenerateLessonNameMutation()
|
||||||
|
|
||||||
|
// Функция debounce для запросов
|
||||||
|
const debouncedGenerateName = useCallback((value: string) => {
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (value.length > 2) {
|
||||||
|
generateLessonName({ courseId: courseId, name: value })
|
||||||
|
} else {
|
||||||
|
setSuggestions([])
|
||||||
|
}
|
||||||
|
}, 300) // 300ms задержка
|
||||||
|
}, [courseId, generateLessonName])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccessGenerateLessonName) {
|
||||||
|
setSuggestions(generateLessonNameData.body.map(suggestion => suggestion.name))
|
||||||
|
}
|
||||||
|
}, [isSuccessGenerateLessonName])
|
||||||
|
|
||||||
|
// Эффект для корректного позиционирования списка подсказок
|
||||||
|
useEffect(() => {
|
||||||
|
const positionSuggestions = () => {
|
||||||
|
if (inputRef.current && suggestionsContainerRef.current && showSuggestions) {
|
||||||
|
const inputRect = inputRef.current.getBoundingClientRect()
|
||||||
|
suggestionsContainerRef.current.style.top = `${inputRect.bottom + window.scrollY}px`
|
||||||
|
suggestionsContainerRef.current.style.left = `${inputRect.left + window.scrollX}px`
|
||||||
|
suggestionsContainerRef.current.style.width = `${inputRect.width}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
positionSuggestions()
|
||||||
|
|
||||||
|
// Обновляем позицию при скролле или изменении размера окна
|
||||||
|
window.addEventListener('scroll', positionSuggestions)
|
||||||
|
window.addEventListener('resize', positionSuggestions)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', positionSuggestions)
|
||||||
|
window.removeEventListener('resize', positionSuggestions)
|
||||||
|
}
|
||||||
|
}, [showSuggestions, suggestions.length])
|
||||||
|
|
||||||
|
// Эффект для очистки timeout при размонтировании компонента
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const getNearestTimeSlot = () => {
|
const getNearestTimeSlot = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -149,27 +225,6 @@ export const LessonForm = ({
|
|||||||
return slots;
|
return slots;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNextTimeSlots = (date: string, count: number = 3) => {
|
|
||||||
const currentDate = new Date();
|
|
||||||
const selectedDate = new Date(date);
|
|
||||||
const isToday = selectedDate.toDateString() === currentDate.toDateString();
|
|
||||||
|
|
||||||
if (!isToday) return [];
|
|
||||||
|
|
||||||
const currentMinutes = currentDate.getHours() * 60 + currentDate.getMinutes();
|
|
||||||
const slots = generateTimeSlots();
|
|
||||||
|
|
||||||
return slots
|
|
||||||
.map(slot => {
|
|
||||||
const [hours, minutes] = slot.split(':').map(Number);
|
|
||||||
const slotMinutes = hours * 60 + minutes;
|
|
||||||
return { slot, minutes: slotMinutes };
|
|
||||||
})
|
|
||||||
.filter(({ minutes }) => minutes > currentMinutes)
|
|
||||||
.slice(0, count)
|
|
||||||
.map(({ slot }) => slot);
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeGroups = {
|
const timeGroups = {
|
||||||
[`${t('journal.pl.days.morning')} (8-12)`]: generateTimeSlots().filter(slot => {
|
[`${t('journal.pl.days.morning')} (8-12)`]: generateTimeSlots().filter(slot => {
|
||||||
const hour = parseInt(slot.split(':')[0]);
|
const hour = parseInt(slot.split(':')[0]);
|
||||||
@ -490,12 +545,30 @@ export const LessonForm = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl isRequired isInvalid={Boolean(errors.name)}>
|
<FormControl isRequired isInvalid={Boolean(errors.name)}>
|
||||||
<FormLabel>{t('journal.pl.lesson.form.title')}</FormLabel>
|
<FormLabel>{t('journal.pl.lesson.form.title')}</FormLabel>
|
||||||
<Input
|
<Box position="relative">
|
||||||
{...field}
|
<InputGroup>
|
||||||
required={false}
|
<Input
|
||||||
placeholder={t('journal.pl.lesson.form.namePlaceholder')}
|
{...field}
|
||||||
size="md"
|
ref={inputRef}
|
||||||
/>
|
required={false}
|
||||||
|
placeholder={t('journal.pl.lesson.form.namePlaceholder')}
|
||||||
|
size="md"
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
setInputValue(value)
|
||||||
|
field.onChange(value)
|
||||||
|
if (value.length > 2 && courceNameSuggestion) {
|
||||||
|
setShowSuggestions(true)
|
||||||
|
debouncedGenerateName(value)
|
||||||
|
} else {
|
||||||
|
setSuggestions([])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => setShowSuggestions(true)}
|
||||||
|
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Box>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<FormErrorMessage>{errors.name.message}</FormErrorMessage>
|
<FormErrorMessage>{errors.name.message}</FormErrorMessage>
|
||||||
)}
|
)}
|
||||||
@ -610,6 +683,39 @@ export const LessonForm = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
|
{/* Выпадающий список подсказок (размещаем вне стандартного потока документа) */}
|
||||||
|
{suggestions.length > 0 && showSuggestions && (
|
||||||
|
<Box
|
||||||
|
position="fixed"
|
||||||
|
ref={suggestionsContainerRef}
|
||||||
|
bg={useColorModeValue('white', 'gray.800')}
|
||||||
|
borderRadius="md"
|
||||||
|
boxShadow="md"
|
||||||
|
zIndex={9999}
|
||||||
|
maxH="200px"
|
||||||
|
overflowY="auto"
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{suggestions.map((suggestion, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={index}
|
||||||
|
p={2}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ bg: useColorModeValue('gray.100', 'gray.700') }}
|
||||||
|
onClick={() => {
|
||||||
|
setValue('name', suggestion)
|
||||||
|
setInputValue(suggestion)
|
||||||
|
setSuggestions([])
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{suggestion}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,23 @@ router.get('/lesson/:courseId/ai/generate-lessons', timer(3000), (req, res) => {
|
|||||||
res.send(modifiedData);
|
res.send(modifiedData);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/lesson/:courseId/ai/generate-lesson-name', timer(3000), (req, res) => {
|
||||||
|
res.send({
|
||||||
|
"success": true,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"name": "Основы CSS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CSS селекторы и свойства"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Анимации и переходы на CSS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
router.post('/lesson', (req, res) => {
|
router.post('/lesson', (req, res) => {
|
||||||
const baseData = readJsonFile('../mocks/lessons/create/success.json');
|
const baseData = readJsonFile('../mocks/lessons/create/success.json');
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user