@@ -1,4 +1,4 @@ -import React, { FC, useEffect } from 'react' +import React, { FC, useCallback, useEffect, useRef } from 'react' import { TextFieldProps } from '@mui/material/TextField/TextField' import classes from './model-input.module.scss' @@ -57,6 +57,22 @@ export const ModelInput: FC = ({ const value = externalValue !== undefined ? externalValue : internalValue const setValue = externalOnChange ? (val: string) => externalOnChange(val) : setInternalValue + const valueRef = useRef(value) + const requiredRef = useRef(required) + valueRef.current = value + requiredRef.current = required + + const wrappedSendMessage = useCallback( + (msg: string, req: (string | null)[]) => { + const result = sendMessage(msg, req) + if (result) { + window.dispatchEvent(new CustomEvent('user-sent-message')) + } + return result + }, + [sendMessage] + ) + useEffect(() => { if (input_types) { setRequired(input_types.map((el) => (el.required ? el.type : null))) @@ -82,6 +98,32 @@ export const ModelInput: FC = ({ } }, [resendValue, setValue]) + useEffect(() => { + const handleTourSuggestedQuery = (e: CustomEvent<{ query: string }>) => { + if (e.detail?.query) { + setValue(e.detail.query) + } + } + window.addEventListener('tour-suggested-query', handleTourSuggestedQuery as EventListener) + return () => { + window.removeEventListener('tour-suggested-query', handleTourSuggestedQuery as EventListener) + } + }, [setValue]) + + useEffect(() => { + const handleTourSendMessage = () => { + const { current: msg } = valueRef + const { current: req } = requiredRef + if (msg && sendMessage(msg, req)) { + setValue('') + if (unpinImage) unpinImage() + window.dispatchEvent(new CustomEvent('user-sent-message')) + } + } + window.addEventListener('tour-send-message', handleTourSendMessage) + return () => window.removeEventListener('tour-send-message', handleTourSendMessage) + }, [sendMessage, setValue, unpinImage]) + return ( <>
= ({ if (e.shiftKey && e.key === 'Enter') { } else if (e.key === 'Enter') { e.preventDefault() - const isSend = sendMessage(value, required) + const isSend = wrappedSendMessage(value, required) if (isSend) { setValue('') if (unpinImage) unpinImage() @@ -154,7 +196,7 @@ export const ModelInput: FC = ({ loading={loading} unpinImage={unpinImage} input={value} - sendMessage={sendMessage} + sendMessage={wrappedSendMessage} setInput={externalOnChange ? (val: string | ((prev: string) => string)) => { const newVal = typeof val === 'function' ? val(value) : val @@ -1,4 +1,4 @@ -import { type Tour } from 'nextstepjs' +import { type Step, type Tour } from 'nextstepjs' import { Device } from '#/shared/lib/types/entities' @@ -18,22 +18,44 @@ const baseTourSteps: Tour[] = [ }, { icon: '', - title: 'Ввод запроса', + title: 'Выбор версии модели', + content: 'В этом блоке вы можете выбрать интересующую вас версию модели', + selector: '#text-models-tour-3', + side: 'left', + showControls: true, + showSkip: true, + }, + { + icon: '', + title: 'Введите свой первый запрос', content: 'Используйте это поле для ввода вашего запроса, а затем кликните на кнопку отправки.', selector: '#text-models-tour-2', side: 'top', showControls: true, showSkip: true, - }, + nextButtonText: 'Напиши за меня →', + suggestedQuery: 'Опиши кратко, что такое искусственный интеллект и как он применяется в повседневной жизни', + } as Step, { icon: '', - title: 'Выбор версии модели', - content: 'В этом блоке вы можете выбрать интересующую вас версию модели', - selector: '#text-models-tour-3', - side: 'left', + title: 'Отправить запрос', + content: 'Нажмите на кнопку, чтобы отправить ваш запрос.', + selector: '#text-models-tour-2', + side: 'top', + showSkip: true, + nextButtonText: 'Отправить', + sendMessageOnClick: true, + } as Step, + { + icon: '', + title: 'Поздравляем!', + content: 'Вы завершили тур! Дождитесь ответа от модели и просмотрите результат.', + selector: '#text-models-tour-5', + side: 'bottom', showControls: true, showSkip: true, - }, + nextButtonText: 'Завершить', + } as Step, ], }, { @@ -44,10 +66,20 @@ const baseTourSteps: Tour[] = [ title: 'Множество популярных моделей! 🖼️', content: 'Здесь представлены мощные модели для генерации изображений: Flux, Stable Diffusion, Midjourney, DALL-E и другие. Выберем одну из них для начала работы.', selector: '#images-models-tour-1', + nextRoute: '/images/flux', side: 'top', showControls: true, showSkip: true, }, + { + icon: '', + title: 'Параметры генерации', + content: 'В этом блоке вы можете корректировать параметры генерации изображения', + selector: '#images-models-tour-3', + side: 'left', + showControls: true, + showSkip: true, + }, { icon: '', title: 'Ввод запроса', @@ -56,16 +88,29 @@ const baseTourSteps: Tour[] = [ side: 'bottom', showControls: true, showSkip: true, - }, + nextButtonText: 'Напиши за меня →', + suggestedQuery: 'Кот в космическом скафандре на фоне звёзд', + } as Step, { icon: '', - title: 'Параметры генерации', - content: 'В этом блоке вы можете корректировать параметры генерации изображения', - selector: '#images-models-tour-3', - side: 'left', + title: 'Отправить запрос', + content: 'Нажмите на кнопку, чтобы отправить ваш запрос.', + selector: '#images-models-tour-2', + side: 'top', + showSkip: true, + nextButtonText: 'Отправить', + sendMessageOnClick: true, + } as Step, + { + icon: '', + title: 'Поздравляем!', + content: 'Вы завершили тур! Дождитесь ответа от модели и просмотрите результат.', + selector: '#images-models-tour-5', + side: 'bottom', showControls: true, showSkip: true, - }, + nextButtonText: 'Завершить', + } as Step, ], }, ] @@ -74,7 +119,9 @@ export const getTourSteps = (device: Device): Tour[] => { if (device === 'mobile') { return baseTourSteps.map(tour => ({ ...tour, - steps: tour.steps.map(step => ({ + steps: tour.steps + .filter(step => step.selector !== '#images-models-tour-3' && step.selector !== '#text-models-tour-3') + .map(step => ({ ...step, side: 'bottom' as const, })), @@ -5,6 +5,8 @@ import Image from 'next/image' import styles from './tour-card.module.scss' +const TOUR_SUGGESTED_QUERY_EVENT = 'tour-suggested-query' + export const TourCard: React.FC = ({ step, currentStep, @@ -14,6 +16,23 @@ export const TourCard: React.FC = ({ skipTour, arrow, }) => { + const extendedStep = step as typeof step & { + suggestedQuery?: string + nextButtonText?: string + sendMessageOnClick?: boolean + } + + const handleNextClick = () => { + if (extendedStep.suggestedQuery && typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent(TOUR_SUGGESTED_QUERY_EVENT, { detail: { query: extendedStep.suggestedQuery } }) + ) + } + if (extendedStep.sendMessageOnClick && typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('tour-send-message')) + } + nextStep() + } return ( {step.showSkip && skipTour && ( @@ -61,10 +80,11 @@ export const TourCard: React.FC = ({ )} @@ -59,6 +59,16 @@ export const TourManager: React.FC = ({ children }) => { setTourSelectorShown() } + useEffect(() => { + const handleUserSentMessage = () => { + if (currentTour) { + setCurrentStep(currentStepRef.current + 1) + } + } + window.addEventListener('user-sent-message', handleUserSentMessage) + return () => window.removeEventListener('user-sent-message', handleUserSentMessage) + }, [currentTour, setCurrentStep]) + useEffect(() => { // Проверяем feature flag: tour-manager работает только если onboarding-enabled включен if (!isOnboardingEnabled) { @@ -122,7 +132,7 @@ export const TourManager: React.FC = ({ children }) => { // 1. Тур еще не завершен // 2. TourSelector еще не был показан в текущей сессии // 3. Пользователь зарегшистрировался недавно - if (!isTourCompleted() && !isTourSelectorShown() && isNewUser()) { + if (isNewUser() && !isTourSelectorShown() && !isTourCompleted()) { setTimeout(() => { setShowTourSelector(true) setTourSelectorShown() @@ -190,6 +190,7 @@ const ImageModelPage: NextPageWithLayout = () => { )} { ) : ( - +
({ }} > = memo(