feat: model usage
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
Generated
+7
@@ -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'}
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 ==========
|
||||||
|
|||||||
@@ -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 ==========
|
||||||
|
|||||||
@@ -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 天'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)}`,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user