Browse Source

feat: the frontend part of mcp (#22131)

Co-authored-by: jZonG <jzongcode@gmail.com>
Co-authored-by: Novice <novice12185727@gmail.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
tags/1.6.0
Joel 3 months ago
parent
commit
5375d9bb27
No account linked to committer's email address
100 changed files with 4105 additions and 518 deletions
  1. 8
    0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx
  2. 15
    18
      web/app/components/app-sidebar/basic.tsx
  3. 50
    18
      web/app/components/app/configuration/config/agent/agent-tools/index.tsx
  4. 7
    5
      web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
  5. 1
    0
      web/app/components/app/overview/appCard.tsx
  6. 3
    0
      web/app/components/base/app-icon/index.tsx
  7. 7
    0
      web/app/components/base/icons/assets/vender/line/others/search-menu.svg
  8. 4
    0
      web/app/components/base/icons/assets/vender/other/mcp.svg
  9. 36
    0
      web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg
  10. 7
    0
      web/app/components/base/icons/assets/vender/workflow/window-cursor.svg
  11. 77
    0
      web/app/components/base/icons/src/vender/line/others/SearchMenu.json
  12. 20
    0
      web/app/components/base/icons/src/vender/line/others/SearchMenu.tsx
  13. 1
    0
      web/app/components/base/icons/src/vender/line/others/index.ts
  14. 35
    0
      web/app/components/base/icons/src/vender/other/Mcp.json
  15. 20
    0
      web/app/components/base/icons/src/vender/other/Mcp.tsx
  16. 279
    0
      web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json
  17. 20
    0
      web/app/components/base/icons/src/vender/other/NoToolPlaceholder.tsx
  18. 2
    0
      web/app/components/base/icons/src/vender/other/index.ts
  19. 62
    0
      web/app/components/base/icons/src/vender/workflow/WindowCursor.json
  20. 20
    0
      web/app/components/base/icons/src/vender/workflow/WindowCursor.tsx
  21. 1
    0
      web/app/components/base/icons/src/vender/workflow/index.ts
  22. 21
    4
      web/app/components/base/prompt-editor/index.tsx
  23. 0
    1
      web/app/components/base/prompt-editor/plugins/custom-text/node.tsx
  24. 1
    1
      web/app/components/base/prompt-editor/plugins/placeholder.tsx
  25. 6
    0
      web/app/components/header/account-setting/model-provider-page/declarations.ts
  26. 3
    0
      web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx
  27. 55
    35
      web/app/components/plugins/marketplace/search-box/index.tsx
  28. 8
    42
      web/app/components/plugins/marketplace/search-box/tags-filter.tsx
  29. 27
    1
      web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx
  30. 30
    32
      web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx
  31. 199
    107
      web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx
  32. 59
    0
      web/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal.tsx
  33. 17
    5
      web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx
  34. 5
    0
      web/app/components/plugins/types.ts
  35. 39
    10
      web/app/components/tools/add-tool-modal/empty.tsx
  36. 1
    0
      web/app/components/tools/edit-custom-collection-modal/modal.tsx
  37. 75
    0
      web/app/components/tools/mcp/create-card.tsx
  38. 308
    0
      web/app/components/tools/mcp/detail/content.tsx
  39. 37
    0
      web/app/components/tools/mcp/detail/list-loading.tsx
  40. 88
    0
      web/app/components/tools/mcp/detail/operation-dropdown.tsx
  41. 56
    0
      web/app/components/tools/mcp/detail/provider-detail.tsx
  42. 41
    0
      web/app/components/tools/mcp/detail/tool-item.tsx
  43. 12
    0
      web/app/components/tools/mcp/hooks.ts
  44. 98
    0
      web/app/components/tools/mcp/index.tsx
  45. 134
    0
      web/app/components/tools/mcp/mcp-server-modal.tsx
  46. 37
    0
      web/app/components/tools/mcp/mcp-server-param-item.tsx
  47. 244
    0
      web/app/components/tools/mcp/mcp-service-card.tsx
  48. 154
    0
      web/app/components/tools/mcp/mock.ts
  49. 221
    0
      web/app/components/tools/mcp/modal.tsx
  50. 152
    0
      web/app/components/tools/mcp/provider-card.tsx
  51. 38
    16
      web/app/components/tools/provider-list.tsx
  52. 12
    12
      web/app/components/tools/provider/custom-create-card.tsx
  53. 1
    1
      web/app/components/tools/provider/tool-item.tsx
  54. 13
    0
      web/app/components/tools/types.ts
  55. 104
    4
      web/app/components/tools/utils/to-form-schema.ts
  56. 4
    2
      web/app/components/workflow-app/components/workflow-header/features-trigger.tsx
  57. 24
    26
      web/app/components/workflow/block-selector/all-tools.tsx
  58. 10
    3
      web/app/components/workflow/block-selector/hooks.ts
  59. 2
    2
      web/app/components/workflow/block-selector/index-bar.tsx
  60. 26
    24
      web/app/components/workflow/block-selector/index.tsx
  61. 15
    13
      web/app/components/workflow/block-selector/market-place-plugin/list.tsx
  62. 18
    10
      web/app/components/workflow/block-selector/tabs.tsx
  63. 20
    6
      web/app/components/workflow/block-selector/tool-picker.tsx
  64. 9
    11
      web/app/components/workflow/block-selector/tool/action-item.tsx
  65. 33
    20
      web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx
  66. 9
    0
      web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx
  67. 9
    0
      web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx
  68. 120
    34
      web/app/components/workflow/block-selector/tool/tool.tsx
  69. 24
    9
      web/app/components/workflow/block-selector/tools.tsx
  70. 5
    0
      web/app/components/workflow/block-selector/types.ts
  71. 31
    0
      web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts
  72. 13
    1
      web/app/components/workflow/hooks/use-workflow.ts
  73. 1
    0
      web/app/components/workflow/index.tsx
  74. 14
    8
      web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx
  75. 11
    2
      web/app/components/workflow/nodes/_base/components/agent-strategy.tsx
  76. 1
    1
      web/app/components/workflow/nodes/_base/components/editor/base.tsx
  77. 1
    1
      web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
  78. 35
    0
      web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx
  79. 279
    0
      web/app/components/workflow/nodes/_base/components/form-input-item.tsx
  80. 47
    0
      web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx
  81. 22
    0
      web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx
  82. 1
    1
      web/app/components/workflow/nodes/_base/components/setting-item.tsx
  83. 1
    0
      web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx
  84. 3
    0
      web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx
  85. 6
    1
      web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx
  86. 6
    0
      web/app/components/workflow/nodes/_base/node.tsx
  87. 35
    19
      web/app/components/workflow/nodes/agent/components/tool-icon.tsx
  88. 16
    2
      web/app/components/workflow/nodes/agent/default.ts
  89. 1
    1
      web/app/components/workflow/nodes/agent/node.tsx
  90. 4
    1
      web/app/components/workflow/nodes/agent/panel.tsx
  91. 3
    0
      web/app/components/workflow/nodes/agent/types.ts
  92. 43
    2
      web/app/components/workflow/nodes/agent/use-config.ts
  93. 3
    1
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts
  94. 9
    4
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx
  95. 5
    1
      web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx
  96. 51
    0
      web/app/components/workflow/nodes/tool/components/copy-id.tsx
  97. 62
    0
      web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx
  98. 51
    0
      web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx
  99. 51
    0
      web/app/components/workflow/nodes/tool/components/tool-form/index.tsx
  100. 0
    0
      web/app/components/workflow/nodes/tool/components/tool-form/item.tsx

+ 8
- 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
import { ToastContext } from '@/app/components/base/toast'
import {
fetchAppDetail,
@@ -31,6 +32,8 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)

const showMCPCard = isInPanel

const updateAppDetail = async () => {
try {
const res = await fetchAppDetail({ url: '/apps', id: appId })
@@ -117,6 +120,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
isInPanel={isInPanel}
onChangeStatus={onChangeApiStatus}
/>
{showMCPCard && (
<MCPServiceCard
appInfo={appDetail}
/>
)}
</div>
)
}

+ 15
- 18
web/app/components/app-sidebar/basic.tsx View File

@@ -2,6 +2,10 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
import {
Code,
WindowCursor,
} from '@/app/components/base/icons/src/vender/workflow'

export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
@@ -14,25 +18,13 @@ export type IAppBasicProps = {
textStyle?: { main?: string; extra?: string }
isExtraInLine?: boolean
mode?: string
hideType?: boolean
}

const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>

const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>

const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>

const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_6294_13848)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
@@ -48,13 +40,17 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm

const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <AppIcon innerIcon={ApiSvg} className='border !border-purple-200 !bg-purple-50' />,
api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'>
<Code className='h-4 w-4 text-text-primary-on-surface' />
</div>,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !border-primary-200 !bg-primary-100' />,
webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
</div>,
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
}

export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) {
const { t } = useTranslation()

return (
@@ -88,9 +84,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
/>
}
</div>
{isExtraInLine ? (
{!hideType && isExtraInLine && (
<div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div>
) : (
)}
{!hideType && !isExtraInLine && (
<div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div>
)}
</div>}

+ 50
- 18
web/app/components/app/configuration/config/agent/agent-tools/index.tsx View File

@@ -30,15 +30,31 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import { canFindTool } from '@/utils'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { useMittContextSelector } from '@/context/mitt-context'

type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
const AgentTools: FC = () => {
const { t } = useTranslation()
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext)
const { modelConfig, setModelConfig } = useContext(ConfigContext)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const collectionList = useMemo(() => {
const allTools = [
...(buildInTools || []),
...(customTools || []),
...(workflowTools || []),
...(mcpTools || []),
]
return allTools
}, [buildInTools, customTools, workflowTools, mcpTools])

const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
@@ -96,23 +112,38 @@ const AgentTools: FC = () => {
}

const [isDeleting, setIsDeleting] = useState<number>(-1)

const getToolValue = (tool: ToolDefaultValue) => {
return {
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.push({
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
})
draft.agentConfig.tools.push(getToolValue(tool))
})
setModelConfig(newModelConfig)
}

const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.push(...tool.map(getToolValue))
})
setModelConfig(newModelConfig)
}
const getProviderShowName = (item: AgentTool) => {
const type = item.provider_type
if(type === CollectionType.builtIn)
return item.provider_name.split('/').pop()
return item.provider_name
}

return (
<>
<Panel
@@ -143,7 +174,9 @@ const AgentTools: FC = () => {
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
selectedTools={tools as any}
onSelectMultiple={handleSelectMultipleTool}
selectedTools={tools as unknown as ToolValue[]}
canChooseMCPTool
/>
</>
)}
@@ -161,7 +194,7 @@ const AgentTools: FC = () => {
<div className='flex w-0 grow items-center'>
{item.isDeleted && <DefaultToolIcon className='h-5 w-5' />}
{!item.isDeleted && (
<div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}>
<div className={cn((item.notAuthor || !item.enabled) && 'shrink-0 opacity-50')}>
{typeof item.icon === 'string' && <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${item.icon})` }} />}
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />}
</div>
@@ -172,7 +205,7 @@ const AgentTools: FC = () => {
(item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '',
)}
>
<span className='system-xs-medium pr-1.5 text-text-secondary'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span>
<span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
<span className='text-text-tertiary'>{item.tool_label}</span>
{!item.isDeleted && (
<Tooltip
@@ -285,8 +318,7 @@ const AgentTools: FC = () => {
<SettingBuiltInTool
toolName={currentTool?.tool_name as string}
setting={currentTool?.tool_parameters}
collection={currentTool?.collection as Collection}
isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn}
collection={currentTool?.collection as ToolWithProvider}
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}

+ 7
- 5
web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx View File

@@ -24,10 +24,11 @@ import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWor
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import type { ToolWithProvider } from '@/app/components/workflow/types'

type Props = {
showBackButton?: boolean
collection: Collection
collection: Collection | ToolWithProvider
isBuiltIn?: boolean
isModel?: boolean
toolName: string
@@ -51,9 +52,10 @@ const SettingBuiltInTool: FC<Props> = ({
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const { t } = useTranslation()

const [isLoading, setIsLoading] = useState(true)
const [tools, setTools] = useState<Tool[]>([])
const passedTools = (collection as ToolWithProvider).tools
const hasPassedTools = passedTools?.length > 0
const [isLoading, setIsLoading] = useState(!hasPassedTools)
const [tools, setTools] = useState<Tool[]>(hasPassedTools ? passedTools : [])
const currTool = tools.find(tool => tool.name === toolName)
const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []
const infoSchemas = formSchemas.filter(item => item.form === 'llm')
@@ -63,7 +65,7 @@ const SettingBuiltInTool: FC<Props> = ({
const [currType, setCurrType] = useState('info')
const isInfoActive = currType === 'info'
useEffect(() => {
if (!collection)
if (!collection || hasPassedTools)
return

(async () => {

+ 1
- 0
web/app/components/app/overview/appCard.tsx View File

@@ -181,6 +181,7 @@ function AppCard({
icon={appInfo.icon}
icon_background={appInfo.icon_background}
name={basicName}
hideType
type={
isApp
? t('appOverview.overview.appInfo.explanation')

+ 3
- 0
web/app/components/base/app-icon/index.tsx View File

@@ -18,6 +18,7 @@ export type AppIconProps = {
imageUrl?: string | null
className?: string
innerIcon?: React.ReactNode
coverElement?: React.ReactNode
onClick?: () => void
}
const appIconVariants = cva(
@@ -51,6 +52,7 @@ const AppIcon: FC<AppIconProps> = ({
imageUrl,
className,
innerIcon,
coverElement,
onClick,
}) => {
const isValidImageIcon = iconType === 'image' && imageUrl
@@ -65,6 +67,7 @@ const AppIcon: FC<AppIconProps> = ({
? <img src={imageUrl} className="h-full w-full" alt="app icon" />
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
}
{coverElement}
</span>
}


+ 7
- 0
web/app/components/base/icons/assets/vender/line/others/search-menu.svg View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 4
- 0
web/app/components/base/icons/assets/vender/other/mcp.svg View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z" fill="white"/>
<path d="M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z" fill="white"/>
</svg>

+ 36
- 0
web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg View File

@@ -0,0 +1,36 @@
<svg width="204" height="36" viewBox="0 0 204 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.1">
<path d="M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z" fill="#101828" fill-opacity="0.3"/>
</g>
<g opacity="0.3">
<path d="M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z" fill="#101828" fill-opacity="0.3"/>
</g>
<g opacity="0.6">
<path d="M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z" fill="#101828" fill-opacity="0.3"/>
</g>
<rect x="84.5" y="0.5" width="35" height="35" rx="9.5" stroke="#101828" stroke-opacity="0.04"/>
<path d="M96.167 16.3333H107.834V25.5H96.167V16.3333Z" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M100.333 19.6667H103.666" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M107.833 12.1667L105.583 15.9167" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z" fill="#676F83"/>
<path d="M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z" fill="#676F83"/>
<g opacity="0.6" clip-path="url(#clip0_9296_51042)">
<path d="M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z" fill="#101828" fill-opacity="0.3"/>
</g>
<g opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z" fill="#101828" fill-opacity="0.3"/>
<path d="M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z" fill="#101828" fill-opacity="0.3"/>
<path d="M163.666 18H161.333V16.6667H163.666V18Z" fill="#101828" fill-opacity="0.3"/>
<path d="M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z" fill="#101828" fill-opacity="0.3"/>
<path d="M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z" fill="#101828" fill-opacity="0.3"/>
<path d="M168 13.6667H166.666V11.3333H168V13.6667Z" fill="#101828" fill-opacity="0.3"/>
</g>
<g opacity="0.1">
<path d="M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z" fill="#101828" fill-opacity="0.3"/>
</g>
<defs>
<clipPath id="clip0_9296_51042">
<rect width="16" height="16" fill="white" transform="translate(132 10)"/>
</clipPath>
</defs>
</svg>

+ 7
- 0
web/app/components/base/icons/assets/vender/workflow/window-cursor.svg View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z" fill="white"/>
<path d="M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z" fill="white"/>
<path d="M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z" fill="white"/>
<path d="M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z" fill="white"/>
<path d="M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z" fill="white"/>
</svg>

+ 77
- 0
web/app/components/base/icons/src/vender/line/others/SearchMenu.json View File

@@ -0,0 +1,77 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "32",
"height": "32",
"viewBox": "0 0 32 32",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 16H6.67155",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 9.33334H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.00488 22.6667H8.00488",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M26 22L29.3333 25.3333",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "SearchMenu"
}

+ 20
- 0
web/app/components/base/icons/src/vender/line/others/SearchMenu.tsx View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './SearchMenu.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'SearchMenu'

export default Icon

+ 1
- 0
web/app/components/base/icons/src/vender/line/others/index.ts View File

@@ -9,4 +9,5 @@ export { default as GlobalVariable } from './GlobalVariable'
export { default as Icon3Dots } from './Icon3Dots'
export { default as LongArrowLeft } from './LongArrowLeft'
export { default as LongArrowRight } from './LongArrowRight'
export { default as SearchMenu } from './SearchMenu'
export { default as Tools } from './Tools'

+ 35
- 0
web/app/components/base/icons/src/vender/other/Mcp.json View File

@@ -0,0 +1,35 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Mcp"
}

+ 20
- 0
web/app/components/base/icons/src/vender/other/Mcp.tsx View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './Mcp.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'Mcp'

export default Icon

+ 279
- 0
web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json View File

@@ -0,0 +1,279 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "204",
"height": "36",
"viewBox": "0 0 204 36",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.1"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.3"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.6"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "rect",
"attributes": {
"x": "84.5",
"y": "0.5",
"width": "35",
"height": "35",
"rx": "9.5",
"stroke": "#101828",
"stroke-opacity": "0.04"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M96.167 16.3333H107.834V25.5H96.167V16.3333Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M100.333 19.6667H103.666",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M107.833 12.1667L105.583 15.9167",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.6",
"clip-path": "url(#clip0_9296_51042)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.3"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M163.666 18H161.333V16.6667H163.666V18Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M168 13.6667H166.666V11.3333H168V13.6667Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"opacity": "0.1"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z",
"fill": "currentColor",
"fill-opacity": "0.3"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_9296_51042"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "16",
"height": "16",
"fill": "white",
"transform": "translate(132 10)"
},
"children": []
}
]
}
]
}
]
},
"name": "NoToolPlaceholder"
}

+ 20
- 0
web/app/components/base/icons/src/vender/other/NoToolPlaceholder.tsx View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './NoToolPlaceholder.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'NoToolPlaceholder'

export default Icon

+ 2
- 0
web/app/components/base/icons/src/vender/other/index.ts View File

@@ -1,5 +1,7 @@
export { default as AnthropicText } from './AnthropicText'
export { default as Generator } from './Generator'
export { default as Group } from './Group'
export { default as Mcp } from './Mcp'
export { default as NoToolPlaceholder } from './NoToolPlaceholder'
export { default as Openai } from './Openai'
export { default as ReplayLine } from './ReplayLine'

+ 62
- 0
web/app/components/base/icons/src/vender/workflow/WindowCursor.json View File

@@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "WindowCursor"
}

+ 20
- 0
web/app/components/base/icons/src/vender/workflow/WindowCursor.tsx View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './WindowCursor.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'WindowCursor'

export default Icon

+ 1
- 0
web/app/components/base/icons/src/vender/workflow/index.ts View File

@@ -19,3 +19,4 @@ export { default as ParameterExtractor } from './ParameterExtractor'
export { default as QuestionClassifier } from './QuestionClassifier'
export { default as TemplatingTransform } from './TemplatingTransform'
export { default as VariableX } from './VariableX'
export { default as WindowCursor } from './WindowCursor'

+ 21
- 4
web/app/components/base/prompt-editor/index.tsx View File

@@ -64,8 +64,9 @@ import cn from '@/utils/classnames'
export type PromptEditorProps = {
instanceId?: string
compact?: boolean
wrapperClassName?: string
className?: string
placeholder?: string
placeholder?: string | JSX.Element
placeholderClassName?: string
style?: React.CSSProperties
value?: string
@@ -85,6 +86,7 @@ export type PromptEditorProps = {
const PromptEditor: FC<PromptEditorProps> = ({
instanceId,
compact,
wrapperClassName,
className,
placeholder,
placeholderClassName,
@@ -147,10 +149,25 @@ const PromptEditor: FC<PromptEditorProps> = ({

return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative min-h-5'>
<div className={cn('relative', wrapperClassName)}>
<RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'text-[13px] leading-5' : 'text-sm leading-6'} text-text-secondary`} style={style || {}} />}
placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />}
contentEditable={
<ContentEditable
className={cn(
'text-text-secondary outline-none',
compact ? 'text-[13px] leading-5' : 'text-sm leading-6',
className,
)}
style={style || {}}
/>
}
placeholder={
<Placeholder
value={placeholder}
className={cn('truncate', placeholderClassName)}
compact={compact}
/>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<ComponentPickerBlock

+ 0
- 1
web/app/components/base/prompt-editor/plugins/custom-text/node.tsx View File

@@ -16,7 +16,6 @@ export class CustomTextNode extends TextNode {

createDOM(config: EditorConfig) {
const dom = super.createDOM(config)
dom.classList.add('align-middle')
return dom
}


+ 1
- 1
web/app/components/base/prompt-editor/plugins/placeholder.tsx View File

@@ -8,7 +8,7 @@ const Placeholder = ({
className,
}: {
compact?: boolean
value?: string
value?: string | JSX.Element
className?: string
}) => {
const { t } = useTranslation()

+ 6
- 0
web/app/components/header/account-setting/model-provider-page/declarations.ts View File

@@ -1,3 +1,5 @@
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'

export type FormValue = Record<string, any>

export type TypeWithI18N<T = string> = {
@@ -19,6 +21,8 @@ export enum FormTypeEnum {
toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
object = 'object',
array = 'array',
dynamicSelect = 'dynamic-select',
}

@@ -109,6 +113,7 @@ export type FormShowOnObject = {
}

export type CredentialFormSchemaBase = {
name: string
variable: string
label: TypeWithI18N
type: FormTypeEnum
@@ -118,6 +123,7 @@ export type CredentialFormSchemaBase = {
show_on: FormShowOnObject[]
url?: string
scope?: string
input_schema?: SchemaRoot
}

export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & {

+ 3
- 0
web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx View File

@@ -54,6 +54,7 @@ type FormProps<
nodeId?: string
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
canChooseMCPTool?: boolean
}

function Form<
@@ -79,6 +80,7 @@ function Form<
nodeId,
nodeOutputVars,
availableNodes,
canChooseMCPTool,
}: FormProps<CustomFormSchema>) {
const language = useLanguage()
const [changeKey, setChangeKey] = useState('')
@@ -377,6 +379,7 @@ function Form<
value={value[variable] || []}
onChange={item => handleFormChange(variable, item as any)}
supportCollapse
canChooseMCPTool={canChooseMCPTool}
/>
{fieldMoreInfo?.(formSchema)}
{validating && changeKey === variable && <ValidatingTip />}

+ 55
- 35
web/app/components/plugins/marketplace/search-box/index.tsx View File

@@ -1,8 +1,9 @@
'use client'
import { RiCloseLine } from '@remixicon/react'
import { RiCloseLine, RiSearchLine } from '@remixicon/react'
import TagsFilter from './tags-filter'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import { RiAddLine } from '@remixicon/react'

type SearchBoxProps = {
search: string
@@ -13,6 +14,9 @@ type SearchBoxProps = {
size?: 'small' | 'large'
placeholder?: string
locale?: string
supportAddCustomTool?: boolean
onShowAddCustomCollectionModal?: () => void
onAddedCustomTool?: () => void
}
const SearchBox = ({
search,
@@ -23,46 +27,62 @@ const SearchBox = ({
size = 'small',
placeholder = '',
locale,
supportAddCustomTool,
onShowAddCustomCollectionModal,
}: SearchBoxProps) => {
return (
<div
className={cn(
'z-[11] flex items-center',
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
inputClassName,
)}
className='z-[11] flex items-center'
>
<TagsFilter
tags={tags}
onTagsChange={onTagsChange}
size={size}
locale={locale}
/>
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
<div className='relative flex grow items-center p-1 pl-2'>
<div className='mr-2 flex w-full items-center'>
<input
className={cn(
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
)}
value={search}
onChange={(e) => {
onSearchChange(e.target.value)
}}
placeholder={placeholder}
/>
{
search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
)
}
<div className={
cn('flex items-center',
size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md',
size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5',
inputClassName,
)
}>
<div className='relative flex grow items-center p-1 pl-2'>
<div className='mr-2 flex w-full items-center'>
<RiSearchLine className='mr-1.5 size-4 text-text-placeholder' />
<input
className={cn(
'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none',
)}
value={search}
onChange={(e) => {
onSearchChange(e.target.value)
}}
placeholder={placeholder}
/>
{
search && (
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
)
}
</div>
</div>
<div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div>
<TagsFilter
tags={tags}
onTagsChange={onTagsChange}
size={size}
locale={locale}
/>
</div>
{supportAddCustomTool && (
<div className='flex shrink-0 items-center'>
<ActionButton
className='ml-2 rounded-full bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
onClick={onShowAddCustomCollectionModal}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
</div>
)}
</div>
)
}

+ 8
- 42
web/app/components/plugins/marketplace/search-box/tags-filter.tsx View File

@@ -2,9 +2,7 @@

import { useState } from 'react'
import {
RiArrowDownSLine,
RiCloseCircleFill,
RiFilter3Line,
RiPriceTag3Line,
} from '@remixicon/react'
import {
PortalToFollowElem,
@@ -57,47 +55,15 @@ const TagsFilter = ({
onClick={() => setOpen(v => !v)}
>
<div className={cn(
'flex cursor-pointer items-center rounded-lg text-text-tertiary hover:bg-state-base-hover',
size === 'large' && 'h-8 px-2 py-1',
size === 'small' && 'h-7 py-0.5 pl-1 pr-1.5 ',
selectedTagsLength && 'text-text-secondary',
open && 'bg-state-base-hover',
'ml-0.5 mr-1.5 flex items-center text-text-tertiary ',
size === 'large' && 'h-8 py-1',
size === 'small' && 'h-7 py-0.5 ',
// selectedTagsLength && 'text-text-secondary',
// open && 'bg-state-base-hover',
)}>
<div className='p-0.5'>
<RiFilter3Line className='h-4 w-4' />
<div className='cursor-pointer rounded-md p-0.5 hover:bg-state-base-hover'>
<RiPriceTag3Line className='h-4 w-4 text-text-tertiary' />
</div>
<div className={cn(
'system-sm-medium flex items-center p-1',
size === 'large' && 'p-1',
size === 'small' && 'px-0.5 py-1',
)}>
{
!selectedTagsLength && t('pluginTags.allTags')
}
{
!!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',')
}
{
selectedTagsLength > 2 && (
<div className='system-xs-medium ml-1 text-text-tertiary'>
+{selectedTagsLength - 2}
</div>
)
}
</div>
{
!!selectedTagsLength && (
<RiCloseCircleFill
className='h-4 w-4 cursor-pointer text-text-quaternary'
onClick={() => onTagsChange([])}
/>
)
}
{
!selectedTagsLength && (
<RiArrowDownSLine className='h-4 w-4' />
)
}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>

+ 27
- 1
web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx View File

@@ -13,6 +13,7 @@ import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { useAllMCPTools } from '@/service/use-tools'

type Props = {
disabled?: boolean
@@ -26,6 +27,7 @@ type Props = {
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string
canChooseMCPTool?: boolean
}

const MultipleToolSelector = ({
@@ -40,9 +42,16 @@ const MultipleToolSelector = ({
nodeOutputVars,
availableNodes,
nodeId,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
const enabledCount = value.filter(item => item.enabled).length
const { data: mcpTools } = useAllMCPTools()
const enabledCount = value.filter((item) => {
const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name)
if(isMCPTool)
return item.enabled && canChooseMCPTool
return item.enabled
}).length
// collapse control
const [collapse, setCollapse] = React.useState(false)
const handleCollapse = () => {
@@ -66,6 +75,19 @@ const MultipleToolSelector = ({
setOpen(false)
}

const handleAddMultiple = (val: ToolValue[]) => {
const newValue = [...value, ...val]
// deduplication
const deduplication = newValue.reduce((acc, cur) => {
if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name))
acc.push(cur)
return acc
}, [] as ToolValue[])
// update value
onChange(deduplication)
setOpen(false)
}

// delete tool
const handleDelete = (index: number) => {
const newValue = [...value]
@@ -140,8 +162,10 @@ const MultipleToolSelector = ({
value={item}
selectedTools={value}
onSelect={item => handleConfigure(item, index)}
onSelectMultiple={handleAddMultiple}
onDelete={() => handleDelete(index)}
supportEnableSwitch
canChooseMCPTool={canChooseMCPTool}
isEdit
/>
</div>
@@ -164,6 +188,8 @@ const MultipleToolSelector = ({
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}
isEdit={false}
canChooseMCPTool={canChooseMCPTool}
onSelectMultiple={handleAddMultiple}
/>
</>
)

+ 30
- 32
web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx View File

@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
RiArrowLeftLine,
RiArrowRightUpLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
@@ -15,6 +14,7 @@ import {
import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form'
@@ -23,13 +23,13 @@ import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'

import { useAppContext } from '@/context/app-context'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
useUpdateProviderCredentials,
@@ -54,15 +54,9 @@ type Props = {
scope?: string
value?: ToolValue
selectedTools?: ToolValue[]
onSelect: (tool: ToolValue) => void
onSelectMultiple: (tool: ToolValue[]) => void
isEdit?: boolean
onSelect: (tool: {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
extra?: Record<string, any>
}) => void
onDelete?: () => void
supportEnableSwitch?: boolean
supportAddCustomTool?: boolean
@@ -74,6 +68,7 @@ type Props = {
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string,
canChooseMCPTool?: boolean,
}
const ToolSelector: FC<Props> = ({
value,
@@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({
placement = 'left',
offset = 4,
onSelect,
onSelectMultiple,
onDelete,
scope,
supportEnableSwitch,
@@ -94,6 +90,7 @@ const ToolSelector: FC<Props> = ({
nodeOutputVars,
availableNodes,
nodeId = '',
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
@@ -105,6 +102,7 @@ const ToolSelector: FC<Props> = ({
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()

@@ -112,18 +110,19 @@ const ToolSelector: FC<Props> = ({
const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)

const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider_name
})
}, [value, buildInTools, customTools, workflowTools])
}, [value, buildInTools, customTools, workflowTools, mcpTools])

const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const getToolValue = (tool: ToolDefaultValue) => {
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
const toolValue = {
return {
provider_name: tool.provider_id,
provider_show_name: tool.provider_name,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
@@ -136,9 +135,16 @@ const ToolSelector: FC<Props> = ({
},
schemas: tool.paramSchemas,
}
}
const handleSelectTool = (tool: ToolDefaultValue) => {
const toolValue = getToolValue(tool)
onSelect(toolValue)
// setIsShowChooseTool(false)
}
const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => {
const toolValues = tool.map(item => getToolValue(item))
onSelectMultiple(toolValues)
}

const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onSelect({
@@ -169,7 +175,6 @@ const ToolSelector: FC<Props> = ({

const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)

const toolValue = {
...value,
settings: newValue,
@@ -250,7 +255,9 @@ const ToolSelector: FC<Props> = ({
<ToolItem
open={isShow}
icon={currentProvider?.icon || manifestIcon}
isMCPTool={currentProvider?.type === CollectionType.mcp}
providerName={value.provider_name}
providerShowName={value.provider_show_name}
toolLabel={value.tool_label || value.tool_name}
showSwitch={supportEnableSwitch}
switchValue={value.enabled}
@@ -272,6 +279,7 @@ const ToolSelector: FC<Props> = ({
</p>
</div>
}
canChooseMCPTool={canChooseMCPTool}
/>
)}
</PortalToFollowElemTrigger>
@@ -285,7 +293,6 @@ const ToolSelector: FC<Props> = ({
<div className='flex flex-col gap-1'>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
<ToolPicker
panelClassName='w-[328px]'
placement='bottom'
offset={offset}
trigger={
@@ -300,8 +307,10 @@ const ToolSelector: FC<Props> = ({
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
<div className='flex flex-col gap-1'>
@@ -390,24 +399,13 @@ const ToolSelector: FC<Props> = ({
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<Form
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
formSchemas={settingsFormSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 h-3 w-3' />
</a>)
: null}
/>
</div>
)}

+ 199
- 107
web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx View File

@@ -3,25 +3,34 @@ import { useTranslation } from 'react-i18next'
import produce from 'immer'
import {
RiArrowRightUpLine,
RiBracesLine,
} from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
import Input from '@/app/components/base/input'
import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch'
import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean'
import { SimpleSelect } from '@/app/components/base/select'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node } from 'reactflow'
import type {
NodeOutPutVar,
ValueSelector,
Var,
} from '@/app/components/workflow/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { useBoolean } from 'ahooks'
import SchemaModal from './schema-modal'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'

type Props = {
value: Record<string, any>
@@ -42,73 +51,46 @@ const ReasoningConfigForm: React.FC<Props> = ({
}) => {
const { t } = useTranslation()
const language = useLanguage()
const handleAutomatic = (key: string, val: any) => {
const getVarKindType = (type: FormTypeEnum) => {
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return VarKindType.variable
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber || type === FormTypeEnum.array || type === FormTypeEnum.object)
return VarKindType.constant
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
return VarKindType.mixed
}

const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => {
onChange({
...value,
[key]: {
value: val ? null : value[key]?.value,
value: val ? null : { type: getVarKindType(type), value: null },
auto: val ? 1 : 0,
},
})
}

const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
setInputsIsFocus((prev) => {
return {
...prev,
[variable]: value,
}
})
}
}, [])
const handleNotMixedTypeChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.type = varKindType
target.value = varValue
}
else {
draft[variable].value = {
type: varKindType,
value: varValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.value = itemValue
}
else {
draft[variable].value = {
type: VarKindType.mixed,
value: itemValue,
}
const handleTypeChange = useCallback((variable: string, defaultValue: any) => {
return (newType: VarKindType) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: newType,
value: newType === VarKindType.variable ? '' : defaultValue,
}
})
onChange(newValue)
onChange(res)
}
}, [value, onChange])
const handleFileChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
}, [onChange, value])
const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => {
return (newValue: any) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: varValue,
type: getVarKindType(varType),
value: newValue,
}
})
onChange(newValue)
onChange(res)
}
}, [value, onChange])
}, [onChange, value])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
@@ -132,9 +114,29 @@ const ReasoningConfigForm: React.FC<Props> = ({
onChange(newValue)
}
}, [onChange, value])
const handleVariableSelectorChange = useCallback((variable: string) => {
return (newValue: ValueSelector | string) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: newValue,
}
})
onChange(res)
}
}, [onChange, value])

const renderField = (schema: any) => {
const [isShowSchema, {
setTrue: showSchema,
setFalse: hideSchema,
}] = useBoolean(false)

const [schema, setSchema] = useState<SchemaRoot | null>(null)
const [schemaRootName, setSchemaRootName] = useState<string>('')

const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => {
const {
default: defaultValue,
variable,
label,
required,
@@ -142,6 +144,9 @@ const ReasoningConfigForm: React.FC<Props> = ({
type,
scope,
url,
input_schema,
placeholder,
options,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
@@ -149,89 +154,150 @@ const ReasoningConfigForm: React.FC<Props> = ({
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
triggerClassName='ml-0.5 w-4 h-4'
asChild={false} />
))
const varInput = value[variable].value
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isObject = type === FormTypeEnum.object
const isArray = type === FormTypeEnum.array
const isShowJSONEditor = isObject || isArray
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isBoolean = type === FormTypeEnum.boolean
const isSelect = type === FormTypeEnum.select
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
const showTypeSwitch = isNumber || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const targetVarType = () => {
if (isString)
return VarType.string
else if (isNumber)
return VarType.number
else if (type === FormTypeEnum.files)
return VarType.arrayFile
else if (type === FormTypeEnum.file)
return VarType.file
else if (isBoolean)
return VarType.boolean
else if (isObject)
return VarType.object
else if (isArray)
return VarType.arrayObject
else
return VarType.string
}
const getFilterVar = () => {
if (isNumber)
return (varPayload: any) => varPayload.type === VarType.number
else if (isString)
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
else if (isFile)
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
else if (isBoolean)
return (varPayload: any) => varPayload.type === VarType.boolean
else if (isObject)
return (varPayload: any) => varPayload.type === VarType.object
else if (isArray)
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
return undefined
}

return (
<div key={variable} className='space-y-1'>
<div key={variable} className='space-y-0.5'>
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn('code-sm-semibold text-text-secondary')}>{label[language] || label.en_US}</span>
<div className='flex items-center'>
<span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}
{tooltipContent}
<span className='system-xs-regular mx-1 text-text-quaternary'>·</span>
<span className='system-xs-regular text-text-tertiary'>{targetVarType()}</span>
{isShowJSONEditor && (
<Tooltip
popupContent={<div className='system-xs-medium text-text-secondary'>
{t('workflow.nodes.agent.clickToViewParameterSchema')}
</div>}
asChild={false}>
<div
className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
onClick={() => showSchema(input_schema as SchemaRoot, label[language] || label.en_US)}
>
<RiBracesLine className='size-3.5'/>
</div>
</Tooltip>
)}

</div>
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto, type)}>
<span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span>
<Switch
size='xs'
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val)}
onChange={val => handleAutomatic(variable, val, type)}
/>
</div>
</div>
{auto === 0 && (
<>
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
{showTypeSwitch && (
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange(variable, defaultValue)}/>
)}
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')}
<MixedVariableTextInput
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
onChange={handleValueChange(variable, type)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onFocusChange={handleInputFocus(variable)}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{/* {isString && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
{isNumber && isConstant && (
<Input
className='h-8 grow'
type='number'
value={varInput?.value || ''}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string}
onChange={handleValueChange(variable, type)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)} */}
{(isNumber || isSelect) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue
filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined}
availableVars={isSelect ? nodeOutputVars : undefined}
schema={schema}
)}
{isBoolean && (
<FormInputBoolean
value={varInput?.value as boolean}
onChange={handleValueChange(variable, type)}
/>
)}
{isFile && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
{isSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
defaultValue={varInput?.value}
items={options.filter((option: { show_on: any[] }) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)

return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleValueChange(variable, type)(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
title='JSON'
value={varInput?.value as any}
isExpand
isInNode
height={100}
language={CodeLanguage.json}
onChange={handleValueChange(variable, type)}
className='w-full'
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
/>
</div>
)}
{isAppSelector && (
<AppSelector
disabled={false}
@@ -250,7 +316,21 @@ const ReasoningConfigForm: React.FC<Props> = ({
scope={scope}
/>
)}
</>
{showVariableSelector && (
<VarReferencePicker
zIndex={1001}
className='h-8 grow'
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleVariableSelectorChange(variable)}
filterVar={getFilterVar()}
schema={schema}
valueTypePlaceHolder={targetVarType()}
/>
)}
</div>
)}
{url && (
<a
@@ -267,7 +347,19 @@ const ReasoningConfigForm: React.FC<Props> = ({
}
return (
<div className='space-y-3 px-4 py-2'>
{schemas.map(schema => renderField(schema))}
{!isShowSchema && schemas.map(schema => renderField(schema, (s: SchemaRoot, rootName: string) => {
setSchema(s)
setSchemaRootName(rootName)
showSchema()
}))}
{isShowSchema && (
<SchemaModal
isShow={isShowSchema}
schema={schema!}
rootName={schemaRootName}
onClose={hideSchema}
/>
)}
</div>
)
}

+ 59
- 0
web/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal.tsx View File

@@ -0,0 +1,59 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Modal from '@/app/components/base/modal'
import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor'
import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types'
import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'

type Props = {
isShow: boolean
schema: SchemaRoot
rootName: string
onClose: () => void
}

const SchemaModal: FC<Props> = ({
isShow,
schema,
rootName,
onClose,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={isShow}
onClose={onClose}
className='max-w-[960px] p-0'
wrapperClassName='z-[9999]'
>
<div className='pb-6'>
{/* Header */}
<div className='relative flex p-6 pb-3 pr-14'>
<div className='title-2xl-semi-bold grow truncate text-text-primary'>
{t('workflow.nodes.agent.parameterSchema')}
</div>
<div className='absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5' onClick={onClose}>
<RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' />
</div>
</div>
{/* Content */}
<div className='flex max-h-[700px] overflow-y-auto px-6 py-2'>
<MittProvider>
<VisualEditorContextProvider>
<VisualEditor
className='w-full'
schema={schema}
rootName={rootName}
readOnly
></VisualEditor>
</VisualEditorContextProvider>
</MittProvider>
</div>
</div>
</Modal>
)
}
export default React.memo(SchemaModal)

+ 17
- 5
web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx View File

@@ -17,10 +17,13 @@ import { ToolTipContent } from '@/app/components/base/tooltip/content'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version'
import cn from '@/utils/classnames'
import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip'

type Props = {
icon?: any
providerName?: string
isMCPTool?: boolean
providerShowName?: string
toolLabel?: string
showSwitch?: boolean
switchValue?: boolean
@@ -35,11 +38,14 @@ type Props = {
onInstall?: () => void
versionMismatch?: boolean
open: boolean
canChooseMCPTool?: boolean,
}

const ToolItem = ({
open,
icon,
isMCPTool,
providerShowName,
providerName,
toolLabel,
showSwitch,
@@ -54,11 +60,13 @@ const ToolItem = ({
isError,
errorTip,
versionMismatch,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
const providerNameText = providerName?.split('/').pop()
const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop()
const isTransparent = uninstalled || versionMismatch || isError
const [isDeleting, setIsDeleting] = useState(false)
const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool

return (
<div className={cn(
@@ -67,7 +75,7 @@ const ToolItem = ({
isDeleting && 'border-state-destructive-border shadow-xs hover:bg-state-destructive-hover',
)}>
{icon && (
<div className={cn('shrink-0', isTransparent && 'opacity-50')}>
<div className={cn('shrink-0', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}>
{typeof icon === 'string' && <div className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge bg-cover bg-center' style={{ backgroundImage: `url(${icon})` }} />}
{typeof icon !== 'string' && <AppIcon className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' size='xs' icon={icon?.content} background={icon?.background} />}
</div>
@@ -75,18 +83,19 @@ const ToolItem = ({
{!icon && (
<div className={cn(
'flex h-7 w-7 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30',
)}>
<div className='flex h-5 w-5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' />
</div>
</div>
)}
<div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50')}>
<div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{providerNameText}</div>
<div className='system-xs-medium text-text-secondary'>{toolLabel}</div>
</div>
<div className='hidden items-center gap-1 group-hover:flex'>
{!noAuth && !isError && !uninstalled && !versionMismatch && (
{!noAuth && !isError && !uninstalled && !versionMismatch && !isShowCanNotChooseMCPTip && (
<ActionButton>
<RiEqualizer2Line className='h-4 w-4' />
</ActionButton>
@@ -103,7 +112,7 @@ const ToolItem = ({
<RiDeleteBinLine className='h-4 w-4' />
</div>
</div>
{!isError && !uninstalled && !noAuth && !versionMismatch && showSwitch && (
{!isError && !uninstalled && !noAuth && !versionMismatch && !isShowCanNotChooseMCPTip && showSwitch && (
<div className='mr-1' onClick={e => e.stopPropagation()}>
<Switch
size='md'
@@ -112,6 +121,9 @@ const ToolItem = ({
/>
</div>
)}
{isShowCanNotChooseMCPTip && (
<McpToolNotSupportTooltip />
)}
{!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant='secondary' size='small' onClick={onAuth}>
{t('tools.notAuthorized')}

+ 5
- 0
web/app/components/plugins/types.ts View File

@@ -462,9 +462,14 @@ export type StrategyDeclaration = {
strategies: StrategyDetail[]
}

export type PluginMeta = {
version: string // the version of dify sdk
}

export type StrategyPluginDetail = {
provider: string
plugin_unique_identifier: string
plugin_id: string
declaration: StrategyDeclaration
meta: PluginMeta
}

+ 39
- 10
web/app/components/tools/add-tool-modal/empty.tsx View File

@@ -1,19 +1,48 @@
'use client'
import { useSearchParams } from 'next/navigation'
import { useTranslation } from 'react-i18next'
const Empty = () => {
import { ToolTypeEnum } from '../../workflow/block-selector/types'
import { RiArrowRightUpLine } from '@remixicon/react'
import Link from 'next/link'
import cn from '@/utils/classnames'
import { NoToolPlaceholder } from '../../base/icons/src/vender/other'
type Props = {
type?: ToolTypeEnum
isAgent?: boolean
}

const getLink = (type?: ToolTypeEnum) => {
switch (type) {
case ToolTypeEnum.Custom:
return '/tools?category=api'
case ToolTypeEnum.MCP:
return '/tools?category=mcp'
default:
return '/tools?category=api'
}
}
const Empty = ({
type,
isAgent,
}: Props) => {
const { t } = useTranslation()
const searchParams = useSearchParams()

const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type)
const Comp = (hasLink ? Link : 'div') as any
const linkProps = hasLink ? { href: getLink(type), target: '_blank' } : {}
const renderType = isAgent ? 'agent' : type
const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title`

return (
<div className='flex flex-col items-center'>
<div className="h-[149px] w-[163px] shrink-0 bg-[url('~@/app/components/tools/add-tool-modal/empty.png')] bg-cover bg-no-repeat"></div>
<div className='mb-1 text-[13px] font-medium leading-[18px] text-text-primary'>
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTitle' : 'emptyTitleCustom'}`)}
</div>
<div className='text-[13px] leading-[18px] text-text-tertiary'>
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTip' : 'emptyTipCustom'}`)}
<div className='flex h-[336px] flex-col items-center justify-center'>
<NoToolPlaceholder />
<div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'>
{hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'}
</div>
{(!isAgent && hasTitle) && (
<Comp className={cn('flex items-center text-[13px] leading-[18px] text-text-tertiary', hasLink && 'cursor-pointer hover:text-text-accent')} {...linkProps}>
{t(`tools.addToolModal.${renderType}.tip`)} {hasLink && <RiArrowRightUpLine className='ml-0.5 h-3 w-3' />}
</Comp>
)}
</div>
)
}

+ 1
- 0
web/app/components/tools/edit-custom-collection-modal/modal.tsx View File

@@ -184,6 +184,7 @@ const EditCustomCollectionModal: FC<Props> = ({
onClose={onHide}
closable
className='!h-[calc(100vh-16px)] !max-w-[630px] !p-0'
wrapperClassName='z-[1000]'
>
<div className='flex h-full flex-col'>
<div className='ml-6 mt-6 text-base font-semibold text-text-primary'>

+ 75
- 0
web/app/components/tools/mcp/create-card.tsx View File

@@ -0,0 +1,75 @@
'use client'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiAddCircleFill,
RiArrowRightUpLine,
RiBookOpenLine,
} from '@remixicon/react'
import MCPModal from './modal'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { useAppContext } from '@/context/app-context'
import { useCreateMCP } from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'

type Props = {
handleCreate: (provider: ToolWithProvider) => void
}

const NewMCPCard = ({ handleCreate }: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const { isCurrentWorkspaceManager } = useAppContext()

const { mutateAsync: createMCP } = useCreateMCP()

const create = async (info: any) => {
const provider = await createMCP(info)
handleCreate(provider)
}

const linkUrl = useMemo(() => {
if (language.startsWith('zh_'))
return 'https://docs.dify.ai/zh-hans/guides/tools/mcp'
if (language.startsWith('ja_jp'))
return 'https://docs.dify.ai/ja_jp/guides/tools/mcp'
return 'https://docs.dify.ai/en/guides/tools/mcp'
}, [language])

const [showModal, setShowModal] = useState(false)

return (
<>
{isCurrentWorkspaceManager && (
<div className='col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
<div className='group grow rounded-t-xl' onClick={() => setShowModal(true)}>
<div className='flex shrink-0 items-center p-4 pb-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
</div>
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.mcp.create.cardTitle')}</div>
</div>
</div>
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<RiBookOpenLine className='h-3 w-3 shrink-0' />
<div className='system-xs-regular grow truncate' title={t('tools.mcp.create.cardLink') || ''}>{t('tools.mcp.create.cardLink')}</div>
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
</a>
</div>
</div>
)}
{showModal && (
<MCPModal
show={showModal}
onConfirm={create}
onHide={() => setShowModal(false)}
/>
)}
</>
)
}
export default NewMCPCard

+ 308
- 0
web/app/components/tools/mcp/detail/content.tsx View File

@@ -0,0 +1,308 @@
'use client'
import React, { useCallback, useEffect } from 'react'
import type { FC } from 'react'
import { useBoolean } from 'ahooks'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import {
RiCloseLine,
RiLoader2Line,
RiLoopLeftLine,
} from '@remixicon/react'
import type { ToolWithProvider } from '../../../workflow/types'
import Icon from '@/app/components/plugins/card/base/card-icon'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Indicator from '@/app/components/header/indicator'
import Tooltip from '@/app/components/base/tooltip'
import MCPModal from '../modal'
import OperationDropdown from './operation-dropdown'
import ListLoading from './list-loading'
import ToolItem from './tool-item'
import {
useAuthorizeMCP,
useDeleteMCP,
useInvalidateMCPTools,
useMCPTools,
useUpdateMCP,
useUpdateMCPTools,
} from '@/service/use-tools'
import { openOAuthPopup } from '@/hooks/use-oauth'
import cn from '@/utils/classnames'

type Props = {
detail: ToolWithProvider
onUpdate: (isDelete?: boolean) => void
onHide: () => void
isTriggerAuthorize: boolean
onFirstCreate: () => void
}

const MCPDetailContent: FC<Props> = ({
detail,
onUpdate,
onHide,
isTriggerAuthorize,
onFirstCreate,
}) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()

const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '')
const invalidateMCPTools = useInvalidateMCPTools()
const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools()
const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP()
const toolList = data?.tools || []

const [isShowUpdateConfirm, {
setTrue: showUpdateConfirm,
setFalse: hideUpdateConfirm,
}] = useBoolean(false)

const handleUpdateTools = useCallback(async () => {
hideUpdateConfirm()
if (!detail)
return
await updateTools(detail.id)
invalidateMCPTools(detail.id)
onUpdate()
}, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, updateTools])

const { mutateAsync: updateMCP } = useUpdateMCP({})
const { mutateAsync: deleteMCP } = useDeleteMCP({})

const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)

const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)

const [deleting, {
setTrue: showDeleting,
setFalse: hideDeleting,
}] = useBoolean(false)

const handleOAuthCallback = useCallback(() => {
if (!isCurrentWorkspaceManager)
return
if (!detail.id)
return
handleUpdateTools()
}, [detail.id, handleUpdateTools, isCurrentWorkspaceManager])

const handleAuthorize = useCallback(async () => {
onFirstCreate()
if (!isCurrentWorkspaceManager)
return
if (!detail)
return
const res = await authorizeMcp({
provider_id: detail.id,
})
if (res.result === 'success')
handleUpdateTools()

else if (res.authorization_url)
openOAuthPopup(res.authorization_url, handleOAuthCallback)
}, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback])

const handleUpdate = useCallback(async (data: any) => {
if (!detail)
return
const res = await updateMCP({
...data,
provider_id: detail.id,
})
if ((res as any)?.result === 'success') {
hideUpdateModal()
onUpdate()
handleAuthorize()
}
}, [detail, updateMCP, hideUpdateModal, onUpdate, handleAuthorize])

const handleDelete = useCallback(async () => {
if (!detail)
return
showDeleting()
const res = await deleteMCP(detail.id)
hideDeleting()
if ((res as any)?.result === 'success') {
hideDeleteConfirm()
onUpdate(true)
}
}, [detail, showDeleting, deleteMCP, hideDeleting, hideDeleteConfirm, onUpdate])

useEffect(() => {
if (isTriggerAuthorize)
handleAuthorize()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

if (!detail)
return null

return (
<>
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
<div className='flex'>
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
<Icon src={detail.icon} />
</div>
<div className='ml-3 w-0 grow'>
<div className='flex h-5 items-center'>
<div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div>
</div>
<div className='mt-0.5 flex items-center gap-1'>
<Tooltip popupContent={t('tools.mcp.identifier')}>
<div className='system-xs-regular shrink-0 cursor-pointer text-text-secondary' onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div>
</Tooltip>
<div className='system-xs-regular shrink-0 text-text-quaternary'>·</div>
<Tooltip popupContent={t('tools.mcp.modal.serverUrl')}>
<div className='system-xs-regular truncate text-text-secondary'>{detail.server_url}</div>
</Tooltip>
</div>
</div>
<div className='flex gap-1'>
<OperationDropdown
onEdit={showUpdateModal}
onRemove={showDeleteConfirm}
/>
<ActionButton onClick={onHide}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
</div>
<div className='mt-5'>
{!isAuthorizing && detail.is_team_authorization && (
<Button
variant='secondary'
className='w-full'
onClick={handleAuthorize}
disabled={!isCurrentWorkspaceManager}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
{!detail.is_team_authorization && !isAuthorizing && (
<Button
variant='primary'
className='w-full'
onClick={handleAuthorize}
disabled={!isCurrentWorkspaceManager}
>
{t('tools.mcp.authorize')}
</Button>
)}
{isAuthorizing && (
<Button
variant='primary'
className='w-full'
disabled
>
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
{t('tools.mcp.authorizing')}
</Button>
)}
</div>
</div>
<div className='flex grow flex-col'>
{((detail.is_team_authorization && isGettingTools) || isUpdating) && (
<>
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
<div className='flex h-6 items-center'>
{!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>}
{isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>}
</div>
<div></div>
</div>
<div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'>
<ListLoading />
</div>
</>
)}
{!isUpdating && detail.is_team_authorization && !isGettingTools && !toolList.length && (
<div className='flex h-full w-full flex-col items-center justify-center'>
<div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div>
<Button
variant='primary'
onClick={handleUpdateTools}
>{t('tools.mcp.getTools')}</Button>
</div>
)}
{!isUpdating && !isGettingTools && toolList.length > 0 && (
<>
<div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'>
<div className='flex h-6 items-center'>
{toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>}
{toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>}
</div>
<div>
<Button size='small' onClick={showUpdateConfirm}>
<RiLoopLeftLine className='mr-1 h-3.5 w-3.5' />
{t('tools.mcp.update')}
</Button>
</div>
</div>
<div className='flex h-0 w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'>
{toolList.map(tool => (
<ToolItem
key={`${detail.id}${tool.name}`}
tool={tool}
/>
))}
</div>
</>
)}

{!isUpdating && !detail.is_team_authorization && (
<div className='flex h-full w-full flex-col items-center justify-center'>
{!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>}
{isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>}
<div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div>
</div>
)}
</div>
{isShowUpdateModal && (
<MCPModal
data={detail}
show={isShowUpdateModal}
onConfirm={handleUpdate}
onHide={hideUpdateModal}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t('tools.mcp.delete')}
content={
<div>
{t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })}
</div>
}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
isLoading={deleting}
isDisabled={deleting}
/>
)}
{isShowUpdateConfirm && (
<Confirm
isShow
title={t('tools.mcp.toolUpdateConfirmTitle')}
content={t('tools.mcp.toolUpdateConfirmContent')}
onCancel={hideUpdateConfirm}
onConfirm={handleUpdateTools}
/>
)}
</>
)
}

export default MCPDetailContent

+ 37
- 0
web/app/components/tools/mcp/detail/list-loading.tsx View File

@@ -0,0 +1,37 @@
'use client'
import React from 'react'
import cn from '@/utils/classnames'

const ListLoading = () => {
return (
<div className={cn('space-y-2')}>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[196px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
<div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'>
<div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div>
<div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div>
<div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div>
</div>
</div>
)
}

export default ListLoading

+ 88
- 0
web/app/components/tools/mcp/detail/operation-dropdown.tsx View File

@@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiDeleteBinLine,
RiEditLine,
RiMoreFill,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'

type Props = {
inCard?: boolean
onOpenChange?: (open: boolean) => void
onEdit: () => void
onRemove: () => void
}

const OperationDropdown: FC<Props> = ({
inCard,
onOpenChange,
onEdit,
onRemove,
}) => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
onOpenChange?.(v)
}, [doSetOpen])

const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])

return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: !inCard ? -12 : 0,
crossAxis: !inCard ? 36 : 0,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton size={inCard ? 'l' : 'm'} className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className={cn('h-4 w-4', inCard && 'h-5 w-5')} />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'>
<div
className='flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-base-hover'
onClick={() => {
onEdit()
handleTrigger()
}}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
<div className='system-md-regular ml-2 text-text-secondary'>{t('tools.mcp.operation.edit')}</div>
</div>
<div
className='group flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-destructive-hover'
onClick={() => {
onRemove()
handleTrigger()
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary group-hover:text-text-destructive-secondary' />
<div className='system-md-regular ml-2 text-text-secondary group-hover:text-text-destructive'>{t('tools.mcp.operation.remove')}</div>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

+ 56
- 0
web/app/components/tools/mcp/detail/provider-detail.tsx View File

@@ -0,0 +1,56 @@
'use client'
import React from 'react'
import type { FC } from 'react'
import Drawer from '@/app/components/base/drawer'
import MCPDetailContent from './content'
import type { ToolWithProvider } from '../../../workflow/types'
import cn from '@/utils/classnames'

type Props = {
detail?: ToolWithProvider
onUpdate: () => void
onHide: () => void
isTriggerAuthorize: boolean
onFirstCreate: () => void
}

const MCPDetailPanel: FC<Props> = ({
detail,
onUpdate,
onHide,
isTriggerAuthorize,
onFirstCreate,
}) => {
const handleUpdate = (isDelete = false) => {
if (isDelete)
onHide()
onUpdate()
}

if (!detail)
return null

return (
<Drawer
isOpen={!!detail}
clickOutsideNotOpen={false}
onClose={onHide}
footer={null}
mask={false}
positionCenter={false}
panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')}
>
{detail && (
<MCPDetailContent
detail={detail}
onHide={onHide}
onUpdate={handleUpdate}
isTriggerAuthorize={isTriggerAuthorize}
onFirstCreate={onFirstCreate}
/>
)}
</Drawer>
)
}

export default MCPDetailPanel

+ 41
- 0
web/app/components/tools/mcp/detail/tool-item.tsx View File

@@ -0,0 +1,41 @@
'use client'
import React from 'react'
import { useContext } from 'use-context-selector'
import type { Tool } from '@/app/components/tools/types'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'

type Props = {
tool: Tool
}

const MCPToolItem = ({
tool,
}: Props) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)

return (
<Tooltip
key={tool.name}
position='left'
popupClassName='!p-0 !px-4 !py-3.5 !w-[360px] !border-[0.5px] !border-components-panel-border !rounded-xl !shadow-lg'
popupContent={(
<div>
<div className='title-xs-semi-bold mb-1 text-text-primary'>{tool.label[language]}</div>
<div className='body-xs-regular text-text-secondary'>{tool.description[language]}</div>
</div>
)}
>
<div
className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover')}
>
<div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div>
<div className='system-xs-regular line-clamp-2 text-text-tertiary' title={tool.description[language]}>{tool.description[language]}</div>
</div>
</Tooltip>
)
}
export default MCPToolItem

+ 12
- 0
web/app/components/tools/mcp/hooks.ts View File

@@ -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 }
}

+ 98
- 0
web/app/components/tools/mcp/index.tsx View File

@@ -0,0 +1,98 @@
'use client'
import { useMemo, useState } from 'react'
import NewMCPCard from './create-card'
import MCPCard from './provider-card'
import MCPDetailPanel from './detail/provider-detail'
import {
useAllToolProviders,
} from '@/service/use-tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'

type Props = {
searchText: string
}

function renderDefaultCard() {
const defaultCards = Array.from({ length: 36 }, (_, index) => (
<div
key={index}
className={cn(
'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10',
index < 4 && 'opacity-60',
index >= 4 && index < 8 && 'opacity-50',
index >= 8 && index < 12 && 'opacity-40',
index >= 12 && index < 16 && 'opacity-30',
index >= 16 && index < 20 && 'opacity-25',
index >= 20 && index < 24 && 'opacity-20',
)}
></div>
))
return defaultCards
}

const MCPList = ({
searchText,
}: Props) => {
const { data: list = [] as ToolWithProvider[], refetch } = useAllToolProviders()
const [isTriggerAuthorize, setIsTriggerAuthorize] = useState<boolean>(false)

const filteredList = useMemo(() => {
return list.filter((collection) => {
if (searchText)
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
return collection.type === 'mcp'
}) as ToolWithProvider[]
}, [list, searchText])

const [currentProviderID, setCurrentProviderID] = useState<string>()

const currentProvider = useMemo(() => {
return list.find(provider => provider.id === currentProviderID)
}, [list, currentProviderID])

const handleCreate = async (provider: ToolWithProvider) => {
await refetch() // update list
setCurrentProviderID(provider.id)
setIsTriggerAuthorize(true)
}

const handleUpdate = async (providerID: string) => {
await refetch() // update list
setCurrentProviderID(providerID)
setIsTriggerAuthorize(true)
}
return (
<>
<div
className={cn(
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
!list.length && 'h-[calc(100vh_-_136px)] overflow-hidden',
)}
>
<NewMCPCard handleCreate={handleCreate} />
{filteredList.map(provider => (
<MCPCard
key={provider.id}
data={provider}
currentProvider={currentProvider as ToolWithProvider}
handleSelect={setCurrentProviderID}
onUpdate={handleUpdate}
onDeleted={refetch}
/>
))}
{!list.length && renderDefaultCard()}
</div>
{currentProvider && (
<MCPDetailPanel
detail={currentProvider as ToolWithProvider}
onHide={() => setCurrentProviderID(undefined)}
onUpdate={refetch}
isTriggerAuthorize={isTriggerAuthorize}
onFirstCreate={() => setIsTriggerAuthorize(false)}
/>
)}
</>
)
}
export default MCPList

+ 134
- 0
web/app/components/tools/mcp/mcp-server-modal.tsx View File

@@ -0,0 +1,134 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
import type {
MCPServerDetail,
} from '@/app/components/tools/types'
import {
useCreateMCPServer,
useInvalidateMCPServerDetail,
useUpdateMCPServer,
} from '@/service/use-tools'
import cn from '@/utils/classnames'

export type ModalProps = {
appID: string
latestParams?: any[]
data?: MCPServerDetail
show: boolean
onHide: () => void
}

const MCPServerModal = ({
appID,
latestParams = [],
data,
show,
onHide,
}: ModalProps) => {
const { t } = useTranslation()
const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer()
const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer()
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()

const [description, setDescription] = React.useState(data?.description || '')
const [params, setParams] = React.useState(data?.parameters || {})

const handleParamChange = (variable: string, value: string) => {
setParams(prev => ({
...prev,
[variable]: value,
}))
}

const getParamValue = () => {
const res = {} as any
latestParams.map((param) => {
res[param.variable] = params[param.variable]
return param
})
return res
}

const submit = async () => {
if (!data) {
await createMCPServer({
appID,
description,
parameters: getParamValue(),
})
invalidateMCPServerDetail(appID)
onHide()
}
else {
await updateMCPServer({
appID,
id: data.id,
description,
parameters: getParamValue(),
})
invalidateMCPServerDetail(appID)
onHide()
}
}

return (
<Modal
isShow={show}
onClose={onHide}
className={cn('relative !max-w-[520px] !p-0')}
>
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
<div className='title-2xl-semi-bold relative p-6 pb-3 text-xl text-text-primary'>
{!data ? t('tools.mcp.server.modal.addTitle') : t('tools.mcp.server.modal.editTitle')}
</div>
<div className='space-y-5 px-6 py-3'>
<div className='space-y-0.5'>
<div className='flex h-6 items-center gap-1'>
<div className='system-sm-medium text-text-secondary'>{t('tools.mcp.server.modal.description')}</div>
<div className='system-xs-regular text-text-destructive-secondary'>*</div>
</div>
<Textarea
className='h-[96px] resize-none'
value={description}
placeholder={t('tools.mcp.server.modal.descriptionPlaceholder')}
onChange={e => setDescription(e.target.value)}
></Textarea>
</div>
{latestParams.length > 0 && (
<div>
<div className='mb-1 flex items-center gap-2'>
<div className='system-xs-medium-uppercase shrink-0 text-text-primary'>{t('tools.mcp.server.modal.parameters')}</div>
<Divider type='horizontal' className='!m-0 !h-px grow bg-divider-subtle' />
</div>
<div className='body-xs-regular mb-2 text-text-tertiary'>{t('tools.mcp.server.modal.parametersTip')}</div>
<div className='space-y-3'>
{latestParams.map(paramItem => (
<MCPServerParamItem
key={paramItem.variable}
data={paramItem}
value={params[paramItem.variable] || ''}
onChange={value => handleParamChange(paramItem.variable, value)}
/>
))}
</div>
</div>
)}
</div>
<div className='flex flex-row-reverse p-6 pt-5'>
<Button disabled={!description || creating || updating} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.server.modal.confirm')}</Button>
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
</div>
</Modal>
)
}

export default MCPServerModal

+ 37
- 0
web/app/components/tools/mcp/mcp-server-param-item.tsx View File

@@ -0,0 +1,37 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'

type Props = {
data?: any
value: string
onChange: (value: string) => void
}

const MCPServerParamItem = ({
data,
value,
onChange,
}: Props) => {
const { t } = useTranslation()

return (
<div className='space-y-0.5'>
<div className='flex h-6 items-center gap-2'>
<div className='system-xs-medium text-text-secondary'>{data.label}</div>
<div className='system-xs-medium text-text-quaternary'>·</div>
<div className='system-xs-medium text-text-secondary'>{data.variable}</div>
<div className='system-xs-medium text-text-tertiary'>{data.type}</div>
</div>
<Textarea
className='h-8 resize-none'
value={value}
placeholder={t('tools.mcp.server.modal.parametersPlaceholder')}
onChange={e => onChange(e.target.value)}
></Textarea>
</div>
)
}

export default MCPServerParamItem

+ 244
- 0
web/app/components/tools/mcp/mcp-service-card.tsx View File

@@ -0,0 +1,244 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiLoopLeftLine,
} from '@remixicon/react'
import {
Mcp,
} from '@/app/components/base/icons/src/vender/other'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Confirm from '@/app/components/base/confirm'
import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app'
import Indicator from '@/app/components/header/indicator'
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
import { useAppWorkflow } from '@/service/use-workflow'
import {
useInvalidateMCPServerDetail,
useMCPServerDetail,
useRefreshMCPServerCode,
useUpdateMCPServer,
} from '@/service/use-tools'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { fetchAppDetail } from '@/service/apps'

export type IAppCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO>
}

function MCPServiceCard({
appInfo,
}: IAppCardProps) {
const { t } = useTranslation()
const appId = appInfo.id
const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showMCPServerModal, setShowMCPServerModal] = useState(false)

const isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow'
const isBasicApp = !isAdvancedApp
const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '')
const [basicAppConfig, setBasicAppConfig] = useState<any>({})
const basicAppInputForm = useMemo(() => {
if(!isBasicApp || !basicAppConfig?.user_input_form)
return []
return basicAppConfig.user_input_form.map((item: any) => {
const type = Object.keys(item)[0]
return {
...item[type],
type: type || 'text-input',
}
})
}, [basicAppConfig.user_input_form, isBasicApp])
useEffect(() => {
if(isBasicApp && appId) {
(async () => {
const res = await fetchAppDetail({ url: '/apps', id: appId })
setBasicAppConfig(res?.model_config || {})
})()
}
}, [appId, isBasicApp])
const { data: detail } = useMCPServerDetail(appId)
const { id, status, server_code } = detail ?? {}

const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at
const serverPublished = !!id
const serverActivated = status === 'active'
const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********'
const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished

const [activated, setActivated] = useState(serverActivated)

const latestParams = useMemo(() => {
if(isAdvancedApp) {
if (!currentWorkflow?.graph)
return []
const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any
return startNode?.data.variables as any[] || []
}
return basicAppInputForm
}, [currentWorkflow, basicAppInputForm, isAdvancedApp])

const onGenCode = async () => {
await refreshMCPServerCode(detail?.id || '')
invalidateMCPServerDetail(appId)
}

const onChangeStatus = async (state: boolean) => {
setActivated(state)
if (state) {
if (!serverPublished) {
setShowMCPServerModal(true)
return
}

await updateMCPServer({
appID: appId,
id: id || '',
description: detail?.description || '',
parameters: detail?.parameters || {},
status: 'active',
})
invalidateMCPServerDetail(appId)
}
else {
await updateMCPServer({
appID: appId,
id: id || '',
description: detail?.description || '',
parameters: detail?.parameters || {},
status: 'inactive',
})
invalidateMCPServerDetail(appId)
}
}

const handleServerModalHide = () => {
setShowMCPServerModal(false)
if (!serverActivated)
setActivated(false)
}

useEffect(() => {
setActivated(serverActivated)
}, [serverActivated])

if (!currentWorkflow && isAdvancedApp)
return null

return (
<>
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}>
<div className='rounded-xl bg-background-default'>
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
<div className='flex w-full items-center gap-3 self-stretch'>
<div className='flex grow items-center'>
<div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'>
<Mcp className='h-4 w-4 text-text-primary-on-surface' />
</div>
<div className="group w-full">
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
{t('tools.mcp.server.title')}
</div>
</div>
</div>
<div className='flex items-center gap-1'>
<Indicator color={serverActivated ? 'green' : 'yellow'} />
<div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}>
{serverActivated
? t('appOverview.overview.status.running')
: t('appOverview.overview.status.disable')}
</div>
</div>
<Tooltip
popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''}
>
<div>
<Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} />
</div>
</Tooltip>
</div>
<div className='flex flex-col items-start justify-center self-stretch'>
<div className="system-xs-medium pb-1 text-text-tertiary">
{t('tools.mcp.server.url')}
</div>
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
{serverURL}
</div>
</div>
{serverPublished && (
<>
<CopyFeedback
content={serverURL}
className={'!size-6'}
/>
<Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />
{isCurrentWorkspaceManager && (
<Tooltip
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
>
<div
className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
onClick={() => setShowConfirmDelete(true)}
>
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/>
</div>
</Tooltip>
)}
</>
)}
</div>
</div>
</div>
<div className='flex items-center gap-1 self-stretch p-3'>
<Button
disabled={toggleDisabled}
size='small'
variant='ghost'
onClick={() => setShowMCPServerModal(true)}
>
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
</Button>
</div>
</div>
</div>
{showMCPServerModal && (
<MCPServerModal
show={showMCPServerModal}
appID={appId}
data={serverPublished ? detail : undefined}
latestParams={latestParams}
onHide={handleServerModalHide}
/>
)}
{/* button copy link/ button regenerate */}
{showConfirmDelete && (
<Confirm
type='warning'
title={t('appOverview.overview.appInfo.regenerate')}
content={t('tools.mcp.server.reGen')}
isShow={showConfirmDelete}
onConfirm={() => {
onGenCode()
setShowConfirmDelete(false)
}}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</>
)
}

export default MCPServiceCard

+ 154
- 0
web/app/components/tools/mcp/mock.ts View File

@@ -0,0 +1,154 @@
const tools = [
{
author: 'Novice',
name: 'NOTION_ADD_PAGE_CONTENT',
label: {
en_US: 'NOTION_ADD_PAGE_CONTENT',
zh_Hans: 'NOTION_ADD_PAGE_CONTENT',
pt_BR: 'NOTION_ADD_PAGE_CONTENT',
ja_JP: 'NOTION_ADD_PAGE_CONTENT',
},
description: {
en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)',
},
parameters: [
{
name: 'after',
label: {
en_US: 'after',
zh_Hans: 'after',
pt_BR: 'after',
ja_JP: 'after',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
},
form: 'llm',
llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.',
},
{
name: 'content_block',
label: {
en_US: 'content_block',
zh_Hans: 'content_block',
pt_BR: 'content_block',
ja_JP: 'content_block',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'Child content to append to a page.',
zh_Hans: 'Child content to append to a page.',
pt_BR: 'Child content to append to a page.',
ja_JP: 'Child content to append to a page.',
},
form: 'llm',
llm_description: 'Child content to append to a page.',
},
{
name: 'parent_block_id',
label: {
en_US: 'parent_block_id',
zh_Hans: 'parent_block_id',
pt_BR: 'parent_block_id',
ja_JP: 'parent_block_id',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: false,
default: null,
min: null,
max: null,
precision: null,
options: [],
type: 'string',
human_description: {
en_US: 'The ID of the page which the children will be added.',
zh_Hans: 'The ID of the page which the children will be added.',
pt_BR: 'The ID of the page which the children will be added.',
ja_JP: 'The ID of the page which the children will be added.',
},
form: 'llm',
llm_description: 'The ID of the page which the children will be added.',
},
],
labels: [],
output_schema: null,
},
]

export const listData = [
{
id: 'fdjklajfkljadslf111',
author: 'KVOJJJin',
name: 'GOGOGO',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: true,
tools,
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO',
zh_Hans: 'GOGOGO',
},
},
{
id: 'fdjklajfkljadslf222',
author: 'KVOJJJin',
name: 'GOGOGO2',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: false,
tools: [],
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO2',
zh_Hans: 'GOGOGO2',
},
},
{
id: 'fdjklajfkljadslf333',
author: 'KVOJJJin',
name: 'GOGOGO3',
icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US',
server_url: 'https://mcp.composio.dev/notion/****/abc',
type: 'mcp',
is_team_authorization: true,
tools,
update_elapsed_time: 1744793369,
label: {
en_US: 'GOGOGO3',
zh_Hans: 'GOGOGO3',
},
},
]

+ 221
- 0
web/app/components/tools/mcp/modal.tsx View File

@@ -0,0 +1,221 @@
'use client'
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getDomain } from 'tldts'
import { RiCloseLine, RiEditLine } from '@remixicon/react'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIcon from '@/app/components/base/app-icon'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import type { AppIconType } from '@/types/app'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { noop } from 'lodash-es'
import Toast from '@/app/components/base/toast'
import { uploadRemoteFileInfo } from '@/service/common'
import cn from '@/utils/classnames'
import { useHover } from 'ahooks'

export type DuplicateAppModalProps = {
data?: ToolWithProvider
show: boolean
onConfirm: (info: {
name: string
server_url: string
icon_type: AppIconType
icon: string
icon_background?: string | null
server_identifier: string
}) => void
onHide: () => void
}

const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' }
const extractFileId = (url: string) => {
const match = url.match(/files\/(.+?)\/file-preview/)
return match ? match[1] : null
}
const getIcon = (data?: ToolWithProvider) => {
if (!data)
return DEFAULT_ICON as AppIconSelection
if (typeof data.icon === 'string')
return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection
return {
...data.icon,
icon: data.icon.content,
type: 'emoji',
} as unknown as AppIconSelection
}

const MCPModal = ({
data,
show,
onConfirm,
onHide,
}: DuplicateAppModalProps) => {
const { t } = useTranslation()
const isCreate = !data

const originalServerUrl = data?.server_url
const originalServerID = data?.server_identifier
const [url, setUrl] = React.useState(data?.server_url || '')
const [name, setName] = React.useState(data?.name || '')
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
const [isFetchingIcon, setIsFetchingIcon] = useState(false)
const appIconRef = useRef<HTMLDivElement>(null)
const isHovering = useHover(appIconRef)

const isValidUrl = (string: string) => {
try {
const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i
return urlPattern.test(string)
}
catch (e) {
return false
}
}

const isValidServerID = (str: string) => {
return /^[a-z0-9_-]{1,24}$/.test(str)
}

const handleBlur = async (url: string) => {
if (data)
return
if (!isValidUrl(url))
return
const domain = getDomain(url)
const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`
setIsFetchingIcon(true)
try {
const res = await uploadRemoteFileInfo(remoteIcon, undefined, true)
setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' })
}
catch (e) {
console.error('Failed to fetch remote icon:', e)
Toast.notify({ type: 'warning', message: 'Failed to fetch remote icon' })
}
finally {
setIsFetchingIcon(false)
}
}

const submit = async () => {
if (!isValidUrl(url)) {
Toast.notify({ type: 'error', message: 'invalid server url' })
return
}
if (!isValidServerID(serverIdentifier.trim())) {
Toast.notify({ type: 'error', message: 'invalid server identifier' })
return
}
await onConfirm({
server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(),
name,
icon_type: appIcon.type,
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
server_identifier: serverIdentifier.trim(),
})
if(isCreate)
onHide()
}

return (
<>
<Modal
isShow={show}
onClose={noop}
className={cn('relative !max-w-[520px]', 'p-6')}
>
<div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}>
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
</div>
<div className='title-2xl-semi-bold relative pb-3 text-xl text-text-primary'>{!isCreate ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}</div>
<div className='space-y-5 py-3'>
<div>
<div className='mb-1 flex h-6 items-center'>
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span>
</div>
<Input
value={url}
onChange={e => setUrl(e.target.value)}
onBlur={e => handleBlur(e.target.value.trim())}
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
/>
{originalServerUrl && originalServerUrl !== url && (
<div className='mt-1 flex h-5 items-center'>
<span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverUrlWarning')}</span>
</div>
)}
</div>
<div className='flex space-x-3'>
<div className='grow pb-1'>
<div className='mb-1 flex h-6 items-center'>
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span>
</div>
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('tools.mcp.modal.namePlaceholder')}
/>
</div>
<div className='pt-2' ref={appIconRef}>
<AppIcon
iconType={appIcon.type}
icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
size='xxl'
className='relative cursor-pointer rounded-2xl'
coverElement={
isHovering
? (<div className='absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-background-overlay-alt'>
<RiEditLine className='size-6 text-text-primary-on-surface' />
</div>) : null
}
onClick={() => { setShowAppIconPicker(true) }}
/>
</div>
</div>
<div>
<div className='flex h-6 items-center'>
<span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverIdentifier')}</span>
</div>
<div className='body-xs-regular mb-1 text-text-tertiary'>{t('tools.mcp.modal.serverIdentifierTip')}</div>
<Input
value={serverIdentifier}
onChange={e => setServerIdentifier(e.target.value)}
placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')}
/>
{originalServerID && originalServerID !== serverIdentifier && (
<div className='mt-1 flex h-5 items-center'>
<span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverIdentifierWarning')}</span>
</div>
)}
</div>
</div>
<div className='flex flex-row-reverse pt-5'>
<Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button>
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
</div>
</Modal>
{showAppIconPicker && <AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(getIcon(data))
setShowAppIconPicker(false)
}}
/>}
</>

)
}

export default MCPModal

+ 152
- 0
web/app/components/tools/mcp/provider-card.tsx View File

@@ -0,0 +1,152 @@
'use client'
import { useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { RiHammerFill } from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import Icon from '@/app/components/plugins/card/base/card-icon'
import { useFormatTimeFromNow } from './hooks'
import type { ToolWithProvider } from '../../workflow/types'
import Confirm from '@/app/components/base/confirm'
import MCPModal from './modal'
import OperationDropdown from './detail/operation-dropdown'
import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools'
import cn from '@/utils/classnames'

type Props = {
currentProvider?: ToolWithProvider
data: ToolWithProvider
handleSelect: (providerID: string) => void
onUpdate: (providerID: string) => void
onDeleted: () => void
}

const MCPCard = ({
currentProvider,
data,
onUpdate,
handleSelect,
onDeleted,
}: Props) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
const { isCurrentWorkspaceManager } = useAppContext()

const { mutateAsync: updateMCP } = useUpdateMCP({})
const { mutateAsync: deleteMCP } = useDeleteMCP({})

const [isOperationShow, setIsOperationShow] = useState(false)

const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)

const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)

const [deleting, {
setTrue: showDeleting,
setFalse: hideDeleting,
}] = useBoolean(false)

const handleUpdate = useCallback(async (form: any) => {
const res = await updateMCP({
...form,
provider_id: data.id,
})
if ((res as any)?.result === 'success') {
hideUpdateModal()
onUpdate(data.id)
}
}, [data, updateMCP, hideUpdateModal, onUpdate])

const handleDelete = useCallback(async () => {
showDeleting()
const res = await deleteMCP(data.id)
hideDeleting()
if ((res as any)?.result === 'success') {
hideDeleteConfirm()
onDeleted()
}
}, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onDeleted])

return (
<div
onClick={() => handleSelect(data.id)}
className={cn(
'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md',
currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt',
)}
>
<div className='flex grow items-center gap-3 rounded-t-xl p-4'>
<div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'>
<Icon src={data.icon} />
</div>
<div className='grow'>
<div className='system-md-semibold mb-1 truncate text-text-secondary' title={data.name}>{data.name}</div>
<div className='system-xs-regular text-text-tertiary'>{data.server_identifier}</div>
</div>
</div>
<div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'>
<div className='flex w-0 grow items-center gap-2'>
<div className='flex items-center gap-1'>
<RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' />
{data.tools.length > 0 && (
<div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div>
)}
{!data.tools.length && (
<div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.noTools')}</div>
)}
</div>
<div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div>
<div className={cn('system-xs-regular truncate text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && ' sm:hidden')} title={`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div>
</div>
{data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />}
{(!data.is_team_authorization || !data.tools.length) && (
<div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'>
{t('tools.mcp.noConfigured')}
<Indicator color='red' />
</div>
)}
</div>
{isCurrentWorkspaceManager && (
<div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}>
<OperationDropdown
inCard
onOpenChange={setIsOperationShow}
onEdit={showUpdateModal}
onRemove={showDeleteConfirm}
/>
</div>
)}
{isShowUpdateModal && (
<MCPModal
data={data}
show={isShowUpdateModal}
onConfirm={handleUpdate}
onHide={hideUpdateModal}
/>
)}
{isShowDeleteConfirm && (
<Confirm
isShow
title={t('tools.mcp.delete')}
content={
<div>
{t('tools.mcp.deleteConfirmTitle', { mcp: data.name })}
</div>
}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
isLoading={deleting}
isDisabled={deleting}
/>
)}
</div>
)
}
export default MCPCard

+ 38
- 16
web/app/components/tools/provider-list.tsx View File

@@ -15,11 +15,29 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import MCPList from './mcp'
import { useAllToolProviders } from '@/service/use-tools'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { ToolTypeEnum } from '../workflow/block-selector/types'

const getToolType = (type: string) => {
switch (type) {
case 'builtin':
return ToolTypeEnum.BuiltIn
case 'api':
return ToolTypeEnum.Custom
case 'workflow':
return ToolTypeEnum.Workflow
case 'mcp':
return ToolTypeEnum.MCP
default:
return ToolTypeEnum.BuiltIn
}
}
const ProviderList = () => {
// const searchParams = useSearchParams()
// searchParams.get('category') === 'workflow'
const { t } = useTranslation()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const containerRef = useRef<HTMLDivElement>(null)
@@ -31,6 +49,7 @@ const ProviderList = () => {
{ value: 'builtin', text: t('tools.type.builtIn') },
{ value: 'api', text: t('tools.type.custom') },
{ value: 'workflow', text: t('tools.type.workflow') },
{ value: 'mcp', text: 'MCP' },
]
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const handleTagsChange = (value: string[]) => {
@@ -85,7 +104,9 @@ const ProviderList = () => {
options={options}
/>
<div className='flex items-center gap-2'>
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
{activeTab !== 'mcp' && (
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
)}
<Input
showLeftIcon
showClearIcon
@@ -96,7 +117,7 @@ const ProviderList = () => {
/>
</div>
</div>
{(filteredCollectionList.length > 0 || activeTab !== 'builtin') && (
{activeTab !== 'mcp' && (
<div className={cn(
'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
@@ -127,25 +148,26 @@ const ProviderList = () => {
/>
</div>
))}
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>}
{!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty type={getToolType(activeTab)} /></div>}
</div>
)}
{!filteredCollectionList.length && activeTab === 'builtin' && (
<Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' />
)}
{
enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
)
}
</div >
</div >
{enable_marketplace && activeTab === 'builtin' && (
<Marketplace
onMarketplaceScroll={() => {
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' })
}}
searchPluginText={keywords}
filterPluginTags={tagFilterValue}
/>
)}
{activeTab === 'mcp' && (
<MCPList searchText={keywords} />
)}
</div>
</div>
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail
collection={currentProvider}

+ 12
- 12
web/app/components/tools/provider/custom-create-card.tsx View File

@@ -3,13 +3,13 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiAddLine,
RiAddCircleFill,
RiArrowRightUpLine,
RiBookOpenLine,
} from '@remixicon/react'
import type { CustomCollectionBackend } from '../types'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import { createCustomCollection } from '@/service/tools'
import Toast from '@/app/components/base/toast'
@@ -47,20 +47,20 @@ const Contribute = ({ onRefreshData }: Props) => {
return (
<>
{isCurrentWorkspaceManager && (
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'>
<div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'>
<div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<div className='flex shrink-0 items-center p-4 pb-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'>
<RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/>
<div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
<RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/>
</div>
<div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
<div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div>
</div>
</div>
<div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'>
<div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'>
<a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
<BookOpen01 className='h-3 w-3 shrink-0' />
<div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<ArrowUpRight className='h-3 w-3 shrink-0' />
<RiBookOpenLine className='h-3 w-3 shrink-0' />
<div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
<RiArrowRightUpLine className='h-3 w-3 shrink-0' />
</a>
</div>
</div>

+ 1
- 1
web/app/components/tools/provider/tool-item.tsx View File

@@ -29,7 +29,7 @@ const ToolItem = ({
return (
<>
<div
className={cn('bg-components-panel-item-bg mb-2 cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')}
className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')}
onClick={() => !disabled && setShowDetail(true)}
>
<div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div>

+ 13
- 0
web/app/components/tools/types.ts View File

@@ -29,6 +29,7 @@ export enum CollectionType {
custom = 'api',
model = 'model',
workflow = 'workflow',
mcp = 'mcp',
}

export type Emoji = {
@@ -50,6 +51,10 @@ export type Collection = {
labels: string[]
plugin_id?: string
letter?: string
// MCP Server
server_url?: string
updated_at?: number
server_identifier?: string
}

export type ToolParameter = {
@@ -168,3 +173,11 @@ export type WorkflowToolProviderResponse = {
}
privacy_policy: string
}

export type MCPServerDetail = {
id: string
server_code: string
description: string
status: string
parameters?: Record<string, string>
}

+ 104
- 4
web/app/components/tools/utils/to-form-schema.ts View File

@@ -1,4 +1,7 @@
import type { ToolCredential, ToolParameter } from '../types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'

export const toType = (type: string) => {
switch (type) {
case 'string':
@@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => {
return formSchemas
}

export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => {
export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
const newValues = { ...value }
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
@@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
return newValues
}

export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
const correctInitialData = (type: string, target: any, defaultValue: any) => {
if (type === 'text-input' || type === 'secret-input')
target.type = 'mixed'

if (type === 'boolean') {
if (typeof defaultValue === 'string')
target.value = defaultValue === 'true' || defaultValue === '1'

if (typeof defaultValue === 'boolean')
target.value = defaultValue

if (typeof defaultValue === 'number')
target.value = defaultValue === 1
}

if (type === 'number-input') {
if (typeof defaultValue === 'string' && defaultValue !== '')
target.value = Number.parseFloat(defaultValue)
}

if (type === 'app-selector' || type === 'model-selector')
target.value = defaultValue

return target
}

export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
const newValues = {} as any
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
const value = formSchema.default
newValues[formSchema.variable] = {
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
value: {
type: 'constant',
value: formSchema.default,
},
...(isReasoning ? { auto: 1, value: null } : {}),
}
if (!isReasoning)
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value)
}
})
return newValues
@@ -80,7 +116,9 @@ export const generateFormValue = (value: Record<string, any>, formSchemas: { var
export const getPlainValue = (value: Record<string, any>) => {
const plainValue = { ...value }
Object.keys(plainValue).forEach((key) => {
plainValue[key] = value[key].value
plainValue[key] = {
...value[key].value,
}
})
return plainValue
}
@@ -94,3 +132,65 @@ export const getStructureValue = (value: Record<string, any>) => {
})
return newValue
}

export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
const newValues = { ...value }
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
const value = formSchema.default
newValues[formSchema.variable] = {
type: 'constant',
value: formSchema.default,
}
newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value)
}
})
return newValues
}

const getVarKindType = (type: FormTypeEnum) => {
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return VarKindType.variable
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
return VarKindType.constant
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
return VarKindType.mixed
}

export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
const newValues = {} as any
if (!isReasoning) {
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
newValues[formSchema.variable] = {
value: {
type: 'constant',
value: itemValue.value,
},
}
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value)
})
}
else {
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if (itemValue.auto === 1) {
newValues[formSchema.variable] = {
auto: 1,
value: null,
}
}
else {
newValues[formSchema.variable] = {
auto: 0,
value: itemValue.value || {
type: getVarKindType(formSchema.type as FormTypeEnum),
value: null,
},
}
}
})
}
return newValues
}

+ 4
- 2
web/app/components/workflow-app/components/workflow-header/features-trigger.tsx View File

@@ -23,7 +23,7 @@ import {
InputVarType,
} from '@/app/components/workflow/types'
import { useToastContext } from '@/app/components/base/toast'
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import type { PublishWorkflowParams } from '@/types/workflow'
import { fetchAppDetail } from '@/service/apps'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -89,6 +89,7 @@ const FeaturesTrigger = () => {
}
}, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const updatePublishedWorkflow = useInvalidateAppWorkflow()
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
@@ -98,6 +99,7 @@ const FeaturesTrigger = () => {

if (res) {
notify({ type: 'success', message: t('common.api.actionSuccess') })
updatePublishedWorkflow(appID!)
updateAppDetail()
workflowStore.getState().setPublishedAt(res.created_at)
resetWorkflowVersionHistory()
@@ -106,7 +108,7 @@ const FeaturesTrigger = () => {
else {
throw new Error('Checklist failed')
}
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
}, [handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])

const onPublisherToggle = useCallback((state: boolean) => {
if (state)

+ 24
- 26
web/app/components/workflow/block-selector/all-tools.tsx View File

@@ -5,10 +5,11 @@ import {
useState,
} from 'react'
import type {
BlockEnum,
OnSelectBlock,
ToolWithProvider,
} from '../types'
import type { ToolValue } from './types'
import type { ToolDefaultValue, ToolValue } from './types'
import { ToolTypeEnum } from './types'
import Tools from './tools'
import { useToolTabs } from './hooks'
@@ -17,8 +18,6 @@ import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import ActionButton from '../../base/action-button'
import { RiAddLine } from '@remixicon/react'
import { PluginType } from '../../plugins/types'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
@@ -31,11 +30,12 @@ type AllToolsProps = {
buildInTools: ToolWithProvider[]
customTools: ToolWithProvider[]
workflowTools: ToolWithProvider[]
mcpTools: ToolWithProvider[]
onSelect: OnSelectBlock
supportAddCustomTool?: boolean
onAddedCustomTool?: () => void
onShowAddCustomCollectionModal?: () => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const DEFAULT_TAGS: AllToolsProps['tags'] = []
@@ -46,12 +46,14 @@ const AllTools = ({
searchText,
tags = DEFAULT_TAGS,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
buildInTools,
workflowTools,
customTools,
supportAddCustomTool,
onShowAddCustomCollectionModal,
mcpTools = [],
selectedTools,
canChooseMCPTool,
}: AllToolsProps) => {
const language = useGetLanguage()
const tabs = useToolTabs()
@@ -64,13 +66,15 @@ const AllTools = ({
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []
if (activeTab === ToolTypeEnum.All)
mergedTools = [...buildInTools, ...customTools, ...workflowTools]
mergedTools = [...buildInTools, ...customTools, ...workflowTools, ...mcpTools]
if (activeTab === ToolTypeEnum.BuiltIn)
mergedTools = buildInTools
if (activeTab === ToolTypeEnum.Custom)
mergedTools = customTools
if (activeTab === ToolTypeEnum.Workflow)
mergedTools = workflowTools
if (activeTab === ToolTypeEnum.MCP)
mergedTools = mcpTools

if (!hasFilter)
return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0)
@@ -80,7 +84,7 @@ const AllTools = ({
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
})
})
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter])
}, [activeTab, buildInTools, customTools, workflowTools, mcpTools, searchText, language, hasFilter])

const {
queryPluginsWithDebounced: fetchPlugins,
@@ -88,7 +92,6 @@ const AllTools = ({
} = useMarketplacePlugins()

const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)

useEffect(() => {
if (!enable_marketplace) return
if (searchText || tags.length > 0) {
@@ -103,10 +106,11 @@ const AllTools = ({

const pluginRef = useRef<ListRef>(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab)

return (
<div className={cn(className)}>
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
<div className='flex items-center justify-between border-b border-divider-subtle px-3'>
<div className='flex h-8 items-center space-x-1'>
{
tabs.map(tab => (
@@ -124,17 +128,8 @@ const AllTools = ({
))
}
</div>
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
{supportAddCustomTool && (
<div className='flex items-center'>
<div className='mr-1.5 h-3.5 w-px bg-divider-regular'></div>
<ActionButton
className='bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text'
onClick={onShowAddCustomCollectionModal}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
</div>
{isSupportGroupView && (
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
)}
</div>
<div
@@ -144,12 +139,15 @@ const AllTools = ({
>
<Tools
className={toolContentClassName}
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
tools={tools}
onSelect={onSelect}
viewType={activeView}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
toolType={activeTab}
viewType={isSupportGroupView ? activeView : ViewType.flat}
hasSearchText={!!searchText}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
{/* Plugins from marketplace */}
{enable_marketplace && <PluginList

+ 10
- 3
web/app/components/workflow/block-selector/hooks.ts View File

@@ -31,10 +31,9 @@ export const useTabs = () => {
]
}

export const useToolTabs = () => {
export const useToolTabs = (isHideMCPTools?: boolean) => {
const { t } = useTranslation()

return [
const tabs = [
{
key: ToolTypeEnum.All,
name: t('workflow.tabs.allTool'),
@@ -52,4 +51,12 @@ export const useToolTabs = () => {
name: t('workflow.tabs.workflowTool'),
},
]
if(!isHideMCPTools) {
tabs.push({
key: ToolTypeEnum.MCP,
name: 'MCP',
})
}

return tabs
}

+ 2
- 2
web/app/components/workflow/block-selector/index-bar.tsx View File

@@ -83,8 +83,8 @@ const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => {
element.scrollIntoView({ behavior: 'smooth' })
}
return (
<div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', className)}>
<div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div>
<div className={classNames('index-bar sticky top-[20px] flex h-full w-6 flex-col items-center justify-center text-xs font-medium text-text-quaternary', className)}>
<div className={classNames('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div>
{letters.map(letter => (
<div className="cursor-pointer hover:text-text-secondary" key={letter} onClick={() => handleIndexClick(letter)}>
{letter}

+ 26
- 24
web/app/components/workflow/block-selector/index.tsx View File

@@ -129,33 +129,35 @@ const NodeSelector: FC<NodeSelectorProps> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
)}

</div>
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<Input
showLeftIcon
showClearIcon
autoFocus
value={searchText}
placeholder={searchPlaceholder}
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
inputClassName='grow'
/>
)}
</div>
}
onSelect={handleSelect}
searchText={searchText}
tags={tags}

+ 15
- 13
web/app/components/workflow/block-selector/market-place-plugin/list.tsx View File

@@ -80,7 +80,7 @@ const List = forwardRef<ListRef, ListProps>(({
)
}

const maxWidthClassName = toolContentClassName || 'max-w-[300px]'
const maxWidthClassName = toolContentClassName || 'max-w-[100%]'

return (
<>
@@ -109,18 +109,20 @@ const List = forwardRef<ListRef, ListProps>(({
onAction={noop}
/>
))}
<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
href={urlWithSearchText}
target='_blank'
className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only'
>
<RiSearchLine className='mr-0.5 h-3 w-3' />
<span>{t('plugin.searchInMarketplace')}</span>
</Link>
<div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div>
</div>
{list.length > 0 && (
<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
href={urlWithSearchText}
target='_blank'
className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only'
>
<RiSearchLine className='mr-0.5 h-3 w-3' />
<span>{t('plugin.searchInMarketplace')}</span>
</Link>
<div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div>
</div>
)}
</div>
</>
)

+ 18
- 10
web/app/components/workflow/block-selector/tabs.tsx View File

@@ -1,6 +1,6 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
@@ -16,6 +16,7 @@ export type TabsProps = {
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
filterElem: React.ReactNode
noBlocks?: boolean
}
const Tabs: FC<TabsProps> = ({
@@ -25,26 +26,28 @@ const Tabs: FC<TabsProps> = ({
searchText,
onSelect,
availableBlocksTypes,
filterElem,
noBlocks,
}) => {
const tabs = useTabs()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()

return (
<div onClick={e => e.stopPropagation()}>
{
!noBlocks && (
<div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'>
<div className='relative flex bg-background-section-burn pl-1 pt-1'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1',
'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ',
activeTab === tab.key
? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600'
? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
: 'text-text-tertiary',
)}
onClick={() => onActiveTabChange(tab.key)}
@@ -56,25 +59,30 @@ const Tabs: FC<TabsProps> = ({
</div>
)
}
{filterElem}
{
activeTab === TabsEnum.Blocks && !noBlocks && (
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
<div className='border-t border-divider-subtle'>
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
</div>
)
}
{
activeTab === TabsEnum.Tools && (
<AllTools
className='w-[315px]'
searchText={searchText}
onSelect={onSelect}
tags={tags}
canNotSelectMultiple
buildInTools={buildInTools || []}
customTools={customTools || []}
workflowTools={workflowTools || []}
mcpTools={mcpTools || []}
canChooseMCPTool
/>
)
}

+ 20
- 6
web/app/components/workflow/block-selector/tool-picker.tsx View File

@@ -23,7 +23,7 @@ import {
} from '@/service/tools'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import Toast from '@/app/components/base/toast'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools'
import cn from '@/utils/classnames'

type Props = {
@@ -35,9 +35,11 @@ type Props = {
isShow: boolean
onShowChange: (isShow: boolean) => void
onSelect: (tool: ToolDefaultValue) => void
onSelectMultiple: (tools: ToolDefaultValue[]) => void
supportAddCustomTool?: boolean
scope?: string
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const ToolPicker: FC<Props> = ({
@@ -48,10 +50,12 @@ const ToolPicker: FC<Props> = ({
isShow,
onShowChange,
onSelect,
onSelectMultiple,
supportAddCustomTool,
scope = 'all',
selectedTools,
panelClassName,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@@ -61,6 +65,7 @@ const ToolPicker: FC<Props> = ({
const { data: customTools } = useAllCustomTools()
const invalidateCustomTools = useInvalidateAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()

const { builtinToolList, customToolList, workflowToolList } = useMemo(() => {
if (scope === 'plugins') {
@@ -102,6 +107,10 @@ const ToolPicker: FC<Props> = ({
onSelect(tool!)
}

const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => {
onSelectMultiple(tools)
}

const [isShowEditCollectionToolModal, {
setFalse: hideEditCustomCollectionModal,
setTrue: showEditCustomCollectionModal,
@@ -142,7 +151,7 @@ const ToolPicker: FC<Props> = ({
</PortalToFollowElemTrigger>

<PortalToFollowElemContent className='z-[1000]'>
<div className={cn('relative min-h-20 w-[356px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
<div className='p-2 pb-1'>
<SearchBox
search={searchText}
@@ -151,21 +160,26 @@ const ToolPicker: FC<Props> = ({
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
inputClassName='grow'

/>
</div>
<AllTools
className='mt-1'
toolContentClassName='max-w-[360px]'
toolContentClassName='max-w-[100%]'
tags={tags}
searchText={searchText}
onSelect={handleSelect}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
</PortalToFollowElemContent>

+ 9
- 11
web/app/components/workflow/block-selector/tool/action-item.tsx View File

@@ -10,13 +10,12 @@ import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { RiCheckLine } from '@remixicon/react'
import Badge from '@/app/components/base/badge'

type Props = {
provider: ToolWithProvider
payload: Tool
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}

@@ -25,6 +24,7 @@ const ToolItem: FC<Props> = ({
payload,
onSelect,
disabled,
isAdded,
}) => {
const { t } = useTranslation()

@@ -71,18 +71,16 @@ const ToolItem: FC<Props> = ({
output_schema: payload.output_schema,
paramSchemas: payload.parameters,
params,
meta: provider.meta,
})
}}
>
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary', disabled && 'opacity-30')}>{payload.label[language]}</div>
{disabled && <Badge
className='flex h-5 items-center space-x-0.5 text-text-tertiary'
uppercase
>
<RiCheckLine className='h-3 w-3 ' />
<div>{t('tools.addToolModal.added')}</div>
</Badge>
}
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
</div>
{isAdded && (
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
)}
</div>
</Tooltip >
)

+ 33
- 20
web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx View File

@@ -11,21 +11,29 @@ import { useMemo } from 'react'
type Props = {
payload: ToolWithProvider[]
isShowLetterIndex: boolean
indexBar: React.ReactNode
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
letters: string[]
toolRefs: any
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const ToolViewFlatView: FC<Props> = ({
letters,
payload,
isShowLetterIndex,
indexBar,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
toolRefs,
selectedTools,
canChooseMCPTool,
}) => {
const firstLetterToolIds = useMemo(() => {
const res: Record<string, string> = {}
@@ -37,26 +45,31 @@ const ToolViewFlatView: FC<Props> = ({
return res
}, [payload, letters])
return (
<div>
{payload.map(tool => (
<div
key={tool.id}
ref={(el) => {
const letter = firstLetterToolIds[tool.id]
if (letter)
toolRefs.current[letter] = el
}}
>
<Tool
payload={tool}
viewType={ViewType.flat}
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
selectedTools={selectedTools}
/>
</div>
))}
<div className='flex w-full'>
<div className='mr-1 grow'>
{payload.map(tool => (
<div
key={tool.id}
ref={(el) => {
const letter = firstLetterToolIds[tool.id]
if (letter)
toolRefs.current[letter] = el
}}
>
<Tool
payload={tool}
viewType={ViewType.flat}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
))}
</div>
{isShowLetterIndex && indexBar}
</div>
)
}

+ 9
- 0
web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx View File

@@ -12,7 +12,10 @@ type Props = {
toolList: ToolWithProvider[]
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const Item: FC<Props> = ({
@@ -20,7 +23,10 @@ const Item: FC<Props> = ({
toolList,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
return (
<div>
@@ -36,7 +42,10 @@ const Item: FC<Props> = ({
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
))}
</div>

+ 9
- 0
web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx View File

@@ -12,14 +12,20 @@ type Props = {
payload: Record<string, ToolWithProvider[]>
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const ToolListTreeView: FC<Props> = ({
payload,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const getI18nGroupName = useCallback((name: string) => {
@@ -46,7 +52,10 @@ const ToolListTreeView: FC<Props> = ({
toolList={payload[groupName]}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
))}
</div>

+ 120
- 34
web/app/components/workflow/block-selector/tool/tool.tsx View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
@@ -13,36 +13,108 @@ import { ViewType } from '../view-type-select'
import ActionItem from './action-item'
import BlockIcon from '../../block-icon'
import { useTranslation } from 'react-i18next'
import { useHover } from 'ahooks'
import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip'
import { Mcp } from '@/app/components/base/icons/src/vender/other'

type Props = {
className?: string
payload: ToolWithProvider
viewType: ViewType
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}

const Tool: FC<Props> = ({
className,
payload,
viewType,
isShowLetterIndex,
hasSearchText,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
selectedTools,
canChooseMCPTool,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.tools
const hasAction = true // Now always support actions
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const getIsDisabled = (tool: ToolType) => {
const ref = useRef(null)
const isHovering = useHover(ref)
const isMCPTool = payload.type === CollectionType.mcp
const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool
const getIsDisabled = useCallback((tool: ToolType) => {
if (!selectedTools || !selectedTools.length) return false
return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name)
}
return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name)
}, [payload.id, payload.name, selectedTools])

const totalToolsNum = actions.length
const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length
const isAllSelected = selectedToolsNum === totalToolsNum

const notShowProviderSelectInfo = useMemo(() => {
if (isAllSelected) {
return (
<span className='system-xs-regular text-text-tertiary'>
{t('tools.addToolModal.added')}
</span>
)
}
}, [isAllSelected, t])
const selectedInfo = useMemo(() => {
if (isHovering && !isAllSelected) {
return (
<span className='system-xs-regular text-components-button-secondary-accent-text'
onClick={(e) => {
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
params[item.name] = ''
})
}
return {
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
tool_label: tool.label[language],
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,
}
}))
}}
>
{t('workflow.tabs.addAll')}
</span>
)
}

if (selectedToolsNum === 0)
return <></>

return (
<span className='system-xs-regular text-text-tertiary'>
{isAllSelected
? t('workflow.tabs.allAdded')
: `${selectedToolsNum} / ${totalToolsNum}`
}
</span>
)
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])

useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
@@ -71,59 +143,73 @@ const Tool: FC<Props> = ({
return (
<div
key={payload.id}
className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')}
className={cn('mb-1 last-of-type:mb-0')}
ref={ref}
>
<div className={cn(className)}>
<div
className='flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
onClick={() => {
if (hasAction)
if (hasAction) {
setFold(!isFold)
return
}

// Now always support actions
// if (payload.parameters) {
// payload.parameters.forEach((item) => {
// params[item.name] = ''
// })
// }
// onSelect(BlockEnum.Tool, {
// provider_id: payload.id,
// provider_type: payload.type,
// provider_name: payload.name,
// tool_name: payload.name,
// tool_label: payload.label[language],
// title: payload.label[language],
// params: {},
// })
const tool = actions[0]
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
params[item.name] = ''
})
}
onSelect(BlockEnum.Tool, {
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
tool_label: tool.label[language],
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,
})
}}
>
<div className='flex h-8 grow items-center'>
<div className={cn('flex h-8 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}>
<BlockIcon
className='shrink-0'
type={BlockEnum.Tool}
toolIcon={payload.icon}
/>
<div className='ml-2 w-0 flex-1 grow truncate text-sm text-text-primary'>{payload.label[language]}</div>
<div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'>
<span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
{isFlatView && groupName && (
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span>
)}
{isMCPTool && <Mcp className='ml-2 size-3.5 shrink-0 text-text-quaternary' />}
</div>
</div>

<div className='flex items-center'>
{isFlatView && (
<div className='system-xs-regular text-text-tertiary'>{groupName}</div>
)}
<div className='ml-2 flex items-center'>
{!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)}
{isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />}
{hasAction && (
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} />
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
)}
</div>
</div>

{hasAction && !isFold && (
{!notShowProvider && hasAction && !isFold && (
actions.map(action => (
<ActionItem
key={action.name}
provider={payload}
payload={action}
onSelect={onSelect}
disabled={getIsDisabled(action)}
disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip}
isAdded={getIsDisabled(action)}
/>
))
)}

+ 24
- 9
web/app/components/workflow/block-selector/tools.tsx View File

@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
import type { BlockEnum, ToolWithProvider } from '../types'
import IndexBar, { groupItems } from './index-bar'
import type { ToolDefaultValue, ToolValue } from './types'
import type { ToolTypeEnum } from './types'
import { ViewType } from './view-type-select'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { useGetLanguage } from '@/context/i18n'
@@ -15,25 +16,34 @@ import ToolListFlatView from './tool/tool-list-flat-view/list'
import classNames from '@/utils/classnames'

type ToolsProps = {
showWorkflowEmpty: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
tools: ToolWithProvider[]
viewType: ViewType
hasSearchText: boolean
toolType?: ToolTypeEnum
isAgent?: boolean
className?: string
indexBarClassName?: string
selectedTools?: ToolValue[]
canChooseMCPTool?: boolean
}
const Blocks = ({
showWorkflowEmpty,
onSelect,
canNotSelectMultiple,
onSelectMultiple,
tools,
viewType,
hasSearchText,
toolType,
isAgent,
className,
indexBarClassName,
selectedTools,
canChooseMCPTool,
}: ToolsProps) => {
// const tools: any = []
const { t } = useTranslation()
const language = useGetLanguage()
const isFlatView = viewType === ViewType.flat
@@ -87,15 +97,15 @@ const Blocks = ({
const toolRefs = useRef({})

return (
<div className={classNames('p-1 max-w-[320px]', className)}>
<div className={classNames('max-w-[100%] p-1', className)}>
{
!tools.length && !showWorkflowEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
!tools.length && hasSearchText && (
<div className='mt-2 flex h-[22px] items-center px-3 text-xs font-medium text-text-secondary'>{t('workflow.tabs.noResult')}</div>
)
}
{!tools.length && showWorkflowEmpty && (
{!tools.length && !hasSearchText && (
<div className='py-10'>
<Empty />
<Empty type={toolType!} isAgent={isAgent}/>
</div>
)}
{!!tools.length && (
@@ -107,19 +117,24 @@ const Blocks = ({
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
/>
) : (
<ToolListTreeView
payload={treeViewToolsData}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
)
)}

{isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
</div>
)
}

+ 5
- 0
web/app/components/workflow/block-selector/types.ts View File

@@ -1,3 +1,5 @@
import type { PluginMeta } from '../../plugins/types'

export enum TabsEnum {
Blocks = 'blocks',
Tools = 'tools',
@@ -8,6 +10,7 @@ export enum ToolTypeEnum {
BuiltIn = 'built-in',
Custom = 'custom',
Workflow = 'workflow',
MCP = 'mcp',
}

export enum BlockClassificationEnum {
@@ -30,10 +33,12 @@ export type ToolDefaultValue = {
params: Record<string, any>
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
meta?: PluginMeta
}

export type ToolValue = {
provider_name: string
provider_show_name?: string
tool_name: string
tool_label: string
tool_description?: string

+ 31
- 0
web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts View File

@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react'

const useCheckVerticalScrollbar = (ref: React.RefObject<HTMLElement>) => {
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false)

useEffect(() => {
const elem = ref.current
if (!elem) return

const checkScrollbar = () => {
setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight)
}

checkScrollbar()

const resizeObserver = new ResizeObserver(checkScrollbar)
resizeObserver.observe(elem)

const mutationObserver = new MutationObserver(checkScrollbar)
mutationObserver.observe(elem, { childList: true, subtree: true, characterData: true })

return () => {
resizeObserver.disconnect()
mutationObserver.disconnect()
}
}, [ref])

return hasVerticalScrollbar
}

export default useCheckVerticalScrollbar

+ 13
- 1
web/app/components/workflow/hooks/use-workflow.ts View File

@@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import {
fetchAllBuiltInTools,
fetchAllCustomTools,
fetchAllMCPTools,
fetchAllWorkflowTools,
} from '@/service/tools'
import { CollectionType } from '@/app/components/tools/types'
@@ -445,6 +446,13 @@ export const useFetchToolsData = () => {
workflowTools: workflowTools || [],
})
}
if(type === 'mcp') {
const mcpTools = await fetchAllMCPTools()

workflowStore.setState({
mcpTools: mcpTools || [],
})
}
}, [workflowStore])

return {
@@ -491,6 +499,8 @@ 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 toolIcon = useMemo(() => {
if(!data)
return ''
@@ -500,11 +510,13 @@ export const useToolIcon = (data: Node['data']) => {
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
}
}, [data, buildInTools, customTools, workflowTools])
}, [data, buildInTools, customTools, mcpTools, workflowTools])

return toolIcon
}

+ 1
- 0
web/app/components/workflow/index.tsx View File

@@ -234,6 +234,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
handleFetchAllTools('mcp')
}, [handleFetchAllTools])

const {

+ 14
- 8
web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx View File

@@ -68,6 +68,7 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
icon: getIcon(item.declaration.identity.icon),
label: item.declaration.identity.label as any,
type: CollectionType.all,
meta: item.meta,
tools: item.declaration.strategies.map(strategy => ({
name: strategy.identity.name,
author: strategy.identity.author,
@@ -89,10 +90,13 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s
export type AgentStrategySelectorProps = {
value?: Strategy,
onChange: (value?: Strategy) => void,
canChooseMCPTool: boolean,
}

export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
const { value, onChange } = props
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)

const { value, onChange, canChooseMCPTool } = props
const [open, setOpen] = useState(false)
const [viewType, setViewType] = useState<ViewType>(ViewType.flat)
const [query, setQuery] = useState('')
@@ -132,8 +136,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()

const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)

useEffect(() => {
if (!enable_marketplace) return
if (query) {
@@ -214,21 +216,25 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
plugin_unique_identifier: tool!.provider_id,
meta: tool!.meta,
})
setOpen(false)
}}
className='h-full max-h-full max-w-none overflow-y-auto'
indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} />
{enable_marketplace
&& <PluginList
indexBarClassName='top-0 xl:top-36'
hasSearchText={false}
canNotSelectMultiple
canChooseMCPTool={canChooseMCPTool}
isAgent
/>
{enable_marketplace && <PluginList
ref={pluginRef}
wrapElemRef={wrapElemRef}
list={notInstalledPlugins}
searchText={query}
tags={DEFAULT_TAGS}
disableMaxWidth
/>
}
/>}
</main>
</div>
</PortalToFollowElemContent>

+ 11
- 2
web/app/components/workflow/nodes/_base/components/agent-strategy.tsx View File

@@ -19,6 +19,8 @@ import { useWorkflowStore } from '../../../store'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import type { NodeOutPutVar } from '../../../types'
import type { Node } from 'reactflow'
import type { PluginMeta } from '@/app/components/plugins/types'
import { noop } from 'lodash'
import { useDocLink } from '@/context/i18n'

export type Strategy = {
@@ -27,6 +29,7 @@ export type Strategy = {
agent_strategy_label: string
agent_output_schema: Record<string, any>
plugin_unique_identifier: string
meta?: PluginMeta
}

export type AgentStrategyProps = {
@@ -38,6 +41,7 @@ export type AgentStrategyProps = {
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
nodeId?: string
canChooseMCPTool: boolean
}

type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
@@ -48,7 +52,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema

export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props
const { t } = useTranslation()
const docLink = useDocLink()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
@@ -57,6 +61,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()

const override: ComponentProps<typeof Form<CustomField>>['override'] = [
[FormTypeEnum.textNumber, FormTypeEnum.textInput],
(schema, props) => {
@@ -168,6 +173,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
value={value}
onSelect={item => onChange(item)}
onDelete={() => onChange(null)}
canChooseMCPTool={canChooseMCPTool}
onSelectMultiple={noop}
/>
</Field>
)
@@ -189,13 +196,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
onChange={onChange}
supportCollapse
required={schema.required}
canChooseMCPTool={canChooseMCPTool}
/>
)
}
}
}
return <div className='space-y-2'>
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
<AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} />
{
strategy
? <div>
@@ -215,6 +223,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
canChooseMCPTool={canChooseMCPTool}
/>
</div>
: <ListEmpty

+ 1
- 1
web/app/components/workflow/nodes/_base/components/editor/base.tsx View File

@@ -76,7 +76,7 @@ const Base: FC<Props> = ({

return (
<Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}>
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}>
<div className='flex h-7 items-center justify-between pl-3 pr-2 pt-1'>
<div className='system-xs-semibold-uppercase text-text-secondary'>{title}</div>
<div className='flex items-center' onClick={(e) => {

+ 1
- 1
web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx View File

@@ -23,7 +23,7 @@ export type Props = {
value?: string | object
placeholder?: React.JSX.Element | string
onChange?: (value: string) => void
title?: React.JSX.Element
title?: string | React.JSX.Element
language: CodeLanguage
headerRight?: React.JSX.Element
readOnly?: boolean

+ 35
- 0
web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx View File

@@ -0,0 +1,35 @@
'use client'
import type { FC } from 'react'
import cn from '@/utils/classnames'

type Props = {
value: boolean
onChange: (value: boolean) => void
}

const FormInputBoolean: FC<Props> = ({
value,
onChange,
}) => {
return (
<div className='flex w-full space-x-1'>
<div
className={cn(
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
!value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
)}
onClick={() => onChange(true)}
>True</div>
<div
className={cn(
'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
!value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
)}
onClick={() => onChange(false)}
>False</div>
</div>
)
}
export default FormInputBoolean

+ 279
- 0
web/app/components/workflow/nodes/_base/components/form-input-item.tsx View File

@@ -0,0 +1,279 @@
'use client'
import type { FC } from 'react'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import type { CredentialFormSchema } 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'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'

import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/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 '@/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'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import cn from '@/utils/classnames'
import type { Tool } from '@/app/components/tools/types'

type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: ToolVarInputs
onChange: (value: any) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
}

const FormInputItem: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTool,
currentProvider,
}) => {
const language = useLanguage()

const {
placeholder,
variable,
type,
default: defaultValue,
options,
scope,
} = schema as any
const varInput = value[variable]
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const isNumber = type === FormTypeEnum.textNumber
const isObject = type === FormTypeEnum.object
const isArray = type === FormTypeEnum.array
const isShowJSONEditor = isObject || isArray
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isBoolean = type === FormTypeEnum.boolean
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
const showTypeSwitch = isNumber || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable

const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
},
})

const targetVarType = () => {
if (isString)
return VarType.string
else if (isNumber)
return VarType.number
else if (type === FormTypeEnum.files)
return VarType.arrayFile
else if (type === FormTypeEnum.file)
return VarType.file
// else if (isSelect)
// return VarType.select
// else if (isAppSelector)
// return VarType.appSelector
// else if (isModelSelector)
// return VarType.modelSelector
// else if (isBoolean)
// return VarType.boolean
else if (isObject)
return VarType.object
else if (isArray)
return VarType.arrayObject
else
return VarType.string
}

const getFilterVar = () => {
if (isNumber)
return (varPayload: any) => varPayload.type === VarType.number
else if (isString)
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
else if (isFile)
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
else if (isBoolean)
return (varPayload: any) => varPayload.type === VarType.boolean
else if (isObject)
return (varPayload: any) => varPayload.type === VarType.object
else if (isArray)
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
return undefined
}

const getVarKindType = () => {
if (isFile)
return VarKindType.variable
if (isSelect || isBoolean || isNumber || isArray || isObject)
return VarKindType.constant
if (isString)
return VarKindType.mixed
}

const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) {
onChange({
...value,
[variable]: {
...varInput,
type: VarKindType.variable,
value: '',
},
})
}
else {
onChange({
...value,
[variable]: {
...varInput,
type: VarKindType.constant,
value: defaultValue,
},
})
}
}

const handleValueChange = (newValue: any) => {
onChange({
...value,
[variable]: {
...varInput,
type: getVarKindType(),
value: isNumber ? Number.parseFloat(newValue) : newValue,
},
})
}

const handleAppOrModelSelect = (newValue: any) => {
onChange({
...value,
[variable]: {
...varInput,
...newValue,
},
})
}

const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => {
onChange({
...value,
[variable]: {
...varInput,
type: VarKindType.variable,
value: newValue || '',
},
})
}

return (
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
{showTypeSwitch && (
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange}/>
)}
{isString && (
<MixedVariableTextInput
readOnly={readOnly}
value={varInput?.value as string || ''}
onChange={handleValueChange}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
)}
{isNumber && isConstant && (
<Input
className='h-8 grow'
type='number'
value={varInput?.value || ''}
onChange={e => handleValueChange(e.target.value)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isBoolean && (
<FormInputBoolean
value={varInput?.value as boolean}
onChange={handleValueChange}
/>
)}
{isSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly}
defaultValue={varInput?.value}
items={options.filter((option: { show_on: any[] }) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)

return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleValueChange(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
title='JSON'
value={varInput?.value as any}
isExpand
isInNode
language={CodeLanguage.json}
onChange={handleValueChange}
className='w-full'
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
/>
</div>
)}
{isAppSelector && (
<AppSelector
disabled={readOnly}
scope={scope || 'all'}
value={varInput?.value as any}
onSelect={handleAppOrModelSelect}
/>
)}
{isModelSelector && isConstant && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput?.value as any}
setModel={handleAppOrModelSelect}
readonly={readOnly}
scope={scope}
/>
)}
{showVariableSelector && (
<VarReferencePicker
zIndex={inPanel ? 1000 : undefined}
className='h-8 grow'
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={value => handleVariableSelectorChange(value, variable)}
filterVar={getFilterVar()}
schema={schema}
valueTypePlaceHolder={targetVarType()}
currentTool={currentTool}
currentProvider={currentProvider}
/>
)}
</div>
)
}
export default FormInputItem

+ 47
- 0
web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx View File

@@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEditLine,
} from '@remixicon/react'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import Tooltip from '@/app/components/base/tooltip'
import { VarType } from '@/app/components/workflow/nodes/tool/types'
import cn from '@/utils/classnames'

type Props = {
value: VarType
onChange: (value: VarType) => void
}

const FormInputTypeSwitch: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<div className='inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'>
<Tooltip
popupContent={value === VarType.variable ? '' : t('workflow.nodes.common.typeSwitch.variable')}
>
<div
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
onClick={() => onChange(VarType.variable)}
>
<Variable02 className='h-4 w-4' />
</div>
</Tooltip>
<Tooltip
popupContent={value === VarType.constant ? '' : t('workflow.nodes.common.typeSwitch.input')}
>
<div
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
onClick={() => onChange(VarType.constant)}
>
<RiEditLine className='h-4 w-4' />
</div>
</Tooltip>
</div>
)
}
export default FormInputTypeSwitch

+ 22
- 0
web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx View File

@@ -0,0 +1,22 @@
'use client'
import Tooltip from '@/app/components/base/tooltip'
import { RiAlertFill } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'

const McpToolNotSupportTooltip: FC = () => {
const { t } = useTranslation()
return (
<Tooltip
popupContent={
<div className='w-[256px]'>
{t('plugin.detailPanel.toolSelector.unsupportedMCPTool')}
</div>
}
>
<RiAlertFill className='size-4 text-text-warning-secondary' />
</Tooltip>
)
}
export default React.memo(McpToolNotSupportTooltip)

+ 1
- 1
web/app/components/workflow/nodes/_base/components/setting-item.tsx View File

@@ -13,7 +13,7 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const needTooltip = ['error', 'warning'].includes(status as any)
return <div className='relative flex items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1.5 py-1 text-xs font-normal'>
<div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}>
<div className={classNames('system-xs-medium-uppercase max-w-full shrink-0 truncate text-text-tertiary', !!children && 'max-w-[100px]')}>
{label}
</div>
<Tooltip popupContent={tooltip} disabled={!needTooltip}>

+ 1
- 0
web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx View File

@@ -528,6 +528,7 @@ const VarReferencePicker: FC<Props> = ({
onChange={handleVarReferenceChange}
itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)}
isSupportFileVar={isSupportFileVar}
zIndex={zIndex}
/>
)}
</PortalToFollowElemContent>

+ 3
- 0
web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx View File

@@ -13,6 +13,7 @@ type Props = {
onChange: (value: ValueSelector, varDetail: Var) => void
itemWidth?: number
isSupportFileVar?: boolean
zIndex?: number
}
const VarReferencePopup: FC<Props> = ({
vars,
@@ -20,6 +21,7 @@ const VarReferencePopup: FC<Props> = ({
onChange,
itemWidth,
isSupportFileVar = true,
zIndex,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
@@ -60,6 +62,7 @@ const VarReferencePopup: FC<Props> = ({
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
zIndex={zIndex}
/>
}
</div >

+ 6
- 1
web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx View File

@@ -46,6 +46,7 @@ type ItemProps = {
isSupportFileVar?: boolean
isException?: boolean
isLoopVar?: boolean
zIndex?: number
}

const objVarTypes = [VarType.object, VarType.file]
@@ -60,6 +61,7 @@ const Item: FC<ItemProps> = ({
isSupportFileVar,
isException,
isLoopVar,
zIndex,
}) => {
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
const isFile = itemData.type === VarType.file && !isStructureOutput
@@ -171,7 +173,7 @@ const Item: FC<ItemProps> = ({
</div >
</PortalToFollowElemTrigger >
<PortalToFollowElemContent style={{
zIndex: 100,
zIndex: zIndex || 100,
}}>
{(isStructureOutput || isObj) && (
<PickerStructurePanel
@@ -260,6 +262,7 @@ type Props = {
maxHeightClass?: string
onClose?: () => void
onBlur?: () => void
zIndex?: number
autoFocus?: boolean
}
const VarReferenceVars: FC<Props> = ({
@@ -272,6 +275,7 @@ const VarReferenceVars: FC<Props> = ({
maxHeightClass,
onClose,
onBlur,
zIndex,
autoFocus = true,
}) => {
const { t } = useTranslation()
@@ -357,6 +361,7 @@ const VarReferenceVars: FC<Props> = ({
isSupportFileVar={isSupportFileVar}
isException={v.isException}
isLoopVar={item.isLoop}
zIndex={zIndex}
/>
))}
</div>))

+ 6
- 0
web/app/components/workflow/nodes/_base/node.tsx View File

@@ -32,6 +32,7 @@ import {
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import { useNodeLoopInteractions } from '../loop/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import CopyID from '../tool/components/copy-id'
import {
NodeSourceHandle,
NodeTargetHandle,
@@ -321,6 +322,11 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
)
}
{data.type === BlockEnum.Tool && (
<div className='px-3 pb-2'>
<CopyID content={data.provider_id || ''} />
</div>
)}
</div>
</div>
)

+ 35
- 19
web/app/components/workflow/nodes/agent/components/tool-icon.tsx View File

@@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames'
import { memo, useMemo, useRef, useState } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { useTranslation } from 'react-i18next'
import { Group } from '@/app/components/base/icons/src/vender/other'
import AppIcon from '@/app/components/base/app-icon'

type Status = 'not-installed' | 'not-authorized' | undefined

@@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools
const { data: mcpTools } = useAllMCPTools()
const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName
return toolWithProvider.name === providerName || toolWithProvider.id === providerName
})
}, [buildInTools, customTools, providerName, workflowTools])
}, [buildInTools, customTools, providerName, workflowTools, mcpTools])

const providerNameParts = providerName.split('/')
const author = providerNameParts[0]
const name = providerNameParts[1]
const icon = useMemo(() => {
if (!isDataReady) return ''
if (currentProvider) return currentProvider.icon as string
if (currentProvider) return currentProvider.icon
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
return iconFromMarketPlace
}, [author, currentProvider, name, isDataReady])
@@ -62,19 +65,32 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
)}
ref={containerRef}
>
{(!iconFetchError && isDataReady)

? <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
: <Group className="h-3 w-3 opacity-35" />
}
{(() => {
if (iconFetchError || !icon)
return <Group className="h-3 w-3 opacity-35" />
if (typeof icon === 'string') {
return <img
src={icon}
alt='tool icon'
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
onError={() => setIconFetchError(true)}
/>
}
if (typeof icon === 'object') {
return <AppIcon
className={classNames(
'w-full h-full size-3.5 object-cover',
notSuccess && 'opacity-50',
)}
icon={icon?.content}
background={icon?.background}
/>
}
return <Group className="h-3 w-3 opacity-35" />
})()}
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
</div>
</Tooltip>

+ 16
- 2
web/app/components/workflow/nodes/agent/default.ts View File

@@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n'

const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
version: '2',
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
@@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
const schemas = toolValue.schemas || []
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
const version = payload.version
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),

+ 1
- 1
web/app/components/workflow/nodes/agent/node.tsx View File

@@ -104,7 +104,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
{t('workflow.nodes.agent.toolbox')}
</GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'>
{tools.map(tool => <ToolIcon {...tool} key={tool.id} />)}
{tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)}
</div>
</Group>}
</div>

+ 4
- 1
web/app/components/workflow/nodes/agent/panel.tsx View File

@@ -38,11 +38,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
readOnly,
outputSchema,
handleMemoryChange,
canChooseMCPTool,
} = useConfig(props.id, props.data)
const { t } = useTranslation()

const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)

return <div className='my-2'>
<Field
required
@@ -56,6 +56,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
plugin_unique_identifier: inputs.plugin_unique_identifier!,
meta: inputs.meta,
} : undefined}
onStrategyChange={(strategy) => {
setInputs({
@@ -65,6 +66,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
plugin_unique_identifier: strategy!.plugin_unique_identifier,
meta: strategy?.meta,
})
resetEditor(Date.now())
}}
@@ -74,6 +76,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
canChooseMCPTool={canChooseMCPTool}
/>
</Field>
<div className='px-4 py-2'>

+ 3
- 0
web/app/components/workflow/nodes/agent/types.ts View File

@@ -1,14 +1,17 @@
import type { CommonNodeType, Memory } from '@/app/components/workflow/types'
import type { ToolVarInputs } from '../tool/types'
import type { PluginMeta } from '@/app/components/plugins/types'

export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string
agent_strategy_name?: string
agent_strategy_label?: string
agent_parameters?: ToolVarInputs
meta?: PluginMeta
output_schema: Record<string, any>
plugin_unique_identifier?: string
memory?: Memory
version?: string
}

export enum AgentFeature {

+ 43
- 2
web/app/components/workflow/nodes/agent/use-config.ts View File

@@ -6,13 +6,16 @@ import {
useIsChatMode,
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import type { Memory, Var } from '../../types'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
import produce from 'immer'
import { isSupportMCP } from '@/utils/plugin-version-feature'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'

export type StrategyStatus = {
plugin: {
@@ -85,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => {
})
const formData = useMemo(() => {
const paramNameList = (currentStrategy?.parameters || []).map(item => item.name)
return Object.fromEntries(
const res = Object.fromEntries(
Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => {
return [key, value.value]
}),
)
return res
}, [inputs.agent_parameters, currentStrategy?.parameters])
const onFormChange = (value: Record<string, any>) => {
const res: ToolVarInputs = {}
@@ -105,6 +109,42 @@ const useConfig = (id: string, payload: AgentNodeType) => {
})
}

const formattingToolData = (data: any) => {
const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any))
const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true)
const res = produce(data, (draft: any) => {
draft.settings = settingValues
draft.parameters = paramValues
})
return res
}

const formattingLegacyData = () => {
if (inputs.version)
return inputs
const newData = produce(inputs, (draft) => {
const schemas = currentStrategy?.parameters || []
Object.keys(draft.agent_parameters || {}).forEach((key) => {
const targetSchema = schemas.find(schema => schema.name === key)
if (targetSchema?.type === FormTypeEnum.toolSelector)
draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value)
if (targetSchema?.type === FormTypeEnum.multiToolSelector)
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool))
})
draft.version = '2'
})
return newData
}

// formatting legacy data
useEffect(() => {
if (!currentStrategy)
return
const newData = formattingLegacyData()
setInputs(newData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStrategy])

// vars

const filterMemoryPromptVar = useCallback((varPayload: Var) => {
@@ -172,6 +212,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
outputSchema,
handleMemoryChange,
isChatMode,
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
}
}


+ 3
- 1
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts View File

@@ -6,6 +6,7 @@ import type { EditData } from './edit-card'
import { ArrayType, type Field, Type } from '../../../types'
import Toast from '@/app/components/base/toast'
import { findPropertyWithPath } from '../../../utils'
import _ from 'lodash'

type ChangeEventParams = {
path: string[],
@@ -19,7 +20,8 @@ type AddEventParams = {
}

export const useSchemaNodeOperations = (props: VisualEditorProps) => {
const { schema: jsonSchema, onChange } = props
const { schema: jsonSchema, onChange: doOnChange } = props
const onChange = doOnChange || _.noop
const backupSchema = useVisualEditorStore(state => state.backupSchema)
const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema)
const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField)

+ 9
- 4
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx View File

@@ -2,24 +2,29 @@ import type { FC } from 'react'
import type { SchemaRoot } from '../../../types'
import SchemaNode from './schema-node'
import { useSchemaNodeOperations } from './hooks'
import cn from '@/utils/classnames'

export type VisualEditorProps = {
className?: string
schema: SchemaRoot
onChange: (schema: SchemaRoot) => void
rootName?: string
readOnly?: boolean
onChange?: (schema: SchemaRoot) => void
}

const VisualEditor: FC<VisualEditorProps> = (props) => {
const { schema } = props
const { className, schema, readOnly } = props
useSchemaNodeOperations(props)

return (
<div className='h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2'>
<div className={cn('h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2', className)}>
<SchemaNode
name='structured_output'
name={props.rootName || 'structured_output'}
schema={schema}
required={false}
path={[]}
depth={0}
readOnly={readOnly}
/>
</div>
)

+ 5
- 1
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx View File

@@ -19,6 +19,7 @@ type SchemaNodeProps = {
path: string[]
parentPath?: string[]
depth: number
readOnly?: boolean
}

// Support 10 levels of indentation
@@ -57,6 +58,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
path,
parentPath,
depth,
readOnly,
}) => {
const [isExpanded, setIsExpanded] = useState(true)
const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty)
@@ -77,11 +79,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({
}

const handleMouseEnter = () => {
if(!readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(path.join('.'))
}

const handleMouseLeave = () => {
if(!readOnly) return
if (advancedEditing || isAddingNewField) return
setHoveringPropertyDebounced(null)
}
@@ -183,7 +187,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({
)}

{
depth === 0 && !isAddingNewField && (
!readOnly && depth === 0 && !isAddingNewField && (
<AddField />
)
}

+ 51
- 0
web/app/components/workflow/nodes/tool/components/copy-id.tsx View File

@@ -0,0 +1,51 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiFileCopyLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { debounce } from 'lodash-es'
import Tooltip from '@/app/components/base/tooltip'

type Props = {
content: string
}

const prefixEmbedded = 'appOverview.overview.appInfo.embedded'

const CopyFeedbackNew = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)

const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)

const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)

return (
<div className='inline-flex pb-0.5' onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}>
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div
className='group/copy flex items-center gap-0.5 '
onClick={onClickCopy}
>
<div
className='system-2xs-regular cursor-pointer text-text-quaternary group-hover:text-text-tertiary'
>{content}</div>
<RiFileCopyLine className='h-3 w-3 text-text-tertiary opacity-0 group-hover/copy:opacity-100' />
</div>
</Tooltip>
</div>
)
}

export default CopyFeedbackNew

+ 62
- 0
web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx View File

@@ -0,0 +1,62 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import Placeholder from './placeholder'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'

type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
return (
<PromptEditor
wrapperClassName={cn(
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
className='caret:text-text-accent'
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
placeholder={<Placeholder />}
onChange={onChange}
/>
)
}

export default memo(MixedVariableTextInput)

+ 51
- 0
web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx View File

@@ -0,0 +1,51 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { FOCUS_COMMAND } from 'lexical'
import { $insertNodes } from 'lexical'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import Badge from '@/app/components/base/badge'

const Placeholder = () => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()

const handleInsert = useCallback((text: string) => {
editor.update(() => {
const textNode = new CustomTextNode(text)
$insertNodes([textNode])
})
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
}, [editor])

return (
<div
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
onClick={(e) => {
e.stopPropagation()
handleInsert('')
}}
>
<div className='flex grow items-center'>
{t('workflow.nodes.tool.insertPlaceholder1')}
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onClick={((e) => {
e.stopPropagation()
handleInsert('/')
})}
>
{t('workflow.nodes.tool.insertPlaceholder2')}
</div>
</div>
<Badge
className='shrink-0'
text='String'
uppercase={false}
/>
</div>
)
}

export default Placeholder

+ 51
- 0
web/app/components/workflow/nodes/tool/components/tool-form/index.tsx View File

@@ -0,0 +1,51 @@
'use client'
import type { FC } from 'react'
import type { ToolVarInputs } from '../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ToolFormItem from './item'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { Tool } from '@/app/components/tools/types'

type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: ToolVarInputs
onChange: (value: ToolVarInputs) => void
onOpen?: (index: number) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
}

const ToolForm: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTool,
currentProvider,
}) => {
return (
<div className='space-y-1'>
{
schema.map((schema, index) => (
<ToolFormItem
key={index}
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentTool}
currentProvider={currentProvider}
/>
))
}
</div>
)
}
export default ToolForm

+ 0
- 0
web/app/components/workflow/nodes/tool/components/tool-form/item.tsx View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save