555 lines
17 KiB
JavaScript
Raw Normal View History

import React, { useContext, useEffect, useRef, useState } from 'react';
2024-03-23 20:22:00 +08:00
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
import { VChart } from "@visactor/react-vchart";
2024-01-10 17:49:55 +08:00
import {
2024-03-23 21:24:39 +08:00
API,
isAdmin,
showError,
timestamp2string,
timestamp2string1,
} from '../../helpers';
import {
getQuotaWithUnit,
modelColorMap,
renderNumber,
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
modelToColor,
2024-03-23 21:24:39 +08:00
} from '../../helpers/render';
import { UserContext } from '../../context/User/index.js';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
2024-01-07 18:31:14 +08:00
const Detail = (props) => {
const { t } = useTranslation();
2024-03-23 21:24:39 +08:00
const formRef = useRef();
let now = new Date();
const [userState, userDispatch] = useContext(UserContext);
const [styleState, styleDispatch] = useContext(StyleContext);
2024-03-23 21:24:39 +08:00
const [inputs, setInputs] = useState({
username: '',
token_name: '',
model_name: '',
start_timestamp:
localStorage.getItem('data_export_default_time') === 'hour'
? timestamp2string(now.getTime() / 1000 - 86400)
: localStorage.getItem('data_export_default_time') === 'week'
? timestamp2string(now.getTime() / 1000 - 86400 * 30)
: timestamp2string(now.getTime() / 1000 - 86400 * 7),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '',
data_export_default_time: '',
});
const { username, model_name, start_timestamp, end_timestamp, channel } =
inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [consumeTokens, setConsumeTokens] = useState(0);
2024-03-23 21:24:39 +08:00
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
);
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
const [lineData, setLineData] = useState([]);
const [spec_pie, setSpecPie] = useState({
type: 'pie',
data: [{
id: 'id0',
values: pieData
}],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
2024-03-23 21:24:39 +08:00
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: t('模型调用次数占比'),
subtext: `${t('总计')}${renderNumber(times)}`,
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
});
const [spec_line, setSpecLine] = useState({
type: 'bar',
data: [{
id: 'barData',
values: lineData
}],
2024-03-23 21:24:39 +08:00
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
stack: true,
legends: {
visible: true,
2024-04-18 19:37:52 +08:00
selectMode: 'single',
2024-03-23 21:24:39 +08:00
},
title: {
visible: true,
text: t('模型消耗分布'),
subtext: `${t('总计')}${renderQuota(consumeQuota, 2)}`,
2024-03-23 21:24:39 +08:00
},
bar: {
state: {
hover: {
stroke: '#000',
lineWidth: 1,
2024-01-07 18:31:14 +08:00
},
2024-03-23 21:24:39 +08:00
},
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['Model'],
value: (datum) => renderQuotaNumberWithDigit(datum['rawQuota'] || 0, 4),
2024-03-23 21:24:39 +08:00
},
2024-01-07 18:31:14 +08:00
],
2024-03-23 21:24:39 +08:00
},
dimension: {
content: [
{
key: (datum) => datum['Model'],
value: (datum) => datum['rawQuota'] || 0,
2024-03-23 21:24:39 +08:00
},
],
updateContent: (array) => {
array.sort((a, b) => b.value - a.value);
let sum = 0;
for (let i = 0; i < array.length; i++) {
if (array[i].key == "其他") {
continue;
}
let value = parseFloat(array[i].value);
if (isNaN(value)) {
value = 0;
}
if (array[i].datum && array[i].datum.TimeSum) {
sum = array[i].datum.TimeSum;
}
array[i].value = renderQuota(value, 4);
2024-03-23 21:24:39 +08:00
}
array.unshift({
key: t('总计'),
value: renderQuota(sum, 4),
2024-03-23 21:24:39 +08:00
});
return array;
2024-01-07 18:31:14 +08:00
},
2024-03-23 21:24:39 +08:00
},
},
color: {
specified: modelColorMap,
},
});
2024-03-23 21:24:39 +08:00
// 添加一个新的状态来存储模型-颜色映射
const [modelColors, setModelColors] = useState({});
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
2024-03-23 21:24:39 +08:00
};
const loadQuotaData = async () => {
2024-03-23 21:24:39 +08:00
setLoading(true);
try {
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
2024-03-23 21:24:39 +08:00
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
});
}
// sort created_at
data.sort((a, b) => a.created_at - b.created_at);
updateChartData(data);
} else {
showError(message);
2024-03-23 21:24:39 +08:00
}
} finally {
setLoading(false);
2024-03-23 21:24:39 +08:00
}
};
2024-01-07 18:31:14 +08:00
2024-03-23 21:24:39 +08:00
const refresh = async () => {
await loadQuotaData();
2024-03-23 21:24:39 +08:00
};
2024-01-07 18:31:14 +08:00
2024-03-23 21:24:39 +08:00
const initChart = async () => {
await loadQuotaData();
2024-03-23 21:24:39 +08:00
};
2024-01-07 18:31:14 +08:00
const updateChartData = (data) => {
let newPieData = [];
let newLineData = [];
let totalQuota = 0;
let totalTimes = 0;
let uniqueModels = new Set();
let totalTokens = 0;
// 收集所有唯一的模型名称
data.forEach(item => {
uniqueModels.add(item.model_name);
totalTokens += item.token_used;
totalQuota += item.quota;
totalTimes += item.count;
});
// 处理颜色映射
const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => {
newModelColors[modelName] = modelColorMap[modelName] ||
modelColors[modelName] ||
modelToColor(modelName);
});
setModelColors(newModelColors);
// 按时间和模型聚合数据
let aggregatedData = new Map();
data.forEach(item => {
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
const modelKey = item.model_name;
const key = `${timeKey}-${modelKey}`;
if (!aggregatedData.has(key)) {
aggregatedData.set(key, {
time: timeKey,
model: modelKey,
quota: 0,
count: 0
2024-03-23 21:24:39 +08:00
});
}
const existing = aggregatedData.get(key);
existing.quota += item.quota;
existing.count += item.count;
});
// 处理饼图数据
let modelTotals = new Map();
for (let [_, value] of aggregatedData) {
if (!modelTotals.has(value.model)) {
modelTotals.set(value.model, 0);
}
modelTotals.set(value.model, modelTotals.get(value.model) + value.count);
}
newPieData = Array.from(modelTotals).map(([model, count]) => ({
type: model,
value: count
}));
// 生成时间点序列
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
if (timePoints.length < 7) {
const lastTime = Math.max(...data.map(item => item.created_at));
const interval = dataExportDefaultTime === 'hour' ? 3600
: dataExportDefaultTime === 'day' ? 86400
: 604800;
timePoints = Array.from({length: 7}, (_, i) =>
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
);
}
// 生成柱状图数据
timePoints.forEach(time => {
// 为每个时间点收集所有模型的数据
let timeData = Array.from(uniqueModels).map(model => {
const key = `${time}-${model}`;
const aggregated = aggregatedData.get(key);
return {
Time: time,
Model: model,
rawQuota: aggregated?.quota || 0,
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
};
});
// 计算该时间点的总计
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
// 按照 rawQuota 从大到小排序
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
// 为每个数据点添加该时间的总计
timeData = timeData.map(item => ({
...item,
TimeSum: timeSum
}));
// 将排序后的数据添加到 newLineData
newLineData.push(...timeData);
});
2024-01-07 18:31:14 +08:00
// 排序
newPieData.sort((a, b) => b.value - a.value);
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
2024-01-10 17:49:55 +08:00
// 更新图表配置和数据
setSpecPie(prev => ({
...prev,
data: [{ id: 'id0', values: newPieData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderNumber(totalTimes)}`
},
color: {
specified: newModelColors
}
}));
2024-01-10 17:49:55 +08:00
setSpecLine(prev => ({
...prev,
data: [{ id: 'barData', values: newLineData }],
title: {
...prev.title,
subtext: `${t('总计')}${renderQuota(totalQuota, 2)}`
},
color: {
specified: newModelColors
}
}));
setPieData(newPieData);
setLineData(newLineData);
setConsumeQuota(totalQuota);
setTimes(totalTimes);
setConsumeTokens(totalTokens);
};
const getUserData = async () => {
let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data;
if (success) {
userDispatch({type: 'login', payload: data});
} else {
showError(message);
}
2024-03-23 21:24:39 +08:00
};
2024-01-10 17:49:55 +08:00
2024-03-23 21:24:39 +08:00
useEffect(() => {
getUserData()
2024-03-23 21:24:39 +08:00
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
});
initialized.current = true;
initChart();
}
2024-03-23 21:24:39 +08:00
}, []);
2024-03-23 21:24:39 +08:00
return (
<>
<Layout>
<Layout.Header>
<h3>{t('数据看板')}</h3>
2024-03-23 21:24:39 +08:00
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.DatePicker
field='start_timestamp'
label={t('起始时间')}
2024-03-23 21:24:39 +08:00
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) =>
handleInputChange(value, 'start_timestamp')
}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label={t('结束时间')}
2024-03-23 21:24:39 +08:00
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
<Form.Select
field='data_export_default_time'
label={t('时间粒度')}
2024-03-23 21:24:39 +08:00
style={{ width: 176 }}
initValue={dataExportDefaultTime}
placeholder={t('时间粒度')}
2024-03-23 21:24:39 +08:00
name='data_export_default_time'
optionList={[
{ label: t('小时'), value: 'hour' },
{ label: t('天'), value: 'day' },
{ label: t('周'), value: 'week' },
2024-03-23 21:24:39 +08:00
]}
onChange={(value) =>
handleInputChange(value, 'data_export_default_time')
}
></Form.Select>
{isAdminUser && (
<>
<Form.Input
field='username'
label={t('用户名称')}
2024-03-23 21:24:39 +08:00
style={{ width: 176 }}
value={username}
placeholder={t('可选值')}
2024-03-23 21:24:39 +08:00
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Button
label={t('查询')}
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
style={{ marginTop: 24 }}
>
{t('查询')}
</Button>
2024-03-23 21:24:39 +08:00
<Form.Section>
</Form.Section>
</>
</Form>
<Spin spinning={loading}>
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
<Col span={styleState.isMobile?24:8}>
<Card className='panel-desc-card'>
<Descriptions row size="small">
<Descriptions.Item itemKey={t('当前余额')}>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('历史消耗')}>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile?24:8}>
<Card>
<Descriptions row size="small">
<Descriptions.Item itemKey={t('统计额度')}>
{renderQuota(consumeQuota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计Tokens')}>
{consumeTokens}
</Descriptions.Item>
<Descriptions.Item itemKey={t('统计次数')}>
{times}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={styleState.isMobile ? 24 : 8}>
<Card>
<Descriptions row size='small'>
<Descriptions.Item itemKey={t('平均RPM')}>
{(times /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('平均TPM')}>
{(consumeTokens /
((Date.parse(end_timestamp) -
Date.parse(start_timestamp)) /
60000)).toFixed(3)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Card style={{marginTop: 20}}>
<Tabs type="line" defaultActiveKey="1">
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
<div style={{ height: 500 }}>
<VChart
spec={spec_line}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
<div style={{ height: 500 }}>
<VChart
spec={spec_pie}
option={{ mode: "desktop-browser" }}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card>
2024-03-23 21:24:39 +08:00
</Spin>
</Layout.Content>
</Layout>
</>
);
2024-01-07 18:31:14 +08:00
};
export default Detail;