@@ -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,17 @@ +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 +} @@ -0,0 +1,2 @@ +export * from './types' +export * from './api' \ No newline at end of file @@ -0,0 +1,11 @@ +import { API_URL } from '@/src/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 '@/src/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,133 @@ +import { useState } from 'react' +import { IModel } from '../types' +import { useSession } from 'next-auth/react' +import { getBotParams } from '../api' +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) + 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,54 @@ +type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' + +export interface IShortModel { + uid: string + title: string + description: string + slug: string + image: string + 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 @@ +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,98 @@ +import { useAppSelector } from '@/src/main/store/store' +import { Device } from '@/src/shared/lib/types/entities' +import { Message } from '@/src/shared/lib/types/model' +import { useEffect, useRef, useState } from 'react' + +export function useImageBotPagination( + isComplete: boolean, + desktop: boolean, + messages: Message[] | null, + getMessagesPagination: (device: Device) => any, + deviceType: Device +) { + const [chatScrollHeight, setChatScrollHeight] = useState(0) + const [scrollBottom, setScrollBottom] = useState(0) + const refScrollMobile = useRef() + const [isPaginating, setIsPaginating] = useState(false) + + function handleMobileScroll() { + if (!refScrollMobile.current) return + + setScrollBottom( + refScrollMobile.current.scrollHeight - + refScrollMobile.current.scrollTop - + refScrollMobile.current.clientHeight + ) + + if (refScrollMobile.current && messages?.length !== 0) { + const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current + if (scrollTop === 0) { + if (getMessagesPagination) { + setIsPaginating(true) + getMessagesPagination(deviceType) + } + } + } + } + + const handleScroll = () => { + if (window.scrollY + window.innerHeight >= document.documentElement.scrollHeight) { + setIsPaginating(true) + getMessagesPagination(deviceType) + } + } + + useEffect(() => { + window.addEventListener('scroll', handleScroll) + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, []) + + useEffect(() => { + const block = deviceType === 'desktop' ? document.body : refScrollMobile.current + + if (!block) return + + 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]) + + function onIsComplete() { + if (!isComplete) return + + if (!desktop) { + const block = refScrollMobile.current + if (block) { + block.scrollTop = block.scrollHeight + } + return + } + + window.scroll(0, 0) + } + + useEffect(onIsComplete, [isComplete]) + + return { + refScrollMobile, + handleMobileScroll, + scrollBottom, + } +} @@ -0,0 +1 @@ +export * from './model' \ 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' @@ -62,6 +62,7 @@ const Page: React.FC = ({ deviceType, deviceOs }) => { isTryRename, setIsTryRename, } = useChats(modelType) + const { messages, sendMessage, loading, getMessagesPagination, deleteMessage } = useModel( currentChat, showError, @@ -215,7 +216,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,5 +1,5 @@ import * as React from 'react' -import { useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { useDispatch } from 'react-redux' import { Collapse, Typography } from '@mui/material' import Box from '@mui/material/Box' @@ -20,12 +20,19 @@ 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' export async function getServerSideProps(context: any): Promise<{ props: any }> { const deviceType = getTypeDevice(context) @@ -41,233 +48,64 @@ 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 { query } = useRouter() - const { messages, loading, createImage, isComplete, getMessagesPagination } = useModelImages<Setting>( - showError, + const { + botParams, + version, 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 { push } = useRouter() - - React.useEffect(() => { - if (!data) return - model_api - .getBotParams(router.asPath.split('/')[2], data.access) - .then((res) => { - if (!res.title) return push('/404') + fetchBotParams, + resetParams, + setDefaultParams, + setVersion, + } = useImageBot(query.slug as string) - 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 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 { messages, loading, createImage, isComplete, getMessagesPagination } = + useImagesPagination(showError, modelType, deviceType) - const handleMobileScroll = () => { - setScrollBottom( - refScrollMobile.current?.scrollHeight - - refScrollMobile.current?.scrollTop - - refScrollMobile.current?.clientHeight - ) + const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput( + version, + includeParams, + createImage + ) - if (refScrollMobile.current && messages?.length !== 0) { - const { scrollTop, scrollHeight, clientHeight } = refScrollMobile.current - if (scrollTop === 0) { - if (getMessagesPagination) { - setIsPaginating(true) - getMessagesPagination(deviceType) - } - } - } - } + const { refScrollMobile, handleMobileScroll, scrollBottom } = useImageBotPagination( + isComplete, + desktop, + messages, + getMessagesPagination, + deviceType + ) - 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() + }, [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' @@ -302,7 +140,9 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> @@ -330,6 +170,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { zIndex: 10, }} onClick={() => { + if (!refScrollMobile.current) return const block = refScrollMobile.current block.scrollTo({ @@ -374,7 +215,9 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { imageLoad={onLoadImage} sendMessage={onCreateImage} unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } /> )} </Stack> @@ -460,7 +303,7 @@ const Images: React.FC<any> = ({ deviceType, deviceOs }) => { /> <ResetFilters desktop={desktop} - closeDrawer={hideMobileSettings} + closeDrawer={() => setOpenFiltersMobile(false)} reset={resetParams} /> </Collapse> @@ -477,7 +320,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 ? ( <> @@ -520,7 +366,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) { @@ -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,4 +1,5 @@ export { getAccessToken } from './get-token' export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' -export * from './string' +export { c } from './string' +export * from './date-helper' @@ -1,3 +1,3 @@ -export const c = (...classes: string[]) => { +export const c = (...classes: Array<string | null | undefined>) => { return classes.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,27 @@ +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 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, + 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,64 @@ +.container { + position: relative; + + @media (max-width: 768px) { + position: static; + } +} + +.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; + + + @media (max-width: 768px) { + left: 0; + right: 0; + width: fit-content; + } + + &_open { + visibility: visible; + opacity: 1; + transform: translateY(0); + } +} @@ -0,0 +1,56 @@ +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 { bots, 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 className={c(styles.popup, popupOpen ? styles.popup_open : null)}> + {bots.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,51 @@ +.model { + // font-family: Inter; + padding: 15px 20px; + cursor: pointer; + display: flex; + align-items: center; + gap: 22px; + justify-content: space-between; + max-width: 500px; + + @media (max-width: 768px) { + // max-width: calc(100% - 100px) !important; + width: 100%; + // max-width: unset; + } + &__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,82 @@ +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 { CostZones } from '../types' + +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 = ({ + uid, + title, + description, + slug, + image, + actual_stat, + active, + averageTokenCost, + setActive, +}: ImagePopupModelProps) => { + // жесткий костыль))) - временный + 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={() => setActive(slug)} className={styles.model}> + <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) { + return await axios.get<Message>(API_URL + `/media/gallery/images?limit=10&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,125 @@ +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 [isComplete, setIsComplete] = useState<boolean>(false) + + const [offset, setOffset] = useState<number>(0) + + const dispatch = useAppDispatch() + + const dataRef = useRef(data) + const messagesRef = useRef(messages) + const offsetRef = useRef(offset) + + useEffect(() => { + dataRef.current = data + }, [data]) + + useEffect(() => { + messagesRef.current = messages + }, [messages]) + + useEffect(() => { + offsetRef.current = offset + }, [offset]) + + async function onFetch() { + 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 === 'desktop') { + setMessages(answer) + return setOffset(answer.length) + } + + setMessages(answer.reverse()) + setOffset(answer.length) + } + + useEffect(() => { + onFetch() + }, [data, type]) + + const getMessagesPagination = useCallback( + async (device: 'mobile' | 'desktop') => { + if (!dataRef.current || !dataRef.current.access) return + + setLoading(true) + const { data: answer, ...response } = await getImagesGalery( + dataRef.current?.access, + offsetRef.current + ) + setLoading(false) + + if (response.status >= 400) return showError('Ошибка загрузки чата') + + if (!Array.isArray(answer) || !messagesRef.current) + return showError('Ошибка загрузки сообщений') + + if (device === 'mobile') { + const newMessages = answer.reverse() + setMessages([...newMessages, ...messagesRef.current]) + return setOffset((prev) => prev + answer.length) + } + + setMessages([...messagesRef.current, ...answer]) + setOffset((prev) => prev + answer.length) + }, + [data, type, offset, messages] + ) + + const createImage = async <T>(dataForSend: MessageSend<T>) => { + 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) => { + if (!prev || !prev.length) return response.data + + if (device === 'desktop') { + return [...response.data, ...prev] + } + return [...prev, ...response.data] + }) + + setIsComplete(true) + } + + return { messages, createImage, loading, isComplete, getMessagesPagination } +} @@ -4,11 +4,11 @@ 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 { ImageModal } from '@/src/features/image-modal' import { ClientOnly } from '@/src/shared' import { createPortal } from 'react-dom' +import { IsNextDay } from './is-next-day' +import { PreviewView } from './preview-view' interface IMessagesList { device: 'mobile' | 'desktop' @@ -11,8 +11,8 @@ import { useAutoScroll } from '@/src/shared/lib/hooks' import { Message } from '@/src/shared/lib/types/model' import styles from './image-messages-list.module.scss' -import { useMessages } from './model/use-messages' import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' interface MessagesList { device: 'mobile' | 'desktop' @@ -59,7 +59,15 @@ 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 @@ -77,7 +85,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' @@ -203,14 +216,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> @@ -219,8 +247,14 @@ 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', @@ -231,19 +265,27 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images, <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} @@ -257,14 +299,19 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images, /> <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, @@ -284,17 +331,21 @@ 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) || @@ -307,12 +358,15 @@ 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, @@ -351,21 +405,35 @@ 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 @@ -375,7 +443,9 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images, .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, useMemo } 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 @@ -7,7 +7,7 @@ import { ImageModal } from '@/src/features/image-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 @@ -147,9 +147,13 @@ export const UserMessage = React.memo(function UserMessage({ setModal, setCurren <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 @@ -297,7 +301,10 @@ export const UserMessage = React.memo(function UserMessage({ setModal, setCurren {!props.message.is_sent && ( <Image onClick={resend} - style={{ cursor: 'pointer', marginRight: '5px' }} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} height={20} width={20} alt='1' @@ -0,0 +1,4 @@ +export * from './ui' +export * from './lib' +export * from './api' +export * from './model' \ No newline at end of file @@ -34,10 +34,6 @@ export const Search = ({ device }: SearchProps) => { 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={{ @@ -254,12 +304,25 @@ export const SideMenu = ({ device, sidemenuDefaultOpen }: { device: Device; side </Box> </> )} - {openErrorModal && <ErrorModalLazy device={'desktop'} open={openErrorModal} handleClose={handleCloseErrorModal} />} + {openErrorModal && ( + <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> @@ -282,7 +345,12 @@ type MenuItemProps = { } export function MenuItem(props: MenuItemProps) { - const isActive = useMemo(() => props.link === props.pathname || props.activeList?.some((el) => props.pathname.includes(el)), [props.pathname]) + const isActive = useMemo( + () => + props.link === props.pathname || + props.activeList?.some((el) => props.pathname.includes(el)), + [props.pathname] + ) const theme = useAppSelector((state) => state.theme.theme) return ( @@ -302,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)', }, }, }} @@ -330,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', }} > @@ -341,7 +412,12 @@ export function MenuItem(props: MenuItemProps) { justifyContent: 'center', }} > - <Image src={props.icon + (isActive ? '' : '-off') + '.svg'} width={19} height={19} alt={''} /> + <Image + src={props.icon + (isActive ? '' : '-off') + '.svg'} + width={19} + height={19} + alt={''} + /> </ListItemIcon> <ListItemText primary={props.title} @@ -349,7 +425,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', }, }} /> @@ -0,0 +1,27 @@ +declare module "*.scss" { + const content: Record<string, string>; + 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<React.SVGProps<SVGSVGElement>>; + 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, @@ -88,6 +88,7 @@ "typescript": "5.1.3" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "@types/draftjs-to-html": "^0.8.4", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195",