@@ -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 = () => {
)}
{
>
) : (
-
+