'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 { useAtomValue } from 'jotai' 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 { useLocale } from '@/context/i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' 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 { normalizeAppPagination } from '@/service/use-apps' import { trackCreateApp } from '@/utils/create-app-tracking' import { hasPermission } from '@/utils/permission' 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({ queryKey: [...consoleQuery.explore.apps.get.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.get.queryOptions({ input: homeContinueWorkAppsInput, select: (response): WorkspaceApp[] => normalizeAppPagination(response).data, }) } function getBannersQueryOptions(locale?: string) { const input = getLocaleQueryInput(locale) const language = input.query?.language return queryOptions({ queryKey: [...consoleQuery.explore.banners.get.queryKey({ input }), language], queryFn: () => fetchBanners(language), }) } function getDisabledBannersQueryOptions() { return queryOptions({ queryKey: disabledBannersQueryKey, queryFn: async () => [], initialData: [], staleTime: 'static', }) } const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { const { t } = useTranslation() const locale = useLocale() const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) 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 allCategoriesEn = t(($) => $['apps.allCategories'], { ns: 'explore', lng: 'en' }) const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') 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 visibleCategories = useMemo(() => { if (!homeQueries.appListData) return [] const categoriesWithApps = new Set() homeQueries.appListData.allList.forEach((app) => { app.categories.forEach((category) => categoriesWithApps.add(category)) }) return homeQueries.appListData.categories.filter((category) => categoriesWithApps.has(category)) }, [homeQueries.appListData]) const activeCategory = visibleCategories.includes(currCategory) ? currCategory : allCategoriesEn const filteredList = useMemo(() => { if (!homeQueries.appListData) return [] return homeQueries.appListData.allList.filter( (item) => activeCategory === allCategoriesEn || item.categories?.includes(activeCategory), ) }, [homeQueries.appListData, activeCategory, 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(null) const [isShowCreateModal, setIsShowCreateModal] = useState(false) const { handleImportDSL, handleImportDSLConfirm, versions, isFetching } = useImportDSL() const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) const [currentTryApp, setCurrentTryApp] = useState(undefined) const currentCreateAppModeRef = useRef(null) const currentCreateAppTrackingRef = useRef | 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 appId = currApp?.app.id if (!appId) return const { export_data, mode } = await fetchAppDetail(appId) 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 (
{homeQueries.isPending ? ( ) : ( <> {systemFeatures.enable_explore_banner && }
)}
{isShowCreateModal && ( setIsShowCreateModal(false)} /> )} {showDSLConfirmModal && ( setShowDSLConfirmModal(false)} onConfirm={onConfirmDSL} confirmDisabled={isFetching} /> )} {isShowTryAppPanel && ( )}
) } export default React.memo(Apps)