Refactor apps service toward TanStack Query (#29004)

This commit is contained in:
yyh
2025-12-02 15:18:33 +08:00
committed by GitHub
parent 369892634d
commit f8b10c2272
11 changed files with 491 additions and 265 deletions

View File

@@ -1,6 +1,5 @@
'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiGraduationCapFill,
@@ -23,8 +22,9 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
import { fetchAppList } from '@/service/apps'
import type { App } from '@/types/app'
import { useAppList } from '@/service/use-apps'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -36,7 +36,7 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
const apps = appList?.data || []
const { mutateUserProfile, userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()

View File

@@ -3,7 +3,6 @@ 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 type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { get } from 'lodash-es'
@@ -13,7 +12,20 @@ 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'
import {
useAppAverageResponseTime,
useAppAverageSessionInteractions,
useAppDailyConversations,
useAppDailyEndUsers,
useAppDailyMessages,
useAppSatisfactionRate,
useAppTokenCosts,
useAppTokensPerSecond,
useWorkflowAverageInteractions,
useWorkflowDailyConversations,
useWorkflowDailyTerminals,
useWorkflowTokenCosts,
} from '@/service/use-apps'
const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = {
@@ -272,8 +284,8 @@ const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages)
if (!response)
const { data: response, isLoading } = useAppDailyMessages(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -286,8 +298,8 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
if (!response)
const { data: response, isLoading } = useAppDailyConversations(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -301,8 +313,8 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
const { data: response, isLoading } = useAppDailyEndUsers(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -315,8 +327,8 @@ 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)
const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -331,8 +343,8 @@ export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
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)
const { data: response, isLoading } = useAppAverageResponseTime(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -348,8 +360,8 @@ export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useAppTokensPerSecond(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -366,8 +378,8 @@ export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
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)
const { data: response, isLoading } = useAppSatisfactionRate(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -384,8 +396,8 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
const { data: response, isLoading } = useAppTokenCosts(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -398,8 +410,8 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
if (!response)
const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -414,8 +426,8 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -429,8 +441,8 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -443,8 +455,8 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
if (!response)
const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query)
if (isLoading || !response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart

View File

@@ -23,7 +23,7 @@ const Empty = () => {
return (
<>
<DefaultCards />
<div className='absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent pointer-events-none'>
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<span className='system-md-medium text-text-tertiary'>
{t('app.newApp.noAppsFound')}
</span>

View File

@@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import {
useRouter,
} from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import {
@@ -19,8 +18,6 @@ import AppCard from './app-card'
import NewAppCard from './new-app-card'
import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import type { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
@@ -35,6 +32,7 @@ import Empty from './empty'
import Footer from './footer'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AppModeEnum } from '@/types/app'
import { useInfiniteAppList } from '@/service/use-apps'
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false,
@@ -43,30 +41,6 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
ssr: false,
})
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
isCreatedByMe: boolean,
tags: string[],
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
if (tags.length)
params.params.tag_ids = tags
return params
}
return null
}
const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
@@ -102,16 +76,24 @@ const List = () => {
enabled: isCurrentWorkspaceEditor,
})
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const appListQueryParams = {
page: 1,
limit: 30,
name: searchKeywords,
tag_ids: tagIDs,
is_created_by_me: isCreatedByMe,
...(activeTab !== 'all' ? { mode: activeTab as AppModeEnum } : {}),
}
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
error,
refetch,
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
@@ -126,9 +108,9 @@ const List = () => {
useEffect(() => {
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()
refetch()
}
}, [mutate, t])
}, [refetch])
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
@@ -136,7 +118,9 @@ const List = () => {
}, [router, isCurrentWorkspaceDatasetOperator])
useEffect(() => {
const hasMore = data?.at(-1)?.has_more ?? true
if (isCurrentWorkspaceDatasetOperator)
return
const hasMore = hasNextPage ?? true
let observer: IntersectionObserver | undefined
if (error) {
@@ -151,8 +135,8 @@ const List = () => {
const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) // Clamps to 100-200px range, using 20% of container height as the base value
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoading && !error && hasMore)
setSize((size: number) => size + 1)
if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore)
fetchNextPage()
}, {
root: containerRef.current,
rootMargin: `${dynamicMargin}px`,
@@ -161,7 +145,7 @@ const List = () => {
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, setSize, data, error])
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
@@ -185,6 +169,9 @@ const List = () => {
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
}, [isCreatedByMe, setQuery])
const pages = data?.pages ?? []
const hasAnyApp = (pages[0]?.total ?? 0) > 0
return (
<>
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
@@ -217,17 +204,17 @@ const List = () => {
/>
</div>
</div>
{(data && data[0].total > 0)
{hasAnyApp
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
{data.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
)))}
</div>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} selectedAppType={activeTab} />}
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
<Empty />
</div>}
@@ -261,7 +248,7 @@ const List = () => {
onSuccess={() => {
setShowCreateFromDSLModal(false)
setDroppedDSLFile(undefined)
mutate()
refetch()
}}
droppedFile={droppedDSLFile}
/>

View File

@@ -1,5 +1,4 @@
'use client'
import useSWR from 'swr'
import { produce } from 'immer'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
@@ -9,7 +8,6 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } fro
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { Item } from '@/app/components/base/select'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import AudioBtn from '@/app/components/base/audio-btn'
@@ -17,6 +15,7 @@ import { languages } from '@/i18n-config/language'
import { TtsAutoPlay } from '@/types/app'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import classNames from '@/utils/classnames'
import { useAppVoices } from '@/service/use-apps'
type VoiceParamConfigProps = {
onClose: () => void
@@ -39,7 +38,7 @@ const VoiceParamConfig = ({
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
const { data: voiceItems } = useAppVoices(appId, language)
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
if (voiceItems && !voiceItem)
voiceItem = voiceItems[0]

View File

@@ -5,7 +5,7 @@ import {
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR, { useSWRConfig } from 'swr'
import useSWR from 'swr'
import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css'
import ActionButton from '@/app/components/base/action-button'
@@ -15,7 +15,6 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
import {
createApikey as createAppApikey,
delApikey as delAppApikey,
fetchApiKeysList as fetchAppApiKeysList,
} from '@/service/apps'
import {
createApikey as createDatasetApikey,
@@ -27,6 +26,7 @@ import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import useTimestamp from '@/hooks/use-timestamp'
import { useAppContext } from '@/context/app-context'
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
type ISecretKeyModalProps = {
isShow: boolean
@@ -45,12 +45,14 @@ const SecretKeyModal = ({
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setVisible] = useState(false)
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
const { mutate } = useSWRConfig()
const commonParams = appId
? { url: `/apps/${appId}/api-keys`, params: {} }
: { url: '/datasets/api-keys', params: {} }
const fetchApiKeysList = appId ? fetchAppApiKeysList : fetchDatasetApiKeysList
const { data: apiKeysList } = useSWR(commonParams, fetchApiKeysList)
const invalidateAppApiKeys = useInvalidateAppApiKeys()
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR(
!appId && isShow ? { url: '/datasets/api-keys', params: {} } : null,
fetchDatasetApiKeysList,
)
const apiKeysList = appId ? appApiKeys : datasetApiKeys
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
const [delKeyID, setDelKeyId] = useState('')
@@ -64,7 +66,10 @@ const SecretKeyModal = ({
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
await delApikey(params)
mutate(commonParams)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
}
const onCreate = async () => {
@@ -75,7 +80,10 @@ const SecretKeyModal = ({
const res = await createApikey(params)
setVisible(true)
setNewKey(res)
mutate(commonParams)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
}
const generateToken = (token: string) => {
@@ -88,7 +96,7 @@ const SecretKeyModal = ({
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
{!apiKeysList && <div className='mt-4'><Loading /></div>}
{isApiKeysLoading && <div className='mt-4'><Loading /></div>}
{
!!apiKeysList?.data?.length && (
<div className='mt-4 flex grow flex-col overflow-hidden'>

View File

@@ -3,7 +3,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { flatten } from 'lodash-es'
import { produce } from 'immer'
import {
@@ -12,33 +11,13 @@ import {
} from '@remixicon/react'
import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { fetchAppList } from '@/service/apps'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import type { AppListResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AppModeEnum } from '@/types/app'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
return params
}
return null
}
import { useInfiniteAppList } from '@/service/use-apps'
const AppNav = () => {
const { t } = useTranslation()
@@ -50,17 +29,21 @@ const AppNav = () => {
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
const [navItems, setNavItems] = useState<NavItem[]>([])
const { data: appsData, setSize, mutate } = useSWRInfinite(
appId
? (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, 'all', '')
: () => null,
fetchAppList,
{ revalidateFirstPage: false },
)
const {
data: appsData,
fetchNextPage,
hasNextPage,
refetch,
} = useInfiniteAppList({
page: 1,
limit: 30,
name: '',
}, { enabled: !!appId })
const handleLoadMore = useCallback(() => {
setSize(size => size + 1)
}, [setSize])
if (hasNextPage)
fetchNextPage()
}, [fetchNextPage, hasNextPage])
const openModal = (state: string) => {
if (state === 'blank')
@@ -73,7 +56,7 @@ const AppNav = () => {
useEffect(() => {
if (appsData) {
const appItems = flatten(appsData?.map(appData => appData.data))
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
const navItems = appItems.map((app) => {
const link = ((isCurrentWorkspaceEditor, app) => {
if (!isCurrentWorkspaceEditor) {
@@ -132,17 +115,17 @@ const AppNav = () => {
<CreateAppModal
show={showNewAppDialog}
onClose={() => setShowNewAppDialog(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
<CreateAppTemplateDialog
show={showNewAppTemplateDialog}
onClose={() => setShowNewAppTemplateDialog(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => setShowCreateFromDSLModal(false)}
onSuccess={() => mutate()}
onSuccess={() => refetch()}
/>
</>
)

View File

@@ -15,32 +15,10 @@ import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import useSWRInfinite from 'swr/infinite'
import { fetchAppList } from '@/service/apps'
import type { AppListResponse } from '@/models/app'
import { useInfiniteAppList } from '@/service/use-apps'
const PAGE_SIZE = 20
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
searchText: string,
) => {
if (pageIndex === 0 || (previousPageData && previousPageData.has_more)) {
const params: any = {
url: 'apps',
params: {
page: pageIndex + 1,
limit: PAGE_SIZE,
name: searchText,
},
}
return params
}
return null
}
type Props = {
value?: {
app_id: string
@@ -72,30 +50,32 @@ const AppSelector: FC<Props> = ({
const [searchText, setSearchText] = useState('')
const [isLoadingMore, setIsLoadingMore] = useState(false)
const { data, isLoading, setSize } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteAppList({
page: 1,
limit: PAGE_SIZE,
name: searchText,
})
const pages = data?.pages ?? []
const displayedApps = useMemo(() => {
if (!data) return []
return data.flatMap(({ data: apps }) => apps)
}, [data])
if (!pages.length) return []
return pages.flatMap(({ data: apps }) => apps)
}, [pages])
const hasMore = data?.at(-1)?.has_more ?? true
const hasMore = hasNextPage ?? true
const handleLoadMore = useCallback(async () => {
if (isLoadingMore || !hasMore) return
if (isLoadingMore || isFetchingNextPage || !hasMore) return
setIsLoadingMore(true)
try {
await setSize((size: number) => size + 1)
await fetchNextPage()
}
finally {
// Add a small delay to ensure state updates are complete
@@ -103,7 +83,7 @@ const AppSelector: FC<Props> = ({
setIsLoadingMore(false)
}, 300)
}
}, [isLoadingMore, hasMore, setSize])
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
const handleTriggerClick = () => {
if (disabled) return
@@ -185,7 +165,7 @@ const AppSelector: FC<Props> = ({
onSelect={handleSelectApp}
scope={scope || 'all'}
apps={displayedApps}
isLoading={isLoading || isLoadingMore}
isLoading={isLoading || isLoadingMore || isFetchingNextPage}
hasMore={hasMore}
onLoadMore={handleLoadMore}
searchText={searchText}

View File

@@ -1,15 +1,14 @@
import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WebhookTriggerResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app'
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
export const fetchAppList = ({ url, params }: { url: string; params?: Record<string, any> }): Promise<AppListResponse> => {
return get<AppListResponse>(url, { params })
}
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
export const fetchAppDetail = ({ url, id }: { url: string; id: string }): Promise<AppDetailResponse> => {
return get<AppDetailResponse>(`${url}/${id}`)
}
@@ -18,24 +17,74 @@ export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: strin
return get<AppDetailResponse>(`${url}/${id}`)
}
export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
export const fetchAppTemplates = ({ url }: { url: string }): Promise<AppTemplatesResponse> => {
return get<AppTemplatesResponse>(url)
}
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppModeEnum; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
export const createApp = ({
name,
icon_type,
icon,
icon_background,
mode,
description,
config,
}: {
name: string
icon_type?: AppIconType
icon?: string
icon_background?: string
mode: AppModeEnum
description?: string
config?: ModelConfig
}): Promise<AppDetailResponse> => {
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
}
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; max_active_requests?: number | null }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }) => {
export const updateAppInfo = ({
appID,
name,
icon_type,
icon,
icon_background,
description,
use_icon_as_answer_icon,
max_active_requests,
}: {
appID: string
name: string
icon_type: AppIconType
icon: string
icon_background?: string
description: string
use_icon_as_answer_icon?: boolean
max_active_requests?: number | null
}): Promise<AppDetailResponse> => {
const body = { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }
return put<AppDetailResponse>(`apps/${appID}`, { body })
}
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppModeEnum; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
export const copyApp = ({
appID,
name,
icon_type,
icon,
icon_background,
mode,
description,
}: {
appID: string
name: string
icon_type: AppIconType
icon: string
icon_background?: string | null
mode: AppModeEnum
description?: string
}): Promise<AppDetailResponse> => {
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
}
export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean; workflowID?: string }> = ({ appID, include = false, workflowID }) => {
export const exportAppConfig = ({ appID, include = false, workflowID }: { appID: string; include?: boolean; workflowID?: string }): Promise<{ data: string }> => {
const params = new URLSearchParams({
include_secret: include.toString(),
})
@@ -44,126 +93,116 @@ export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include
return get<{ data: string }>(`apps/${appID}/export?${params.toString()}`)
}
// TODO: delete
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
}
// TODO: delete
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
}
export const importDSL: Fetcher<DSLImportResponse, { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }) => {
export const importDSL = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }: { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }): Promise<DSLImportResponse> => {
return post<DSLImportResponse>('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } })
}
export const importDSLConfirm: Fetcher<DSLImportResponse, { import_id: string }> = ({ import_id }) => {
export const importDSLConfirm = ({ import_id }: { import_id: string }): Promise<DSLImportResponse> => {
return post<DSLImportResponse>(`apps/imports/${import_id}/confirm`, { body: {} })
}
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
export const switchApp = ({ appID, name, icon_type, icon, icon_background }: { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }): Promise<{ new_app_id: string }> => {
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
}
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
export const deleteApp = (appID: string): Promise<CommonResponse> => {
return del<CommonResponse>(`apps/${appID}`)
}
export const updateAppSiteStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
export const updateAppSiteStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppApiStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
export const updateAppApiStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
return post<AppDetailResponse>(url, { body })
}
// path: /apps/{appId}/rate-limit
export const updateAppRateLimit: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
export const updateAppRateLimit = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppSiteAccessToken: Fetcher<UpdateAppSiteCodeResponse, { url: string }> = ({ url }) => {
export const updateAppSiteAccessToken = ({ url }: { url: string }): Promise<UpdateAppSiteCodeResponse> => {
return post<UpdateAppSiteCodeResponse>(url)
}
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }) => {
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
return post<AppDetailResponse>(url, { body })
}
export const getAppDailyMessages: Fetcher<AppDailyMessagesResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getAppDailyMessages = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyMessagesResponse> => {
return get<AppDailyMessagesResponse>(url, { params })
}
export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getAppDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyConversationsResponse> => {
return get<AppDailyConversationsResponse>(url, { params })
}
export const getWorkflowDailyConversations: Fetcher<WorkflowDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getWorkflowDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<WorkflowDailyConversationsResponse> => {
return get<WorkflowDailyConversationsResponse>(url, { params })
}
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getAppStatistics = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppStatisticsResponse> => {
return get<AppStatisticsResponse>(url, { params })
}
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getAppDailyEndUsers = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyEndUsersResponse> => {
return get<AppDailyEndUsersResponse>(url, { params })
}
export const getAppTokenCosts: Fetcher<AppTokenCostsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const getAppTokenCosts = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppTokenCostsResponse> => {
return get<AppTokenCostsResponse>(url, { params })
}
export const updateAppModelConfig: Fetcher<UpdateAppModelConfigResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
export const updateAppModelConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<UpdateAppModelConfigResponse> => {
return post<UpdateAppModelConfigResponse>(url, { body })
}
// For temp testing
export const fetchAppListNoMock: Fetcher<AppListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const fetchAppListNoMock = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppListResponse> => {
return get<AppListResponse>(url, params)
}
export const fetchApiKeysList: Fetcher<ApiKeysListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const fetchApiKeysList = ({ url, params }: { url: string; params: Record<string, any> }): Promise<ApiKeysListResponse> => {
return get<ApiKeysListResponse>(url, params)
}
export const delApikey: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
export const delApikey = ({ url, params }: { url: string; params: Record<string, any> }): Promise<CommonResponse> => {
return del<CommonResponse>(url, params)
}
export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
export const createApikey = ({ url, body }: { url: string; body: Record<string, any> }): Promise<CreateApiKeyResponse> => {
return post<CreateApiKeyResponse>(url, body)
}
export const validateOpenAIKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
export const validateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<ValidateOpenAIKeyResponse> => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const updateOpenAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
export const updateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<UpdateOpenAIKeyResponse> => {
return post<UpdateOpenAIKeyResponse>(url, { body })
}
export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { url: string; body: { prompt_template: string } }> = ({ url, body }) => {
export const generationIntroduction = ({ url, body }: { url: string; body: { prompt_template: string } }): Promise<GenerationIntroductionResponse> => {
return post<GenerationIntroductionResponse>(url, { body })
}
export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => {
export const fetchAppVoices = ({ appId, language }: { appId: string; language?: string }): Promise<AppVoicesListResponse> => {
language = language || 'en-US'
return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`)
}
// Tracing
export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => {
return get(`/apps/${appId}/trace`)
export const fetchTracingStatus = ({ appId }: { appId: string }): Promise<TracingStatus> => {
return get<TracingStatus>(`/apps/${appId}/trace`)
}
export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace`, { body })
export const updateTracingStatus = ({ appId, body }: { appId: string; body: Record<string, any> }): Promise<CommonResponse> => {
return post<CommonResponse>(`/apps/${appId}/trace`, { body })
}
// Webhook Trigger
export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; nodeId: string }> = ({ appId, nodeId }) => {
export const fetchWebhookUrl = ({ appId, nodeId }: { appId: string; nodeId: string }): Promise<WebhookTriggerResponse> => {
return get<WebhookTriggerResponse>(
`apps/${appId}/workflows/triggers/webhook`,
{ params: { node_id: nodeId } },
@@ -171,22 +210,22 @@ export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; n
)
}
export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return get(`/apps/${appId}/trace-config`, {
export const fetchTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<TracingConfig & { has_not_configured: true }> => {
return get<TracingConfig & { has_not_configured: true }>(`/apps/${appId}/trace-config`, {
params: {
tracing_provider: provider,
},
})
}
export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace-config`, { body })
export const addTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
return post<CommonResponse>(`/apps/${appId}/trace-config`, { body })
}
export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return patch(`/apps/${appId}/trace-config`, { body })
export const updateTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
return patch<CommonResponse>(`/apps/${appId}/trace-config`, { body })
}
export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
export const removeTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<CommonResponse> => {
return del<CommonResponse>(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
}

View File

@@ -1,38 +1,85 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { createApp, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import Loading from '@/app/components/base/loading'
import { AppModeEnum } from '@/types/app'
import {
useAppDailyConversations,
useAppDailyEndUsers,
useAppDetail,
useAppList,
} from '../use-apps'
const Service: FC = () => {
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
const { data: updateAppSiteStatusRes, error: err1 } = useSWR({ url: '/apps', id: '1', body: { enable_site: false } }, updateAppSiteStatus)
const { data: updateAppApiStatusRes, error: err2 } = useSWR({ url: '/apps', id: '1', body: { enable_api: true } }, updateAppApiStatus)
const { data: updateAppRateLimitRes, error: err3 } = useSWR({ url: '/apps', id: '1', body: { api_rpm: 10, api_rph: 20 } }, updateAppRateLimit)
const { data: updateAppSiteCodeRes, error: err4 } = useSWR({ url: '/apps', id: '1', body: {} }, updateAppSiteAccessToken)
const { data: updateAppSiteConfigRes, error: err5 } = useSWR({ url: '/apps', id: '1', body: { title: 'title test', author: 'author test' } }, updateAppSiteConfig)
const { data: getAppDailyConversationsRes, error: err6 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyConversations)
const { data: getAppDailyEndUsersRes, error: err7 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyEndUsers)
const { data: updateAppModelConfigRes, error: err8 } = useSWR({ url: '/apps', id: '1', body: { model_id: 'gpt-100' } }, updateAppModelConfig)
const appId = '1'
const queryClient = useQueryClient()
const { mutate } = useSWRConfig()
const { data: appList, error: appListError, isLoading: isAppListLoading } = useAppList({ page: 1, limit: 30, name: '' })
const { data: firstApp, error: appDetailError, isLoading: isAppDetailLoading } = useAppDetail(appId)
const handleCreateApp = async () => {
await createApp({
const { data: updateAppSiteStatusRes, error: err1, isLoading: isUpdatingSiteStatus } = useQuery({
queryKey: ['demo', 'updateAppSiteStatus', appId],
queryFn: () => updateAppSiteStatus({ url: '/apps', body: { enable_site: false } }),
})
const { data: updateAppApiStatusRes, error: err2, isLoading: isUpdatingApiStatus } = useQuery({
queryKey: ['demo', 'updateAppApiStatus', appId],
queryFn: () => updateAppApiStatus({ url: '/apps', body: { enable_api: true } }),
})
const { data: updateAppRateLimitRes, error: err3, isLoading: isUpdatingRateLimit } = useQuery({
queryKey: ['demo', 'updateAppRateLimit', appId],
queryFn: () => updateAppRateLimit({ url: '/apps', body: { api_rpm: 10, api_rph: 20 } }),
})
const { data: updateAppSiteCodeRes, error: err4, isLoading: isUpdatingSiteCode } = useQuery({
queryKey: ['demo', 'updateAppSiteAccessToken', appId],
queryFn: () => updateAppSiteAccessToken({ url: '/apps' }),
})
const { data: updateAppSiteConfigRes, error: err5, isLoading: isUpdatingSiteConfig } = useQuery({
queryKey: ['demo', 'updateAppSiteConfig', appId],
queryFn: () => updateAppSiteConfig({ url: '/apps', body: { title: 'title test', author: 'author test' } }),
})
const { data: getAppDailyConversationsRes, error: err6, isLoading: isConversationsLoading } = useAppDailyConversations(appId, { start: '1', end: '2' })
const { data: getAppDailyEndUsersRes, error: err7, isLoading: isEndUsersLoading } = useAppDailyEndUsers(appId, { start: '1', end: '2' })
const { data: updateAppModelConfigRes, error: err8, isLoading: isUpdatingModelConfig } = useQuery({
queryKey: ['demo', 'updateAppModelConfig', appId],
queryFn: () => updateAppModelConfig({ url: '/apps', body: { model_id: 'gpt-100' } }),
})
const { mutateAsync: mutateCreateApp } = useMutation({
mutationKey: ['demo', 'createApp'],
mutationFn: () => createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: AppModeEnum.CHAT,
})
// reload app list
mutate({ url: '/apps', params: { page: 1 } })
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['apps', 'list'],
})
},
})
const handleCreateApp = async () => {
await mutateCreateApp()
}
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
return <div>{JSON.stringify(appListError)}</div>
return <div>{JSON.stringify(appListError ?? appDetailError ?? err1 ?? err2 ?? err3 ?? err4 ?? err5 ?? err6 ?? err7 ?? err8)}</div>
if (!appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
const isLoading = isAppListLoading
|| isAppDetailLoading
|| isUpdatingSiteStatus
|| isUpdatingApiStatus
|| isUpdatingRateLimit
|| isUpdatingSiteCode
|| isUpdatingSiteConfig
|| isConversationsLoading
|| isEndUsersLoading
|| isUpdatingModelConfig
if (isLoading || !appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
return <Loading />
return (

View File

@@ -1,31 +1,63 @@
import { get, post } from './base'
import type { App } from '@/types/app'
import type { AppListResponse } from '@/models/app'
import type {
ApiKeysListResponse,
AppDailyConversationsResponse,
AppDailyEndUsersResponse,
AppDailyMessagesResponse,
AppListResponse,
AppStatisticsResponse,
AppTokenCostsResponse,
AppVoicesListResponse,
WorkflowDailyConversationsResponse,
} from '@/models/app'
import type { App, AppModeEnum } from '@/types/app'
import { useInvalid } from './use-base'
import { useQuery } from '@tanstack/react-query'
import {
useInfiniteQuery,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
const NAME_SPACE = 'apps'
// TODO paging for list
type AppListParams = {
page?: number
limit?: number
name?: string
mode?: AppModeEnum | 'all'
tag_ids?: string[]
is_created_by_me?: boolean
}
type DateRangeParams = {
start?: string
end?: string
}
const normalizeAppListParams = (params: AppListParams) => {
const {
page = 1,
limit = 30,
name = '',
mode,
tag_ids,
is_created_by_me,
} = params
return {
page,
limit,
name,
...(mode && mode !== 'all' ? { mode } : {}),
...(tag_ids?.length ? { tag_ids } : {}),
...(is_created_by_me ? { is_created_by_me } : {}),
}
}
const appListKey = (params: AppListParams) => [NAME_SPACE, 'list', params]
const useAppFullListKey = [NAME_SPACE, 'full-list']
export const useAppFullList = () => {
return useQuery<AppListResponse>({
queryKey: useAppFullListKey,
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100 } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
})
}
export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => {
return useQuery({
@@ -39,3 +71,142 @@ export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean)
retry: 0,
})
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
enabled: !!appID,
})
}
export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
const normalizedParams = normalizeAppListParams(params)
return useQuery<AppListResponse>({
queryKey: appListKey(normalizedParams),
queryFn: () => get<AppListResponse>('/apps', { params: normalizedParams }),
...options,
})
}
export const useAppFullList = () => {
return useQuery<AppListResponse>({
queryKey: useAppFullListKey,
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100, name: '' } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useInfiniteAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
const normalizedParams = normalizeAppListParams(params)
return useInfiniteQuery<AppListResponse>({
queryKey: appListKey(normalizedParams),
queryFn: ({ pageParam = normalizedParams.page }) => get<AppListResponse>('/apps', { params: { ...normalizedParams, page: pageParam } }),
getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined,
initialPageParam: normalizedParams.page,
...options,
})
}
export const useInvalidateAppList = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'list'],
})
}
}
const useAppStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
return useQuery<T>({
queryKey: [NAME_SPACE, 'statistics', metric, appId, params],
queryFn: () => get<T>(`/apps/${appId}/statistics/${metric}`, { params }),
enabled: !!appId,
})
}
const useWorkflowStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
return useQuery<T>({
queryKey: [NAME_SPACE, 'workflow-statistics', metric, appId, params],
queryFn: () => get<T>(`/apps/${appId}/workflow/statistics/${metric}`, { params }),
enabled: !!appId,
})
}
export const useAppDailyMessages = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyMessagesResponse>('daily-messages', appId, params)
}
export const useAppDailyConversations = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyConversationsResponse>('daily-conversations', appId, params)
}
export const useAppDailyEndUsers = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyEndUsersResponse>('daily-end-users', appId, params)
}
export const useAppAverageSessionInteractions = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('average-session-interactions', appId, params)
}
export const useAppAverageResponseTime = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('average-response-time', appId, params)
}
export const useAppTokensPerSecond = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('tokens-per-second', appId, params)
}
export const useAppSatisfactionRate = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('user-satisfaction-rate', appId, params)
}
export const useAppTokenCosts = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
}
export const useWorkflowDailyConversations = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<WorkflowDailyConversationsResponse>('daily-conversations', appId, params)
}
export const useWorkflowDailyTerminals = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppDailyEndUsersResponse>('daily-terminals', appId, params)
}
export const useWorkflowTokenCosts = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
}
export const useWorkflowAverageInteractions = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppStatisticsResponse>('average-app-interactions', appId, params)
}
export const useAppVoices = (appId?: string, language?: string) => {
return useQuery<AppVoicesListResponse>({
queryKey: [NAME_SPACE, 'voices', appId, language || 'en-US'],
queryFn: () => get<AppVoicesListResponse>(`/apps/${appId}/text-to-audio/voices`, { params: { language: language || 'en-US' } }),
enabled: !!appId,
})
}
export const useAppApiKeys = (appId?: string, options?: { enabled?: boolean }) => {
return useQuery<ApiKeysListResponse>({
queryKey: [NAME_SPACE, 'api-keys', appId],
queryFn: () => get<ApiKeysListResponse>(`/apps/${appId}/api-keys`),
enabled: !!appId && (options?.enabled ?? true),
})
}
export const useInvalidateAppApiKeys = () => {
const queryClient = useQueryClient()
return (appId?: string) => {
if (!appId)
return
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'api-keys', appId],
})
}
}