diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 2cddc01876..15a03b428a 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -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() diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index 8f28e16402..5dfdad6c82 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -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 = ({ 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 const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppDailyConversations(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppDailyEndUsers(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppAverageResponseTime(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppTokensPerSecond(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppSatisfactionRate(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useAppTokenCosts(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) 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) + const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return = ({ id, period }) => { 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) + const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query) + if (isLoading || !response) return const noDataFlag = !response.data || response.data.length === 0 return { return ( <> -
+
{t('app.newApp.noAppsFound')} diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 4a52505d80..b58b82b631 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -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(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 ( <>
@@ -217,17 +204,17 @@ const List = () => { />
- {(data && data[0].total > 0) + {hasAnyApp ?
{isCurrentWorkspaceEditor - && } - {data.map(({ data: apps }) => apps.map(app => ( - + && } + {pages.map(({ data: apps }) => apps.map(app => ( + )))}
:
{isCurrentWorkspaceEditor - && } + && }
} @@ -261,7 +248,7 @@ const List = () => { onSuccess={() => { setShowCreateFromDSLModal(false) setDroppedDSLFile(undefined) - mutate() + refetch() }} droppedFile={droppedDSLFile} /> diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx index b14417e665..6b8bf2d567 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -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] diff --git a/web/app/components/develop/secret-key/secret-key-modal.tsx b/web/app/components/develop/secret-key/secret-key-modal.tsx index bde1811d05..0c0a5091b7 100644 --- a/web/app/components/develop/secret-key/secret-key-modal.tsx +++ b/web/app/components/develop/secret-key/secret-key-modal.tsx @@ -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(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 = ({

{t('appApi.apiKeyModal.apiSecretKeyTips')}

- {!apiKeysList &&
} + {isApiKeysLoading &&
} { !!apiKeysList?.data?.length && (
diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index 740e790630..1fd5c6e29d 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -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([]) - 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 = () => { setShowNewAppDialog(false)} - onSuccess={() => mutate()} + onSuccess={() => refetch()} /> setShowNewAppTemplateDialog(false)} - onSuccess={() => mutate()} + onSuccess={() => refetch()} /> setShowCreateFromDSLModal(false)} - onSuccess={() => mutate()} + onSuccess={() => refetch()} /> ) diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx index c2d5b76de5..0f7ab60bd3 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx @@ -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 = ({ 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 = ({ setIsLoadingMore(false) }, 300) } - }, [isLoadingMore, hasMore, setSize]) + }, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage]) const handleTriggerClick = () => { if (disabled) return @@ -185,7 +165,7 @@ const AppSelector: FC = ({ onSelect={handleSelectApp} scope={scope || 'all'} apps={displayedApps} - isLoading={isLoading || isLoadingMore} + isLoading={isLoading || isLoadingMore || isFetchingNextPage} hasMore={hasMore} onLoadMore={handleLoadMore} searchText={searchText} diff --git a/web/service/apps.ts b/web/service/apps.ts index 7a4cfb93ff..89001bffec 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -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 }> = ({ url, params }) => { +export const fetchAppList = ({ url, params }: { url: string; params?: Record }): Promise => { return get(url, { params }) } -export const fetchAppDetail: Fetcher = ({ url, id }) => { +export const fetchAppDetail = ({ url, id }: { url: string; id: string }): Promise => { return get(`${url}/${id}`) } @@ -18,24 +17,74 @@ export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: strin return get(`${url}/${id}`) } -export const fetchAppTemplates: Fetcher = ({ url }) => { +export const fetchAppTemplates = ({ url }: { url: string }): Promise => { return get(url) } -export const createApp: Fetcher = ({ 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 => { return post('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) } -export const updateAppInfo: Fetcher = ({ 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 => { const body = { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests } return put(`apps/${appID}`, { body }) } -export const copyApp: Fetcher = ({ 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 => { return post(`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 = ({ data, name, description, icon_type, icon, icon_background }) => { - return post('apps/import', { body: { data, name, description, icon_type, icon, icon_background } }) -} - -// TODO: delete -export const importAppFromUrl: Fetcher = ({ url, name, description, icon, icon_background }) => { - return post('apps/import/url', { body: { url, name, description, icon, icon_background } }) -} - -export const importDSL: Fetcher = ({ 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 => { return post('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } }) } -export const importDSLConfirm: Fetcher = ({ import_id }) => { +export const importDSLConfirm = ({ import_id }: { import_id: string }): Promise => { return post(`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 = (appID) => { +export const deleteApp = (appID: string): Promise => { return del(`apps/${appID}`) } -export const updateAppSiteStatus: Fetcher }> = ({ url, body }) => { +export const updateAppSiteStatus = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const updateAppApiStatus: Fetcher }> = ({ url, body }) => { +export const updateAppApiStatus = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } // path: /apps/{appId}/rate-limit -export const updateAppRateLimit: Fetcher }> = ({ url, body }) => { +export const updateAppRateLimit = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const updateAppSiteAccessToken: Fetcher = ({ url }) => { +export const updateAppSiteAccessToken = ({ url }: { url: string }): Promise => { return post(url) } -export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record }) => { +export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } -export const getAppDailyMessages: Fetcher }> = ({ url, params }) => { +export const getAppDailyMessages = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const getAppDailyConversations: Fetcher }> = ({ url, params }) => { +export const getAppDailyConversations = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const getWorkflowDailyConversations: Fetcher }> = ({ url, params }) => { +export const getWorkflowDailyConversations = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const getAppStatistics: Fetcher }> = ({ url, params }) => { +export const getAppStatistics = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const getAppDailyEndUsers: Fetcher }> = ({ url, params }) => { +export const getAppDailyEndUsers = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const getAppTokenCosts: Fetcher }> = ({ url, params }) => { +export const getAppTokenCosts = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, { params }) } -export const updateAppModelConfig: Fetcher }> = ({ url, body }) => { +export const updateAppModelConfig = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, { body }) } // For temp testing -export const fetchAppListNoMock: Fetcher }> = ({ url, params }) => { +export const fetchAppListNoMock = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, params) } -export const fetchApiKeysList: Fetcher }> = ({ url, params }) => { +export const fetchApiKeysList = ({ url, params }: { url: string; params: Record }): Promise => { return get(url, params) } -export const delApikey: Fetcher }> = ({ url, params }) => { +export const delApikey = ({ url, params }: { url: string; params: Record }): Promise => { return del(url, params) } -export const createApikey: Fetcher }> = ({ url, body }) => { +export const createApikey = ({ url, body }: { url: string; body: Record }): Promise => { return post(url, body) } -export const validateOpenAIKey: Fetcher = ({ url, body }) => { +export const validateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise => { return post(url, { body }) } -export const updateOpenAIKey: Fetcher = ({ url, body }) => { +export const updateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise => { return post(url, { body }) } -export const generationIntroduction: Fetcher = ({ url, body }) => { +export const generationIntroduction = ({ url, body }: { url: string; body: { prompt_template: string } }): Promise => { return post(url, { body }) } -export const fetchAppVoices: Fetcher = ({ appId, language }) => { +export const fetchAppVoices = ({ appId, language }: { appId: string; language?: string }): Promise => { language = language || 'en-US' return get(`apps/${appId}/text-to-audio/voices?language=${language}`) } // Tracing -export const fetchTracingStatus: Fetcher = ({ appId }) => { - return get(`/apps/${appId}/trace`) +export const fetchTracingStatus = ({ appId }: { appId: string }): Promise => { + return get(`/apps/${appId}/trace`) } -export const updateTracingStatus: Fetcher }> = ({ appId, body }) => { - return post(`/apps/${appId}/trace`, { body }) +export const updateTracingStatus = ({ appId, body }: { appId: string; body: Record }): Promise => { + return post(`/apps/${appId}/trace`, { body }) } // Webhook Trigger -export const fetchWebhookUrl: Fetcher = ({ appId, nodeId }) => { +export const fetchWebhookUrl = ({ appId, nodeId }: { appId: string; nodeId: string }): Promise => { return get( `apps/${appId}/workflows/triggers/webhook`, { params: { node_id: nodeId } }, @@ -171,22 +210,22 @@ export const fetchWebhookUrl: Fetcher = ({ appId, provider }) => { - return get(`/apps/${appId}/trace-config`, { +export const fetchTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise => { + return get(`/apps/${appId}/trace-config`, { params: { tracing_provider: provider, }, }) } -export const addTracingConfig: Fetcher = ({ appId, body }) => { - return post(`/apps/${appId}/trace-config`, { body }) +export const addTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise => { + return post(`/apps/${appId}/trace-config`, { body }) } -export const updateTracingConfig: Fetcher = ({ appId, body }) => { - return patch(`/apps/${appId}/trace-config`, { body }) +export const updateTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise => { + return patch(`/apps/${appId}/trace-config`, { body }) } -export const removeTracingConfig: Fetcher = ({ appId, provider }) => { - return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`) +export const removeTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise => { + return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`) } diff --git a/web/service/demo/index.tsx b/web/service/demo/index.tsx index 5cbfa7c52a..b0b76bfcff 100644 --- a/web/service/demo/index.tsx +++ b/web/service/demo/index.tsx @@ -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
{JSON.stringify(appListError)}
+ return
{JSON.stringify(appListError ?? appDetailError ?? err1 ?? err2 ?? err3 ?? err4 ?? err5 ?? err6 ?? err7 ?? err8)}
- 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 return ( diff --git a/web/service/use-apps.ts b/web/service/use-apps.ts index 8a2df8db1d..cc408c5d1a 100644 --- a/web/service/use-apps.ts +++ b/web/service/use-apps.ts @@ -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({ - queryKey: useAppFullListKey, - queryFn: () => get('/apps', { params: { page: 1, limit: 100 } }), - }) -} - -export const useInvalidateAppFullList = () => { - return useInvalid(useAppFullListKey) -} - -export const useAppDetail = (appID: string) => { - return useQuery({ - queryKey: [NAME_SPACE, 'detail', appID], - queryFn: () => get(`/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({ + queryKey: [NAME_SPACE, 'detail', appID], + queryFn: () => get(`/apps/${appID}`), + enabled: !!appID, + }) +} + +export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => { + const normalizedParams = normalizeAppListParams(params) + return useQuery({ + queryKey: appListKey(normalizedParams), + queryFn: () => get('/apps', { params: normalizedParams }), + ...options, + }) +} + +export const useAppFullList = () => { + return useQuery({ + queryKey: useAppFullListKey, + queryFn: () => get('/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({ + queryKey: appListKey(normalizedParams), + queryFn: ({ pageParam = normalizedParams.page }) => get('/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 = (metric: string, appId: string, params?: DateRangeParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'statistics', metric, appId, params], + queryFn: () => get(`/apps/${appId}/statistics/${metric}`, { params }), + enabled: !!appId, + }) +} + +const useWorkflowStatisticsQuery = (metric: string, appId: string, params?: DateRangeParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'workflow-statistics', metric, appId, params], + queryFn: () => get(`/apps/${appId}/workflow/statistics/${metric}`, { params }), + enabled: !!appId, + }) +} + +export const useAppDailyMessages = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('daily-messages', appId, params) +} + +export const useAppDailyConversations = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('daily-conversations', appId, params) +} + +export const useAppDailyEndUsers = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('daily-end-users', appId, params) +} + +export const useAppAverageSessionInteractions = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('average-session-interactions', appId, params) +} + +export const useAppAverageResponseTime = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('average-response-time', appId, params) +} + +export const useAppTokensPerSecond = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('tokens-per-second', appId, params) +} + +export const useAppSatisfactionRate = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('user-satisfaction-rate', appId, params) +} + +export const useAppTokenCosts = (appId: string, params?: DateRangeParams) => { + return useAppStatisticsQuery('token-costs', appId, params) +} + +export const useWorkflowDailyConversations = (appId: string, params?: DateRangeParams) => { + return useWorkflowStatisticsQuery('daily-conversations', appId, params) +} + +export const useWorkflowDailyTerminals = (appId: string, params?: DateRangeParams) => { + return useWorkflowStatisticsQuery('daily-terminals', appId, params) +} + +export const useWorkflowTokenCosts = (appId: string, params?: DateRangeParams) => { + return useWorkflowStatisticsQuery('token-costs', appId, params) +} + +export const useWorkflowAverageInteractions = (appId: string, params?: DateRangeParams) => { + return useWorkflowStatisticsQuery('average-app-interactions', appId, params) +} + +export const useAppVoices = (appId?: string, language?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'voices', appId, language || 'en-US'], + queryFn: () => get(`/apps/${appId}/text-to-audio/voices`, { params: { language: language || 'en-US' } }), + enabled: !!appId, + }) +} + +export const useAppApiKeys = (appId?: string, options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: [NAME_SPACE, 'api-keys', appId], + queryFn: () => get(`/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], + }) + } +}