courceNameSuggestion feature
This commit is contained in:
parent
5debb1ebe9
commit
386d2b409d
@ -44,6 +44,11 @@ module.exports = {
|
||||
value: '',
|
||||
key: 'courses.statistics',
|
||||
},
|
||||
'courceNameSuggestion': {
|
||||
on: true,
|
||||
value: '',
|
||||
key: 'courceNameSuggestion',
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
@ -73,6 +73,14 @@ export const api = createApi({
|
||||
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<
|
||||
BaseResponse<Lesson>,
|
||||
Partial<Lesson> & Pick<Lesson, 'name' | 'date'> & { courseId: string }
|
||||
|
@ -119,6 +119,7 @@ export const Item: React.FC<ItemProps> = ({
|
||||
onCancel={() => {
|
||||
setEdit(false)
|
||||
}}
|
||||
courseId={courseId}
|
||||
lesson={{ _id: id, id, name, date }}
|
||||
title={t('journal.pl.lesson.editTitle')}
|
||||
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 {
|
||||
Box,
|
||||
@ -29,7 +30,11 @@ import {
|
||||
Wrap,
|
||||
WrapItem,
|
||||
IconButton,
|
||||
Center
|
||||
Center,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
List,
|
||||
ListItem
|
||||
} from '@chakra-ui/react'
|
||||
import { AddIcon, CheckIcon, WarningIcon, RepeatIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -40,6 +45,9 @@ import { formatDate } from '../../../utils/dayjs-config'
|
||||
import { dateToCalendarFormat } from '../../../utils/time'
|
||||
import { Lesson } from '../../../__data__/model'
|
||||
import { ErrorSpan } from '../style'
|
||||
import { api } from '../../../__data__/api/api'
|
||||
|
||||
const courceNameSuggestion = getFeatures('journal')['courceNameSuggestion']
|
||||
|
||||
interface NewLessonForm {
|
||||
name: string
|
||||
@ -49,6 +57,7 @@ interface NewLessonForm {
|
||||
|
||||
interface LessonFormProps {
|
||||
lesson?: Partial<Lesson> | any // Разрешаем передавать как Lesson, так и AI-сгенерированный урок
|
||||
courseId: string
|
||||
isLoading: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (lesson: Lesson) => void
|
||||
@ -65,6 +74,7 @@ interface LessonFormProps {
|
||||
|
||||
export const LessonForm = ({
|
||||
lesson,
|
||||
courseId,
|
||||
isLoading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
@ -85,6 +95,72 @@ export const LessonForm = ({
|
||||
const suggestionHoverBgColor = useColorModeValue('blue.100', 'blue.800')
|
||||
const borderColor = useColorModeValue('blue.200', 'blue.700')
|
||||
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 now = new Date();
|
||||
@ -149,27 +225,6 @@ export const LessonForm = ({
|
||||
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 = {
|
||||
[`${t('journal.pl.days.morning')} (8-12)`]: generateTimeSlots().filter(slot => {
|
||||
const hour = parseInt(slot.split(':')[0]);
|
||||
@ -490,12 +545,30 @@ export const LessonForm = ({
|
||||
render={({ field }) => (
|
||||
<FormControl isRequired isInvalid={Boolean(errors.name)}>
|
||||
<FormLabel>{t('journal.pl.lesson.form.title')}</FormLabel>
|
||||
<Box position="relative">
|
||||
<InputGroup>
|
||||
<Input
|
||||
{...field}
|
||||
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 && (
|
||||
<FormErrorMessage>{errors.name.message}</FormErrorMessage>
|
||||
)}
|
||||
@ -610,6 +683,39 @@ export const LessonForm = ({
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -131,6 +131,23 @@ router.get('/lesson/:courseId/ai/generate-lessons', timer(3000), (req, res) => {
|
||||
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) => {
|
||||
const baseData = readJsonFile('../mocks/lessons/create/success.json');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user