Files
npc0-hue 7947b7976b feat: 1.12.1初步合并
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
2026-02-09 09:51:18 +08:00

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)