| '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 |
|
|