'use client' import type { FC } from 'react' import React, { useCallback, useEffect, useRef } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import type { OffsetOptions, Placement, } from '@floating-ui/react' import Input from '@/app/components/base/input' import AppIcon from '@/app/components/base/app-icon' import type { App } from '@/types/app' import { useTranslation } from 'react-i18next' type Props = { scope: string disabled: boolean trigger: React.ReactNode placement?: Placement offset?: OffsetOptions isShow: boolean onShowChange: (isShow: boolean) => void onSelect: (app: App) => void apps: App[] isLoading: boolean hasMore: boolean onLoadMore: () => void searchText: string onSearchChange: (text: string) => void } const AppPicker: FC = ({ scope, disabled, trigger, placement = 'right-start', offset = 0, isShow, onShowChange, onSelect, apps, isLoading, hasMore, onLoadMore, searchText, onSearchChange, }) => { const { t } = useTranslation() const observerTarget = useRef(null) const observerRef = useRef(null) const loadingRef = useRef(false) const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { const target = entries[0] if (!target.isIntersecting || loadingRef.current || !hasMore || isLoading) return loadingRef.current = true onLoadMore() // Reset loading state setTimeout(() => { loadingRef.current = false }, 500) }, [hasMore, isLoading, onLoadMore]) useEffect(() => { if (!isShow) { if (observerRef.current) { observerRef.current.disconnect() observerRef.current = null } return } let mutationObserver: MutationObserver | null = null const setupIntersectionObserver = () => { if (!observerTarget.current) return // Create new observer observerRef.current = new IntersectionObserver(handleIntersection, { root: null, rootMargin: '100px', threshold: 0.1, }) observerRef.current.observe(observerTarget.current) } // Set up MutationObserver to watch DOM changes mutationObserver = new MutationObserver((mutations) => { if (observerTarget.current) { setupIntersectionObserver() mutationObserver?.disconnect() } }) // Watch body changes since Portal adds content to body mutationObserver.observe(document.body, { childList: true, subtree: true, }) // If element exists, set up IntersectionObserver directly if (observerTarget.current) setupIntersectionObserver() return () => { if (observerRef.current) { observerRef.current.disconnect() observerRef.current = null } mutationObserver?.disconnect() } }, [isShow, handleIntersection]) const getAppType = (app: App) => { switch (app.mode) { case 'advanced-chat': return 'chatflow' case 'agent-chat': return 'agent' case 'chat': return 'chat' case 'completion': return 'completion' case 'workflow': return 'workflow' } } const handleTriggerClick = () => { if (disabled) return onShowChange(true) } return ( {trigger}
onSearchChange(e.target.value)} onClear={() => onSearchChange('')} />
{apps.map(app => (
onSelect(app)} >
{app.name}
{getAppType(app)}
))}
{isLoading && (
{t('common.loading')}
)}
) } export default React.memo(AppPicker)