mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-14 20:41:21 +08:00
7947b7976b
Merge upstream release cd03e0a (hotfix/1.12.1-fix.0) into main
# Conflicts:
# api/.env.example
# api/controllers/service_api/app/annotation.py
# api/controllers/service_api/app/completion.py
# api/controllers/service_api/app/conversation.py
# api/controllers/service_api/app/message.py
# api/core/file/file_manager.py
# api/core/rag/datasource/retrieval_service.py
# api/extensions/ext_celery.py
# api/libs/gmpy2_pkcs10aep_cipher.py
# api/uv.lock
# web/pnpm-lock.yaml
# web/service/client.ts
293 lines
8.6 KiB
TypeScript
293 lines
8.6 KiB
TypeScript
'use client'
|
|
|
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
|
import type { App } from '@/models/explore'
|
|
import { useDebounceFn } from 'ahooks'
|
|
import { useQueryState } from 'nuqs'
|
|
import * as React from 'react'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useContext, useContextSelector } from 'use-context-selector'
|
|
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
|
import Button from '@/app/components/base/button'
|
|
import Input from '@/app/components/base/input'
|
|
import Loading from '@/app/components/base/loading'
|
|
import AppCard from '@/app/components/explore/app-card'
|
|
import Banner from '@/app/components/explore/banner/banner'
|
|
import Category from '@/app/components/explore/category'
|
|
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
|
import ExploreContext from '@/context/explore-context'
|
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
|
import { useImportDSL } from '@/hooks/use-import-dsl'
|
|
import {
|
|
DSLImportMode,
|
|
} from '@/models/app'
|
|
import { fetchAppDetail } from '@/service/explore'
|
|
import { useExploreAppList } from '@/service/use-explore'
|
|
import { cn } from '@/utils/classnames'
|
|
import TryApp from '../try-app'
|
|
import s from './style.module.css'
|
|
// Extend: start Explore Add Search
|
|
import SearchInput from '@/app/components/base/search-input'
|
|
import TagFilter from '@/app/components/base/tag-management/filter'
|
|
// Extend: stop Explore Add Search
|
|
|
|
type AppsProps = {
|
|
onSuccess?: () => void
|
|
}
|
|
|
|
const Apps = ({
|
|
onSuccess,
|
|
}: AppsProps) => {
|
|
const { t } = useTranslation()
|
|
const { systemFeatures } = useGlobalPublicStore()
|
|
const { hasEditPermission } = useContext(ExploreContext)
|
|
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
|
|
|
// Extend: start Explore Add Search
|
|
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
|
const [keywordsValue, setKeywordsValue] = useState<string>('')
|
|
// Extend: stop Explore Add Search
|
|
const [keywords, setKeywords] = useState('')
|
|
const [searchKeywords, setSearchKeywords] = useState('')
|
|
|
|
const hasFilterCondition = !!keywords
|
|
const handleResetFilter = useCallback(() => {
|
|
setKeywords('')
|
|
setSearchKeywords('')
|
|
}, [])
|
|
|
|
const { run: handleSearch } = useDebounceFn(() => {
|
|
setSearchKeywords(keywords)
|
|
}, { wait: 500 })
|
|
|
|
const handleKeywordsChange = (value: string) => {
|
|
setKeywords(value)
|
|
handleSearch()
|
|
}
|
|
|
|
const [currCategory, setCurrCategory] = useQueryState('category', {
|
|
defaultValue: allCategoriesEn,
|
|
})
|
|
|
|
const {
|
|
data,
|
|
isLoading,
|
|
isError,
|
|
// extend: start sync app
|
|
refetch,
|
|
// extend: stop sync app
|
|
} = useExploreAppList()
|
|
|
|
// extend: start sync app
|
|
// Get recommended apps list to check if app is synced
|
|
const recommendedAppIds = useMemo(() => {
|
|
if (!data)
|
|
return new Set<string>()
|
|
// Extract app_id from allList to check sync status
|
|
return new Set(data.allList.map(item => item.app_id))
|
|
}, [data])
|
|
// extend: stop sync app
|
|
|
|
// Extend: start Filtered list with search and tag filter
|
|
const filteredListExtend = useMemo(() => {
|
|
if (!data)
|
|
return []
|
|
|
|
let result = data.allList
|
|
|
|
// Apply category filter
|
|
if (currCategory !== allCategoriesEn) {
|
|
result = result.filter(item => item.category === currCategory)
|
|
}
|
|
|
|
// Apply tag filter
|
|
if (tagFilterValue.length > 0) {
|
|
result = result.filter(item => tagFilterValue.includes(item.category))
|
|
}
|
|
|
|
// Apply keyword search
|
|
if (keywordsValue.length > 0) {
|
|
const lowerCaseKeywords = keywordsValue.toLowerCase()
|
|
result = result.filter(item =>
|
|
item.description?.toLowerCase().includes(lowerCaseKeywords) ||
|
|
item.app?.name?.toLowerCase().includes(lowerCaseKeywords)
|
|
)
|
|
}
|
|
|
|
return result
|
|
}, [data, currCategory, allCategoriesEn, tagFilterValue, keywordsValue])
|
|
|
|
const handleTagsChange = (value: string[]) => {
|
|
setTagFilterValue(value)
|
|
}
|
|
// Extend: stop Filtered list with search and tag filter
|
|
|
|
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
|
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
|
|
|
const {
|
|
handleImportDSL,
|
|
handleImportDSLConfirm,
|
|
versions,
|
|
isFetching,
|
|
} = useImportDSL()
|
|
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
|
|
|
const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel)
|
|
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
|
const hideTryAppPanel = useCallback(() => {
|
|
setShowTryAppPanel(false)
|
|
}, [setShowTryAppPanel])
|
|
const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp)
|
|
const handleShowFromTryApp = useCallback(() => {
|
|
setCurrApp(appParams?.app || null)
|
|
setIsShowCreateModal(true)
|
|
}, [appParams?.app])
|
|
|
|
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
description,
|
|
}) => {
|
|
hideTryAppPanel()
|
|
|
|
const { export_data } = await fetchAppDetail(
|
|
currApp?.app.id as string,
|
|
)
|
|
const payload = {
|
|
mode: DSLImportMode.YAML_CONTENT,
|
|
yaml_content: export_data,
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
description,
|
|
}
|
|
await handleImportDSL(payload, {
|
|
onSuccess: () => {
|
|
setIsShowCreateModal(false)
|
|
},
|
|
onPending: () => {
|
|
setShowDSLConfirmModal(true)
|
|
},
|
|
})
|
|
}
|
|
|
|
const onConfirmDSL = useCallback(async () => {
|
|
await handleImportDSLConfirm({
|
|
onSuccess,
|
|
})
|
|
}, [handleImportDSLConfirm, onSuccess])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-full items-center">
|
|
<Loading type="area" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (isError || !data)
|
|
return null
|
|
|
|
const { categories } = data
|
|
|
|
return (
|
|
<div className={cn(
|
|
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
|
|
)}
|
|
>
|
|
{systemFeatures.enable_explore_banner && (
|
|
<div className="mt-4 px-12">
|
|
<Banner />
|
|
</div>
|
|
)}
|
|
<div className={cn(
|
|
'mt-6 flex items-center justify-between px-12',
|
|
)}
|
|
>
|
|
<Category
|
|
list={categories}
|
|
value={currCategory}
|
|
onChange={setCurrCategory}
|
|
allCategoriesEn={allCategoriesEn}
|
|
/>
|
|
{/* Extend: start Explore Add Search */}
|
|
<div className="flex items-center gap-2">
|
|
<TagFilter type="app" value={tagFilterValue} onChange={handleTagsChange} />
|
|
<SearchInput className="w-[200px]" value={keywordsValue} onChange={handleKeywordsChange}/>
|
|
</div>
|
|
{/* Extend: stop Explore Add Search */}
|
|
</div>
|
|
|
|
<div className={cn(
|
|
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
|
|
)}
|
|
>
|
|
<nav
|
|
className={cn(
|
|
s.appList,
|
|
'grid shrink-0 content-start gap-4 px-6 sm:px-12',
|
|
)}
|
|
>
|
|
{filteredListExtend.map(app => (
|
|
<AppCard
|
|
key={app.app_id}
|
|
isExplore
|
|
app={app}
|
|
canCreate={hasEditPermission}
|
|
onCreate={() => {
|
|
setCurrApp(app)
|
|
setIsShowCreateModal(true)
|
|
}}
|
|
// extend: start sync app
|
|
onApp={recommendedAppIds.has(app.app_id)}
|
|
onRefresh={() => refetch()}
|
|
// extend: stop sync app
|
|
/>
|
|
))}
|
|
</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}
|
|
confirmDisabled={isFetching}
|
|
onHide={() => setIsShowCreateModal(false)}
|
|
/>
|
|
)}
|
|
{
|
|
showDSLConfirmModal && (
|
|
<DSLConfirmModal
|
|
versions={versions}
|
|
onCancel={() => setShowDSLConfirmModal(false)}
|
|
onConfirm={onConfirmDSL}
|
|
confirmDisabled={isFetching}
|
|
/>
|
|
)
|
|
}
|
|
|
|
{isShowTryAppPanel && (
|
|
<TryApp
|
|
appId={appParams?.appId || ''}
|
|
app={appParams?.app}
|
|
category={appParams?.app?.category}
|
|
onClose={hideTryAppPanel}
|
|
onCreate={handleShowFromTryApp}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(Apps)
|