|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
}); |
|
|
}; |
|
|
} |
|
|
|