feat: model usage
This commit is contained in:
@@ -1,40 +1,112 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
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 { DatePicker, Select, Space, Table } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
import { queryDashboardUsageData } from '../../apis';
|
||||
import { exportTableColumns } from '../../config';
|
||||
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
|
||||
import { Table, TableColumnType } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { TableRow } from '../../config/types';
|
||||
import useUsageData from './use-usage-data';
|
||||
|
||||
const ExportData: React.FC<{
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
}> = (props) => {
|
||||
const {
|
||||
dataSource,
|
||||
rowSelection,
|
||||
queryParams,
|
||||
modalRef,
|
||||
handleDelete,
|
||||
handleDeleteBatch,
|
||||
fetchData,
|
||||
handlePageChange,
|
||||
handleTableChange,
|
||||
handleSearch,
|
||||
handleNameChange
|
||||
} = useTableFetch({
|
||||
fetchAPI: queryDashboardUsageData
|
||||
});
|
||||
const { open, onCancel } = props || {};
|
||||
const intl = useIntl();
|
||||
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
|
||||
range: 60
|
||||
});
|
||||
const { FilterBar, loading, init, result, userList, modelList, query } =
|
||||
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 (
|
||||
<ScrollerModal
|
||||
@@ -46,7 +118,10 @@ const ExportData: React.FC<{
|
||||
closeIcon={true}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={800}
|
||||
width={1000}
|
||||
style={{
|
||||
top: '10%'
|
||||
}}
|
||||
styles={{
|
||||
content: {
|
||||
padding: '0px'
|
||||
@@ -70,53 +145,18 @@ const ExportData: React.FC<{
|
||||
></ModalFooter>
|
||||
}
|
||||
>
|
||||
<Space size={12}>
|
||||
<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>
|
||||
<FilterBar></FilterBar>
|
||||
<Table
|
||||
columns={exportTableColumns}
|
||||
tableLayout={dataSource.loadend ? 'auto' : 'fixed'}
|
||||
tableLayout={'auto'}
|
||||
style={{ width: '100%', marginTop: '16px' }}
|
||||
dataSource={dataSource.dataList}
|
||||
loading={dataSource.loading}
|
||||
dataSource={result.data?.items || []}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSize: queryParams.perPage,
|
||||
current: queryParams.page,
|
||||
total: dataSource.total,
|
||||
hideOnSinglePage: queryParams.perPage === 10,
|
||||
onChange: handlePageChange
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
</Table>
|
||||
virtual
|
||||
scroll={{ y: 400 }}
|
||||
pagination={false}
|
||||
></Table>
|
||||
</ScrollerModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,65 +1,70 @@
|
||||
import { ExportOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Col, DatePicker, Row, Select, Tooltip } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { FC, memo, useContext, useState } from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import { FC, useContext, useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { baseColorMap } from '../../config';
|
||||
import { DashboardContext } from '../../config/dashboard-context';
|
||||
import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
|
||||
import { DashboardUsageData } from '../../config/types';
|
||||
import ExportData from './export-data';
|
||||
import RequestTokenInner from './request-token-inner';
|
||||
import TopUser from './top-user';
|
||||
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`
|
||||
margin: 26px 0px;
|
||||
margin-bottom: 38px;
|
||||
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 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 [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 = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const topUserData = useMemo(() => {
|
||||
const topUsers = model_usage?.top_users || [];
|
||||
|
||||
const handleExport = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
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 {
|
||||
userData: [topUserCompletion, topUserPrompt],
|
||||
topUserList: [...new Set(topUserNames)] as string[]
|
||||
};
|
||||
}, [model_usage?.top_users]);
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -83,47 +88,13 @@ const UsageInner: FC<{ paddingRight: string }> = ({ paddingRight }) => {
|
||||
<TitleWrapper>
|
||||
{intl.formatMessage({ id: 'dashboard.usage' })}
|
||||
</TitleWrapper>
|
||||
<FilterWrapper>
|
||||
<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>
|
||||
<FilterBar></FilterBar>
|
||||
</div>
|
||||
<RequestTokenInner
|
||||
onExport={handleExport}
|
||||
requestData={requestTokenData.requestData}
|
||||
xAxisData={requestTokenData.xAxisData}
|
||||
tokenData={requestTokenData.tokenData}
|
||||
requestData={usageData?.requestTokenData.requestData}
|
||||
xAxisData={usageData?.requestTokenData.xAxisData}
|
||||
tokenData={usageData?.requestTokenData.tokenData}
|
||||
></RequestTokenInner>
|
||||
</Col>
|
||||
<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 { SimpleCard } from '@/components/card-wrapper/simple-card';
|
||||
import MixLineBar from '@/components/echarts/mix-line-bar';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
import { Button } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { baseColorMap } from '../../config';
|
||||
|
||||
@@ -38,6 +38,11 @@ interface RequestTokenInnerProps {
|
||||
data: { time: string; value: number }[];
|
||||
}[];
|
||||
xAxisData: string[];
|
||||
overViewData?: {
|
||||
requestCount: number;
|
||||
completionCount: number;
|
||||
promptCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
const labelFormatter = (v: any) => {
|
||||
@@ -46,20 +51,23 @@ const labelFormatter = (v: any) => {
|
||||
|
||||
const dataList = [
|
||||
{
|
||||
label: '100M',
|
||||
label: '0',
|
||||
value: 'Completion Tokens',
|
||||
key: 'completionCount',
|
||||
iconType: 'roundRect',
|
||||
color: baseColorMap.base
|
||||
},
|
||||
{
|
||||
label: '50M',
|
||||
label: '0',
|
||||
value: 'Prompt Tokens',
|
||||
key: 'promptCount',
|
||||
iconType: 'roundRect',
|
||||
color: baseColorMap.baseR3
|
||||
},
|
||||
{
|
||||
label: '120K',
|
||||
label: '0',
|
||||
value: 'API Requests',
|
||||
key: 'requestCount',
|
||||
iconType: 'circle',
|
||||
color: baseColorMap.baseR1
|
||||
}
|
||||
@@ -73,20 +81,27 @@ const legendData = [
|
||||
|
||||
const RequestTokenInner: React.FC<RequestTokenInnerProps> = (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 (
|
||||
<CardWrapperBox>
|
||||
<CardWrapper style={{ width: '100%', position: 'relative' }}>
|
||||
{/* <DownloadButton
|
||||
type="link"
|
||||
icon={<ExportOutlined />}
|
||||
size="small"
|
||||
onClick={onExport}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.export' })}
|
||||
</DownloadButton> */}
|
||||
<SimpleCard dataList={dataList} height={80}></SimpleCard>
|
||||
<SimpleCard dataList={totalData} height={80}></SimpleCard>
|
||||
<MixLineBar
|
||||
chartData={{
|
||||
line: requestData,
|
||||
@@ -95,7 +110,7 @@ const RequestTokenInner: React.FC<RequestTokenInnerProps> = (props) => {
|
||||
seriesData={[]}
|
||||
xAxisData={xAxisData}
|
||||
height={360}
|
||||
smooth={true}
|
||||
smooth={false}
|
||||
legendData={legendData}
|
||||
labelFormatter={labelFormatter}
|
||||
></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 HBar from '@/components/echarts/h-bar';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React from 'react';
|
||||
|
||||
interface TopUserProps {
|
||||
@@ -9,7 +8,6 @@ interface TopUserProps {
|
||||
}
|
||||
const TopUser: React.FC<TopUserProps> = (props) => {
|
||||
const { userData, topUserList } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user