'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { useRouter, } from 'next/navigation' import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import { RiApps2Line, RiDragDropLine, RiExchange2Line, RiFile4Line, RiMessage3Line, RiRobot3Line, } from '@remixicon/react' 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 { useAppContext } from '@/context/app-context' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { CheckModal } from '@/hooks/use-pay' import TabSliderNew from '@/app/components/base/tab-slider-new' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import Input from '@/app/components/base/input' import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import TagFilter from '@/app/components/base/tag-management/filter' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' import dynamic from 'next/dynamic' 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, }) const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { ssr: false, }) const List = () => { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() const router = useRouter() const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const [activeTab, setActiveTab] = useTabSearchParams({ defaultTab: 'all', }) const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false }, setQuery } = useAppsQueryState() const [isCreatedByMe, setIsCreatedByMe] = useState(queryIsCreatedByMe) const [tagFilterValue, setTagFilterValue] = useState(tagIDs) const [searchKeywords, setSearchKeywords] = useState(keywords) const newAppCardRef = useRef(null) const containerRef = useRef(null) const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) const [droppedDSLFile, setDroppedDSLFile] = useState() const setKeywords = useCallback((keywords: string) => { setQuery(prev => ({ ...prev, keywords })) }, [setQuery]) const setTagIDs = useCallback((tagIDs: string[]) => { setQuery(prev => ({ ...prev, tagIDs })) }, [setQuery]) const handleDSLFileDropped = useCallback((file: File) => { setDroppedDSLFile(file) setShowCreateFromDSLModal(true) }, []) const { dragging } = useDSLDragDrop({ onDSLFileDropped: handleDSLFileDropped, containerRef, enabled: isCurrentWorkspaceEditor, }) 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 = [ { value: 'all', text: t('app.types.all'), icon: }, { value: AppModeEnum.WORKFLOW, text: t('app.types.workflow'), icon: }, { value: AppModeEnum.ADVANCED_CHAT, text: t('app.types.advanced'), icon: }, { value: AppModeEnum.CHAT, text: t('app.types.chatbot'), icon: }, { value: AppModeEnum.AGENT_CHAT, text: t('app.types.agent'), icon: }, { value: AppModeEnum.COMPLETION, text: t('app.types.completion'), icon: }, ] useEffect(() => { if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) refetch() } }, [refetch]) useEffect(() => { if (isCurrentWorkspaceDatasetOperator) return router.replace('/datasets') }, [router, isCurrentWorkspaceDatasetOperator]) useEffect(() => { if (isCurrentWorkspaceDatasetOperator) return const hasMore = hasNextPage ?? true let observer: IntersectionObserver | undefined if (error) { if (observer) observer.disconnect() return } if (anchorRef.current && containerRef.current) { // Calculate dynamic rootMargin: clamps to 100-200px range, using 20% of container height as the base value for better responsiveness const containerHeight = containerRef.current.clientHeight 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 && !isFetchingNextPage && !error && hasMore) fetchNextPage() }, { root: containerRef.current, rootMargin: `${dynamicMargin}px`, threshold: 0.1, // Trigger when 10% of the anchor element is visible }) observer.observe(anchorRef.current) } return () => observer?.disconnect() }, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator]) const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) }, { wait: 500 }) const handleKeywordsChange = (value: string) => { setKeywords(value) handleSearch() } const { run: handleTagsUpdate } = useDebounceFn(() => { setTagIDs(tagFilterValue) }, { wait: 500 }) const handleTagsChange = (value: string[]) => { setTagFilterValue(value) handleTagsUpdate() } const handleCreatedByMeChange = useCallback(() => { const newValue = !isCreatedByMe setIsCreatedByMe(newValue) setQuery(prev => ({ ...prev, isCreatedByMe: newValue })) }, [isCreatedByMe, setQuery]) const pages = data?.pages ?? [] const hasAnyApp = (pages[0]?.total ?? 0) > 0 return ( <>
{dragging && (
)}
handleKeywordsChange(e.target.value)} onClear={() => handleKeywordsChange('')} />
{hasAnyApp ?
{isCurrentWorkspaceEditor && } {pages.map(({ data: apps }) => apps.map(app => ( )))}
:
{isCurrentWorkspaceEditor && }
} {isCurrentWorkspaceEditor && (
{t('app.newApp.dropDSLToCreateApp')}
)} {!systemFeatures.branding.enabled && (
)}
{showTagManagementModal && ( )}
{showCreateFromDSLModal && ( { setShowCreateFromDSLModal(false) setDroppedDSLFile(undefined) }} onSuccess={() => { setShowCreateFromDSLModal(false) setDroppedDSLFile(undefined) refetch() }} droppedFile={droppedDSLFile} /> )} ) } export default List