@@ -0,0 +1 @@ +export * from './message.routes' \ No newline at end of file @@ -0,0 +1,34 @@ +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, + validateStatus: (status) => status < 500, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + }) +} + +export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { + return await axios.get( + API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, + { + validateStatus: (status) => status < 500, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) +} @@ -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 @@ -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,55 @@ +type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' + +export interface IShortModel { + uid: string + title: string + description: string + slug: string + image: string + blocked: boolean + actual_stat: { + generation_time: string + tokens_cost: string + } +} + +export interface IModel { + uid: string + title: string + description: string + slug: string + image: string + settings: { is_active: boolean } + parameters: IModelParams[] + versions: IModelVersions[] + inputs: IModelInputs[] +} +export interface IModelParams { + name: string + description: string + key: string + type: string + required: boolean + values: { + availables: string[] + default: any + end: number + start: number + step: number + } + versions: string[] +} +export interface IModelVersions { + name: string + description: string + default: boolean + slug: string +} +export interface IModelInputs { + // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' + type: string + required: boolean + versions: string[] +} + +export default ModelForChats @@ -0,0 +1,3 @@ +export * from './api' +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-image-bot-create' \ No newline at end of file @@ -0,0 +1,76 @@ +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 { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react' + +export function useImageBotCreateImage( + showError: (message: string) => void, + type: string, + device: Device, + setMessages: Dispatch>, + mobileScrollContainer: RefObject +) { + const [isComplete, setIsComplete] = useState(false) + + const [createLoading, setCreateLoading] = useState(false) + + const { data } = useSession() + + const dispatch = useAppDispatch() + + const createImage = async (dataForSend: MessageSend) => { + const { content, file } = dataForSend + + setIsComplete(false) + setCreateLoading(true) + + const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + + const { data: messages } = await sendImage(type, dataSending, data?.access) + + setCreateLoading(false) + + if (typeof messages === 'string') { + showError(messages) + return + } + + dispatch(getUserBalance(data?.access)) + + setMessages((prev: Message[]) => { + if (!prev || !prev.length) return messages + + if (device === 'desktop') { + return [...messages, ...prev] + } + return [...prev, ...messages] + }) + + setIsComplete(true) + + if (device === 'desktop') { + return window.scrollTo({ + top: 0, + behavior: 'smooth', + }) + } + + setTimeout(() => { + if (!mobileScrollContainer.current) return + + mobileScrollContainer.current.scroll({ + top: mobileScrollContainer.current.scrollHeight, + behavior: 'smooth', + }) + }, 500) + } + + return { + createImage, + isComplete, + createLoading, + } +} @@ -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,64 @@ +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,120 @@ +import { useAppSelector } from '@/src/main/store/store' +import { getImagesBySlug, Message } from '@/src/entities/message' +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 { useEffect, useRef, useState } from 'react' +import { LimitSize, Limit } from '../types' +import { useRouter } from 'next/router' + +export function useImageBotPagination(deviceType: Device) { + const refScrollMobile = useRef(null) + const refScrollDesktop = useRef(null) + const mobileScrollContainer = useRef(null) + const offset = useRef(0) + + const { query } = useRouter() + + 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 getImagesBySlug( + query.slug as string, + 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(() => { + if (!mobileScrollContainer.current) return + 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 @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction } from 'react' +import React, { Dispatch, SetStateAction, useMemo } from 'react' import Image from 'next/image' import styles from './modal-styles.module.scss' @@ -6,10 +6,13 @@ import styles from './modal-styles.module.scss' interface IProps { modal: boolean setModal: Dispatch> - image: string + image: string | null } export default function FullScreenModal({ modal, setModal, image }: IProps) { - const isSvg = image.includes('.svg') + const isSvg = useMemo(() => { + if (!image) return false + return image.includes('.svg') + }, []) return (
setModal(false)}> - + ) : ( - К сожалению, изображение не загрузилось + К сожалению, изображение не загрузилось )}
@@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import CircularProgress from '@mui/material/CircularProgress' import Image from 'next/image' @@ -11,9 +11,20 @@ interface IProps { required: (string | null)[] } -export const SendBtn = ({ loading, unpinImage, input, sendMessage, setInput, required }: IProps) => { +export const SendBtn = ({ + loading, + unpinImage, + input, + sendMessage, + setInput, + required, +}: IProps) => { const send_icon = '/svg/chatgpt/send_message.svg' + useEffect(() => { + console.log(loading) + }, [loading]) + if (loading) { return ( = ({ const [types, setTypes] = React.useState([]) const [typeVersions, setTypeVersions] = React.useState({}) const [value, setValue] = React.useState('') + + useEffect(() => { + console.log(loading) + }, [loading]) // const [disableOnChange, setDisableOnChange] = React.useState(false) React.useEffect(() => { @@ -63,7 +67,10 @@ export const UniqInput: FC = ({ React.useEffect(() => { if (input_types && typeVersions && !typeVersions['text']) { setDisabled(true) - } else if (typeVersions['text'] && (typeVersions['text'].length === 0 || typeVersions['text'].includes(currentVersion))) { + } else if ( + typeVersions['text'] && + (typeVersions['text'].length === 0 || typeVersions['text'].includes(currentVersion)) + ) { setDisabled(false) } }, [typeVersions]) @@ -99,7 +106,12 @@ export const UniqInput: FC = ({ borderRadius: '15px', ...styleInputWithoutBorderFocus, '& label': { color: '#8853FA' }, - backgroundColor: theme === 'light' ? 'white' : styles === 'images' ? '#151518' : 'transparent', + backgroundColor: + theme === 'light' + ? 'white' + : styles === 'images' + ? '#151518' + : 'transparent', textarea: { color: theme === 'light' ? '#272727' : '#E1E1E1', '::-webkit-scrollbar': { @@ -123,7 +135,8 @@ export const UniqInput: FC = ({ fontSize: '16px', }, '& .Mui-disabled': { - webkitTextFillColor: theme === 'light' ? '#bbbbbb !important' : '#646464 !important', + webkitTextFillColor: + theme === 'light' ? '#bbbbbb !important' : '#646464 !important', fontSize: '16px', letterSpacing: '0.05px', fontWeight: '600', @@ -155,7 +168,8 @@ export const UniqInput: FC = ({ > {typeVersions['image'] && - (typeVersions['image'].length === 0 || typeVersions['image'].includes(currentVersion)) && ( + (typeVersions['image'].length === 0 || + typeVersions['image'].includes(currentVersion)) && ( <> = ({ { -export async function getServerSideProps(context: any): Promise<{ props: any }> { - const deviceType = getTypeDevice(context) - const deviceOs = getDeviceOs(context) - return { - props: { - ...(await serverSideTranslations(context.locale, ['common'])), - deviceType, - deviceOs, - }, - } + return { + props: { + deviceType: getTypeDevice(c), + deviceOs: getDeviceOs(c), + }, + } } -const Images: React.FC = ({ deviceType, deviceOs }) => { - const [image, setImage] = React.useState(null) - const desktop = deviceType === 'desktop' - const ios = deviceOs === 'ios' - const { error, showError } = useShowData() - const [openFiltersMobile, setOpenFiltersMobile] = React.useState(false) - const router = useRouter() - const { data } = useSession() - const [botParams, setBotParams] = React.useState(null) - const [version, setVersion] = React.useState('') - const [modelType, setModelType] = React.useState('') - const [params, setParams] = React.useState(false) - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useDispatch() - - const { messages, loading, createImage, isComplete, getMessagesPagination } = - useModelImages(showError, modelType, deviceType) - - const [chatScrollHeight, setChatScrollHeight] = React.useState(0) - const [scrollBottom, setScrollBottom] = React.useState(0) - const refScrollMobile = useRef() - const [isPaginating, setIsPaginating] = React.useState(false) - - 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) => { - if (event.target.files) { - setImage(event.target.files[0]) - // showError('Файл успешно загружен, можете отправлять его!') - } - } - - const viewMobileSettings = () => { - setOpenFiltersMobile(true) - } - - const hideMobileSettings = () => { - setOpenFiltersMobile(false) - } - - 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 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 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, - } - } - - createImage({ - content: input, - file: image, - info: { - ...data, - }, - }) - return true - } - - 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 handleMobileScroll = () => { - 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) - } - } - - 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]) - - React.useEffect(() => { - if (deviceType === 'mobile') { - document.body.style.setProperty('overflow-y', 'hidden') - } - - return () => { - document.body.style.setProperty('overflow-y', 'scroll') - } - }, []) - - return ( - - - <Box - display={'flex'} - justifyContent='space-between' - alignItems='start' - flexDirection={desktop ? 'row' : 'column-reverse'} - sx={{ - marginBottom: desktop ? 0 : 3, - width: desktop ? '97%' : '100%', - marginTop: desktop ? 3 : '15px', - overflowY: 'hidden', - }} - > - <Box - sx={{ - width: desktop ? '73%' : '100%', - marginLeft: 0, - display: 'flex', - flexDirection: desktop ? 'column' : 'column-reverse', - }} - > - {desktop ? ( - <> - <Stack alignItems='center' sx={{ marginBottom: '15px' }}> - {botParams && ( - <UniqInput - currentVersion={version} - styles={'images'} - input_types={botParams.inputs} - image={image} - value={prompt} - desktop={desktop} - loading={loading} - imageLoad={onLoadImage} - sendMessage={onCreateImage} - unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} - /> - )} - </Stack> - <Box - className={'bg-color-block border-radius-main'} - sx={{ padding: '30px' }} - > - <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, - }} - onClick={() => { - const block = refScrollMobile.current - - block.scrollTo({ - top: block.scrollHeight, - behavior: 'smooth', // добавляем плавную прокрутку - }) - }} - > - <ArrowDownScroll /> - </Box> - )} - <Box - ref={refScrollMobile} - sx={{ - padding: '30px', - height: 'calc(100dvh - 116px - 61px - 10px)', - overflowY: 'scroll', - overflowX: 'hidden', - }} - className={'smallScroll'} - onScroll={handleMobileScroll} - > - <ImageMessagesList - isComplete={isComplete} - device={deviceType} - images={messages} - /> - </Box> - <Stack alignItems='center'> - {botParams && ( - <UniqInput - currentVersion={version} - styles={'images'} - input_types={botParams.inputs} - image={image} - value={prompt} - desktop={desktop} - loading={loading} - imageLoad={onLoadImage} - sendMessage={onCreateImage} - unpinImage={() => setImage(null)} - viewMobileSettings={viewMobileSettings} - /> - )} - </Stack> - </Box> - )} - </Box> - {desktop && ( - <Stack - spacing={2} - className='pd-30 bg-color-block border-radius-main' - sx={{ width: '25.5%', height: 'auto' }} - > - {botParams?.versions && botParams.versions.length !== 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - ) : ( - <></> - )} - {botParams && botParams.parameters?.length > 0 && ( - <Box - display={'flex'} - alignItems={'center'} - gap={'5px'} - sx={{ cursor: 'pointer' }} - onClick={() => { - setParams(!params) - }} - > - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ПАРАМЕТРЫ - </Typography> - <svg - width='14' - height='20' - viewBox='0 0 21 13' - fill='none' - xmlns='http://www.w3.org/2000/svg' - className={`${params ? 'rotate-180' : 'rotate-0'}`} - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M0.614851 0.615358C1.00866 0.221668 1.54271 0.000505666 2.09955 0.000505642C2.6564 0.000505617 3.19044 0.221668 3.58425 0.615357L10.4996 7.53066L17.4149 0.615357C17.8109 0.232825 18.3414 0.0211567 18.892 0.0259414C19.4426 0.0307261 19.9693 0.25158 20.3587 0.640937C20.748 1.03029 20.9689 1.557 20.9737 2.10761C20.9784 2.65823 20.7668 3.18869 20.3842 3.58476L11.9843 11.9848C11.5904 12.3784 11.0564 12.5996 10.4996 12.5996C9.94271 12.5996 9.40866 12.3784 9.01485 11.9848L0.614851 3.58476C0.221162 3.19095 -4.3461e-07 2.6569 -4.5895e-07 2.10006C-4.8329e-07 1.54321 0.221162 1.00917 0.614851 0.615358Z' - fill='#7f7df3' - /> - </svg> - </Box> - )} - - {botParams && botParams.parameters?.length > 0 ? ( - <Collapse - in={params} - orientation='vertical' - collapsedSize={0} - sx={{}} - > - <BotParamsMap - currentVersion={version} - params={botParams?.parameters} - /> - <ResetFilters - desktop={desktop} - closeDrawer={hideMobileSettings} - reset={resetParams} - /> - </Collapse> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - )} - <DrawerCustom open={openFiltersMobile} onClose={hideMobileSettings}> - <Stack spacing={1} padding={2.4}> - {botParams?.versions && botParams.versions.length !== 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ВЕРСИИ - </Typography> - <ChatSelect - setDefaultParams={setDefaultParams} - value={version} - list={botParams.versions} - setValue={setVersion} - /> - </> - ) : ( - <></> - )} - {botParams && botParams.parameters?.length > 0 ? ( - <> - <Typography - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - margin: '20px 0px 0px !important', - }} - > - ПАРАМЕТРЫ - </Typography> - <BotParamsMap - currentVersion={version} - params={botParams?.parameters} - /> - <ResetFilters - closeDrawer={hideMobileSettings} - desktop={desktop} - reset={resetParams} - /> - </> - ) : ( - <Typography - sx={{ - color: '#6e6e6e', - fontSize: '15px', - fontWeight: '500', - }} - > - Параметры отсутствуют - </Typography> - )} - </Stack> - </DrawerCustom> - <Error error={error} open={Boolean(error)} /> - </Box> - </Layout> - ) -} - -export default Images +export default ImageModelPage @@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; + +type ClientOnlyProps = { children: JSX.Element }; +const ClientOnly = (props: ClientOnlyProps) => { + const { children } = props; + + return children; +}; + +export default dynamic(() => Promise.resolve(ClientOnly), { + ssr: false, +}); @@ -0,0 +1 @@ +export { default as ClientOnly } from './client-only' @@ -0,0 +1 @@ +export * from './ui' @@ -12,9 +12,7 @@ export const getTypeDevice = (context: GetServerSidePropsContext): Device => { return 'desktop' } - const isMobile = Boolean( - UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i) - ) + const isMobile = Boolean(UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)) return isMobile ? 'mobile' : 'desktop' } @@ -26,13 +24,12 @@ export const getDeviceType = (): Device => { return 'desktop' } - const isMobile = Boolean( - UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i) - ) + const isMobile = Boolean(UA.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)) return isMobile ? 'mobile' : 'desktop' } + /** * @deprecated */ @@ -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 } } @@ -0,0 +1,374 @@ +import { ChatSelect } from '@/src/main/components/chat_select' +import { ResetFilters } from '@/src/main/components/filters/reset_filters' +import { UniqInput } from '@/src/main/components/uniq_input' +import { useImageBot } from '@/src/entities/model-entity/model/use-image-bot' +import BotParamsMap from '@/src/features/bot-params/bot-params-map' +import { useImageBotCreateImage } from '@/src/features/image-bot-create-image' +import { useImagesBotFilters } from '@/src/features/image-bot-filters' +import { useImagesUniqInput } from '@/src/features/image-bot-input' +import { useImageBotPagination } from '@/src/features/image-bot-pagination' +import Title from '@/src/features/title/title' +import { useShowData, DrawerCustom, Error } from '@/src/shared' +import { useThemeAndDevice } from '@/src/shared/lib/hooks' +import { ArrowDownScroll } from '@/src/shared/ui/icon-components/scroll-down-arrow' +import { useImagesPagination, ImageMessagesList } from '@/src/widgets/messages' +import { Layout } from '@/src/main/layout' +import { Box, Typography, Collapse, Stack } from '@mui/material' +import { useSession } from 'next-auth/react' +import Head from 'next/head' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { getDeviceType, getOs } from '@/src/shared/lib/helpers' +import { Device, DeviceOs } from '@/src/shared/lib/types/entities' + +export interface ImageModelPageProps { + deviceType: Device + deviceOs: DeviceOs +} + +const ImageModelPage = ({ deviceType, deviceOs }: ImageModelPageProps) => { + const { query } = useRouter() + + const { + botParams, + version, + modelType, + fetchBotParams, + resetParams, + setDefaultParams, + setVersion, + } = useImageBot(query.slug as string) + + const { ios, desktop } = useThemeAndDevice(deviceType, deviceOs) + + const { error, showError } = useShowData() + + const router = useRouter() + + const { data: session } = useSession() + + const { openFiltersMobile, setOpenFiltersMobile, params, setParams, includeParams } = + useImagesBotFilters() + + const { + refScrollMobile, + refScrollDesktop, + mobileScrollContainer, + onObserverMounted, + setMessages, + fetchMessages, + loading, + messages, + } = useImageBotPagination(deviceType) + + const { createImage, isComplete, createLoading } = useImageBotCreateImage( + showError, + modelType, + deviceType, + setMessages, + mobileScrollContainer + ) + + const { onCreateImage, onLoadImage, image, setImage } = useImagesUniqInput( + version, + includeParams, + createImage + ) + + async function onFetch() { + await Promise.all([fetchBotParams()]) + } + + useEffect(() => { + console.log(createLoading) + }, [createLoading]) + + useEffect(() => { + onFetch() + onObserverMounted() + }, [session, router.query]) + + return ( + <Layout device={deviceType} titlePage={botParams ? botParams.title : 'Загрузка...'}> + <Box sx={{ display: 'flex', alignItems: 'center', position: 'relative' }}> + <Title + title={botParams ? botParams.title : 'Загрузка...'} + type={'Изображения'} + linkBack={'/images'} + /> + </Box> + <Box + display={'flex'} + justifyContent='space-between' + alignItems='start' + flexDirection={desktop ? 'row' : 'column-reverse'} + sx={{ + marginBottom: desktop ? 0 : 3, + width: desktop ? '97%' : '100%', + marginTop: desktop ? 3 : '15px', + }} + > + <Box + sx={{ + width: desktop ? '73%' : '100%', + marginLeft: 0, + display: 'flex', + flexDirection: desktop ? 'column' : 'column-reverse', + }} + > + {desktop ? ( + <> + <Stack alignItems='center' sx={{ marginBottom: '15px' }}> + {botParams && ( + <UniqInput + currentVersion={version} + styles={'images'} + input_types={botParams.inputs} + image={image} + value={prompt} + desktop={desktop} + loading={createLoading} + imageLoad={onLoadImage} + sendMessage={onCreateImage} + unpinImage={() => setImage(null)} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } + /> + )} + </Stack> + <Box + className={'bg-color-block border-radius-main'} + sx={{ padding: '30px', position: 'relative' }} + > + <ImageMessagesList + isComplete={isComplete} + device={deviceType} + images={messages} + getMessagesPagination={fetchMessages} + /> + <Box + sx={{ + position: 'absolute', + bottom: '0', + visibility: 'hidden', + height: '800px', + width: '100%', + }} + ref={refScrollDesktop} + ></Box> + </Box> + </> + ) : ( + <Box className={'bg-color-block border-radius-main'}> + <Box + sx={{ + padding: '30px', + height: 'calc(100dvh - 116px - 61px - 10px)', + overflowY: 'scroll', + overflowX: 'hidden', + position: 'relative', + }} + ref={mobileScrollContainer} + className={'smallScroll'} + > + <div + style={{ position: 'absolute', top: 300 }} + ref={refScrollMobile} + ></div> + <ImageMessagesList + isComplete={isComplete} + device={deviceType} + images={messages} + getMessagesPagination={fetchMessages} + /> + </Box> + <Stack alignItems='center'> + {botParams && ( + <UniqInput + currentVersion={version} + styles={'images'} + input_types={botParams.inputs} + image={image} + value={prompt} + desktop={desktop} + loading={createLoading} + imageLoad={onLoadImage} + sendMessage={onCreateImage} + unpinImage={() => setImage(null)} + viewMobileSettings={() => + setOpenFiltersMobile(true) + } + /> + )} + </Stack> + </Box> + )} + </Box> + {desktop && ( + <Stack + spacing={2} + className='pd-30 bg-color-block border-radius-main' + sx={{ width: '25.5%', height: 'auto' }} + > + {botParams?.versions && botParams.versions.length !== 0 ? ( + <> + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + }} + > + ВЕРСИИ + </Typography> + <ChatSelect + setDefaultParams={setDefaultParams} + value={version} + list={botParams.versions} + setValue={setVersion} + /> + </> + ) : ( + <></> + )} + {botParams && botParams.parameters?.length > 0 && ( + <Box + display={'flex'} + alignItems={'center'} + gap={'5px'} + sx={{ cursor: 'pointer' }} + onClick={() => { + setParams(!params) + }} + > + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + }} + > + ПАРАМЕТРЫ + </Typography> + <svg + width='14' + height='20' + viewBox='0 0 21 13' + fill='none' + xmlns='http://www.w3.org/2000/svg' + className={`${params ? 'rotate-180' : 'rotate-0'}`} + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M0.614851 0.615358C1.00866 0.221668 1.54271 0.000505666 2.09955 0.000505642C2.6564 0.000505617 3.19044 0.221668 3.58425 0.615357L10.4996 7.53066L17.4149 0.615357C17.8109 0.232825 18.3414 0.0211567 18.892 0.0259414C19.4426 0.0307261 19.9693 0.25158 20.3587 0.640937C20.748 1.03029 20.9689 1.557 20.9737 2.10761C20.9784 2.65823 20.7668 3.18869 20.3842 3.58476L11.9843 11.9848C11.5904 12.3784 11.0564 12.5996 10.4996 12.5996C9.94271 12.5996 9.40866 12.3784 9.01485 11.9848L0.614851 3.58476C0.221162 3.19095 -4.3461e-07 2.6569 -4.5895e-07 2.10006C-4.8329e-07 1.54321 0.221162 1.00917 0.614851 0.615358Z' + fill='#7f7df3' + /> + </svg> + </Box> + )} + + {botParams && botParams.parameters?.length > 0 ? ( + <Collapse + in={params} + orientation='vertical' + collapsedSize={0} + sx={{}} + > + <BotParamsMap + currentVersion={version} + params={botParams?.parameters} + /> + <ResetFilters + desktop={desktop} + closeDrawer={() => setOpenFiltersMobile(false)} + reset={resetParams} + /> + </Collapse> + ) : ( + <Typography + sx={{ + color: '#6e6e6e', + fontSize: '15px', + fontWeight: '500', + }} + > + Параметры отсутствуют + </Typography> + )} + </Stack> + )} + <DrawerCustom + open={openFiltersMobile} + onClose={() => setOpenFiltersMobile(false)} + > + <Stack spacing={1} padding={2.4}> + {botParams?.versions && botParams.versions.length !== 0 ? ( + <> + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + margin: '20px 0px 0px !important', + }} + > + ВЕРСИИ + </Typography> + <ChatSelect + setDefaultParams={setDefaultParams} + value={version} + list={botParams.versions} + setValue={setVersion} + /> + </> + ) : ( + <></> + )} + {botParams && botParams.parameters?.length > 0 ? ( + <> + <Typography + sx={{ + color: '#A4AAB5', + fontWeight: '600', + fontSize: '14px', + letterSpacing: '0.1px', + margin: '20px 0px 0px !important', + }} + > + ПАРАМЕТРЫ + </Typography> + <BotParamsMap + currentVersion={version} + params={botParams?.parameters} + /> + <ResetFilters + closeDrawer={() => setOpenFiltersMobile(false)} + desktop={desktop} + reset={resetParams} + /> + </> + ) : ( + <Typography + sx={{ + color: '#6e6e6e', + fontSize: '15px', + fontWeight: '500', + }} + > + Параметры отсутствуют + </Typography> + )} + </Stack> + </DrawerCustom> + <Error error={error} open={Boolean(error)} /> + </Box> + </Layout> + ) +} + +export default ImageModelPage @@ -0,0 +1 @@ +export { default as ImageModelPage } from './image-model' @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,15 @@ +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,17 @@ +type DateResponse = { + month: string + year: string + day: string +} + +export const getDateFromString = (inputDate: string): DateResponse | null => { + if (inputDate) { + const date = inputDate.split('T')[0].split('-') + return { + day: date[2], + month: date[1], + year: date[0], + } + } + return null +} @@ -0,0 +1,21 @@ +export const getDayMontsString = (inputDate: string): string => { + const monthNames = [ + 'января', + 'февраля', + 'марта', + 'апреля', + 'мая', + 'июня', + 'июля', + 'августа', + 'сентября', + 'октября', + 'ноября', + 'декабря', + ] + + const date = new Date(inputDate) + const day = date.getDate() + const month = monthNames[date.getMonth()] + return `${day} ${month}` +} @@ -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 @@ -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' 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} />*/} @@ -0,0 +1,2 @@ +export * from './use-images-pagination' +export * from './use-image-icons' @@ -0,0 +1,39 @@ +import { useShowData } from '@/src/shared' +import { useState } from 'react' + +export function useImageIcons() { + const [iconsMenu, setIconsMenu] = useState<string>('') + const { error, showError, isError } = useShowData() + + const toggleMenu = (uid: string) => { + if (iconsMenu === uid) { + setIconsMenu('') + } else { + setIconsMenu(uid) + } + } + + const downloadFile = (url: string | null, content: string | undefined) => { + if (url) { + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + // const image_name = '123' + const image_name = content?.replaceAll(' ', '_').substring(0, 25) + link.setAttribute('download', `${image_name}`) + document.body.appendChild(link) + link.click() + }) + .catch((error) => { + showError('Что-то пошло не так', true) + }) + } else { + showError('Изображение не найдено', true) + } + } + + return { iconsMenu, toggleMenu, downloadFile } +} @@ -0,0 +1,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 } +} @@ -0,0 +1,26 @@ +import { Message } from '@/src/shared/lib/types/model' +import { useMemo, useState } from 'react' + +export const useMessages = (messages: Message[]) => { + const [chosenImage, setChosenImage] = useState<string | null>(null) + + const [loaded, setLoaded] = useState(false) + + // типизация ну супер кривая))) + const computedLibraryImages = useMemo<Message[]>(() => { + return messages + .map((el) => { + if (el.file && (el.file as any).includes('.zip')) return null + return el + }) + .filter((el) => el !== null) as Message[] + }, [messages]) + + return { + chosenImage, + setChosenImage, + loaded, + setLoaded, + computedLibraryImages, + } +} @@ -0,0 +1,27 @@ +import React from 'react' +import { Box } from '@mui/material' +import Link from 'next/link' + +import { Message } from '@/src/shared/lib/types/model' + +export function AnswerWrap(props: { message: Message }) { + if (props.message.file) { + const messageText = props.message.from_model + ? 'Ваша ссылка для скачивания \n (нажмите для просмотра)' + : 'Тут ссылка на ваш файл (нажмите для просмотра)' + + return ( + <Box> + <Link + onClick={(e) => e.stopPropagation()} + target='_blank' + href={props.message.file.toString()} + > + {messageText} + </Link> + </Box> + ) + } + + return <>{props.message.content}</> +} @@ -0,0 +1,333 @@ +import React, { useEffect } from 'react' +import { Box, Menu, MenuItem, Slide, Typography } from '@mui/material' +import Stack from '@mui/material/Stack' +import Image from 'next/image' + +import { useAppSelector } from '@/src/main/store/store' +import { TooltipCustom } from '@/src/shared' +import { Markdown } from '@/src/widgets/markdown/markdown' + +export function BotMessage(props: any) { + // const LazyCode = dynamic(() => import('src/widgets/chat-gpt-field/ui/code')) + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const open = Boolean(anchorEl) + const [markdownMessage, setMarkDownMessage] = React.useState<string>('') + + const copy = (text: string) => { + navigator.clipboard.writeText(text) + } + const handleClick = (event: React.MouseEvent<HTMLElement>) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + const theme = useAppSelector((state) => state.theme.theme) + + useEffect(() => { + if (props.message.file) { + let splitFileName = props.message.file.split('/')[4].split('?')[0] + let fileName: string = '' + if (splitFileName.length > 50) { + fileName = + splitFileName.slice(0, 25) + + '...' + + splitFileName + .slice(splitFileName.length - 8, splitFileName.length) + .split('.')[0] + + '.' + + splitFileName.split('.')[1] + } else { + fileName = splitFileName + } + setMarkDownMessage(fileName) + } + }, [props]) + + const flexStyle = + props.message.file && props.modelType !== 'chatgpt' + ? { + display: 'flex', + alignItems: 'center', + justifyContent: 'start', + gap: '12px', + color: '#7f7df3 !important', + fontWeight: '600 !important', + lineHeight: '140%', + letterSpacing: '0.2px', + cursor: 'pointer', + } + : {} + + return ( + // <Slide direction='right' in={props.isNewMessage} mountOnEnter unmountOnExit> + <Box> + <Box + sx={{ + display: 'flex', + alignItems: 'center', + marginTop: 2, + marginBottom: 2, + }} + > + {props.desktop && ( + <Box sx={{ marginRight: 1 }}> + <Image + height={36} + width={36} + src='/svg/chatgpt/avatar.svg' + alt={''} + /> + </Box> + )} + <Box + sx={{ + paddingLeft: 0, + width: 'fit-content', + maxWidth: props.desktop ? '68%' : '95%', + marginRight: 'auto', + }} + > + <Stack direction='row' justifyContent='space-between'> + <Typography + variant='body2' + sx={{ + color: props.theme === 'light' ? '#868686' : '#A6A5A5', + lineHeight: '16.8px', + fontSize: '13px', + fontWeight: '600', + marginRight: 2, + }} + > + {props.modelTitle} + </Typography> + <Typography + variant='body2' + sx={{ + color: props.theme === 'light' ? '#868686' : '#A6A5A5', + lineHeight: '19.6px', + fontSize: '13px', + fontWeight: '600', + marginRight: 4, + }} + > + {props.message.created_at.slice(10, 16).replace('T', ' ')} + </Typography> + </Stack> + <Box display={'flex'} width='100%'> + <Box + onClick={() => { + if (props.message.file) { + window.open(props.message.file, '_blank') + } + }} + className='smallScroll' + sx={{ + overflowY: 'scroll', + position: 'relative', + padding: '15px 23px', + border: `1px solid ${ + props.theme === 'dark' ? '#303035' : '#EFF0F2' + }`, + color: props.theme === 'light' ? '#5E5E5E' : '#A6A5A5', + boxShadow: 'none', + lineHeight: '22.5px', + fontSize: '15px', + marginTop: 0.5, + textAlign: 'left', + fontFamily: 'Raleway,sans-serif', + borderRadius: '13px', + '& p': { + color: 'inherit', + fontStyle: 'inherit', + }, + ...flexStyle, + }} + > + {props.message.file && props.modelType !== 'chatgpt' ? ( + <svg + width='16' + height='18' + viewBox='0 0 15 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M10.3286 0.578662C9.95416 0.20795 9.44851 0 8.92158 0H2C0.895433 0 0 0.895431 0 2V17C0 18.1046 0.895431 19 2 19H13C14.1046 19 15 18.1046 15 17V6.03743C15 5.50349 14.7865 4.99173 14.4071 4.61609L10.3286 0.578662ZM10.0669 5.37846C9.79079 5.37846 9.56693 5.1546 9.56693 4.87846V2.19238C9.56693 1.74815 10.103 1.52452 10.4187 1.83705L13.132 4.52313C13.4495 4.83737 13.227 5.37846 12.7803 5.37846H11.7815H10.0669Z' + fill='#7f7df3' + /> + </svg> + ) : ( + '' + )} + <Markdown + content={ + props.message.file && props.modelType !== 'chatgpt' + ? markdownMessage + : props.message.content + } + theme={theme} + /> + </Box> + <Box marginLeft={1} sx={{ cursor: 'pointer' }}> + <Box + onClick={handleClick} + aria-controls={open ? 'message-menu' : undefined} + aria-haspopup='true' + aria-expanded={open ? 'true' : undefined} + > + <TooltipCustom title={'Открыть меню'}> + <svg + width='22' + height='22' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <rect + x='1' + y='1' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='1' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='11' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> + </svg> + </TooltipCustom> + </Box> + <Menu + autoFocus={false} + open={open} + anchorEl={anchorEl} + id='message-menu' + onClose={handleClose} + onClick={handleClose} + sx={{ + '& .MuiMenu-list': { + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', + color: '#8280FF', + borderRadius: '15px', + }, + '& .MuiPopover-paper': { + backgroundColor: + theme === 'dark' ? '#303035' : '#EFF0F2', + borderRadius: '15px', + }, + }} + > + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '10px', + }} + onClick={() => { + copy( + props.message.content + ? props.message.content + : props.message.file + .split('/')[4] + .split('?')[0] + ) + }} + > + <svg + width='14' + height='14' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <g id='Group 29094'> + <rect + id='Rectangle 3340' + x='0.85' + y='3.85' + width='14.3' + height='14.3' + rx='2.15' + fill='transparent' + stroke='#8280FF' + strokeWidth='1.7' + /> + <path + id='Vector 40' + d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' + stroke='#8280FF' + strokeWidth='1.7' + strokeLinecap='round' + strokeLinejoin='round' + /> + </g> + </svg> + Копировать + </MenuItem> + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '5px', + }} + onClick={() => { + props.deleteMessage(props.message.uid) + }} + > + <svg + width='21' + height='14' + viewBox='0 0 60 42' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' + fill='#8280FF' + /> + </svg> + Удалить + </MenuItem> + </Menu> + </Box> + </Box> + </Box> + </Box> + </Box> + ) +} @@ -0,0 +1,110 @@ +import { useAppSelector } from '@/src/main/store/store' +import { TooltipCustom, useShowData } from '@/src/shared' +import { Grow, Box } from '@mui/material' +import { useImageIcons } from '../model' + +export interface ImageIconsProps { + uid: string + url: string | null + content?: string +} + +export const ImageIcons = ({ uid, url, content }: ImageIconsProps) => { + const theme = useAppSelector((state) => state.theme.theme) + + const { toggleMenu, downloadFile, iconsMenu } = useImageIcons() + + return ( + <> + <svg + style={{ + position: 'absolute', + top: '10px', + right: '10px', + cursor: 'pointer', + zIndex: '5', + }} + onClick={(e) => { + e.stopPropagation() + toggleMenu(uid) + }} + width='35' + height='35' + viewBox='0 0 35 35' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <circle + cx='17.5' + cy='17.5' + r='17.5' + fill={theme === 'dark' ? '#303035' : '#FFFFFF'} + /> + <path + fillRule='evenodd' + clipRule='evenodd' + d='M11 12.4377C11 12.1957 11.0978 11.9637 11.272 11.7926C11.4461 11.6215 11.6823 11.5254 11.9286 11.5254H23.0714C23.3177 11.5254 23.5539 11.6215 23.728 11.7926C23.9022 11.9637 24 12.1957 24 12.4377C24 12.6796 23.9022 12.9117 23.728 13.0828C23.5539 13.2538 23.3177 13.35 23.0714 13.35H11.9286C11.6823 13.35 11.4461 13.2538 11.272 13.0828C11.0978 12.9117 11 12.6796 11 12.4377ZM11 16.9991C11 16.7571 11.0978 16.5251 11.272 16.354C11.4461 16.1829 11.6823 16.0868 11.9286 16.0868H23.0714C23.3177 16.0868 23.5539 16.1829 23.728 16.354C23.9022 16.5251 24 16.7571 24 16.9991C24 17.241 23.9022 17.4731 23.728 17.6442C23.5539 17.8152 23.3177 17.9114 23.0714 17.9114H11.9286C11.6823 17.9114 11.4461 17.8152 11.272 17.6442C11.0978 17.4731 11 17.241 11 16.9991ZM11 21.5605C11 21.3185 11.0978 21.0865 11.272 20.9154C11.4461 20.7443 11.6823 20.6482 11.9286 20.6482H23.0714C23.3177 20.6482 23.5539 20.7443 23.728 20.9154C23.9022 21.0865 24 21.3185 24 21.5605C24 21.8024 23.9022 22.0345 23.728 22.2056C23.5539 22.3766 23.3177 22.4728 23.0714 22.4728H11.9286C11.6823 22.4728 11.4461 22.3766 11.272 22.2056C11.0978 22.0345 11 21.8024 11 21.5605Z' + fill='#827FFF' + /> + </svg> + + <Grow in={iconsMenu === uid}> + <Box + flexDirection={'column'} + gap={'20px'} + sx={{ + zIndex: '5', + background: theme === 'dark' ? '#303035' : '#FFFFFF', + position: 'absolute', + top: '50px', + right: '11px', + padding: '12px 9px', + borderRadius: '60px', + display: iconsMenu === uid ? 'flex' : 'none', + }} + > + <TooltipCustom title={'Открыть в новой вкладке'} placement={'right'}> + <svg + onClick={() => { + window.open(`${url ? url : ''}`, '_blank') + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + d='M5.875 2.625H2.625C2.19402 2.625 1.7807 2.7962 1.47595 3.10095C1.1712 3.4057 1 3.81902 1 4.25V12.375C1 12.806 1.1712 13.2193 1.47595 13.524C1.7807 13.8288 2.19402 14 2.625 14H10.75C11.181 14 11.5943 13.8288 11.899 13.524C12.2038 13.2193 12.375 12.806 12.375 12.375V9.125M9.125 1H14M14 1V5.875M14 1L5.875 9.125' + stroke='#827FFF' + strokeWidth='2' + strokeLinecap='round' + strokeLinejoin='round' + /> + </svg> + </TooltipCustom> + <TooltipCustom title={'Скачать изображение'} placement={'right'}> + <svg + onClick={() => { + downloadFile(url, content) + }} + width='15' + height='15' + viewBox='0 0 15 15' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + d='M1 10.75V11.5625C1 12.209 1.25681 12.829 1.71393 13.2861C2.17105 13.7432 2.79103 14 3.4375 14H11.5625C12.209 14 12.829 13.7432 13.2861 13.2861C13.7432 12.829 14 12.209 14 11.5625V10.75M10.75 7.5L7.5 10.75M7.5 10.75L4.25 7.5M7.5 10.75V1' + stroke='#827FFF' + strokeWidth='2' + strokeLinecap='round' + strokeLinejoin='round' + /> + </svg> + </TooltipCustom> + </Box> + </Grow> + </> + ) +} @@ -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%; + } + } +} @@ -0,0 +1,349 @@ +import React, { memo, useState } from 'react' +import { Grow, Skeleton, Typography } from '@mui/material' +import Box from '@mui/material/Box' +import Image from 'next/image' +import Link from 'next/link' + +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 styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { Message } from '@/src/entities/message' +import { ImageIcons } from './image-icons' + +interface MessagesList { + device: 'mobile' | 'desktop' + images: Message[] + getMessagesPagination?: () => Promise<void> + isComplete: boolean +} + +export const ImageMessagesList: React.FC<MessagesList> = memo( + ({ device, images, getMessagesPagination }) => { + const { error, showError, isError } = useShowData() + const [modal, setModal] = useState<boolean>(false) + const theme = useAppSelector((state) => state.theme.theme) + + const { chosenImage, setChosenImage, loaded, setLoaded, computedLibraryImages } = + useMessages(images) + + return ( + <> + <FullScreenModal modal={modal} setModal={setModal} image={chosenImage} /> + <Success message={error} open={Boolean(error)} isError={isError} /> + <Box className={'mt-15'}> + <Typography + sx={{ + paddingBottom: '30px', + fontSize: '16px', + color: '#A4AAB5', + fontWeight: '600', + letterSpacing: '0.3px', + }} + > + ГЕНЕРАЦИИ + </Typography> + + <Box + id={'messages-list'} + sx={{ + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + gap: '20px', + justifyContent: device === 'mobile' ? 'center' : 'start', + }} + > + {images?.length !== 0 && + Array.isArray(images) && + 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' }}> + {isZip ? ( + <Box + display='flex' + alignItems='center' + justifyContent='center' + sx={{ + border: '1px solid #8280FF', + borderRadius: 5, + 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> + </Box> + </Box> + ) : ( + <Box> + <Box + sx={{ + cursor: 'pointer', + width: + device === 'desktop' + ? '250px' + : '291px', + height: + device === 'desktop' + ? '250px' + : '291px', + position: 'relative', + overflow: 'hidden', + borderRadius: '15px', + }} + > + {!isSvg ? ( + <> + <Image + className={ + styles.image + } + onClick={() => { + setChosenImage( + message.file as string + ) + setModal(true) + }} + onLoadingComplete={() => + setLoaded(true) + } + style={{ + position: + 'relative', + zIndex: '2', + borderRadius: 15, + width: '100%', + height: '100%', + opacity: loaded + ? '100%' + : '0%', + userSelect: + 'none', + objectFit: + 'contain', + }} + width={500} + height={500} + src={ + (message.file as unknown as string) || + '' + } + alt={ + 'К сожалению, изображение не загрузилось' + } + /> + <Image + className={ + styles.image + } + onLoadingComplete={() => + setLoaded(true) + } + style={{ + position: + 'absolute', + zIndex: '1', + borderRadius: 15, + width: '100%', + height: '100%', + opacity: loaded + ? '100%' + : '0%', + userSelect: + 'none', + objectFit: + 'cover', + right: 0, + filter: 'blur(10px) brightness(0.7)', + }} + width={10} + height={10} + src={ + (message.file as unknown as string) || + '' + } + alt='' + /> + </> + ) : ( + <> + <img + className={ + styles.image + } + onClick={() => { + setChosenImage( + message.file as string + ) + setModal(true) + }} + style={{ + position: + 'relative', + zIndex: '2', + borderRadius: 15, + width: '100%', + height: '100%', + userSelect: + 'none', + objectFit: + 'contain', + }} + src={ + (message.file as unknown as string) || + '' + } + alt='К сожалению, изображение не загрузилось' + /> + <img + className={ + styles.image + } + width='10px' + height='10px' + style={{ + position: + 'absolute', + zIndex: '1', + borderRadius: 15, + width: '100%', + height: '100%', + opacity: loaded + ? '100%' + : '0%', + userSelect: + 'none', + objectFit: + 'cover', + right: 0, + filter: 'blur(10px) brightness(0.7)', + }} + src={ + (message.file as unknown as string) || + '' + } + alt='К сожалению, изображение не загрузилось' + /> + </> + )} + + {!loaded && ( + <Skeleton + sx={{ + 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%', + }} + variant='rectangular' + /> + )} + <ImageIcons + key={message.uid} + uid={message.uid} + content={message.content} + url={ + message.file + ? message.file.toString() + : null + } + /> + </Box> + + <TooltipCustom + key={message.uid} + placement='right' + title={ + message.content.length > 30 + ? message.content + : '' + } + > + <Typography + sx={{ + fontSize: '15px', + maxWidth: + device === 'desktop' + ? '250px' + : '100%', + marginTop: '12px', + color: + theme === 'dark' + ? '#A4AAB5' + : '#555556', + }} + > + {!loaded + ? '' + : message.content + .length > 0 + ? message?.content + .replaceAll( + '"', + '' + ) + .slice(0, 30) + : 'описание отсутствует'} + {message?.content.length > + 30 && + loaded && + '...'} + </Typography> + </TooltipCustom> + </Box> + )} + </Box> + ) + })} + </Box> + </Box> + </> + ) + } +) + +ImageMessagesList.displayName = 'ImageMessagesList' @@ -0,0 +1,7 @@ +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' +export * from './image-icons' @@ -0,0 +1,134 @@ +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/date-from-string' +import { getDayMontsString } from '@/src/widgets/messages/lib/day-months-string' +import { UserMessage } from '@/src/widgets/messages' + +interface IProps { + message: Message + index: number + messageResponse: Message[] | null + modelTitle: string | undefined + deleteMessage?: (message_uid: string) => void + setCurrentSrc: (value: string) => void + isNewMessage: boolean + setModal: (value: boolean) => void + modelType: string + device: 'mobile' | 'desktop' + setResendValue: (value: string) => void + onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void +} + +export const IsNextDay = memo( + ({ + message, + index, + onLoadImage, + messageResponse, + modelTitle, + deleteMessage, + setCurrentSrc, + setModal, + isNewMessage, + modelType, + device, + setResendValue, + }: IProps) => { + if (messageResponse && message.created_at) { + const date = getDateFromString(message.created_at) + const prevDate = getDateFromString(messageResponse[index]?.created_at) + + if (date && prevDate) { + if (date.day > prevDate.day && date.month === prevDate.month) { + return ( + <> + <Box display={'flex'} justifyContent={'center'}> + <Typography + sx={{ + backgroundColor: '#8280FF26', + color: '#8280FF', + display: 'inline-block', + padding: '7px 25px', + borderRadius: '100px', + fontSize: '15px', + marginY: '40px', + fontWeight: '600', + }} + > + {getDayMontsString(message.created_at)} + </Typography> + </Box> + <UserMessage + onLoadImage={onLoadImage} + setResendValue={setResendValue} + modelTitle={modelTitle} + deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} + key={message.uid} + setModal={setModal} + message={message} + isNewMessage={isNewMessage} + modelType={modelType} + device={device} + /> + </> + ) + } + } + if (!prevDate) { + return ( + <> + <Box display={'flex'} justifyContent={'center'}> + <Typography + sx={{ + backgroundColor: '#8280FF26', + color: '#8280FF', + display: 'inline-block', + padding: '7px 25px', + borderRadius: '100px', + fontSize: '15px', + marginY: '40px', + fontWeight: '600', + }} + > + {getDayMontsString(message.created_at)} + </Typography> + </Box> + <UserMessage + setModal={setModal} + onLoadImage={onLoadImage} + setResendValue={setResendValue} + modelTitle={modelTitle} + setCurrentSrc={setCurrentSrc} + deleteMessage={deleteMessage} + key={message.uid} + message={message} + isNewMessage={isNewMessage} + modelType={modelType} + device={device} + /> + </> + ) + } + } + return ( + <UserMessage + onLoadImage={onLoadImage} + setResendValue={setResendValue} + modelTitle={modelTitle} + deleteMessage={deleteMessage} + setCurrentSrc={setCurrentSrc} + key={message.uid} + setModal={setModal} + message={message} + isNewMessage={isNewMessage} + modelType={modelType} + device={device} + /> + ) + } +) + +IsNextDay.displayName = 'IsNextDay' @@ -0,0 +1,44 @@ +import React from 'react' +import { Box, Typography } from '@mui/material' + +const previewMessages = ['Что такое орбитальная механика?', 'Как приготовить омлет?', 'Кто такие Фиксики?'] + +export function PreviewView(props: any) { + return ( + <Box className='content-center-translate'> + <Typography className='text'>Не знаете, с чего начать?</Typography> + <Typography className='text' sx={{ marginBottom: '15px' }}> + Попробуйте, например, вот так: + </Typography> + <Box + sx={{ + textAlign: 'center', + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + {previewMessages.map((el) => { + return ( + <Box + key={el} + onClick={() => props.setValue(el)} + sx={{ + cursor: 'pointer', + padding: '15px', + color: '#8280FF', + width: 'fit-content', + marginTop: '15px', + backgroundColor: 'rgba(130, 128, 255, 0.10)', + borderRadius: '12px', + }} + > + {el} + </Box> + ) + })} + </Box> + </Box> + ) +} @@ -0,0 +1,569 @@ +import React, { useMemo, useState } from 'react' +import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' +import Stack from '@mui/material/Stack' +import Image from 'next/image' + +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/ui/bot-message' + +interface IMessagesList { + // messageResponse: Message[] | null + // mode?: string + // sendMessage?: (title: string) => void + // getMessagesPagination?: () => Promise<void> + + device: 'mobile' | 'desktop' + modelType: string + isNewMessage: boolean + setCurrentSrc: (value: string) => void + message: Message + setModal: (value: boolean) => void + deleteMessage?: (message_uid: string) => void + modelTitle: string | undefined + setResendValue: (value: string) => void + onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void +} + +export const UserMessage = React.memo(function UserMessage({ + setModal, + setCurrentSrc, + ...props +}: IMessagesList) { + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const open = Boolean(anchorEl) + const desktop = props.device === 'desktop' + const [loaded, setLoaded] = useState(false) + + const copy = (text: string) => { + navigator.clipboard.writeText(text) + } + const handleClick = (event: React.MouseEvent<HTMLElement>) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + + const resend = async () => { + props.setResendValue(props.message.content) + if (props.message.file !== null) { + fetch(props.message.file as string, { + method: 'GET', + headers: {}, + }).then((response) => { + response.arrayBuffer().then(function (buffer) { + const url = new Blob([buffer]) + const binaryFile = new File([url], 'image.png') + if (props.onLoadImage) { + props.onLoadImage(null, binaryFile) + } + }) + }) + } + } + + const theme = useAppSelector((state) => state.theme.theme) + + return ( + <> + <span className='tutorial-message'> + {!props.message.from_model ? ( + // <Slide className='tutorial-message-me' direction='left' in={props.isNewMessage} mountOnEnter unmountOnExit> + <Box + className='smallScroll' + sx={{ + width: 'fit-content', + marginLeft: 'auto', + maxWidth: desktop ? '68%' : '100%', + marginTop: 2, + marginBottom: 2, + }} + > + <Stack direction='row' justifyContent='end' alignItems='center'> + <Typography + variant='body2' + sx={{ + color: theme === 'light' ? '#868686' : '#A6A5A5', + lineHeight: '19.6px', + fontSize: '13px', + fontWeight: '600', + marginLeft: 1, + }} + > + {props.message.created_at.slice(10, 16).replace('T', ' ')} + </Typography> + </Stack> + + <Box display='flex' alignItems='center' justifyContent='end'> + {props.message.file ? ( + <Box + sx={{ + display: 'flex', + alignItems: 'end', + flexDirection: 'column', + height: '60%', + width: '60%', + }} + > + <Box sx={{ position: 'relative' }}> + <Image + onClick={() => { + if (!props.message.file) return + setCurrentSrc( + props.message.file.toString() + ) + setModal(true) + }} + onLoadingComplete={() => setLoaded(true)} + style={{ + objectFit: 'contain', + height: '100%', + width: '100%', + borderRadius: '13px', + cursor: 'pointer', + opacity: loaded ? '100%' : '0%', + minWidth: '100px', + minHeight: '100px', + }} + width={500} + height={500} + src={props.message.file.toString()} + alt={ + 'К сожалению, изображение не загрузилось' + } + /> + {!loaded && ( + <Skeleton + sx={{ + background: + theme === 'dark' + ? '#2d2d2f' + : '#EFF0F2', + position: 'absolute', + top: '0', + left: '0', + zIndex: '1', + borderRadius: 5, + width: '100%', + height: '100%', + }} + variant='rectangular' + /> + )} + </Box> + <Box display='flex' alignItems='center'> + <Box marginRight={1} sx={{ cursor: 'pointer' }}> + <Box + onClick={handleClick} + aria-controls={ + open ? 'message-menu' : undefined + } + aria-haspopup='true' + aria-expanded={ + open ? 'true' : undefined + } + > + <TooltipCustom title={'Открыть меню'}> + <svg + width='22' + height='22' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <rect + x='1' + y='1' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='1' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='11' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> + </svg> + </TooltipCustom> + </Box> + <Menu + autoFocus={false} + open={open} + anchorEl={anchorEl} + id='message-menu' + onClose={handleClose} + onClick={handleClose} + sx={{ + '& .MuiMenu-list': { + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', + color: '#8280FF', + borderRadius: '15px', + }, + '& .MuiPopover-paper': { + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', + borderRadius: '15px', + }, + }} + > + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '10px', + }} + onClick={() => { + copy(props.message.content) + }} + > + <svg + width='14' + height='14' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <g id='Group 29094'> + <rect + id='Rectangle 3340' + x='0.85' + y='3.85' + width='14.3' + height='14.3' + rx='2.15' + fill='transparent' + stroke='#8280FF' + strokeWidth='1.7' + /> + <path + id='Vector 40' + d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' + stroke='#8280FF' + strokeWidth='1.7' + strokeLinecap='round' + strokeLinejoin='round' + /> + </g> + </svg> + Копировать + </MenuItem> + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '5px', + }} + onClick={() => { + if (props.deleteMessage) + props.deleteMessage( + props.message.uid + ) + }} + > + <svg + width='21' + height='14' + viewBox='0 0 60 42' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' + fill='#8280FF' + /> + </svg> + Удалить + </MenuItem> + </Menu> + </Box> + <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> + {!props.message.is_sent && ( + <Image + onClick={resend} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} + height={20} + width={20} + alt='1' + src={'/svg/important.svg'} + /> + )} + </TooltipCustom> + <Typography + className='smallScroll' + sx={{ + overflowY: 'scroll', + padding: '10px 15px', + backgroundColor: '#7F7DF3', + borderRadius: '13px', + color: '#FFFFFF', + lineHeight: '21px', + fontSize: '15px', + fontWeight: '400px', + marginTop: 0.4, + textAlign: 'left', + whiteSpace: + props.message.content.length > 30 + ? 'pre-wrap' + : 'pre', + }} + > + {/*<Markdown content={props.message.content} />*/} + {props.message.content} + </Typography> + </Box> + </Box> + ) : ( + <Box display='flex' alignItems='center'> + <Box marginRight={1} sx={{ cursor: 'pointer' }}> + <Box + onClick={handleClick} + aria-controls={ + open ? 'message-menu' : undefined + } + aria-haspopup='true' + aria-expanded={open ? 'true' : undefined} + > + <TooltipCustom title={'Открыть меню'}> + <svg + width='22' + height='22' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <rect + x='1' + y='1' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='1' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <rect + x='11' + y='11' + width='7' + height='7' + rx='2' + stroke='#A4AAB5' + strokeWidth='2' + /> + <circle + cx='14.5' + cy='4.5' + r='3.5' + stroke='#A4AAB5' + strokeWidth='2' + /> + </svg> + </TooltipCustom> + </Box> + <Menu + autoFocus={false} + open={open} + anchorEl={anchorEl} + id='message-menu' + onClose={handleClose} + onClick={handleClose} + sx={{ + '& .MuiMenu-list': { + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', + color: '#8280FF', + borderRadius: '15px', + }, + '& .MuiPopover-paper': { + backgroundColor: + theme === 'dark' + ? '#303035' + : '#EFF0F2', + borderRadius: '15px', + }, + }} + > + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '10px', + }} + onClick={() => { + copy(props.message.content) + }} + > + <svg + width='14' + height='14' + viewBox='0 0 19 19' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <g id='Group 29094'> + <rect + id='Rectangle 3340' + x='0.85' + y='3.85' + width='14.3' + height='14.3' + rx='2.15' + fill='transparent' + stroke='#8280FF' + strokeWidth='1.7' + /> + <path + id='Vector 40' + d='M6.5 1H15C16.6569 1 18 2.34315 18 4V12.5' + stroke='#8280FF' + strokeWidth='1.7' + strokeLinecap='round' + strokeLinejoin='round' + /> + </g> + </svg> + Копировать + </MenuItem> + <MenuItem + autoFocus={false} + sx={{ + fontSize: '15px', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + gap: '5px', + }} + onClick={() => { + if (props.deleteMessage) + props.deleteMessage( + props.message.uid + ) + }} + > + <svg + width='21' + height='14' + viewBox='0 0 60 42' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M17.4216 27.9757C18.1603 28.707 19.3566 28.707 20.0972 27.9757L24.4097 23.6633L28.6079 27.8633C29.3391 28.5946 30.5278 28.5946 31.2609 27.8633C31.9941 27.1321 31.9941 25.932 31.2609 25.2007L27.0629 21.0195L31.3154 16.7632C32.0542 16.032 32.0542 14.8321 31.3154 14.0821C30.5767 13.3509 29.3785 13.3509 28.6398 14.0821L24.3873 18.3382L20.1891 14.1382C19.456 13.4069 18.2673 13.4069 17.536 14.1382C16.801 14.8694 16.801 16.0695 17.536 16.8008L21.7341 20.982L17.4216 25.2946C16.681 26.0446 16.681 27.2257 17.4216 27.9757ZM3.75289 7.87575C3.75289 5.81325 5.43291 4.12575 7.50666 4.12576L40.3735 4.12576L55.6566 21.0383L40.4279 37.8758L7.50666 37.8758C5.43291 37.8758 3.75288 36.1883 3.75288 34.1258L3.75289 7.87575ZM7.50666 41.6258L41.1534 41.6258C41.6784 41.6633 42.2147 41.4945 42.616 41.1007L59.4648 22.4633C59.8604 22.0696 60.031 21.5445 60.0029 21.0383C60.031 20.5133 59.8604 19.9883 59.4648 19.5945L42.616 0.957118C42.2485 0.600868 41.7685 0.413295 41.2885 0.413295L41.2885 0.375758L7.50666 0.375755C3.36104 0.375755 -0.000889326 3.732 -0.000889688 7.87575L-0.000891983 34.1258C-0.000892345 38.2695 3.36104 41.6258 7.50666 41.6258Z' + fill='#8280FF' + /> + </svg> + Удалить + </MenuItem> + </Menu> + </Box> + <TooltipCustom title='При отправке данного сообщения произошла ошибка, нажмите чтобы отправить заново'> + {!props.message.is_sent && ( + <Image + onClick={resend} + style={{ + cursor: 'pointer', + marginRight: '5px', + }} + height={20} + width={20} + alt='1' + src={'/svg/important.svg'} + /> + )} + </TooltipCustom> + <Typography + className='smallScroll' + sx={{ + overflowY: 'scroll', + padding: '10px 15px', + backgroundColor: '#7F7DF3', + borderRadius: '13px', + color: '#FFFFFF', + lineHeight: '21px', + fontSize: '15px', + fontWeight: '400px', + marginTop: 0.4, + textAlign: 'left', + whiteSpace: + props.message.content.length > 30 + ? 'pre-wrap' + : 'pre', + }} + > + {/*<Markdown content={props.message.content} />*/} + {props.message.content} + </Typography> + </Box> + )} + </Box> + </Box> + ) : ( + <BotMessage + modelTitle={props.modelTitle} + deleteMessage={props.deleteMessage} + isNewMessage={props.isNewMessage} + message={props.message} + desktop={desktop} + theme={theme} + modelType={props.modelType} + /> + )} + </span> + </> + ) +}) @@ -0,0 +1,4 @@ +export * from './ui' +export * from './lib' +export * from './api' +export * from './model' \ No newline at end of file