@@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,3 +1,4 @@ - - + + \ No newline at end of file @@ -24,5 +24,5 @@ height: 25px; border-radius: 7px; border: var(--new-ui-ctrl-f-button-border); - background: var(--new-ui-ctrl-f-button-bg); + background: var(--new-ui-ctrl-f-button-bg); } @@ -17,10 +17,10 @@ export const store = configureStore({ balance: balanceSlice.reducer, stepper: stepperSlice.reducer, user: userSlice.reducer, + copy: copySlice.reducer, notification: notificationSlice.reducer, params: paramsStore.reducer, settings: settingsSlice.reducer, - copy: copySlice.reducer, }, }) @@ -15,14 +15,21 @@ --border-color2: #e7e7e7; --bg-audio: #f2f2fe; - --new-ui-bg-app-color: #eff0f2; - --new-ui-main-color: white; - --new-ui-gray-color: #a4aab5; - --new-ui-text-color: #2b2b42; - --new-ui-border: 2px solid #eff0f2; - --new-ui-btn-danger-bg: #ff23721a; - --new-ui-ctrl-f-button-bg: #f9f9fc; - --new-ui-ctrl-f-button-border: 1px solid #c4cbd8; + --new-ui-bg-app-color: #eff0f2; + --new-ui-main-color: white; + --new-ui-gray-color: #A4AAB5; + --new-ui-text-color: #2B2B42; + --new-ui-border: 2px solid #EFF0F2; + --new-ui-btn-danger-bg: #FF23721A; + --new-ui-ctrl-f-button-bg:#F9F9FC; + --new-ui-ctrl-f-button-border:1px solid #C4CBD8; + + --copy-border: #EFF0F2; + --copy-color: #343437; + + --cards-hover:#F9F9FF; + --choosen-tab: #FFFFFF; + --choosen-tab-color:#373737 ; } :root[data-theme='dark'] { @@ -42,14 +49,21 @@ --border-color2: #2c2c2c; --bg-audio: #303030; - --new-ui-bg-app-color: #303035; - --new-ui-border: 1px solid #40404e; - --new-ui-main-color: #151518; - --new-ui-gray-color: #a4aab5; - --new-ui-text-color: white; - --new-ui-btn-danger-bg: #ff23721a; - --new-ui-ctrl-f-button-bg: #242428; - --new-ui-ctrl-f-button-border: 1px solid #303035; + --new-ui-bg-app-color: #303035; + --new-ui-border: 1px solid #40404E; + --new-ui-main-color: #151518; + --new-ui-gray-color: #A4AAB5; + --new-ui-text-color: white; + --new-ui-btn-danger-bg: #FF23721A; + --new-ui-ctrl-f-button-bg:#242428; + --new-ui-ctrl-f-button-border:1px solid #303035; + + --copy-border: #343437; + --copy-color:#EFF0F2; + + --cards-hover:#303047; + --choosen-tab: #151518; + --choosen-tab-color: #FFFFFF; } * { @@ -319,19 +333,24 @@ textarea { } .rdw-editor-toolbar { - background-color: transparent !important; - padding-bottom: 15px !important; - border: none !important; - border-bottom: 1px solid #eff0f2 !important; + background-color: transparent !important; + padding: 20px 0 !important; + border: none !important; + border-bottom: 1px solid var(--copy-border) !important; + border-top: 1px solid var(--copy-border) !important; } .rdw-dropdown-wrapper { - background-color: transparent !important; - border: 2px solid var(--new-ui-bg-app-color) !important; - border-radius: 10px !important; - padding: 10px !important; - height: 36px !important; - min-width: 40px !important; + background-color: transparent !important; + border: 2px solid var(--new-ui-bg-app-color) !important; + border-radius: 10px !important; + padding: 0 !important; + height: 36px !important; + min-width: 40px !important; +} + +.rdw-dropdown-selectedtext{ + padding: 0 16px 0 12px !important; } .rdw-dropdown-wrapper:hover { @@ -348,22 +367,61 @@ textarea { } .rdw-dropdown-optionwrapper { - width: 100% !important; - margin-top: 15px !important; - overflow: hidden; - color: inherit !important; - overflow-y: hidden !important; + border: none !important; + border-radius: 10px !important; + width: 100% !important; + margin-top: 12px !important; + overflow: hidden; + color: inherit !important; + background-color: var(--background-color-main) !important; + overflow-y: hidden !important; } .rdw-block-dropdown { width: 150px !important; } .rdw-dropdown-optionwrapper > li { - color: inherit !important; + color: var(--new-ui-text-color) !important; + padding: 0 16px 0 12px !important; +} + +.rdw-dropdown-optionwrapper > li:hover{ + background-color:var(--new-ui-gray-color) !important +} + +.rdw-dropdownoption-active{ + background: var(--new-ui-gray-color) !important } .rdw-dropdown-optionwrapper:hover { - box-shadow: none; - color: inherit !important; + border: none !important; + box-shadow: none !important; + color: inherit !important; +} + +.rdw-dropdown-carettoclose{ + border-radius: 5px !important; + border-bottom-color: var(--new-ui-gray-color) !important; +} + +.rdw-dropdown-carettoopen{ + border-radius: 5px !important; + border-top-color: var(--new-ui-gray-color) !important; +} + +.rdw-text-align-wrapper{ + margin: 0 !important; +} + +.rdw-list-wrapper{ + margin: 0 !important; +} + +.rdw-history-wrapper{ + margin: 0 !important; +} + +.rdw-block-wrapper{ + margin: 0 !important; } .border-bottom-1px-gray { @@ -390,8 +448,9 @@ textarea { } .smallScroll::-webkit-scrollbar-track { - background: initial; - margin: 21px 0; + background: initial; + /*margin: 21px 0;*/ + margin: 5px 0; } .smallScroll::-webkit-scrollbar-thumb { @@ -409,8 +468,87 @@ textarea { transition-duration: 250ms; } -.rotate-0 { - transform: rotate(0deg); - transition: all; - transition-duration: 250ms; +.rotate-0{ + transform: rotate(0deg); + transition: all; + transition-duration: 250ms; +} + + +/*COPY*/ +.toolbarClassName{ + align-items: center; + gap: 15px; +} +.wrapperClassName{ + +} + +.editorClassName{ + border: 1px solid transparent; + transition: border-color 0.3s; + cursor: text; +} + +.editorClassName div:focus{ + outline: none !important; + border-color: transparent !important; +} + +.editorClassName div .public-DraftStyleDefault-block{ + display: inline-block; + padding:0 1px; + margin: 0.5em 0 !important; +} + +.public-DraftEditor-content{ + overflow-y: scroll; + max-height: calc(75vh - 200px) ; + + @media (max-width: 768px) { + max-height: calc(70vh - 200px) ; + } + +} + + +.public-DraftEditor-content::-webkit-scrollbar{ + height: 5px; + width: 2px; +} + +.public-DraftEditor-content::-webkit-scrollbar-track { + background: initial; + margin: 21px 0; +} + +.public-DraftEditor-content::-webkit-scrollbar-thumb { + background-color: rgba(217, 217, 217, 0.49); + border-radius: 5px; +} + +.inline{ + gap:3px; + margin: 0 !important; +} + +.inline-btn{ + width: 12px; + height: 25px !important; + margin: 0 !important; + padding: 0 !important; +} + +.rdw-option-active{ + background: rgba(229, 229, 229, 0.18) !important; + -webkit-box-shadow: inset 0 0 5px #c1c1c1 !important; + -moz-box-shadow: inset 0 0 5px #c1c1c1 !important; + box-shadow: inset 0 0 5px #c1c1c1 !important; + outline: none !important; +} + +.copy-color{ + color:var(--copy-color); + border-color: var(--copy-border); + } @@ -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,116 @@ +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom } from '#/shared' +import { API_URL } from '#/shared/lib/constants' +import { InputStyleSmallLight, InputStyleSmallDark } from '#/shared/ui/input' +import { TableRow, TableCell, TextField, Stack } from '@mui/material' +import { useMask } from '@react-input/mask' +import axios from 'axios' +import { useSession } from 'next-auth/react' +import Image from 'next/image' +import { useState, useMemo, useEffect } from 'react' + +export interface KeyRowProps extends ApiKeyDTO { + deleteKey: (name: string) => void + keyValue: string +} + +export const KeyRow = ({ + key, + name, + user, + keyValue, + deleteKey, + token_limit, + created_at, + expires_at, + ...props +}: KeyRowProps) => { + const [isCopy, setIsCopy] = useState(false) + const [limit, setLimit] = useState(token_limit) + const theme = useAppSelector((state) => state.theme.theme) + const { data: session } = useSession() + + const inputRef = useMask({ + mask: '_'.repeat(10), + replacement: { + _: /\d+/, + }, + }) + + const copy = (text: string) => { + navigator.clipboard.writeText(text) + setIsCopy(true) + setTimeout(() => setIsCopy(false), 3000) + } + + const computedLimit = useMemo(() => { + return limit === '' || limit === 'Бесконечно' ? null : Number(limit) + }, [limit]) + + useEffect(() => { + let timeout = window.setTimeout(() => { + console.log(limit) + if (limit !== token_limit) { + axios.patch( + API_URL + '/public/api-key', + { token_limit: computedLimit, name }, + { headers: { Authorization: `Bearer ${session?.access}` } } + ) + } + }, 1000) + return () => window.clearTimeout(timeout) + }, [limit]) + + return ( + + + {name} + + + {keyValue} + + + setLimit(e.target.value)} + sx={theme === 'light' ? { ...InputStyleSmallLight } : { ...InputStyleSmallDark }} + /> + + + {created_at.split('T')[0]} + + + {expires_at !== null ? expires_at : 'Бессрочно'} + + + + {isCopy ? ( + {'copy'} + ) : ( + + copy(keyValue)} + src={'/svg/copy.svg'} + width={20} + height={20} + style={{ cursor: 'pointer' }} + alt={'copy'} + /> + + )} + deleteKey(name)} + style={{ cursor: 'pointer' }} + src='/svg/main_menu/trash.svg' + width={20} + height={20} + alt='Удалить' + /> + + + + ) +} @@ -0,0 +1 @@ +export * from './message.routes' \ No newline at end of file @@ -0,0 +1,16 @@ +import { API_URL } from '#/shared/lib/constants' +import { IMessageRequest } from '#/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, + }, + }) +} @@ -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 '#/shared/lib/constants' +import axios from 'axios' +import { IModel } from '../types' + +export async function getBotParams(slug: string, token?: string) { + return await axios.get(API_URL + `/ml_models/${slug}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,11 @@ +import { API_URL } from '#/shared/lib/constants' +import axios from 'axios' +import { IShortModel } from '../types' + +export async function getModelsImages(token?: string) { + return await axios.get(API_URL + '/ml_models/?category=images', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) +} @@ -0,0 +1,2 @@ +export * from './bot.route' +export * from './images-bots.route' @@ -0,0 +1 @@ +export * from './use-images-bots' \ No newline at end of file @@ -0,0 +1,118 @@ +import { useState } from 'react' +import { IModel } from '../types' +import { useSession } from 'next-auth/react' +import { getBotParams } from '../api' +import { useAppDispatch } from '#/app/store/store' +import { setParams } from '#/app/store/model-parametres-store' + +export function useImageBot(slug: string) { + const [botParams, setBotParams] = useState(null) + const [version, setVersion] = useState('') + const [modelType, setModelType] = useState('') + + const dispatch = useAppDispatch() + + const { data } = useSession() + + function setDefault(bot: IModel) { + setVersion('') + + dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + } + + // это пиз**ц + // нужен рефакторинг (я то в этом не разбираюсь) + // а стажеры и подавно)))) + function setStoreParams(bot: IModel) { + dispatch( + setParams( + bot.parameters.reduce( + (a, v) => + v.versions.includes(bot.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } + + function setAllParams(bot: IModel) { + setBotParams(bot) + setModelType(bot.slug) + + // store + if (bot.versions.length === 0) return setDefault(bot) + + setVersion(bot.versions[0].slug) + setStoreParams(bot) + } + + // это пиз**ц + const resetParams = () => { + if (botParams) { + dispatch(setParams({})) + if (botParams.versions.length !== 0) { + setVersion(botParams.versions[0].slug) + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => + v.versions.includes(botParams.versions[0].slug) + ? { ...a, [v.key]: v.values.default } + : { ...a }, + {} + ) + ) + ) + } else { + setVersion(botParams.slug) + dispatch( + setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) + } + } + } + + // это пиз**ц + const setDefaultParams = () => { + if (!botParams) return + + dispatch(setParams({})) + + if (version === '') { + return dispatch( + setParams(botParams.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {})) + ) + } + + dispatch( + setParams( + botParams.parameters.reduce( + (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), + {} + ) + ) + ) + } + + // api functions + + async function fetchBotParams() { + if (!data) return + + const { data: bot, ...response } = await getBotParams(slug, data.access) + + if (response.status < 400) setAllParams(bot) + } + + return { + botParams, + version, + modelType, + fetchBotParams, + setVersion, + resetParams, + setDefaultParams, + } +} @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { getModelsImages } from '../api' +import { useSession } from 'next-auth/react' +import { IShortModel } from '../types' + +export function useImagesBots() { + const [bots, setBots] = useState([]) + + const { data } = useSession() + + async function fetchBots() { + if (!data) return + + const response = await getModelsImages(data.access) + + if (response.status < 400) setBots(response.data) + } + + return { + bots, + fetchBots, + setBots + } +} @@ -0,0 +1 @@ +export * from './model.types' \ No newline at end of file @@ -0,0 +1,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,164 @@ +import axios, { AxiosResponse } from 'axios' +import { User } from 'next-auth' + +import { API_URL } from '#/shared/lib/constants' +import { IOffer } from '#/widgets/payment/model/payment' +import { AccountType, DataForLogin, PayProductRequest, PayProductResponse } from '../model/types' + +export async function getPaymentsPlans(token: string): Promise { + try { + const { data } = await axios.get(API_URL + '/payments/plans', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return data + } catch (err) { + return null + } +} + +export async function payProduct(token: string | null, plan: string): Promise { + if (token === null) { + return null + } + + try { + const { data } = await axios.post>( + API_URL + '/payments/plans', + { + uid: plan, + is_test: 1, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + return data.payment_url + } catch (err) { + return null + } +} + +export async function changePassword( + token: string | null, + password_1: string, + password_2: string, + current_password: string +): Promise { + try { + const { status } = await axios.put( + API_URL + '/auth/reset-pass', + { + password_1, + password_2, + current_password, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + return status + } catch (err) { + return 400 + } +} + +export async function getApiKeys(token: string | null): Promise { + try { + const { data } = await axios.get(API_URL + '/public/api-key', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return data + } catch (err) { + return null + } +} + +export async function createApiKeys(data: any, token: string | null): Promise { + try { + const { data: result } = await axios.post(API_URL + '/public/api-key', data, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return result + } catch (err) { + return null + } +} + +export async function deleteApiKey(name: any, token: string | null): Promise { + try { + const { data: result, status } = await axios.delete(API_URL + '/public/api-key', { + data: { + name, + }, + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return status + } catch (err) { + return null + } +} + +export async function loginByEmail(email: string, password: string): Promise { + try { + const { data } = await axios.post>(API_URL + '/auth/login', { + email, + password, + }) + + return data + } catch (err) { + return null + } +} + +export async function removeSub(token?: string) { + try { + const { status } = await axios.delete(API_URL + '/payments/plans', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + return status + } catch (err) { + return null + } +} + +export const getAccountType = async (token: string | null | undefined): Promise => { + if (!token) { + return 'regular' + } + + try { + const { data } = await axios.get>( + API_URL + '/auth/account-type', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + return data.status + } catch (err) { + return 'regular' + } +} @@ -1,25 +0,0 @@ -import axios, { AxiosResponse } from 'axios' - -import { API_URL } from '#/shared/lib/constants' - -import { AccountType } from '../model/types' -export const getAccountType = async (token: string | null | undefined): Promise => { - if (!token) { - return 'regular' - } - - try { - const { data } = await axios.get>( - API_URL + '/auth/account-type', - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return data.status - } catch (err) { - return 'regular' - } -} @@ -0,0 +1 @@ +export * from './account-endpoints' @@ -14,3 +14,21 @@ export interface IUserSetting { type: SettingType value: SettingValueType } + +export interface UserBalance { + current_token_balance: number +} + +export interface PayProductRequest { + uid: string + is_test: number +} + +export interface PayProductResponse { + payment_url: string +} + +export interface DataForLogin { + email: string + password: string +} @@ -1 +1,2 @@ export { getAllInfo, userSlice } from './model/user-type-slice' +export * from './api' @@ -7,12 +7,11 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '#/app/store/store' import { ButtonGray, ButtonUI, Error, InputStyleDark, InputStyleLight, Loader, Modal } from '#/shared' -import { accountApi } from '#/shared/api/account-endpoints' -import { API_URL } from '#/shared/lib/constants' import { useShowData } from '#/shared/lib/hooks' import { DateInput } from '#/shared/ui/date-input/date-input' import styles from '../invite-person-in-business/ui/invite-modal.module.scss' +import { createApiKeys, getApiKeys } from '#/entities/user-account' export const ApiKeyModal = ({ open, @@ -45,13 +44,10 @@ export const ApiKeyModal = ({ setIsLoading(true) const { result } = endDate && endDate !== '' - ? await accountApi.createApiKeys( - { name: title !== '' ? title : keyName, expires_at: endDate }, - data?.access - ) - : await accountApi.createApiKeys({ name: title !== '' ? title : keyName }, data?.access) + ? await createApiKeys({ name: title !== '' ? title : keyName, expires_at: endDate }, data?.access) + : await createApiKeys({ name: title !== '' ? title : keyName }, data?.access) if (result !== null) { - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setIsLoading(false) }) @@ -1,7 +1,7 @@ -import { accountApi } from '#/shared/api/account-endpoints' +import { loginByEmail } from "#/entities/user-account" export const authTelegram = async (email: any, password: any) => { - const user = await accountApi.loginByEmail(email, password) + const user = await loginByEmail(email, password) if (user !== null) { ;(window as any).Telegram.WebApp.sendData(user.token.access) @@ -9,7 +9,7 @@ export const authTelegram = async (email: any, password: any) => { } export const authTelegramYandex = async (email: any, password: any) => { - const user = await accountApi.loginByEmail(email, password) + const user = await loginByEmail(email, password) if (user !== null) { ;(window as any).Telegram.WebApp.sendData(user.token.access) @@ -0,0 +1 @@ +export * from './use-images-bot-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { useAppSelector } from '#/app/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 '#/shared' +import { MessageSend } from '#/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 '#/app/store/store' +import { Device } from '#/shared/lib/types/entities' +import { Message } from '#/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 @@ -0,0 +1,3 @@ + + +export * from './use-images-library' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface ImageWithState { + image: string + state: boolean +} \ No newline at end of file @@ -0,0 +1,53 @@ +import { useEffect, useMemo, useState } from 'react' +import { ImageWithState } from './types' + +export const useImagesLibrary = (images: string[], current: string | null, reverse: boolean) => { + const [imagesWithState, setImagesWithState] = useState(getImagesWithState(images)) + const [initialCount, setInitialCount] = useState(0) + + function getImagesWithState(images: string[]) { + const imagesWithState = images.map((image) => ({ + image, + state: false, + })) + + return imagesWithState + } + + function updateImageState(image: string, state: boolean) { + setImagesWithState((prev) => prev.map((item) => (item.image === image ? { ...item, state } : item))) + } + + const currentImageIndex = useMemo( + () => (!current ? undefined : imagesWithState.findIndex((image) => image.image === current)), + [imagesWithState, current] + ) + + useEffect(() => { + if (initialCount === images.length) { + return + } + + setInitialCount(images.length) + + const filteredMessages = images.filter((item) => + !imagesWithState.find((i) => i.image === item) ? true : false + ) + + if (filteredMessages.length === images.length) { + return setImagesWithState(getImagesWithState(filteredMessages)) + } + + if (reverse) setImagesWithState([...imagesWithState, ...getImagesWithState(filteredMessages)]) + else setImagesWithState([...getImagesWithState(filteredMessages), ...imagesWithState]) + }, [images]) + + return { + imagesWithState, + currentImageIndex, + initialCount, + setInitialCount, + setImagesWithState, + updateImageState, + } +} @@ -0,0 +1,53 @@ +import { debounce } from 'lodash' +import { useCallback, useEffect, useState } from 'react' +import { Swiper as SwiperCore } from 'swiper' +import { ImageWithState } from './types' + +export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefined, reverse: boolean) => { + const [swiper, setSwiper] = useState(null) + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + e.preventDefault() + slideNext() + } + if (e.key === 'ArrowLeft') { + e.preventDefault() + slidePrev() + } + } + + const slidePrev = function () { + const result = swiper?.slidePrev() + + if (!result && onSlideFalse && !reverse) { + onSlideFalse() + } + } + + const slideNext = function () { + const result = swiper?.slideNext() + + if (!result && onSlideFalse && reverse) { + onSlideFalse() + } + } + + useEffect(() => { + document.addEventListener('keydown', keydown, true) + return () => { + document.removeEventListener('keydown', keydown, true) + } + }, [swiper]) + + // useEffect(() => { + // swiper?.slideTo(currentImageIndex || 0) + // }, [currentImageIndex]) + + return { + swiper, + setSwiper, + slidePrev, + slideNext, + } +} @@ -1,27 +1,58 @@ -import React, { Dispatch, SetStateAction } from 'react' +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react' import Image from 'next/image' import styles from './modal-styles.module.scss' +import { ArrowDropDown } from '@mui/icons-material' +import { c, Loader } from '#/shared' +import { useImagesLibrary } from '../model' + +import { Swiper, SwiperSlide } from 'swiper/react' +import 'swiper/css' +import { Swiper as SwiperCore } from 'swiper' +import { useLibrarySwiper } from '../model/use-swiper' interface IProps { modal: boolean setModal: Dispatch> - image: string + current: string | null + setCurrent?: (value: string | null) => void + onSlideFalse?: (...args: any) => any + reverse?: boolean + images: string[] } -export default function FullScreenModal({ modal, setModal, image }: IProps) { - const isSvg = image.includes('.svg') + +export default function FullScreenModal({ modal, setModal, current, images, onSlideFalse, reverse = false }: IProps) { + const { imagesWithState, updateImageState, currentImageIndex, initialCount } = useImagesLibrary( + images, + current, + reverse + ) + + const { swiper, setSwiper, slideNext, slidePrev } = useLibrarySwiper(onSlideFalse, reverse) + + useEffect(() => { + if (!(initialCount < images.length && swiper)) return + + if (reverse) { + setTimeout(() => swiper.slideTo(initialCount, 1000), 1000) + return + } + + setTimeout(() => swiper.slideTo(images.length - initialCount - 1, 1000), 1000) + }, [images]) return (
setModal(false)} >
- {!isSvg && image ? ( - К сожалению, изображение не загрузилось - ) : ( - К сожалению, изображение не загрузилось + + {modal && ( + setSwiper(swiper)} + > + {imagesWithState.map(({ image, state }, index) => ( + + {!state && } + {!image.includes('.svg') ? ( + e.stopPropagation()} + onLoadingComplete={() => { + updateImageState(image, true) + }} + loading='lazy' + src={image} + width={'1500'} + height={'1500'} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + ) : ( + e.stopPropagation()} + onLoad={() => updateImageState(image, true)} + alt='К сожалению, изображение не загрузилось' + className={styles.image_style} + /> + )} + + ))} + )} +
) @@ -0,0 +1 @@ +export { default as ImageModal } from './full-screen-modal' @@ -0,0 +1,92 @@ +.close_block { + position: absolute; + z-index: 105; + cursor: pointer; + right: 25px; + top: 25px; +} + +.arrow { + position: absolute; + z-index: 1205; + cursor: pointer; + top: 50%; + + width: 50px; + height: 50px; + + background: transparent; + border: none; + outline: none; + + svg { + fill: white; + width: 50px; + height: 50px; + } + + &_left { + transform: translateY(-50%) rotate(90deg); + left: -40px; + @media screen and (max-width: 768px) { + left: -3px; + } + } + + &_right { + transform: translateY(-50%) rotate(-90deg); + right: -50px; + + @media screen and (max-width: 768px) { + right: -10px; + } + } +} + +.image_block { + position: absolute; + padding: 0 20px; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 105; + margin: auto; + width: fit-content; + height: fit-content; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.image_style { + width: auto; + max-width: 70vw; + height: 100%; + object-fit: contain; + + @media screen and (max-width: 1024px) { + width: auto; + max-width: 70vw; + height: auto; + } +} + +.slide { + width: 80vw !important; + display: flex !important; + align-items: center; + justify-content: center; +} + +.swiper { + position: relative; + width: 100%; + max-width: 80vw; + max-height: 1000px; + height: 90vh; +} @@ -0,0 +1 @@ +export * from './ui' @@ -1,30 +0,0 @@ -.close_block { - position: absolute; - z-index: 105; - cursor: pointer; - right: 25px; - top: 25px; -} - -.image_block { - position: absolute; - padding: 0 20px; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 105; - margin: auto; - width: fit-content; - height: fit-content; -} - -.image_style { - position: relative; - width: 100%; - height: 100%; - max-width: 960px; - max-height: 700px; - border-radius: 15px; - object-fit: contain; -} @@ -4,7 +4,14 @@ import Link from 'next/link' import styles from '#/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}
+
) } @@ -0,0 +1,8 @@ +interface ApiKeyDTO { + created_at: string // ISO 8601 format + name: string + key: string + expires_at: string | null + user: User + token_limit: string +} @@ -0,0 +1,37 @@ +interface UserDTO { + uid: string + first_name: string + last_name: string + username: string + created_at: string // ISO 8601 format + email: string + is_active: boolean + is_superuser: boolean + is_staff: boolean + is_confirmed: boolean + is_subscribed_to_emails: boolean + show_balance: boolean + profile_picture_link: string + account_type: string + token: { + access: string + refresh: string + } + payment_plan: { + uid: string + plan: { + uid: string + title: string + price: string + tokens_per_plan: string + duration: string + accessed_models: string[] + } + last_payment_at: string // ISO 8601 date + next_payment_at: string // ISO 8601 date + current_token_balance: number + } + referral_code: string | null + is_social: boolean + social_auth: any[] // Assuming it can hold any type of objects +} @@ -21,6 +21,8 @@ axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, }) +axios.defaults.validateStatus = (status) => status < 500 + const inter = Raleway({ subsets: ['latin'] }) export type NextPageWithLayout

= NextPage & { @@ -35,6 +35,7 @@ export default function Document() {

+ @@ -247,7 +247,7 @@ export function useModelImages(showError: (message: string) => void, type: st const { getData, sendData } = ModelsWithImagesEndpoints - const [messages, setMessages] = useState(null) + const [messages, setMessages] = useState([]) const [loading, setLoading] = useState(false) @@ -1,162 +0,0 @@ -import axios, { AxiosResponse } from 'axios' -import { User } from 'next-auth' - -import { API_URL } from '#/shared/lib/constants' -import { IOffer } from '#/widgets/payment/model/payment' - -interface IUserBalance { - current_token_balance: number -} - -interface IPayProductRequest { - uid: string - is_test: number -} - -interface IPayProductResponse { - payment_url: string -} - -interface DataForLogin { - email: string - password: string -} - -export const accountApi = { - async getPaymentsPlans(token: string): Promise { - try { - const { data } = await axios.get(API_URL + '/payments/plans', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } - }, - - async payProduct(token: string | null, plan: string): Promise { - if (token === null) { - return null - } - - try { - const { data } = await axios.post>( - API_URL + '/payments/plans', - { - uid: plan, - is_test: 1, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - return data.payment_url - } catch (err) { - return null - } - }, - - async changePassword( - token: string | null, - password_1: string, - password_2: string, - current_password: string - ): Promise { - try { - const { status } = await axios.put( - API_URL + '/auth/reset-pass', - { - password_1, - password_2, - current_password, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - return status - } catch (err) { - return 400 - } - }, - - async getApiKeys(token: string | null): Promise { - try { - const { data } = await axios.get(API_URL + '/public/api-key', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data - } catch (err) { - return null - } - }, - - async createApiKeys(data: any, token: string | null): Promise { - try { - const { data: result } = await axios.post(API_URL + '/public/api-key', data, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return result - } catch (err) { - return null - } - }, - - async deleteApiKey(name: any, token: string | null): Promise { - try { - const { data: result, status } = await axios.delete(API_URL + '/public/api-key', { - data: { - name, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return status - } catch (err) { - return null - } - }, - - async loginByEmail(email: string, password: string): Promise { - try { - const { data } = await axios.post>(API_URL + '/auth/login', { - email, - password, - }) - - return data - } catch (err) { - return null - } - }, - - async removeSub(token?: string) { - try { - const { status } = await axios.delete(API_URL + '/payments/plans', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return status - } catch (err) { - return null - } - }, -} @@ -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' @@ -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,5 +1,7 @@ export { getAccessToken } from './get-token' export { getTypeDevice } from './get-type-device' export { useConcat } from './reactive-concat' +export { c } from './string' +export * from './date-helper' export * from './get-type-device' export * from './get-random-image' @@ -0,0 +1,3 @@ +export const c = (...classes: Array) => { + return classes.filter(Boolean).join(' ') +} @@ -1,13 +1,11 @@ +import { DeviceOs } from '../types/entities' import { useAppSelector } from '#/app/store/store' -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,4 +1,5 @@ export { emailOptions, onlyNumbersOption } from './lib/constants/hook-form-options' +export { useConcat } from './lib/helpers' export { translateTypeModel } from './lib/helpers/model-helpers' export { useShowData } from './lib/hooks' export { useAutoLoad } from './lib/hooks/use-auto-load' @@ -8,14 +9,15 @@ export { ButtonUI } from './ui/button/button' export { ButtonGray } from './ui/button/button-gray' export { CheckBoxAgreeWithRules } from './ui/check-box-agree-with-rules' export { DalleFilterMenu } from './ui/dalle-filter-menu' +export * from './client-only' export * from './ui/drawer' export { Error } from './ui/error' +export * from './lib/helpers' export * from './ui/graphics-main-page' export { Input, InputStyleDark, InputStyleLight } from './ui/input' export { InputImagesModels } from './ui/input-images-models/input-images-models' export { Loader } from './ui/loader/loader' export type { ModalProps } from './ui/modal/modal' -export { useConcat } from './lib/helpers' export { Modal } from './ui/modal/modal' export { NotificationMenu } from './ui/notification-menu' export { RandomImage } from './ui/random-image' @@ -13,7 +13,6 @@ import { useAppDispatch, useAppSelector } from '#/app/store/store' import styles2 from '#/app/styles/accountTabs.module.css' import styles from '#/app/styles/business.module.scss' import { ButtonUI, Input, Loader, Modal, Success, SwitchCustom, useShowData } from '#/shared' -import { accountApi } from '#/shared/api/account-endpoints' import { API_URL } from '#/shared/lib/constants/constants' import { getDeviceType } from '#/shared/lib/helpers' import { ScreenForInactive } from '#/widgets/business' @@ -24,6 +23,7 @@ import { Referral } from '#/widgets/referral' import { NextPageWithLayout } from '#/pages/_app' import { DownloadModal } from '#/features/business-security-download' import { scopes } from '../config' +import { changePassword } from '#/entities/user-account' const Account: NextPageWithLayout = () => { @@ -124,13 +124,13 @@ const Account: NextPageWithLayout = () => { if (Object.keys(query).length === 0) addQueryParams('setting') }, []) - const changePassword = async () => { + const onChangePassword = async () => { if (!data) return if (newPassword1 !== newPassword2) { showError('Укажите одинаковые новы пароли!', true) return } - const status = await accountApi.changePassword(data.access, newPassword1, newPassword2, currentPassword) + const status = await changePassword(data.access, newPassword1, newPassword2, currentPassword) if (status === 200) { showError('Пароль успешно изменён!') @@ -510,7 +510,7 @@ const Account: NextPageWithLayout = () => { { const [keys, setKeys] = useState | null>(null) @@ -33,7 +33,7 @@ const ApiKeys: NextPageWithLayout = () => { useEffect(() => { if (status === 'authenticated') { setLoading(true) - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setLoading(false) }) @@ -59,9 +59,9 @@ const ApiKeys: NextPageWithLayout = () => { } setLoading(true) - const result = await accountApi.deleteApiKey(name, data?.access) + const result = await deleteApiKey(name, data?.access) if (result !== null && result === 200) { - accountApi.getApiKeys(data?.access).then((res) => { + getApiKeys(data?.access).then((res) => { setKeys(res) setLoading(false) }) @@ -188,7 +188,7 @@ const Page: NextPageWithLayout = () => { {botParams ? botParams.title : 'Загрузка...'} - + <Title title={botParams ? botParams.title : 'Загрузка...'} type={'Чат-боты'} linkBack={'/chat-bot'} /> <Box className={styles.main}> <Box className={styles.chatWindow}> <AllChatWindow @@ -21,10 +21,10 @@ import { useModelImages } from '#/shared/api/models/endpoints' import { IModel } from '#/shared/api/models/models' import { useShowData } from '#/shared/lib/hooks' import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' -import { ImageMessagesList } from '#/widgets/messages/image-messages-list' import { getDeviceType, getOs } from '#/shared/lib/helpers' import Head from 'next/head' import { NextPageWithLayout } from '#/pages/_app' +import { ImageMessagesList } from '#/widgets/messages' const ImageModelPage: NextPageWithLayout = () => { const [image, setImage] = React.useState<File | null>(null) @@ -256,7 +256,7 @@ const ImageModelPage: NextPageWithLayout = () => { <Head> <title>{botParams ? botParams.title : 'Загрузка...'} - + <Title title={botParams ? botParams.title : 'Загрузка...'} type={'Изображения'} linkBack={'/images'} /> <Box display={'flex'} justifyContent='space-between' @@ -7,11 +7,11 @@ import styles from '#/app/styles/styles-pages/index-styles.module.scss' import { ReferralBlock } from '#/shared' import { getCurrentTimeAndGreeting } from '#/shared/lib/helpers/getCurrentDateForMainScreen' -import { DemoInfo } from '#//shared/ui/demo-info' -import { SocialMedia } from '#//shared/ui/social-media-block' -import { Statistics } from '#//widgets/stats/stats' -import { NextPageWithLayout } from '#//pages/_app' -import { getDeviceType } from '#//shared/lib/helpers/get-type-device' +import { DemoInfo } from '#/shared/ui/demo-info' +import { SocialMedia } from '#/shared/ui/social-media-block' +import { Statistics } from '#/widgets/stats/stats' +import { NextPageWithLayout } from '#/pages/_app' +import { getDeviceType } from '#/shared/lib/helpers/get-type-device' export interface MainPageProps {} @@ -0,0 +1 @@ +export * from './use-image-models-popup' \ No newline at end of file @@ -0,0 +1,27 @@ +import { useImagesBots } from '#/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,54 @@ +import { Typography } from '@mui/material' +import React, { useEffect } from 'react' +import styles from './image-models-popup.module.scss' +import { c } from '#/shared/lib/helpers' +import { useImageModelsPopup } from '../model' +import { ImagePopupModel } from './image-popup-model' +import { IModel } from '#/entities/model-entity' +import { useRouter } from 'next/router' +import ArrowDownSvg from '#/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,85 @@ +import { IShortModel } from '#/entities/model-entity' +import React, { useMemo } from 'react' + +import styles from './image-popup-model.module.scss' +import { c, formatDate } from '#/shared/lib/helpers' +import SuccessRoundedSvg from '#/assets/svg/success-rounded.svg?react' +import ClockSvg from '#/assets/svg/clock.svg?react' +import LightningSvg from '#/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,12 @@ +import { Message, MessageSend } from '#/entities/message' +import { API_URL } from '#/shared/lib/constants' +import { IMessageRequest } from '#/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 '#/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 @@ -3,11 +3,12 @@ import { Box, Menu, MenuItem, Skeleton, Slide, Typography } from '@mui/material' import Stack from '@mui/material/Stack' import Image from 'next/image' -import FullScreenModal from '#/features/image-modal/full-screen-modal' +import { ImageModal } from '#/features/image-modal' import { useAppSelector } from '#/app/store/store' import { TooltipCustom } from '#/shared' import { Message } from '#/shared/lib/types/model' -import { BotMessage } from '#/widgets/messages/message-components/bot-message' +import { BotMessage } from '../ui' + interface IMessagesList { // messageResponse: Message[] | null @@ -64,9 +65,9 @@ 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 && ( + <ImageModal 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> @@ -0,0 +1 @@ +export * from './use-images-pagination' \ No newline at end of file @@ -0,0 +1,117 @@ +import { getUserBalance } from '#/entities/balance' +import { Message, MessageSend, sendImage } from '#/entities/message' +import { useAppDispatch } from '#/app/store/store' +import { useSession } from 'next-auth/react' +import { useState, useRef, useEffect, useCallback } from 'react' +import { getImagesGalery } from '../api' +import { Device } from '#/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 } +} @@ -0,0 +1,26 @@ +import { Message } from '#/shared/lib/types/model' +import { useMemo, useState } from 'react' + +export const useMessages = (messages: Message[]) => { + const [chosenImage, setChosenImage] = useState<string | null>(null) + + const [loaded, setLoaded] = useState(false) + + // типизация ну супер кривая))) + const computedLibraryImages = useMemo<string[]>(() => { + return messages + .map((el) => { + if (el.file && (el.file as any).includes('.zip')) return null + return el.file as string + }) + .filter((el) => el !== null) as string[] + }, [messages]) + + return { + chosenImage, + setChosenImage, + loaded, + setLoaded, + computedLibraryImages, + } +} @@ -0,0 +1,212 @@ +import { Box, CircularProgress } from '@mui/material' +import React, { memo, useEffect, useMemo, useRef, useState } from 'react' +import { useSession } from 'next-auth/react' + +import { Message } from '#/shared/lib/types/model' +import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' +import { ImageModal } from '#/features/image-modal' +import { ClientOnly } from '#/shared' +import { createPortal } from 'react-dom' +import { IsNextDay } from './is-next-day' +import { PreviewView } from './preview-view' + +interface IMessagesList { + device: 'mobile' | 'desktop' + modelType: string + messageResponse: Message[] | null + mode?: string + sendMessage?: (title: string) => void + getMessagesPagination?: () => Promise<void> + deleteMessage?: (message_uid: string) => void + modelTitle: string | undefined + setResendValue: (value: string) => void + onLoadImage?: (event: React.ChangeEvent<HTMLInputElement> | null, file?: File) => void + loading: boolean +} + +export const ChatMessagesList: React.FC<IMessagesList> = memo( + ({ + messageResponse, + onLoadImage, + setResendValue, + modelTitle, + device, + modelType, + mode, + getMessagesPagination, + deleteMessage, + loading, + }) => { + const paginationScroll = React.useRef<any>() + const [isPaginating, setIsPaginating] = React.useState(false) + const [chatScrollHeight, setChatScrollHeight] = React.useState(0) + const [scrollBottom, setScrollBottom] = React.useState(0) + + const desktop = device === 'desktop' + const { status } = useSession() + const [modal, setModal] = useState<boolean>(false) + const [isNewMessage, setIsNewMessage] = useState(false) + + const [currentSrc, setCurrentSrc] = useState<string | null>(null) + + const onlyImageMessage = useMemo(() => { + if (!messageResponse) return [] + console.log(messageResponse) + return messageResponse + .map((item) => item.file) + .filter((item) => item !== null && !(item as string).includes('.zip')) as string[] + }, [messageResponse]) + + React.useEffect(() => { + const block = paginationScroll.current + + if (messageResponse != undefined && !isPaginating) { + setChatScrollHeight(paginationScroll.current.scrollHeight) + + const time = setTimeout(() => { + if (block) { + //@ts-ignore + block.scrollTo({ + top: block.scrollHeight, + behavior: 'smooth', // добавляем плавную прокрутку + }) + } + }, 250) + return () => clearTimeout(time) + } else if (messageResponse != undefined && isPaginating) { + if (block) { + //@ts-ignore + block.scrollTop = block.scrollHeight - chatScrollHeight + setChatScrollHeight(paginationScroll.current.scrollHeight) + } + } + setIsPaginating(false) + }, [messageResponse]) + + useEffect(() => { + setIsNewMessage(true) + }, [messageResponse]) + + const handleScroll = () => { + setScrollBottom( + paginationScroll.current?.scrollHeight - + paginationScroll.current?.scrollTop - + paginationScroll.current?.clientHeight + ) + + if (paginationScroll.current && messageResponse?.length !== 0) { + const { scrollTop, scrollHeight, clientHeight } = paginationScroll.current + if (scrollTop === 0) { + if (getMessagesPagination) { + setIsPaginating(true) + getMessagesPagination() + } + } + } + } + + return ( + <> + <ClientOnly> + {createPortal( + <ImageModal + modal={modal} + current={currentSrc} + setModal={setModal} + onSlideFalse={async () => { + getMessagesPagination && (await getMessagesPagination()) + }} + images={onlyImageMessage} + />, + document.getElementById('modal-container')! + )} + </ClientOnly> + <Box + sx={{ + overflowY: 'scroll', + overflowX: 'hidden', + '::-webkit-scrollbar': { + display: 'none', + }, + paddingRight: desktop ? 2 : 0, + paddingLeft: desktop ? 1 : 0, + }} + ref={paginationScroll} + onScroll={handleScroll} + > + {scrollBottom > 500 && ( + <Box + sx={{ + position: 'absolute', + left: 0, + right: 0, + width: 'fit-content', + cursor: 'pointer', + margin: '0 auto', + bottom: '100px', + zIndex: 10, + }} + onClick={() => { + //@ts-ignore + const block = paginationScroll.current + + block.scrollTo({ + top: block.scrollHeight, + behavior: 'smooth', // добавляем плавную прокрутку + }) + }} + > + <ArrowDownScroll /> + </Box> + )} + + {loading && ( + <Box + sx={{ + position: 'absolute', + left: 0, + right: 0, + width: 'fit-content', + margin: '0 auto', + top: '10px', + zIndex: 10, + }} + > + <CircularProgress + size={18} + thickness={3} + sx={{ + color: '#7F7DF3', + }} + /> + </Box> + )} + + {messageResponse?.length === 0 && status === 'authenticated' ? ( + <>{desktop && modelType !== 'deepl' && <PreviewView setValue={setResendValue} />}</> + ) : ( + messageResponse?.map((message, idx) => { + return ( + <IsNextDay + onLoadImage={onLoadImage} + setResendValue={setResendValue} + key={idx} + message={message} + index={idx - 1} + messageResponse={messageResponse} + isNewMessage={isNewMessage} + modelType={modelType} + modelTitle={modelTitle} + deleteMessage={deleteMessage} + device={device} + /> + ) + }) + )} + </Box> + </> + ) + } +) + +ChatMessagesList.displayName = 'ChatMessagesList' @@ -4,27 +4,30 @@ import Box from '@mui/material/Box' import Image from 'next/image' import Link from 'next/link' -import FullScreenModal from '#/features/image-modal/full-screen-modal' import { useAppSelector } from '#/app/store/store' -import { Success, TooltipCustom, useShowData } from '#/shared' +import { ClientOnly, Success, TooltipCustom, useShowData } from '#/shared' import { useAutoScroll } from '#/shared/lib/hooks' import { Message } from '#/shared/lib/types/model' import styles from './image-messages-list.module.scss' +import { createPortal } from 'react-dom' +import { useMessages } from '../model/use-messages' +import { ImageModal } from '#/features/image-modal' interface MessagesList { device: 'mobile' | 'desktop' - images: Message[] | null + images: Message[] + getMessagesPagination?: () => Promise<void> isComplete: boolean } -export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images }) => { +export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images, getMessagesPagination }) => { const { error, showError, isError } = useShowData() const [modal, setModal] = useState<boolean>(false) - const [chosenImage, setChosenImage] = useState<string>('') const theme = useAppSelector((state) => state.theme.theme) const [iconsMenu, setIconsMenu] = React.useState<string>('') - const [loaded, setLoaded] = useState(false) + + const { chosenImage, setChosenImage, loaded, setLoaded, computedLibraryImages } = useMessages(images) const downloadFile = (url: string | null, content: string | undefined) => { if (url) { @@ -56,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 @@ -74,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' @@ -146,7 +162,19 @@ export const ImageMessagesList: React.FC<MessagesList> = memo(({ device, images return ( <> - <FullScreenModal modal={modal} setModal={setModal} image={chosenImage} /> + <ClientOnly> + {createPortal( + <ImageModal + images={computedLibraryImages} + modal={modal} + onSlideFalse={() => getMessagesPagination && getMessagesPagination()} + setModal={setModal} + reverse={device === 'desktop'} + current={chosenImage} + />, + document.getElementById('modal-container')! + )} + </ClientOnly> <Success message={error} open={Boolean(error)} isError={isError} /> <Box className={'mt-15'}> <Typography @@ -188,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> @@ -204,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', @@ -216,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} @@ -242,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, @@ -269,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) || @@ -292,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, @@ -336,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 @@ -360,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 @@ -1,10 +1,9 @@ -import React, { memo } from 'react' +import React, { memo, useMemo } from 'react' import { Box, Typography } from '@mui/material' import { Message } from '#/shared/lib/types/model' -import { getDateFromString } from '#/widgets/messages/lib/getDateFromString' -import { getDayMontsString } from '#/widgets/messages/lib/getDayMontsString' import { UserMessage } from '#/widgets/messages/message-components/user-message' +import { getDateFromString, getDayMontsString } from '../lib' interface IProps { message: Message @@ -13,7 +12,6 @@ interface IProps { modelTitle: string | undefined deleteMessage?: (message_uid: string) => void isNewMessage: boolean - // clickPreview: (text: string) => void modelType: string device: 'mobile' | 'desktop' setResendValue: (value: string) => void @@ -0,0 +1,534 @@ +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 { ImageModal } from '#/features/image-modal' +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom } from '#/shared' +import { Message } from '#/shared/lib/types/model' +import { BotMessage } from '#/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> + </> + ) +}) @@ -4,8 +4,7 @@ import { useSession } from 'next-auth/react' import { Message } from '#/shared/lib/types/model' import { ArrowDownScroll } from '#/shared/ui/icon-components/scroll-down-arrow' -import { IsNextDay } from '#/widgets/messages/is-next-day' -import { PreviewView } from '#/widgets/messages/message-components/preview-view' +import { PreviewView, IsNextDay } from './ui' interface IMessagesList { device: 'mobile' | 'desktop' @@ -0,0 +1,4 @@ +export * from './ui' +export * from './lib' +export * from './api' +export * from './model' \ No newline at end of file @@ -1,7 +1,8 @@ -import { NavigationSearchModelLink } from './types' import { API_URL } from '#/shared/lib/constants' import axios from 'axios' +import { NavigationSearchModelLink } from './types' + export const getModelChatLinks = async (token: string) => { return await axios.get<NavigationSearchModelLink[]>(API_URL + '/api/chats/links', { headers: { Authorization: `Bearer ${token}` }, @@ -1,9 +1,10 @@ import { useEffect, useId, useMemo, useState } from 'react' -import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' -import { getModelChatLinks, getModelMediaLinks } from '../api/api-get-model-links' import { uniqueId } from 'lodash' import { useSession } from 'next-auth/react' +import { getModelChatLinks, getModelMediaLinks } from '../api/api-get-model-links' +import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' + export const useNavigationSearchChatLinks = () => { const [chatLinks, setChatLinks] = useState<NavigationSearchModelLink[]>([]) @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState } from 'react' -import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' -import { getModelMediaLinks } from '../api/api-get-model-links' import { uniqueId } from 'lodash' import { useSession } from 'next-auth/react' +import { getModelMediaLinks } from '../api/api-get-model-links' +import { NavigationSearchLink, NavigationSearchModelLink } from '../api/types' + export const useNavigationSearchMediaLinks = () => { const [mediaLinks, setMediaLinks] = useState<NavigationSearchModelLink[]>([]) @@ -1,28 +1,22 @@ -import { Box, Typography } from '@mui/material' import React from 'react' +import { Box, Typography } from '@mui/material' import styles from './key-for-search.module.scss' -interface KeyForSearchProps { - userAgent: string | null -} - -export const KeyForSearch = ({ userAgent }: KeyForSearchProps) => { - if (userAgent === null) { - return null - } - +export const KeyForSearch = () => { const checkType = () => { - if (userAgent.includes('Windows')) { - return 'Ctrl + F' + if (navigator.userAgent && !navigator.userAgent.includes('Windows')) { + return '⌘+F' } else { - return '⌥+F' + return 'Ctrl + F' } } return ( <Box className={styles.ctrl}> - <Typography sx={{ fontSize: '13px', color: '#A4AAB5', fontWeight: 500 }}>{checkType()}</Typography> + <Typography sx={{ fontSize: '13px', color: '#A4AAB5', fontWeight: 500 }}> + {checkType()} + </Typography> </Box> ) } @@ -7,9 +7,12 @@ import { staticLinks } from '../config' import { InputStyleDark, InputStyleLight, useConcat } from '#/shared' import { useAppSelector } from '#/app/store/store' import { useRouter } from 'next/router' + + import { NavigationSearchLink } from '../api/types' import { useNavigationSearchMediaLinks } from '../model/use-media-links' + interface SearchProps { device: string } @@ -25,7 +28,11 @@ export const Search = ({ device }: SearchProps) => { const { fetchMediaLinks, visibleMediaLinks } = useNavigationSearchMediaLinks() - const links = useConcat<NavigationSearchLink[]>(visibleChatLinks, visibleMediaLinks, staticLinks) + const links = useConcat<NavigationSearchLink[]>( + visibleChatLinks, + visibleMediaLinks, + staticLinks + ) useEffect(() => { console.log(links) @@ -132,9 +139,7 @@ export const Search = ({ device }: SearchProps) => { }} /> ), - endAdornment: ( - <KeyForSearch userAgent={desktop ? window.navigator.userAgent : null} /> - ), + endAdornment: <KeyForSearch />, }} /> )} @@ -1,2 +1,2 @@ -export * from './ui' export * from './config' +export * from './ui' @@ -6,11 +6,11 @@ import { useSession } from 'next-auth/react' import { useAppSelector } from '#/app/store/store' import { CheckBoxAgreeWithRules, Loader, Select } from '#/shared' -import { accountApi } from '#/shared/api/account-endpoints' import { getPaymentsHistory, PaymentHistory } from '#/shared/api/endpoints' import Offer from '#/widgets/payment/ui/offer' import styles from '../ui/payment.module.scss' +import { getPaymentsPlans, payProduct } from '#/entities/user-account' export interface IOffer { uid: string @@ -56,11 +56,11 @@ export const Payment: React.FC<IPaymentsProps & any> = ({ device, changeClose }) }, [data?.access]) useEffect(() => { - if (data?.access) accountApi.getPaymentsPlans(data?.access).then((res) => setOffers(res)) + if (data?.access) getPaymentsPlans(data?.access).then((res) => setOffers(res)) }, [data?.access]) const pay = async (uid: string) => { - const urlForPay = await accountApi.payProduct(data!.access, uid) + const urlForPay = await payProduct(data!.access, uid) urlForPay ? await Router.push(urlForPay) : null } @@ -46,7 +46,7 @@ export const menuListMiddle = [ }, { title: 'Изображения', - link: '/images', + link: '/images/flux', icon: '/svg/side-menu/image', activeList: ['images'], }, @@ -88,22 +88,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) @@ -353,7 +361,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)', }, }, }} @@ -381,7 +390,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', }} > @@ -0,0 +1,139 @@ +import React, { memo } from 'react' +import MenuIcon from '@mui/icons-material/Menu' +import { Box, Button, Stack, Typography } from '@mui/material' +import dynamic from 'next/dynamic' +import Link from 'next/link' +import Router, { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' + +import { useAppSelector } from '#/app/store/store' +import { Balance } from '#/shared' +import { NavigateTopBar } from '#/widgets/top-bar-model/ui/navigate-top-bar' +import { NavigateTopBarMobile } from '#/widgets/top-bar-model/ui/navigate-top-bar-mobile' + +interface IModelTopBar { + device: 'mobile' | 'desktop' + favorites: any[] | [] + token: string | null + title: string +} + +export const ModelTopBar: React.FC<IModelTopBar> = memo(({ device, token, favorites, title }) => { + const theme = useAppSelector((state) => state.theme.theme) + + const [anchorElModel, setAnchorElModel] = React.useState<HTMLElement | null>(null) + + const desktop = device === 'desktop' + + const { status } = useSession() + + const { pathname } = useRouter() + + const balance = useAppSelector((state) => state.balance.balance) + + const closeMenuModel = () => setAnchorElModel(null) + const openMenuModel = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorElModel(event.currentTarget) + } + const goToPayment = () => { + Router.push('/account') + } + + const LazyTutorial = dynamic(() => import('#/features/tutorial-gpt').then((component) => component.Tutorial)) + + const LazyArrowIcon = dynamic(() => import('./arrow-up-or-down')) + + return ( + <Stack + direction='row' + justifyContent='space-between' + alignItems='center' + sx={{ + backgroundColor: desktop + ? theme === 'light' + ? '#F8F8F8' + : '#4B4B4B' + : theme === 'light' + ? '#F8F8F8' + : 'transparent', + borderRadius: desktop ? '15px' : '15px 15px', + marginTop: desktop ? 1 : 0, + marginBottom: desktop ? 2 : 1, + padding: desktop ? '8px 5px' : '4px 0', + boxShadow: desktop + ? 'none' + : 'rgba(17, 17, 26, 0.05) 0px 4px 16px, rgba(17, 17, 26, 0.05) 0px 8px 32px;', + }} + > + <Stack direction='row' alignItems='center'> + {desktop && ( + <Button onClick={openMenuModel}>{desktop && <MenuIcon sx={{ color: '#7F7DF3' }} />}</Button> + )} + <Stack onClick={openMenuModel as any} direction='row' alignItems='center'> + <Typography + onClick={openMenuModel} + variant='body2' + sx={{ + color: '#7F7DF3', + lineHeight: '26.6px', + fontSize: '19px', + fontWeight: '600px', + marginLeft: desktop ? 0 : 2, + }} + > + {title} + </Typography> + {!desktop && <LazyArrowIcon directionY={!!anchorElModel} />} + </Stack> + + {desktop ? ( + <NavigateTopBar favorites={favorites} closeMenu={closeMenuModel} anchorEl={anchorElModel} /> + ) : ( + <NavigateTopBarMobile + anchorEl={anchorElModel} + closeMenu={closeMenuModel} + favorites={favorites} + /> + )} + </Stack> + + <Box display='flex' alignItems='center'> + {desktop && status === 'authenticated' && pathname === '/chatgpt' && <LazyTutorial />} + <Stack + direction='row' + className='tutorial-balance' + sx={{ + backgroundColor: theme === 'light' ? (desktop ? 'white' : 'transparent') : '#373737', + marginRight: 1.2, + padding: desktop ? '8px 15px' : '8px 0px', + borderRadius: '10px', + marginLeft: 3, + }} + > + {token && ( + <Link href={'/account'}> + <Balance balance={balance} /> + </Link> + )} + {desktop && ( + <Typography + variant='body2' + sx={{ + color: '#7F7DF3', + lineHeight: '21px', + fontSize: '15px', + marginLeft: token ? 3 : 0, + '&:hover': { cursor: 'pointer' }, + }} + onClick={goToPayment} + > + Купить токены + </Typography> + )} + </Stack> + </Box> + </Stack> + ) +}) + +ModelTopBar.displayName = 'ModelTopBar' @@ -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; +} @@ -1,17 +1,26 @@ services: - app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - build: - context: . - dockerfile: Dockerfile - container_name: frontend - restart: unless-stopped - ports: - - '3000:3000' - env_file: - - .env.production + frontend: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + container_name: frontend + restart: unless-stopped + networks: + - infrastructure + - ui + expose: + - "3000" + labels: + - "traefik.enable=true" + - "traefik.docker.network=infrastructure" + - "traefik.http.routers.frontend.rule=Host(`$UI_DOMAIN`)" + - "traefik.http.routers.frontend.entrypoints=web" + env_file: + - .env.production networks: - default: - name: 'air' - external: true + infrastructure: + external: true + ui: + name: ui @@ -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, @@ -35,13 +35,15 @@ "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^4.5.8", + "@lottiefiles/dotlottie-react": "^0.12.0", "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@mui/styled-engine-sc": "^5.11.11", "@mui/x-date-pickers": "^6.6.0", "@next/bundle-analyzer": "^13.4.3", + "@react-input/mask": "^2.0.4", "@reduxjs/toolkit": "^1.9.5", - "@sentry/nextjs": "^7.59.2", + "@sentry/nextjs": "^8.42.0", "@types/cookie": "^0.5.1", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", @@ -51,11 +53,14 @@ "@types/react-syntax-highlighter": "^15.5.7", "axios": "^0.24.0", "base64-encode-file": "^1.0.7", + "buffer": "^6.0.3", "chart.js": "^4.4.0", "cookie": "^0.5.0", "cross-env": "^7.0.3", "dayjs": "^1.11.8", "draft-js": "^0.11.7", + "draft-js-import-html": "^1.4.1", + "draftjs-to-html": "^0.9.1", "eslint": "^8.7.0", "eslint-config-next": "13.2.4", "eslint-plugin-prettier": "^4.2.1", @@ -71,6 +76,7 @@ "prettier": "^2.8.8", "react": "18.2.0", "react-chartjs-2": "^5.2.0", + "react-color": "^2.19.3", "react-cookie": "^4.1.1", "react-dom": "18.2.0", "react-draft-wysiwyg": "^1.15.0", @@ -82,12 +88,17 @@ "remark-gfm": "^3.0.1", "sass": "^1.63.4", "styled-components": "^5.3.9", + "swiper": "^11.2.1", "typescript": "5.1.3" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", + "@types/draftjs-to-html": "^0.8.4", "@types/intro.js": "^5.1.1", "@types/lodash": "^4.14.195", + "@types/react-color": "^3.0.13", "@types/react-draft-wysiwyg": "^1.13.8", + "@types/react-lottie": "^1.2.10", "@types/react-syntax-highlighter": "^15.5.7", "eslint-config-prettier": "^8.8.0", "lint-staged": "^13.2.2",