@@ -0,0 +1,3 @@ +{ + "postman.settings.dotenv-detection-notification-visibility": false +} @@ -0,0 +1,19 @@ +import fetch from 'cross-fetch' + +export function fetchToCrossfetch() { + global.fetch = (...params: Parameters) => { + let url = params[0] + + const baseUrl = 'http://localhost:3000/' + + if ( + typeof url === 'string' && + !(url as string).startsWith(baseUrl) && + !(url as string).includes('http') + ) { + url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) + params[0] : baseUrl + params[0] + } + + return fetch(url, params[1]) + } +} Binary files /dev/null and b/public/pages/image-model/1.jpg differ Binary files /dev/null and b/public/pages/image-model/10.jpg differ Binary files /dev/null and b/public/pages/image-model/11.jpg differ Binary files /dev/null and b/public/pages/image-model/12.jpg differ Binary files /dev/null and b/public/pages/image-model/13.jpg differ Binary files /dev/null and b/public/pages/image-model/14.jpg differ Binary files /dev/null and b/public/pages/image-model/15.jpg differ Binary files /dev/null and b/public/pages/image-model/16.jpg differ Binary files /dev/null and b/public/pages/image-model/17.jpg differ Binary files /dev/null and b/public/pages/image-model/18.jpg differ Binary files /dev/null and b/public/pages/image-model/19.jpg differ Binary files /dev/null and b/public/pages/image-model/2.jpg differ Binary files /dev/null and b/public/pages/image-model/20.jpg differ Binary files /dev/null and b/public/pages/image-model/21.jpg differ Binary files /dev/null and b/public/pages/image-model/22.jpg differ Binary files /dev/null and b/public/pages/image-model/23.jpg differ Binary files /dev/null and b/public/pages/image-model/24.jpg differ Binary files /dev/null and b/public/pages/image-model/25.jpg differ Binary files /dev/null and b/public/pages/image-model/26.jpg differ Binary files /dev/null and b/public/pages/image-model/27.jpg differ Binary files /dev/null and b/public/pages/image-model/3.jpg differ Binary files /dev/null and b/public/pages/image-model/4.jpg differ Binary files /dev/null and b/public/pages/image-model/5.jpg differ Binary files /dev/null and b/public/pages/image-model/6.jpg differ Binary files /dev/null and b/public/pages/image-model/7.jpg differ Binary files /dev/null and b/public/pages/image-model/8.jpg differ Binary files /dev/null and b/public/pages/image-model/9.jpg differ Binary files /dev/null and b/public/pages/main/capibara.png differ Binary files /dev/null and b/public/pages/main/chat-bots.png differ Binary files /dev/null and b/public/pages/main/copywriting.png differ Binary files /dev/null and b/public/pages/main/flux.png differ Binary files /dev/null and b/public/pages/main/lion.png differ Binary files /dev/null and b/public/pages/main/woman.png differ Binary files /dev/null and b/public/pages/upscale/woman-result.jpg differ Binary files /dev/null and b/public/pages/upscale/woman.jpg differ @@ -1,6 +1,10 @@ - - + + \ No newline at end of file @@ -1,3 +1,5 @@ - - + + \ No newline at end of file @@ -1,67 +0,0 @@ -import React from 'react' -import { FormControlLabel, Typography } from '@mui/material' -import Switch from '@mui/material/Switch' - -import { setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch } from '#/app/store/store' -import TooltipModelTypes from '#/widgets/filters-gpt/ui/tooltip-model-types' - -interface IProps { - name: string - value: boolean - filters: any - item_key: string - description: string - setNewParam: (payload: { [p: string]: string | number | number[] | boolean }) => void -} - -export const CheckboxFilter = ({ name, value, filters, item_key, description, setNewParam }: IProps) => { - const [check, setCheck] = React.useState(value || false) - - React.useEffect(() => { - if (filters[item_key] !== undefined && filters[item_key] !== check) { - setCheck(filters[item_key]) - } - }, [filters]) - - return ( - - { - setCheck(checked) - setNewParam({ [item_key]: checked }) - }} - /> - } - labelPlacement={'end'} - label={ - - {name} - - } - /> - - ) -} @@ -1,64 +0,0 @@ -import React from 'react' -import { Typography } from '@mui/material' -import Box from '@mui/material/Box' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import TooltipModelTypes from '#/widgets/filters-gpt/ui/tooltip-model-types' -import { CommonTextArea } from '#/shared/ui/common-textarea' - -interface IProps { - title: string - item_key: string - values: { - default?: string - } - filters: any - description: string - setNewParam: (payload: { [p: string]: string | number | number[] | boolean }) => void -} - -export const InputFilter = ({ filters, item_key, values, title, description, setNewParam }: IProps) => { - const { theme } = useThemeAndDevice() - const [value, setValue] = React.useState(values?.default || '') - - React.useEffect(() => { - if (filters[item_key] !== value) { - if (filters[item_key] == undefined) { - setValue('') - } else { - setValue(filters[item_key]) - } - } - }, [filters]) - - return ( - - - - - {title} - - { - e.target.style.height = 'auto' - e.target.style.height = Math.min(e.target.scrollHeight, 177) + 'px' - - setNewParam({ [item_key]: e.target.value }) - setValue(e.target.value) - }} - /> - - - - ) -} @@ -9,7 +9,12 @@ interface IProps { export const ResetFilters = ({ reset, desktop, closeDrawer }: IProps) => { return ( - + > - list: IModelVersions[] + setValue: (value: string) => void + list: ModelVersions[] [x: string]: any setDefaultParams: () => void } -export const ChatSelect: React.FC = ({ value, setValue, list, setDefaultParams, ...args }) => { +export const ChatSelect: React.FC = ({ + value, + setValue, + list, + setDefaultParams, + ...args +}) => { const theme = useAppSelector((state) => state.theme.theme) const onChange = (event: SelectChangeEvent) => { @@ -34,7 +40,8 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul MenuListProps: { sx: { color: theme === 'light' ? '#373737' : '#A6A5A5', - backgroundColor: theme === 'light' ? 'transparent' : '#151518', + backgroundColor: + theme === 'light' ? 'transparent' : '#151518', }, }, }, @@ -52,10 +59,16 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul color: '#A6A5A5', }, '&& fieldset': { - border: theme === 'light' ? '1px solid #E9E9E9' : '0px solid transparent', + border: + theme === 'light' + ? '1px solid #E9E9E9' + : '0px solid transparent', }, '&.Mui-focused': { - border: theme === 'light' ? `1px solid ${baseColor}` : '2px solid #40404E;', + border: + theme === 'light' + ? `1px solid ${baseColor}` + : '2px solid #40404E;', borderColor: baseColor, '& .MuiOutlinedInput-notchedOutline': { border: 'none', @@ -63,7 +76,10 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul }, '&:hover': { '&& fieldset': { - border: theme === 'light' ? '1px solid #E9E9E9' : '0px solid transparent', + border: + theme === 'light' + ? '1px solid #E9E9E9' + : '0px solid transparent', }, }, }} @@ -73,7 +89,10 @@ export const ChatSelect: React.FC = ({ value, setValue, list, setDefaul {list.map((item, idx) => { return ( - + = { English: 'en', } -async function getModels(token?: string): Promise { +async function getModels(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/', { headers: { @@ -43,43 +44,21 @@ async function getModels(token?: string): Promise { } } -type LangParam = T extends true ? LangFull : LangShort - -type LangReturn = T extends true ? LangShort : LangFull - -const convertLang = (toShort: T, lang: LangParam): LangReturn => { - const arrLang = Object.entries(languages) as [LangFull, LangShort][] - if (toShort) { - return arrLang.find(([key]) => lang === key)![1] as LangReturn - } - return arrLang.find(([_, value]) => lang === value)![0] as LangReturn -} - const InfoBar: React.FC = ({ device }) => { const theme = useAppSelector((state) => state.theme.theme) const balance = useAppSelector((state) => state.balance.balance) - const show_balance = useAppSelector((state) => state.user.show_balance) + const { show_balance } = useUserSelector() const { pathname, replace } = useRouter() const { data } = useSession() - const { email, first_name, last_name, profile_picture_link, account_type } = useAppSelector((state) => state.user) + const { email, first_name, last_name, profile_picture_link, account_type } = useUserSelector() const dispatch = useAppDispatch() - const [search, setSearch] = useState('') - - const [searchOpen, setSearchOpen] = useState(false) - - const [models, setModels] = useState([]) - - useEffect(() => { - getModels(data?.access).then((res) => setModels(res)) - }, [data?.access]) - const [anchorEl, setAnchorEl] = React.useState(null) const [anchorEl2, setAnchorEl2] = React.useState(null) @@ -88,10 +67,6 @@ const InfoBar: React.FC = ({ device }) => { setAnchorEl(event.currentTarget) } - const handleClick2 = (event: React.MouseEvent) => { - setAnchorEl2(event.currentTarget) - } - const handleClose = () => { setAnchorEl(null) } @@ -172,7 +147,7 @@ const InfoBar: React.FC = ({ device }) => { {show_balance && ( - {declineToken(balance.toString())} + {declineToken(balance?.toString())} )} @@ -242,7 +217,9 @@ const InfoBar: React.FC = ({ device }) => { router.push('/account?scope=business')} + onClick={() => + router.push('/account?scope=business') + } > = ({ device }) => { router.push('/account?scope=referral')} + onClick={() => + router.push('/account?scope=referral') + } > = ({ device }) => { router.push('/account?scope=subscribe')} + onClick={() => + router.push('/account?scope=subscribe') + } > = ({ device }) => { }} sx={{ marginTop: '15px', cursor: 'pointer' }} > - {''} - + {''} + Выйти @@ -1,153 +0,0 @@ -import React, { useState } from 'react' -import { Box } from '@mui/material' -import { ThemeProvider } from '@mui/material/styles' -import Head from 'next/head' -import { useRouter } from 'next/router' -import { useSession } from 'next-auth/react' - -import { getUserBalance } from '#/entities/balance' -import { useTheme } from '#/entities/theme' -import { getAllInfo } from '#/entities/user-account' -import { isSettingExist } from '#/entities/user-account/lib/helpers/is-setting-exist' -import { getUserAccountSettings, setSettings } from '#/entities/user-account/model/settings' -import InfoBar from '#/app/layout/ui/info-bar' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { Loader } from '#/shared' -import { pingFangFont } from '#/shared/lib/constants/font/font' -import { Device } from '#/shared/lib/types/entities' -import { MainMenuMobile } from '#/widgets/main-menu' -import { SideMenu } from '#/widgets/side-menu' -import { getDeviceType } from '#/shared/lib/helpers' - -const freeRoutes = ['/login', '/register', '/reset', '/change-password'] - -interface Props { - children: React.ReactNode - titlePage: string - isAuthPage?: boolean - title?: string | React.ReactNode | null - isLoader?: boolean -} - -export const Layout: React.FC = ({ children, isAuthPage = false, titlePage, title = titlePage, isLoader }) => { - const { data: sessionData } = useSession() - const appState = useAppSelector((state) => state) - const dispatch = useAppDispatch() - - const device = getDeviceType() - - const desktop = device === 'desktop' - - const [sidemenuDefaultOpen, setSidemenuDefaultOpen] = useState(true) - - useTheme() - - const { data, status } = useSession() - - const router = useRouter() - - React.useEffect(() => { - dispatch(getUserBalance(data?.access)) - dispatch(getAllInfo(data?.access)) - }, [data]) - - React.useEffect(() => { - if ( - (sessionData && !sessionStorage.getItem('firstRender')) || - (sessionData && !localStorage.getItem('global_settings')) - ) { - dispatch(getUserAccountSettings(sessionData.access)) - sessionStorage.setItem('firstRender', 'true') - } - }, [sessionData]) - - React.useEffect(() => { - const lsData = localStorage.getItem('global_settings') - if (lsData !== null) { - dispatch(setSettings(JSON.parse(lsData))) - } - - window.addEventListener('beforeunload', () => { - sessionStorage.removeItem('firstRender') - sessionStorage.clear() - }) - }, []) - - React.useEffect(() => { - if (appState.settings.state !== null && device) { - let setting = isSettingExist({ - settings: appState.settings.state, - targetDevice: device, - targetType: 'sidemenu', - }) - if (setting) - setting?.value?.sidemenu_state === 'opened' - ? setSidemenuDefaultOpen(true) - : setSidemenuDefaultOpen(false) - } - }, [appState.settings.state]) - - if (status === 'loading' || isLoader) { - return ( - - - - ) - } - - return ( - <> - - {titlePage} - - - - - - - - - - {!isAuthPage ? ( - desktop ? ( - - - - - - {children} - - - - ) : ( - - - {children} - - ) - ) : ( - <>{children} - )} - - - - - ) -} @@ -1 +0,0 @@ -export { Layout } from '#/app/layout/ui/layout' @@ -4,7 +4,6 @@ import { configureStore } from '@reduxjs/toolkit' import { balanceSlice } from '#/entities/balance' import { themeSlice } from '#/entities/theme' import { userSlice } from '#/entities/user-account' -import { settingsSlice } from '#/entities/user-account/model/settings' import { stepperSlice } from '#/features/register-business' import { copySlice } from '#/features/use-copy/copy-slice' import { paramsStore } from '#/app/store/model-parametres-store' @@ -18,10 +17,9 @@ 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, loading: pendingSlice.reducer, }, }) @@ -1,4 +1,7 @@ + + :root[data-theme='light'] { + --scrol-bar-color: #e5e5e5; --air-color: #8280ff; --background-color-main: #ffffff; --background-color-additional: #ffffff; @@ -15,18 +18,26 @@ --border-color2: #e7e7e7; --bg-audio: #f2f2fe; + --new-ui-bg-color: #eff0f2; + --new-ui-bg-app-color: #eff0f2; --new-ui-main-color: white; --new-ui-gray-color: #a4aab5; + --new-ui-element-bg: #ffffff; + --new-ui-gray-text-color: #868686; --new-ui-text-color: #2b2b42; --new-ui-border: 2px solid #eff0f2; + --new-ui-border-color: #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-table-cell-text: #97989f; + + --new-ui-message-text-color: #5e5e5e; } :root[data-theme='dark'] { + --scrol-bar-color: #49494a; --air-color: #8280ff; --background-color-main: #303030; --background-color-page: #303030; @@ -43,15 +54,21 @@ --border-color2: #2c2c2c; --bg-audio: #303030; + --new-ui-bg-color: #303035; + --new-ui-bg-app-color: #303035; --new-ui-border: 1px solid #40404e; + --new-ui-border-color: #303035; --new-ui-main-color: #151518; --new-ui-gray-color: #a4aab5; + --new-ui-gray-text-color: #a6a5a5; --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-table-cell-text: #97989f; + + --new-ui-message-text-color: #a6a5a5; } * { @@ -70,6 +87,13 @@ body { font-feature-settings: 'lnum' 1; } +button { + outline: none; + border: none; + background-color: transparent; + cursor: pointer; +} + a { color: inherit; text-decoration: none; @@ -186,8 +210,12 @@ input:-webkit-autofill:active { /* Изменение цвета активного элемента в выпадающем списке */ .MuiAutocomplete-option.Mui-selected { - background-color: var(--new-ui-main-color); /* Замените #your-selected-color на цвет активного элемента */ - color: var(--new-ui-main-color); /* Замените #your-selected-text-color на цвет текста активного элемента */ + background-color: var( + --new-ui-main-color + ); /* Замените #your-selected-color на цвет активного элемента */ + color: var( + --new-ui-main-color + ); /* Замените #your-selected-text-color на цвет текста активного элемента */ } .introjs-tooltiptext { @@ -356,20 +384,25 @@ textarea { .rdw-editor-toolbar { background-color: transparent !important; - padding-bottom: 15px !important; + padding: 20px 0 !important; border: none !important; - border-bottom: 1px solid #eff0f2 !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; + padding: 0 !important; height: 36px !important; min-width: 40px !important; } +.rdw-dropdown-selectedtext { + padding: 0 16px 0 12px !important; +} + .rdw-dropdown-wrapper:hover { box-shadow: none !important; } @@ -384,10 +417,13 @@ textarea { } .rdw-dropdown-optionwrapper { + border: none !important; + border-radius: 10px !important; width: 100% !important; - margin-top: 15px !important; + margin-top: 12px !important; overflow: hidden; color: inherit !important; + background-color: var(--background-color-main) !important; overflow-y: hidden !important; } @@ -396,14 +432,50 @@ textarea { } .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; + 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 { border-bottom: 1px solid #eff0f2 !important; } @@ -422,17 +494,19 @@ textarea { } /*scroll styles*/ -.smallScroll::-webkit-scrollbar { + +*::-webkit-scrollbar { height: 5px; width: 2px; } -.smallScroll::-webkit-scrollbar-track { +*::-webkit-scrollbar-track { background: initial; - margin: 21px 0; + /*margin: 21px 0;*/ + margin: 5px 0; } -.smallScroll::-webkit-scrollbar-thumb { +*::-webkit-scrollbar-thumb { background-color: rgba(217, 217, 217, 0.49); border-radius: 5px; } @@ -447,10 +521,266 @@ 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); + +} + +.title-h1 { + font-weight: 600; + font-size: 34px; + @media (max-width: 768px) { + font-size: 28px; + } +} + +.title-h2 { + font-weight: 600; + font-size: 28px; + + @media (max-width: 768px) { + font-size: 24px; + } +} + +.item-enter { + opacity: 0; + transform: translateY(-20px); +} +.item-enter-active { + opacity: 1; + transform: translateY(0); + transition: all 500ms; +} +.item-exit { + opacity: 1; + transform: translateY(0); +} +.item-exit-active { + opacity: 0; + transform: translateY(-20px); + transition: all 500ms; +} + +.opacity-enter { + opacity: 0; +} +.opacity-enter-active { + opacity: 1; + transition: all 500ms; +} +.opacity-exit { + opacity: 1; +} +.opacity-exit-active { + opacity: 0; + transition: all 500ms; +} + +/*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); +} + +.title-h1 { + font-weight: 600; + font-size: 34px; + color: var(--new-ui-text-color); + @media (max-width: 768px) { + font-size: 28px; + } +} + +.title-h2 { + font-weight: 600; + font-size: 28px; + color: var(--new-ui-text-color); + @media (max-width: 768px) { + font-size: 24px; + } +} + +.item-enter { + opacity: 0; + transform: translateY(-20px); +} +.item-enter-active { + opacity: 1; + transform: translateY(0); + transition: all 500ms; +} +.item-exit { + opacity: 1; + transform: translateY(0); +} +.item-exit-active { + opacity: 0; + transform: translateY(-20px); + transition: all 500ms; +} + +.opacity-enter { + opacity: 0; +} +.opacity-enter-active { + opacity: 1; + transition: all 500ms; +} +.opacity-exit { + opacity: 1; +} +.opacity-exit-active { + opacity: 0; + transition: all 500ms; } button { @@ -474,11 +804,11 @@ button { } .my-node-enter { - opacity: 0; + opacity: 0 !important; } .my-node-enter-active { - opacity: 1; + opacity: 1 !important; transition: opacity 400ms; } @@ -486,25 +816,74 @@ button { opacity: 1; } +.my-node-exit-done { + opacity: 0; +} + .my-node-exit-active { opacity: 0; transition: opacity 400ms; } - -.fade-enter{ - opacity: 0; +.fade-enter { + opacity: 0; } -.fade-exit{ - opacity: 1; +.fade-exit { + opacity: 1; } -.fade-enter-active{ - opacity: 1; +.fade-enter-active { + opacity: 1; } -.fade-exit-active{ - opacity: 0; +.fade-exit-active { + opacity: 0; } .fade-enter-active, -.fade-exit-active{ - transition: opacity 500ms; +.fade-exit-active { + transition: opacity 500ms; +} + +.transform-enter { + opacity: 0 !important; + transform: translateY(-40px); +} + +.transform-enter-active { + opacity: 1 !important; + transition: all 400ms; + transform: translateY(0px) !important; +} + +.transform-exit { + opacity: 1; +} + +.transform-exit-active { + opacity: 0; + transition: all 400ms; +} + +.tooltip-enter-active { + opacity: 1; + transform: translateX(-50%) scale(1); + transition: all 0.3s ease-in-out; +} + +.tooltip-exit { + opacity: 0; + transform: translateX(-50%) scale(0.9); + transition: all 0.3s ease-in-out; +} + +.tooltip-exit-active { +} + +.flag { + display: inline-block; + height: 15px; + width: 6px; + background-color: var(--air-color); +} + +.class-chat-bot-_slug_{ + overflow: hidden !important; } \ No newline at end of file @@ -0,0 +1 @@ + \ No newline at end of file @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + \ No newline at end of file @@ -0,0 +1,4 @@ + + + \ No newline at end of file @@ -0,0 +1,6 @@ + + + + \ No newline at end of file @@ -0,0 +1,9 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,7 @@ + + + \ No newline at end of file @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,11 @@ + + + + + + + + + + + @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,3 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ No newline at end of file @@ -0,0 +1,6 @@ + + + + + + @@ -0,0 +1,9 @@ + + + + \ No newline at end of file @@ -0,0 +1,6 @@ + + + \ No newline at end of file @@ -0,0 +1,12 @@ + + + \ No newline at end of file @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,10 @@ + + + \ No newline at end of file @@ -0,0 +1,40 @@ + + + + + + \ No newline at end of file @@ -0,0 +1,8 @@ + + + + \ 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,14 @@ + + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + + + @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file @@ -0,0 +1,18 @@ + + + + + \ 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,6 @@ + + + + \ 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,5 @@ + + + \ No newline at end of file @@ -0,0 +1,5 @@ + + + \ 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,8 @@ + + + + \ No newline at end of file @@ -0,0 +1,3 @@ + + + @@ -0,0 +1,3 @@ + + + @@ -2,7 +2,7 @@ import axios from 'axios' import { Template } from '#/domains/copywrite/proxy/types/template' import { API_URL } from '#/shared/lib/constants' -import { Message } from '#/shared/lib/types/model' +import { Message } from '#/entities/message' export class CopywriteProxy { token?: string @@ -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='Удалить' + /> + + + + ) +} @@ -1,8 +1,8 @@ -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { API_URL } from '#/shared/lib/constants' import axios from 'axios' -export async function getAudio(token?: string): Promise { +export async function getAudio(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/?category=audio', { headers: { @@ -0,0 +1,18 @@ +import { api } from '#/shared/api' +import { ChatDTO, CreateChatDTO } from '../types' + +export async function putChat(uid: string, data: Partial) { + return await api.put(`/chats/${uid}/`, data) +} + +export async function getChats(model: string) { + return await api.get('/chats', { params: { model } }) +} + +export async function removeChat(uid: string) { + return await api.delete(`/chats/${uid}/`) +} + +export async function postChat({ title, model }: CreateChatDTO) { + return await api.post('chats/', { title, model }, { params: { model } }) +} @@ -0,0 +1 @@ +export * from './chat.api' \ No newline at end of file @@ -0,0 +1,17 @@ +import { create } from 'zustand' +import { ChatDTO } from '../types' + +export interface ChatsStore { + chats: ChatDTO[] + setChats: (chats: ChatDTO[]) => void +} + +export const useChatsStore = create((set, get) => { + function setChats(chats: ChatDTO[]) { + set({ ...get(), chats }) + } + return { + chats: [], + setChats, + } +}) @@ -0,0 +1,2 @@ +export * from './chats.store' +export * from './use-chat-actions' \ No newline at end of file @@ -0,0 +1,57 @@ +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { postChat, putChat, removeChat } from '../api' +import { useChatsStore } from '.' +import { CreateChatDTO } from '../types' +import { useCurrentChat } from '#/features/chats' +import { useEffect, useState } from 'react' + +export function useChatActions() { + const { showMessage } = useShowDataStore() + + const { chats, setChats } = useChatsStore() + + const [loaded, setLoaded] = useState(false) + + const onRenameChat = makePrivateRequest(async (uid: string, title: string) => { + const chat = chats.find((x) => x.uid === uid) + + setLoaded(false) + + const { data, status } = await putChat(uid, { title }) + + setLoaded(true) + + if (status !== 200 || !chat) return showMessage('Ошибка при переименовании чата') + + chat.title = data.title + + setChats([...chats]) + }) + + const onRemoveChat = makePrivateRequest(async (uid: string) => { + const chat = chats.find((x) => x.uid === uid) + + const { status } = await removeChat(uid) + + if (status !== 204 || !chat) return showMessage('Ошибка при переименовании чата') + + setChats([...chats.filter((c) => c.uid !== uid)]) + }) + + const onCreateChat = makePrivateRequest(async (dto: CreateChatDTO) => { + const { status, data: chat } = await postChat(dto) + + if (status !== 200) return showMessage('Ошибка при переименовании чата') + + const chats = useChatsStore.getState().chats + + setChats([chat, ...chats]) + }) + + return { + onRenameChat, + onRemoveChat, + onCreateChat, + } +} @@ -0,0 +1,11 @@ +export interface ChatDTO { + uid: string + title: string + created_at: string +} + + +export interface CreateChatDTO { + title: string + model: string +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './chat.dto' \ No newline at end of file @@ -0,0 +1,15 @@ +.popup { + min-width: 200px; + + &__title { + color: #a4aab5; + font-weight: 600; + font-size: 16px; + margin-bottom: 20px; + } +} + + +.wrap{ + padding: unset !important; +} \ No newline at end of file @@ -0,0 +1,19 @@ +import { getPopupById, PopupTemplate } from '#/shared/ui/popup' +import { POPUP_CHAT_BOT_PARAMS } from '#/shared/ui/popup' +import React from 'react' +import styles from './chat-bot-options-popup.module.scss' +import { ChatModelOptions } from '#/widgets/chat-model-options' + +export const ChatBotOptionsPopup = () => { + const popup = getPopupById(POPUP_CHAT_BOT_PARAMS) + + return ( + +
+
+ +
+
+
+ ) +} @@ -0,0 +1,63 @@ +.main { + display: flex; + border: none; + justify-content: space-between; + background-color: transparent; + border-radius: 13px; + padding: 12px 15px; + text-transform: none; + width: 100%; + align-items: center; + position: relative; + + &__settings { + color: var(--air-color); + position: relative; + } + + &__text { + font-weight: 500; + font-size: 15px; + color: var(--new-ui-gray-color); + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &_active { + font-weight: 500; + font-size: 15px; + color: var(--air-color); + } + } + + &_active { + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + border: none; + border-radius: 13px; + padding: 12px 15px; + padding-bottom: 10px; + gap: 12px; + text-transform: none; + background-color: rgba(130, 128, 255, 0.15); + } +} + +.chat { +} + +@media screen and (max-width: 1000px) { + .main { + width: 100%; + max-width: 150px; + + &__text { + width: 100%; + flex: 1; + } + } +} @@ -0,0 +1,79 @@ +import { ChatItemActionsPopup } from '#/features/chat-item-actions' +import { useCurrentChat } from '#/features/chats' +import { CommonInput } from '#/shared/ui/common-input' +import React, { forwardRef, useEffect, useRef, useState } from 'react' +import { ChatDTO } from '../types' +import styles from './chat-button.module.scss' + +import RenameSvg from '#/assets/svg/rename.svg?react' +import ChatSettingsSvg from '#/assets/svg/chat-settings.svg?react' +import { PopupTemplateVertical, getPopupById } from '#/shared/ui/popup' +import { c } from '#/shared' +import { useChatActions } from '../model' +import { ChatMobileRename } from '#/features/chat-mobile-rename/ui/chat-mobile-rename-popup' + +interface ChatButtonProps extends ChatDTO { + popupVertical?: PopupTemplateVertical +} + +export const ChatButton = forwardRef( + ({ uid, title, popupVertical = 'top' }, ref) => { + const { setCurrentChat, currentChat: chat } = useCurrentChat() + + const { onRenameChat } = useChatActions() + + const input = useRef(null) + + const [rename, setRename] = useState(false) + + const popup = getPopupById(uid) + + return ( + + } + /> + ) : ( +
+

+ {title} +

+ + {uid === chat && ( + + )} +
+ )} + + ) + } +) @@ -0,0 +1 @@ +export * from './chat-button' \ No newline at end of file @@ -0,0 +1,4 @@ +export * from './types' +export * from './api' +export * from './ui' +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './styles.static' \ No newline at end of file @@ -0,0 +1,8 @@ + + +export interface ImageStyle { + category: string; + image: string; + uid: string; + label: string; +} \ No newline at end of file @@ -0,0 +1,53 @@ +.style { + border-radius: 10px; + position: relative; + &__image { + border-radius: 10px; + width: 100%; + height: 100%; + object-fit: cover; + } + + &__popup { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba($color: #151518, $alpha: 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + border-radius: 10px; + + opacity: 0; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } + + span { + color: var(--new-ui-text-color); + max-width: 80px; + text-align: center; + font-size: 14px; + } + } + + &__favorite{ + position: absolute; + top: 10px; + right: 10px; + z-index: 1; + background-color: rgba($color: #A4AAB5, $alpha: 0.1); + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + } +} @@ -0,0 +1,39 @@ +import React from 'react' +import { ImageStyle } from '../types' +import styles from './image-style.module.scss' +import Image from 'next/image' +import HeartSvg from '#/assets/svg/heart.svg?react' +import { c } from '#/shared' + +type ImageStyleProps = ImageStyle & + React.HTMLAttributes & { + inFavorites: boolean + onCnageFavorite: Function + } + +export const ScopedImageStyle = ({ + label, + image, + className, + inFavorites, + onCnageFavorite, + ...props +}: ImageStyleProps) => { + return ( + + + {label} + + ) +} @@ -0,0 +1 @@ +export * from './image-style' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './ui' +export * from './types' \ No newline at end of file @@ -1 +1,2 @@ -export * from './message.routes' \ No newline at end of file +export * from './message-media.routes' +export * from './message-chat-bot.routes' \ No newline at end of file @@ -0,0 +1,22 @@ +import { Message, MessageSend } from '../types' +import { api } from '#/shared/api' + +export async function getMessagesBySlug(chat: string, offset?: number, limit = 10) { + return await api.get(`/chats/${chat}/messages`, { params: { limit, offset } }) +} + +export async function postMessage(uid: string, { file, content, info }: MessageSend) { + const data = new FormData() + + if (file) data.append('file', file) + + data.append('content', content) + data.append('info', JSON.stringify(info)) + + return await api.post(`/chats/${uid}/messages/`, data) +} + + +export async function removeMessage(chatUid: string, uid: string) { + return api.delete(`/chats/${chatUid}/messages/${uid}`) +} \ No newline at end of file @@ -0,0 +1,23 @@ +import { API_URL } from '#/shared/lib/constants' +import axios, { AxiosResponse } from 'axios' +import { MediaMessageListResponse, Message, MessageSend } from '../types' +import { api } from '#/shared/api' +import { objectToFormdata } from '#/shared/lib/helpers/form' + +export async function postImageMessage(uid: string, dto: MessageSend) { + return await api.post(`/media/image/${uid}`, objectToFormdata(dto)) +} + +export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { + console.log('offser::', offset) + + return await axios.get( + API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, + { + validateStatus: (status) => status < 500, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) +} @@ -1,26 +0,0 @@ -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, - validateStatus: (status) => status < 500, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': HeaderDataType, - }, - }) -} - -export async function getImagesBySlug(slug: string, token: string, offset?: number, limit = 10) { - return await axios.get(API_URL + `/media/image/${slug}?limit=${limit}&offset=${offset}`, { - validateStatus: (status) => status < 500, - headers: { - Authorization: `Bearer ${token}`, - }, - }) -} @@ -0,0 +1,34 @@ + import { create } from 'zustand' + import { Message } from '../types' + + export interface ChatBotMessagesStore { + messages: Message[] + loading: boolean + loaded: boolean + setMessages: (messages: Message[]) => void + setLoading: (loading: boolean) => void + setLoaded: (loaded: boolean) => void + } + + export const useChatBotMessages = create((set, get) => { + function setMessages(messages: Message[]) { + set({ ...get(), messages }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setLoaded(loaded: boolean) { + set({ ...get(), loaded }) + } + + return { + messages: [], + loading: false, + loaded: false, + setLoaded, + setMessages, + setLoading, + } + }) @@ -0,0 +1,34 @@ + import { create } from 'zustand' + import { Message } from '../types' + + export interface ImageBotMessagesStore { + messages: Message[] + loading: boolean + loaded: boolean + setMessages: (messages: Message[]) => void + setLoading: (loading: boolean) => void + setLoaded: (loaded: boolean) => void + } + + export const useImageBotMessages = create((set, get) => { + function setMessages(messages: Message[]) { + set({ ...get(), messages }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setLoaded(loaded: boolean) { + set({ ...get(), loaded }) + } + + return { + messages: [], + loading: false, + loaded: false, + setLoaded, + setMessages, + setLoading, + } + }) @@ -0,0 +1,8 @@ +export * from './use-message-time' +export * from './chat-bot-messages.store' +export * from './use-user-message-actions' +export * from './use-user-message-image' +export * from './use-chat-messages-events' +export * from './image-bot-messages.store' +export * from './use-image-messages-events' +export * from './use-image-icons' @@ -0,0 +1,55 @@ +import { useCurrentChat } from '#/features/chats' +import { AsyncQueue, EventBus, eventBus } from '#/shared/classes' +import { useEventSource } from '#/shared/lib/event-source' +import { useCallback } from 'react' +import { MessageEventStreamResponse } from '../types' +import { useChatBotMessages } from './chat-bot-messages.store' + +export function useChatMessagesEvents() { + const { event, addOpenCallback } = useEventSource() + const { currentChat } = useCurrentChat() + const { setMessages, loaded } = useChatBotMessages() + + const asyncQueue = new AsyncQueue() + + const callback = useCallback( + ({ id, content }: MessageEventStreamResponse) => { + if (!currentChat || !loaded) return + + const currentMessages = useChatBotMessages.getState().messages + const message = currentMessages.find((m) => m.uid === id) + + if (!message) return + + if (!message.content || message.content === '') { + message.content = content + + return eventBus.publish(`bot-message-update-${message.uid}`, message.content) + } + + asyncQueue.push(async () => { + for (const word of content) { + message.content += word + + eventBus.publish(`bot-message-update-${message.uid}`, message.content) + + await new Promise((resolve) => setTimeout(resolve, 10)) + } + }) + }, + [currentChat, loaded] + ) + + const makeEvent = useCallback( + (name: string, onOpened?: (value: Event) => void) => { + if (!currentChat || !loaded) return + event(name, `api/chats/${currentChat}/messages/stream`, callback) + onOpened && addOpenCallback(name, onOpened) + }, + [currentChat, loaded] + ) + + return { + makeEvent, + } +} @@ -0,0 +1,45 @@ +import { useCurrentChat } from '#/features/chats' +import { AsyncQueue, EventBus, eventBus } from '#/shared/classes' +import { useEventSource } from '#/shared/lib/event-source' +import { useCallback, useEffect, useRef, useState } from 'react' +import { MessageEventStreamResponse } from '../types' +import { useChatBotMessages } from './chat-bot-messages.store' +import { useImageObjectId } from '#/features/image-object-id' +import { useImageBotMessages } from './image-bot-messages.store' +import { addBase64Padding, base64Decode, validateBase64 } from '#/shared' + +export function useImageMessagesEvents() { + const chunks = useRef([]) + + const { event, addOpenCallback } = useEventSource() + + const { imageObjectId } = useImageObjectId() + + const { messages, setMessages } = useImageBotMessages() + + const callback = useCallback( + ({ id, content }: MessageEventStreamResponse) => { + const file = chunks.current.join('') + content + + const { messages } = useImageBotMessages.getState() + + setMessages([...messages.map((m) => (m.uid !== id ? m : { ...m, file }))]) + + chunks.current = [...chunks.current, content] + }, + [chunks] + ) + + const makeEvent = useCallback( + (name: string, onOpened?: (value: Event) => void) => { + chunks.current = [] + event(name, `api/media/images/${imageObjectId}/messages/stream`, callback) + onOpened && addOpenCallback(name, onOpened) + }, + [imageObjectId] + ) + + return { + makeEvent, + } +} @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react' + +export function useMessageTime(created_at: string | null | undefined) { + const formatVariant = { + 60: (value: number) => getOneOrMore(value / 60, 'минуту', 'минут'), + 3600: (value: number) => getOneOrMore(value / 3600, 'час', 'часов'), + 86400: (value: number) => getOneOrMore(value / 86400, 'день', 'дней'), + } + + const [formatedDate, setFormatedDate] = useState(getFormatedMessageTime(created_at)) + + useEffect(() => { + setInterval(() => { + setFormatedDate(getFormatedMessageTime(created_at)) + }, 1000 * 60) + }, []) + + function getOneOrMore(value: number, one: string, more: string) { + const formated = Math.floor(value) === 0 ? 1 : Math.floor(value) + + return formated !== 0 ? `${formated} ${more} назад` : `${formated} ${one} назад` + } + + function getFormatedMessageTime(created_at?: string | null) { + if (!created_at) created_at = '' + + const date = new Date() + const createdDate = new Date(created_at) + + // разница в секундах + const difference = (date.getTime() - createdDate.getTime()) / 1000 + + for (const [key, value] of Object.entries(formatVariant).reverse()) { + if (difference > Number(key)) { + return value(difference) + } + } + + return formatVariant['60'](difference) + } + return { + getFormatedMessageTime, + formatedDate, + } +} @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { Message, MessageSend } from '../types' +import { makePrivateRequest } from '#/shared/api' +import { postMessage } from '../api' +import { getBlobFromUrl } from '#/shared' +import { useAppSelector } from '#/app/store/store' +import { useChatBot } from '#/entities/model-entity' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useChatBotMessages } from './chat-bot-messages.store' +import { useCurrentChat } from '#/features/chats' +import { useModelInputStore } from '#/features/model-input/model' +import { useChatWindowContext } from '#/widgets/chat-window/model' + +export function useUserMessageActions(message: Message) { + const includeParams = useAppSelector((state) => state.params.params) + + const { showMessage } = useShowDataStore() + + const { currentChat } = useCurrentChat() + + const { setFile, setValueInput, inputTypes } = useChatWindowContext() + + const { inference } = useChatBot() + + const resend = async () => { + if (!currentChat || !inference) return + + const { file, content } = message + + if (!content && !file) { + return showMessage('Нет данных для повторной отправки сообщения') + } + setValueInput(content) + + if (inputTypes.length === 0) { + return showMessage('Модель не поддерживает отправку файлов') + } + + if (file && typeof file === 'string') { + const newFile = await getBlobFromUrl(file) + setFile(newFile ? newFile : null) + } else if (file && (file as any) instanceof File) { + setFile(file as any) + } else { + setFile(null) + } + } + + return { + resend, + } +} @@ -0,0 +1,20 @@ +import { useMemo, useState } from "react" +import { Message } from "../types" + +export function useUserMessageImage(message: Message) { + + const [loaded, setLoaded] = useState(false) + + const fileName = useMemo(() => { + if (!message.file) return 'Подбираем имя файла...' + const matches = (message.file as string).match(/air-messages\/(.+?)\.(.+?)\?/) + if (!matches) return 'Подбираем имя файла...' + return `${matches[1]}.${matches[2]}` + }, []) + + return { + fileName, + loaded, + setLoaded + } +} @@ -1,18 +1,31 @@ -export interface MessageSend { +export interface MessageSend { content: string file?: File | null - info: T + info: Record } -export interface Message { +export interface Message { content: string created_at: string elapsed_time: string file: T from_model: boolean - info: null + info: Record | null is_favourite: boolean is_sent: boolean uid: string model: string } + +export interface OptimisticMessage extends Omit {} + + +export interface MessageEventStreamResponse { + id: string + content: string +} + +export interface MediaMessageListResponse { + id: string + messages: Message[] +} @@ -0,0 +1,26 @@ + + +.archive{ + display: flex; + align-items: center; + justify-content: center; + + border: 1px solid var(--air-color); + border-radius: 20px; + width: 250px; + height: 284px; + + @media screen and (max-width: 1000px) { + width: 291px; + height: 291px; + } + + &__text{ + + } + + &__download{ + color: var(--air-color); + margin-top: 8px; + } +} \ No newline at end of file @@ -0,0 +1,23 @@ +import React, { useMemo } from 'react' + +import styles from './archive-image-message.module.scss' +import Link from 'next/link' +import { Message } from '../types' +import { fileIsLink } from '#/shared' + +interface ArchiveImageMessageProps extends Message {} + +export const ArchiveImageMessage = ({ file }: ArchiveImageMessageProps) => { + const link = useMemo(() => (file && fileIsLink(file) ? file : ''), [file]) + + return ( +
+

+ Эта генерация является архивом + + Скачать + +

+
+ ) +} @@ -0,0 +1,95 @@ +.button { + background-color: transparent; + border: 0; + cursor: pointer; +} + +.message { + display: flex; + align-items: center; + margin: 8px 0px; + transition: all 0.3s; + + &__avatar { + margin-right: 6px; + @media screen and (max-width: 1000px) { + display: none; + } + } +} + +html[data-theme='dark'] { + .content__wrap { + * { + color: var(--new-ui-gray-text-color) !important; + } + } +} + +.content { + width: fit-content; + max-width: 68%; + margin-right: auto; + transition: all 0.3s; + + @media screen and (max-width: 1000px) { + max-width: 95%; + } + + &__header { + display: flex; + justify-content: space-between; + + p { + color: var(--new-ui-gray-text-color); + font-size: 13px; + font-weight: 600; + } + } + + &__title { + line-height: 17px; + margin-right: 8px; + } + &__date { + line-height: 20px; + margin-right: 32px; + } + + &__description { + display: flex; + align-items: flex-start; + } + + &__menu { + margin-left: 8px; + position: relative; + } + + &__wrap { + overflow-y: scroll; + position: relative; + padding: 15px 23px; + border: 1px solid var(--new-ui-border-color); + color: var(--new-ui-message-text-color); + line-height: 23px; + font-size: 15px; + margin-top: 2px; + border-radius: 13px; + + * { + color: #5e5e5e !important; + font-weight: 500; + } + + &_file { + display: flex; + align-items: center; + gap: 12px; + color: var(--text-color-purple); + font-weight: 600; + line-height: 140%; + cursor: pointer; + } + } +} @@ -0,0 +1,95 @@ +import React, { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import styles from './bot-message.module.scss' + +import { Markdown } from '#/widgets/markdown/markdown' +import { c, formatDate } from '#/shared/lib/helpers' +import { Model } from '#/entities/model-entity' +import { Message } from '../types' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import AvatarSvg from '#/assets/svg/avatar.svg?react' +import DocSvg from '#/assets/svg/doc.svg?react' +import MenuSvg from '#/assets/svg/menu.svg?react' +import { CommonTooltip, CommonTooltipPosition } from '#/shared/ui/tooltip' +import { UserMessageActionsPopup } from '#/features/user-message-actions-popup' +import { getPopupById } from '#/shared/ui/popup' +import { TransitionGroup, CSSTransition } from 'react-transition-group' + +export interface BotMessageProps { + model: Model + message: Message + tooltipPosition?: CommonTooltipPosition + offsetBottom: number + translate: number +} + +export const BotMessage = memo( + forwardRef( + ( + { model, offsetBottom, translate, tooltipPosition = 'bottom', message: { file, ...message } }, + ref + ) => { + const fileName = useMemo( + () => (file ? (file as string).match(/(.+)\/(.+)\?/)![2] : ''), + [file] + ) + + const localRef = useRef(null) + + useImperativeHandle(ref, () => localRef.current) + + const content = useMemo(() => (file ? fileName : message.content), [message.content, file]) + + const popup = getPopupById(`bot-message-${message.uid}`) + + return ( +
+
+ +
+
+
+

{model.title}

+

+ {formatDate(message.created_at, { + hour: 'numeric', + minute: 'numeric', + })} +

+
+
+
{ + if (!file) return + window.open(file as string, '_blank') + }} + > + {file && } + +
+
+ + + +
+
+
+
+ ) + } + ) +) @@ -0,0 +1,102 @@ +@keyframes fadein { + 0% { + background-color: #dcdcdc; + } + 50% { + background-color: #c0c0c0; + } + 100% { + background-color: #dcdcdc; + } +} + +.wrap { + width: 250px; + height: 250px; + position: relative; + border-radius: 15px; + + @media screen and (max-width: 1000px) { + width: 291px; + height: 291px; + } + + &__content { + width: 100%; + padding-top: 12px; + color: var(--text-color-main); + font-size: 15px; + font-weight: 400; + line-height: 150%; + letter-spacing: -1%; + } + + &__image { + position: relative; + z-index: 2; + border-radius: 15px; + user-select: none; + transition: opacity 0.3s ease-in-out; + position: absolute; + top: 0; + left: 0; + width: 250px; + height: 250px; + + @media screen and (max-width: 1000px) { + height: 291px; + width: 291px; + } + + &_loaded { + opacity: 1; + transition: opacity 0.3s ease-in-out; + } + &_hidden { + opacity: 0; + transition: opacity 0.3s ease-in-out; + } + + &__wrapper { + display: flex; + flex-direction: column; + align-items: start; + position: relative; + overflow: hidden; + width: 250px; + height: 250px; + font: unset; + + @media screen and (max-width: 1000px) { + height: 291px; + width: 291px; + } + } + } +} + +.skeleton { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + border-radius: 15px; + width: 250px; + height: 250px; + user-select: none; + border: 4px solid var(--air-color); + + &__text { + font-size: 15px; + font-weight: 600; + line-height: 150%; + letter-spacing: -1%; + color: var(--new-ui-gray-color); + } + + @media screen and (max-width: 1000px) { + width: 291px; + height: 291px; + } +} @@ -0,0 +1,58 @@ +import React, { useEffect, useMemo, useState } from 'react' + +import styles from './file-image-message.module.scss' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { Message } from '../types' +import { c, cutString } from '#/shared' +import { ImageIcons } from './image-icons' +import PhorographSvg from '#/assets/svg/photograph.svg?react' +import Image from 'next/image' +import { useMediaQuery } from 'usehooks-ts' +import { useEventStore } from '#/shared/lib/event-source' +import { eventBus } from '#/shared/classes' + +interface FileImageMessageProps extends Message {} + +export const FileImageMessage = ({ file, content, uid, ...message }: FileImageMessageProps) => { + const fileContent = useMemo(() => file ?? '', [file]) + + const isMobile = useMediaQuery('(max-width: 1000px)') + + const [loaded, setLoaded] = useState(false) + + const [hasEvent, setHasEvent] = useState(false) + + useEffect(() => { + eventBus.subscribe(`image-generate-${uid}`, (data) => { + setHasEvent(data) + }) + }, []) + + return ( + + ) +} @@ -0,0 +1,14 @@ +.wrap { + padding: 12px 9px; + background-color: var(--new-ui-bg-color); + min-width: unset !important; + border-radius: 100px; + + margin-top: 10px; + + &__content{ + display: flex; + flex-direction: column; + gap: 20px; + } +} @@ -0,0 +1,48 @@ +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import React, { Dispatch, SetStateAction } from 'react' +import styles from './image-icons-popup.module.scss' +import { Message } from '../types' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { useImageIcons } from '../model' +import BlankLinkSvg from '#/assets/svg/blank-link.svg?react' +import DownloadSvg from '#/assets/svg/download.svg?react' +import { CSSTransition } from 'react-transition-group' + +interface ImageIconsPopupProps extends Message { + opened: boolean + setOpened: Dispatch> +} + +export const ImageIconsPopup = ({ uid, file, content, opened, setOpened }: ImageIconsPopupProps) => { + const { downloadFile } = useImageIcons() + + return ( + +
+
+ + + + + + +
+
+
+ ) +} @@ -0,0 +1,20 @@ +.gamburger { + color: white; + position: absolute; + top: 10px; + right: 10px; + z-index: 5; + display: flex; + align-items: center; + flex-direction: column; + + @media screen and (max-width: 1000px) { + align-items: start; + } +} + +:root[data-theme='dark'] { + .gamburger { + color: #303035; + } +} @@ -0,0 +1,34 @@ +import { useAppSelector } from '#/app/store/store' +import { TooltipCustom, c } from '#/shared' +import { Grow, Box } from '@mui/material' +import { Message } from '../types' +import { useImageIcons } from '../model' +import GamburgerSvg from '#/assets/svg/gamburger.svg?react' + +import styles from './image-icons.module.scss' +import { getPopupById } from '#/shared/ui/popup' +import { ImageIconsPopup } from './image-icons-popup' +import { useState } from 'react' + +export interface ImageIconsProps extends Message {} + +export const ImageIcons = ({ uid, file, content, ...message }: ImageIconsProps) => { + const [opened, setOpened] = useState(false) + + return ( + + ) +} @@ -0,0 +1,102 @@ +.container { + padding: 5px; + background-color: var(--new-ui-element-bg); + display: flex; + gap: 30px; + border-radius: 15px; + height: 100%; + text-align: left; + + @media screen and (max-width: 1000px) { + flex-direction: column; + } +} + +.description { + padding: 10px; + width: max-content; + min-width: 220px; + display: flex; + flex-direction: column; + justify-content: space-between; + &__trash { + background-color: rgba($color: #b01e1e, $alpha: 0.25) !important; + // margin-left: auto; + } + &__time { + font-weight: 600; + font-size: 20px; + width: max-content; + margin-bottom: 7px; + } + &__content { + color: #a4aab5; + } + &__model { + display: flex !important; + align-items: center; + width: max-content; + gap: 5px; + margin-bottom: 20px; + } + &__controls { + display: flex; + align-items: center; + gap: 5px; + padding-top: 20px; + } +} + +@keyframes skeleton { + 0% { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + } + 100% { + background-color: rgba($color: #a4aab5, $alpha: 0.3); + } +} + +.images { + display: grid; + grid-template-columns: repeat(4, 1fr); + height: 100%; + width: 100%; + gap: 5px; + @media screen and (max-width: 1400px) { + grid-template-columns: repeat(2, 1fr); + } + @media screen and (max-width: 600px) { + grid-template-columns: repeat(1, 1fr); + } + &__skeleton { + width: 100%; + height: 100%; + border-radius: 10px; + background-color: rgba($color: #a4aab5, $alpha: 0.1); + animation: skeleton 1s infinite ease-in-out alternate; + position: absolute; + top: 0; + left: 0; + } + &__imagebox { + position: relative; + } + + &__image { + width: 100%; + height: auto; + border-radius: 10px; + min-height: 314px; + object-fit: cover; + + &_empty { + visibility: collapse; + } + } +} + +.button { + padding: 9px 12px; + background-color: rgba(#a4aab5, 0.1); + border-radius: 8px; +} @@ -0,0 +1,77 @@ +import React, { memo, useMemo, useState } from 'react' +import { Message } from '#/entities/message' +import styles from './image-message.module.scss' +import Image from 'next/image' +import { c, formatDate } from '#/shared' +import { CommonBagde } from '#/shared/ui/badge' +import NeuralSvg from '#/assets/svg/neural.svg?react' +import RepeatSvg from '#/assets/svg/repeat.svg?react' +import DownloadSvg from '#/assets/svg/download.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' +import { useMessageTime } from '../model' + +interface ImageMessageProps extends Omit { + file: string | null + ref: any +} + +export const ImageMessage = memo(({ file, uid, created_at, model, content }: ImageMessageProps) => { + const { formatedDate } = useMessageTime(created_at) + + const [imageLoadStates, setImageLoadStates] = useState(new Array(4).fill(false)) + + return ( +
+
+
+

{formatedDate}

+ + + {model} + +

+ {content.length < 30 ? content : `${content.slice(0, 30)}...`} +

+
+
+ + + +
+
+
+ {new Array(4).fill(file).map((el, index) => ( +
+ + setImageLoadStates((prev) => { + const newState = [...prev] + newState[index] = true + return newState + }) + } + height={300} + src={el} + alt={`image-${uid}-${index}`} + /> + + {!imageLoadStates[index] && ( +
+ )} +
+ ))} +
+
+ ) +}) @@ -0,0 +1,3 @@ +.message{ + position: relative; +} \ No newline at end of file @@ -0,0 +1,33 @@ +import React, { CSSProperties, MouseEventHandler, useEffect, useMemo } from 'react' + +import styles from './image-message.module.scss' +import { c } from '#/shared' +import { Message } from '../types' +import { ArchiveImageMessage } from './archive-image-message' +import { FileImageMessage } from './file-image-message' + +interface ImageMessageProps extends Message { + onClick?: MouseEventHandler + className?: string + style?: CSSProperties +} + +export const ImageOldMessage = ({ file, onClick, className, style, ...message }: ImageMessageProps) => { + const extention = useMemo(() => { + if (!file) return 'unknown' + + const matches = file.match(/\.(\w+)\?/) + + return matches ? matches[1] : 'unknown' + }, [file]) + + return ( +
+ {extention === 'zip' ? ( + + ) : ( + + )} +
+ ) +} @@ -0,0 +1,11 @@ + + +.popup{ + min-width: 200px; + &__title{ + color: #A4AAB5; + font-weight: 600; + font-size: 16px; + margin-bottom: 20px; + } +} \ No newline at end of file @@ -0,0 +1,26 @@ +import { getPopupById, PopupTemplate } from '#/shared/ui/popup' +import { POPUP_IMAGE_SETTINGS } from '#/shared/ui/popup' +import React, { useEffect } from 'react' +import styles from './image-settings.popup.module.scss' +import { InferenceParams } from '#/entities/model-entity' +import { BotParamsMap } from '#/features/bot-params' + +export const ImageSettingsPopup = () => { + const popup = getPopupById(POPUP_IMAGE_SETTINGS) + + const botParams = popup.getStoreProperty('botParams') + const currentVersion = popup.getStoreProperty('currentVersion') + + return ( + +
+

НАСТРОЙКИ

+ {currentVersion && ( +
+ +
+ )} +
+
+ ) +} @@ -0,0 +1,6 @@ +export * from './image-message' +export * from './image-settings.popup' +export * from './user-message' +export * from './user-message' +export * from './image-message' +export * from './image-old-message' @@ -0,0 +1,120 @@ +.button { + background-color: transparent; + border: 0; + cursor: pointer; +} + +.message { + width: fit-content; + max-width: 68%; + margin: 8px 0px; + margin-left: auto; + + @media screen and (max-width: 1000px) { + max-width: 100%; + } + + &__time { + align-items: center; + display: flex; + justify-content: end; + color: var(--new-ui-gray-color); + font-size: 13px; + font-weight: 600; + margin-left: 4px; + } +} + +.content { + display: flex; + flex-direction: column; + align-items: end; + gap: 8px; + + &__filebox { + display: flex; + align-items: center; + overflow-y: scroll; + position: relative; + padding: 15px 23px; + border: 1px solid var(--new-ui-bg-app-color); + border-radius: 9px; + font-size: 15px; + margin-top: 2px; + text-align: left; + justify-content: start; + gap: 12px; + font-weight: 600; + line-height: 140%; + letter-spacing: 0.2px; + color: var(--air-color); + + p { + color: var(--air-color); + } + } + + &__message{ + display: flex; + width: 100%; + align-items: flex-start; + justify-content: end; + gap: 4px; + } + + &__text { + overflow-y: scroll; + padding: 10px 15px; + background-color: #7F7DF3; + color: white; + border-radius: 13px; + line-height: 21px; + font-size: 15px; + font-weight: 400; + margin-top: 2px; + text-align: left; + } + + &__imagebox { + display: flex; + align-items: end; + flex-direction: column; + height: 60%; + width: 60%; + } + + &__image { + position: relative; + img { + object-fit: contain; + width: 100%; + height: 100%; + border-radius: 13px; + cursor: pointer; + min-width: 100px; + min-height: 100px; + } + } + + &__skeleton { + background-color: #eff0f2; + position: absolute; + top: 0; + left: 0; + z-index: 1; + border-radius: 13px; + width: 100%; + height: 100%; + } +} + +.menu { + display: flex; + margin-right: 4px; +} + +html[data-theme='dark'] { + .content__skeleton { + background-color: #2d2d2f; + } +} @@ -0,0 +1,116 @@ +import React, { forwardRef, memo, useMemo, useState } from 'react' +import Image from 'next/image' + +import { c, formatDate, stringIsImage } from '#/shared/lib/helpers' +import styles from './user-message.module.scss' +import { CommonTooltip, CommonTooltipPosition } from '#/shared/ui/tooltip' + +import MenuSvg from '#/assets/svg/menu.svg?react' +import DocSvg from '#/assets/svg/doc.svg?react' +import WarningSvg from '#/assets/svg/warning.svg?react' +import { UserMessageActionsPopup } from '#/features/user-message-actions-popup' +import { getPopupById } from '#/shared/ui/popup' +import { Model } from '#/entities/model-entity' +import { useUserMessageActions, useUserMessageImage } from '../model' +import { Message } from '../types' +import { GALLERY_IMAGES, getModalById } from '#/features/modals' + +interface UserMessageProps { + message: Message + model: Model + tooltipPosition?: CommonTooltipPosition + setCurrentSrc: (current: string) => void + offsetBottom: number + translate: number +} + +export const UserMessage = forwardRef( + ({ message, tooltipPosition = 'bottom', setCurrentSrc }, ref) => { + const { loaded, setLoaded, fileName } = useUserMessageImage(message) + + const { resend } = useUserMessageActions(message) + + const imagesModal = getModalById(GALLERY_IMAGES) + + const popup = getPopupById(message.uid) + + return ( +
+

+ {formatDate(message.created_at, { + hour: 'numeric', + minute: 'numeric', + })} +

+ +
+ {message.file && + (stringIsImage(message.file as string) ? ( +
{ + setCurrentSrc(message.file as string) + imagesModal.setState(true) + }} + className={styles.content__imagebox} + > +
+ setLoaded(true)} + width={500} + height={500} + src={(message.file as string).split('?type')[0]} + alt={'К сожалению, изображение не загрузилось'} + /> + {!loaded &&
} +
+
+ ) : ( + + ))} + +
+
+ + + + +
+ + {!message.is_sent && ( + + )} + +

30 ? 'pre-wrap' : 'pre', + }} + > + {message.content} +

+
+
+
+ ) + } +) @@ -1,2 +1,4 @@ export * from './types' -export * from './api' \ No newline at end of file +export * from './api' +export * from './model' +export * from './ui' @@ -1,11 +1,14 @@ -import { API_URL } from '#/shared/lib/constants' -import axios from 'axios' -import { IModel } from '../types' +import { Inference, Model, ShortModel } from '../types' +import { api } from '#/shared/api' -export async function getBotParams(slug: string, token?: string) { - return await axios.get(API_URL + `/ml_models/${slug}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) +export async function getBotParams(slug: string) { + return await api.get(`/api/ai/model/${slug}`) +} + +export async function getInference(id: string) { + return await api.get(`api/ai/inferences/${id}`) +} + +export async function getChatBots() { + return await api.get(`api/chats/models/`) } @@ -1,11 +1,6 @@ -import { API_URL } from '#/shared/lib/constants' -import axios from 'axios' -import { IShortModel } from '../types' +import { ShortModel } from '../types' +import { api } from '#/shared/api' -export async function getModelsImages(token?: string) { - return await axios.get(API_URL + '/ml_models/?category=images', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) +export async function getImageBots() { + return await api.get(`api/media/images/models/`) } @@ -1 +1,4 @@ -export * from './use-images-bots' \ No newline at end of file +export * from './use-images-bots' +export * from './use-chat-bot' +export * from './use-chat-bot-params.store' +export * from './use-image-bot-params.store' @@ -0,0 +1,64 @@ +import { create } from 'zustand' +import { Inference, Model, InferenceParams, InferenceInput } from '../types' + +export interface ChatBotParamsStore { + botParams: Model | null + inferenceParams: InferenceParams[] + setInferenceParams: (params: InferenceParams[]) => void + infrerenceValues: Record + setInferenceInputs: (inputs: InferenceInput[]) => void + inferenceInputs: InferenceInput[] + setInferenceValues: (infrerenceValues: Record) => void, + setInferenceValueByKey: (key: string, value: any) => void + loading: boolean + setBotParams: (botParams: Model) => void + setLoading: (loading: boolean) => void + inference: Inference | null + setInference: (inference: Inference) => void +} + +export const useChatBotParams = create((set, get) => { + function setBotParams(botParams: Model) { + set({ ...get(), botParams }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setInference(inference: Inference) { + set({ ...get(), inference }) + } + + function setInferenceParams(inferenceParams: InferenceParams[]) { + set({ ...get(), inferenceParams }) + } + + function setInferenceInputs(inferenceInputs: InferenceInput[]) { + set({ ...get(), inferenceInputs }) + } + + function setInferenceValues(infrerenceValues: Record) { + set({ ...get(), infrerenceValues }) + } + + function setInferenceValueByKey(key: string, value: any) { + set({ ...get(), infrerenceValues: { ...get().infrerenceValues, [key]: value } }) + } + + return { + botParams: null, + loading: false, + inferenceInputs: [], + inferenceParams: [], + infrerenceValues: {}, + setInferenceParams, + setInferenceInputs, + setInferenceValues, + setInferenceValueByKey, + inference: null, + setInference, + setBotParams, + setLoading, + } +}) @@ -0,0 +1,72 @@ +import { useEffect } from 'react' +import { makePrivateRequest } from '#/shared/api' +import { getBotParams, getInference } from '../api' +import { useRouter } from 'next/router' +import { useChatBotParams } from '.' +import { Inference } from '../types' + +export function useChatBot() { + const { + botParams, + setBotParams, + inference, + setInference, + setInferenceParams, + setInferenceValues, + inferenceParams, + inferenceInputs, + setInferenceInputs, + } = useChatBotParams() + + const { query, push } = useRouter() + + const fetchBotParams = makePrivateRequest(async () => { + const { status, data } = await getBotParams(query.slug as string) + + if (status !== 200) return push('/404') + + setBotParams(data) + + onSetInferenceParams(data.inferences[0].slug) + }) + + const onSetInferenceParams = makePrivateRequest(async (inference: string | null) => { + const { botParams } = useChatBotParams.getState() + + if (!botParams) return + + if (!inference) inference = botParams.inferences[0].slug + + const result = botParams.inferences.find((i) => i.slug === inference) + + if (!result) return + + const { + data: { parameters, ...data }, + } = await getInference(result.id) + + // console.log(parameters) + + // console.log(data) + + setInferenceInputs(data.inputs) + + setInferenceParams(parameters) + + setInference({ ...data, parameters }) + + setInferenceValues( + parameters.reduce((p, { key: k, values: v }) => ({ ...p, [k]: v.default }), {}) + ) + }) + + return { + botParams, + fetchBotParams, + inference, + setInference, + onSetInferenceParams, + inferenceParams, + inferenceInputs, + } +} @@ -0,0 +1,64 @@ +import { create } from 'zustand' +import { Inference, Model, InferenceParams, InferenceInput } from '../types' + +export interface ImageBotParamsStore { + botParams: Model | null + inferenceParams: InferenceParams[] + setInferenceParams: (params: InferenceParams[]) => void + infrerenceValues: Record + setInferenceInputs: (inputs: InferenceInput[]) => void + inferenceInputs: InferenceInput[] + setInferenceValues: (infrerenceValues: Record) => void, + setInferenceValueByKey: (key: string, value: any) => void + loading: boolean + setBotParams: (botParams: Model) => void + setLoading: (loading: boolean) => void + inference: Inference | null + setInference: (inference: Inference) => void +} + +export const useImageBotParams = create((set, get) => { + function setBotParams(botParams: Model) { + set({ ...get(), botParams }) + } + + function setLoading(loading: boolean) { + set({ ...get(), loading }) + } + + function setInference(inference: Inference) { + set({ ...get(), inference }) + } + + function setInferenceParams(inferenceParams: InferenceParams[]) { + set({ ...get(), inferenceParams }) + } + + function setInferenceInputs(inferenceInputs: InferenceInput[]) { + set({ ...get(), inferenceInputs }) + } + + function setInferenceValues(infrerenceValues: Record) { + set({ ...get(), infrerenceValues }) + } + + function setInferenceValueByKey(key: string, value: any) { + set({ ...get(), infrerenceValues: { ...get().infrerenceValues, [key]: value } }) + } + + return { + botParams: null, + loading: false, + inferenceInputs: [], + inferenceParams: [], + infrerenceValues: {}, + setInferenceParams, + setInferenceInputs, + setInferenceValues, + setInferenceValueByKey, + inference: null, + setInference, + setBotParams, + setLoading, + } +}) @@ -1,118 +1,66 @@ -import { useState } from 'react' -import { IModel } from '../types' -import { useSession } from 'next-auth/react' -import { getBotParams } from '../api' -import { useAppDispatch } from '#/app/store/store' -import { setParams } from '#/app/store/model-parametres-store' +import { version } from 'react' +import { getBotParams, getInference } from '../api' +import { makePrivateRequest } from '#/shared/api' +import { useRouter } from 'next/router' +import { useChatBotParams } from './use-chat-bot-params.store' +import { useImageBotParams } from './use-image-bot-params.store' + +export function useImageBot() { + const { + botParams, + setBotParams, + inference, + setInference, + setInferenceParams, + setInferenceValues, + inferenceParams, + inferenceInputs, + setInferenceInputs, + } = useImageBotParams() -export function useImageBot(slug: string) { - const [botParams, setBotParams] = useState(null) - const [version, setVersion] = useState('') - const [modelType, setModelType] = useState('') + const { query, push } = useRouter() - const dispatch = useAppDispatch() + const fetchBotParams = makePrivateRequest(async () => { + const { status, data } = await getBotParams(query.slug as string) - const { data } = useSession() + if (status !== 200) return push('/404') - function setDefault(bot: IModel) { - setVersion('') + setBotParams(data) - dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } + onSetInferenceParams(data.inferences[0].slug) + }) - // это пиз**ц - // нужен рефакторинг (я то в этом не разбираюсь) - // а стажеры и подавно)))) - 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 }, - {} - ) - ) - ) - } + const onSetInferenceParams = makePrivateRequest(async (inference: string | null) => { + const { botParams } = useImageBotParams.getState() - function setAllParams(bot: IModel) { - setBotParams(bot) - setModelType(bot.slug) - - // store - if (bot.versions.length === 0) return setDefault(bot) + if (!botParams) return - setVersion(bot.versions[0].slug) - setStoreParams(bot) - } + if (!inference) inference = botParams.inferences[0].slug - // это пиз**ц - 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 result = botParams.inferences.find((i) => i.slug === inference) - // это пиз**ц - const setDefaultParams = () => { - if (!botParams) return + if (!result) 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 }), - {} - ) - ) - ) - } + const { + data: { parameters, ...data }, + } = await getInference(result.id) - // api functions + setInferenceInputs(data.inputs) - async function fetchBotParams() { - if (!data) return + setInferenceParams(parameters) - const { data: bot, ...response } = await getBotParams(slug, data.access) + setInference({ ...data, parameters }) - if (response.status < 400) setAllParams(bot) - } + setInferenceValues(parameters.reduce((p, { key: k, values: v }) => ({ ...p, [k]: v.default }), {})) + }) return { botParams, - version, - modelType, fetchBotParams, - setVersion, - resetParams, - setDefaultParams, + inference, + setInference, + onSetInferenceParams, + inferenceParams, + inferenceInputs, } } @@ -1,24 +1,21 @@ import { useState } from 'react' -import { getModelsImages } from '../api' import { useSession } from 'next-auth/react' -import { IShortModel } from '../types' +import { ShortModel } from '../types' +import { getImageBots } from '../api' +import { makePrivateRequest } from '#/shared/api' export function useImagesBots() { - const [bots, setBots] = useState([]) + const [bots, setBots] = useState([]) - const { data } = useSession() - - async function fetchBots() { - if (!data) return - - const response = await getModelsImages(data.access) + const fetchBots = makePrivateRequest(async () => { + const response = await getImageBots() if (response.status < 400) setBots(response.data) - } + }) - return { - bots, - fetchBots, - setBots - } + return { + bots, + fetchBots, + setBots, + } } @@ -1,60 +1,78 @@ type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' -export interface IShortModel { +export interface ShortModel { uid: string title: string description: string slug: string image: string - blocked: boolean - tags: IModelTag[] - actual_stat: { - generation_time: string - tokens_cost: string - } + enabled: boolean + tags: ModelTag[] + types: string[] } -export interface IModelTag { - title: string - icon: string - color: string +export interface ModelTag { + title: string + icon: string + color: string } -export interface IModel { - uid: string +export interface Model { + id: string title: string - blocked: boolean + enabled: boolean description: string slug: string image: string - settings: { is_active: boolean } - parameters: IModelParams[] - versions: IModelVersions[] - inputs: IModelInputs[] - tags: IModelTag[] + inferences: ShortModelInference[] + tags: ModelTag[] + types: string[] } -export interface IModelParams { + +export type InferenceParamType = 'floatrange' | 'intrange' | 'bool' | 'choices' | 'int' | 'str' + +export interface InferenceParams { name: string description: string key: string - type: string + type: InferenceParamType required: boolean values: { - availables: string[] + availables: string[] | Array default: any - end: number + stop: number start: number step: number } versions: string[] } -export interface IModelVersions { + +export interface ShortModelInference { + id: string + name: string + slug: string + description: string +} + +export interface Inference { + id: string + tags: string[] + tracking_records: any[] + parameters: InferenceParams[] + inputs: InferenceInput[] name: string description: string - default: boolean slug: string + enabled: false } -export interface IModelInputs { + +// export interface ModelVersions { +// name: string +// description: string +// default: boolean +// slug: string +// } +export interface InferenceInput { // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' type: string required: boolean @@ -1,34 +1,50 @@ -import React, { useEffect, useMemo } from 'react' -import { Avatar, Box, Button, Typography } from '@mui/material' +import React, { useMemo } from 'react' import Image from 'next/image' import Link from 'next/link' -import LockSvg from '#/assets/svg/lock.svg?react' -import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card-chat.module.scss' + +import BlockedSvg from '#/assets/svg/blocked.svg?react' +import LockSvg from '#/assets/svg/lock.svg?react' import { useThemeAndDevice } from '#/shared/lib/hooks' import { c } from '#/shared/lib/helpers' -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { SvgIcon } from '#/shared/ui/svg' import { CommonButton } from '#/shared/ui/button' -import { uid } from 'chart.js/dist/helpers/helpers.core' -export interface ChatModelCardProps extends IShortModel { +export interface ChatModelCardProps extends ShortModel { accessed_models: string[] | null } -export function ChatModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ChatModelCardProps) { +export function ChatModelCard({ + description, + image, + title, + slug, + accessed_models, + enabled, + tags, +}: ChatModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` + return accessed_models && !accessed_models.includes(slug) + ? '/account?scope=subscribe' + : `chat-bot/${slug}` }, [accessed_models]) - const { theme } = useThemeAndDevice() - return ( -
+
- {title} + {title}
@@ -36,10 +52,12 @@ export function ChatModelCard({ description, image, title, slug, accessed_models

{description}

- {blocked && ( + {!enabled && (
- Модель недоступна + + Модель недоступна +
)} {accessed_models && !accessed_models.includes(slug) && ( @@ -59,7 +77,12 @@ export function ChatModelCard({ description, image, title, slug, accessed_models {tags && tags.map((tag, index) => (
- + {tag.title}
))} @@ -0,0 +1,15 @@ +.popup { + min-width: 200px; + + &__title { + color: #a4aab5; + font-weight: 600; + font-size: 16px; + margin-bottom: 20px; + } +} + + +.wrap{ + padding: unset !important; +} \ No newline at end of file @@ -0,0 +1,16 @@ +import { POPUP_IMAGE_BOT_PARAMS, PopupTemplate } from '#/shared/ui/popup' +import React from 'react' +import styles from './image-bot-options-popup.module.scss' +import { ImageModelOptions } from '#/widgets/image-model-options' + +export const ImageBotOptionsPopup = () => { + return ( + +
+
+ +
+
+
+ ) +} @@ -8,26 +8,43 @@ import BlockedSvg from '#/assets/svg/blocked.svg?react' import styles from './card.module.scss' import { useThemeAndDevice } from '#/shared/lib/hooks' import { c } from '#/shared/lib/helpers' -import { IShortModel } from '#/entities/model-entity' +import { ShortModel } from '#/entities/model-entity' import { SvgIcon } from '#/shared/ui/svg' import { CommonButton } from '#/shared/ui/button' -export interface ImageModelCardProps extends IShortModel { +export interface ImageModelCardProps extends ShortModel { accessed_models: string[] | null } -export function ImageModelCard({ description, image, title, slug, accessed_models, blocked, tags }: ImageModelCardProps) { +export function ImageModelCard({ + description, + image, + title, + slug, + accessed_models, + enabled, + tags, +}: ImageModelCardProps) { const link = useMemo(() => { - return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `images/${slug}` + return accessed_models && !accessed_models.includes(slug) + ? '/account?scope=subscribe' + : `deprecated/${slug}` }, [accessed_models]) - const { theme } = useThemeAndDevice() - return ( -
+
- {title} + {title}
@@ -35,7 +52,7 @@ export function ImageModelCard({ description, image, title, slug, accessed_model

{description}

- {blocked && ( + {!enabled && (
Модель недоступна @@ -58,7 +75,12 @@ export function ImageModelCard({ description, image, title, slug, accessed_model {tags && tags.map((tag, index) => (
- + {tag.title}
))} @@ -1,2 +1,3 @@ export * from './chat-model-card' -export * from './image-model-card' \ No newline at end of file +export * from './image-model-card' +export * from './image-bot-options-popup' \ 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,8 +1,8 @@ import axios from 'axios' import { API_URL } from '#/shared/lib/constants' +import { IUserSetting } from '../types' -import { IUserSetting } from '../model/types' export const addUserSettings = async ( token: string, @@ -1,8 +1,8 @@ import axios, { AxiosResponse } from 'axios' import { API_URL } from '#/shared/lib/constants' +import { AccountType } from '../types' -import { AccountType } from '../model/types' export const getAccountType = async (token: string | null | undefined): Promise => { if (!token) { return 'regular' @@ -1,8 +1,8 @@ import axios from 'axios' import { API_URL } from '#/shared/lib/constants' +import { IUserSetting } from '../types' -import { IUserSetting } from '../model/types' export const getUserSettings = async (token: string): Promise => { try { @@ -0,0 +1,2 @@ +export * from './account-endpoints' +export * from './settings.routes' @@ -0,0 +1,16 @@ +import { api } from '#/shared/api' +import { API_URL } from '#/shared/lib/constants' +import { Agent } from 'https' +import { IUserSetting } from '../model/types' + +export function getUserSettings() { + return api.get('/api/users/settings/') +} + +export function postUserSettings(option: Omit) { + return api.post('/api/users/settings/', option) +} + +export function updateUserSettings(id: string, value: any) { + return api.put(`/api/users/settings/${id}`, { value }) +} @@ -3,7 +3,7 @@ import axios from 'axios' import { API_URL } from '#/shared/lib/constants' import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' -import { IUserSetting, SettingValueType } from '../model/types' +import { IUserSetting, SettingValueType } from '../types' export const updateUserSettings = async ( token: string, @@ -1,6 +1,6 @@ import { Device } from '#/shared/lib/types/entities' -import { IUserSetting, SettingType } from '../../model/types' +import { IUserSetting, SettingType } from '../../types' interface IProps { settings: Omit | null @@ -1,4 +1,4 @@ -import { IUserSetting, SettingValueType } from '../../model/types' +import { IUserSetting, SettingValueType } from '../../types' interface IProps { settings: IUserSetting[] @@ -0,0 +1,5 @@ + +export * from './user-type-slice' +export * from './use-user-selector' +export * from './settings-context' +export * from './use-global-settings' @@ -0,0 +1,9 @@ +import { createUseContext } from '#/shared' +import { createContext } from 'react' +import { useGlobalSettings } from './use-global-settings' + +export const UserSettingsContext = createContext | null>(null) + +export const useUserSettingsContext = createUseContext(UserSettingsContext) + +export const UserSettingsContextProvider = UserSettingsContext.Provider @@ -1,92 +0,0 @@ -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' - -import { addUserSettings } from '../api/add-user-settings' -import { getUserSettings } from '../api/get-user-settings' -import { updateUserSettings } from '../api/update-user-settings' -import { getUpdatedSettingsLocal } from '../lib/helpers/update-setting-local' - -import { IUserSetting, SettingValueType } from './types' - -interface IAddSettings { - token: string | undefined | null - setting: Omit -} - -export const getUserAccountSettings = createAsyncThunk( - 'users/getSettings', - async (token: string | undefined | null) => { - if (!token) { - return initialState.state - } - return await getUserSettings(token) - } -) - -export const addUserAccountSettings = createAsyncThunk( - 'users/addSettings', - async ({ token, setting }: IAddSettings) => { - if (!token) { - return - } - return await addUserSettings(token, setting) - } -) - -export const updateUserAccountSettings = createAsyncThunk( - 'users/updateSettings', - async ({ - token, - id, - value, - settings, - }: { - token: string | undefined | null - id: string - value: SettingValueType - settings: IUserSetting[] | null - }) => { - if (!token) { - return initialState.state - } - if (!settings) { - return initialState.state - } - return await updateUserSettings(token, id, value, settings) - } -) - -const initialState: { state: IUserSetting[] | null } = { - state: null, -} - -export const settingsSlice = createSlice({ - name: 'balance', - initialState, - reducers: { - setSettings: (state, action) => { - state.state = action.payload - }, - }, - extraReducers: (builder) => { - builder.addCase(getUserAccountSettings.fulfilled, (state, action) => { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - }) - builder.addCase(addUserAccountSettings.fulfilled, (state, action) => { - if (state.state === null) state.state = [] - if (action.payload) { - state.state = [...state.state, action.payload] - localStorage.setItem('global_settings', JSON.stringify([...state.state, action.payload])) - } - }) - - builder.addCase(updateUserAccountSettings.fulfilled, (state, action) => { - if (action.payload) { - state.state = action.payload - localStorage.setItem('global_settings', JSON.stringify(action.payload)) - } - }) - }, -}) - -export const { setSettings } = settingsSlice.actions @@ -2,7 +2,7 @@ import { Device } from '#/shared/lib/types/entities' export type AccountType = 'regular' | 'business_host' | 'business_account' -export type SettingType = 'sidemenu' +export type SettingType = 'sidemenu' | string export type SettingValueType = { sidemenu_state?: 'opened' | 'closed' @@ -12,5 +12,23 @@ export interface IUserSetting { id: string device: Device type: SettingType - value: SettingValueType + value: SettingValueType | any +} + +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 } @@ -0,0 +1,65 @@ +import { useLocalStorage } from 'usehooks-ts' +import { IUserSetting } from './types' +import { makePrivateRequest } from '#/shared/api' +import { getUserSettings, postUserSettings, updateUserSettings } from '../api' +import { useShowDataStore } from '#/shared/lib/hooks' +import { getDeviceType } from '#/shared' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useSession } from 'next-auth/react' + +export function useGlobalSettings() { + const [settings, setSettings] = useLocalStorage('global_settings', []) + + const { showMessage } = useShowDataStore() + + const device = getDeviceType() + + const { data } = useSession() + + const addSettings = makePrivateRequest(async (type: string, value: any) => { + const { status, data } = await postUserSettings({ device, type, value }) + + if (status !== 200) return showMessage('Ошибка создания настроек') + + setSettings((s) => [...s, data]) + }) + + const updateSettings = useCallback( + makePrivateRequest(async (type: string, value: any) => { + const option = settings.find((x) => x.type === type) + + if (!option) return showMessage('Ошибка присвоения настроек') + + const { status } = await updateUserSettings(option.id, value) + + if (status !== 204) return showMessage('Ошибка создания настроек') + + setSettings((s) => [...s.filter((x) => x.type !== type), { ...option, value }]) + }), + [settings, data] + ) + + const fetchUserSettings = makePrivateRequest(async () => { + const { status, data } = await getUserSettings() + + const device = getDeviceType() + + if (status !== 200) return showMessage('Ошибка загрузки данных пользователя') + + setSettings(data.filter((s) => s.device === device)) + }) + + const getOptionValue = (type: string, initial: any) => { + const option = settings.find((o) => o.type === type) + return option ? option.value : initial + } + + return { + settings, + setSettings, + addSettings, + fetchUserSettings, + updateSettings, + getOptionValue, + } +} @@ -0,0 +1,4 @@ +import { useAppSelector } from "#/app/store/store"; +import { UserDTO } from "../types"; + +export const useUserSelector = () => useAppSelector((state) => state.user.user) as UserDTO @@ -2,96 +2,22 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAction } from '@reduxjs/toolkit/src' import axios, { AxiosResponse } from 'axios' -import { loadingThunk } from '#/app/store/store' +import { loadingThunk, useAppSelector } from '#/app/store/store' import { API_URL } from '#/shared/lib/constants' +import { UserDTO } from '../types' type UserState = { status: loadingThunk - referral: string + user: UserDTO | null } -export type ResponseAllInfo = { - uid: string - first_name: string - last_name: string - username: string - created_at: string - email: string - is_active: boolean - is_staff: boolean - show_balance: boolean - is_confirmed: boolean - is_social: boolean - social_auth: string[] - is_subscribed_to_emails: boolean - profile_picture_link: string | null - account_type: string - referral_code: { - code: string - } - token: { - access: string - refresh: string - } - payment_plan: { - uid: string - plan: { - uid: string - price: string - tokens_per_plan: string - title: string - duration: string - accessed_models: string[] | null - } - last_payment_at: string - next_payment_at: string - current_token_balance: number - } -} - -const initialState: UserState & ResponseAllInfo = { +const initialState: UserState = { status: 'idle', - referral: '', - uid: '', - first_name: '', - last_name: '', - username: '', - created_at: '', - email: '', - show_balance: true, - is_active: false, - social_auth: [], - is_staff: false, - is_confirmed: false, - referral_code: { - code: '', - }, - token: { - access: '', - refresh: '', - }, - is_subscribed_to_emails: false, - is_social: false, - profile_picture_link: null, - account_type: 'regular', - payment_plan: { - uid: '', - plan: { - uid: '', - price: '', - tokens_per_plan: '', - duration: '', - title: '', - accessed_models: null, - }, - last_payment_at: '', - next_payment_at: '', - current_token_balance: 0, - }, + user: null, } -export const getAll = async (token: string | null | undefined): Promise => { - const { data } = await axios.get>(API_URL + '/auth/me', { +export const getAll = async (token: string | null | undefined): Promise => { + const { data } = await axios.get>(API_URL + '/auth/me', { headers: { Authorization: `Bearer ${token}`, }, @@ -101,12 +27,9 @@ export const getAll = async (token: string | null | undefined): Promise { - return await getAll(token) - } -) +export const getAllInfo = createAsyncThunk('user/getAllInfo', async (token: string | null | undefined) => { + return await getAll(token) +}) export const unfollowEmail = createAsyncThunk( 'user/unfollowEmail', @@ -122,48 +45,11 @@ export const unfollowEmail = createAsyncThunk( export const userSlice = createSlice({ name: 'userSlice', initialState, - reducers: { - addReferral: (state, action) => { - state.referral = action.payload - }, - }, + reducers: {}, extraReducers: (builder) => { builder.addCase(getAllInfo.fulfilled, (state, action) => { state.status = 'succeeded' - state.referral = '' - const { - uid, - is_subscribed_to_emails, - email, - account_type, - first_name, - last_name, - profile_picture_link, - is_active, - is_staff, - created_at, - username, - payment_plan, - is_confirmed, - is_social, - referral_code, - show_balance, - } = action.payload - state.is_subscribed_to_emails = is_subscribed_to_emails - state.account_type = account_type - state.email = email - state.payment_plan = payment_plan - state.is_confirmed = is_confirmed - state.first_name = first_name - state.last_name = last_name - state.profile_picture_link = profile_picture_link - state.is_active = is_active - state.is_staff = is_staff - state.created_at = created_at - state.username = username - state.is_social = is_social - state.referral_code = referral_code - state.show_balance = show_balance + state.user = action.payload }) builder.addCase(getAllInfo.pending, (state) => { state.status = 'pending' @@ -171,10 +57,12 @@ export const userSlice = createSlice({ builder.addCase(getAllInfo.rejected, (state) => { state.status = 'failed' }) - builder.addCase(unfollowEmail.fulfilled, (state, action) => { - state.is_subscribed_to_emails = !state.is_subscribed_to_emails + builder.addCase(unfollowEmail.fulfilled, ({ user }, action) => { + if (!user) return + user.is_subscribed_to_emails = !user.is_subscribed_to_emails }) }, }) -export const { addReferral } = userSlice.actions +export const {} = userSlice.actions + @@ -0,0 +1,74 @@ +import { Device } from '#/shared/lib/types/entities' + +export type UserDTO = { + uid: string + first_name: string + last_name: string + username: string + created_at: string + email: string + is_active: boolean + is_staff: boolean + show_balance: boolean + is_confirmed: boolean + is_social: boolean + social_auth: string[] + is_subscribed_to_emails: boolean + profile_picture_link: string | null + account_type: string + referral_code: { + code: string + } + token: { + access: string + refresh: string + } + payment_plan: { + uid: string + plan: { + uid: string + price: string + tokens_per_plan: string + title: string + duration: string + accessed_models: string[] | null + } + last_payment_at: string + next_payment_at: string + current_token_balance: number + } +} + +export type AccountType = 'regular' | 'business_host' | 'business_account' + +export type SettingType = 'sidemenu' + +export type SettingValueType = { + sidemenu_state?: 'opened' | 'closed' +} + +export interface IUserSetting { + id: string + device: Device + type: SettingType + value: SettingValueType +} + +export interface CreateUserUtmDTO { + utm_source: string + utm_medium: string + utm_campaign: string + utm_term: string + utm_content: string +} + +export interface CreateUserDTO { + email: string + password: string +} + +export interface CreateUserRefererDTO { + referer?: string | null +} + +export type CreateUserWithRelations = CreateUserDTO & CreateUserRefererDTO & CreateUserUtmDTO & {} @@ -0,0 +1 @@ +export * from './dto.user' \ No newline at end of file @@ -1 +1,4 @@ export { getAllInfo, userSlice } from './model/user-type-slice' +export * from './api' +export * from './types' +export * from './model' @@ -13,6 +13,8 @@ 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, setOpen, @@ -48,7 +50,7 @@ export const ApiKeyModal = ({ ) : await accountApi.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,6 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; + padding-right: 8px; +} @@ -0,0 +1,86 @@ +import React from 'react' +import styles from './bot-params-map.module.scss' + +import { SwitchFilter } from '#/features/switch-filter/' +import { InputFilter } from '#/features/input-filter/ui/input-filter' +import { useChatBotParams } from '#/entities/model-entity' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { RangeSlider } from '#/shared/ui/range-slider' +import { CommonSelect } from '#/shared/ui/common-select' + +interface BotParamsMap {} + +export function BotParamsMap({}: BotParamsMap) { + const { infrerenceValues, setInferenceValueByKey, inferenceParams } = useChatBotParams() + + return ( +
+ {inferenceParams.map(({ key, type, name, description, values }) => { + if (type == 'floatrange' || type == 'intrange') { + return ( + + setInferenceValueByKey(key, value)} + max={values.stop} + min={values.start} + step={values.step} + /> + + ) + } + if (type == 'bool') { + return ( + setInferenceValueByKey(key, value)} + name={name} + description={description} + /> + ) + } + + if (type == 'choices') { + return ( + setInferenceValueByKey(key, value)} + withoutTooltip + items={values.availables.map((a) => { + return { + value: a[0], + label: a[1], + } + })} + /> + ) + } + + if (type == 'int' || type == 'str') { + return ( + setInferenceValueByKey(key, value)} + withoutTooltip={!description || description.length == 0} + onlyNumber={type == 'int'} + /> + ) + } + })} +
+ ) +} @@ -0,0 +1 @@ +export * from './bot-params-map' \ No newline at end of file @@ -1,97 +0,0 @@ -import React from 'react' -import { Stack } from '@mui/material' - -import { CheckboxFilter } from '#/app/components/filters/checkbox_filter' -import { InputFilter } from '#/app/components/filters/input_filter' -import { SelectFilter } from '#/app/components/filters/select_filter' -import { Slide } from '#/app/components/filters/slide_filter' -import { setParams } from '#/app/store/model-parametres-store' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { IModelParams } from '#/shared/api/models/models' - -interface IProps { - params: IModelParams[] - currentVersion: string -} -export default function BotParamsMap({ params, currentVersion }: IProps) { - const includeParams = useAppSelector((state) => state.params.params) - const dispatch = useAppDispatch() - - const setNewParam = (payload: { [key: string]: string | number | number[] | boolean }) => { - dispatch(setParams(payload)) - } - - React.useEffect(() => { - if (currentVersion && params) { - const res = params.map((el) => - el.versions.length !== 0 && !el.versions.includes(currentVersion) ? null : el - ) - if (res.every((el) => el === null)) { - setNewParam({}) - } - } - }, [currentVersion, params]) - - return ( - - {params.map((item, idx) => { - if (item.versions.length === 0 || item.versions.includes(currentVersion)) { - if (item.type == 'floatrange' || item.type == 'intrange') { - return ( - - ) - } - if (item.type == 'bool') { - return ( - - ) - } - - if (item.type == 'list') { - return ( - - ) - } - - if (item.type == 'int' || item.type == 'str') { - return ( - - ) - } - } - })} - - ) -} @@ -34,7 +34,11 @@ export const useChangePassword = () => { return setError('newPassword', { message: 'Пароли не совпадают' }) } - const { data, status } = await changePassword(modal.getStoreProperty('email')!, password, newPassword) + const { data, status } = await changePassword( + modal.getStoreProperty('email')!, + password, + newPassword + ) if (status !== 200) return showMessage(data.detail) @@ -0,0 +1 @@ +export * from './use-chat-bot-input' \ No newline at end of file @@ -0,0 +1,58 @@ +import { useAppSelector } from '#/app/store/store' +import { MessageSend } from '#/entities/message' +import { Inference, useChatBotParams } from '#/entities/model-entity' +import { acceptTypes } from '#/shared/common-load-file/config' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useMemo, useState } from 'react' + +export function useChatBotInput( + inference: Inference | null, + sendMessage: (data: MessageSend) => Promise +) { + const { showMessage } = useShowDataStore() + const [file, setFile] = useState(null) + const { infrerenceValues } = useChatBotParams() + const [valueInput, setValueInput] = useState('') + + const inputTypes = useMemo(() => { + if (!inference?.inputs) return [] + + return inference.inputs + .map(({ type }) => acceptTypes[type]) + .filter((value): value is string => value !== null) + }, [inference?.inputs]) + + const onSendMessage = async (content: string, required: (string | null)[]) => { + const filterFile = file && inputTypes.length === 0 ? null : file + if (file && inputTypes.length === 0) { + setFile(null) // Если модель не поддерживает отправку файлов, сбрасываем файл + showMessage('Модель не поддерживает отправку файлов') + } + + if (required.includes('text') && content === '') return showMessage('Введите сообщение!') + + if (required.includes('image') && file === null) return showMessage('Прикрепите изображение!') + + if (!inference) return showMessage('Не выбрана версия модели') + + await sendMessage({ + content, + file: filterFile, + info: { + inference: inference.slug, + ...infrerenceValues, + }, + }) + } + + + + return { + onSendMessage, + file, + setFile, + valueInput, + setValueInput, + inputTypes, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-chat-bot-pagination' \ No newline at end of file @@ -0,0 +1,149 @@ +import { useAppSelector } from '#/app/store/store' +import { getImagesBySlug, Message, useChatBotMessages } from '#/entities/message' +import { Device } from '#/shared/lib/types/entities' +import { getImagesGalery } from '#/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { createRef, useEffect, useMemo, useRef, useState } from 'react' +import { LimitSize, Limit } from '../types' +import { useRouter } from 'next/router' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useThemeAndDevice } from '#/shared/lib/hooks' +import { getMessagesBySlug } from '#/entities/message' +import { makePrivateRequest } from '#/shared/api' +import { useCurrentChat } from '#/features/chats' +import { CommonScrollbarRef } from '#/shared/ui/scrollbar' + +export function useChatBotPagination() { + const { currentChat } = useCurrentChat() + + const refScroll = useRef(null) + + const scrollContainer = createRef() + + const offset = useRef(0) + + const observer = useRef(null) + + const [isHidden, setIsHidden] = useState(false) + + const { messages, setMessages, loading, setLoading, loaded, setLoaded } = useChatBotMessages() + + const { showMessage } = useShowDataStore() + + const { data } = useSession() + + const messagesWithRefs = useMemo( + () => messages.map((m) => ({ ...m, ref: createRef() })), + [messages] + ) + + // TODO: Подумать как написать лучше + useEffect(() => { + setMessages([]) + offset.current = 0 + setLoaded(false) + if (currentChat && observer.current) { + observer.current.disconnect() + observer.current = null + onObserverMounted() + } + if (!scrollContainer.current) return + scrollContainer.current.scrollToBottom() + }, [currentChat]) + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 10, + firstLimit: 40, + }, + medium: { + active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), + limit: 30, + firstLimit: 50, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 45, + firstLimit: 70, + }, + } + + const fetchMessages = makePrivateRequest(async (count?: number) => { + if (!data) return + + setLoading(true) + + const { currentChat } = useCurrentChat.getState() + + if (!currentChat) return + + const { data: answer, ...response } = await getMessagesBySlug( + currentChat, + offset.current, + count || 10 + ) + + setLoading(false) + + setLoaded(true) + + if (response.status >= 400 || !Array.isArray(answer)) return showMessage('Ошибка загрузки чата') + + const { messages } = useChatBotMessages.getState() + + setMessages([...answer.reverse(), ...messages]) + + offset.current += answer.length + }) + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (isHidden) return setIsHidden(false) + + const active = Object.values(limits).find((item) => item.active) + + if (!active) return + + if (offset.current > 0) { + fetchMessages(active.limit) + } else { + fetchMessages(active?.firstLimit) + } + } + + function onObserverMounted() { + if (!refScroll.current || observer.current) return + + observer.current = new IntersectionObserver(callback) + + observer.current.observe(refScroll.current) + } + + function isHiddenHandler() { + if (document.visibilityState === 'hidden') return setIsHidden(true) + setIsHidden(false) + } + + useEffect(() => { + document.addEventListener('visibilitychange', isHiddenHandler) + return () => document.removeEventListener('visibilitychange', isHiddenHandler) + }, []) + + return { + onObserverMounted, + messages, + loading, + setLoading, + offset, + setMessages, + fetchMessages, + scrollContainer, + messagesWithRefs, + refScroll, + loaded, + observer, + } +} @@ -0,0 +1 @@ +export * from './limits' \ No newline at end of file @@ -0,0 +1,7 @@ +export type LimitSize = 'small' | 'medium' | 'large' + +export interface Limit { + active: boolean + limit: number + firstLimit: number +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-chat-imagel-load' \ No newline at end of file @@ -0,0 +1,37 @@ +import { useState, useCallback } from 'react' + +export function useChatImageLoad( + setFile: React.Dispatch> +) { + + const [resendValue, setResendValue] = useState('') + + const onLoadImage = useCallback( + (event: React.ChangeEvent | null, file?: File) => { + if (event?.target.files) { + setFile(event.target.files[0]) + } + if (file) { + setFile(file) + } + }, + [] + ) + + const handleSetResetValue = useCallback( + (value: string) => { + if (resendValue === value) { + setResendValue((prev) => prev + ' ') + } else { + setResendValue(value) + } + }, + [resendValue] + ) + + return { + onLoadImage, + resendValue, + handleSetResetValue + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,31 @@ +.container { + padding: 6px 0px !important; + color: var(--air-color); +} + +.popup { + &__button { + font-size: 15px; + display: flex; + font-weight: 500; + align-items: center; + gap: 5px; + width: max-content; + padding: 8px 14px; + transition: all 0.3s ease; + width: 100%; + + &:hover { + backdrop-filter: brightness(90%); + } + } +} + +.container { + padding: 6px 0px !important; + color: var(--air-color); + + @media screen and (max-width: 1000px) { + padding: 10px !important; + } +} \ No newline at end of file @@ -0,0 +1,45 @@ +import { PopupTemplate, PopupTemplateProps, getPopupById } from '#/shared/ui/popup' +import React, { Dispatch, SetStateAction, useMemo } from 'react' +import styles from './chat-item-actions-popup.module.scss' +import Rename2Svg from '#/assets/svg/rename-2.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' +import { useChatActions } from '#/entities/chat' +import { useMediaQuery } from 'usehooks-ts' + +export interface ChatItemActionsPopupProps extends PopupTemplateProps { + id: string + setRename: Dispatch> +} + +export const ChatItemActionsPopup = ({ id, setRename, ...props }: ChatItemActionsPopupProps) => { + const { onRemoveChat } = useChatActions() + + const isMobile = useMediaQuery('(max-width: 1000px)') + + const popup = getPopupById(id) + + const renamePopup = getPopupById(`rename-${id}`) + + return ( + +
+ + +
+
+ ) +} @@ -0,0 +1 @@ +export * from './chat-item-actions-popup' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,9 @@ +.popup{ + &__save{ + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + } +} \ No newline at end of file @@ -0,0 +1,52 @@ +import React, { useState } from 'react' + +import styles from './chat-mobile-rename-popup.module.scss' +import { PopupTemplate, getPopupById } from '#/shared/ui/popup' +import { CommonInput } from '#/shared/ui/common-input' +import RenameSvg from '#/assets/svg/rename.svg?react' +import { useChatActions } from '#/entities/chat' +import { title } from 'process' +import { CommonButton } from '#/shared/ui/button' + +interface ChatMobileRenameProps { + id: string +} + +export const ChatMobileRename = ({ id }: ChatMobileRenameProps) => { + const { onRenameChat } = useChatActions() + + const popup = getPopupById('rename-' + id) + + const [value, setValue] = useState('') + + return ( + +
+ setValue(e.target.value)} + /> + + { + onRenameChat(id, value) + popup.setState(false) + }} + > + + Сохранить + +
+
+ ) +} @@ -0,0 +1,2 @@ +export * from './use-chats' +export * from './use-current-chat' \ No newline at end of file @@ -0,0 +1,49 @@ +import React, { Dispatch, SetStateAction, createRef, useEffect, useMemo, useState } from 'react' +import axios from 'axios' +import { useSession } from 'next-auth/react' + +import { createChat, getAllChats } from '#/shared/api/endpoints' +import { API_URL } from '#/shared/lib/constants' +import { useCurrentChat } from '.' +import { useRouter } from 'next/router' +import { ChatDTO, getChats, postChat, useChatsStore } from '#/entities/chat' +import { makePrivateRequest } from '#/shared/api' +import { useShowDataStore } from '#/shared/lib/hooks' + +export function useChats() { + const { query } = useRouter() + + const model = useMemo(() => query.slug as string, [query.slug]) + + const { chats, setChats } = useChatsStore() + + const { showMessage } = useShowDataStore() + + const { currentChat: chat, setCurrentChat } = useCurrentChat() + + const fetchChats = makePrivateRequest(async () => { + const { data, status } = await getChats(model) + + if (status !== 200) return showMessage('Ошибка при получении чатов') + + setChats(data) + + if (!data.length) return + + setCurrentChat(data[0].uid) + }) + + const chatsWithRefs = useMemo( + () => chats.map((c) => ({ ...c, ref: createRef() })), + [chats] + ) + + return { + chat, + chats, + fetchChats, + createChat, + setCurrentChat, + chatsWithRefs, + } +} @@ -0,0 +1,17 @@ +import { create } from 'zustand' + +export interface CurrentChatStore { + currentChat: string | null + setCurrentChat: (currentChat: string | null) => void +} + +export const useCurrentChat = create((set, get) => { + function setCurrentChat(currentChat: string | null) { + set({ currentChat }) + } + + return { + currentChat: null, + setCurrentChat + } +}) @@ -1,2 +1 @@ -export type { Chat, ChatsReturn } from './use-chats' -export { useChats } from './use-chats' +export * from './model' \ No newline at end of file @@ -1,96 +0,0 @@ -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react' -import axios from 'axios' -import { useSession } from 'next-auth/react' - -import { createChat, getAllChats } from '#/shared/api/endpoints' -import { API_URL } from '#/shared/lib/constants' - -export type Chat = { - uid: string - title: string - created_at: string -} - -export type ChatsReturn = { - currentChat: string | null - chats: Chat[] | null - chatSetting: HTMLButtonElement | null - isTryRename?: string | undefined - desktop?: boolean - setIsTryRename?: Dispatch> - setChatSetting: Dispatch> - removeChat: (uid: string) => void - createNewChat: () => void - handleClickChatSetting: (event: React.MouseEvent) => void - setChat: (value: string | null) => void -} - -export function useChats(model: string): ChatsReturn { - const [chats, setChats] = useState(null) - - const [currentChat, setCurrentChat] = useState(null) - - const [isTryRename, setIsTryRename] = useState(undefined) - - const [chatSetting, setChatSetting] = useState(null) - - const { data } = useSession() - - useEffect(() => { - if (model && data?.access) { - getAllChats(model, data?.access).then((res) => { - setChats(res) - if (res !== null && res.length > 0 && res[0] !== null) { - setCurrentChat(res[0].uid) - } - }) - } - }, [model, data?.access]) - - const createNewChat = async () => { - const chat = await createChat(model, data?.access) - - if (chat === null) { - return - } - - setChats((prev) => [...prev!, chat]) - setCurrentChat(chat.uid) - } - - const removeChat = async (uid: string) => { - try { - setChatSetting(null) - const { status } = await axios.delete(API_URL + `/chats/${uid}/`, { - headers: { Authorization: `Bearer ${data?.access}` }, - }) - - if (status === 204) { - let newChats = chats!.filter((el) => el.uid !== uid) - setChats(newChats) - if (newChats.length !== 0) setCurrentChat(newChats[0].uid) - } - } catch (e) {} - } - - const handleClickChatSetting = (event: React.MouseEvent) => { - setChatSetting(event.currentTarget) - } - - const setChat: any = (value: string | null) => { - setCurrentChat(value) - } - - return { - currentChat, - chats, - chatSetting, - setChatSetting, - removeChat, - createNewChat, - handleClickChatSetting, - isTryRename, - setIsTryRename, - setChat, - } -} @@ -5,11 +5,12 @@ import { sendReport } from '../api' import { makePrivateRequest } from '#/shared/api' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { useRouter } from 'next/router' +import { useUserSelector } from '#/entities/user-account' const MAX_FILE_SIZE = 4.5 * 1024 * 1024 // 4.5 MB export const useErrorReport = () => { - const email = useAppSelector((state) => state.user.email) + const {email} = useUserSelector() const [file, setFile] = React.useState() const { showMessage } = useShowDataStore() @@ -19,14 +19,21 @@ export const ErrorReportPlate = () => { {isSend ? (

Спасибо!

-

Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную при регистрации

+

+ Ваше сообщение направлено нашим специалистам. Ответ придет на почту, указанную + при регистрации +

) : (
e.stopPropagation()}>

Сообщить об ошибке

{ id='file-input' /> @@ -52,7 +63,10 @@ export const ErrorReportPlate = () => {

Добавлено 1 изображение: {file.name}

-
@@ -0,0 +1,43 @@ +.area { + padding: 40px; + min-width: 660px; + display: flex; + align-items: center; + justify-content: center; + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='20' ry='20' stroke='%23A4AAB50D' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='round'/%3e%3c/svg%3e"); + border-radius: 20px; + background-color: var(--new-ui-element-bg); + + &__input { + display: none; + } + &__icon { + margin-bottom: 18px; + } + + &__title { + margin-bottom: 5px; + color: var(--new-ui-gray-color); + } + + &__content { + margin-bottom: 25px; + text-align: center; + } + + &__text { + color: var(--new-ui-gray-color); + } + + &__button { + display: flex; + align-items: center; + gap: 12px; + } +} + +.container { + display: flex; + flex-direction: column; + align-items: center; +} @@ -0,0 +1,55 @@ +import React, { useRef } from 'react' +import styles from './image-area.module.scss' + +import PlusSvg from '#/assets/svg/plus.svg?react' +import { c } from '#/shared' +import { CommonButton } from '#/shared/ui/button' + +interface ImageAreaProps { + files: File[] + setFiles: React.Dispatch> +} + +export const ImageArea = ({ files, setFiles }: ImageAreaProps) => { + const fileInput = useRef(null) + + function onDrop(e: React.DragEvent) { + e.preventDefault() + setFiles(Array.from(e.dataTransfer.files)) + } + + function onInputChange(e: React.ChangeEvent) { + e.preventDefault() + + const { target } = e + + if (!target.files) return + + const file = target.files.item(0)! + + if (['png', 'jpg', 'jpeg'].some((ext) => file.name.includes(ext))) { + setFiles([file]) + } + } + + return ( +
e.preventDefault()} className={styles.area}> +
+ +
+

Загрузить фото

+

или просто перетащите мышью

+
+ + fileInput.current?.click()} + className={styles.area__button} + variant='primary' + > + + Загрузить + +
+
+ ) +} @@ -0,0 +1 @@ +export * from './image-area' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,62 +1,80 @@ import { getUserBalance } from '#/entities/balance' -import { Message, MessageSend, sendImage } from '#/entities/message' +import { + Message, + MessageSend, + postImageMessage, + useImageBotMessages, + useImageMessagesEvents, +} from '#/entities/message' import { useAppDispatch } from '#/app/store/store' import { Device } from '#/shared/lib/types/entities' -import { formDataHelper } from '#/widgets/messages' +import { MutableRefObject, RefObject, useEffect, useState } from 'react' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' import { useSession } from 'next-auth/react' -import { Dispatch, RefObject, SetStateAction, useEffect, useState } from 'react' +import { makePrivateRequest } from '#/shared/api' +import { useEventSource } from '#/shared/lib/event-source' +import { eventBus } from '#/shared/classes' export function useImageBotCreateImage( - showError: (message: string) => void, - type: string, device: Device, - setMessages: Dispatch>, - mobileScrollContainer: RefObject + mobileScrollContainer: RefObject, + offset: MutableRefObject ) { - const [isComplete, setIsComplete] = useState(false) + const { botParams } = useImageBot() const [createLoading, setCreateLoading] = useState(false) - const { data } = useSession() + const { makeEvent } = useImageMessagesEvents() + + const { showMessage } = useShowDataStore() + + const { setMessages, messages } = useImageBotMessages() const dispatch = useAppDispatch() - const createImage = async (dataForSend: MessageSend) => { - const { content, file } = dataForSend + const { data: session } = useSession() + + const { addOpenCallback, addCloseEvent } = useEventSource() + + const createImage = makePrivateRequest(async (dto: MessageSend) => { + if (!botParams) return - setIsComplete(false) setCreateLoading(true) - const dataSending = file ? formDataHelper(file, dataForSend) : dataForSend + if (!dto.file) delete dto.file - const { data: messages } = await sendImage(type, dataSending, data?.access) + const { data, status } = await postImageMessage(botParams.slug, dto) setCreateLoading(false) - if (typeof messages === 'string') { - showError(messages) - return - } + if (status !== 200) + return showMessage((data as { detail: string }).detail ?? 'Ошибка при получении сообщений') - dispatch(getUserBalance(data?.access)) + dispatch(getUserBalance(session?.access)) - setMessages((prev: Message[]) => { - if (!prev || !prev.length) return messages + const messages = useImageBotMessages.getState().messages - if (device === 'desktop') { - return [...messages, ...prev] - } - return [...prev, ...messages] - }) + offset.current++ + + const answer = (data as Message[])[1] - setIsComplete(true) + if (device === 'desktop') setMessages([answer, ...messages]) + else setMessages([...messages, answer]) - if (device === 'desktop') { + setTimeout(() => eventBus.publish(`image-generate-${answer.uid}`, true), 100) + + makeEvent('images') + + addCloseEvent('images', () => { + eventBus.publish(`image-generate-${answer.uid}`, false) + }) + + if (device === 'desktop') return window.scrollTo({ top: 0, behavior: 'smooth', }) - } setTimeout(() => { if (!mobileScrollContainer.current) return @@ -66,11 +84,14 @@ export function useImageBotCreateImage( behavior: 'smooth', }) }, 500) - } + }) + + useEffect(() => { + console.log(messages) + }, [messages]) return { createImage, - isComplete, createLoading, } } @@ -1 +1 @@ -export * from './use-images-uniq-input' \ No newline at end of file +export * from './use-images-model-input' \ No newline at end of file @@ -0,0 +1,45 @@ +import { MessageSend } from '#/entities/message' +import { Inference, useImageBotParams } from '#/entities/model-entity' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useMemo, useState } from 'react' + +export function useImageBotInput(sendMessage: (data: MessageSend) => Promise) { + const { showMessage } = useShowDataStore() + const [file, setFile] = useState(null) + const { infrerenceValues, inference, inferenceInputs: inputs } = useImageBotParams() + + const [valueInput, setValueInput] = useState('') + + const inputTypes = useMemo(() => inputs.map((p) => p.type), [inputs]) + + const requiredTypes = useMemo(() => inputs.filter((p) => p.required).map((p) => p.type), [inputs]) + + const onSendMessage = async (content: string, required: (string | null)[]) => { + if (required.includes('text') && content === '') return showMessage('Введите сообщение!') + + if (required.includes('zip') && file === null) showMessage('Прикрепите архив!') + + if (required.includes('image') && file === null) return showMessage('Прикрепите изображение!') + + if (!inference) return showMessage('Не выбрана версия модели') + + await sendMessage({ + content, + file, + info: { + inference: inference.slug, + ...infrerenceValues, + }, + }) + } + + return { + onSendMessage, + file, + setFile, + valueInput, + setValueInput, + inputTypes, + requiredTypes, + } +} @@ -1,64 +0,0 @@ -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' -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 { showMessage } = useShowDataStore() - - 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)) { - showMessage('Введите сообщение!') - return false - } - if (required.includes('image') && image === null) { - showMessage('Прикрепите изображение!') - return false - } - if (required.includes('zip') && image === null) { - showMessage('Прикрепите архив!') - 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, - } -} @@ -1 +1,2 @@ +export * from './use-images-bot-pagination-old' export * from './use-images-bot-pagination' \ No newline at end of file @@ -0,0 +1,150 @@ +import { useAppSelector } from '#/app/store/store' +import { getImagesBySlug, Message, useImageBotMessages } from '#/entities/message' +import { Device } from '#/shared/lib/types/entities' +import { getImagesGalery } from '#/widgets/messages' +import { useMediaQuery } from '@mui/material' +import { useSession } from 'next-auth/react' +import { useEffect, useRef, useState } from 'react' +import { LimitSize, Limit } from '../types' +import { useRouter } from 'next/router' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useImageObjectId } from '#/features/image-object-id' + +export function useImageBotPaginationOld(deviceType: Device) { + const refScrollMobile = useRef(null) + const refScrollDesktop = useRef(null) + const mobileScrollContainer = useRef(null) + const offset = useRef(0) + const observer = useRef(null) + + const [isHidden, setIsHidden] = useState(false) + + const { setImageObjectId } = useImageObjectId() + + const { query } = useRouter() + + const { messages, setLoading, setMessages, loading } = useImageBotMessages() + + const { showMessage } = useShowDataStore() + + const { data } = useSession() + + const limits: Record = { + small: { + active: useMediaQuery('(max-height: 600px)'), + limit: 20, + firstLimit: 30, + }, + medium: { + active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), + limit: 30, + firstLimit: 50, + }, + large: { + active: useMediaQuery('(min-height: 900px)'), + limit: 45, + firstLimit: 70, + }, + } + + const fetchMessages = async (count?: number) => { + if (!data) return + + setLoading(true) + + const { data: answer, ...response } = await getImagesBySlug( + query.slug as string, + data.access, + offset.current, + count || 10 + ) + + if (response.status >= 400 || !Array.isArray(answer.messages)) + return showMessage('Ошибка загрузки чата') + + const messages = useImageBotMessages.getState().messages + + if (deviceType === 'desktop') setMessages([...messages, ...answer.messages]) + else setMessages([...answer.messages.reverse(), ...messages]) + + offset.current += answer.messages.length + + // console.log(offset.current) + + setImageObjectId(answer.id) + } + + const callback = async function (entries: IntersectionObserverEntry[]) { + if (!entries[0].isIntersecting) return + + if (isHidden) return setIsHidden(false) + + if (deviceType === 'desktop') { + const active = Object.values(limits).find((item) => item.active) + + if (!active) return + + return fetchMessages(offset.current > 0 ? active.limit : active?.firstLimit) + } + + const { current } = mobileScrollContainer + + if (!current) return + + const scrollBottom = current.scrollHeight - current.scrollTop + + await fetchMessages() + + setTimeout(() => { + const { current } = mobileScrollContainer + + if (!current) return + + if (offset.current === 0) current.scrollTop = current.scrollHeight - scrollBottom + else { + current.scroll({ + top: current.scrollHeight - scrollBottom, + behavior: 'smooth', + }) + } + + setLoading(false) + }, 500) + } + + function onObserverMounted() { + setMessages([]) + + const currentObserver = + deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current + + if (!currentObserver || observer.current) return + + observer.current = new IntersectionObserver(callback, { rootMargin: '400px' }) + + observer.current.observe(currentObserver!) + } + + function isHiddenHandler() { + if (document.visibilityState === 'hidden') return setIsHidden(true) + setIsHidden(false) + } + + useEffect(() => { + document.addEventListener('visibilitychange', isHiddenHandler) + return () => document.removeEventListener('visibilitychange', isHiddenHandler) + }, []) + + return { + refScrollMobile, + refScrollDesktop, + onObserverMounted, + messages, + loading, + setLoading, + offset, + setMessages, + fetchMessages, + mobileScrollContainer, + } +} @@ -1,28 +1,21 @@ -import { useAppSelector } from '#/app/store/store' -import { getImagesBySlug, Message } from '#/entities/message' -import { Device } from '#/shared/lib/types/entities' -import { getImagesGalery } from '#/widgets/messages' +import { getImagesGalery, useMessagesStore } from '#/widgets/messages' import { useMediaQuery } from '@mui/material' import { useSession } from 'next-auth/react' -import { useEffect, useRef, useState } from 'react' +import { useMemo, useRef } from 'react' + +import { useAppSelector } from '#/app/store/store' +import { getImagesBySlug, Message, useImageBotMessages } from '#/entities/message' +import { Device } from '#/shared/lib/types/entities' import { LimitSize, Limit } from '../types' import { useRouter } from 'next/router' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { useImageObjectId } from '#/features/image-object-id' -export function useImageBotPagination(deviceType: Device) { - const refScrollMobile = useRef(null) - const refScrollDesktop = useRef(null) - const mobileScrollContainer = useRef(null) +export function useImageBotPagination(container: React.RefObject) { + const refScroll = useRef(null) const offset = useRef(0) - const observer = useRef(null) - - const [isHidden, setIsHidden] = useState(false) - const { query } = useRouter() - - const [messages, setMessages] = useState([]) - - const [loading, setLoading] = useState(true) + const { messages, loading, setLoading, addMessages } = useMessagesStore() const { showMessage } = useShowDataStore() @@ -31,118 +24,81 @@ export function useImageBotPagination(deviceType: Device) { const limits: Record = { small: { active: useMediaQuery('(max-height: 600px)'), - limit: 20, - firstLimit: 30, + limit: 10, + firstLimit: 20, }, medium: { active: useMediaQuery('(min-height: 600px) and (max-height: 900px)'), - limit: 30, - firstLimit: 50, + limit: 15, + firstLimit: 30, }, large: { active: useMediaQuery('(min-height: 900px)'), - limit: 45, - firstLimit: 70, + limit: 20, + firstLimit: 40, }, } + const revertedMessages = useMemo(() => [...messages].reverse(), [messages]) + const fetchMessages = async (count?: number) => { if (!data) return setLoading(true) - const { data: answer, ...response } = await getImagesBySlug( - query.slug as string, + const { data: answer, ...response } = await getImagesGalery( data.access, offset.current, count || 10 ) + setLoading(false) - if (response.status >= 400 || !Array.isArray(answer)) + if (response.status >= 400 || !Array.isArray(answer.messages)) return showMessage('Ошибка загрузки чата') - if (deviceType === 'desktop') { - setMessages((prev) => [...prev, ...answer]) - } else { - setMessages((prev) => [...answer.reverse(), ...prev]) - } + addMessages(answer.messages) - offset.current += answer.length + offset.current = offset.current + answer.messages.length } const callback = async function (entries: IntersectionObserverEntry[]) { if (!entries[0].isIntersecting) return - if (isHidden) return setIsHidden(false) - - if (deviceType === 'desktop') { - const active = Object.values(limits).find((item) => item.active) - - if (!active) return + if (!refScroll.current) return - if (offset.current > 0) { - fetchMessages(active.limit) - } else { - fetchMessages(active?.firstLimit) - } - } + const scrollBottom = container.current!.scrollHeight - container.current!.scrollTop - const { current } = mobileScrollContainer + const active = Object.values(limits).find((item) => item.active) - if (!current) return - - const scrollBottom = current.scrollHeight - current.scrollTop - - await fetchMessages() + if (offset.current > 0) await fetchMessages(active?.limit) + else await fetchMessages(active?.firstLimit) setTimeout(() => { - const { current } = mobileScrollContainer - - if (!current) return - - if (offset.current === 0) current.scrollTop = current.scrollHeight - scrollBottom - else { - current.scroll({ - top: current.scrollHeight - scrollBottom, - behavior: 'smooth', - }) - } - - setLoading(false) + if (!container.current) return + container.current.scroll({ + top: container.current!.scrollHeight - scrollBottom + 70, + behavior: 'smooth', + }) }, 500) } function onObserverMounted() { - const currentObserver = - deviceType === 'desktop' ? refScrollDesktop.current : refScrollMobile.current - - if (!currentObserver) return + if (!refScroll.current) return - observer.current = new IntersectionObserver(callback, { rootMargin: '400px' }) + const observer = new IntersectionObserver(callback, { + rootMargin: '400px', + }) - observer.current.observe(currentObserver!) + observer.observe(refScroll.current) } - function isHiddenHandler() { - if (document.visibilityState === 'hidden') return setIsHidden(true) - setIsHidden(false) - } - - useEffect(() => { - document.addEventListener('visibilitychange', isHiddenHandler) - return () => document.removeEventListener('visibilitychange', isHiddenHandler) - }, []) - return { - refScrollMobile, - refScrollDesktop, + refScroll, onObserverMounted, messages, loading, setLoading, - offset, - setMessages, fetchMessages, - mobileScrollContainer, + revertedMessages, } } @@ -0,0 +1,6 @@ +.container { + display: flex; + flex-direction: column; + gap: 16px; + padding-right: 8px; +} @@ -0,0 +1,86 @@ +import React from 'react' +import styles from './image-bot-params-map.module.scss' + +import { SwitchFilter } from '#/features/switch-filter/' +import { InputFilter } from '#/features/input-filter/ui/input-filter' +import { useChatBotParams, useImageBotParams } from '#/entities/model-entity' +import { CommonTooltip } from '#/shared/ui/tooltip' +import { RangeSlider } from '#/shared/ui/range-slider' +import { CommonSelect } from '#/shared/ui/common-select' + +interface ImageBotParamsMap {} + +export function ImageBotParamsMap({}: ImageBotParamsMap) { + const { infrerenceValues, setInferenceValueByKey, inferenceParams } = useImageBotParams() + + return ( +
+ {inferenceParams.map(({ key, type, name, description, values }) => { + if (type == 'floatrange' || type == 'intrange') { + return ( + + setInferenceValueByKey(key, value)} + max={values.stop} + min={values.start} + step={values.step} + /> + + ) + } + if (type == 'bool') { + return ( + setInferenceValueByKey(key, value)} + name={name} + description={description} + /> + ) + } + + if (type == 'choices') { + return ( + setInferenceValueByKey(key, value)} + withoutTooltip + items={values.availables.map((a) => { + return { + value: a[0], + label: a[1], + } + })} + /> + ) + } + + if (type == 'int' || type == 'str') { + return ( + setInferenceValueByKey(key, value)} + withoutTooltip={!description || description.length == 0} + onlyNumber={type == 'int'} + /> + ) + } + })} +
+ ) +} @@ -0,0 +1 @@ +export * from './image-bot-params-map' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,25 +1,22 @@ import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react' import { ImageWithState } from './types' import { Message } from '#/entities/message' +import { GALLERY_IMAGES, getModalById } from '#/features/modals' + +export const useImagesLibrary = (current: string | null, images: Message[]) => { + + const modal = getModalById(GALLERY_IMAGES) -export const useImagesLibrary = ( - setModal: Dispatch>, - current: string | null, - images: Message[] -) => { const [initialCount, setInitialCount] = useState(0) const esc = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() - setModal(false) + modal.setState(false) } } - const currentIndex = useMemo( - () => images.findIndex((item) => item.file === current), - [current] - ) + const currentIndex = useMemo(() => images.findIndex((item) => item.file === current), [current]) useEffect(() => { window.addEventListener('keydown', esc) @@ -29,6 +26,6 @@ export const useImagesLibrary = ( return { initialCount, setInitialCount, - currentIndex, + currentIndex, } } @@ -9,34 +9,23 @@ import { useImagesLibrary } from '../model' import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' import { useLibrarySwiper } from '../model/use-swiper' -import { ImageIcons, useImageIcons } from '#/widgets/messages' import { Typography } from '@mui/material' import { ModalImage } from './modal-image' -import { Message } from '#/entities/message' +import { Message, useImageIcons } from '#/entities/message' +import { GALLERY_IMAGES, getModalById, PlateTemplate } from '#/features/modals' +import { CSSTransition } from 'react-transition-group' interface IProps { - modal: boolean - setModal: Dispatch> - current: string | null - setCurrent?: (value: string | null) => void onSlideFalse?: (...args: any) => any reverse?: boolean images: Message[] + current: string | null } -export default function FullScreenModal({ - modal, - setModal, - current, - images, - onSlideFalse, - reverse = false, -}: IProps) { - const { initialCount, setInitialCount, currentIndex } = useImagesLibrary( - setModal, - current, - images - ) +export default function FullScreenModal({ images, onSlideFalse, reverse = false, current }: IProps) { + const modal = getModalById(GALLERY_IMAGES) + + const { currentIndex } = useImagesLibrary(current, images) const count = useRef(0) @@ -45,7 +34,6 @@ export default function FullScreenModal({ const { downloadFile } = useImageIcons() useEffect(() => { - // debugger if (reverse) setTimeout(() => { swiper?.slideNext() @@ -59,28 +47,7 @@ export default function FullScreenModal({ }, [images]) return ( -
setModal(false)} - > -
+
-
setModal(false)}> +
modal.setState(false)}> - {modal && ( + { - console.log(swiper?.activeIndex) - }} simulateTouch={false} initialSlide={currentIndex} onSwiper={(swiper) => setSwiper(swiper)} @@ -173,7 +138,7 @@ export default function FullScreenModal({ ))} - )} +
-
+ ) } @@ -13,6 +13,10 @@ gap: 15px; } +.container{ + background-color: rgba($color: #000000, $alpha: 0.8) !important; +} + .arrow { position: absolute; z-index: 1205; @@ -0,0 +1,70 @@ +.container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.select { + position: relative; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 16px 8px 16px 14px; + cursor: pointer; + + border-radius: 13px; + border: 2px solid #40404e; + &_open { + border: 2px solid #7f7df3; + } + + &__text { + font-size: 18px; + color: var(--new-ui-text-color); + line-height: 1.5; + } +} + +.icon { + color: #a6a5a5; +} + +html[data-theme='light'] { + .select { + border: 1px solid #e9e9e9; + &_open { + border: 1px solid #7f7df3; + } + } +} + +.dropdown { + position: absolute; + top: 60px; + left: 0; + right: 0; + z-index: 1000; + background-color: var(--new-ui-main-color); + width: 100%; + border-radius: 13px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, + rgba(0, 0, 0, 0.12) 0px 3px 14px 2px; + + &__option { + list-style: none; + padding: 10px 16px; + + &_active { + background-color: rgba(25, 118, 210, 0.12); + } + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active { + background-color: rgba(0, 0, 0, 0.2); + } + } + &__name { + font-size: 18px; + } +} @@ -0,0 +1,29 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useChatBot } from '#/entities/model-entity' +import { CommonSelect, SelectItem } from '#/shared/ui/common-select' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' + +interface ImageModelParamsSelectProps {} + +export const ImageModelParamsSelect = ({ ...props }: ImageModelParamsSelectProps) => { + const { botParams, inference, onSetInferenceParams } = useImageBot() + + const selectItems = useMemo(() => { + if (!botParams) return [] + return botParams.inferences.map(({ description, slug, name }) => ({ + value: slug, + label: name, + description, + })) + }, [botParams]) + + const value = useMemo(() => (inference ? inference.slug : undefined), [inference]) + + return ( + onSetInferenceParams(slug)} + items={selectItems} + /> + ) +} @@ -0,0 +1 @@ +export * from './image-model-params-select' @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1,23 @@ +import { ImageStyle } from '#/entities/image-style' +import { getModalById, PLATE_IMAGE_STYLES } from '#/features/modals' +import { useChosenStyle } from '#/widgets/images-style' +import { useEffect } from 'react' + +export function useImageInputStyles() { + const modal = getModalById(PLATE_IMAGE_STYLES) + + const { setStyle } = useChosenStyle() + + function onSetStyle(data: ImageStyle) { + setStyle(data) + modal.setState(false) + } + + function onStylesButtonClick() { + modal.setState(true, { + callback: onSetStyle, + }) + } + + return { onStylesButtonClick } +} @@ -0,0 +1,71 @@ +import { useAppSelector } from '#/app/store/store' +import { Message, MessageSend, postImageMessage } from '#/entities/message' +import { Model, useImageBotParams } from '#/entities/model-entity' +import { useMessagesStore } from '#/widgets/messages' +import { useSession } from 'next-auth/react' +import { useState } from 'react' + +export function useImageInput(model: Model | null, container: React.RefObject) { + const [text, setText] = useState('') + const [image, setImage] = useState(null) + const [loading, setLoading] = useState(false) + + const { inference } = useImageBotParams() + + const { setMessages, messages, addOptimistic } = useMessagesStore() + + const includeParams = useAppSelector((state) => state.params.params) + + const { data: session } = useSession() + + function createMessage() { + if (!inference) return + const data = { + file: image, + content: text, + info: { + ...includeParams, + inference: inference.id, + }, + } + + onSend(data) + } + + async function onSend(param: MessageSend) { + if (!model || !session) return + + addOptimistic(param.content, param.info, model.slug) + + setLoading(true) + + setTimeout(() => { + if (!container.current) return + + const scrollBottom = container.current.scrollHeight - container.current.scrollTop + + container.current.scroll({ + top: container.current.scrollHeight + scrollBottom, + behavior: 'smooth', + }) + }, 500) + + const { data } = await postImageMessage(model.slug, param) + + setLoading(false) + + if (typeof data === 'string') return + + setMessages([...(data as Message[]), ...messages]) + } + + return { + onSend, + text, + setText, + image, + setImage, + createMessage, + loading, + } +} @@ -0,0 +1,2 @@ +export * from './image-input-styles' +export * from './image-input' \ No newline at end of file @@ -0,0 +1,122 @@ +.container { + display: flex; + align-items: center; + gap: 22px; +} +.wrapper { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; + border-radius: 15px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.2); + background: var(--new-ui-element-bg); +} + +.controls { + display: flex; + flex-direction: column; + gap: 5px; +} + +.stroke { + height: 60px; + width: 1px; + background-color: #40404e; + + @media screen and (max-width: 800px) { + display: none; + } +} + +.rounded { + background-color: rgba($color: #a4aab5, $alpha: 0.1); + border-radius: 100%; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + + &__svg { + padding-left: 2px; + padding-top: 2px; + } +} + +.buttons { + display: flex; + align-items: center; + gap: 10px; + + @media screen and (max-width: 800px) { + flex-direction: column; + } +} + +.button { + background-color: rgba($color: #a4aab5, $alpha: 0.05); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #a4aab5; + gap: 5px; + height: 61px; + width: 61px; + font-size: 12px; + font-weight: 500; + border-radius: 15px; +} + +.textarea { + border: none; + outline: none; + min-width: 400px; + resize: none; + padding: 0 10px; + height: 50px !important; + + @media screen and (max-width: 800px) { + height: 70px !important; + min-width: 300px; + } + + @media screen and (max-width: 500px) { + min-width: 200px; + } +} + +.main { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + @media screen and (max-width: 800px) { + display: none; + } + &__settings { + position: relative; + } +} + +.mobcontols { + display: none; + gap: 10px; + position: relative; + &__generate { + width: 100%; + } + &__settings { + min-width: 50px; + min-height: 50px; + padding: 0px; + display: flex; + justify-content: center; + align-items: center; + } + + @media screen and (max-width: 800px) { + display: flex; + } +} @@ -0,0 +1,112 @@ +import React, { MouseEventHandler, useState } from 'react' +import styles from './image-input.module.scss' +import { c } from '#/shared' +import BranchesSvg from '#/assets/svg/branches.svg?react' +import PaintSvg from '#/assets/svg/paint.svg?react' +import RatioSvg from '#/assets/svg/ratio.svg?react' +import CubesSvg from '#/assets/svg/cubes.svg?react' +import SettingsSvg from '#/assets/svg/settings.svg?react' +import { SendBtn } from '#/shared/ui/send-button' +import { useImageInput, useImageInputStyles } from '../model' +import { CommonButton } from '#/shared/ui/button' +import { ImageSettingsPopup, useImageBotMessages } from '#/entities/message' +import { getPopupById, POPUP_IMAGE_SETTINGS } from '#/shared/ui/popup' +import { Model } from '#/entities/model-entity' +import { useImageBotParams } from '#/entities/model-entity' + +export interface ImageInputProps { + model: Model | null + container: React.RefObject +} + +export const ImageInput = ({ model, container }: ImageInputProps) => { + const { createMessage, text, setText, loading } = useImageInput(model, container) + + const { inferenceParams, inference } = useImageBotParams() + + const { onStylesButtonClick } = useImageInputStyles() + + const imageSettingsPopup = getPopupById(POPUP_IMAGE_SETTINGS) + + return ( +
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+
+ +
+ + Сгенерировать + + { + if (!model) return + + imageSettingsPopup.toogleState({ + botParams: inferenceParams, + currentVersion: inference?.id, + }) + }} + className={styles.mobcontols__settings} + variant='primary-outline' + > + + + + +
+
+ ) +} @@ -0,0 +1 @@ +export * from './image-input' @@ -0,0 +1,2 @@ +export * from './ui' +export * from './model' @@ -0,0 +1 @@ +export * from './use-image-object-id' \ No newline at end of file @@ -0,0 +1,17 @@ +import { create } from 'zustand' + +export interface ImageObjectIdStore { + imageObjectId: string | null + setImageObjectId: (imageObjectId: string | null) => void +} + +export const useImageObjectId = create((set, get) => { + function setImageObjectId(imageObjectId: string | null) { + set({ imageObjectId }) + } + + return { + imageObjectId: null, + setImageObjectId, + } +}) @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './input-filter' \ No newline at end of file @@ -0,0 +1,6 @@ +.title { + margin-bottom: 16px; + font-size: 15px; + color: var(--new-ui-gray-color); + font-weight: 400; +} \ No newline at end of file @@ -0,0 +1,52 @@ +import React from 'react' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { CommonTooltip } from '#/shared/ui/tooltip' +import styles from './input-filter.module.scss' +import { isNumericString } from '#/shared' + +interface InputFilterProps { + title: string + value: string + description: string + setValue: (value: string) => void + withoutTooltip?: boolean + onlyNumber?: boolean +} + +export const InputFilter = ({ + value, + setValue, + title, + description, + withoutTooltip = false, + onlyNumber = false, +}: InputFilterProps) => { + return ( +
+ +

{title}

+ { + e.target.style.height = 'auto' + e.target.style.height = Math.min(e.target.scrollHeight, 177) + 'px' + + setValue( + onlyNumber && !isNumericString(e.target.value) + ? value + : e.target.value + ) + }} + /> +
+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1 @@ +export * from './use-login-cookies' \ No newline at end of file @@ -0,0 +1,17 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { useCookies } from 'react-cookie' + +export function useLoginCookies() { + const [, setCookie] = useCookies() + + const { query } = useRouter() + + function setAllQueryToCookies() { + Object.entries(query).forEach(([key, value]) => setCookie(key, value)) + } + + return { + setAllQueryToCookies + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './use-login-form' +export * from './use-login-validate' \ No newline at end of file @@ -0,0 +1,58 @@ +import { useRouter } from 'next/router' +import { useForm } from 'react-hook-form' +import { LoginForm } from '../types' +import { useEffect, useState } from 'react' +import { signIn } from 'next-auth/react' +import { useShowDataStore } from '#/shared/lib/hooks' +import { captureMessage } from '@sentry/nextjs' +import { ERROR_MAPPING } from '#/pages/api/auth/constants' +import { keysToValues } from '#/shared' + +export function useLoginForm() { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm() + + const [pending, setPending] = useState(false) + + const { showMessage } = useShowDataStore() + + const { push } = useRouter() + + useEffect(() => { + if (Object.values(errors).length === 0) return + showMessage(Object.values(errors)[0].message || 'Неверные данные') + }, [errors]) + + const onSubmit = handleSubmit(async (data: any) => { + setPending(true) + + const response = await signIn('credentials', { + username: data.email, + password: data.password, + redirect: false, + }) + + setPending(false) + + const { ok, error } = response! + + if (ok) return push('/') + + if (!error) return showMessage('Не удалось выполнить вход, попробуйте позже') + + const expectedError = keysToValues(ERROR_MAPPING)[error] + + if (!expectedError) return captureMessage(error) + + showMessage(expectedError) + }) + + return { + onSubmit, + register, + pending, + } +} @@ -0,0 +1,30 @@ +import { RegisterOptions } from 'react-hook-form' +import { EMAIL_REGEXP } from '#/shared/lib/constants' +import { LoginForm } from '../types' + +export function useLoginValidate() { + const emailOptions: RegisterOptions = { + required: 'Поле email обязательно к заполенению!', + minLength: { + value: 5, + message: 'Слишком короткий email', + }, + pattern: { + value: EMAIL_REGEXP, + message: 'Введите валидный email', + }, + } + + const passwordOptions: RegisterOptions = { + required: 'Поле пароль обязательно к заполнению!', + minLength: { + value: 5, + message: 'Слишком короткий пароль', + }, + } + + return { + emailOptions, + passwordOptions + } +} @@ -0,0 +1 @@ +export * from './login-form' \ No newline at end of file @@ -0,0 +1,3 @@ +import { CreateUserDTO } from '#/entities/user-account' + +export interface LoginForm extends CreateUserDTO {} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-login-query-errors' \ No newline at end of file @@ -0,0 +1,23 @@ +import { ERRROR_YANDEX_TRANSLATE_MAPPING } from '#/pages/api/auth/constants' +import { useShowDataStore } from '#/shared/lib/hooks' +import { captureMessage } from '@sentry/nextjs' + +export function useLoginQueryErrors() { + const { showMessage } = useShowDataStore() + + function onMounted() { + const params = new URL(window.location.href).searchParams + + const error = params.get('error') + + if (!error || error === '') return + + const expectedError = ERRROR_YANDEX_TRANSLATE_MAPPING[error] + showMessage(expectedError ?? 'Не удалось выполнить вход, попробуйте позже') + if (!expectedError) captureMessage(error) + } + + return { + onMounted + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-messages-filters' \ No newline at end of file @@ -0,0 +1,16 @@ +import { Message } from '#/entities/message' +import { useState } from 'react' + +export function useMessagesFilters(messages: Message[]) { + const [value, setValue] = useState('') + + const filteredMessages = messages.filter((message) => { + return message.content.toLowerCase().includes(value.toLowerCase()) + }) + + return { + filteredMessages, + value, + setValue + } +} @@ -0,0 +1 @@ +export * from './message-fast-choice-menu' @@ -0,0 +1,81 @@ +.menu { + display: flex; + flex-direction: column; + gap: 15px; + padding: 10px; + border-radius: 15px; + border: 1px solid rgba($color: #a4aab5, $alpha: 0.2); + background: var(--new-ui-element-bg); + width: fit-content; + transition: all 0.6s ease-in-out; + + &:hover { + width: 280px !important; + .menu__new { + span { + display: inline-block; + } + } + + .menu__input { + display: inline-block; + width: 150px !important; + } + + .menu__inputbox { + justify-content: flex-end; + } + } + + &__new { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 10px 15px; + span { + display: none; + color: var(--new-ui-gray-color); + } + } + + &__inputbox { + display: flex; + align-items: center; + justify-content: center !important; + gap: 5px; + padding: 5px 7px; + } + + &__input { + outline: none; + border: none; + background: transparent; + color: var(--text-color-main); + display: none; + &::placeholder { + color: var(--new-ui-gray-color); + } + } +} + +.list { + height: 350px; + overflow-y: scroll; + + &__empty { + color: var(--new-ui-gray-color); + text-align: center; + } +} + +.loader { + text-align: center; + // width: max-content; +} + +@media (max-width: 800px) { + .list { + height: 200px; + } +} @@ -0,0 +1,64 @@ +import React, { MouseEventHandler, useState } from 'react' +import styles from './message-fast-choice-menu.module.scss' +import { c, Loader } from '#/shared' +import { Message } from '#/entities/message' +import PlusSvg from '#/assets/svg/plus.svg?react' +import Image from 'next/image' +import { MessageItem } from './message-item' +import SearchSvg from '#/assets/svg/search.svg?react' +import { useMessagesFilters } from '../model' + +export interface MessageFastChoiceMenuProps { + messages: Message[] + loading: boolean +} + +export const MessageFastChoiceMenu = ({ messages, loading }: MessageFastChoiceMenuProps) => { + const { filteredMessages, value, setValue } = useMessagesFilters(messages) + + const [hovered, setHovered] = useState(false) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className={styles.menu} + > + +
    + {loading && ( +
    + +
    + )} + {!filteredMessages.length && !loading && ( +
  • Ничего не найдено
  • + )} + {filteredMessages.map((message) => ( +
  • + {}} + {...message} + /> +
  • + ))} +
+ +
+ + setValue(e.target.value)} + className={styles.menu__input} + placeholder='Поиск генераций' + type='text' + /> +
+
+ ) +} @@ -0,0 +1,58 @@ +.item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px; + border-radius: 10px; + transition: width 0.6s ease-in-out; + + &_hovered { + .item__desc { + display: block; + opacity: 1; + } + + .item__right { + opacity: 1; + display: block; + } + } + + &:hover { + background-color: var(--new-ui-bg-app-color); + } + + &__desc { + opacity: 0; + transition: opacity 0.6s ease-in-out; + display: none; + } + + &__left { + display: flex; + gap: 7px; + align-items: center; + } + + &__right { + padding-right: 10px; + opacity: 0; + transition: opacity 0.6s ease-in-out; + display: none; + } + + &__img { + border-radius: 5px; + object-fit: cover; + } + + &__content { + font-weight: 500; + font-size: 14px; + } + + &__time { + font-size: 11px; + color: var(--new-ui-gray-color); + } +} @@ -0,0 +1,48 @@ +import { Message, useMessageTime } from '#/entities/message' +import Image from 'next/image' +import React from 'react' + +import styles from './message-item.module.scss' +import { c } from '#/shared' +import TrashSvg from '#/assets/svg/trash.svg?react' + +interface MessageItemProps extends Message { + deleteMessage: () => void + className?: string + hovered?: boolean +} + +export const MessageItem = ({ + created_at, + className, + file, + hovered, + content, + deleteMessage, +}: MessageItemProps) => { + const { formatedDate } = useMessageTime(created_at) + + return ( +
+
+ {content +
+

{formatedDate}

+

{content}

+
+
+ +
+ +
+
+ ) +} @@ -0,0 +1,2 @@ +export * from './ui' +export * from './model' @@ -0,0 +1 @@ +export * from './store-message-fullscreen-modal' @@ -0,0 +1,28 @@ +import { create } from 'zustand' + +type TextScaleState = { + scale: number + minScale: number + maxScale: number + increaseScale: () => void + decreaseScale: () => void + resetScale: () => void +} + +export const useTextScaleStore = create((set) => ({ + scale: 1, + minScale: 0.8, + maxScale: 1.5, + + increaseScale: () => + set((state) => ({ + scale: Math.min(state.scale + 0.1, state.maxScale), + })), + + decreaseScale: () => + set((state) => ({ + scale: Math.max(state.scale - 0.1, state.minScale), + })), + + resetScale: () => set({ scale: 1 }), +})) @@ -0,0 +1,126 @@ +.containerScroll { + min-height: 403px; + overflow: auto; + font-size: 15px; + line-height: 150%; + padding-right: 20px; + + width: 100%; + height: 100%; + position: relative; + overscroll-behavior: contain; + + &__scaledContent { + transform-origin: top left; + display: inline-block; + + width: 100%; + transition: transform 0.3s ease; + + display: inline-block; + white-space: normal; + word-wrap: break-word; + } + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + &::-webkit-scrollbar-track { + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrol-bar-color); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb:hover { + background: #746c6c; + } + + ul, + ol { + margin: 0; + padding: 0 15px 0 15px; + list-style-position: outside; + } + + li { + margin-bottom: 6px; + white-space: normal; + } + + ul { + list-style-type: disc; + color: var(--new-ui-message-text-color); + } + + ol { + list-style-type: decimal; + color: var(--new-ui-message-text-color); + } + *:not(ul):not(ol):not(li) { + max-width: 827px; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: var(--new-ui-text-color); + } +} + +.modal { + display: flex; + flex-direction: column; + position: relative; + width: 887px; + height: 583px; + min-width: 887px; + max-height: 583px; + + @media screen and (max-width: 600px) { + min-width: 350px; + } +} + +.header { + display: flex; + align-items: center; + column-gap: 10px; + margin: 35px 0px 25px 0px; + letter-spacing: -0.02em; + + &__text { + font-size: 30px; + font-weight: 650; + } +} + +.footer { + display: flex; + column-gap: 10px; + min-height: 85px; + + &__containerButton { + display: flex; + align-items: center; + column-gap: 10px; + } + + &__buttonCnanges { + display: flex; + align-items: center; + justify-content: center; + + width: 42px; + height: 42px; + border-radius: 15px; + padding: 0; + } +} @@ -0,0 +1 @@ +export * from './message-fullscreen-modal' \ No newline at end of file @@ -0,0 +1,86 @@ +import * as React from 'react' +import styles from './fullscreen-modal.module.scss' +import AvatarIcon from '#/assets/svg/avatar.svg?react' +import ZoomIconMinus from '#/assets/svg/zoom-icon-minus.svg?react' +import ZoomIconPlus from '#/assets/svg/zoom-icon-plus.svg?react' +import { FULLSCREEN_CHAT_MESSAGE, getModalById, PlateTemplate } from '#/features/modals' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { CommonButton } from '#/shared/ui/button' +import { useChatBotMessages } from '#/entities/message' +import { Message } from '#/entities/message' +import { Markdown } from '#/widgets/markdown/markdown' +import { Skeleton } from '@mui/material' +import { useTextScaleStore } from '../model' + +interface MessageFullscreenModalI { + message: Message +} + +export const MessageFullscreenModal = () => { + const modal = getModalById(FULLSCREEN_CHAT_MESSAGE) + + const { scale, increaseScale, decreaseScale, resetScale } = useTextScaleStore() + + const message = modal.getStoreProperty('message')! + + React.useEffect(() => { + resetScale() + }, [modal.state]) + + return ( + + {message && ( + <> +
+
+
+ + +

{message.model}

+
+ +
+
+ +
+
+ +
+
+ + + + + + + +
+
+
+
+ + )} +
+ ) +} @@ -0,0 +1 @@ + @@ -1,3 +1,8 @@ + +export const PLATE_IMAGE_STYLES = 'plate-image-styles' export const PLATE_CHANGE_PASSWORD = 'plate-change-password' export const RESEND_INVATION_PASSWORD = 'resend-invation-password' export const ERROR_REPORT = 'error-report' +export const GALLERY_IMAGES = 'gallery-images' +export const FULLSCREEN_CHAT_MESSAGE = 'fullscreen-chat-message' + @@ -1,78 +1,81 @@ -import { create } from "zustand"; +import { useMemo } from 'react' +import { create } from 'zustand' type Set = { - (partial: (state: PlatesStore) => Partial): void; -}; + (partial: (state: PlatesStore) => Partial): void +} -type Get = () => PlatesStore; +type Get = () => PlatesStore function makeModalInstance(set: Set, get: Get, key: string) { - const ModalInstance: Modal = { - id: key, - state: false, - store: {}, - setState(state, store) { - const modals = get().modals; + const ModalInstance: Modal = { + id: key, + state: false, + store: {}, + setState(state, store) { + const modals = get().modals + + store = store ?? {} - store = store ?? {}; + store = Object.entries(store).reduce((acc, [key, value]) => { + return { ...acc, [key]: value } + }, {}) - set(() => ({ - modals: { ...modals, [this.id]: { ...modals[key], state, store } }, - })); - }, - setStoreProperty(key, value) { - const modals = get().modals; + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], state, store } }, + })) + }, + setStoreProperty(key, value) { + const modals = get().modals - this.store[key] = value; + this.store[key] = value - set(() => ({ - modals: { ...modals, [this.id]: { ...modals[key], store: this.store } }, - })); - }, - getStoreProperty(key) { - return this.store[key]; - }, - }; + set(() => ({ + modals: { ...modals, [this.id]: { ...modals[key], store: this.store } }, + })) + }, + getStoreProperty(key) { + return this.store[key] + }, + } - return ModalInstance; + return ModalInstance } export interface Modal { - id: string; - state: boolean; - store: Record; - setState: (state: boolean, store?: Record) => void; - setStoreProperty: (key: string, value: any) => void; - getStoreProperty: (key: string) => T | undefined; + id: string + state: boolean + store: Record + setState: (state: boolean, store?: Record) => void + setStoreProperty: (key: string, value: any) => void + getStoreProperty: (key: string) => T | undefined } export interface PlatesStore { - modals: Record; - setModal: (key: string) => void; - getModal: (key: string) => Modal; + modals: Record + setModal: (key: string) => void + getModal: (key: string) => Modal } export const usePlatesStore = create((set, get) => { - const modals: Record = {}; + const modals: Record = {} - function setModal(key: string) { - set((state) => ({ - modals: { - ...state.modals, - [key]: Object.assign({}, makeModalInstance(set, get, key)), - }, - })); - } + function setModal(key: string) { + set((state) => ({ + modals: { + ...state.modals, + [key]: Object.assign({}, makeModalInstance(set, get, key)), + }, + })) + } - function getModal(key: string) { - return ( - get().modals[key] ?? Object.assign({}, makeModalInstance(set, get, key)) - ); - } + function getModal(key: string) { + return get().modals[key] ?? Object.assign({}, makeModalInstance(set, get, key)) + } - return { - modals, - setModal, - getModal, - }; -}); + return { + modals, + setModal, + getModal, + } +}) @@ -12,6 +12,15 @@ opacity: 0; visibility: hidden; + &_blur { + &-on { + backdrop-filter: blur(15.5px); + } + &-off { + backdrop-filter: none; + } + } + &_visible { opacity: 1; visibility: visible; @@ -78,9 +87,9 @@ } // &__header { - // display: flex; - // justify-content: space-between; - // padding: 25px; + // display: flex; + // justify-content: space-between; + // padding: 25px; // } &__close-button { @@ -93,9 +102,18 @@ &__content-body { transition: all 0.3s ease-in-out; - padding: 0px 32px 32px 32px; overflow: hidden; + &_padding { + &-default { + padding: 0px 32px 32px 32px; + } + + &-small { + padding: 0px 5px 0 30px; + } + } + @media screen and (max-width: 1000px) { padding: 0px 10px 10px 10px; } @@ -9,6 +9,8 @@ import { useThemeAndDevice } from '#/shared/lib/hooks' export type AlignX = 'left' | 'center' | 'right' export type AlignY = 'top' | 'center' | 'bottom' export type Variant = 'primary' | 'secondary' +export type PaddingVariant = 'default' | 'small' +export type BlurToogle = 'on' | 'off' export interface PlatesTemplateProps { id: string @@ -23,6 +25,9 @@ export interface PlatesTemplateProps { animationBehaviorClass?: string headerClassName?: string variant?: Variant + containerClassName?: string + paddingVariant?: PaddingVariant + blurToogle?: BlurToogle } export default function PlatesTemplate({ @@ -36,8 +41,10 @@ export default function PlatesTemplate({ animationClass = styles['template__animation'], animationBehaviorClass = styles['template__animation-behavior'], closeModal = () => {}, - headerClassName, + containerClassName, variant = 'primary', + paddingVariant = 'default', + blurToogle = 'off', }: PlatesTemplateProps) { const { setModal, getModal } = usePlatesStore() @@ -52,7 +59,9 @@ export default function PlatesTemplate({ modal.state ? styles['template_visible'] : styles['template_invisible'], hasBg ? styles['template_flex'] : styles['template_w-fit'], styles[`template_align-y-${alignY}`], - hasBg ? styles[`template_align-x-${alignX}`] : '' + styles[`template_blur-${blurToogle}`], + hasBg ? styles[`template_align-x-${alignX}`] : '', + containerClassName )} onClick={() => { if (hasBg) modal.setState(false) @@ -75,12 +84,26 @@ export default function PlatesTemplate({ closeModal() }} > - + -
{children}
+
+ {children} +
) : ( -
{children}
+
+ {children} +
)}
) @@ -6,4 +6,5 @@ export function getModalById(id: string) { } export * from './ui' -export * from './config' \ No newline at end of file +export * from './config' + @@ -0,0 +1,2 @@ +export * from './use-model-input' +export * from './model-input.store' \ No newline at end of file @@ -0,0 +1,24 @@ +import { create } from "zustand"; + +export interface ModelInputStore { + modelInputValue: string; + modelInputFile: File | null; + setModelInputValue: (modelInput: string) => void; + setModelInputFile: (file: File | null) => void; +} + +export const useModelInputStore = create((set, get) => { + function setModelInputValue(modelInput: string) { + set({ ...get(), modelInputValue: modelInput }); + } + function setModelInputFile(file: File | null) { + set({ ...get(), modelInputFile: file }); + } + + return { + modelInputFile: null, + modelInputValue: "", + setModelInputValue, + setModelInputFile + }; +}) \ No newline at end of file @@ -0,0 +1,45 @@ +import { getDeviceType } from '#/shared' +import { FormEventHandler, KeyboardEventHandler, useEffect, useRef, useState } from 'react' +import { useMediaQuery } from 'usehooks-ts' + +export function useModelInput( + value: string, + setValue: (value: string) => void, + sendMessage: (message: string) => void, + setFile: (file: File | null) => void +) { + const isMobile = useMediaQuery('(max-width: 1000px)') + const textareaRef = useRef(null) + + const onInputHeightCorrect: FormEventHandler = () => { + if (!textareaRef.current) return + + textareaRef.current.style.height = 'auto' + + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' + } + + function onSendMessage() { + sendMessage(value) + setValue('') + setFile(null) + } + + const keyDownSend: KeyboardEventHandler = (e) => { + if (e.shiftKey || e.key !== 'Enter' || isMobile) return + e.preventDefault() + + onSendMessage() + + if (textareaRef.current) { + textareaRef.current.style.height = 'auto' + } + } + + return { + onInputHeightCorrect, + keyDownSend, + onSendMessage, + textareaRef + } +} @@ -5,6 +5,14 @@ width: 100%; border-radius: 15px; gap: 10px; + background-color: var(--new-ui-main-color); + position: relative; + &__settings { + display: none; + @media screen and (max-width: 1000px) { + display: block; + } + } } :root[data-theme='light'] { @@ -34,7 +42,8 @@ -ms-overflow-style: none; padding: 0px; font-size: 16px; - max-height: 82px; + max-height: 62px; + position: relative; &::-webkit-scrollbar { display: none; /* Chrome, Brave, Edge */ @@ -1,167 +1,77 @@ -import React, { FC, SyntheticEvent, useEffect } from 'react' -import { Divider, InputAdornment, TextField } from '@mui/material' -import Stack from '@mui/material/Stack' -import { TextFieldProps } from '@mui/material/TextField/TextField' -import Image from 'next/image' +import React, { useEffect } from 'react' import { LoadImage } from '#/app/components/input_components/load_image' import { SendBtn } from '#/app/components/input_components/send_button' -import { useAppSelector } from '#/app/store/store' -import { IModelInputs } from '#/shared/api/models/models' -import { styleInputWithoutBorderFocus } from '#/shared/ui/input' -import { useThemeAndDevice } from '#/shared/lib/hooks' +import { ModelInputs } from '#/shared/api/models/models' import classes from './model-input.module.scss' +import { useChatBotMessages } from '#/entities/message' +import InputSettingsSvg from '#/assets/svg/input-setting.svg?react' +import { CommonTextArea } from '#/shared/ui/common-textarea' +import { useModelInput } from '../model' +import { CommonSendButton } from '#/shared/ui/common-send-button' +import { CommonLoadFile } from '#/shared/common-load-file' -interface Input { - loading: boolean - // requestImage: () => void - wonderMe?: () => void - desktop: boolean - image?: File | null - count?: number - quality?: string - unpinImage?: () => void - imageLoad?: React.ChangeEventHandler - // openFilters: (event: React.MouseEvent) => void - sendMessage: (message: string, required: (string | null)[]) => boolean - // setInput: React.Dispatch> - // setImage: React.Dispatch> - styles: 'images' | 'chats' | 'audio' - input_types?: IModelInputs[] +export interface ModelInputProps { + value: string + setValue: (value: string) => void + file: File | null + setFile: (file: File | null) => void + sendMessage: (message: string) => void + inputTypes: string[] blocked?: boolean viewMobileSettings: () => void - currentVersion: string - resendValue?: string } -export const ModelInput: FC = ({ - image, - loading, - unpinImage, +export const ModelInput = ({ + value, + setValue, + file, sendMessage, - desktop, - imageLoad, - styles, - input_types, + setFile, + inputTypes, viewMobileSettings, - currentVersion, - resendValue, - blocked, -}: Input) => { - const theme = useAppSelector((state) => state.theme.theme) - const [disabled, setDisabled] = React.useState(true) - const [required, setRequired] = React.useState<(string | null)[]>([]) - const [types, setTypes] = React.useState([]) - const [typeVersions, setTypeVersions] = React.useState({}) - const [value, setValue] = React.useState('') + blocked = false, +}: ModelInputProps) => { + const { loading } = useChatBotMessages() - useEffect(() => { - if (input_types) { - setRequired(input_types.map((el) => (el.required ? el.type : null))) - setTypes(input_types.map((el) => el.type)) - setTypeVersions(input_types.reduce((a, v) => ({ ...a, [v.type]: v.versions }), {})) - } - }, [input_types]) - - useEffect(() => { - if (input_types && typeVersions && !typeVersions['text']) { - setDisabled(true) - } else if ( - typeVersions['text'] && - (typeVersions['text'].length === 0 || typeVersions['text'].includes(currentVersion)) - ) { - setDisabled(false) - } - }, [typeVersions]) - - useEffect(() => { - if (resendValue) { - setValue(resendValue) - } - }, [resendValue]) + const { onInputHeightCorrect, keyDownSend, onSendMessage, textareaRef } = useModelInput( + value, + setValue, + sendMessage, + setFile + ) return ( -
- {!desktop && ( - - - - )} +
+ + onInput={onInputHeightCorrect} + onKeyDown={keyDownSend} + rows={1} + placeholder={blocked ? 'Ввод текста недоступен для этой модели' : 'Ваше сообщение'} + className={classes.area} + disabled={blocked} + />
- {typeVersions['image'] && - (typeVersions['image'].length === 0 || - typeVersions['image'].includes(currentVersion)) && ( - <> - {!blocked && ( - - )} -
- - )} - {!disabled && !blocked && ( - setFile(null)} + setFile={(file) => setFile(file)} + inputTypes={inputTypes} /> )} +
+ + {!blocked && onSendMessage()} />}
) @@ -0,0 +1 @@ +export * from './model-params-select' @@ -0,0 +1,70 @@ +.container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.select { + position: relative; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 16px 8px 16px 14px; + cursor: pointer; + + border-radius: 13px; + border: 2px solid #40404e; + &_open { + border: 2px solid #7f7df3; + } + + &__text { + font-size: 18px; + color: var(--new-ui-text-color); + line-height: 1.5; + } +} + +.icon { + color: #a6a5a5; +} + +html[data-theme='light'] { + .select { + border: 1px solid #e9e9e9; + &_open { + border: 1px solid #7f7df3; + } + } +} + +.dropdown { + position: absolute; + top: 60px; + left: 0; + right: 0; + z-index: 1000; + background-color: var(--new-ui-main-color); + width: 100%; + border-radius: 13px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, + rgba(0, 0, 0, 0.12) 0px 3px 14px 2px; + + &__option { + list-style: none; + padding: 10px 16px; + + &_active { + background-color: rgba(25, 118, 210, 0.12); + } + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + &:active { + background-color: rgba(0, 0, 0, 0.2); + } + } + &__name { + font-size: 18px; + } +} @@ -0,0 +1,28 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useChatBot } from '#/entities/model-entity' +import { CommonSelect, SelectItem } from '#/shared/ui/common-select' + +interface ModelParamsSelectProps {} + +export const ModelParamsSelect = ({ ...props }: ModelParamsSelectProps) => { + const { botParams, inference, onSetInferenceParams } = useChatBot() + + const selectItems = useMemo(() => { + if (!botParams) return [] + return botParams.inferences.map(({ description, slug, name }) => ({ + value: slug, + label: name, + description, + })) + }, [botParams]) + + const value = useMemo(() => (inference ? inference.slug : undefined), [inference]) + + return ( + onSetInferenceParams(slug)} + items={selectItems} + /> + ) +} @@ -0,0 +1 @@ +export * from './ui' @@ -0,0 +1,10 @@ + + +export interface NavigationBlock { + title: string, + content: string | React.ReactNode + link?: string + enabled: boolean + image: string + cols: number +} \ No newline at end of file @@ -0,0 +1 @@ +export * from './block' \ No newline at end of file @@ -0,0 +1 @@ +export * from './navigation-main-page-block' \ No newline at end of file @@ -0,0 +1,85 @@ +.block { + padding: 25px 30px; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + + @media screen and (max-width: 768px) { + grid-column: unset !important; + padding: 15px 20px; + } + + &_disabled { + pointer-events: none; + cursor: default; + } + + &__image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 30px; + } + + &__desc { + position: relative; + z-index: 1; + } + + &__title { + font-size: 40px; + font-weight: 800; + color: white; + margin-bottom: 5px; + + @media screen and (max-width: 768px) { + font-size: 34px; + } + } + + &__content { + font-size: 16px; + font-weight: 400; + color: white; + max-width: 450px; + + @media screen and (max-width: 768px) { + font-size: 16px; + font-weight: 500; + } + } + + &__arrow { + transition: color 0.3s ease-in-out; + } + + &__more { + display: flex; + align-items: center; + gap: 3px; + + &_disabled { + &:hover { + .block__arrow_disabled { + color: white; + } + } + } + + &:hover { + .block__arrow { + color: black; + } + } + } + + &__controls { + position: relative; + z-index: 1; + } +} @@ -0,0 +1,55 @@ +import React from 'react' +import { NavigationBlock } from '../types' + +import styles from './navigation-main-page-block.module.scss' +import { CommonButton } from '#/shared/ui/button' +import Image from 'next/image' +import ArrowSelectSvg from '#/assets/svg/arrow-select.svg?react' +import Link from 'next/link' +import { c } from '#/shared/lib/helpers' + +interface NavigationMainPageBlockProps extends NavigationBlock {} + +export const NavigationMainPageBlock = ({ + title, + cols, + content, + enabled, + link, + image, +}: NavigationMainPageBlockProps) => { + return ( + + {title} +
+

{title}

+

{content}

+
+ +
+ + {enabled ? 'Попробовать' : 'Скоро будет доступно'} + {enabled && ( + + )} + +
+ + ) +} @@ -0,0 +1,2 @@ +export * from './types' +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-referal-info' \ No newline at end of file @@ -0,0 +1,12 @@ +import { useEffect, useState } from 'react' + +export function useReferrall() { + const [referral, setReferral] = useState(null) + + useEffect(() => setReferral(localStorage.getItem('referral')), []) + + return { + referral, + setReferral, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { useForm } from 'react-hook-form' +import { RegisterOptions, useForm } from 'react-hook-form' import { SubmitErrorHandler } from 'react-hook-form/dist/types/form' import { Autocomplete, Box, TextField, Typography } from '@mui/material' @@ -97,7 +97,7 @@ export const StepLegalInformative = () => { }, } } - {...methods.register('companyName', companyNameOptions)} + {...methods.register('companyName', companyNameOptions as any)} /> )} componentsProps={{ @@ -115,7 +115,7 @@ export const StepLegalInformative = () => { @@ -123,7 +123,7 @@ export const StepLegalInformative = () => { @@ -0,0 +1 @@ +export * from './register' \ No newline at end of file @@ -0,0 +1,10 @@ +import { CreateUserWithRelations } from '#/entities/user-account' +import { api } from '#/shared/api' + +export async function getWhitelist(email: string) { + return await api.get('/auth/mail/whitelist', { params: { email } }) +} + +export async function apiRegister(data: CreateUserWithRelations) { + return await api.post('/auth/register', data) +} \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './use-register' +export * from './use-register-validate' \ No newline at end of file @@ -3,3 +3,4 @@ export interface IEmailForms { password1: string password2: string } + \ No newline at end of file @@ -0,0 +1,28 @@ +import { EMAIL_REGEXP } from '#/shared/lib/constants' + +export function useRegisterValidate() { + const emailOptions = { + required: 'Поле email обязательно к заполенению!', + minLength: { + value: 5, + message: 'Слишком короткий email', + }, + pattern: { + value: EMAIL_REGEXP, + message: 'Введите валидный email', + }, + } + + const passwordOptions = { + required: 'Поле пароль обязательно к заполнению!', + minLength: { + value: 5, + message: 'Слишком короткий пароль', + }, + } + + return { + emailOptions, + passwordOptions + } +} @@ -0,0 +1,78 @@ +import { useState } from 'react' +import { apiRegister, getWhitelist } from '../api' +import { SubmitErrorHandler, SubmitHandler, useForm } from 'react-hook-form' +import { RegisterForm } from '../types/register-form' +import { useShowDataStore } from '#/shared/lib/hooks' +import { useCookies } from 'react-cookie' +import { useReferrall } from '#/features/referal' +import { useRouter } from 'next/router' + +export function useRegister(successLogin: Function) { + const { register, handleSubmit, reset, setValue, watch } = useForm() + + const [pending, setPending] = useState(false) + + const { showMessage } = useShowDataStore() + + const { referral } = useReferrall() + + const [{ utm_source, utm_medium, utm_campaign }] = useCookies() + + const { push } = useRouter() + + function onSuccess() { + reset() + successLogin() + setTimeout(() => push('/login'), 7000) + } + + async function onRegister(form: RegisterForm) { + const { status, data } = await apiRegister({ + ...form, + utm_source, + utm_medium, + utm_campaign, + utm_term: utm_campaign, + utm_content: utm_campaign, + referer: referral ?? undefined, + }) + + setPending(false) + + if (status === 201) return onSuccess() + + showMessage(data.detail) + } + + const onValid: SubmitHandler = async ({ rules, confirm, password, ...data }) => { + if (password !== confirm) return showMessage('Пароли не совпадают') + + setPending(true) + + if (rules) return onRegister({ ...data, confirm, password }) + + const response = await getWhitelist(data.email) + + if (response.status !== 200) { + showMessage('Примите пользовательское соглашение') + return setPending(false) + } + + onRegister({ ...data, confirm, password }) + } + + const onInvalid: SubmitErrorHandler = (errors) => { + const error = Object.values(errors).at(0) + if (error) return showMessage(error.message as string) + } + + const onSubmit = handleSubmit(onValid, onInvalid) + + return { + onSubmit, + register, + watch, + setValue, + pending, + } +} @@ -0,0 +1,2 @@ +export * from './use-form-fields' +export * from './use-form-fields-change' \ No newline at end of file @@ -0,0 +1,32 @@ +import { fireEvent, screen } from '@testing-library/dom' +import { useFormFields } from '.' + +export function useFormFieldsChange() { + const { emailInput, passwordInput, confirmInput, spamCheckbox, rulesCheckbox, submitButton } = + useFormFields() + + function allInputsFilled() { + fireEvent.change(emailInput, { + target: { value: 'aleksander.freelancer@gmail.com' }, + }) + fireEvent.change(passwordInput, { target: { value: 'geraldisrivii' } }) + fireEvent.change(confirmInput, { target: { value: 'geraldisrivii' } }) + } + + function allFieldsFilled() { + allInputsFilled() + fireEvent.click(rulesCheckbox) + fireEvent.click(spamCheckbox) + } + + function fieldsFilledWithoutSpam() { + allInputsFilled() + fireEvent.click(rulesCheckbox) + } + + return { + allFieldsFilled, + fieldsFilledWithoutSpam, + allInputsFilled, + } +} @@ -0,0 +1,12 @@ +import { screen } from '@testing-library/dom' + +export function useFormFields() { + return { + emailInput: screen.getByTestId('email-input'), + passwordInput: screen.getByTestId('password-input'), + confirmInput: screen.getByTestId('confirm-input'), + rulesCheckbox: screen.getByTestId('rules-checkbox'), + spamCheckbox: screen.getByTestId('spam-checkbox'), + submitButton: screen.getByTestId('submit-button'), + } +} @@ -0,0 +1 @@ +export * from './register-form' \ No newline at end of file @@ -0,0 +1,7 @@ +import { CreateUserDTO } from '#/entities/user-account' + +export interface RegisterForm extends CreateUserDTO { + confirm: string + rules?: boolean + spam?: boolean +} @@ -0,0 +1 @@ +export * from './register-email-form' \ No newline at end of file @@ -0,0 +1,59 @@ +.form { + &__inputs { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 10px; + } + + &__checkboxes { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__navigate { + padding-top: 10px; + text-align: center; + color: #a7a8bb; + font-size: 16px; + } +} + +.input{ + &__pass{ + display: flex; + align-items: center; + } +} + +.controls { + padding-top: 25px; + width: 100%; + + &__progress { + display: flex; + justify-content: center; + + span{ + color: var(--air-color); + } + } + + &__create { + width: 100%; + } +} + +.checkbox { + padding-left: 12px; + &__text { + color: #868686; + line-height: 1.5; + width: max-content; + } + + &__link { + color: var(--air-color); + } +} @@ -0,0 +1,171 @@ +import { RegisterPage } from '#/views/register' +import '@testing-library/jest-dom' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { jestRender } from '#/../jest/utils/render' +import { windowMock } from '#/../jest/utils/window-mock' +import { RegisterEmailForm } from './register-email-form' +import { api } from '#/shared/api' +import { useFormFields, useFormFieldsChange } from '../test' +import { useRouter } from '#/../__mocks__/next/router' +import { useShowDataStore } from '#/shared/lib/hooks' + +jest.mock('#/shared/api') + +jest.mock('next/router') +jest.mock('@sentry/nextjs') + +const showMessageMock = jest.fn() + +jest.mock('#/shared/lib/hooks', () => { + return { + useShowDataStore: jest.fn(() => { + return { + showMessage: showMessageMock, + } + }), + } +}) + +windowMock() + +describe('register-email-form', () => { + beforeEach(() => { + ;(api.post as jest.Mock).mockClear() + ;(api.get as jest.Mock).mockClear() + }) + + it('renders in register', () => { + jestRender() + + const form = screen.getByTestId('register-email-form') + + expect(form).toBeInTheDocument() + }) + + it('submit by button click', async () => { + await act(async () => { + jestRender( {}} />) + }) + + const { submitButton } = useFormFields() + + const { allFieldsFilled } = useFormFieldsChange() + + ;(api.post as jest.Mock).mockResolvedValue({ + status: 201, + }) + + allFieldsFilled() + + await act(async () => { + fireEvent.click(submitButton) + }) + + await waitFor(() => { + expect(api.post).toHaveBeenCalledWith('/auth/register', { + confirm: 'geraldisrivii', + email: 'aleksander.freelancer@gmail.com', + password: 'geraldisrivii', + spam: true, + }) + }) + + /** Больно долго его ждать (7000ms по timeout) - замедляет тесты (но работает) */ + // await waitFor(() => { + // const { push } = useRouter() + // expect(push).toHaveBeenCalledWith('/login') + // }, {timeout: 8000}) + }) + + it('submit by Enter', async () => { + await act(async () => { + jestRender( {}} />) + }) + ;(api.post as jest.Mock).mockResolvedValue({ + status: 201, + }) + + const { allFieldsFilled } = useFormFieldsChange() + + const { emailInput } = useFormFields() + + allFieldsFilled() + + await userEvent.type(emailInput, '{enter}') + + await waitFor(() => { + expect(api.post).toHaveBeenCalledWith('/auth/register', { + confirm: 'geraldisrivii', + email: 'aleksander.freelancer@gmail.com', + password: 'geraldisrivii', + spam: true, + }) + }) + }) + + it('submit without rules and white list not include sended email', async () => { + await act(async () => { + jestRender( {}} />) + }) + ;(api.get as jest.Mock).mockResolvedValue({ + status: 400, + }) + ;(useShowDataStore as any as jest.Mock).mockImplementation(() => ({ + showMessage: showMessageMock, + })) + + const { allInputsFilled } = useFormFieldsChange() + + const { emailInput } = useFormFields() + + allInputsFilled() + + await userEvent.type(emailInput, '{enter}') + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/auth/mail/whitelist', { + params: { email: 'aleksander.freelancer@gmail.com' }, + }) + }) + + await waitFor(() => { + expect(showMessageMock).toHaveBeenCalledWith('Примите пользовательское соглашение') + }) + }) + + it('submit without rules and white list included sended email', async () => { + await act(async () => { + jestRender( {}} />) + }) + ;(api.get as jest.Mock).mockResolvedValue({ + status: 200, + }) + ;(api.post as jest.Mock).mockResolvedValue({ + status: 201, + }) + + const { allInputsFilled } = useFormFieldsChange() + + const { emailInput } = useFormFields() + + allInputsFilled() + + await userEvent.type(emailInput, '{enter}') + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/auth/mail/whitelist', { + params: { email: 'aleksander.freelancer@gmail.com' }, + }) + }) + + await waitFor(() => { + expect(api.post).toHaveBeenCalledWith('/auth/register', { + confirm: 'geraldisrivii', + email: 'aleksander.freelancer@gmail.com', + password: 'geraldisrivii', + spam: false, + }) + }) + }) +}) @@ -1,303 +1,129 @@ import React, { useState } from 'react' -import { useCookies } from 'react-cookie' -import { useForm } from 'react-hook-form' -import { SubmitErrorHandler, SubmitHandler } from 'react-hook-form/dist/types/form' -import { Button, Checkbox, InputAdornment, TextField } from '@mui/material' -import Box from '@mui/material/Box' import CircularProgress from '@mui/material/CircularProgress' -import Typography from '@mui/material/Typography' -import axios from 'axios' import Link from 'next/link' -import { useRouter } from 'next/router' - -import { PasswordOptions } from '#/features/register-by-email/lib/constants' -import { IEmailForms } from '#/features/register-by-email/model/types' -import { useAppSelector } from '#/app/store/store' -import { emailOptions } from '#/shared' -import { CheckBoxAgreeWithRules } from '#/shared' -import { Error } from '#/shared' -import { InputStyleDark, InputStyleLight } from '#/shared' -import { API_URL } from '#/shared/lib/constants/constants' -import { useThemeAndDevice } from '#/shared/lib/hooks' +import { c } from '#/shared' import OpenedEyeSvg from '#/assets/svg/opened-eye.svg?react' import ClosedEyeSvg from '#/assets/svg/closed-eye.svg?react' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' - -interface IRegisterEmailFormProps { - successLogin: () => void +import { useRegister, useRegisterValidate } from '../model' +import { CommonInput } from '#/shared/ui/common-input' +import styles from './register-email-form.module.scss' +import { CommonCheckbox } from '#/shared/ui/checkbox' +import { CommonButton } from '#/shared/ui/button' + +export interface RegisterEmailFormProps { + successLogin: Function } -export const RegisterEmailForm: React.FC = ({ successLogin }) => { - const { register, handleSubmit, reset, watch } = useForm() - - const { theme, desktop } = useThemeAndDevice() - - const referral = useAppSelector((state) => state.user.referral) - - const { showMessage } = useShowDataStore() - - const { push } = useRouter() - - const [loading, setLoading] = React.useState(false) - - const [isEmailWhite, setIsEmailWhite] = React.useState(true) +export function RegisterEmailForm({ successLogin }: RegisterEmailFormProps) { + const { register, onSubmit, watch, pending } = useRegister(successLogin) - const [cookie] = useCookies() - const [passwordShowed, showPassword] = React.useState(false) + const { emailOptions, passwordOptions } = useRegisterValidate() - const makeNewAccount: SubmitHandler = async (data) => { - setLoading(true) - - if (!data.rules) { - try { - const { status } = await axios.get( - API_URL + `/auth/mail/whitelist?email=${data.email}`, { validateStatus: (status) => status < 400 } - ) - if (status === 200) { - setIsEmailWhite(true) - } - } catch (e) { - showMessage('Примите пользовательское соглашение') - setLoading(false) - return - } - } - - const req_data: { - email: any - password: any - utm_source: any - utm_medium: any - utm_campaign: any - utm_term: any - utm_content: any - referer?: string | null - } = { - email: data.email, - password: data.password1, - utm_source: cookie.utm_source, - utm_medium: cookie.utm_medium, - utm_campaign: cookie.utm_campaign, - utm_term: cookie.utm_campaign, - utm_content: cookie.utm_campaign, - } - - if (localStorage.getItem('referral')) { - req_data.referer = localStorage.getItem('referral') - } - - try { - const { status } = await axios.post(API_URL + '/auth/register', req_data, { validateStatus: (status) => status < 400 }) - if (status === 201) { - setLoading(false) - reset() - successLogin() - setTimeout(() => push('/login'), 7000) - } - } catch (err: any) { - setLoading(false) - showMessage(err.response.data.detail) - } - } - - const checkError: SubmitErrorHandler = (data) => { - showMessage(Object.values(data)[0].message || 'Неверные данные') - } - - const handleInputChangeTrim = (event: any) => { - event.target.value = event.target.value.trim() - } - - const handleKeyDown = (event: any) => { - if (event.key === 'Enter') { - handleSubmit(makeNewAccount)() - } - } + const [passwordShowed, showPassword] = useState(false) return ( -
- - Email - - - - - Пароль - - - -
showPassword((x) => !x)} - style={{ - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - }} - > - {passwordShowed ? ( - - ) : ( - - )} -
- - ), - }} - sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} - {...register('password1', { - ...PasswordOptions, - })} - /> - - - Подтвердите пароль - - - { - if (watch('password1') != val) { - return 'Пароли не совпадают' - } - }, - })} - /> - - + +
+ - - showPassword((x) => !x)} + > + {passwordShowed ? ( + + ) : ( + + )} + + } /> - +
+ +
+ - {' '} - Я соглашаюсь на получение-информационно-рекламных писем - - + + Я соглашаюсь с условиями  + + + Политики обработки персональных данных  + + и  + + Публичной офертой + + - - {loading ? ( - - - + + + Я соглашаюсь на получение-информационно-рекламных писем + + +
+ +
+ {pending ? ( +
+ +
) : ( - + )} - - - +

+ Есть аккаунт?  + - Есть аккаунт?{' '} - - Войти - - - + Войти + +

) } @@ -1 +1,4 @@ export { RegisterEmailForm } from './ui/register-email-form' +export * from './model' +export * from './ui' +export * from './test' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './reset-bot-filters' @@ -0,0 +1,30 @@ +.container { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + &__text { + line-height: 19.6px; + font-size: 14px; + font-weight: 400; + display: none; + cursor: pointer; + + @media screen and (max-width: 1000px) { + display: block; + } + + &_reset { + color: #ff4170; + } + &_close { + color: #808283; + } + } + + @media screen and (max-width: 1000px) { + margin-top: 24px; + } +} @@ -0,0 +1,31 @@ +import React from 'react' +import styles from './reset-bot-filters.module.scss' +import { c } from '#/shared' +import { POPUP_CHAT_BOT_PARAMS, getPopupById } from '#/shared/ui/popup' +import { useChatBot } from '#/entities/model-entity' + +interface ResetBotFilters {} + +export const ResetBotFilters = ({}: ResetBotFilters) => { + const popup = getPopupById(POPUP_CHAT_BOT_PARAMS) + + const { onSetInferenceParams, inference } = useChatBot() + + return ( +
+

onSetInferenceParams(inference?.slug ?? null)} + > + Сбросить настройки +

+ +

popup.setState(false)} + > + Закрыть +

+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './reset-image-bot-filters' @@ -0,0 +1,30 @@ +.container { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + &__text { + line-height: 19.6px; + font-size: 14px; + font-weight: 400; + display: none; + cursor: pointer; + + @media screen and (max-width: 1000px) { + display: block; + } + + &_reset { + color: #ff4170; + } + &_close { + color: #808283; + } + } + + @media screen and (max-width: 1000px) { + margin-top: 24px; + } +} @@ -0,0 +1,32 @@ +import React from 'react' +import styles from './reset-image-bot-filters.module.scss' +import { c } from '#/shared' +import { POPUP_CHAT_BOT_PARAMS, POPUP_IMAGE_BOT_PARAMS, getPopupById } from '#/shared/ui/popup' +import { useChatBot } from '#/entities/model-entity' +import { useImageBot } from '#/entities/model-entity/model/use-image-bot' + +interface ResetImageBotFilters {} + +export const ResetImageBotFilters = ({}: ResetImageBotFilters) => { + const popup = getPopupById(POPUP_IMAGE_BOT_PARAMS) + + const { onSetInferenceParams, inference } = useImageBot() + + return ( +
+

onSetInferenceParams(inference?.slug ?? null)} + > + Сбросить настройки +

+ +

popup.setState(false)} + > + Закрыть +

+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './switch-filter' \ No newline at end of file @@ -0,0 +1,52 @@ +.wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.switch { + display: none; + + &__label { + color: #a4aab5; + font-size: 15px; + line-height: 19.6px; + font-weight: 400; + } + & { + display: none; + + &:checked + .switch__slider { + background-color: #7f7df280; + + &::before { + transform: translateX(21px); + background-color: #7f7df3; + } + } + } + + &__slider { + position: relative; + width: 34px; + height: 13px; + background-color: #40404f61; + border-radius: 20px; + cursor: pointer; + transition: background-color 0.25s ease; + + &::before { + content: ''; + position: absolute; + top: -3.5px; + left: -3.5px; + width: 20px; + height: 20px; + background-color: #fff; + border-radius: 50%; + transition: transform 0.25s ease; + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + } +} @@ -0,0 +1,42 @@ +import React from 'react' +import { CommonTooltip } from '#/shared/ui/tooltip' +import styles from './switch-filter.module.scss' + +interface SwitchFilterProps { + name: string + value: boolean + setValue: (value: boolean) => void + description: string + withoutTooltip?: boolean +} + +export const SwitchFilter = ({ + name, + value, + description, + setValue, + withoutTooltip = false, +}: SwitchFilterProps) => { + return ( + + + + ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,17 +0,0 @@ -.wrapModelTitle { - margin-bottom: 20px; - margin-top: 15px; - // width: max-content; - width: 100% !important; - @media (max-width: 768px) { - margin: 0; - margin-top: 15px; - } - .bigTitle { - min-width: max-content !important; - @media (max-width: 768px) { - display: none; - overflow: hidden; - } - } -} @@ -1,40 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import Link from 'next/link' - -import styles from './title.module.scss' - -export interface TitleProps { - type: string - title: string - linkBack?: string - rightSlot?: React.ReactNode -} - -export default function Title(props: TitleProps) { - - return ( - - - - {' '} - {props.type} •{' '} - - -  {props.title} - - -
- - {props.title} - -
- {props.rightSlot} -
-
-
- ) -} @@ -0,0 +1 @@ +export * from './upscale-settings.popup' \ No newline at end of file @@ -0,0 +1,4 @@ +.popup{ + min-width: 200px; + min-height: 250px; +} \ No newline at end of file @@ -0,0 +1,15 @@ +import { getPopupById, POPUP_UPSCALE_SETTINGS, PopupTemplate } from '#/shared/ui/popup' +import React, { useEffect } from 'react' +import styles from './upscale-settings.popup.module.scss' + +export const UpscaleSettingsPopup = () => { + const popup = getPopupById(POPUP_UPSCALE_SETTINGS) + + return ( + +
+

НАСТРОЙКИ

+
+
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1 @@ +export * from './use-chat-message-actions' \ No newline at end of file @@ -0,0 +1,125 @@ +import { useAppDispatch } from '#/app/store/store' +import { getUserBalance } from '#/entities/balance' +import { + Message, + MessageEventStreamResponse, + MessageSend, + postMessage, + removeMessage, + useChatBotMessages, + useChatMessagesEvents, +} from '#/entities/message' +import { useChatBotParams } from '#/entities/model-entity' +import { useChatBotPagination } from '#/features/chat-bot-pagination' +import { useCurrentChat } from '#/features/chats' +import { makePrivateRequest } from '#/shared/api' +import { useEventSource } from '#/shared/lib/event-source' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { EventSourcePolyfill } from 'event-source-polyfill' +import { cloneDeep } from 'lodash' +import { useSession } from 'next-auth/react' +import { useEffect } from 'react' + +export function useChatMessageActions() { + const dispatch = useAppDispatch() + + const { messages, setLoading, setMessages, loading } = useChatBotMessages() + + const { botParams } = useChatBotParams() + + const { currentChat } = useCurrentChat() + + const { showMessage } = useShowDataStore() + + const { data: session } = useSession() + + const { event, addOpenCallback } = useEventSource() + + const { makeEvent } = useChatMessagesEvents() + + function getFileLink(file: File) { + return URL.createObjectURL(file) + '?type=.' + file.name.split('.')[1] + } + + function getOptimisticMessage(data: MessageSend): Message { + const date = new Date() + + const file = data.file ? getFileLink(data.file) : null + + return { + ...data, + file, + is_sent: true, + model: '', + from_model: false, + uid: 'new-send', + elapsed_time: '', + is_favourite: false, + created_at: date.toISOString(), + } + } + + const sendMessage = makePrivateRequest(async (data: MessageSend) => { + if (loading || !botParams || !currentChat) return + + const userMessage = getOptimisticMessage(data) + + const date = new Date() + + const modelMessage: Message = { + ...userMessage, + content: `Ваш вопрос получен. Ожидание ответа от ${botParams.slug}...`, + from_model: true, + file: null, + uid: 'new-bot-send', + created_at: date.toISOString(), + } + + setLoading(true) + + setMessages([...messages, userMessage, modelMessage]) + + const r = useChatBotMessages.getState().messages + + const initialMessages = r.filter((m) => !['new-bot-send', 'new-send'].includes(m.uid)) + + const { data: result, status } = await postMessage(currentChat, data) + + setLoading(false) + + if (status >= 400) { + userMessage.is_sent = false + + setMessages([...initialMessages, userMessage]) + + const error = result as { detail: string } + + if (error.detail) return showMessage(error.detail) + + return showMessage('Непредвиденная ошибка, попробуйте еще раз') + } + + ;(result as Message[])[1].content = modelMessage.content + + setMessages([...initialMessages, ...(result as Message[])]) + + makeEvent('chat-bot', () => { + ;(result as Message[])[1].content = '' + }) + + dispatch(getUserBalance(session?.access)) + }) + + const deleteMessage = makePrivateRequest(async (uid: string) => { + if (!currentChat) return + + const { status } = await removeMessage(currentChat, uid) + + if (status) setMessages(messages.filter((el) => el.uid !== uid)) + }) + + return { + sendMessage, + deleteMessage, + } +} @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { CopywriteProxy } from '#/domains/copywrite/proxy/copywrite-proxy' import { Template } from '#/domains/copywrite/proxy/types/template' -import { Message } from '#/shared/lib/types/model' +import { Message } from '#/entities/message' export interface Theme { templates: Template[] | null @@ -6,7 +6,7 @@ import { emptyTemplate } from '#/domains/copywrite/lib/constants' import { Template } from '#/domains/copywrite/proxy/types/template' import { loadGeneration, loadTemplates } from '#/features/use-copy/copy-slice' import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { Message } from '#/shared/lib/types/model' +import { Message } from '#/entities/message' interface UseCopy { currentTemplate: Template | null @@ -43,7 +43,8 @@ export const useCopy = (): UseCopy => { uid: '123', created_at: '123', file: null, - info: null, + info: {}, + model: 'unknown', from_model: false, elapsed_time: '12', is_favourite: false, @@ -7,8 +7,9 @@ import { Template } from '#/domains/copywrite/proxy/types/template' import { loadGeneration } from '#/features/use-copy/copy-slice' import { useAppDispatch } from '#/app/store/store' import { API_URL } from '#/shared/lib/constants' -import { Message, MessageSend } from '#/shared/lib/types/model' -import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' +import { MessageSend } from '#/shared/lib/types/model' +import { Message } from '#/entities/message' +import { useShowDataStore } from '#/shared/lib/hooks' type Languages = 'ru' | 'en' | 'it' | 'fr' type LanguagesText = 'Русский' | 'Английский' | 'Итальянский' | 'Французский' @@ -24,7 +25,10 @@ export const languages = { ...langs, Немецкий: 'de' } export const target_audiences = ['Вся', '18+', '21+', '30+', '14-20', '35-40'] export const tovs = ['Нейтральный', 'Спокойный', 'Агрессивный', 'Серьезный', 'Провокационный', 'Остроумный', 'Наставнический', 'Дружелюбный'] -type Setting = Pick +type Setting = Pick< + Template, + 'tov' | 'language' | 'resources_urls' | 'keywords' | 'target_audience' | 'theme' +> type UseTemplate = { text: EditorState @@ -78,7 +82,9 @@ export const useTemplate = (currentTemplate: Template | null): UseTemplate => { } }) - const [targetAudiences, setTargetAudiences] = useState(currentTemplate?.target_audience || target_audiences[0]) + const [targetAudiences, setTargetAudiences] = useState( + currentTemplate?.target_audience || target_audiences[0] + ) const [tov, setTov] = useState(currentTemplate?.tov || tovs[0]) @@ -120,12 +126,16 @@ export const useTemplate = (currentTemplate: Template | null): UseTemplate => { try { setIsLoading(true) - const { data } = await axios.post>(API_URL + '/copywrite/', dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${session?.access}`, - }, - }) + const { data } = await axios.post>( + API_URL + '/copywrite/', + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${session?.access}`, + }, + } + ) setIsLoading(false) const newContentState = ContentState.createFromText(data[0].content) @@ -0,0 +1 @@ +export * from './user-message-popup' \ No newline at end of file @@ -0,0 +1,22 @@ +.container { + padding: 6px 0px !important; + color: var(--air-color); +} + +.popup { + &__button { + font-size: 15px; + display: flex; + font-weight: 500; + align-items: center; + gap: 5px; + width: max-content; + padding: 8px 14px; + transition: all 0.3s ease; + width: 100%; + + &:hover { + backdrop-filter: brightness(90%); + } + } +} @@ -0,0 +1,74 @@ +import React from 'react' +import CopySvg from '#/assets/svg/copy.svg?react' +import TrashSvg from '#/assets/svg/trash.svg?react' +import FullscreeenSvg from '#/assets/svg/fullscreen-icon.svg?react' + +import styles from './user-message-popup.module.scss' + +import { useChatMessageActions } from '#/features/use-chat-message-actions' +import { MessageFullscreenModal } from '#/features/message-fullscreen-modal/ui/message-fullscreen-modal' +import { FULLSCREEN_CHAT_MESSAGE, getModalById } from '#/features/modals' +import { Message, useChatBotMessages } from '#/entities/message' +import { getPopupById, PopupTemplate, PoputTemplateHorizontal } from '#/shared/ui/popup' + +interface UserMessageActionsPopupProps { + id: string + horizontal?: PoputTemplateHorizontal + message: Message +} + +export const UserMessageActionsPopup = ({ + id, + horizontal = 'right', + message, +}: UserMessageActionsPopupProps) => { + const { deleteMessage } = useChatMessageActions() + const popup = getPopupById(id) + const content = popup.getStoreProperty('content')! + const modal = getModalById(FULLSCREEN_CHAT_MESSAGE) + + const handleAction = (e: React.MouseEvent, callback: () => void) => { + e.stopPropagation() + callback() + popup.setState(false) + } + + return ( + popup.setState(false)} + horizontal={horizontal} + className={styles.container} + id={id} + > +
+ +
+ +
+ + + +
+ ) +} @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -0,0 +1,25 @@ +.button { + background-color: #f1faff; + color: rgb(43, 43, 66); + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 10px 16px; + border-radius: 13px; + transition: opacity 0.3s ease-in-out; + gap: 15px; + font-size: 16px; + font-weight: 500; + + &:hover { + opacity: 0.8; + } +} + +html[data-theme='dark'] { + .button { + background-color: #1C1C1E; + color: white; + } +} @@ -0,0 +1,45 @@ +import { LoginPage } from '#/views/login' +import { RegisterPage } from '#/views/register' +import '@testing-library/jest-dom' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { jestRender } from '#/../jest/utils/render' +import { YandexAuthButton } from './yandex-auth-button' +import { windowMock } from '#/../jest/utils/window-mock' +import { fetchToCrossfetch } from '#/../jest/utils/fetch-to-crossfetch' + +jest.mock('next/router') +jest.mock('@sentry/nextjs') + +windowMock() +fetchToCrossfetch() + +describe('yandex-auth-button', () => { + it('renders in login', () => { + jestRender() + + const button = screen.getByTestId('yandex-auth-button') + + expect(button).toBeInTheDocument() + }) + + it('renders in register', () => { + jestRender() + + const button = screen.getByTestId('yandex-auth-button') + + expect(button).toBeInTheDocument() + }) + + it('opened signin url', async () => { + jestRender() + + const button = screen.getByTestId('yandex-auth-button') + + await userEvent.click(button) + + await waitFor(() => { + expect(window.location.href).toContain('api/auth/signin?csrf=true') + }) + }) +}) @@ -1,51 +1,19 @@ -import theme from '#/entities/theme/model/theme' -import { useThemeAndDevice } from '#/shared/lib/hooks' -import { Button } from '@mui/material' +import React from 'react' +import YandexSvg from '#/assets/svg/yandex.svg?react' +import styles from './yandex-auth-button.module.scss' import { signIn } from 'next-auth/react' -import Image from 'next/image' -import React, { useState } from 'react' interface YandexAuthButtonProps {} export const YandexAuthButton = ({}: YandexAuthButtonProps) => { - const { theme } = useThemeAndDevice() - - const [hovered, setHovered] = useState(false) - return ( - + + Войти с Яндекс ID + ) } @@ -0,0 +1,70 @@ +import React, { useEffect, useMemo } from 'react' +import { Avatar, Box, Button, Typography } from '@mui/material' +import Image from 'next/image' +import Link from 'next/link' +import LockSvg from '#/assets/svg/lock.svg?react' +import BlockedSvg from '#/assets/svg/blocked.svg?react' + +import styles from './card.module.scss' +import { useThemeAndDevice } from '#/shared/lib/hooks' + +export type CardProps = { + title: string + icon: string + text: string + uid: string + blocked?: boolean + slug: string + accessed_models?: string[] | null +} + +const ChatCard = ({ text, icon, title, uid, slug, accessed_models, blocked }: CardProps) => { + const link = useMemo(() => { + return accessed_models && !accessed_models.includes(slug) ? '/account?scope=subscribe' : `chat-bot/${slug}` + }, [accessed_models]) + + const { theme } = useThemeAndDevice() + + return ( + + + + + + + {title} + {text} + + + {blocked ? ( +
+ + + Модель недоступна + +
+ ) : ( + accessed_models && + !accessed_models.includes(slug) && ( +
+ + Недоступно в текущем тарифе +
+ ) + )} +
+ + ) +} + +export default ChatCard @@ -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 +} @@ -1,6 +1,6 @@ -import { ChatBotsPage } from "#/views/chat-bot"; -import { getDefaultLayout } from "#/widgets/layouts"; +import { ChatBotsPage } from '#/views/chat-bot' +import { getDefaultLayout } from '#/widgets/layouts' -ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }); +ChatBotsPage.getLayout = getDefaultLayout({ titlePage: 'Чат-боты' }) export default ChatBotsPage @@ -1,48 +0,0 @@ -import React from 'react' -import { Avatar, Box, Typography } from '@mui/material' -import Image from 'next/image' -import Link from 'next/link' - -import styles from './card.module.scss' - -export type CardProps = { - title: string - icon: string - changeFavorite: (uid: string) => Promise - isFavorite: boolean - text: string - link: string - uid: string - companies: string -} - -const Card = ({ changeFavorite, isFavorite, text, icon, title, link, uid, companies }: CardProps) => { - return ( - - - - - { - e.preventDefault() - changeFavorite(uid) - }} - src={isFavorite ? '/svg/sub_menu/favourite.svg' : '/svg/sub_menu/favourite_off.svg'} - width={25} - height={25} - alt={''} - /> - - - {title} - {text} - - {companies} - - - - - ) -} - -export default Card @@ -1,278 +0,0 @@ -import * as React from 'react' -import { useState } from 'react' -import { Stack, Typography } from '@mui/material' -import Box from '@mui/material/Box' -import Button from '@mui/material/Button' -import dynamic from 'next/dynamic' -import Image from 'next/image' -import { getSession } from 'next-auth/react' - -import { toEditorState } from '#/domains/copywrite/lib/helper' -import { useCopy } from '#/features/use-copy/use-copy' -import { languages, target_audiences, tovs, useTemplate } from '#/features/use-copy/use-template' -import { Layout } from '#/app/layout' -import { Input, Loader, TooltipCustom } from '#/shared' -import { api } from '#/shared/api/endpoints' -import { getTypeDevice } from '#/shared/lib/helpers' -import { Message } from '#/shared/lib/types/model' -import { IDalleProps } from '#/shared/lib/types/types-dalle' -import { SelectUI } from '#/shared/ui/select' - -import Title from '../../features/title/title' - -import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' - -const toolbarOptions = { - options: ['inline', 'blockType', 'list', 'textAlign', 'history'], - inline: { - options: ['bold', 'italic', 'underline'], - }, - blockType: { - options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote'], - }, - fontSize: { - options: [12, 14, 16, 18, 24, 30, 36], - }, - fontFamily: { - options: ['Arial', 'Georgia', 'Impact', 'Tahoma', 'Times New Roman', 'Verdana'], - }, - list: { - options: ['unordered', 'ordered'], - }, - textAlign: { - options: ['left', 'center', 'right'], - }, -} - -export async function getServerSideProps(context: any): Promise<{ props: IDalleProps }> { - const device = getTypeDevice(context) - - const { req } = context - - const session = await getSession({ req }) - - const token = session?.access || null - - const favorites = await api.getFavoritesModel(token, session) - - return { - props: { - device, - token, - favorites, - }, - } -} - -const Create: React.FC = ({ device, token, favorites }) => { - const desktop = device === 'desktop' - - const [showGeneration, setShowGeneration] = useState(false) - - const [isCopy, setIsCopy] = useState(false) - - const copy = (text: string) => { - navigator.clipboard.writeText(text) - setIsCopy(true) - } - - const { currentTemplate, generations, pickGeneration, setPickGeneration, createEmpty } = useCopy() - - const { - tov, - lang, - setLang, - setTov, - setTargetAudiences, - targetAudiences, - setResourceUrls, - resource_urls, - setTheme, - theme, - keywords, - setKeywords, - createText, - text, - clearSetting, - isLoading, - content, - setContent, - onEditorChange, - } = useTemplate(currentTemplate) - - const onSetGeneration = (message: Message) => { - setPickGeneration(message) - onEditorChange(toEditorState(message.content)) - setShowGeneration(false) - } - - const Editor = dynamic(() => import('react-draft-wysiwyg').then((res) => res.Editor), { - ssr: false, - }) - - const EditorWrap = (): JSX.Element | null => { - if (showGeneration) { - if (!generations || generations.length === 0) { - return null - } - - //@ts-ignore - return generations.map((el) => ( - onSetGeneration(el)} - display='flex' - justifyContent='space-between' - className='border-bottom-1px-gray' - sx={{ cursor: 'pointer' }} - padding='10px' - key={el.uid} - > - {el.content.slice(0, 60)}... - - copy(el.content)} - src={'/svg/copy.svg'} - width={22} - height={22} - alt={'copy'} - /> - - - )) - } - - return ( - - ) - } - - return ( - - - <Box - display={'flex'} - justifyContent='space-between' - flexDirection={desktop ? 'row' : 'column-reverse'} - sx={{ - marginBottom: desktop ? 0 : 3, - width: desktop ? '98%' : '100%', - marginTop: desktop ? 3 : 0, - }} - > - <Box className='pd-30 bg-color-block border-radius-main' width='100%' position='relative'> - <Box height='95%' sx={{ overflowY: 'auto' }}> - <Box width='50%' display='flex' marginBottom={2}> - {!showGeneration && ( - <Button - fullWidth - className='btn-classic' - onClick={() => setShowGeneration(true)} - > - Мои генерации - </Button> - )} - <Button - fullWidth - sx={{ marginLeft: 1 }} - className='btn-classic' - onClick={() => { - onEditorChange(toEditorState('')) - setShowGeneration(false) - }} - > - Пустой шаблон - </Button> - </Box> - <EditorWrap /> - </Box> - </Box> - <Box - height='auto' - className='pd-30 bg-color-block border-radius-main' - width='500px' - marginLeft={1.5} - > - <Typography className='font-16 color-gray'>Настройки генерации</Typography> - <Stack spacing={0.5} sx={{ marginTop: 2.5 }}> - <Typography className='title-main-gray'>Ваш запрос</Typography> - <textarea value={content} onChange={(e) => setContent(e.target.value)} /> - </Stack> - <Box sx={{ marginTop: 1 }}> - <SelectUI - title={'Тон'} - value={tov} - onChange={(e) => setTov(e.target.value)} - list={tovs} - /> - </Box> - <Box sx={{ marginTop: 1 }}> - <SelectUI - title={'Язык'} - value={lang} - onChange={(e) => setLang(e.target.value)} - list={Object.keys(languages)} - /> - </Box> - <Box sx={{ marginTop: 1 }}> - <SelectUI - title={'Аудитория'} - value={targetAudiences} - onChange={(e) => setTargetAudiences(e.target.value)} - list={target_audiences} - /> - </Box> - <Stack spacing={0.5} sx={{ marginTop: 1 }}> - <Typography className='title-main-gray'>Тема</Typography> - <Input value={theme} onChange={(e) => setTheme(e.target.value)} /> - </Stack> - <Stack spacing={0.5} sx={{ marginTop: 1 }}> - <Typography className='title-main-gray'>Ключевые слова (через запятую)</Typography> - <Input value={keywords} onChange={(e) => setKeywords(e.target.value.split(','))} /> - </Stack> - <Stack spacing={0.5} sx={{ marginTop: 1 }}> - <Typography className='title-main-gray'>Ресурсы (ссылки, через запятую)</Typography> - <Input - value={resource_urls} - onChange={(e) => setResourceUrls(e.target.value.split(','))} - /> - </Stack> - <Stack sx={{ marginTop: 1 }}> - <Typography - onClick={clearSetting} - className='text' - color='#FF2372 !important' - sx={{ cursor: 'pointer' }} - > - Сбросить настройки - </Typography> - </Stack> - <Stack sx={{ marginTop: 3 }}> - {isLoading ? ( - <Box width='100%' display='flex' justifyContent='center'> - <Loader /> - </Box> - ) : ( - <Button className='btn-classic' onClick={createText}> - Сгенерировать - </Button> - )} - </Stack> - </Box> - </Box> - </Layout> - ) -} - -export default Create @@ -1,83 +0,0 @@ -import * as React from 'react' -import { useEffect } from 'react' -import { Typography } from '@mui/material' -import Box from '@mui/material/Box' -import { getSession, useSession } from 'next-auth/react' - -import { loadTemplates } from '#/features/use-copy/copy-slice' -import { Layout } from '#/app/layout' -import { useAppDispatch, useAppSelector } from '#/app/store/store' -import { api } from '#/shared/api/endpoints' -import { getTypeDevice } from '#/shared/lib/helpers' -import { IDalleProps } from '#/shared/lib/types/types-dalle' - -import Card from './card' - -export async function getServerSideProps(context: any): Promise<{ props: IDalleProps }> { - const device = getTypeDevice(context) - - const { req } = context - - const session = await getSession({ req }) - - const token = session?.access || null - - const favorites = await api.getFavoritesModel(token, session) - - return { - props: { - device, - token, - favorites, - }, - } -} - -const CopyPage: React.FC<IDalleProps> = ({ device, token, favorites }) => { - const desktop = device === 'desktop' - - const { data } = useSession() - - const templates = useAppSelector((state) => state.copy.templates) - - const dispatch = useAppDispatch() - - useEffect(() => { - if (data?.access) { - dispatch(loadTemplates(data.access)) - } - }, [data?.access]) - - return ( - <Layout titlePage={'Копирайтинг'}> - <Typography sx={{ fontSize: 24, fontWeight: 'bold', marginTop: '25px' }}>Копирайтинг</Typography> - <Box - display={'flex'} - flexDirection={desktop ? 'row' : 'column-reverse'} - sx={{ - marginBottom: desktop ? 0 : 3, - width: desktop ? '98%' : '100%', - marginTop: desktop ? 3 : 0, - }} - > - {templates?.map((el) => { - return ( - <Card - icon={el.picture} - key={el.id} - changeFavorite={async () => {}} - link={`/copy/create?id=${el.id}`} - isFavorite={false} - text={el.description} - title={el.title} - uid={el.id.toString()} - companies={''} - /> - ) - })} - </Box> - </Layout> - ) -} - -export default CopyPage @@ -0,0 +1,6 @@ +import { MainPage } from '#/views/index' +import { getDefaultLayout } from '#/widgets/layouts' + +MainPage.getLayout = getDefaultLayout({ titlePage: 'AIR' }) + +export default MainPage @@ -0,0 +1,6 @@ +import { ImageModelPage } from '#/views/old-images' +import { getDefaultLayout } from '#/widgets/layouts' + +ImageModelPage.getLayout = getDefaultLayout() + +export default ImageModelPage @@ -0,0 +1,6 @@ +import { ImageModelsPage } from '#/views/old-images' +import { getDefaultLayout } from '#/widgets/layouts' + +ImageModelsPage.getLayout = getDefaultLayout({ title: 'Изображения' }) + +export default ImageModelsPage @@ -1,6 +0,0 @@ -import { ImageModelsPage } from "#/views/images"; -import { getDefaultLayout } from "#/widgets/layouts"; - -ImageModelsPage.getLayout = getDefaultLayout({ titlePage: 'Изображения' }) - -export default ImageModelsPage @@ -13,27 +13,13 @@ import { store, useAppSelector } from '#/app/store/store' import ErrorBoundary from './error-boundary' -import '#/app/styles/globals.css' +import '#/app/styles/globals.scss' import '#/app/styles/styles-pages/system.scss' import { NextPage } from 'next' import { pingFangFont } from '#/shared/lib/constants/font/font' import { useBlockTelegram } from '#/shared/lib/hooks/use-block-telegram' import { Error } from '#/shared' - -// import fetch from 'cross-fetch' - -// global.fetch = (...params: Parameters<typeof fetch>) => { -// let url = params[0] - -// const baseUrl = process.env.NEXT_PUBLIC_NEXTAUTH_URL! - -// if (typeof url === 'string' && !(url as string).startsWith(baseUrl) && !(url as string).includes('http')) { -// url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) + params[0] : baseUrl + params[0] -// console.log(url) -// } - -// return fetch(url, params[1]) -// } +import { Providers } from '#/widgets/providers' axios.defaults.httpsAgent = new https.Agent({ rejectUnauthorized: false, @@ -75,20 +61,8 @@ export interface AppPropsWithLayout extends AppProps { function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) { useBlockTelegram() - const getLayout = Component.getLayout ?? ((page) => page) - if ('serviceWorker' in navigator) { - navigator.serviceWorker - .getRegistrations() - .then(function (registrations) { - for (let registration of registrations) { - registration.unregister() - } - }) - .catch(function (error) { - console.error('Ошибка при размонтировании service worker:', error) - }) - } + const getLayout = Component.getLayout ?? ((page) => page) return ( <> @@ -103,8 +77,10 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa <Provider store={store}> <SessionProvider session={session} refetchInterval={5 * 60}> <ThemeProvider theme={pingFangFont}> - {getLayout(<Component {...pageProps} />)} - <Error /> + <Providers> + {getLayout(<Component {...pageProps} />)} + <Error /> + </Providers> </ThemeProvider> </SessionProvider> </Provider> @@ -1,6 +1,8 @@ +import { PopupBackdrop } from '#/shared/ui/popup' import { Head, Html, Main, NextScript } from 'next/document' export default function Document() { + return ( <Html lang='ru'> <Head> @@ -35,7 +37,9 @@ export default function Document() { </div> </noscript> <Main /> + <div id='popup-container'></div> <div id='modal-container'></div> + <div id='popup-container'></div> <NextScript /> </body> </Html> @@ -1,258 +0,0 @@ -import React from 'react' -import { Box, Card, CardMedia, Stack, TextField, Typography } from '@mui/material' -import Button from '@mui/material/Button' -import axios from 'axios' -import Head from 'next/head' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/router' - -import { Layout } from '#/app/layout' -import { useAppSelector } from '#/app/store/store' -import { API_URL } from '#/shared/lib/constants/constants' -import { getRandomImage, getTypeDevice } from '#/shared/lib/helpers' -import { TDeviceProp } from '#/shared/lib/types/entities' - -export async function getServerSideProps(context: any): Promise<{ props: TDeviceProp }> { - const device = getTypeDevice(context) - - return { - props: { - device, - }, - } -} - -const ChangePassword: React.FC<TDeviceProp> = ({ device }) => { - const [password1, setPassword1] = React.useState<string>('') - - const [password2, setPassword2] = React.useState<string>('') - - const [isChange, setIsChange] = React.useState(false) - - const { query, push } = useRouter() - - const changePassword = async () => { - let formData: any = new FormData() - - formData.append('password_1', password1) - formData.append('password_2', password2) - - if (password1.trim() && password2.trim()) { - try { - await axios.post(API_URL + `/auth/change-pass?token=${query.token}`, formData, { - headers: { - 'content-type': 'multipart/form-data ', - }, - }) - setIsChange(true) - setTimeout(() => push('/login'), 5000) - } catch (err) {} - } - } - - const desktop = device === 'desktop' - - const refImage = React.useRef(getRandomImage()) - - const theme = useAppSelector((state) => state.theme.theme) - - return ( - <Layout titlePage={'Авторизация'} isAuthPage={true}> - <Head> - <title>Восстановление аккаунта - - - - - - - {''} - - {desktop && ( - - - - - Midjourney - - - by honeynek - - - - - )} - {isChange ? ( - - - Ваш пароль успешно изменён! - - - Войти в аккаунт - - - ) : ( - - - Восстановление пароля - - - Новый пароль - - setPassword1(e.target.value)} - > - - Подтверждение пароля - - setPassword2(e.target.value)} - > - - - - - )} - - - ) -} - -export default ChangePassword @@ -1,7 +1,7 @@ -import { MainPage } from '#/views/index' +import { MainPage } from '#/views/main' import { getDefaultLayout } from '#/widgets/layouts' +MainPage.getLayout = getDefaultLayout({ titlePage: 'Главная' }) -MainPage.getLayout = getDefaultLayout({ titlePage: 'AIR' }) export default MainPage @@ -1,6 +1,7 @@ import { LoginPage } from '#/views/login' import { getLayout, LayoutWithoutSideMenu } from '#/widgets/layouts' + LoginPage.getLayout = getLayout(LayoutWithoutSideMenu, { titlePage: 'Авторизация' }) export default LoginPage @@ -0,0 +1,6 @@ +import { UpscalePage } from '#/views/upscale' +import { getDefaultLayout } from '#/widgets/layouts' + +UpscalePage.getLayout = getDefaultLayout({ titlePage: 'Upscale' }) + +export default UpscalePage @@ -1,25 +1,28 @@ import axios from 'axios' import { API_URL } from '#/shared/lib/constants' -import { IShortModel, IModel } from '#/entities/model-entity' +import { ShortModel, Model } from '#/entities/model-entity' const model_api = { - async getBots(token?: string): Promise { + async getBots(token?: string): Promise { try { - const { data } = await axios.get(API_URL + '/ml_models/?category=chat-bots', { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get( + API_URL + '/ml_models/?category=chat-bots', + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return err } }, - async getBotParams(slug: string, token?: string): Promise { + async getBotParams(slug: string, token?: string): Promise { try { - const { data } = await axios.get(API_URL + `/ml_models/${slug}`, { + const { data } = await axios.get(API_URL + `/ml_models/${slug}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -30,7 +33,7 @@ const model_api = { } }, - async getImages(token?: string): Promise { + async getImages(token?: string): Promise { try { const { data } = await axios.get(API_URL + '/ml_models/?category=images', { headers: { @@ -1,3 +1,4 @@ +// @ts-nocheck import React, { useCallback, useEffect, useRef, useState } from 'react' import axios, { AxiosError, AxiosResponse } from 'axios' import base64 from 'base64-encode-file' @@ -12,7 +13,7 @@ import { IMessageRequest } from '#/shared/lib/types/types-gpt' import { Message, MessageSend } from '#/entities/message' import { Variant } from '#/shared/lib/hooks/use-show-data' -const formDataHelper = (file: File, dataForSend: MessageSend): FormData => { +const formDataHelper = (file: File, dataForSend: MessageSend): FormData => { const FD = new FormData() FD.append('file', file) FD.append('info', JSON.stringify(dataForSend.info)) @@ -24,11 +25,14 @@ const formDataHelper = (file: File, dataForSend: MessageSend): FormData => export const ModelsWithChatsEndpoints = { getData: async (chatUid: string, offset: number, token?: string) => { try { - const { data } = await axios.get(API_URL + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get( + API_URL + `/chats/${chatUid}/messages/?limit=10&offset=${offset}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return { @@ -38,8 +42,13 @@ export const ModelsWithChatsEndpoints = { } } }, - sendData: async (chatUid: string | null, dataForSend: MessageSend | FormData, token?: string) => { - const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + sendData: async ( + chatUid: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' return await axios.post>( API_URL + `/chats/${chatUid}/messages/`, @@ -58,8 +67,7 @@ export const ModelsWithChatsEndpoints = { export function useModel( currentChat: string | null, showMessage: (message: string, variant?: Variant) => void, - modelType: string, - clearInput?: () => void + modelType: string ) { const { data } = useSession() const [messages, setMessages] = useState(null) @@ -72,7 +80,11 @@ export function useModel( setMessages([]) ;(async () => { setLoading(true) - const answer = await ModelsWithChatsEndpoints.getData(currentChat, 0, data?.access) + const answer = await ModelsWithChatsEndpoints.getData( + currentChat, + 0, + data?.access + ) setLoading(false) if (Array.isArray(answer)) { @@ -89,7 +101,11 @@ export function useModel( const getMessagesPagination = async () => { if (currentChat) { setLoading(true) - const answer = await ModelsWithChatsEndpoints.getData(currentChat, offset, data?.access) + const answer = await ModelsWithChatsEndpoints.getData( + currentChat, + offset, + data?.access + ) setLoading(false) if (Array.isArray(answer) && messages != null) { @@ -103,7 +119,7 @@ export function useModel( } } - const sendMessage = async (dataForSend: MessageSend) => { + const sendMessage = async (dataForSend: MessageSend) => { if (loading) { return } @@ -115,7 +131,11 @@ export function useModel( info: dataForSend.info as any, is_sent: true, model: '', - file: dataForSend.file ? ((URL.createObjectURL(dataForSend.file) + '?type=.' + dataForSend.file.name.split('.')[1]) as string) : null, + file: dataForSend.file + ? ((URL.createObjectURL(dataForSend.file) + + '?type=.' + + dataForSend.file.name.split('.')[1]) as string) + : null, from_model: false, uid: 'new-send', elapsed_time: '', @@ -151,16 +171,19 @@ export function useModel( formData = FD } - const { data: result, status } = await ModelsWithChatsEndpoints.sendData(currentChat, formData ?? dataForSend, data?.access) + const { data: result, status } = await ModelsWithChatsEndpoints.sendData( + currentChat, + formData ?? dataForSend, + data?.access + ) setMessages((prev) => prev!.slice(0, -2)) setLoading(false) - // clearTimeout(timeout) - if (status >= 400) { userMessage.is_sent = false + setMessages((prev) => [...prev!, userMessage]) const error = result as { detail: string } if (error.detail) return showMessage(error.detail) @@ -169,15 +192,19 @@ export function useModel( setMessages((prev) => [...prev!, ...(result as Message[])]) } + setMessages((prev) => [...prev!, ...(result as Message[])]) dispatch(getUserBalance(data?.access)) } const deleteMessage = (message_uid: string) => { - axios.delete(process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, { - headers: { - Authorization: `Bearer ${data?.access}`, - }, - }).then(() => { + axios.delete( + process.env.NEXT_PUBLIC_API_HOST + `/chats/${currentChat}/messages/${message_uid}`, + { + headers: { + Authorization: `Bearer ${data?.access}`, + }, + } + ).then(() => { if (messages) setMessages(messages?.filter((el) => el.uid !== message_uid)) }) } @@ -188,11 +215,14 @@ export function useModel( export const ModelsWithImagesEndpoints = { getData: async (type: string, offset: number, token?: string) => { try { - const { data } = await axios.get(API_URL + `/media/image/${type}?limit=10&offset=${offset}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get( + API_URL + `/media/image/${type}?limit=10&offset=${offset}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { return { @@ -202,17 +232,26 @@ export const ModelsWithImagesEndpoints = { } } }, - sendData: async (type: string | null, dataForSend: MessageSend | FormData, token?: string) => { - const HeaderDataType = dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' + sendData: async ( + type: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { + const HeaderDataType = + dataForSend instanceof FormData ? 'multipart/form-data' : 'application/json' try { - const { data } = await axios.post>(API_URL + `/media/image/${type}`, dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': HeaderDataType, - }, - }) + const { data } = await axios.post>( + API_URL + `/media/image/${type}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': HeaderDataType, + }, + } + ) return data } catch (err: any) { @@ -225,7 +264,11 @@ export const ModelsWithImagesEndpoints = { }, } -export function useModelImages(showMessage: (message: string) => void, type: string, device: Device) { +export function useModelImages( + showMessage: (message: string) => void, + type: string, + device: Device +) { const { data } = useSession() const { getData, sendData } = ModelsWithImagesEndpoints @@ -289,21 +332,35 @@ export function useModelImages(showMessage: (message: string) => void, type: // console.log(offsetRef.current); // console.log(messagesRef.current); - if (dataRef.current?.access && typeRef.current !== '' && typeRef.current !== undefined) { + if ( + dataRef.current?.access && + typeRef.current !== '' && + typeRef.current !== undefined + ) { setLoading(true) - const answer = await getData(typeRef.current, offsetRef.current, dataRef.current?.access) + const answer = await getData( + typeRef.current, + offsetRef.current, + dataRef.current?.access + ) setLoading(false) - if (Array.isArray(answer) && messagesRef.current != null && device === 'mobile') { + if ( + Array.isArray(answer) && + messagesRef.current != null && + device === 'mobile' + ) { const newMessages = answer.reverse() setMessages([...newMessages, ...messagesRef.current]) setOffset((prev) => prev + answer.length) - } else if (Array.isArray(answer) && messagesRef.current != null && device === 'desktop') { + } else if ( + Array.isArray(answer) && + messagesRef.current != null && + device === 'desktop' + ) { setMessages([...messagesRef.current, ...answer]) setOffset((prev) => prev + answer.length) } else { - console.log('err') - showMessage('Ошибка загрузки сообщений') return } @@ -312,7 +369,7 @@ export function useModelImages(showMessage: (message: string) => void, type: [data?.access, type, offset, messages] ) - const createImage = async (dataForSend: MessageSend) => { + const createImage = async (dataForSend: MessageSend) => { const { content, file } = dataForSend setIsComplete(false) @@ -325,8 +382,9 @@ export function useModelImages(showMessage: (message: string) => void, type: setLoading(false) if (result.hasOwnProperty('error')) { - //@ts-ignore - const message = (result.details as AxiosError).response.data.trim() ?? 'Ошибка отправки сообщения' + const message = + (result.details as AxiosError).response.data.trim() ?? + 'Ошибка отправки сообщения' showMessage(message) return } @@ -367,14 +425,22 @@ export const ModelsMediaApi = { } } }, - sendData: async (type: string | null, dataForSend: MessageSend | FormData, token?: string) => { + sendData: async ( + type: string | null, + dataForSend: MessageSend | FormData, + token?: string + ) => { try { - const { data } = await axios.post>(API_URL + `/media/audio/${type}`, dataForSend, { - withCredentials: true, - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.post>( + API_URL + `/media/audio/${type}`, + dataForSend, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data } catch (err: any) { @@ -414,7 +480,7 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri } }, [data?.access]) - const sendMessage = async (dataForSend: MessageSend) => { + const sendMessage = async (dataForSend: MessageSend) => { if (input.trim() === '') { showMessage('Введите запрос!') return @@ -426,9 +492,9 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri const userMessage: Message = { content: input, - info: null, + info: {}, + model: modelType, is_sent: true, - model: '', file: null, from_model: false, uid: 'new-send', @@ -456,7 +522,9 @@ export function useMedia(showMessage: (message: string) => void, modelType: stri if (result.hasOwnProperty('error')) { try { //@ts-ignore - const message = (result.details as AxiosError).response.data.trim() ?? 'Ошибка отправки сообщения' + const message = + (result.details as AxiosError).response.data.trim() ?? + 'Ошибка отправки сообщения' showMessage(message) return } catch (e) { @@ -1,6 +1,6 @@ type ModelForChats = 'chatgpt' | 'llama2' | 'vicuna' | 'deepl' | 'mistral' -export interface IShortModel { +export interface ShortModel { uid: string title: string description: string @@ -8,18 +8,18 @@ export interface IShortModel { image: string } -export interface IModel { +export interface Model { uid: string title: string description: string slug: string image: string settings: { is_active: boolean } - parameters: IModelParams[] - versions: IModelVersions[] - inputs: IModelInputs[] + parameters: InferenceParams[] + versions: ModelVersions[] + inputs: ModelInputs[] } -export interface IModelParams { +export interface InferenceParams { name: string description: string key: string @@ -34,13 +34,13 @@ export interface IModelParams { } versions: string[] } -export interface IModelVersions { +export interface ModelVersions { name: string description: string default: boolean slug: string } -export interface IModelInputs { +export interface ModelInputs { // type: 'image' | 'zip' | 'text' | 'audio' | 'pdf' | 'txt' type: string required: boolean @@ -7,7 +7,7 @@ import { Error } from '#/shared/lib/types/entities' import { IImagesResponse } from '#/shared/lib/types/types-dalle' import { IMessageRequest, ISendMessageResponse } from '#/shared/lib/types/types-gpt' -import { Message } from '../lib/types/model' +import { Message } from '#/entities/message' const API_URL = process.env.NEXT_PUBLIC_API_HOST @@ -45,7 +45,10 @@ export const api = { } }, - async getImagesDalle(token: string | null, url: string): Promise | null> { + async getImagesDalle( + token: string | null, + url: string + ): Promise | null> { try { const { data } = await axios.get(url, { headers: { @@ -94,7 +97,11 @@ export const api = { } }, - async sendMessageChatGPT(token: string, message: any, uid: string): Promise { + async sendMessageChatGPT( + token: string, + message: any, + uid: string + ): Promise { try { const { data } = await axios.post>( API_URL + `/chats/${uid}/messages/`, @@ -113,13 +120,19 @@ export const api = { } }, - async getFavoritesModel(token: string | null, session: Session | null): Promise { + async getFavoritesModel( + token: string | null, + session: Session | null + ): Promise { try { - const { data } = await axios.get(API_URL + `/ml_models/${session?.user?.name}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + const { data } = await axios.get( + API_URL + `/ml_models/${session?.user?.name}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) return data?.filter((model) => model.is_favourite) } catch (err) { @@ -1,2 +1,2 @@ export * from './instance' -export * from './private-request' \ No newline at end of file +export * from './private-request' @@ -3,6 +3,7 @@ import axios from 'axios' export const api = axios.create({ validateStatus: () => true, baseURL: process.env.NEXT_PUBLIC_API_HOST, + withCredentials: true, }) api.interceptors.response.use( @@ -1,14 +1,14 @@ import { useSession } from 'next-auth/react' import { api } from './instance' -export function makePrivateRequest(callback: (...params: any) => Promise) { +export function makePrivateRequest( + callback: (...params: Params) => Promise +) { const { data } = useSession() - return async (...params: any) => { - if (!data) return - + return async (...params: Params) => { api.interceptors.request.use((config) => { - config.headers.Authorization = `Bearer ${data.access}` + config.headers.Authorization = `Bearer ${data?.access}` return config }) @@ -0,0 +1,33 @@ +type Task = () => Promise + +export class AsyncQueue { + queue: Task[] + processing: boolean + + constructor() { + this.queue = [] + this.processing = false + } + + async push(task: Task): Promise { + this.queue.push(task) + await this.processQueue() + } + + private async processQueue(): Promise { + if (this.processing) { + return + } + + this.processing = true + + while (this.queue.length > 0) { + const task = this.queue.shift() + if (task) { + await task() + } + } + + this.processing = false + } +} @@ -0,0 +1 @@ +export * from './async-queue' \ No newline at end of file @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,27 @@ +// @ts-nocheck +export class EventBus { + private eventObject: object = {}; + + publish(eventName: string, data?: any) { + const callbackList = this.eventObject[eventName]; + + if (!callbackList) return console.warn(eventName + ' not found!'); + + if(typeof data === 'object'){ + data = JSON.stringify(data); + } + + for (let callback of callbackList) { + callback(data); + } + } + subscribe(eventName: string, callback: (data: any) => void) { + if (!this.eventObject[eventName]) { + this.eventObject[eventName] = []; + } + + this.eventObject[eventName].push(callback); + } +} + +export const eventBus = new EventBus(); @@ -0,0 +1 @@ +export * from './event-bus' \ No newline at end of file @@ -0,0 +1 @@ +export * from './model' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './async-queue' +export * from './event-bus' \ No newline at end of file @@ -0,0 +1 @@ +export * from './load-file-types' \ No newline at end of file @@ -0,0 +1,10 @@ +export const acceptTypes: Record = { + image: 'image/png,image/jpeg', + zip: 'application/zip', + pdf: 'application/pdf', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + doc: 'application/msword', + audio: 'audio/*', + txt: '.txt', + text: null, +} @@ -0,0 +1,54 @@ +import React, { useMemo, useRef } from 'react' +import { acceptTypes } from '../config' +import PinnedFileSvg from '#/assets/svg/pinned-file.svg?react' +import AttachFileSvg from '#/assets/svg/attach-file.svg?react' + +interface CommonLoadFileProps { + loading: boolean + unpinFile?: () => void + file: File | null | undefined + setFile: (file: File) => void + inputTypes: string[] +} + +export const CommonLoadFile = ({ + loading, + unpinFile, + file, + setFile, + inputTypes, +}: CommonLoadFileProps) => { + const ref = useRef(null) + + if (loading || inputTypes.length === 0) return <> + + return ( + <> + {file ? ( + + ) : ( + <> + { + if (!e.target.files) return + setFile(e.target.files[0]) + }} + alt='Загрузка файла' + /> + + + )} + + ) +} @@ -0,0 +1 @@ +export * from './common-load-file' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,9 +1,10 @@ import { RegisterOptions } from 'react-hook-form/dist/types/validator' -const EMAIL_REGEXP = +export const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu -const PHONE_REGEXP = /^((8|\+7)[\- ]?)?(\(?\d{3}\)?[\- ]?)?[\d\- ]{7,10}$/ +export const PHONE_REGEXP = /^((8|\+7)[\- ]?)?(\(?\d{3}\)?[\- ]?)?[\d\- ]{7,10}$/ + export const emailOptions: RegisterOptions = { required: 'Поле email обязательно к заполенению!', @@ -1 +1,2 @@ export { API_HOST, API_URL, ModelPagesList, surpriseMePrompts } from './constants' +export * from './hook-form-options' \ No newline at end of file @@ -0,0 +1,101 @@ +const API_URL = process.env.NEXT_PUBLIC_API_HOST + +import { EventSourcePolyfill } from 'event-source-polyfill' +import { useSession } from 'next-auth/react' +import { useEventStore } from './events-store' + +export function useEventSource() { + const { events, setEvents } = useEventStore() + + const { data } = useSession() + + function event(name: string, src: string, callback?: (value: T) => void) { + if (!API_URL) return + + const url = API_URL.slice(-1) !== '/' ? API_URL : API_URL.slice(-1) + + const { events } = useEventStore.getState() + + setEvents([ + ...events, + { + name, + obj: new EventSourcePolyfill(`${url}/${src}`, { + headers: { Authorization: `Bearer ${data?.access}` }, + }), + }, + ]) + + if (callback) { + addEventCallback(name, callback) + } + } + + function addCloseEvent(name: string, callback: EventListenerOrEventListenerObject) { + const { events } = useEventStore.getState() + + const index = events.findIndex((e) => e.name === name) + + if (index === -1) return false + + events[index].obj.addEventListener('error', callback) + + return true + } + + function closeEvent(name: string): boolean { + const { events } = useEventStore.getState() + + const index = events.findIndex((e) => e.name === name) + + if (index === -1) return false + + events[index].obj.close() + + setEvents([...events.filter((x) => x.name !== name)]) + + return true + } + + function addEventCallback(name: string, callback: (value: T) => void) { + const { events } = useEventStore.getState() + const event = events.find((e) => e.name === name) + + if (!event) { + throw new Error('Event not found') + } + + event.obj.addEventListener('error', (error) => { + ;(error.target as any).close() + const { events } = useEventStore.getState() + setEvents(events.filter((x) => x.name !== event.name)) + }) + + event.obj.addEventListener('message', (event) => { + callback(JSON.parse(event.data)) + }) + } + + function addOpenCallback(name: string, callback: (value: Event) => void) { + const { events } = useEventStore.getState() + + const event = events.find((e) => e.name === name) + + if (!event) { + throw new Error('Event not found') + } + + event.obj.addEventListener('open', (e) => { + callback(e) + }) + } + + return { + event, + addEventCallback, + addOpenCallback, + closeEvent, + events, + addCloseEvent, + } +} @@ -0,0 +1,18 @@ +import { create } from 'zustand' +import { Event } from '../types' + +export interface EventsStore { + events: Event[] + setEvents: (events: Event[]) => void +} + +export const useEventStore = create((set, get) => { + function setEvents(events: Event[]) { + set({ ...get, events }) + } + + return { + events: [], + setEvents, + } +}) @@ -0,0 +1,2 @@ +export * from './event-source' +export * from './events-store' \ No newline at end of file @@ -0,0 +1,4 @@ +export interface Event { + name: string + obj: EventSource +} @@ -0,0 +1 @@ +export * from './event' \ No newline at end of file @@ -0,0 +1,2 @@ +export * from './model' +export * from './types' \ No newline at end of file @@ -0,0 +1,13 @@ +import { useContext } from "react" + +export function createUseContext(context: React.Context) { + return () => { + const contextValue = useContext(context) + if (!contextValue) { + throw new Error("useContext must be inside a Provider with a value") + } + return contextValue + } +} + + @@ -1,5 +1,18 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[]) { - const monthNames = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'] + const monthNames = [ + 'янв', + 'фев', + 'мар', + 'апр', + 'май', + 'июн', + 'июл', + 'авг', + 'сен', + 'окт', + 'ноя', + 'дек', + ] const formattedData = inputArray.map((item: any) => { const date = new Date(item.date) @@ -23,14 +36,14 @@ export function formatAndSortDates(inputArray: { date: string; value: number }[] } export function formatDate( - date: string | number | Date, - options: Intl.DateTimeFormatOptions, - lang?: string + date: string | number | Date, + options: Intl.DateTimeFormatOptions, + lang?: string ) { - if (typeof date !== "object") { - date = new Date(typeof date === "number" ? date : Date.parse(date)) - } + if (typeof date !== 'object') { + date = new Date(typeof date === 'number' ? date : Date.parse(date)) + } - const formatter = Intl.DateTimeFormat("ru", options) - return formatter.format(date) + const formatter = Intl.DateTimeFormat('ru', options) + return formatter.format(date) } @@ -0,0 +1,63 @@ +export async function getBlobFromUrl(url: string) { + const extension = url.match(/\.(\w+)\?/) + + if (!extension) return + + const buffer = await fetch(url).then((res) => res.arrayBuffer()) + + return new File([buffer], `file.${extension[1]}`) +} + +export function fileIsLink(file: string | null) { + if (!file) return false + + return file.includes('http') +} + +export function fileIsBase64(file: string | null) { + if (!file) return false + + return file.includes('base64') +} + +export function addBase64Padding(str: string) { + // debugger + const paddingNeeded = str.length % 3 + + if (paddingNeeded > 0) { + return str + '='.repeat(3 - paddingNeeded) + } + + return str +} + +export function validateBase64(base64String: string) { + // Удаляем пробелы, проверяем на наличие недопустимых символов + const cleanedString = base64String.replace(/\s/g, '').replace(/=+$/, '') + const validBase64Regex = /^[A-Za-z0-9+/]*[=]{0,2}$/ + + return validBase64Regex.test(cleanedString) +} + +export function base64Decode(str: string) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + let output = '' + str = str.replace(/[^A-Za-z0-9+/=]/g, '') // Удаляем недопустимые символы + + for (let i = 0; i < str.length; i += 4) { + const encoded1 = chars.indexOf(str.charAt(i)) + const encoded2 = chars.indexOf(str.charAt(i + 1)) + const encoded3 = chars.indexOf(str.charAt(i + 2)) + const encoded4 = chars.indexOf(str.charAt(i + 3)) + + const byte1 = (encoded1 << 2) | (encoded2 >> 4) + const byte2 = ((encoded2 & 15) << 4) | (encoded3 >> 2) + const byte3 = ((encoded3 & 3) << 6) | encoded4 + + output += String.fromCharCode(byte1) + if (encoded3 !== 64) output += String.fromCharCode(byte2) + if (encoded4 !== 64) output += String.fromCharCode(byte3) + } + + return output +} @@ -0,0 +1,32 @@ +import { FieldValues, UseFormSetValue, UseFormTrigger } from "react-hook-form" + +export function makeShouldDirtySetValue( + setValue: UseFormSetValue +) { + return (...params: Parameters) => { + params[2] = { shouldDirty: true } + return setValue(...params) + } +} + +export function makeTriggeredSetValue( + setValue: UseFormSetValue, + trigger: UseFormTrigger +) { + return (...params: Parameters) => { + setValue(...params) + trigger(params[0]) + return + } +} + +export function objectToFormdata(object: Record) { + const formData = new FormData() + + Object.entries(object).forEach(([k, v]) => { + if (typeof v !== "object" || v instanceof File) return formData.append(k, v) + return formData.append(k, JSON.stringify(v)) + }) + + return formData +} @@ -0,0 +1,12 @@ +export function getImageResolution(url: string): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => { + resolve({ width: img.width, height: img.height }) + } + img.onerror = () => { + reject(new Error('Не удалось загрузить изображение')) + } + img.src = url + }) +} @@ -5,3 +5,8 @@ export * from './get-type-device' export * from './get-random-image' export * from './string' export * from './date-helper' +export * from './object' +export * from './image' +export * from './file' +export * from './pending' +export * from './context' @@ -0,0 +1,6 @@ +export function keysToValues(obj: Record) { + return Object.keys(obj).reduce( + (prev, curr) => ({ ...prev, [obj[curr]]: curr }), + {} as Record + ) +} @@ -0,0 +1,20 @@ +import { useState } from 'react' + +export function withPending( + callback: (...params: Params) => Promise +): [(...params: Params) => Promise, boolean] { + const [pending, setPending] = useState(false) + + return [ + async (...params: Params) => { + setPending(true) + + const result = await callback(...params) + + setPending(false) + + return result + }, + pending, + ] +} @@ -0,0 +1,12 @@ +import { RefObject } from 'react' + +export function isBottom( + ref: RefObject, + scrollableRef: RefObject, + offset: number = 80 +) { + const [{ current }, { current: refCurrent }] = [scrollableRef, ref] + if (!current || !refCurrent) return false + + return current.offsetHeight - refCurrent.offsetTop < offset +} @@ -11,3 +11,20 @@ export function commaSeparated(input: number | string | null | undefined): strin return '' } } + +export function isNumericString(str: string) { + if (typeof str !== 'string') return false + const num = Number(str) + return !Number.isNaN(num) +} + +export function stringIsImage(str: string | null | undefined) { + if (!str) return false + return /\.jpg|\.png|\.jpeg|\.gif|\.webp|\.svg|\.wav/.test(str) +} + +export function cutString(str: string | null | undefined, length: number, postfix = '...') { + if (!str) return null + + return str.length > length ? str.slice(0, length) + postfix : str +} @@ -1,2 +1,3 @@ export { useAutoScroll } from './use-auto-scroll' export { useThemeAndDevice } from './use-theme-and-device' +export * from './use-show-data' \ No newline at end of file @@ -1,26 +1,12 @@ import { useEffect } from 'react' import { useRouter } from 'next/router' -// этот хук проверяет используется ли Telegram Web App - export const useBlockTelegram = () => { - const router = useRouter() + const { replace, pathname: path } = useRouter() useEffect(() => { - if (typeof window === 'undefined') return - - try { - if ( - typeof window !== 'undefined' && - 'Telegram' in window && - (window as any).Telegram?.WebApp && - router.pathname !== '/telegram-blocked' - ) { - router.replace('/telegram-blocked') - } else { - } - } catch (err) { - console.error('Ошибка при проверке WebApp:', err) - } - }, [router]) + // @ts-ignore + if (window.Telegram && window.Telegram.WebApp && path !== '/telegram-blocked') + replace('/telegram-blocked') + }, []) } @@ -1,11 +1,12 @@ import { useAppSelector } from '#/app/store/store' +import { getDeviceType } from '../helpers' import { DeviceOs } from '../types/entities' -export const useThemeAndDevice = (device?: 'desktop' | 'mobile', deviceOs?: DeviceOs) => { +export const useThemeAndDevice = (device: 'desktop' | 'mobile' = getDeviceType(), deviceOs?: DeviceOs) => { const theme = useAppSelector((state) => state.theme.theme) const desktop = device === 'desktop' const ios = deviceOs === 'ios' - return { theme, desktop, ios } + return { theme, desktop, ios, device } } @@ -1,7 +1,8 @@ import React from 'react' -import { IModelInputs } from '#/shared/api/models/models' +import { ModelInputs } from '#/shared/api/models/models' import { Device, DeviceOs } from '#/shared/lib/types/entities' +import { Message } from '#/entities/message' export type MessageSend = { content: string @@ -9,18 +10,6 @@ export type MessageSend = { info: T } -export type 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 -} - /* Дополнительные элементы в Инпуте **/ type Elements = { element: React.ReactElement @@ -43,7 +32,7 @@ export type ChatProps = { elements?: Elements[] model?: string getMessagesPagination?: () => Promise - input_types?: IModelInputs[] + input_types?: ModelInputs[] deleteMessage: (message_uid: string) => void modelTitle: string | undefined currentVersion: string @@ -0,0 +1,44 @@ +.main { + width: 100%; + display: flex; + justify-content: flex-start; + gap: 20px; + margin-bottom: 24px; + + .chatWindow { + width: 70%; + } + + .settings { + width: 25%; + } + + @media (max-width: 768px) { + width: 100%; + display: flex; + + flex-direction: column-reverse; + + .chatWindow { + width: 100%; + } + + .settings { + width: 100%; + } + } +} +.wrapModelTitle { + margin-bottom: 20px; + margin-top: 15px; + @media (max-width: 768px) { + margin: 0; + margin-top: 15px; + } +} + +.bigTitle { + @media (max-width: 768px) { + font-size: 26px !important; + } +} @@ -2,8 +2,11 @@ border: none; outline: none; background-color: transparent; - padding: 15px 25px; + padding: 16px 25px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; &_outline { border-radius: 15px; @@ -47,13 +50,10 @@ color: white; border-radius: 12px; font-weight: 500; - font-size: 15px; + font-size: 16px; transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; &:hover { background-color: var(--text-color-purple); - &:hover { - opacity: 0.7; - } } } @@ -73,6 +73,6 @@ html[data-theme='dark'] { .button_gray { color: white !important; - background-color: #5d5a5a !important; + background-color: #D9D9D91A !important; } } @@ -8,7 +8,13 @@ export interface CommonButtonProps extends React.ButtonHTMLAttributes { +export const CommonButton = ({ + children, + variant = 'outline', + className, + disabled, + ...props +}: CommonButtonProps) => { return ( + ) +} @@ -0,0 +1 @@ +export * from './common-send-button' \ No newline at end of file @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -1,8 +1,9 @@ import { c } from '#/shared/lib/helpers' import { FieldError } from 'react-hook-form' import styles from './common-textarea.module.scss' -import React, { ForwardedRef, forwardRef, useEffect } from 'react' +import React, { ForwardedRef, forwardRef, useEffect, useImperativeHandle } from 'react' import { Raleway } from 'next/font/google' +import { useMask } from '@react-input/mask' export type TextAreaVariant = 'default' | 'outline' @@ -14,12 +15,27 @@ export interface CommonTextAreaProps extends React.TextareaHTMLAttributes( - ({ showError = true, buttomSlot, label, error, className, disabled, trim = false, variant = 'default', ...props }, ref) => { + ( + { + showError = true, + buttomSlot, + label, + error, + className, + disabled, + trim = false, + variant = 'default', + wrapperClassName, + ...props + }, + ref + ) => { const onChange = (event: React.ChangeEvent) => { event.target.value = trim ? event.target.value.trim() : event.target.value } @@ -27,14 +43,25 @@ export const CommonTextArea = forwardRef
- {label && } + {label && ( + + )}