@@ -0,0 +1,11 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1 @@ +export * from './message.routes' \ No newline at end of file @@ -0,0 +1,25 @@ +import { API_URL } from "@/src/shared/lib/constants" +import { IMessageRequest } from "@/src/shared/lib/types/types-gpt" +import axios, { AxiosResponse } from "axios" +import { Message, MessageSend } from "../types" + +export async function sendImage( + model: string | null, + dataForSend: MessageSend | FormData, + token?: string +) { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + + return await axios.post( + API_URL + `/media/image/${model}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + } + ) +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './message' \ No newline at end of file @@ -0,0 +1,18 @@ +export interface MessageSend { + content: string + file?: File | null + info: T +} + +export interface Message { + content: string + created_at: string + elapsed_time: string + file: File | null | string + from_model: boolean + info: null + is_favourite: boolean + is_sent: boolean + uid: string + model: string +} @@ -0,0 +1,2 @@ +export * from './types' +export * from './api' \ No newline at end of file @@ -1,4 +1,4 @@ -import { API_URL } from '#/shared/lib/constants' +import { API_URL } from '@/src/shared/lib/constants' import axios from 'axios' import { IModel } from '../types' @@ -1,4 +1,4 @@ -import { API_URL } from '#/shared/lib/constants' +import { API_URL } from '@/src/shared/lib/constants' import axios from 'axios' import { IShortModel } from '../types' @@ -2,8 +2,8 @@ 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' +import { useAppDispatch } from '@/src/main/store/store' +import { setParams } from '@/src/main/store/model-parametres-store' export function useImageBot(slug: string) { const [botParams, setBotParams] = useState(null) @@ -17,10 +17,12 @@ export function useImageBot(slug: string) { function setDefault(bot: IModel) { setVersion('') - dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + dispatch( + setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) } - // это пиз**ц + // это пиз**ц // нужен рефакторинг (я то в этом не разбираюсь) // а стажеры и подавно)))) function setStoreParams(bot: IModel) { @@ -68,13 +70,18 @@ export function useImageBot(slug: string) { } else { setVersion(botParams.slug) dispatch( - setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + setParams( + botParams.parameters.reduce( + (a, v) => ({ ...a, [v.key]: v.values.default }), + {} + ) + ) ) } } } - // это пиз**ц + // это пиз**ц const setDefaultParams = () => { if (!botParams) return @@ -82,14 +89,22 @@ export function useImageBot(slug: string) { if (version === '') { return dispatch( - setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + 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 }), + (a, v) => + v.versions.includes(version) + ? { ...a, [v.key]: v.values.default } + : { ...a }, {} ) ) @@ -111,8 +126,8 @@ export function useImageBot(slug: string) { version, modelType, fetchBotParams, - setVersion, - resetParams, - setDefaultParams, + setVersion, + resetParams, + setDefaultParams } } @@ -0,0 +1 @@ +export * from './use-image-bot-create' \ No newline at end of file @@ -0,0 +1,54 @@ +import { getUserBalance } from '@/src/entities/balance' +import { Message, MessageSend, sendImage } from '@/src/entities/message' +import { useAppDispatch } from '@/src/main/store/store' +import { Device } from '@/src/shared/lib/types/entities' +import { formDataHelper } from '@/src/widgets/messages' +import { useSession } from 'next-auth/react' +import { useState } from 'react' + +export function useImageBotCreateImage( + showError: (message: string) => void, + type: string, + device: Device, + setLoading: (value: boolean) => void, + setMessages: React.Dispatch> +) { + const [isComplete, setIsComplete] = useState(false) + + const { data } = useSession() + + const dispatch = useAppDispatch() + + const createImage = async (dataForSend: MessageSend) => { + const { content, file } = dataForSend + + setIsComplete(false) + setLoading(true) + + const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + + const response = await sendImage(type, dataSending, data?.access) + + if (response.status >= 400) showError('Ошибка отправки сообщения') + + setLoading(false) + + dispatch(getUserBalance(data?.access)) + + setMessages((prev: Message[]) => { + if (!prev || !prev.length) return response.data + + if (device === 'desktop') { + return [...response.data, ...prev] + } + return [...prev, ...response.data] + }) + + setIsComplete(true) + } + + return { + createImage, + isComplete + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { useAppSelector } from '@/src/main/store/store' +import { useState } from 'react' + +export function useImagesBotFilters() { + const [openFiltersMobile, setOpenFiltersMobile] = useState(false) + const [params, setParams] = useState(false) + const includeParams = useAppSelector((state) => state.params.params) + + return { + openFiltersMobile, + setOpenFiltersMobile, + params, + setParams, + includeParams, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-uniq-input' \ No newline at end of file @@ -0,0 +1,65 @@ +import { useShowData } from '@/src/shared' +import { MessageSend } from '@/src/shared/lib/types/model' +import { useState, ChangeEvent } from 'react' + +export function useImagesUniqInput( + version: string, + includeParams: object, + createImage: (dataForSend: MessageSend) => any +) { + const [image, setImage] = useState(null) + + const { error, showError } = useShowData() + + function onLoadImage(event: ChangeEvent) { + if (event.target.files) { + setImage(event.target.files[0]) + } + } + + function onCreateImage(input: string, required: (string | null)[]) { + + // про switch не слышали люди)) + if (required.includes('text') && (input === '' || input === null)) { + showError('Введите сообщение!') + return false + } + if (required.includes('image') && image === null) { + showError('Прикрепите изображение!') + return false + } + if (required.includes('zip') && image === null) { + showError('Прикрепите архив!') + return false + } + + // Снова какой то пиз**ц + let data = {} + if (version === '') { + data = { + ...includeParams, + } + } else { + data = { + version: version, + ...includeParams, + } + } + + createImage({ + content: input, + file: image, + info: { + ...data, + }, + }) + return true + } + + return { + image, + setImage, + onLoadImage, + onCreateImage, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-bot-pagination' \ No newline at end of file @@ -0,0 +1,115 @@ +import { Message } from '@/src/entities/message' +import { useAppSelector } from '@/src/main/store/store' +import { useShowData } from '@/src/shared' +import { Device } from '@/src/shared/lib/types/entities' +import { getImagesGalery } from '@/src/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { useRef, useState } from 'react' +import { Limit, LimitSize } from '../types' + +export function useImageBotPagination(deviceType: Device) { + const refScrollMobile = useRef(null) + const refScrollDesktop = useRef(null) + const mobileScrollContainer = useRef(null) + const offset = useRef(0) + + const [messages, setMessages] = useState([]) + + const [loading, setLoading] = useState(false) + + const { showError } = useShowData() + + const { data } = useSession() + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 20, + firstLimit: 30, + }, + medium: { + active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), + limit: 30, + firstLimit: 50, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 45, + firstLimit: 70, + }, + } + + const fetchMessages = async (count?: number) => { + if (!data) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery( + data.access, + offset.current, + count || 10 + ) + setLoading(false) + + if (response.status >= 400 || !Array.isArray(answer)) + return showError('Ошибка загрузки чата') + + if (deviceType === 'desktop') { + setMessages((prev) => [...prev, ...answer]) + offset.current = offset.current + answer.length + return + } + + setMessages((prev) => [...answer.reverse(), ...prev]) + offset.current = offset.current + answer.length + } + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (deviceType === 'desktop') { + const active = Object.values(limits).find((item) => item.active) + + if (offset.current > 0) return fetchMessages(active?.limit) + + fetchMessages(active?.firstLimit) + } + + if (!mobileScrollContainer.current) return + + const scrollBottom = + mobileScrollContainer.current.scrollHeight - mobileScrollContainer.current.scrollTop + + await fetchMessages() + + setTimeout(() => { + mobileScrollContainer.current!.scroll({ + top: mobileScrollContainer.current!.scrollHeight - scrollBottom, + behavior: 'smooth', + }) + }, 500) + } + + function onObserverMounted() { + const currentObserver = + deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current + + if (!currentObserver) return + + const observer = new IntersectionObserver(callback, { rootMargin: '400px' }) + + observer.observe(currentObserver!) + } + + return { + refScrollMobile, + refScrollDesktop, + onObserverMounted, + messages, + loading, + setLoading, + setMessages, + fetchMessages, + mobileScrollContainer, + } +} @@ -0,0 +1 @@ +export * from './limits' \ No newline at end of file @@ -0,0 +1,7 @@ +export type LimitSize = 'small' | 'medium' | 'large' + +export interface Limit { + active: boolean + limit: number + firstLimit: number +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -4,7 +4,14 @@ import Link from 'next/link' import styles from '@/src/shared/styles/chats-bot-pages.module.scss' -export default function Title(props: any) { +export interface TitleProps { + type: string + title: string + linkBack?: string + rightSlot?: React.ReactNode +} + +export default function Title(props: TitleProps) { return ( @@ -12,11 +19,19 @@ export default function Title(props: any) { {' '} {props.type} •{' '} -  {props.title} + +  {props.title} + - - {props.title} - +
+ + {props.title} + +
{props.rightSlot}
+
) } @@ -5,7 +5,7 @@ import axios from 'axios' import { UniqInput } from '@/src/main/components/uniq_input' import { useAppSelector } from '@/src/main/store/store' import { ChatProps } from '@/src/shared/lib/types/model' -import { ChatMessagesList } from '@/src/widgets/messages/chat-messages-list' +import { ChatMessagesList } from '@/src/widgets/messages/ui/chat-messages-list' import 'intro.js/introjs.css' @@ -29,7 +29,14 @@ interface Props { isLoader?: boolean } -export const Layout: React.FC = ({ children, device, isAuthPage = false, titlePage, title = titlePage, isLoader }) => { +export const Layout: React.FC = ({ + children, + device, + isAuthPage = false, + titlePage, + title = titlePage, + isLoader, +}) => { const { data: sessionData } = useSession() const appState = useAppSelector((state) => state) const dispatch = useAppDispatch() @@ -56,7 +63,10 @@ export const Layout: React.FC = ({ children, device, isAuthPage = false, }, [status]) React.useEffect(() => { - if ((sessionData && !sessionStorage.getItem('firstRender')) || (sessionData && !localStorage.getItem('global_settings'))) { + if ( + (sessionData && !sessionStorage.getItem('firstRender')) || + (sessionData && !localStorage.getItem('global_settings')) + ) { dispatch(getUserAccountSettings(sessionData.access)) sessionStorage.setItem('firstRender', 'true') } @@ -81,7 +91,10 @@ export const Layout: React.FC = ({ children, device, isAuthPage = false, targetDevice: device, targetType: 'sidemenu', }) - if (setting) setting?.value?.sidemenu_state === 'opened' ? setSidemenuDefaultOpen(true) : setSidemenuDefaultOpen(false) + if (setting) + setting?.value?.sidemenu_state === 'opened' + ? setSidemenuDefaultOpen(true) + : setSidemenuDefaultOpen(false) } }, [appState.settings.state]) @@ -126,9 +139,23 @@ export const Layout: React.FC = ({ children, device, isAuthPage = false, {!isAuthPage ? ( desktop ? ( - - - + + + {children} @@ -10,6 +10,7 @@ import { addToFavorites, deleteFromFavorites } from '@/src/shared/api/endpoints' import { Card } from '@/src/shared/card/card' import { API_URL } from '@/src/shared/lib/constants' import { getTypeDevice } from '@/src/shared/lib/helpers' +import { IShortModel } from '@/src/entities/model-entity' interface Model { uid: string @@ -46,7 +47,7 @@ export async function getServerSideProps(context: any): Promise<{ props: any }> } } -async function getImages(token?: string): Promise { +async function getImages(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { headers: { @@ -60,7 +61,7 @@ async function getImages(token?: string): Promise { } export const Images: React.FC = ({ deviceType }) => { - const [bots, setBots] = useState(null) + const [bots, setBots] = useState(null) const { data } = useSession() @@ -72,26 +73,7 @@ export const Images: React.FC = ({ deviceType }) => { getImages(data?.access).then((res) => setBots(res)) }, [data?.access]) - const changeFavorite = async (uid: string) => { - let res = 400 - if (bots?.find((el) => el.uid === uid)!.is_favourite === true) { - res = await deleteFromFavorites(uid, data?.access) - } else { - res = await addToFavorites(uid, data?.access) - } - - if (res !== 400) - setBots((prev) => - prev!.map((el) => { - if (el.uid === uid) { - el.is_favourite = !el.is_favourite - } - - return el - }) - ) - } return ( @@ -103,7 +85,8 @@ export const Images: React.FC = ({ deviceType }) => { uid={item.uid} key={item.uid} title={item.title} - icon={item.image_link} + blocked={item.blocked} + icon={item.image} text={item.description} slug={item.slug} /> @@ -62,6 +62,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { isTryRename, setIsTryRename, } = useChats(modelType) + const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel( currentChat, showError, @@ -211,7 +212,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { title={botParams?.title} > - + <Title title={botParams?.title ? botParams?.title : ''} type={'Чат-боты'} linkBack={'/chat-bot'} /> <Box className={styles.main}> <Box className={styles.chatWindow}> <AllChatWindow @@ -1,7 +1,7 @@ import * as React from 'react' -import { useRef } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useDispatch } from 'react-redux' -import { Collapse, Typography } from '@mui/material' +import { Collapse, Typography, useMediaQuery } from '@mui/material' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' import { useRouter } from 'next/router' @@ -20,12 +20,20 @@ import { useAppSelector } from '@/src/main/store/store' import { DrawerCustom, Error } from '@/src/shared' import model_api from '@/src/shared/api/models/api' import { useModelImages } from '@/src/shared/api/models/endpoints' -import { IModel } from '@/src/shared/api/models/models' +import { IModel, IShortModel } from '@/src/shared/api/models/models' import { getTypeDevice } from '@/src/shared/lib/helpers' import { getDeviceOs } from '@/src/shared/lib/helpers/get-type-device' -import { useShowData } from '@/src/shared/lib/hooks' +import { useShowData, useThemeAndDevice } from '@/src/shared/lib/hooks' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { ImageMessagesList } from '@/src/widgets/messages/image-messages-list' +import { ImageMessagesList } from '@/src/widgets/messages/ui/image-messages-list' +import { useImagesBots } from '@/src/entities/model-entity' +import { useImageBot } from '@/src/entities/model-entity/model/use-image-bot' +import { useImagesUniqInput } from '@/src/features/image-bot-input' +import { useImagesBotFilters } from '@/src/features/image-bot-filters' +import { useImageBotPagination } from '@/src/features/image-bot-pagination' +import { useImagesPagination } from '@/src/widgets/messages' +import { ImageModelsPopup } from '@/src/widgets/image-models-popup/ui' +import { useImageBotCreateImage } from '@/src/features/image-bot-create-image' export async function getServerSideProps(context: any): Promise<{ props: any }> { const deviceType = getTypeDevice(context) @@ -41,237 +49,74 @@ export async function getServerSideProps(context: any): Promise<{ props: any }> } const Images: React.FC<any> = ({ deviceType, deviceOs }) => { - const [image, setImage] = React.useState<File | null>(null) - const desktop = deviceType === 'desktop' - const ios = deviceOs === 'ios' - const { error, showError } = useShowData() - const [openFiltersMobile, setOpenFiltersMobile] = React.useState<boolean>(false) - const router = useRouter() - const { data } = useSession() - const [botParams, setBotParams] = React.useState<IModel | null>(null) - const [version, setVersion] = React.useState<string>('') - const [modelType, setModelType] = React.useState<string>('') - const [params, setParams] = React.useState<boolean>(false) - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() - - const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages<Setting>(showError, modelType, deviceType) - - const [chatScrollHeight, setChatScrollHeight] = React.useState(0) - const [scrollBottom, setScrollBottom] = React.useState(0) - const refScrollMobile = useRef<any>() - const [isPaginating, setIsPaginating] = React.useState(false) + const { query } = useRouter() - React.useEffect(() => { - model_api - .getBotParams(router.asPath.split('/')[2], data?.access) - .then((res) => { - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => - v.versions.includes(res.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - setVersion('') - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - }) - .catch((err) => {}) - }, [data?.access, router.query]) - - const onLoadImage = (event: React.ChangeEvent<HTMLInputElement>) => { - if (event.target.files) { - setImage(event.target.files[0]) - // showError('Файл успешно загружен, можете отправлять его!') - } - } - - const viewMobileSettings = () => { - setOpenFiltersMobile(true) - } + const { + botParams, + version, + modelType, + fetchBotParams, + resetParams, + setDefaultParams, + setVersion, + } = useImageBot(query.slug as string) - const hideMobileSettings = () => { - setOpenFiltersMobile(false) - } + const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs) - const resetParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (botParams.versions.length !== 0) { - setVersion(botParams.versions[0].slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => - v.versions.includes(botParams.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - setVersion(botParams.slug) - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - } - } + const { error, showError } = useShowData() - const setDefaultParams = () => { - if (botParams) { - dispatch(setParametres({})) - if (version !== '') { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => - v.versions.includes(version) - ? { ...a, [v.key]: v.values.default } - : { ...a }, - {} - ) - ) - ) - } else { - dispatch( - setParametres( - botParams.parameters.reduce( - (a, v) => ({ ...a, [v.key]: v.values.default }), - {} - ) - ) - ) - } - } - } + const router = useRouter() - const onCreateImage = (input: string, required: (string | null)[]) => { - if (required.includes('text') && (input === '' || input === null)) { - showError('Введите сообщение!') - return false - } - if (required.includes('image') && image === null) { - showError('Прикрепите изображение!') - return false - } - if (required.includes('zip') && image === null) { - showError('Прикрепите архив!') - return false - } - let data = {} - if (version === '') { - data = { - ...includeParams, - } - } else { - data = { - version: version, - ...includeParams, - } - } + const { data: session } = useSession() - createImage({ - content: input, - file: image, - info: { - ...data, - }, - }) - return true - } + const { openFiltersMobile, setOpenFiltersMobile, params, setParams, includeParams } = + useImagesBotFilters() - React.useEffect(() => { - if (isComplete) { - if (!desktop) { - const block = refScrollMobile.current - if (block) { - //@ts-ignore - block.scrollTop = block.scrollHeight - } - } else { - window.scroll(0, 0) - } - } - }, [isComplete]) + const { + refScrollMobile, + refScrollDesktop, + mobileScrollContainer, + onObserverMounted, + setLoading, + setMessages, + fetchMessages, + loading, + messages, + } = useImageBotPagination(deviceType) - const handleMobileScroll = () => { - setScrollBottom(refScrollMobile.current?.scrollHeight - refScrollMobile.current?.scrollTop - refScrollMobile.current?.clientHeight) + const { createImage, isComplete } = useImageBotCreateImage( + showError, + modelType, + deviceType, + setLoading, + setMessages + ) - if (refScrollMobile.current && messages?.length !== 0) { - const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current - if (scrollTop === 0) { - if (getMessagesPagination) { - setIsPaginating(true) - getMessagesPagination(deviceType) - } - } - } - } + const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput( + version, + includeParams, + createImage + ) - const handleScroll = () => { - if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight) { - setIsPaginating(true) - getMessagesPagination(deviceType) - } + async function onFetch() { + await Promise.all([fetchBotParams()]) } - React.useEffect(() => { - window.addEventListener('scroll', handleScroll) - return () => { - window.removeEventListener('scroll', handleScroll) - } - }, []) - - React.useEffect(() => { - const block = deviceType === 'desktop' ? window : refScrollMobile.current - if (block) { - if (messages != undefined && !isPaginating) { - setChatScrollHeight(block.scrollHeight) - const time = setTimeout(() => { - //@ts-ignore - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }, 350) - return () => clearTimeout(time) - } else if (messages != undefined && isPaginating) { - //@ts-ignore - block.scrollTop = block.scrollHeight - chatScrollHeight - - setChatScrollHeight(block.scrollHeight) - } - } - setIsPaginating(false) - }, [messages]) + useEffect(() => { + onFetch() + onObserverMounted() + }, [session, router.query]) return ( <Layout titlePage={botParams?.title ? botParams?.title : ''} device={deviceType}> - <Title title={botParams?.title} type={'Изображения'} linkBack={'/images'} /> + <Box sx={{ display: 'flex', alignItems: 'center', position: 'relative' }}> + <Title + rightSlot={<ImageModelsPopup model={botParams} />} + title={'Модель'} + type={'Изображения'} + linkBack={'/images'} + /> + </Box> <Box display={'flex'} justifyContent='space-between' @@ -306,49 +151,36 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> <Box className={'bg-color-block border-radius-main'} - sx={{ padding: '30px' }} + sx={{ padding: '30px', position: 'relative' }} > <ImageMessagesList isComplete={isComplete} device={deviceType} images={messages} /> - </Box> - </> - ) : ( - <Box className={'bg-color-block border-radius-main'}> - {scrollBottom > 500 && ( <Box sx={{ position: 'absolute', - left: 0, - right: 0, - width: 'fit-content', - cursor: 'pointer', - margin: '0 auto', - bottom: '100px', - zIndex: 10, + visibility: 'hidden', + bottom: '0', + height: '800px', + width: '100%', }} - onClick={() => { - const block = refScrollMobile.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - <ArrowDownScroll /> - </Box> - )} + ref={refScrollDesktop} + ></Box> + </Box> + </> + ) : ( + <Box className={'bg-color-block border-radius-main'}> <Box - ref={refScrollMobile} sx={{ padding: '30px', height: ios @@ -356,10 +188,15 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { : 'calc(200px + (400 - 200) * ((100vh - 400px) / (600 - 400)))', overflowY: 'scroll', overflowX: 'hidden', + position: 'relative', }} + ref={mobileScrollContainer} className={'smallScroll'} - onScroll={handleMobileScroll} > + <div + style={{ position: 'absolute', top: 300 }} + ref={refScrollMobile} + ></div> <ImageMessagesList isComplete={isComplete} device={deviceType} @@ -379,7 +216,9 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> @@ -465,7 +304,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { /> <ResetFilters desktop={desktop} - closeDrawer={hideMobileSettings} + closeDrawer={() => setOpenFiltersMobile(false)} reset={resetParams} /> </Collapse> @@ -482,7 +321,10 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { )} </Stack> )} - <DrawerCustom open={openFiltersMobile} onClose={hideMobileSettings}> + <DrawerCustom + open={openFiltersMobile} + onClose={() => setOpenFiltersMobile(false)} + > <Stack spacing={1} padding={2.4}> {botParams?.versions && botParams.versions.length !== 0 ? ( <> @@ -525,7 +367,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { params={botParams?.parameters} /> <ResetFilters - closeDrawer={hideMobileSettings} + closeDrawer={() => setOpenFiltersMobile(false)} desktop={desktop} reset={resetParams} /> @@ -19,6 +19,8 @@ axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, }) +axios.defaults.validateStatus = (status) => status < 500 + const inter = Raleway({ subsets: ['latin'] }) function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { @@ -19,6 +19,7 @@ interface 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' @@ -21,3 +21,16 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[] return el }) } + +export function formatDate( + date: string | number | Date, + options: Intl.DateTimeFormatOptions, + lang?: string +) { + if (typeof date !== "object") { + date = new Date(typeof date === "number" ? date : Date.parse(date)) + } + + const formatter = Intl.DateTimeFormat("ru", options) + return formatter.format(date) +} @@ -1,3 +1,5 @@ export { getAccessToken } from './get-token' export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' +export { c } from './string' +export * from './date-helper' @@ -0,0 +1,3 @@ +export function c(...args: Array<string | null | undefined | boolean>) { + return args.filter(Boolean).join(" ") +} @@ -1,13 +1,11 @@ import { useAppSelector } from '@/src/main/store/store' +import { DeviceOs } from '../types/entities' -interface IThemeAndDevice { - theme: 'dark' | 'light' - desktop: boolean -} -export const useThemeAndDevice = (device?: 'desktop' | 'mobile'): IThemeAndDevice => { +export const useThemeAndDevice = (device?: 'desktop' | 'mobile', deviceOs?: DeviceOs) => { const theme = useAppSelector((state) => state.theme.theme) const desktop = device === 'desktop' + const ios = deviceOs === 'ios' - return { theme, desktop } + return { theme, desktop, ios } } @@ -1,44 +1,44 @@ .main { - width: 100%; - display: flex; - justify-content: flex-start; - gap: 20px; - margin-bottom: 24px; + width: 100%; + display: flex; + justify-content: flex-start; + gap: 20px; + margin-bottom: 24px; - .chatWindow { - width: 70%; - } + .chatWindow { + width: 70%; + } - .settings { - width: 25%; - } + .settings { + width: 25%; + } - @media (max-width:768px) { - width: 100%; - display: flex; - - flex-direction: column-reverse; - - .chatWindow { - width: 100%; - } + @media (max-width: 768px) { + width: 100%; + display: flex; - .settings { - width: 100%; - } - } + flex-direction: column-reverse; + + .chatWindow { + width: 100%; + } + + .settings { + width: 100%; + } + } } -.wrapModelTitle{ - margin-bottom: 20px; - margin-top: 15px; - @media (max-width:768px) { - margin: 0; - margin-top: 15px; - } - .bigTitle { - @media (max-width:768px) { - display: none; - overflow: hidden; - } - } +.wrapModelTitle { + margin-bottom: 20px; + margin-top: 15px; + @media (max-width: 768px) { + margin: 0; + margin-top: 15px; + } +} + +.bigTitle { + @media (max-width: 768px) { + font-size: 26px !important; + } } @@ -0,0 +1 @@ +export * from './use-image-models-popup' \ No newline at end of file @@ -0,0 +1,36 @@ +import { useImagesBots } from '@/src/entities/model-entity' +import { useMemo, useState } from 'react' + +export function useImageModelsPopup() { + const { bots, fetchBots, setBots } = useImagesBots() + + const [popupOpen, setPopupOpen] = useState(false) + + const fileteredModels = useMemo( + () => + bots.sort((a, b) => + a.blocked && !b.blocked ? 1 : !a.blocked && b.blocked ? -1 : 0 + ), + [bots] + ) + + const averageTokenCost = useMemo(() => { + if (!bots.length) return 0 + + const prices = bots.filter((item) => item.actual_stat).map((item) => item.actual_stat) + + const sum = prices.reduce((acc, item) => acc + Number(item.tokens_cost), 0) + + return sum / prices.length + }, [bots]) + + return { + bots, + setBots, + fetchBots, + averageTokenCost, + popupOpen, + fileteredModels, + setPopupOpen, + } +} @@ -0,0 +1 @@ +export type CostZones = 30 | 60 | 90 @@ -0,0 +1 @@ +export * from './cost' \ No newline at end of file @@ -0,0 +1,80 @@ +.container { + position: relative; + + @media (max-width: 768px) { + position: static; + } +} +.wrapper { + background: rgba($color: #000000, $alpha: 0.4); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 104; + visibility: hidden; + opacity: 0; + transition: all 0.3s ease-in-out; + + &_open { + visibility: visible; + opacity: 1; + } +} +.select { + padding: 0px 18px; + background: rgba(#8280ff, 0.1); + border-radius: 10px; + display: flex; + align-items: center; + width: max-content; + gap: 10px; + cursor: pointer; + + &__arrow { + padding-top: 5px; + @media (max-width: 768px) { + width: 15px; + height: 15px; + } + } + + &__title { + color: var(--air-color); + font-size: 35px; + font-weight: 600; + + @media (max-width: 768px) { + font-size: 26px !important; + } + } +} + +.popup { + background: var(--new-ui-main-color); + border-radius: 15px; + width: max-content; + position: absolute; + z-index: 105; + transform: translateY(20px); + visibility: hidden; + opacity: 0; + transition: all 0.3s ease-in-out; + border: 1px solid #40404e4b; + max-height: 70vh; + overflow-y: scroll; + top: calc(100% + 10px); + + @media (max-width: 768px) { + left: 0; + right: 0; + width: fit-content; + } + + &_open { + visibility: visible; + opacity: 1; + transform: translateY(0); + } +} @@ -0,0 +1,61 @@ +import { Typography } from '@mui/material' +import React, { useEffect } from 'react' +import styles from './image-models-popup.module.scss' +import { c } from '@/src/shared/lib/helpers' +import { useImageModelsPopup } from '../model' +import { ImagePopupModel } from './image-popup-model' +import { IModel } from '@/src/entities/model-entity' +import { useRouter } from 'next/router' +import ArrowDownSvg from '@/src/assets/svg/arrow-down.svg?react' + +interface ImageModelsPopupProps { + model: IModel | null +} + +export const ImageModelsPopup = ({ model }: ImageModelsPopupProps) => { + const { fileteredModels, fetchBots, averageTokenCost, popupOpen, setPopupOpen } = + useImageModelsPopup() + + const { push } = useRouter() + + useEffect(() => { + fetchBots() + }, []) + + return ( + <> + <div className={c(styles.container)}> + <div onClick={() => setPopupOpen((prev) => !prev)} className={c(styles.select)}> + <Typography className={styles.select__title}> + {model ? model.title : 'Загрузка...'} + </Typography> + <ArrowDownSvg + className={styles.select__arrow} + style={{ transform: popupOpen ? 'rotate(180deg)' : 'rotate(0deg)' }} + fill='#8280FF' + width={20} + height={20} + /> + </div> + <div + onClick={() => setPopupOpen(false)} + className={c(styles.wrapper, popupOpen ? styles.wrapper_open : null)} + ></div> + <div className={c(styles.popup, popupOpen ? styles.popup_open : null)}> + {fileteredModels.map((item) => ( + <ImagePopupModel + averageTokenCost={averageTokenCost} + active={model ? item.slug === model.slug : false} + setActive={(slug) => { + setPopupOpen(false) + push(`/images/${slug}`) + }} + {...item} + key={item.uid} + /> + ))} + </div> + </div> + </> + ) +} @@ -0,0 +1,99 @@ +.model { + // font-family: Inter; + padding: 15px 20px; + cursor: pointer; + display: flex; + align-items: center; + gap: 22px; + justify-content: space-between; + max-width: 500px; + transition: background-color 0.3s ease; + position: relative; + + &:hover { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + } + + @media (max-width: 768px) { + // max-width: calc(100% - 100px) !important; + width: 100%; + // max-width: unset; + } + + &_blocked { + &:hover { + background-color: transparent; + } + } + + &__blocked { + background-color: rgba($color: #000000, $alpha: 0.7); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-right: 10px; + span { + color: white; + font-weight: 600; + padding-left: 8px; + } + } + + &__locked { + background-color: rgba($color: #000000, $alpha: 0.7); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-right: 10px; + span { + color: white; + font-weight: 600; + padding-left: 8px; + } + } + &__title { + font-weight: 600; + font-size: 18px; + margin-bottom: 4px; + } + + &__description { + font-size: 14px; + color: #a4aab5; + margin-bottom: 7px; + } + + &__badges { + display: flex; + gap: 10px; + } + + &__badge { + padding: 4px 6px; + font-size: 13px; + display: flex; + align-items: center; + gap: 2px; + background-color: rgba($color: #a4aab5, $alpha: 0.1); + color: white; + font-weight: 500; + border-radius: 5px; + } + + &__time { + gap: 6px; + color: var(--new-ui-gray-color); + } + &__cost { + } +} @@ -0,0 +1,118 @@ +import { IShortModel } from '@/src/entities/model-entity' +import React, { useMemo } from 'react' + +import styles from './image-popup-model.module.scss' +import { c, formatDate } from '@/src/shared/lib/helpers' +import SuccessRoundedSvg from '@/src/assets/svg/success-rounded.svg?react' +import ClockSvg from '@/src/assets/svg/clock.svg?react' +import LightningSvg from '@/src/assets/svg/lightning.svg?react' +import LockSvg from '@/src/assets/svg/lock.svg?react' +import BlockedSvg from '@/src/assets/svg/blocked.svg?react' +import { useAppSelector } from '@/src/main/store/store' + +interface ImagePopupModelProps extends IShortModel { + active: boolean + setActive: (slug: string) => void + averageTokenCost: number +} + +export interface CostZone { + remainder: number + color: string +} + +export const costZoneColors: CostZone[] = [ + { remainder: -20, color: '#1EB034' }, + { remainder: 20, color: '#B0891E' }, + { remainder: 1000, color: '#B01E1E' }, +] + +export const ImagePopupModel = ({ + title, + description, + slug, + blocked, + actual_stat, + active, + averageTokenCost, + setActive, +}: ImagePopupModelProps) => { + const user = useAppSelector((state) => state.user) + + const accessed_models = useMemo( + () => (user.payment_plan ? user.payment_plan.plan.accessed_models : []), + [user] + ) + + const isAccess = useMemo( + () => accessed_models && accessed_models.includes(slug), + [accessed_models] + ) + + // жесткий костыль))) - временный + const formatedTime = useMemo(() => { + if (!actual_stat) return null + + const seconds = Number(actual_stat.generation_time.split(':')[2]) + + const range = Math.ceil(seconds - seconds / 3) + + return `${range} - ${seconds} сек.` + }, [actual_stat]) + + const costZone = useMemo(() => { + if (!actual_stat) return null + + const remainder = Number(actual_stat.tokens_cost) - averageTokenCost + + return costZoneColors.find((item) => item.remainder > remainder) + }, [actual_stat, averageTokenCost]) + + return ( + <div + onClick={() => { + if (blocked || !isAccess) return + setActive(slug) + }} + className={c(styles.model, blocked && styles.model_blocked)} + > + {blocked ? ( + <div className={styles.model__blocked}> + <BlockedSvg width={28} height={28} color='#FF2372' /> + <span>Модель недоступна</span> + </div> + ) : ( + !isAccess && ( + <div className={styles.model__locked}> + <LockSvg width={23} height={23} color='#8280FF' /> + <span>Недоступно в текущем тарифе</span> + </div> + ) + )} + + <div className={styles.model__content}> + <p className={styles.model__title}>{title}</p> + <p className={styles.model__description}>{description}</p> + + {actual_stat && ( + <div className={styles.model__badges}> + <div className={c(styles.model__badge, styles.model__time)}> + <ClockSvg width={16} height={16} /> + <span>{formatedTime}</span> + </div> + + <div + style={{ color: costZone?.color }} + className={c(styles.model__badge, styles.model__cost)} + > + <LightningSvg fill={costZone?.color} width={10} height={18} /> + {Number(actual_stat.tokens_cost)} + </div> + </div> + )} + </div> + + {active && <SuccessRoundedSvg width={24} height={24} />} + </div> + ) +} @@ -0,0 +1,2 @@ +export * from './image-models-popup' +export * from './image-popup-model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './types' \ No newline at end of file @@ -0,0 +1,14 @@ +import { Message, MessageSend } from '@/src/entities/message' +import { API_URL } from '@/src/shared/lib/constants' +import { IMessageRequest } from '@/src/shared/lib/types/types-gpt' +import axios, { AxiosError, AxiosResponse } from 'axios' + +export async function getImagesGalery(token?: string, offset?: number, limit = 10) { + return await axios.get<Message>(API_URL + `/media/gallery/images?limit=${limit}&offset=${offset}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} + + @@ -0,0 +1 @@ +export * from './image-messages.routes' \ No newline at end of file @@ -0,0 +1,10 @@ +import { MessageSend } from '@/src/entities/message' + +export function formDataHelper(file: File, dataForSend: MessageSend<any>): FormData { + const FD = new FormData() + FD.append('file', file) + FD.append('info', JSON.stringify(dataForSend.info)) + FD.append('content', JSON.stringify(dataForSend.content)) + + return FD +} @@ -0,0 +1,3 @@ +export * from './date-from-string' +export * from './day-months-string' +export * from './form-data' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-images-pagination' \ No newline at end of file @@ -0,0 +1,72 @@ +import { getUserBalance } from '@/src/entities/balance' +import { Message, MessageSend, sendImage } from '@/src/entities/message' +import { useAppDispatch } from '@/src/main/store/store' +import { useSession } from 'next-auth/react' +import { useState, useRef, useEffect, useCallback } from 'react' +import { getImagesGalery } from '../api' +import { Device } from '@/src/shared/lib/types/entities' +import { formDataHelper } from '../lib' + +export function useImagesPagination( + showError: (message: string) => void, + type: string, + device: Device +) { + const { data } = useSession() + + const [messages, setMessages] = useState<Message[]>([]) + + const [loading, setLoading] = useState<boolean>(false) + + const [offset, setOffset] = useState<number>(0) + + const dispatch = useAppDispatch() + + async function onFetch() { + if (!data) return + + setLoading(true) + console.log(offset) + const { data: answer, ...response } = await getImagesGalery(data.access, offset) + setLoading(false) + + if (response.status >= 400) return showError('Ошибка загрузки чата') + + if (!Array.isArray(answer)) return showError('Ошибка загрузки чата') + + if (device === 'desktop') { + setMessages(answer) + return setOffset(answer.length) + } + + setMessages(answer.reverse()) + setOffset(answer.length) + } + + useEffect(() => { + onFetch() + }, [data, type]) + + const getMessagesPagination = async (device: 'mobile' | 'desktop') => { + if (!data) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery(data.access, offset) + setLoading(false) + + if (response.status >= 400) return showError('Ошибка загрузки чата') + + if (!Array.isArray(answer)) return showError('Ошибка загрузки сообщений') + + if (device === 'mobile') { + const newMessages = answer.reverse() + setMessages([...newMessages, ...messages]) + return setOffset((prev) => prev + answer.length) + } + + setMessages([...messages, ...answer]) + setOffset((prev) => prev + answer.length) + } + + return { messages, loading, getMessagesPagination } +} @@ -4,8 +4,8 @@ import { useSession } from 'next-auth/react' import { Message } from '@/src/shared/lib/types/model' import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' -import { IsNextDay } from '@/src/widgets/messages/is-next-day' -import { PreviewView } from '@/src/widgets/messages/message-components/preview-view' +import { IsNextDay } from '@/src/widgets/messages/ui/is-next-day' +import { PreviewView } from '@/src/widgets/messages/ui/preview-view' interface IMessagesList { device: 'mobile' | 'desktop' @@ -22,7 +22,18 @@ interface IMessagesList { } export const ChatMessagesList: React.FC<IMessagesList> = memo( - ({ messageResponse, onLoadImage, setResendValue, modelTitle, device, modelType, mode, getMessagesPagination, deleteMessage, loading }) => { + ({ + messageResponse, + onLoadImage, + setResendValue, + modelTitle, + device, + modelType, + mode, + getMessagesPagination, + deleteMessage, + loading, + }) => { const paginationScroll = React.useRef<any>() const [isPaginating, setIsPaginating] = React.useState(false) const [chatScrollHeight, setChatScrollHeight] = React.useState(0) @@ -62,8 +73,16 @@ export const ChatMessagesList: React.FC<IMessagesList> = memo( }, [messageResponse]) const handleScroll = () => { - console.log(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) - setScrollBottom(paginationScroll.current?.scrollHeight - paginationScroll.current?.scrollTop - paginationScroll.current?.clientHeight) + console.log( + paginationScroll.current?.scrollHeight - + paginationScroll.current?.scrollTop - + paginationScroll.current?.clientHeight + ) + setScrollBottom( + paginationScroll.current?.scrollHeight - + paginationScroll.current?.scrollTop - + paginationScroll.current?.clientHeight + ) if (paginationScroll.current && messageResponse?.length !== 0) { const { scrollTop, scrollHeight, clientHeight } = paginationScroll.current @@ -139,7 +158,11 @@ export const ChatMessagesList: React.FC<IMessagesList> = memo( )} {messageResponse?.length === 0 && status === 'authenticated' ? ( - <>{desktop && modelType !== 'deepl' && <PreviewView setValue={setResendValue} />}</> + <> + {desktop && modelType !== 'deepl' && ( + <PreviewView setValue={setResendValue} /> + )} + </> ) : ( messageResponse?.map((message, idx) => { return ( @@ -0,0 +1,47 @@ +.model { + position: absolute; + top: 12px; + left: 12px; + z-index: 100; + background-color: rgba($color: #151518, $alpha: 0.5); + font-size: 13px; + padding: 2px 8px; + border-radius: 10px; + font-weight: 500; + color: #fff; +} +.wrap { + display: flex; + overflow: hidden; + width: 100%; + flex-wrap: wrap; + gap: 20px; + @media (max-width: 766px) { + height: 70vh; + width: 100%; + overflow-y: scroll; + text-align: center; + justify-content: center; + } + div { + margin-right: 10px; + @media (max-width: 766px) { + width: 100%; + } + + .image { + margin: 10px auto; + width: 250px; + height: 250px; + border-radius: 15px !important; + } + + @media (max-width: 1700px) { + } + + @media (max-width: 766px) { + text-align: center; + width: 80%; + } + } +} @@ -8,9 +8,9 @@ import FullScreenModal from '@/src/features/image-modal/full-screen-modal' import { useAppSelector } from '@/src/main/store/store' import { Success, TooltipCustom, useShowData } from '@/src/shared' import { useAutoScroll } from '@/src/shared/lib/hooks' -import { Message } from '@/src/shared/lib/types/model' import styles from './image-messages-list.module.scss' +import { Message } from '@/src/entities/message' interface MessagesList { device: 'mobile' | 'desktop' @@ -56,11 +56,25 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images } } - const ImageIcons = ({ uid, url, content }: { uid: string; url: string | null; content: string | undefined }) => { + const ImageIcons = ({ + uid, + url, + content, + }: { + uid: string + url: string | null + content: string | undefined + }) => { return ( <> <svg - style={{ position: 'absolute', top: '10px', right: '10px', cursor: 'pointer', zIndex: '5' }} + style={{ + position: 'absolute', + top: '10px', + right: '10px', + cursor: 'pointer', + zIndex: '5', + }} onClick={() => toggleMenu(uid)} width='35' height='35' @@ -68,7 +82,12 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images fill='none' xmlns='http://www.w3.org/2000/svg' > - <circle cx='17.5' cy='17.5' r='17.5' fill={theme === 'dark' ? '#303035' : '#FFFFFF'} /> + <circle + cx='17.5' + cy='17.5' + r='17.5' + fill={theme === 'dark' ? '#303035' : '#FFFFFF'} + /> <path fillRule='evenodd' clipRule='evenodd' @@ -167,13 +186,13 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images > {images?.length !== 0 && Array.isArray(images) && - images.map((message) => { + images.map((message, index) => { //@ts-ignore const isZip = message.file && message.file.includes('.zip') //@ts-ignore const isSvg = message.file && message.file.includes('.svg') return ( - <Box key={message.uid} sx={{ position: 'relative' }}> + <Box key={index} sx={{ position: 'relative' }}> {isZip ? ( <Box display='flex' @@ -182,14 +201,29 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images sx={{ border: '1px solid #8280FF', borderRadius: 5, - width: device === 'desktop' ? '250px' : '291px', - height: device === 'desktop' ? '284px' : '291px', + width: + device === 'desktop' + ? '250px' + : '291px', + height: + device === 'desktop' + ? '284px' + : '291px', }} > <Box textAlign='center'> - <Typography>Эта генерация является архивом</Typography> - <Typography color='#8280FF' marginTop={2}> - <Link href={message.file!.toString()}>Скачать</Link> + <Typography> + Эта генерация является архивом + </Typography> + <Typography + color='#8280FF' + marginTop={2} + > + <Link + href={message.file!.toString()} + > + Скачать + </Link> </Typography> </Box> </Box> @@ -198,47 +232,74 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images <Box sx={{ cursor: 'pointer', - width: device === 'desktop' ? '250px' : '291px', - height: device === 'desktop' ? '250px' : '291px', + width: + device === 'desktop' + ? '250px' + : '291px', + height: + device === 'desktop' + ? '250px' + : '291px', position: 'relative', overflow: 'hidden', borderRadius: '15px', }} > + <span className={styles.model}> + {message.model} + </span> {!isSvg ? ( <> <Image className={styles.image} onClick={() => { - setChosenImage(message.file as string) + setChosenImage( + message.file as string + ) setModal(true) }} - onLoadingComplete={() => setLoaded(true)} + onLoadingComplete={() => + setLoaded(true) + } style={{ - position: 'relative', + position: + 'relative', zIndex: '2', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', - objectFit: 'contain', + objectFit: + 'contain', }} width={500} height={500} - src={(message.file as unknown as string) || ''} - alt={'К сожалению, изображение не загрузилось'} + src={ + (message.file as unknown as string) || + '' + } + alt={ + 'К сожалению, изображение не загрузилось' + } /> <Image className={styles.image} - onLoadingComplete={() => setLoaded(true)} + onLoadingComplete={() => + setLoaded(true) + } style={{ - position: 'absolute', + position: + 'absolute', zIndex: '1', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', objectFit: 'cover', right: 0, @@ -246,7 +307,10 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images }} width={10} height={10} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='' /> </> @@ -255,19 +319,26 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images <img className={styles.image} onClick={() => { - setChosenImage(message.file as string) + setChosenImage( + message.file as string + ) setModal(true) }} style={{ - position: 'relative', + position: + 'relative', zIndex: '2', borderRadius: 15, width: '100%', height: '100%', userSelect: 'none', - objectFit: 'contain', + objectFit: + 'contain', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> <img @@ -275,18 +346,24 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images width='10px' height='10px' style={{ - position: 'absolute', + position: + 'absolute', zIndex: '1', borderRadius: 15, width: '100%', height: '100%', - opacity: loaded ? '100%' : '0%', + opacity: loaded + ? '100%' + : '0%', userSelect: 'none', objectFit: 'cover', right: 0, filter: 'blur(10px) brightness(0.7)', }} - src={(message.file as unknown as string) || ''} + src={ + (message.file as unknown as string) || + '' + } alt='К сожалению, изображение не загрузилось' /> </> @@ -295,14 +372,23 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images {!loaded && ( <Skeleton sx={{ - background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', + background: + theme === 'dark' + ? '#2d2d2f' + : '#EFF0F2', position: 'absolute', top: '0', left: '0', zIndex: '3', borderRadius: 5, - width: device === 'desktop' ? '250px' : '100%', - height: device === 'desktop' ? '250px' : '100%', + width: + device === 'desktop' + ? '250px' + : '100%', + height: + device === 'desktop' + ? '250px' + : '100%', }} variant='rectangular' /> @@ -311,29 +397,47 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images key={message.uid} uid={message.uid} content={message.content} - url={message.file ? message.file.toString() : null} + url={ + message.file + ? message.file.toString() + : null + } /> </Box> <TooltipCustom key={message.uid} placement='right' - title={message.content.length > 30 ? message.content : ''} + title={ + message.content.length > 30 + ? message.content + : '' + } > <Typography sx={{ fontSize: '15px', - maxWidth: device === 'desktop' ? '250px' : '100%', + maxWidth: + device === 'desktop' + ? '250px' + : '100%', marginTop: '12px', - color: theme === 'dark' ? '#A4AAB5' : '#555556', + color: + theme === 'dark' + ? '#A4AAB5' + : '#555556', }} > {!loaded ? '' : message.content.length > 0 - ? message?.content.replaceAll('"', '').slice(0, 30) + ? message?.content + .replaceAll('"', '') + .slice(0, 30) : 'описание отсутствует'} - {message?.content.length > 30 && loaded && '...'} + {message?.content.length > 30 && + loaded && + '...'} </Typography> </TooltipCustom> </Box> @@ -0,0 +1,7 @@ +export * from './chat-messages-list' +export * from './image-messages-list' +export * from './answer-wrap' +export * from './bot-message' +export * from './preview-view' +export * from './user-message' +export * from './is-next-day' \ No newline at end of file @@ -2,9 +2,9 @@ import React, { memo } from 'react' import { Box, Typography } from '@mui/material' import { Message } from '@/src/shared/lib/types/model' -import { getDateFromString } from '@/src/widgets/messages/lib/getDateFromString' -import { getDayMontsString } from '@/src/widgets/messages/lib/getDayMontsString' -import { UserMessage } from '@/src/widgets/messages/message-components/user-message' +import { getDateFromString } from '@/src/widgets/messages/lib/date-from-string' +import { getDayMontsString } from '@/src/widgets/messages/lib/day-months-string' +import { UserMessage } from '@/src/widgets/messages/ui/user-message' interface IProps { message: Message @@ -21,7 +21,18 @@ interface IProps { } export const IsNextDay = memo( - ({ message, index, onLoadImage, messageResponse, modelTitle, deleteMessage, isNewMessage, modelType, device, setResendValue }: IProps) => { + ({ + message, + index, + onLoadImage, + messageResponse, + modelTitle, + deleteMessage, + isNewMessage, + modelType, + device, + setResendValue, + }: IProps) => { if (messageResponse && message.created_at) { const date = getDateFromString(message.created_at) const prevDate = getDateFromString(messageResponse[index]?.created_at) @@ -7,7 +7,7 @@ import FullScreenModal from '@/src/features/image-modal/full-screen-modal' import { useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' import { Message } from '@/src/shared/lib/types/model' -import { BotMessage } from '@/src/widgets/messages/message-components/bot-message' +import { BotMessage } from '@/src/widgets/messages/ui/bot-message' interface IMessagesList { // messageResponse: Message[] | null @@ -64,7 +64,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) return ( <> - {props.message.file && <FullScreenModal modal={modal} setModal={setModal} image={props.message.file.toString()} />} + {props.message.file && ( + <FullScreenModal + modal={modal} + setModal={setModal} + image={props.message.file.toString()} + /> + )} <span className='tutorial-message'> {!props.message.from_model ? ( // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> @@ -90,7 +96,9 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) marginLeft: 1, }} > - {props.message.created_at.slice(10, 16).replace('T', ' ')} + {props.message.created_at + .slice(10, 16) + .replace('T', ' ')} </Typography> </Stack> @@ -110,7 +118,9 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={() => { setModal(true) }} - onLoadingComplete={() => setLoaded(true)} + onLoadingComplete={() => + setLoaded(true) + } style={{ objectFit: 'contain', height: '100%', @@ -124,12 +134,17 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) width={500} height={500} src={props.message.file.toString()} - alt={'К сожалению, изображение не загрузилось'} + alt={ + 'К сожалению, изображение не загрузилось' + } /> {!loaded && ( <Skeleton sx={{ - background: theme === 'dark' ? '#2d2d2f' : '#EFF0F2', + background: + theme === 'dark' + ? '#2d2d2f' + : '#EFF0F2', position: 'absolute', top: '0', left: '0', @@ -143,14 +158,25 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) )} </Box> <Box display='flex' alignItems='center'> - <Box marginRight={1} sx={{ cursor: 'pointer' }}> + <Box + marginRight={1} + sx={{ cursor: 'pointer' }} + > <Box onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} + aria-controls={ + open + ? 'message-menu' + : undefined + } aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} + aria-expanded={ + open ? 'true' : undefined + } > - <TooltipCustom title={'Открыть меню'}> + <TooltipCustom + title={'Открыть меню'} + > <svg width='22' height='22' @@ -185,7 +211,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) stroke='#A4AAB5' strokeWidth='2' /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> </svg> </TooltipCustom> </Box> @@ -198,12 +230,18 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={handleClose} sx={{ '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', color: '#8280FF', borderRadius: '15px', }, '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', borderRadius: '15px', }, }} @@ -218,7 +256,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '10px', }} onClick={() => { - copy(props.message.content) + copy( + props.message + .content + ) }} > <svg @@ -262,7 +303,11 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '5px', }} onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) + if (props.deleteMessage) + props.deleteMessage( + props.message + .uid + ) }} > <svg @@ -287,7 +332,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) {!props.message.is_sent && ( <Image onClick={resend} - style={{ cursor: 'pointer', marginRight: '5px' }} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} height={20} width={20} alt='1' @@ -308,7 +356,11 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) fontWeight: '400px', marginTop: 0.4, textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', + whiteSpace: + props.message.content.length > + 30 + ? 'pre-wrap' + : 'pre', }} > {/*<Markdown content={props.message.content} />*/} @@ -321,9 +373,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) <Box marginRight={1} sx={{ cursor: 'pointer' }}> <Box onClick={handleClick} - aria-controls={open ? 'message-menu' : undefined} + aria-controls={ + open ? 'message-menu' : undefined + } aria-haspopup='true' - aria-expanded={open ? 'true' : undefined} + aria-expanded={ + open ? 'true' : undefined + } > <TooltipCustom title={'Открыть меню'}> <svg @@ -360,7 +416,13 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) stroke='#A4AAB5' strokeWidth='2' /> - <circle cx='14.5' cy='4.5' r='3.5' stroke='#A4AAB5' strokeWidth='2' /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> </svg> </TooltipCustom> </Box> @@ -373,12 +435,18 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) onClick={handleClose} sx={{ '& .MuiMenu-list': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', color: '#8280FF', borderRadius: '15px', }, '& .MuiPopover-paper': { - backgroundColor: theme === 'dark' ? '#303035' : '#EFF0F2', + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', borderRadius: '15px', }, }} @@ -437,7 +505,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) gap: '5px', }} onClick={() => { - if (props.deleteMessage) props.deleteMessage(props.message.uid) + if (props.deleteMessage) + props.deleteMessage( + props.message.uid + ) }} > <svg @@ -462,7 +533,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) {!props.message.is_sent && ( <Image onClick={resend} - style={{ cursor: 'pointer', marginRight: '5px' }} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} height={20} width={20} alt='1' @@ -483,7 +557,10 @@ export const UserMessage = React.memo(function UserMessage(props: IMessagesList) fontWeight: '400px', marginTop: 0.4, textAlign: 'left', - whiteSpace: props.message.content.length > 30 ? 'pre-wrap' : 'pre', + whiteSpace: + props.message.content.length > 30 + ? 'pre-wrap' + : 'pre', }} > {/*<Markdown content={props.message.content} />*/} @@ -1,36 +0,0 @@ -.wrap { - display: flex; - overflow: hidden; - width: 100%; - flex-wrap: wrap; - gap: 20px; - @media (max-width: 766px) { - height: 70vh; - width: 100%; - overflow-y: scroll; - text-align: center; - justify-content: center; - } - div { - margin-right: 10px; - @media (max-width: 766px) { - width: 100%; - } - - .image { - margin:10px auto; - width: 250px; - height: 250px; - border-radius: 15px !important; - } - - @media (max-width: 1700px) { - - } - - @media (max-width: 766px) { - text-align: center; - width: 80%; - } - } -} \ No newline at end of file @@ -0,0 +1,4 @@ +export * from './ui' +export * from './lib' +export * from './api' +export * from './model' \ No newline at end of file @@ -30,10 +30,6 @@ export const Search = ({ device }: SearchProps) => { visibleMediaLinks, staticLinks ) - - useEffect(() => { - console.log(links) - }, [links]) const { search, setSearch, searchOpen, setSearchOpen, searchRef, filteredLinks } = useNavigationSearchModel(links) @@ -18,7 +18,10 @@ import { signOut, useSession } from 'next-auth/react' import { addUserSettings } from '@/src/entities/user-account/api/add-user-settings' import { updateUserSettings } from '@/src/entities/user-account/api/update-user-settings' import { isSettingExist } from '@/src/entities/user-account/lib/helpers/is-setting-exist' -import { addUserAccountSettings, updateUserAccountSettings } from '@/src/entities/user-account/model/settings' +import { + addUserAccountSettings, + updateUserAccountSettings, +} from '@/src/entities/user-account/model/settings' import { getAll, ResponseAllInfo } from '@/src/entities/user-account/model/user-type-slice' import { useAppDispatch, useAppSelector } from '@/src/main/store/store' import { TooltipCustom } from '@/src/shared' @@ -29,12 +32,27 @@ import styles from '../styles/styles.module.css' export const menuListTop = [ { title: 'Дашборд', link: '/', icon: '/svg/side-menu/market', activeList: [] }, - { title: 'Оплата', link: '/account?scope=subscribe', icon: '/svg/side-menu/star', activeList: ['subscribe'] }, + { + title: 'Оплата', + link: '/account?scope=subscribe', + icon: '/svg/side-menu/star', + activeList: ['subscribe'], + }, ] export const menuListMiddle = [ - { title: 'Чат-боты', link: '/chat-bot', icon: '/svg/side-menu/chat', activeList: ['chat-bot'] }, - { title: 'Изображения', link: '/images', icon: '/svg/side-menu/image', activeList: ['images'] }, + { + title: 'Чат-боты', + link: '/chat-bot', + icon: '/svg/side-menu/chat', + activeList: ['chat-bot'], + }, + { + title: 'Изображения', + link: '/images/flux', + icon: '/svg/side-menu/image', + activeList: ['images'], + }, // { title: 'Копирайтинг', link: '/copywriting/my', icon: '/svg/side-menu/copyrating', activeList: ['copywriting'] }, // { title: 'Видео', link: '/video', icon: '/svg/side-menu/video', activeList: [] }, // { title: 'Аудио', link: '/audio', icon: '/svg/side-menu/audio', activeList: [] }, @@ -73,22 +91,30 @@ const closedMixin = (theme: Theme): CSSObject => ({ }, }) -const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})) +const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( + ({ theme, open }) => ({ + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', + boxSizing: 'border-box', + ...(open && { + ...openedMixin(theme), + '& .MuiDrawer-paper': openedMixin(theme), + }), + ...(!open && { + ...closedMixin(theme), + '& .MuiDrawer-paper': closedMixin(theme), + }), + }) +) -export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; sidemenuDefaultOpen: boolean }) => { +export const SideMenu = ({ + device, + sidemenuDefaultOpen, +}: { + device: Device + sidemenuDefaultOpen: boolean +}) => { const [open, setOpen] = React.useState(sidemenuDefaultOpen) const [isInitialValueSet, setIsInitialValueSet] = React.useState(false) @@ -125,7 +151,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side useEffect(() => { if (settings.state !== null && isInitialValueSet) { - let response = isSettingExist({ settings: settings.state, targetDevice: device, targetType: 'sidemenu' }) + let response = isSettingExist({ + settings: settings.state, + targetDevice: device, + targetType: 'sidemenu', + }) if (response && data?.access) { dispatch( @@ -140,7 +170,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side dispatch( addUserAccountSettings({ token: data.access, - setting: { device, type: 'sidemenu', value: { sidemenu_state: open ? 'opened' : 'closed' } }, + setting: { + device, + type: 'sidemenu', + value: { sidemenu_state: open ? 'opened' : 'closed' }, + }, }) ) } @@ -149,7 +183,11 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side useEffect(() => { if (settings.state !== null && !isInitialValueSet) { - let setting = isSettingExist({ settings: settings.state, targetDevice: device, targetType: 'sidemenu' }) + let setting = isSettingExist({ + settings: settings.state, + targetDevice: device, + targetType: 'sidemenu', + }) if (setting) { setting?.value?.sidemenu_state === 'opened' ? setOpen(true) : setOpen(false) } @@ -160,7 +198,12 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side return ( <Drawer className={styles.drawer} - PaperProps={{ sx: { backgroundColor: theme.theme === 'light' ? 'white' : '#151518', border: 'none' } }} + PaperProps={{ + sx: { + backgroundColor: theme.theme === 'light' ? 'white' : '#151518', + border: 'none', + }, + }} variant='permanent' open={open} > @@ -180,7 +223,14 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side alignItems={'center'} justifyContent='space-between' > - <Image style={{ cursor: 'pointer', marginLeft: 23 }} priority src='/logo.svg' height={26} width={31} alt='Error' /> + <Image + style={{ cursor: 'pointer', marginLeft: 23 }} + priority + src='/logo.svg' + height={26} + width={31} + alt='Error' + /> <Image style={{ @@ -255,13 +305,24 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side </> )} {openErrorModal && ( - <ErrorModalLazy device={'desktop'} open={openErrorModal} handleClose={handleCloseErrorModal} /> + <ErrorModalLazy + device={'desktop'} + open={openErrorModal} + handleClose={handleCloseErrorModal} + /> )} <Box sx={{ position: 'absolute', bottom: 5, width: '100%', left: open ? 25 : 0 }}> {open && ( <Box sx={{ width: 'fit-content' }}> <Link href={'https://air.fail/requisites'} target='_blank'> - <Typography className='title-main-gray' sx={{ fontSize: 14, marginBottom: '7px', textDecorationStyle: 'dashed' }}> + <Typography + className='title-main-gray' + sx={{ + fontSize: 14, + marginBottom: '7px', + textDecorationStyle: 'dashed', + }} + > Реквизиты </Typography> </Link> @@ -285,7 +346,9 @@ type MenuItemProps = { export function MenuItem(props: MenuItemProps) { const isActive = useMemo( - () => props.link === props.pathname || props.activeList?.some((el) => props.pathname.includes(el)), + () => + props.link === props.pathname || + props.activeList?.some((el) => props.pathname.includes(el)), [props.pathname] ) const theme = useAppSelector((state) => state.theme.theme) @@ -307,7 +370,8 @@ export function MenuItem(props: MenuItemProps) { '& .MuiTooltip-arrow': { color: theme === 'light' ? '#E8E8FA' : '#4B4B4B', }, - boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.04), 0px 4px 32px rgba(0, 0, 0, 0.16)', + boxShadow: + '0px 0px 4px rgba(0, 0, 0, 0.04), 0px 4px 32px rgba(0, 0, 0, 0.16)', }, }, }} @@ -335,7 +399,9 @@ export function MenuItem(props: MenuItemProps) { <ListItem component='div' sx={{ - backgroundColor: isActive ? 'rgba(130, 128, 255, 0.08)' : 'none', + backgroundColor: isActive + ? 'rgba(130, 128, 255, 0.08)' + : 'none', borderRadius: '10px', }} > @@ -369,7 +435,11 @@ export function MenuItem(props: MenuItemProps) { opacity: props.open ? 1 : 0, '.MuiTypography-root': { fontSize: 16, - color: isActive ? '#8280FF' : props.theme === 'light' ? '#A4AAB5' : '#D4D4D4', + color: isActive + ? '#8280FF' + : props.theme === 'light' + ? '#A4AAB5' + : '#D4D4D4', }, }} /> @@ -113,9 +113,15 @@ } }, "node_modules/@babel/compat-data": { +<<<<<<< HEAD + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", +======= "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", +>>>>>>> task-feature-35 "license": "MIT", "engines": { "node": ">=6.9.0" @@ -168,6 +174,15 @@ } }, "node_modules/@babel/generator": { +<<<<<<< HEAD + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", @@ -175,6 +190,7 @@ "dependencies": { "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9", +>>>>>>> task-feature-35 "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -212,18 +228,30 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { +<<<<<<< HEAD + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", +>>>>>>> task-feature-35 "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", +<<<<<<< HEAD + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", +======= "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.26.9", +>>>>>>> task-feature-35 "semver": "^6.3.1" }, "engines": { @@ -440,12 +468,21 @@ } }, "node_modules/@babel/parser": { +<<<<<<< HEAD + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", "license": "MIT", "dependencies": { "@babel/types": "^7.26.9" +>>>>>>> task-feature-35 }, "bin": { "parser": "bin/babel-parser.js" @@ -648,6 +685,17 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { +<<<<<<< HEAD + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" +======= "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", @@ -657,6 +705,7 @@ "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-remap-async-to-generator": "^7.25.9", "@babel/traverse": "^7.26.8" +>>>>>>> task-feature-35 }, "engines": { "node": ">=6.9.0" @@ -902,6 +951,15 @@ } }, "node_modules/@babel/plugin-transform-for-of": { +<<<<<<< HEAD + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", @@ -909,6 +967,7 @@ "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", +>>>>>>> task-feature-35 "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { @@ -1455,6 +1514,15 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { +<<<<<<< HEAD + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" +======= "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", @@ -1462,6 +1530,7 @@ "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" +>>>>>>> task-feature-35 }, "engines": { "node": ">=6.9.0" @@ -1487,9 +1556,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { +<<<<<<< HEAD + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.7.tgz", + "integrity": "sha512-5cJurntg+AT+cgelGP9Bt788DKiAw9gIMSMU2NJrLAilnj0m8WZWUNZPSLOmadYsujHutpgElO+50foX+ib/Wg==", +======= "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.8.tgz", "integrity": "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw==", +>>>>>>> task-feature-35 "dev": true, "license": "MIT", "dependencies": { @@ -1574,6 +1649,15 @@ } }, "node_modules/@babel/preset-env": { +<<<<<<< HEAD + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.7.tgz", + "integrity": "sha512-Ycg2tnXwixaXOVb29rana8HNPgLVBof8qqtNQ9LE22IoyZboQbGSxI6ZySMdW3K5nAe6gu35IaJefUJflhUFTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", @@ -1581,6 +1665,7 @@ "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.8", +>>>>>>> task-feature-35 "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", @@ -1594,7 +1679,11 @@ "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.9", +<<<<<<< HEAD + "@babel/plugin-transform-async-generator-functions": "^7.25.9", +======= "@babel/plugin-transform-async-generator-functions": "^7.26.8", +>>>>>>> task-feature-35 "@babel/plugin-transform-async-to-generator": "^7.25.9", "@babel/plugin-transform-block-scoped-functions": "^7.26.5", "@babel/plugin-transform-block-scoping": "^7.25.9", @@ -1609,7 +1698,11 @@ "@babel/plugin-transform-dynamic-import": "^7.25.9", "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", +<<<<<<< HEAD + "@babel/plugin-transform-for-of": "^7.25.9", +======= "@babel/plugin-transform-for-of": "^7.26.9", +>>>>>>> task-feature-35 "@babel/plugin-transform-function-name": "^7.25.9", "@babel/plugin-transform-json-strings": "^7.25.9", "@babel/plugin-transform-literals": "^7.25.9", @@ -1637,7 +1730,11 @@ "@babel/plugin-transform-shorthand-properties": "^7.25.9", "@babel/plugin-transform-spread": "^7.25.9", "@babel/plugin-transform-sticky-regex": "^7.25.9", +<<<<<<< HEAD + "@babel/plugin-transform-template-literals": "^7.25.9", +======= "@babel/plugin-transform-template-literals": "^7.26.8", +>>>>>>> task-feature-35 "@babel/plugin-transform-typeof-symbol": "^7.26.7", "@babel/plugin-transform-unicode-escapes": "^7.25.9", "@babel/plugin-transform-unicode-property-regex": "^7.25.9", @@ -1645,9 +1742,15 @@ "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", +<<<<<<< HEAD + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", +======= "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", +>>>>>>> task-feature-35 "semver": "^6.3.1" }, "engines": { @@ -1725,6 +1828,16 @@ } }, "node_modules/@babel/template": { +<<<<<<< HEAD + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", @@ -1733,12 +1846,25 @@ "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" +>>>>>>> task-feature-35 }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { +<<<<<<< HEAD + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", @@ -1749,6 +1875,7 @@ "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", +>>>>>>> task-feature-35 "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1757,9 +1884,15 @@ } }, "node_modules/@babel/types": { +<<<<<<< HEAD + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", +======= "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", +>>>>>>> task-feature-35 "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -4040,6 +4173,16 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { +<<<<<<< HEAD + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" +======= "version": "0.11.1", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", @@ -4048,6 +4191,7 @@ "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" +>>>>>>> task-feature-35 }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4211,9 +4355,15 @@ } }, "node_modules/caniuse-lite": { +<<<<<<< HEAD + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", +======= "version": "1.0.30001699", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", +>>>>>>> task-feature-35 "funding": [ { "type": "opencollective", @@ -4841,9 +4991,15 @@ "dev": true }, "node_modules/electron-to-chromium": { +<<<<<<< HEAD + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", +======= "version": "1.5.101", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz", "integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==", +>>>>>>> task-feature-35 "license": "ISC" }, "node_modules/emoji-regex": {