From 386d2b409d31e3864cff76a7ed9b96545a416016 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Sun, 6 Apr 2025 17:44:26 +0300 Subject: [PATCH] courceNameSuggestion feature --- bro.config.js | 5 + src/__data__/api/api.ts | 8 + src/pages/lesson-list/components/item.tsx | 1 + .../lesson-list/components/lessons-form.tsx | 164 ++++++++++++++---- stubs/api/index.js | 17 ++ 5 files changed, 166 insertions(+), 29 deletions(-) diff --git a/bro.config.js b/bro.config.js index c376278..a2ab2d9 100644 --- a/bro.config.js +++ b/bro.config.js @@ -44,6 +44,11 @@ module.exports = { value: '', key: 'courses.statistics', }, + 'courceNameSuggestion': { + on: true, + value: '', + key: 'courceNameSuggestion', + }, }, }, config: { diff --git a/src/__data__/api/api.ts b/src/__data__/api/api.ts index 799b1c7..77178ac 100644 --- a/src/__data__/api/api.ts +++ b/src/__data__/api/api.ts @@ -73,6 +73,14 @@ export const api = createApi({ query: (courseId) => `/lesson/${courseId}/ai/generate-lessons`, }), + generateLessonName: builder.mutation, { courseId: string, name: string }>({ + query: ({ courseId, name }) => ({ + url: `/lesson/${courseId}/ai/generate-lesson-name`, + method: 'POST', + body: { name }, + }), + }), + createLesson: builder.mutation< BaseResponse, Partial & Pick & { courseId: string } diff --git a/src/pages/lesson-list/components/item.tsx b/src/pages/lesson-list/components/item.tsx index df92be0..eff9420 100644 --- a/src/pages/lesson-list/components/item.tsx +++ b/src/pages/lesson-list/components/item.tsx @@ -119,6 +119,7 @@ export const Item: React.FC = ({ onCancel={() => { setEdit(false) }} + courseId={courseId} lesson={{ _id: id, id, name, date }} title={t('journal.pl.lesson.editTitle')} nameButton={t('journal.pl.save')} diff --git a/src/pages/lesson-list/components/lessons-form.tsx b/src/pages/lesson-list/components/lessons-form.tsx index 51970cd..6eff60c 100644 --- a/src/pages/lesson-list/components/lessons-form.tsx +++ b/src/pages/lesson-list/components/lessons-form.tsx @@ -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 | 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([]) + const [showSuggestions, setShowSuggestions] = useState(false) + const [inputValue, setInputValue] = useState('') + const inputRef = useRef(null) + const suggestionsContainerRef = useRef(null) + const debounceTimeoutRef = useRef(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 }) => ( {t('journal.pl.lesson.form.title')} - + + + { + 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)} + /> + + {errors.name && ( {errors.name.message} )} @@ -610,6 +683,39 @@ export const LessonForm = ({ )} + + {/* Выпадающий список подсказок (размещаем вне стандартного потока документа) */} + {suggestions.length > 0 && showSuggestions && ( + + + {suggestions.map((suggestion, index) => ( + { + setValue('name', suggestion) + setInputValue(suggestion) + setSuggestions([]) + setShowSuggestions(false) + }} + > + {suggestion} + + ))} + + + )} ) } diff --git a/stubs/api/index.js b/stubs/api/index.js index 3a7c81a..aa1689e 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -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');