feat: model usage

This commit is contained in:
jialin
2025-06-24 16:54:03 +08:00
parent 645801fe86
commit 34af730934
22 changed files with 750 additions and 385 deletions
+1
View File
@@ -41,6 +41,7 @@
"driver.js": "^1.3.1", "driver.js": "^1.3.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"epubjs": "^0.3.93", "epubjs": "^0.3.93",
"file-saver": "^2.0.5",
"has-ansi": "^5.0.1", "has-ansi": "^5.0.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jotai": "^2.8.4", "jotai": "^2.8.4",
+7
View File
@@ -84,6 +84,9 @@ dependencies:
epubjs: epubjs:
specifier: ^0.3.93 specifier: ^0.3.93
version: 0.3.93 version: 0.3.93
file-saver:
specifier: ^2.0.5
version: 2.0.5
has-ansi: has-ansi:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1 version: 5.0.1
@@ -10788,6 +10791,10 @@ packages:
webpack: 5.97.1 webpack: 5.97.1
dev: true dev: true
/file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==, tarball: https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz}
dev: false
/fill-range@7.0.1: /fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
+8 -5
View File
@@ -54,6 +54,13 @@ const Chart: React.FC<{
const currentChart = chart.current; const currentChart = chart.current;
const optionsYAxis = currentChart.getOption()?.yAxis; const optionsYAxis = currentChart.getOption()?.yAxis;
console.log(
'chart finished',
finished.current,
optionsYAxis,
chart.current
);
if ( if (
!optionsYAxis || !optionsYAxis ||
!Array.isArray(optionsYAxis) || !Array.isArray(optionsYAxis) ||
@@ -81,10 +88,6 @@ const Chart: React.FC<{
const newMax0 = intervals[0] * (unifiedCount - 1); const newMax0 = intervals[0] * (unifiedCount - 1);
const newMax1 = intervals[1] * (unifiedCount - 1); const newMax1 = intervals[1] * (unifiedCount - 1);
// get yaxis max value
const maxValue0 = Math.max();
const maxValue1 = yAxisModels[1].get('max');
// if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis // if newMax0 equal to maxValue0, and newMax1 equal to maxValue1, do not update yAxis
if (counts[0] === counts[1]) return; if (counts[0] === counts[1]) return;
@@ -127,7 +130,7 @@ const Chart: React.FC<{
resize(); resize();
setOption(options); setOption(options);
resizeable.current = true; resizeable.current = true;
}, [options]); }, [setOption]);
useEffect(() => { useEffect(() => {
const handleResize = throttle(() => { const handleResize = throttle(() => {
+2 -2
View File
@@ -2,7 +2,7 @@ import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config'; import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data'; import EmptyData from '@/components/empty-data';
import _ from 'lodash'; import _ from 'lodash';
import React, { memo, useMemo } from 'react'; import React, { useMemo } from 'react';
import { ChartProps } from './types'; import { ChartProps } from './types';
const MixLineBarChart: React.FC< const MixLineBarChart: React.FC<
@@ -147,4 +147,4 @@ const MixLineBarChart: React.FC<
); );
}; };
export default memo(MixLineBarChart); export default MixLineBarChart;
@@ -0,0 +1,25 @@
import AutoTooltip from '@/components/auto-tooltip';
interface SelectRenderProps {
maxTagWidth?: number;
}
export default function useSelectRender(config?: SelectRenderProps) {
const { maxTagWidth = 100 } = config || {};
const TagRender = (props: any) => {
const { label } = props;
return (
<AutoTooltip
maxWidth={maxTagWidth}
closable={props.closable}
onClose={props.onClose}
>
{label}
</AutoTooltip>
);
};
return {
TagRender
};
}
+4 -1
View File
@@ -27,5 +27,8 @@ export default {
'dashboard.usage.export': 'Export Data', 'dashboard.usage.export': 'Export Data',
'dashboard.usage.export.user': 'User', 'dashboard.usage.export.user': 'User',
'dashboard.usage.export.model': 'Model', 'dashboard.usage.export.model': 'Model',
'dashboard.usage.export.date': 'Date' 'dashboard.usage.export.date': 'Date',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
}; };
+8 -2
View File
@@ -27,7 +27,10 @@ export default {
'dashboard.usage.export': 'Export Data', 'dashboard.usage.export': 'Export Data',
'dashboard.usage.export.user': 'User', 'dashboard.usage.export.user': 'User',
'dashboard.usage.export.model': 'Model', 'dashboard.usage.export.model': 'Model',
'dashboard.usage.export.date': 'Date' 'dashboard.usage.export.date': 'Date',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -36,5 +39,8 @@ export default {
// 3. 'dashboard.usage.export': 'Export Data', // 3. 'dashboard.usage.export': 'Export Data',
// 4. 'dashboard.usage.export.user': 'User', // 4. 'dashboard.usage.export.user': 'User',
// 5. 'dashboard.usage.export.model': 'Model', // 5. 'dashboard.usage.export.model': 'Model',
// 6. 'dashboard.usage.export.date': 'Date' // 6. 'dashboard.usage.export.date': 'Date',
// 7. 'dashboard.usage.datePicker.last7days': 'Last 7 Days',
// 8.'dashboard.usage.datePicker.last30days': 'Last 30 Days',
// 9. 'dashboard.usage.datePicker.last60days': 'Last 60 Days'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+7 -2
View File
@@ -27,9 +27,14 @@ export default {
'dashboard.usage.export': 'Экспорт данных', 'dashboard.usage.export': 'Экспорт данных',
'dashboard.usage.export.user': 'Пользователь', 'dashboard.usage.export.user': 'Пользователь',
'dashboard.usage.export.model': 'Модель', 'dashboard.usage.export.model': 'Модель',
'dashboard.usage.export.date': 'Дата' 'dashboard.usage.export.date': 'Дата',
'dashboard.usage.datePicker.last7days': 'Last 7 Days',
'dashboard.usage.datePicker.last30days': 'Last 30 Days',
'dashboard.usage.datePicker.last60days': 'Last 60 Days'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'dashboard.usage.datePicker.last7days': 'Last 7 Days',
// 2. 'dashboard.usage.datePicker.last30days': 'Last 30 Days',
// 3. 'dashboard.usage.datePicker.last60days': 'Last 60 Days'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+4 -1
View File
@@ -27,5 +27,8 @@ export default {
'dashboard.usage.export': '导出数据', 'dashboard.usage.export': '导出数据',
'dashboard.usage.export.user': '用户', 'dashboard.usage.export.user': '用户',
'dashboard.usage.export.model': '模型', 'dashboard.usage.export.model': '模型',
'dashboard.usage.export.date': '日期' 'dashboard.usage.export.date': '日期',
'dashboard.usage.datePicker.last7days': '最近 7 天',
'dashboard.usage.datePicker.last30days': '最近 30 天',
'dashboard.usage.datePicker.last60days': '最近 60 天'
}; };
+17 -3
View File
@@ -1,4 +1,5 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import qs from 'query-string';
export const DASHBOARD_API = '/dashboard'; export const DASHBOARD_API = '/dashboard';
@@ -6,7 +7,20 @@ export async function queryDashboardData() {
return request(DASHBOARD_API); return request(DASHBOARD_API);
} }
export async function queryDashboardUsageData() { export async function queryDashboardUsageData<T>(
// return request(`${DASHBOARD_API}/usage`); params: {
return {}; start_date: string;
end_date: string;
model_ids?: number[];
user_ids?: number[];
raw?: boolean;
},
options?: {
token?: any;
}
) {
return request<T>(`${DASHBOARD_API}/usage?${qs.stringify(params)}`, {
method: 'GET',
cancelToken: options?.token
});
} }
@@ -1,40 +1,112 @@
import AutoTooltip from '@/components/auto-tooltip';
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal'; import ScrollerModal from '@/components/scroller-modal';
import useTableFetch from '@/hooks/use-table-fetch'; import { exportJsonToExcel } from '@/utils/excel-reader';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { DatePicker, Select, Space, Table } from 'antd'; import { Table, TableColumnType } from 'antd';
import dayjs from 'dayjs'; import React, { useEffect } from 'react';
import React from 'react'; import { TableRow } from '../../config/types';
import { queryDashboardUsageData } from '../../apis'; import useUsageData from './use-usage-data';
import { exportTableColumns } from '../../config';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const ExportData: React.FC<{ const ExportData: React.FC<{
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
}> = (props) => { }> = (props) => {
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch({
fetchAPI: queryDashboardUsageData
});
const { open, onCancel } = props || {}; const { open, onCancel } = props || {};
const intl = useIntl(); const intl = useIntl();
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({ const { FilterBar, loading, init, result, userList, modelList, query } =
range: 60 useUsageData<{
}); items: TableRow[];
}>({
raw: true
});
const handleSubmit = () => {}; const exportTableColumns: TableColumnType[] = [
{
title: intl.formatMessage({ id: 'resources.table.index' }),
width: 60,
render(text: any, row: any, index: number) {
return index + 1;
}
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.date' }),
dataIndex: 'date'
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
dataIndex: 'user_id',
render: (text: string) => {
return (
<AutoTooltip ghost>
{userList.find((item) => item.value === text)?.label}
</AutoTooltip>
);
}
},
{
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
dataIndex: 'model_id',
render: (text: string) => {
return (
<AutoTooltip ghost>
{modelList.find((item) => item.value === text)?.label || text}
</AutoTooltip>
);
}
},
{
title: 'Completion Tokens',
dataIndex: 'completion_token_count',
width: 170,
align: 'right'
},
{
title: 'Prompt Tokens',
dataIndex: 'prompt_token_count',
align: 'right',
width: 150
},
{
title: 'API Requests',
dataIndex: 'request_count',
align: 'right',
width: 150
}
];
const handleSubmit = () => {
const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`;
exportJsonToExcel({
jsonData: result.data?.items || [],
fileName: fileName,
fields: exportTableColumns
.map((col) => col.dataIndex)
.filter(Boolean) as string[],
fieldLabels: {
user_id: 'User',
model_id: 'Model',
date: 'Date',
prompt_token_count: 'Prompt Tokens',
completion_token_count: 'Completion Tokens',
request_count: 'API Requests'
},
formatMap: {
user_id: (value: string) => {
return userList.find((item) => item.value === value)?.label || value;
},
model_id: (value: string) => {
return modelList.find((item) => item.value === value)?.label || value;
}
}
});
};
useEffect(() => {
if (open) {
init();
}
}, [open]);
return ( return (
<ScrollerModal <ScrollerModal
@@ -46,7 +118,10 @@ const ExportData: React.FC<{
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={800} width={1000}
style={{
top: '10%'
}}
styles={{ styles={{
content: { content: {
padding: '0px' padding: '0px'
@@ -70,53 +145,18 @@ const ExportData: React.FC<{
></ModalFooter> ></ModalFooter>
} }
> >
<Space size={12}> <FilterBar></FilterBar>
<DatePicker.RangePicker
style={{ width: 240 }}
defaultValue={[dayjs().add(-30, 'd'), dayjs()]}
disabledDate={disabledRangeDaysDate}
presets={rangePresets}
allowClear={false}
onChange={(dates) => {
handleSearch();
}}
></DatePicker.RangePicker>
<Select
mode="multiple"
maxTagCount={1}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectuser'
})}
style={{ width: 160 }}
></Select>
<Select
mode="multiple"
maxTagCount={1}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
style={{ width: 160 }}
></Select>
</Space>
<Table <Table
columns={exportTableColumns} columns={exportTableColumns}
tableLayout={dataSource.loadend ? 'auto' : 'fixed'} tableLayout={'auto'}
style={{ width: '100%', marginTop: '16px' }} style={{ width: '100%', marginTop: '16px' }}
dataSource={dataSource.dataList} dataSource={result.data?.items || []}
loading={dataSource.loading} loading={loading}
rowKey="id" rowKey="id"
onChange={handleTableChange} virtual
pagination={{ scroll={{ y: 400 }}
showSizeChanger: true, pagination={false}
pageSize: queryParams.perPage, ></Table>
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
>
{' '}
</Table>
</ScrollerModal> </ScrollerModal>
); );
}; };
@@ -1,65 +1,70 @@
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Col, DatePicker, Row, Select, Tooltip } from 'antd'; import { Col, Row } from 'antd';
import dayjs from 'dayjs'; import { FC, useContext, useEffect, useMemo } from 'react';
import { FC, memo, useContext, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { baseColorMap } from '../../config';
import { DashboardContext } from '../../config/dashboard-context'; import { DashboardContext } from '../../config/dashboard-context';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset'; import { DashboardUsageData } from '../../config/types';
import ExportData from './export-data'; import ExportData from './export-data';
import RequestTokenInner from './request-token-inner'; import RequestTokenInner from './request-token-inner';
import TopUser from './top-user'; import TopUser from './top-user';
import useUsageData from './use-usage-data'; import useUsageData from './use-usage-data';
const FilterWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0px;
.selection {
display: flex;
align-items: center;
gap: 12px;
}
`;
const TitleWrapper = styled.div` const TitleWrapper = styled.div`
margin: 26px 0px; margin: 26px 0px;
margin-bottom: 38px; margin-bottom: 38px;
font-weight: 700; font-weight: 700;
`; `;
const dataList = [
{ label: '100M', value: 'Completion Tokens' },
{ label: '50M', value: 'Prompt Tokens' },
{ label: '120K', value: 'API Requests' }
];
const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => { const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
const intl = useIntl(); const intl = useIntl();
const [query, setQuery] = useState({
startDate: dayjs().subtract(30, 'days').format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD'),
user: [],
model: []
});
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: 60
});
const { model_usage } = useContext(DashboardContext); const { model_usage } = useContext(DashboardContext);
const [open, setOpen] = useState(false);
const { requestTokenData, topUserData } = useUsageData(model_usage || {}); const { usageData, handleOnCancel, handleExport, FilterBar, init, open } =
useUsageData<DashboardUsageData>({
raw: false,
defaultData: {
api_request_history: [],
completion_token_history: [],
prompt_token_history: []
}
});
const handleOnCancel = () => { const topUserData = useMemo(() => {
setOpen(false); const topUsers = model_usage?.top_users || [];
};
const handleExport = () => { const topUserPrompt: any = {
setOpen(true); name: 'Prompt tokens',
}; color: baseColorMap.baseR3,
data: [] as { name: string; value: number }[]
};
const topUserCompletion: any = {
name: 'Completion tokens',
color: baseColorMap.base,
data: [] as { name: string; value: number }[]
};
const topUserNames = topUsers.map((item: any) => {
topUserPrompt.data.push({
name: item.username,
value: item.prompt_token_count
});
topUserCompletion.data.push({
name: item.username,
value: item.completion_token_count
});
return item.username;
});
return {
userData: [topUserCompletion, topUserPrompt],
topUserList: [...new Set(topUserNames)] as string[]
};
}, [model_usage?.top_users]);
useEffect(() => {
init();
}, []);
return ( return (
<div> <div>
@@ -83,47 +88,13 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
<TitleWrapper> <TitleWrapper>
{intl.formatMessage({ id: 'dashboard.usage' })} {intl.formatMessage({ id: 'dashboard.usage' })}
</TitleWrapper> </TitleWrapper>
<FilterWrapper> <FilterBar></FilterBar>
<div className="selection">
<DatePicker.RangePicker
defaultValue={[dayjs().add(-30, 'd'), dayjs()]}
disabledDate={disabledRangeDaysDate}
presets={rangePresets}
allowClear={false}
style={{ width: 240 }}
></DatePicker.RangePicker>
<Select
mode="multiple"
maxTagCount={1}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectuser'
})}
style={{ width: 160 }}
></Select>
<Select
mode="multiple"
maxTagCount={1}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
style={{ width: 160 }}
></Select>
<Tooltip
title={intl.formatMessage({ id: 'common.button.export' })}
>
<Button
icon={<ExportOutlined />}
onClick={handleExport}
></Button>
</Tooltip>
</div>
</FilterWrapper>
</div> </div>
<RequestTokenInner <RequestTokenInner
onExport={handleExport} onExport={handleExport}
requestData={requestTokenData.requestData} requestData={usageData?.requestTokenData.requestData}
xAxisData={requestTokenData.xAxisData} xAxisData={usageData?.requestTokenData.xAxisData}
tokenData={requestTokenData.tokenData} tokenData={usageData?.requestTokenData.tokenData}
></RequestTokenInner> ></RequestTokenInner>
</Col> </Col>
<Col xs={24} sm={24} md={24} lg={24} xl={8} style={{ marginTop: 12 }}> <Col xs={24} sm={24} md={24} lg={24} xl={8} style={{ marginTop: 12 }}>
@@ -141,4 +112,4 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
); );
}; };
export default memo(UsageInner); export default UsageInner;
@@ -1,10 +1,10 @@
import CardWrapper from '@/components/card-wrapper'; import CardWrapper from '@/components/card-wrapper';
import { SimpleCard } from '@/components/card-wrapper/simple-card'; import { SimpleCard } from '@/components/card-wrapper/simple-card';
import MixLineBar from '@/components/echarts/mix-line-bar'; import MixLineBar from '@/components/echarts/mix-line-bar';
import { useIntl } from '@umijs/max'; import { formatLargeNumber } from '@/utils';
import { Button } from 'antd'; import { Button } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import React from 'react'; import React, { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { baseColorMap } from '../../config'; import { baseColorMap } from '../../config';
@@ -38,6 +38,11 @@ interface RequestTokenInnerProps {
data: { time: string; value: number }[]; data: { time: string; value: number }[];
}[]; }[];
xAxisData: string[]; xAxisData: string[];
overViewData?: {
requestCount: number;
completionCount: number;
promptCount: number;
};
} }
const labelFormatter = (v: any) => { const labelFormatter = (v: any) => {
@@ -46,20 +51,23 @@ const labelFormatter = (v: any) => {
const dataList = [ const dataList = [
{ {
label: '100M', label: '0',
value: 'Completion Tokens', value: 'Completion Tokens',
key: 'completionCount',
iconType: 'roundRect', iconType: 'roundRect',
color: baseColorMap.base color: baseColorMap.base
}, },
{ {
label: '50M', label: '0',
value: 'Prompt Tokens', value: 'Prompt Tokens',
key: 'promptCount',
iconType: 'roundRect', iconType: 'roundRect',
color: baseColorMap.baseR3 color: baseColorMap.baseR3
}, },
{ {
label: '120K', label: '0',
value: 'API Requests', value: 'API Requests',
key: 'requestCount',
iconType: 'circle', iconType: 'circle',
color: baseColorMap.baseR1 color: baseColorMap.baseR1
} }
@@ -73,20 +81,27 @@ const legendData = [
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => { const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
const { requestData, tokenData, xAxisData, onExport } = props; const { requestData, tokenData, xAxisData, onExport } = props;
const intl = useIntl();
const totalData = useMemo(() => {
const data: Record<string, number> = {
requestCount:
requestData[0]?.data.reduce((sum, item) => sum + item.value, 0) || 0,
completionCount:
tokenData[0]?.data.reduce((sum, item) => sum + item.value, 0) || 0,
promptCount:
tokenData[1]?.data.reduce((sum, item) => sum + item.value, 0) || 0
};
return dataList.map((item) => ({
...item,
label: formatLargeNumber(data[item.key] || 0) as string
}));
}, [requestData, tokenData]);
return ( return (
<CardWrapperBox> <CardWrapperBox>
<CardWrapper style={{ width: '100%', position: 'relative' }}> <CardWrapper style={{ width: '100%', position: 'relative' }}>
{/* <DownloadButton <SimpleCard dataList={totalData} height={80}></SimpleCard>
type="link"
icon={<ExportOutlined />}
size="small"
onClick={onExport}
>
{intl.formatMessage({ id: 'common.button.export' })}
</DownloadButton> */}
<SimpleCard dataList={dataList} height={80}></SimpleCard>
<MixLineBar <MixLineBar
chartData={{ chartData={{
line: requestData, line: requestData,
@@ -95,7 +110,7 @@ const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
seriesData={[]} seriesData={[]}
xAxisData={xAxisData} xAxisData={xAxisData}
height={360} height={360}
smooth={true} smooth={false}
legendData={legendData} legendData={legendData}
labelFormatter={labelFormatter} labelFormatter={labelFormatter}
></MixLineBar> ></MixLineBar>
@@ -104,4 +119,4 @@ const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
); );
}; };
export default React.memo(RequestTokenInner); export default RequestTokenInner;
@@ -1,6 +1,5 @@
import CardWrapper from '@/components/card-wrapper'; import CardWrapper from '@/components/card-wrapper';
import HBar from '@/components/echarts/h-bar'; import HBar from '@/components/echarts/h-bar';
import { useIntl } from '@umijs/max';
import React from 'react'; import React from 'react';
interface TopUserProps { interface TopUserProps {
@@ -9,7 +8,6 @@ interface TopUserProps {
} }
const TopUser: React.FC<TopUserProps> = (props) => { const TopUser: React.FC<TopUserProps> = (props) => {
const { userData, topUserList } = props; const { userData, topUserList } = props;
const intl = useIntl();
return ( return (
<CardWrapper> <CardWrapper>
@@ -18,4 +16,4 @@ const TopUser: React.FC<TopUserProps> = (props) => {
); );
}; };
export default React.memo(TopUser); export default TopUser;
@@ -1,144 +0,0 @@
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { baseColorMap } from '../../config';
interface RequestTokenData {
requestData: {
name: string;
color: string;
areaStyle: any;
data: { time: string; value: number }[];
}[];
tokenData: {
data: { time: string; value: number }[];
}[];
xAxisData: string[];
}
interface TopUserData {
userData: { name: string; value: number }[];
topUserList: string[];
}
const getLast30Days = () => {
const dates: string[] = [];
for (let i = 29; i >= 0; i--) {
const date = dayjs().subtract(i, 'day').format('YYYY-MM-DD');
dates.push(date);
}
return dates;
};
const generateValueMap = (list: { timestamp: number; value: number }[]) => {
return new Map(
list.map((item) => [
dayjs(item.timestamp * 1000).format('YYYY-MM-DD'),
item.value
])
);
};
const generateData = (dateRage: string[], valueMap: Map<string, number>) => {
return dateRage.map((date) => {
const value = valueMap.get(date) || 0;
return {
time: date,
value: value
};
});
};
export default function useUseageData(data: any) {
const usageData = useMemo<{
requestTokenData: RequestTokenData;
topUserData: TopUserData;
}>(() => {
const dateRange = getLast30Days();
const completionTokenHistory = data.completion_token_history || [];
const promptTokenHistory = data.prompt_token_history || [];
const apiRequestHistory = data.api_request_history || [];
const topUsers = data.top_users || [];
if (!completionTokenHistory.length) {
return {
requestTokenData: {
requestData: [],
tokenData: [],
xAxisData: []
},
topUserData: {
userData: [],
topUserList: []
}
};
}
// ========== API request ==============
const requestList: {
name: string;
color: string;
areaStyle: any;
data: { time: string; value: number }[];
} = {
name: 'API requests',
areaStyle: {
color: 'rgba(13,171,219,0.15)'
},
color: baseColorMap.baseR1,
data: generateData(dateRange, generateValueMap(apiRequestHistory))
};
// =========== token usage data ==============
const completionData: any = {
name: 'Completion tokens',
color: baseColorMap.base,
data: generateData(dateRange, generateValueMap(completionTokenHistory))
};
const promptData: any = {
name: 'Prompt tokens',
color: baseColorMap.baseR3,
data: generateData(dateRange, generateValueMap(promptTokenHistory))
};
// ========== top user data ==============
const topUserPrompt: any = {
name: 'Prompt tokens',
color: baseColorMap.baseR3,
data: [] as { name: string; value: number }[]
};
const topUserCompletion: any = {
name: 'Completion tokens',
color: baseColorMap.base,
data: [] as { name: string; value: number }[]
};
const topUserNames = topUsers.map((item: any) => {
topUserPrompt.data.push({
name: item.username,
value: item.prompt_token_count
});
topUserCompletion.data.push({
name: item.username,
value: item.completion_token_count
});
return item.username;
});
return {
requestTokenData: {
requestData: [requestList],
tokenData: [completionData, promptData],
xAxisData: dateRange
},
topUserData: {
userData: [topUserCompletion, topUserPrompt],
topUserList: [...new Set(topUserNames)] as string[]
}
};
}, [data]);
return usageData;
}
@@ -0,0 +1,379 @@
import useSelectRender from '@/components/seal-form/hooks/use-select-render';
import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import { queryUsersList } from '@/pages/users/apis';
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, DatePicker, Select, Tooltip } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useMemo, useState } from 'react';
import styled from 'styled-components';
import { queryDashboardUsageData } from '../../apis';
import { baseColorMap } from '../../config';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const FilterWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0px;
.selection {
display: flex;
align-items: center;
gap: 12px;
}
`;
interface RequestTokenData {
requestData: {
name: string;
color: string;
areaStyle: any;
data: { time: string; value: number }[];
}[];
tokenData: {
data: { time: string; value: number }[];
}[];
xAxisData: string[];
}
interface TopUserData {
userData: { name: string; value: number }[];
topUserList: string[];
}
const getLast30Days = () => {
const dates: string[] = [];
for (let i = 29; i >= 0; i--) {
const date = dayjs().subtract(i, 'day').format('YYYY-MM-DD');
dates.push(date);
}
return dates;
};
const getAllDays = (start: string, end: string) => {
const startDate = dayjs(start);
const endDate = dayjs(end);
const days: string[] = [];
for (
let d = startDate;
d.isBefore(endDate) || d.isSame(endDate, 'day');
d = d.add(1, 'day')
) {
days.push(d.format('YYYY-MM-DD'));
}
return days;
};
const generateValueMap = (list: { timestamp: number; value: number }[]) => {
return new Map(
list.map((item) => [
dayjs(item.timestamp * 1000).format('YYYY-MM-DD'),
item.value
])
);
};
const generateData = (dateRage: string[], valueMap: Map<string, number>) => {
return dateRage.map((date) => {
const value = valueMap.get(date) || 0;
return {
time: date,
value: value
};
});
};
export default function useUseageData<T>(config?: {
raw?: boolean;
defaultData?: T;
}) {
const { raw = false, defaultData } = config || {};
const intl = useIntl();
const { TagRender } = useSelectRender({
maxTagWidth: 100
});
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: 60
});
const [open, setOpen] = useState(false);
const [result, setResult] = useState<{
start_date?: string;
end_date?: string;
data: T;
}>({
start_date: dayjs().subtract(30, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
data: defaultData as T
});
const [query, setQuery] = useState<{
start_date: string;
end_date: string;
model_ids: number[];
user_ids: number[];
raw: boolean;
}>({
raw: raw,
start_date: dayjs().subtract(30, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [],
user_ids: []
});
const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]);
const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]);
const [loading, setLoading] = useState(false);
const usageData = useMemo<{
requestTokenData: RequestTokenData;
}>(() => {
if (raw) {
return {
requestTokenData: {
requestData: [],
tokenData: [],
xAxisData: []
}
};
}
const { start_date, end_date, data } = result as {
start_date?: string;
end_date?: string;
data: {
api_request_history: { timestamp: number; value: number }[];
completion_token_history: { timestamp: number; value: number }[];
prompt_token_history: { timestamp: number; value: number }[];
};
};
const dateRange =
start_date && end_date
? getAllDays(start_date, end_date)
: getLast30Days();
const completionTokenHistory = data?.completion_token_history || [];
const promptTokenHistory = data?.prompt_token_history || [];
const apiRequestHistory = data?.api_request_history || [];
if (!completionTokenHistory.length) {
return {
requestTokenData: {
requestData: [],
tokenData: [],
xAxisData: []
}
};
}
// ========== API request ==============
const requestList: {
name: string;
color: string;
areaStyle: any;
data: { time: string; value: number }[];
} = {
name: 'API requests',
areaStyle: {
color: 'rgba(13,171,219,0.15)'
},
color: baseColorMap.baseR1,
data: generateData(dateRange, generateValueMap(apiRequestHistory))
};
// =========== token usage data ==============
const completionData: any = {
name: 'Completion tokens',
color: baseColorMap.base,
data: generateData(dateRange, generateValueMap(completionTokenHistory))
};
const promptData: any = {
name: 'Prompt tokens',
color: baseColorMap.baseR3,
data: generateData(dateRange, generateValueMap(promptTokenHistory))
};
return {
requestTokenData: {
requestData: [requestList],
tokenData: [completionData, promptData],
xAxisData: dateRange
}
};
}, [result, raw]);
const fetchModelsList = async () => {
try {
const params = {
page: 1,
page_size: 100
};
const response = await queryModelsList(params);
const list = _.map(response.items || [], (item: ModelListItem) => {
return {
label: item.name,
value: item.id
};
});
setModelList(list);
} catch (error) {
setModelList([]);
}
};
const fetchUsersList = async () => {
try {
const params = {
page: 1,
page_size: 100
};
const response = await queryUsersList(params);
const list = _.map(response.items || [], (item: any) => {
return {
label: item.username,
value: item.id
};
});
setUserList(list);
} catch (error) {
setUserList([]);
}
};
const fetchUsageData = async (queryParams: any) => {
try {
setLoading(true);
const response = await queryDashboardUsageData<T>(queryParams);
setResult({
start_date: queryParams.start_date,
end_date: queryParams.end_date,
data: response
});
} catch (error) {
setResult({
start_date: queryParams.start_date,
end_date: queryParams.end_date,
data: {} as T
});
} finally {
setLoading(false);
}
};
const handleDateChange = (dates: any, dateString: string[]) => {
setQuery((pre) => {
return {
...pre,
start_date: dateString[0],
end_date: dateString[1]
};
});
fetchUsageData({
...query,
start_date: dateString[0],
end_date: dateString[1]
});
};
const handleOnCancel = () => {
setOpen(false);
};
const handleExport = () => {
setOpen(true);
};
const handleUsersChange = (value: number[]) => {
setQuery((pre) => {
return {
...pre,
user_ids: value
};
});
fetchUsageData({ ...query, user_ids: value });
};
const handleModelsChange = (value: number[]) => {
setQuery((pre) => {
return {
...pre,
model_ids: value
};
});
fetchUsageData({ ...query, model_ids: value });
};
const init = () => {
fetchUsageData(query);
fetchModelsList();
fetchUsersList();
};
const FilterBar = () => {
return (
<FilterWrapper>
<div className="selection">
<DatePicker.RangePicker
defaultValue={[dayjs().add(-30, 'd'), dayjs()]}
disabledDate={disabledRangeDaysDate}
presets={rangePresets}
allowClear={false}
style={{ width: 240 }}
value={[dayjs(query.start_date), dayjs(query.end_date)]}
onChange={handleDateChange}
></DatePicker.RangePicker>
<Select
allowClear
mode="multiple"
options={userList}
maxTagCount={1}
tagRender={TagRender}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectuser'
})}
style={{ maxWidth: 200, minWidth: 160 }}
value={query.user_ids}
onChange={handleUsersChange}
></Select>
<Select
allowClear
mode="multiple"
options={modelList}
maxTagCount={1}
tagRender={TagRender}
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
value={query.model_ids}
style={{ maxWidth: 200, minWidth: 160 }}
onChange={handleModelsChange}
></Select>
{!raw && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button icon={<ExportOutlined />} onClick={handleExport}></Button>
</Tooltip>
)}
</div>
</FilterWrapper>
);
};
return {
usageData,
result,
open,
loading,
userList,
modelList,
query,
init,
setResult,
FilterBar,
handleOnCancel,
handleExport
};
}
-49
View File
@@ -1,6 +1,3 @@
import { formatLargeNumber } from '@/utils';
import dayjs from 'dayjs';
export const overviewConfigs = [ export const overviewConfigs = [
{ {
key: 'worker_count', key: 'worker_count',
@@ -25,52 +22,6 @@ export const overviewConfigs = [
} }
]; ];
export const exportTableColumns = [
{
title: 'Date',
dataIndex: 'date',
render: (text: string) => {
return dayjs(text).format('YYYY-MM-DD');
}
},
{
title: 'User',
dataIndex: 'user',
render: (text: string) => {
return text || 'admin';
}
},
{
title: 'Model',
dataIndex: 'model',
render: (text: string) => {
return text || 'All Models';
}
},
{
title: 'Completion Tokens',
dataIndex: 'completion_tokens',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
},
{
title: 'Prompt Tokens',
dataIndex: 'prompt_tokens',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
},
{
title: 'API Requests',
dataIndex: 'request_count',
render: (text: number) => {
return formatLargeNumber(text) || 0;
}
}
];
export const baseColorMap = { export const baseColorMap = {
baseL2: 'rgba(13,171,219,0.8)', baseL2: 'rgba(13,171,219,0.8)',
baseL1: 'rgba(0,34,255,0.8)', baseL1: 'rgba(0,34,255,0.8)',
+17
View File
@@ -44,3 +44,20 @@ export interface DashboardProps {
}; };
active_models: any[]; active_models: any[];
} }
export interface DashboardUsageData {
api_request_history: any[];
completion_token_history: any[];
prompt_token_history: any[];
}
export interface TableRow {
id: number;
prompt_token_count: number;
completion_token_count: number;
operation: string;
model_id: number;
date: string;
user_id: number;
request_count: number;
}
@@ -1,3 +1,4 @@
import { useIntl } from '@umijs/max';
import { DatePickerProps } from 'antd'; import { DatePickerProps } from 'antd';
import dayjs, { type Dayjs } from 'dayjs'; import dayjs, { type Dayjs } from 'dayjs';
@@ -14,6 +15,7 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
range: number; range: number;
} { } {
const { range = 60 } = options || {}; const { range = 60 } = options || {};
const intl = useIntl();
const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month(); const getYearMonth = (date: Dayjs) => date.year() * 12 + date.month();
@@ -43,7 +45,7 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
); );
default: default:
return Math.abs(current.diff(from, 'days')) >= range; return Math.abs(current.diff(from, 'days')) > range;
} }
} }
@@ -54,9 +56,22 @@ export default function useRangePickerPreset(options?: RangePickerPreset): {
label: React.ReactNode; label: React.ReactNode;
value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]); value: [Dayjs, Dayjs] | (() => [Dayjs, Dayjs]);
}[] = [ }[] = [
{ label: 'Last 7 Days', value: [dayjs().add(-7, 'd'), dayjs()] }, {
{ label: 'Last 30 Days', value: [dayjs().add(-30, 'd'), dayjs()] }, label: intl.formatMessage({ id: 'dashboard.usage.datePicker.last7days' }),
{ label: 'Last 60 Days', value: [dayjs().add(-60, 'd'), dayjs()] } value: [dayjs().add(-7, 'd'), dayjs()]
},
{
label: intl.formatMessage({
id: 'dashboard.usage.datePicker.last30days'
}),
value: [dayjs().add(-30, 'd'), dayjs()]
},
{
label: intl.formatMessage({
id: 'dashboard.usage.datePicker.last60days'
}),
value: [dayjs().add(-60, 'd'), dayjs()]
}
]; ];
return { return {
+1 -1
View File
@@ -41,7 +41,7 @@ export async function queryModelsInstances(
} }
export async function queryModelsList( export async function queryModelsList(
params: Global.SearchParams, params: Global.SearchParams,
options?: any options?: Record<string, any>
) { ) {
return request<Global.PageResponse<ListItem>>( return request<Global.PageResponse<ListItem>>(
`${MODELS_API}?${qs.stringify(params)}`, `${MODELS_API}?${qs.stringify(params)}`,
+55
View File
@@ -1,3 +1,4 @@
import { saveAs } from 'file-saver';
import XLSX from 'xlsx'; import XLSX from 'xlsx';
export default function readExcelContent(file: File): Promise<string> { export default function readExcelContent(file: File): Promise<string> {
@@ -14,3 +15,57 @@ export default function readExcelContent(file: File): Promise<string> {
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}); });
} }
interface FormatMap {
[key: string]: (value: any, row?: any) => any;
}
/**
* @param jsonData raw JSON data to export
* @param fields export fields (keys in the JSON objects)
* @param fieldLabels custom the table header labels
* @param formatMap custom the cell format functions
* @param fileName file name for the exported Excel file
*/
export function exportJsonToExcel(data: {
jsonData: any[];
fields: string[];
fieldLabels?: Record<string, string>;
formatMap?: FormatMap;
fileName: string;
}) {
const {
jsonData,
fields,
fieldLabels,
formatMap,
fileName = 'data.xlsx'
} = data;
// 1. Process data: filter fields and format values
const formattedData = jsonData.map((row) => {
const result: Record<string, any> = {};
for (const field of fields) {
const rawValue = row[field];
const formatFn = formatMap?.[field];
result[field] = formatFn ? formatFn(rawValue, row) : rawValue;
}
return result;
});
// 2. Convert to worksheet
const worksheet = XLSX.utils.json_to_sheet(formattedData, { header: fields });
// 3. Add headers if provided
if (fieldLabels) {
const headerRow = fields.map((key) => fieldLabels[key] || key);
XLSX.utils.sheet_add_aoa(worksheet, [headerRow], { origin: 'A1' });
}
// 4. Create workbook and write to file
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([excelBuffer], { type: 'application/octet-stream' });
saveAs(blob, fileName);
}
+1
View File
@@ -25,5 +25,6 @@ declare module 'colorthief';
declare module 'vibrant'; declare module 'vibrant';
declare module 'node-vibrant'; declare module 'node-vibrant';
declare module 'lamejs'; declare module 'lamejs';
declare module 'file-saver';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;