'use client' import type { FC } from 'react' import React from 'react' import ReactECharts from 'echarts-for-react' import type { EChartsOption } from 'echarts' import useSWR from 'swr' import dayjs from 'dayjs' import { get } from 'lodash-es' import Decimal from 'decimal.js' 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, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app' import { getAppDailyConversations, getAppDailyEndUsers, getAppDailyMessages, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps' const valueFormatter = (v: string | number) => v const COLOR_TYPE_MAP = { green: { lineColor: 'rgba(6, 148, 162, 1)', bgColor: ['rgba(6, 148, 162, 0.2)', 'rgba(67, 174, 185, 0.08)'], }, orange: { lineColor: 'rgba(255, 138, 76, 1)', bgColor: ['rgba(254, 145, 87, 0.2)', 'rgba(255, 138, 76, 0.1)'], }, blue: { lineColor: 'rgba(28, 100, 242, 1)', bgColor: ['rgba(28, 100, 242, 0.3)', 'rgba(28, 100, 242, 0.1)'], }, } const COMMON_COLOR_MAP = { label: '#9CA3AF', splitLineLight: '#F3F4F6', splitLineDark: '#E5E7EB', } type IColorType = 'green' | 'orange' | 'blue' type IChartType = 'messages' | 'conversations' | 'endUsers' | 'costs' | 'workflowCosts' type IChartConfigType = { colorType: IColorType; showTokens?: boolean } const commonDateFormat = 'MMM D, YYYY' const CHART_TYPE_CONFIG: Record = { messages: { colorType: 'green', }, conversations: { colorType: 'green', }, endUsers: { colorType: 'orange', }, costs: { colorType: 'blue', showTokens: true, }, workflowCosts: { colorType: 'blue', }, } const sum = (arr: Decimal.Value[]): number => { return Decimal.sum(...arr).toNumber() } const defaultPeriod = { start: dayjs().subtract(7, 'day').format(commonDateFormat), end: dayjs().format(commonDateFormat), } export type PeriodParams = { name: string query?: { start: string end: string } } export type IBizChartProps = { period: PeriodParams id: string } export type IChartProps = { className?: string basicInfo: { title: string; explanation: string; timePeriod: string } valueKey?: string isAvg?: boolean unit?: string yMax?: number chartType: IChartType chartData: AppDailyMessagesResponse | AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> } } const Chart: React.FC = ({ basicInfo: { title, explanation, timePeriod }, chartType = 'conversations', chartData, valueKey, isAvg, unit = '', yMax, className, }) => { const { t } = useTranslation() const statistics = chartData.data const statisticsLen = statistics.length const extraDataForMarkLine = new Array(statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen).fill('1') extraDataForMarkLine.push('') extraDataForMarkLine.unshift('') const xData = statistics.map(({ date }) => date) 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 }) const options: EChartsOption = { dataset: { dimensions: ['date', yField], source: statistics, }, grid: { top: 8, right: 36, bottom: 0, left: 0, containLabel: true }, tooltip: { trigger: 'item', position: 'top', borderWidth: 0, }, xAxis: [{ type: 'category', boundaryGap: false, axisLabel: { color: COMMON_COLOR_MAP.label, hideOverlap: true, overflow: 'break', formatter(value) { return dayjs(value).format(commonDateFormat) }, }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: true, lineStyle: { color: COMMON_COLOR_MAP.splitLineLight, width: 1, type: [10, 10], }, interval(index) { return index === 0 || index === xData.length - 1 }, }, }, { position: 'bottom', boundaryGap: false, data: extraDataForMarkLine, axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: true, lineStyle: { color: COMMON_COLOR_MAP.splitLineDark, }, interval(index, value) { return !!value }, }, }], yAxis: { max: yMax ?? 'dataMax', type: 'value', axisLabel: { color: COMMON_COLOR_MAP.label, hideOverlap: true }, splitLine: { lineStyle: { color: COMMON_COLOR_MAP.splitLineLight, }, }, }, series: [ { type: 'line', showSymbol: true, // symbol: 'circle', // triggerLineEvent: true, symbolSize: 4, lineStyle: { color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].lineColor, width: 2, }, itemStyle: { color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].lineColor, }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].bgColor[0], }, { offset: 1, color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].bgColor[1], }], global: false, }, }, tooltip: { padding: [8, 12, 8, 12], formatter(params) { return `
${params.name}
${valueFormatter((params.data as any)[yField])} ${!CHART_TYPE_CONFIG[chartType].showTokens ? '' : ` ( ~$${get(params.data, 'total_price', 0)} ) `}
` }, }, }, ], } const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData) return (
{t('appOverview.analysis.tokenUsage.consumed')} Tokens ( ~{sum(statistics.map(item => parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })} ) } textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} />
) } 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: '', [key]: 0 })).map((item, index) => { item.date = dayjs(start).add(index, 'day').format(commonDateFormat) return item }) } export const MessagesChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const ConversationsChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const EndUsersChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } 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 TokenPerSecond: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, 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() const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const WorkflowMessagesChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const WorkflowDailyTerminalsChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const WorkflowCostChart: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export const AvgUserInteractions: FC = ({ id, period }) => { const { t } = useTranslation() const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics) if (!response) return const noDataFlag = !response.data || response.data.length === 0 return } export default Chart