@@ -73,3 +73,8 @@ .generationIcon { margin-top: 3px; } + +.settingsIcon { + flex-shrink: 0; + cursor: pointer; +} @@ -2,14 +2,11 @@ import React, { FC, useCallback, useEffect, useRef } from 'react' import { TextFieldProps } from '@mui/material/TextField/TextField' import classes from './model-input.module.scss' +import { PredictPrice } from './predict-price' 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' function buildTypeVersionsMap(inputs: IModelInputs[]): Record { const byType = new Map() @@ -96,7 +93,7 @@ export const ModelInput: FC = ({ if (type === 'text') return false return Array.isArray(versions) && (versions.length === 0 || versions.includes(currentVersion)) }) - + // Используем внешнее значение если оно передано, иначе внутреннее const value = externalValue !== undefined ? externalValue : internalValue const setValue = externalOnChange ? (val: string) => externalOnChange(val) : setInternalValue @@ -220,16 +217,7 @@ export const ModelInput: FC = ({ >
- {typeof predictedPrice === 'string' && ( - -
- - - {Math.ceil(Number(predictedPrice))} - -
-
- )} + {hasAttachInput && ( <> {!blocked && ( @@ -0,0 +1,41 @@ +.predictPrice { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 14px; + border-radius: 16px; + margin-right: 8px; + gap: 4px; + color: #7f7df3; + background-color: #7f7df31a; + + :global(svg path) { + fill: currentColor; + } + + &--low { + color: #10b981; + background-color: rgba(16, 185, 129, 0.12); + } + + &--mid { + color: #f59e0b; + background-color: rgba(245, 158, 11, 0.12); + } + + &--high { + color: #f15179; + background-color: rgba(241, 81, 121, 0.12); + } +} + +.generationIcon { + margin-top: 3px; + color: inherit; +} + +.value { + font-size: 14px; + font-weight: 600; + line-height: 1; +} @@ -0,0 +1,31 @@ +import { TooltipCustom } from '#/shared' +import { SvgIcon } from '#/shared/ui/svg' + +import classes from './predict-price.module.scss' + +const HIGH_COST_TOOLTIP = + 'Генерация может выйти очень дорогой, т.к вы ввели очень большой запрос или в чате накопились большие сообщения. Если хотите снизить стоимость генерации, создайте новый чат или уменьшите количество запросов' + +function tierModifierClass(token: number): string { + if (token < 75) return classes['predictPrice--low'] + if (token < 150) return classes['predictPrice--mid'] + return classes['predictPrice--high'] +} + +export function PredictPrice({ token, isChatBot = false }: { token: number; isChatBot?: boolean }) { + const modifier = isChatBot ? tierModifierClass(token) : '' + const isHighCost = isChatBot && token >= 150 + + return ( + +
+ + {token} +
+
+ ) +} @@ -1,4 +1,4 @@ export const baseColor = '#7F7DF3' -export const errorColor = '#F15179;' +export const errorColor = '#F15179' export const colorLight = '#5E5E5E' export const colorDark = '#A6A5A5' @@ -6,6 +6,10 @@ border-radius: 16px; background: rgba(130, 128, 255, 0.16); color: #8280ff; + cursor: pointer; + border: none; + outline: none; + font: inherit; &_expiring { background: #B01E1E26; @@ -13,6 +17,11 @@ } } +.badge:hover { + background: rgba(130, 128, 255, 0.16); + color: #8280ff; +} + .icon { font-size: 24px !important; } @@ -21,12 +30,33 @@ font-size: 15px; font-weight: 600; white-space: nowrap; + position: relative; &_highlight { font-weight: 700; } } +.textDefault { + opacity: 1; +} + +.textHover { + position: absolute; + right: 0; + top: 0; + opacity: 0; + pointer-events: none; +} + +.badge:hover .textDefault { + opacity: 0; +} + +.badge:hover .textHover { + opacity: 1; +} + @media (max-width: 900px) { .badge { padding: 6px 10px; @@ -1,4 +1,10 @@ import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined' +import Router from 'next/router' +import { useSession } from 'next-auth/react' + +import { useAppSelector } from '#/app/store/store' +import { accountApi } from '#/shared/api/account-endpoints' +import { useShowDataStore } from '#/shared/lib/hooks/use-show-data' import styles from './subscription-days-badge.module.scss' @@ -18,15 +24,48 @@ function declineDays(days: number): string { export const SubscriptionDaysBadge = ({ days }: SubscriptionDaysBadgeProps) => { const normalizedDays = Number.isFinite(days) ? Math.max(0, Math.trunc(days)) : 0 - const isExpiring = normalizedDays <= 3; + const isExpiring = normalizedDays <= 3 + const currentPlanUid = useAppSelector((state) => state.user.payment_plan.plan.uid) + const { data } = useSession() + const { showMessage } = useShowDataStore() + + const handleRenew = async () => { + if (!data?.access || !currentPlanUid) { + showMessage('Произошла ошибка при оплате') + return + } + try { + const urlForPay = await accountApi.payProduct(data.access, currentPlanUid) + if (urlForPay) { + await Router.push(urlForPay) + } + } catch (err: any) { + const detail = err?.response?.data?.detail + const message = detail ?? 'Произошла ошибка при оплате' + showMessage(message) + } + } return ( -
+
+ ) } @@ -92,13 +92,13 @@ const Offer: React.FC = ({ uid, tokens_per_plan, price, grouped_fea

{commaSeparated(parseInt(price) ?? 0)} ₽

{ e.stopPropagation() handlePayClick() }} > - {isCurrentSubscription ? 'Текущая подписка' : 'Оплатить'} + {isCurrentSubscription ? 'Нажмите, чтобы пополнить баланс' : 'Оплатить'}
@@ -228,7 +228,7 @@ export const ImageMessagesList: React.FC = memo(({ device, images, 30 ? message.content : ''} + title={message.content.length > (device === 'mobile' ? 35 : 30) ? message.content : ''} > message.content.length > 0 && onPromptClick?.(message.content)} @@ -243,9 +243,9 @@ export const ImageMessagesList: React.FC = memo(({ device, images, {!loaded ? '' : message.content.length > 0 - ? message?.content.replaceAll('"', '').slice(0, 30) + ? message?.content.replaceAll('"', '').slice(0, device === 'mobile' ? 35 : 30) : 'описание отсутствует'} - {message?.content.length > 30 && loaded && '...'} + {message?.content.length > (device === 'mobile' ? 35 : 30) && loaded && '...'} @@ -98,7 +98,7 @@ export const Referral = ({ device }: IProps) => { direction={'row'} alignItems={'center'} > - {device === 'mobile' ? url.slice(0, 20) + '...' : url.slice(0, 40) + '...'} + {device === 'mobile' ? url.slice(0, 35) + '...' : url.slice(0, 40) + '...'}