| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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,
|
| Server,
|
| } 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 'deployment':
|
| return <Server {...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:
|
| case 57:
|
| 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];
|
| }
|
|
|
|
|
| const groupColors = [
|
| 'red',
|
| 'orange',
|
| 'yellow',
|
| 'lime',
|
| 'green',
|
| 'cyan',
|
| 'blue',
|
| 'indigo',
|
| 'violet',
|
| 'purple',
|
| 'pink',
|
| 'amber',
|
| 'grey',
|
| ];
|
|
|
| export function groupToColor(str) {
|
|
|
| let hash = 0;
|
| for (let i = 0; i < str.length; i++) {
|
| hash = (hash << 5) - hash + str.charCodeAt(i);
|
| hash = hash & hash;
|
| }
|
| hash = Math.abs(hash);
|
| return groupColors[hash % groupColors.length];
|
| }
|
|
|
|
|
| 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] || groupToColor(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;
|
|
|
| const { symbol, rate } = getCurrencyConfig();
|
| if (modelPrice !== -1) {
|
| const displayPrice = (modelPrice * rate).toFixed(6);
|
| return i18next.t('价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}', {
|
| symbol: symbol,
|
| price: displayPrice,
|
| 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;
|
| }
|
| });
|
| };
|
| }
|
|
|