mirror of https://github.com/langgenius/dify.git
feat: try to add tools suggestions
This commit is contained in:
parent
14acd05846
commit
28fe58f3dd
|
|
@ -29,6 +29,9 @@ import { PluginCategoryEnum } from '../../plugins/types'
|
|||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import RAGToolSuggestions from './rag-tool-suggestions'
|
||||
import FeaturedTools from './featured-tools'
|
||||
import { useCheckInstalled, useRecommendedMarketplacePlugins } from '@/service/use-plugins'
|
||||
import { useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
import Link from 'next/link'
|
||||
|
||||
type AllToolsProps = {
|
||||
|
|
@ -80,6 +83,16 @@ const AllTools = ({
|
|||
const isMatchingKeywords = (text: string, keywords: string) => {
|
||||
return text.toLowerCase().includes(keywords.toLowerCase())
|
||||
}
|
||||
const allProviders = useMemo(() => [...buildInTools, ...customTools, ...workflowTools, ...mcpTools], [buildInTools, customTools, workflowTools, mcpTools])
|
||||
const providerMap = useMemo(() => {
|
||||
const map = new Map<string, ToolWithProvider>()
|
||||
allProviders.forEach((provider) => {
|
||||
const key = provider.plugin_id || provider.id
|
||||
if (key)
|
||||
map.set(key, provider)
|
||||
})
|
||||
return map
|
||||
}, [allProviders])
|
||||
const tools = useMemo(() => {
|
||||
let mergedTools: ToolWithProvider[] = []
|
||||
if (activeTab === ToolTypeEnum.All)
|
||||
|
|
@ -136,6 +149,27 @@ const AllTools = ({
|
|||
} = useMarketplacePlugins()
|
||||
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const {
|
||||
data: recommendedPlugins = [],
|
||||
isLoading: isLoadingRecommended,
|
||||
} = useRecommendedMarketplacePlugins({
|
||||
enabled: enable_marketplace,
|
||||
})
|
||||
const recommendedPluginIds = useMemo(
|
||||
() => recommendedPlugins.map(plugin => plugin.plugin_id),
|
||||
[recommendedPlugins],
|
||||
)
|
||||
const installedCheck = useCheckInstalled({
|
||||
pluginIds: recommendedPluginIds,
|
||||
enabled: recommendedPluginIds.length > 0,
|
||||
})
|
||||
const installedPluginIds = useMemo(
|
||||
() => new Set(installedCheck.data?.plugins.map(plugin => plugin.plugin_id) ?? []),
|
||||
[installedCheck.data],
|
||||
)
|
||||
const loadingRecommendedInstallStatus = installedCheck.isLoading || installedCheck.isRefetching
|
||||
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (hasFilter) {
|
||||
|
|
@ -155,6 +189,11 @@ const AllTools = ({
|
|||
const hasToolsContent = tools.length > 0
|
||||
const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0
|
||||
const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent
|
||||
const shouldShowFeatured = enable_marketplace
|
||||
&& activeTab === ToolTypeEnum.All
|
||||
&& !hasFilter
|
||||
&& !isLoadingRecommended
|
||||
&& recommendedPlugins.length > 0
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
|
|
@ -193,6 +232,21 @@ const AllTools = ({
|
|||
onTagsChange={onTagsChange}
|
||||
/>
|
||||
)}
|
||||
{shouldShowFeatured && (
|
||||
<FeaturedTools
|
||||
plugins={recommendedPlugins}
|
||||
providerMap={providerMap}
|
||||
onSelect={onSelect}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
installedPluginIds={installedPluginIds}
|
||||
loadingInstalledStatus={loadingRecommendedInstallStatus}
|
||||
onInstallSuccess={async () => {
|
||||
invalidateBuiltInTools()
|
||||
await installedCheck.refetch()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Tools
|
||||
className={toolContentClassName}
|
||||
tools={tools}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type ToolWithProvider } from '../types'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import Button from '@/app/components/base/button'
|
||||
import ActionItem from './tool/action-item'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiLoader2Line } from '@remixicon/react'
|
||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
|
||||
const MAX_RECOMMENDED_COUNT = 15
|
||||
const INITIAL_VISIBLE_COUNT = 5
|
||||
|
||||
type FeaturedToolsProps = {
|
||||
plugins: Plugin[]
|
||||
providerMap: Map<string, ToolWithProvider>
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
installedPluginIds: Set<string>
|
||||
loadingInstalledStatus: boolean
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
|
||||
function isToolSelected(tool: Tool, provider: ToolWithProvider, selectedTools?: ToolValue[]): boolean {
|
||||
if (!selectedTools || !selectedTools.length)
|
||||
return false
|
||||
return selectedTools.some(item => (item.provider_name === provider.name || item.provider_name === provider.id) && item.tool_name === tool.name)
|
||||
}
|
||||
|
||||
const FeaturedTools = ({
|
||||
plugins,
|
||||
providerMap,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
installedPluginIds,
|
||||
loadingInstalledStatus,
|
||||
onInstallSuccess,
|
||||
}: FeaturedToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [installingIdentifier, setInstallingIdentifier] = useState<string | null>(null)
|
||||
const installMutation = useInstallPackageFromMarketPlace({
|
||||
onSuccess: () => {
|
||||
onInstallSuccess?.()
|
||||
},
|
||||
onSettled: () => {
|
||||
setInstallingIdentifier(null)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleCount(INITIAL_VISIBLE_COUNT)
|
||||
}, [plugins])
|
||||
|
||||
const visiblePlugins = useMemo(
|
||||
() => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)),
|
||||
[plugins, visibleCount],
|
||||
)
|
||||
|
||||
if (!visiblePlugins.length)
|
||||
return null
|
||||
|
||||
const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length)
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3 pt-2'>
|
||||
<div className='system-xs-medium mb-2 text-text-tertiary'>
|
||||
{t('workflow.tabs.featuredTools')}
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{visiblePlugins.map(plugin => renderFeaturedToolItem({
|
||||
plugin,
|
||||
providerMap,
|
||||
installedPluginIds,
|
||||
installMutationPending: installMutation.isPending,
|
||||
installingIdentifier,
|
||||
loadingInstalledStatus,
|
||||
canChooseMCPTool,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
language,
|
||||
installPlugin: installMutation.mutate,
|
||||
setInstallingIdentifier,
|
||||
}))}
|
||||
</div>
|
||||
{showMore && (
|
||||
<Button
|
||||
className='mt-2 w-full'
|
||||
size='small'
|
||||
variant='ghost'
|
||||
onClick={() => {
|
||||
setVisibleCount(count => Math.min(count + INITIAL_VISIBLE_COUNT, MAX_RECOMMENDED_COUNT, plugins.length))
|
||||
}}
|
||||
>
|
||||
{t('workflow.tabs.showMoreFeatured')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FeaturedToolItemProps = {
|
||||
plugin: Plugin
|
||||
provider: ToolWithProvider | undefined
|
||||
isInstalled: boolean
|
||||
installDisabled: boolean
|
||||
canChooseMCPTool?: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
language: string
|
||||
onInstall: () => void
|
||||
isInstalling: boolean
|
||||
}
|
||||
|
||||
function FeaturedToolItem({
|
||||
plugin,
|
||||
provider,
|
||||
isInstalled,
|
||||
installDisabled,
|
||||
canChooseMCPTool,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
language,
|
||||
onInstall,
|
||||
isInstalling,
|
||||
}: FeaturedToolItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setExpanded] = useState(false)
|
||||
const hasProvider = Boolean(provider)
|
||||
const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 })
|
||||
const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasProvider)
|
||||
setExpanded(false)
|
||||
}, [hasProvider])
|
||||
|
||||
let toggleLabel: string
|
||||
if (!hasProvider)
|
||||
toggleLabel = t('workflow.common.syncingData')
|
||||
else if (isExpanded)
|
||||
toggleLabel = t('workflow.tabs.hideActions')
|
||||
else
|
||||
toggleLabel = t('workflow.tabs.usePlugin')
|
||||
|
||||
return (
|
||||
<div className='rounded-lg border border-divider-subtle bg-components-panel-bg-blur px-3 py-2'>
|
||||
<div className='flex items-start gap-2'>
|
||||
<BlockIcon type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='truncate text-sm font-medium text-text-primary'>
|
||||
{plugin.label?.[language] || plugin.name}
|
||||
</div>
|
||||
{isInstalled && (
|
||||
<span className='system-xs-regular rounded-full border border-divider-subtle px-2 py-0.5 text-text-tertiary'>
|
||||
{t('workflow.tabs.installed')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular mt-0.5 line-clamp-2 text-text-secondary'>
|
||||
{description}
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 flex items-center gap-2 text-text-tertiary'>
|
||||
<span>{installCountLabel}</span>
|
||||
{plugin.org && <span>{t('workflow.tabs.pluginByAuthor', { author: plugin.org })}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-2 flex shrink-0 flex-col items-end gap-1'>
|
||||
{!isInstalled && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='primary'
|
||||
disabled={installDisabled}
|
||||
onClick={onInstall}
|
||||
className='flex items-center gap-1'
|
||||
>
|
||||
{isInstalling ? t('workflow.nodes.agent.pluginInstaller.installing') : t('workflow.nodes.agent.pluginInstaller.install')}
|
||||
{isInstalling && <RiLoader2Line className='size-3 animate-spin' />}
|
||||
</Button>
|
||||
)}
|
||||
{isInstalled && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary'
|
||||
onClick={() => setExpanded(expanded => !expanded)}
|
||||
disabled={!hasProvider}
|
||||
className='flex items-center gap-1'
|
||||
>
|
||||
{toggleLabel}
|
||||
{hasProvider && (isExpanded ? <RiArrowUpSLine className='size-3.5' /> : <RiArrowDownSLine className='size-3.5' />)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isInstalled && hasProvider && isExpanded && (
|
||||
<div className='mt-2 space-y-1 border-t border-divider-subtle pt-2'>
|
||||
{provider.tools.map((tool) => {
|
||||
const isSelected = isToolSelected(tool, provider, selectedTools)
|
||||
const isMCPTool = provider.type === CollectionType.mcp
|
||||
const disabled = isSelected || (!canChooseMCPTool && isMCPTool)
|
||||
|
||||
return (
|
||||
<ActionItem
|
||||
key={tool.name}
|
||||
provider={provider}
|
||||
payload={tool}
|
||||
onSelect={onSelect}
|
||||
disabled={disabled}
|
||||
isAdded={isSelected}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type RenderFeaturedToolParams = {
|
||||
plugin: Plugin
|
||||
providerMap: Map<string, ToolWithProvider>
|
||||
installedPluginIds: Set<string>
|
||||
installMutationPending: boolean
|
||||
installingIdentifier: string | null
|
||||
loadingInstalledStatus: boolean
|
||||
canChooseMCPTool?: boolean
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
language: string
|
||||
installPlugin: (uniqueIdentifier: string) => void
|
||||
setInstallingIdentifier: (identifier: string | null) => void
|
||||
}
|
||||
|
||||
function renderFeaturedToolItem({
|
||||
plugin,
|
||||
providerMap,
|
||||
installedPluginIds,
|
||||
installMutationPending,
|
||||
installingIdentifier,
|
||||
loadingInstalledStatus,
|
||||
canChooseMCPTool,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
language,
|
||||
installPlugin,
|
||||
setInstallingIdentifier,
|
||||
}: RenderFeaturedToolParams) {
|
||||
const provider = providerMap.get(plugin.plugin_id)
|
||||
const isInstalled = installedPluginIds.has(plugin.plugin_id)
|
||||
const isInstalling = installMutationPending && installingIdentifier === plugin.latest_package_identifier
|
||||
|
||||
return (
|
||||
<FeaturedToolItem
|
||||
key={plugin.plugin_id}
|
||||
plugin={plugin}
|
||||
provider={provider}
|
||||
isInstalled={isInstalled}
|
||||
installDisabled={loadingInstalledStatus || installMutationPending}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onSelect={onSelect}
|
||||
selectedTools={selectedTools}
|
||||
language={language}
|
||||
onInstall={() => {
|
||||
if (installMutationPending)
|
||||
return
|
||||
setInstallingIdentifier(plugin.latest_package_identifier)
|
||||
installPlugin(plugin.latest_package_identifier)
|
||||
}}
|
||||
isInstalling={isInstalling}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturedTools
|
||||
|
|
@ -277,6 +277,12 @@ const translation = {
|
|||
'addAll': 'Add all',
|
||||
'sources': 'Sources',
|
||||
'searchDataSource': 'Search Data Source',
|
||||
'featuredTools': 'Featured',
|
||||
'showMoreFeatured': 'Show more',
|
||||
'installed': 'Installed',
|
||||
'pluginByAuthor': 'By {{author}}',
|
||||
'usePlugin': 'Select tool',
|
||||
'hideActions': 'Hide tools',
|
||||
},
|
||||
blocks: {
|
||||
'start': 'User Input',
|
||||
|
|
|
|||
|
|
@ -263,6 +263,12 @@ const translation = {
|
|||
'sources': '数据源',
|
||||
'searchDataSource': '搜索数据源',
|
||||
'start': '开始',
|
||||
'featuredTools': '精选推荐',
|
||||
'showMoreFeatured': '查看更多',
|
||||
'installed': '已安装',
|
||||
'pluginByAuthor': '来自 {{author}}',
|
||||
'usePlugin': '选择工具',
|
||||
'hideActions': '收起工具',
|
||||
},
|
||||
blocks: {
|
||||
'start': '用户输入',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere
|
|||
import { uninstallPlugin } from '@/service/plugins'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
|
||||
const NAME_SPACE = 'plugins'
|
||||
|
||||
|
|
@ -66,6 +67,35 @@ export const useCheckInstalled = ({
|
|||
})
|
||||
}
|
||||
|
||||
const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins']
|
||||
export const useRecommendedMarketplacePlugins = ({
|
||||
category = PluginCategoryEnum.tool,
|
||||
enabled = true,
|
||||
limit = 15,
|
||||
}: {
|
||||
category?: string
|
||||
enabled?: boolean
|
||||
limit?: number
|
||||
} = {}) => {
|
||||
return useQuery<Plugin[]>({
|
||||
queryKey: [...useRecommendedMarketplacePluginsKey, category, limit],
|
||||
queryFn: async () => {
|
||||
const response = await postMarketplace<{ data: { plugins: Plugin[] } }>(
|
||||
'/collections/__recommended-plugins-overall/plugins',
|
||||
{
|
||||
body: {
|
||||
category,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
)
|
||||
return response.data.plugins.map(plugin => getFormattedPlugin(plugin))
|
||||
},
|
||||
enabled,
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => {
|
||||
const fetchPlugins = async ({ pageParam = 1 }) => {
|
||||
const response = await get<InstalledPluginListWithTotalResponse>(
|
||||
|
|
|
|||
Loading…
Reference in New Issue