@@ -32,14 +32,14 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson const { handleSubmit, register, reset, setValue, getValues } = useForm() const [isLoading, setIsLoading] = useState(false) const [businessGroups, setBusinessGroups] = useState() - const [currentGroup, setCurrentGroup] = useState('') + const [currentGroupUid, setCurrentGroupUid] = useState('') const { showMessage } = useShowDataStore() const { data: session } = useSession() const onSubmit = async (data: any) => { setIsLoading(true) - const group: string | undefined = businessGroups?.find((el) => (el.title === currentGroup ? el.uid : ''))?.uid || "" + const group = currentGroupUid || '' const res = await invitePerson(role, data.limit, data.email, group, session?.access) if (res && res.data.detail && res.status >= 300) { @@ -55,9 +55,8 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson setIsLoading(false) onClose() - + setCurrentGroupUid('') reset() - showNewPersons(res.data) } const checkError: SubmitErrorHandler = (data) => { @@ -87,16 +86,14 @@ export const InviteModal: FC = ({ open, onClose, showNewPerson Выберите роль - {/* Поле с выбором группы */} Выберите бизнес-группу + list={[{ uid: '', title: 'Без группы' }, ...(businessGroups ?? [])].reverse()} + valueKey="uid" + labelKey="title" + onChange={(e) => setCurrentGroupUid(e.target.value)} + value={currentGroupUid} + /> Укажите адрес электронной почты void - predictedPrice?: string | null + predictedPrice?: string | number | null } export const ModelInput: FC = ({ @@ -130,6 +130,31 @@ export const ModelInput: FC = ({ return () => window.removeEventListener('tour-send-message', handleTourSendMessage) }, [sendMessage, setValue, unpinImage]) + const parsedPredictedPrice = + typeof predictedPrice === 'number' + ? predictedPrice + : typeof predictedPrice === 'string' + ? Number(predictedPrice.toString().replace(/\s+/g, '').replace(',', '.')) + : null + const predictedPriceValue = parsedPredictedPrice !== null && Number.isFinite(parsedPredictedPrice) + ? Math.ceil(parsedPredictedPrice) + : null + const isPriceDanger = predictedPriceValue !== null && predictedPriceValue > 150 + const isPriceWarning = predictedPriceValue !== null && predictedPriceValue > 75 && predictedPriceValue < 150 + const isPriceSuccess = predictedPriceValue !== null && predictedPriceValue > 0 && predictedPriceValue < 75 + + const predictPriceToneClassName = isPriceDanger + ? classes.predictPriceDanger + : isPriceWarning + ? classes.predictPriceWarning + : isPriceSuccess + ? classes.predictPriceSuccess + : '' + + const predictPriceTooltip = desktop && isPriceDanger + ? 'Генерация может выйти очень дорогой, т.к вы ввели очень большой запрос или в чате накопились большие сообщения. Если хотите уменьшить стоимость генерации - создайте новый чат или уменьшите запрос' + : 'Стоимость генерации' + return ( <>
= ({ >
- {typeof predictedPrice === 'string' && ( - -
+ {predictedPriceValue !== null && ( + +
- - {Math.ceil(Number(predictedPrice))} + + {predictedPriceValue}
@@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { debounce } from 'lodash' -import { predictPrice } from '#/shared/api/models/predict-price' +import { PredictPriceResponse, predictPrice } from '#/shared/api/models/predict-price' interface UsePredictPriceParams { modelSlug: string @@ -12,6 +12,43 @@ interface UsePredictPriceParams { enabled?: boolean } +function parseTokenValue(value: unknown): string | number | null { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null + } + + if (typeof value === 'string') { + const normalized = value.trim().replace(/\s+/g, '').replace(',', '.') + const direct = Number(normalized) + if (Number.isFinite(direct)) return direct + + const match = value.match(/-?\d+([.,]\d+)?/) + if (!match) return null + const extracted = Number(match[0].replace(',', '.')) + return Number.isFinite(extracted) ? extracted : null + } + + return null +} + +function pickPrice(result: PredictPriceResponse): string | number | null { + const candidates = [ + result.price, + result.tokens_cost, + result.tokens, + result.cost, + result.total_tokens, + result.predict_price, + ] + + for (const candidate of candidates) { + const parsed = parseTokenValue(candidate) + if (parsed !== null) return parsed + } + + return null +} + export function usePredictPrice({ modelSlug, content, @@ -20,7 +57,7 @@ export function usePredictPrice({ token, enabled = true, }: UsePredictPriceParams) { - const [price, setPrice] = useState(null) + const [price, setPrice] = useState(null) const debouncedPredictRef = useRef( debounce( @@ -41,7 +78,7 @@ export function usePredictPrice({ }, accessToken ) - setPrice(typeof result.price === 'string' ? result.price : null) + setPrice(pickPrice(result)) } catch { setPrice(null) } @@ -10,7 +10,12 @@ export interface PredictPriceRequest { } export interface PredictPriceResponse { - price: string | null + price: string | number | null + tokens_cost?: string | number | null + tokens?: string | number | null + cost?: string | number | null + total_tokens?: string | number | null + predict_price?: string | number | null } export async function predictPrice( @@ -1 +1,2 @@ export { API_HOST, API_URL, ModelPagesList, surpriseMePrompts } from './constants' +export { MODEL_PRICE_FALLBACKS } from './model-price-fallbacks' @@ -0,0 +1,3 @@ +export const MODEL_PRICE_FALLBACKS: Record = { + hunyuan: 378, +} @@ -1,5 +1,5 @@ import * as React from 'react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Box, Card, CardMedia, Link, Stack, Typography } from '@mui/material' import Button from '@mui/material/Button' import axios from 'axios' @@ -34,7 +34,10 @@ const Reset: NextPageWithLayout = () => { const desktop = device === 'desktop' - const refImage = React.useRef(getRandomImage()) + const [cardImage, setCardImage] = useState(null) + useEffect(() => { + setCardImage(getRandomImage()) + }, []) return ( { {desktop && ( { enabled: !!modelType && !!session?.access && scope === 'playground', }) + const fallbackModelTokensCost = (botParams as unknown as { actual_stat?: { tokens_cost?: string | number | null } })?.actual_stat?.tokens_cost + const modelFallbackPrice = modelType ? MODEL_PRICE_FALLBACKS[modelType] : undefined + const displayedPredictedPrice = predictedPrice ?? fallbackModelTokensCost ?? modelFallbackPrice ?? null + return ( <> @@ -213,7 +218,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} - predictedPrice={predictedPrice} + predictedPrice={displayedPredictedPrice} /> )} @@ -317,7 +322,7 @@ const VideoModelPage: NextPageWithLayout = () => { sendMessage={onCreateImage} unpinImage={() => setImage(null)} viewMobileSettings={() => setOpenFiltersMobile(true)} - predictedPrice={predictedPrice} + predictedPrice={displayedPredictedPrice} /> )} {isProgressVisible && ( @@ -39,7 +39,8 @@ import { API_URL } from '#/shared/lib/constants' // import styles from '#/widgets/business-models/ui/models-list/models-list.module.scss' import { getBusinessGroups } from '#/widgets/business-persons/api/get-businessGroups' import ArrowUpOrDown from '#/widgets/top-bar-model/ui/arrow-up-or-down' -import { getModalById, PLATE_CHANGE_PASSWORD, RESEND_INVATION_PASSWORD } from '#/features/modals' +import { getModalById, PLATE_CHANGE_PASSWORD } from '#/features/modals' +import { resendInvation } from '#/features/resend-invation-corp/api/resend-invation' import { formatDateStatus, formatEmail, translateEmailStatus } from '../../lib/lib' import { XMark } from '#/features/remove-person' @@ -60,7 +61,21 @@ export const SecurityList = ({ const [currentPerson, setCurrentPerson] = useState(null) const passChangeModal = getModalById(PLATE_CHANGE_PASSWORD) - const passResendModal = getModalById(RESEND_INVATION_PASSWORD) + + const handleResendInvitation = async (email: string) => { + const response = await resendInvation(email, data?.access) + if (response.status === 200) { + showMessage('Приглашение переотправлено!', 'success') + setPersons((prev) => { + if (!prev || !Array.isArray(prev)) return prev + return prev.map((el) => + el.email === email ? { ...el, acceptance_status: 'pending' as const } : el + ) + }) + } else { + showMessage(response.data) + } + } useEffect(() => { setPersons(securityList) @@ -159,34 +174,33 @@ export const SecurityList = ({ - {searchPersons?.map((person: ResponseGetPersons) => { + {searchPersons?.map((person: ResponseGetPersons, idx: number) => { const statusPerson = translateEmailStatus(person.acceptance_status) + const rowKey = person.uid ?? `${person.email ?? 'row'}-${idx}` + const email = person?.email ?? '' + const roleLabel = person?.account_type ? RoleSelect[person.account_type] : '—' return ( - {formatEmail(person.email)} + {formatEmail(email) || email || '—'} - {RoleSelect[person.account_type]} + {roleLabel} {statusPerson + ' '} - {statusPerson === 'Приглашен' ? formatDateStatus(person.created_at) : null} + {statusPerson === 'Приглашен' + ? formatDateStatus(person.created_at) + : null} {statusPerson === 'Приглашен' ? (