Pārlūkot izejas kodu

Feat/dashboard more chart (#266)

tags/0.3.2
Joel pirms 2 gadiem
vecāks
revīzija
5239b2c7ab
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 23
- 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx Parādīt failu

import dayjs from 'dayjs' import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { fetchAppDetail } from '@/service/apps'
import type { PeriodParams } from '@/app/components/app/overview/appChart' 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 type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter' import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
} }


export default function ChartView({ appId }: 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 { 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) } }) const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })


setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
} }


if (!response)
return null

return ( return (
<div> <div>
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'> <div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
<EndUsersChart period={period} id={appId} /> <EndUsersChart period={period} id={appId} />
</div> </div>
</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} /> <CostChart period={period} id={appId} />
</div> </div>
) )

+ 64
- 10
web/app/components/app/overview/appChart.tsx Parādīt failu

import useSWR from 'swr' import useSWR from 'swr'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import { formatNumber } from '@/utils/format'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic' import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app' 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 valueFormatter = (v: string | number) => v


const COLOR_TYPE_MAP = { const COLOR_TYPE_MAP = {
export type IChartProps = { export type IChartProps = {
className?: string className?: string
basicInfo: { title: string; explanation: string; timePeriod: string } basicInfo: { title: string; explanation: string; timePeriod: string }
valueKey?: string
isAvg?: boolean
unit?: string
yMax?: number yMax?: number
chartType: IChartType chartType: IChartType
chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> } chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> }
basicInfo: { title, explanation, timePeriod }, basicInfo: { title, explanation, timePeriod },
chartType = 'conversations', chartType = 'conversations',
chartData, chartData,
valueKey,
isAvg,
unit = '',
yMax, yMax,
className, className,
}) => { }) => {
extraDataForMarkLine.unshift('') extraDataForMarkLine.unshift('')


const xData = statistics.map(({ date }) => date) 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) => { const yData = statistics.map((item) => {
// @ts-expect-error field is valid // @ts-expect-error field is valid
return item[yField] || 0 return item[yField] || 0
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div> return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
<div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])} <div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])}
${!CHART_TYPE_CONFIG[chartType].showTokens ${!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='margin-left:4px;color:#6B7280'>(</span>
<span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span> <span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span>
<span style='color:#6B7280'>)</span> <span style='color:#6B7280'>)</span>
}, },
], ],
} }

const sumData = sum(yData)
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)


return ( return (
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}> <div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
</div> </div>
<div className='mb-4'> <div className='mb-4'>
<Basic <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 type={!CHART_TYPE_CONFIG[chartType].showTokens
? '' ? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'> : <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
) )
} }


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') 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) item.date = dayjs(start).add(index, 'day').format(commonDateFormat)
return item return item
}) })
/> />
} }


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 }) => { export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation() const { t } = useTranslation()



+ 1
- 0
web/i18n/lang/app-overview.en.ts Parādīt failu

}, },
analysis: { analysis: {
title: 'Analysis', title: 'Analysis',
ms: 'ms',
totalMessages: { totalMessages: {
title: 'Total Messages', title: 'Total Messages',
explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.', explanation: 'Daily AI interactions count; prompt engineering/debugging excluded.',

+ 1
- 0
web/i18n/lang/app-overview.zh.ts Parādīt failu

}, },
analysis: { analysis: {
title: '分析', title: '分析',
ms: '毫秒',
totalMessages: { totalMessages: {
title: '全部消息数', title: '全部消息数',
explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。', explanation: '反映 AI 每天的互动总次数,每回答用户一个问题算一条 Message。提示词编排和调试的消息不计入。',

+ 4
- 0
web/models/app.ts Parādīt failu

data: Array<{ date: string; conversation_count: number }> data: Array<{ date: string; conversation_count: number }>
} }


export type AppStatisticsResponse = {
data: Array<{ date: string }>
}

export type AppDailyEndUsersResponse = { export type AppDailyEndUsersResponse = {
data: Array<{ date: string; terminal_count: number }> data: Array<{ date: string; terminal_count: number }>
} }

+ 6
- 2
web/service/apps.ts Parādīt failu

import type { Fetcher } from 'swr' import type { Fetcher } from 'swr'
import { del, get, post } from './base' 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 { CommonResponse } from '@/models/common'
import type { AppMode, ModelConfig } from '@/types/app' import type { AppMode, ModelConfig } from '@/types/app'


return get(url) as Promise<AppTemplatesResponse> 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> return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise<AppDetailResponse>
} }


return get(url, { params }) as Promise<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 }) => { export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get(url, { params }) as Promise<AppDailyEndUsersResponse> return get(url, { params }) as Promise<AppDailyEndUsersResponse>
} }

Notiek ielāde…
Atcelt
Saglabāt