mirror of
https://github.com/langgenius/dify.git
synced 2026-06-17 23:21:12 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
|
import type { Banner as BannerType } from '@/models/app'
|
|
import type { App } from '@/models/explore'
|
|
import type { App as WorkspaceApp } from '@/types/app'
|
|
import type { TryAppSelection } from '@/types/try-app'
|
|
import type { TrackCreateAppParams } from '@/utils/create-app-tracking'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { queryOptions, useQueries, useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useDebounceFn } from 'ahooks'
|
|
import { useQueryState } from 'nuqs'
|
|
import * as React from 'react'
|
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
|
import AppCard from '@/app/components/explore/app-card'
|
|
import Banner from '@/app/components/explore/banner/banner'
|
|
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
|
import { useAppContext } from '@/context/app-context'
|
|
import { useLocale } from '@/context/i18n'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { useImportDSL } from '@/hooks/use-import-dsl'
|
|
import { DSLImportMode } from '@/models/app'
|
|
import dynamic from '@/next/dynamic'
|
|
import { consoleQuery } from '@/service/client'
|
|
import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore'
|
|
import { useMembers } from '@/service/use-common'
|
|
import { trackCreateApp } from '@/utils/create-app-tracking'
|
|
import { ExploreAppListHeader } from './explore-app-list-header'
|
|
import { ExploreRecommendations } from './explore-recommendations'
|
|
import { ExploreHomeSkeleton } from './loading-skeletons'
|
|
import s from './style.module.css'
|
|
|
|
const TryApp = dynamic(() => import('../try-app'), { ssr: false })
|
|
|
|
type ExploreAppListData = {
|
|
categories: string[]
|
|
allList: App[]
|
|
}
|
|
|
|
const homeContinueWorkAppsInput = {
|
|
query: {
|
|
page: 1,
|
|
limit: 8,
|
|
name: '',
|
|
},
|
|
}
|
|
|
|
const disabledBannersQueryKey = ['explore', 'home', 'banners', 'disabled'] as const
|
|
|
|
function getLocaleQueryInput(locale?: string) {
|
|
return locale
|
|
? { query: { language: locale } }
|
|
: {}
|
|
}
|
|
|
|
function getExploreAppListQueryOptions(locale?: string) {
|
|
const input = getLocaleQueryInput(locale)
|
|
const language = input.query?.language
|
|
|
|
return queryOptions<ExploreAppListData>({
|
|
queryKey: [...consoleQuery.explore.apps.queryKey({ input }), language],
|
|
queryFn: async () => {
|
|
const { categories, recommended_apps } = await fetchAppList(language)
|
|
return {
|
|
categories,
|
|
allList: [...recommended_apps].sort((a, b) => a.position - b.position),
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
function getContinueWorkAppsQueryOptions() {
|
|
return consoleQuery.apps.list.queryOptions({
|
|
input: homeContinueWorkAppsInput,
|
|
select: (response): WorkspaceApp[] => response.data ?? [],
|
|
})
|
|
}
|
|
|
|
function getBannersQueryOptions(locale?: string) {
|
|
const input = getLocaleQueryInput(locale)
|
|
const language = input.query?.language
|
|
|
|
return queryOptions<BannerType[]>({
|
|
queryKey: [...consoleQuery.explore.banners.queryKey({ input }), language],
|
|
queryFn: () => fetchBanners(language),
|
|
})
|
|
}
|
|
|
|
function getDisabledBannersQueryOptions() {
|
|
return queryOptions<BannerType[]>({
|
|
queryKey: disabledBannersQueryKey,
|
|
queryFn: async () => [],
|
|
initialData: [],
|
|
staleTime: 'static',
|
|
})
|
|
}
|
|
|
|
const Apps = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|
const { t } = useTranslation()
|
|
const locale = useLocale()
|
|
const { userProfile } = useAppContext()
|
|
const { data: systemFeatures } = useSuspenseQuery(
|
|
systemFeaturesQueryOptions(),
|
|
)
|
|
const homeQueries = useQueries({
|
|
queries: [
|
|
getExploreAppListQueryOptions(locale),
|
|
getContinueWorkAppsQueryOptions(),
|
|
systemFeatures.enable_explore_banner
|
|
? getBannersQueryOptions(locale)
|
|
: getDisabledBannersQueryOptions(),
|
|
],
|
|
combine: ([exploreAppListQuery, continueWorkAppsQuery, bannersQuery]) => ({
|
|
appListData: exploreAppListQuery.data,
|
|
continueWorkApps: continueWorkAppsQuery.data ?? [],
|
|
banners: bannersQuery.data ?? [],
|
|
isPending: exploreAppListQuery.isPending || continueWorkAppsQuery.isPending || bannersQuery.isPending,
|
|
isAppListError: exploreAppListQuery.isError || (!exploreAppListQuery.isPending && !exploreAppListQuery.data),
|
|
}),
|
|
})
|
|
const { data: membersData } = useMembers()
|
|
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
|
const userAccount = membersData?.accounts?.find(
|
|
account => account.id === userProfile.id,
|
|
)
|
|
const hasEditPermission = !!userAccount && userAccount.role !== 'normal'
|
|
|
|
const [keywords, setKeywords] = useState('')
|
|
const [searchKeywords, setSearchKeywords] = useState('')
|
|
|
|
const { run: handleSearch } = useDebounceFn(
|
|
() => {
|
|
setSearchKeywords(keywords)
|
|
},
|
|
{ wait: 500 },
|
|
)
|
|
|
|
const handleKeywordsChange = (value: string) => {
|
|
setKeywords(value)
|
|
handleSearch()
|
|
}
|
|
|
|
const [currCategory, setCurrCategory] = useQueryState('category', {
|
|
defaultValue: allCategoriesEn,
|
|
})
|
|
|
|
const filteredList = useMemo(() => {
|
|
if (!homeQueries.appListData)
|
|
return []
|
|
return homeQueries.appListData.allList.filter(
|
|
item =>
|
|
currCategory === allCategoriesEn
|
|
|| item.categories?.includes(currCategory),
|
|
)
|
|
}, [homeQueries.appListData, currCategory, allCategoriesEn])
|
|
|
|
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] = useState<App | null>(null)
|
|
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
|
|
|
|
const { handleImportDSL, handleImportDSLConfirm, versions, isFetching }
|
|
= useImportDSL()
|
|
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
|
|
|
const [currentTryApp, setCurrentTryApp] = useState<
|
|
TryAppSelection | undefined
|
|
>(undefined)
|
|
const currentCreateAppModeRef = useRef<App['app']['mode'] | null>(null)
|
|
const currentCreateAppTrackingRef = useRef<Pick<
|
|
TrackCreateAppParams,
|
|
'source' | 'templateId'
|
|
> | null>(null)
|
|
const isShowTryAppPanel = !!currentTryApp
|
|
const hideTryAppPanel = useCallback(() => {
|
|
setCurrentTryApp(undefined)
|
|
}, [])
|
|
const handleTryApp = useCallback((params: TryAppSelection) => {
|
|
setCurrentTryApp(params)
|
|
}, [])
|
|
const handleShowFromTryApp = useCallback(() => {
|
|
setCurrApp(currentTryApp?.app || null)
|
|
currentCreateAppTrackingRef.current = {
|
|
source: 'explore_template_preview',
|
|
templateId: currentTryApp?.appId || currentTryApp?.app.app_id,
|
|
}
|
|
setIsShowCreateModal(true)
|
|
}, [currentTryApp?.app, currentTryApp?.appId])
|
|
const handleCreateFromLearnDify = useCallback((app: App) => {
|
|
setCurrApp(app)
|
|
setIsShowCreateModal(true)
|
|
}, [])
|
|
const handleCreateFromAppList = useCallback((app: App) => {
|
|
currentCreateAppTrackingRef.current = {
|
|
source: 'explore_template_list',
|
|
templateId: app.app_id,
|
|
}
|
|
setCurrApp(app)
|
|
setIsShowCreateModal(true)
|
|
}, [])
|
|
const trackCurrentCreateApp = useCallback(
|
|
(appMode?: App['app']['mode'] | null) => {
|
|
const currentCreateAppTracking = currentCreateAppTrackingRef.current
|
|
const resolvedAppMode = appMode ?? currentCreateAppModeRef.current
|
|
if (!resolvedAppMode || !currentCreateAppTracking)
|
|
return
|
|
|
|
trackCreateApp({
|
|
...currentCreateAppTracking,
|
|
appMode: resolvedAppMode,
|
|
})
|
|
currentCreateAppTrackingRef.current = null
|
|
currentCreateAppModeRef.current = null
|
|
},
|
|
[],
|
|
)
|
|
|
|
const onCreate: CreateAppModalProps['onConfirm'] = useCallback(
|
|
async ({ name, icon_type, icon, icon_background, description }) => {
|
|
hideTryAppPanel()
|
|
|
|
const { export_data, mode } = await fetchAppDetail(
|
|
currApp?.app.id as string,
|
|
)
|
|
currentCreateAppModeRef.current = mode
|
|
const payload = {
|
|
mode: DSLImportMode.YAML_CONTENT,
|
|
yaml_content: export_data,
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
description,
|
|
}
|
|
await handleImportDSL(payload, {
|
|
onSuccess: (response) => {
|
|
trackCurrentCreateApp(response.app_mode)
|
|
setIsShowCreateModal(false)
|
|
},
|
|
onPending: () => {
|
|
setShowDSLConfirmModal(true)
|
|
},
|
|
})
|
|
},
|
|
[currApp?.app.id, handleImportDSL, hideTryAppPanel, trackCurrentCreateApp],
|
|
)
|
|
|
|
const onConfirmDSL = useCallback(async () => {
|
|
await handleImportDSLConfirm({
|
|
onSuccess: (response) => {
|
|
trackCurrentCreateApp(response.app_mode)
|
|
onSuccess?.()
|
|
},
|
|
})
|
|
}, [handleImportDSLConfirm, onSuccess, trackCurrentCreateApp])
|
|
|
|
if (homeQueries.isAppListError)
|
|
return null
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex h-full min-h-0 flex-col overflow-hidden border-l-[0.5px] border-divider-regular',
|
|
)}
|
|
>
|
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
|
{homeQueries.isPending
|
|
? (
|
|
<ExploreHomeSkeleton showBanner={systemFeatures.enable_explore_banner} />
|
|
)
|
|
: (
|
|
<>
|
|
{systemFeatures.enable_explore_banner && (
|
|
<Banner banners={homeQueries.banners} />
|
|
)}
|
|
<ExploreRecommendations
|
|
canCreate={hasEditPermission}
|
|
continueWorkApps={homeQueries.continueWorkApps}
|
|
onCreate={handleCreateFromLearnDify}
|
|
onTry={handleTryApp}
|
|
/>
|
|
|
|
<ExploreAppListHeader
|
|
allCategoriesEn={allCategoriesEn}
|
|
categories={homeQueries.appListData?.categories ?? []}
|
|
currCategory={currCategory}
|
|
keywords={keywords}
|
|
onCategoryChange={setCurrCategory}
|
|
onKeywordsChange={handleKeywordsChange}
|
|
/>
|
|
|
|
<div className={cn('relative flex flex-1 shrink-0 grow flex-col pb-6')}>
|
|
<nav
|
|
className={cn(
|
|
s.appList,
|
|
'grid shrink-0 content-start gap-3 px-8',
|
|
)}
|
|
>
|
|
{searchFilteredList.map(app => (
|
|
<AppCard
|
|
key={app.app_id}
|
|
app={app}
|
|
canCreate={hasEditPermission}
|
|
onCreate={() => handleCreateFromAppList(app)}
|
|
onTry={handleTryApp}
|
|
/>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</>
|
|
)}
|
|
</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}
|
|
confirmDisabled={isFetching}
|
|
onHide={() => setIsShowCreateModal(false)}
|
|
/>
|
|
)}
|
|
{showDSLConfirmModal && (
|
|
<DSLConfirmModal
|
|
versions={versions}
|
|
onCancel={() => setShowDSLConfirmModal(false)}
|
|
onConfirm={onConfirmDSL}
|
|
confirmDisabled={isFetching}
|
|
/>
|
|
)}
|
|
|
|
{isShowTryAppPanel && (
|
|
<TryApp
|
|
appId={currentTryApp?.appId || ''}
|
|
app={currentTryApp?.app}
|
|
categories={currentTryApp?.app?.categories}
|
|
onClose={hideTryAppPanel}
|
|
onCreate={handleShowFromTryApp}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(Apps)
|