diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index 251c29ad1a..1d1287f75d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -3,8 +3,10 @@ import React, { useState } from 'react' import dayjs from 'dayjs' import quarterOfYear from 'dayjs/plugin/quarterOfYear' import { useTranslation } from 'react-i18next' +import useSWR from 'swr' +import { fetchAppDetail } from '@/service/apps' import type { PeriodParams } from '@/app/components/app/overview/appChart' -import { ConversationsChart, CostChart, EndUsersChart } from '@/app/components/app/overview/appChart' +import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, UserSatisfactionRate } from '@/app/components/app/overview/appChart' import type { Item } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select' import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter' @@ -20,6 +22,9 @@ export type IChartViewProps = { } export default function ChartView({ appId }: IChartViewProps) { + const detailParams = { url: '/apps', id: appId } + const { data: response } = useSWR(detailParams, fetchAppDetail) + const isChatApp = response?.mode === 'chat' const { t } = useTranslation() const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) @@ -27,6 +32,9 @@ export default function ChartView({ appId }: IChartViewProps) { setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) } + if (!response) + return null + return (
@@ -46,6 +54,20 @@ export default function ChartView({ appId }: IChartViewProps) {
+
+
+ {isChatApp + ? ( + + ) + : ( + + )} +
+
+ +
+
) diff --git a/web/app/components/app/overview/appChart.tsx b/web/app/components/app/overview/appChart.tsx index a8dae564e6..277592bf4f 100644 --- a/web/app/components/app/overview/appChart.tsx +++ b/web/app/components/app/overview/appChart.tsx @@ -6,12 +6,12 @@ import type { EChartsOption } from 'echarts' import useSWR from 'swr' import dayjs from 'dayjs' import { get } from 'lodash-es' -import { formatNumber } from '@/utils/format' import { useTranslation } from 'react-i18next' +import { formatNumber } from '@/utils/format' import Basic from '@/app/components/app-sidebar/basic' import Loading from '@/app/components/base/loading' import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app' -import { getAppDailyConversations, getAppDailyEndUsers, getAppTokenCosts } from '@/service/apps' +import { getAppDailyConversations, getAppDailyEndUsers, getAppStatistics, getAppTokenCosts } from '@/service/apps' const valueFormatter = (v: string | number) => v const COLOR_TYPE_MAP = { @@ -76,6 +76,9 @@ export type IBizChartProps = { export type IChartProps = { className?: string basicInfo: { title: string; explanation: string; timePeriod: string } + valueKey?: string + isAvg?: boolean + unit?: string yMax?: number chartType: IChartType chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> } @@ -85,6 +88,9 @@ const Chart: React.FC = ({ basicInfo: { title, explanation, timePeriod }, chartType = 'conversations', chartData, + valueKey, + isAvg, + unit = '', yMax, className, }) => { @@ -96,7 +102,7 @@ const Chart: React.FC = ({ extraDataForMarkLine.unshift('') const xData = statistics.map(({ date }) => date) - const yField = Object.keys(statistics[0]).find(name => name.includes('count')) || '' + const yField = valueKey || Object.keys(statistics[0]).find(name => name.includes('count')) || '' const yData = statistics.map((item) => { // @ts-expect-error field is valid return item[yField] || 0 @@ -199,8 +205,8 @@ const Chart: React.FC = ({ return `
${params.name}
${valueFormatter((params.data as any)[yField])} ${!CHART_TYPE_CONFIG[chartType].showTokens - ? '' - : ` + ? '' + : ` ( ~$${get(params.data, 'total_price', 0)} ) @@ -211,8 +217,7 @@ const Chart: React.FC = ({ }, ], } - - const sumData = sum(yData) + const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData) return (
@@ -221,7 +226,7 @@ const Chart: React.FC = ({
{t('appOverview.analysis.tokenUsage.consumed')} Tokens @@ -236,9 +241,9 @@ const Chart: React.FC = ({ ) } -const getDefaultChartData = ({ start, end }: { start: string; end: string }) => { +const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end: string; key?: string }) => { const diffDays = dayjs(end).diff(dayjs(start), 'day') - return Array.from({ length: diffDays || 1 }, () => ({ date: '', count: 0 })).map((item, index) => { + return Array.from({ length: diffDays || 1 }, () => ({ date: '', [key]: 0 })).map((item, index) => { item.date = dayjs(start).add(index, 'day').format(commonDateFormat) return item }) @@ -273,6 +278,55 @@ export const EndUsersChart: FC = ({ id, period }) => { /> } +export const AvgSessionInteractions: FC = ({ id, period }) => { + const { t } = useTranslation() + const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics) + if (!response) + return + const noDataFlag = !response.data || response.data.length === 0 + return +} + +export const AvgResponseTime: FC = ({ id, period }) => { + const { t } = useTranslation() + const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics) + if (!response) + return + const noDataFlag = !response.data || response.data.length === 0 + return +} + +export const UserSatisfactionRate: FC = ({ id, period }) => { + const { t } = useTranslation() + const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics) + if (!response) + return + const noDataFlag = !response.data || response.data.length === 0 + return +} + export const CostChart: FC = ({ id, period }) => { const { t } = useTranslation() diff --git a/web/i18n/lang/app-overview.en.ts b/web/i18n/lang/app-overview.en.ts index 97136db98c..5df0abd476 100644 --- a/web/i18n/lang/app-overview.en.ts +++ b/web/i18n/lang/app-overview.en.ts @@ -71,6 +71,7 @@ const translation = { }, analysis: { title: 'Analysis', + ms: 'ms', totalMessages: { title: 'Total Messages', explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.', diff --git a/web/i18n/lang/app-overview.zh.ts b/web/i18n/lang/app-overview.zh.ts index 3a045602f9..c590dfa741 100644 --- a/web/i18n/lang/app-overview.zh.ts +++ b/web/i18n/lang/app-overview.zh.ts @@ -71,6 +71,7 @@ const translation = { }, analysis: { title: '分析', + ms: '毫秒', totalMessages: { title: '全部消息数', explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。', diff --git a/web/models/app.ts b/web/models/app.ts index 8c5bfd0fab..952bcdb52b 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -83,6 +83,10 @@ export type AppDailyConversationsResponse = { data: Array<{ date: string; conversation_count: number }> } +export type AppStatisticsResponse = { + data: Array<{ date: string }> +} + export type AppDailyEndUsersResponse = { data: Array<{ date: string; terminal_count: number }> } diff --git a/web/service/apps.ts b/web/service/apps.ts index f06b7c0ff4..b428efab1b 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -1,6 +1,6 @@ import type { Fetcher } from 'swr' import { del, get, post } from './base' -import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppNameResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app' +import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppNameResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app' import type { CommonResponse } from '@/models/common' import type { AppMode, ModelConfig } from '@/types/app' @@ -16,7 +16,7 @@ export const fetchAppTemplates: Fetcher = return get(url) as Promise } -export const createApp: Fetcher = ({ name, icon, icon_background, mode, config }) => { +export const createApp: Fetcher = ({ name, icon, icon_background, mode, config }) => { return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise } @@ -54,6 +54,10 @@ export const getAppDailyConversations: Fetcher } +export const getAppStatistics: Fetcher }> = ({ url, params }) => { + return get(url, { params }) as Promise +} + export const getAppDailyEndUsers: Fetcher }> = ({ url, params }) => { return get(url, { params }) as Promise }