@@ -27,6 +27,7 @@ export const SendBtn = ({ loading, unpinImage, input, sendMessage, setInput, req } return ( { if (!loading) { const isSend = sendMessage(input, required) @@ -150,7 +150,7 @@ background-color: rgba(0, 0, 0, 0.7); border-radius: 15px; padding: 20px; - z-index: 1000; + z-index: 10; &:hover { outline: 4px solid #8280ff66; outline-offset: -3px; @@ -148,7 +148,7 @@ background-color: rgba(0, 0, 0, 0.7); border-radius: 15px; padding: 20px; - z-index: 1000; + z-index: 10; &:hover { outline: 4px solid #8280ff66; outline-offset: -3px; @@ -2,136 +2,176 @@ import { type Step, type Tour } from 'nextstepjs' import { Device } from '#/shared/lib/types/entities' -const baseTourSteps: Tour[] = [ +export const tourStepsDesktop: Tour[] = [ { tour: 'text-models', steps: [ - { - icon: '', - title: 'Давайте начнем! 🚀', - content: 'Здесь вы сможете найти ChatGPT и её различные версии, а также LLaMA и Mistral и другие. Выберем одну из них для начала работы.', - selector: '#text-models-tour-1', - side: 'right', - showControls: true, - nextRoute: '/chat-bot/chatgpt', - showSkip: true, - }, - { - icon: '', - 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-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, + { + icon: '', + title: 'Давайте начнем! 🚀', + content: 'Здесь вы сможете найти ChatGPT и её различные версии, а также LLaMA и Mistral и другие. Выберем одну из них для начала работы.', + selector: '#text-models-tour-1', + side: 'right', + showControls: true, + nextRoute: '/chat-bot/chatgpt', + showSkip: true, + }, + { + icon: '', + 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-4', + side: 'top', + showSkip: true, + sendMessageOnClick: true, + } as Step, + ], + }, + { + tour: 'images', + steps: [ + { + icon: '', + 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: 'Ввод запроса', + content: 'Используйте это поле для ввода вашего запроса, а затем кликните на кнопку отправки.', + selector: '#images-models-tour-2', + side: 'top', + showControls: true, + showSkip: true, + nextButtonText: 'Напиши за меня →', + suggestedQuery: 'Кот в космическом скафандре на фоне звёзд', + } as Step, + { + icon: '', + title: 'Отправить запрос', + content: 'Нажмите на кнопку, чтобы отправить ваш запрос.', + selector: '#text-models-tour-4', + side: 'top', + showSkip: true, + sendMessageOnClick: true, + } as Step, + ], + }, +] + +export const tourStepsMobile: Tour[] = [ + { + tour: 'text-models', + steps: [ + { + icon: '', + title: 'Давайте начнем! 🚀', + content: 'Здесь вы сможете найти ChatGPT и её различные версии, а также LLaMA и Mistral и другие. Выберем одну из них для начала работы.', + selector: '#text-models-tour-1', + side: 'bottom', + showControls: true, + nextRoute: '/chat-bot/chatgpt', + 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-2', + side: 'left', + showSkip: true, + sendMessageOnClick: true, + } as Step, + ], + }, + { + tour: 'images', + steps: [ + { + icon: '', + title: 'Множество популярных моделей! 🖼️', + content: 'Здесь представлены мощные модели для генерации изображений: Flux, Stable Diffusion, Midjourney, DALL-E и другие. Выберем одну из них для начала работы.', + selector: '#images-models-tour-1', + nextRoute: '/images/flux', + side: 'bottom', + showControls: true, + showSkip: true, + }, + { + icon: '', + title: 'Ввод запроса', + content: 'Используйте это поле для ввода вашего запроса, а затем кликните на кнопку отправки.', + selector: '#images-models-tour-2', + side: 'top', + showControls: true, + showSkip: true, + nextButtonText: 'Напиши за меня →', + suggestedQuery: 'Кот в космическом скафандре на фоне звёзд', + } as Step, + { + icon: '', + title: 'Отправить запрос', + content: 'Нажмите на кнопку, чтобы отправить ваш запрос.', + selector: '#images-models-tour-2', + side: 'top', + showSkip: true, + sendMessageOnClick: true, + } as Step, ], }, - { - tour: 'images', - steps: [ - { - icon: '', - 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: 'Ввод запроса', - content: 'Используйте это поле для ввода вашего запроса, а затем кликните на кнопку отправки.', - selector: '#images-models-tour-2', - side: 'bottom', - showControls: true, - showSkip: true, - nextButtonText: 'Напиши за меня →', - suggestedQuery: 'Кот в космическом скафандре на фоне звёзд', - } as Step, - { - icon: '', - 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, - ], - }, ] export const getTourSteps = (device: Device): Tour[] => { - if (device === 'mobile') { - return baseTourSteps.map(tour => ({ - ...tour, - steps: tour.steps - .filter(step => step.selector !== '#images-models-tour-3' && step.selector !== '#text-models-tour-3') - .map(step => ({ - ...step, - side: 'bottom' as const, - })), - })) - } - return baseTourSteps + return device === 'mobile' ? tourStepsMobile : tourStepsDesktop } // Для обратной совместимости (по умолчанию desktop) -export const tourSteps = baseTourSteps +export const tourSteps = tourStepsDesktop export type TourName = 'text-models' | 'images' @@ -1,7 +1,7 @@ .tourCard { background-color: #373737; border-radius: 20px; - padding: 16px; + padding: 18px; min-width: 400px; max-width: 500px; position: relative; @@ -10,7 +10,7 @@ @media (max-width: 600px) { min-width: 90vw; max-width: 90vw; - padding: 12px; + padding: 16px; } } @@ -71,6 +71,10 @@ .controls { margin-top: 8px; width: 100%; + @media (max-width: 600px) { + flex-direction: column-reverse; + gap: 8px; + } } .button { @@ -98,6 +102,10 @@ &:hover { background-color: #6B69E0 !important; } + + @media (max-width: 600px) { + margin-left: 0px !important; + } } .stepIndicator { @@ -123,3 +131,16 @@ } } +.skipLink { + color: rgba(255, 255, 255, 0.6); + font-size: 0.9rem; + cursor: pointer; + align-self: flex-end; + transition: color 0.3s ease; + margin-top: 8px; + + &:hover { + color: rgba(255, 255, 255, 0.9); + } +} + @@ -1,8 +1,9 @@ -import React from 'react' +import React, { useState } from 'react' import { Box, Button, IconButton, Stack, Typography } from '@mui/material' import { type CardComponentProps } from 'nextstepjs' import Image from 'next/image' +import { TourConfirmSkipModal } from './tour-confirm-skip-modal' import styles from './tour-card.module.scss' const TOUR_SUGGESTED_QUERY_EVENT = 'tour-suggested-query' @@ -12,7 +13,6 @@ export const TourCard: React.FC = ({ currentStep, totalSteps, nextStep, - prevStep, skipTour, arrow, }) => { @@ -22,6 +22,17 @@ export const TourCard: React.FC = ({ sendMessageOnClick?: boolean } + const [confirmSkipOpen, setConfirmSkipOpen] = useState(false) + + const handleSkipClick = () => { + setConfirmSkipOpen(true) + } + + const handleConfirmSkip = () => { + setConfirmSkipOpen(false) + skipTour?.() + } + const handleNextClick = () => { if (extendedStep.suggestedQuery && typeof window !== 'undefined') { window.dispatchEvent( @@ -38,7 +49,7 @@ export const TourCard: React.FC = ({ {step.showSkip && skipTour && ( Закрыть @@ -68,13 +79,13 @@ export const TourCard: React.FC = ({ {step.showControls && ( - {currentStep > 0 && ( + {skipTour && ( )} @@ -89,6 +100,11 @@ export const TourCard: React.FC = ({ )} + setConfirmSkipOpen(false)} + /> ) } @@ -0,0 +1,47 @@ +.container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.title { + color: white; + font-weight: bold; + font-size: 1.3rem; + text-align: left; + margin: 0; + + @media (max-width: 600px) { + font-size: 1.25rem; + } +} + +.contentText { + color: rgba(255, 255, 255, 0.9); + font-size: 1rem; + line-height: 1.5; + text-align: left; + margin: 0; + + @media (max-width: 600px) { + font-size: 0.9rem; + } +} + +.controls { + margin-top: 8px; + width: 100%; +} + +.button { + text-transform: none; + padding: 10px 24px; + border-radius: 8px; + background-color: #7f7df3; + color: white; + flex: 1; + + &:hover { + background-color: #6b69e0; + } +} @@ -0,0 +1,31 @@ +import React from 'react' +import { Box, Button, Stack, Typography } from '@mui/material' + +import { TourModal } from './tour-modal' + +import styles from './tour-completion-modal.module.scss' + +interface TourCompletionModalProps { + open: boolean + onClose: () => void +} + +export const TourCompletionModal: React.FC = ({ open, onClose }) => { + return ( + + + + Вы завершили обучение! + + + Теперь вы можете использовать всю мощь ИИ⚡️ + + + + + + + ) +} @@ -0,0 +1,48 @@ +.content { + margin: -22px; +} + +.title { + color: var(--new-ui-text-color); + font-size: 20px; + font-style: normal; + font-weight: 600; + + @media (max-width: 600px) { + font-size: 16px; + width: 90%; + } +} + +.confirmModalButtons { + display: flex; + gap: 16px; + margin-top: 16px; + + @media (max-width: 600px) { + flex-direction: column-reverse; + gap: 8px; + } +} + +.confirmButton { + background-color: #7F7DF3 !important; + color: white !important; + text-transform: none !important; + + + &:hover { + background-color: #6B69E0 !important; + } +} + +.skipButton { + border-color: rgba(255, 255, 255, 0.3) !important; + color: white !important; + text-transform: none !important; + + &:hover { + border-color: rgba(255, 255, 255, 0.5) !important; + background-color: rgba(255, 255, 255, 0.1) !important; + } +} @@ -0,0 +1,34 @@ +import React from 'react' +import { Box, Button, Stack, Typography } from '@mui/material' + +import { Modal } from '#/shared/ui/modal/modal' + +import styles from './tour-confirm-skip-modal.module.scss' + +interface TourConfirmSkipModalProps { + open: boolean + onConfirm: () => void + onClose: () => void +} + +export const TourConfirmSkipModal: React.FC = ({ + open, + onConfirm, + onClose, +}) => ( + + + + Вы уверены, что хотите пропустить обучение? + + + + + + + +) @@ -5,20 +5,24 @@ import { useNextStep } from 'nextstepjs' import { useAppSelector } from '#/app/store/store' import { useFeatureFlag } from '#/shared/lib/hooks/use-feature-flag' -import { TourName } from '../lib/constants' +import { getDeviceType } from '#/shared/lib/helpers' +import { TourName, getTourSteps } from '../lib/constants' import { setTourCompleted, isTourCompleted } from '../lib/tour-storage' import { TourSelector } from './tour-selector' +import { TourCompletionModal } from './tour-completion-modal' interface TourManagerProps { children: React.ReactNode } export const TourManager: React.FC = ({ children }) => { - const { startNextStep, setCurrentStep, currentTour, currentStep } = useNextStep() + const { startNextStep, setCurrentStep, currentTour, currentStep, isNextStepVisible, closeNextStep } = useNextStep() const { data: session } = useSession() const isOnboardingEnabled = useFeatureFlag('onboarding-enabled') const [showTourSelector, setShowTourSelector] = useState(false) + const [showCompletionModal, setShowCompletionModal] = useState(false) const [pendingTour, setPendingTour] = useState(null) + const prevTourStateRef = useRef<{ tour: string | null; visible: boolean }>({ tour: null, visible: false }) const hasAdvancedStep = useRef(false) const currentStepRef = useRef(currentStep) const router = useRouter() @@ -59,15 +63,39 @@ export const TourManager: React.FC = ({ children }) => { setTourSelectorShown() } + // Обработчик завершения тура на основе хуков nextstepjs + useEffect(() => { + const wasVisibleWithTour = prevTourStateRef.current.visible && prevTourStateRef.current.tour + const isNowClosed = !isNextStepVisible && !currentTour + + if (wasVisibleWithTour && isNowClosed) { + const tourSteps = getTourSteps(getDeviceType()) + const tourConfig = tourSteps.find((t) => t.tour === prevTourStateRef.current.tour) + const totalSteps = tourConfig?.steps.length ?? 0 + if (currentStep === totalSteps - 1) { + setShowCompletionModal(true) + } + } + + prevTourStateRef.current = { tour: currentTour, visible: isNextStepVisible } + }, [currentTour, isNextStepVisible, currentStep]) + + const handleCompletionModalClose = () => { + setTourCompleted() + setShowCompletionModal(false) + setTourSelectorShown() + } + useEffect(() => { const handleUserSentMessage = () => { if (currentTour) { - setCurrentStep(currentStepRef.current + 1) + closeNextStep() + setShowCompletionModal(true) } } window.addEventListener('user-sent-message', handleUserSentMessage) return () => window.removeEventListener('user-sent-message', handleUserSentMessage) - }, [currentTour, setCurrentStep]) + }, [currentTour, closeNextStep]) useEffect(() => { // Проверяем feature flag: tour-manager работает только если onboarding-enabled включен @@ -117,8 +145,8 @@ export const TourManager: React.FC = ({ children }) => { const now = new Date() const minutesSinceCreation = (now.getTime() - createdDate.getTime()) / (1000 * 60) - // Пользователь считается новым, если зарегистрирован менее 5 минут назад - return minutesSinceCreation < 5 + // Пользователь считается новым, если зарегистрирован менее 60 минут назад + return minutesSinceCreation < 60 } useEffect(() => { @@ -136,7 +164,7 @@ export const TourManager: React.FC = ({ children }) => { setTimeout(() => { setShowTourSelector(true) setTourSelectorShown() - }, 3000) + }, 1000) } } }, [session, isOnboardingEnabled]) @@ -144,6 +172,7 @@ export const TourManager: React.FC = ({ children }) => { return ( <> + {children} ) @@ -31,3 +31,24 @@ margin: 20px !important; } } + +.dialogCompact { + :global(.MuiPaper-root) { + max-width: 500px !important; + + @media (max-width: 600px) { + max-width: 90vw; + } + } +} + +.wrapCompact { + padding: 16px; + width: 100%; + text-align: left; + box-sizing: border-box; + + @media (max-width: 600px) { + padding: 12px; + } +} @@ -9,15 +9,16 @@ interface TourModalProps { open: boolean onClose: (e?: any) => void children?: React.ReactNode + compact?: boolean } -export const TourModal: React.FC = ({ open, onClose, children }) => { +export const TourModal: React.FC = ({ open, onClose, children, compact }) => { return ( = ({ open, onClose, children }) height={14} alt={''} /> - {children} + {children} ) } @@ -1,14 +1,14 @@ .container { + margin: -22px -22px -26px -22px; display: flex; flex-direction: column; - gap: 12px; - width: 100%; - max-width: 742px; - padding: 0; + gap: 10px; + padding-right: 8px; box-sizing: border-box; @media (max-width: 800px) { max-width: 100%; + margin: 0; } } @@ -152,29 +152,3 @@ } } -.confirmModalButtons { - display: flex; - gap: 16px; - margin-top: 16px; -} - -.confirmButton { - background-color: #7F7DF3 !important; - color: white !important; - text-transform: none !important; - - &:hover { - background-color: #6B69E0 !important; - } -} - -.skipButton { - border-color: rgba(255, 255, 255, 0.3) !important; - color: white !important; - text-transform: none !important; - - &:hover { - border-color: rgba(255, 255, 255, 0.5) !important; - background-color: rgba(255, 255, 255, 0.1) !important; - } -} @@ -1,10 +1,10 @@ import React, { useState } from 'react' -import { Box, Button, Stack, Typography } from '@mui/material' +import { Box, Button, Typography } from '@mui/material' import { SvgIcon } from '#/shared/ui/svg' -import { Modal } from '#/shared/ui/modal/modal' import { TourName } from '../lib/constants' import { TourModal } from './tour-modal' +import { TourConfirmSkipModal } from './tour-confirm-skip-modal' import styles from './tour-selector.module.scss' @@ -70,28 +70,11 @@ export const TourSelector: React.FC = ({ open, onSelect, onSk - - - Вы уверены, что хотите пропустить обучение? - - - - - - + setConfirmSkipModal(false)} + /> ) } @@ -1,4 +1,4 @@ -export { tourSteps, getTourSteps, type TourName } from './lib/constants' +export { tourSteps, tourStepsDesktop, tourStepsMobile, getTourSteps, type TourName } from './lib/constants' export { TourSelector } from './ui/tour-selector' export { TourManager } from './ui/tour-manager' export { TourCard } from './ui/tour-card' @@ -76,6 +76,7 @@ function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; p return ( @@ -4,7 +4,7 @@ padding: 40px; width: 30vw; @media (max-width: 800px) { - width: 85vw; + width: 80vw; } @media (min-width: 1900px) { width: 560px; @@ -1,5 +1,5 @@ import * as React from 'react' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { Box, Tooltip, Typography, useMediaQuery } from '@mui/material' import MuiDrawer from '@mui/material/Drawer' import List from '@mui/material/List' @@ -12,6 +12,7 @@ import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' import { useSession } from 'next-auth/react' +import { useNextStep } from 'nextstepjs' import { useAppSelector } from '#/app/store/store' import { useUserSettingsContext } from '#/entities/user-account' @@ -116,6 +117,7 @@ export const SideMenu = ({}) => { const { pathname, asPath } = useRouter() const { getOptionValue, settings, updateSettings } = useUserSettingsContext() const account_type = useAppSelector((state) => state.user.account_type) + const { isNextStepVisible } = useNextStep() const error_report = getModalById(ERROR_REPORT) const business_error_report = getModalById(BUSINESS_ERROR_REPORT) @@ -123,6 +125,10 @@ export const SideMenu = ({}) => { const open = useMemo(() => getOptionValue('sidemenu', { sidemenu_state: 'opened' }).sidemenu_state, [settings]) + useEffect(() => { + if (isNextStepVisible) updateSettings('sidemenu', { sidemenu_state: 'closed' }) + }, [isNextStepVisible, updateSettings]) + const filteredMenuListTop = useMemo(() => { return menuListTop.filter((item) => { if (item.title === 'Главная' || item.title === 'Подписка') { @@ -134,7 +140,13 @@ export const SideMenu = ({}) => { }, [account_type]) const handleCloseDrawer = () => { - updateSettings('sidemenu', { sidemenu_state: 'closed' }) + if (!isNextStepVisible) updateSettings('sidemenu', { sidemenu_state: 'closed' }) + } + + const handleToggleOpen = () => { + if (!isNextStepVisible) { + updateSettings('sidemenu', { sidemenu_state: open === 'opened' ? 'closed' : 'opened' }) + } } return ( @@ -158,7 +170,7 @@ export const SideMenu = ({}) => { sx: { backgroundColor: '#151518', border: 'none', - zIndex: 1200, + zIndex: isNextStepVisible ? 800 : 1200, }, }} variant='permanent' @@ -174,11 +186,7 @@ export const SideMenu = ({}) => { }} > { - updateSettings('sidemenu', { - sidemenu_state: open === 'opened' ? 'closed' : 'opened', - }) - }} + onClick={handleToggleOpen} width='100%' display={'flex'} alignItems={'center'}