@@ -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,9 @@ 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 { SvgIcon } from '#/shared/ui/svg' +import { Typography } from '@mui/material' +import { ThreePOutlined } from '@mui/icons-material' interface Input { loading: boolean @@ -29,6 +32,7 @@ interface Input { resendValue?: string value?: string onValueChange?: (value: string) => void + predictedPrice?: string | null } export const ModelInput: FC = ({ @@ -46,6 +50,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 +180,12 @@ export const ModelInput: FC = ({ >
+ {typeof predictedPrice === 'string' && ( +
+ + {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 +} @@ -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,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 ( <> @@ -200,6 +215,7 @@ const AudioModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} @@ -303,6 +319,7 @@ const AudioModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {isProgressVisible && ( @@ -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 ( <> @@ -259,6 +275,9 @@ const Page: NextPageWithLayout = () => { modelTitle={botParams?.title} currentVersion={version} tags={botParams?.tags ?? []} + inputValue={inputContent} + onInputValueChange={setInputContent} + predictedPrice={predictedPrice} /> @@ -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 ( <> @@ -180,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} @@ -187,6 +205,7 @@ const ImageModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {botParams?.blocked && ( @@ -279,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} @@ -286,6 +307,7 @@ const ImageModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {botParams?.blocked && ( @@ -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 ( <> @@ -198,6 +213,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} @@ -299,6 +315,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} + predictedPrice={predictedPrice} /> )} {isProgressVisible && ( @@ -43,6 +43,9 @@ export interface ChatProps { currentVersion: string botParams: IModel | null tags: IModelTag[] + inputValue?: string + onInputValueChange?: (value: string) => void + predictedPrice?: string | null } function Chat({ @@ -63,6 +66,9 @@ function Chat({ currentVersion, deviceOs, tags, + inputValue, + onInputValueChange, + predictedPrice, }: ChatProps) { const desktop = device === 'desktop' @@ -139,6 +145,9 @@ function Chat({ image={file} unpinImage={clearImage} imageLoad={onLoadImage} + value={inputValue} + onValueChange={onInputValueChange} + predictedPrice={predictedPrice} /> {blocked && (