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> | ||||
|                   <Input | ||||
|                     {...field} | ||||
|                     required={false} | ||||
|                     placeholder={t('journal.pl.lesson.form.namePlaceholder')} | ||||
|                     size="md" | ||||
|                   /> | ||||
|                   <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