|
|
|
@@ -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() |
|
|
|
|