✨ feat: Add user available model list
This commit is contained in:
parent
3cf92daef5
commit
8f74fadf8a
@ -12,7 +12,8 @@ import {
|
||||
IconActivity,
|
||||
IconBrandTelegram,
|
||||
IconReceipt2,
|
||||
IconBrush
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
// constant
|
||||
@ -29,7 +30,8 @@ const icons = {
|
||||
IconActivity,
|
||||
IconBrandTelegram,
|
||||
IconReceipt2,
|
||||
IconBrush
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot
|
||||
};
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
@ -133,6 +135,15 @@ const panel = {
|
||||
breadcrumbs: false,
|
||||
isAdmin: true
|
||||
},
|
||||
{
|
||||
id: 'model_price',
|
||||
title: '可用模型',
|
||||
type: 'item',
|
||||
url: '/panel/model_price',
|
||||
icon: icons.IconBrandGithubCopilot,
|
||||
breadcrumbs: false,
|
||||
isAdmin: true
|
||||
},
|
||||
{
|
||||
id: 'setting',
|
||||
title: '设置',
|
||||
|
@ -17,6 +17,7 @@ const Analytics = Loadable(lazy(() => import('views/Analytics')));
|
||||
const Telegram = Loadable(lazy(() => import('views/Telegram')));
|
||||
const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
||||
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
||||
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
||||
|
||||
// dashboard routing
|
||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||
@ -86,6 +87,10 @@ const MainRoutes = {
|
||||
{
|
||||
path: 'midjourney',
|
||||
element: <Midjourney />
|
||||
},
|
||||
{
|
||||
path: 'model_price',
|
||||
element: <ModelPrice />
|
||||
}
|
||||
]
|
||||
};
|
||||
|
28
web/src/ui-component/TableNoData.js
Normal file
28
web/src/ui-component/TableNoData.js
Normal file
@ -0,0 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Box, Typography, TableRow, TableCell } from '@mui/material';
|
||||
|
||||
const TableNoData = ({ message = '暂无数据' }) => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={1000}>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '490px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3" color={'#697586'}>
|
||||
{message}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
export default TableNoData;
|
||||
|
||||
TableNoData.propTypes = {
|
||||
message: PropTypes.string
|
||||
};
|
39
web/src/views/ModelPrice/component/TableRow.js
Normal file
39
web/src/views/ModelPrice/component/TableRow.js
Normal file
@ -0,0 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TableRow, TableCell } from '@mui/material';
|
||||
|
||||
import Label from 'ui-component/Label';
|
||||
import { copy } from 'utils/common';
|
||||
|
||||
export default function PricesTableRow({ item }) {
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.model}>
|
||||
<TableCell>
|
||||
<Label
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
key={item.model}
|
||||
onClick={() => {
|
||||
copy(item.model, '模型名称');
|
||||
}}
|
||||
>
|
||||
{item.model}
|
||||
</Label>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell>{item.channel_type}</TableCell>
|
||||
|
||||
<TableCell>{item.input}</TableCell>
|
||||
<TableCell>{item.output}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PricesTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
userModelList: PropTypes.object,
|
||||
ownedby: PropTypes.array
|
||||
};
|
136
web/src/views/ModelPrice/index.js
Normal file
136
web/src/views/ModelPrice/index.js
Normal file
@ -0,0 +1,136 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
|
||||
import { Card } from '@mui/material';
|
||||
import PricesTableRow from './component/TableRow';
|
||||
import TableNoData from 'ui-component/TableNoData';
|
||||
import KeywordTableHead from 'ui-component/TableHead';
|
||||
import { API } from 'utils/api';
|
||||
import { showError } from 'utils/common';
|
||||
import { ValueFormatter, priceType } from 'views/Pricing/component/util';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
export default function ModelPrice() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [userModelList, setUserModelList] = useState([]);
|
||||
const [prices, setPrices] = useState({});
|
||||
const [ownedby, setOwnedby] = useState([]);
|
||||
|
||||
const fetchOwnedby = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get('/api/ownedby');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let ownedbyList = [];
|
||||
for (let key in data) {
|
||||
ownedbyList.push({ value: parseInt(key), label: data[key] });
|
||||
}
|
||||
setOwnedby(ownedbyList);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchPrices = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get('/api/prices');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let pricesObj = {};
|
||||
data.forEach((price) => {
|
||||
if (pricesObj[price.model] === undefined) {
|
||||
pricesObj[price.model] = price;
|
||||
}
|
||||
});
|
||||
setPrices(pricesObj);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchUserModelList = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get('/api/user/models');
|
||||
if (res === undefined) {
|
||||
setUserModelList([]);
|
||||
return;
|
||||
}
|
||||
setUserModelList(res.data.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userModelList.length === 0 || Object.keys(prices).length === 0 || ownedby.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newRows = [];
|
||||
userModelList.forEach((model) => {
|
||||
const price = prices[model.id];
|
||||
const type_label = priceType.find((pt) => pt.value === price?.type);
|
||||
const channel_label = ownedby.find((ob) => ob.value === price?.channel_type);
|
||||
newRows.push({
|
||||
model: model.id,
|
||||
type: type_label?.label || '未知',
|
||||
channel_type: channel_label?.label || '未知',
|
||||
input: ValueFormatter(price?.input || 30),
|
||||
output: ValueFormatter(price?.output || 30)
|
||||
});
|
||||
});
|
||||
setRows(newRows);
|
||||
}, [userModelList, ownedby, prices]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
await Promise.all([fetchOwnedby(), fetchUserModelList()]);
|
||||
fetchPrices();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [fetchOwnedby, fetchUserModelList, fetchPrices]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<KeywordTableHead
|
||||
headLabel={[
|
||||
{ id: 'model', label: '模型名称', disableSort: true },
|
||||
{ id: 'type', label: '类型', disableSort: true },
|
||||
{ id: 'channel_type', label: '供应商', disableSort: true },
|
||||
{ id: 'input', label: '输入(/1k tokens)', disableSort: true },
|
||||
{ id: 'output', label: '输出(/1k tokens)', disableSort: true }
|
||||
]}
|
||||
/>
|
||||
<TableBody>
|
||||
{rows.length === 0 ? (
|
||||
<TableNoData message="无可用模型" />
|
||||
) : (
|
||||
rows.map((row) => <PricesTableRow item={row} key={row.model} />)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user