mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
Ai proxy monitor (#5009)
* Time granularity and Table of a single model (#4990) * Aiproxy ModelBoard * Add components LineChartComponent and Make some revisions * Time granularity and Table of a single model * Modify the logic and sort the tables in ascending or descending order * Use theme and present in seconds * Add the channel name section (#5005) * Add components LineChartComponent and Make some revisions * Time granularity and Table of a single model * Modify the logic and sort the tables in ascending or descending order * Add the channel name section * The channel_name is transmitted from the outer layer * Restore the channel * perf: dashboard code * perf: ai proxy monitor * code --------- Co-authored-by: Zhuangzai fa <143257420+ctrlz526@users.noreply.github.com>
This commit is contained in:
@@ -9,8 +9,8 @@ weight: 788
|
|||||||
|
|
||||||
## 🚀 新增内容
|
## 🚀 新增内容
|
||||||
|
|
||||||
1. AI proxy 服务,支持以图表形式展示模型调用情况。
|
1. AI proxy 监控完善,支持以图表/表格形式查看模型调用和性能情况。
|
||||||
1. 商业版支持知识库分块时,LLM 进行自动分段识别。
|
2. 商业版支持知识库分块时,LLM 进行自动分段识别。
|
||||||
|
|
||||||
## ⚙️ 优化
|
## ⚙️ 优化
|
||||||
|
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
"aipoint_usage": "AI points",
|
"aipoint_usage": "AI points",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"api_key": "API key",
|
"api_key": "API key",
|
||||||
|
"avg_response_time": "Average call time (seconds)",
|
||||||
|
"avg_ttfb": "Average first word duration (seconds)",
|
||||||
"azure": "Azure",
|
"azure": "Azure",
|
||||||
"base_url": "Base url",
|
"base_url": "Base url",
|
||||||
"channel_name": "Channel",
|
"channel_name": "Channel",
|
||||||
@@ -14,14 +16,28 @@
|
|||||||
"channel_status_enabled": "Enable",
|
"channel_status_enabled": "Enable",
|
||||||
"channel_status_unknown": "unknown",
|
"channel_status_unknown": "unknown",
|
||||||
"channel_type": "Manufacturer",
|
"channel_type": "Manufacturer",
|
||||||
|
"chart_mode_cumulative": "Cumulative",
|
||||||
|
"chart_mode_incremental": "Incremental",
|
||||||
"clear_model": "Clear the model",
|
"clear_model": "Clear the model",
|
||||||
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
|
||||||
"copy_model_id_success": "Copyed model id",
|
"copy_model_id_success": "Copyed model id",
|
||||||
"create_channel": "Added channels",
|
"create_channel": "Added channels",
|
||||||
|
"dashboard_call_trend": "Model Call Trend",
|
||||||
|
"dashboard_channel": "Channel",
|
||||||
|
"dashboard_cost_trend": "Cost Consumption",
|
||||||
"dashboard_error_calls": "Error Calls",
|
"dashboard_error_calls": "Error Calls",
|
||||||
|
"dashboard_input_tokens": "Input Tokens",
|
||||||
"dashboard_model": "Model",
|
"dashboard_model": "Model",
|
||||||
|
"dashboard_no_data": "No data available",
|
||||||
|
"dashboard_output_tokens": "Output Tokens",
|
||||||
"dashboard_points": "points",
|
"dashboard_points": "points",
|
||||||
|
"dashboard_success_calls": "Success Calls",
|
||||||
|
"dashboard_token_trend": "Token Usage Trend",
|
||||||
"dashboard_token_usage": "Tokens",
|
"dashboard_token_usage": "Tokens",
|
||||||
|
"dashboard_total_calls": "Total Calls:",
|
||||||
|
"dashboard_total_cost": "Total Cost",
|
||||||
|
"dashboard_total_cost_label": "Total Cost:",
|
||||||
|
"dashboard_total_tokens": "Total Tokens",
|
||||||
"default_url": "Default address",
|
"default_url": "Default address",
|
||||||
"detail": "Detail",
|
"detail": "Detail",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
@@ -38,7 +54,9 @@
|
|||||||
"mapping": "Model Mapping",
|
"mapping": "Model Mapping",
|
||||||
"mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.",
|
"mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.",
|
||||||
"maxToken_tip": "Model max_tokens parameter",
|
"maxToken_tip": "Model max_tokens parameter",
|
||||||
|
"max_rpm": "Max RPM (Requests Per Minute)",
|
||||||
"max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.",
|
"max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.",
|
||||||
|
"max_tpm": "Max TPM (Tokens Per Minute)",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"model_error_rate": "Error rate",
|
"model_error_rate": "Error rate",
|
||||||
"model_error_request_times": "Number of failures",
|
"model_error_request_times": "Number of failures",
|
||||||
@@ -60,7 +78,15 @@
|
|||||||
"selected_model_empty": "Choose at least one model",
|
"selected_model_empty": "Choose at least one model",
|
||||||
"start_test": "Batch test {{num}} models",
|
"start_test": "Batch test {{num}} models",
|
||||||
"test_failed": "There are {{num}} models that report errors",
|
"test_failed": "There are {{num}} models that report errors",
|
||||||
|
"timespan_day": "Day",
|
||||||
|
"timespan_hour": "Hour",
|
||||||
|
"timespan_label": "Time Granularity",
|
||||||
|
"timespan_minute": "Minute",
|
||||||
|
"total_call_volume": "Request amount",
|
||||||
|
"view_chart": "Chart",
|
||||||
|
"view_table": "Table",
|
||||||
"vlm_model": "Vlm",
|
"vlm_model": "Vlm",
|
||||||
"vlm_model_tip": "Used to generate additional indexing of images in a document in the knowledge base",
|
"vlm_model_tip": "Used to generate additional indexing of images in a document in the knowledge base",
|
||||||
|
"volunme_of_failed_calls": "Error amount",
|
||||||
"waiting_test": "Waiting for testing"
|
"waiting_test": "Waiting for testing"
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Hunyuan": "腾讯混元",
|
"Hunyuan": "腾讯混元",
|
||||||
|
"aipoint_usage": "积分消耗",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"api_key": "API 密钥",
|
"api_key": "API 密钥",
|
||||||
|
"avg_response_time": "平均调用时长 (秒)",
|
||||||
|
"avg_ttfb": "平均首字时长 (秒)",
|
||||||
"azure": "微软 Azure",
|
"azure": "微软 Azure",
|
||||||
"base_url": "代理地址",
|
"base_url": "代理地址",
|
||||||
"channel_name": "渠道名",
|
"channel_name": "渠道名",
|
||||||
@@ -13,15 +16,28 @@
|
|||||||
"channel_status_enabled": "启用",
|
"channel_status_enabled": "启用",
|
||||||
"channel_status_unknown": "未知",
|
"channel_status_unknown": "未知",
|
||||||
"channel_type": "厂商",
|
"channel_type": "厂商",
|
||||||
|
"chart_mode_cumulative": "累积",
|
||||||
|
"chart_mode_incremental": "分时",
|
||||||
"clear_model": "清空模型",
|
"clear_model": "清空模型",
|
||||||
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
|
||||||
"copy_model_id_success": "已复制模型id",
|
"copy_model_id_success": "已复制模型id",
|
||||||
"create_channel": "新增渠道",
|
"create_channel": "新增渠道",
|
||||||
"aipoint_usage": "积分消耗",
|
"dashboard_call_trend": "模型调用次数趋势",
|
||||||
|
"dashboard_channel": "渠道",
|
||||||
|
"dashboard_cost_trend": "积分消耗",
|
||||||
"dashboard_error_calls": "错误次数",
|
"dashboard_error_calls": "错误次数",
|
||||||
|
"dashboard_input_tokens": "输入Tokens",
|
||||||
"dashboard_model": "模型",
|
"dashboard_model": "模型",
|
||||||
|
"dashboard_no_data": "暂无数据",
|
||||||
|
"dashboard_output_tokens": "输出Tokens",
|
||||||
"dashboard_points": "积分",
|
"dashboard_points": "积分",
|
||||||
|
"dashboard_success_calls": "成功次数",
|
||||||
|
"dashboard_token_trend": "Tokens使用趋势",
|
||||||
"dashboard_token_usage": "Tokens 消耗",
|
"dashboard_token_usage": "Tokens 消耗",
|
||||||
|
"dashboard_total_calls": "总调用次数:",
|
||||||
|
"dashboard_total_cost": "总成本",
|
||||||
|
"dashboard_total_cost_label": "总成本:",
|
||||||
|
"dashboard_total_tokens": "总Tokens",
|
||||||
"default_url": "默认地址",
|
"default_url": "默认地址",
|
||||||
"detail": "详情",
|
"detail": "详情",
|
||||||
"duration": "耗时",
|
"duration": "耗时",
|
||||||
@@ -38,7 +54,9 @@
|
|||||||
"mapping": "模型映射",
|
"mapping": "模型映射",
|
||||||
"mapping_tip": "需填写一个有效 Json。可在向实际地址发送请求时,对模型进行映射。例如:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\n当 FastGPT 请求 gpt-4o 模型时,会向实际地址发送 gpt-4o-test 的模型,而不是 gpt-4o。",
|
"mapping_tip": "需填写一个有效 Json。可在向实际地址发送请求时,对模型进行映射。例如:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\n当 FastGPT 请求 gpt-4o 模型时,会向实际地址发送 gpt-4o-test 的模型,而不是 gpt-4o。",
|
||||||
"maxToken_tip": "模型 max_tokens 参数",
|
"maxToken_tip": "模型 max_tokens 参数",
|
||||||
|
"max_rpm": "最大RPM (每分钟请求数)",
|
||||||
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
|
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
|
||||||
|
"max_tpm": "最大TPM (每分钟Token数)",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
"model_error_rate": "失败率",
|
"model_error_rate": "失败率",
|
||||||
"model_error_request_times": "失败次数",
|
"model_error_request_times": "失败次数",
|
||||||
@@ -60,7 +78,15 @@
|
|||||||
"selected_model_empty": "至少选择一个模型",
|
"selected_model_empty": "至少选择一个模型",
|
||||||
"start_test": "批量测试{{num}}个模型",
|
"start_test": "批量测试{{num}}个模型",
|
||||||
"test_failed": "有{{num}}个模型报错",
|
"test_failed": "有{{num}}个模型报错",
|
||||||
|
"timespan_day": "天",
|
||||||
|
"timespan_hour": "小时",
|
||||||
|
"timespan_label": "时间颗粒度",
|
||||||
|
"timespan_minute": "分钟",
|
||||||
|
"total_call_volume": "调用总量",
|
||||||
|
"view_chart": "图表",
|
||||||
|
"view_table": "表格",
|
||||||
"vlm_model": "图片理解模型",
|
"vlm_model": "图片理解模型",
|
||||||
"vlm_model_tip": "用于知识库中对文档中的图片进行额外的索引生成",
|
"vlm_model_tip": "用于知识库中对文档中的图片进行额外的索引生成",
|
||||||
|
"volunme_of_failed_calls": "调用失败量",
|
||||||
"waiting_test": "等待测试"
|
"waiting_test": "等待测试"
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
"aipoint_usage": "積分消耗",
|
"aipoint_usage": "積分消耗",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"api_key": "API 金鑰",
|
"api_key": "API 金鑰",
|
||||||
|
"avg_response_time": "平均調用時長 (秒)",
|
||||||
|
"avg_ttfb": "平均首字時長 (秒)",
|
||||||
"azure": "Azure",
|
"azure": "Azure",
|
||||||
"base_url": "代理地址",
|
"base_url": "代理地址",
|
||||||
"channel_name": "管道名稱",
|
"channel_name": "管道名稱",
|
||||||
@@ -14,11 +16,28 @@
|
|||||||
"channel_status_enabled": "啟用",
|
"channel_status_enabled": "啟用",
|
||||||
"channel_status_unknown": "未知",
|
"channel_status_unknown": "未知",
|
||||||
"channel_type": "廠商",
|
"channel_type": "廠商",
|
||||||
|
"chart_mode_cumulative": "累積",
|
||||||
|
"chart_mode_incremental": "分時",
|
||||||
"clear_model": "清空模型",
|
"clear_model": "清空模型",
|
||||||
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
|
||||||
"copy_model_id_success": "已復制模型 id",
|
"copy_model_id_success": "已復制模型 id",
|
||||||
"create_channel": "新增管道",
|
"create_channel": "新增管道",
|
||||||
|
"dashboard_call_trend": "模型呼叫次數趨勢",
|
||||||
|
"dashboard_channel": "管道",
|
||||||
|
"dashboard_cost_trend": "積分消耗",
|
||||||
|
"dashboard_error_calls": "錯誤次數",
|
||||||
|
"dashboard_input_tokens": "輸入Tokens",
|
||||||
|
"dashboard_model": "模型",
|
||||||
|
"dashboard_no_data": "暫無資料",
|
||||||
|
"dashboard_output_tokens": "輸出Tokens",
|
||||||
|
"dashboard_points": "積分",
|
||||||
|
"dashboard_success_calls": "成功次數",
|
||||||
|
"dashboard_token_trend": "Tokens使用趨勢",
|
||||||
"dashboard_token_usage": "Tokens 消耗",
|
"dashboard_token_usage": "Tokens 消耗",
|
||||||
|
"dashboard_total_calls": "總呼叫次數:",
|
||||||
|
"dashboard_total_cost": "總成本",
|
||||||
|
"dashboard_total_cost_label": "總成本:",
|
||||||
|
"dashboard_total_tokens": "總Tokens",
|
||||||
"default_url": "預設地址",
|
"default_url": "預設地址",
|
||||||
"detail": "詳細資訊",
|
"detail": "詳細資訊",
|
||||||
"duration": "耗時",
|
"duration": "耗時",
|
||||||
@@ -35,7 +54,9 @@
|
|||||||
"mapping": "模型對映",
|
"mapping": "模型對映",
|
||||||
"mapping_tip": "需填寫一個有效 Json。\n可在向實際地址傳送請求時,對模型進行對映。\n例如:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\n當 FastGPT 請求 gpt-4o 模型時,會向實際地址傳送 gpt-4o-test 的模型,而不是 gpt-4o。",
|
"mapping_tip": "需填寫一個有效 Json。\n可在向實際地址傳送請求時,對模型進行對映。\n例如:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\n當 FastGPT 請求 gpt-4o 模型時,會向實際地址傳送 gpt-4o-test 的模型,而不是 gpt-4o。",
|
||||||
"maxToken_tip": "模型 max_tokens 參數",
|
"maxToken_tip": "模型 max_tokens 參數",
|
||||||
|
"max_rpm": "最大RPM (每分鐘請求數)",
|
||||||
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
|
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
|
||||||
|
"max_tpm": "最大TPM (每分鐘Token數)",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
"model_error_rate": "失敗率",
|
"model_error_rate": "失敗率",
|
||||||
"model_error_request_times": "失敗次數",
|
"model_error_request_times": "失敗次數",
|
||||||
@@ -57,7 +78,15 @@
|
|||||||
"selected_model_empty": "至少選擇一個模型",
|
"selected_model_empty": "至少選擇一個模型",
|
||||||
"start_test": "批次測試{{num}}個模型",
|
"start_test": "批次測試{{num}}個模型",
|
||||||
"test_failed": "有{{num}}個模型報錯",
|
"test_failed": "有{{num}}個模型報錯",
|
||||||
|
"timespan_day": "天",
|
||||||
|
"timespan_hour": "小時",
|
||||||
|
"timespan_label": "時間顆粒度",
|
||||||
|
"timespan_minute": "分鐘",
|
||||||
|
"total_call_volume": "調用總量",
|
||||||
|
"view_chart": "圖表",
|
||||||
|
"view_table": "表格",
|
||||||
"vlm_model": "圖片理解模型",
|
"vlm_model": "圖片理解模型",
|
||||||
"vlm_model_tip": "用於知識庫中對文件中的圖片進行額外的索引生成",
|
"vlm_model_tip": "用於知識庫中對文件中的圖片進行額外的索引生成",
|
||||||
|
"volunme_of_failed_calls": "調用失敗量",
|
||||||
"waiting_test": "等待測試"
|
"waiting_test": "等待測試"
|
||||||
}
|
}
|
||||||
|
11
projects/app/src/global/aiproxy/type.d.ts
vendored
11
projects/app/src/global/aiproxy/type.d.ts
vendored
@@ -54,11 +54,16 @@ export type ChannelLogListItemType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardDataItemType = {
|
export type DashboardDataItemType = {
|
||||||
|
channel_id?: number;
|
||||||
model: string;
|
model: string;
|
||||||
request_count: number;
|
request_count?: number;
|
||||||
used_amount: number;
|
used_amount?: number;
|
||||||
exception_count: number;
|
exception_count?: number;
|
||||||
|
total_time_milliseconds?: number;
|
||||||
|
total_ttfb_milliseconds?: number;
|
||||||
input_tokens?: number;
|
input_tokens?: number;
|
||||||
output_tokens?: number;
|
output_tokens?: number;
|
||||||
total_tokens?: number;
|
total_tokens?: number;
|
||||||
|
max_rpm?: number;
|
||||||
|
max_tpm?: number;
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,307 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Table, TableContainer, Thead, Tbody, Tr, Th, Td, Button } from '@chakra-ui/react';
|
||||||
|
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||||
|
import type { DashboardDataItemType } from '@/global/aiproxy/type.d';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
|
||||||
|
export type DashboardDataEntry = {
|
||||||
|
timestamp: number;
|
||||||
|
summary: DashboardDataItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataTableComponentProps = {
|
||||||
|
data: DashboardDataEntry[];
|
||||||
|
filterProps: {
|
||||||
|
channelId?: string;
|
||||||
|
model?: string;
|
||||||
|
};
|
||||||
|
channelList: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
modelPriceMap: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
inputPrice?: number;
|
||||||
|
outputPrice?: number;
|
||||||
|
charsPointsPrice?: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
onViewDetail: (model: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortFieldType = 'totalCalls' | 'errorCalls' | 'totalCost';
|
||||||
|
|
||||||
|
const DataTableComponent = ({
|
||||||
|
data,
|
||||||
|
filterProps,
|
||||||
|
onViewDetail,
|
||||||
|
channelList,
|
||||||
|
modelPriceMap
|
||||||
|
}: DataTableComponentProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { feConfigs } = useSystemStore();
|
||||||
|
const [sortField, setSortField] = useState<SortFieldType>('totalCalls');
|
||||||
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
|
// Create a mapping from channel ID to channel name
|
||||||
|
const channelIdToNameMap = useMemo(() => {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
channelList.forEach((channel) => {
|
||||||
|
if (channel.value && channel.value !== '') {
|
||||||
|
const channelId = parseInt(channel.value);
|
||||||
|
if (!isNaN(channelId)) {
|
||||||
|
map.set(channelId, channel.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [channelList]);
|
||||||
|
|
||||||
|
// display the channel column
|
||||||
|
const showChannelColumn = !!filterProps.model;
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows: {
|
||||||
|
model: string;
|
||||||
|
channelName?: string;
|
||||||
|
totalCalls: number;
|
||||||
|
errorCalls: number;
|
||||||
|
totalCost: number;
|
||||||
|
avgResponseTime: number;
|
||||||
|
avgTtfb: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
if (showChannelColumn) {
|
||||||
|
// When a specific model is selected, aggregate the data by channel_id
|
||||||
|
const channelMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
model: string;
|
||||||
|
totalCalls: number;
|
||||||
|
errorCalls: number;
|
||||||
|
totalCost: number;
|
||||||
|
totalResponseTime: number;
|
||||||
|
totalTtfb: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
data.forEach((dayData) => {
|
||||||
|
const summary = dayData.summary;
|
||||||
|
|
||||||
|
summary.forEach((item: DashboardDataItemType) => {
|
||||||
|
const channelId = `${item.channel_id!}`;
|
||||||
|
const existing = channelMap.get(channelId) || {
|
||||||
|
model: item.model || '-',
|
||||||
|
totalCalls: 0,
|
||||||
|
errorCalls: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
totalResponseTime: 0,
|
||||||
|
totalTtfb: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
existing.totalCalls += item.request_count || 0;
|
||||||
|
existing.errorCalls += item.exception_count || 0;
|
||||||
|
existing.totalResponseTime += item.total_time_milliseconds || 0;
|
||||||
|
existing.totalTtfb += item.total_ttfb_milliseconds || 0;
|
||||||
|
|
||||||
|
const modelPricing = modelPriceMap.get(item.model);
|
||||||
|
if (modelPricing) {
|
||||||
|
const inputTokens = item.input_tokens || 0;
|
||||||
|
const outputTokens = item.output_tokens || 0;
|
||||||
|
const isIOPriceType =
|
||||||
|
typeof modelPricing.inputPrice === 'number' && modelPricing.inputPrice > 0;
|
||||||
|
|
||||||
|
const totalPoints = isIOPriceType
|
||||||
|
? (modelPricing.inputPrice || 0) * (inputTokens / 1000) +
|
||||||
|
(modelPricing.outputPrice || 0) * (outputTokens / 1000)
|
||||||
|
: ((modelPricing.charsPointsPrice || 0) * (inputTokens + outputTokens)) / 1000;
|
||||||
|
|
||||||
|
existing.totalCost += totalPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelMap.set(channelId, existing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
channelMap.forEach((item, channelId) => {
|
||||||
|
const successCalls = item.totalCalls - item.errorCalls;
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
channelName: channelIdToNameMap.get(parseInt(channelId)) || '',
|
||||||
|
model: item.model,
|
||||||
|
totalCalls: item.totalCalls,
|
||||||
|
errorCalls: item.errorCalls,
|
||||||
|
totalCost: item.totalCost,
|
||||||
|
avgResponseTime: successCalls > 0 ? item.totalResponseTime / successCalls / 1000 : 0,
|
||||||
|
avgTtfb: successCalls > 0 ? item.totalTtfb / successCalls / 1000 : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// When no specific model is selected, aggregate the data by the model.
|
||||||
|
const modelMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
totalCalls: number;
|
||||||
|
errorCalls: number;
|
||||||
|
totalCost: number;
|
||||||
|
totalResponseTime: number;
|
||||||
|
totalTtfb: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
data.forEach((dayData) => {
|
||||||
|
const summary = dayData.summary;
|
||||||
|
|
||||||
|
summary.forEach((item: DashboardDataItemType) => {
|
||||||
|
const modelName = item.model || '-';
|
||||||
|
const existing = modelMap.get(modelName) || {
|
||||||
|
totalCalls: 0,
|
||||||
|
errorCalls: 0,
|
||||||
|
totalCost: 0,
|
||||||
|
totalResponseTime: 0,
|
||||||
|
totalTtfb: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
existing.totalCalls += item.request_count || 0;
|
||||||
|
existing.errorCalls += item.exception_count || 0;
|
||||||
|
existing.totalResponseTime += item.total_time_milliseconds || 0;
|
||||||
|
existing.totalTtfb += item.total_ttfb_milliseconds || 0;
|
||||||
|
|
||||||
|
const modelPricing = modelPriceMap.get(item.model);
|
||||||
|
if (modelPricing) {
|
||||||
|
const inputTokens = item.input_tokens || 0;
|
||||||
|
const outputTokens = item.output_tokens || 0;
|
||||||
|
const isIOPriceType =
|
||||||
|
typeof modelPricing.inputPrice === 'number' && modelPricing.inputPrice > 0;
|
||||||
|
|
||||||
|
const totalPoints = isIOPriceType
|
||||||
|
? (modelPricing.inputPrice || 0) * (inputTokens / 1000) +
|
||||||
|
(modelPricing.outputPrice || 0) * (outputTokens / 1000)
|
||||||
|
: ((modelPricing.charsPointsPrice || 0) * (inputTokens + outputTokens)) / 1000;
|
||||||
|
|
||||||
|
existing.totalCost += totalPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelMap.set(modelName, existing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelMap.forEach((item, modelName) => {
|
||||||
|
const successCalls = item.totalCalls - item.errorCalls;
|
||||||
|
rows.push({
|
||||||
|
model: modelName,
|
||||||
|
totalCalls: item.totalCalls,
|
||||||
|
errorCalls: item.errorCalls,
|
||||||
|
totalCost: item.totalCost,
|
||||||
|
avgResponseTime: successCalls > 0 ? item.totalResponseTime / successCalls / 1000 : 0,
|
||||||
|
avgTtfb: successCalls > 0 ? item.totalTtfb / successCalls / 1000 : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort
|
||||||
|
if (sortField) {
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aVal = a[sortField];
|
||||||
|
const bVal = b[sortField];
|
||||||
|
return sortDirection === 'desc' ? bVal - aVal : aVal - bVal;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}, [data, showChannelColumn, sortField, modelPriceMap, channelIdToNameMap, sortDirection]);
|
||||||
|
|
||||||
|
const handleSort = (field: SortFieldType) => {
|
||||||
|
if (sortField === field) {
|
||||||
|
// Toggle between desc and asc
|
||||||
|
setSortDirection(sortDirection === 'desc' ? 'asc' : 'desc');
|
||||||
|
} else {
|
||||||
|
// Switch to new field, default to desc
|
||||||
|
setSortField(field);
|
||||||
|
setSortDirection('desc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortIcon = (field: SortFieldType) => {
|
||||||
|
if (sortField !== field) return null;
|
||||||
|
return sortDirection === 'desc' ? '↓' : '↑';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyBox h={'100%'}>
|
||||||
|
<TableContainer fontSize={'sm'}>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr userSelect={'none'}>
|
||||||
|
<Th>{t('account_model:dashboard_model')}</Th>
|
||||||
|
{showChannelColumn && <Th>{t('account_model:dashboard_channel')}</Th>}
|
||||||
|
<Th
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => handleSort('totalCalls')}
|
||||||
|
_hover={{ color: 'primary.600' }}
|
||||||
|
>
|
||||||
|
{t('account_model:total_call_volume')} {getSortIcon('totalCalls')}
|
||||||
|
</Th>
|
||||||
|
<Th
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => handleSort('errorCalls')}
|
||||||
|
_hover={{ color: 'primary.600' }}
|
||||||
|
>
|
||||||
|
{t('account_model:volunme_of_failed_calls')} {getSortIcon('errorCalls')}
|
||||||
|
</Th>
|
||||||
|
{feConfigs?.isPlus && (
|
||||||
|
<Th
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => handleSort('totalCost')}
|
||||||
|
_hover={{ color: 'primary.600' }}
|
||||||
|
>
|
||||||
|
{t('account_model:aipoint_usage')} {getSortIcon('totalCost')}
|
||||||
|
</Th>
|
||||||
|
)}
|
||||||
|
<Th>{t('account_model:avg_response_time')}</Th>
|
||||||
|
<Th>{t('account_model:avg_ttfb')}</Th>
|
||||||
|
<Th></Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{tableData.map((item, index) => (
|
||||||
|
<Tr key={index}>
|
||||||
|
<Td>{item.model}</Td>
|
||||||
|
{showChannelColumn && <Td>{item.channelName}</Td>}
|
||||||
|
<Td color={'primary.700'}>{formatNumber(item.totalCalls).toLocaleString()}</Td>
|
||||||
|
<Td color={'red.700'}>{formatNumber(item.errorCalls)}</Td>
|
||||||
|
{feConfigs?.isPlus && <Td>{formatNumber(item.totalCost).toLocaleString()}</Td>}
|
||||||
|
<Td color={item.avgResponseTime > 10 ? 'yellow.700' : ''}>
|
||||||
|
{item.avgResponseTime > 0 ? `${item.avgResponseTime.toFixed(2)}` : '-'}
|
||||||
|
</Td>
|
||||||
|
<Td>{item.avgTtfb > 0 ? `${item.avgTtfb.toFixed(2)}` : '-'}</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
leftIcon={<MyIcon name={'menu'} w={'1rem'} />}
|
||||||
|
size={'sm'}
|
||||||
|
variant={'whiteBase'}
|
||||||
|
onClick={() => onViewDetail(item.model)}
|
||||||
|
>
|
||||||
|
{t('account_model:detail')}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
{tableData.length === 0 && <EmptyTip text={t('account_model:dashboard_no_data')} />}
|
||||||
|
</MyBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataTableComponent;
|
@@ -12,10 +12,9 @@ import {
|
|||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
|
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
|
||||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||||
|
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||||
type XAxisConfig = {
|
import { useTranslation } from 'next-i18next';
|
||||||
dataKey: string;
|
import { cloneDeep } from 'lodash';
|
||||||
};
|
|
||||||
|
|
||||||
type LineConfig = {
|
type LineConfig = {
|
||||||
dataKey: string;
|
dataKey: string;
|
||||||
@@ -29,15 +28,16 @@ type TooltipItem = {
|
|||||||
dataKey: string;
|
dataKey: string;
|
||||||
color: string;
|
color: string;
|
||||||
formatter?: (value: number) => string;
|
formatter?: (value: number) => string;
|
||||||
customValue?: (data: any) => number;
|
customValue?: (data: Record<string, any>) => number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LineChartComponentProps = {
|
type LineChartComponentProps = {
|
||||||
data: Record<string, any>[];
|
data: Record<string, any>[];
|
||||||
title: string;
|
title: string;
|
||||||
HeaderRightChildren?: React.ReactNode;
|
HeaderLeftChildren?: React.ReactNode;
|
||||||
lines: LineConfig[];
|
lines: LineConfig[];
|
||||||
tooltipItems?: TooltipItem[];
|
tooltipItems?: TooltipItem[];
|
||||||
|
enableCumulative?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomTooltip = ({
|
const CustomTooltip = ({
|
||||||
@@ -47,48 +47,51 @@ const CustomTooltip = ({
|
|||||||
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
|
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
|
||||||
const data = payload?.[0]?.payload;
|
const data = payload?.[0]?.payload;
|
||||||
|
|
||||||
if (active && data && tooltipItems) {
|
if (!active || !data || !tooltipItems) {
|
||||||
return (
|
return null;
|
||||||
<Box bg={'white'} p={3} borderRadius={'md'} border={'base'} boxShadow={'sm'}>
|
|
||||||
<Box fontSize={'sm'} color={'myGray.900'} mb={2}>
|
|
||||||
{data.x}
|
|
||||||
</Box>
|
|
||||||
{tooltipItems.map((item, index) => {
|
|
||||||
const value = (() => {
|
|
||||||
if (item.customValue) {
|
|
||||||
return item.customValue(data);
|
|
||||||
} else {
|
|
||||||
return data[item.dataKey];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const displayValue = (() => {
|
|
||||||
const val = item.formatter ? item.formatter(value) : formatNumber(value);
|
|
||||||
return val.toLocaleString();
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack key={index} fontSize={'sm'} _notLast={{ mb: 1 }}>
|
|
||||||
<Box w={2} h={2} borderRadius={'full'} bg={item.color} />
|
|
||||||
<Box>{item.label}</Box>
|
|
||||||
<Box>{displayValue}</Box>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return (
|
||||||
|
<Box bg="white" p={3} borderRadius="md" border="base" boxShadow="sm">
|
||||||
|
<Box fontSize="sm" color="myGray.900" mb={2}>
|
||||||
|
{data.xLabel || data.x}
|
||||||
|
</Box>
|
||||||
|
{tooltipItems.map((item, index) => {
|
||||||
|
const value = item.customValue ? item.customValue(data) : data[item.dataKey];
|
||||||
|
const displayValue = item.formatter ? item.formatter(value) : formatNumber(value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack key={index} fontSize="sm" _notLast={{ mb: 1 }}>
|
||||||
|
<Box w={2} h={2} borderRadius="full" bg={item.color} />
|
||||||
|
<Box>{item.label}</Box>
|
||||||
|
<Box>{displayValue.toLocaleString()}</Box>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LineChartComponent = ({
|
const LineChartComponent = ({
|
||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
HeaderRightChildren,
|
HeaderLeftChildren,
|
||||||
lines,
|
lines,
|
||||||
tooltipItems
|
tooltipItems,
|
||||||
|
enableCumulative = true
|
||||||
}: LineChartComponentProps) => {
|
}: LineChartComponentProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [displayMode, setDisplayMode] = useState<'incremental' | 'cumulative'>('incremental');
|
||||||
|
|
||||||
|
// Tab list constant
|
||||||
|
const tabList = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: t('account_model:chart_mode_incremental'), value: 'incremental' as const },
|
||||||
|
{ label: t('account_model:chart_mode_cumulative'), value: 'cumulative' as const }
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
// Y-axis number formatter function
|
// Y-axis number formatter function
|
||||||
const formatYAxisNumber = useCallback((value: number): string => {
|
const formatYAxisNumber = useCallback((value: number): string => {
|
||||||
@@ -100,11 +103,35 @@ const LineChartComponent = ({
|
|||||||
return value.toString();
|
return value.toString();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Process data based on display mode
|
||||||
|
const processedData = useMemo(() => {
|
||||||
|
if (displayMode === 'incremental' || !enableCumulative) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cumulative mode: accumulate values for each line's dataKey
|
||||||
|
const cloneData = cloneDeep(data);
|
||||||
|
|
||||||
|
const dataKeys = lines.map((item) => item.dataKey);
|
||||||
|
|
||||||
|
return cloneData.map((item, index) => {
|
||||||
|
if (index === 0) return item;
|
||||||
|
|
||||||
|
dataKeys.forEach((key) => {
|
||||||
|
if (typeof item[key] === 'number') {
|
||||||
|
item[key] += cloneData[index - 1][key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}, [data, displayMode, lines, enableCumulative]);
|
||||||
|
|
||||||
// Generate gradient definitions
|
// Generate gradient definitions
|
||||||
const gradientDefs = useMemo(() => {
|
const gradientDefs = useMemo(
|
||||||
return (
|
() => (
|
||||||
<defs>
|
<defs>
|
||||||
{lines.map((line, index) => (
|
{lines.map((line) => (
|
||||||
<linearGradient
|
<linearGradient
|
||||||
key={`gradient-${line.color}`}
|
key={`gradient-${line.color}`}
|
||||||
id={`gradient-${line.color}`}
|
id={`gradient-${line.color}`}
|
||||||
@@ -118,8 +145,9 @@ const LineChartComponent = ({
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
))}
|
))}
|
||||||
</defs>
|
</defs>
|
||||||
);
|
),
|
||||||
}, [lines]);
|
[lines]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -127,33 +155,44 @@ const LineChartComponent = ({
|
|||||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
|
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
|
||||||
{title}
|
{title}
|
||||||
</Box>
|
</Box>
|
||||||
{HeaderRightChildren && HeaderRightChildren}
|
<HStack spacing={2}>
|
||||||
|
{HeaderLeftChildren}
|
||||||
|
{enableCumulative && (
|
||||||
|
<FillRowTabs<'incremental' | 'cumulative'>
|
||||||
|
list={tabList}
|
||||||
|
py={0.5}
|
||||||
|
px={2}
|
||||||
|
value={displayMode}
|
||||||
|
onChange={setDisplayMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<ResponsiveContainer width="100%" height={'100%'}>
|
<ResponsiveContainer width="100%" height={'100%'}>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
data={data}
|
data={processedData}
|
||||||
margin={{ top: 5, right: 30, left: 0, bottom: HeaderRightChildren ? 30 : 15 }}
|
margin={{ top: 5, right: 30, left: 0, bottom: HeaderLeftChildren ? 20 : 15 }}
|
||||||
>
|
>
|
||||||
{gradientDefs}
|
{gradientDefs}
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey={'x'}
|
dataKey="x"
|
||||||
tickMargin={10}
|
tickMargin={10}
|
||||||
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
||||||
interval={'preserveStartEnd'}
|
interval="preserveStartEnd"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickSize={0}
|
tickSize={0}
|
||||||
tickMargin={10}
|
tickMargin={10}
|
||||||
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
|
||||||
interval={'preserveStartEnd'}
|
interval="preserveStartEnd"
|
||||||
tickFormatter={formatYAxisNumber}
|
tickFormatter={formatYAxisNumber}
|
||||||
/>
|
/>
|
||||||
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
|
||||||
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
|
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
|
||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => (
|
||||||
<Area
|
<Area
|
||||||
key={index}
|
key={line.dataKey}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
name={line.name}
|
name={line.name}
|
||||||
dataKey={line.dataKey}
|
dataKey={line.dataKey}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import type { BoxProps } from '@chakra-ui/react';
|
import type { BoxProps } from '@chakra-ui/react';
|
||||||
import { Box, Flex, Grid, HStack, useTheme } from '@chakra-ui/react';
|
import { Box, Grid, HStack, useTheme } from '@chakra-ui/react';
|
||||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
|
||||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@@ -18,9 +17,11 @@ import { getModelProvider } from '@fastgpt/global/core/ai/provider';
|
|||||||
import LineChartComponent from './LineChartComponent';
|
import LineChartComponent from './LineChartComponent';
|
||||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import DataTableComponent from './DataTableComponent';
|
||||||
|
|
||||||
export type ModelDashboardData = {
|
export type ModelDashboardData = {
|
||||||
x: string;
|
x: string;
|
||||||
|
xLabel?: string;
|
||||||
totalCalls: number;
|
totalCalls: number;
|
||||||
errorCalls: number;
|
errorCalls: number;
|
||||||
errorRate: number;
|
errorRate: number;
|
||||||
@@ -28,6 +29,10 @@ export type ModelDashboardData = {
|
|||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
totalTokens: number;
|
totalTokens: number;
|
||||||
totalCost: number;
|
totalCost: number;
|
||||||
|
avgResponseTime: number;
|
||||||
|
avgTtfb: number;
|
||||||
|
maxRpm: number;
|
||||||
|
maxTpm: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChartsBoxStyles: BoxProps = {
|
const ChartsBoxStyles: BoxProps = {
|
||||||
@@ -40,31 +45,52 @@ const ChartsBoxStyles: BoxProps = {
|
|||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Displays model usage statistics, token consumption and cost visualization
|
// Default date range: Past 7 days
|
||||||
|
const getDefaultDateRange = (): DateRangeType => {
|
||||||
|
const from = addDays(new Date(), -7);
|
||||||
|
from.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const to = new Date();
|
||||||
|
to.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
return { from, to };
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTimeDiffs = (from: Date, to: Date) => {
|
||||||
|
const startDate = dayjs(from);
|
||||||
|
const endDate = dayjs(to);
|
||||||
|
return {
|
||||||
|
daysDiff: endDate.diff(startDate, 'day'),
|
||||||
|
hoursDiff: endDate.diff(startDate, 'hour')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { feConfigs } = useSystemStore();
|
const { feConfigs } = useSystemStore();
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
|
||||||
|
|
||||||
|
// view detail handler
|
||||||
|
const handleViewDetail = (model: string) => {
|
||||||
|
setFilterProps({
|
||||||
|
...filterProps,
|
||||||
|
model
|
||||||
|
});
|
||||||
|
setViewMode('chart');
|
||||||
|
};
|
||||||
|
|
||||||
const [filterProps, setFilterProps] = useState<{
|
const [filterProps, setFilterProps] = useState<{
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
dateRange: DateRangeType;
|
dateRange: DateRangeType;
|
||||||
|
timespan: 'minute' | 'hour' | 'day';
|
||||||
}>({
|
}>({
|
||||||
channelId: undefined,
|
channelId: undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
dateRange: {
|
timespan: 'day',
|
||||||
from: (() => {
|
dateRange: getDefaultDateRange()
|
||||||
const today = addDays(new Date(), -7);
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
return today;
|
|
||||||
})(),
|
|
||||||
to: (() => {
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(23, 59, 59, 999);
|
|
||||||
return today;
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch channel list with "All" option
|
// Fetch channel list with "All" option
|
||||||
@@ -113,85 +139,9 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
...res
|
...res
|
||||||
];
|
];
|
||||||
}, [systemModelList, t]);
|
}, [systemModelList, t]);
|
||||||
|
// Model price map
|
||||||
// Fetch dashboard data with date range and channel filters
|
const modelPriceMap = useMemo(() => {
|
||||||
const { data: dashboardData = [], loading: isLoading } = useRequest2(
|
const map = new Map<
|
||||||
async () => {
|
|
||||||
const params = {
|
|
||||||
channel: filterProps.channelId ? parseInt(filterProps.channelId) : 0,
|
|
||||||
start_timestamp: filterProps.dateRange.from
|
|
||||||
? Math.floor(filterProps.dateRange.from.getTime())
|
|
||||||
: undefined,
|
|
||||||
end_timestamp: filterProps.dateRange.to
|
|
||||||
? Math.floor(filterProps.dateRange.to.getTime())
|
|
||||||
: undefined,
|
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
timespan: 'day' as const
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = await getDashboardV2(params);
|
|
||||||
|
|
||||||
if (filterProps.model) {
|
|
||||||
data = data.map((item) => {
|
|
||||||
const filterModels = item.models.filter((model) => model.model === filterProps.model);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
models: filterModels
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-fill missing days
|
|
||||||
if (filterProps.dateRange.from && filterProps.dateRange.to) {
|
|
||||||
const startDate = dayjs(filterProps.dateRange.from);
|
|
||||||
const endDate = dayjs(filterProps.dateRange.to);
|
|
||||||
const daysDiff = endDate.diff(startDate, 'day') + 1;
|
|
||||||
|
|
||||||
// Create complete date list
|
|
||||||
const completeDateList = Array.from({ length: daysDiff }, (_, i) =>
|
|
||||||
startDate.add(i, 'day')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a map of existing data by timestamp
|
|
||||||
const existingDataMap = new Map(
|
|
||||||
data.map((item) => [dayjs(item.timestamp * 1000).format('YYYY-MM-DD'), item])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fill missing days with empty data
|
|
||||||
const completeData = completeDateList.map((date) => {
|
|
||||||
const dateKey = date.format('YYYY-MM-DD');
|
|
||||||
const existingItem = existingDataMap.get(dateKey);
|
|
||||||
|
|
||||||
if (existingItem) {
|
|
||||||
return existingItem;
|
|
||||||
} else {
|
|
||||||
// Create empty data structure for missing days
|
|
||||||
return {
|
|
||||||
timestamp: Math.floor(date.valueOf() / 1000),
|
|
||||||
models: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
data = completeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
manual: false,
|
|
||||||
refreshDeps: [filterProps.channelId, filterProps.dateRange, filterProps.model]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process chart data - aggregate daily model calls, token usage and cost data
|
|
||||||
const chartData: ModelDashboardData[] = useMemo(() => {
|
|
||||||
if (dashboardData.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model price map
|
|
||||||
const modelPriceMap = new Map<
|
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
inputPrice?: number;
|
inputPrice?: number;
|
||||||
@@ -200,24 +150,188 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
systemModelList.forEach((model) => {
|
systemModelList.forEach((model) => {
|
||||||
modelPriceMap.set(model.model, {
|
map.set(model.model, {
|
||||||
inputPrice: model.inputPrice,
|
inputPrice: model.inputPrice,
|
||||||
outputPrice: model.outputPrice,
|
outputPrice: model.outputPrice,
|
||||||
charsPointsPrice: model.charsPointsPrice
|
charsPointsPrice: model.charsPointsPrice
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return map;
|
||||||
|
}, [systemModelList]);
|
||||||
|
|
||||||
|
const computeTimespan = (daysDiff: number, hoursDiff: number) => {
|
||||||
|
const options: { label: string; value: 'minute' | 'hour' | 'day' }[] = [];
|
||||||
|
if (daysDiff <= 1) {
|
||||||
|
options.push({ label: t('account_model:timespan_minute'), value: 'minute' });
|
||||||
|
}
|
||||||
|
if (daysDiff < 7) {
|
||||||
|
options.push({ label: t('account_model:timespan_hour'), value: 'hour' });
|
||||||
|
}
|
||||||
|
if (daysDiff >= 1) {
|
||||||
|
options.push({ label: t('account_model:timespan_day'), value: 'day' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTimespan: 'minute' | 'hour' | 'day' = (() => {
|
||||||
|
if (hoursDiff < 1) {
|
||||||
|
return 'minute';
|
||||||
|
} else if (daysDiff < 2) {
|
||||||
|
return 'hour';
|
||||||
|
} else {
|
||||||
|
return 'day';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { options, defaultTimespan };
|
||||||
|
};
|
||||||
|
const [timespanOptions, setTimespanOptions] = useState(computeTimespan(30, 60).options);
|
||||||
|
|
||||||
|
// Handle date range change with automatic timespan adjustment
|
||||||
|
const handleDateRangeChange = (dateRange: DateRangeType) => {
|
||||||
|
const newFilterProps = { ...filterProps, dateRange };
|
||||||
|
|
||||||
|
// Computed timespan
|
||||||
|
if (dateRange.from && dateRange.to) {
|
||||||
|
const { daysDiff, hoursDiff } = calculateTimeDiffs(dateRange.from, dateRange.to);
|
||||||
|
const { options: newTimespanOptions, defaultTimespan: newDefaultTimespan } = computeTimespan(
|
||||||
|
daysDiff,
|
||||||
|
hoursDiff
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimespanOptions(newTimespanOptions);
|
||||||
|
newFilterProps.timespan = newDefaultTimespan;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilterProps(newFilterProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch dashboard data with date range and channel filters
|
||||||
|
const { data: dashboardData = [], loading: isLoading } = useRequest2(
|
||||||
|
async () => {
|
||||||
|
const params = {
|
||||||
|
channel: filterProps.channelId ? parseInt(filterProps.channelId) : undefined,
|
||||||
|
model: filterProps.model,
|
||||||
|
start_timestamp: filterProps.dateRange.from
|
||||||
|
? Math.floor(filterProps.dateRange.from.getTime())
|
||||||
|
: undefined,
|
||||||
|
end_timestamp: filterProps.dateRange.to
|
||||||
|
? Math.floor(filterProps.dateRange.to.getTime())
|
||||||
|
: undefined,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
timespan: filterProps.timespan
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await getDashboardV2(params);
|
||||||
|
|
||||||
|
// Auto-fill missing periods based on timespan
|
||||||
|
const startDate = dayjs(filterProps.dateRange.from);
|
||||||
|
const currentTime = dayjs();
|
||||||
|
const endDate = dayjs(filterProps.dateRange.to).isBefore(currentTime)
|
||||||
|
? dayjs(filterProps.dateRange.to)
|
||||||
|
: currentTime;
|
||||||
|
const timespan = filterProps.timespan;
|
||||||
|
|
||||||
|
const { periodCount } = (() => {
|
||||||
|
if (timespan === 'minute') {
|
||||||
|
return {
|
||||||
|
periodCount: endDate.diff(startDate, 'minute') + 1
|
||||||
|
};
|
||||||
|
} else if (timespan === 'hour') {
|
||||||
|
return {
|
||||||
|
periodCount: endDate.diff(startDate, 'hour') + 1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
periodCount: endDate.diff(startDate, 'day') + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Create complete period list
|
||||||
|
const completePeriodList = Array.from({ length: periodCount }, (_, i) =>
|
||||||
|
startDate.add(i, timespan)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a map of existing data by timestamp
|
||||||
|
const existingDataMap = new Map(
|
||||||
|
data.map((item) => [dayjs(item.timestamp * 1000).format('YYYY-MM-DD HH:mm'), item])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fill missing periods with empty data
|
||||||
|
return completePeriodList.map((period) => {
|
||||||
|
const periodKey = period.format('YYYY-MM-DD HH:mm');
|
||||||
|
const existingItem = existingDataMap.get(periodKey);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
return existingItem;
|
||||||
|
} else {
|
||||||
|
// Create empty data structure for missing periods
|
||||||
|
return {
|
||||||
|
timestamp: Math.floor(period.valueOf() / 1000),
|
||||||
|
summary: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: false,
|
||||||
|
refreshDeps: [
|
||||||
|
filterProps.channelId,
|
||||||
|
filterProps.dateRange,
|
||||||
|
filterProps.model,
|
||||||
|
filterProps.timespan
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process chart data - aggregate model calls, token usage and cost data based on timespan
|
||||||
|
const chartData: ModelDashboardData[] = useMemo(() => {
|
||||||
|
if (dashboardData.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return dashboardData.map((item) => {
|
return dashboardData.map((item) => {
|
||||||
const date = dayjs(item.timestamp * 1000).format('MM-DD');
|
// Format date based on timespan
|
||||||
const totalCalls = item.models.reduce((acc, model) => acc + model.request_count, 0);
|
const dateFormat = (() => {
|
||||||
const errorCalls = item.models.reduce((acc, model) => acc + model.exception_count, 0);
|
if (filterProps.timespan === 'minute') {
|
||||||
|
return 'HH:mm';
|
||||||
|
} else if (filterProps.timespan === 'hour') {
|
||||||
|
return 'HH:00';
|
||||||
|
} else {
|
||||||
|
return 'MM-DD';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const date = dayjs(item.timestamp * 1000).format(dateFormat);
|
||||||
|
const xLabel = dayjs(item.timestamp * 1000).format('YYYY-MM-DD HH:mm');
|
||||||
|
const summary = item.summary || [];
|
||||||
|
const totalCalls = summary.reduce((acc, model) => acc + (model.request_count || 0), 0);
|
||||||
|
const errorCalls = summary.reduce((acc, model) => acc + (model.exception_count || 0), 0);
|
||||||
const errorRate = totalCalls === 0 ? 0 : Number((errorCalls / totalCalls).toFixed(2));
|
const errorRate = totalCalls === 0 ? 0 : Number((errorCalls / totalCalls).toFixed(2));
|
||||||
|
|
||||||
const inputTokens = item.models.reduce((acc, model) => acc + (model?.input_tokens || 0), 0);
|
const inputTokens = summary.reduce((acc, model) => acc + (model.input_tokens || 0), 0);
|
||||||
const outputTokens = item.models.reduce((acc, model) => acc + (model?.output_tokens || 0), 0);
|
const outputTokens = summary.reduce((acc, model) => acc + (model?.output_tokens || 0), 0);
|
||||||
const totalTokens = item.models.reduce((acc, model) => acc + (model?.total_tokens || 0), 0);
|
const totalTokens = summary.reduce((acc, model) => acc + (model.total_tokens || 0), 0);
|
||||||
|
|
||||||
const totalCost = item.models.reduce((acc, model) => {
|
const successCalls = totalCalls - errorCalls;
|
||||||
|
const avgResponseTime = successCalls
|
||||||
|
? summary.reduce((acc, model) => acc + (model.total_time_milliseconds || 0), 0) /
|
||||||
|
successCalls /
|
||||||
|
1000
|
||||||
|
: 0;
|
||||||
|
const avgTtfb = successCalls
|
||||||
|
? summary.reduce((acc, model) => acc + (model.total_ttfb_milliseconds || 0), 0) /
|
||||||
|
successCalls /
|
||||||
|
1000
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const maxRpm = filterProps.model
|
||||||
|
? summary.reduce((acc, model) => Math.max(acc, model.max_rpm || 0), 0)
|
||||||
|
: 0;
|
||||||
|
const maxTpm = filterProps.model
|
||||||
|
? summary.reduce((acc, model) => Math.max(acc, model.max_tpm || 0), 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const totalCost = summary.reduce((acc, model) => {
|
||||||
const modelPricing = modelPriceMap.get(model.model);
|
const modelPricing = modelPriceMap.get(model.model);
|
||||||
|
|
||||||
if (modelPricing) {
|
if (modelPricing) {
|
||||||
@@ -239,198 +353,346 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
x: date,
|
x: date,
|
||||||
|
xLabel: xLabel,
|
||||||
totalCalls,
|
totalCalls,
|
||||||
errorCalls,
|
errorCalls,
|
||||||
errorRate,
|
errorRate,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
totalCost
|
totalCost,
|
||||||
|
avgResponseTime: Math.round(avgResponseTime * 100) / 100,
|
||||||
|
avgTtfb: Math.round(avgTtfb * 100) / 100,
|
||||||
|
maxRpm,
|
||||||
|
maxTpm
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [dashboardData, systemModelList]);
|
}, [dashboardData, filterProps.model, filterProps.timespan, modelPriceMap]);
|
||||||
|
|
||||||
const [tokensUsageType, setTokensUsageType] = useState<
|
const [tokensUsageType, setTokensUsageType] = useState<
|
||||||
'inputTokens' | 'outputTokens' | 'totalTokens'
|
'inputTokens' | 'outputTokens' | 'totalTokens'
|
||||||
>('totalTokens');
|
>('totalTokens');
|
||||||
console.log(chartData);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>{Tab}</Box>
|
<Box>{Tab}</Box>
|
||||||
|
|
||||||
<HStack spacing={4}>
|
<HStack spacing={4} justifyContent="space-between">
|
||||||
<HStack>
|
<HStack spacing={4}>
|
||||||
<FormLabel>{t('common:user.Time')}</FormLabel>
|
<HStack>
|
||||||
<Box>
|
<FormLabel>{t('common:user.Time')}</FormLabel>
|
||||||
<DateRangePicker
|
<Box>
|
||||||
defaultDate={filterProps.dateRange}
|
<DateRangePicker
|
||||||
dateRange={filterProps.dateRange}
|
defaultDate={filterProps.dateRange}
|
||||||
position="bottom"
|
dateRange={filterProps.dateRange}
|
||||||
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
|
position="bottom"
|
||||||
/>
|
onSuccess={handleDateRangeChange}
|
||||||
</Box>
|
/>
|
||||||
</HStack>
|
</Box>
|
||||||
<HStack>
|
</HStack>
|
||||||
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
<HStack>
|
||||||
<Box flex={'1 0 0'}>
|
<FormLabel>{t('account_model:channel_name')}</FormLabel>
|
||||||
<MySelect<string>
|
<Box flex={'1 0 0'}>
|
||||||
bg={'myGray.50'}
|
<MySelect<string>
|
||||||
isSearch
|
bg={'myGray.50'}
|
||||||
list={channelList}
|
isSearch
|
||||||
placeholder={t('account_model:select_channel')}
|
list={channelList}
|
||||||
value={filterProps.channelId}
|
placeholder={t('account_model:select_channel')}
|
||||||
onChange={(val) => setFilterProps({ ...filterProps, channelId: val })}
|
value={filterProps.channelId}
|
||||||
/>
|
onChange={(val) => setFilterProps({ ...filterProps, channelId: val })}
|
||||||
</Box>
|
/>
|
||||||
</HStack>
|
</Box>
|
||||||
<HStack>
|
</HStack>
|
||||||
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
<HStack>
|
||||||
<Box flex={'1 0 0'}>
|
<FormLabel>{t('account_model:model_name')}</FormLabel>
|
||||||
<MySelect<string>
|
<Box flex={'1 0 0'}>
|
||||||
bg={'myGray.50'}
|
<MySelect<string>
|
||||||
isSearch
|
bg={'myGray.50'}
|
||||||
list={modelList}
|
isSearch
|
||||||
placeholder={t('account_model:select_model')}
|
list={modelList}
|
||||||
value={filterProps.model}
|
placeholder={t('account_model:select_model')}
|
||||||
onChange={(val) => setFilterProps({ ...filterProps, model: val })}
|
value={filterProps.model}
|
||||||
/>
|
onChange={(val) => setFilterProps({ ...filterProps, model: val })}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
{viewMode === 'chart' && (
|
||||||
|
<HStack>
|
||||||
|
<FormLabel>{t('account_model:timespan_label')}</FormLabel>
|
||||||
|
<Box flex={'1 0 0'}>
|
||||||
|
<MySelect<'minute' | 'hour' | 'day'>
|
||||||
|
bg={'myGray.50'}
|
||||||
|
list={timespanOptions}
|
||||||
|
value={filterProps.timespan}
|
||||||
|
onChange={(val) => {
|
||||||
|
setFilterProps({ ...filterProps, timespan: val });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
<FillRowTabs<'chart' | 'table'>
|
||||||
|
list={[
|
||||||
|
{
|
||||||
|
label: t('account_model:view_chart'),
|
||||||
|
value: 'chart'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('account_model:view_table'),
|
||||||
|
value: 'table'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
py={1.5}
|
||||||
|
px={4}
|
||||||
|
value={viewMode}
|
||||||
|
onChange={(val) => setViewMode(val)}
|
||||||
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<MyBox flex={'1 0 0'} h={0} overflowY={'auto'} isLoading={isLoading}>
|
<MyBox flex={'1 0 0'} h={0} overflowY={'auto'} isLoading={isLoading}>
|
||||||
{dashboardData && dashboardData.length > 0 && (
|
{viewMode === 'chart' ? (
|
||||||
<>
|
dashboardData.length > 0 && (
|
||||||
<Box {...ChartsBoxStyles}>
|
<>
|
||||||
<LineChartComponent
|
|
||||||
data={chartData}
|
|
||||||
title={t('account_model:model_request_times')}
|
|
||||||
lines={[
|
|
||||||
{
|
|
||||||
dataKey: 'totalCalls',
|
|
||||||
name: t('account_model:model_request_times'),
|
|
||||||
color: theme.colors.primary['600']
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
tooltipItems={[
|
|
||||||
{
|
|
||||||
label: t('account_model:model_request_times'),
|
|
||||||
dataKey: 'totalCalls',
|
|
||||||
color: theme.colors.primary['600']
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
|
|
||||||
<Box {...ChartsBoxStyles}>
|
<Box {...ChartsBoxStyles}>
|
||||||
<LineChartComponent
|
<LineChartComponent
|
||||||
data={chartData}
|
data={chartData}
|
||||||
title={t('account_model:model_error_request_times')}
|
title={t('account_model:model_request_times')}
|
||||||
|
enableCumulative={true}
|
||||||
lines={[
|
lines={[
|
||||||
{
|
{
|
||||||
dataKey: 'errorCalls',
|
dataKey: 'totalCalls',
|
||||||
name: t('account_model:model_error_request_times'),
|
name: t('account_model:model_request_times'),
|
||||||
color: '#f98e1a'
|
color: theme.colors.primary['600']
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
tooltipItems={[
|
tooltipItems={[
|
||||||
{
|
{
|
||||||
label: t('account_model:model_error_request_times'),
|
label: t('account_model:model_request_times'),
|
||||||
dataKey: 'errorCalls',
|
dataKey: 'totalCalls',
|
||||||
color: '#f98e1a'
|
color: theme.colors.primary['600']
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box {...ChartsBoxStyles}>
|
|
||||||
<LineChartComponent
|
|
||||||
data={chartData}
|
|
||||||
title={t('account_model:model_error_rate')}
|
|
||||||
lines={[
|
|
||||||
{
|
|
||||||
dataKey: 'errorRate',
|
|
||||||
name: t('account_model:model_error_rate'),
|
|
||||||
color: '#e84738'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
tooltipItems={[
|
|
||||||
{
|
|
||||||
label: t('account_model:model_error_rate'),
|
|
||||||
dataKey: 'errorRate',
|
|
||||||
color: '#e84738'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Box mt={5} {...ChartsBoxStyles}>
|
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
|
||||||
<LineChartComponent
|
<Box {...ChartsBoxStyles}>
|
||||||
data={chartData}
|
<LineChartComponent
|
||||||
title={t('account_model:dashboard_token_usage')}
|
data={chartData}
|
||||||
lines={[
|
title={t('account_model:model_error_request_times')}
|
||||||
{
|
enableCumulative={false}
|
||||||
dataKey: tokensUsageType,
|
lines={[
|
||||||
name: t('account_model:dashboard_token_usage'),
|
|
||||||
color: theme.colors.primary['600']
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
tooltipItems={[
|
|
||||||
{
|
|
||||||
label: t('account_model:dashboard_token_usage'),
|
|
||||||
dataKey: tokensUsageType,
|
|
||||||
color: theme.colors.primary['600']
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
HeaderRightChildren={
|
|
||||||
<FillRowTabs<'inputTokens' | 'outputTokens' | 'totalTokens'>
|
|
||||||
list={[
|
|
||||||
{
|
{
|
||||||
label: t('account_model:all'),
|
dataKey: 'errorCalls',
|
||||||
value: 'totalTokens'
|
name: t('account_model:model_error_request_times'),
|
||||||
},
|
color: '#f98e1a'
|
||||||
{
|
}
|
||||||
label: t('account_model:input'),
|
]}
|
||||||
value: 'inputTokens'
|
tooltipItems={[
|
||||||
},
|
{
|
||||||
{
|
label: t('account_model:model_error_request_times'),
|
||||||
label: t('account_model:output'),
|
dataKey: 'errorCalls',
|
||||||
value: 'outputTokens'
|
color: '#f98e1a'
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
py={1}
|
|
||||||
px={5}
|
|
||||||
value={tokensUsageType}
|
|
||||||
onChange={(val) => setTokensUsageType(val)}
|
|
||||||
/>
|
/>
|
||||||
}
|
</Box>
|
||||||
/>
|
<Box {...ChartsBoxStyles}>
|
||||||
</Box>
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:model_error_rate')}
|
||||||
|
enableCumulative={false}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'errorRate',
|
||||||
|
name: t('account_model:model_error_rate'),
|
||||||
|
color: '#e84738'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:model_error_rate'),
|
||||||
|
dataKey: 'errorRate',
|
||||||
|
color: '#e84738'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{feConfigs?.isPlus && (
|
|
||||||
<Box mt={5} {...ChartsBoxStyles}>
|
<Box mt={5} {...ChartsBoxStyles}>
|
||||||
<LineChartComponent
|
<LineChartComponent
|
||||||
data={chartData}
|
data={chartData}
|
||||||
title={t('account_model:aipoint_usage')}
|
title={t('account_model:dashboard_token_usage')}
|
||||||
|
enableCumulative={true}
|
||||||
lines={[
|
lines={[
|
||||||
{
|
{
|
||||||
dataKey: 'totalCost',
|
dataKey: tokensUsageType,
|
||||||
name: t('account_model:aipoint_usage'),
|
name: t('account_model:dashboard_token_usage'),
|
||||||
color: '#8774EE'
|
color: theme.colors.primary['600']
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
tooltipItems={[
|
tooltipItems={[
|
||||||
{
|
{
|
||||||
label: t('account_model:aipoint_usage'),
|
label: t('account_model:dashboard_token_usage'),
|
||||||
dataKey: 'totalCost',
|
dataKey: tokensUsageType,
|
||||||
color: '#8774EE'
|
color: theme.colors.primary['600']
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
HeaderLeftChildren={
|
||||||
|
<FillRowTabs<'inputTokens' | 'outputTokens' | 'totalTokens'>
|
||||||
|
list={[
|
||||||
|
{
|
||||||
|
label: t('account_model:all'),
|
||||||
|
value: 'totalTokens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('account_model:input'),
|
||||||
|
value: 'inputTokens'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('account_model:output'),
|
||||||
|
value: 'outputTokens'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
py={0.5}
|
||||||
|
px={2}
|
||||||
|
value={tokensUsageType}
|
||||||
|
onChange={(val) => setTokensUsageType(val)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
</>
|
{feConfigs?.isPlus && (
|
||||||
|
<Box mt={5} {...ChartsBoxStyles}>
|
||||||
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:aipoint_usage')}
|
||||||
|
enableCumulative={true}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'totalCost',
|
||||||
|
name: t('account_model:aipoint_usage'),
|
||||||
|
color: '#8774EE'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:aipoint_usage'),
|
||||||
|
dataKey: 'totalCost',
|
||||||
|
color: '#8774EE'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
|
||||||
|
<Box {...ChartsBoxStyles}>
|
||||||
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:avg_response_time')}
|
||||||
|
enableCumulative={false}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'avgResponseTime',
|
||||||
|
name: t('account_model:avg_response_time'),
|
||||||
|
color: '#36B37E'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:avg_response_time'),
|
||||||
|
dataKey: 'avgResponseTime',
|
||||||
|
color: '#36B37E',
|
||||||
|
formatter: (value: number) => `${value.toFixed(2)}s`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box {...ChartsBoxStyles}>
|
||||||
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:avg_ttfb')}
|
||||||
|
enableCumulative={false}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'avgTtfb',
|
||||||
|
name: t('account_model:avg_ttfb'),
|
||||||
|
color: '#FF5630'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:avg_ttfb'),
|
||||||
|
dataKey: 'avgTtfb',
|
||||||
|
color: '#FF5630',
|
||||||
|
formatter: (value: number) => `${value.toFixed(2)}s`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{filterProps?.model && (
|
||||||
|
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5}>
|
||||||
|
<Box {...ChartsBoxStyles}>
|
||||||
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:max_rpm')}
|
||||||
|
enableCumulative={false}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'maxRpm',
|
||||||
|
name: t('account_model:max_rpm'),
|
||||||
|
color: '#6554C0'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:max_rpm'),
|
||||||
|
dataKey: 'maxRpm',
|
||||||
|
color: '#6554C0'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box {...ChartsBoxStyles}>
|
||||||
|
<LineChartComponent
|
||||||
|
data={chartData}
|
||||||
|
title={t('account_model:max_tpm')}
|
||||||
|
enableCumulative={false}
|
||||||
|
lines={[
|
||||||
|
{
|
||||||
|
dataKey: 'maxTpm',
|
||||||
|
name: t('account_model:max_tpm'),
|
||||||
|
color: '#FF8B00'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
tooltipItems={[
|
||||||
|
{
|
||||||
|
label: t('account_model:max_tpm'),
|
||||||
|
dataKey: 'maxTpm',
|
||||||
|
color: '#FF8B00'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<DataTableComponent
|
||||||
|
data={dashboardData}
|
||||||
|
filterProps={filterProps}
|
||||||
|
channelList={channelList}
|
||||||
|
modelPriceMap={modelPriceMap}
|
||||||
|
onViewDetail={handleViewDetail}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MyBox>
|
</MyBox>
|
||||||
</>
|
</>
|
||||||
|
@@ -191,22 +191,17 @@ export const getLogDetail = (id: number) =>
|
|||||||
|
|
||||||
export const getDashboardV2 = (params: {
|
export const getDashboardV2 = (params: {
|
||||||
channel?: number;
|
channel?: number;
|
||||||
|
model?: string;
|
||||||
start_timestamp?: number;
|
start_timestamp?: number;
|
||||||
end_timestamp?: number;
|
end_timestamp?: number;
|
||||||
timezone?: string;
|
timezone: string;
|
||||||
timespan?: 'day' | 'hour';
|
timespan: 'day' | 'hour' | 'minute';
|
||||||
}) =>
|
}) =>
|
||||||
GET<
|
GET<
|
||||||
{
|
{
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
models: DashboardDataItemType[];
|
summary: DashboardDataItemType[];
|
||||||
}[]
|
}[]
|
||||||
>('/dashboardv2/', {
|
>('/dashboardv2/', params);
|
||||||
channel: params.channel,
|
|
||||||
start_timestamp: params.start_timestamp,
|
|
||||||
end_timestamp: params.end_timestamp,
|
|
||||||
timezone: params.timezone || 'Local',
|
|
||||||
timespan: params.timespan || 'day'
|
|
||||||
});
|
|
||||||
|
|
||||||
export { responseSuccess, checkRes, responseError, instance, request };
|
export { responseSuccess, checkRes, responseError, instance, request };
|
||||||
|
Reference in New Issue
Block a user