diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index f6659942..ea45ea6d 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1231,6 +1231,12 @@ export const en = { priority: 'Structured Integration', addTool: 'Add Tool', tool: 'Tool', + + statistics: 'Data Statistics', + daily_conversations: 'Daily Conversations', + daily_new_users: 'Daily New Users', + daily_api_calls: 'Daily API Calls', + daily_tokens: 'Token Consumption', }, userMemory: { userMemory: 'User Memory', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b9f03694..0e5c9288 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -658,7 +658,13 @@ export const zh = { priority: '结构化整合', addTool: '添加工具', tool: '工具', - variableConfig: '配置变量' + variableConfig: '配置变量', + + statistics: '数据统计', + daily_conversations: '消息会话数', + daily_new_users: '新增用户数', + daily_api_calls: '调用次数', + daily_tokens: 'Token消耗', }, role: { roleManagement: '角色管理', diff --git a/web/src/views/ApplicationConfig/Statistics.tsx b/web/src/views/ApplicationConfig/Statistics.tsx new file mode 100644 index 00000000..8a76ab06 --- /dev/null +++ b/web/src/views/ApplicationConfig/Statistics.tsx @@ -0,0 +1,86 @@ +import { type FC, useState, useEffect } from 'react'; +import { Row, Col, Flex, DatePicker } from 'antd'; +import type { Dayjs } from 'dayjs' +import dayjs from 'dayjs'; + +const { RangePicker } = DatePicker; + +import type { Application } from '@/views/ApplicationManagement/types' +import { getAppStatistics } from '@/api/application'; +import LineCard from './components/LineCard' +import type { StatisticsData, StatisticsItem } from './types' + +const TotalObj: Record = { + daily_conversations: 'total_conversations', + daily_new_users: 'total_new_users', + daily_api_calls: 'total_api_calls', + daily_tokens: 'total_tokens', +} +const Statistics: FC<{ application: Application | null }> = ({ application }) => { + const [data, setData] = useState({ + daily_conversations: [], + total_conversations: 0, + daily_new_users: [], + total_new_users: 0, + daily_api_calls: [], + total_api_calls: 0, + daily_tokens: [], + total_tokens: 0 + }) + const [query, setQuery] = useState({ + start_date: dayjs().subtract(6, 'd'), + end_date: dayjs().subtract(0, 'd'), + }) + + useEffect(() => { + getData() + }, [application, query]) + const getData = () => { + if (!application?.id) { + return + } + const params = { + start_date: query.start_date.startOf('d').valueOf(), + end_date: query.end_date.endOf('d').valueOf(), + } + + getAppStatistics(application.id, params) + .then(res => { + setData(res as StatisticsData) + }) + } + const handleChange = (date: [Dayjs | null, Dayjs | null] | null) => { + if (!date || !date[0] || !date[1]) return + setQuery({ + start_date: date[0], + end_date: date[1], + }) + } + return ( +
+ + + + + + + {Object.entries(data).map(([key, value]) => { + if (key.includes('total')) { + return null + } + const totalKey = TotalObj[key]; + return ( + + + + ) + })} + +
+ ); +} +export default Statistics; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 94ef0ef7..db1e0fa5 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -17,7 +17,7 @@ import CopyModal from './CopyModal' const { Header } = Layout; -const tabKeys = ['arrangement', 'api', 'release'] +const tabKeys = ['arrangement', 'api', 'release', 'statistics'] const menuIcons: Record = { edit: editIcon, copy: copyIcon, diff --git a/web/src/views/ApplicationConfig/components/LineCard.tsx b/web/src/views/ApplicationConfig/components/LineCard.tsx new file mode 100644 index 00000000..0cfc3f0e --- /dev/null +++ b/web/src/views/ApplicationConfig/components/LineCard.tsx @@ -0,0 +1,127 @@ +import { type FC, useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import ReactEcharts from 'echarts-for-react'; +import * as echarts from 'echarts'; +import Empty from '@/components/Empty' + +import Card from './Card' +import type { StatisticsItem } from '../types' + +interface LineCardProps { + chartData: StatisticsItem[]; + type: string; + total: number; +} + +const SeriesConfig = { + type: 'line', + stack: 'Total', + smooth: true, + lineStyle: { + width: 3 + }, + showSymbol: true, + label: { + show: false, + position: 'top' + }, + emphasis: { + focus: 'series' + }, +} + +const ColorObj: Record = { + daily_conversations: '#FFB048', + daily_new_users: '#4DA8FF', + daily_api_calls: '#155EEF', + daily_tokens: '#AD88FF' +} + +const LineCard: FC = ({ chartData, type, total }) => { + const { t } = useTranslation() + const chartRef = useRef(null); + + useEffect(() => { + + }, [chartData]) + + const getSeries = () => { + return [{ + ...SeriesConfig, + name: t(`application.${type}`), + data: chartData.map(vo => vo.count), + areaStyle: { + opacity: 0.8, + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: ColorObj[type] }, + { offset: 1, color: '#FFFFFF' } + ]) + }, + }] + } + + return ( + {t(`application.${type}`)} {total}} + > + {chartData && chartData.length > 0 ? ( + item.date), + boundaryGap: false, + }, + yAxis: { + type: 'value', + axisLabel: { + color: '#A8A9AA', + fontFamily: 'PingFangSC, PingFang SC', + align: 'right', + lineHeight: 17, + }, + axisLine: { + lineStyle: { + color: '#EBEBEB', + } + }, + }, + series: getSeries() + }} + style={{ height: '265px', width: '100%', minWidth: '100%', boxSizing: 'border-box' }} + opts={{ renderer: 'canvas' }} + notMerge={true} + lazyUpdate={true} + /> + ) : } + + ) +} + +export default LineCard diff --git a/web/src/views/ApplicationConfig/index.tsx b/web/src/views/ApplicationConfig/index.tsx index 7d5d5950..4dd9231a 100644 --- a/web/src/views/ApplicationConfig/index.tsx +++ b/web/src/views/ApplicationConfig/index.tsx @@ -9,6 +9,7 @@ import ReleasePage from './ReleasePage' import Cluster from './Cluster' import { getApplication } from '@/api/application' import Workflow from '@/views/Workflow'; +import Statistics from './Statistics' const ApplicationConfig: React.FC = () => { const { id } = useParams(); @@ -68,6 +69,7 @@ const ApplicationConfig: React.FC = () => { {activeTab === 'arrangement' && application?.type === 'workflow' && } {activeTab === 'api' && } {activeTab === 'release' && } + {activeTab === 'statistics' && } ); }; diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 6f641ebb..9df6e04a 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -150,4 +150,19 @@ export interface AiPromptForm { } export interface ChatVariableConfigModalRef { handleOpen: (values: Variable[]) => void; +} + +export interface StatisticsItem { + count: number; + date: string; +} +export interface StatisticsData { + daily_conversations: StatisticsItem[]; + daily_new_users: StatisticsItem[]; + daily_api_calls: StatisticsItem[]; + daily_tokens: StatisticsItem[]; + total_conversations: number; + total_new_users: number; + total_api_calls: number; + total_tokens: number; } \ No newline at end of file