mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-17 23:17:27 +00:00
- Updated the GetPricing function in the backend to include user group information, allowing for dynamic adjustment of group ratios based on the user's group. - Implemented logic to filter group ratios based on the user's usable groups, improving the accuracy of pricing data returned. - Modified the ModelPricing component to utilize the new usable group data, ensuring only relevant groups are displayed in the UI. - Enhanced state management in the frontend to accommodate the new usable group information, improving user experience and data consistency.
425 lines
12 KiB
JavaScript
425 lines
12 KiB
JavaScript
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||
import { useTranslation } from 'react-i18next';
|
||
|
||
import {
|
||
Banner,
|
||
Input,
|
||
Layout,
|
||
Modal,
|
||
Space,
|
||
Table,
|
||
Tag,
|
||
Tooltip,
|
||
Popover,
|
||
ImagePreview,
|
||
Button,
|
||
} from '@douyinfe/semi-ui';
|
||
import {
|
||
IconMore,
|
||
IconVerify,
|
||
IconUploadError,
|
||
IconHelpCircle,
|
||
} from '@douyinfe/semi-icons';
|
||
import { UserContext } from '../context/User/index.js';
|
||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||
|
||
const ModelPricing = () => {
|
||
const { t } = useTranslation();
|
||
const [filteredValue, setFilteredValue] = useState([]);
|
||
const compositionRef = useRef({ isComposition: false });
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||
const [selectedGroup, setSelectedGroup] = useState('default');
|
||
|
||
const rowSelection = useMemo(
|
||
() => ({
|
||
onChange: (selectedRowKeys, selectedRows) => {
|
||
setSelectedRowKeys(selectedRowKeys);
|
||
},
|
||
}),
|
||
[]
|
||
);
|
||
|
||
const handleChange = (value) => {
|
||
if (compositionRef.current.isComposition) {
|
||
return;
|
||
}
|
||
const newFilteredValue = value ? [value] : [];
|
||
setFilteredValue(newFilteredValue);
|
||
};
|
||
const handleCompositionStart = () => {
|
||
compositionRef.current.isComposition = true;
|
||
};
|
||
|
||
const handleCompositionEnd = (event) => {
|
||
compositionRef.current.isComposition = false;
|
||
const value = event.target.value;
|
||
const newFilteredValue = value ? [value] : [];
|
||
setFilteredValue(newFilteredValue);
|
||
};
|
||
|
||
function renderQuotaType(type) {
|
||
// Ensure all cases are string literals by adding quotes.
|
||
switch (type) {
|
||
case 1:
|
||
return (
|
||
<Tag color='teal' size='large'>
|
||
{t('按次计费')}
|
||
</Tag>
|
||
);
|
||
case 0:
|
||
return (
|
||
<Tag color='violet' size='large'>
|
||
{t('按量计费')}
|
||
</Tag>
|
||
);
|
||
default:
|
||
return t('未知');
|
||
}
|
||
}
|
||
|
||
function renderAvailable(available) {
|
||
return available ? (
|
||
<Popover
|
||
content={
|
||
<div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
|
||
}
|
||
position='top'
|
||
key={available}
|
||
style={{
|
||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||
color: 'var(--semi-color-white)',
|
||
borderWidth: 1,
|
||
borderStyle: 'solid',
|
||
}}
|
||
>
|
||
<IconVerify style={{ color: 'green' }} size="large" />
|
||
</Popover>
|
||
) : (
|
||
<Popover
|
||
content={
|
||
<div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
|
||
}
|
||
position='top'
|
||
key={available}
|
||
style={{
|
||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||
color: 'var(--semi-color-white)',
|
||
borderWidth: 1,
|
||
borderStyle: 'solid',
|
||
}}
|
||
>
|
||
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
|
||
</Popover>
|
||
);
|
||
}
|
||
|
||
const columns = [
|
||
{
|
||
title: t('可用性'),
|
||
dataIndex: 'available',
|
||
render: (text, record, index) => {
|
||
// if record.enable_groups contains selectedGroup, then available is true
|
||
return renderAvailable(record.enable_groups.includes(selectedGroup));
|
||
},
|
||
sorter: (a, b) => a.available - b.available,
|
||
},
|
||
{
|
||
title: t('模型名称'),
|
||
dataIndex: 'model_name',
|
||
render: (text, record, index) => {
|
||
return (
|
||
<>
|
||
<Tag
|
||
color='green'
|
||
size='large'
|
||
onClick={() => {
|
||
copyText(text);
|
||
}}
|
||
>
|
||
{text}
|
||
</Tag>
|
||
</>
|
||
);
|
||
},
|
||
onFilter: (value, record) =>
|
||
record.model_name.toLowerCase().includes(value.toLowerCase()),
|
||
filteredValue,
|
||
},
|
||
{
|
||
title: t('计费类型'),
|
||
dataIndex: 'quota_type',
|
||
render: (text, record, index) => {
|
||
return renderQuotaType(parseInt(text));
|
||
},
|
||
sorter: (a, b) => a.quota_type - b.quota_type,
|
||
},
|
||
{
|
||
title: t('可用分组'),
|
||
dataIndex: 'enable_groups',
|
||
render: (text, record, index) => {
|
||
|
||
// enable_groups is a string array
|
||
return (
|
||
<Space>
|
||
{text.map((group) => {
|
||
if (usableGroup[group]) {
|
||
if (group === selectedGroup) {
|
||
return (
|
||
<Tag
|
||
color='blue'
|
||
size='large'
|
||
prefixIcon={<IconVerify />}
|
||
>
|
||
{group}
|
||
</Tag>
|
||
);
|
||
} else {
|
||
return (
|
||
<Tag
|
||
color='blue'
|
||
size='large'
|
||
onClick={() => {
|
||
setSelectedGroup(group);
|
||
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
||
group: group,
|
||
ratio: groupRatio[group]
|
||
}));
|
||
}}
|
||
>
|
||
{group}
|
||
</Tag>
|
||
);
|
||
}
|
||
}
|
||
})}
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: () => (
|
||
<span style={{'display':'flex','alignItems':'center'}}>
|
||
{t('倍率')}
|
||
<Popover
|
||
content={
|
||
<div style={{ padding: 8 }}>
|
||
{t('倍率是为了方便换算不同价格的模型')}<br/>
|
||
{t('点击查看倍率说明')}
|
||
</div>
|
||
}
|
||
position='top'
|
||
style={{
|
||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||
color: 'var(--semi-color-white)',
|
||
borderWidth: 1,
|
||
borderStyle: 'solid',
|
||
}}
|
||
>
|
||
<IconHelpCircle
|
||
onClick={() => {
|
||
setModalImageUrl('/ratio.png');
|
||
setIsModalOpenurl(true);
|
||
}}
|
||
/>
|
||
</Popover>
|
||
</span>
|
||
),
|
||
dataIndex: 'model_ratio',
|
||
render: (text, record, index) => {
|
||
let content = text;
|
||
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
|
||
content = (
|
||
<>
|
||
<Text>{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}</Text>
|
||
<br />
|
||
<Text>{t('补全倍率')}:{record.quota_type === 0 ? completionRatio : t('无')}</Text>
|
||
<br />
|
||
<Text>{t('分组倍率')}:{groupRatio[selectedGroup]}</Text>
|
||
</>
|
||
);
|
||
return <div>{content}</div>;
|
||
},
|
||
},
|
||
{
|
||
title: t('模型价格'),
|
||
dataIndex: 'model_price',
|
||
render: (text, record, index) => {
|
||
let content = text;
|
||
if (record.quota_type === 0) {
|
||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||
let inputRatioPrice = record.model_ratio * 2 * groupRatio[selectedGroup];
|
||
let completionRatioPrice =
|
||
record.model_ratio *
|
||
record.completion_ratio * 2 *
|
||
groupRatio[selectedGroup];
|
||
content = (
|
||
<>
|
||
<Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
|
||
<br />
|
||
<Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
|
||
</>
|
||
);
|
||
} else {
|
||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||
content = <>${t('模型价格')}:${price}</>;
|
||
}
|
||
return <div>{content}</div>;
|
||
},
|
||
},
|
||
];
|
||
|
||
const [models, setModels] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [userState, userDispatch] = useContext(UserContext);
|
||
const [groupRatio, setGroupRatio] = useState({});
|
||
const [usableGroup, setUsableGroup] = useState({});
|
||
|
||
const setModelsFormat = (models, groupRatio) => {
|
||
for (let i = 0; i < models.length; i++) {
|
||
models[i].key = models[i].model_name;
|
||
models[i].group_ratio = groupRatio[models[i].model_name];
|
||
}
|
||
// sort by quota_type
|
||
models.sort((a, b) => {
|
||
return a.quota_type - b.quota_type;
|
||
});
|
||
|
||
// sort by model_name, start with gpt is max, other use localeCompare
|
||
models.sort((a, b) => {
|
||
if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
|
||
return -1;
|
||
} else if (
|
||
!a.model_name.startsWith('gpt') &&
|
||
b.model_name.startsWith('gpt')
|
||
) {
|
||
return 1;
|
||
} else {
|
||
return a.model_name.localeCompare(b.model_name);
|
||
}
|
||
});
|
||
|
||
setModels(models);
|
||
};
|
||
|
||
const loadPricing = async () => {
|
||
setLoading(true);
|
||
|
||
let url = '';
|
||
url = `/api/pricing`;
|
||
const res = await API.get(url);
|
||
const { success, message, data, group_ratio, usable_group } = res.data;
|
||
if (success) {
|
||
setGroupRatio(group_ratio);
|
||
setUsableGroup(usable_group);
|
||
setSelectedGroup(userState.user ? userState.user.group : 'default')
|
||
setModelsFormat(data, group_ratio);
|
||
} else {
|
||
showError(message);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const refresh = async () => {
|
||
await loadPricing();
|
||
};
|
||
|
||
const copyText = async (text) => {
|
||
if (await copy(text)) {
|
||
showSuccess('已复制:' + text);
|
||
} else {
|
||
// setSearchKeyword(text);
|
||
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
refresh().then();
|
||
}, []);
|
||
|
||
return (
|
||
<>
|
||
<Layout>
|
||
{userState.user ? (
|
||
<Banner
|
||
type="success"
|
||
fullMode={false}
|
||
closeIcon="null"
|
||
description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
|
||
group: userState.user.group,
|
||
ratio: groupRatio[userState.user.group]
|
||
})}
|
||
/>
|
||
) : (
|
||
<Banner
|
||
type='warning'
|
||
fullMode={false}
|
||
closeIcon="null"
|
||
description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
|
||
ratio: groupRatio['default']
|
||
})}
|
||
/>
|
||
)}
|
||
<br/>
|
||
<Banner
|
||
type="info"
|
||
fullMode={false}
|
||
description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
|
||
closeIcon="null"
|
||
/>
|
||
<br/>
|
||
<Space style={{ marginBottom: 16 }}>
|
||
<Input
|
||
placeholder={t('模糊搜索模型名称')}
|
||
style={{ width: 200 }}
|
||
onCompositionStart={handleCompositionStart}
|
||
onCompositionEnd={handleCompositionEnd}
|
||
onChange={handleChange}
|
||
showClear
|
||
/>
|
||
<Button
|
||
theme='light'
|
||
type='tertiary'
|
||
style={{width: 150}}
|
||
onClick={() => {
|
||
copyText(selectedRowKeys);
|
||
}}
|
||
disabled={selectedRowKeys == ""}
|
||
>
|
||
{t('复制选中模型')}
|
||
</Button>
|
||
</Space>
|
||
<Table
|
||
style={{ marginTop: 5 }}
|
||
columns={columns}
|
||
dataSource={models}
|
||
loading={loading}
|
||
pagination={{
|
||
formatPageText: (page) =>
|
||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||
start: page.currentStart,
|
||
end: page.currentEnd,
|
||
total: models.length
|
||
}),
|
||
pageSize: models.length,
|
||
showSizeChanger: false,
|
||
}}
|
||
rowSelection={rowSelection}
|
||
/>
|
||
<ImagePreview
|
||
src={modalImageUrl}
|
||
visible={isModalOpenurl}
|
||
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
||
/>
|
||
</Layout>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default ModelPricing;
|