| | |
| | | 'use client' |
| | | |
| | | import { useCallback, useEffect, useRef, useState } from 'react' |
| | | import { |
| | | useRouter, |
| | | } from 'next/navigation' |
| | | import { useRouter } from 'next/navigation' |
| | | import useSWRInfinite from 'swr/infinite' |
| | | import { useTranslation } from 'react-i18next' |
| | | import { useDebounceFn } from 'ahooks' |
| | | import { |
| | | RiApps2Line, |
| | | RiExchange2Line, |
| | | RiFile4Line, |
| | | RiMessage3Line, |
| | | RiRobot3Line, |
| | | } from '@remixicon/react' |
| | |
| | | const [activeTab, setActiveTab] = useTabSearchParams({ |
| | | defaultTab: 'all', |
| | | }) |
| | | const { query: { tagIDs = [], keywords = '', isCreatedByMe: queryIsCreatedByMe = false }, setQuery } = useAppsQueryState() |
| | | const [isCreatedByMe, setIsCreatedByMe] = useState(queryIsCreatedByMe) |
| | | const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState() |
| | | const [isCreatedByMe, setIsCreatedByMe] = useState(false) |
| | | const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs) |
| | | const [searchKeywords, setSearchKeywords] = useState(keywords) |
| | | const newAppCardRef = useRef<HTMLDivElement>(null) |
| | | const setKeywords = useCallback((keywords: string) => { |
| | | setQuery(prev => ({ ...prev, keywords })) |
| | | }, [setQuery]) |
| | |
| | | setQuery(prev => ({ ...prev, tagIDs })) |
| | | }, [setQuery]) |
| | | |
| | | const { data, isLoading, error, setSize, mutate } = useSWRInfinite( |
| | | const { data, isLoading, setSize, mutate } = useSWRInfinite( |
| | | (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords), |
| | | fetchAppList, |
| | | { |
| | | revalidateFirstPage: true, |
| | | shouldRetryOnError: false, |
| | | dedupingInterval: 500, |
| | | errorRetryCount: 3, |
| | | }, |
| | | { revalidateFirstPage: true }, |
| | | ) |
| | | |
| | | const anchorRef = useRef<HTMLDivElement>(null) |
| | | const options = [ |
| | | { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> }, |
| | | { 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` |
| | | document.title = `${t('common.menus.apps')} - Dify` |
| | | if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { |
| | | localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) |
| | | mutate() |
| | |
| | | useEffect(() => { |
| | | const hasMore = data?.at(-1)?.has_more ?? true |
| | | let observer: IntersectionObserver | undefined |
| | | |
| | | if (error) { |
| | | if (observer) |
| | | observer.disconnect() |
| | | return |
| | | } |
| | | |
| | | if (anchorRef.current) { |
| | | observer = new IntersectionObserver((entries) => { |
| | | if (entries[0].isIntersecting && !isLoading && !error && hasMore) |
| | | 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, error]) |
| | | }, [isLoading, setSize, anchorRef, mutate, data]) |
| | | |
| | | const { run: handleSearch } = useDebounceFn(() => { |
| | | setSearchKeywords(keywords) |
| | |
| | | handleTagsUpdate() |
| | | } |
| | | |
| | | const handleCreatedByMeChange = useCallback(() => { |
| | | const newValue = !isCreatedByMe |
| | | setIsCreatedByMe(newValue) |
| | | setQuery(prev => ({ ...prev, isCreatedByMe: newValue })) |
| | | }, [isCreatedByMe, setQuery]) |
| | | |
| | | return ( |
| | | <> |
| | | <div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'> |
| | | <div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'> |
| | | <TabSliderNew |
| | | value={activeTab} |
| | | onChange={setActiveTab} |
| | |
| | | className='mr-2' |
| | | label={t('app.showMyCreatedAppsOnly')} |
| | | isChecked={isCreatedByMe} |
| | | onChange={handleCreatedByMeChange} |
| | | onChange={() => setIsCreatedByMe(!isCreatedByMe)} |
| | | /> |
| | | <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} /> |
| | | <Input |
| | |
| | | </div> |
| | | </div> |
| | | {(data && data[0].total > 0) |
| | | ? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> |
| | | ? <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative'> |
| | | {isCurrentWorkspaceEditor |
| | | && <NewAppCard ref={newAppCardRef} onSuccess={mutate} />} |
| | | && <NewAppCard onSuccess={mutate} />} |
| | | {data.map(({ data: apps }) => apps.map(app => ( |
| | | <AppCard key={app.id} app={app} onRefresh={mutate} /> |
| | | )))} |
| | | </div> |
| | | : <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> |
| | | : <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative overflow-hidden'> |
| | | {isCurrentWorkspaceEditor |
| | | && <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />} |
| | | && <NewAppCard className='z-10' onSuccess={mutate} />} |
| | | <NoAppsFound /> |
| | | </div>} |
| | | <CheckModal /> |
| | |
| | | const { t } = useTranslation() |
| | | function renderDefaultCard() { |
| | | const defaultCards = Array.from({ length: 36 }, (_, index) => ( |
| | | <div key={index} className='inline-flex h-[160px] rounded-xl bg-background-default-lighter'></div> |
| | | <div key={index} className='h-[160px] inline-flex rounded-xl bg-background-default-lighter'></div> |
| | | )) |
| | | return defaultCards |
| | | } |
| | | return ( |
| | | <> |
| | | {renderDefaultCard()} |
| | | <div className='absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'> |
| | | <div className='absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'> |
| | | <span className='system-md-medium text-text-tertiary'>{t('app.newApp.noAppsFound')}</span> |
| | | </div> |
| | | </> |