@@ -91,10 +81,10 @@ const HeadersInput = ({
{t('tools.mcp.modal.headerKey')}
{t('tools.mcp.modal.headerValue')}
- {headerItems.map((item, index) => (
-
(
+
- {!readonly && headerItems.length > 1 && (
+ {!readonly && !!headersItems.length && (
handleRemoveItem(index)}
className='mr-2'
diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx
index 1d888c57e8..987a517ef5 100644
--- a/web/app/components/tools/mcp/modal.tsx
+++ b/web/app/components/tools/mcp/modal.tsx
@@ -1,6 +1,7 @@
'use client'
-import React, { useRef, useState } from 'react'
+import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { v4 as uuid } from 'uuid'
import { getDomain } from 'tldts'
import { RiCloseLine, RiEditLine } from '@remixicon/react'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
@@ -11,6 +12,7 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import HeadersInput from './headers-input'
+import type { HeaderItem } from './headers-input'
import type { AppIconType } from '@/types/app'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { noop } from 'lodash-es'
@@ -19,6 +21,9 @@ import { uploadRemoteFileInfo } from '@/service/common'
import cn from '@/utils/classnames'
import { useHover } from 'ahooks'
import { shouldUseMcpIconForAppIcon } from '@/utils/mcp'
+import TabSlider from '@/app/components/base/tab-slider'
+import { MCPAuthMethod } from '@/app/components/tools/types'
+import Switch from '@/app/components/base/switch'
export type DuplicateAppModalProps = {
data?: ToolWithProvider
@@ -30,9 +35,17 @@ export type DuplicateAppModalProps = {
icon: string
icon_background?: string | null
server_identifier: string
- timeout: number
- sse_read_timeout: number
headers?: Record
+ is_dynamic_registration?: boolean
+ authentication?: {
+ client_id?: string
+ client_secret?: string
+ grant_type?: string
+ }
+ configuration: {
+ timeout: number
+ sse_read_timeout: number
+ }
}) => void
onHide: () => void
}
@@ -63,6 +76,20 @@ const MCPModal = ({
const { t } = useTranslation()
const isCreate = !data
+ const authMethods = [
+ {
+ text: t('tools.mcp.modal.authentication'),
+ value: MCPAuthMethod.authentication,
+ },
+ {
+ text: t('tools.mcp.modal.headers'),
+ value: MCPAuthMethod.headers,
+ },
+ {
+ text: t('tools.mcp.modal.configurations'),
+ value: MCPAuthMethod.configurations,
+ },
+ ]
const originalServerUrl = data?.server_url
const originalServerID = data?.server_identifier
const [url, setUrl] = React.useState(data?.server_url || '')
@@ -72,12 +99,16 @@ const MCPModal = ({
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
const [timeout, setMcpTimeout] = React.useState(data?.timeout || 30)
const [sseReadTimeout, setSseReadTimeout] = React.useState(data?.sse_read_timeout || 300)
- const [headers, setHeaders] = React.useState>(
- data?.masked_headers || {},
+ const [headers, setHeaders] = React.useState(
+ Object.entries(data?.masked_headers || {}).map(([key, value]) => ({ id: uuid(), key, value })),
)
const [isFetchingIcon, setIsFetchingIcon] = useState(false)
const appIconRef = useRef(null)
const isHovering = useHover(appIconRef)
+ const [authMethod, setAuthMethod] = useState(MCPAuthMethod.authentication)
+ const [isDynamicRegistration, setIsDynamicRegistration] = useState(isCreate ? true : data?.is_dynamic_registration)
+ const [clientID, setClientID] = useState(data?.authentication?.client_id || '')
+ const [credentials, setCredentials] = useState(data?.authentication?.client_secret || '')
// Update states when data changes (for edit mode)
React.useEffect(() => {
@@ -87,8 +118,11 @@ const MCPModal = ({
setServerIdentifier(data.server_identifier || '')
setMcpTimeout(data.timeout || 30)
setSseReadTimeout(data.sse_read_timeout || 300)
- setHeaders(data.masked_headers || {})
+ setHeaders(Object.entries(data.masked_headers || {}).map(([key, value]) => ({ id: uuid(), key, value })))
setAppIcon(getIcon(data))
+ setIsDynamicRegistration(data.is_dynamic_registration)
+ setClientID(data.authentication?.client_id || '')
+ setCredentials(data.authentication?.client_secret || '')
}
else {
// Reset for create mode
@@ -97,8 +131,11 @@ const MCPModal = ({
setServerIdentifier('')
setMcpTimeout(30)
setSseReadTimeout(300)
- setHeaders({})
+ setHeaders([])
setAppIcon(DEFAULT_ICON as AppIconSelection)
+ setIsDynamicRegistration(true)
+ setClientID('')
+ setCredentials('')
}
}, [data])
@@ -150,6 +187,11 @@ const MCPModal = ({
Toast.notify({ type: 'error', message: 'invalid server identifier' })
return
}
+ const formattedHeaders = headers.reduce((acc, item) => {
+ if (item.key.trim())
+ acc[item.key.trim()] = item.value
+ return acc
+ }, {} as Record)
await onConfirm({
server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(),
name,
@@ -157,14 +199,25 @@ const MCPModal = ({
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
server_identifier: serverIdentifier.trim(),
- timeout: timeout || 30,
- sse_read_timeout: sseReadTimeout || 300,
- headers: Object.keys(headers).length > 0 ? headers : undefined,
+ headers: Object.keys(formattedHeaders).length > 0 ? formattedHeaders : undefined,
+ is_dynamic_registration: isDynamicRegistration,
+ authentication: {
+ client_id: clientID,
+ client_secret: credentials,
+ },
+ configuration: {
+ timeout: timeout || 30,
+ sse_read_timeout: sseReadTimeout || 300,
+ },
})
if(isCreate)
onHide()
}
+ const handleAuthMethodChange = useCallback((value: string) => {
+ setAuthMethod(value as MCPAuthMethod)
+ }, [])
+
return (
<>
)}
-
-
- {t('tools.mcp.modal.timeout')}
-
-
setMcpTimeout(Number(e.target.value))}
- onBlur={e => handleBlur(e.target.value.trim())}
- placeholder={t('tools.mcp.modal.timeoutPlaceholder')}
- />
-
-
-
- {t('tools.mcp.modal.sseReadTimeout')}
-
-
setSseReadTimeout(Number(e.target.value))}
- onBlur={e => handleBlur(e.target.value.trim())}
- placeholder={t('tools.mcp.modal.timeoutPlaceholder')}
- />
-
-
-
- {t('tools.mcp.modal.headers')}
-
-
{t('tools.mcp.modal.headersTip')}
-
0}
- />
-
+
{
+ return `flex-1 ${isActive && 'text-text-accent-light-mode-only'}`
+ }}
+ value={authMethod}
+ onChange={handleAuthMethodChange}
+ options={authMethods}
+ />
+ {
+ authMethod === MCPAuthMethod.authentication && (
+ <>
+
+
+
+ {t('tools.mcp.modal.useDynamicClientRegistration')}
+
+
+
+
+ {t('tools.mcp.modal.clientID')}
+
+
setClientID(e.target.value)}
+ onBlur={e => handleBlur(e.target.value.trim())}
+ placeholder={t('tools.mcp.modal.clientID')}
+ disabled={isDynamicRegistration}
+ />
+
+
+
+ {t('tools.mcp.modal.clientSecret')}
+
+
setCredentials(e.target.value)}
+ onBlur={e => handleBlur(e.target.value.trim())}
+ placeholder={t('tools.mcp.modal.clientSecretPlaceholder')}
+ disabled={isDynamicRegistration}
+ />
+
+ >
+ )
+ }
+ {
+ authMethod === MCPAuthMethod.headers && (
+
+
+ {t('tools.mcp.modal.headers')}
+
+
{t('tools.mcp.modal.headersTip')}
+
item.key.trim()).length > 0}
+ />
+
+ )
+ }
+ {
+ authMethod === MCPAuthMethod.configurations && (
+ <>
+
+
+ {t('tools.mcp.modal.timeout')}
+
+
setMcpTimeout(Number(e.target.value))}
+ onBlur={e => handleBlur(e.target.value.trim())}
+ placeholder={t('tools.mcp.modal.timeoutPlaceholder')}
+ />
+
+
+
+ {t('tools.mcp.modal.sseReadTimeout')}
+
+
setSseReadTimeout(Number(e.target.value))}
+ onBlur={e => handleBlur(e.target.value.trim())}
+ placeholder={t('tools.mcp.modal.timeoutPlaceholder')}
+ />
+
+ >
+ )
+ }
diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts
index 623a7b6d8a..1bfccc04e5 100644
--- a/web/app/components/tools/types.ts
+++ b/web/app/components/tools/types.ts
@@ -65,6 +65,15 @@ export type Collection = {
masked_headers?: Record
is_authorized?: boolean
provider?: string
+ is_dynamic_registration?: boolean
+ authentication?: {
+ client_id?: string
+ client_secret?: string
+ }
+ configuration?: {
+ timeout?: number
+ sse_read_timeout?: number
+ }
}
export type ToolParameter = {
@@ -192,3 +201,9 @@ export type MCPServerDetail = {
parameters?: Record
headers?: Record
}
+
+export enum MCPAuthMethod {
+ authentication = 'authentication',
+ headers = 'headers',
+ configurations = 'configurations',
+}
diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx
index 6a2e07a411..7db8b9acf5 100644
--- a/web/app/components/workflow/block-selector/all-tools.tsx
+++ b/web/app/components/workflow/block-selector/all-tools.tsx
@@ -25,7 +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'
+import RAGToolRecommendations from './rag-tool-recommendations'
type AllToolsProps = {
className?: string
@@ -148,7 +148,7 @@ const AllTools = ({
onScroll={pluginRef.current?.handleScroll}
>
{isShowRAGRecommendations && (
- >
}
-const RAGToolSuggestions: React.FC = ({
+const RAGToolRecommendations = ({
viewType,
onSelect,
onTagsChange,
-}) => {
+}: RAGToolRecommendationsProps) => {
const { t } = useTranslation()
const {
data: ragRecommendedPlugins,
+ isLoading: isLoadingRAGRecommendedPlugins,
isFetching: isFetchingRAGRecommendedPlugins,
} = useRAGRecommendedPlugins()
const recommendedPlugins = useMemo(() => {
if (ragRecommendedPlugins)
- return [...ragRecommendedPlugins.installed_recommended_plugins]
+ return ragRecommendedPlugins.installed_recommended_plugins
+ return []
+ }, [ragRecommendedPlugins])
+
+ const unInstalledPlugins = useMemo(() => {
+ if (ragRecommendedPlugins)
+ return (ragRecommendedPlugins.uninstalled_recommended_plugins).map(getFormattedPlugin)
return []
}, [ragRecommendedPlugins])
@@ -48,15 +55,16 @@ const RAGToolSuggestions: React.FC = ({
{t('pipeline.ragToolSuggestions.title')}
- {isFetchingRAGRecommendedPlugins && (
+ {/* For first time loading, show loading */}
+ {isLoadingRAGRecommendedPlugins && (
)}
- {!isFetchingRAGRecommendedPlugins && recommendedPlugins.length === 0 && (
+ {!isFetchingRAGRecommendedPlugins && recommendedPlugins.length === 0 && unInstalledPlugins.length === 0 && (
= ({
/>
)}
- {!isFetchingRAGRecommendedPlugins && recommendedPlugins.length > 0 && (
+ {(recommendedPlugins.length > 0 || unInstalledPlugins.length > 0) && (
<>
-
= ({
)
}
-export default React.memo(RAGToolSuggestions)
+export default React.memo(RAGToolRecommendations)
diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx
new file mode 100644
index 0000000000..19378caf48
--- /dev/null
+++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/list.tsx
@@ -0,0 +1,102 @@
+import {
+ useMemo,
+ useRef,
+} from 'react'
+import type { BlockEnum, ToolWithProvider } from '../../types'
+import type { ToolDefaultValue } from '../types'
+import { ViewType } from '../view-type-select'
+import { useGetLanguage } from '@/context/i18n'
+import { groupItems } from '../index-bar'
+import cn from '@/utils/classnames'
+import ToolListTreeView from '../tool/tool-list-tree-view/list'
+import ToolListFlatView from '../tool/tool-list-flat-view/list'
+import UninstalledItem from './uninstalled-item'
+import type { Plugin } from '@/app/components/plugins/types'
+
+type ListProps = {
+ onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
+ tools: ToolWithProvider[]
+ viewType: ViewType
+ unInstalledPlugins: Plugin[]
+ className?: string
+}
+
+const List = ({
+ onSelect,
+ tools,
+ viewType,
+ unInstalledPlugins,
+ className,
+}: ListProps) => {
+ const language = useGetLanguage()
+ const isFlatView = viewType === ViewType.flat
+
+ const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => tool.label[language][0])
+ const treeViewToolsData = useMemo(() => {
+ const result: Record
= {}
+ Object.keys(withLetterAndGroupViewToolsData).forEach((letter) => {
+ Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
+ if (!result[groupName])
+ result[groupName] = []
+ result[groupName].push(...withLetterAndGroupViewToolsData[letter][groupName])
+ })
+ })
+ return result
+ }, [withLetterAndGroupViewToolsData])
+
+ const listViewToolData = useMemo(() => {
+ const result: ToolWithProvider[] = []
+ letters.forEach((letter) => {
+ Object.keys(withLetterAndGroupViewToolsData[letter]).forEach((groupName) => {
+ result.push(...withLetterAndGroupViewToolsData[letter][groupName].map((item) => {
+ return {
+ ...item,
+ letter,
+ }
+ }))
+ })
+ })
+
+ return result
+ }, [withLetterAndGroupViewToolsData, letters])
+
+ const toolRefs = useRef({})
+
+ return (
+
+ {!!tools.length && (
+ isFlatView ? (
+
+ ) : (
+
+ )
+ )}
+ {
+ unInstalledPlugins.map((item) => {
+ return (
+
+ )
+ })
+ }
+
+ )
+}
+
+export default List
diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/uninstalled-item.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/uninstalled-item.tsx
new file mode 100644
index 0000000000..98395ec25a
--- /dev/null
+++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/uninstalled-item.tsx
@@ -0,0 +1,63 @@
+'use client'
+import React from 'react'
+import { useContext } from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import type { Plugin } from '@/app/components/plugins/types'
+import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
+import I18n from '@/context/i18n'
+import { useBoolean } from 'ahooks'
+import { BlockEnum } from '../../types'
+import BlockIcon from '../../block-icon'
+
+type UninstalledItemProps = {
+ payload: Plugin
+}
+
+const UninstalledItem = ({
+ payload,
+}: UninstalledItemProps) => {
+ const { t } = useTranslation()
+ const { locale } = useContext(I18n)
+
+ const getLocalizedText = (obj: Record | undefined) =>
+ obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
+ const [isShowInstallModal, {
+ setTrue: showInstallModal,
+ setFalse: hideInstallModal,
+ }] = useBoolean(false)
+
+ return (
+
+
+
+
+
+ {getLocalizedText(payload.label)}
+
+
+ {payload.org}
+
+
+
+ {t('plugin.installAction')}
+
+ {isShowInstallModal && (
+
+ )}
+
+
+ )
+}
+export default React.memo(UninstalledItem)
diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx
index feb34d2651..71ed4092a3 100644
--- a/web/app/components/workflow/block-selector/tools.tsx
+++ b/web/app/components/workflow/block-selector/tools.tsx
@@ -30,7 +30,7 @@ type ToolsProps = {
canChooseMCPTool?: boolean
isShowRAGRecommendations?: boolean
}
-const Blocks = ({
+const Tools = ({
onSelect,
canNotSelectMultiple,
onSelectMultiple,
@@ -146,4 +146,4 @@ const Blocks = ({
)
}
-export default memo(Blocks)
+export default memo(Tools)
diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts
index 32945a8927..d29827f273 100644
--- a/web/app/components/workflow/hooks/use-checklist.ts
+++ b/web/app/components/workflow/hooks/use-checklist.ts
@@ -45,14 +45,19 @@ import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variabl
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
const language = useGetLanguage()
const { nodesMap: nodesExtraData } = useNodesMetaData()
- const buildInTools = useStore(s => s.buildInTools)
- const customTools = useStore(s => s.customTools)
- const workflowTools = useStore(s => s.workflowTools)
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
const dataSourceList = useStore(s => s.dataSourceList)
const { data: strategyProviders } = useStrategyProviders()
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
@@ -104,7 +109,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
let usedVars: ValueSelector[] = []
if (node.data.type === BlockEnum.Tool)
- moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
+ moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language)
if (node.data.type === BlockEnum.DataSource)
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
@@ -194,6 +199,9 @@ export const useChecklistBeforePublish = () => {
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
let checkData = data
@@ -221,7 +229,7 @@ export const useChecklistBeforePublish = () => {
} as CommonNodeType
}
return checkData
- }, [])
+ }, [embeddingModelList, rerankModelList])
const handleCheckBeforePublish = useCallback(async () => {
const {
@@ -230,9 +238,6 @@ export const useChecklistBeforePublish = () => {
} = store.getState()
const {
dataSourceList,
- buildInTools,
- customTools,
- workflowTools,
} = workflowStore.getState()
const nodes = getNodes()
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
@@ -275,7 +280,7 @@ export const useChecklistBeforePublish = () => {
let moreDataForCheckValid
let usedVars: ValueSelector[] = []
if (node.data.type === BlockEnum.Tool)
- moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
+ moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language)
if (node.data.type === BlockEnum.DataSource)
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
@@ -340,7 +345,7 @@ export const useChecklistBeforePublish = () => {
}
return true
- }, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, getStartNodes, workflowStore])
+ }, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, getStartNodes, workflowStore, buildInTools, customTools, workflowTools])
return {
handleCheckBeforePublish,
diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts
index 1527fb82e2..60f839b93d 100644
--- a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts
+++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts
@@ -11,6 +11,12 @@ import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/compone
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import type { SchemaTypeDefinition } from '@/service/use-common'
import { useCallback } from 'react'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
type Params = {
flowType: FlowType
@@ -27,17 +33,17 @@ export const useSetWorkflowVarsWithValue = ({
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 { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
const allPluginInfoList = {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
}
const setInspectVarsToStore = (inspectVars: VarInInspect[], passedInAllPluginInfoList?: Record, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]) => {
diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts
index f35f0c7dab..6b7acd0a85 100644
--- a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts
+++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts
@@ -18,6 +18,12 @@ import type { FlowType } from '@/types/common'
import useFLow from '@/service/use-flow'
import { useStoreApi } from 'reactflow'
import type { SchemaTypeDefinition } from '@/service/use-common'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
type Params = {
flowId: string
@@ -51,6 +57,11 @@ export const useInspectVarsCrudCommon = ({
const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId)
const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync()
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
+
const getNodeInspectVars = useCallback((nodeId: string) => {
const { nodesWithInspectVars } = workflowStore.getState()
const node = nodesWithInspectVars.find(node => node.nodeId === nodeId)
@@ -98,10 +109,6 @@ export const useInspectVarsCrudCommon = ({
const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => {
const {
setNodeInspectVars,
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
dataSourceList,
} = workflowStore.getState()
const nodeId = selector[0]
@@ -119,11 +126,11 @@ export const useInspectVarsCrudCommon = ({
const nodeArr = getNodes()
const currentNode = nodeArr.find(node => node.id === nodeId)
const allPluginInfoList = {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
}
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
@@ -135,7 +142,7 @@ export const useInspectVarsCrudCommon = ({
}
})
setNodeInspectVars(nodeId, varsWithSchemaType)
- }, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues])
+ }, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools])
// after last run would call this
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts
index fa61cdeb8c..4de53c431c 100644
--- a/web/app/components/workflow/hooks/use-nodes-interactions.ts
+++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts
@@ -1445,6 +1445,7 @@ export const useNodesInteractions = () => {
// If no nodeId is provided, fall back to the current behavior
const bundledNodes = nodes.filter((node) => {
if (!node.data._isBundled) return false
+ if (node.type === CUSTOM_NOTE_NODE) return true
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
if (metaData.isSingleton) return false
return !node.data.isInIteration && !node.data.isInLoop
@@ -1457,6 +1458,7 @@ export const useNodesInteractions = () => {
const selectedNode = nodes.find((node) => {
if (!node.data.selected) return false
+ if (node.type === CUSTOM_NOTE_NODE) return true
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
return !metaData.isSingleton
})
@@ -1495,7 +1497,7 @@ export const useNodesInteractions = () => {
= generateNewNode({
type: nodeToPaste.type,
data: {
- ...nodesMetaDataMap![nodeType].defaultValue,
+ ...(nodeToPaste.type !== CUSTOM_NOTE_NODE && nodesMetaDataMap![nodeType].defaultValue),
...nodeToPaste.data,
selected: false,
_isBundled: false,
diff --git a/web/app/components/workflow/hooks/use-nodes-meta-data.ts b/web/app/components/workflow/hooks/use-nodes-meta-data.ts
index cfeb41de34..fd63f23590 100644
--- a/web/app/components/workflow/hooks/use-nodes-meta-data.ts
+++ b/web/app/components/workflow/hooks/use-nodes-meta-data.ts
@@ -7,6 +7,11 @@ import { CollectionType } from '@/app/components/tools/types'
import { useStore } from '@/app/components/workflow/store'
import { canFindTool } from '@/utils'
import { useGetLanguage } from '@/context/i18n'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
export const useNodesMetaData = () => {
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
@@ -21,9 +26,9 @@ export const useNodesMetaData = () => {
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 { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
const dataSourceList = useStore(s => s.dataSourceList)
const availableNodesMetaData = useNodesMetaData()
const { data } = node
@@ -34,10 +39,10 @@ export const useNodeMetaData = (node: Node) => {
if (data.type === BlockEnum.Tool) {
if (data.provider_type === CollectionType.builtIn)
- return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
+ 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 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])
@@ -47,10 +52,10 @@ export const useNodeMetaData = (node: Node) => {
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]
+ 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 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])
diff --git a/web/app/components/workflow/hooks/use-tool-icon.ts b/web/app/components/workflow/hooks/use-tool-icon.ts
index 734a7da390..32d65365db 100644
--- a/web/app/components/workflow/hooks/use-tool-icon.ts
+++ b/web/app/components/workflow/hooks/use-tool-icon.ts
@@ -14,12 +14,18 @@ import {
} from '../store'
import { CollectionType } from '@/app/components/tools/types'
import { canFindTool } from '@/utils'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
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: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
// const a = useStore(s => s.data)
const toolIcon = useMemo(() => {
@@ -27,15 +33,15 @@ export const useToolIcon = (data?: Node['data']) => {
return ''
if (data.type === BlockEnum.Tool) {
// eslint-disable-next-line sonarjs/no-dead-store
- let targetTools = buildInTools
+ let targetTools = buildInTools || []
if (data.provider_type === CollectionType.builtIn)
- targetTools = buildInTools
+ targetTools = buildInTools || []
else if (data.provider_type === CollectionType.custom)
- targetTools = customTools
+ targetTools = customTools || []
else if (data.provider_type === CollectionType.mcp)
- targetTools = mcpTools
+ targetTools = mcpTools || []
else
- targetTools = workflowTools
+ targetTools = workflowTools || []
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
}
if (data.type === BlockEnum.DataSource)
@@ -46,24 +52,24 @@ export const useToolIcon = (data?: Node['data']) => {
}
export const useGetToolIcon = () => {
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
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
+ let targetTools = buildInTools || []
if (data.provider_type === CollectionType.builtIn)
- targetTools = buildInTools
+ targetTools = buildInTools || []
else if (data.provider_type === CollectionType.custom)
- targetTools = customTools
+ targetTools = customTools || []
else
- targetTools = workflowTools
+ targetTools = workflowTools || []
return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon
}
diff --git a/web/app/components/workflow/hooks/use-workflow-search.tsx b/web/app/components/workflow/hooks/use-workflow-search.tsx
index 095ae4577a..68ad9873f9 100644
--- a/web/app/components/workflow/hooks/use-workflow-search.tsx
+++ b/web/app/components/workflow/hooks/use-workflow-search.tsx
@@ -8,11 +8,16 @@ import { workflowNodesAction } from '@/app/components/goto-anything/actions/work
import BlockIcon from '@/app/components/workflow/block-icon'
import { setupNodeSelectionListener } from '../utils/node-navigation'
import { BlockEnum } from '../types'
-import { useStore } from '../store'
import type { Emoji } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import { canFindTool } from '@/utils'
import type { LLMNodeType } from '../nodes/llm/types'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
/**
* Hook to register workflow nodes search functionality
@@ -22,23 +27,23 @@ export const useWorkflowSearch = () => {
const { handleNodeSelect } = useNodesInteractions()
// Filter and process nodes for search
- 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: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
// Extract tool icon logic - clean separation of concerns
const getToolIcon = useCallback((nodeData: CommonNodeType): string | Emoji | undefined => {
if (nodeData?.type !== BlockEnum.Tool) return undefined
const toolCollections: Record = {
- [CollectionType.builtIn]: buildInTools,
- [CollectionType.custom]: customTools,
- [CollectionType.mcp]: mcpTools,
+ [CollectionType.builtIn]: buildInTools || [],
+ [CollectionType.custom]: customTools || [],
+ [CollectionType.mcp]: mcpTools || [],
}
const targetTools = (nodeData.provider_type && toolCollections[nodeData.provider_type]) || workflowTools
- return targetTools.find((tool: any) => canFindTool(tool.id, nodeData.provider_id))?.icon
+ return targetTools?.find((tool: any) => canFindTool(tool.id, nodeData.provider_id))?.icon
}, [buildInTools, customTools, workflowTools, mcpTools])
// Extract model info logic - clean extraction
diff --git a/web/app/components/workflow/hooks/use-workflow-variables.ts b/web/app/components/workflow/hooks/use-workflow-variables.ts
index 0a0dc073ab..468a0142b3 100644
--- a/web/app/components/workflow/hooks/use-workflow-variables.ts
+++ b/web/app/components/workflow/hooks/use-workflow-variables.ts
@@ -10,20 +10,25 @@ 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'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
export const useWorkflowVariables = () => {
const { t } = useTranslation()
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 { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
+
const getNodeAvailableVars = useCallback(({
parentNode,
beforeNodes,
@@ -47,6 +52,7 @@ export const useWorkflowVariables = () => {
conversationVariables,
environmentVariables,
ragPipelineVariables,
+ dataSourceList,
} = workflowStore.getState()
return toNodeAvailableVars({
parentNode,
@@ -58,17 +64,17 @@ export const useWorkflowVariables = () => {
ragVariables: ragPipelineVariables,
filterVar,
allPluginInfoList: {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
},
schemaTypeDefinitions,
conversationVariablesFirst,
memoryVarSortFn,
})
- }, [t, workflowStore, schemaTypeDefinitions, buildInTools])
+ }, [t, workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
const getCurrentVariableType = useCallback(({
parentNode,
@@ -93,10 +99,6 @@ export const useWorkflowVariables = () => {
conversationVariables,
environmentVariables,
ragPipelineVariables,
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
dataSourceList,
} = workflowStore.getState()
return getVarType({
@@ -111,16 +113,16 @@ export const useWorkflowVariables = () => {
conversationVariables,
ragVariables: ragPipelineVariables,
allPluginInfoList: {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
dataSourceList: dataSourceList ?? [],
},
schemaTypeDefinitions,
preferSchemaType,
})
- }, [workflowStore, getVarType, schemaTypeDefinitions])
+ }, [workflowStore, getVarType, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
return {
getNodeAvailableVars,
diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts
index 3f9f8106cf..66c499dc59 100644
--- a/web/app/components/workflow/hooks/use-workflow.ts
+++ b/web/app/components/workflow/hooks/use-workflow.ts
@@ -32,15 +32,9 @@ import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
import { useAvailableBlocks } from './use-available-blocks'
import { useStore as useAppStore } from '@/app/components/app/store'
-import {
- fetchAllBuiltInTools,
- fetchAllCustomTools,
- fetchAllMCPTools,
- fetchAllWorkflowTools,
-} from '@/service/tools'
+
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 { useNodesMetaData } from '.'
export const useIsChatMode = () => {
@@ -416,51 +410,6 @@ export const useWorkflow = () => {
}
}
-export const useFetchToolsData = () => {
- const workflowStore = useWorkflowStore()
-
- const handleFetchAllTools = useCallback(async (type: string) => {
- if (type === 'builtin') {
- const buildInTools = await fetchAllBuiltInTools()
-
- if (basePath) {
- buildInTools.forEach((item) => {
- if (typeof item.icon == 'string' && !item.icon.includes(basePath))
- item.icon = `${basePath}${item.icon}`
- })
- }
- workflowStore.setState({
- buildInTools: buildInTools || [],
- })
- }
- if (type === 'custom') {
- const customTools = await fetchAllCustomTools()
-
- workflowStore.setState({
- customTools: customTools || [],
- })
- }
- if (type === 'workflow') {
- const workflowTools = await fetchAllWorkflowTools()
-
- workflowStore.setState({
- workflowTools: workflowTools || [],
- })
- }
- if (type === 'mcp') {
- const mcpTools = await fetchAllMCPTools()
-
- workflowStore.setState({
- mcpTools: mcpTools || [],
- })
- }
- }, [workflowStore])
-
- return {
- handleFetchAllTools,
- }
-}
-
export const useWorkflowReadOnly = () => {
const workflowStore = useWorkflowStore()
const workflowRunningData = useStore(s => s.workflowRunningData)
diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx
index b289cafefd..86c6bf153e 100644
--- a/web/app/components/workflow/index.tsx
+++ b/web/app/components/workflow/index.tsx
@@ -37,7 +37,6 @@ import {
} from './types'
import {
useEdgesInteractions,
- useFetchToolsData,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
@@ -92,6 +91,12 @@ import useMatchSchemaType from './nodes/_base/components/variable/use-match-sche
import type { VarInInspect } from '@/types/workflow'
import { fetchAllInspectVars } from '@/service/workflow'
import cn from '@/utils/classnames'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
const Confirm = dynamic(() => import('@/app/components/base/confirm'), {
ssr: false,
@@ -242,13 +247,6 @@ export const Workflow: FC = memo(({
})
}
})
- const { handleFetchAllTools } = useFetchToolsData()
- useEffect(() => {
- handleFetchAllTools('builtin')
- handleFetchAllTools('custom')
- handleFetchAllTools('workflow')
- handleFetchAllTools('mcp')
- }, [handleFetchAllTools])
const {
handleNodeDragStart,
@@ -299,10 +297,10 @@ export const Workflow: FC = memo(({
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 { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
const configsMap = useHooksStore(s => s.configsMap)
@@ -323,10 +321,10 @@ export const Workflow: FC = memo(({
passInVars: true,
vars,
passedInAllPluginInfoList: {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
dataSourceList: dataSourceList ?? [],
},
passedInSchemaTypeDefinitions: schemaTypeDefinitions,
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
index 03b142ba43..29aebd4fd5 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
@@ -75,6 +75,7 @@ import { DataSourceClassification } from '@/app/components/workflow/nodes/data-s
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 { useAllBuiltInTools } from '@/service/use-tools'
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
const nodeType = params.payload.type
@@ -259,9 +260,9 @@ const BasePanel: FC = ({
return {}
})()
- const buildInTools = useStore(s => s.buildInTools)
+ const { data: buildInTools } = useAllBuiltInTools()
const currCollection = useMemo(() => {
- return buildInTools.find(item => canFindTool(item.id, data.provider_id))
+ return buildInTools?.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
@@ -450,6 +451,7 @@ const BasePanel: FC = ({
className='px-4 pb-2'
pluginPayload={{
provider: currCollection?.name || '',
+ providerType: currCollection?.type || '',
category: AuthCategory.tool,
}}
>
@@ -461,6 +463,7 @@ const BasePanel: FC = ({
= {
[BlockEnum.LLM]: checkLLMValid,
@@ -133,21 +140,23 @@ const useOneStepRun = ({
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const workflowStore = useWorkflowStore()
const { schemaTypeDefinitions } = useMatchSchemaType()
+
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
+
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 ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
}
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions)
diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx
index 9bcd4b9671..65dac6f5be 100644
--- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx
+++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx
@@ -42,6 +42,12 @@ import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/compo
import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow'
import useMatchSchemaType from '../../../_base/components/variable/use-match-schema-type'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
type ConditionItemProps = {
@@ -91,15 +97,12 @@ const ConditionItem = ({
const [isHovered, setIsHovered] = useState(false)
const [open, setOpen] = useState(false)
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
+
const workflowStore = useWorkflowStore()
- const {
- setControlPromptEditorRerenderKey,
- buildInTools,
- customTools,
- mcpTools,
- workflowTools,
- dataSourceList,
- } = workflowStore.getState()
const doUpdateCondition = useCallback((newCondition: Condition) => {
if (isSubVariableKey)
@@ -213,6 +216,8 @@ const ConditionItem = ({
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
+ setControlPromptEditorRerenderKey,
+ dataSourceList,
} = workflowStore.getState()
const resolvedVarType = getVarType({
valueSelector,
@@ -220,11 +225,11 @@ const ConditionItem = ({
availableNodes,
isChatMode,
allPluginInfoList: {
- buildInTools,
- customTools,
- mcpTools,
- workflowTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ mcpTools: mcpTools || [],
+ workflowTools: workflowTools || [],
+ dataSourceList: dataSourceList || [],
},
schemaTypeDefinitions,
})
@@ -241,12 +246,12 @@ const ConditionItem = ({
})
doUpdateCondition(newCondition)
setOpen(false)
- }, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey, schemaTypeDefinitions])
+ }, [condition, doUpdateCondition, availableNodes, isChatMode, schemaTypeDefinitions, buildInTools, customTools, mcpTools, workflowTools])
const showBooleanInput = useMemo(() => {
if(condition.varType === VarType.boolean)
return true
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
+
if(condition.varType === VarType.arrayBoolean && [ComparisonOperator.contains, ComparisonOperator.notContains].includes(condition.comparison_operator!))
return true
return false
diff --git a/web/app/components/workflow/nodes/iteration/default.ts b/web/app/components/workflow/nodes/iteration/default.ts
index 450379ec6b..c375dbdcbf 100644
--- a/web/app/components/workflow/nodes/iteration/default.ts
+++ b/web/app/components/workflow/nodes/iteration/default.ts
@@ -22,6 +22,7 @@ const nodeDefault: NodeDefault = {
is_parallel: false,
parallel_nums: 10,
error_handle_mode: ErrorHandleMode.Terminated,
+ flatten_output: true,
},
checkValid(payload: IterationNodeType, t: any) {
let errorMessages = ''
diff --git a/web/app/components/workflow/nodes/iteration/panel.tsx b/web/app/components/workflow/nodes/iteration/panel.tsx
index 23e93b0dd5..63e0d5f8cd 100644
--- a/web/app/components/workflow/nodes/iteration/panel.tsx
+++ b/web/app/components/workflow/nodes/iteration/panel.tsx
@@ -46,6 +46,7 @@ const Panel: FC> = ({
changeParallel,
changeErrorResponseMode,
changeParallelNums,
+ changeFlattenOutput,
} = useConfig(id, data)
return (
@@ -117,6 +118,18 @@ const Panel: FC> = ({
+
+
+
+
+ {t(`${i18nPrefix}.flattenOutputDesc`)}
}
+ inline
+ >
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/iteration/types.ts b/web/app/components/workflow/nodes/iteration/types.ts
index 9a68050d6f..f35b838b83 100644
--- a/web/app/components/workflow/nodes/iteration/types.ts
+++ b/web/app/components/workflow/nodes/iteration/types.ts
@@ -17,5 +17,6 @@ export type IterationNodeType = CommonNodeType & {
is_parallel: boolean // open the parallel mode or not
parallel_nums: number // the numbers of parallel
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
+ flatten_output: boolean // whether to flatten the output array if all elements are lists
_isShowTips: boolean // when answer node in parallel mode iteration show tips
}
diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts
index 5d449a4756..2e47bb3740 100644
--- a/web/app/components/workflow/nodes/iteration/use-config.ts
+++ b/web/app/components/workflow/nodes/iteration/use-config.ts
@@ -15,6 +15,12 @@ import type { Item } from '@/app/components/base/select'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { isEqual } from 'lodash-es'
import { useStore } from '../../store'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
const useConfig = (id: string, payload: IterationNodeType) => {
const {
@@ -40,17 +46,17 @@ const useConfig = (id: string, payload: IterationNodeType) => {
// output
const { getIterationNodeChildren } = useWorkflow()
const iterationChildrenNodes = getIterationNodeChildren(id)
- 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: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
const allPluginInfoList = {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
}
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode, undefined, [], [], [], allPluginInfoList)
@@ -98,6 +104,14 @@ const useConfig = (id: string, payload: IterationNodeType) => {
})
setInputs(newInputs)
}, [inputs, setInputs])
+
+ const changeFlattenOutput = useCallback((value: boolean) => {
+ const newInputs = produce(inputs, (draft) => {
+ draft.flatten_output = value
+ })
+ setInputs(newInputs)
+ }, [inputs, setInputs])
+
return {
readOnly,
inputs,
@@ -109,6 +123,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
changeParallel,
changeErrorResponseMode,
changeParallelNums,
+ changeFlattenOutput,
}
}
diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts
index fcf437eb96..e8504fb5e9 100644
--- a/web/app/components/workflow/nodes/loop/use-config.ts
+++ b/web/app/components/workflow/nodes/loop/use-config.ts
@@ -15,9 +15,24 @@ import useNodeCrud from '../_base/hooks/use-node-crud'
import { toNodeOutputVars } from '../_base/components/variable/utils'
import { getOperators } from './utils'
import { LogicalOperator } from './types'
-import type { HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, LoopNodeType } from './types'
+import type {
+ HandleAddCondition,
+ HandleAddSubVariableCondition,
+ HandleRemoveCondition,
+ HandleToggleConditionLogicalOperator,
+ HandleToggleSubVariableConditionLogicalOperator,
+ HandleUpdateCondition,
+ HandleUpdateSubVariableCondition,
+ LoopNodeType,
+} from './types'
import useIsVarFileAttribute from './use-is-var-file-attribute'
import { useStore } from '@/app/components/workflow/store'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
const useConfig = (id: string, payload: LoopNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -38,17 +53,17 @@ const useConfig = (id: string, payload: LoopNodeType) => {
// output
const { getLoopNodeChildren } = useWorkflow()
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
- 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: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const dataSourceList = useStore(s => s.dataSourceList)
const allPluginInfoList = {
- buildInTools,
- customTools,
- workflowTools,
- mcpTools,
- dataSourceList: dataSourceList ?? [],
+ buildInTools: buildInTools || [],
+ customTools: customTools || [],
+ workflowTools: workflowTools || [],
+ mcpTools: mcpTools || [],
+ dataSourceList: dataSourceList || [],
}
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables, [], allPluginInfoList)
diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx
index d93d08a0ac..9392f28736 100644
--- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx
+++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx
@@ -8,7 +8,6 @@ import { useTranslation } from 'react-i18next'
import BlockSelector from '../../../../block-selector'
import type { Param, ParamType } from '../../types'
import cn from '@/utils/classnames'
-import { useStore } from '@/app/components/workflow/store'
import type {
DataSourceDefaultValue,
ToolDefaultValue,
@@ -18,6 +17,11 @@ import { CollectionType } from '@/app/components/tools/types'
import type { BlockEnum } from '@/app/components/workflow/types'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { canFindTool } from '@/utils'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllWorkflowTools,
+} from '@/service/use-tools'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
@@ -42,9 +46,9 @@ const ImportFromTool: FC
= ({
const { t } = useTranslation()
const language = useLanguage()
- const buildInTools = useStore(s => s.buildInTools)
- const customTools = useStore(s => s.customTools)
- const workflowTools = useStore(s => s.workflowTools)
+ const { data: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue | DataSourceDefaultValue) => {
if (!toolInfo || 'datasource_name' in toolInfo)
@@ -54,11 +58,11 @@ const ImportFromTool: FC = ({
const currentTools = (() => {
switch (provider_type) {
case CollectionType.builtIn:
- return buildInTools
+ return buildInTools || []
case CollectionType.custom:
- return customTools
+ return customTools || []
case CollectionType.workflow:
- return workflowTools
+ return workflowTools || []
default:
return []
}
diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts
index 5b8827936c..fe3fe543e9 100644
--- a/web/app/components/workflow/nodes/tool/use-config.ts
+++ b/web/app/components/workflow/nodes/tool/use-config.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { produce } from 'immer'
import { useBoolean } from 'ahooks'
-import { useStore, useWorkflowStore } from '../../store'
+import { useWorkflowStore } from '../../store'
import type { ToolNodeType, ToolVarInputs } from './types'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
@@ -15,15 +15,20 @@ import {
import Toast from '@/app/components/base/toast'
import type { InputVar } from '@/app/components/workflow/types'
import {
- useFetchToolsData,
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { canFindTool } from '@/utils'
+import {
+ useAllBuiltInTools,
+ useAllCustomTools,
+ useAllMCPTools,
+ useAllWorkflowTools,
+ useInvalidToolsByType,
+} from '@/service/use-tools'
const useConfig = (id: string, payload: ToolNodeType) => {
const workflowStore = useWorkflowStore()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
- const { handleFetchAllTools } = useFetchToolsData()
const { t } = useTranslation()
const language = useLanguage()
@@ -43,21 +48,21 @@ const useConfig = (id: string, payload: ToolNodeType) => {
tool_parameters,
} = inputs
const isBuiltIn = provider_type === CollectionType.builtIn
- 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: buildInTools } = useAllBuiltInTools()
+ const { data: customTools } = useAllCustomTools()
+ const { data: workflowTools } = useAllWorkflowTools()
+ const { data: mcpTools } = useAllMCPTools()
const currentTools = useMemo(() => {
switch (provider_type) {
case CollectionType.builtIn:
- return buildInTools
+ return buildInTools || []
case CollectionType.custom:
- return customTools
+ return customTools || []
case CollectionType.workflow:
- return workflowTools
+ return workflowTools || []
case CollectionType.mcp:
- return mcpTools
+ return mcpTools || []
default:
return []
}
@@ -75,6 +80,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
{ setTrue: showSetAuthModal, setFalse: hideSetAuthModal },
] = useBoolean(false)
+ const invalidToolsByType = useInvalidToolsByType(provider_type)
const handleSaveAuth = useCallback(
async (value: any) => {
await updateBuiltInToolCredential(currCollection?.name as string, value)
@@ -83,14 +89,14 @@ const useConfig = (id: string, payload: ToolNodeType) => {
type: 'success',
message: t('common.api.actionSuccess'),
})
- handleFetchAllTools(provider_type)
+ invalidToolsByType()
hideSetAuthModal()
},
[
currCollection?.name,
hideSetAuthModal,
t,
- handleFetchAllTools,
+ invalidToolsByType,
provider_type,
],
)
@@ -241,17 +247,15 @@ const useConfig = (id: string, payload: ToolNodeType) => {
name: outputKey,
type:
output.type === 'array'
- ? `Array[${
- output.items?.type
- ? output.items.type.slice(0, 1).toLocaleUpperCase()
- + output.items.type.slice(1)
- : 'Unknown'
+ ? `Array[${output.items?.type
+ ? output.items.type.slice(0, 1).toLocaleUpperCase()
+ + output.items.type.slice(1)
+ : 'Unknown'
}]`
- : `${
- output.type
- ? output.type.slice(0, 1).toLocaleUpperCase()
- + output.type.slice(1)
- : 'Unknown'
+ : `${output.type
+ ? output.type.slice(0, 1).toLocaleUpperCase()
+ + output.type.slice(1)
+ : 'Unknown'
}`,
description: output.description,
})
diff --git a/web/app/components/workflow/note-node/index.tsx b/web/app/components/workflow/note-node/index.tsx
index 7f2cde42d6..5a0b2677c1 100644
--- a/web/app/components/workflow/note-node/index.tsx
+++ b/web/app/components/workflow/note-node/index.tsx
@@ -1,6 +1,5 @@
import {
memo,
- useCallback,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
@@ -51,10 +50,6 @@ const NoteNode = ({
} = useNodesInteractions()
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
- const handleDeleteNode = useCallback(() => {
- handleNodeDelete(id)
- }, [id, handleNodeDelete])
-
useClickAway(() => {
handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } })
}, ref)
@@ -102,9 +97,9 @@ const NoteNode = ({
handleNodesCopy(id)}
+ onDuplicate={() => handleNodesDuplicate(id)}
+ onDelete={() => handleNodeDelete(id)}
showAuthor={data.showAuthor}
onShowAuthorChange={handleShowAuthorChange}
/>
diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
index 1a97357da5..6fba10bf81 100644
--- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
@@ -12,7 +12,7 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
-import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
+import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useFeatures } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
@@ -117,7 +117,7 @@ const ChatWrapper = (
)
}, [handleSend, workflowStore, conversationId, chatList, appDetail])
- const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
+ const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
doSend(editedQuestion ? editedQuestion.message : question.content,
diff --git a/web/app/components/workflow/store/workflow/tool-slice.ts b/web/app/components/workflow/store/workflow/tool-slice.ts
index d6d89abcf0..c5180022fc 100644
--- a/web/app/components/workflow/store/workflow/tool-slice.ts
+++ b/web/app/components/workflow/store/workflow/tool-slice.ts
@@ -1,30 +1,11 @@
import type { StateCreator } from 'zustand'
-import type {
- ToolWithProvider,
-} from '@/app/components/workflow/types'
export type ToolSliceShape = {
- buildInTools: ToolWithProvider[]
- setBuildInTools: (tools: ToolWithProvider[]) => void
- customTools: ToolWithProvider[]
- setCustomTools: (tools: ToolWithProvider[]) => void
- workflowTools: ToolWithProvider[]
- setWorkflowTools: (tools: ToolWithProvider[]) => void
- mcpTools: ToolWithProvider[]
- setMcpTools: (tools: ToolWithProvider[]) => void
toolPublished: boolean
setToolPublished: (toolPublished: boolean) => void
}
export const createToolSlice: StateCreator = set => ({
- buildInTools: [],
- setBuildInTools: buildInTools => set(() => ({ buildInTools })),
- customTools: [],
- setCustomTools: customTools => set(() => ({ customTools })),
- workflowTools: [],
- setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
- mcpTools: [],
- setMcpTools: mcpTools => set(() => ({ mcpTools })),
toolPublished: false,
setToolPublished: toolPublished => set(() => ({ toolPublished })),
})
diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts
index a39426df57..e517a8edaf 100644
--- a/web/app/components/workflow/types.ts
+++ b/web/app/components/workflow/types.ts
@@ -19,7 +19,7 @@ import type {
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'
-import type { PluginMeta } from '../plugins/types'
+import type { Plugin, PluginMeta } from '@/app/components/plugins/types'
import type { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
import type { SchemaTypeDefinition } from '@/service/use-common'
@@ -478,16 +478,9 @@ export type ToolWithProvider = Collection & {
meta: PluginMeta
}
-export type UninstalledRecommendedPlugin = {
- plugin_id: string
- name: string
- icon: string
- plugin_unique_identifier: string
-}
-
export type RAGRecommendedPlugins = {
installed_recommended_plugins: ToolWithProvider[]
- uninstalled_recommended_plugins: UninstalledRecommendedPlugin[]
+ uninstalled_recommended_plugins: Plugin[]
}
export enum SupportUploadFileTypes {
diff --git a/web/app/components/workflow/utils/layout.ts b/web/app/components/workflow/utils/elk-layout.ts
similarity index 97%
rename from web/app/components/workflow/utils/layout.ts
rename to web/app/components/workflow/utils/elk-layout.ts
index b3cf3b0d88..69acbf9aff 100644
--- a/web/app/components/workflow/utils/layout.ts
+++ b/web/app/components/workflow/utils/elk-layout.ts
@@ -4,18 +4,18 @@ import { cloneDeep } from 'lodash-es'
import type {
Edge,
Node,
-} from '../types'
+} from '@/app/components/workflow/types'
import {
BlockEnum,
-} from '../types'
+} from '@/app/components/workflow/types'
import {
CUSTOM_NODE,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_VERTICAL_PADDING,
-} from '../constants'
-import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
-import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
-import type { CaseItem, IfElseNodeType } from '../nodes/if-else/types'
+} from '@/app/components/workflow/constants'
+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 type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
// Although the file name refers to Dagre, the implementation now relies on ELK's layered algorithm.
// Keep the export signatures unchanged to minimise the blast radius while we migrate the layout stack.
diff --git a/web/app/components/workflow/utils/index.ts b/web/app/components/workflow/utils/index.ts
index e9ae2d1ef0..53a423de34 100644
--- a/web/app/components/workflow/utils/index.ts
+++ b/web/app/components/workflow/utils/index.ts
@@ -1,7 +1,7 @@
export * from './node'
export * from './edge'
export * from './workflow-init'
-export * from './layout'
+export * from './elk-layout'
export * from './common'
export * from './tool'
export * from './workflow'
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index 1be802460b..c83ea7fd85 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -57,6 +57,7 @@ const LocaleLayout = async ({
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL,
[DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL,
+ [DatasetAttr.DATA_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX]: process.env.NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_WIDGET_KEY]: process.env.NEXT_PUBLIC_ZENDESK_WIDGET_KEY,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_ENVIRONMENT,
[DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION]: process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_VERSION,
diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx
index 920a992b4f..29e21b8ba2 100644
--- a/web/app/signin/normal-form.tsx
+++ b/web/app/signin/normal-form.tsx
@@ -135,8 +135,8 @@ const NormalForm = () => {
{!systemFeatures.branding.enabled && {t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}
}
: