2024-12-13 19:03:14 +08:00
|
|
|
|
import i18next from 'i18next';
|
2024-12-30 19:51:00 +08:00
|
|
|
|
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
|
2025-06-04 00:01:41 +08:00
|
|
|
|
import { copy, isMobile, showSuccess } from './utils';
|
2025-06-03 23:56:39 +08:00
|
|
|
|
import { visit } from 'unist-util-visit';
|
2025-06-04 14:31:54 +08:00
|
|
|
|
import {
|
|
|
|
|
|
OpenAI,
|
|
|
|
|
|
Claude,
|
|
|
|
|
|
Gemini,
|
|
|
|
|
|
Moonshot,
|
|
|
|
|
|
Zhipu,
|
|
|
|
|
|
Qwen,
|
|
|
|
|
|
DeepSeek,
|
|
|
|
|
|
Minimax,
|
|
|
|
|
|
Wenxin,
|
|
|
|
|
|
Spark,
|
|
|
|
|
|
Midjourney,
|
|
|
|
|
|
Hunyuan,
|
|
|
|
|
|
Cohere,
|
|
|
|
|
|
Cloudflare,
|
|
|
|
|
|
Ai360,
|
|
|
|
|
|
Yi,
|
|
|
|
|
|
Jina,
|
|
|
|
|
|
Mistral,
|
|
|
|
|
|
XAI,
|
|
|
|
|
|
Ollama,
|
|
|
|
|
|
Doubao,
|
✨ feat: Add lucide-react icons to all table Tag components
- Add semantic icons to ChannelsTable.js for channel status, response time, and quota display
- Add status and quota icons to TokensTable.js for better visual distinction
- Add status and quota icons to RedemptionsTable.js for redemption code management
- Add role, status, and statistics icons to UsersTable.js for user management
- Import appropriate lucide-react icons for each table component
- Enhance UI consistency and user experience across all table interfaces
Icons added include:
- Status indicators: CheckCircle, XCircle, AlertCircle, HelpCircle
- Performance metrics: Zap, Timer, Clock, AlertTriangle, TestTube
- Financial data: Coins, DollarSign
- User roles: User, Shield, Crown
- Activity tracking: Activity, Users, UserPlus
This improves visual clarity and makes table data more intuitive to understand.
2025-06-08 23:13:45 +08:00
|
|
|
|
Suno,
|
|
|
|
|
|
Xinference,
|
|
|
|
|
|
OpenRouter,
|
|
|
|
|
|
Dify,
|
|
|
|
|
|
Coze,
|
|
|
|
|
|
SiliconCloud,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
FastGPT,
|
2025-06-04 14:31:54 +08:00
|
|
|
|
} from '@lobehub/icons';
|
|
|
|
|
|
|
2025-06-06 20:55:52 +08:00
|
|
|
|
import {
|
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
|
TerminalSquare,
|
|
|
|
|
|
MessageSquare,
|
|
|
|
|
|
Key,
|
|
|
|
|
|
BarChart3,
|
|
|
|
|
|
Image as ImageIcon,
|
|
|
|
|
|
CheckSquare,
|
|
|
|
|
|
CreditCard,
|
|
|
|
|
|
Layers,
|
|
|
|
|
|
Gift,
|
|
|
|
|
|
User,
|
|
|
|
|
|
Settings,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
CircleUser,
|
2025-06-06 20:55:52 +08:00
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
|
|
// 侧边栏图标颜色映射
|
|
|
|
|
|
export const sidebarIconColors = {
|
2025-06-07 12:32:20 +08:00
|
|
|
|
dashboard: '#4F46E5', // 紫蓝色
|
|
|
|
|
|
terminal: '#10B981', // 绿色
|
|
|
|
|
|
message: '#06B6D4', // 青色
|
|
|
|
|
|
key: '#3B82F6', // 蓝色
|
|
|
|
|
|
chart: '#8B5CF6', // 紫色
|
|
|
|
|
|
image: '#EC4899', // 粉色
|
|
|
|
|
|
check: '#F59E0B', // 琥珀色
|
|
|
|
|
|
credit: '#F97316', // 橙色
|
|
|
|
|
|
layers: '#EF4444', // 红色
|
|
|
|
|
|
gift: '#F43F5E', // 玫红色
|
|
|
|
|
|
user: '#6366F1', // 靛蓝色
|
|
|
|
|
|
settings: '#6B7280', // 灰色
|
2025-06-06 20:55:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取侧边栏Lucide图标组件
|
|
|
|
|
|
export function getLucideIcon(key, selected = false) {
|
|
|
|
|
|
const size = 16;
|
|
|
|
|
|
const strokeWidth = 2;
|
|
|
|
|
|
const commonProps = {
|
|
|
|
|
|
size,
|
|
|
|
|
|
strokeWidth,
|
|
|
|
|
|
className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据不同的key返回不同的图标
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
|
case 'detail':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<LayoutDashboard
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.dashboard : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'playground':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<TerminalSquare
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.terminal : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'chat':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<MessageSquare
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.message : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'token':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Key
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.key : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'log':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<BarChart3
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.chart : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'midjourney':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<ImageIcon
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.image : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'task':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<CheckSquare
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.check : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'topup':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<CreditCard
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.credit : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'channel':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Layers
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.layers : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'redemption':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Gift
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.gift : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'user':
|
|
|
|
|
|
case 'personal':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<User
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.user : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
case 'setting':
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Settings
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.settings : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
default:
|
2025-06-07 12:32:20 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<CircleUser
|
|
|
|
|
|
{...commonProps}
|
|
|
|
|
|
color={selected ? sidebarIconColors.user : 'currentColor'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2025-06-06 20:55:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 14:31:54 +08:00
|
|
|
|
// 获取模型分类
|
|
|
|
|
|
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,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: () => true,
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
openai: {
|
|
|
|
|
|
label: 'OpenAI',
|
|
|
|
|
|
icon: <OpenAI />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) =>
|
|
|
|
|
|
model.model_name.toLowerCase().includes('gpt') ||
|
2025-06-04 14:31:54 +08:00
|
|
|
|
model.model_name.toLowerCase().includes('dall-e') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('whisper') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('tts') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('text-') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('babbage') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('davinci') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('curie') ||
|
2025-06-07 02:57:16 +08:00
|
|
|
|
model.model_name.toLowerCase().includes('ada') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('o1') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('o3') ||
|
2025-06-07 12:32:20 +08:00
|
|
|
|
model.model_name.toLowerCase().includes('o4'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
anthropic: {
|
|
|
|
|
|
label: 'Anthropic',
|
|
|
|
|
|
icon: <Claude.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('claude'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
gemini: {
|
|
|
|
|
|
label: 'Gemini',
|
|
|
|
|
|
icon: <Gemini.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('gemini'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
moonshot: {
|
|
|
|
|
|
label: 'Moonshot',
|
|
|
|
|
|
icon: <Moonshot />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('moonshot'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
zhipu: {
|
|
|
|
|
|
label: t('智谱'),
|
|
|
|
|
|
icon: <Zhipu.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) =>
|
|
|
|
|
|
model.model_name.toLowerCase().includes('chatglm') ||
|
|
|
|
|
|
model.model_name.toLowerCase().includes('glm-'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
qwen: {
|
|
|
|
|
|
label: t('通义千问'),
|
|
|
|
|
|
icon: <Qwen.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('qwen'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
deepseek: {
|
|
|
|
|
|
label: 'DeepSeek',
|
|
|
|
|
|
icon: <DeepSeek.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
minimax: {
|
|
|
|
|
|
label: 'MiniMax',
|
|
|
|
|
|
icon: <Minimax.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('abab'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
baidu: {
|
|
|
|
|
|
label: t('文心一言'),
|
|
|
|
|
|
icon: <Wenxin.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('ernie'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
xunfei: {
|
|
|
|
|
|
label: t('讯飞星火'),
|
|
|
|
|
|
icon: <Spark.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('spark'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
midjourney: {
|
|
|
|
|
|
label: 'Midjourney',
|
|
|
|
|
|
icon: <Midjourney />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('mj_'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
tencent: {
|
|
|
|
|
|
label: t('腾讯混元'),
|
|
|
|
|
|
icon: <Hunyuan.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
cohere: {
|
|
|
|
|
|
label: 'Cohere',
|
|
|
|
|
|
icon: <Cohere.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('command'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
cloudflare: {
|
|
|
|
|
|
label: 'Cloudflare',
|
|
|
|
|
|
icon: <Cloudflare.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
ai360: {
|
|
|
|
|
|
label: t('360智脑'),
|
|
|
|
|
|
icon: <Ai360.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('360'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
yi: {
|
|
|
|
|
|
label: t('零一万物'),
|
|
|
|
|
|
icon: <Yi.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('yi'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
jina: {
|
|
|
|
|
|
label: 'Jina',
|
|
|
|
|
|
icon: <Jina />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('jina'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
mistral: {
|
|
|
|
|
|
label: 'Mistral AI',
|
|
|
|
|
|
icon: <Mistral.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('mistral'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
xai: {
|
|
|
|
|
|
label: 'xAI',
|
|
|
|
|
|
icon: <XAI />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('grok'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
llama: {
|
|
|
|
|
|
label: 'Llama',
|
|
|
|
|
|
icon: <Ollama />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('llama'),
|
2025-06-04 14:31:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
doubao: {
|
|
|
|
|
|
label: t('豆包'),
|
|
|
|
|
|
icon: <Doubao.Color />,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
filter: (model) => model.model_name.toLowerCase().includes('doubao'),
|
|
|
|
|
|
},
|
2025-06-04 14:31:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
lastLocale = currentLocale;
|
|
|
|
|
|
return categoriesCache;
|
|
|
|
|
|
};
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
✨ feat: Add lucide-react icons to all table Tag components
- Add semantic icons to ChannelsTable.js for channel status, response time, and quota display
- Add status and quota icons to TokensTable.js for better visual distinction
- Add status and quota icons to RedemptionsTable.js for redemption code management
- Add role, status, and statistics icons to UsersTable.js for user management
- Import appropriate lucide-react icons for each table component
- Enhance UI consistency and user experience across all table interfaces
Icons added include:
- Status indicators: CheckCircle, XCircle, AlertCircle, HelpCircle
- Performance metrics: Zap, Timer, Clock, AlertTriangle, TestTube
- Financial data: Coins, DollarSign
- User roles: User, Shield, Crown
- Activity tracking: Activity, Users, UserPlus
This improves visual clarity and makes table data more intuitive to understand.
2025-06-08 23:13:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据渠道类型返回对应的厂商图标
|
|
|
|
|
|
* @param {number} channelType - 渠道类型值
|
|
|
|
|
|
* @returns {JSX.Element|null} - 对应的厂商图标组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getChannelIcon(channelType) {
|
|
|
|
|
|
const iconSize = 14;
|
|
|
|
|
|
|
|
|
|
|
|
switch (channelType) {
|
|
|
|
|
|
case 1: // OpenAI
|
|
|
|
|
|
case 3: // Azure OpenAI
|
|
|
|
|
|
return <OpenAI size={iconSize} />;
|
|
|
|
|
|
case 2: // Midjourney Proxy
|
|
|
|
|
|
case 5: // Midjourney Proxy Plus
|
|
|
|
|
|
return <Midjourney size={iconSize} />;
|
|
|
|
|
|
case 36: // Suno API
|
|
|
|
|
|
return <Suno size={iconSize} />;
|
|
|
|
|
|
case 4: // Ollama
|
|
|
|
|
|
return <Ollama size={iconSize} />;
|
|
|
|
|
|
case 14: // Anthropic Claude
|
|
|
|
|
|
case 33: // AWS Claude
|
|
|
|
|
|
return <Claude.Color size={iconSize} />;
|
|
|
|
|
|
case 41: // Vertex AI
|
|
|
|
|
|
return <Gemini.Color size={iconSize} />;
|
|
|
|
|
|
case 34: // Cohere
|
|
|
|
|
|
return <Cohere.Color size={iconSize} />;
|
|
|
|
|
|
case 39: // Cloudflare
|
|
|
|
|
|
return <Cloudflare.Color size={iconSize} />;
|
|
|
|
|
|
case 43: // DeepSeek
|
|
|
|
|
|
return <DeepSeek.Color size={iconSize} />;
|
|
|
|
|
|
case 15: // 百度文心千帆
|
|
|
|
|
|
case 46: // 百度文心千帆V2
|
|
|
|
|
|
return <Wenxin.Color size={iconSize} />;
|
|
|
|
|
|
case 17: // 阿里通义千问
|
|
|
|
|
|
return <Qwen.Color size={iconSize} />;
|
|
|
|
|
|
case 18: // 讯飞星火认知
|
|
|
|
|
|
return <Spark.Color size={iconSize} />;
|
|
|
|
|
|
case 16: // 智谱 ChatGLM
|
|
|
|
|
|
case 26: // 智谱 GLM-4V
|
|
|
|
|
|
return <Zhipu.Color size={iconSize} />;
|
|
|
|
|
|
case 24: // Google Gemini
|
|
|
|
|
|
case 11: // Google PaLM2
|
|
|
|
|
|
return <Gemini.Color size={iconSize} />;
|
|
|
|
|
|
case 47: // Xinference
|
|
|
|
|
|
return <Xinference.Color size={iconSize} />;
|
|
|
|
|
|
case 25: // Moonshot
|
|
|
|
|
|
return <Moonshot size={iconSize} />;
|
|
|
|
|
|
case 20: // OpenRouter
|
|
|
|
|
|
return <OpenRouter size={iconSize} />;
|
|
|
|
|
|
case 19: // 360 智脑
|
|
|
|
|
|
return <Ai360.Color size={iconSize} />;
|
|
|
|
|
|
case 23: // 腾讯混元
|
|
|
|
|
|
return <Hunyuan.Color size={iconSize} />;
|
|
|
|
|
|
case 31: // 零一万物
|
|
|
|
|
|
return <Yi.Color size={iconSize} />;
|
|
|
|
|
|
case 35: // MiniMax
|
|
|
|
|
|
return <Minimax.Color size={iconSize} />;
|
|
|
|
|
|
case 37: // Dify
|
|
|
|
|
|
return <Dify.Color size={iconSize} />;
|
|
|
|
|
|
case 38: // Jina
|
|
|
|
|
|
return <Jina size={iconSize} />;
|
|
|
|
|
|
case 40: // SiliconCloud
|
|
|
|
|
|
return <SiliconCloud.Color size={iconSize} />;
|
|
|
|
|
|
case 42: // Mistral AI
|
|
|
|
|
|
return <Mistral.Color size={iconSize} />;
|
|
|
|
|
|
case 45: // 字节火山方舟、豆包通用
|
|
|
|
|
|
return <Doubao.Color size={iconSize} />;
|
|
|
|
|
|
case 48: // xAI
|
|
|
|
|
|
return <XAI size={iconSize} />;
|
|
|
|
|
|
case 49: // Coze
|
|
|
|
|
|
return <Coze size={iconSize} />;
|
|
|
|
|
|
case 8: // 自定义渠道
|
|
|
|
|
|
case 22: // 知识库:FastGPT
|
|
|
|
|
|
return <FastGPT.Color size={iconSize} />;
|
|
|
|
|
|
case 21: // 知识库:AI Proxy
|
|
|
|
|
|
case 44: // 嵌入模型:MokaAI M3E
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null; // 未知类型或自定义渠道不显示图标
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 14:31:54 +08:00
|
|
|
|
// 颜色列表
|
|
|
|
|
|
const colors = [
|
|
|
|
|
|
'amber',
|
|
|
|
|
|
'blue',
|
|
|
|
|
|
'cyan',
|
|
|
|
|
|
'green',
|
|
|
|
|
|
'grey',
|
|
|
|
|
|
'indigo',
|
|
|
|
|
|
'light-blue',
|
|
|
|
|
|
'lime',
|
|
|
|
|
|
'orange',
|
|
|
|
|
|
'pink',
|
|
|
|
|
|
'purple',
|
|
|
|
|
|
'red',
|
|
|
|
|
|
'teal',
|
|
|
|
|
|
'violet',
|
|
|
|
|
|
'yellow',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 基础10色色板 (N ≤ 10)
|
|
|
|
|
|
const baseColors = [
|
|
|
|
|
|
'#1664FF', // 主色
|
|
|
|
|
|
'#1AC6FF',
|
|
|
|
|
|
'#FF8A00',
|
|
|
|
|
|
'#3CC780',
|
|
|
|
|
|
'#7442D4',
|
|
|
|
|
|
'#FFC400',
|
|
|
|
|
|
'#304D77',
|
|
|
|
|
|
'#B48DEB',
|
|
|
|
|
|
'#009488',
|
|
|
|
|
|
'#FF7DDA',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 扩展20色色板 (10 < N ≤ 20)
|
|
|
|
|
|
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-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
|
|
|
|
|
'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
|
|
|
|
|
|
'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
|
|
|
|
|
|
// 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
|
|
|
|
|
|
'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-0314': 'rgb(70,130,180)', // 钢蓝色
|
|
|
|
|
|
'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-0314': 'rgb(90,105,205)', // 暗灰蓝色
|
|
|
|
|
|
'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-002': 'rgb(199,21,133)', // 中紫罗兰红色
|
|
|
|
|
|
'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
|
|
|
|
|
|
'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)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
|
|
|
|
|
|
'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) {
|
|
|
|
|
|
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
|
|
|
|
|
if (modelColorMap[modelName]) {
|
|
|
|
|
|
return modelColorMap[modelName];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 生成一个稳定的数字作为索引
|
|
|
|
|
|
let hash = 0;
|
|
|
|
|
|
for (let i = 0; i < modelName.length; i++) {
|
|
|
|
|
|
hash = (hash << 5) - hash + modelName.charCodeAt(i);
|
|
|
|
|
|
hash = hash & hash; // Convert to 32-bit integer
|
|
|
|
|
|
}
|
|
|
|
|
|
hash = Math.abs(hash);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 根据模型名称长度选择不同的色板
|
|
|
|
|
|
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 使用hash值选择颜色
|
|
|
|
|
|
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 = {}) {
|
2025-06-07 12:26:23 +08:00
|
|
|
|
const {
|
|
|
|
|
|
color,
|
|
|
|
|
|
size = 'large',
|
|
|
|
|
|
shape = 'circle',
|
|
|
|
|
|
onClick,
|
|
|
|
|
|
suffixIcon,
|
|
|
|
|
|
} = options;
|
2025-06-04 14:31:54 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
2025-06-04 20:13:02 +08:00
|
|
|
|
color={color || stringToColor(modelName)}
|
2025-06-04 14:31:54 +08:00
|
|
|
|
prefixIcon={icon}
|
|
|
|
|
|
suffixIcon={suffixIcon}
|
|
|
|
|
|
size={size}
|
|
|
|
|
|
shape={shape}
|
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
|
>
|
|
|
|
|
|
{modelName}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2023-06-07 23:26:00 +08:00
|
|
|
|
|
2023-05-16 21:33:59 +08:00
|
|
|
|
export function renderText(text, limit) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
if (text.length > limit) {
|
|
|
|
|
|
return text.slice(0, limit - 3) + '...';
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
2023-06-07 23:26:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-02 13:16:02 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Render group tags based on the input group string
|
|
|
|
|
|
* @param {string} group - The input group string
|
|
|
|
|
|
* @returns {JSX.Element} - The rendered group tags
|
|
|
|
|
|
*/
|
2023-06-07 23:26:00 +08:00
|
|
|
|
export function renderGroup(group) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
if (group === '') {
|
2024-04-04 18:18:18 +08:00
|
|
|
|
return (
|
2025-05-23 00:24:08 +08:00
|
|
|
|
<Tag size='large' key='default' color='orange' shape='circle'>
|
2024-12-13 19:03:14 +08:00
|
|
|
|
{i18next.t('用户分组')}
|
2024-04-04 18:18:18 +08:00
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
2024-04-02 13:16:02 +08:00
|
|
|
|
|
|
|
|
|
|
const tagColors = {
|
2024-04-04 18:18:18 +08:00
|
|
|
|
vip: 'yellow',
|
|
|
|
|
|
pro: 'yellow',
|
|
|
|
|
|
svip: 'red',
|
|
|
|
|
|
premium: 'red',
|
2024-04-02 13:16:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const groups = group.split(',').sort();
|
|
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
return (
|
2024-04-02 10:58:21 +08:00
|
|
|
|
<span key={group}>
|
2024-04-02 13:16:02 +08:00
|
|
|
|
{groups.map((group) => (
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
size='large'
|
|
|
|
|
|
color={tagColors[group] || stringToColor(group)}
|
|
|
|
|
|
key={group}
|
2025-05-23 00:24:08 +08:00
|
|
|
|
shape='circle'
|
2024-12-28 15:34:28 +08:00
|
|
|
|
onClick={async (event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (await copy(group)) {
|
|
|
|
|
|
showSuccess(i18next.t('已复制:') + group);
|
|
|
|
|
|
} else {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
Modal.error({
|
✨ feat: Add lucide-react icons to all table Tag components
- Add semantic icons to ChannelsTable.js for channel status, response time, and quota display
- Add status and quota icons to TokensTable.js for better visual distinction
- Add status and quota icons to RedemptionsTable.js for redemption code management
- Add role, status, and statistics icons to UsersTable.js for user management
- Import appropriate lucide-react icons for each table component
- Enhance UI consistency and user experience across all table interfaces
Icons added include:
- Status indicators: CheckCircle, XCircle, AlertCircle, HelpCircle
- Performance metrics: Zap, Timer, Clock, AlertTriangle, TestTube
- Financial data: Coins, DollarSign
- User roles: User, Shield, Crown
- Activity tracking: Activity, Users, UserPlus
This improves visual clarity and makes table data more intuitive to understand.
2025-06-08 23:13:45 +08:00
|
|
|
|
title: i18next.t('无法复制到剪贴板,请手动复制'),
|
2025-04-04 12:00:38 +08:00
|
|
|
|
content: group,
|
|
|
|
|
|
});
|
2024-12-28 15:34:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
2024-04-02 13:16:02 +08:00
|
|
|
|
>
|
|
|
|
|
|
{group}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))}
|
2024-04-02 10:58:21 +08:00
|
|
|
|
</span>
|
2024-03-23 21:24:39 +08:00
|
|
|
|
);
|
2023-06-16 15:20:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-30 19:51:00 +08:00
|
|
|
|
export function renderRatio(ratio) {
|
|
|
|
|
|
let color = 'green';
|
|
|
|
|
|
if (ratio > 5) {
|
|
|
|
|
|
color = 'red';
|
|
|
|
|
|
} else if (ratio > 3) {
|
|
|
|
|
|
color = 'orange';
|
|
|
|
|
|
} else if (ratio > 1) {
|
|
|
|
|
|
color = 'blue';
|
|
|
|
|
|
}
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Tag color={color}>
|
|
|
|
|
|
{ratio}x {i18next.t('倍率')}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
2024-12-30 19:51:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-04 12:00:38 +08:00
|
|
|
|
const measureTextWidth = (
|
|
|
|
|
|
text,
|
|
|
|
|
|
style = {
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontFamily:
|
|
|
|
|
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
|
|
|
|
},
|
|
|
|
|
|
containerWidth,
|
|
|
|
|
|
) => {
|
2025-02-19 23:25:42 +08:00
|
|
|
|
const span = document.createElement('span');
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
span.style.visibility = 'hidden';
|
|
|
|
|
|
span.style.position = 'absolute';
|
|
|
|
|
|
span.style.whiteSpace = 'nowrap';
|
|
|
|
|
|
span.style.fontSize = style.fontSize;
|
|
|
|
|
|
span.style.fontFamily = style.fontFamily;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
span.textContent = text;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
document.body.appendChild(span);
|
|
|
|
|
|
const width = span.offsetWidth;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
document.body.removeChild(span);
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
return width;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function truncateText(text, maxWidth = 200) {
|
|
|
|
|
|
if (!isMobile()) {
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!text) return text;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// Handle percentage-based maxWidth
|
|
|
|
|
|
let actualMaxWidth = maxWidth;
|
|
|
|
|
|
if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
|
|
|
|
|
|
const percentage = parseFloat(maxWidth) / 100;
|
|
|
|
|
|
// Use window width as fallback container width
|
|
|
|
|
|
actualMaxWidth = window.innerWidth * percentage;
|
|
|
|
|
|
}
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
const width = measureTextWidth(text);
|
|
|
|
|
|
if (width <= actualMaxWidth) return text;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
let left = 0;
|
|
|
|
|
|
let right = text.length;
|
|
|
|
|
|
let result = text;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
while (left <= right) {
|
|
|
|
|
|
const mid = Math.floor((left + right) / 2);
|
|
|
|
|
|
const truncated = text.slice(0, mid) + '...';
|
|
|
|
|
|
const currentWidth = measureTextWidth(truncated);
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
if (currentWidth <= actualMaxWidth) {
|
|
|
|
|
|
result = truncated;
|
|
|
|
|
|
left = mid + 1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
right = mid - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-02-19 23:25:42 +08:00
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
'Text measurement failed, falling back to character count',
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
2025-02-19 23:25:42 +08:00
|
|
|
|
if (text.length > 20) {
|
|
|
|
|
|
return text.slice(0, 17) + '...';
|
|
|
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-30 19:51:00 +08:00
|
|
|
|
export const renderGroupOption = (item) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
disabled,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
focused,
|
|
|
|
|
|
className,
|
|
|
|
|
|
style,
|
|
|
|
|
|
onMouseEnter,
|
|
|
|
|
|
onClick,
|
|
|
|
|
|
empty,
|
|
|
|
|
|
emptyContent,
|
|
|
|
|
|
...rest
|
|
|
|
|
|
} = item;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-30 19:51:00 +08:00
|
|
|
|
const baseStyle = {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
2024-12-30 19:51:00 +08:00
|
|
|
|
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': {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
backgroundColor: !disabled && 'var(--semi-color-fill-1)',
|
|
|
|
|
|
},
|
2024-12-30 19:51:00 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClick = () => {
|
|
|
|
|
|
if (!disabled && onClick) {
|
|
|
|
|
|
onClick();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseEnter = (e) => {
|
|
|
|
|
|
if (!disabled && onMouseEnter) {
|
|
|
|
|
|
onMouseEnter(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-30 19:51:00 +08:00
|
|
|
|
return (
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<div
|
2024-12-30 19:51:00 +08:00
|
|
|
|
style={baseStyle}
|
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
|
onMouseEnter={handleMouseEnter}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
|
|
|
|
<Typography.Text strong type={disabled ? 'tertiary' : undefined}>
|
|
|
|
|
|
{value}
|
|
|
|
|
|
</Typography.Text>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<Typography.Text type='secondary' size='small'>
|
2024-12-30 19:51:00 +08:00
|
|
|
|
{label}
|
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{item.ratio && renderRatio(item.ratio)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-06-16 15:20:06 +08:00
|
|
|
|
export function renderNumber(num) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2024-01-08 11:32:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function renderQuotaNumberWithDigit(num, digits = 2) {
|
2024-12-25 23:16:35 +08:00
|
|
|
|
if (typeof num !== 'number' || isNaN(num)) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
2024-03-23 21:24:39 +08:00
|
|
|
|
let displayInCurrency = localStorage.getItem('display_in_currency');
|
|
|
|
|
|
num = num.toFixed(digits);
|
|
|
|
|
|
if (displayInCurrency) {
|
|
|
|
|
|
return '$' + num;
|
|
|
|
|
|
}
|
|
|
|
|
|
return num;
|
2023-06-20 20:09:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-11 20:42:51 +08:00
|
|
|
|
export function renderNumberWithPoint(num) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
if (num === undefined) return '';
|
2024-03-23 21:24:39 +08:00
|
|
|
|
num = num.toFixed(2);
|
|
|
|
|
|
if (num >= 100000) {
|
|
|
|
|
|
// Convert number to string to manipulate it
|
|
|
|
|
|
let numStr = num.toString();
|
|
|
|
|
|
// Find the position of the decimal point
|
|
|
|
|
|
let decimalPointIndex = numStr.indexOf('.');
|
|
|
|
|
|
|
|
|
|
|
|
let wholePart = numStr;
|
|
|
|
|
|
let decimalPart = '';
|
|
|
|
|
|
|
|
|
|
|
|
// If there is a decimal point, split the number into whole and decimal parts
|
|
|
|
|
|
if (decimalPointIndex !== -1) {
|
|
|
|
|
|
wholePart = numStr.slice(0, decimalPointIndex);
|
|
|
|
|
|
decimalPart = numStr.slice(decimalPointIndex);
|
|
|
|
|
|
}
|
2023-12-11 20:42:51 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
// Take the first two and last two digits of the whole number part
|
|
|
|
|
|
let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
|
2024-01-08 11:32:27 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
// Return the formatted number
|
|
|
|
|
|
return shortenedWholePart + decimalPart;
|
|
|
|
|
|
}
|
2024-01-08 11:32:27 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
// If the number is less than 100,000, return it unmodified
|
|
|
|
|
|
return num;
|
2023-12-11 20:42:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-11-21 16:35:51 +08:00
|
|
|
|
export function getQuotaPerUnit() {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
|
|
|
|
|
quotaPerUnit = parseFloat(quotaPerUnit);
|
|
|
|
|
|
return quotaPerUnit;
|
2023-11-21 16:35:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-04 18:18:18 +08:00
|
|
|
|
export function renderUnitWithQuota(quota) {
|
|
|
|
|
|
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
|
|
|
|
|
quotaPerUnit = parseFloat(quotaPerUnit);
|
|
|
|
|
|
quota = parseFloat(quota);
|
|
|
|
|
|
return quotaPerUnit * quota;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-07 18:31:14 +08:00
|
|
|
|
export function getQuotaWithUnit(quota, digits = 6) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
|
|
|
|
|
quotaPerUnit = parseFloat(quotaPerUnit);
|
|
|
|
|
|
return (quota / quotaPerUnit).toFixed(digits);
|
2024-01-07 18:31:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-04 18:18:18 +08:00
|
|
|
|
export function renderQuotaWithAmount(amount) {
|
|
|
|
|
|
let displayInCurrency = localStorage.getItem('display_in_currency');
|
|
|
|
|
|
displayInCurrency = displayInCurrency === 'true';
|
|
|
|
|
|
if (displayInCurrency) {
|
|
|
|
|
|
return '$' + amount;
|
|
|
|
|
|
} else {
|
2025-06-09 01:16:35 +08:00
|
|
|
|
return renderNumber(renderUnitWithQuota(amount));
|
2024-04-04 18:18:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-06-20 20:09:17 +08:00
|
|
|
|
export function renderQuota(quota, digits = 2) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
|
|
|
|
|
let displayInCurrency = localStorage.getItem('display_in_currency');
|
|
|
|
|
|
quotaPerUnit = parseFloat(quotaPerUnit);
|
|
|
|
|
|
displayInCurrency = displayInCurrency === 'true';
|
|
|
|
|
|
if (displayInCurrency) {
|
|
|
|
|
|
return '$' + (quota / quotaPerUnit).toFixed(digits);
|
|
|
|
|
|
}
|
|
|
|
|
|
return renderNumber(quota);
|
2023-06-21 15:45:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-11 23:46:59 +08:00
|
|
|
|
function isValidGroupRatio(ratio) {
|
|
|
|
|
|
return ratio !== undefined && ratio !== null && ratio !== -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-05-12 15:35:57 +08:00
|
|
|
|
export function renderModelPrice(
|
2024-05-13 15:08:01 +08:00
|
|
|
|
inputTokens,
|
|
|
|
|
|
completionTokens,
|
2024-05-12 15:35:57 +08:00
|
|
|
|
modelRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
completionRatio,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-08 01:30:50 +08:00
|
|
|
|
cacheTokens = 0,
|
|
|
|
|
|
cacheRatio = 1.0,
|
2025-04-24 21:41:38 +08:00
|
|
|
|
image = false,
|
|
|
|
|
|
imageRatio = 1.0,
|
|
|
|
|
|
imageOutputTokens = 0,
|
2025-05-07 01:08:20 +08:00
|
|
|
|
webSearch = false,
|
|
|
|
|
|
webSearchCallCount = 0,
|
|
|
|
|
|
webSearchPrice = 0,
|
|
|
|
|
|
fileSearch = false,
|
|
|
|
|
|
fileSearchCallCount = 0,
|
|
|
|
|
|
fileSearchPrice = 0,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
audioInputSeperatePrice = false,
|
|
|
|
|
|
audioInputTokens = 0,
|
|
|
|
|
|
audioInputPrice = 0,
|
2024-05-12 15:35:57 +08:00
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2024-05-12 15:35:57 +08:00
|
|
|
|
if (modelPrice !== -1) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
total: modelPrice * groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2024-05-12 15:35:57 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (completionRatio === undefined) {
|
|
|
|
|
|
completionRatio = 0;
|
|
|
|
|
|
}
|
2024-07-01 00:56:37 +08:00
|
|
|
|
let inputRatioPrice = modelRatio * 2.0;
|
|
|
|
|
|
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
2025-03-08 01:30:50 +08:00
|
|
|
|
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
2025-04-24 21:41:38 +08:00
|
|
|
|
let imageRatioPrice = modelRatio * 2.0 * imageRatio;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-03-08 01:30:50 +08:00
|
|
|
|
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
2025-04-24 21:41:38 +08:00
|
|
|
|
let effectiveInputTokens =
|
2025-04-04 12:00:38 +08:00
|
|
|
|
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
2025-05-07 01:08:20 +08:00
|
|
|
|
// Handle image tokens if present
|
2025-04-24 21:41:38 +08:00
|
|
|
|
if (image && imageOutputTokens > 0) {
|
2025-05-07 01:08:20 +08:00
|
|
|
|
effectiveInputTokens =
|
|
|
|
|
|
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
|
2025-04-24 21:41:38 +08:00
|
|
|
|
}
|
2025-06-07 12:26:23 +08:00
|
|
|
|
if (audioInputTokens > 0) {
|
|
|
|
|
|
effectiveInputTokens -= audioInputTokens;
|
|
|
|
|
|
}
|
2024-05-13 15:08:01 +08:00
|
|
|
|
let price =
|
2025-03-08 01:30:50 +08:00
|
|
|
|
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
2025-06-07 12:26:23 +08:00
|
|
|
|
(audioInputTokens / 1000000) * audioInputPrice * groupRatio +
|
2025-05-07 01:08:20 +08:00
|
|
|
|
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
2025-05-07 19:33:32 +08:00
|
|
|
|
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
|
|
|
|
|
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-05-12 15:35:57 +08:00
|
|
|
|
return (
|
2024-05-13 15:08:01 +08:00
|
|
|
|
<>
|
|
|
|
|
|
<article>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<p>
|
2025-06-07 12:26:23 +08:00
|
|
|
|
{i18next.t('输入价格:${{price}} / 1M tokens{{audioPrice}}', {
|
2025-03-08 23:47:02 +08:00
|
|
|
|
price: inputRatioPrice,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
audioPrice: audioInputSeperatePrice
|
|
|
|
|
|
? `,音频 $${audioInputPrice} / 1M tokens`
|
|
|
|
|
|
: '',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
2025-04-24 21:41:38 +08:00
|
|
|
|
'输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
total: completionRatioPrice,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{cacheTokens > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
total: inputRatioPrice * cacheRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
2025-03-08 01:30:50 +08:00
|
|
|
|
)}
|
2025-04-24 21:41:38 +08:00
|
|
|
|
{image && imageOutputTokens > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: imageRatioPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
total: imageRatioPrice * groupRatio,
|
|
|
|
|
|
imageRatio: imageRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2025-05-07 01:08:20 +08:00
|
|
|
|
{webSearch && webSearchCallCount > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t('Web搜索价格:${{price}} / 1K 次', {
|
|
|
|
|
|
price: webSearchPrice,
|
|
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{fileSearch && fileSearchCallCount > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t('文件搜索价格:${{price}} / 1K 次', {
|
|
|
|
|
|
price: fileSearchPrice,
|
|
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2024-05-13 23:02:35 +08:00
|
|
|
|
<p></p>
|
2024-05-13 15:08:01 +08:00
|
|
|
|
<p>
|
2025-06-07 12:26:23 +08:00
|
|
|
|
{(() => {
|
|
|
|
|
|
// 构建输入部分描述
|
|
|
|
|
|
let inputDesc = '';
|
|
|
|
|
|
if (image && imageOutputTokens > 0) {
|
|
|
|
|
|
inputDesc = i18next.t(
|
|
|
|
|
|
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{
|
2025-06-03 23:56:39 +08:00
|
|
|
|
nonImageInput: inputTokens - imageOutputTokens,
|
|
|
|
|
|
imageInput: imageOutputTokens,
|
|
|
|
|
|
imageRatio: imageRatio,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
},
|
2025-06-07 12:26:23 +08:00
|
|
|
|
);
|
|
|
|
|
|
} else if (cacheTokens > 0) {
|
|
|
|
|
|
inputDesc = i18next.t(
|
|
|
|
|
|
'(输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
nonCacheInput: inputTokens - cacheTokens,
|
|
|
|
|
|
cacheInput: cacheTokens,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
cachePrice: cacheRatioPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (audioInputSeperatePrice && audioInputTokens > 0) {
|
|
|
|
|
|
inputDesc = i18next.t(
|
|
|
|
|
|
'(输入 {{nonAudioInput}} tokens / 1M tokens * ${{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * ${{audioPrice}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
nonAudioInput: inputTokens - audioInputTokens,
|
|
|
|
|
|
audioInput: audioInputTokens,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
audioPrice: audioInputPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
inputDesc = i18next.t(
|
|
|
|
|
|
'(输入 {{input}} tokens / 1M tokens * ${{price}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
input: inputTokens,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建输出部分描述
|
|
|
|
|
|
const outputDesc = i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * {{ratioType}} {{ratio}}',
|
2025-06-07 12:26:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
completion: completionTokens,
|
|
|
|
|
|
compPrice: completionRatioPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
ratioType: ratioLabel,
|
2025-06-07 12:26:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 构建额外服务描述
|
|
|
|
|
|
const extraServices = [
|
|
|
|
|
|
webSearch && webSearchCallCount > 0
|
2025-06-03 23:56:39 +08:00
|
|
|
|
? i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
count: webSearchCallCount,
|
|
|
|
|
|
price: webSearchPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-06-07 12:26:23 +08:00
|
|
|
|
: '',
|
|
|
|
|
|
fileSearch && fileSearchCallCount > 0
|
|
|
|
|
|
? i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
count: fileSearchCallCount,
|
|
|
|
|
|
price: fileSearchPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-06-07 12:26:23 +08:00
|
|
|
|
: '',
|
|
|
|
|
|
].join('');
|
|
|
|
|
|
|
|
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'{{inputDesc}} + {{outputDesc}}{{extraServices}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
inputDesc,
|
|
|
|
|
|
outputDesc,
|
|
|
|
|
|
extraServices,
|
|
|
|
|
|
total: price.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
2024-05-13 15:08:01 +08:00
|
|
|
|
</p>
|
2024-12-13 19:03:14 +08:00
|
|
|
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
2024-05-13 15:08:01 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</>
|
2024-05-12 15:35:57 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-24 21:41:38 +08:00
|
|
|
|
export function renderLogContent(
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
completionRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
groupRatio,
|
|
|
|
|
|
user_group_ratio,
|
|
|
|
|
|
image = false,
|
|
|
|
|
|
imageRatio = 1.0,
|
2025-05-07 01:08:20 +08:00
|
|
|
|
useUserGroupRatio = undefined,
|
|
|
|
|
|
webSearch = false,
|
|
|
|
|
|
webSearchCallCount = 0,
|
|
|
|
|
|
fileSearch = false,
|
|
|
|
|
|
fileSearchCallCount = 0,
|
2025-04-24 21:41:38 +08:00
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
2025-05-07 01:08:20 +08:00
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
2025-04-24 21:41:38 +08:00
|
|
|
|
const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
|
|
|
|
|
|
|
|
|
|
|
if (modelPrice !== -1) {
|
|
|
|
|
|
return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratioType: ratioLabel,
|
2025-05-07 01:08:20 +08:00
|
|
|
|
ratio,
|
2025-04-24 21:41:38 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (image) {
|
2025-05-07 01:08:20 +08:00
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
modelRatio: modelRatio,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
imageRatio: imageRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
ratio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (webSearch) {
|
|
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
|
|
|
|
|
|
{
|
|
|
|
|
|
modelRatio: modelRatio,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
ratio,
|
|
|
|
|
|
webSearchCallCount,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-04-24 21:41:38 +08:00
|
|
|
|
} else {
|
2025-05-07 01:08:20 +08:00
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
modelRatio: modelRatio,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
ratio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-04-24 21:41:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-11 21:06:26 +08:00
|
|
|
|
export function renderModelPriceSimple(
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-08 01:30:50 +08:00
|
|
|
|
cacheTokens = 0,
|
|
|
|
|
|
cacheRatio = 1.0,
|
2025-04-24 21:41:38 +08:00
|
|
|
|
image = false,
|
|
|
|
|
|
imageRatio = 1.0,
|
2024-12-11 21:06:26 +08:00
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2024-12-11 21:06:26 +08:00
|
|
|
|
if (modelPrice !== -1) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
|
2024-12-13 19:03:14 +08:00
|
|
|
|
price: modelPrice,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
ratio: groupRatio,
|
2024-12-13 19:03:14 +08:00
|
|
|
|
});
|
2024-12-11 21:06:26 +08:00
|
|
|
|
} else {
|
2025-04-24 21:41:38 +08:00
|
|
|
|
if (image && cacheTokens !== 0) {
|
|
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
ratio: modelRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
groupRatio: groupRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
imageRatio: imageRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (image) {
|
|
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
ratio: modelRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
groupRatio: groupRatio,
|
|
|
|
|
|
imageRatio: imageRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (cacheTokens !== 0) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
ratio: modelRatio,
|
|
|
|
|
|
groupRatio: groupRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-03-08 01:30:50 +08:00
|
|
|
|
} else {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
return i18next.t('模型: {{ratio}} * {{ratioType}}:{{groupRatio}}', {
|
2025-03-08 01:30:50 +08:00
|
|
|
|
ratio: modelRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
groupRatio: groupRatio,
|
2025-03-08 01:30:50 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-12-11 21:06:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-07 16:12:09 +08:00
|
|
|
|
export function renderAudioModelPrice(
|
|
|
|
|
|
inputTokens,
|
|
|
|
|
|
completionTokens,
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
completionRatio,
|
|
|
|
|
|
audioInputTokens,
|
|
|
|
|
|
audioCompletionTokens,
|
|
|
|
|
|
audioRatio,
|
|
|
|
|
|
audioCompletionRatio,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-08 01:30:50 +08:00
|
|
|
|
cacheTokens = 0,
|
|
|
|
|
|
cacheRatio = 1.0,
|
2024-11-07 16:12:09 +08:00
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2024-11-07 16:12:09 +08:00
|
|
|
|
// 1 ratio = $0.002 / 1K tokens
|
|
|
|
|
|
if (modelPrice !== -1) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
total: modelPrice * groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2024-11-07 16:12:09 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (completionRatio === undefined) {
|
|
|
|
|
|
completionRatio = 0;
|
|
|
|
|
|
}
|
2025-01-04 17:54:02 +08:00
|
|
|
|
|
|
|
|
|
|
// try toFixed audioRatio
|
|
|
|
|
|
audioRatio = parseFloat(audioRatio).toFixed(6);
|
2024-11-07 16:12:09 +08:00
|
|
|
|
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
|
|
|
|
|
let inputRatioPrice = modelRatio * 2.0;
|
|
|
|
|
|
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
2025-03-08 01:30:50 +08:00
|
|
|
|
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2025-03-08 01:30:50 +08:00
|
|
|
|
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
2025-04-04 12:00:38 +08:00
|
|
|
|
const effectiveInputTokens =
|
|
|
|
|
|
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
|
|
|
|
|
|
2025-03-08 23:47:02 +08:00
|
|
|
|
let textPrice =
|
2025-03-08 01:30:50 +08:00
|
|
|
|
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
2025-04-04 12:00:38 +08:00
|
|
|
|
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
|
2025-03-08 23:47:02 +08:00
|
|
|
|
let audioPrice =
|
2024-11-07 16:12:09 +08:00
|
|
|
|
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
2025-04-04 12:00:38 +08:00
|
|
|
|
(audioCompletionTokens / 1000000) *
|
2025-06-11 23:46:59 +08:00
|
|
|
|
inputRatioPrice *
|
|
|
|
|
|
audioRatio *
|
|
|
|
|
|
audioCompletionRatio *
|
|
|
|
|
|
groupRatio;
|
2025-03-08 23:47:02 +08:00
|
|
|
|
let price = textPrice + audioPrice;
|
2024-11-07 16:12:09 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<article>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t('提示价格:${{price}} / 1M tokens', {
|
2025-03-08 23:47:02 +08:00
|
|
|
|
price: inputRatioPrice,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
total: completionRatioPrice,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{cacheTokens > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
total: inputRatioPrice * cacheRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
2025-03-08 01:30:50 +08:00
|
|
|
|
)}
|
2024-11-07 16:12:09 +08:00
|
|
|
|
<p>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
|
|
|
|
|
|
{
|
2025-03-08 01:30:50 +08:00
|
|
|
|
price: inputRatioPrice,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
total: inputRatioPrice * audioRatio,
|
|
|
|
|
|
audioRatio: audioRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
|
|
|
|
|
|
{
|
2025-03-08 01:30:50 +08:00
|
|
|
|
price: inputRatioPrice,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
total: inputRatioPrice * audioRatio * audioCompletionRatio,
|
|
|
|
|
|
audioRatio: audioRatio,
|
|
|
|
|
|
audioCompRatio: audioCompletionRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
2024-11-07 16:12:09 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{cacheTokens > 0
|
|
|
|
|
|
? i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
nonCacheInput: inputTokens - cacheTokens,
|
|
|
|
|
|
cacheInput: cacheTokens,
|
|
|
|
|
|
cachePrice: inputRatioPrice * cacheRatio,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
completion: completionTokens,
|
|
|
|
|
|
compPrice: completionRatioPrice,
|
|
|
|
|
|
total: textPrice.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-04-04 12:00:38 +08:00
|
|
|
|
: i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
input: inputTokens,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
completion: completionTokens,
|
|
|
|
|
|
compPrice: completionRatioPrice,
|
|
|
|
|
|
total: textPrice.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
2024-11-07 16:12:09 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
input: audioInputTokens,
|
|
|
|
|
|
completion: audioCompletionTokens,
|
|
|
|
|
|
audioInputPrice: audioRatio * inputRatioPrice,
|
|
|
|
|
|
audioCompPrice:
|
|
|
|
|
|
audioRatio * audioCompletionRatio * inputRatioPrice,
|
|
|
|
|
|
total: audioPrice.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
total: price.toFixed(6),
|
|
|
|
|
|
textPrice: textPrice.toFixed(6),
|
|
|
|
|
|
audioPrice: audioPrice.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
2024-11-07 16:12:09 +08:00
|
|
|
|
</p>
|
2025-01-04 17:46:06 +08:00
|
|
|
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
2024-11-07 16:12:09 +08:00
|
|
|
|
</article>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-06-21 15:45:30 +08:00
|
|
|
|
export function renderQuotaWithPrompt(quota, digits) {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
let displayInCurrency = localStorage.getItem('display_in_currency');
|
|
|
|
|
|
displayInCurrency = displayInCurrency === 'true';
|
|
|
|
|
|
if (displayInCurrency) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return (
|
|
|
|
|
|
' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
|
|
|
|
|
|
);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
return '';
|
2023-10-31 00:03:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 21:31:46 +08:00
|
|
|
|
export function renderClaudeModelPrice(
|
|
|
|
|
|
inputTokens,
|
|
|
|
|
|
completionTokens,
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
completionRatio,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
cacheTokens = 0,
|
|
|
|
|
|
cacheRatio = 1.0,
|
|
|
|
|
|
cacheCreationTokens = 0,
|
|
|
|
|
|
cacheCreationRatio = 1.0,
|
|
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2025-03-12 21:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
if (modelPrice !== -1) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
total: modelPrice * groupRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-03-12 21:31:46 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (completionRatio === undefined) {
|
|
|
|
|
|
completionRatio = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const completionRatioValue = completionRatio || 0;
|
|
|
|
|
|
const inputRatioPrice = modelRatio * 2.0;
|
|
|
|
|
|
const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
|
|
|
|
|
|
let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
|
|
|
|
|
|
let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
|
|
|
|
|
|
const nonCachedTokens = inputTokens;
|
2025-04-04 12:00:38 +08:00
|
|
|
|
const effectiveInputTokens =
|
|
|
|
|
|
nonCachedTokens +
|
|
|
|
|
|
cacheTokens * cacheRatio +
|
|
|
|
|
|
cacheCreationTokens * cacheCreationRatio;
|
2025-03-12 21:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
let price =
|
|
|
|
|
|
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
|
|
|
|
|
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<article>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t('提示价格:${{price}} / 1M tokens', {
|
2025-03-12 21:31:46 +08:00
|
|
|
|
price: inputRatioPrice,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
})}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
ratio: completionRatio,
|
|
|
|
|
|
total: completionRatioPrice,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{cacheTokens > 0 && (
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
ratio: cacheRatio,
|
|
|
|
|
|
total: cacheRatioPrice,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
2025-03-12 21:31:46 +08:00
|
|
|
|
)}
|
|
|
|
|
|
{cacheCreationTokens > 0 && (
|
2025-04-04 12:00:38 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
{i18next.t(
|
|
|
|
|
|
'缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
|
|
|
|
|
|
{
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
ratio: cacheCreationRatio,
|
|
|
|
|
|
total: cacheCreationRatioPrice,
|
|
|
|
|
|
cacheCreationRatio: cacheCreationRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
2025-03-12 21:31:46 +08:00
|
|
|
|
)}
|
|
|
|
|
|
<p></p>
|
|
|
|
|
|
<p>
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{cacheTokens > 0 || cacheCreationTokens > 0
|
|
|
|
|
|
? i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
nonCacheInput: nonCachedTokens,
|
|
|
|
|
|
cacheInput: cacheTokens,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
cacheCreationInput: cacheCreationTokens,
|
|
|
|
|
|
cacheCreationRatio: cacheCreationRatio,
|
|
|
|
|
|
cachePrice: cacheRatioPrice,
|
|
|
|
|
|
cacheCreationPrice: cacheCreationRatioPrice,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
completion: completionTokens,
|
|
|
|
|
|
compPrice: completionRatioPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
total: price.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-04-04 12:00:38 +08:00
|
|
|
|
: i18next.t(
|
2025-06-11 23:46:59 +08:00
|
|
|
|
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
input: inputTokens,
|
|
|
|
|
|
price: inputRatioPrice,
|
|
|
|
|
|
completion: completionTokens,
|
|
|
|
|
|
compPrice: completionRatioPrice,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
total: price.toFixed(6),
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
2025-03-12 21:31:46 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function renderClaudeLogContent(
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
completionRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
cacheRatio = 1.0,
|
|
|
|
|
|
cacheCreationRatio = 1.0,
|
|
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2025-03-12 21:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
if (modelPrice !== -1) {
|
|
|
|
|
|
return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
ratio: groupRatio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
2025-04-24 21:41:38 +08:00
|
|
|
|
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
modelRatio: modelRatio,
|
|
|
|
|
|
completionRatio: completionRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
cacheCreationRatio: cacheCreationRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
ratio: groupRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-03-12 21:31:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function renderClaudeModelPriceSimple(
|
|
|
|
|
|
modelRatio,
|
|
|
|
|
|
modelPrice = -1,
|
|
|
|
|
|
groupRatio,
|
2025-06-11 23:46:59 +08:00
|
|
|
|
user_group_ratio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
cacheTokens = 0,
|
|
|
|
|
|
cacheRatio = 1.0,
|
|
|
|
|
|
cacheCreationTokens = 0,
|
|
|
|
|
|
cacheCreationRatio = 1.0,
|
|
|
|
|
|
) {
|
2025-06-11 23:46:59 +08:00
|
|
|
|
const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
|
|
|
|
|
|
const ratioLabel = useUserGroupRatio
|
|
|
|
|
|
? i18next.t('专属倍率')
|
|
|
|
|
|
: i18next.t('分组倍率');
|
|
|
|
|
|
groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
|
2025-03-12 21:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
if (modelPrice !== -1) {
|
|
|
|
|
|
return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
|
|
|
|
|
|
price: modelPrice,
|
|
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
ratio: groupRatio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
return i18next.t(
|
|
|
|
|
|
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
|
|
|
|
|
|
{
|
|
|
|
|
|
ratio: modelRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
|
|
|
|
|
groupRatio: groupRatio,
|
|
|
|
|
|
cacheRatio: cacheRatio,
|
|
|
|
|
|
cacheCreationRatio: cacheCreationRatio,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-03-12 21:31:46 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
|
|
|
|
|
|
ratio: modelRatio,
|
|
|
|
|
|
ratioType: ratioLabel,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
groupRatio: groupRatio,
|
2025-03-12 21:31:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-03 23:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
|
|
|
|
|
* 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function rehypeSplitWordsIntoSpans(options = {}) {
|
|
|
|
|
|
const { previousContentLength = 0 } = options;
|
|
|
|
|
|
|
|
|
|
|
|
return (tree) => {
|
|
|
|
|
|
let currentCharCount = 0; // 当前已处理的字符数
|
|
|
|
|
|
|
|
|
|
|
|
visit(tree, 'element', (node) => {
|
|
|
|
|
|
if (
|
2025-06-07 12:26:23 +08:00
|
|
|
|
['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
|
|
|
|
|
|
node.tagName,
|
|
|
|
|
|
) &&
|
2025-06-03 23:56:39 +08:00
|
|
|
|
node.children
|
|
|
|
|
|
) {
|
|
|
|
|
|
const newChildren = [];
|
|
|
|
|
|
node.children.forEach((child) => {
|
|
|
|
|
|
if (child.type === 'text') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 使用 Intl.Segmenter 精准拆分中英文及标点
|
2025-06-07 12:26:23 +08:00
|
|
|
|
const segmenter = new Intl.Segmenter('zh', {
|
|
|
|
|
|
granularity: 'word',
|
|
|
|
|
|
});
|
2025-06-03 23:56:39 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// 判断这个词是否是新增的(在 previousContentLength 之后)
|
|
|
|
|
|
const isNewContent = wordStartPos >= previousContentLength;
|
|
|
|
|
|
|
|
|
|
|
|
newChildren.push({
|
|
|
|
|
|
type: 'element',
|
|
|
|
|
|
tagName: 'span',
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
className: isNewContent ? ['animate-fade-in'] : [],
|
|
|
|
|
|
},
|
|
|
|
|
|
children: [{ type: 'text', value: word }],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
currentCharCount = wordEndPos;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// Fallback:如果浏览器不支持 Segmenter
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-06-07 12:26:23 +08:00
|
|
|
|
}
|