|
'use client' |
|
|
|
import React, { useMemo, useState } from 'react' |
|
import { useRouter } from 'next/navigation' |
|
import { useTranslation } from 'react-i18next' |
|
import { useContext } from 'use-context-selector' |
|
import useSWR from 'swr' |
|
import { useDebounceFn } from 'ahooks' |
|
import Toast from '../../base/toast' |
|
import s from './style.module.css' |
|
import cn from '@/utils/classnames' |
|
import ExploreContext from '@/context/explore-context' |
|
import type { App } from '@/models/explore' |
|
import Category from '@/app/components/explore/category' |
|
import AppCard from '@/app/components/explore/app-card' |
|
import { fetchAppDetail, fetchAppList } from '@/service/explore' |
|
import { importApp } from '@/service/apps' |
|
import { useTabSearchParams } from '@/hooks/use-tab-searchparams' |
|
import CreateAppModal from '@/app/components/explore/create-app-modal' |
|
import AppTypeSelector from '@/app/components/app/type-selector' |
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' |
|
import Loading from '@/app/components/base/loading' |
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' |
|
import { useAppContext } from '@/context/app-context' |
|
import { getRedirection } from '@/utils/app-redirection' |
|
import Input from '@/app/components/base/input' |
|
|
|
type AppsProps = { |
|
pageType?: PageType |
|
onSuccess?: () => void |
|
} |
|
|
|
export enum PageType { |
|
EXPLORE = 'explore', |
|
CREATE = 'create', |
|
} |
|
|
|
const Apps = ({ |
|
pageType = PageType.EXPLORE, |
|
onSuccess, |
|
}: AppsProps) => { |
|
const { t } = useTranslation() |
|
const { isCurrentWorkspaceEditor } = useAppContext() |
|
const { push } = useRouter() |
|
const { hasEditPermission } = useContext(ExploreContext) |
|
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' }) |
|
|
|
const [keywords, setKeywords] = useState('') |
|
const [searchKeywords, setSearchKeywords] = useState('') |
|
|
|
const { run: handleSearch } = useDebounceFn(() => { |
|
setSearchKeywords(keywords) |
|
}, { wait: 500 }) |
|
|
|
const handleKeywordsChange = (value: string) => { |
|
setKeywords(value) |
|
handleSearch() |
|
} |
|
|
|
const [currentType, setCurrentType] = useState<string>('') |
|
const [currCategory, setCurrCategory] = useTabSearchParams({ |
|
defaultTab: allCategoriesEn, |
|
disableSearchParams: pageType !== PageType.EXPLORE, |
|
}) |
|
|
|
const { |
|
data: { categories, allList }, |
|
} = useSWR( |
|
['/explore/apps'], |
|
() => |
|
fetchAppList().then(({ categories, recommended_apps }) => ({ |
|
categories, |
|
allList: recommended_apps.sort((a, b) => a.position - b.position), |
|
})), |
|
{ |
|
fallbackData: { |
|
categories: [], |
|
allList: [], |
|
}, |
|
}, |
|
) |
|
|
|
const filteredList = useMemo(() => { |
|
if (currCategory === allCategoriesEn) { |
|
if (!currentType) |
|
return allList |
|
else if (currentType === 'chatbot') |
|
return allList.filter(item => (item.app.mode === 'chat' || item.app.mode === 'advanced-chat')) |
|
else if (currentType === 'agent') |
|
return allList.filter(item => (item.app.mode === 'agent-chat')) |
|
else |
|
return allList.filter(item => (item.app.mode === 'workflow')) |
|
} |
|
else { |
|
if (!currentType) |
|
return allList.filter(item => item.category === currCategory) |
|
else if (currentType === 'chatbot') |
|
return allList.filter(item => (item.app.mode === 'chat' || item.app.mode === 'advanced-chat') && item.category === currCategory) |
|
else if (currentType === 'agent') |
|
return allList.filter(item => (item.app.mode === 'agent-chat') && item.category === currCategory) |
|
else |
|
return allList.filter(item => (item.app.mode === 'workflow') && item.category === currCategory) |
|
} |
|
}, [currentType, currCategory, allCategoriesEn, allList]) |
|
|
|
const searchFilteredList = useMemo(() => { |
|
if (!searchKeywords || !filteredList || filteredList.length === 0) |
|
return filteredList |
|
|
|
const lowerCaseSearchKeywords = searchKeywords.toLowerCase() |
|
|
|
return filteredList.filter(item => |
|
item.app && item.app.name && item.app.name.toLowerCase().includes(lowerCaseSearchKeywords), |
|
) |
|
}, [searchKeywords, filteredList]) |
|
|
|
const [currApp, setCurrApp] = React.useState<App | null>(null) |
|
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) |
|
const onCreate: CreateAppModalProps['onConfirm'] = async ({ |
|
name, |
|
icon_type, |
|
icon, |
|
icon_background, |
|
description, |
|
}) => { |
|
const { export_data } = await fetchAppDetail( |
|
currApp?.app.id as string, |
|
) |
|
try { |
|
const app = await importApp({ |
|
data: export_data, |
|
name, |
|
icon_type, |
|
icon, |
|
icon_background, |
|
description, |
|
}) |
|
setIsShowCreateModal(false) |
|
Toast.notify({ |
|
type: 'success', |
|
message: t('app.newApp.appCreated'), |
|
}) |
|
if (onSuccess) |
|
onSuccess() |
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') |
|
getRedirection(isCurrentWorkspaceEditor, app, push) |
|
} |
|
catch (e) { |
|
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) |
|
} |
|
} |
|
|
|
if (!categories || categories.length === 0) { |
|
return ( |
|
<div className="flex h-full items-center"> |
|
<Loading type="area" /> |
|
</div> |
|
) |
|
} |
|
|
|
return ( |
|
<div className={cn( |
|
'flex flex-col', |
|
pageType === PageType.EXPLORE ? 'h-full border-l border-gray-200' : 'h-[calc(100%-56px)]', |
|
)}> |
|
{pageType === PageType.EXPLORE && ( |
|
<div className='shrink-0 pt-6 px-12'> |
|
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div> |
|
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div> |
|
</div> |
|
)} |
|
<div className={cn( |
|
'flex items-center justify-between mt-6', |
|
pageType === PageType.EXPLORE ? 'px-12' : 'px-8', |
|
)}> |
|
<> |
|
{pageType !== PageType.EXPLORE && ( |
|
<> |
|
<AppTypeSelector value={currentType} onChange={setCurrentType}/> |
|
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'/> |
|
</> |
|
)} |
|
<Category |
|
list={categories} |
|
value={currCategory} |
|
onChange={setCurrCategory} |
|
allCategoriesEn={allCategoriesEn} |
|
/> |
|
</> |
|
<Input |
|
showLeftIcon |
|
showClearIcon |
|
wrapperClassName='w-[200px]' |
|
value={keywords} |
|
onChange={e => handleKeywordsChange(e.target.value)} |
|
onClear={() => handleKeywordsChange('')} |
|
/> |
|
|
|
</div> |
|
|
|
<div className={cn( |
|
'relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow', |
|
pageType === PageType.EXPLORE ? 'mt-4' : 'mt-0 pt-2', |
|
)}> |
|
<nav |
|
className={cn( |
|
s.appList, |
|
'grid content-start shrink-0', |
|
pageType === PageType.EXPLORE ? 'gap-4 px-6 sm:px-12' : 'gap-3 px-8 sm:!grid-cols-2 md:!grid-cols-3 lg:!grid-cols-4', |
|
)}> |
|
{searchFilteredList.map(app => ( |
|
<AppCard |
|
key={app.app_id} |
|
isExplore={pageType === PageType.EXPLORE} |
|
app={app} |
|
canCreate={hasEditPermission} |
|
onCreate={() => { |
|
setCurrApp(app) |
|
setIsShowCreateModal(true) |
|
}} |
|
/> |
|
))} |
|
</nav> |
|
</div> |
|
{isShowCreateModal && ( |
|
<CreateAppModal |
|
appIconType={currApp?.app.icon_type || 'emoji'} |
|
appIcon={currApp?.app.icon || ''} |
|
appIconBackground={currApp?.app.icon_background || ''} |
|
appIconUrl={currApp?.app.icon_url} |
|
appName={currApp?.app.name || ''} |
|
appDescription={currApp?.app.description || ''} |
|
show={isShowCreateModal} |
|
onConfirm={onCreate} |
|
onHide={() => setIsShowCreateModal(false)} |
|
/> |
|
)} |
|
</div> |
|
) |
|
} |
|
|
|
export default React.memo(Apps) |
|
|