mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
Feat/dashboard more chart (#266)
This commit is contained in:
parent
ae94b067b3
commit
5239b2c7ab
|
@ -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<PeriodParams>({ 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 (
|
||||
<div>
|
||||
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
|
||||
|
@ -46,6 +54,20 @@ export default function ChartView({ appId }: IChartViewProps) {
|
|||
<EndUsersChart period={period} id={appId} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row w-full mb-6'>
|
||||
<div className='flex-1 mr-3'>
|
||||
{isChatApp
|
||||
? (
|
||||
<AvgSessionInteractions period={period} id={appId} />
|
||||
)
|
||||
: (
|
||||
<AvgResponseTime period={period} id={appId} />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 ml-3'>
|
||||
<UserSatisfactionRate period={period} id={appId} />
|
||||
</div>
|
||||
</div>
|
||||
<CostChart period={period} id={appId} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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<IChartProps> = ({
|
|||
basicInfo: { title, explanation, timePeriod },
|
||||
chartType = 'conversations',
|
||||
chartData,
|
||||
valueKey,
|
||||
isAvg,
|
||||
unit = '',
|
||||
yMax,
|
||||
className,
|
||||
}) => {
|
||||
|
@ -96,7 +102,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||
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<IChartProps> = ({
|
|||
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
|
||||
<div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])}
|
||||
${!CHART_TYPE_CONFIG[chartType].showTokens
|
||||
? ''
|
||||
: `<span style='font-size:12px'>
|
||||
? ''
|
||||
: `<span style='font-size:12px'>
|
||||
<span style='margin-left:4px;color:#6B7280'>(</span>
|
||||
<span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span>
|
||||
<span style='color:#6B7280'>)</span>
|
||||
|
@ -211,8 +217,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||
},
|
||||
],
|
||||
}
|
||||
|
||||
const sumData = sum(yData)
|
||||
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
|
||||
|
@ -221,7 +226,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||
</div>
|
||||
<div className='mb-4'>
|
||||
<Basic
|
||||
name={chartType !== 'costs' ? sumData.toLocaleString() : `${sumData < 1000 ? sumData : (formatNumber(Math.round(sumData / 1000)) + 'k')}`}
|
||||
name={chartType !== 'costs' ? (sumData.toLocaleString() + unit) : `${sumData < 1000 ? sumData : (`${formatNumber(Math.round(sumData / 1000))}k`)}`}
|
||||
type={!CHART_TYPE_CONFIG[chartType].showTokens
|
||||
? ''
|
||||
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
||||
|
@ -236,9 +241,9 @@ const Chart: React.FC<IChartProps> = ({
|
|||
)
|
||||
}
|
||||
|
||||
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<IBizChartProps> = ({ id, period }) => {
|
|||
/>
|
||||
}
|
||||
|
||||
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.avgSessionInteractions.title'), explanation: t('appOverview.analysis.avgSessionInteractions.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'interactions' }) } as any}
|
||||
chartType='conversations'
|
||||
valueKey='interactions'
|
||||
isAvg
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.avgResponseTime.title'), explanation: t('appOverview.analysis.avgResponseTime.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'latency' }) } as any}
|
||||
valueKey='latency'
|
||||
chartType='conversations'
|
||||
isAvg
|
||||
unit={t('appOverview.analysis.ms') as string}
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.userSatisfactionRate.title'), explanation: t('appOverview.analysis.userSatisfactionRate.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'rate' }) } as any}
|
||||
valueKey='rate'
|
||||
chartType='endUsers'
|
||||
isAvg
|
||||
{...(noDataFlag && { yMax: 1000 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ const translation = {
|
|||
},
|
||||
analysis: {
|
||||
title: 'Analysis',
|
||||
ms: 'ms',
|
||||
totalMessages: {
|
||||
title: 'Total Messages',
|
||||
explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.',
|
||||
|
|
|
@ -71,6 +71,7 @@ const translation = {
|
|||
},
|
||||
analysis: {
|
||||
title: '分析',
|
||||
ms: '毫秒',
|
||||
totalMessages: {
|
||||
title: '全部消息数',
|
||||
explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。',
|
||||
|
|
|
@ -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 }>
|
||||
}
|
||||
|
|
|
@ -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<AppTemplatesResponse, { url: string }> =
|
|||
return get(url) as Promise<AppTemplatesResponse>
|
||||
}
|
||||
|
||||
export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string, icon_background: string, mode: AppMode; config?: ModelConfig }> = ({ name, icon, icon_background, mode, config }) => {
|
||||
export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string; icon_background: string; mode: AppMode; config?: ModelConfig }> = ({ name, icon, icon_background, mode, config }) => {
|
||||
return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise<AppDetailResponse>
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,10 @@ export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, {
|
|||
return get(url, { params }) as Promise<AppDailyConversationsResponse>
|
||||
}
|
||||
|
||||
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
return get(url, { params }) as Promise<AppStatisticsResponse>
|
||||
}
|
||||
|
||||
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
return get(url, { params }) as Promise<AppDailyEndUsersResponse>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user