mirror of
https://github.com/langgenius/dify.git
synced 2026-03-16 20:37:42 +08:00
Merge remote-tracking branch 'origin/main' into feat/trigger
This commit is contained in:
@ -6,12 +6,14 @@ import {
|
||||
Answer,
|
||||
Assigner,
|
||||
Code,
|
||||
Datasource,
|
||||
DocsExtractor,
|
||||
End,
|
||||
Home,
|
||||
Http,
|
||||
IfElse,
|
||||
Iteration,
|
||||
KnowledgeBase,
|
||||
KnowledgeRetrieval,
|
||||
ListFilter,
|
||||
Llm,
|
||||
@ -25,6 +27,7 @@ import {
|
||||
WebhookLine,
|
||||
} from '@/app/components/base/icons/src/vender/workflow'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type BlockIconProps = {
|
||||
type: BlockEnum
|
||||
@ -62,6 +65,9 @@ const getIcon = (type: BlockEnum, className: string) => {
|
||||
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
|
||||
[BlockEnum.ListFilter]: <ListFilter className={className} />,
|
||||
[BlockEnum.Agent]: <Agent className={className} />,
|
||||
[BlockEnum.KnowledgeBase]: <KnowledgeBase className={className} />,
|
||||
[BlockEnum.DataSource]: <Datasource className={className} />,
|
||||
[BlockEnum.DataSourceEmpty]: <></>,
|
||||
[BlockEnum.TriggerSchedule]: <Schedule className={className} />,
|
||||
[BlockEnum.TriggerWebhook]: <WebhookLine className={className} />,
|
||||
[BlockEnum.TriggerPlugin]: null,
|
||||
@ -83,11 +89,14 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.TemplateTransform]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.VariableAssigner]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.VariableAggregator]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.Tool]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.Assigner]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.ParameterExtractor]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
|
||||
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
|
||||
[BlockEnum.KnowledgeBase]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.DataSource]: 'bg-components-icon-bg-midnight-solid',
|
||||
[BlockEnum.TriggerSchedule]: 'bg-util-colors-violet-violet-500',
|
||||
[BlockEnum.TriggerWebhook]: 'bg-util-colors-blue-blue-500',
|
||||
[BlockEnum.TriggerPlugin]: 'bg-util-colors-white-white-500',
|
||||
@ -98,17 +107,21 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
className,
|
||||
toolIcon,
|
||||
}) => {
|
||||
const isToolOrDataSourceOrTriggerPlugin = type === BlockEnum.Tool || type === BlockEnum.DataSource || type === BlockEnum.TriggerPlugin
|
||||
const showDefaultIcon = !isToolOrDataSourceOrTriggerPlugin || !toolIcon
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center justify-center border-[0.5px] border-white/2 text-white
|
||||
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
|
||||
${ICON_CONTAINER_BG_COLOR_MAP[type]}
|
||||
${toolIcon && '!shadow-none'}
|
||||
${className}
|
||||
`}
|
||||
<div className={
|
||||
cn(
|
||||
'flex items-center justify-center border-[0.5px] border-white/2 text-white',
|
||||
ICON_CONTAINER_CLASSNAME_SIZE_MAP[size],
|
||||
showDefaultIcon && ICON_CONTAINER_BG_COLOR_MAP[type],
|
||||
toolIcon && '!shadow-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{
|
||||
type !== BlockEnum.Tool && type !== BlockEnum.TriggerPlugin && (
|
||||
showDefaultIcon && (
|
||||
getIcon(type,
|
||||
(type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook)
|
||||
? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5')
|
||||
@ -117,7 +130,7 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
(type === BlockEnum.Tool || type === BlockEnum.TriggerPlugin) && toolIcon && (
|
||||
!showDefaultIcon && (
|
||||
<>
|
||||
{
|
||||
typeof toolIcon === 'string'
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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] rounded-xl'
|
||||
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>
|
||||
|
||||
@ -2,6 +2,36 @@ import type { Block } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { BlockClassificationEnum } from './types'
|
||||
|
||||
export const BLOCK_CLASSIFICATIONS: string[] = [
|
||||
BlockClassificationEnum.Default,
|
||||
BlockClassificationEnum.QuestionUnderstand,
|
||||
BlockClassificationEnum.Logic,
|
||||
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',
|
||||
]
|
||||
|
||||
export const START_BLOCKS: Block[] = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
@ -23,7 +53,6 @@ export const START_BLOCKS: Block[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// Entry node types that can start a workflow
|
||||
export const ENTRY_NODE_TYPES = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
@ -31,104 +60,96 @@ export const ENTRY_NODE_TYPES = [
|
||||
BlockEnum.TriggerPlugin,
|
||||
] as const
|
||||
|
||||
export const BLOCKS: Block[] = [
|
||||
{
|
||||
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,
|
||||
BlockClassificationEnum.Logic,
|
||||
BlockClassificationEnum.Transform,
|
||||
BlockClassificationEnum.Utilities,
|
||||
]
|
||||
// export const BLOCKS: Block[] = [
|
||||
// {
|
||||
// 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',
|
||||
// },
|
||||
// ]
|
||||
|
||||
92
web/app/components/workflow/block-selector/data-sources.tsx
Normal file
92
web/app/components/workflow/block-selector/data-sources.tsx
Normal 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
|
||||
@ -1,54 +1,80 @@
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BLOCKS, START_BLOCKS } from './constants'
|
||||
// import { BLOCKS, START_BLOCKS } from './constants'
|
||||
import {
|
||||
TabsEnum,
|
||||
ToolTypeEnum,
|
||||
} from './types'
|
||||
|
||||
export const useBlocks = () => {
|
||||
// export const useBlocks = () => {
|
||||
// const { t } = useTranslation()
|
||||
|
||||
// return BLOCKS.map((block) => {
|
||||
// return {
|
||||
// ...block,
|
||||
// title: t(`workflow.blocks.${block.type}`),
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// export const useStartBlocks = () => {
|
||||
// const { t } = useTranslation()
|
||||
|
||||
// return START_BLOCKS.map((block) => {
|
||||
// return {
|
||||
// ...block,
|
||||
// title: t(`workflow.blocks.${block.type}`),
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
export const useTabs = ({ noBlocks, noSources, noTools, noStart = true }: {
|
||||
noBlocks?: boolean
|
||||
noSources?: boolean
|
||||
noTools?: boolean
|
||||
noStart?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return BLOCKS.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
title: t(`workflow.blocks.${block.type}`),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useStartBlocks = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return START_BLOCKS.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
title: t(`workflow.blocks.${block.type}`),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useTabs = (showStartTab = false) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
const tabs = useMemo(() => {
|
||||
return [{
|
||||
key: TabsEnum.Blocks,
|
||||
name: t('workflow.tabs.blocks'),
|
||||
},
|
||||
{
|
||||
show: !noBlocks,
|
||||
}, {
|
||||
key: TabsEnum.Sources,
|
||||
name: t('workflow.tabs.sources'),
|
||||
show: !noSources,
|
||||
}, {
|
||||
key: TabsEnum.Tools,
|
||||
name: t('workflow.tabs.tools'),
|
||||
show: !noTools,
|
||||
},
|
||||
]
|
||||
|
||||
if (showStartTab) {
|
||||
tabs.push({
|
||||
{
|
||||
key: TabsEnum.Start,
|
||||
name: t('workflow.tabs.start'),
|
||||
})
|
||||
}
|
||||
show: !noStart,
|
||||
}].filter(tab => tab.show)
|
||||
}, [t, noBlocks, noSources, noTools, noStart])
|
||||
|
||||
return tabs
|
||||
const initialTab = useMemo(() => {
|
||||
if (noBlocks)
|
||||
return noTools ? TabsEnum.Sources : TabsEnum.Tools
|
||||
|
||||
if (noTools)
|
||||
return noBlocks ? TabsEnum.Sources : TabsEnum.Blocks
|
||||
|
||||
return TabsEnum.Blocks
|
||||
}, [noBlocks, noSources, noTools])
|
||||
const [activeTab, setActiveTab] = useState(initialTab)
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
}
|
||||
}
|
||||
|
||||
export const useToolTabs = (isHideMCPTools?: boolean) => {
|
||||
@ -71,7 +97,7 @@ export const useToolTabs = (isHideMCPTools?: boolean) => {
|
||||
name: t('workflow.tabs.workflowTool'),
|
||||
},
|
||||
]
|
||||
if(!isHideMCPTools) {
|
||||
if (!isHideMCPTools) {
|
||||
tabs.push({
|
||||
key: ToolTypeEnum.MCP,
|
||||
name: 'MCP',
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,195 +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
|
||||
showStartTab?: boolean
|
||||
defaultActiveTab?: TabsEnum
|
||||
forceShowStartContent?: boolean
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
placement = 'right',
|
||||
offset = 6,
|
||||
triggerClassName,
|
||||
triggerInnerClassName,
|
||||
triggerStyle,
|
||||
popupClassName,
|
||||
asChild,
|
||||
availableBlocksTypes,
|
||||
disabled,
|
||||
noBlocks = false,
|
||||
showStartTab = false,
|
||||
defaultActiveTab,
|
||||
forceShowStartContent = 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, pluginDefaultValue) => {
|
||||
handleOpenChange(false)
|
||||
onSelect(type, pluginDefaultValue)
|
||||
}, [handleOpenChange, onSelect])
|
||||
if (block.metaData.type === BlockEnum.DataSource)
|
||||
return false
|
||||
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
defaultActiveTab || (noBlocks ? TabsEnum.Tools : TabsEnum.Blocks),
|
||||
)
|
||||
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
|
||||
setActiveTab(newActiveTab)
|
||||
}, [])
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Start)
|
||||
return t('workflow.tabs.searchTrigger')
|
||||
if (activeTab === TabsEnum.Blocks)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
if (activeTab === TabsEnum.Tools)
|
||||
return t('workflow.tabs.searchTool')
|
||||
return ''
|
||||
}, [activeTab, t])
|
||||
if (block.metaData.type === BlockEnum.Tool)
|
||||
return false
|
||||
|
||||
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-xl 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.Start && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={searchPlaceholder}
|
||||
inputClassName='grow'
|
||||
/>
|
||||
)}
|
||||
{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={searchPlaceholder}
|
||||
inputClassName='grow'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
noBlocks={noBlocks}
|
||||
showStartTab={showStartTab}
|
||||
forceShowStartContent={forceShowStartContent}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
<NodeSelector
|
||||
{...props}
|
||||
blocks={blocks}
|
||||
dataSources={dataSourceList || []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeSelector)
|
||||
export default NodeSelectorWrapper
|
||||
|
||||
229
web/app/components/workflow/block-selector/main.tsx
Normal file
229
web/app/components/workflow/block-selector/main.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
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
|
||||
showStartTab?: boolean
|
||||
// defaultActiveTab?: TabsEnum
|
||||
forceShowStartContent?: 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,
|
||||
showStartTab = false,
|
||||
// defaultActiveTab,
|
||||
forceShowStartContent = 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, pluginDefaultValue) => {
|
||||
handleOpenChange(false)
|
||||
onSelect(type, pluginDefaultValue)
|
||||
}, [handleOpenChange, onSelect])
|
||||
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
tabs,
|
||||
} = useTabs({ noBlocks, noSources: !dataSources.length, noTools, noStart: !showStartTab })
|
||||
|
||||
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
|
||||
setActiveTab(newActiveTab)
|
||||
}, [setActiveTab])
|
||||
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Start)
|
||||
return t('workflow.tabs.searchTrigger')
|
||||
|
||||
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.Start && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={searchPlaceholder}
|
||||
inputClassName='grow'
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
forceShowStartContent={forceShowStartContent}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeSelector)
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStickyScroll, { ScrollPosition } from '../use-sticky-scroll'
|
||||
import Item from './item'
|
||||
@ -17,20 +17,22 @@ export type ListProps = {
|
||||
tags: string[]
|
||||
toolContentClassName?: string
|
||||
disableMaxWidth?: boolean
|
||||
ref?: React.Ref<ListRef>
|
||||
}
|
||||
|
||||
export type ListRef = { handleScroll: () => void }
|
||||
|
||||
const List = forwardRef<ListRef, ListProps>(({
|
||||
const List = ({
|
||||
wrapElemRef,
|
||||
searchText,
|
||||
tags,
|
||||
list,
|
||||
toolContentClassName,
|
||||
disableMaxWidth = false,
|
||||
}, ref) => {
|
||||
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)
|
||||
@ -66,7 +68,7 @@ const List = forwardRef<ListRef, ListProps>(({
|
||||
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'
|
||||
@ -108,7 +110,7 @@ const List = forwardRef<ListRef, ListProps>(({
|
||||
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
|
||||
@ -125,7 +127,7 @@ const List = forwardRef<ListRef, ListProps>(({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
List.displayName = 'List'
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -9,10 +9,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import BlockIcon from '../block-icon'
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import { BlockEnum as BlockEnumValues } from '../types'
|
||||
import { useNodesExtraData } from '../hooks'
|
||||
// import { useNodeMetaData } from '../hooks'
|
||||
import { START_BLOCKS } from './constants'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAvailableNodesMetaData } from '../../workflow-app/hooks'
|
||||
|
||||
type StartBlocksProps = {
|
||||
searchText: string
|
||||
@ -29,7 +30,8 @@ const StartBlocks = ({
|
||||
}: StartBlocksProps) => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
// const nodeMetaData = useNodeMetaData()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
|
||||
const filteredBlocks = useMemo(() => {
|
||||
// Check if Start node already exists in workflow
|
||||
@ -74,7 +76,7 @@ const StartBlocks = ({
|
||||
: t(`workflow.blocks.${block.type}`)
|
||||
}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-secondary'>{nodesExtraData[block.type].about}</div>
|
||||
{/* <div className='system-xs-regular text-text-secondary'>{availableNodesMetaData.nodesMap?.[block.type]?.description}</div> */}
|
||||
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
|
||||
<div className='system-xs-regular mb-1 mt-1 text-text-tertiary'>
|
||||
{t('tools.author')} {t('workflow.difyTeam')}
|
||||
@ -99,7 +101,7 @@ const StartBlocks = ({
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
), [nodesExtraData, onSelect, t])
|
||||
), [availableNodesMetaData, onSelect, t])
|
||||
|
||||
if (isEmpty)
|
||||
return null
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
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 { PluginDefaultValue } from './types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import { TabsEnum } from './types'
|
||||
import Blocks from './blocks'
|
||||
import AllStartBlocks from './all-start-blocks'
|
||||
import AllTools from './all-tools'
|
||||
import DataSources from './data-sources'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type TabsProps = {
|
||||
@ -15,26 +19,36 @@ export type TabsProps = {
|
||||
onActiveTabChange: (activeTab: TabsEnum) => void
|
||||
searchText: string
|
||||
tags: string[]
|
||||
onSelect: (type: BlockEnum, plugin?: PluginDefaultValue) => void
|
||||
onTagsChange: Dispatch<SetStateAction<string[]>>
|
||||
onSelect: OnSelectBlock
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
blocks: NodeDefault[]
|
||||
dataSources?: ToolWithProvider[]
|
||||
tabs: Array<{
|
||||
key: TabsEnum
|
||||
name: string
|
||||
}>
|
||||
filterElem: React.ReactNode
|
||||
noBlocks?: boolean
|
||||
showStartTab?: boolean
|
||||
noTools?: boolean
|
||||
forceShowStartContent?: boolean // Force show Start content even when noBlocks=true
|
||||
}
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
activeTab,
|
||||
onActiveTabChange,
|
||||
tags,
|
||||
onTagsChange,
|
||||
searchText,
|
||||
onSelect,
|
||||
availableBlocksTypes,
|
||||
blocks,
|
||||
dataSources = [],
|
||||
tabs = [],
|
||||
filterElem,
|
||||
noBlocks,
|
||||
showStartTab = false,
|
||||
noTools,
|
||||
forceShowStartContent = false,
|
||||
}) => {
|
||||
const tabs = useTabs(showStartTab)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
@ -84,12 +98,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}
|
||||
@ -100,6 +126,8 @@ const Tabs: FC<TabsProps> = ({
|
||||
workflowTools={workflowTools || []}
|
||||
mcpTools={mcpTools || []}
|
||||
canChooseMCPTool
|
||||
onTagsChange={onTagsChange}
|
||||
isInRAGPipeline={dataSources.length > 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 || []}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { PluginMeta, SupportedCreationMethods } from '../../plugins/types'
|
||||
import type { Collection, Trigger } from '../../tools/types'
|
||||
import type { TypeWithI18N } from '../../base/form/types'
|
||||
|
||||
export enum TabsEnum {
|
||||
Start = 'start',
|
||||
Blocks = 'blocks',
|
||||
Tools = 'tools',
|
||||
Sources = 'sources',
|
||||
}
|
||||
|
||||
export enum ToolTypeEnum {
|
||||
@ -57,6 +58,16 @@ export type ToolDefaultValue = PluginDefaultValue & {
|
||||
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
|
||||
@ -70,6 +81,40 @@ export type ToolValue = {
|
||||
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
|
||||
}
|
||||
|
||||
// Backend API types - exact match with Python definitions
|
||||
export type TriggerParameter = {
|
||||
multiple: boolean
|
||||
|
||||
36
web/app/components/workflow/block-selector/utils.ts
Normal file
36
web/app/components/workflow/block-selector/utils.ts
Normal 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: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { BlockEnum } from './types'
|
||||
|
||||
export const ALL_AVAILABLE_BLOCKS = Object.values(BlockEnum)
|
||||
export const ALL_CHAT_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
|
||||
export const ALL_COMPLETION_AVAILABLE_BLOCKS = ALL_AVAILABLE_BLOCKS.filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
|
||||
@ -62,9 +62,9 @@ const CandidateNode = () => {
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteAdd)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteAdd, { nodeId: candidateNode.id })
|
||||
else
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: candidateNode.id })
|
||||
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
|
||||
|
||||
@ -1,455 +1,5 @@
|
||||
import type { Var } from './types'
|
||||
import { BlockEnum, VarType } from './types'
|
||||
import StartNodeDefault from './nodes/start/default'
|
||||
import AnswerDefault from './nodes/answer/default'
|
||||
import LLMDefault from './nodes/llm/default'
|
||||
import KnowledgeRetrievalDefault from './nodes/knowledge-retrieval/default'
|
||||
import QuestionClassifierDefault from './nodes/question-classifier/default'
|
||||
import IfElseDefault from './nodes/if-else/default'
|
||||
import CodeDefault from './nodes/code/default'
|
||||
import TemplateTransformDefault from './nodes/template-transform/default'
|
||||
import HttpRequestDefault from './nodes/http/default'
|
||||
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
|
||||
import ToolDefault from './nodes/tool/default'
|
||||
import VariableAssignerDefault from './nodes/variable-assigner/default'
|
||||
import AssignerDefault from './nodes/assigner/default'
|
||||
import EndNodeDefault from './nodes/end/default'
|
||||
import IterationDefault from './nodes/iteration/default'
|
||||
import LoopDefault from './nodes/loop/default'
|
||||
import DocExtractorDefault from './nodes/document-extractor/default'
|
||||
import ListFilterDefault from './nodes/list-operator/default'
|
||||
import IterationStartDefault from './nodes/iteration-start/default'
|
||||
import AgentDefault from './nodes/agent/default'
|
||||
import LoopStartDefault from './nodes/loop-start/default'
|
||||
import LoopEndDefault from './nodes/loop-end/default'
|
||||
import TriggerScheduleDefault from './nodes/trigger-schedule/default'
|
||||
import TriggerWebhookDefault from './nodes/trigger-webhook/default'
|
||||
import TriggerPluginDefault from './nodes/trigger-plugin/default'
|
||||
|
||||
type NodesExtraData = {
|
||||
author: string
|
||||
about: string
|
||||
availablePrevNodes: BlockEnum[]
|
||||
availableNextNodes: BlockEnum[]
|
||||
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
|
||||
checkValid: any
|
||||
defaultRunInputData?: Record<string, any>
|
||||
}
|
||||
export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||
[BlockEnum.Start]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: StartNodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: StartNodeDefault.getAvailableNextNodes,
|
||||
checkValid: StartNodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.End]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: EndNodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: EndNodeDefault.getAvailableNextNodes,
|
||||
checkValid: EndNodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Answer]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: AnswerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: AnswerDefault.getAvailableNextNodes,
|
||||
checkValid: AnswerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LLM]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LLMDefault.getAvailableNextNodes,
|
||||
checkValid: LLMDefault.checkValid,
|
||||
defaultRunInputData: LLMDefault.defaultRunInputData,
|
||||
},
|
||||
[BlockEnum.KnowledgeRetrieval]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: KnowledgeRetrievalDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: KnowledgeRetrievalDefault.getAvailableNextNodes,
|
||||
checkValid: KnowledgeRetrievalDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.IfElse]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IfElseDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IfElseDefault.getAvailableNextNodes,
|
||||
checkValid: IfElseDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Iteration]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IterationDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IterationDefault.getAvailableNextNodes,
|
||||
checkValid: IterationDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IterationStartDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IterationStartDefault.getAvailableNextNodes,
|
||||
checkValid: IterationStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Loop]: {
|
||||
author: 'AICT-Team',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopDefault.getAvailableNextNodes,
|
||||
checkValid: LoopDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopStart]: {
|
||||
author: 'AICT-Team',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopStartDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes,
|
||||
checkValid: LoopStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes,
|
||||
checkValid: LoopEndDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: CodeDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: CodeDefault.getAvailableNextNodes,
|
||||
checkValid: CodeDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.TemplateTransform]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: TemplateTransformDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: TemplateTransformDefault.getAvailableNextNodes,
|
||||
checkValid: TemplateTransformDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.QuestionClassifier]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: QuestionClassifierDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: QuestionClassifierDefault.getAvailableNextNodes,
|
||||
checkValid: QuestionClassifierDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.HttpRequest]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: HttpRequestDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: HttpRequestDefault.getAvailableNextNodes,
|
||||
checkValid: HttpRequestDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.VariableAssigner]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
||||
checkValid: VariableAssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Assigner]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: AssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: AssignerDefault.getAvailableNextNodes,
|
||||
checkValid: AssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.VariableAggregator]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
||||
checkValid: VariableAssignerDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.ParameterExtractor]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ParameterExtractorDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ParameterExtractorDefault.getAvailableNextNodes,
|
||||
checkValid: ParameterExtractorDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Tool]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ToolDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ToolDefault.getAvailableNextNodes,
|
||||
checkValid: ToolDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.DocExtractor]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: DocExtractorDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: DocExtractorDefault.getAvailableNextNodes,
|
||||
checkValid: DocExtractorDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.ListFilter]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
|
||||
checkValid: ListFilterDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Agent]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes,
|
||||
checkValid: AgentDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.TriggerSchedule]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: TriggerScheduleDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: TriggerScheduleDefault.getAvailableNextNodes,
|
||||
checkValid: TriggerScheduleDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.TriggerWebhook]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: TriggerWebhookDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: TriggerWebhookDefault.getAvailableNextNodes,
|
||||
checkValid: TriggerWebhookDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.TriggerPlugin]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: TriggerPluginDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: TriggerPluginDefault.getAvailableNextNodes,
|
||||
checkValid: TriggerPluginDefault.checkValid,
|
||||
},
|
||||
}
|
||||
|
||||
export const NODES_INITIAL_DATA = {
|
||||
[BlockEnum.Start]: {
|
||||
type: BlockEnum.Start,
|
||||
title: '',
|
||||
desc: '',
|
||||
...StartNodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.End]: {
|
||||
type: BlockEnum.End,
|
||||
title: '',
|
||||
desc: '',
|
||||
...EndNodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Answer]: {
|
||||
type: BlockEnum.Answer,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AnswerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LLM]: {
|
||||
type: BlockEnum.LLM,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...LLMDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.KnowledgeRetrieval]: {
|
||||
type: BlockEnum.KnowledgeRetrieval,
|
||||
title: '',
|
||||
desc: '',
|
||||
query_variable_selector: [],
|
||||
dataset_ids: [],
|
||||
retrieval_mode: 'single',
|
||||
...KnowledgeRetrievalDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.IfElse]: {
|
||||
type: BlockEnum.IfElse,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IfElseDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Iteration]: {
|
||||
type: BlockEnum.Iteration,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IterationDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
type: BlockEnum.IterationStart,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IterationStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Loop]: {
|
||||
type: BlockEnum.Loop,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopStart]: {
|
||||
type: BlockEnum.LoopStart,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopEndDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
type: BlockEnum.Code,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
code_language: 'python3',
|
||||
code: '',
|
||||
outputs: [],
|
||||
...CodeDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.TemplateTransform]: {
|
||||
type: BlockEnum.TemplateTransform,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
template: '',
|
||||
...TemplateTransformDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.QuestionClassifier]: {
|
||||
type: BlockEnum.QuestionClassifier,
|
||||
title: '',
|
||||
desc: '',
|
||||
query_variable_selector: [],
|
||||
topics: [],
|
||||
...QuestionClassifierDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.HttpRequest]: {
|
||||
type: BlockEnum.HttpRequest,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...HttpRequestDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.ParameterExtractor]: {
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
...ParameterExtractorDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.VariableAssigner]: {
|
||||
type: BlockEnum.VariableAssigner,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
output_type: '',
|
||||
...VariableAssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.VariableAggregator]: {
|
||||
type: BlockEnum.VariableAggregator,
|
||||
title: '',
|
||||
desc: '',
|
||||
variables: [],
|
||||
output_type: '',
|
||||
...VariableAssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Assigner]: {
|
||||
type: BlockEnum.Assigner,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AssignerDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Tool]: {
|
||||
type: BlockEnum.Tool,
|
||||
title: '',
|
||||
desc: '',
|
||||
...ToolDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.DocExtractor]: {
|
||||
type: BlockEnum.DocExtractor,
|
||||
title: '',
|
||||
desc: '',
|
||||
...DocExtractorDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.ListFilter]: {
|
||||
type: BlockEnum.ListFilter,
|
||||
title: '',
|
||||
desc: '',
|
||||
...ListFilterDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Agent]: {
|
||||
type: BlockEnum.Agent,
|
||||
title: '',
|
||||
desc: '',
|
||||
...AgentDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.TriggerSchedule]: {
|
||||
type: BlockEnum.TriggerSchedule,
|
||||
title: '',
|
||||
desc: '',
|
||||
...TriggerScheduleDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.TriggerWebhook]: {
|
||||
type: BlockEnum.TriggerWebhook,
|
||||
title: '',
|
||||
desc: '',
|
||||
...TriggerWebhookDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.TriggerPlugin]: {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: '',
|
||||
desc: '',
|
||||
...TriggerPluginDefault.defaultValue,
|
||||
},
|
||||
}
|
||||
export const MAX_ITERATION_PARALLEL_NUM = 10
|
||||
export const MIN_ITERATION_PARALLEL_NUM = 1
|
||||
export const DEFAULT_ITER_TIMES = 1
|
||||
@ -512,7 +62,7 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
|
||||
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
|
||||
BlockEnum.ParameterExtractor, BlockEnum.Iteration, BlockEnum.Loop,
|
||||
BlockEnum.DocExtractor, BlockEnum.ListFilter,
|
||||
BlockEnum.Agent,
|
||||
BlockEnum.Agent, BlockEnum.DataSource,
|
||||
]
|
||||
|
||||
export const AGENT_OUTPUT_STRUCT: Var[] = [
|
||||
@ -527,6 +77,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [
|
||||
variable: 'text',
|
||||
type: VarType.string,
|
||||
},
|
||||
{
|
||||
variable: 'reasoning_content',
|
||||
type: VarType.string,
|
||||
},
|
||||
{
|
||||
variable: 'usage',
|
||||
type: VarType.object,
|
||||
|
||||
44
web/app/components/workflow/constants/node.ts
Normal file
44
web/app/components/workflow/constants/node.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import knowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
||||
|
||||
import questionClassifierDefault from '@/app/components/workflow/nodes/question-classifier/default'
|
||||
|
||||
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||
import iterationDefault from '@/app/components/workflow/nodes/iteration/default'
|
||||
import iterationStartDefault from '@/app/components/workflow/nodes/iteration-start/default'
|
||||
import loopDefault from '@/app/components/workflow/nodes/loop/default'
|
||||
import loopStartDefault from '@/app/components/workflow/nodes/loop-start/default'
|
||||
import loopEndDefault from '@/app/components/workflow/nodes/loop-end/default'
|
||||
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
import templateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
|
||||
import variableAggregatorDefault from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
|
||||
import parameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
|
||||
import listOperatorDefault from '@/app/components/workflow/nodes/list-operator/default'
|
||||
import toolDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
|
||||
export const WORKFLOW_COMMON_NODES = [
|
||||
llmDefault,
|
||||
knowledgeRetrievalDefault,
|
||||
agentDefault,
|
||||
questionClassifierDefault,
|
||||
ifElseDefault,
|
||||
iterationDefault,
|
||||
iterationStartDefault,
|
||||
loopDefault,
|
||||
loopStartDefault,
|
||||
loopEndDefault,
|
||||
codeDefault,
|
||||
templateTransformDefault,
|
||||
variableAggregatorDefault,
|
||||
documentExtractorDefault,
|
||||
assignerDefault,
|
||||
parameterExtractorDefault,
|
||||
httpRequestDefault,
|
||||
listOperatorDefault,
|
||||
toolDefault,
|
||||
]
|
||||
@ -2,18 +2,18 @@ import {
|
||||
createContext,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type { SliceFromInjection } from './store'
|
||||
import {
|
||||
createWorkflowStore,
|
||||
} from './store'
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { WorkflowSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice'
|
||||
|
||||
type WorkflowStore = ReturnType<typeof createWorkflowStore>
|
||||
export const WorkflowContext = createContext<WorkflowStore | null>(null)
|
||||
|
||||
export type WorkflowProviderProps = {
|
||||
children: React.ReactNode
|
||||
injectWorkflowStoreSliceFn?: StateCreator<WorkflowSliceShape>
|
||||
injectWorkflowStoreSliceFn?: StateCreator<SliceFromInjection>
|
||||
}
|
||||
export const WorkflowContextProvider = ({ children, injectWorkflowStoreSliceFn }: WorkflowProviderProps) => {
|
||||
const storeRef = useRef<WorkflowStore | undefined>(undefined)
|
||||
|
||||
@ -56,8 +56,8 @@ const CustomEdge = ({
|
||||
})
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration, (data as Edge['data'])?.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration, (data as Edge['data'])?.isInLoop)
|
||||
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration || (data as Edge['data'])?.isInLoop)
|
||||
const {
|
||||
_sourceRunningStatus,
|
||||
_targetRunningStatus,
|
||||
|
||||
60
web/app/components/workflow/features.tsx
Normal file
60
web/app/components/workflow/features.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useStore } from './store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from './hooks'
|
||||
import { type CommonNodeType, type InputVar, InputVarType, type Node } from './types'
|
||||
import useConfig from './nodes/start/use-config'
|
||||
import type { StartNodeType } from './nodes/start/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
|
||||
const Features = () => {
|
||||
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
|
||||
const isChatMode = useIsChatMode()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
|
||||
const startNode = nodes.find(node => node.data.type === 'start')
|
||||
const { id, data } = startNode as Node<StartNodeType>
|
||||
const { handleAddVariable } = useConfig(id, data)
|
||||
|
||||
const handleAddOpeningStatementVariable = (variables: PromptVariable[]) => {
|
||||
const newVariable = variables[0]
|
||||
const startNodeVariable: InputVar = {
|
||||
variable: newVariable.key,
|
||||
label: newVariable.name,
|
||||
type: InputVarType.textInput,
|
||||
max_length: newVariable.max_length,
|
||||
required: newVariable.required || false,
|
||||
options: [],
|
||||
}
|
||||
handleAddVariable(startNodeVariable)
|
||||
}
|
||||
|
||||
const handleFeaturesChange = useCallback(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
setShowFeaturesPanel(true)
|
||||
}, [handleSyncWorkflowDraft, setShowFeaturesPanel])
|
||||
|
||||
return (
|
||||
<NewFeaturePanel
|
||||
show
|
||||
isChatMode={isChatMode}
|
||||
disabled={nodesReadOnly}
|
||||
onChange={handleFeaturesChange}
|
||||
onClose={() => setShowFeaturesPanel(false)}
|
||||
onAutoAddPromptVariable={handleAddOpeningStatementVariable}
|
||||
workflowVariables={data.variables}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Features)
|
||||
@ -4,17 +4,20 @@ import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
|
||||
const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
const { closeAllInputFieldPanels } = useInputFieldPanel()
|
||||
|
||||
const handleClick = () => {
|
||||
setShowEnvPanel(true)
|
||||
setShowChatVariablePanel(false)
|
||||
setShowDebugAndPreviewPanel(false)
|
||||
closeAllInputFieldPanels()
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -13,10 +13,12 @@ import {
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import type { RunAndHistoryProps } from './run-and-history'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import EditingTitle from './editing-title'
|
||||
import EnvButton from './env-button'
|
||||
import VersionHistoryButton from './version-history-button'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import ScrollToSelectedNodeButton from './scroll-to-selected-node-button'
|
||||
|
||||
export type HeaderInNormalProps = {
|
||||
@ -24,9 +26,11 @@ export type HeaderInNormalProps = {
|
||||
left?: React.ReactNode
|
||||
middle?: React.ReactNode
|
||||
}
|
||||
runAndHistoryProps?: RunAndHistoryProps
|
||||
}
|
||||
const HeaderInNormal = ({
|
||||
components,
|
||||
runAndHistoryProps,
|
||||
}: HeaderInNormalProps) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
@ -39,6 +43,7 @@ const HeaderInNormal = ({
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const { handleBackupDraft } = useWorkflowRun()
|
||||
const { closeAllInputFieldPanels } = useInputFieldPanel()
|
||||
|
||||
const onStartRestoring = useCallback(() => {
|
||||
workflowStore.setState({ isRestoring: true })
|
||||
@ -51,6 +56,7 @@ const HeaderInNormal = ({
|
||||
setShowDebugAndPreviewPanel(false)
|
||||
setShowVariableInspectPanel(false)
|
||||
setShowChatVariablePanel(false)
|
||||
closeAllInputFieldPanels()
|
||||
}, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel])
|
||||
|
||||
return (
|
||||
@ -65,7 +71,7 @@ const HeaderInNormal = ({
|
||||
{components?.left}
|
||||
<EnvButton disabled={nodesReadOnly} />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<RunAndHistory />
|
||||
<RunAndHistory {...runAndHistoryProps} />
|
||||
{components?.middle}
|
||||
<VersionHistoryButton onClick={onStartRestoring} />
|
||||
</div>
|
||||
|
||||
@ -17,8 +17,8 @@ import {
|
||||
import Toast from '../../base/toast'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { useHooksStore } from '../hooks-store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
@ -31,9 +31,8 @@ const HeaderInRestoring = ({
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
|
||||
const invalidAllLastRun = useInvalidAllLastRun(appDetail!.id)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const {
|
||||
deleteAllInspectVars,
|
||||
} = workflowStore.getState()
|
||||
|
||||
@ -10,11 +10,17 @@ import {
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import RunningTitle from './running-title'
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import ViewHistory from './view-history'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const HeaderInHistory = () => {
|
||||
export type HeaderInHistoryProps = {
|
||||
viewHistoryProps?: ViewHistoryProps
|
||||
}
|
||||
const HeaderInHistory = ({
|
||||
viewHistoryProps,
|
||||
}: HeaderInHistoryProps) => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
@ -33,7 +39,7 @@ const HeaderInHistory = () => {
|
||||
<RunningTitle />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<ViewHistory withText />
|
||||
<ViewHistory {...viewHistoryProps} withText />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<Button
|
||||
variant='primary'
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
} from '../hooks'
|
||||
import type { HeaderInNormalProps } from './header-in-normal'
|
||||
import HeaderInNormal from './header-in-normal'
|
||||
import type { HeaderInHistoryProps } from './header-in-view-history'
|
||||
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||
import { useStore } from '../store'
|
||||
import dynamic from 'next/dynamic'
|
||||
@ -17,14 +18,17 @@ const HeaderInRestoring = dynamic(() => import('./header-in-restoring'), {
|
||||
|
||||
export type HeaderProps = {
|
||||
normal?: HeaderInNormalProps
|
||||
viewHistory?: HeaderInHistoryProps
|
||||
restoring?: HeaderInRestoringProps
|
||||
}
|
||||
const Header = ({
|
||||
normal: normalProps,
|
||||
viewHistory: viewHistoryProps,
|
||||
restoring: restoringProps,
|
||||
}: HeaderProps) => {
|
||||
const pathname = usePathname()
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const {
|
||||
normal,
|
||||
restoring,
|
||||
@ -34,9 +38,9 @@ const Header = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
className='absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
>
|
||||
{inWorkflowCanvas && maximizeCanvas && <div className='h-14 w-[52px]' />}
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className='h-14 w-[52px]' />}
|
||||
{
|
||||
normal && (
|
||||
<HeaderInNormal
|
||||
@ -46,7 +50,9 @@ const Header = ({
|
||||
}
|
||||
{
|
||||
viewHistory && (
|
||||
<HeaderInHistory />
|
||||
<HeaderInHistory
|
||||
{...viewHistoryProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
|
||||
@ -1,127 +1,17 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiLoader2Line,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflowRun,
|
||||
useWorkflowRunValidation,
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
import { WorkflowRunningStatus } from '../types'
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import TestRunMenu, { type TestRunMenuRef } from './test-run-menu'
|
||||
import type { TriggerOption } from './test-run-menu'
|
||||
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
StopCircle,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
|
||||
const RunMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun()
|
||||
const { handleStopRun } = useWorkflowRun()
|
||||
const { validateBeforeRun } = useWorkflowRunValidation()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
const dynamicOptions = useDynamicTestRunOptions()
|
||||
const testRunMenuRef = useRef<TestRunMenuRef>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error - Dynamic property for backward compatibility with keyboard shortcuts
|
||||
window._toggleTestRunDropdown = () => {
|
||||
testRunMenuRef.current?.toggle()
|
||||
}
|
||||
return () => {
|
||||
// @ts-expect-error - Dynamic property cleanup
|
||||
delete window._toggleTestRunDropdown
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleStop = () => {
|
||||
handleStopRun(workflowRunningData?.task_id || '')
|
||||
}
|
||||
|
||||
const handleTriggerSelect = (option: TriggerOption) => {
|
||||
// Validate checklist before running any workflow
|
||||
if (!validateBeforeRun())
|
||||
return
|
||||
|
||||
if (option.type === 'user_input') {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}
|
||||
else {
|
||||
// Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
|
||||
console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === EVENT_WORKFLOW_STOP)
|
||||
handleStop()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
isRunning
|
||||
? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
'!cursor-not-allowed bg-state-accent-hover',
|
||||
)}
|
||||
>
|
||||
<RiLoader2Line className='mr-1 h-4 w-4 animate-spin' />
|
||||
{t('workflow.common.running')}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<TestRunMenu
|
||||
ref={testRunMenuRef}
|
||||
options={dynamicOptions}
|
||||
onSelect={handleTriggerSelect}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
'cursor-pointer hover:bg-state-accent-hover',
|
||||
)}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<RiPlayLargeLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.run')}
|
||||
<ShortcutsName keys={['alt', 'r']} className="ml-1" textColor="secondary" />
|
||||
</div>
|
||||
</TestRunMenu>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRunning && (
|
||||
<div
|
||||
className='ml-0.5 flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-black/5'
|
||||
onClick={handleStop}
|
||||
>
|
||||
<StopCircle className='h-4 w-4 text-components-button-ghost-text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
})
|
||||
RunMode.displayName = 'RunMode'
|
||||
import RunMode from './run-mode'
|
||||
|
||||
const PreviewMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
@ -140,30 +30,45 @@ const PreviewMode = memo(() => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
PreviewMode.displayName = 'PreviewMode'
|
||||
|
||||
const RunAndHistory: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const isChatMode = useIsChatMode()
|
||||
export type RunAndHistoryProps = {
|
||||
showRunButton?: boolean
|
||||
runButtonText?: string
|
||||
isRunning?: boolean
|
||||
showPreviewButton?: boolean
|
||||
viewHistoryProps?: ViewHistoryProps
|
||||
components?: {
|
||||
RunMode?: React.ComponentType<
|
||||
{
|
||||
text?: string
|
||||
}
|
||||
>
|
||||
}
|
||||
}
|
||||
const RunAndHistory = ({
|
||||
showRunButton,
|
||||
runButtonText,
|
||||
showPreviewButton,
|
||||
viewHistoryProps,
|
||||
components,
|
||||
}: RunAndHistoryProps) => {
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { RunMode: CustomRunMode } = components || {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(
|
||||
'flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}>
|
||||
{
|
||||
!isChatMode && <RunMode />
|
||||
}
|
||||
{
|
||||
isChatMode && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<ViewHistory />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
</>
|
||||
<div className='flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs'>
|
||||
{
|
||||
showRunButton && (
|
||||
CustomRunMode ? <CustomRunMode text={runButtonText} /> : <RunMode text={runButtonText} />
|
||||
)
|
||||
}
|
||||
{
|
||||
showPreviewButton && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<ViewHistory {...viewHistoryProps} />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
146
web/app/components/workflow/header/run-mode.tsx
Normal file
146
web/app/components/workflow/header/run-mode.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useCallback } from 'react'
|
||||
// import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowRun, /* useWorkflowRunValidation, */ useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiLoader2Line, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
// import ShortcutsName from '../shortcuts-name'
|
||||
// import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
// import TestRunMenu, { type TestRunMenuRef, type TriggerOption } from './test-run-menu'
|
||||
|
||||
type RunModeProps = {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const RunMode = ({
|
||||
text,
|
||||
}: RunModeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun()
|
||||
const { handleStopRun } = useWorkflowRun()
|
||||
// const { validateBeforeRun } = useWorkflowRunValidation()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
|
||||
// const dynamicOptions = useDynamicTestRunOptions()
|
||||
// const testRunMenuRef = useRef<TestRunMenuRef>(null)
|
||||
|
||||
// useEffect(() => {
|
||||
// // @ts-expect-error - Dynamic property for backward compatibility with keyboard shortcuts
|
||||
// window._toggleTestRunDropdown = () => {
|
||||
// testRunMenuRef.current?.toggle()
|
||||
// }
|
||||
// return () => {
|
||||
// // @ts-expect-error - Dynamic property cleanup
|
||||
// delete window._toggleTestRunDropdown
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
handleStopRun(workflowRunningData?.task_id || '')
|
||||
}, [handleStopRun, workflowRunningData?.task_id])
|
||||
|
||||
// const handleTriggerSelect = (option: TriggerOption) => {
|
||||
// // Validate checklist before running any workflow
|
||||
// if (!validateBeforeRun())
|
||||
// return
|
||||
|
||||
// if (option.type === 'user_input') {
|
||||
// handleWorkflowStartRunInWorkflow()
|
||||
// }
|
||||
// else {
|
||||
// // Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
|
||||
// console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
|
||||
// }
|
||||
// }
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === EVENT_WORKFLOW_STOP)
|
||||
handleStop()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-px'>
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'system-xs-medium flex h-7 items-center gap-x-1 px-1.5 text-text-accent hover:bg-state-accent-hover',
|
||||
isRunning && 'cursor-not-allowed bg-state-accent-hover',
|
||||
isRunning ? 'rounded-l-md' : 'rounded-md',
|
||||
)}
|
||||
onClick={() => {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{
|
||||
isRunning
|
||||
? (
|
||||
<>
|
||||
<RiLoader2Line className='mr-1 size-4 animate-spin' />
|
||||
{t('workflow.common.running')}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<RiPlayLargeLine className='mr-1 size-4' />
|
||||
{text ?? t('workflow.common.run')}
|
||||
</>
|
||||
// <TestRunMenu
|
||||
// ref={testRunMenuRef}
|
||||
// options={dynamicOptions}
|
||||
// onSelect={handleTriggerSelect}
|
||||
// >
|
||||
// <div
|
||||
// className={cn(
|
||||
// 'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
// 'cursor-pointer hover:bg-state-accent-hover',
|
||||
// )}
|
||||
// style={{ userSelect: 'none' }}
|
||||
// >
|
||||
// <RiPlayLargeLine className='mr-1 size-4' />
|
||||
// {text ?? t('workflow.common.run')}
|
||||
// <ShortcutsName keys={['alt', 'r']} className="ml-1" textColor="secondary" />
|
||||
// </div>
|
||||
// </TestRunMenu>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isRunning && (
|
||||
<div className='system-kbd flex items-center gap-x-0.5 text-text-tertiary'>
|
||||
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
{
|
||||
isRunning && (
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'flex size-7 items-center justify-center rounded-r-md bg-state-accent-active',
|
||||
)}
|
||||
onClick={handleStop}
|
||||
>
|
||||
<StopCircle className='size-4 text-text-accent' />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(RunMode)
|
||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import Button from '../../base/button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem } from '../utils'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
@ -12,7 +12,7 @@ type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
}
|
||||
|
||||
const VERSION_HISTORY_SHORTCUT = ['⌘', '⇧', 'H']
|
||||
const VERSION_HISTORY_SHORTCUT = ['ctrl', '⇧', 'H']
|
||||
|
||||
const PopupContent = React.memo(() => {
|
||||
const { t } = useTranslation()
|
||||
@ -27,7 +27,7 @@ const PopupContent = React.memo(() => {
|
||||
key={key}
|
||||
className='system-kbd rounded-[4px] bg-components-kbd-bg-white px-[1px] text-text-tertiary'
|
||||
>
|
||||
{key}
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@ -48,8 +48,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.h`, (e) => {
|
||||
e.preventDefault()
|
||||
handleViewVersionHistory()
|
||||
},
|
||||
{ exactMatch: true, useCapture: true })
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
return <Tooltip
|
||||
popupContent={<PopupContent />}
|
||||
|
||||
@ -2,9 +2,10 @@ import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { Fetcher } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { noop } from 'lodash-es'
|
||||
import {
|
||||
RiCheckboxCircleLine,
|
||||
RiCloseLine,
|
||||
@ -26,27 +27,30 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
ClockPlay,
|
||||
ClockPlaySlim,
|
||||
} from '@/app/components/base/icons/src/vender/line/time'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import {
|
||||
fetchChatRunHistory,
|
||||
fetchWorkflowRunHistory,
|
||||
} from '@/service/workflow'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import type { WorkflowRunHistoryResponse } from '@/types/workflow'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
|
||||
type ViewHistoryProps = {
|
||||
export type ViewHistoryProps = {
|
||||
withText?: boolean
|
||||
onClearLogAndMessageModal?: () => void
|
||||
historyUrl?: string
|
||||
historyFetcher?: Fetcher<WorkflowRunHistoryResponse, string>
|
||||
}
|
||||
const ViewHistory = ({
|
||||
withText,
|
||||
onClearLogAndMessageModal,
|
||||
historyUrl,
|
||||
historyFetcher,
|
||||
}: ViewHistoryProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
@ -60,18 +64,15 @@ const ViewHistory = ({
|
||||
} = useWorkflowInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})))
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const { handleBackupDraft } = useWorkflowRun()
|
||||
const { data: runList, isLoading: runListLoading } = useSWR((appDetail && !isChatMode && open) ? `/apps/${appDetail.id}/workflow-runs` : null, fetchWorkflowRunHistory)
|
||||
const { data: chatList, isLoading: chatListLoading } = useSWR((appDetail && isChatMode && open) ? `/apps/${appDetail.id}/advanced-chat/workflow-runs` : null, fetchChatRunHistory)
|
||||
const { closeAllInputFieldPanels } = useInputFieldPanel()
|
||||
|
||||
const data = isChatMode ? chatList : runList
|
||||
const isLoading = isChatMode ? chatListLoading : runListLoading
|
||||
const fetcher = historyFetcher ?? (noop as Fetcher<WorkflowRunHistoryResponse, string>)
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
} = useSWR((open && historyUrl && historyFetcher) ? historyUrl : null, fetcher)
|
||||
|
||||
return (
|
||||
(
|
||||
@ -107,8 +108,7 @@ const ViewHistory = ({
|
||||
<div
|
||||
className={cn('group flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-state-accent-hover', open && 'bg-state-accent-hover')}
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
onClearLogAndMessageModal?.()
|
||||
}}
|
||||
>
|
||||
<ClockPlay className={cn('h-4 w-4 group-hover:text-components-button-secondary-accent-text', open ? 'text-components-button-secondary-accent-text' : 'text-components-button-ghost-text')} />
|
||||
@ -129,8 +129,7 @@ const ViewHistory = ({
|
||||
<div
|
||||
className='flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center'
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
onClearLogAndMessageModal?.()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
@ -171,6 +170,7 @@ const ViewHistory = ({
|
||||
showInputsPanel: false,
|
||||
showEnvPanel: false,
|
||||
})
|
||||
closeAllInputFieldPanels()
|
||||
handleBackupDraft()
|
||||
setOpen(false)
|
||||
handleNodesCancelSelected()
|
||||
|
||||
@ -89,10 +89,19 @@ const ViewWorkflowHistory = () => {
|
||||
|
||||
const calculateChangeList: ChangeHistoryList = useMemo(() => {
|
||||
const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => {
|
||||
const nodes = (state.nodes || store.getState().nodes) || []
|
||||
const nodeId = state?.workflowHistoryEventMeta?.nodeId
|
||||
const targetTitle = nodes.find(n => n.id === nodeId)?.data?.title ?? ''
|
||||
return {
|
||||
label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent),
|
||||
index: reverse ? list.length - 1 - index - startIndex : index - startIndex,
|
||||
state,
|
||||
state: {
|
||||
...state,
|
||||
workflowHistoryEventMeta: state.workflowHistoryEventMeta ? {
|
||||
...state.workflowHistoryEventMeta,
|
||||
nodeTitle: state.workflowHistoryEventMeta.nodeTitle || targetTitle,
|
||||
} : undefined,
|
||||
},
|
||||
}
|
||||
}).filter(Boolean)
|
||||
|
||||
@ -110,6 +119,12 @@ const ViewWorkflowHistory = () => {
|
||||
}
|
||||
}, [futureStates, getHistoryLabel, pastStates, store])
|
||||
|
||||
const composeHistoryItemLabel = useCallback((nodeTitle: string | undefined, baseLabel: string) => {
|
||||
if (!nodeTitle)
|
||||
return baseLabel
|
||||
return `${nodeTitle} ${baseLabel}`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
(
|
||||
<PortalToFollowElem
|
||||
@ -197,7 +212,10 @@ const ViewWorkflowHistory = () => {
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')})
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('workflow.changeHistory.sessionStart'),
|
||||
)} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -222,7 +240,10 @@ const ViewWorkflowHistory = () => {
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)})
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('workflow.changeHistory.sessionStart'),
|
||||
)} ({calculateStepLabel(item?.index)})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,14 +7,26 @@ import {
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { HooksStoreContext } from './provider'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
ToolWithProvider,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import type {
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { FileUpload } from '../../base/features/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
|
||||
type CommonHooksFnMap = {
|
||||
export type AvailableNodesMetaData = {
|
||||
nodes: NodeDefault[]
|
||||
nodesMap?: Record<BlockEnum, NodeDefault<any>>
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
@ -33,10 +45,14 @@ type CommonHooksFnMap = {
|
||||
handleStartWorkflowRun: () => void
|
||||
handleWorkflowStartRunInWorkflow: () => void
|
||||
handleWorkflowStartRunInChatflow: () => void
|
||||
fetchInspectVars: () => Promise<void>
|
||||
availableNodesMetaData?: AvailableNodesMetaData
|
||||
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string; traceUrl: string }
|
||||
exportCheck?: () => Promise<void>
|
||||
handleExportDSL?: (include?: boolean, flowId?: string) => Promise<void>
|
||||
fetchInspectVars: (params: { passInVars?: boolean, vars?: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[] }) => Promise<void>
|
||||
hasNodeInspectVars: (nodeId: string) => boolean
|
||||
hasSetInspectVar: (nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => boolean
|
||||
fetchInspectVarValue: (selector: ValueSelector) => Promise<void>
|
||||
fetchInspectVarValue: (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => Promise<void>
|
||||
editInspectVarValue: (nodeId: string, varId: string, value: any) => Promise<void>
|
||||
renameInspectVarName: (nodeId: string, oldName: string, newName: string) => Promise<void>
|
||||
appendNodeInspectVars: (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => void
|
||||
@ -50,8 +66,8 @@ type CommonHooksFnMap = {
|
||||
invalidateConversationVarValues: () => void
|
||||
configsMap?: {
|
||||
flowId: string
|
||||
conversationVarsUrl: string
|
||||
systemVarsUrl: string
|
||||
flowType: FlowType
|
||||
fileSettings: FileUpload
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +87,15 @@ export const createHooksStore = ({
|
||||
handleStartWorkflowRun = noop,
|
||||
handleWorkflowStartRunInWorkflow = noop,
|
||||
handleWorkflowStartRunInChatflow = noop,
|
||||
availableNodesMetaData = {
|
||||
nodes: [],
|
||||
},
|
||||
getWorkflowRunAndTraceUrl = () => ({
|
||||
runUrl: '',
|
||||
traceUrl: '',
|
||||
}),
|
||||
exportCheck = async () => noop(),
|
||||
handleExportDSL = async () => noop(),
|
||||
fetchInspectVars = async () => noop(),
|
||||
hasNodeInspectVars = () => false,
|
||||
hasSetInspectVar = () => false,
|
||||
@ -100,6 +125,10 @@ export const createHooksStore = ({
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
availableNodesMetaData,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-data'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
@ -15,7 +14,12 @@ export * from './use-workflow-variables'
|
||||
export * from './use-shortcuts'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-nodes-meta-data'
|
||||
export * from './use-available-blocks'
|
||||
export * from './use-workflow-refresh-draft'
|
||||
export * from './use-tool-icon'
|
||||
export * from './use-DSL'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-set-workflow-vars-with-value'
|
||||
export * from './use-workflow-search'
|
||||
export * from './use-format-time-from-now'
|
||||
|
||||
11
web/app/components/workflow/hooks/use-DSL.ts
Normal file
11
web/app/components/workflow/hooks/use-DSL.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useDSL = () => {
|
||||
const exportCheck = useHooksStore(s => s.exportCheck)
|
||||
const handleExportDSL = useHooksStore(s => s.handleExportDSL)
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
}
|
||||
}
|
||||
58
web/app/components/workflow/hooks/use-available-blocks.ts
Normal file
58
web/app/components/workflow/hooks/use-available-blocks.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
|
||||
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
|
||||
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase))
|
||||
return false
|
||||
|
||||
if (!inContainer && nodeType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
const {
|
||||
nodes: availableNodes,
|
||||
} = useNodesMetaData()
|
||||
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
|
||||
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
let availablePrevBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
|
||||
availablePrevBlocks = []
|
||||
|
||||
let availableNextBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
availableNextBlocks = []
|
||||
|
||||
return {
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [availableNodesType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
getAvailableBlocks,
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [getAvailableBlocks, availablePrevBlocks, availableNextBlocks, inContainer])
|
||||
}
|
||||
@ -13,41 +13,49 @@ import type {
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getDataSourceCheckParams,
|
||||
getToolCheckParams,
|
||||
getValidTreeNodes,
|
||||
} from '../utils'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
} from '../constants'
|
||||
import {
|
||||
useGetToolIcon,
|
||||
useWorkflow,
|
||||
} from '../hooks'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { AgentNodeType } from '../nodes/agent/types'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import useNodesAvailableVarList from './use-nodes-available-var-list'
|
||||
import { getNodeUsedVars, isConversationVar, isENV, isSystemVar } from '../nodes/_base/components/variable/utils'
|
||||
import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
|
||||
const { getStartNodes } = useWorkflow()
|
||||
const getToolIcon = useGetToolIcon()
|
||||
|
||||
const map = useNodesAvailableVarList(nodes)
|
||||
|
||||
@ -70,28 +78,28 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
const needWarningNodes = useMemo(() => {
|
||||
const list = []
|
||||
const { validNodes } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const startNodes = getStartNodes(filteredNodes)
|
||||
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
|
||||
const validNodes = validNodesFlattened.reduce((acc, curr) => {
|
||||
if (curr.validNodes)
|
||||
acc.push(...curr.validNodes)
|
||||
return acc
|
||||
}, [] as Node[])
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
let toolIcon
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
let moreDataForCheckValid
|
||||
let usedVars: ValueSelector[] = []
|
||||
|
||||
if (node.data.type === BlockEnum.Tool) {
|
||||
const { provider_type } = node.data
|
||||
|
||||
if (node.data.type === BlockEnum.Tool)
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
|
||||
if (provider_type === CollectionType.builtIn)
|
||||
toolIcon = buildInTools.find(tool => canFindTool(tool.id, node.data.provider_id || ''))?.icon
|
||||
|
||||
if (provider_type === CollectionType.custom)
|
||||
toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (provider_type === CollectionType.workflow)
|
||||
toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon
|
||||
}
|
||||
else if (node.data.type === BlockEnum.Agent) {
|
||||
const toolIcon = getToolIcon(node.data)
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
|
||||
@ -109,16 +117,14 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
let { errorMessage } = nodesExtraData[node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
let { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
||||
for (const variable of usedVars) {
|
||||
const isEnv = isENV(variable)
|
||||
const isConvVar = isConversationVar(variable)
|
||||
const isSysVar = isSystemVar(variable)
|
||||
if (!isEnv && !isConvVar && !isSysVar) {
|
||||
const isSpecialVars = isSpecialVar(variable[0])
|
||||
if (!isSpecialVars) {
|
||||
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
@ -156,14 +162,14 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
|
||||
// Check for start nodes (including triggers)
|
||||
const startNodes = nodes.filter(node =>
|
||||
const startNodesFiltered = nodes.filter(node =>
|
||||
node.data.type === BlockEnum.Start
|
||||
|| node.data.type === BlockEnum.TriggerSchedule
|
||||
|| node.data.type === BlockEnum.TriggerWebhook
|
||||
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||
)
|
||||
|
||||
if (startNodes.length === 0) {
|
||||
if (startNodesFiltered.length === 0) {
|
||||
list.push({
|
||||
id: 'start-node-required',
|
||||
type: BlockEnum.Start,
|
||||
@ -172,26 +178,21 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||
list.push({
|
||||
id: 'answer-need-added',
|
||||
type: BlockEnum.Answer,
|
||||
title: t('workflow.blocks.answer'),
|
||||
errorMessage: t('workflow.common.needAnswerNode'),
|
||||
})
|
||||
}
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
|
||||
list.push({
|
||||
id: 'end-need-added',
|
||||
type: BlockEnum.End,
|
||||
title: t('workflow.blocks.end'),
|
||||
errorMessage: t('workflow.common.needEndNode'),
|
||||
})
|
||||
}
|
||||
isRequiredNodesType.forEach((type: string) => {
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
list.push({
|
||||
id: `${type}-need-added`,
|
||||
type,
|
||||
title: t(`workflow.blocks.${type}`),
|
||||
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}, [nodes, edges, isChatMode, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders, getCheckData])
|
||||
}, [nodes, getStartNodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
@ -199,16 +200,15 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
export const useChecklistBeforePublish = () => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const { notify } = useToastContext()
|
||||
const isChatMode = useIsChatMode()
|
||||
const store = useStoreApi()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
|
||||
const updateTime = useRef(0)
|
||||
const { getStartNodes } = useWorkflow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
let checkData = data
|
||||
@ -236,18 +236,31 @@ export const useChecklistBeforePublish = () => {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
|
||||
const {
|
||||
validNodes,
|
||||
maxDepth,
|
||||
} = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
|
||||
dataSourceList,
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
} = workflowStore.getState()
|
||||
const nodes = getNodes()
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const startNodes = getStartNodes(filteredNodes)
|
||||
const validNodesFlattened = startNodes.map(startNode => getValidTreeNodes(startNode, filteredNodes, edges))
|
||||
const validNodes = validNodesFlattened.reduce((acc, curr) => {
|
||||
if (curr.validNodes)
|
||||
acc.push(...curr.validNodes)
|
||||
return acc
|
||||
}, [] as Node[])
|
||||
const maxDepthArr = validNodesFlattened.map(item => item.maxDepth)
|
||||
|
||||
if (maxDepth > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
for (let i = 0; i < maxDepthArr.length; i++) {
|
||||
if (maxDepthArr[i] > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
|
||||
const knowledgeRetrievalNodes = nodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
const knowledgeRetrievalNodes = filteredNodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
const allDatasetIds = knowledgeRetrievalNodes.reduce<string[]>((acc, node) => {
|
||||
return Array.from(new Set([...acc, ...(node.data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids]))
|
||||
}, [])
|
||||
@ -264,13 +277,17 @@ export const useChecklistBeforePublish = () => {
|
||||
updateDatasetsDetail(datasetsDetail)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
const map = getNodesAvailableVarList(nodes)
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
let moreDataForCheckValid
|
||||
let usedVars: ValueSelector[] = []
|
||||
if (node.data.type === BlockEnum.Tool)
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
|
||||
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
@ -283,45 +300,67 @@ export const useChecklistBeforePublish = () => {
|
||||
isReadyForCheckValid,
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
usedVars = getNodeUsedVars(node).filter(v => v.length > 0)
|
||||
}
|
||||
const checkData = getCheckData(node.data, datasets)
|
||||
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
|
||||
return false
|
||||
}
|
||||
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
||||
for (const variable of usedVars) {
|
||||
const isSpecialVars = isSpecialVar(variable[0])
|
||||
if (!isSpecialVars) {
|
||||
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
if (!usedVar) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!validNodes.find(n => n.id === node.id)) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startNodes = nodes.filter(node =>
|
||||
const startNodesFiltered = nodes.filter(node =>
|
||||
node.data.type === BlockEnum.Start
|
||||
|| node.data.type === BlockEnum.TriggerSchedule
|
||||
|| node.data.type === BlockEnum.TriggerWebhook
|
||||
|| node.data.type === BlockEnum.TriggerPlugin,
|
||||
)
|
||||
|
||||
if (startNodes.length === 0) {
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
|
||||
return false
|
||||
}
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needEndNode') })
|
||||
return false
|
||||
for (let i = 0; i < isRequiredNodesType.length; i++) {
|
||||
const type = isRequiredNodesType[i]
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, getStartNodes, workflowStore])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
|
||||
@ -1,33 +1,52 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { fetchAllInspectVars } from '@/service/workflow'
|
||||
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Params = {
|
||||
flowType: FlowType
|
||||
flowId: string
|
||||
conversationVarsUrl: string
|
||||
systemVarsUrl: string
|
||||
}
|
||||
|
||||
export const useSetWorkflowVarsWithValue = ({
|
||||
flowType,
|
||||
flowId,
|
||||
conversationVarsUrl,
|
||||
systemVarsUrl,
|
||||
}: Params) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl)
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(flowType, flowId)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(flowType, flowId)
|
||||
const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync()
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const allPluginInfoList = {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
}
|
||||
|
||||
const setInspectVarsToStore = useCallback((inspectVars: VarInInspect[]) => {
|
||||
const setInspectVarsToStore = (inspectVars: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]) => {
|
||||
const { setNodesWithInspectVars } = workflowStore.getState()
|
||||
const { getNodes } = store.getState()
|
||||
|
||||
const nodeArr = getNodes()
|
||||
const allNodesOutputVars = toNodeOutputVars(nodeArr, false, () => true, [], [], [], passedInAllPluginInfoList || allPluginInfoList, passedInSchemaTypeDefinitions || schemaTypeDefinitions)
|
||||
|
||||
const nodesKeyValue: Record<string, Node> = {}
|
||||
nodeArr.forEach((node) => {
|
||||
nodesKeyValue[node.id] = node
|
||||
@ -51,27 +70,41 @@ export const useSetWorkflowVarsWithValue = ({
|
||||
const varsUnderTheNode = inspectVars.filter((varItem) => {
|
||||
return varItem.selector[0] === nodeId
|
||||
})
|
||||
const nodeVar = allNodesOutputVars.find(item => item.nodeId === nodeId)
|
||||
|
||||
const nodeWithVar = {
|
||||
nodeId,
|
||||
nodePayload: node.data,
|
||||
nodeType: node.data.type,
|
||||
title: node.data.title,
|
||||
vars: varsUnderTheNode,
|
||||
vars: varsUnderTheNode.map((item) => {
|
||||
const schemaType = nodeVar ? nodeVar.vars.find(v => v.variable === item.name)?.schemaType : ''
|
||||
return {
|
||||
...item,
|
||||
schemaType,
|
||||
}
|
||||
}),
|
||||
isSingRunRunning: false,
|
||||
isValueFetched: false,
|
||||
}
|
||||
return nodeWithVar
|
||||
})
|
||||
setNodesWithInspectVars(res)
|
||||
}, [workflowStore, store])
|
||||
}
|
||||
|
||||
const fetchInspectVars = useCallback(async () => {
|
||||
const fetchInspectVars = useCallback(async (params: {
|
||||
passInVars?: boolean,
|
||||
vars?: VarInInspect[],
|
||||
passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>,
|
||||
passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]
|
||||
}) => {
|
||||
const { passInVars, vars, passedInAllPluginInfoList, passedInSchemaTypeDefinitions } = params
|
||||
invalidateConversationVarValues()
|
||||
invalidateSysVarValues()
|
||||
const data = await fetchAllInspectVars(flowId)
|
||||
setInspectVarsToStore(data)
|
||||
const data = passInVars ? vars! : await fetchAllInspectVars(flowType, flowId)
|
||||
setInspectVarsToStore(data, passedInAllPluginInfoList, passedInSchemaTypeDefinitions)
|
||||
handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status
|
||||
}, [invalidateConversationVarValues, invalidateSysVarValues, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus])
|
||||
}, [invalidateConversationVarValues, invalidateSysVarValues, flowType, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus, schemaTypeDefinitions, getMatchedSchemaType])
|
||||
return {
|
||||
fetchInspectVars,
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback } from 'react'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
|
||||
export const useFormatTimeFromNow = () => {
|
||||
const { locale } = useI18N()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
}, [locale])
|
||||
|
||||
return { formatTimeFromNow }
|
||||
}
|
||||
@ -3,38 +3,46 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import {
|
||||
useDeleteAllInspectorVars,
|
||||
useDeleteInspectVar,
|
||||
useDeleteNodeInspectorVars,
|
||||
useEditInspectorVar,
|
||||
useInvalidateConversationVarValues,
|
||||
useInvalidateSysVarValues,
|
||||
useResetConversationVar,
|
||||
useResetToLastRunValue,
|
||||
} from '@/service/use-workflow'
|
||||
import { useCallback } from 'react'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
isSystemVar,
|
||||
toNodeOutputVars,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import produce from 'immer'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useFLow from '@/service/use-flow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
|
||||
type Params = {
|
||||
flowId: string
|
||||
conversationVarsUrl: string
|
||||
systemVarsUrl: string
|
||||
flowType: FlowType
|
||||
}
|
||||
export const useInspectVarsCrudCommon = ({
|
||||
flowId,
|
||||
conversationVarsUrl,
|
||||
systemVarsUrl,
|
||||
flowType,
|
||||
}: Params) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl!)
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
useInvalidateConversationVarValues,
|
||||
useInvalidateSysVarValues,
|
||||
useResetConversationVar,
|
||||
useResetToLastRunValue,
|
||||
useDeleteAllInspectorVars,
|
||||
useDeleteNodeInspectorVars,
|
||||
useDeleteInspectVar,
|
||||
useEditInspectorVar,
|
||||
} = useFLow({ flowType })
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(flowId)
|
||||
const { mutateAsync: doResetConversationVar } = useResetConversationVar(flowId)
|
||||
const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(flowId)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl!)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(flowId)
|
||||
|
||||
const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(flowId)
|
||||
const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(flowId)
|
||||
@ -87,10 +95,14 @@ export const useInspectVarsCrudCommon = ({
|
||||
return !!getNodeInspectVars(nodeId)
|
||||
}, [getNodeInspectVars])
|
||||
|
||||
const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => {
|
||||
const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => {
|
||||
const {
|
||||
appId,
|
||||
setNodeInspectVars,
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
const nodeId = selector[0]
|
||||
const isSystemVar = nodeId === 'sys'
|
||||
@ -103,9 +115,27 @@ export const useInspectVarsCrudCommon = ({
|
||||
invalidateConversationVarValues()
|
||||
return
|
||||
}
|
||||
const vars = await fetchNodeInspectVars(appId, nodeId)
|
||||
setNodeInspectVars(nodeId, vars)
|
||||
}, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues])
|
||||
const { getNodes } = store.getState()
|
||||
const nodeArr = getNodes()
|
||||
const currentNode = nodeArr.find(node => node.id === nodeId)
|
||||
const allPluginInfoList = {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
}
|
||||
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
|
||||
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
|
||||
const varsWithSchemaType = vars.map((varItem) => {
|
||||
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
|
||||
return {
|
||||
...varItem,
|
||||
schemaType,
|
||||
}
|
||||
})
|
||||
setNodeInspectVars(nodeId, varsWithSchemaType)
|
||||
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues])
|
||||
|
||||
// after last run would call this
|
||||
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
|
||||
@ -128,7 +158,7 @@ export const useInspectVarsCrudCommon = ({
|
||||
}
|
||||
else {
|
||||
draft[index].vars = payload
|
||||
// put the node to the topAdd commentMore actions
|
||||
// put the node to the topAdd commentMore actions
|
||||
draft.unshift(draft.splice(index, 1)[0])
|
||||
}
|
||||
}
|
||||
@ -140,14 +170,14 @@ export const useInspectVarsCrudCommon = ({
|
||||
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
|
||||
const { nodesWithInspectVars } = workflowStore.getState()
|
||||
const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId)
|
||||
if(!targetNode || !targetNode.vars)
|
||||
if (!targetNode || !targetNode.vars)
|
||||
return false
|
||||
return targetNode.vars.some(item => item.id === varId)
|
||||
}, [workflowStore])
|
||||
|
||||
const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => {
|
||||
const { deleteInspectVar } = workflowStore.getState()
|
||||
if(hasNodeInspectVar(nodeId, varId)) {
|
||||
if (hasNodeInspectVar(nodeId, varId)) {
|
||||
await doDeleteInspectVar(varId)
|
||||
deleteInspectVar(nodeId, varId)
|
||||
}
|
||||
@ -215,7 +245,7 @@ export const useInspectVarsCrudCommon = ({
|
||||
const isSysVar = nodeId === 'sys'
|
||||
const data = await doResetToLastRunValue(varId)
|
||||
|
||||
if(isSysVar)
|
||||
if (isSysVar)
|
||||
invalidateSysVarValues()
|
||||
else
|
||||
resetToLastRunVar(nodeId, varId, data.value)
|
||||
|
||||
@ -4,12 +4,14 @@ import {
|
||||
useConversationVarValues,
|
||||
useSysVarValues,
|
||||
} from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
const useInspectVarsCrud = () => {
|
||||
const nodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.conversationVarsUrl)
|
||||
const { data: systemVars } = useSysVarValues(configsMap?.systemVarsUrl)
|
||||
const isRagPipeline = configsMap?.flowType === FlowType.ragPipeline
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { data: systemVars } = useSysVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const hasNodeInspectVars = useHooksStore(s => s.hasNodeInspectVars)
|
||||
const hasSetInspectVar = useHooksStore(s => s.hasSetInspectVar)
|
||||
const fetchInspectVarValue = useHooksStore(s => s.fetchInspectVarValue)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
@ -72,4 +73,52 @@ const useNodesAvailableVarList = (nodes: Node[], {
|
||||
return nodeAvailabilityMap
|
||||
}
|
||||
|
||||
export const useGetNodesAvailableVarList = () => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
const getNodesAvailableVarList = useCallback((nodes: Node[], {
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
passedInAvailableNodes,
|
||||
}: Params = {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const nodeAvailabilityMap: { [key: string ]: { availableVars: NodeOutPutVar[], availableNodes: Node[] } } = {}
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeId = node.id
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
if (node.data.type === BlockEnum.Loop)
|
||||
availableNodes.push(node)
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
} = getNodeInfo(nodeId, nodes)
|
||||
|
||||
const availableVars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
})
|
||||
const result = {
|
||||
node,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
}
|
||||
nodeAvailabilityMap[nodeId] = result
|
||||
})
|
||||
return nodeAvailabilityMap
|
||||
}, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode])
|
||||
return {
|
||||
getNodesAvailableVarList,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNodesAvailableVarList
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
NODES_EXTRA_DATA,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../constants'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
|
||||
export const useNodesInitialData = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => produce(NODES_INITIAL_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].title = t(`workflow.blocks.${key}`)
|
||||
})
|
||||
}), [t])
|
||||
}
|
||||
|
||||
export const useNodesExtraData = () => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
return useMemo(() => produce(NODES_EXTRA_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].about = t(`workflow.blocksAbout.${key}`)
|
||||
draft[key as BlockEnum].availablePrevNodes = draft[key as BlockEnum].getAvailablePrevNodes(isChatMode)
|
||||
draft[key as BlockEnum].availableNextNodes = draft[key as BlockEnum].getAvailableNextNodes(isChatMode)
|
||||
})
|
||||
}), [t, isChatMode])
|
||||
}
|
||||
|
||||
export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean, isInLoop?: boolean) => {
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || !nodesExtraData[nodeType])
|
||||
return []
|
||||
return nodesExtraData[nodeType].availablePrevNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType || !nodesExtraData[nodeType])
|
||||
return []
|
||||
return nodesExtraData[nodeType].availableNextNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
availablePrevBlocks: availablePrevBlocks.filter((nType) => {
|
||||
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
return !(!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
}),
|
||||
availableNextBlocks: availableNextBlocks.filter((nType) => {
|
||||
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
return !(!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
}),
|
||||
}
|
||||
}, [isInIteration, availablePrevBlocks, availableNextBlocks, isInLoop])
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
65
web/app/components/workflow/hooks/use-nodes-meta-data.ts
Normal file
65
web/app/components/workflow/hooks/use-nodes-meta-data.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
export const useNodesMetaData = () => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData?.nodes || [],
|
||||
nodesMap: availableNodesMetaData?.nodesMap || {},
|
||||
} as AvailableNodesMetaData
|
||||
}, [availableNodesMetaData])
|
||||
}
|
||||
|
||||
export const useNodeMetaData = (node: Node) => {
|
||||
const language = useGetLanguage()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const availableNodesMetaData = useNodesMetaData()
|
||||
const { data } = node
|
||||
const nodeMetaData = availableNodesMetaData.nodesMap?.[data.type]
|
||||
const author = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author
|
||||
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}
|
||||
return nodeMetaData?.metaData.author
|
||||
}, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList])
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.description[language]
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.description[language]
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}
|
||||
return nodeMetaData?.metaData.description
|
||||
}, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList, language])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...nodeMetaData?.metaData,
|
||||
author,
|
||||
description,
|
||||
}
|
||||
}, [author, nodeMetaData, description])
|
||||
}
|
||||
81
web/app/components/workflow/hooks/use-tool-icon.ts
Normal file
81
web/app/components/workflow/hooks/use-tool-icon.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type {
|
||||
Node,
|
||||
} from '../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
|
||||
export const useToolIcon = (data?: Node['data']) => {
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
// const a = useStore(s => s.data)
|
||||
const toolIcon = useMemo(() => {
|
||||
if (!data)
|
||||
return ''
|
||||
if (data.type === BlockEnum.TriggerPlugin) {
|
||||
const targetTools = triggerPlugins || []
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
// eslint-disable-next-line sonarjs/no-dead-store
|
||||
let targetTools = buildInTools
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools
|
||||
else if (data.provider_type === CollectionType.mcp)
|
||||
targetTools = mcpTools
|
||||
else
|
||||
targetTools = workflowTools
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon
|
||||
}, [data, dataSourceList, buildInTools, customTools, mcpTools, workflowTools, triggerPlugins])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
|
||||
export const useGetToolIcon = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const getToolIcon = useCallback((data: Node['data']) => {
|
||||
const {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
// eslint-disable-next-line sonarjs/no-dead-store
|
||||
let targetTools = buildInTools
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools
|
||||
else
|
||||
targetTools = workflowTools
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon
|
||||
}, [workflowStore])
|
||||
|
||||
return getToolIcon
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
import type { WorkflowHistoryEventMeta } from '../workflow-history-store'
|
||||
|
||||
/**
|
||||
* All supported Events that create a new history state.
|
||||
@ -64,20 +65,21 @@ export const useWorkflowHistory = () => {
|
||||
// Some events may be triggered multiple times in a short period of time.
|
||||
// We debounce the history state update to avoid creating multiple history states
|
||||
// with minimal changes.
|
||||
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent) => {
|
||||
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
|
||||
workflowHistoryStore.setState({
|
||||
workflowHistoryEvent: event,
|
||||
workflowHistoryEventMeta: meta,
|
||||
nodes: store.getState().getNodes(),
|
||||
edges: store.getState().edges,
|
||||
})
|
||||
}, 500))
|
||||
|
||||
const saveStateToHistory = useCallback((event: WorkflowHistoryEvent) => {
|
||||
const saveStateToHistory = useCallback((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
|
||||
switch (event) {
|
||||
case WorkflowHistoryEvent.NoteChange:
|
||||
// Hint: Note change does not trigger when note text changes,
|
||||
// because the note editors have their own history states.
|
||||
saveStateToHistoryRef.current(event)
|
||||
saveStateToHistoryRef.current(event, meta)
|
||||
break
|
||||
case WorkflowHistoryEvent.NodeTitleChange:
|
||||
case WorkflowHistoryEvent.NodeDescriptionChange:
|
||||
@ -93,7 +95,7 @@ export const useWorkflowHistory = () => {
|
||||
case WorkflowHistoryEvent.NoteAdd:
|
||||
case WorkflowHistoryEvent.LayoutOrganize:
|
||||
case WorkflowHistoryEvent.NoteDelete:
|
||||
saveStateToHistoryRef.current(event)
|
||||
saveStateToHistoryRef.current(event, meta)
|
||||
break
|
||||
default:
|
||||
// We do not create a history state for every event.
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
CUSTOM_NODE, DSL_EXPORT_CHECK,
|
||||
CUSTOM_NODE,
|
||||
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||
NODE_LAYOUT_VERTICAL_PADDING,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
@ -30,10 +28,6 @@ import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-withou
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { exportAppConfig } from '@/service/apps'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
export const useWorkflowInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -340,71 +334,6 @@ export const useWorkflowUpdate = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useDSL = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
const handleExportDSL = useCallback(async (include = false) => {
|
||||
if (!appDetail)
|
||||
return
|
||||
|
||||
if (exporting)
|
||||
return
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
await doSyncWorkflowDraft()
|
||||
const { data } = await exportAppConfig({
|
||||
appID: appDetail.id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
a.href = URL.createObjectURL(file)
|
||||
a.download = `${appDetail.name}.yml`
|
||||
a.click()
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}, [appDetail, notify, t, doSyncWorkflowDraft, exporting])
|
||||
|
||||
const exportCheck = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
|
||||
if (list.length === 0) {
|
||||
handleExportDSL()
|
||||
return
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
type: DSL_EXPORT_CHECK,
|
||||
payload: {
|
||||
data: list,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}, [appDetail, eventEmitter, handleExportDSL, notify, t])
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowCanvasMaximize = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export const useWorkflowAgentLog = () => {
|
||||
|
||||
if (current.execution_metadata) {
|
||||
if (current.execution_metadata.agent_log) {
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.id === data.id)
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||
if (currentLogIndex > -1) {
|
||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||
...current.execution_metadata.agent_log[currentLogIndex],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '../store'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { getVarType, toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type {
|
||||
Node,
|
||||
@ -10,13 +10,20 @@ import type {
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import type { Type } from '../nodes/llm/types'
|
||||
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
|
||||
export const useWorkflowVariables = () => {
|
||||
const { t } = useTranslation()
|
||||
const environmentVariables = useStore(s => s.environmentVariables)
|
||||
const conversationVariables = useStore(s => s.conversationVariables)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const getNodeAvailableVars = useCallback(({
|
||||
parentNode,
|
||||
beforeNodes,
|
||||
@ -32,6 +39,11 @@ export const useWorkflowVariables = () => {
|
||||
hideEnv?: boolean
|
||||
hideChatVar?: boolean
|
||||
}): NodeOutPutVar[] => {
|
||||
const {
|
||||
conversationVariables,
|
||||
environmentVariables,
|
||||
ragPipelineVariables,
|
||||
} = workflowStore.getState()
|
||||
return toNodeAvailableVars({
|
||||
parentNode,
|
||||
t,
|
||||
@ -39,9 +51,18 @@ export const useWorkflowVariables = () => {
|
||||
isChatMode,
|
||||
environmentVariables: hideEnv ? [] : environmentVariables,
|
||||
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
|
||||
ragVariables: ragPipelineVariables,
|
||||
filterVar,
|
||||
allPluginInfoList: {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
},
|
||||
schemaTypeDefinitions,
|
||||
})
|
||||
}, [conversationVariables, environmentVariables, t])
|
||||
}, [t, workflowStore, schemaTypeDefinitions, buildInTools])
|
||||
|
||||
const getCurrentVariableType = useCallback(({
|
||||
parentNode,
|
||||
@ -51,6 +72,7 @@ export const useWorkflowVariables = () => {
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
preferSchemaType,
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
@ -59,7 +81,18 @@ export const useWorkflowVariables = () => {
|
||||
availableNodes: any[]
|
||||
isChatMode: boolean
|
||||
isConstant?: boolean
|
||||
preferSchemaType?: boolean
|
||||
}) => {
|
||||
const {
|
||||
conversationVariables,
|
||||
environmentVariables,
|
||||
ragPipelineVariables,
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
return getVarType({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
@ -70,8 +103,18 @@ export const useWorkflowVariables = () => {
|
||||
isConstant,
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
ragVariables: ragPipelineVariables,
|
||||
allPluginInfoList: {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
},
|
||||
schemaTypeDefinitions,
|
||||
preferSchemaType,
|
||||
})
|
||||
}, [conversationVariables, environmentVariables])
|
||||
}, [workflowStore, getVarType, schemaTypeDefinitions])
|
||||
|
||||
return {
|
||||
getNodeAvailableVars,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -13,21 +12,19 @@ import type {
|
||||
Connection,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getParallelInfo,
|
||||
} from '../utils'
|
||||
import { getParallelInfo } from '../utils'
|
||||
import {
|
||||
getWorkflowEntryNode,
|
||||
isWorkflowEntryNode,
|
||||
@ -36,9 +33,11 @@ import {
|
||||
PARALLEL_DEPTH_LIMIT,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
@ -46,13 +45,11 @@ import {
|
||||
fetchAllMCPTools,
|
||||
fetchAllWorkflowTools,
|
||||
} from '@/service/tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { MAX_PARALLEL_LIMIT } from '@/config'
|
||||
import { useNodesMetaData } from '.'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
@ -64,7 +61,17 @@ export const useWorkflow = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { getAvailableBlocks } = useAvailableBlocks()
|
||||
const { nodesMap } = useNodesMetaData()
|
||||
|
||||
const getNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
return currentNode
|
||||
}, [store])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
@ -72,13 +79,18 @@ export const useWorkflow = () => {
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
let startNode = getWorkflowEntryNode(nodes)
|
||||
// let startNode = getWorkflowEntryNode(nodes)
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (currentNode?.parentId)
|
||||
startNode = nodes.find(node => node.parentId === currentNode.parentId && (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE))
|
||||
let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
|
||||
if (!startNode)
|
||||
if (currentNode?.parentId) {
|
||||
const startNode = nodes.find(node => node.parentId === currentNode.parentId && (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE))
|
||||
if (startNode)
|
||||
startNodes = [startNode]
|
||||
}
|
||||
|
||||
if (!startNodes.length)
|
||||
return []
|
||||
|
||||
const list: Node[] = []
|
||||
@ -97,8 +109,10 @@ export const useWorkflow = () => {
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
preOrder(startNode, (node) => {
|
||||
list.push(node)
|
||||
startNodes.forEach((startNode) => {
|
||||
preOrder(startNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
})
|
||||
|
||||
const incomers = getIncomers({ id: nodeId } as Node, nodes, edges)
|
||||
@ -108,7 +122,7 @@ export const useWorkflow = () => {
|
||||
return uniqBy(list, 'id').filter((item: Node) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}, [store])
|
||||
}, [store, nodesMap])
|
||||
|
||||
const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const {
|
||||
@ -322,28 +336,102 @@ export const useWorkflow = () => {
|
||||
return true
|
||||
}, [store, workflowStore, t])
|
||||
|
||||
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => {
|
||||
const getRootNodesById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
parallelList,
|
||||
hasAbnormalEdges,
|
||||
} = getParallelInfo(nodes, edges, parentNodeId)
|
||||
const { workflowConfig } = workflowStore.getState()
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (hasAbnormalEdges)
|
||||
return false
|
||||
const rootNodes: Node[] = []
|
||||
|
||||
for (let i = 0; i < parallelList.length; i++) {
|
||||
const parallel = parallelList[i]
|
||||
if (!currentNode)
|
||||
return rootNodes
|
||||
|
||||
if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
|
||||
if (currentNode.parentId) {
|
||||
const parentNode = nodes.find(node => node.id === currentNode.parentId)
|
||||
if (parentNode) {
|
||||
const parentList = getRootNodesById(parentNode.id)
|
||||
|
||||
rootNodes.push(...parentList)
|
||||
}
|
||||
}
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const incomers = getIncomers(root, nodes, edges)
|
||||
|
||||
if (incomers.length) {
|
||||
incomers.forEach((node) => {
|
||||
traverse(node, callback)
|
||||
})
|
||||
}
|
||||
else {
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
rootNodes.push(node)
|
||||
})
|
||||
|
||||
const length = rootNodes.length
|
||||
if (length)
|
||||
return uniqBy(rootNodes, 'id')
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
|
||||
const getStartNodes = useCallback((nodes: Node[], currentNode?: Node) => {
|
||||
const { id, parentId } = currentNode || {}
|
||||
let startNodes: Node[] = []
|
||||
|
||||
if (parentId) {
|
||||
const parentNode = nodes.find(node => node.id === parentId)
|
||||
if (!parentNode)
|
||||
throw new Error('Parent node not found')
|
||||
|
||||
const startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id)
|
||||
if (startNode)
|
||||
startNodes = [startNode]
|
||||
}
|
||||
else {
|
||||
startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
}
|
||||
|
||||
if (!startNodes.length)
|
||||
startNodes = getRootNodesById(id || '')
|
||||
|
||||
return startNodes
|
||||
}, [nodesMap, getRootNodesById])
|
||||
|
||||
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], targetNode?: Node) => {
|
||||
const startNodes = getStartNodes(nodes, targetNode)
|
||||
|
||||
for (let i = 0; i < startNodes.length; i++) {
|
||||
const {
|
||||
parallelList,
|
||||
hasAbnormalEdges,
|
||||
} = getParallelInfo(startNodes[i], nodes, edges)
|
||||
const { workflowConfig } = workflowStore.getState()
|
||||
|
||||
if (hasAbnormalEdges)
|
||||
return false
|
||||
|
||||
for (let i = 0; i < parallelList.length; i++) {
|
||||
const parallel = parallelList[i]
|
||||
|
||||
if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [t, workflowStore])
|
||||
}, [t, workflowStore, getStartNodes])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
@ -364,8 +452,8 @@ export const useWorkflow = () => {
|
||||
return false
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
|
||||
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
|
||||
const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks
|
||||
const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks
|
||||
|
||||
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
|
||||
return false
|
||||
@ -389,7 +477,7 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, nodesExtraData, checkParallelLimit])
|
||||
}, [store, checkParallelLimit, getAvailableBlocks])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
@ -399,6 +487,7 @@ export const useWorkflow = () => {
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
getNodeById,
|
||||
getTreeLeafNodes,
|
||||
getBeforeNodesInSameBranch,
|
||||
getBeforeNodesInSameBranchIncludeParent,
|
||||
@ -410,11 +499,13 @@ export const useWorkflow = () => {
|
||||
checkParallelLimit,
|
||||
checkNestedParallelLimit,
|
||||
isValidConnection,
|
||||
isFromStartNode,
|
||||
getNode,
|
||||
getBeforeNodeById,
|
||||
getIterationNodeChildren,
|
||||
getLoopNodeChildren,
|
||||
getRootNodesById,
|
||||
getStartNodes,
|
||||
isFromStartNode,
|
||||
getNode,
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,6 +567,7 @@ export const useWorkflowReadOnly = () => {
|
||||
getWorkflowReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodesReadOnly = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
@ -498,38 +590,6 @@ export const useNodesReadOnly = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useToolIcon = (data: Node['data']) => {
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
const toolIcon = useMemo(() => {
|
||||
if (!data)
|
||||
return ''
|
||||
|
||||
if (data.type === BlockEnum.TriggerPlugin) {
|
||||
const targetTools = triggerPlugins || []
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
let targetTools = workflowTools
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
targetTools = buildInTools
|
||||
else if (data.provider_type === CollectionType.custom)
|
||||
targetTools = customTools
|
||||
else if (data.provider_type === CollectionType.mcp)
|
||||
targetTools = mcpTools
|
||||
|
||||
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
|
||||
}
|
||||
}, [data, buildInTools, customTools, mcpTools, triggerPlugins, workflowTools])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
|
||||
export const useIsNodeInIteration = (iterationId: string) => {
|
||||
const store = useStoreApi()
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
@ -57,6 +58,8 @@ import CustomLoopStartNode from './nodes/loop-start'
|
||||
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
|
||||
import CustomSimpleNode from './simple-node'
|
||||
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
|
||||
import CustomDataSourceEmptyNode from './nodes/data-source-empty'
|
||||
import { CUSTOM_DATA_SOURCE_EMPTY_NODE } from './nodes/data-source-empty/constants'
|
||||
import Operator from './operator'
|
||||
import { useWorkflowSearch } from './hooks/use-workflow-search'
|
||||
import Control from './operator/control'
|
||||
@ -83,9 +86,13 @@ import {
|
||||
import { WorkflowHistoryProvider } from './workflow-history-store'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import DatasetsDetailProvider from './datasets-detail-store/provider'
|
||||
import { HooksStoreContextProvider } from './hooks-store'
|
||||
import { HooksStoreContextProvider, useHooksStore } from './hooks-store'
|
||||
import type { Shape as HooksStoreShape } from './hooks-store'
|
||||
import dynamic from 'next/dynamic'
|
||||
import useMatchSchemaType from './nodes/_base/components/variable/use-match-schema-type'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { fetchAllInspectVars } from '@/service/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const Confirm = dynamic(() => import('@/app/components/base/confirm'), {
|
||||
ssr: false,
|
||||
@ -97,6 +104,7 @@ const nodeTypes = {
|
||||
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
||||
[CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode,
|
||||
}
|
||||
const edgeTypes = {
|
||||
[CUSTOM_EDGE]: CustomEdge,
|
||||
@ -290,10 +298,42 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
return setupScrollToNodeListener(nodes, reactflow)
|
||||
}, [nodes, reactflow])
|
||||
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const [isLoadedVars, setIsLoadedVars] = useState(false)
|
||||
const [vars, setVars] = useState<VarInInspect[]>([])
|
||||
useEffect(() => {
|
||||
fetchInspectVars()
|
||||
}, [])
|
||||
(async () => {
|
||||
if (!configsMap?.flowType || !configsMap?.flowId)
|
||||
return
|
||||
const data = await fetchAllInspectVars(configsMap.flowType, configsMap.flowId)
|
||||
setVars(data)
|
||||
setIsLoadedVars(true)
|
||||
})()
|
||||
}, [configsMap?.flowType, configsMap?.flowId])
|
||||
useEffect(() => {
|
||||
if (schemaTypeDefinitions && isLoadedVars) {
|
||||
fetchInspectVars({
|
||||
passInVars: true,
|
||||
vars,
|
||||
passedInAllPluginInfoList: {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
},
|
||||
passedInSchemaTypeDefinitions: schemaTypeDefinitions,
|
||||
})
|
||||
}
|
||||
}, [schemaTypeDefinitions, fetchInspectVars, isLoadedVars, vars, customTools, buildInTools, workflowTools, mcpTools, dataSourceList])
|
||||
|
||||
const store = useStoreApi()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@ -307,17 +347,17 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
return (
|
||||
<div
|
||||
id='workflow-container'
|
||||
className={`
|
||||
relative h-full w-full min-w-[960px]
|
||||
${workflowReadOnly && 'workflow-panel-animation'}
|
||||
${nodeAnimation && 'workflow-node-animation'}
|
||||
`}
|
||||
className={cn(
|
||||
'relative h-full w-full min-w-[960px]',
|
||||
workflowReadOnly && 'workflow-panel-animation',
|
||||
nodeAnimation && 'workflow-node-animation',
|
||||
)}
|
||||
ref={workflowContainerRef}
|
||||
>
|
||||
<SyncingDataModal />
|
||||
<CandidateNode />
|
||||
<div
|
||||
className='absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2'
|
||||
className='pointer-events-none absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2'
|
||||
style={{ height: controlHeight }}
|
||||
>
|
||||
<Control />
|
||||
|
||||
@ -17,7 +17,6 @@ import Textarea from '@/app/components/base/textarea'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
@ -26,6 +25,7 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import BoolInput from './bool-input'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
type Props = {
|
||||
payload: InputVar
|
||||
@ -46,7 +46,8 @@ const FormItem: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const fileSettings = useHooksStore(s => s.configsMap?.fileSettings)
|
||||
|
||||
const handleArrayItemChange = useCallback((index: number) => {
|
||||
return (newValue: any) => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
|
||||
@ -39,6 +39,7 @@ type Props = {
|
||||
tip?: React.JSX.Element
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
@ -57,6 +58,7 @@ const Base: FC<Props> = ({
|
||||
showFileList,
|
||||
showCodeGenerator = false,
|
||||
tip,
|
||||
footer,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
@ -128,6 +130,7 @@ const Base: FC<Props> = ({
|
||||
{showFileList && fileList.length > 0 && (
|
||||
<FileListInLog fileList={fileList} />
|
||||
)}
|
||||
{footer}
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
|
||||
@ -39,6 +39,7 @@ export type Props = {
|
||||
showCodeGenerator?: boolean
|
||||
className?: string
|
||||
tip?: React.JSX.Element
|
||||
footer?: React.ReactNode
|
||||
}
|
||||
|
||||
export const languageMap = {
|
||||
@ -67,6 +68,7 @@ const CodeEditor: FC<Props> = ({
|
||||
showCodeGenerator = false,
|
||||
className,
|
||||
tip,
|
||||
footer,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
@ -191,6 +193,7 @@ const CodeEditor: FC<Props> = ({
|
||||
showFileList={showFileList}
|
||||
showCodeGenerator={showCodeGenerator}
|
||||
tip={tip}
|
||||
footer={footer}
|
||||
>
|
||||
{main}
|
||||
</Base>
|
||||
|
||||
@ -17,7 +17,7 @@ const ErrorHandleTip = ({
|
||||
|
||||
if (type === ErrorHandleTypeEnum.defaultValue)
|
||||
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
|
||||
}, [])
|
||||
}, [t, type])
|
||||
|
||||
if (!type)
|
||||
return null
|
||||
|
||||
@ -47,7 +47,7 @@ const FileTypeItem: FC<Props> = ({
|
||||
? (
|
||||
<div>
|
||||
<div className='flex items-center border-b border-divider-subtle p-3 pb-2'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<FileTypeIcon className='shrink-0' type={type} size='lg' />
|
||||
<div className='system-sm-medium mx-2 grow text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
@ -62,7 +62,7 @@ const FileTypeItem: FC<Props> = ({
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<FileTypeIcon className='shrink-0' type={type} size='lg' />
|
||||
<div className='mx-2 grow'>
|
||||
<div className='system-sm-medium text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<div className='system-2xs-regular-uppercase mt-1 text-text-tertiary'>{type !== SupportUploadFileTypes.custom ? FILE_EXTS[type].join(', ') : t('appDebug.variableConfig.file.custom.description')}</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { type BaseResource, type BaseResourceProvider, type ResourceVarInputs, VarKindType } from '../types'
|
||||
import { type ResourceVarInputs, VarKindType } from '../types'
|
||||
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -9,12 +9,13 @@ import { VarType } from '@/app/components/workflow/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import MixedVariableTextInput from './mixed-variable-text-input'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import FormInputBoolean from './form-input-boolean'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
@ -32,8 +33,10 @@ type Props = {
|
||||
value: ResourceVarInputs
|
||||
onChange: (value: any) => void
|
||||
inPanel?: boolean
|
||||
currentResource?: BaseResource
|
||||
currentProvider?: BaseResourceProvider
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
providerType?: string
|
||||
}
|
||||
@ -45,8 +48,10 @@ const FormInputItem: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentResource,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
extraParams,
|
||||
providerType,
|
||||
}) => {
|
||||
@ -75,7 +80,7 @@ const FormInputItem: FC<Props> = ({
|
||||
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
|
||||
@ -96,14 +101,14 @@ const FormInputItem: FC<Props> = ({
|
||||
return VarType.arrayFile
|
||||
else if (type === FormTypeEnum.file)
|
||||
return VarType.file
|
||||
// else if (isSelect)
|
||||
// return VarType.select
|
||||
else if (isSelect)
|
||||
return VarType.string
|
||||
// else if (isAppSelector)
|
||||
// return VarType.appSelector
|
||||
// else if (isModelSelector)
|
||||
// return VarType.modelSelector
|
||||
// else if (isBoolean)
|
||||
// return VarType.boolean
|
||||
else if (isBoolean)
|
||||
return VarType.boolean
|
||||
else if (isObject)
|
||||
return VarType.object
|
||||
else if (isArray)
|
||||
@ -141,7 +146,7 @@ const FormInputItem: FC<Props> = ({
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '',
|
||||
currentProvider?.name || '',
|
||||
currentResource?.name || '',
|
||||
currentTool?.name || '',
|
||||
variable || '',
|
||||
providerType,
|
||||
extraParams,
|
||||
@ -151,10 +156,11 @@ const FormInputItem: FC<Props> = ({
|
||||
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
|
||||
plugin_id: currentProvider?.plugin_id || '',
|
||||
provider: currentProvider?.name || '',
|
||||
action: currentResource?.name || '',
|
||||
action: currentTool?.name || '',
|
||||
parameter: variable || '',
|
||||
extra: extraParams,
|
||||
}, isDynamicSelect && providerType === 'trigger' && !!currentResource && !!currentProvider)
|
||||
credential_id: currentProvider?.credential_id || '',
|
||||
}, isDynamicSelect && providerType === 'trigger' && !!currentTool && !!currentProvider)
|
||||
|
||||
// Computed values for dynamic options (unified for triggers and tools)
|
||||
const dynamicOptions = providerType === 'trigger' ? triggerDynamicOptions?.options || [] : toolsOptions
|
||||
@ -163,7 +169,7 @@ const FormInputItem: FC<Props> = ({
|
||||
// Fetch dynamic options for tools only (triggers use hook directly)
|
||||
useEffect(() => {
|
||||
const fetchToolOptions = async () => {
|
||||
if (isDynamicSelect && currentResource && currentProvider && providerType === 'tool') {
|
||||
if (isDynamicSelect && currentTool && currentProvider && providerType === 'tool') {
|
||||
setIsLoadingToolsOptions(true)
|
||||
try {
|
||||
const data = await fetchDynamicOptions()
|
||||
@ -182,7 +188,7 @@ const FormInputItem: FC<Props> = ({
|
||||
fetchToolOptions()
|
||||
}, [
|
||||
isDynamicSelect,
|
||||
currentResource?.name,
|
||||
currentTool?.name,
|
||||
currentProvider?.name,
|
||||
variable,
|
||||
extraParams,
|
||||
@ -266,7 +272,7 @@ const FormInputItem: FC<Props> = ({
|
||||
return (
|
||||
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
|
||||
{showTypeSwitch && (
|
||||
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange}/>
|
||||
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange} />
|
||||
)}
|
||||
{isString && (
|
||||
<MixedVariableTextInput
|
||||
@ -275,6 +281,8 @@ const FormInputItem: FC<Props> = ({
|
||||
onChange={handleValueChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
/>
|
||||
)}
|
||||
{isNumber && isConstant && (
|
||||
@ -286,13 +294,13 @@ const FormInputItem: FC<Props> = ({
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isBoolean && (
|
||||
{isBoolean && isConstant && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
)}
|
||||
{isSelect && !isMultipleSelect && (
|
||||
{isSelect && isConstant && !isMultipleSelect && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='h-8 grow'
|
||||
disabled={readOnly}
|
||||
@ -319,7 +327,7 @@ const FormInputItem: FC<Props> = ({
|
||||
) : undefined}
|
||||
/>
|
||||
)}
|
||||
{isSelect && isMultipleSelect && (
|
||||
{isSelect && isConstant && isMultipleSelect && (
|
||||
<Listbox
|
||||
multiple
|
||||
value={varInput?.value || []}
|
||||
@ -516,8 +524,9 @@ const FormInputItem: FC<Props> = ({
|
||||
filterVar={getFilterVar()}
|
||||
schema={schema}
|
||||
valueTypePlaceHolder={targetVarType()}
|
||||
currentResource={currentResource}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
isFilterFileVar={isBoolean}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
const Add = () => {
|
||||
return (
|
||||
<ActionButton>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Add
|
||||
@ -0,0 +1,24 @@
|
||||
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import Add from './add'
|
||||
|
||||
const InputField = () => {
|
||||
return (
|
||||
<BoxGroupField
|
||||
fieldProps={{
|
||||
supportCollapse: true,
|
||||
fieldTitleProps: {
|
||||
title: 'input field',
|
||||
operation: <Add />,
|
||||
},
|
||||
}}
|
||||
boxGroupProps={{
|
||||
boxProps: {
|
||||
withBorderBottom: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
input field
|
||||
</BoxGroupField>
|
||||
)
|
||||
}
|
||||
export default InputField
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
type Props = {
|
||||
export type InputNumberWithSliderProps = {
|
||||
value: number
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
@ -12,7 +12,7 @@ type Props = {
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputNumberWithSlider: FC<Props> = ({
|
||||
const InputNumberWithSlider: FC<InputNumberWithSliderProps> = ({
|
||||
value,
|
||||
defaultValue = 0,
|
||||
min,
|
||||
|
||||
@ -13,6 +13,7 @@ import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type Props = {
|
||||
instanceId?: string
|
||||
@ -55,6 +56,9 @@ const Editor: FC<Props> = ({
|
||||
onFocusChange?.(isFocus)
|
||||
}, [isFocus])
|
||||
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'relative')}>
|
||||
<>
|
||||
@ -102,6 +106,8 @@ const Editor: FC<Props> = ({
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!readOnly}
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiAlignLeft, RiBracesLine, RiCheckboxLine, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
|
||||
import {
|
||||
RiAlignLeft,
|
||||
RiBracesLine,
|
||||
RiCheckboxLine,
|
||||
RiCheckboxMultipleLine,
|
||||
RiFileCopy2Line,
|
||||
RiFileList2Line,
|
||||
RiHashtag,
|
||||
RiTextSnippet,
|
||||
} from '@remixicon/react'
|
||||
import { InputVarType } from '../../../types'
|
||||
|
||||
type Props = {
|
||||
@ -19,6 +28,7 @@ const getIcon = (type: InputVarType) => {
|
||||
[InputVarType.jsonObject]: RiBracesLine,
|
||||
[InputVarType.singleFile]: RiFileList2Line,
|
||||
[InputVarType.multiFiles]: RiFileCopy2Line,
|
||||
[InputVarType.checkbox]: RiCheckboxLine,
|
||||
} as any)[type] || RiTextSnippet
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type {
|
||||
BoxGroupProps,
|
||||
FieldProps,
|
||||
} from '.'
|
||||
import {
|
||||
BoxGroup,
|
||||
Field,
|
||||
} from '.'
|
||||
|
||||
type BoxGroupFieldProps = {
|
||||
children?: ReactNode
|
||||
boxGroupProps?: Omit<BoxGroupProps, 'children'>
|
||||
fieldProps?: Omit<FieldProps, 'children'>
|
||||
}
|
||||
export const BoxGroupField = memo(({
|
||||
children,
|
||||
fieldProps,
|
||||
boxGroupProps,
|
||||
}: BoxGroupFieldProps) => {
|
||||
return (
|
||||
<BoxGroup {...boxGroupProps}>
|
||||
<Field {...fieldProps}>
|
||||
{children}
|
||||
</Field>
|
||||
</BoxGroup>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Group,
|
||||
} from '.'
|
||||
import type {
|
||||
BoxProps,
|
||||
GroupProps,
|
||||
} from '.'
|
||||
|
||||
export type BoxGroupProps = {
|
||||
children?: ReactNode
|
||||
boxProps?: Omit<BoxProps, 'children'>
|
||||
groupProps?: Omit<GroupProps, 'children'>
|
||||
}
|
||||
export const BoxGroup = memo(({
|
||||
children,
|
||||
boxProps,
|
||||
groupProps,
|
||||
}: BoxGroupProps) => {
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<Group {...groupProps}>
|
||||
{children}
|
||||
</Group>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type BoxProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
}
|
||||
export const Box = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
}: BoxProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,72 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type FieldTitleProps = {
|
||||
title?: string
|
||||
operation?: ReactNode
|
||||
subTitle?: string | ReactNode
|
||||
tooltip?: string
|
||||
showArrow?: boolean
|
||||
disabled?: boolean
|
||||
collapsed?: boolean
|
||||
onCollapse?: (collapsed: boolean) => void
|
||||
}
|
||||
export const FieldTitle = memo(({
|
||||
title,
|
||||
operation,
|
||||
subTitle,
|
||||
tooltip,
|
||||
showArrow,
|
||||
disabled,
|
||||
collapsed,
|
||||
onCollapse,
|
||||
}: FieldTitleProps) => {
|
||||
const [collapsedLocal, setCollapsedLocal] = useState(true)
|
||||
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
|
||||
|
||||
return (
|
||||
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
|
||||
<div
|
||||
className='group/collapse flex items-center justify-between py-1'
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setCollapsedLocal(!collapsedMerged)
|
||||
onCollapse?.(!collapsedMerged)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='system-sm-semibold-uppercase flex items-center text-text-secondary'>
|
||||
{title}
|
||||
{
|
||||
showArrow && (
|
||||
<ArrowDownRoundFill
|
||||
className={cn(
|
||||
'h-4 w-4 cursor-pointer text-text-quaternary group-hover/collapse:text-text-secondary',
|
||||
collapsedMerged && 'rotate-[270deg]',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
tooltip && (
|
||||
<Tooltip
|
||||
popupContent={tooltip}
|
||||
triggerClassName='w-4 h-4 ml-1'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{operation}
|
||||
</div>
|
||||
{
|
||||
subTitle
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { FieldTitleProps } from '.'
|
||||
import { FieldTitle } from '.'
|
||||
|
||||
export type FieldProps = {
|
||||
fieldTitleProps?: FieldTitleProps
|
||||
children?: ReactNode
|
||||
disabled?: boolean
|
||||
supportCollapse?: boolean
|
||||
}
|
||||
export const Field = memo(({
|
||||
fieldTitleProps,
|
||||
children,
|
||||
supportCollapse,
|
||||
disabled,
|
||||
}: FieldProps) => {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldTitle
|
||||
{...fieldTitleProps}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
showArrow={supportCollapse}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{supportCollapse && !collapsed && children}
|
||||
{!supportCollapse && children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,29 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type {
|
||||
FieldProps,
|
||||
GroupProps,
|
||||
} from '.'
|
||||
import {
|
||||
Field,
|
||||
Group,
|
||||
} from '.'
|
||||
|
||||
type GroupFieldProps = {
|
||||
children?: ReactNode
|
||||
groupProps?: Omit<GroupProps, 'children'>
|
||||
fieldProps?: Omit<FieldProps, 'children'>
|
||||
}
|
||||
export const GroupField = memo(({
|
||||
children,
|
||||
fieldProps,
|
||||
groupProps,
|
||||
}: GroupFieldProps) => {
|
||||
return (
|
||||
<Group {...groupProps}>
|
||||
<Field {...fieldProps}>
|
||||
{children}
|
||||
</Field>
|
||||
</Group>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { memo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type GroupProps = {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
withBorderBottom?: boolean
|
||||
}
|
||||
export const Group = memo(({
|
||||
className,
|
||||
children,
|
||||
withBorderBottom,
|
||||
}: GroupProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'px-4 py-2',
|
||||
withBorderBottom && 'border-b border-divider-subtle',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,7 @@
|
||||
export * from './box'
|
||||
export * from './group'
|
||||
export * from './box-group'
|
||||
export * from './field-title'
|
||||
export * from './field'
|
||||
export * from './group-field'
|
||||
export * from './box-group-field'
|
||||
@ -38,7 +38,7 @@ const Add = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
const { checkParallelLimit } = useWorkflow()
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
@ -80,7 +80,7 @@ const Add = ({
|
||||
${nodesReadOnly && '!cursor-not-allowed'}
|
||||
`}
|
||||
>
|
||||
<div className='bg-background-default-dimm mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px]'>
|
||||
<div className='mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px] bg-background-default-dimmed'>
|
||||
<RiAddLine className='h-3 w-3' />
|
||||
</div>
|
||||
<div className='flex items-center uppercase'>
|
||||
|
||||
@ -36,7 +36,7 @@ const ChangeItem = ({
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
|
||||
@ -47,7 +47,7 @@ export const NodeTargetHandle = memo(({
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availablePrevBlocks.length
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
@ -129,7 +129,7 @@ export const NodeSourceHandle = memo(({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const isConnectable = !!availableNextBlocks.length
|
||||
const isChatMode = useIsChatMode()
|
||||
const { checkParallelLimit } = useWorkflow()
|
||||
|
||||
@ -30,7 +30,7 @@ const ChangeBlock = ({
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availablePrevBlocks.length && availableNextBlocks.length)
|
||||
|
||||
@ -31,7 +31,6 @@ const PanelOperator = ({
|
||||
crossAxis: 53,
|
||||
},
|
||||
onOpenChange,
|
||||
inNode,
|
||||
showHelpLink = true,
|
||||
}: PanelOperatorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -1,28 +1,21 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import { useNodeHelpLink } from '../../hooks/use-node-help-link'
|
||||
import ChangeBlock from './change-block'
|
||||
import {
|
||||
canRunBySingle,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesExtraData,
|
||||
useNodeMetaData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
type PanelOperatorPopupProps = {
|
||||
id: string
|
||||
@ -37,7 +30,6 @@ const PanelOperatorPopup = ({
|
||||
showHelpLink,
|
||||
}: PanelOperatorPopupProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const edges = useEdges()
|
||||
const {
|
||||
handleNodeDelete,
|
||||
@ -48,41 +40,9 @@ const PanelOperatorPopup = ({
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
|
||||
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}, [data, nodesExtraData, buildInTools, customTools, workflowTools])
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.description[language]
|
||||
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools, workflowTools])
|
||||
|
||||
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop
|
||||
|
||||
const link = useNodeHelpLink(data.type)
|
||||
|
||||
const nodeMetaData = useNodeMetaData({ id, data } as Node)
|
||||
const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
|
||||
return (
|
||||
@ -124,53 +84,65 @@ const PanelOperatorPopup = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.Start && !nodesReadOnly && (
|
||||
!nodesReadOnly && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesCopy(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.copy')}
|
||||
<ShortcutsName keys={['ctrl', 'c']} />
|
||||
</div>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesDuplicate(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.duplicate')}
|
||||
<ShortcutsName keys={['ctrl', 'd']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary
|
||||
hover:bg-state-destructive-hover hover:text-red-500
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
<ShortcutsName keys={['del']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
{
|
||||
!nodeMetaData.isSingleton && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesCopy(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.copy')}
|
||||
<ShortcutsName keys={['ctrl', 'c']} />
|
||||
</div>
|
||||
<div
|
||||
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesDuplicate(id)
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.duplicate')}
|
||||
<ShortcutsName keys={['ctrl', 'd']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodeMetaData.isUndeletable && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary
|
||||
hover:bg-state-destructive-hover hover:text-text-destructive
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
<ShortcutsName keys={['del']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-px bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
showHelpLink && link && (
|
||||
showHelpLink && nodeMetaData.helpLinkUri && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<a
|
||||
href={link}
|
||||
href={nodeMetaData.helpLinkUri}
|
||||
target='_blank'
|
||||
className='flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
>
|
||||
@ -186,9 +158,9 @@ const PanelOperatorPopup = ({
|
||||
<div className='mb-1 flex h-[22px] items-center font-medium'>
|
||||
{t('workflow.panel.about').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-1 leading-[18px] text-text-secondary'>{about}</div>
|
||||
<div className='mb-1 leading-[18px] text-text-secondary'>{nodeMetaData.description}</div>
|
||||
<div className='leading-[18px]'>
|
||||
{t('workflow.panel.createdBy')} {author}
|
||||
{t('workflow.panel.createdBy')} {nodeMetaData.author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,6 +42,7 @@ type Props = {
|
||||
headerClassName?: string
|
||||
instanceId?: string
|
||||
nodeId?: string
|
||||
editorId?: string
|
||||
title: string | React.JSX.Element
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
@ -85,6 +86,7 @@ const Editor: FC<Props> = ({
|
||||
headerClassName,
|
||||
instanceId,
|
||||
nodeId,
|
||||
editorId,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
@ -148,6 +150,8 @@ const Editor: FC<Props> = ({
|
||||
}
|
||||
|
||||
const getVarType = useWorkflowVariableType()
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
return (
|
||||
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
|
||||
@ -163,6 +167,7 @@ const Editor: FC<Props> = ({
|
||||
{isSupportPromptGenerator && (
|
||||
<PromptGeneratorBtn
|
||||
nodeId={nodeId!}
|
||||
editorId={editorId}
|
||||
className='ml-[5px]'
|
||||
onGenerated={onGenerated}
|
||||
modelConfig={modelConfig}
|
||||
@ -261,7 +266,7 @@ const Editor: FC<Props> = ({
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: nodesOutputVars || [],
|
||||
getVarType,
|
||||
getVarType: getVarType as any,
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
@ -278,6 +283,8 @@ const Editor: FC<Props> = ({
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
onBlur={setBlur}
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
VariableLabelInSelect,
|
||||
@ -27,18 +27,19 @@ const VariableTag = ({
|
||||
availableNodes,
|
||||
}: VariableTagProps) => {
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const isRagVar = isRagVariableVar(valueSelector)
|
||||
const node = useMemo(() => {
|
||||
if (isSystemVar(valueSelector)) {
|
||||
const startNode = availableNodes?.find(n => n.data.type === BlockEnum.Start)
|
||||
if (startNode)
|
||||
return startNode
|
||||
}
|
||||
return getNodeInfoById(availableNodes || nodes, valueSelector[0])
|
||||
}, [nodes, valueSelector, availableNodes])
|
||||
return getNodeInfoById(availableNodes || nodes, isRagVar ? valueSelector[1] : valueSelector[0])
|
||||
}, [nodes, valueSelector, availableNodes, isRagVar])
|
||||
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const isValid = Boolean(node) || isEnv || isChatVar
|
||||
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar
|
||||
|
||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||
const isException = isExceptionVariable(variableName, node?.data.type)
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
|
||||
type ManageInputFieldProps = {
|
||||
onManage: () => void
|
||||
}
|
||||
|
||||
const ManageInputField = ({
|
||||
onManage,
|
||||
}: ManageInputFieldProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center border-t border-divider-subtle pt-1'>
|
||||
<div
|
||||
className='flex h-8 grow cursor-pointer items-center px-3'
|
||||
onClick={onManage}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4 text-text-tertiary' />
|
||||
<div
|
||||
className='system-xs-medium truncate text-text-tertiary'
|
||||
title='Create user input field'
|
||||
>
|
||||
{t('pipeline.inputField.create')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-1 h-3 w-[1px] shrink-0 bg-divider-regular'></div>
|
||||
<div
|
||||
className='system-xs-medium flex h-8 shrink-0 cursor-pointer items-center justify-center px-3 text-text-tertiary'
|
||||
onClick={onManage}
|
||||
>
|
||||
{t('pipeline.inputField.manage')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageInputField
|
||||
@ -0,0 +1,162 @@
|
||||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
describe('match the schema type', () => {
|
||||
it('should return true for identical primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'string' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number' }, { type: 'number' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for different primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'number' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should ignore values and only compare types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for structural differences but no types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle nested objects with same structure and types', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', value: 'Alice' },
|
||||
age: { type: 'number', value: 30 },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string', value: '123 Main St' },
|
||||
city: { type: 'string', value: 'Wonderland' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(true)
|
||||
})
|
||||
it('should return false for nested objects with different structures', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
address: { type: 'string' },
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(false)
|
||||
})
|
||||
|
||||
it('file struct should match file type', () => {
|
||||
const fileSchema = {
|
||||
$id: 'https://dify.ai/schemas/v1/file.json',
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
version: '1.0.0',
|
||||
type: 'object',
|
||||
title: 'File Schema',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
const file = {
|
||||
type: 'object',
|
||||
title: 'File',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
expect(matchTheSchemaType(fileSchema, file)).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,42 @@
|
||||
export type AnyObj = Record<string, any> | null
|
||||
|
||||
const isObj = (x: any): x is object => x !== null && typeof x === 'object'
|
||||
|
||||
// only compare type in object
|
||||
function matchTheSchemaType(scheme: AnyObj, target: AnyObj): boolean {
|
||||
const isMatch = (schema: AnyObj, t: AnyObj): boolean => {
|
||||
const oSchema = isObj(schema)
|
||||
const oT = isObj(t)
|
||||
if(!oSchema)
|
||||
return true
|
||||
if (!oT) { // ignore the object without type
|
||||
// deep find oSchema has type
|
||||
for (const key in schema) {
|
||||
if (key === 'type')
|
||||
return false
|
||||
if (isObj((schema as any)[key]) && !isMatch((schema as any)[key], null))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// check current `type`
|
||||
const tx = (schema as any).type
|
||||
const ty = (t as any).type
|
||||
const isTypeValueObj = isObj(tx)
|
||||
|
||||
if(!isTypeValueObj) // caution: type can be object, so that it would not be compare by value
|
||||
if (tx !== ty) return false
|
||||
|
||||
// recurse into all keys
|
||||
const keys = new Set([...Object.keys(schema as object), ...Object.keys(t as object)])
|
||||
for (const k of keys) {
|
||||
if (k === 'type' && !isTypeValueObj) continue // already checked
|
||||
if (!isMatch((schema as any)[k], (t as any)[k])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return isMatch(scheme, target)
|
||||
}
|
||||
|
||||
export default matchTheSchemaType
|
||||
@ -9,7 +9,7 @@ import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
root: { nodeId?: string, nodeName?: string, attrName: string }
|
||||
root: { nodeId?: string, nodeName?: string, attrName: string, attrAlias?: string }
|
||||
payload: StructuredOutput
|
||||
readonly?: boolean
|
||||
onSelect?: (valueSelector: ValueSelector) => void
|
||||
@ -52,8 +52,7 @@ export const PickerPanelMain: FC<Props> = ({
|
||||
)}
|
||||
<div className='system-sm-medium text-text-secondary'>{root.attrName}</div>
|
||||
</div>
|
||||
{/* It must be object */}
|
||||
<div className='system-xs-regular ml-2 shrink-0 text-text-tertiary'>object</div>
|
||||
<div className='system-xs-regular ml-2 truncate text-text-tertiary' title={root.attrAlias || 'object'}>{root.attrAlias || 'object'}</div>
|
||||
</div>
|
||||
{fieldNames.map(name => (
|
||||
<Field
|
||||
|
||||
@ -44,7 +44,7 @@ const Field: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
|
||||
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
|
||||
</div>
|
||||
{payload.description && (
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import { useSchemaTypeDefinitions } from '@/service/use-common'
|
||||
import type { AnyObj } from './match-schema-type'
|
||||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
export const getMatchedSchemaType = (obj: AnyObj, schemaTypeDefinitions?: SchemaTypeDefinition[]): string => {
|
||||
if(!schemaTypeDefinitions || obj === undefined || obj === null) return ''
|
||||
const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema))
|
||||
return matched ? matched.name : ''
|
||||
}
|
||||
|
||||
const useMatchSchemaType = () => {
|
||||
const { data: schemaTypeDefinitions, isLoading } = useSchemaTypeDefinitions()
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schemaTypeDefinitions,
|
||||
}
|
||||
}
|
||||
|
||||
export default useMatchSchemaType
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,14 +10,18 @@ import {
|
||||
RiMoreLine,
|
||||
} from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import {
|
||||
useNodes,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import RemoveButton from '../remove-button'
|
||||
import useAvailableVarList from '../../hooks/use-available-var-list'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
|
||||
import ConstantField from './constant-field'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
@ -34,7 +38,7 @@ import {
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
|
||||
// import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
@ -42,6 +46,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import VarFullPathPanel from './var-full-path-panel'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
|
||||
@ -59,6 +64,7 @@ type Props = {
|
||||
defaultVarKindType?: VarKindType
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
isFilterFileVar?: boolean
|
||||
availableNodes?: Node[]
|
||||
availableVars?: NodeOutPutVar[]
|
||||
isAddBtnTrigger?: boolean
|
||||
@ -72,8 +78,9 @@ type Props = {
|
||||
minWidth?: number
|
||||
popupFor?: 'assigned' | 'toAssigned'
|
||||
zIndex?: number
|
||||
currentResource?: BaseResource
|
||||
currentProvider?: BaseResourceProvider
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
|
||||
@ -90,6 +97,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
defaultVarKindType = VarKindType.constant,
|
||||
onlyLeafNodeVar,
|
||||
filterVar = () => true,
|
||||
isFilterFileVar,
|
||||
availableNodes: passedInAvailableNodes,
|
||||
availableVars: passedInAvailableVars,
|
||||
isAddBtnTrigger,
|
||||
@ -103,16 +111,14 @@ const VarReferencePicker: FC<Props> = ({
|
||||
minWidth,
|
||||
popupFor,
|
||||
zIndex,
|
||||
currentResource,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar,
|
||||
@ -126,12 +132,12 @@ const VarReferencePicker: FC<Props> = ({
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const node = getNodes().find(n => n.id === nodeId)
|
||||
const isInIteration = !!node?.data.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === node.parentId) : null
|
||||
const node = nodes.find(n => n.id === nodeId)
|
||||
const isInIteration = !!(node?.data as any)?.isInIteration
|
||||
const iterationNode = isInIteration ? nodes.find(n => n.id === node?.parentId) : null
|
||||
|
||||
const isInLoop = !!node?.data.isInLoop
|
||||
const loopNode = isInLoop ? getNodes().find(n => n.id === node.parentId) : null
|
||||
const isInLoop = !!(node?.data as any)?.isInLoop
|
||||
const loopNode = isInLoop ? nodes.find(n => n.id === node?.parentId) : null
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState(TRIGGER_DEFAULT_WIDTH)
|
||||
@ -143,7 +149,10 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
||||
|
||||
const outputVars = useMemo(() => (passedInAvailableVars || availableVars), [passedInAvailableVars, availableVars])
|
||||
const outputVars = useMemo(() => {
|
||||
const results = passedInAvailableVars || availableVars
|
||||
return isFilterFileVar ? removeFileVars(results) : results
|
||||
}, [passedInAvailableVars, availableVars, isFilterFileVar])
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
@ -190,7 +199,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
}
|
||||
}, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode])
|
||||
|
||||
const isShowAPart = (value as ValueSelector).length > 2
|
||||
const isShowAPart = (value as ValueSelector).length > 2 && !isRagVariableVar((value as ValueSelector))
|
||||
|
||||
const varName = useMemo(() => {
|
||||
if (!hasValue)
|
||||
@ -275,21 +284,24 @@ const VarReferencePicker: FC<Props> = ({
|
||||
}, [availableNodes, reactflow, store])
|
||||
|
||||
const type = getCurrentVariableType({
|
||||
parentNode: isInIteration ? iterationNode : loopNode,
|
||||
parentNode: (isInIteration ? iterationNode : loopNode) as any,
|
||||
valueSelector: value as ValueSelector,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: !!isConstant,
|
||||
preferSchemaType,
|
||||
})
|
||||
|
||||
const { isEnv, isChatVar, isValidVar, isException } = useMemo(() => {
|
||||
const { isEnv, isChatVar, isRagVar, isValidVar, isException } = useMemo(() => {
|
||||
const isEnv = isENV(value as ValueSelector)
|
||||
const isChatVar = isConversationVar(value as ValueSelector)
|
||||
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
|
||||
const isRagVar = isRagVariableVar(value as ValueSelector)
|
||||
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isRagVar
|
||||
const isException = isExceptionVariable(varName, outputVarNode?.type)
|
||||
return {
|
||||
isEnv,
|
||||
isChatVar,
|
||||
isRagVar,
|
||||
isValidVar,
|
||||
isException,
|
||||
}
|
||||
@ -328,11 +340,11 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '', currentProvider?.name || '', currentResource?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
|
||||
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
|
||||
'tool',
|
||||
)
|
||||
const handleFetchDynamicOptions = async () => {
|
||||
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentResource || !currentProvider)
|
||||
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
|
||||
return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
@ -345,7 +357,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
}
|
||||
useEffect(() => {
|
||||
handleFetchDynamicOptions()
|
||||
}, [currentResource, currentProvider, schema])
|
||||
}, [currentTool, currentProvider, schema])
|
||||
|
||||
const schemaWithDynamicSelect = useMemo(() => {
|
||||
if (schema?.type !== FormTypeEnum.dynamicSelect)
|
||||
@ -382,8 +394,9 @@ const VarReferencePicker: FC<Props> = ({
|
||||
if (isEnv) return 'environment'
|
||||
if (isChatVar) return 'conversation'
|
||||
if (isLoopVar) return 'loop'
|
||||
if (isRagVar) return 'rag'
|
||||
return 'system'
|
||||
}, [isEnv, isChatVar, isLoopVar])
|
||||
}, [isEnv, isChatVar, isLoopVar, isRagVar])
|
||||
|
||||
return (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||
@ -455,7 +468,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && (
|
||||
{isShowNodeName && !isEnv && !isChatVar && !isRagVar && (
|
||||
<div className='flex items-center' onClick={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
e.stopPropagation()
|
||||
@ -553,6 +566,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import ListEmpty from '@/app/components/base/list-empty'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type Props = {
|
||||
@ -14,6 +15,7 @@ type Props = {
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
zIndex?: number
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
vars,
|
||||
@ -22,8 +24,12 @@ const VarReferencePopup: FC<Props> = ({
|
||||
itemWidth,
|
||||
isSupportFileVar = true,
|
||||
zIndex,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const showManageRagInputFields = useMemo(() => !!pipelineId, [pipelineId])
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
const docLink = useDocLink()
|
||||
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
|
||||
return (
|
||||
@ -63,6 +69,9 @@ const VarReferencePopup: FC<Props> = ({
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
zIndex={zIndex}
|
||||
showManageInputField={showManageRagInputFields}
|
||||
onManageInputField={() => setShowInputFieldPanel?.(true)}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
}
|
||||
</div >
|
||||
|
||||
@ -16,11 +16,12 @@ import { checkKeys } from '@/utils/var'
|
||||
import type { StructuredOutput } from '../../../llm/types'
|
||||
import { Type } from '../../../llm/types'
|
||||
import PickerStructurePanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker'
|
||||
import { varTypeToStructType } from './utils'
|
||||
import { isSpecialVar, varTypeToStructType } from './utils'
|
||||
import type { Field } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { noop } from 'lodash-es'
|
||||
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import ManageInputField from './manage-input-field'
|
||||
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
@ -33,6 +34,7 @@ type ObjectChildrenProps = {
|
||||
onHovering?: (value: boolean) => void
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
@ -50,6 +52,7 @@ type ItemProps = {
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
zIndex?: number
|
||||
className?: string
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
|
||||
const objVarTypes = [VarType.object, VarType.file]
|
||||
@ -68,6 +71,7 @@ const Item: FC<ItemProps> = ({
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
zIndex,
|
||||
className,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
|
||||
const isFile = itemData.type === VarType.file && !isStructureOutput
|
||||
@ -75,6 +79,7 @@ const Item: FC<ItemProps> = ({
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
const isRagVariable = itemData.isRagVariable
|
||||
const flatVarIcon = useMemo(() => {
|
||||
if (!isFlat)
|
||||
return null
|
||||
@ -156,7 +161,7 @@ const Item: FC<ItemProps> = ({
|
||||
if (isFlat) {
|
||||
onChange([itemData.variable], itemData)
|
||||
}
|
||||
else if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
|
||||
else if (isSys || isEnv || isChatVar || isRagVariable) { // system variable | environment variable | conversation variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
else {
|
||||
@ -167,8 +172,9 @@ const Item: FC<ItemProps> = ({
|
||||
if (isEnv) return 'environment'
|
||||
if (isChatVar) return 'conversation'
|
||||
if (isLoopVar) return 'loop'
|
||||
if (isRagVariable) return 'rag'
|
||||
return 'system'
|
||||
}, [isEnv, isChatVar, isSys, isLoopVar])
|
||||
}, [isEnv, isChatVar, isSys, isLoopVar, isRagVariable])
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
@ -195,7 +201,7 @@ const Item: FC<ItemProps> = ({
|
||||
/>}
|
||||
{isFlat && flatVarIcon}
|
||||
|
||||
{!isEnv && !isChatVar && (
|
||||
{!isEnv && !isChatVar && !isRagVariable && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
|
||||
)}
|
||||
{isEnv && (
|
||||
@ -204,8 +210,11 @@ const Item: FC<ItemProps> = ({
|
||||
{isChatVar && (
|
||||
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('conversation.', '')}</div>
|
||||
)}
|
||||
{isRagVariable && (
|
||||
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.split('.').slice(-1)[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{itemData.type}</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
|
||||
{
|
||||
(isObj || isStructureOutput) && (
|
||||
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
||||
@ -218,7 +227,7 @@ const Item: FC<ItemProps> = ({
|
||||
}}>
|
||||
{(isStructureOutput || isObj) && (
|
||||
<PickerStructurePanel
|
||||
root={{ nodeId, nodeName: title, attrName: itemData.variable }}
|
||||
root={{ nodeId, nodeName: title, attrName: itemData.variable, attrAlias: itemData.schemaType }}
|
||||
payload={structuredOutput!}
|
||||
onHovering={setIsChildrenHovering}
|
||||
onSelect={(valueSelector) => {
|
||||
@ -240,6 +249,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
|
||||
onHovering,
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const currObjPath = objPath
|
||||
const itemRef = useRef<HTMLDivElement>(null)
|
||||
@ -284,6 +294,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
|
||||
onHovering={setIsChildrenHovering}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
))
|
||||
}
|
||||
@ -303,7 +314,10 @@ type Props = {
|
||||
onBlur?: () => void
|
||||
zIndex?: number
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
autoFocus?: boolean
|
||||
preferSchemaType?: boolean
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
hideSearch,
|
||||
@ -317,7 +331,10 @@ const VarReferenceVars: FC<Props> = ({
|
||||
onBlur,
|
||||
zIndex,
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
autoFocus = true,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
@ -330,7 +347,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}
|
||||
|
||||
const filteredVars = vars.filter((v) => {
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
|
||||
return children.length > 0
|
||||
}).filter((node) => {
|
||||
if (!searchText)
|
||||
@ -341,7 +358,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
})
|
||||
return children.length > 0
|
||||
}).map((node) => {
|
||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
|
||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
|
||||
if (searchText) {
|
||||
const searchTextLower = searchText.toLowerCase()
|
||||
if (!node.title.toLowerCase().includes(searchTextLower))
|
||||
@ -407,6 +424,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
isFlat={item.isFlat}
|
||||
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
|
||||
zIndex={zIndex}
|
||||
preferSchemaType={preferSchemaType}
|
||||
/>
|
||||
))}
|
||||
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
|
||||
@ -420,6 +438,13 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}
|
||||
</div>
|
||||
: <div className='mt-2 pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>}
|
||||
{
|
||||
showManageInputField && (
|
||||
<ManageInputField
|
||||
onManage={onManageInputField || noop}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import { useMemo } from 'react'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
isRagVariableVar,
|
||||
isSystemVar,
|
||||
} from '../utils'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
@ -13,6 +15,9 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
|
||||
if (variableCategory === 'loop')
|
||||
return Loop
|
||||
|
||||
if (variableCategory === 'rag' || isRagVariableVar(variables))
|
||||
return InputField
|
||||
|
||||
if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment')
|
||||
return Env
|
||||
|
||||
@ -41,7 +46,11 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
|
||||
}
|
||||
|
||||
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
|
||||
const variableFullPathName = variables.slice(1).join('.')
|
||||
let variableFullPathName = variables.slice(1).join('.')
|
||||
|
||||
if (isRagVariableVar(variables))
|
||||
variableFullPathName = variables.slice(2).join('.')
|
||||
|
||||
const variablesLength = variables.length
|
||||
const varName = useMemo(() => {
|
||||
const isSystem = isSystemVar(variables)
|
||||
|
||||
@ -2,7 +2,7 @@ import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
import React, {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
@ -35,6 +35,7 @@ import {
|
||||
useAvailableBlocks,
|
||||
useNodeDataUpdate,
|
||||
useNodesInteractions,
|
||||
useNodesMetaData,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
useWorkflowHistory,
|
||||
@ -43,6 +44,7 @@ import {
|
||||
canRunBySingle,
|
||||
hasErrorHandleNode,
|
||||
hasRetryNode,
|
||||
isSupportCustomRunForm,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
@ -55,18 +57,37 @@ import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { NODES_EXTRA_DATA } from '@/app/components/workflow/constants'
|
||||
import { useLogs } from '@/app/components/workflow/run/hooks'
|
||||
import PanelWrap from '../before-run-form/panel-wrap'
|
||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { FlowType } from '@/types/common'
|
||||
import {
|
||||
AuthorizedInDataSourceNode,
|
||||
AuthorizedInNode,
|
||||
PluginAuth,
|
||||
PluginAuthInDataSourceNode,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { canFindTool } from '@/utils'
|
||||
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import NodeAuth from './node-auth-factory'
|
||||
|
||||
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
|
||||
const nodeType = params.payload.type
|
||||
switch (nodeType) {
|
||||
case BlockEnum.DataSource:
|
||||
return <DataSourceBeforeRunForm {...params} />
|
||||
default:
|
||||
return <div>Custom Run Form: {nodeType} not found</div>
|
||||
}
|
||||
}
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
id: Node['id']
|
||||
@ -143,7 +164,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
@ -155,11 +176,11 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
|
||||
const handleTitleBlur = useCallback((title: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange, { nodeId: id })
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
const handleDescriptionChange = useCallback((desc: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange, { nodeId: id })
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
@ -170,11 +191,11 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if(data._singleRunningStatus === NodeRunningStatus.Running) {
|
||||
if (data._singleRunningStatus === NodeRunningStatus.Running) {
|
||||
hasClickRunning.current = true
|
||||
setIsPaused(false)
|
||||
}
|
||||
else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
|
||||
else if (data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
|
||||
setIsPaused(true)
|
||||
hasClickRunning.current = false
|
||||
}
|
||||
@ -191,10 +212,13 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
}, [handleNodeDataUpdate, id, data])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log(`id changed: ${id}, hasClickRunning: ${hasClickRunning.current}`)
|
||||
hasClickRunning.current = false
|
||||
}, [id])
|
||||
const {
|
||||
nodesMap,
|
||||
} = useNodesMetaData()
|
||||
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
@ -202,11 +226,14 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
runInputData,
|
||||
runInputDataRef,
|
||||
runResult,
|
||||
setRunResult,
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
tabType,
|
||||
isRunAfterSingleRun,
|
||||
setIsRunAfterSingleRun,
|
||||
setTabType,
|
||||
handleAfterCustomSingleRun,
|
||||
singleRunParams,
|
||||
nodeInfo,
|
||||
setRunInputData,
|
||||
@ -216,8 +243,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
getFilteredExistVarForms,
|
||||
} = useLastRun<typeof data>({
|
||||
id,
|
||||
flowId: configsMap?.flowId || '',
|
||||
flowType: configsMap?.flowType || FlowType.appFlow,
|
||||
data,
|
||||
defaultRunInputData: NODES_EXTRA_DATA[data.type]?.defaultRunInputData || {},
|
||||
defaultRunInputData: nodesMap?.[data.type]?.defaultRunInputData || {},
|
||||
isPaused,
|
||||
})
|
||||
|
||||
@ -271,6 +300,11 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
return (data.type === BlockEnum.Tool && currCollection?.allow_delete)
|
||||
}, [data.type, currCollection?.allow_delete])
|
||||
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const currentDataSource = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
|
||||
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
|
||||
}, [dataSourceList, data.plugin_id, data.type, data.provider_type])
|
||||
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
@ -279,6 +313,14 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
},
|
||||
})
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const handleJumpToDataSourcePage = useCallback(() => {
|
||||
setShowAccountSettingModal({ payload: 'data-source' })
|
||||
}, [setShowAccountSettingModal])
|
||||
|
||||
const {
|
||||
appendNodeInspectVars,
|
||||
} = useInspectVarsCrud()
|
||||
|
||||
const handleSubscriptionChange = useCallback((subscription_id: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
@ -289,7 +331,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
})
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
if(logParams.showSpecialResultPanel) {
|
||||
if (logParams.showSpecialResultPanel) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
@ -315,6 +357,20 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
}
|
||||
|
||||
if (isShowSingleRun) {
|
||||
const form = getCustomRunForm({
|
||||
nodeId: id,
|
||||
flowId: configsMap?.flowId || '',
|
||||
flowType: configsMap?.flowType || FlowType.appFlow,
|
||||
payload: data,
|
||||
setRunResult,
|
||||
setIsRunAfterSingleRun,
|
||||
isPaused,
|
||||
isRunAfterSingleRun,
|
||||
onSuccess: handleAfterCustomSingleRun,
|
||||
onCancel: hideSingleRun,
|
||||
appendNodeInspectVars,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
@ -326,26 +382,36 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
width: `${nodePanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<BeforeRunForm
|
||||
nodeName={data.title}
|
||||
nodeType={data.type}
|
||||
onHide={hideSingleRun}
|
||||
onRun={handleRunWithParams}
|
||||
{...singleRunParams!}
|
||||
{...passedLogParams}
|
||||
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
|
||||
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
|
||||
/>
|
||||
{isSupportCustomRunForm(data.type) ? (
|
||||
form
|
||||
) : (
|
||||
<BeforeRunForm
|
||||
nodeName={data.title}
|
||||
nodeType={data.type}
|
||||
onHide={hideSingleRun}
|
||||
onRun={handleRunWithParams}
|
||||
{...singleRunParams!}
|
||||
{...passedLogParams}
|
||||
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
|
||||
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative mr-1 h-full',
|
||||
showMessageLogModal && 'absolute z-0 mr-2 w-[400px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
|
||||
)}
|
||||
style={{
|
||||
right: !showMessageLogModal ? '0' : `${otherPanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'>
|
||||
@ -353,7 +419,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-[width] ease-linear', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${nodePanelWidth}px`,
|
||||
}}
|
||||
@ -381,7 +447,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if(isSingleRunning) {
|
||||
if (isSingleRunning) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
@ -434,15 +500,42 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
<NodeAuth
|
||||
<AuthorizedInNode
|
||||
pluginPayload={{
|
||||
provider: currCollection?.name || '',
|
||||
category: AuthCategory.tool,
|
||||
}}
|
||||
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||
credentialId={data.credential_id}
|
||||
/>
|
||||
{/* <NodeAuth
|
||||
data={data}
|
||||
onAuthorizationChange={handleAuthorizationItemClick}
|
||||
onSubscriptionChange={handleSubscriptionChange}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
</PluginAuth>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!currentDataSource && (
|
||||
<PluginAuthInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
isAuthorized={currentDataSource.is_authorized}
|
||||
>
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
<AuthorizedInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
authorizationsNum={3}
|
||||
/>
|
||||
</div>
|
||||
</PluginAuthInDataSourceNode>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowTriggerAuthSelector && (
|
||||
<AuthMethodSelector
|
||||
@ -467,7 +560,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsToolAuth && data.type !== BlockEnum.TriggerPlugin && (
|
||||
!needsToolAuth && !currentDataSource && data.type !== BlockEnum.TriggerPlugin && (
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
|
||||
@ -8,6 +8,8 @@ import NoData from './no-data'
|
||||
import { useLastRun } from '@/service/use-workflow'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
@ -35,6 +37,7 @@ const LastRun: FC<Props> = ({
|
||||
isPaused,
|
||||
...otherResultPanelProps
|
||||
}) => {
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const isOneStepRunSucceed = oneStepRunRunningStatus === NodeRunningStatus.Succeeded
|
||||
const isOneStepRunFailed = oneStepRunRunningStatus === NodeRunningStatus.Failed
|
||||
// hide page and return to page would lost the oneStepRunRunningStatus
|
||||
@ -44,7 +47,7 @@ const LastRun: FC<Props> = ({
|
||||
|
||||
const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!)
|
||||
const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished)
|
||||
const { data: lastRunResult, isFetching, error } = useLastRun(appId, nodeId, canRunLastRun)
|
||||
const { data: lastRunResult, isFetching, error } = useLastRun(configsMap?.flowType || FlowType.appFlow, configsMap?.flowId || '', nodeId, canRunLastRun)
|
||||
const isRunning = useMemo(() => {
|
||||
if(isPaused)
|
||||
return false
|
||||
|
||||
@ -19,6 +19,7 @@ import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use
|
||||
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
|
||||
import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params'
|
||||
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
|
||||
import useKnowledgeBaseSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-base/use-single-run-form-params'
|
||||
|
||||
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
@ -32,6 +33,7 @@ import {
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
|
||||
|
||||
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: useLLMSingleRunFormParams,
|
||||
@ -50,6 +52,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.IfElse]: useIfElseSingleRunFormParams,
|
||||
[BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams,
|
||||
[BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams,
|
||||
[BlockEnum.KnowledgeBase]: useKnowledgeBaseSingleRunFormParams,
|
||||
[BlockEnum.VariableAssigner]: undefined,
|
||||
[BlockEnum.End]: undefined,
|
||||
[BlockEnum.Answer]: undefined,
|
||||
@ -57,6 +60,8 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.IterationStart]: undefined,
|
||||
[BlockEnum.LoopStart]: undefined,
|
||||
[BlockEnum.LoopEnd]: undefined,
|
||||
[BlockEnum.DataSource]: undefined,
|
||||
[BlockEnum.DataSourceEmpty]: undefined,
|
||||
}
|
||||
|
||||
const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => {
|
||||
@ -89,6 +94,9 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.Assigner]: undefined,
|
||||
[BlockEnum.LoopStart]: undefined,
|
||||
[BlockEnum.LoopEnd]: undefined,
|
||||
[BlockEnum.DataSource]: undefined,
|
||||
[BlockEnum.DataSourceEmpty]: undefined,
|
||||
[BlockEnum.KnowledgeBase]: undefined,
|
||||
}
|
||||
|
||||
const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => {
|
||||
@ -111,6 +119,7 @@ const useLastRun = <T>({
|
||||
const isIterationNode = blockType === BlockEnum.Iteration
|
||||
const isLoopNode = blockType === BlockEnum.Loop
|
||||
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
|
||||
const isCustomRunNode = isSupportCustomRunForm(blockType)
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
getData: getDataForCheckMore,
|
||||
@ -119,6 +128,8 @@ const useLastRun = <T>({
|
||||
|
||||
const {
|
||||
id,
|
||||
flowId,
|
||||
flowType,
|
||||
data,
|
||||
} = oneStepRunParams
|
||||
const oneStepRunRes = useOneStepRun({
|
||||
@ -129,7 +140,6 @@ const useLastRun = <T>({
|
||||
})
|
||||
|
||||
const {
|
||||
appId,
|
||||
hideSingleRun,
|
||||
handleRun: doCallRunApi,
|
||||
getInputVars,
|
||||
@ -164,7 +174,7 @@ const useLastRun = <T>({
|
||||
})
|
||||
|
||||
const toSubmitData = useCallback((data: Record<string, any>) => {
|
||||
if(!isIterationNode && !isLoopNode)
|
||||
if (!isIterationNode && !isLoopNode)
|
||||
return data
|
||||
|
||||
const allVarObject = singleRunParams?.allVarObject || {}
|
||||
@ -173,7 +183,7 @@ const useLastRun = <T>({
|
||||
const [varSectorStr, nodeId] = key.split(DELIMITER)
|
||||
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
|
||||
})
|
||||
if(isIterationNode) {
|
||||
if (isIterationNode) {
|
||||
const iteratorInputKey = `${id}.input_selector`
|
||||
formattedData[iteratorInputKey] = data[iteratorInputKey]
|
||||
}
|
||||
@ -193,16 +203,16 @@ const useLastRun = <T>({
|
||||
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
|
||||
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
|
||||
useEffect(() => {
|
||||
if(initShowLastRunTab)
|
||||
if (initShowLastRunTab)
|
||||
setTabType(TabType.lastRun)
|
||||
|
||||
setInitShowLastRunTab(false)
|
||||
}, [initShowLastRunTab])
|
||||
const invalidLastRun = useInvalidLastRun(appId!, id)
|
||||
const invalidLastRun = useInvalidLastRun(flowType, flowId, id)
|
||||
|
||||
const handleRunWithParams = async (data: Record<string, any>) => {
|
||||
const { isValid } = checkValid()
|
||||
if(!isValid)
|
||||
if (!isValid)
|
||||
return
|
||||
setNodeRunning()
|
||||
setIsRunAfterSingleRun(true)
|
||||
@ -226,14 +236,14 @@ const useLastRun = <T>({
|
||||
const values: Record<string, boolean> = {}
|
||||
form.inputs.forEach(({ variable, getVarValueFromDependent }) => {
|
||||
const isGetValueFromDependent = getVarValueFromDependent || !variable.includes('.')
|
||||
if(isGetValueFromDependent && !singleRunParams?.getDependentVar)
|
||||
if (isGetValueFromDependent && !singleRunParams?.getDependentVar)
|
||||
return
|
||||
|
||||
const selector = isGetValueFromDependent ? (singleRunParams?.getDependentVar(variable) || []) : variable.slice(1, -1).split('.')
|
||||
if(!selector || selector.length === 0)
|
||||
if (!selector || selector.length === 0)
|
||||
return
|
||||
const [nodeId, varName] = selector.slice(0, 2)
|
||||
if(!isStartNode && nodeId === id) { // inner vars like loop vars
|
||||
if (!isStartNode && nodeId === id) { // inner vars like loop vars
|
||||
values[variable] = true
|
||||
return
|
||||
}
|
||||
@ -247,7 +257,7 @@ const useLastRun = <T>({
|
||||
}
|
||||
|
||||
const isAllVarsHasValue = (vars?: ValueSelector[]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
if (!vars || vars.length === 0)
|
||||
return true
|
||||
return vars.every((varItem) => {
|
||||
const [nodeId, varName] = varItem.slice(0, 2)
|
||||
@ -257,7 +267,7 @@ const useLastRun = <T>({
|
||||
}
|
||||
|
||||
const isSomeVarsHasValue = (vars?: ValueSelector[]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
if (!vars || vars.length === 0)
|
||||
return true
|
||||
return vars.some((varItem) => {
|
||||
const [nodeId, varName] = varItem.slice(0, 2)
|
||||
@ -284,7 +294,7 @@ const useLastRun = <T>({
|
||||
}
|
||||
|
||||
const checkAggregatorVarsSet = (vars: ValueSelector[][]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
if (!vars || vars.length === 0)
|
||||
return true
|
||||
// in each group, at last one set is ok
|
||||
return vars.every((varItem) => {
|
||||
@ -292,10 +302,20 @@ const useLastRun = <T>({
|
||||
})
|
||||
}
|
||||
|
||||
const handleAfterCustomSingleRun = () => {
|
||||
invalidLastRun()
|
||||
setTabType(TabType.lastRun)
|
||||
hideSingleRun()
|
||||
}
|
||||
|
||||
const handleSingleRun = () => {
|
||||
const { isValid } = checkValid()
|
||||
if(!isValid)
|
||||
if (!isValid)
|
||||
return
|
||||
if (isCustomRunNode) {
|
||||
showSingleRun()
|
||||
return
|
||||
}
|
||||
const vars = singleRunParams?.getDependentVars?.()
|
||||
// no need to input params
|
||||
if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) {
|
||||
@ -315,7 +335,9 @@ const useLastRun = <T>({
|
||||
...oneStepRunRes,
|
||||
tabType,
|
||||
isRunAfterSingleRun,
|
||||
setIsRunAfterSingleRun,
|
||||
setTabType: handleTabClicked,
|
||||
handleAfterCustomSingleRun,
|
||||
singleRunParams,
|
||||
nodeInfo,
|
||||
setRunInputData,
|
||||
|
||||
@ -4,7 +4,11 @@ import {
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type { Node, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, type Node, type ValueSelector, type Var } from '@/app/components/workflow/types'
|
||||
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { inputVarTypeToVarType } from '../../data-source/utils'
|
||||
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
hideEnv?: boolean
|
||||
@ -24,29 +28,57 @@ const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getTreeLeafNodes, getNodeById, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
} = useNodeInfo(nodeId)
|
||||
|
||||
const availableVars = getNodeAvailableVars({
|
||||
const currNode = getNodeById(nodeId)
|
||||
const ragPipelineVariables = useWorkflowStore(s => s.ragPipelineVariables)
|
||||
const isDataSourceNode = currNode?.data?.type === BlockEnum.DataSource
|
||||
const dataSourceRagVars: NodeOutPutVar[] = []
|
||||
if (isDataSourceNode) {
|
||||
const ragVariablesInDataSource = ragPipelineVariables?.filter(ragVariable => ragVariable.belong_to_node_id === nodeId)
|
||||
const filterVars = ragVariablesInDataSource?.filter(v => filterVar({
|
||||
variable: v.variable,
|
||||
type: inputVarTypeToVarType(v.type),
|
||||
nodeId,
|
||||
isRagVariable: true,
|
||||
}, ['rag', nodeId, v.variable]))
|
||||
if (filterVars?.length) {
|
||||
dataSourceRagVars.push({
|
||||
nodeId,
|
||||
title: currNode.data?.title,
|
||||
vars: filterVars.map((v) => {
|
||||
return {
|
||||
variable: `rag.${nodeId}.${v.variable}`,
|
||||
type: inputVarTypeToVarType(v.type),
|
||||
description: v.label,
|
||||
isRagVariable: true,
|
||||
} as Var
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
const availableVars = [...getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
})
|
||||
}), ...dataSourceRagVars]
|
||||
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent: availableNodes,
|
||||
availableNodesWithParent: [
|
||||
...availableNodes,
|
||||
...(isDataSourceNode ? [currNode] : []),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,67 +1,15 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useDocLink, useGetLanguage } from '@/context/i18n'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
const language = useGetLanguage()
|
||||
const docLink = useDocLink()
|
||||
const prefixLink = useMemo(() => {
|
||||
return docLink('/guides/workflow/node/')
|
||||
}, [language])
|
||||
const linkMap = useMemo(() => {
|
||||
if (language === 'zh_Hans') {
|
||||
return {
|
||||
[BlockEnum.Start]: 'start',
|
||||
[BlockEnum.End]: 'end',
|
||||
[BlockEnum.Answer]: 'answer',
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}
|
||||
const availableNodesMetaData = useNodesMetaData()
|
||||
|
||||
return {
|
||||
[BlockEnum.Start]: 'start',
|
||||
[BlockEnum.End]: 'end',
|
||||
[BlockEnum.Answer]: 'answer',
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}, [language]) as Record<string, string>
|
||||
const link = useMemo(() => {
|
||||
const result = availableNodesMetaData?.nodesMap?.[nodeType]?.metaData.helpLinkUri || ''
|
||||
|
||||
const link = linkMap[nodeType]
|
||||
return result
|
||||
}, [availableNodesMetaData, nodeType])
|
||||
|
||||
if (!link)
|
||||
return ''
|
||||
|
||||
return `${prefixLink}${link}`
|
||||
return link
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVar
|
||||
|
||||
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@ -52,6 +51,8 @@ import {
|
||||
} from 'reactflow'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useMatchSchemaType from '../components/variable/use-match-schema-type'
|
||||
// eslint-disable-next-line ts/no-unsafe-function-type
|
||||
const checkValidFns: Record<BlockEnum, Function> = {
|
||||
[BlockEnum.LLM]: checkLLMValid,
|
||||
@ -72,6 +73,8 @@ const checkValidFns: Record<BlockEnum, Function> = {
|
||||
|
||||
export type Params<T> = {
|
||||
id: string
|
||||
flowId: string
|
||||
flowType: FlowType
|
||||
data: CommonNodeType<T>
|
||||
defaultRunInputData: Record<string, any>
|
||||
moreDataForCheckValid?: any
|
||||
@ -108,6 +111,8 @@ const varTypeToInputVarType = (type: VarType, {
|
||||
|
||||
const useOneStepRun = <T>({
|
||||
id,
|
||||
flowId,
|
||||
flowType,
|
||||
data,
|
||||
defaultRunInputData,
|
||||
moreDataForCheckValid,
|
||||
@ -126,9 +131,26 @@ const useOneStepRun = <T>({
|
||||
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
const getVar = (valueSelector: ValueSelector): Var | undefined => {
|
||||
const isSystem = valueSelector[0] === 'sys'
|
||||
const {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
const allPluginInfoList = {
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
}
|
||||
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions)
|
||||
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
|
||||
if (!targetVar)
|
||||
return undefined
|
||||
@ -155,7 +177,6 @@ const useOneStepRun = <T>({
|
||||
|
||||
const checkValid = checkValidFns[data.type]
|
||||
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
|
||||
const runInputDataRef = useRef(runInputData)
|
||||
const handleSetRunInputData = useCallback((data: Record<string, any>) => {
|
||||
@ -166,11 +187,10 @@ const useOneStepRun = <T>({
|
||||
const loopTimes = loopInputKey ? runInputData[loopInputKey]?.length : 0
|
||||
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
setShowSingleRunPanel,
|
||||
} = workflowStore.getState()
|
||||
const invalidLastRun = useInvalidLastRun(appId!, id)
|
||||
const invalidLastRun = useInvalidLastRun(flowType, flowId!, id)
|
||||
const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null)
|
||||
const {
|
||||
appendNodeInspectVars,
|
||||
@ -197,7 +217,7 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
|
||||
// run fail may also update the inspect vars when the node set the error default output.
|
||||
const vars = await fetchNodeInspectVars(appId!, id)
|
||||
const vars = await fetchNodeInspectVars(flowType, flowId!, id)
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
appendNodeInspectVars(id, vars, nodes)
|
||||
@ -207,7 +227,7 @@ const useOneStepRun = <T>({
|
||||
invalidateSysVarValues()
|
||||
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
|
||||
}
|
||||
}, [isRunAfterSingleRun, runningStatus, appId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
|
||||
}, [isRunAfterSingleRun, runningStatus, flowId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
|
||||
|
||||
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
|
||||
const setNodeRunning = () => {
|
||||
@ -306,14 +326,14 @@ const useOneStepRun = <T>({
|
||||
else {
|
||||
postData.inputs = submitData
|
||||
}
|
||||
res = await singleNodeRun(appId!, id, postData) as any
|
||||
res = await singleNodeRun(flowType, flowId!, id, postData) as any
|
||||
}
|
||||
else if (isIteration) {
|
||||
setIterationRunResult([])
|
||||
let _iterationResult: NodeTracing[] = []
|
||||
let _runResult: any = null
|
||||
ssePost(
|
||||
getIterationSingleNodeRunUrl(isChatMode, appId!, id),
|
||||
getIterationSingleNodeRunUrl(flowType, isChatMode, flowId!, id),
|
||||
{ body: { inputs: submitData } },
|
||||
{
|
||||
onWorkflowStarted: noop,
|
||||
@ -416,7 +436,7 @@ const useOneStepRun = <T>({
|
||||
let _loopResult: NodeTracing[] = []
|
||||
let _runResult: any = null
|
||||
ssePost(
|
||||
getLoopSingleNodeRunUrl(isChatMode, appId!, id),
|
||||
getLoopSingleNodeRunUrl(flowType, isChatMode, flowId!, id),
|
||||
{ body: { inputs: submitData } },
|
||||
{
|
||||
onWorkflowStarted: noop,
|
||||
@ -634,7 +654,6 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
|
||||
return {
|
||||
appId,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
showSingleRun,
|
||||
@ -649,6 +668,7 @@ const useOneStepRun = <T>({
|
||||
runInputDataRef,
|
||||
setRunInputData: handleSetRunInputData,
|
||||
runResult,
|
||||
setRunResult: doSetRunResult,
|
||||
iterationRunResult,
|
||||
loopRunResult,
|
||||
setNodeRunning,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user