| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import i18next from 'i18next'; |
| | import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui'; |
| | import { copy, showSuccess } from './utils'; |
| | import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile'; |
| | import { visit } from 'unist-util-visit'; |
| | import * as LobeIcons from '@lobehub/icons'; |
| | import { |
| | OpenAI, |
| | Claude, |
| | Gemini, |
| | Moonshot, |
| | Zhipu, |
| | Qwen, |
| | DeepSeek, |
| | Minimax, |
| | Wenxin, |
| | Spark, |
| | Midjourney, |
| | Hunyuan, |
| | Cohere, |
| | Cloudflare, |
| | Ai360, |
| | Yi, |
| | Jina, |
| | Mistral, |
| | XAI, |
| | Ollama, |
| | Doubao, |
| | Suno, |
| | Xinference, |
| | OpenRouter, |
| | Dify, |
| | Coze, |
| | SiliconCloud, |
| | FastGPT, |
| | Kling, |
| | Jimeng, |
| | Perplexity, |
| | Replicate, |
| | } from '@lobehub/icons'; |
| |
|
| | import { |
| | LayoutDashboard, |
| | TerminalSquare, |
| | MessageSquare, |
| | Key, |
| | BarChart3, |
| | Image as ImageIcon, |
| | CheckSquare, |
| | CreditCard, |
| | Layers, |
| | Gift, |
| | User, |
| | Settings, |
| | CircleUser, |
| | Package, |
| | } from 'lucide-react'; |
| |
|
| | |
| | export function getLucideIcon(key, selected = false) { |
| | const size = 16; |
| | const strokeWidth = 2; |
| | const SELECTED_COLOR = 'var(--semi-color-primary)'; |
| | const iconColor = selected ? SELECTED_COLOR : 'currentColor'; |
| | const commonProps = { |
| | size, |
| | strokeWidth, |
| | className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`, |
| | }; |
| |
|
| | |
| | switch (key) { |
| | case 'detail': |
| | return <LayoutDashboard {...commonProps} color={iconColor} />; |
| | case 'playground': |
| | return <TerminalSquare {...commonProps} color={iconColor} />; |
| | case 'chat': |
| | return <MessageSquare {...commonProps} color={iconColor} />; |
| | case 'token': |
| | return <Key {...commonProps} color={iconColor} />; |
| | case 'log': |
| | return <BarChart3 {...commonProps} color={iconColor} />; |
| | case 'midjourney': |
| | return <ImageIcon {...commonProps} color={iconColor} />; |
| | case 'task': |
| | return <CheckSquare {...commonProps} color={iconColor} />; |
| | case 'topup': |
| | return <CreditCard {...commonProps} color={iconColor} />; |
| | case 'channel': |
| | return <Layers {...commonProps} color={iconColor} />; |
| | case 'redemption': |
| | return <Gift {...commonProps} color={iconColor} />; |
| | case 'user': |
| | case 'personal': |
| | return <User {...commonProps} color={iconColor} />; |
| | case 'models': |
| | return <Package {...commonProps} color={iconColor} />; |
| | case 'setting': |
| | return <Settings {...commonProps} color={iconColor} />; |
| | default: |
| | return <CircleUser {...commonProps} color={iconColor} />; |
| | } |
| | } |
| |
|
| | |
| | export const getModelCategories = (() => { |
| | let categoriesCache = null; |
| | let lastLocale = null; |
| |
|
| | return (t) => { |
| | const currentLocale = i18next.language; |
| | if (categoriesCache && lastLocale === currentLocale) { |
| | return categoriesCache; |
| | } |
| |
|
| | categoriesCache = { |
| | all: { |
| | label: t('全部模型'), |
| | icon: null, |
| | filter: () => true, |
| | }, |
| | openai: { |
| | label: 'OpenAI', |
| | icon: <OpenAI />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('gpt') || |
| | model.model_name.toLowerCase().includes('dall-e') || |
| | model.model_name.toLowerCase().includes('whisper') || |
| | model.model_name.toLowerCase().includes('tts-1') || |
| | model.model_name.toLowerCase().includes('text-embedding-3') || |
| | model.model_name.toLowerCase().includes('text-moderation') || |
| | model.model_name.toLowerCase().includes('babbage') || |
| | model.model_name.toLowerCase().includes('davinci') || |
| | model.model_name.toLowerCase().includes('curie') || |
| | model.model_name.toLowerCase().includes('ada') || |
| | model.model_name.toLowerCase().includes('o1') || |
| | model.model_name.toLowerCase().includes('o3') || |
| | model.model_name.toLowerCase().includes('o4'), |
| | }, |
| | anthropic: { |
| | label: 'Anthropic', |
| | icon: <Claude.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('claude'), |
| | }, |
| | gemini: { |
| | label: 'Gemini', |
| | icon: <Gemini.Color />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('gemini') || |
| | model.model_name.toLowerCase().includes('gemma') || |
| | model.model_name.toLowerCase().includes('learnlm') || |
| | model.model_name.toLowerCase().startsWith('embedding-') || |
| | model.model_name.toLowerCase().includes('text-embedding-004') || |
| | model.model_name.toLowerCase().includes('imagen-4') || |
| | model.model_name.toLowerCase().includes('veo-') || |
| | model.model_name.toLowerCase().includes('aqa') , |
| | }, |
| | moonshot: { |
| | label: 'Moonshot', |
| | icon: <Moonshot />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('moonshot') || |
| | model.model_name.toLowerCase().includes('kimi'), |
| | }, |
| | zhipu: { |
| | label: t('智谱'), |
| | icon: <Zhipu.Color />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('chatglm') || |
| | model.model_name.toLowerCase().includes('glm-') || |
| | model.model_name.toLowerCase().includes('cogview') || |
| | model.model_name.toLowerCase().includes('cogvideo'), |
| | }, |
| | qwen: { |
| | label: t('通义千问'), |
| | icon: <Qwen.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('qwen'), |
| | }, |
| | deepseek: { |
| | label: 'DeepSeek', |
| | icon: <DeepSeek.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('deepseek'), |
| | }, |
| | minimax: { |
| | label: 'MiniMax', |
| | icon: <Minimax.Color />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('abab') || |
| | model.model_name.toLowerCase().includes('minimax'), |
| | }, |
| | baidu: { |
| | label: t('文心一言'), |
| | icon: <Wenxin.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('ernie'), |
| | }, |
| | xunfei: { |
| | label: t('讯飞星火'), |
| | icon: <Spark.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('spark'), |
| | }, |
| | midjourney: { |
| | label: 'Midjourney', |
| | icon: <Midjourney />, |
| | filter: (model) => model.model_name.toLowerCase().includes('mj_'), |
| | }, |
| | tencent: { |
| | label: t('腾讯混元'), |
| | icon: <Hunyuan.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('hunyuan'), |
| | }, |
| | cohere: { |
| | label: 'Cohere', |
| | icon: <Cohere.Color />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('command') || |
| | model.model_name.toLowerCase().includes('c4ai-') || |
| | model.model_name.toLowerCase().includes('embed-'), |
| | }, |
| | cloudflare: { |
| | label: 'Cloudflare', |
| | icon: <Cloudflare.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('@cf/'), |
| | }, |
| | ai360: { |
| | label: t('360智脑'), |
| | icon: <Ai360.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('360'), |
| | }, |
| | jina: { |
| | label: 'Jina', |
| | icon: <Jina />, |
| | filter: (model) => model.model_name.toLowerCase().includes('jina'), |
| | }, |
| | mistral: { |
| | label: 'Mistral AI', |
| | icon: <Mistral.Color />, |
| | filter: (model) => |
| | model.model_name.toLowerCase().includes('mistral') || |
| | model.model_name.toLowerCase().includes('codestral') || |
| | model.model_name.toLowerCase().includes('pixtral') || |
| | model.model_name.toLowerCase().includes('voxtral') || |
| | model.model_name.toLowerCase().includes('magistral'), |
| | }, |
| | xai: { |
| | label: 'xAI', |
| | icon: <XAI />, |
| | filter: (model) => model.model_name.toLowerCase().includes('grok'), |
| | }, |
| | llama: { |
| | label: 'Llama', |
| | icon: <Ollama />, |
| | filter: (model) => model.model_name.toLowerCase().includes('llama'), |
| | }, |
| | doubao: { |
| | label: t('豆包'), |
| | icon: <Doubao.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('doubao'), |
| | }, |
| | yi: { |
| | label: t('零一万物'), |
| | icon: <Yi.Color />, |
| | filter: (model) => model.model_name.toLowerCase().includes('yi'), |
| | }, |
| | }; |
| |
|
| | lastLocale = currentLocale; |
| | return categoriesCache; |
| | }; |
| | })(); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function getChannelIcon(channelType) { |
| | const iconSize = 14; |
| |
|
| | switch (channelType) { |
| | case 1: |
| | case 3: |
| | return <OpenAI size={iconSize} />; |
| | case 2: |
| | case 5: |
| | return <Midjourney size={iconSize} />; |
| | case 36: |
| | return <Suno size={iconSize} />; |
| | case 4: |
| | return <Ollama size={iconSize} />; |
| | case 14: |
| | case 33: |
| | return <Claude.Color size={iconSize} />; |
| | case 41: |
| | return <Gemini.Color size={iconSize} />; |
| | case 34: |
| | return <Cohere.Color size={iconSize} />; |
| | case 39: |
| | return <Cloudflare.Color size={iconSize} />; |
| | case 43: |
| | return <DeepSeek.Color size={iconSize} />; |
| | case 15: |
| | case 46: |
| | return <Wenxin.Color size={iconSize} />; |
| | case 17: |
| | return <Qwen.Color size={iconSize} />; |
| | case 18: |
| | return <Spark.Color size={iconSize} />; |
| | case 16: |
| | case 26: |
| | return <Zhipu.Color size={iconSize} />; |
| | case 24: |
| | case 11: |
| | return <Gemini.Color size={iconSize} />; |
| | case 47: |
| | return <Xinference.Color size={iconSize} />; |
| | case 25: |
| | return <Moonshot size={iconSize} />; |
| | case 27: |
| | return <Perplexity.Color size={iconSize} />; |
| | case 20: |
| | return <OpenRouter size={iconSize} />; |
| | case 19: |
| | return <Ai360.Color size={iconSize} />; |
| | case 23: |
| | return <Hunyuan.Color size={iconSize} />; |
| | case 31: |
| | return <Yi.Color size={iconSize} />; |
| | case 35: |
| | return <Minimax.Color size={iconSize} />; |
| | case 37: |
| | return <Dify.Color size={iconSize} />; |
| | case 38: |
| | return <Jina size={iconSize} />; |
| | case 40: |
| | return <SiliconCloud.Color size={iconSize} />; |
| | case 42: |
| | return <Mistral.Color size={iconSize} />; |
| | case 45: |
| | return <Doubao.Color size={iconSize} />; |
| | case 48: |
| | return <XAI size={iconSize} />; |
| | case 49: |
| | return <Coze size={iconSize} />; |
| | case 50: |
| | return <Kling.Color size={iconSize} />; |
| | case 51: |
| | return <Jimeng.Color size={iconSize} />; |
| | case 54: |
| | return <Doubao.Color size={iconSize} />; |
| | case 56: |
| | return <Replicate size={iconSize} />; |
| | case 8: |
| | case 22: |
| | return <FastGPT.Color size={iconSize} />; |
| | case 21: |
| | case 44: |
| | default: |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function getLobeHubIcon(iconName, size = 14) { |
| | if (typeof iconName === 'string') iconName = iconName.trim(); |
| | |
| | if (!iconName) { |
| | return <Avatar size='extra-extra-small'>?</Avatar>; |
| | } |
| |
|
| | |
| | const segments = String(iconName).split('.'); |
| | const baseKey = segments[0]; |
| | const BaseIcon = LobeIcons[baseKey]; |
| |
|
| | let IconComponent = undefined; |
| | let propStartIndex = 1; |
| |
|
| | if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) { |
| | IconComponent = BaseIcon[segments[1]]; |
| | propStartIndex = 2; |
| | } else { |
| | IconComponent = LobeIcons[baseKey]; |
| | propStartIndex = 1; |
| | } |
| |
|
| | |
| | if ( |
| | !IconComponent || |
| | (typeof IconComponent !== 'function' && typeof IconComponent !== 'object') |
| | ) { |
| | const firstLetter = String(iconName).charAt(0).toUpperCase(); |
| | return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>; |
| | } |
| |
|
| | |
| | const props = {}; |
| |
|
| | const parseValue = (raw) => { |
| | if (raw == null) return true; |
| | let v = String(raw).trim(); |
| | |
| | if (v.startsWith('{') && v.endsWith('}')) { |
| | v = v.slice(1, -1).trim(); |
| | } |
| | |
| | if ( |
| | (v.startsWith('"') && v.endsWith('"')) || |
| | (v.startsWith("'") && v.endsWith("'")) |
| | ) { |
| | return v.slice(1, -1); |
| | } |
| | |
| | if (v === 'true') return true; |
| | if (v === 'false') return false; |
| | |
| | if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v); |
| | |
| | return v; |
| | }; |
| |
|
| | for (let i = propStartIndex; i < segments.length; i++) { |
| | const seg = segments[i]; |
| | if (!seg) continue; |
| | const eqIdx = seg.indexOf('='); |
| | if (eqIdx === -1) { |
| | props[seg.trim()] = true; |
| | continue; |
| | } |
| | const key = seg.slice(0, eqIdx).trim(); |
| | const valRaw = seg.slice(eqIdx + 1).trim(); |
| | props[key] = parseValue(valRaw); |
| | } |
| |
|
| | |
| | if (props.size == null && size != null) props.size = size; |
| |
|
| | return <IconComponent {...props} />; |
| | } |
| |
|
| | |
| | const colors = [ |
| | 'amber', |
| | 'blue', |
| | 'cyan', |
| | 'green', |
| | 'grey', |
| | 'indigo', |
| | 'light-blue', |
| | 'lime', |
| | 'orange', |
| | 'pink', |
| | 'purple', |
| | 'red', |
| | 'teal', |
| | 'violet', |
| | 'yellow', |
| | ]; |
| |
|
| | |
| | const baseColors = [ |
| | '#1664FF', |
| | '#1AC6FF', |
| | '#FF8A00', |
| | '#3CC780', |
| | '#7442D4', |
| | '#FFC400', |
| | '#304D77', |
| | '#B48DEB', |
| | '#009488', |
| | '#FF7DDA', |
| | ]; |
| |
|
| | |
| | const extendedColors = [ |
| | '#1664FF', |
| | '#B2CFFF', |
| | '#1AC6FF', |
| | '#94EFFF', |
| | '#FF8A00', |
| | '#FFCE7A', |
| | '#3CC780', |
| | '#B9EDCD', |
| | '#7442D4', |
| | '#DDC5FA', |
| | '#FFC400', |
| | '#FAE878', |
| | '#304D77', |
| | '#8B959E', |
| | '#B48DEB', |
| | '#EFE3FF', |
| | '#009488', |
| | '#59BAA8', |
| | '#FF7DDA', |
| | '#FFCFEE', |
| | ]; |
| |
|
| | |
| | export const modelColorMap = { |
| | 'dall-e': 'rgb(147,112,219)', |
| | |
| | 'dall-e-3': 'rgb(153,50,204)', |
| | 'gpt-3.5-turbo': 'rgb(184,227,167)', |
| | |
| | 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', |
| | 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', |
| | 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', |
| | 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', |
| | 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', |
| | 'gpt-4': 'rgb(135,206,235)', |
| | |
| | 'gpt-4-0613': 'rgb(100,149,237)', |
| | 'gpt-4-1106-preview': 'rgb(30,144,255)', |
| | 'gpt-4-0125-preview': 'rgb(2,177,236)', |
| | 'gpt-4-turbo-preview': 'rgb(2,177,255)', |
| | 'gpt-4-32k': 'rgb(104,111,238)', |
| | |
| | 'gpt-4-32k-0613': 'rgb(61,71,139)', |
| | 'gpt-4-all': 'rgb(65,105,225)', |
| | 'gpt-4-gizmo-*': 'rgb(0,0,255)', |
| | 'gpt-4-vision-preview': 'rgb(25,25,112)', |
| | 'text-ada-001': 'rgb(255,192,203)', |
| | 'text-babbage-001': 'rgb(255,160,122)', |
| | 'text-curie-001': 'rgb(219,112,147)', |
| | |
| | 'text-davinci-003': 'rgb(219,112,147)', |
| | 'text-davinci-edit-001': 'rgb(255,105,180)', |
| | 'text-embedding-ada-002': 'rgb(255,182,193)', |
| | 'text-embedding-v1': 'rgb(255,174,185)', |
| | 'text-moderation-latest': 'rgb(255,130,171)', |
| | 'text-moderation-stable': 'rgb(255,160,122)', |
| | 'tts-1': 'rgb(255,140,0)', |
| | 'tts-1-1106': 'rgb(255,165,0)', |
| | 'tts-1-hd': 'rgb(255,215,0)', |
| | 'tts-1-hd-1106': 'rgb(255,223,0)', |
| | 'whisper-1': 'rgb(245,245,220)', |
| | 'claude-3-opus-20240229': 'rgb(255,132,31)', |
| | 'claude-3-sonnet-20240229': 'rgb(253,135,93)', |
| | 'claude-3-haiku-20240307': 'rgb(255,175,146)', |
| | 'claude-2.1': 'rgb(255,209,190)', |
| | }; |
| |
|
| | export function modelToColor(modelName) { |
| | |
| | if (modelColorMap[modelName]) { |
| | return modelColorMap[modelName]; |
| | } |
| |
|
| | |
| | let hash = 0; |
| | for (let i = 0; i < modelName.length; i++) { |
| | hash = (hash << 5) - hash + modelName.charCodeAt(i); |
| | hash = hash & hash; |
| | } |
| | hash = Math.abs(hash); |
| |
|
| | |
| | const colorPalette = modelName.length > 10 ? extendedColors : baseColors; |
| |
|
| | |
| | const index = hash % colorPalette.length; |
| | return colorPalette[index]; |
| | } |
| |
|
| | export function stringToColor(str) { |
| | let sum = 0; |
| | for (let i = 0; i < str.length; i++) { |
| | sum += str.charCodeAt(i); |
| | } |
| | let i = sum % colors.length; |
| | return colors[i]; |
| | } |
| |
|
| | |
| | export function renderModelTag(modelName, options = {}) { |
| | const { |
| | color, |
| | size = 'default', |
| | shape = 'circle', |
| | onClick, |
| | suffixIcon, |
| | } = options; |
| |
|
| | const categories = getModelCategories(i18next.t); |
| | let icon = null; |
| |
|
| | for (const [key, category] of Object.entries(categories)) { |
| | if (key !== 'all' && category.filter({ model_name: modelName })) { |
| | icon = category.icon; |
| | break; |
| | } |
| | } |
| |
|
| | return ( |
| | <Tag |
| | color={color || stringToColor(modelName)} |
| | prefixIcon={icon} |
| | suffixIcon={suffixIcon} |
| | size={size} |
| | shape={shape} |
| | onClick={onClick} |
| | > |
| | {modelName} |
| | </Tag> |
| | ); |
| | } |
| |
|
| | export function renderText(text, limit) { |
| | if (text.length > limit) { |
| | return text.slice(0, limit - 3) + '...'; |
| | } |
| | return text; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function renderGroup(group) { |
| | if (group === '') { |
| | return ( |
| | <Tag key='default' color='white' shape='circle'> |
| | {i18next.t('用户分组')} |
| | </Tag> |
| | ); |
| | } |
| |
|
| | const tagColors = { |
| | vip: 'yellow', |
| | pro: 'yellow', |
| | svip: 'red', |
| | premium: 'red', |
| | }; |
| |
|
| | const groups = group.split(',').sort(); |
| |
|
| | return ( |
| | <span key={group}> |
| | {groups.map((group) => ( |
| | <Tag |
| | color={tagColors[group] || stringToColor(group)} |
| | key={group} |
| | shape='circle' |
| | onClick={async (event) => { |
| | event.stopPropagation(); |
| | if (await copy(group)) { |
| | showSuccess(i18next.t('已复制:') + group); |
| | } else { |
| | Modal.error({ |
| | title: i18next.t('无法复制到剪贴板,请手动复制'), |
| | content: group, |
| | }); |
| | } |
| | }} |
| | > |
| | {group} |
| | </Tag> |
| | ))} |
| | </span> |
| | ); |
| | } |
| |
|
| | export function renderRatio(ratio) { |
| | let color = 'green'; |
| | if (ratio > 5) { |
| | color = 'red'; |
| | } else if (ratio > 3) { |
| | color = 'orange'; |
| | } else if (ratio > 1) { |
| | color = 'blue'; |
| | } |
| | return ( |
| | <Tag color={color}> |
| | {ratio}x {i18next.t('倍率')} |
| | </Tag> |
| | ); |
| | } |
| |
|
| | const measureTextWidth = ( |
| | text, |
| | style = { |
| | fontSize: '14px', |
| | fontFamily: |
| | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', |
| | }, |
| | containerWidth, |
| | ) => { |
| | const span = document.createElement('span'); |
| |
|
| | span.style.visibility = 'hidden'; |
| | span.style.position = 'absolute'; |
| | span.style.whiteSpace = 'nowrap'; |
| | span.style.fontSize = style.fontSize; |
| | span.style.fontFamily = style.fontFamily; |
| |
|
| | span.textContent = text; |
| |
|
| | document.body.appendChild(span); |
| | const width = span.offsetWidth; |
| |
|
| | document.body.removeChild(span); |
| |
|
| | return width; |
| | }; |
| |
|
| | export function truncateText(text, maxWidth = 200) { |
| | const isMobileScreen = window.matchMedia( |
| | `(max-width: ${MOBILE_BREAKPOINT - 1}px)`, |
| | ).matches; |
| | if (!isMobileScreen) { |
| | return text; |
| | } |
| | if (!text) return text; |
| |
|
| | try { |
| | |
| | let actualMaxWidth = maxWidth; |
| | if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) { |
| | const percentage = parseFloat(maxWidth) / 100; |
| | |
| | actualMaxWidth = window.innerWidth * percentage; |
| | } |
| |
|
| | const width = measureTextWidth(text); |
| | if (width <= actualMaxWidth) return text; |
| |
|
| | let left = 0; |
| | let right = text.length; |
| | let result = text; |
| |
|
| | while (left <= right) { |
| | const mid = Math.floor((left + right) / 2); |
| | const truncated = text.slice(0, mid) + '...'; |
| | const currentWidth = measureTextWidth(truncated); |
| |
|
| | if (currentWidth <= actualMaxWidth) { |
| | result = truncated; |
| | left = mid + 1; |
| | } else { |
| | right = mid - 1; |
| | } |
| | } |
| |
|
| | return result; |
| | } catch (error) { |
| | console.warn( |
| | 'Text measurement failed, falling back to character count', |
| | error, |
| | ); |
| | if (text.length > 20) { |
| | return text.slice(0, 17) + '...'; |
| | } |
| | return text; |
| | } |
| | } |
| |
|
| | export const renderGroupOption = (item) => { |
| | const { |
| | disabled, |
| | selected, |
| | label, |
| | value, |
| | focused, |
| | className, |
| | style, |
| | onMouseEnter, |
| | onClick, |
| | empty, |
| | emptyContent, |
| | ...rest |
| | } = item; |
| |
|
| | const baseStyle = { |
| | display: 'flex', |
| | justifyContent: 'space-between', |
| | alignItems: 'center', |
| | padding: '8px 16px', |
| | cursor: disabled ? 'not-allowed' : 'pointer', |
| | backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent', |
| | opacity: disabled ? 0.5 : 1, |
| | ...(selected && { |
| | backgroundColor: 'var(--semi-color-primary-light-default)', |
| | }), |
| | '&:hover': { |
| | backgroundColor: !disabled && 'var(--semi-color-fill-1)', |
| | }, |
| | }; |
| |
|
| | const handleClick = () => { |
| | if (!disabled && onClick) { |
| | onClick(); |
| | } |
| | }; |
| |
|
| | const handleMouseEnter = (e) => { |
| | if (!disabled && onMouseEnter) { |
| | onMouseEnter(e); |
| | } |
| | }; |
| |
|
| | return ( |
| | <div |
| | style={baseStyle} |
| | onClick={handleClick} |
| | onMouseEnter={handleMouseEnter} |
| | > |
| | <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> |
| | <Typography.Text strong type={disabled ? 'tertiary' : undefined}> |
| | {value} |
| | </Typography.Text> |
| | <Typography.Text type='secondary' size='small'> |
| | {label} |
| | </Typography.Text> |
| | </div> |
| | {item.ratio && renderRatio(item.ratio)} |
| | </div> |
| | ); |
| | }; |
| |
|
| | export function renderNumber(num) { |
| | if (num >= 1000000000) { |
| | return (num / 1000000000).toFixed(1) + 'B'; |
| | } else if (num >= 1000000) { |
| | return (num / 1000000).toFixed(1) + 'M'; |
| | } else if (num >= 10000) { |
| | return (num / 1000).toFixed(1) + 'k'; |
| | } else { |
| | return num; |
| | } |
| | } |
| |
|
| | export function renderQuotaNumberWithDigit(num, digits = 2) { |
| | if (typeof num !== 'number' || isNaN(num)) { |
| | return 0; |
| | } |
| | const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| | num = num.toFixed(digits); |
| | if (quotaDisplayType === 'CNY') { |
| | return '¥' + num; |
| | } else if (quotaDisplayType === 'USD') { |
| | return '$' + num; |
| | } else if (quotaDisplayType === 'CUSTOM') { |
| | const statusStr = localStorage.getItem('status'); |
| | let symbol = '¤'; |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | symbol = s?.custom_currency_symbol || symbol; |
| | } |
| | } catch (e) {} |
| | return symbol + num; |
| | } else { |
| | return num; |
| | } |
| | } |
| |
|
| | export function renderNumberWithPoint(num) { |
| | if (num === undefined) return ''; |
| | num = num.toFixed(2); |
| | if (num >= 100000) { |
| | |
| | let numStr = num.toString(); |
| | |
| | let decimalPointIndex = numStr.indexOf('.'); |
| |
|
| | let wholePart = numStr; |
| | let decimalPart = ''; |
| |
|
| | |
| | if (decimalPointIndex !== -1) { |
| | wholePart = numStr.slice(0, decimalPointIndex); |
| | decimalPart = numStr.slice(decimalPointIndex); |
| | } |
| |
|
| | |
| | let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2); |
| |
|
| | |
| | return shortenedWholePart + decimalPart; |
| | } |
| |
|
| | |
| | return num; |
| | } |
| |
|
| | export function getQuotaPerUnit() { |
| | let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| | quotaPerUnit = parseFloat(quotaPerUnit); |
| | return quotaPerUnit; |
| | } |
| |
|
| | export function renderUnitWithQuota(quota) { |
| | let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| | quotaPerUnit = parseFloat(quotaPerUnit); |
| | quota = parseFloat(quota); |
| | return quotaPerUnit * quota; |
| | } |
| |
|
| | export function getQuotaWithUnit(quota, digits = 6) { |
| | let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| | quotaPerUnit = parseFloat(quotaPerUnit); |
| | return (quota / quotaPerUnit).toFixed(digits); |
| | } |
| |
|
| | export function renderQuotaWithAmount(amount) { |
| | const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| | if (quotaDisplayType === 'TOKENS') { |
| | return renderNumber(renderUnitWithQuota(amount)); |
| | } |
| | if (quotaDisplayType === 'CNY') { |
| | return '¥' + amount; |
| | } else if (quotaDisplayType === 'CUSTOM') { |
| | const statusStr = localStorage.getItem('status'); |
| | let symbol = '¤'; |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | symbol = s?.custom_currency_symbol || symbol; |
| | } |
| | } catch (e) {} |
| | return symbol + amount; |
| | } |
| | return '$' + amount; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function getCurrencyConfig() { |
| | const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| | const statusStr = localStorage.getItem('status'); |
| |
|
| | let symbol = '$'; |
| | let rate = 1; |
| |
|
| | if (quotaDisplayType === 'CNY') { |
| | symbol = '¥'; |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | rate = s?.usd_exchange_rate || 7; |
| | } |
| | } catch (e) {} |
| | } else if (quotaDisplayType === 'CUSTOM') { |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | symbol = s?.custom_currency_symbol || '¤'; |
| | rate = s?.custom_currency_exchange_rate || 1; |
| | } |
| | } catch (e) {} |
| | } |
| |
|
| | return { symbol, rate, type: quotaDisplayType }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function convertUSDToCurrency(usdAmount, digits = 2) { |
| | const { symbol, rate } = getCurrencyConfig(); |
| | const convertedAmount = usdAmount * rate; |
| | return symbol + convertedAmount.toFixed(digits); |
| | } |
| |
|
| | export function renderQuota(quota, digits = 2) { |
| | let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| | const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| | quotaPerUnit = parseFloat(quotaPerUnit); |
| | if (quotaDisplayType === 'TOKENS') { |
| | return renderNumber(quota); |
| | } |
| | const resultUSD = quota / quotaPerUnit; |
| | let symbol = '$'; |
| | let value = resultUSD; |
| | if (quotaDisplayType === 'CNY') { |
| | const statusStr = localStorage.getItem('status'); |
| | let usdRate = 1; |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | usdRate = s?.usd_exchange_rate || 1; |
| | } |
| | } catch (e) {} |
| | value = resultUSD * usdRate; |
| | symbol = '¥'; |
| | } else if (quotaDisplayType === 'CUSTOM') { |
| | const statusStr = localStorage.getItem('status'); |
| | let symbolCustom = '¤'; |
| | let rate = 1; |
| | try { |
| | if (statusStr) { |
| | const s = JSON.parse(statusStr); |
| | symbolCustom = s?.custom_currency_symbol || symbolCustom; |
| | rate = s?.custom_currency_exchange_rate || rate; |
| | } |
| | } catch (e) {} |
| | value = resultUSD * rate; |
| | symbol = symbolCustom; |
| | } |
| | const fixedResult = value.toFixed(digits); |
| | if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) { |
| | const minValue = Math.pow(10, -digits); |
| | return symbol + minValue.toFixed(digits); |
| | } |
| | return symbol + fixedResult; |
| | } |
| |
|
| | function isValidGroupRatio(ratio) { |
| | return Number.isFinite(ratio) && ratio !== -1; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | function getEffectiveRatio(groupRatio, user_group_ratio) { |
| | const useUserGroupRatio = isValidGroupRatio(user_group_ratio); |
| | const ratioLabel = useUserGroupRatio |
| | ? i18next.t('专属倍率') |
| | : i18next.t('分组倍率'); |
| | const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio; |
| |
|
| | return { |
| | ratio: effectiveRatio, |
| | label: ratioLabel, |
| | useUserGroupRatio: useUserGroupRatio, |
| | }; |
| | } |
| |
|
| | |
| | function renderPriceSimpleCore({ |
| | modelRatio, |
| | modelPrice = -1, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens = 0, |
| | cacheRatio = 1.0, |
| | cacheCreationTokens = 0, |
| | cacheCreationRatio = 1.0, |
| | cacheCreationTokens5m = 0, |
| | cacheCreationRatio5m = 1.0, |
| | cacheCreationTokens1h = 0, |
| | cacheCreationRatio1h = 1.0, |
| | image = false, |
| | imageRatio = 1.0, |
| | isSystemPromptOverride = false, |
| | }) { |
| | const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| | groupRatio, |
| | user_group_ratio, |
| | ); |
| | const finalGroupRatio = effectiveGroupRatio; |
| |
|
| | if (modelPrice !== -1) { |
| | return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { |
| | price: modelPrice, |
| | ratioType: ratioLabel, |
| | ratio: finalGroupRatio, |
| | }); |
| | } |
| |
|
| | const hasSplitCacheCreation = |
| | cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| |
|
| | const shouldShowLegacyCacheCreation = |
| | !hasSplitCacheCreation && cacheCreationTokens !== 0; |
| |
|
| | const shouldShowCache = cacheTokens !== 0; |
| | const shouldShowCacheCreation5m = |
| | hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| | const shouldShowCacheCreation1h = |
| | hasSplitCacheCreation && cacheCreationTokens1h > 0; |
| |
|
| | const parts = []; |
| | |
| | parts.push(i18next.t('模型: {{ratio}}')); |
| |
|
| | |
| | if (shouldShowCache) { |
| | parts.push(i18next.t('缓存: {{cacheRatio}}')); |
| | } |
| |
|
| | if (hasSplitCacheCreation) { |
| | if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) { |
| | parts.push( |
| | i18next.t( |
| | '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}', |
| | ), |
| | ); |
| | } else if (shouldShowCacheCreation5m) { |
| | parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}')); |
| | } else if (shouldShowCacheCreation1h) { |
| | parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}')); |
| | } |
| | } else if (shouldShowLegacyCacheCreation) { |
| | parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}')); |
| | } |
| |
|
| | |
| | if (image) { |
| | parts.push(i18next.t('图片输入: {{imageRatio}}')); |
| | } |
| |
|
| | parts.push(`{{ratioType}}: {{groupRatio}}`); |
| |
|
| | let result = i18next.t(parts.join(' * '), { |
| | ratio: modelRatio, |
| | ratioType: ratioLabel, |
| | groupRatio: finalGroupRatio, |
| | cacheRatio: cacheRatio, |
| | cacheCreationRatio: cacheCreationRatio, |
| | cacheCreationRatio5m: cacheCreationRatio5m, |
| | cacheCreationRatio1h: cacheCreationRatio1h, |
| | imageRatio: imageRatio, |
| | }); |
| |
|
| | if (isSystemPromptOverride) { |
| | result += '\n\r' + i18next.t('系统提示覆盖'); |
| | } |
| |
|
| | return result; |
| | } |
| |
|
| | export function renderModelPrice( |
| | inputTokens, |
| | completionTokens, |
| | modelRatio, |
| | modelPrice = -1, |
| | completionRatio, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens = 0, |
| | cacheRatio = 1.0, |
| | image = false, |
| | imageRatio = 1.0, |
| | imageOutputTokens = 0, |
| | webSearch = false, |
| | webSearchCallCount = 0, |
| | webSearchPrice = 0, |
| | fileSearch = false, |
| | fileSearchCallCount = 0, |
| | fileSearchPrice = 0, |
| | audioInputSeperatePrice = false, |
| | audioInputTokens = 0, |
| | audioInputPrice = 0, |
| | imageGenerationCall = false, |
| | imageGenerationCallPrice = 0, |
| | ) { |
| | const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| | groupRatio, |
| | user_group_ratio, |
| | ); |
| | groupRatio = effectiveGroupRatio; |
| |
|
| | |
| | const { symbol, rate } = getCurrencyConfig(); |
| |
|
| | if (modelPrice !== -1) { |
| | const displayPrice = (modelPrice * rate).toFixed(6); |
| | const displayTotal = (modelPrice * groupRatio * rate).toFixed(6); |
| | return i18next.t( |
| | '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| | { |
| | symbol: symbol, |
| | price: displayPrice, |
| | ratio: groupRatio, |
| | total: displayTotal, |
| | ratioType: ratioLabel, |
| | }, |
| | ); |
| | } else { |
| | if (completionRatio === undefined) { |
| | completionRatio = 0; |
| | } |
| | let inputRatioPrice = modelRatio * 2.0; |
| | let completionRatioPrice = modelRatio * 2.0 * completionRatio; |
| | let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; |
| | let imageRatioPrice = modelRatio * 2.0 * imageRatio; |
| |
|
| | |
| | let effectiveInputTokens = |
| | inputTokens - cacheTokens + cacheTokens * cacheRatio; |
| | |
| | if (image && imageOutputTokens > 0) { |
| | effectiveInputTokens = |
| | inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; |
| | } |
| | if (audioInputTokens > 0) { |
| | effectiveInputTokens -= audioInputTokens; |
| | } |
| | let price = |
| | (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| | (audioInputTokens / 1000000) * audioInputPrice * groupRatio + |
| | (completionTokens / 1000000) * completionRatioPrice * groupRatio + |
| | (webSearchCallCount / 1000) * webSearchPrice * groupRatio + |
| | (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + |
| | imageGenerationCallPrice * groupRatio; |
| |
|
| | return ( |
| | <> |
| | <article> |
| | <p> |
| | {i18next.t( |
| | '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | audioPrice: audioInputSeperatePrice |
| | ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` |
| | : '', |
| | }, |
| | )} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: (completionRatioPrice * rate).toFixed(6), |
| | completionRatio: completionRatio, |
| | }, |
| | )} |
| | </p> |
| | {cacheTokens > 0 && ( |
| | <p> |
| | {i18next.t( |
| | '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: (inputRatioPrice * cacheRatio * rate).toFixed(6), |
| | cacheRatio: cacheRatio, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {image && imageOutputTokens > 0 && ( |
| | <p> |
| | {i18next.t( |
| | '图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (imageRatioPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | total: (imageRatioPrice * groupRatio * rate).toFixed(6), |
| | imageRatio: imageRatio, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {webSearch && webSearchCallCount > 0 && ( |
| | <p> |
| | {i18next.t('Web搜索价格:{{symbol}}{{price}} / 1K 次', { |
| | symbol: symbol, |
| | price: (webSearchPrice * rate).toFixed(6), |
| | })} |
| | </p> |
| | )} |
| | {fileSearch && fileSearchCallCount > 0 && ( |
| | <p> |
| | {i18next.t('文件搜索价格:{{symbol}}{{price}} / 1K 次', { |
| | symbol: symbol, |
| | price: (fileSearchPrice * rate).toFixed(6), |
| | })} |
| | </p> |
| | )} |
| | {imageGenerationCall && imageGenerationCallPrice > 0 && ( |
| | <p> |
| | {i18next.t('图片生成调用:{{symbol}}{{price}} / 1次', { |
| | symbol: symbol, |
| | price: (imageGenerationCallPrice * rate).toFixed(6), |
| | })} |
| | </p> |
| | )} |
| | <p> |
| | {(() => { |
| | // 构建输入部分描述 |
| | let inputDesc = ''; |
| | if (image && imageOutputTokens > 0) { |
| | inputDesc = i18next.t( |
| | '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}', |
| | { |
| | nonImageInput: inputTokens - imageOutputTokens, |
| | imageInput: imageOutputTokens, |
| | imageRatio: imageRatio, |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | }, |
| | ); |
| | } else if (cacheTokens > 0) { |
| | inputDesc = i18next.t( |
| | '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}', |
| | { |
| | nonCacheInput: inputTokens - cacheTokens, |
| | cacheInput: cacheTokens, |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | cachePrice: (cacheRatioPrice * rate).toFixed(6), |
| | }, |
| | ); |
| | } else if (audioInputSeperatePrice && audioInputTokens > 0) { |
| | inputDesc = i18next.t( |
| | '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}', |
| | { |
| | nonAudioInput: inputTokens - audioInputTokens, |
| | audioInput: audioInputTokens, |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | audioPrice: (audioInputPrice * rate).toFixed(6), |
| | }, |
| | ); |
| | } else { |
| | inputDesc = i18next.t( |
| | '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', |
| | { |
| | input: inputTokens, |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | }, |
| | ); |
| | } |
| | |
| | // 构建输出部分描述 |
| | const outputDesc = i18next.t( |
| | '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', |
| | { |
| | completion: completionTokens, |
| | symbol: symbol, |
| | compPrice: (completionRatioPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | ratioType: ratioLabel, |
| | }, |
| | ); |
| | |
| | // 构建额外服务描述 |
| | const extraServices = [ |
| | webSearch && webSearchCallCount > 0 |
| | ? i18next.t( |
| | ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', |
| | { |
| | count: webSearchCallCount, |
| | symbol: symbol, |
| | price: (webSearchPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | ratioType: ratioLabel, |
| | }, |
| | ) |
| | : '', |
| | fileSearch && fileSearchCallCount > 0 |
| | ? i18next.t( |
| | ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', |
| | { |
| | count: fileSearchCallCount, |
| | symbol: symbol, |
| | price: (fileSearchPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | ratioType: ratioLabel, |
| | }, |
| | ) |
| | : '', |
| | imageGenerationCall && imageGenerationCallPrice > 0 |
| | ? i18next.t( |
| | ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}', |
| | { |
| | symbol: symbol, |
| | price: (imageGenerationCallPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | ratioType: ratioLabel, |
| | }, |
| | ) |
| | : '', |
| | ].join(''); |
| | |
| | return i18next.t( |
| | '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}', |
| | { |
| | inputDesc, |
| | outputDesc, |
| | extraServices, |
| | symbol: symbol, |
| | total: (price * rate).toFixed(6), |
| | }, |
| | ); |
| | })()} |
| | </p> |
| | <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| | </article> |
| | </> |
| | ); |
| | } |
| | } |
| |
|
| | export function renderLogContent( |
| | modelRatio, |
| | completionRatio, |
| | modelPrice = -1, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheRatio = 1.0, |
| | image = false, |
| | imageRatio = 1.0, |
| | webSearch = false, |
| | webSearchCallCount = 0, |
| | fileSearch = false, |
| | fileSearchCallCount = 0, |
| | ) { |
| | const { |
| | ratio, |
| | label: ratioLabel, |
| | useUserGroupRatio: useUserGroupRatio, |
| | } = getEffectiveRatio(groupRatio, user_group_ratio); |
| |
|
| | |
| | const { symbol, rate } = getCurrencyConfig(); |
| |
|
| | if (modelPrice !== -1) { |
| | return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { |
| | symbol: symbol, |
| | price: (modelPrice * rate).toFixed(6), |
| | ratioType: ratioLabel, |
| | ratio, |
| | }); |
| | } else { |
| | if (image) { |
| | return i18next.t( |
| | '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', |
| | { |
| | modelRatio: modelRatio, |
| | cacheRatio: cacheRatio, |
| | completionRatio: completionRatio, |
| | imageRatio: imageRatio, |
| | ratioType: ratioLabel, |
| | ratio, |
| | }, |
| | ); |
| | } else if (webSearch) { |
| | return i18next.t( |
| | '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次', |
| | { |
| | modelRatio: modelRatio, |
| | cacheRatio: cacheRatio, |
| | completionRatio: completionRatio, |
| | ratioType: ratioLabel, |
| | ratio, |
| | webSearchCallCount, |
| | }, |
| | ); |
| | } else { |
| | return i18next.t( |
| | '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', |
| | { |
| | modelRatio: modelRatio, |
| | cacheRatio: cacheRatio, |
| | completionRatio: completionRatio, |
| | ratioType: ratioLabel, |
| | ratio, |
| | }, |
| | ); |
| | } |
| | } |
| | } |
| |
|
| | export function renderModelPriceSimple( |
| | modelRatio, |
| | modelPrice = -1, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens = 0, |
| | cacheRatio = 1.0, |
| | cacheCreationTokens = 0, |
| | cacheCreationRatio = 1.0, |
| | cacheCreationTokens5m = 0, |
| | cacheCreationRatio5m = 1.0, |
| | cacheCreationTokens1h = 0, |
| | cacheCreationRatio1h = 1.0, |
| | image = false, |
| | imageRatio = 1.0, |
| | isSystemPromptOverride = false, |
| | provider = 'openai', |
| | ) { |
| | return renderPriceSimpleCore({ |
| | modelRatio, |
| | modelPrice, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens, |
| | cacheRatio, |
| | cacheCreationTokens, |
| | cacheCreationRatio, |
| | cacheCreationTokens5m, |
| | cacheCreationRatio5m, |
| | cacheCreationTokens1h, |
| | cacheCreationRatio1h, |
| | image, |
| | imageRatio, |
| | isSystemPromptOverride, |
| | }); |
| | } |
| |
|
| | export function renderAudioModelPrice( |
| | inputTokens, |
| | completionTokens, |
| | modelRatio, |
| | modelPrice = -1, |
| | completionRatio, |
| | audioInputTokens, |
| | audioCompletionTokens, |
| | audioRatio, |
| | audioCompletionRatio, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens = 0, |
| | cacheRatio = 1.0, |
| | ) { |
| | const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| | groupRatio, |
| | user_group_ratio, |
| | ); |
| | groupRatio = effectiveGroupRatio; |
| |
|
| | |
| | const { symbol, rate } = getCurrencyConfig(); |
| |
|
| | |
| | if (modelPrice !== -1) { |
| | return i18next.t( |
| | '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| | { |
| | symbol: symbol, |
| | price: (modelPrice * rate).toFixed(6), |
| | ratio: groupRatio, |
| | total: (modelPrice * groupRatio * rate).toFixed(6), |
| | ratioType: ratioLabel, |
| | }, |
| | ); |
| | } else { |
| | if (completionRatio === undefined) { |
| | completionRatio = 0; |
| | } |
| |
|
| | |
| | audioRatio = parseFloat(audioRatio).toFixed(6); |
| | |
| | let inputRatioPrice = modelRatio * 2.0; |
| | let completionRatioPrice = modelRatio * 2.0 * completionRatio; |
| | let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; |
| |
|
| | |
| | const effectiveInputTokens = |
| | inputTokens - cacheTokens + cacheTokens * cacheRatio; |
| |
|
| | let textPrice = |
| | (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| | (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| | let audioPrice = |
| | (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + |
| | (audioCompletionTokens / 1000000) * |
| | inputRatioPrice * |
| | audioRatio * |
| | audioCompletionRatio * |
| | groupRatio; |
| | let price = textPrice + audioPrice; |
| | return ( |
| | <> |
| | <article> |
| | <p> |
| | {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | })} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: (completionRatioPrice * rate).toFixed(6), |
| | completionRatio: completionRatio, |
| | }, |
| | )} |
| | </p> |
| | {cacheTokens > 0 && ( |
| | <p> |
| | {i18next.t( |
| | '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: (inputRatioPrice * cacheRatio * rate).toFixed(6), |
| | cacheRatio: cacheRatio, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | <p> |
| | {i18next.t( |
| | '音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: (inputRatioPrice * audioRatio * rate).toFixed(6), |
| | audioRatio: audioRatio, |
| | }, |
| | )} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | total: ( |
| | inputRatioPrice * |
| | audioRatio * |
| | audioCompletionRatio * |
| | rate |
| | ).toFixed(6), |
| | audioRatio: audioRatio, |
| | audioCompRatio: audioCompletionRatio, |
| | }, |
| | )} |
| | </p> |
| | <p> |
| | {cacheTokens > 0 |
| | ? i18next.t( |
| | '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', |
| | { |
| | nonCacheInput: inputTokens - cacheTokens, |
| | cacheInput: cacheTokens, |
| | symbol: symbol, |
| | cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed( |
| | 6, |
| | ), |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | completion: completionTokens, |
| | compPrice: (completionRatioPrice * rate).toFixed(6), |
| | total: (textPrice * rate).toFixed(6), |
| | }, |
| | ) |
| | : i18next.t( |
| | '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', |
| | { |
| | input: inputTokens, |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | completion: completionTokens, |
| | compPrice: (completionRatioPrice * rate).toFixed(6), |
| | total: (textPrice * rate).toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}', |
| | { |
| | input: audioInputTokens, |
| | completion: audioCompletionTokens, |
| | symbol: symbol, |
| | audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed( |
| | 6, |
| | ), |
| | audioCompPrice: ( |
| | audioRatio * |
| | audioCompletionRatio * |
| | inputRatioPrice * |
| | rate |
| | ).toFixed(6), |
| | total: (audioPrice * rate).toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}', |
| | { |
| | symbol: symbol, |
| | total: (price * rate).toFixed(6), |
| | textPrice: (textPrice * rate).toFixed(6), |
| | audioPrice: (audioPrice * rate).toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| | </article> |
| | </> |
| | ); |
| | } |
| | } |
| |
|
| | export function renderQuotaWithPrompt(quota, digits) { |
| | const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| | if (quotaDisplayType !== 'TOKENS') { |
| | return i18next.t('等价金额:') + renderQuota(quota, digits); |
| | } |
| | return ''; |
| | } |
| |
|
| | export function renderClaudeModelPrice( |
| | inputTokens, |
| | completionTokens, |
| | modelRatio, |
| | modelPrice = -1, |
| | completionRatio, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheTokens = 0, |
| | cacheRatio = 1.0, |
| | cacheCreationTokens = 0, |
| | cacheCreationRatio = 1.0, |
| | cacheCreationTokens5m = 0, |
| | cacheCreationRatio5m = 1.0, |
| | cacheCreationTokens1h = 0, |
| | cacheCreationRatio1h = 1.0, |
| | ) { |
| | const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| | groupRatio, |
| | user_group_ratio, |
| | ); |
| | groupRatio = effectiveGroupRatio; |
| |
|
| | |
| | const { symbol, rate } = getCurrencyConfig(); |
| |
|
| | if (modelPrice !== -1) { |
| | return i18next.t( |
| | '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| | { |
| | symbol: symbol, |
| | price: (modelPrice * rate).toFixed(6), |
| | ratioType: ratioLabel, |
| | ratio: groupRatio, |
| | total: (modelPrice * groupRatio * rate).toFixed(6), |
| | }, |
| | ); |
| | } else { |
| | if (completionRatio === undefined) { |
| | completionRatio = 0; |
| | } |
| |
|
| | const completionRatioValue = completionRatio || 0; |
| | const inputRatioPrice = modelRatio * 2.0; |
| | const completionRatioPrice = modelRatio * 2.0 * completionRatioValue; |
| | const cacheRatioPrice = modelRatio * 2.0 * cacheRatio; |
| | const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio; |
| | const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m; |
| | const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h; |
| |
|
| | const hasSplitCacheCreation = |
| | cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| |
|
| | const shouldShowCache = cacheTokens > 0; |
| | const shouldShowLegacyCacheCreation = |
| | !hasSplitCacheCreation && cacheCreationTokens > 0; |
| | const shouldShowCacheCreation5m = |
| | hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| | const shouldShowCacheCreation1h = |
| | hasSplitCacheCreation && cacheCreationTokens1h > 0; |
| |
|
| | |
| | const nonCachedTokens = inputTokens; |
| | const legacyCacheCreationTokens = hasSplitCacheCreation |
| | ? 0 |
| | : cacheCreationTokens; |
| | const effectiveInputTokens = |
| | nonCachedTokens + |
| | cacheTokens * cacheRatio + |
| | legacyCacheCreationTokens * cacheCreationRatio + |
| | cacheCreationTokens5m * cacheCreationRatio5m + |
| | cacheCreationTokens1h * cacheCreationRatio1h; |
| |
|
| | let price = |
| | (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| | (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| |
|
| | const inputUnitPrice = inputRatioPrice * rate; |
| | const completionUnitPrice = completionRatioPrice * rate; |
| | const cacheUnitPrice = cacheRatioPrice * rate; |
| | const cacheCreationUnitPrice = cacheCreationRatioPrice * rate; |
| | const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate; |
| | const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate; |
| | const cacheCreationUnitPriceTotal = |
| | cacheCreationUnitPrice5m + cacheCreationUnitPrice1h; |
| |
|
| | const breakdownSegments = [ |
| | i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| | input: inputTokens, |
| | symbol, |
| | price: inputUnitPrice.toFixed(6), |
| | }), |
| | ]; |
| |
|
| | if (shouldShowCache) { |
| | breakdownSegments.push( |
| | i18next.t( |
| | '缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})', |
| | { |
| | tokens: cacheTokens, |
| | symbol, |
| | price: cacheUnitPrice.toFixed(6), |
| | ratio: cacheRatio, |
| | }, |
| | ), |
| | ); |
| | } |
| |
|
| | if (shouldShowLegacyCacheCreation) { |
| | breakdownSegments.push( |
| | i18next.t( |
| | '缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})', |
| | { |
| | tokens: cacheCreationTokens, |
| | symbol, |
| | price: cacheCreationUnitPrice.toFixed(6), |
| | ratio: cacheCreationRatio, |
| | }, |
| | ), |
| | ); |
| | } |
| |
|
| | if (shouldShowCacheCreation5m) { |
| | breakdownSegments.push( |
| | i18next.t( |
| | '5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})', |
| | { |
| | tokens: cacheCreationTokens5m, |
| | symbol, |
| | price: cacheCreationUnitPrice5m.toFixed(6), |
| | ratio: cacheCreationRatio5m, |
| | }, |
| | ), |
| | ); |
| | } |
| |
|
| | if (shouldShowCacheCreation1h) { |
| | breakdownSegments.push( |
| | i18next.t( |
| | '1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})', |
| | { |
| | tokens: cacheCreationTokens1h, |
| | symbol, |
| | price: cacheCreationUnitPrice1h.toFixed(6), |
| | ratio: cacheCreationRatio1h, |
| | }, |
| | ), |
| | ); |
| | } |
| |
|
| | breakdownSegments.push( |
| | i18next.t( |
| | '补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}', |
| | { |
| | completion: completionTokens, |
| | symbol, |
| | price: completionUnitPrice.toFixed(6), |
| | }, |
| | ), |
| | ); |
| |
|
| | const breakdownText = breakdownSegments.join(' + '); |
| |
|
| | return ( |
| | <> |
| | <article> |
| | <p> |
| | {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | })} |
| | </p> |
| | <p> |
| | {i18next.t( |
| | '补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | ratio: completionRatio, |
| | total: (completionRatioPrice * rate).toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | {shouldShowCache && ( |
| | <p> |
| | {i18next.t( |
| | '缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | ratio: cacheRatio, |
| | total: cacheUnitPrice.toFixed(6), |
| | cacheRatio: cacheRatio, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {shouldShowLegacyCacheCreation && ( |
| | <p> |
| | {i18next.t( |
| | '缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | ratio: cacheCreationRatio, |
| | total: cacheCreationUnitPrice.toFixed(6), |
| | cacheCreationRatio: cacheCreationRatio, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {shouldShowCacheCreation5m && ( |
| | <p> |
| | {i18next.t( |
| | '5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | ratio: cacheCreationRatio5m, |
| | total: cacheCreationUnitPrice5m.toFixed(6), |
| | cacheCreationRatio5m: cacheCreationRatio5m, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {shouldShowCacheCreation1h && ( |
| | <p> |
| | {i18next.t( |
| | '1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})', |
| | { |
| | symbol: symbol, |
| | price: (inputRatioPrice * rate).toFixed(6), |
| | ratio: cacheCreationRatio1h, |
| | total: cacheCreationUnitPrice1h.toFixed(6), |
| | cacheCreationRatio1h: cacheCreationRatio1h, |
| | }, |
| | )} |
| | </p> |
| | )} |
| | {shouldShowCacheCreation5m && shouldShowCacheCreation1h && ( |
| | <p> |
| | {i18next.t( |
| | '缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens', |
| | { |
| | symbol: symbol, |
| | five: cacheCreationUnitPrice5m.toFixed(6), |
| | one: cacheCreationUnitPrice1h.toFixed(6), |
| | total: cacheCreationUnitPriceTotal.toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | )} |
| | <p></p> |
| | <p> |
| | {i18next.t( |
| | '{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| | { |
| | breakdown: breakdownText, |
| | ratioType: ratioLabel, |
| | ratio: groupRatio, |
| | symbol: symbol, |
| | total: (price * rate).toFixed(6), |
| | }, |
| | )} |
| | </p> |
| | <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| | </article> |
| | </> |
| | ); |
| | } |
| | } |
| |
|
| | export function renderClaudeLogContent( |
| | modelRatio, |
| | completionRatio, |
| | modelPrice = -1, |
| | groupRatio, |
| | user_group_ratio, |
| | cacheRatio = 1.0, |
| | cacheCreationRatio = 1.0, |
| | cacheCreationTokens5m = 0, |
| | cacheCreationRatio5m = 1.0, |
| | cacheCreationTokens1h = 0, |
| | cacheCreationRatio1h = 1.0, |
| | ) { |
| | const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| | groupRatio, |
| | user_group_ratio, |
| | ); |
| | groupRatio = effectiveGroupRatio; |
| |
|
| | |
| | const { symbol, rate } = getCurrencyConfig(); |
| |
|
| | if (modelPrice !== -1) { |
| | return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { |
| | symbol: symbol, |
| | price: (modelPrice * rate).toFixed(6), |
| | ratioType: ratioLabel, |
| | ratio: groupRatio, |
| | }); |
| | } else { |
| | const hasSplitCacheCreation = |
| | cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| | const shouldShowCacheCreation5m = |
| | hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| | const shouldShowCacheCreation1h = |
| | hasSplitCacheCreation && cacheCreationTokens1h > 0; |
| |
|
| | let cacheCreationPart = null; |
| | if (hasSplitCacheCreation) { |
| | if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) { |
| | cacheCreationPart = i18next.t( |
| | '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}', |
| | { |
| | cacheCreationRatio5m, |
| | cacheCreationRatio1h, |
| | }, |
| | ); |
| | } else if (shouldShowCacheCreation5m) { |
| | cacheCreationPart = i18next.t( |
| | '缓存创建倍率 5m {{cacheCreationRatio5m}}', |
| | { |
| | cacheCreationRatio5m, |
| | }, |
| | ); |
| | } else if (shouldShowCacheCreation1h) { |
| | cacheCreationPart = i18next.t( |
| | '缓存创建倍率 1h {{cacheCreationRatio1h}}', |
| | { |
| | cacheCreationRatio1h, |
| | }, |
| | ); |
| | } |
| | } |
| |
|
| | if (!cacheCreationPart) { |
| | cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', { |
| | cacheCreationRatio, |
| | }); |
| | } |
| |
|
| | const parts = [ |
| | i18next.t('模型倍率 {{modelRatio}}', { modelRatio }), |
| | i18next.t('输出倍率 {{completionRatio}}', { completionRatio }), |
| | i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }), |
| | cacheCreationPart, |
| | i18next.t('{{ratioType}} {{ratio}}', { |
| | ratioType: ratioLabel, |
| | ratio: groupRatio, |
| | }), |
| | ]; |
| |
|
| | return parts.join(','); |
| | } |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | export function rehypeSplitWordsIntoSpans(options = {}) { |
| | const { previousContentLength = 0 } = options; |
| |
|
| | return (tree) => { |
| | let currentCharCount = 0; |
| |
|
| | visit(tree, 'element', (node) => { |
| | if ( |
| | ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes( |
| | node.tagName, |
| | ) && |
| | node.children |
| | ) { |
| | const newChildren = []; |
| | node.children.forEach((child) => { |
| | if (child.type === 'text') { |
| | try { |
| | |
| | const segmenter = new Intl.Segmenter('zh', { |
| | granularity: 'word', |
| | }); |
| | const segments = segmenter.segment(child.value); |
| |
|
| | Array.from(segments) |
| | .map((seg) => seg.segment) |
| | .filter(Boolean) |
| | .forEach((word) => { |
| | const wordStartPos = currentCharCount; |
| | const wordEndPos = currentCharCount + word.length; |
| |
|
| | |
| | const isNewContent = wordStartPos >= previousContentLength; |
| |
|
| | newChildren.push({ |
| | type: 'element', |
| | tagName: 'span', |
| | properties: { |
| | className: isNewContent ? ['animate-fade-in'] : [], |
| | }, |
| | children: [{ type: 'text', value: word }], |
| | }); |
| |
|
| | currentCharCount = wordEndPos; |
| | }); |
| | } catch (_) { |
| | |
| | const textStartPos = currentCharCount; |
| | const isNewContent = textStartPos >= previousContentLength; |
| |
|
| | if (isNewContent) { |
| | |
| | newChildren.push({ |
| | type: 'element', |
| | tagName: 'span', |
| | properties: { |
| | className: ['animate-fade-in'], |
| | }, |
| | children: [{ type: 'text', value: child.value }], |
| | }); |
| | } else { |
| | |
| | newChildren.push(child); |
| | } |
| |
|
| | currentCharCount += child.value.length; |
| | } |
| | } else { |
| | newChildren.push(child); |
| | } |
| | }); |
| | node.children = newChildren; |
| | } |
| | }); |
| | }; |
| | } |
| |
|