@@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,3 +1,4 @@ - - + + \ No newline at end of file @@ -29,3 +29,43 @@ } } } + +.blocked { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 20px; + right: 20px; + background-color: white; + border-radius: 100px; + padding-right: 20px; + padding: 8px 12px; + + span { + color: #ff2372; + font-weight: 500; + padding-left: 10px; + font-size: 14px; + } +} +.locked { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + padding-right: 20px; + + span { + color: white; + padding-top: 10px; + font-weight: 500; + } +} @@ -2,26 +2,32 @@ import React, { useEffect, useMemo } from 'react' import { Avatar, Box, Button, Typography } from '@mui/material' import Image from 'next/image' import Link from 'next/link' +import LockSvg from '#/assets/svg/lock.svg?react' +import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card.module.scss' +import { useThemeAndDevice } from '#/shared/lib/hooks' export type CardProps = { title: string icon: string text: string uid: string + blocked?: boolean slug: string accessed_models?: string[] | null } -const ChatCard = ({ text, icon, title, uid, slug, accessed_models }: CardProps) => { +const ChatCard = ({ text, icon, title, uid, slug, accessed_models, blocked }: CardProps) => { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` }, [accessed_models]) + const { theme } = useThemeAndDevice() + return ( - + @@ -30,42 +36,24 @@ const ChatCard = ({ text, icon, title, uid, slug, accessed_models }: CardProps) {text} - {accessed_models && !accessed_models.includes(slug) && ( - - - - - + + + Модель недоступна + + + ) : ( + accessed_models && + !accessed_models.includes(slug) && ( +
+ + Недоступно в текущем тарифе +
+ ) )}
@@ -5,7 +5,7 @@ import axios from 'axios' import { UniqInput } from '#/app/components/uniq_input' import { useAppSelector } from '#/app/store/store' import { ChatProps } from '#/shared/lib/types/model' -import { ChatMessagesList } from '#/widgets/messages/chat-messages-list' +import { ChatMessagesList } from '#/widgets/messages' import 'intro.js/introjs.css' @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -1,8 +1,8 @@ -import { AudioModel } from '#/app/types/model' +import { IShortModel } from '#/entities/model-entity' import { API_URL } from '#/shared/lib/constants' import axios from 'axios' -export async function getAudio(token?: string): Promise { +export async function getAudio(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { headers: { @@ -0,0 +1,11 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { IModel } from '../types' + +export async function getBotParams(slug: string, token?: string) { + return await axios.get(API_URL + `/ml_models/${slug}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,11 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { IShortModel } from '../types' + +export async function getModelsImages(token?: string) { + return await axios.get(API_URL + '/ml_models/?category=images', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,2 @@ +export * from './bot.route' +export * from './images-bots.route' @@ -0,0 +1 @@ +export * from './use-images-bots' \ No newline at end of file @@ -0,0 +1,118 @@ +import { useState } from 'react' +import { IModel } from '../types' +import { useSession } from 'next-auth/react' +import { getBotParams } from '../api' +import { useAppDispatch } from '#/app/store/store' +import { setParams } from '#/app/store/model-parametres-store' + +export function useImageBot(slug: string) { + const [botParams, setBotParams] = useState(null) + const [version, setVersion] = useState('') + const [modelType, setModelType] = useState('') + + const dispatch = useAppDispatch() + + const { data } = useSession() + + function setDefault(bot: IModel) { + setVersion('') + + dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + } + + // это пиз**ц + // нужен рефакторинг (я то в этом не разбираюсь) + // а стажеры и подавно)))) + function setStoreParams(bot: IModel) { + dispatch( + setParams( + bot.parameters.reduce( + (a, v) => + v.versions.includes(bot.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } + + function setAllParams(bot: IModel) { + setBotParams(bot) + setModelType(bot.slug) + + // store + if (bot.versions.length === 0) return setDefault(bot) + + setVersion(bot.versions[0].slug) + setStoreParams(bot) + } + + // это пиз**ц + const resetParams = () => { + if (botParams) { + dispatch(setParams({})) + if (botParams.versions.length !== 0) { + setVersion(botParams.versions[0].slug) + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => + v.versions.includes(botParams.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } else { + setVersion(botParams.slug) + dispatch( + setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) + } + } + } + + // это пиз**ц + const setDefaultParams = () => { + if (!botParams) return + + dispatch(setParams({})) + + if (version === '') { + return dispatch( + setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) + } + + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), + {} + ) + ) + ) + } + + // api functions + + async function fetchBotParams() { + if (!data) return + + const { data: bot, ...response } = await getBotParams(slug, data.access) + + if (response.status < 400) setAllParams(bot) + } + + return { + botParams, + version, + modelType, + fetchBotParams, + setVersion, + resetParams, + setDefaultParams, + } +} @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { getModelsImages } from '../api' +import { useSession } from 'next-auth/react' +import { IShortModel } from '../types' + +export function useImagesBots() { + const [bots, setBots] = useState([]) + + const { data } = useSession() + + async function fetchBots() { + if (!data) return + + const response = await getModelsImages(data.access) + + if (response.status < 400) setBots(response.data) + } + + return { + bots, + fetchBots, + setBots + } +} @@ -0,0 +1 @@ +export * from './model.types' \ No newline at end of file @@ -0,0 +1,55 @@ +type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' + +export interface IShortModel { + uid: string + title: string + description: string + slug: string + image: string + blocked: boolean + actual_stat: { + generation_time: string + tokens_cost: string + } +} + +export interface IModel { + uid: string + title: string + description: string + slug: string + image: string + settings: { is_active: boolean } + parameters: IModelParams[] + versions: IModelVersions[] + inputs: IModelInputs[] +} +export interface IModelParams { + name: string + description: string + key: string + type: string + required: boolean + values: { + availables: string[] + default: any + end: number + start: number + step: number + } + versions: string[] +} +export interface IModelVersions { + name: string + description: string + default: boolean + slug: string +} +export interface IModelInputs { + // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' + type: string + required: boolean + versions: string[] +} + +export default ModelForChats @@ -0,0 +1,3 @@ +export * from './api' +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1,3 @@ + + +export * from './use-images-library' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface ImageWithState { + image: string + state: boolean +} \ No newline at end of file @@ -0,0 +1,71 @@ +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import { ImageWithState } from './types' +import { Message } from '#/shared/lib/types/model' + +export const useImagesLibrary = ( + images: string[], + current: string | null, + reverse: boolean, + setModal: Dispatch> +) => { + const [imagesWithState, setImagesWithState] = useState(getImagesWithState(images)) + const [initialCount, setInitialCount] = useState(0) + + const esc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + setModal(false) + } + } + + function getImagesWithState(images: string[]) { + const imagesWithState = images.map((image) => ({ + image, + state: false, + })) + + return imagesWithState + } + + function updateImageState(image: string, state: boolean) { + setImagesWithState((prev) => prev.map((item) => (item.image === image ? { ...item, state } : item))) + } + + const currentImageIndex = useMemo( + () => (!current ? undefined : imagesWithState.findIndex((image) => image.image === current)), + [imagesWithState, current] + ) + + useEffect(() => { + if (initialCount === images.length) { + return + } + + setInitialCount(images.length) + + const filteredMessages = images.filter((item) => + !imagesWithState.find((i) => i.image === item) ? true : false + ) + + if (filteredMessages.length === images.length) { + return setImagesWithState(getImagesWithState(filteredMessages)) + } + + if (reverse) setImagesWithState([...imagesWithState, ...getImagesWithState(filteredMessages)]) + else setImagesWithState([...getImagesWithState(filteredMessages), ...imagesWithState]) + }, [images]) + + useEffect(() => { + window.addEventListener('keydown', esc) + return () => window.removeEventListener('keydown', esc) + }, []) + + return { + imagesWithState, + currentImageIndex, + initialCount, + setInitialCount, + setImagesWithState, + updateImageState, + } +} @@ -0,0 +1,54 @@ +import { debounce } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { Swiper as SwiperCore } from 'swiper' +import { ImageWithState } from './types' + +export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { + const [swiper, setSwiper] = useState(null) + + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault() + slideNext() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + slidePrev() + } + } + + const slidePrev = function () { + const result = swiper?.slidePrev() + + if (!result && onSlideFalse && !reverse) { + onSlideFalse() + } + } + + const slideNext = function () { + const result = swiper?.slideNext() + + if (!result && onSlideFalse && reverse) { + onSlideFalse() + } + } + + useEffect(() => { + document.addEventListener('keydown', keydown, true) + return () => { + document.removeEventListener('keydown', keydown, true) + } + }, [swiper]) + + // useEffect(() => { + // swiper?.slideTo(currentImageIndex || 0) + // }, [currentImageIndex]) + + return { + swiper, + setSwiper, + slidePrev, + slideNext, + } +} @@ -0,0 +1,193 @@ +import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' +import Image from 'next/image' + +import styles from './modal-styles.module.scss' +import { ArrowDropDown } from '@mui/icons-material' +import { c, Loader, TooltipCustom } from '#/shared' +import { useImagesLibrary } from '../model' + +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/css' +import { useLibrarySwiper } from '../model/use-swiper' +import { ImageIcons, useImageIcons } from '#/widgets/messages' +import { Message } from '#/shared/lib/types/model' +import { Typography } from '@mui/material' + +interface IProps { + modal: boolean + setModal: Dispatch> + current: string | null + setCurrent?: (value: string | null) => void + onSlideFalse?: (...args: any) => any + reverse?: boolean + images: Message[] +} + +export default function FullScreenModal({ modal, setModal, current, images, onSlideFalse, reverse = false }: IProps) { + const filteredMessages = useMemo(() => images.map((message) => message.file as string), [images]) + + const { imagesWithState, updateImageState, currentImageIndex, initialCount } = useImagesLibrary( + filteredMessages, + current, + reverse, + setModal + ) + + const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) + + const { downloadFile } = useImageIcons() + + useEffect(() => { + if (!(initialCount < images.length && swiper)) return + + if (reverse) { + setTimeout(() => swiper.slideTo(initialCount, 1000), 1000) + return + } + + setTimeout(() => swiper.slideTo(images.length - initialCount - 1, 1000), 1000) + }, [images]) + + return ( +
setModal(false)} + > +
+
+
+ { + e.stopPropagation() + if (!swiper) return + const { file, content } = images[swiper.activeIndex] + downloadFile(file as string, content) + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
+ { + e.stopPropagation() + if (!swiper) return + const { file } = images[swiper.activeIndex] + window.open(file as string, '_blank') + }} + width='24' + height='24' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + +
+
setModal(false)}> + + + +
+
+
+ + {modal && ( + setSwiper(swiper)} + > + {imagesWithState.map(({ image, state }, index) => ( + + {!state && } +
+ {!image.includes('.svg') ? ( + e.stopPropagation()} + onLoadingComplete={() => { + updateImageState(image, true) + }} + loading='lazy' + src={image} + width={'1500'} + height={'1500'} + alt='К сожалению, изображение не загрузилось' + /> + ) : ( + e.stopPropagation()} + onLoad={() => updateImageState(image, true)} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + )} +
+
+ ))} +
+ )} + +
+
+ ) +} @@ -0,0 +1 @@ +export { default as ImageModal } from './full-screen-modal' @@ -0,0 +1,105 @@ +.close_block { +} +.download { +} +.actions { + position: absolute; + z-index: 105; + cursor: pointer; + right: 25px; + top: 25px; + display: flex; + align-items: center; + gap: 15px; +} + +.arrow { + position: absolute; + z-index: 1205; + cursor: pointer; + top: 50%; + + width: 50px; + height: 50px; + + background: transparent; + border: none; + outline: none; + + svg { + fill: white; + width: 50px; + height: 50px; + } + + &_left { + transform: translateY(-50%) rotate(90deg); + left: -40px; + @media screen and (max-width: 768px) { + left: -3px; + } + } + + &_right { + transform: translateY(-50%) rotate(-90deg); + right: -50px; + + @media screen and (max-width: 768px) { + right: -10px; + } + } +} + +.image_block { + position: absolute; + padding: 0 20px; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 105; + margin: auto; + width: fit-content; + height: fit-content; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.image_style { + width: auto; + max-width: 70vw; + height: 100%; + position: relative; + user-select: none; + display: flex; + justify-content: center; + align-items: center; + img { + width: 100%; + height: 100%; + object-fit: contain; + user-select: none; + } +} + +.slide { + width: 80vw !important; + display: flex !important; + align-items: center; + justify-content: center; + height: 100% !important; + max-height: unset; +} + +.swiper { + position: relative; + width: 100%; + max-width: 80vw; + max-height: 1000px; + height: 80vh; +} @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1,72 @@ +.card { + background-color: var(--new-ui-main-color); + width: 381px; + height: 227px; + border-radius: 15px; + margin-right: 20px; + margin-top: 20px; + + cursor: pointer; + padding: 30px; + box-sizing: border-box; + + @media (max-width: 420px) { + width: 92vw; + } + + .description { + margin-top: 20px; + + .title { + font-weight: 600; + } + + .text { + color: var(--new-ui-gray-color); + font-weight: 400; + margin-top: 6px; + font-size: 15px; + } + } +} + +.blocked { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 20px; + right: 20px; + background-color: white; + border-radius: 100px; + padding-right: 20px; + padding: 8px 12px; + + span { + color: #ff2372; + font-weight: 500; + padding-left: 10px; + font-size: 14px; + } +} + +.locked { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + padding-right: 20px; + + span { + color: white; + padding-top: 10px; + font-weight: 500; + } +} @@ -3,4 +3,4 @@ import { getDefaultLayout } from "#/widgets/layouts"; ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }); -export default ChatBotsPage \ No newline at end of file +export default ChatBotsPage @@ -3,4 +3,4 @@ import { getDefaultLayout } from "#/widgets/layouts"; ImageModelsPage.getLayout = getDefaultLayout({ titlePage: 'Изображения' }) -export default ImageModelsPage \ No newline at end of file +export default ImageModelsPage @@ -35,6 +35,7 @@ export default function Document() {
+ @@ -1,7 +1,8 @@ import axios from 'axios' -import { IModel, IShortModel } from '#/shared/api/models/models' +import { IModel } from '#/shared/api/models/models' import { API_URL } from '#/shared/lib/constants' +import { IShortModel } from '#/entities/model-entity' const model_api = { async getBots(token?: string): Promise { @@ -247,7 +247,7 @@ export function useModelImages(showError: (message: string) => void, type: st const { getData, sendData } = ModelsWithImagesEndpoints - const [messages, setMessages] = useState(null) + const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) @@ -28,3 +28,43 @@ } } } + +.blocked { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 20px; + right: 20px; + background-color: white; + border-radius: 100px; + padding-right: 20px; + padding: 8px 12px; + + span { + color: #ff2372; + font-weight: 500; + padding-left: 10px; + font-size: 14px; + } +} +.locked { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 15px; + padding-right: 20px; + + span { + color: white; + padding-top: 10px; + font-weight: 500; + } +} @@ -2,8 +2,11 @@ import React, { useMemo } from 'react' import { Box, Button, Typography } from '@mui/material' import Image from 'next/image' import Link from 'next/link' +import LockSvg from '#/assets/svg/lock.svg?react' +import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card.module.scss' +import { useThemeAndDevice } from '../lib/hooks' interface IProps { text: string @@ -11,17 +14,20 @@ interface IProps { title: string slug: string uid: string + blocked: boolean accessed_models?: string[] | null } -export const Card = ({ text, icon, title, uid, slug, accessed_models }: IProps) => { +export const Card = ({ text, icon, title, blocked, slug, accessed_models }: IProps) => { const link = useMemo(() => { return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `/images/${slug}` }, [accessed_models]) + const { theme } = useThemeAndDevice() + return ( - + {text} - {accessed_models && !accessed_models.includes(slug) && ( - - - - - + + + Модель недоступна + + + ) : ( + accessed_models && + !accessed_models.includes(slug) && ( +
+ + Недоступно в текущем тарифе +
+ ) )}
@@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; + +type ClientOnlyProps = { children: JSX.Element }; +const ClientOnly = (props: ClientOnlyProps) => { + const { children } = props; + + return children; +}; + +export default dynamic(() => Promise.resolve(ClientOnly), { + ssr: false, +}); @@ -0,0 +1 @@ +export { default as ClientOnly } from './client-only' @@ -0,0 +1 @@ +export * from './ui' @@ -3,3 +3,4 @@ export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' export * from './get-type-device' export * from './get-random-image' +export * from './string' @@ -0,0 +1,3 @@ +export const c = (...classes: string[]) => { + return classes.filter(Boolean).join(' ') +} @@ -3,10 +3,11 @@ import { Tooltip } from '@mui/material' import { useAppSelector } from '#/app/store/store' -import styles from './styles.module.scss' interface Props { title?: string children: React.ReactNode + className?: string + maxWidth?: string placement?: | 'bottom-end' | 'bottom-start' @@ -21,11 +22,17 @@ interface Props { | 'top-start' | 'top' } -export const TooltipCustom: React.FC = ({ children, title, placement }) => { +export const TooltipCustom: React.FC = ({ children, title, placement, className, maxWidth }) => { const theme = useAppSelector((state) => state.theme.theme) return ( e.stopPropagation()} + PopperProps={{ + onClick(e) { + e.stopPropagation() + }, + }} componentsProps={{ tooltip: { sx: { @@ -33,6 +40,7 @@ export const TooltipCustom: React.FC = ({ children, title, placement }) = color: '#7F7DF3', fontSize: 15, borderRadius: '10px', + maxWidth: maxWidth || 'auto', padding: '10px 13px', '& .MuiTooltip-arrow': { color: theme === 'light' ? '#E8E8FA' : '#4B4B4B', @@ -41,6 +49,7 @@ export const TooltipCustom: React.FC = ({ children, title, placement }) = }, }, }} + className={className} title={title} arrow placement={placement} @@ -8,8 +8,10 @@ export { ButtonUI } from './ui/button/button' export { ButtonGray } from './ui/button/button-gray' export { CheckBoxAgreeWithRules } from './ui/check-box-agree-with-rules' export { DalleFilterMenu } from './ui/dalle-filter-menu' +export * from './client-only' export * from './ui/drawer' export { Error } from './ui/error' +export * from './lib/helpers' export * from './ui/graphics-main-page' export { Input, InputStyleDark, InputStyleLight } from './ui/input' export { InputImagesModels } from './ui/input-images-models/input-images-models' @@ -25,10 +25,7 @@ import { NextPageWithLayout } from '#/pages/_app' import { DownloadModal } from '#/features/business-security-download' import { scopes } from '../config' - const Account: NextPageWithLayout = () => { - const device = getDeviceType() - const { email, is_subscribed_to_emails, @@ -39,14 +36,19 @@ const Account: NextPageWithLayout = () => { profile_picture_link, is_social, status: userInfoLoaded, + referral_code, } = useAppSelector((state) => state.user) + const device = getDeviceType() + const { status, account_type: type } = useAppSelector((state) => state.user) const [name, setName] = useState('') const [lastName, setLastName] = useState('') + const [userName, setUserName] = useState('') + const fileInputRef = useRef(null) const [loading, setLoading] = useState(false) @@ -82,15 +84,15 @@ const Account: NextPageWithLayout = () => { } } - const [currentPassword, setCurrentPassword] = useState('') + const [currentPassword, setCurrentPassword] = React.useState('') - const [newPassword1, setNewPassword1] = useState('') + const [newPassword1, setNewPassword1] = React.useState('') - const [newPassword2, setNewPassword2] = useState('') + const [newPassword2, setNewPassword2] = React.useState('') - const [promocode, setPromocode] = useState('') + const [promocode, setPromocode] = React.useState('') - const [success, setSuccess] = useState('') + const [success, setSuccess] = React.useState('') const [confirmDeleteModal, setConfirmDeleteModal] = useState(false) @@ -189,9 +191,13 @@ const Account: NextPageWithLayout = () => { } } - const isUserDataChange = useMemo(() => name !== first_name || last_name !== lastName, [name, lastName]) + const isUserDataChange = useMemo( + () => name !== first_name || last_name !== lastName || username !== userName, + [name, lastName, userName] + ) const promocodeActivate = async () => { + if (!data) return if (!promocode.trim()) { showError('Введите корректный промокод', true) return @@ -201,7 +207,7 @@ const Account: NextPageWithLayout = () => { const { status } = await axios.post( API_URL + '/payments/promocode', { code: promocode }, - { headers: { Authorization: `Bearer ${data?.access}` } } + { headers: { Authorization: `Bearer ${data.access}` } } ) resStatus = status } catch (e) {} @@ -224,6 +230,7 @@ const Account: NextPageWithLayout = () => { } async function changeUserData() { + if (!data) return if (!isUserDataChange) { showError('Вы не изменили данные', true) return @@ -233,24 +240,25 @@ const Account: NextPageWithLayout = () => { await axios.put( API_URL + '/auth/user-data', { - username: username, + username: userName, email: email, first_name: name, last_name: lastName, }, - { headers: { Authorization: `Bearer ${data?.access}` } } + { headers: { Authorization: `Bearer ${data.access}` } } ) showError('Данные успешно изменены!') - dispatch(getAllInfo(data?.access)) + dispatch(getAllInfo(data.access)) } catch (e) {} } const deleteAccount = async () => { + if (!data) return try { const { status } = await axios.delete(API_URL + '/auth/remove', { headers: { - Authorization: `Bearer ${data?.access}`, + Authorization: `Bearer ${data.access}`, }, }) @@ -261,7 +269,8 @@ const Account: NextPageWithLayout = () => { useEffect(() => { setName(first_name) setLastName(last_name) - }, [first_name, last_name]) + setUserName(username) + }, [first_name, last_name, username]) return ( { { - await dispatch(unfollowEmail(data?.access)) + if (!data) return + await dispatch(unfollowEmail(data.access)) showError('Данные изменены!') }} /> @@ -426,7 +436,12 @@ const Account: NextPageWithLayout = () => { Никнейм - + setUserName(e.target.value)} + fullWidth + /> { - const [bots, setBots] = useState(null) + const [bots, setBots] = useState(null) const { data, status } = useSession() @@ -25,10 +25,11 @@ export const AudioModelsPage: NextPageWithLayout = () => { {bots?.map((item, idx) => { return ( @@ -68,9 +68,12 @@ const Page: NextPageWithLayout = () => { const deleteMessageMemo = useCallback(deleteMessage, [currentChat, messages]) + const { push } = useRouter() + React.useEffect(() => { if (data?.access) { model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { + if (!res.title) return push('/404') setBotParams(res) setModelType(res.slug) if (res.versions.length !== 0) { @@ -7,8 +7,8 @@ import { useSession } from 'next-auth/react' import ChatCard from '#/app/components/chat_bot_card' import { useAppSelector } from '#/app/store/store' import model_api from '#/shared/api/models/api' -import { IShortModel } from '#/shared/api/models/models' import { NextPageWithLayout } from '#/pages/_app' +import { IShortModel } from '#/entities/model-entity' const Page: NextPageWithLayout = () => { const [bots, setBots] = useState(null) @@ -37,6 +37,7 @@ const Page: NextPageWithLayout = () => { accessed_models={user.payment_plan.plan.accessed_models} uid={item.uid} key={idx} + blocked={item.blocked} title={item.title} icon={item.image} text={item.description} @@ -21,7 +21,7 @@ import { useModelImages } from '#/shared/api/models/endpoints' import { IModel } from '#/shared/api/models/models' import { useShowData } from '#/shared/lib/hooks' import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' -import { ImageMessagesList } from '#/widgets/messages/image-messages-list' +import { ImageMessagesList } from '#/widgets/messages/ui/image-messages-list' import { getDeviceType, getOs } from '#/shared/lib/helpers' import Head from 'next/head' import { NextPageWithLayout } from '#/pages/_app' @@ -61,6 +61,7 @@ const ImageModelPage: NextPageWithLayout = () => { model_api .getBotParams(router.asPath.split('/')[2], data?.access) .then((res) => { + if (!res.title) return router.push('/404') setBotParams(res) setModelType(res.slug) if (res.versions.length !== 0) { @@ -297,6 +298,7 @@ const ImageModelPage: NextPageWithLayout = () => { getMessagesPagination(deviceType)} isComplete={isComplete} device={deviceType} images={messages} @@ -343,6 +345,7 @@ const ImageModelPage: NextPageWithLayout = () => { onScroll={handleMobileScroll} > getMessagesPagination(deviceType)} isComplete={isComplete} device={deviceType} images={messages} @@ -6,9 +6,9 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '#/app/store/store' import model_api from '#/shared/api/models/api' -import { IShortModel } from '#/shared/api/models/models' import { Card } from '#/shared/card/card' import { NextPageWithLayout } from '#/pages/_app' +import { IShortModel } from '#/entities/model-entity' export const ImageModelsPage: NextPageWithLayout = () => { const [bots, setBots] = useState(null) @@ -29,6 +29,7 @@ export const ImageModelsPage: NextPageWithLayout = () => { bots.map((item, idx) => { return ( void message: Message + setModal: (value: boolean) => void deleteMessage?: (message_uid: string) => void modelTitle: string | undefined setResendValue: (value: string) => void onLoadImage?: (event: React.ChangeEvent | null, file?: File) => void } -export const UserMessage = React.memo(function UserMessage(props: IMessagesList) { +export const UserMessage = React.memo(function UserMessage({ setModal, setCurrentSrc, ...props }: IMessagesList) { const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const desktop = props.device === 'desktop' - const [modal, setModal] = useState(false) const [loaded, setLoaded] = useState(false) const copy = (text: string) => { @@ -64,13 +65,15 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) return ( <> - {props.message.file && ( - - )} {!props.message.from_model ? ( - // - + { + if (!props.message.file) return + setCurrentSrc(props.message.file.toString()) setModal(true) }} onLoadingComplete={() => setLoaded(true)} @@ -528,7 +533,7 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) )} - + ) : ( ('') + const { error, showError, isError } = useShowData() + + const toggleMenu = (uid: string) => { + if (iconsMenu === uid) { + setIconsMenu('') + } else { + setIconsMenu(uid) + } + } + + const downloadFile = (url: string | null, content: string | undefined) => { + if (url) { + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + // const image_name = '123' + const image_name = content?.replaceAll(' ', '_').substring(0, 25) + link.setAttribute('download', `${image_name}`) + document.body.appendChild(link) + link.click() + }) + .catch((error) => { + showError('Что-то пошло не так', true) + }) + } else { + showError('Изображение не найдено', true) + } + } + + return { iconsMenu, toggleMenu, downloadFile } +} @@ -0,0 +1,26 @@ +import { Message } from '#/shared/lib/types/model' +import { useMemo, useState } from 'react' + +export const useMessages = (messages: Message[]) => { + const [chosenImage, setChosenImage] = useState(null) + + const [loaded, setLoaded] = useState(false) + + // типизация ну супер кривая))) + const computedLibraryImages = useMemo(() => { + return messages + .map((el) => { + if (el.file && (el.file as any).includes('.zip')) return null + return el + }) + .filter((el) => el !== null) as Message[] + }, [messages]) + + return { + chosenImage, + setChosenImage, + loaded, + setLoaded, + computedLibraryImages, + } +} @@ -1,11 +1,14 @@ -import React, { memo, useEffect } from 'react' +import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import { Box, CircularProgress } from '@mui/material' import { useSession } from 'next-auth/react' import { Message } from '#/shared/lib/types/model' import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' -import { IsNextDay } from '#/widgets/messages/is-next-day' +import { IsNextDay } from '#/widgets/messages/ui/is-next-day' import { PreviewView } from '#/widgets/messages/message-components/preview-view' +import { ImageModal } from '#/features/image-modal' +import { ClientOnly } from '#/shared' +import { createPortal } from 'react-dom' interface IMessagesList { device: 'mobile' | 'desktop' @@ -41,13 +44,25 @@ export const ChatMessagesList: React.FC = memo( const desktop = device === 'desktop' const { status } = useSession() - const [isNewMessage, setIsNewMessage] = React.useState(false) + const [modal, setModal] = useState(false) + const [isNewMessage, setIsNewMessage] = useState(false) + + const [currentSrc, setCurrentSrc] = useState(null) + + const onlyImageMessage = useMemo(() => { + if (!messageResponse) return [] + console.log(messageResponse) + return messageResponse + .map((item) => item) + .filter((item) => item.file !== null && !(item.file as string).includes('.zip')) as Message[] + }, [messageResponse]) React.useEffect(() => { const block = paginationScroll.current if (messageResponse != undefined && !isPaginating) { setChatScrollHeight(paginationScroll.current.scrollHeight) + const time = setTimeout(() => { if (block) { //@ts-ignore @@ -73,11 +88,6 @@ export const ChatMessagesList: React.FC = memo( }, [messageResponse]) const handleScroll = () => { - console.log( - paginationScroll.current?.scrollHeight - - paginationScroll.current?.scrollTop - - paginationScroll.current?.clientHeight - ) setScrollBottom( paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - @@ -96,89 +106,108 @@ export const ChatMessagesList: React.FC = memo( } return ( - - {scrollBottom > 500 && ( - { - //@ts-ignore - const block = paginationScroll.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - - - )} - - {loading && ( - - + + {createPortal( + { + getMessagesPagination && (await getMessagesPagination()) + }} + images={onlyImageMessage} + />, + document.getElementById('modal-container')! + )} + + + + {scrollBottom > 500 && ( + { + //@ts-ignore + const block = paginationScroll.current + + block.scrollTo({ + top: block.scrollHeight, + behavior: 'smooth', // добавляем плавную прокрутку + }) + }} + > + + + )} + + {loading && ( + - - )} - - {messageResponse?.length === 0 && status === 'authenticated' ? ( - <>{desktop && modelType !== 'deepl' && } - ) : ( - messageResponse?.map((message, idx) => { - return ( - + - ) - }) - )} - + + )} + + {messageResponse?.length === 0 && status === 'authenticated' ? ( + <>{desktop && modelType !== 'deepl' && } + ) : ( + messageResponse?.map((message, idx) => { + return ( + + ) + }) + )} + + ) } ) @@ -0,0 +1,105 @@ +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom, useShowData } from '#/shared' +import { Grow, Box } from '@mui/material' +import { useImageIcons } from '../model' + +export interface ImageIconsProps { + uid: string + url: string | null + content?: string +} + +export const ImageIcons = ({ uid, url, content }: ImageIconsProps) => { + const theme = useAppSelector((state) => state.theme.theme) + + const { toggleMenu, downloadFile, iconsMenu } = useImageIcons() + + return ( + <> + { + e.stopPropagation() + toggleMenu(uid) + }} + width='35' + height='35' + viewBox='0 0 35 35' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + + + + { + window.open(`${url ? url : ''}`, '_blank') + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + { + downloadFile(url, content) + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + + + + + + + ) +} @@ -4,149 +4,47 @@ import Box from '@mui/material/Box' import Image from 'next/image' import Link from 'next/link' -import FullScreenModal from '#/features/image-modal/full-screen-modal' import { useAppSelector } from '#/app/store/store' -import { Success, TooltipCustom, useShowData } from '#/shared' -import { useAutoScroll } from '#/shared/lib/hooks' +import { ClientOnly, Success, TooltipCustom, useShowData } from '#/shared' import { Message } from '#/shared/lib/types/model' import styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { ImageIcons } from './image-icons' +import { ImageModal } from '#/features/image-modal' interface MessagesList { device: 'mobile' | 'desktop' - images: Message[] | null + images: Message[] + getMessagesPagination?: () => Promise isComplete: boolean } -export const ImageMessagesList: React.FC = memo(({ device, images }) => { +export const ImageMessagesList: React.FC = memo(({ device, images, getMessagesPagination }) => { const { error, showError, isError } = useShowData() const [modal, setModal] = useState(false) - const [chosenImage, setChosenImage] = useState('') const theme = useAppSelector((state) => state.theme.theme) - const [iconsMenu, setIconsMenu] = React.useState('') - const [loaded, setLoaded] = useState(false) + - const downloadFile = (url: string | null, content: string | undefined) => { - if (url) { - fetch(url) - .then((response) => response.blob()) - .then((blob) => { - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - // const image_name = '123' - const image_name = content?.replaceAll(' ', '_').substring(0, 25) - link.setAttribute('download', `${image_name}`) - document.body.appendChild(link) - link.click() - }) - .catch((error) => { - showError('Что-то пошло не так', true) - }) - } else { - showError('Изображение не найдено', true) - } - } - - const toggleMenu = (uid: string) => { - if (iconsMenu === uid) { - setIconsMenu('') - } else { - setIconsMenu(uid) - } - } - - const ImageIcons = ({ uid, url, content }: { uid: string; url: string | null; content: string | undefined }) => { - return ( - <> - toggleMenu(uid)} - width='35' - height='35' - viewBox='0 0 35 35' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - - { - window.open(`${url ? url : ''}`, '_blank') - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - { - downloadFile(url, content) - }} - width='15' - height='15' - viewBox='0 0 15 15' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - - - - - - - ) - } + const { chosenImage, setChosenImage, loaded, setLoaded, computedLibraryImages } = useMessages(images) return ( <> - + + {createPortal( + getMessagesPagination && getMessagesPagination()} + setModal={setModal} + reverse={device === 'desktop'} + current={chosenImage} + />, + document.getElementById('modal-container')! + )} + + void + setCurrentSrc: (value: string) => void isNewMessage: boolean - // clickPreview: (text: string) => void + setModal: (value: boolean) => void modelType: string device: 'mobile' | 'desktop' setResendValue: (value: string) => void @@ -28,6 +29,8 @@ export const IsNextDay = memo( messageResponse, modelTitle, deleteMessage, + setCurrentSrc, + setModal, isNewMessage, modelType, device, @@ -62,7 +65,9 @@ export const IsNextDay = memo( setResendValue={setResendValue} modelTitle={modelTitle} deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} key={message.uid} + setModal={setModal} message={message} isNewMessage={isNewMessage} modelType={modelType} @@ -92,9 +97,11 @@ export const IsNextDay = memo( { const [chatLinks, setChatLinks] = useState([]) - const { data } = useSession() + const { data: session } = useSession() const fetchChatLinks = async () => { - if (!data) return - setChatLinks((await getModelChatLinks(data.access)).data) + if (!session) return + + const { status, data } = await getModelChatLinks(session.access) + + if (status >= 400) return + + setChatLinks(data) } const visibleChatLinks = useMemo( @@ -3,26 +3,20 @@ import React from 'react' import styles from './key-for-search.module.scss' -interface KeyForSearchProps { - userAgent: string | null -} - -export const KeyForSearch = ({ userAgent }: KeyForSearchProps) => { - if (userAgent === null) { - return null - } - +export const KeyForSearch = () => { const checkType = () => { - if (userAgent.includes('Windows')) { - return 'Ctrl + F' + if (navigator.userAgent && !navigator.userAgent.includes('Windows')) { + return '⌘+F' } else { - return '⌥+F' + return 'Ctrl + F' } } return ( - {checkType()} + + {checkType()} + ) } @@ -132,9 +132,7 @@ export const Search = ({ device }: SearchProps) => { }} /> ), - endAdornment: ( - - ), + endAdornment: , }} /> )} @@ -358,7 +358,7 @@ export function MenuItem(props: MenuItemProps) { }, }} > - + !props.link && e.preventDefault()} href={props.link || ''}> - {''} + {typeof props.icon === 'string' ? ( + {''} + ) : ( +
+ {props.icon} +
+ )} = memo(({ device, token, favorites, title }) => { + const theme = useAppSelector((state) => state.theme.theme) + + const [anchorElModel, setAnchorElModel] = React.useState(null) + + const desktop = device === 'desktop' + + const { status } = useSession() + + const { pathname } = useRouter() + + const balance = useAppSelector((state) => state.balance.balance) + + const closeMenuModel = () => setAnchorElModel(null) + const openMenuModel = (event: React.MouseEvent) => { + setAnchorElModel(event.currentTarget) + } + const goToPayment = () => { + Router.push('/account') + } + + const LazyTutorial = dynamic(() => import('#/features/tutorial-gpt').then((component) => component.Tutorial)) + + const LazyArrowIcon = dynamic(() => import('./arrow-up-or-down')) + + return ( + + + {desktop && ( + + )} + + + {title} + + {!desktop && } + + + {desktop ? ( + + ) : ( + + )} + + + + {desktop && status === 'authenticated' && pathname === '/chatgpt' && } + + {token && ( + + + + )} + {desktop && ( + + Купить токены + + )} + + + + ) +}) + +ModelTopBar.displayName = 'ModelTopBar' @@ -0,0 +1,27 @@ +declare module "*.scss" { + const content: Record; + export default content; +} + +declare module "*.css" { + const content: string; + export default content; +} + +declare function ym(...args: any[]): void; + +declare module "*.svg?react" { + const content: React.FC>; + export default content; +} + +declare module "*.svg?url" { + const content: { + blurHeight: number; + blurWidth: number; + height: number; + src: string; + width: number; + }; + export default content; +} @@ -9,6 +9,24 @@ const nextConfig = { layers: true, } + const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')) + + config.module.rules.push( + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: /react/, + use: ['@svgr/webpack'], + }, + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + } + ) + + fileLoaderRule.exclude = /\.svg$/i + return config }, reactStrictMode: false, @@ -41,7 +41,7 @@ "@mui/x-date-pickers": "^6.6.0", "@next/bundle-analyzer": "^13.4.3", "@reduxjs/toolkit": "^1.9.5", - "@sentry/nextjs": "^7.59.2", + "@sentry/nextjs": "^8.42.0", "@types/cookie": "^0.5.1", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", @@ -51,6 +51,7 @@ "@types/react-syntax-highlighter": "^15.5.7", "axios": "^0.24.0", "base64-encode-file": "^1.0.7", + "buffer": "^6.0.3", "chart.js": "^4.4.0", "cookie": "^0.5.0", "cross-env": "^7.0.3", @@ -82,9 +83,11 @@ "remark-gfm": "^3.0.1", "sass": "^1.63.4", "styled-components": "^5.3.9", + "swiper": "^11.2.1", "typescript": "5.1.3" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", "@types/react-draft-wysiwyg": "^1.13.8",