feat: knowledge pipeline (#25360)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: jyong <718720800@qq.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: quicksand <quicksandzn@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Harry <xh001x@hotmail.com>
This commit is contained in:
-LAN-
2025-09-18 12:49:10 +08:00
committed by GitHub
parent 7dadb33003
commit 85cda47c70
1772 changed files with 102407 additions and 31710 deletions

View File

@@ -1,3 +1,7 @@
import type {
Dispatch,
SetStateAction,
} from 'react'
import {
useEffect,
useMemo,
@@ -21,6 +25,7 @@ import PluginList, { type ListProps } from '@/app/components/workflow/block-sele
import { PluginType } from '../../plugins/types'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
import RAGToolSuggestions from './rag-tool-suggestions'
type AllToolsProps = {
className?: string
@@ -36,6 +41,8 @@ type AllToolsProps = {
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
onTagsChange: Dispatch<SetStateAction<string[]>>
isInRAGPipeline?: boolean
}
const DEFAULT_TAGS: AllToolsProps['tags'] = []
@@ -54,6 +61,8 @@ const AllTools = ({
mcpTools = [],
selectedTools,
canChooseMCPTool,
onTagsChange,
isInRAGPipeline = false,
}: AllToolsProps) => {
const language = useGetLanguage()
const tabs = useToolTabs()
@@ -107,6 +116,8 @@ const AllTools = ({
const wrapElemRef = useRef<HTMLDivElement>(null)
const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab)
const isShowRAGRecommendations = isInRAGPipeline && activeTab === ToolTypeEnum.All && !hasFilter
return (
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
<div className='flex items-center justify-between border-b border-divider-subtle px-3'>
@@ -136,6 +147,13 @@ const AllTools = ({
className='max-h-[464px] overflow-y-auto'
onScroll={pluginRef.current?.handleScroll}
>
{isShowRAGRecommendations && (
<RAGToolSuggestions
viewType={isSupportGroupView ? activeView : ViewType.flat}
onSelect={onSelect}
onTagsChange={onTagsChange}
/>
)}
<Tools
className={toolContentClassName}
tools={tools}
@@ -147,16 +165,19 @@ const AllTools = ({
hasSearchText={!!searchText}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
isShowRAGRecommendations={isShowRAGRecommendations}
/>
{/* Plugins from marketplace */}
{enable_marketplace && <PluginList
ref={pluginRef}
wrapElemRef={wrapElemRef}
list={notInstalledPlugins}
searchText={searchText}
toolContentClassName={toolContentClassName}
tags={tags}
/>}
{enable_marketplace && (
<PluginList
ref={pluginRef}
wrapElemRef={wrapElemRef}
list={notInstalledPlugins}
searchText={searchText}
toolContentClassName={toolContentClassName}
tags={tags}
/>
)}
</div>
</div>
)

View File

@@ -3,16 +3,13 @@ import {
useCallback,
useMemo,
} from 'react'
import { useStoreApi } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { groupBy } from 'lodash-es'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import {
useIsChatMode,
useNodesExtraData,
} from '../hooks'
import type { NodeDefault } from '../types'
import { BLOCK_CLASSIFICATIONS } from './constants'
import { useBlocks } from './hooks'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
import Badge from '@/app/components/base/badge'
@@ -21,24 +18,21 @@ type BlocksProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
blocks: NodeDefault[]
}
const Blocks = ({
searchText,
onSelect,
availableBlocksTypes = [],
blocks,
}: BlocksProps) => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
const nodesExtraData = useNodesExtraData()
const blocks = useBlocks()
const store = useStoreApi()
const groups = useMemo(() => {
return BLOCK_CLASSIFICATIONS.reduce((acc, classification) => {
const list = groupBy(blocks, 'classification')[classification].filter((block) => {
if (block.type === BlockEnum.Answer && !isChatMode)
return false
return block.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.type)
const list = groupBy(blocks, 'metaData.classification')[classification].filter((block) => {
return block.metaData.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.metaData.type)
})
return {
@@ -46,11 +40,19 @@ const Blocks = ({
[classification]: list,
}
}, {} as Record<string, typeof blocks>)
}, [blocks, isChatMode, searchText, availableBlocksTypes])
}, [blocks, searchText, availableBlocksTypes])
const isEmpty = Object.values(groups).every(list => !list.length)
const renderGroup = useCallback((classification: string) => {
const list = groups[classification]
const list = groups[classification].sort((a, b) => a.metaData.sort - b.metaData.sort)
const { getNodes } = store.getState()
const nodes = getNodes()
const hasKnowledgeBaseNode = nodes.some(node => node.data.type === BlockEnum.KnowledgeBase)
const filteredList = list.filter((block) => {
if (hasKnowledgeBaseNode)
return block.metaData.type !== BlockEnum.KnowledgeBase
return true
})
return (
<div
@@ -58,16 +60,16 @@ const Blocks = ({
className='mb-1 last-of-type:mb-0'
>
{
classification !== '-' && !!list.length && (
classification !== '-' && !!filteredList.length && (
<div className='flex h-[22px] items-start px-3 text-xs font-medium text-text-tertiary'>
{t(`workflow.tabs.${classification}`)}
</div>
)
}
{
list.map(block => (
filteredList.map(block => (
<Tooltip
key={block.type}
key={block.metaData.type}
position='right'
popupClassName='w-[200px]'
needsDelay={false}
@@ -76,25 +78,25 @@ const Blocks = ({
<BlockIcon
size='md'
className='mb-2'
type={block.type}
type={block.metaData.type}
/>
<div className='system-md-medium mb-1 text-text-primary'>{block.title}</div>
<div className='system-xs-regular text-text-tertiary'>{nodesExtraData[block.type].about}</div>
<div className='system-md-medium mb-1 text-text-primary'>{block.metaData.title}</div>
<div className='system-xs-regular text-text-tertiary'>{block.metaData.description}</div>
</div>
)}
>
<div
key={block.type}
key={block.metaData.type}
className='flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover'
onClick={() => onSelect(block.type)}
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
className='mr-2 shrink-0'
type={block.type}
type={block.metaData.type}
/>
<div className='grow text-sm text-text-secondary'>{block.title}</div>
<div className='grow text-sm text-text-secondary'>{block.metaData.title}</div>
{
block.type === BlockEnum.LoopEnd && (
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('workflow.nodes.loop.loopNode')}
className='ml-2 shrink-0'
@@ -107,10 +109,10 @@ const Blocks = ({
}
</div>
)
}, [groups, nodesExtraData, onSelect, t])
}, [groups, onSelect, t, store])
return (
<div className='p-1'>
<div className='max-h-[480px] overflow-y-auto p-1'>
{
isEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>

View File

@@ -1,107 +1,5 @@
import type { Block } from '../types'
import { BlockEnum } from '../types'
import { BlockClassificationEnum } from './types'
export const BLOCKS: Block[] = [
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Start,
title: 'Start',
description: '',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.LLM,
title: 'LLM',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.KnowledgeRetrieval,
title: 'Knowledge Retrieval',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.End,
title: 'End',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Answer,
title: 'Direct Answer',
},
{
classification: BlockClassificationEnum.QuestionUnderstand,
type: BlockEnum.QuestionClassifier,
title: 'Question Classifier',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.IfElse,
title: 'IF/ELSE',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.LoopEnd,
title: 'Exit Loop',
description: '',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.Iteration,
title: 'Iteration',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.Loop,
title: 'Loop',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Code,
title: 'Code',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.TemplateTransform,
title: 'Templating Transform',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.VariableAggregator,
title: 'Variable Aggregator',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.DocExtractor,
title: 'Doc Extractor',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Assigner,
title: 'Variable Assigner',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.ParameterExtractor,
title: 'Parameter Extractor',
},
{
classification: BlockClassificationEnum.Utilities,
type: BlockEnum.HttpRequest,
title: 'HTTP Request',
},
{
classification: BlockClassificationEnum.Utilities,
type: BlockEnum.ListFilter,
title: 'List Filter',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Agent',
},
]
export const BLOCK_CLASSIFICATIONS: string[] = [
BlockClassificationEnum.Default,
BlockClassificationEnum.QuestionUnderstand,
@@ -109,3 +7,25 @@ export const BLOCK_CLASSIFICATIONS: string[] = [
BlockClassificationEnum.Transform,
BlockClassificationEnum.Utilities,
]
export const DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE = [
'txt',
'markdown',
'mdx',
'pdf',
'html',
'xlsx',
'xls',
'vtt',
'properties',
'doc',
'docx',
'csv',
'eml',
'msg',
'pptx',
'xml',
'epub',
'ppt',
'md',
]

View File

@@ -0,0 +1,92 @@
import {
useCallback,
useRef,
} from 'react'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine } from '@remixicon/react'
import { BlockEnum } from '../types'
import type {
OnSelectBlock,
ToolWithProvider,
} from '../types'
import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
import Tools from './tools'
import { ViewType } from './view-type-select'
import cn from '@/utils/classnames'
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { getMarketplaceUrl } from '@/utils/var'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from './constants'
type AllToolsProps = {
className?: string
toolContentClassName?: string
searchText: string
onSelect: OnSelectBlock
dataSources: ToolWithProvider[]
}
const DataSources = ({
className,
toolContentClassName,
searchText,
onSelect,
dataSources,
}: AllToolsProps) => {
const { t } = useTranslation()
const pluginRef = useRef<ListRef>(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
const handleSelect = useCallback((_: any, toolDefaultValue: ToolDefaultValue) => {
let defaultValue: DataSourceDefaultValue = {
plugin_id: toolDefaultValue?.provider_id,
provider_type: toolDefaultValue?.provider_type,
provider_name: toolDefaultValue?.provider_name,
datasource_name: toolDefaultValue?.tool_name,
datasource_label: toolDefaultValue?.tool_label,
title: toolDefaultValue?.title,
}
// Update defaultValue with fileExtensions if this is the local file data source
if (toolDefaultValue?.provider_id === 'langgenius/file' && toolDefaultValue?.provider_name === 'file') {
defaultValue = {
...defaultValue,
fileExtensions: DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE,
}
}
onSelect(BlockEnum.DataSource, toolDefaultValue && defaultValue)
}, [onSelect])
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
return (
<div className={cn(className)}>
<div
ref={wrapElemRef}
className='max-h-[464px] overflow-y-auto'
onScroll={pluginRef.current?.handleScroll}
>
<Tools
className={toolContentClassName}
tools={dataSources}
onSelect={handleSelect as OnSelectBlock}
viewType={ViewType.flat}
hasSearchText={!!searchText}
canNotSelectMultiple
/>
{
enable_marketplace && (
<Link
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
href={getMarketplaceUrl('')}
target='_blank'
>
<span>{t('plugin.findMoreInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
</Link>
)
}
</div>
</div>
)
}
export default DataSources

View File

@@ -1,34 +1,65 @@
import {
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { BLOCKS } from './constants'
import {
TabsEnum,
ToolTypeEnum,
} from './types'
export const useBlocks = () => {
export const useTabs = (noBlocks?: boolean, noSources?: boolean, noTools?: boolean) => {
const { t } = useTranslation()
const tabs = useMemo(() => {
return [
...(
noBlocks
? []
: [
{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
},
]
),
...(
noSources
? []
: [
{
key: TabsEnum.Sources,
name: t('workflow.tabs.sources'),
},
]
),
...(
noTools
? []
: [
{
key: TabsEnum.Tools,
name: t('workflow.tabs.tools'),
},
]
),
]
}, [t, noBlocks, noSources, noTools])
const initialTab = useMemo(() => {
if (noBlocks)
return noTools ? TabsEnum.Sources : TabsEnum.Tools
return BLOCKS.map((block) => {
return {
...block,
title: t(`workflow.blocks.${block.type}`),
}
})
}
if (noTools)
return noBlocks ? TabsEnum.Sources : TabsEnum.Blocks
export const useTabs = () => {
const { t } = useTranslation()
return TabsEnum.Blocks
}, [noBlocks, noSources, noTools])
const [activeTab, setActiveTab] = useState(initialTab)
return [
{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
},
{
key: TabsEnum.Tools,
name: t('workflow.tabs.tools'),
},
]
return {
tabs,
activeTab,
setActiveTab,
}
}
export const useToolTabs = (isHideMCPTools?: boolean) => {
@@ -51,7 +82,7 @@ export const useToolTabs = (isHideMCPTools?: boolean) => {
name: t('workflow.tabs.workflowTool'),
},
]
if(!isHideMCPTools) {
if (!isHideMCPTools) {
tabs.push({
key: ToolTypeEnum.MCP,
name: 'MCP',

View File

@@ -6,6 +6,7 @@ import classNames from '@/utils/classnames'
export const CUSTOM_GROUP_NAME = '@@@custom@@@'
export const WORKFLOW_GROUP_NAME = '@@@workflow@@@'
export const DATA_SOURCE_GROUP_NAME = '@@@data_source@@@'
export const AGENT_GROUP_NAME = '@@@agent@@@'
/*
{
@@ -49,6 +50,8 @@ export const groupItems = (items: ToolWithProvider[], getFirstChar: (item: ToolW
groupName = CUSTOM_GROUP_NAME
else if (item.type === CollectionType.workflow)
groupName = WORKFLOW_GROUP_NAME
else if (item.type === CollectionType.datasource)
groupName = DATA_SOURCE_GROUP_NAME
else
groupName = AGENT_GROUP_NAME

View File

@@ -1,173 +1,49 @@
import type {
FC,
MouseEventHandler,
} from 'react'
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type { BlockEnum, OnSelectBlock } from '../types'
import Tabs from './tabs'
import { TabsEnum } from './types'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Input from '@/app/components/base/input'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import type { NodeSelectorProps } from './main'
import NodeSelector from './main'
import { useHooksStore } from '@/app/components/workflow/hooks-store/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { useStore } from '../store'
import {
Plus02,
} from '@/app/components/base/icons/src/vender/line/general'
const NodeSelectorWrapper = (props: NodeSelectorProps) => {
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
const dataSourceList = useStore(s => s.dataSourceList)
type NodeSelectorProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
onSelect: OnSelectBlock
trigger?: (open: boolean) => React.ReactNode
placement?: Placement
offset?: OffsetOptions
triggerStyle?: React.CSSProperties
triggerClassName?: (open: boolean) => string
triggerInnerClassName?: string
popupClassName?: string
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
disabled?: boolean
noBlocks?: boolean
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
onOpenChange,
onSelect,
trigger,
placement = 'right',
offset = 6,
triggerClassName,
triggerInnerClassName,
triggerStyle,
popupClassName,
asChild,
availableBlocksTypes,
disabled,
noBlocks = false,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const [localOpen, setLocalOpen] = useState(false)
const open = openFromProps === undefined ? localOpen : openFromProps
const handleOpenChange = useCallback((newOpen: boolean) => {
setLocalOpen(newOpen)
const blocks = useMemo(() => {
const result = availableNodesMetaData?.nodes || []
if (!newOpen)
setSearchText('')
return result.filter((block) => {
if (block.metaData.type === BlockEnum.Start)
return false
if (onOpenChange)
onOpenChange(newOpen)
}, [onOpenChange])
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
if (disabled)
return
e.stopPropagation()
handleOpenChange(!open)
}, [handleOpenChange, open, disabled])
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleOpenChange(false)
onSelect(type, toolDefaultValue)
}, [handleOpenChange, onSelect])
if (block.metaData.type === BlockEnum.DataSource)
return false
const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
setActiveTab(newActiveTab)
}, [])
const searchPlaceholder = useMemo(() => {
if (activeTab === TabsEnum.Blocks)
return t('workflow.tabs.searchBlock')
if (block.metaData.type === BlockEnum.Tool)
return false
if (activeTab === TabsEnum.Tools)
return t('workflow.tabs.searchTool')
return ''
}, [activeTab, t])
if (block.metaData.type === BlockEnum.IterationStart)
return false
if (block.metaData.type === BlockEnum.LoopStart)
return false
if (block.metaData.type === BlockEnum.DataSourceEmpty)
return false
return true
})
}, [availableNodesMetaData?.nodes])
return (
<PortalToFollowElem
placement={placement}
offset={offset}
open={open}
onOpenChange={handleOpenChange}
>
<PortalToFollowElemTrigger
asChild={asChild}
onClick={handleTrigger}
className={triggerInnerClassName}
>
{
trigger
? trigger(open)
: (
<div
className={`
z-10 flex h-4
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
${triggerClassName?.(open)}
`}
style={triggerStyle}
>
<Plus02 className='h-2.5 w-2.5' />
</div>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
inputClassName='grow'
/>
)}
</div>
}
onSelect={handleSelect}
searchText={searchText}
tags={tags}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<NodeSelector
{...props}
blocks={blocks}
dataSources={dataSourceList || []}
/>
)
}
export default memo(NodeSelector)
export default NodeSelectorWrapper

View File

@@ -0,0 +1,208 @@
import type {
FC,
MouseEventHandler,
} from 'react'
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type {
BlockEnum,
NodeDefault,
OnSelectBlock,
ToolWithProvider,
} from '../types'
import Tabs from './tabs'
import { TabsEnum } from './types'
import { useTabs } from './hooks'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Input from '@/app/components/base/input'
import {
Plus02,
} from '@/app/components/base/icons/src/vender/line/general'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
export type NodeSelectorProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
onSelect: OnSelectBlock
trigger?: (open: boolean) => React.ReactNode
placement?: Placement
offset?: OffsetOptions
triggerStyle?: React.CSSProperties
triggerClassName?: (open: boolean) => string
triggerInnerClassName?: string
popupClassName?: string
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
disabled?: boolean
blocks?: NodeDefault[]
dataSources?: ToolWithProvider[]
noBlocks?: boolean
noTools?: boolean
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
onOpenChange,
onSelect,
trigger,
placement = 'right',
offset = 6,
triggerClassName,
triggerInnerClassName,
triggerStyle,
popupClassName,
asChild,
availableBlocksTypes,
disabled,
blocks = [],
dataSources = [],
noBlocks = false,
noTools = false,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const [localOpen, setLocalOpen] = useState(false)
const open = openFromProps === undefined ? localOpen : openFromProps
const handleOpenChange = useCallback((newOpen: boolean) => {
setLocalOpen(newOpen)
if (!newOpen)
setSearchText('')
if (onOpenChange)
onOpenChange(newOpen)
}, [onOpenChange])
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
if (disabled)
return
e.stopPropagation()
handleOpenChange(!open)
}, [handleOpenChange, open, disabled])
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleOpenChange(false)
onSelect(type, toolDefaultValue)
}, [handleOpenChange, onSelect])
const {
activeTab,
setActiveTab,
tabs,
} = useTabs(noBlocks, !dataSources.length, noTools)
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
setActiveTab(newActiveTab)
}, [setActiveTab])
const searchPlaceholder = useMemo(() => {
if (activeTab === TabsEnum.Blocks)
return t('workflow.tabs.searchBlock')
if (activeTab === TabsEnum.Tools)
return t('workflow.tabs.searchTool')
if (activeTab === TabsEnum.Sources)
return t('workflow.tabs.searchDataSource')
return ''
}, [activeTab, t])
return (
<PortalToFollowElem
placement={placement}
offset={offset}
open={open}
onOpenChange={handleOpenChange}
>
<PortalToFollowElemTrigger
asChild={asChild}
onClick={handleTrigger}
className={triggerInnerClassName}
>
{
trigger
? trigger(open)
: (
<div
className={`
z-10 flex h-4
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
${triggerClassName?.(open)}
`}
style={triggerStyle}
>
<Plus02 className='h-2.5 w-2.5' />
</div>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<Tabs
tabs={tabs}
activeTab={activeTab}
blocks={blocks}
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Sources && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
placeholder={t('plugin.searchTools')!}
inputClassName='grow'
/>
)}
</div>
}
onSelect={handleSelect}
searchText={searchText}
tags={tags}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
dataSources={dataSources}
noTools={noTools}
onTagsChange={setTags}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(NodeSelector)

View File

@@ -32,7 +32,7 @@ const List = ({
ref,
}: ListProps) => {
const { t } = useTranslation()
const hasFilter = !searchText
const noFilter = !searchText && tags.length === 0
const hasRes = list.length > 0
const urlWithSearchText = getMarketplaceUrl('', { q: searchText, tags: tags.join(',') })
const nextToStickyELemRef = useRef<HTMLDivElement>(null)
@@ -68,7 +68,7 @@ const List = ({
window.open(urlWithSearchText, '_blank')
}
if (hasFilter) {
if (noFilter) {
return (
<Link
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
@@ -110,7 +110,7 @@ const List = ({
onAction={noop}
/>
))}
{list.length > 0 && (
{hasRes && (
<div className='mb-3 mt-2 flex items-center justify-center space-x-2'>
<div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div>
<Link

View File

@@ -0,0 +1,101 @@
import type { Dispatch, SetStateAction } from 'react'
import React, { useCallback, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import type { OnSelectBlock } from '../types'
import Tools from './tools'
import { ToolTypeEnum } from './types'
import type { ViewType } from './view-type-select'
import { RiMoreLine } from '@remixicon/react'
import Loading from '@/app/components/base/loading'
import Link from 'next/link'
import { getMarketplaceUrl } from '@/utils/var'
import { useRAGRecommendedPlugins } from '@/service/use-tools'
type RAGToolSuggestionsProps = {
viewType: ViewType
onSelect: OnSelectBlock
onTagsChange: Dispatch<SetStateAction<string[]>>
}
const RAGToolSuggestions: React.FC<RAGToolSuggestionsProps> = ({
viewType,
onSelect,
onTagsChange,
}) => {
const { t } = useTranslation()
const {
data: ragRecommendedPlugins,
isFetching: isFetchingRAGRecommendedPlugins,
} = useRAGRecommendedPlugins()
const recommendedPlugins = useMemo(() => {
if (ragRecommendedPlugins)
return [...ragRecommendedPlugins.installed_recommended_plugins]
return []
}, [ragRecommendedPlugins])
const loadMore = useCallback(() => {
onTagsChange((prev) => {
if (prev.includes('rag'))
return prev
return [...prev, 'rag']
})
}, [onTagsChange])
return (
<div className='flex flex-col p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
{t('pipeline.ragToolSuggestions.title')}
</div>
{isFetchingRAGRecommendedPlugins && (
<div className='py-2'>
<Loading type='app' />
</div>
)}
{!isFetchingRAGRecommendedPlugins && recommendedPlugins.length === 0 && (
<p className='system-xs-regular px-3 py-1 text-text-tertiary'>
<Trans
i18nKey='pipeline.ragToolSuggestions.noRecommendationPluginsInstalled'
components={{
CustomLink: (
<Link
className='text-text-accent'
target='_blank'
rel='noopener noreferrer'
href={getMarketplaceUrl('', { tags: 'rag' })}
/>
),
}}
/>
</p>
)}
{!isFetchingRAGRecommendedPlugins && recommendedPlugins.length > 0 && (
<>
<Tools
className='p-0'
tools={recommendedPlugins}
onSelect={onSelect}
canNotSelectMultiple
toolType={ToolTypeEnum.All}
viewType={viewType}
hasSearchText={false}
/>
<div
className='flex cursor-pointer items-center gap-x-2 py-1 pl-3 pr-2'
onClick={loadMore}
>
<div className='px-1'>
<RiMoreLine className='size-4 text-text-tertiary' />
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('common.operation.more')}
</div>
</div>
</>
)}
</div>
)
}
export default React.memo(RAGToolSuggestions)

View File

@@ -1,12 +1,16 @@
import type { FC } from 'react'
import type { Dispatch, FC, SetStateAction } from 'react'
import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import type {
BlockEnum,
NodeDefault,
OnSelectBlock,
ToolWithProvider,
} from '../types'
import { TabsEnum } from './types'
import Blocks from './blocks'
import AllTools from './all-tools'
import DataSources from './data-sources'
import cn from '@/utils/classnames'
export type TabsProps = {
@@ -14,22 +18,34 @@ export type TabsProps = {
onActiveTabChange: (activeTab: TabsEnum) => void
searchText: string
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onTagsChange: Dispatch<SetStateAction<string[]>>
onSelect: OnSelectBlock
availableBlocksTypes?: BlockEnum[]
blocks: NodeDefault[]
dataSources?: ToolWithProvider[]
tabs: Array<{
key: TabsEnum
name: string
}>
filterElem: React.ReactNode
noBlocks?: boolean
noTools?: boolean
}
const Tabs: FC<TabsProps> = ({
activeTab,
onActiveTabChange,
tags,
onTagsChange,
searchText,
onSelect,
availableBlocksTypes,
blocks,
dataSources = [],
tabs = [],
filterElem,
noBlocks,
noTools,
}) => {
const tabs = useTabs()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
@@ -67,12 +83,24 @@ const Tabs: FC<TabsProps> = ({
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
blocks={blocks}
/>
</div>
)
}
{
activeTab === TabsEnum.Tools && (
activeTab === TabsEnum.Sources && !!dataSources.length && (
<div className='border-t border-divider-subtle'>
<DataSources
searchText={searchText}
onSelect={onSelect}
dataSources={dataSources}
/>
</div>
)
}
{
activeTab === TabsEnum.Tools && !noTools && (
<AllTools
searchText={searchText}
onSelect={onSelect}
@@ -83,6 +111,8 @@ const Tabs: FC<TabsProps> = ({
workflowTools={workflowTools || []}
mcpTools={mcpTools || []}
canChooseMCPTool
onTagsChange={onTagsChange}
isInRAGPipeline={dataSources.length > 0}
/>
)
}

View File

@@ -13,7 +13,7 @@ import type {
} from '@floating-ui/react'
import AllTools from '@/app/components/workflow/block-selector/all-tools'
import type { ToolDefaultValue, ToolValue } from './types'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
@@ -158,13 +158,11 @@ const ToolPicker: FC<Props> = ({
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
inputClassName='grow'
/>
</div>
<AllTools
@@ -172,7 +170,7 @@ const ToolPicker: FC<Props> = ({
toolContentClassName='max-w-[100%]'
tags={tags}
searchText={searchText}
onSelect={handleSelect}
onSelect={handleSelect as OnSelectBlock}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}

View File

@@ -69,7 +69,6 @@ const ToolItem: FC<Props> = ({
tool_description: payload.description[language],
title: payload.label[language],
is_team_authorization: provider.is_team_authorization,
output_schema: payload.output_schema,
paramSchemas: payload.parameters,
params,
meta: provider.meta,

View File

@@ -90,7 +90,6 @@ const Tool: FC<Props> = ({
tool_description: tool.description[language],
title: tool.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema,
paramSchemas: tool.parameters,
params,
}
@@ -170,7 +169,6 @@ const Tool: FC<Props> = ({
tool_description: tool.description[language],
title: tool.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema,
paramSchemas: tool.parameters,
params,
})

View File

@@ -28,6 +28,7 @@ type ToolsProps = {
indexBarClassName?: string
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
isShowRAGRecommendations?: boolean
}
const Blocks = ({
onSelect,
@@ -42,6 +43,7 @@ const Blocks = ({
indexBarClassName,
selectedTools,
canChooseMCPTool,
isShowRAGRecommendations = false,
}: ToolsProps) => {
// const tools: any = []
const { t } = useTranslation()
@@ -105,7 +107,12 @@ const Blocks = ({
}
{!tools.length && !hasSearchText && (
<div className='py-10'>
<Empty type={toolType!} isAgent={isAgent}/>
<Empty type={toolType!} isAgent={isAgent} />
</div>
)}
{!!tools.length && isShowRAGRecommendations && (
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
{t('tools.allTools')}
</div>
)}
{!!tools.length && (

View File

@@ -1,8 +1,11 @@
import type { PluginMeta } from '../../plugins/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
export enum TabsEnum {
Blocks = 'blocks',
Tools = 'tools',
Sources = 'sources',
}
export enum ToolTypeEnum {
@@ -32,11 +35,20 @@ export type ToolDefaultValue = {
is_team_authorization: boolean
params: Record<string, any>
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
credential_id?: string
meta?: PluginMeta
}
export type DataSourceDefaultValue = {
plugin_id: string
provider_type: string
provider_name: string
datasource_name: string
datasource_label: string
title: string
fileExtensions?: string[]
}
export type ToolValue = {
provider_name: string
provider_show_name?: string
@@ -49,3 +61,37 @@ export type ToolValue = {
extra?: Record<string, any>
credential_id?: string
}
export type DataSourceItem = {
plugin_id: string
plugin_unique_identifier: string
provider: string
declaration: {
credentials_schema: any[]
provider_type: string
identity: {
author: string
description: TypeWithI18N
icon: string | { background: string; content: string }
label: TypeWithI18N
name: string
tags: string[]
}
datasources: {
description: TypeWithI18N
identity: {
author: string
icon?: string | { background: string; content: string }
label: TypeWithI18N
name: string
provider: string
}
parameters: any[]
output_schema?: {
type: string
properties: Record<string, any>
}
}[]
}
is_authorized: boolean
}

View File

@@ -0,0 +1,36 @@
import type { Tool } from '@/app/components/tools/types'
import type { DataSourceItem } from './types'
export const transformDataSourceToTool = (dataSourceItem: DataSourceItem) => {
return {
id: dataSourceItem.plugin_id,
provider: dataSourceItem.provider,
name: dataSourceItem.provider,
author: dataSourceItem.declaration.identity.author,
description: dataSourceItem.declaration.identity.description,
icon: dataSourceItem.declaration.identity.icon,
label: dataSourceItem.declaration.identity.label,
type: dataSourceItem.declaration.provider_type,
team_credentials: {},
allow_delete: true,
is_team_authorization: dataSourceItem.is_authorized,
is_authorized: dataSourceItem.is_authorized,
labels: dataSourceItem.declaration.identity.tags || [],
plugin_id: dataSourceItem.plugin_id,
tools: dataSourceItem.declaration.datasources.map((datasource) => {
return {
name: datasource.identity.name,
author: datasource.identity.author,
label: datasource.identity.label,
description: datasource.description,
parameters: datasource.parameters,
labels: [],
output_schema: datasource.output_schema,
} as Tool
}),
credentialsSchema: dataSourceItem.declaration.credentials_schema || [],
meta: {
version: '',
},
}
}