'use client' import type { FC } from 'react' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import AppTrigger from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger' import AppPicker from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker' import AppInputsPanel from '@/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel' import type { App } from '@/types/app' import type { OffsetOptions, Placement, } from '@floating-ui/react' import { useInfiniteAppList } from '@/service/use-apps' const PAGE_SIZE = 20 type Props = { value?: { app_id: string inputs: Record files?: any[] } scope?: string disabled?: boolean placement?: Placement offset?: OffsetOptions onSelect: (app: { app_id: string inputs: Record files?: any[] }) => void supportAddCustomTool?: boolean } const AppSelector: FC = ({ value, scope, disabled, placement = 'bottom', offset = 4, onSelect, }) => { const { t } = useTranslation() const [isShow, onShowChange] = useState(false) const [searchText, setSearchText] = useState('') const [isLoadingMore, setIsLoadingMore] = useState(false) const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, } = useInfiniteAppList({ page: 1, limit: PAGE_SIZE, name: searchText, }) const pages = data?.pages ?? [] const displayedApps = useMemo(() => { if (!pages.length) return [] return pages.flatMap(({ data: apps }) => apps) }, [pages]) const hasMore = hasNextPage ?? true const handleLoadMore = useCallback(async () => { if (isLoadingMore || isFetchingNextPage || !hasMore) return setIsLoadingMore(true) try { await fetchNextPage() } finally { // Add a small delay to ensure state updates are complete setTimeout(() => { setIsLoadingMore(false) }, 300) } }, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage]) const handleTriggerClick = () => { if (disabled) return onShowChange(true) } const [isShowChooseApp, setIsShowChooseApp] = useState(false) const handleSelectApp = (app: App) => { const clearValue = app.id !== value?.app_id const appValue = { app_id: app.id, inputs: clearValue ? {} : value?.inputs || {}, files: clearValue ? [] : value?.files || [], } onSelect(appValue) setIsShowChooseApp(false) } const handleFormChange = (inputs: Record) => { const newFiles = inputs['#image#'] delete inputs['#image#'] const newValue = { app_id: value?.app_id || '', inputs, files: newFiles ? [newFiles] : value?.files || [], } onSelect(newValue) } const formattedValue = useMemo(() => { return { app_id: value?.app_id || '', inputs: { ...value?.inputs, ...(value?.files?.length ? { '#image#': value.files[0] } : {}), }, } }, [value]) const currentAppInfo = useMemo(() => { if (!displayedApps || !value) return undefined return displayedApps.find(app => app.id === value.app_id) }, [displayedApps, value]) return ( <>
{t('app.appSelector.label')}
} isShow={isShowChooseApp} onShowChange={setIsShowChooseApp} disabled={false} onSelect={handleSelectApp} scope={scope || 'all'} apps={displayedApps} isLoading={isLoading || isLoadingMore || isFetchingNextPage} hasMore={hasMore} onLoadMore={handleLoadMore} searchText={searchText} onSearchChange={setSearchText} />
{/* app inputs config panel */} {currentAppInfo && ( )}
) } export default React.memo(AppSelector)