✨ feat: Add user available model list
This commit is contained in:
parent
3cf92daef5
commit
8f74fadf8a
@ -12,7 +12,8 @@ import {
|
|||||||
IconActivity,
|
IconActivity,
|
||||||
IconBrandTelegram,
|
IconBrandTelegram,
|
||||||
IconReceipt2,
|
IconReceipt2,
|
||||||
IconBrush
|
IconBrush,
|
||||||
|
IconBrandGithubCopilot
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
@ -29,7 +30,8 @@ const icons = {
|
|||||||
IconActivity,
|
IconActivity,
|
||||||
IconBrandTelegram,
|
IconBrandTelegram,
|
||||||
IconReceipt2,
|
IconReceipt2,
|
||||||
IconBrush
|
IconBrush,
|
||||||
|
IconBrandGithubCopilot
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||||
@ -133,6 +135,15 @@ const panel = {
|
|||||||
breadcrumbs: false,
|
breadcrumbs: false,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'model_price',
|
||||||
|
title: '可用模型',
|
||||||
|
type: 'item',
|
||||||
|
url: '/panel/model_price',
|
||||||
|
icon: icons.IconBrandGithubCopilot,
|
||||||
|
breadcrumbs: false,
|
||||||
|
isAdmin: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'setting',
|
id: 'setting',
|
||||||
title: '设置',
|
title: '设置',
|
||||||
|
@ -17,6 +17,7 @@ const Analytics = Loadable(lazy(() => import('views/Analytics')));
|
|||||||
const Telegram = Loadable(lazy(() => import('views/Telegram')));
|
const Telegram = Loadable(lazy(() => import('views/Telegram')));
|
||||||
const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
||||||
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
||||||
|
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
||||||
|
|
||||||
// dashboard routing
|
// dashboard routing
|
||||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||||
@ -86,6 +87,10 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: 'midjourney',
|
path: 'midjourney',
|
||||||
element: <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