| 'use client' | |
| 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 { | |
| RiApps2Line, | |
| RiExchange2Line, | |
| RiMessage3Line, | |
| RiRobot3Line, | |
| } from '@remixicon/react' | |
| import AppCard from './AppCard' | |
| import NewAppCard from './NewAppCard' | |
| import useAppsQueryState from './hooks/useAppsQueryState' | |
| 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' | |
| 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 TagManagementModal from '@/app/components/base/tag-management' | |
| import TagFilter from '@/app/components/base/tag-management/filter' | |
| const getKey = ( | |
| pageIndex: number, | |
| previousPageData: AppListResponse, | |
| activeTab: string, | |
| tags: 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 | |
| if (tags.length) | |
| params.params.tag_ids = tags | |
| return params | |
| } | |
| return null | |
| } | |
| const Apps = () => { | |
| const { t } = useTranslation() | |
| const router = useRouter() | |
| const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() | |
| const showTagManagementModal = useTagStore(s => s.showTagManagementModal) | |
| const [activeTab, setActiveTab] = useTabSearchParams({ | |
| defaultTab: 'all', | |
| }) | |
| const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState() | |
| const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs) | |
| const [searchKeywords, setSearchKeywords] = useState(keywords) | |
| const setKeywords = useCallback((keywords: string) => { | |
| setQuery(prev => ({ ...prev, keywords })) | |
| }, [setQuery]) | |
| const setTagIDs = useCallback((tagIDs: string[]) => { | |
| setQuery(prev => ({ ...prev, tagIDs })) | |
| }, [setQuery]) | |
| const { data, isLoading, setSize, mutate } = useSWRInfinite( | |
| (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords), | |
| fetchAppList, | |
| { revalidateFirstPage: true }, | |
| ) | |
| const anchorRef = useRef<HTMLDivElement>(null) | |
| const options = [ | |
| { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='w-[14px] h-[14px] mr-1' /> }, | |
| { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='w-[14px] h-[14px] mr-1' /> }, | |
| { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='w-[14px] h-[14px] mr-1' /> }, | |
| { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='w-[14px] h-[14px] mr-1' /> }, | |
| ] | |
| useEffect(() => { | |
| document.title = `${t('common.menus.apps')} - Dify` | |
| if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { | |
| localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) | |
| mutate() | |
| } | |
| }, [mutate, t]) | |
| useEffect(() => { | |
| if (isCurrentWorkspaceDatasetOperator) | |
| return router.replace('/datasets') | |
| }, [router, isCurrentWorkspaceDatasetOperator]) | |
| useEffect(() => { | |
| const hasMore = data?.at(-1)?.has_more ?? true | |
| let observer: IntersectionObserver | undefined | |
| if (anchorRef.current) { | |
| observer = new IntersectionObserver((entries) => { | |
| if (entries[0].isIntersecting && !isLoading && hasMore) | |
| setSize((size: number) => size + 1) | |
| }, { rootMargin: '100px' }) | |
| observer.observe(anchorRef.current) | |
| } | |
| return () => observer?.disconnect() | |
| }, [isLoading, setSize, anchorRef, mutate, data]) | |
| 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() | |
| } | |
| return ( | |
| <> | |
| <div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'> | |
| <TabSliderNew | |
| value={activeTab} | |
| onChange={setActiveTab} | |
| options={options} | |
| /> | |
| <div className='flex items-center gap-2'> | |
| <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} /> | |
| <Input | |
| showLeftIcon | |
| showClearIcon | |
| wrapperClassName='w-[200px]' | |
| value={keywords} | |
| onChange={e => handleKeywordsChange(e.target.value)} | |
| onClear={() => handleKeywordsChange('')} | |
| /> | |
| </div> | |
| </div> | |
| <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> | |
| {isCurrentWorkspaceEditor | |
| && <NewAppCard onSuccess={mutate} />} | |
| {data?.map(({ data: apps }) => apps.map(app => ( | |
| <AppCard key={app.id} app={app} onRefresh={mutate} /> | |
| )))} | |
| <CheckModal /> | |
| </nav> | |
| <div ref={anchorRef} className='h-0'> </div> | |
| {showTagManagementModal && ( | |
| <TagManagementModal type='app' show={showTagManagementModal} /> | |
| )} | |
| </> | |
| ) | |
| } | |
| export default Apps | |