| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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, |
| Switch, |
| } 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'; |
| import Decimal from 'decimal.js'; |
|
|
| 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 [showRatio, setShowRatio] = useState(false); |
| const [hideUnavailable, setHideUnavailable] = useState(true); |
|
|
| 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) { |
| |
| 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 ( |
| <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> |
| ); |
| } |
|
|
| const columns = [ |
| { |
| title: t('可用性'), |
| dataIndex: 'available', |
| render: (text, record, index) => { |
| |
| 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) => { |
| |
| 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) { |
| |
| let inputRatioPrice = new Decimal(record.model_ratio).mul(2).mul(groupRatio[selectedGroup]).toFixed(3); |
| let completionRatioPrice = new Decimal(record.model_ratio).mul(record.completion_ratio).mul(2).mul(groupRatio[selectedGroup]).toFixed(3); |
| content = ( |
| <> |
| <Text> |
| {t('提示')} ${inputRatioPrice} / 1M tokens |
| </Text> |
| <br /> |
| <Text> |
| {t('补全')} ${completionRatioPrice} / 1M tokens |
| </Text> |
| </> |
| ); |
| } else { |
| let price = new Decimal(text).mul(groupRatio[selectedGroup]).toFixed(3); |
| 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]; |
| } |
| |
| models.sort((a, b) => { |
| return a.quota_type - b.quota_type; |
| }); |
|
|
| |
| 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 { |
| |
| 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.length === 0} |
| > |
| {t('复制选中模型')} |
| </Button> |
| <Space style={{ marginLeft: 16, alignItems: 'center' }}> |
| <span>{t('显示倍率')}:</span> |
| <Switch |
| checked={showRatio} |
| onChange={setShowRatio} |
| /> |
| </Space> |
| <Space style={{ marginLeft: 16, alignItems: 'center' }}> |
| <span>{t('隐藏不可用模型')}:</span> |
| <Switch |
| checked={hideUnavailable} |
| onChange={setHideUnavailable} |
| /> |
| </Space> |
| </Space> |
| <Table |
| style={{ marginTop: 5 }} |
| columns={columns.filter(column => { |
| if (column.dataIndex === 'available' && hideUnavailable) return false; |
| if (column.dataIndex === 'model_ratio' && !showRatio) return false; |
| return true; |
| })} |
| dataSource={models.filter(model => { |
| if (hideUnavailable && !model.enable_groups.includes(selectedGroup)) return false; |
| return true; |
| })} |
| 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; |
|
|