@@ -0,0 +1,3 @@ + + + @@ -26,3 +26,18 @@ border: var(--new-ui-ctrl-f-button-border); background: var(--new-ui-ctrl-f-button-bg); } + +.infoContainer { + display: flex; + flex-direction: column; + align-items: self-end; +} + +.balanceBox { + display: flex; + gap: 4px; +} + +.generationIcon { + margin-top: 3px; +} \ No newline at end of file @@ -10,6 +10,7 @@ import { signOut } from 'next-auth/react' import styles from '#/app/layout/styles/styles.module.css' import { useAppSelector } from '#/app/store/store' import { TooltipCustom } from '#/shared' +import { SvgIcon } from '#/shared/ui/svg' import { declineToken } from '#/shared/lib/helpers/get-token' import { IProps } from '#/shared/lib/types/entities' import { NavigationSearch } from '#/widgets/navigation-search' @@ -112,14 +113,15 @@ const InfoBar: React.FC = ({ device }) => { - + {first_name + ' ' + last_name} {show_balance && ( - + + {declineToken(balance.toString())} @@ -22,17 +22,17 @@ export function useImageBot(slug: string) { dispatch(setParams(bot.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) } - // это пиз**ц - // нужен рефакторинг (я то в этом не разбираюсь) - // а стажеры и подавно)))) + function isParamForVersion(param: IModel['parameters'][0], versionSlug: string) { + return param.versions.length === 0 || param.versions.includes(versionSlug) + } + function setStoreParams(bot: IModel) { + const versionSlug = bot.versions[0].slug dispatch( setParams( bot.parameters.reduce( (a, v) => - v.versions.includes(bot.versions[0].slug) - ? { ...a, [v.key]: v.values.default } - : { ...a }, + isParamForVersion(v, versionSlug) ? { ...a, [v.key]: v.values.default } : { ...a }, {} ) ) @@ -50,7 +50,6 @@ export function useImageBot(slug: string) { setStoreParams(bot) } - // это пиз**ц const resetParams = () => { if (botParams) { dispatch(setParams({})) @@ -60,7 +59,7 @@ export function useImageBot(slug: string) { setParams( botParams.parameters.reduce( (a, v) => - v.versions.includes(botParams.versions[0].slug) + isParamForVersion(v, botParams.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }, {} @@ -91,7 +90,8 @@ export function useImageBot(slug: string) { dispatch( setParams( botParams.parameters.reduce( - (a, v) => (v.versions.includes(version) ? { ...a, [v.key]: v.values.default } : { ...a }), + (a, v) => + isParamForVersion(v, version) ? { ...a, [v.key]: v.values.default } : { ...a }, {} ) ) @@ -35,8 +35,11 @@ } .endAdornment { + position: relative; display: flex; align-items: center; + justify-content: center; + padding-left: 66px; /* резерв под predictPrice, чтобы не перекрывать */ } .divider { @@ -45,3 +48,18 @@ margin: 0px 8px; background-color: #40404e; } + +.predictPrice { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 14px; + border-radius: 16px; + margin-right: 8px; + background-color: #7F7DF31A; + gap: 4px; +} + +.generationIcon { + margin-top: 3px; +} @@ -6,6 +6,10 @@ import classes from './model-input.module.scss' import { LoadImage } from '#/app/components/input_components/load_image' import { SendBtn } from '#/app/components/input_components/send_button' import { IModelInputs } from '#/shared/api/models/models' +import { TooltipCustom } from '#/shared' +import { SvgIcon } from '#/shared/ui/svg' +import { Typography } from '@mui/material' +import { ThreePOutlined } from '@mui/icons-material' interface Input { loading: boolean @@ -29,6 +33,7 @@ interface Input { resendValue?: string value?: string onValueChange?: (value: string) => void + predictedPrice?: string | null } export const ModelInput: FC = ({ @@ -46,6 +51,7 @@ export const ModelInput: FC = ({ blocked, value: externalValue, onValueChange: externalOnChange, + predictedPrice, }: Input) => { const [disabled, setDisabled] = React.useState(true) const [required, setRequired] = React.useState<(string | null)[]>([]) @@ -175,6 +181,16 @@ export const ModelInput: FC = ({ >
+ {typeof predictedPrice === 'string' && ( + +
+ + + {Math.ceil(Number(predictedPrice))} + +
+
+ )} {typeVersions['image'] && (typeVersions['image'].length === 0 || typeVersions['image'].includes(currentVersion)) && ( @@ -0,0 +1 @@ +export { usePredictPrice } from './use-predict-price' @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState } from 'react' +import { debounce } from 'lodash' + +import { predictPrice } from '#/shared/api/models/predict-price' + +interface UsePredictPriceParams { + modelSlug: string + content: string + fileExists: boolean + info: Record + token?: string + enabled?: boolean +} + +export function usePredictPrice({ + modelSlug, + content, + fileExists, + info, + token, + enabled = true, +}: UsePredictPriceParams) { + const [price, setPrice] = useState(null) + + const debouncedPredictRef = useRef( + debounce( + async ( + slug: string, + text: string, + hasFile: boolean, + params: Record, + accessToken?: string + ) => { + try { + const result = await predictPrice( + { + model_slug: slug, + content: text, + file_exists: hasFile, + info: params, + }, + accessToken + ) + setPrice(typeof result.price === 'string' ? result.price : null) + } catch { + setPrice(null) + } + }, + 500 + ) + ) + + useEffect(() => { + if (!enabled || !modelSlug || !token) { + setPrice(null) + return + } + + const debouncedPredict = debouncedPredictRef.current + debouncedPredict(modelSlug, content, fileExists, info, token) + + return () => { + debouncedPredict.cancel() + } + }, [modelSlug, content, fileExists, info, token, enabled]) + + return price +} @@ -1,11 +1,6 @@ .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; @@ -27,12 +27,6 @@ export default function Title(props: TitleProps) {
- - {props.title} -
{props.rightSlot}
@@ -0,0 +1,31 @@ +import axios from 'axios' + +import { API_URL } from '#/shared/lib/constants' + +export interface PredictPriceRequest { + model_slug: string + content: string + file_exists: boolean + info: Record +} + +export interface PredictPriceResponse { + price: string | null +} + +export async function predictPrice( + data: PredictPriceRequest, + token?: string +): Promise { + const { data: result } = await axios.post( + API_URL + '/ml_model/predict-price/', + data, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + } + ) + return result as PredictPriceResponse +} @@ -16,16 +16,31 @@ } } +.staticTabsWrapper { + display: flex; + align-items: center; + width: calc(24.5% + 2px); + margin-left: auto; + margin-right: calc(3%); + margin-top: 0; +} + .header { - width: 70%; - margin-bottom: 50px; + display: flex; + align-items: center; + width: 100%; + gap: 22px; + margin: 14px 0; + + > *:first-child { + min-width: 200px; + } @media screen and (max-width: 768px) { width: 100%; - margin-bottom: 0px; + gap: 12px; } } - @keyframes fadein { 0% { opacity: 0; @@ -41,16 +56,12 @@ .icon { } + .tags { - min-width: 100%; display: flex; gap: 8px; align-items: center; justify-content: flex-end; - - @media screen and (max-width: 768px) { - justify-content: flex-start; - } } .tag { @@ -16,6 +16,7 @@ import { useImagesBotFilters } from '#/features/image-bot-filters' import { useImagesUniqInput } from '#/features/image-bot-input' import { useMediaBotPagination } from '#/features/image-bot-pagination' import { ModelInput } from '#/features/model-input' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, Loader } from '#/shared' @@ -123,6 +124,20 @@ const AudioModelPage: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) + const predictPriceInfo = useMemo( + () => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), + [includeParams, version] + ) + + const predictedPrice = usePredictPrice({ + modelSlug: modelType, + content: prompt, + fileExists: !!image, + info: predictPriceInfo, + token: session?.access, + enabled: !!modelType && !!session?.access && scope === 'playground', + }) + return ( <> @@ -133,8 +148,8 @@ const AudioModelPage: NextPageWithLayout = () => { title={botParams ? botParams.title : 'Загрузка...'} type={'Аудио'} linkBack={'/audio'} - rightSlot={ -
+ /> +
{botParams && botParams.tags.map((tag, index) => (
@@ -144,9 +159,18 @@ const AudioModelPage: NextPageWithLayout = () => {
))}
- } - /> + {desktop && ( + + + )}
+ + {!desktop && ( + + + + )} + { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} @@ -294,6 +319,7 @@ const AudioModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {isProgressVisible && ( @@ -312,11 +338,6 @@ const AudioModelPage: NextPageWithLayout = () => { - - - {desktop && ( {botParams?.versions && botParams.versions.length !== 0 ? ( @@ -17,6 +17,7 @@ import { TutorialContext } from '#/features/tutorial-context/tutorial-context' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, useModel } from '#/shared' import model_api from '#/shared/api/models/api' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import { getDeviceType, getOs } from '#/shared/lib/helpers' import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import { SvgIcon } from '#/shared/ui/svg' @@ -37,6 +38,7 @@ const Page: NextPageWithLayout = () => { const [params, setParams] = React.useState(false) const [file, setFile] = React.useState(null) + const [inputContent, setInputContent] = React.useState('') const { showMessage } = useShowDataStore() const [scope, setScope] = React.useState<'playground' | 'api'>('playground') @@ -199,6 +201,20 @@ const Page: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) + const predictPriceInfo = useMemo( + () => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), + [includeParams, version] + ) + + const predictedPrice = usePredictPrice({ + modelSlug: modelType, + content: inputContent, + fileExists: !!file, + info: predictPriceInfo, + token: data?.access, + enabled: !!modelType && !!data?.access && scope === 'playground', + }) + return ( <> @@ -208,23 +224,36 @@ const Page: NextPageWithLayout = () => {
- {botParams && - botParams.tags.map((tag, index) => ( - <div key={index} className={styles.tag}> - <SvgIcon width={23} height={23} url={tag.icon} className={styles.tag__icon} /> - <span className={styles.tag__text}>{tag.title}</span> - </div> - ))} - </div> - } type={'Чат-боты'} linkBack={'/chat-bot'} /> - </div> + <div className={styles.tags}> + {botParams && + botParams.tags.map((tag, index) => ( + <div key={index} className={styles.tag}> + <SvgIcon width={23} height={23} url={tag.icon} className={styles.tag__icon} /> + <span className={styles.tag__text}>{tag.title}</span> + </div> + ))} + </div> + {desktop && <ChatsContainer desktop={desktop} modelType={modelType} />} + + {desktop && ( + <Stack sx={{ display: 'flex', alignItems: 'center', width: '25%', minWidth: '25%', marginRight: 'calc(5% - 22px)', marginTop: '0px' }}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} + </div> + {!desktop && ( + <Stack sx={{ display: 'flex', alignItems: 'center', width: '100%', marginBottom: '15px', marginTop: '10px' }}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} + + {!desktop && <ChatsContainer desktop={desktop} modelType={modelType} />} <Box sx={{ height: 'calc(100% - 100px)' }} className={styles.main}> + <Box className={styles.chatWindow}> <Box sx={{ display: scope === 'playground' ? 'block' : 'none' }}> <AllChatWindow @@ -246,6 +275,9 @@ const Page: NextPageWithLayout<ChatBotPageProps> = () => { modelTitle={botParams?.title} currentVersion={version} tags={botParams?.tags ?? []} + inputValue={inputContent} + onInputValueChange={setInputContent} + predictedPrice={predictedPrice} /> </Box> <Box sx={{ display: scope === 'api' ? 'block' : 'none' }}> @@ -271,10 +303,7 @@ const Page: NextPageWithLayout<ChatBotPageProps> = () => { </Box> </Box> <Box className={styles.settings}> - <Stack sx={{ display: 'flex', alignItems: 'center', width: '100%', marginBottom: '15px', marginTop: desktop ? '0px' : '10px' }}> - <StaticTabs scope={scope} setScope={setScope} /> - </Stack> - <ChatsContainer desktop={desktop} modelType={modelType} /> + {desktop && ( <Stack id='text-models-tour-3' className='pd-30 bg-color-block border-radius-main' spacing={2}> {botParams?.versions && botParams.versions?.length !== 0 && ( @@ -11,10 +11,19 @@ } .header { - width: 70%; + display: flex; + align-items: center; + width: 100%; + gap: 22px; + margin: 14px 0; + + > *:first-child { + min-width: 200px; + } @media screen and (max-width: 768px) { width: 100%; + gap: 0; } } @@ -67,15 +76,10 @@ } .tags { - min-width: 100%; display: flex; gap: 8px; align-items: center; justify-content: flex-end; - - @media screen and (max-width: 768px) { - justify-content: flex-start; - } } .main { @@ -17,12 +17,19 @@ } .header { - width: 70%; - margin-bottom: 50px; + display: flex; + align-items: center; + width: 100%; + gap: 22px; + margin: 14px 0; + + > *:first-child { + min-width: 200px; + } @media screen and (max-width: 768px) { width: 100%; - margin-bottom: 0px; + gap: 12px; } } @@ -38,38 +45,20 @@ } } -// .blocked { -// display: flex; -// align-items: center; -// justify-content: center; -// background-color: white; -// border-radius: 100px; -// padding-right: 20px; -// padding: 8px 12px; -// width: max-content; - -// span { -// color: #ff2372; -// font-weight: 500; -// padding-left: 10px; -// font-size: 14px; -// width: max-content; -// } -// } - -.icon { -} - .tags { - min-width: 100%; display: flex; gap: 8px; align-items: center; justify-content: flex-end; +} - @media screen and (max-width: 768px) { - justify-content: flex-start; - } +.staticTabsWrapper { + display: flex; + align-items: center; + width: calc(24.5% + 2px); + margin-left: auto; + margin-right: calc(3%); + margin-top: 0; } .tag { @@ -14,6 +14,7 @@ import { useImagesBotFilters } from '#/features/image-bot-filters' import { useImagesUniqInput } from '#/features/image-bot-input' import { useMediaBotPagination } from '#/features/image-bot-pagination' import { ModelInput } from '#/features/model-input' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, Loader } from '#/shared' @@ -33,6 +34,7 @@ const ImageModelPage: NextPageWithLayout = () => { const { data: session } = useSession() const [scope, setScope] = useState<'playground' | 'api'>('playground') + const [prompt, setPrompt] = useState('') const { botParams, version, modelType, fetchBotParams, resetParams, setDefaultParams, setVersion } = useImageBot(query.slug as string) const { desktop } = useDeviceType(deviceType, deviceOs) @@ -113,6 +115,20 @@ const ImageModelPage: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) + const predictPriceInfo = useMemo( + () => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), + [includeParams, version] + ) + + const predictedPrice = usePredictPrice({ + modelSlug: modelType, + content: prompt, + fileExists: !!image, + info: predictPriceInfo, + token: session?.access, + enabled: !!modelType && !!session?.access && scope === 'playground', + }) + return ( <> <Head> @@ -123,8 +139,8 @@ const ImageModelPage: NextPageWithLayout = () => { title={botParams ? botParams.title : 'Загрузка...'} type={'Изображения'} linkBack={'/images'} - rightSlot={ - <div className={styles.tags}> + /> + <div className={styles.tags}> {botParams && botParams.tags.map((tag, index) => ( <div key={index} className={styles.tag}> @@ -133,10 +149,18 @@ const ImageModelPage: NextPageWithLayout = () => { <span className={styles.tag__text}>{tag.title}</span> </div> ))} - </div> - } - /> + </div> + {desktop && ( + <Stack className={styles.staticTabsWrapper}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} </div> + {!desktop && ( + <Stack sx={{ display: 'flex', alignItems: 'center', width: '100%', marginTop: '10px' }}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} <Box display={'flex'} justifyContent='space-between' @@ -145,7 +169,6 @@ const ImageModelPage: NextPageWithLayout = () => { sx={{ marginBottom: desktop ? 0 : 2, width: desktop ? '97%' : '100%', - marginTop: desktop ? 3 : '15px', }} > <Box @@ -173,6 +196,8 @@ const ImageModelPage: NextPageWithLayout = () => { styles={'images'} input_types={botParams.inputs} image={image} + value={prompt} + onValueChange={(value: string) => setPrompt(value)} desktop={desktop} blocked={scope === 'playground' ? botParams.blocked : true} loading={createLoading} @@ -180,6 +205,7 @@ const ImageModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {botParams?.blocked && ( @@ -272,6 +298,8 @@ const ImageModelPage: NextPageWithLayout = () => { styles={'images'} input_types={botParams.inputs} image={image} + value={prompt} + onValueChange={(value: string) => setPrompt(value)} blocked={scope === 'playground' ? botParams.blocked : true} desktop={desktop} loading={createLoading} @@ -279,6 +307,7 @@ const ImageModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {botParams?.blocked && ( @@ -294,11 +323,6 @@ const ImageModelPage: NextPageWithLayout = () => { </Box> <Stack spacing={2} sx={{ width: desktop ? '25.5%' : '100%', paddingBottom: desktop ? '' : '15px' }}> - <Stack - sx={{ display: 'flex', alignItems: 'center', width: '100%', marginBottom: '15px', marginTop: desktop ? '0px' : '10px' }} - > - <StaticTabs scope={scope} setScope={setScope} /> - </Stack> {desktop && ( <Stack id='images-models-tour-3' spacing={2} className='pd-30 bg-color-block border-radius-main' sx={{ height: 'auto' }}> {botParams?.versions && botParams.versions.length !== 0 ? ( @@ -16,13 +16,30 @@ } } + +.staticTabsWrapper { + display: flex; + align-items: center; + width: calc(24.5% + 2px); + margin-left: auto; + margin-right: calc(3%); + margin-top: 0; +} + .header { - width: 70%; - margin-bottom: 50px; + display: flex; + align-items: center; + width: 100%; + gap: 22px; + margin: 14px 0; + + > *:first-child { + min-width: 200px; + } @media screen and (max-width: 768px) { width: 100%; - margin-bottom: 0px; + gap: 12px; } } @@ -39,15 +56,10 @@ } .tags { - min-width: 100%; display: flex; gap: 8px; align-items: center; justify-content: flex-end; - - @media screen and (max-width: 768px) { - justify-content: flex-start; - } } .tag { @@ -16,6 +16,7 @@ import { useImagesBotFilters } from '#/features/image-bot-filters' import { useImagesUniqInput } from '#/features/image-bot-input' import { useMediaBotPagination } from '#/features/image-bot-pagination' import { ModelInput } from '#/features/model-input' +import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { NextPageWithLayout } from '#/pages/_app' import { DrawerCustom, Error, Loader } from '#/shared' @@ -121,6 +122,20 @@ const VideoModelPage: NextPageWithLayout = () => { }) }, [botParams?.inputs, version]) + const predictPriceInfo = useMemo( + () => ({ ...(includeParams || {}), ...(version ? { version } : {}) }), + [includeParams, version] + ) + + const predictedPrice = usePredictPrice({ + modelSlug: modelType, + content: prompt, + fileExists: !!image, + info: predictPriceInfo, + token: session?.access, + enabled: !!modelType && !!session?.access && scope === 'playground', + }) + return ( <> <Head> @@ -131,8 +146,8 @@ const VideoModelPage: NextPageWithLayout = () => { title={botParams ? botParams.title : 'Загрузка...'} type={'Видео'} linkBack={'/videos'} - rightSlot={ - <div className={styles.tags}> + /> + <div className={styles.tags}> {botParams && botParams.tags.map((tag, index) => ( <div key={index} className={styles.tag}> @@ -142,9 +157,18 @@ const VideoModelPage: NextPageWithLayout = () => { </div> ))} </div> - } - /> + {desktop && ( + <Stack className={styles.staticTabsWrapper}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} </div> + {!desktop && ( + <Stack sx={{ display: 'flex', alignItems: 'center', width: '100%', marginTop: '10px' }}> + <StaticTabs scope={scope} setScope={setScope} /> + </Stack> + )} + <Box display={'flex'} justifyContent='space-between' @@ -189,6 +213,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} <Box sx={{ width: '100%', marginTop: '10px' }}> @@ -290,6 +315,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {isProgressVisible && ( @@ -308,11 +334,6 @@ const VideoModelPage: NextPageWithLayout = () => { )} </Box> <Stack spacing={2} sx={{ width: desktop ? '25.5%' : '100%', paddingBottom: desktop ? '' : '15px' }}> - <Stack - sx={{ display: 'flex', alignItems: 'center', width: '100%', marginBottom: '15px', marginTop: desktop ? '0px' : '10px' }} - > - <StaticTabs scope={scope} setScope={setScope} /> - </Stack> {desktop && ( <Stack spacing={2} className='pd-30 bg-color-block border-radius-main' sx={{ height: 'auto' }}> {botParams?.versions && botParams.versions.length !== 0 ? ( @@ -43,6 +43,9 @@ export interface ChatProps<T> { currentVersion: string botParams: IModel | null tags: IModelTag[] + inputValue?: string + onInputValueChange?: (value: string) => void + predictedPrice?: string | null } function Chat<T>({ @@ -63,6 +66,9 @@ function Chat<T>({ currentVersion, deviceOs, tags, + inputValue, + onInputValueChange, + predictedPrice, }: ChatProps<T>) { const desktop = device === 'desktop' @@ -139,6 +145,9 @@ function Chat<T>({ image={file} unpinImage={clearImage} imageLoad={onLoadImage} + value={inputValue} + onValueChange={onInputValueChange} + predictedPrice={predictedPrice} /> {blocked && ( @@ -1,34 +1,83 @@ .title { + color: #a4aab5; + font-weight: 600; + font-size: 14px; + letter-spacing: 0.1px; + @media screen and (max-width: 768px) { display: none; } } +.createButtonWrapper { + position: relative; + display: flex; + align-items: center; +} + +.createButton { + background-color: #151518; + padding: 9px 11px; + border-radius: 10px; + cursor: pointer; +} + +.createButtonIcon { + margin-top: 3px; +} + +.wrapMobile { + margin: 15px 0 0; +} + .wrap { - width: 100%; + margin-left: auto; + width: fit-content; + max-width: 50%; background-color: var(--new-ui-main-color); border-radius: 15px; - padding: 30px; + padding: 4px; + display: flex; + flex: 0 1 auto; height: fit-content; - //margin-bottom: 15px; max-height: 350px; overflow-y: auto; @media (max-width: 768px) { - padding: 10px 20px; - display: flex; - flex-direction: row-reverse; + max-width: 100%; + margin-top: 0; + padding-left: 8px; + display: flex; } .container { - display: block; + display: flex; overflow: auto; + overflow-y: auto; + max-height: 210px; + scrollbar-width: thin; + scrollbar-color: rgba(217, 217, 217, 0.49) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(217, 217, 217, 0.49); + border-radius: 6px; + border: 2px solid transparent; + background-clip: content-box; + } @media (max-width: 768px) { display: flex; overflow-x: scroll; scrollbar-width: none; - width: 100%; + width: 100%; -ms-overflow-style: none; &::-webkit-scrollbar { width: 2px; @@ -38,6 +87,7 @@ .item { padding: 0; border: none; + min-width: 120px; width: 100%; &:hover { @@ -46,42 +96,119 @@ .main { display: flex; + align-items: center; width: 100%; + height: 44px; + min-height: 44px; border: none; justify-content: space-between; background-color: transparent; border-radius: 13px; - padding: 12px 15px; + padding: 0 15px; text-transform: none; + overflow: hidden; &_active { display: flex; align-items: center; justify-content: space-between; + gap: 10px; width: 100%; + height: 44px; + min-height: 44px; border: none; border-radius: 13px; - padding: 12px 15px; + padding: 0 15px; text-transform: none; background-color: rgba(130, 128, 255, 0.15); + overflow: hidden; @media (max-width: 768px) { - margin-right: 10px; - // margin-top: 10px; width: 125px; + min-width: 125px; max-width: 130px; - overflow: hidden; - padding: 8px 12px; + height: 36px; + min-height: 36px; + padding: 0 12px; justify-content: space-between; } } @media (max-width: 768px) { - // margin-top: 10px; width: 100px; + min-width: 100px; + height: 36px; + min-height: 36px; padding: 0; } } + + .chatTitle { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; + } + + .chatIcon { + flex-shrink: 0; + margin-top: 6px; + } } } } + +.textField { + :global(.MuiInputBase-input) { + padding: 12px 5px 12px 15px; + border-radius: 13px; + outline: none; + width: 100%; + color: #a4aab5; + font-size: 15px; + font-weight: 500; + } + + :global(.MuiInputBase-root) { + border-radius: 13px !important; + background-color: rgba(130, 128, 255, 0.15); + } + + :global(.MuiFormControl-root-MuiTextField-root) { + border-radius: 13px; + } +} + +.saveIconBox { + cursor: pointer; + display: flex; + align-items: center; +} + +.menuPaper { + background-color: #303035; + border-radius: 15px; +} + +.menuList { + background-color: #303035; + color: #8280ff; + border-radius: 15px; +} + +.menuItem { + font-size: 15px; + font-weight: 500; + display: flex; + align-items: center; + gap: 10px; +} + +.menuItemDelete { + font-size: 15px; + font-weight: 500; + display: flex; + align-items: center; + gap: 12px; +} @@ -5,8 +5,6 @@ import Image from 'next/image' import styles2 from '#/app/styles/account.module.css' import { Chat, Chats as ChatProps } from '#/features/chats' import { TooltipCustom } from '#/shared' -import { makeThinScrollbar } from '#/shared/lib/constants/styles' - import { DeleteMenuIcon, RenameMenuIcon, SaveRenameIcon } from './icons' import styles from './chats.module.scss' @@ -80,44 +78,8 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr } return ( - <Box className={styles.wrap} sx={{ margin: desktop ? '0 0 15px 0' : '15px 0 0' }}> - <Box display='flex' alignItems='center' justifyContent='space-between'> - <Typography - className={styles.title} - sx={{ - color: '#A4AAB5', - fontWeight: '600', - fontSize: '14px', - letterSpacing: '0.1px', - }} - > - ЧАТЫ - </Typography> - <TooltipCustom title='Создать чат'> - <Box - onClick={createNewChat} - sx={{ - backgroundColor: '#151518', - padding: '9px 11px', - borderRadius: '10px', - cursor: 'pointer', - }} - > - <Image src={'/plus.svg'} style={{ marginTop: '3px' }} width={18} height={18} alt={''} /> - </Box> - </TooltipCustom> - </Box> - <ToggleButtonGroup - className={styles.container} - color='primary' - exclusive - aria-label='chats' - sx={{ - overflowY: 'auto', - maxHeight: '210px', - ...makeThinScrollbar() - }} - > + <Box className={`${styles.wrap} ${desktop ? styles.wrapDesktop : styles.wrapMobile}`}> + <ToggleButtonGroup className={styles.container} color='primary' exclusive aria-label='chats'> {!(localChats === null || localChats?.length <= 0) && localChats?.map((el) => { return ( @@ -147,30 +109,11 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr value={newTitle} onChange={(e) => setNewTitle(e.target.value)} variant='outlined' - sx={{ - '& .MuiInputBase-input': { - padding: '12px 5px 12px 15px', - borderRadius: '13px', - outline: 'none', - width: '100%', - color: '#A4AAB5', - fontSize: '15px', - fontWeight: '500', - }, - '& .MuiInputBase-root': { - borderRadius: '13px !important', - backgroundColor: 'rgba(130, 128, 255, 0.15)', - }, - '& .MuiFormControl-root-MuiTextField-root': { - borderRadius: '13px', - }, - }} + className={styles.textField} InputProps={{ endAdornment: ( <Box - sx={{ cursor: 'pointer' }} - display={'flex'} - alignItems={'center'} + className={styles.saveIconBox} onClick={() => { setRename(false) renameChat() @@ -184,20 +127,23 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr ) : ( <Box className={el?.uid === currentChat ? styles.main_active : styles.main}> <Typography - className={el?.uid === currentChat ? styles2.toggle_text_active_chat : styles2.toggle_text} + className={`${styles.chatTitle} ${el?.uid === currentChat ? styles2.toggle_text_active_chat : styles2.toggle_text}`} + title={el?.title ?? ''} > {el?.title} </Typography> {el?.uid === currentChat && ( - <Image - onClick={handleClick} - aria-describedby={Boolean(chatSetting) ? 'simple-popover' : undefined} - src={'/svg/chat-setting.svg'} - alt={'Настройка чата'} - width={18} - height={18} - /> + <Box className={styles.chatIcon}> + <Image + onClick={handleClick} + aria-describedby={Boolean(chatSetting) ? 'simple-popover' : undefined} + src={'/svg/chat-setting.svg'} + alt={'Настройка чата'} + width={18} + height={18} + /> + </Box> )} <Menu autoFocus={false} @@ -206,27 +152,12 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr id='message-menu' onClose={handleClose} onClick={handleClose} - sx={{ - '& .MuiMenu-list': { - backgroundColor: '#303035', - color: '#8280FF', - borderRadius: '15px', - }, - '& .MuiPopover-paper': { - backgroundColor: '#303035', - borderRadius: '15px', - }, - }} + PaperProps={{ className: styles.menuPaper }} + MenuListProps={{ className: styles.menuList }} > <MenuItem autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '10px', - }} + className={styles.menuItem} onClick={() => { setRename(true) }} @@ -236,13 +167,7 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr </MenuItem> <MenuItem autoFocus={false} - sx={{ - fontSize: '15px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '12px', - }} + className={styles.menuItemDelete} onClick={() => { removeChat(localChat) }} @@ -257,6 +182,15 @@ export const Chats = ({ chats, currentChat, removeChat, setChat, chatSetting, cr ) })} </ToggleButtonGroup> + <Box className={styles.createButtonWrapper} display='flex' alignItems='center' justifyContent='space-between'> + <Box> + <TooltipCustom title='Создать чат'> + <Box onClick={createNewChat} className={styles.createButton}> + <Image src={'/plus.svg'} className={styles.createButtonIcon} width={18} height={18} alt={''} /> + </Box> + </TooltipCustom> + </Box> + </Box> </Box> ) } @@ -49,7 +49,6 @@ export const MainMenuMobile = memo(() => { <MenuIcon sx={{ color: '#868686', - marginRight: '28px', width: 30, }} /> @@ -0,0 +1,75 @@ +.container { + display: flex; + border: none; + gap: 8px; + background-color: var(--new-ui-main-color); + padding: 4px; + border-radius: 13px; + width: 100%; + + + .item { + padding: 0; + flex: 1; + border: none; + + &:hover { + background-color: transparent; + } + + .main { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + min-height: 44px; + border: none; + background-color: transparent; + border-radius: 13px; + padding: 0 15px; + text-transform: none; + overflow: hidden; + + &_active { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 44px; + min-height: 44px; + border: none; + border-radius: 13px; + padding: 0 15px; + text-transform: none; + background-color: #1D1D21; + overflow: hidden; + + @media (max-width: 768px) { + height: 36px; + min-height: 36px; + padding: 0 12px; + } + } + + @media (max-width: 768px) { + height: 36px; + min-height: 36px; + padding: 0 12px; + } + } + + .tabText { + font-weight: 600; + font-size: 15px; + + color: #FFFFFF; + } + + .tabTextActive { + font-weight: 600; + font-size: 15px; + color: #FFFFFF; + } + } +} @@ -1,45 +1,42 @@ import React, { memo } from 'react' -import { Tab, Tabs, Typography } from '@mui/material' - -import styles from '#/app/styles/accountTabs.module.css' +import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material' +import styles from './static-tabs.module.scss' interface IProps { - scope:'playground' | 'api' - setScope:React.Dispatch<React.SetStateAction<'playground' | 'api'>> + scope: 'playground' | 'api' + setScope: React.Dispatch<React.SetStateAction<'playground' | 'api'>> } export const StaticTabs = memo(({ scope, setScope }: IProps) => { - - const tabs: { - scope: 'playground' | 'api' - title: string - }[] = [ + const tabs: { scope: 'playground' | 'api'; title: string }[] = [ { scope: 'playground', title: 'Playground' }, { scope: 'api', title: 'API' }, ] return ( - <Tabs + <ToggleButtonGroup + className={styles.container} value={scope} - variant='scrollable' - className={styles.wrap_toggle_button} + exclusive aria-label='Platform' - scrollButtons={false} - sx={{ '& .MuiTabs-indicator': { display: 'none' } }} + onChange={(_, value) => value && setScope(value)} > - {tabs.map((el) => { - return ( - <Tab - disableRipple - onClick={() => setScope(el.scope)} - key={el.title} - className={scope === el.scope ? styles.wrap_toggle_button_active : styles.wrap_toggle_button} - value={el.scope} - label={<Typography sx={{ fontSize: 18, fontWeight: 'bold' }}>{el.title}</Typography>} - /> - ) - })} - </Tabs> + {tabs.map((el) => ( + <ToggleButton + disableRipple + onClick={() => setScope(el.scope)} + key={el.scope} + className={styles.item} + value={el.scope} + > + <Box className={scope === el.scope ? styles.main_active : styles.main}> + <Typography className={scope === el.scope ? styles.tabTextActive : styles.tabText}> + {el.title} + </Typography> + </Box> + </ToggleButton> + ))} + </ToggleButtonGroup> ) })