Bladeren bron

feat: workflow new nodes (#4683)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Patryk Garstecki <patryk20120@yahoo.pl>
Co-authored-by: Sebastian.W <thiner@gmail.com>
Co-authored-by: 呆萌闷油瓶 <253605712@qq.com>
Co-authored-by: takatost <takatost@users.noreply.github.com>
Co-authored-by: rechardwang <wh_goodjob@163.com>
Co-authored-by: Nite Knite <nkCoding@gmail.com>
Co-authored-by: Chenhe Gu <guchenhe@gmail.com>
Co-authored-by: Joshua <138381132+joshua20231026@users.noreply.github.com>
Co-authored-by: Weaxs <459312872@qq.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: leejoo0 <81673835+leejoo0@users.noreply.github.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: sino <sino2322@gmail.com>
Co-authored-by: Vikey Chen <vikeytk@gmail.com>
Co-authored-by: wanghl <Wang-HL@users.noreply.github.com>
Co-authored-by: Haolin Wang-汪皓临 <haolin.wang@atlaslovestravel.com>
Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Bowen Liang <bowenliang@apache.org>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: fanghongtai <42790567+fanghongtai@users.noreply.github.com>
Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn>
Co-authored-by: Matri <qjp@bithuman.io>
Co-authored-by: Benjamin <benjaminx@gmail.com>
zxhlyh 11 maanden geleden
bovenliggende
commit
45deaee762
100 gewijzigde bestanden met toevoegingen van 3572 en 1134 verwijderingen
  1. 1 1
      web/app/(commonLayout)/datasets/NewDatasetCard.tsx
  2. 0 10
      web/app/(commonLayout)/tools/custom/page.tsx
  3. 2 9
      web/app/(commonLayout)/tools/page.tsx
  4. 0 10
      web/app/(commonLayout)/tools/third-part/page.tsx
  5. 26 1
      web/app/components/app/app-publisher/index.tsx
  6. 0 77
      web/app/components/app/configuration/config/agent/agent-tools/choose-tool/index.tsx
  7. 10 18
      web/app/components/app/configuration/config/agent/agent-tools/index.tsx
  8. 9 6
      web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
  9. 3 1
      web/app/components/app/log/list.tsx
  10. 15 1
      web/app/components/app/store.ts
  11. 7 2
      web/app/components/base/chat/chat/answer/index.tsx
  12. 16 1
      web/app/components/base/chat/chat/answer/workflow-process.tsx
  13. 39 0
      web/app/components/base/chat/chat/hooks.ts
  14. 11 1
      web/app/components/base/drawer-plus/index.tsx
  15. 6 5
      web/app/components/base/drawer/index.tsx
  16. 16 0
      web/app/components/base/icons/assets/public/common/d.svg
  17. 5 0
      web/app/components/base/icons/assets/vender/line/files/folder.svg
  18. 5 0
      web/app/components/base/icons/assets/vender/line/others/apps-02.svg
  19. 10 0
      web/app/components/base/icons/assets/vender/line/others/colors.svg
  20. 3 0
      web/app/components/base/icons/assets/vender/line/others/exchange-02.svg
  21. 3 0
      web/app/components/base/icons/assets/vender/line/others/file-code.svg
  22. 14 0
      web/app/components/base/icons/assets/vender/line/others/tools.svg
  23. 5 0
      web/app/components/base/icons/assets/vender/workflow/iteration-start.svg
  24. 2 0
      web/app/components/base/icons/assets/vender/workflow/iteration.svg
  25. 28 0
      web/app/components/base/icons/assets/vender/workflow/parameter-extractor.svg
  26. 125 0
      web/app/components/base/icons/src/public/common/D.json
  27. 16 0
      web/app/components/base/icons/src/public/common/D.tsx
  28. 1 0
      web/app/components/base/icons/src/public/common/index.ts
  29. 39 0
      web/app/components/base/icons/src/vender/line/files/Folder.json
  30. 16 0
      web/app/components/base/icons/src/vender/line/files/Folder.tsx
  31. 1 0
      web/app/components/base/icons/src/vender/line/files/index.ts
  32. 36 0
      web/app/components/base/icons/src/vender/line/others/Apps02.json
  33. 16 0
      web/app/components/base/icons/src/vender/line/others/Apps02.tsx
  34. 66 0
      web/app/components/base/icons/src/vender/line/others/Colors.json
  35. 16 0
      web/app/components/base/icons/src/vender/line/others/Colors.tsx
  36. 26 0
      web/app/components/base/icons/src/vender/line/others/Exchange02.json
  37. 16 0
      web/app/components/base/icons/src/vender/line/others/Exchange02.tsx
  38. 26 0
      web/app/components/base/icons/src/vender/line/others/FileCode.json
  39. 16 0
      web/app/components/base/icons/src/vender/line/others/FileCode.tsx
  40. 119 0
      web/app/components/base/icons/src/vender/line/others/Tools.json
  41. 16 0
      web/app/components/base/icons/src/vender/line/others/Tools.tsx
  42. 5 0
      web/app/components/base/icons/src/vender/line/others/index.ts
  43. 25 0
      web/app/components/base/icons/src/vender/workflow/Iteration.json
  44. 16 0
      web/app/components/base/icons/src/vender/workflow/Iteration.tsx
  45. 36 0
      web/app/components/base/icons/src/vender/workflow/IterationStart.json
  46. 16 0
      web/app/components/base/icons/src/vender/workflow/IterationStart.tsx
  47. 266 0
      web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json
  48. 16 0
      web/app/components/base/icons/src/vender/workflow/ParameterExtractor.tsx
  49. 3 0
      web/app/components/base/icons/src/vender/workflow/index.ts
  50. 39 7
      web/app/components/base/message-log-modal/index.tsx
  51. 0 78
      web/app/components/base/panel/index.tsx
  52. 3 1
      web/app/components/base/portal-to-follow-elem/index.tsx
  53. 8 2
      web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx
  54. 4 1
      web/app/components/base/select/index.tsx
  55. 1 1
      web/app/components/header/HeaderWrapper.tsx
  56. 1 0
      web/app/components/header/account-setting/model-provider-page/declarations.ts
  57. 1 1
      web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx
  58. 32 0
      web/app/components/share/text-generation/result/index.tsx
  59. BIN
      web/app/components/tools/add-tool-modal/D.png
  60. 70 0
      web/app/components/tools/add-tool-modal/category.tsx
  61. BIN
      web/app/components/tools/add-tool-modal/empty.png
  62. 15 0
      web/app/components/tools/add-tool-modal/empty.tsx
  63. 235 0
      web/app/components/tools/add-tool-modal/index.tsx
  64. 146 0
      web/app/components/tools/add-tool-modal/tools.tsx
  65. 34 0
      web/app/components/tools/add-tool-modal/type.tsx
  66. 0 31
      web/app/components/tools/contribute.tsx
  67. 3 0
      web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx
  68. 23 3
      web/app/components/tools/edit-custom-collection-modal/index.tsx
  69. 4 0
      web/app/components/tools/edit-custom-collection-modal/test-api.tsx
  70. 0 259
      web/app/components/tools/index.tsx
  71. 0 38
      web/app/components/tools/info/no-custom-tool.tsx
  72. 0 38
      web/app/components/tools/info/no-search-res.tsx
  73. 6 0
      web/app/components/tools/labels/constant.ts
  74. 144 0
      web/app/components/tools/labels/filter.tsx
  75. 128 0
      web/app/components/tools/labels/selector.tsx
  76. 15 0
      web/app/components/tools/labels/store.ts
  77. 0 26
      web/app/components/tools/no-custom-tool-placeholder.tsx
  78. 117 0
      web/app/components/tools/provider-list.tsx
  79. 83 0
      web/app/components/tools/provider/card.tsx
  80. 38 0
      web/app/components/tools/provider/contribute.tsx
  81. 70 0
      web/app/components/tools/provider/custom-create-card.tsx
  82. 343 0
      web/app/components/tools/provider/detail.tsx
  83. 5 0
      web/app/components/tools/provider/grid_bg.svg
  84. 53 0
      web/app/components/tools/provider/tool-item.tsx
  85. 0 41
      web/app/components/tools/search.tsx
  86. 3 3
      web/app/components/tools/setting/build-in/config-credentials.tsx
  87. 0 77
      web/app/components/tools/tool-list/header.tsx
  88. 0 220
      web/app/components/tools/tool-list/index.tsx
  89. 0 84
      web/app/components/tools/tool-list/item.tsx
  90. 0 28
      web/app/components/tools/tool-nav-list/index.tsx
  91. 0 50
      web/app/components/tools/tool-nav-list/item.tsx
  92. 48 0
      web/app/components/tools/types.ts
  93. 225 0
      web/app/components/tools/workflow-tool/configure-button.tsx
  94. 47 0
      web/app/components/tools/workflow-tool/confirm-modal/index.tsx
  95. 3 0
      web/app/components/tools/workflow-tool/confirm-modal/style.module.css
  96. 282 0
      web/app/components/tools/workflow-tool/index.tsx
  97. 77 0
      web/app/components/tools/workflow-tool/method-selector.tsx
  98. 8 0
      web/app/components/workflow/block-icon.tsx
  99. 76 0
      web/app/components/workflow/block-selector/all-tools.tsx
  100. 12 2
      web/app/components/workflow/block-selector/constants.tsx

+ 1 - 1
web/app/(commonLayout)/datasets/NewDatasetCard.tsx

@@ -9,7 +9,7 @@ const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
 
   return (
     <a ref={ref} className='group flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-white hover:shadow-lg' href='/datasets/create'>
-      <div className='shrnik-0 flex items-center p-4 pb-3'>
+      <div className='shrink-0 flex items-center p-4 pb-3'>
         <div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg'>
           <Plus className='w-4 h-4 text-gray-500'/>
         </div>

+ 0 - 10
web/app/(commonLayout)/tools/custom/page.tsx

@@ -1,10 +0,0 @@
-import React from 'react'
-
-const Custom = () => {
-  return (
-    <div>
-      Custom
-    </div>
-  )
-}
-export default Custom

+ 2 - 9
web/app/(commonLayout)/tools/page.tsx

@@ -2,8 +2,7 @@
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import React, { useEffect } from 'react'
-import Tools from '@/app/components/tools'
-import { LOC } from '@/app/components/tools/types'
+import ToolProviderList from '@/app/components/tools/provider-list'
 
 const Layout: FC = () => {
   const { t } = useTranslation()
@@ -12,12 +11,6 @@ const Layout: FC = () => {
     document.title = `${t('tools.title')} - Dify`
   }, [])
 
-  return (
-    <div className='overflow-hidden' style={{
-      height: 'calc(100vh - 56px)',
-    }}>
-      <Tools loc={LOC.tools} />
-    </div>
-  )
+  return <ToolProviderList />
 }
 export default React.memo(Layout)

+ 0 - 10
web/app/(commonLayout)/tools/third-part/page.tsx

@@ -1,10 +0,0 @@
-import React from 'react'
-
-const ThirdPart = () => {
-  return (
-    <div>
-      Third part
-    </div>
-  )
-}
-export default ThirdPart

+ 26 - 1
web/app/components/app/app-publisher/index.tsx

@@ -23,6 +23,8 @@ import { PlayCircle } from '@/app/components/base/icons/src/vender/line/mediaAnd
 import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
 import { LeftIndent02 } from '@/app/components/base/icons/src/vender/line/editor'
 import { FileText } from '@/app/components/base/icons/src/vender/line/files'
+import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
+import type { InputVar } from '@/app/components/workflow/types'
 
 export type AppPublisherProps = {
   disabled?: boolean
@@ -37,6 +39,9 @@ export type AppPublisherProps = {
   onRestore?: () => Promise<any> | any
   onToggle?: (state: boolean) => void
   crossAxisOffset?: number
+  toolPublished?: boolean
+  inputs?: InputVar[]
+  onRefreshData?: () => void
 }
 
 const AppPublisher = ({
@@ -50,6 +55,9 @@ const AppPublisher = ({
   onRestore,
   onToggle,
   crossAxisOffset = 0,
+  toolPublished,
+  inputs,
+  onRefreshData,
 }: AppPublisherProps) => {
   const { t } = useTranslation()
   const [published, setPublished] = useState(false)
@@ -122,7 +130,7 @@ const AppPublisher = ({
         </Button>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent className='z-[11]'>
-        <div className='w-[320px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl'>
+        <div className='w-[336px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl'>
           <div className='p-4 pt-3'>
             <div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>
               {publishedAt ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
@@ -202,6 +210,23 @@ const AppPublisher = ({
                 </SuggestedAction>
               )}
             <SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
+            {appDetail?.mode === 'workflow' && (
+              <WorkflowToolConfigureButton
+                disabled={!publishedAt}
+                published={!!toolPublished}
+                detailNeedUpdate={!!toolPublished && published}
+                workflowAppId={appDetail?.id}
+                icon={{
+                  content: appDetail?.icon,
+                  background: appDetail?.icon_background,
+                }}
+                name={appDetail?.name}
+                description={appDetail?.description}
+                inputs={inputs}
+                handlePublish={handlePublish}
+                onRefreshData={onRefreshData}
+              />
+            )}
           </div>
         </div>
       </PortalToFollowElemContent>

+ 0 - 77
web/app/components/app/configuration/config/agent/agent-tools/choose-tool/index.tsx

@@ -1,77 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useContext } from 'use-context-selector'
-import { useTranslation } from 'react-i18next'
-import produce from 'immer'
-import Tools from '@/app/components/tools'
-import { LOC } from '@/app/components/tools/types'
-import Drawer from '@/app/components/base/drawer-plus'
-import ConfigContext from '@/context/debug-configuration'
-import type { ModelConfig } from '@/models/debug'
-import I18n from '@/context/i18n'
-
-type Props = {
-  show: boolean
-  onHide: () => void
-  selectedProviderId?: string
-}
-
-const ChooseTool: FC<Props> = ({
-  show,
-  onHide,
-  selectedProviderId,
-}) => {
-  const { t } = useTranslation()
-  const { locale } = useContext(I18n)
-  const {
-    modelConfig,
-    setModelConfig,
-  } = useContext(ConfigContext)
-  if (!show)
-    return null
-
-  return (
-    <Drawer
-      isShow
-      onHide={onHide}
-      title={t('tools.addTool') as string}
-      panelClassName='mt-2 !w-[760px]'
-      maxWidthClassName='!max-w-[760px]'
-      height='calc(100vh - 16px)'
-      contentClassName='!bg-gray-100'
-      headerClassName='!border-b-black/5'
-      body={
-        <Tools
-          loc={LOC.app}
-          selectedProviderId={selectedProviderId}
-          onAddTool={(collection, tool) => {
-            const parameters: Record<string, string> = {}
-            if (tool.parameters) {
-              tool.parameters.forEach((item) => {
-                parameters[item.name] = ''
-              })
-            }
-
-            const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => {
-              draft.agentConfig.tools.push({
-                provider_id: collection.id || collection.name,
-                provider_type: collection.type,
-                provider_name: collection.name,
-                tool_name: tool.name,
-                tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')],
-                tool_parameters: parameters,
-                enabled: true,
-              })
-            })
-            setModelConfig(nexModelConfig)
-          }}
-          addedTools={(modelConfig?.agentConfig?.tools as any) || []}
-        />
-      }
-      isShowMask={true}
-      clickOutsideNotOpen={false}
-    />
-  )
-}
-export default React.memo(ChooseTool)

+ 10 - 18
web/app/components/app/configuration/config/agent/agent-tools/index.tsx

@@ -6,7 +6,6 @@ import cn from 'classnames'
 import { useContext } from 'use-context-selector'
 import produce from 'immer'
 import { useFormattingChangedDispatcher } from '../../../debug/hooks'
-import ChooseTool from './choose-tool'
 import SettingBuiltInTool from './setting-built-in-tool'
 import Panel from '@/app/components/app/configuration/base/feature-panel'
 import Tooltip from '@/app/components/base/tooltip'
@@ -22,6 +21,7 @@ import { MAX_TOOLS_NUM } from '@/config'
 import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
 import TooltipPlus from '@/app/components/base/tooltip-plus'
 import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
+import AddToolModal from '@/app/components/tools/add-tool-modal'
 
 type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
 const AgentTools: FC = () => {
@@ -31,7 +31,6 @@ const AgentTools: FC = () => {
   const formattingChangedDispatcher = useFormattingChangedDispatcher()
 
   const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
-  const [selectedProviderId, setSelectedProviderId] = useState<string | undefined>(undefined)
   const [isShowSettingTool, setIsShowSettingTool] = useState(false)
   const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
     const collection = collectionList.find(collection => collection.id === item.provider_id && collection.type === item.provider_type)
@@ -78,10 +77,7 @@ const AgentTools: FC = () => {
             {tools.length < MAX_TOOLS_NUM && (
               <>
                 <div className='ml-3 mr-1 h-3.5 w-px bg-gray-200'></div>
-                <OperationBtn type="add" onClick={() => {
-                  setSelectedProviderId(undefined)
-                  setIsShowChooseTool(true)
-                }} />
+                <OperationBtn type="add" onClick={() => setIsShowChooseTool(true)} />
               </>
             )}
           </div>
@@ -116,10 +112,14 @@ const AgentTools: FC = () => {
                         />
                       ))}
                 <div
-                  title={item.tool_name}
                   className={cn((item.isDeleted || item.notAuthor) ? 'line-through opacity-50' : '', 'grow w-0 ml-2 leading-[18px] text-[13px] font-medium text-gray-800  truncate')}
                 >
-                  {item.tool_label || item.tool_name}
+                  <span className='text-gray-800 pr-2'>{item.provider_type === CollectionType.builtIn ? item.provider_name : item.tool_label}</span>
+                  <TooltipPlus
+                    popupContent={t('tools.toolNameUsageTip')}
+                  >
+                    <span className='text-gray-500'>{item.tool_name}</span>
+                  </TooltipPlus>
                 </div>
               </div>
               <div className='shrink-0 ml-1 flex items-center'>
@@ -130,10 +130,8 @@ const AgentTools: FC = () => {
                         popupContent={t(`tools.${item.isDeleted ? 'toolRemoved' : 'notAuthorized'}`)}
                       >
                         <div className='mr-1 p-1 rounded-md hover:bg-black/5  cursor-pointer' onClick={() => {
-                          if (item.notAuthor) {
-                            setSelectedProviderId(item.provider_id)
+                          if (item.notAuthor)
                             setIsShowChooseTool(true)
-                          }
                         }}>
                           <AlertTriangle className='w-4 h-4 text-[#F79009]' />
                         </div>
@@ -153,7 +151,6 @@ const AgentTools: FC = () => {
                   )
                   : (
                     <div className='hidden group-hover:flex items-center'>
-                      {/* {item.provider_type === CollectionType.builtIn && ( */}
                       <TooltipPlus
                         popupContent={t('tools.setBuiltInTools.infoAndSetting')}
                       >
@@ -164,7 +161,6 @@ const AgentTools: FC = () => {
                           <InfoCircle className='w-4 h-4 text-gray-500' />
                         </div>
                       </TooltipPlus>
-                      {/* )} */}
 
                       <div className='p-1 rounded-md hover:bg-black/5 cursor-pointer' onClick={() => {
                         const newModelConfig = produce(modelConfig, (draft) => {
@@ -197,11 +193,7 @@ const AgentTools: FC = () => {
         </div >
       </Panel >
       {isShowChooseTool && (
-        <ChooseTool
-          show
-          onHide={() => setIsShowChooseTool(false)}
-          selectedProviderId={selectedProviderId}
-        />
+        <AddToolModal onHide={() => setIsShowChooseTool(false)} />
       )}
       {
         isShowSettingTool && (

+ 9 - 6
web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx

@@ -8,7 +8,8 @@ import Drawer from '@/app/components/base/drawer-plus'
 import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
 import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
 import type { Collection, Tool } from '@/app/components/tools/types'
-import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList } from '@/service/tools'
+import { CollectionType } from '@/app/components/tools/types'
+import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools'
 import I18n from '@/context/i18n'
 import Button from '@/app/components/base/button'
 import Loading from '@/app/components/base/loading'
@@ -64,6 +65,8 @@ const SettingBuiltInTool: FC<Props> = ({
               resolve(await fetchModelToolList(collection.name))
             else if (isBuiltIn)
               resolve(await fetchBuiltInToolList(collection.name))
+            else if (collection.type === CollectionType.workflow)
+              resolve(await fetchWorkflowToolList(collection.id))
             else
               resolve(await fetchCustomToolList(collection.name))
           }())
@@ -78,7 +81,7 @@ const SettingBuiltInTool: FC<Props> = ({
       catch (e) { }
       setIsLoading(false)
     })()
-  }, [collection?.name])
+  }, [collection?.name, collection?.id, collection?.type])
 
   useEffect(() => {
     setCurrType((!readonly && hasSetting) ? 'setting' : 'info')
@@ -150,7 +153,7 @@ const SettingBuiltInTool: FC<Props> = ({
       onHide={onHide}
       title={(
         <div className='flex'>
-          {collection.icon === 'string'
+          {typeof collection.icon === 'string'
             ? (
               <div
                 className='w-6 h-6 bg-cover bg-center rounded-md'
@@ -189,8 +192,8 @@ const SettingBuiltInTool: FC<Props> = ({
           </>)}
         </div>
       )}
-      panelClassName='mt-[65px] !w-[480px]'
-      maxWidthClassName='!max-w-[480px]'
+      panelClassName='mt-[65px] !w-[405px]'
+      maxWidthClassName='!max-w-[405px]'
       height='calc(100vh - 65px)'
       headerClassName='!border-b-black/5'
       body={
@@ -212,7 +215,7 @@ const SettingBuiltInTool: FC<Props> = ({
             </div>)}
         </div>
       }
-      isShowMask={true}
+      isShowMask={false}
       clickOutsideNotOpen={false}
     />
   )

+ 3 - 1
web/app/components/app/log/list.tsx

@@ -164,7 +164,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
   const { userProfile: { timezone } } = useAppContext()
   const { formatTime } = useTimestamp()
   const { onClose, appDetail } = useContext(DrawerContext)
-  const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
+  const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
     currentLogItem: state.currentLogItem,
     setCurrentLogItem: state.setCurrentLogItem,
     showPromptLogModal: state.showPromptLogModal,
@@ -173,6 +173,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
     setShowAgentLogModal: state.setShowAgentLogModal,
     showMessageLogModal: state.showMessageLogModal,
     setShowMessageLogModal: state.setShowMessageLogModal,
+    currentLogModalActiveTab: state.currentLogModalActiveTab,
   })))
   const { t } = useTranslation()
   const [items, setItems] = React.useState<IChatItem[]>([])
@@ -444,6 +445,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
             setCurrentLogItem()
             setShowMessageLogModal(false)
           }}
+          defaultTab={currentLogModalActiveTab}
         />
       )}
     </div>

+ 15 - 1
web/app/components/app/store.ts

@@ -6,6 +6,7 @@ type State = {
   appDetail?: App
   appSidebarExpand: string
   currentLogItem?: IChatItem
+  currentLogModalActiveTab: string
   showPromptLogModal: boolean
   showAgentLogModal: boolean
   showMessageLogModal: boolean
@@ -15,6 +16,7 @@ type Action = {
   setAppDetail: (appDetail?: App) => void
   setAppSiderbarExpand: (state: string) => void
   setCurrentLogItem: (item?: IChatItem) => void
+  setCurrentLogModalActiveTab: (tab: string) => void
   setShowPromptLogModal: (showPromptLogModal: boolean) => void
   setShowAgentLogModal: (showAgentLogModal: boolean) => void
   setShowMessageLogModal: (showMessageLogModal: boolean) => void
@@ -26,11 +28,23 @@ export const useStore = create<State & Action>(set => ({
   appSidebarExpand: '',
   setAppSiderbarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })),
   currentLogItem: undefined,
+  currentLogModalActiveTab: 'DETAIL',
   setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })),
+  setCurrentLogModalActiveTab: currentLogModalActiveTab => set(() => ({ currentLogModalActiveTab })),
   showPromptLogModal: false,
   setShowPromptLogModal: showPromptLogModal => set(() => ({ showPromptLogModal })),
   showAgentLogModal: false,
   setShowAgentLogModal: showAgentLogModal => set(() => ({ showAgentLogModal })),
   showMessageLogModal: false,
-  setShowMessageLogModal: showMessageLogModal => set(() => ({ showMessageLogModal })),
+  setShowMessageLogModal: showMessageLogModal => set(() => {
+    if (showMessageLogModal) {
+      return { showMessageLogModal }
+    }
+    else {
+      return {
+        showMessageLogModal,
+        currentLogModalActiveTab: 'DETAIL',
+      }
+    }
+  }),
 }))

+ 7 - 2
web/app/components/base/chat/chat/answer/index.tsx

@@ -97,7 +97,7 @@ const Answer: FC<AnswerProps> = ({
           )
         }
       </div>
-      <div className='chat-answer-container grow w-0 ml-4' ref={containerRef}>
+      <div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}>
         <div className={`group relative pr-10 ${chatAnswerContainerInner}`}>
           <AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
           <div
@@ -131,7 +131,12 @@ const Answer: FC<AnswerProps> = ({
             }
             {
               workflowProcess && (
-                <WorkflowProcess data={workflowProcess} hideInfo hideProcessDetail={hideProcessDetail} />
+                <WorkflowProcess
+                  data={workflowProcess}
+                  item={item}
+                  hideInfo
+                  hideProcessDetail={hideProcessDetail}
+                />
               )
             }
             {

+ 16 - 1
web/app/components/base/chat/chat/answer/workflow-process.tsx

@@ -1,20 +1,23 @@
 import {
+  useCallback,
   useEffect,
   useMemo,
   useState,
 } from 'react'
 import cn from 'classnames'
 import { useTranslation } from 'react-i18next'
-import type { WorkflowProcess } from '../../types'
+import type { ChatItem, WorkflowProcess } from '../../types'
 import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
 import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
 import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
 import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
 import { WorkflowRunningStatus } from '@/app/components/workflow/types'
 import NodePanel from '@/app/components/workflow/run/node'
+import { useStore as useAppStore } from '@/app/components/app/store'
 
 type WorkflowProcessProps = {
   data: WorkflowProcess
+  item?: ChatItem
   grayBg?: boolean
   expand?: boolean
   hideInfo?: boolean
@@ -22,6 +25,7 @@ type WorkflowProcessProps = {
 }
 const WorkflowProcessItem = ({
   data,
+  item,
   grayBg,
   expand = false,
   hideInfo = false,
@@ -48,6 +52,16 @@ const WorkflowProcessItem = ({
     setCollapse(!expand)
   }, [expand])
 
+  const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
+  const setShowMessageLogModal = useAppStore(s => s.setShowMessageLogModal)
+  const setCurrentLogModalActiveTab = useAppStore(s => s.setCurrentLogModalActiveTab)
+
+  const showIterationDetail = useCallback(() => {
+    setCurrentLogItem(item)
+    setCurrentLogModalActiveTab('TRACING')
+    setShowMessageLogModal(true)
+  }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
+
   return (
     <div
       className={cn(
@@ -97,6 +111,7 @@ const WorkflowProcessItem = ({
                     nodeInfo={node}
                     hideInfo={hideInfo}
                     hideProcessDetail={hideProcessDetail}
+                    onShowIterationDetail={showIterationDetail}
                   />
                 </div>
               ))

+ 39 - 0
web/app/components/base/chat/chat/hooks.ts

@@ -237,6 +237,8 @@ export const useChat = (
       isAnswer: true,
     }
 
+    let isInIteration = false
+
     handleResponding(true)
     hasStopResponded.current = false
 
@@ -467,7 +469,41 @@ export const useChat = (
             }
           }))
         },
+        onIterationStart: ({ data }) => {
+          responseItem.workflowProcess!.tracing!.push({
+            ...data,
+            status: WorkflowRunningStatus.Running,
+          } as any)
+          handleUpdateChatList(produce(chatListRef.current, (draft) => {
+            const currentIndex = draft.findIndex(item => item.id === responseItem.id)
+            draft[currentIndex] = {
+              ...draft[currentIndex],
+              ...responseItem,
+            }
+          }))
+          isInIteration = true
+        },
+        onIterationFinish: ({ data }) => {
+          const tracing = responseItem.workflowProcess!.tracing!
+          tracing[tracing.length - 1] = {
+            ...tracing[tracing.length - 1],
+            ...data,
+            status: WorkflowRunningStatus.Succeeded,
+          } as any
+
+          handleUpdateChatList(produce(chatListRef.current, (draft) => {
+            const currentIndex = draft.findIndex(item => item.id === responseItem.id)
+            draft[currentIndex] = {
+              ...draft[currentIndex],
+              ...responseItem,
+            }
+          }))
+          isInIteration = false
+        },
         onNodeStarted: ({ data }) => {
+          if (isInIteration)
+            return
+
           responseItem.workflowProcess!.tracing!.push({
             ...data,
             status: WorkflowRunningStatus.Running,
@@ -481,6 +517,9 @@ export const useChat = (
           }))
         },
         onNodeFinished: ({ data }) => {
+          if (isInIteration)
+            return
+
           const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
           responseItem.workflowProcess!.tracing[currentIndex] = data as any
           handleUpdateChatList(produce(chatListRef.current, (draft) => {

+ 11 - 1
web/app/components/base/drawer-plus/index.tsx

@@ -20,6 +20,7 @@ type Props = {
   foot?: JSX.Element
   isShowMask?: boolean
   clickOutsideNotOpen?: boolean
+  positionCenter?: boolean
 }
 
 const DrawerPlus: FC<Props> = ({
@@ -36,6 +37,7 @@ const DrawerPlus: FC<Props> = ({
   foot,
   isShowMask,
   clickOutsideNotOpen = true,
+  positionCenter,
 }) => {
   const ref = useRef(null)
   const media = useBreakpoints()
@@ -46,7 +48,15 @@ const DrawerPlus: FC<Props> = ({
 
   return (
     // clickOutsideNotOpen to fix confirm modal click cause drawer close
-    <Drawer isOpen={isShow} clickOutsideNotOpen={clickOutsideNotOpen} onClose={onHide} footer={null} mask={isMobile || isShowMask} panelClassname={`mt-16 mx-2 sm:mr-2 mb-3 !p-0 ${panelClassName} ${maxWidthClassName} rounded-xl`}>
+    <Drawer
+      isOpen={isShow}
+      clickOutsideNotOpen={clickOutsideNotOpen}
+      onClose={onHide}
+      footer={null}
+      mask={isMobile || isShowMask}
+      positionCenter={positionCenter}
+      panelClassname={cn('mt-16 mx-2 sm:mr-2 mb-3 !p-0 rounded-xl', panelClassName, maxWidthClassName)}
+    >
       <div
         className={cn(contentClassName, 'w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl')}
         style={{

+ 6 - 5
web/app/components/base/drawer/index.tsx

@@ -1,4 +1,5 @@
 'use client'
+import cn from 'classnames'
 import { Dialog } from '@headlessui/react'
 import { useTranslation } from 'react-i18next'
 import { XMarkIcon } from '@heroicons/react/24/outline'
@@ -11,6 +12,7 @@ export type IDrawerProps = {
   children: React.ReactNode
   footer?: React.ReactNode
   mask?: boolean
+  positionCenter?: boolean
   isOpen: boolean
   // closable: boolean
   showClose?: boolean
@@ -27,6 +29,7 @@ export default function Drawer({
   children,
   footer,
   mask = true,
+  positionCenter,
   showClose = false,
   isOpen,
   clickOutsideNotOpen,
@@ -42,14 +45,12 @@ export default function Drawer({
       onClose={() => !clickOutsideNotOpen && onClose()}
       className="fixed z-30 inset-0 overflow-y-auto"
     >
-      <div className="flex w-screen h-screen justify-end">
+      <div className={cn('flex w-screen h-screen justify-end', positionCenter && '!justify-center')}>
         {/* mask */}
         <Dialog.Overlay
-          className={`z-40 fixed inset-0 ${!mask ? '' : 'bg-black bg-opacity-30'}`}
+          className={cn('z-40 fixed inset-0', mask && 'bg-black bg-opacity-30')}
         />
-        <div className={`relative z-50 flex flex-col justify-between bg-white w-full
-        max-w-sm p-6 overflow-hidden text-left align-middle
-        shadow-xl ${panelClassname}`}>
+        <div className={cn('relative z-50 flex flex-col justify-between bg-white w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
           <>
             {title && <Dialog.Title
               as="h3"

+ 16 - 0
web/app/components/base/icons/assets/public/common/d.svg

@@ -0,0 +1,16 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z" fill="white"/>
+<path d="M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z" fill="url(#paint0_angular_19344_240446)"/>
+<path d="M7.94336 8H8.20751V15H7.94336V8Z" fill="url(#paint1_linear_19344_240446)"/>
+<defs>
+<radialGradient id="paint0_angular_19344_240446" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.9434 8) rotate(90) scale(8.75 8.75)">
+<stop stop-color="#001FC2"/>
+<stop offset="0.711334" stop-color="#0667F8" stop-opacity="0.2"/>
+<stop offset="1" stop-color="#155EEF" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint1_linear_19344_240446" x1="8.06244" y1="8.43754" x2="7.93744" y2="9.20317" gradientUnits="userSpaceOnUse">
+<stop stop-color="white" stop-opacity="0"/>
+<stop offset="1" stop-color="white"/>
+</linearGradient>
+</defs>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/files/folder.svg

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="folder">
+<path id="Icon" d="M12.8327 11.0833C12.8327 11.3928 12.7098 11.6895 12.491 11.9083C12.2722 12.1271 11.9754 12.25 11.666 12.25H2.33268C2.02326 12.25 1.72652 12.1271 1.50772 11.9083C1.28893 11.6895 1.16602 11.3928 1.16602 11.0833V2.91667C1.16602 2.60725 1.28893 2.3105 1.50772 2.09171C1.72652 1.87292 2.02326 1.75 2.33268 1.75H5.24935L6.41602 3.5H11.666C11.9754 3.5 12.2722 3.62292 12.491 3.84171C12.7098 4.0605 12.8327 4.35725 12.8327 4.66667V11.0833Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/others/apps-02.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="apps-2-line">
+<path id="Vector" d="M4.66602 7.6665C3.00916 7.6665 1.66602 6.32336 1.66602 4.6665C1.66602 3.00965 3.00916 1.6665 4.66602 1.6665C6.32287 1.6665 7.66602 3.00965 7.66602 4.6665C7.66602 6.32336 6.32287 7.6665 4.66602 7.6665ZM4.66602 14.3332C3.00916 14.3332 1.66602 12.99 1.66602 11.3332C1.66602 9.6763 3.00916 8.33317 4.66602 8.33317C6.32287 8.33317 7.66602 9.6763 7.66602 11.3332C7.66602 12.99 6.32287 14.3332 4.66602 14.3332ZM11.3327 7.6665C9.67582 7.6665 8.33268 6.32336 8.33268 4.6665C8.33268 3.00965 9.67582 1.6665 11.3327 1.6665C12.9895 1.6665 14.3327 3.00965 14.3327 4.6665C14.3327 6.32336 12.9895 7.6665 11.3327 7.6665ZM11.3327 14.3332C9.67582 14.3332 8.33268 12.99 8.33268 11.3332C8.33268 9.6763 9.67582 8.33317 11.3327 8.33317C12.9895 8.33317 14.3327 9.6763 14.3327 11.3332C14.3327 12.99 12.9895 14.3332 11.3327 14.3332ZM4.66602 6.33317C5.58649 6.33317 6.33268 5.58698 6.33268 4.6665C6.33268 3.74603 5.58649 2.99984 4.66602 2.99984C3.74554 2.99984 2.99935 3.74603 2.99935 4.6665C2.99935 5.58698 3.74554 6.33317 4.66602 6.33317ZM4.66602 12.9998C5.58649 12.9998 6.33268 12.2536 6.33268 11.3332C6.33268 10.4127 5.58649 9.6665 4.66602 9.6665C3.74554 9.6665 2.99935 10.4127 2.99935 11.3332C2.99935 12.2536 3.74554 12.9998 4.66602 12.9998ZM11.3327 6.33317C12.2531 6.33317 12.9993 5.58698 12.9993 4.6665C12.9993 3.74603 12.2531 2.99984 11.3327 2.99984C10.4122 2.99984 9.66602 3.74603 9.66602 4.6665C9.66602 5.58698 10.4122 6.33317 11.3327 6.33317ZM11.3327 12.9998C12.2531 12.9998 12.9993 12.2536 12.9993 11.3332C12.9993 10.4127 12.2531 9.6665 11.3327 9.6665C10.4122 9.6665 9.66602 10.4127 9.66602 11.3332C9.66602 12.2536 10.4122 12.9998 11.3327 12.9998Z" fill="#155EEF"/>
+</g>
+</svg>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/others/colors.svg

@@ -0,0 +1,10 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="colors" clip-path="url(#clip0_18499_53582)">
+<path id="Icon" d="M7.00032 11.9422C7.61954 12.4964 8.43724 12.8334 9.33366 12.8334C11.2667 12.8334 12.8337 11.2664 12.8337 9.33342C12.8337 7.71938 11.7411 6.36051 10.2552 5.95602M3.74543 5.95601C2.25954 6.3605 1.16699 7.71937 1.16699 9.33341C1.16699 11.2664 2.734 12.8334 4.66699 12.8334C6.59999 12.8334 8.16699 11.2664 8.16699 9.33341C8.16699 8.87813 8.08006 8.44314 7.92189 8.04415M10.5003 4.66675C10.5003 6.59974 8.93332 8.16675 7.00033 8.16675C5.06733 8.16675 3.50033 6.59974 3.50033 4.66675C3.50033 2.73375 5.06733 1.16675 7.00033 1.16675C8.93332 1.16675 10.5003 2.73375 10.5003 4.66675Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_18499_53582">
+<rect width="14" height="14" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/others/exchange-02.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.66602 14.3334C3.00916 14.3334 1.66602 12.9903 1.66602 11.3334C1.66602 9.67655 3.00916 8.33342 4.66602 8.33342C6.32287 8.33342 7.66602 9.67655 7.66602 11.3334C7.66602 12.9903 6.32287 14.3334 4.66602 14.3334ZM11.3327 7.66675C9.67582 7.66675 8.33268 6.3236 8.33268 4.66675C8.33268 3.00989 9.67582 1.66675 11.3327 1.66675C12.9895 1.66675 14.3327 3.00989 14.3327 4.66675C14.3327 6.3236 12.9895 7.66675 11.3327 7.66675ZM4.66602 13.0001C5.58649 13.0001 6.33268 12.2539 6.33268 11.3334C6.33268 10.4129 5.58649 9.66675 4.66602 9.66675C3.74554 9.66675 2.99935 10.4129 2.99935 11.3334C2.99935 12.2539 3.74554 13.0001 4.66602 13.0001ZM11.3327 6.33342C12.2531 6.33342 12.9993 5.58722 12.9993 4.66675C12.9993 3.74627 12.2531 3.00008 11.3327 3.00008C10.4122 3.00008 9.66602 3.74627 9.66602 4.66675C9.66602 5.58722 10.4122 6.33342 11.3327 6.33342ZM1.99935 5.33341C1.99935 3.49247 3.49174 2.00008 5.33268 2.00008H7.33268V3.33341H5.33268C4.22812 3.33341 3.33268 4.22885 3.33268 5.33341V7.33342H1.99935V5.33341ZM13.9993 8.66675H12.666V10.6667C12.666 11.7713 11.7706 12.6667 10.666 12.6667H8.66602V14.0001H10.666C12.5069 14.0001 13.9993 12.5077 13.9993 10.6667V8.66675Z" fill="#344054"/>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/others/file-code.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 2.66659H3.33333V13.3333H12.6667V5.33325H10V2.66659ZM2 1.99445C2 1.62929 2.29833 1.33325 2.66567 1.33325H10.6667L13.9998 4.66658L14 13.9949C14 14.3659 13.7034 14.6666 13.3377 14.6666H2.66227C2.29651 14.6666 2 14.3631 2 14.0054V1.99445ZM11.7713 7.99992L9.4142 10.3569L8.4714 9.41412L9.8856 7.99992L8.4714 6.58571L9.4142 5.6429L11.7713 7.99992ZM4.22877 7.99992L6.58579 5.6429L7.5286 6.58571L6.11438 7.99992L7.5286 9.41412L6.58579 10.3569L4.22877 7.99992Z" fill="#344054"/>
+</svg>

+ 14 - 0
web/app/components/base/icons/assets/vender/line/others/tools.svg

@@ -0,0 +1,14 @@
+<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Tools" clip-path="url(#clip0_5381_39479)">
+<path id="vector" d="M13.4375 14.4375V6.8125H2.5625V14.4375C2.5625 14.9898 3.01022 15.4375 3.5625 15.4375H12.4375C12.9898 15.4375 13.4375 14.9898 13.4375 14.4375Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="vector_2" d="M13.6254 2.875L11.1738 6.47327" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="vector_3" d="M6.3125 9.8125H9.6875" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path id="vector_4" d="M8.63355 1.88044L8.75 1.64754L8.86645 1.88044C8.97531 2.09816 9.15184 2.27469 9.36956 2.38355L9.60246 2.5L9.36956 2.61645C9.15184 2.72531 8.97531 2.90184 8.86645 3.11956L8.75 3.35246L8.63355 3.11956C8.52469 2.90184 8.34816 2.72531 8.13044 2.61645L7.89754 2.5L8.13044 2.38355C8.34816 2.27469 8.52469 2.09816 8.63355 1.88044Z" stroke="#344054" stroke-width="1.25" stroke-linecap="square" stroke-linejoin="round"/>
+<path id="vector_5" d="M4.625 3.14754L4.61865 3.16025C4.51946 3.35862 4.35862 3.51946 4.16025 3.61865L4.14754 3.625L4.16025 3.63135C4.35862 3.73054 4.51946 3.89138 4.61865 4.08975L4.625 4.10246L4.63135 4.08975C4.73054 3.89138 4.89138 3.73054 5.08975 3.63135L5.10246 3.625L5.08975 3.61865C4.89138 3.51946 4.73054 3.35862 4.63135 3.16025L4.625 3.14754ZM4.625 3.14754L4.63135 3.16025L4.625 3.14754Z" stroke="#344054" stroke-width="1.25" stroke-linecap="square" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_5381_39479">
+<rect width="16" height="16" fill="white" transform="translate(0 0.5)"/>
+</clipPath>
+</defs>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/workflow/iteration-start.svg

@@ -0,0 +1,5 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="icons/block-start">
+<path id="Vector" d="M6.8498 1.72732C6.3379 1.3754 5.6621 1.3754 5.1502 1.72732L2.1502 3.78982C1.74317 4.06965 1.5 4.53193 1.5 5.02588V8.99983C1.5 9.82828 2.17158 10.4998 3 10.4998H4.25C4.52614 10.4998 4.75 10.276 4.75 9.99983V8.24983C4.75 7.55948 5.30965 6.99983 6 6.99983C6.69035 6.99983 7.25 7.55948 7.25 8.24983V9.99983C7.25 10.276 7.47385 10.4998 7.75 10.4998H9C9.82845 10.4998 10.5 9.82828 10.5 8.99983V5.02588C10.5 4.53193 10.2568 4.06965 9.8498 3.78982L6.8498 1.72732Z" fill="white"/>
+</g>
+</svg>

File diff suppressed because it is too large
+ 2 - 0
web/app/components/base/icons/assets/vender/workflow/iteration.svg


+ 28 - 0
web/app/components/base/icons/assets/vender/workflow/parameter-extractor.svg

@@ -0,0 +1,28 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="icons/parma-extractor">
+<path id="Vector" d="M7.58398 10.3543C7.58398 10.0322 7.84514 9.771 8.16732 9.771C8.48949 9.771 8.75065 10.0322 8.75065 10.3543C8.75065 10.6765 8.48949 10.9377 8.16732 10.9377C7.84514 10.9377 7.58398 10.6765 7.58398 10.3543Z" fill="white"/>
+<path id="Vector_2" d="M9.625 10.3543C9.625 10.0322 9.88616 9.771 10.2083 9.771C10.5305 9.771 10.7917 10.0322 10.7917 10.3543C10.7917 10.6765 10.5305 10.9377 10.2083 10.9377C9.88616 10.9377 9.625 10.6765 9.625 10.3543Z" fill="white"/>
+<path id="Vector_3" d="M7.58398 3.64583C7.58398 3.32366 7.84514 3.0625 8.16732 3.0625C8.48949 3.0625 8.75065 3.32366 8.75065 3.64583C8.75065 3.968 8.48949 4.22917 8.16732 4.22917C7.84514 4.22917 7.58398 3.968 7.58398 3.64583Z" fill="white"/>
+<path id="Vector_4" d="M7.72852 12.104C7.72852 11.8624 7.9244 11.6665 8.16602 11.6665C8.40763 11.6665 8.60352 11.8624 8.60352 12.104C8.60352 12.3456 8.40763 12.5415 8.16602 12.5415C7.9244 12.5415 7.72852 12.3456 7.72852 12.104Z" fill="white"/>
+<path id="Vector_5" d="M11.375 8.1665C11.375 7.92489 11.5709 7.729 11.8125 7.729C12.0541 7.729 12.25 7.92489 12.25 8.1665C12.25 8.40812 12.0541 8.604 11.8125 8.604C11.5709 8.604 11.375 8.40812 11.375 8.1665Z" fill="white"/>
+<path id="Vector_6" d="M11.375 5.8335C11.375 5.59187 11.5709 5.396 11.8125 5.396C12.0541 5.396 12.25 5.59187 12.25 5.8335C12.25 6.07511 12.0541 6.271 11.8125 6.271C11.5709 6.271 11.375 6.07511 11.375 5.8335Z" fill="white"/>
+<path id="Vector_7" d="M7.72852 1.896C7.72852 1.65437 7.9244 1.4585 8.16602 1.4585C8.40763 1.4585 8.60352 1.65437 8.60352 1.896C8.60352 2.13762 8.40763 2.3335 8.16602 2.3335C7.9244 2.3335 7.72852 2.13762 7.72852 1.896Z" fill="white"/>
+<path id="Vector_8" d="M7.29102 8.1665C7.29102 7.68327 7.68278 7.2915 8.16602 7.2915C8.64925 7.2915 9.04102 7.68327 9.04102 8.1665C9.04102 8.64974 8.64925 9.0415 8.16602 9.0415C7.68278 9.0415 7.29102 8.64974 7.29102 8.1665Z" fill="white"/>
+<path id="Vector_9" d="M7.29102 5.8335C7.29102 5.35025 7.68278 4.9585 8.16602 4.9585C8.64925 4.9585 9.04102 5.35025 9.04102 5.8335C9.04102 6.31673 8.64925 6.7085 8.16602 6.7085C7.68278 6.7085 7.29102 6.31673 7.29102 5.8335Z" fill="white"/>
+<path id="Vector_10" d="M9.625 8.16683C9.625 7.84465 9.88616 7.5835 10.2083 7.5835C10.5305 7.5835 10.7917 7.84465 10.7917 8.16683C10.7917 8.489 10.5305 8.75016 10.2083 8.75016C9.88616 8.75016 9.625 8.489 9.625 8.16683Z" fill="white"/>
+<path id="Vector_11" d="M9.625 5.83333C9.625 5.51116 9.88616 5.25 10.2083 5.25C10.5305 5.25 10.7917 5.51116 10.7917 5.83333C10.7917 6.15551 10.5305 6.41667 10.2083 6.41667C9.88616 6.41667 9.625 6.15551 9.625 5.83333Z" fill="white"/>
+<path id="Vector_12" d="M9.625 3.64583C9.625 3.32366 9.88616 3.0625 10.2083 3.0625C10.5305 3.0625 10.7917 3.32366 10.7917 3.64583C10.7917 3.968 10.5305 4.22917 10.2083 4.22917C9.88616 4.22917 9.625 3.968 9.625 3.64583Z" fill="white"/>
+<path id="Vector_13" d="M6.41667 3.64583C6.41667 3.968 6.15551 4.22917 5.83333 4.22917C5.51117 4.22917 5.25 3.968 5.25 3.64583C5.25 3.32367 5.51117 3.0625 5.83333 3.0625C6.15551 3.0625 6.41667 3.32367 6.41667 3.64583Z" fill="white"/>
+<path id="Vector_14" d="M4.37565 3.64583C4.37565 3.968 4.11448 4.22917 3.79232 4.22917C3.47015 4.22917 3.20898 3.968 3.20898 3.64583C3.20898 3.32367 3.47015 3.0625 3.79232 3.0625C4.11448 3.0625 4.37565 3.32367 4.37565 3.64583Z" fill="white"/>
+<path id="Vector_15" d="M6.41667 10.3543C6.41667 10.6765 6.15551 10.9377 5.83333 10.9377C5.51117 10.9377 5.25 10.6765 5.25 10.3543C5.25 10.0322 5.51117 9.771 5.83333 9.771C6.15551 9.771 6.41667 10.0322 6.41667 10.3543Z" fill="white"/>
+<path id="Vector_16" d="M6.27148 1.896C6.27148 2.13762 6.0756 2.3335 5.83398 2.3335C5.59236 2.3335 5.39648 2.13762 5.39648 1.896C5.39648 1.65437 5.59236 1.4585 5.83398 1.4585C6.0756 1.4585 6.27148 1.65437 6.27148 1.896Z" fill="white"/>
+<path id="Vector_17" d="M2.625 5.8335C2.625 6.07511 2.42912 6.271 2.1875 6.271C1.94588 6.271 1.75 6.07511 1.75 5.8335C1.75 5.59187 1.94588 5.396 2.1875 5.396C2.42912 5.396 2.625 5.59187 2.625 5.8335Z" fill="white"/>
+<path id="Vector_18" d="M2.625 8.1665C2.625 8.40812 2.42912 8.604 2.1875 8.604C1.94588 8.604 1.75 8.40812 1.75 8.1665C1.75 7.92489 1.94588 7.729 2.1875 7.729C2.42912 7.729 2.625 7.92489 2.625 8.1665Z" fill="white"/>
+<path id="Vector_19" d="M6.27148 12.104C6.27148 12.3456 6.0756 12.5415 5.83398 12.5415C5.59236 12.5415 5.39648 12.3456 5.39648 12.104C5.39648 11.8624 5.59236 11.6665 5.83398 11.6665C6.0756 11.6665 6.27148 11.8624 6.27148 12.104Z" fill="white"/>
+<path id="Vector_20" d="M6.70898 5.8335C6.70898 6.31673 6.31722 6.7085 5.83398 6.7085C5.35073 6.7085 4.95898 6.31673 4.95898 5.8335C4.95898 5.35025 5.35073 4.9585 5.83398 4.9585C6.31722 4.9585 6.70898 5.35025 6.70898 5.8335Z" fill="white"/>
+<path id="Vector_21" d="M6.70898 8.1665C6.70898 8.64974 6.31722 9.0415 5.83398 9.0415C5.35073 9.0415 4.95898 8.64974 4.95898 8.1665C4.95898 7.68327 5.35073 7.2915 5.83398 7.2915C6.31722 7.2915 6.70898 7.68327 6.70898 8.1665Z" fill="white"/>
+<path id="Vector_22" d="M4.37565 5.83333C4.37565 6.15551 4.11448 6.41667 3.79232 6.41667C3.47015 6.41667 3.20898 6.15551 3.20898 5.83333C3.20898 5.51117 3.47015 5.25 3.79232 5.25C4.11448 5.25 4.37565 5.51117 4.37565 5.83333Z" fill="white"/>
+<path id="Vector_23" d="M4.37565 8.16683C4.37565 8.489 4.11448 8.75016 3.79232 8.75016C3.47015 8.75016 3.20898 8.489 3.20898 8.16683C3.20898 7.84465 3.47015 7.5835 3.79232 7.5835C4.11448 7.5835 4.37565 7.84465 4.37565 8.16683Z" fill="white"/>
+<path id="Vector_24" d="M4.37565 10.3543C4.37565 10.6765 4.11448 10.9377 3.79232 10.9377C3.47015 10.9377 3.20898 10.6765 3.20898 10.3543C3.20898 10.0322 3.47015 9.771 3.79232 9.771C4.11448 9.771 4.37565 10.0322 4.37565 10.3543Z" fill="white"/>
+</g>
+</svg>

+ 125 - 0
web/app/components/base/icons/src/public/common/D.json

@@ -0,0 +1,125 @@
+{
+	"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": "M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z",
+					"fill": "white"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M2 1H7.94339C11.8094 1 14.9434 4.13401 14.9434 8C14.9434 11.866 11.8094 15 7.9434 15H2V1Z",
+					"fill": "url(#paint0_angular_19344_240446)"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M7.94336 8H8.20751V15H7.94336V8Z",
+					"fill": "url(#paint1_linear_19344_240446)"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "radialGradient",
+						"attributes": {
+							"id": "paint0_angular_19344_240446",
+							"cx": "0",
+							"cy": "0",
+							"r": "1",
+							"gradientUnits": "userSpaceOnUse",
+							"gradientTransform": "translate(7.9434 8) rotate(90) scale(8.75 8.75)"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"stop-color": "#001FC2"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"offset": "0.711334",
+									"stop-color": "#0667F8",
+									"stop-opacity": "0.2"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"offset": "1",
+									"stop-color": "#155EEF",
+									"stop-opacity": "0"
+								},
+								"children": []
+							}
+						]
+					},
+					{
+						"type": "element",
+						"name": "linearGradient",
+						"attributes": {
+							"id": "paint1_linear_19344_240446",
+							"x1": "8.06244",
+							"y1": "8.43754",
+							"x2": "7.93744",
+							"y2": "9.20317",
+							"gradientUnits": "userSpaceOnUse"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"stop-color": "white",
+									"stop-opacity": "0"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"offset": "1",
+									"stop-color": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "D"
+}

+ 16 - 0
web/app/components/base/icons/src/public/common/D.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './D.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'D'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/public/common/index.ts

@@ -1,3 +1,4 @@
+export { default as D } from './D'
 export { default as DiagonalDividingLine } from './DiagonalDividingLine'
 export { default as Dify } from './Dify'
 export { default as Github } from './Github'

+ 39 - 0
web/app/components/base/icons/src/vender/line/files/Folder.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "folder"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M12.8327 11.0833C12.8327 11.3928 12.7098 11.6895 12.491 11.9083C12.2722 12.1271 11.9754 12.25 11.666 12.25H2.33268C2.02326 12.25 1.72652 12.1271 1.50772 11.9083C1.28893 11.6895 1.16602 11.3928 1.16602 11.0833V2.91667C1.16602 2.60725 1.28893 2.3105 1.50772 2.09171C1.72652 1.87292 2.02326 1.75 2.33268 1.75H5.24935L6.41602 3.5H11.666C11.9754 3.5 12.2722 3.62292 12.491 3.84171C12.7098 4.0605 12.8327 4.35725 12.8327 4.66667V11.0833Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Folder"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/files/Folder.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Folder.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Folder'
+
+export default Icon

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

@@ -7,3 +7,4 @@ export { default as FileDownload02 } from './FileDownload02'
 export { default as FilePlus01 } from './FilePlus01'
 export { default as FilePlus02 } from './FilePlus02'
 export { default as FileText } from './FileText'
+export { default as Folder } from './Folder'

+ 36 - 0
web/app/components/base/icons/src/vender/line/others/Apps02.json

@@ -0,0 +1,36 @@
+{
+	"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": "g",
+				"attributes": {
+					"id": "apps-2-line"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector",
+							"d": "M4.66602 7.6665C3.00916 7.6665 1.66602 6.32336 1.66602 4.6665C1.66602 3.00965 3.00916 1.6665 4.66602 1.6665C6.32287 1.6665 7.66602 3.00965 7.66602 4.6665C7.66602 6.32336 6.32287 7.6665 4.66602 7.6665ZM4.66602 14.3332C3.00916 14.3332 1.66602 12.99 1.66602 11.3332C1.66602 9.6763 3.00916 8.33317 4.66602 8.33317C6.32287 8.33317 7.66602 9.6763 7.66602 11.3332C7.66602 12.99 6.32287 14.3332 4.66602 14.3332ZM11.3327 7.6665C9.67582 7.6665 8.33268 6.32336 8.33268 4.6665C8.33268 3.00965 9.67582 1.6665 11.3327 1.6665C12.9895 1.6665 14.3327 3.00965 14.3327 4.6665C14.3327 6.32336 12.9895 7.6665 11.3327 7.6665ZM11.3327 14.3332C9.67582 14.3332 8.33268 12.99 8.33268 11.3332C8.33268 9.6763 9.67582 8.33317 11.3327 8.33317C12.9895 8.33317 14.3327 9.6763 14.3327 11.3332C14.3327 12.99 12.9895 14.3332 11.3327 14.3332ZM4.66602 6.33317C5.58649 6.33317 6.33268 5.58698 6.33268 4.6665C6.33268 3.74603 5.58649 2.99984 4.66602 2.99984C3.74554 2.99984 2.99935 3.74603 2.99935 4.6665C2.99935 5.58698 3.74554 6.33317 4.66602 6.33317ZM4.66602 12.9998C5.58649 12.9998 6.33268 12.2536 6.33268 11.3332C6.33268 10.4127 5.58649 9.6665 4.66602 9.6665C3.74554 9.6665 2.99935 10.4127 2.99935 11.3332C2.99935 12.2536 3.74554 12.9998 4.66602 12.9998ZM11.3327 6.33317C12.2531 6.33317 12.9993 5.58698 12.9993 4.6665C12.9993 3.74603 12.2531 2.99984 11.3327 2.99984C10.4122 2.99984 9.66602 3.74603 9.66602 4.6665C9.66602 5.58698 10.4122 6.33317 11.3327 6.33317ZM11.3327 12.9998C12.2531 12.9998 12.9993 12.2536 12.9993 11.3332C12.9993 10.4127 12.2531 9.6665 11.3327 9.6665C10.4122 9.6665 9.66602 10.4127 9.66602 11.3332C9.66602 12.2536 10.4122 12.9998 11.3327 12.9998Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Apps02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/others/Apps02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Apps02.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Apps02'
+
+export default Icon

+ 66 - 0
web/app/components/base/icons/src/vender/line/others/Colors.json

@@ -0,0 +1,66 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "colors",
+					"clip-path": "url(#clip0_18499_53582)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M7.00032 11.9422C7.61954 12.4964 8.43724 12.8334 9.33366 12.8334C11.2667 12.8334 12.8337 11.2664 12.8337 9.33342C12.8337 7.71938 11.7411 6.36051 10.2552 5.95602M3.74543 5.95601C2.25954 6.3605 1.16699 7.71937 1.16699 9.33341C1.16699 11.2664 2.734 12.8334 4.66699 12.8334C6.59999 12.8334 8.16699 11.2664 8.16699 9.33341C8.16699 8.87813 8.08006 8.44314 7.92189 8.04415M10.5003 4.66675C10.5003 6.59974 8.93332 8.16675 7.00033 8.16675C5.06733 8.16675 3.50033 6.59974 3.50033 4.66675C3.50033 2.73375 5.06733 1.16675 7.00033 1.16675C8.93332 1.16675 10.5003 2.73375 10.5003 4.66675Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_18499_53582"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "14",
+									"height": "14",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Colors"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/others/Colors.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Colors.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Colors'
+
+export default Icon

+ 26 - 0
web/app/components/base/icons/src/vender/line/others/Exchange02.json

@@ -0,0 +1,26 @@
+{
+	"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": "M4.66602 14.3334C3.00916 14.3334 1.66602 12.9903 1.66602 11.3334C1.66602 9.67655 3.00916 8.33342 4.66602 8.33342C6.32287 8.33342 7.66602 9.67655 7.66602 11.3334C7.66602 12.9903 6.32287 14.3334 4.66602 14.3334ZM11.3327 7.66675C9.67582 7.66675 8.33268 6.3236 8.33268 4.66675C8.33268 3.00989 9.67582 1.66675 11.3327 1.66675C12.9895 1.66675 14.3327 3.00989 14.3327 4.66675C14.3327 6.3236 12.9895 7.66675 11.3327 7.66675ZM4.66602 13.0001C5.58649 13.0001 6.33268 12.2539 6.33268 11.3334C6.33268 10.4129 5.58649 9.66675 4.66602 9.66675C3.74554 9.66675 2.99935 10.4129 2.99935 11.3334C2.99935 12.2539 3.74554 13.0001 4.66602 13.0001ZM11.3327 6.33342C12.2531 6.33342 12.9993 5.58722 12.9993 4.66675C12.9993 3.74627 12.2531 3.00008 11.3327 3.00008C10.4122 3.00008 9.66602 3.74627 9.66602 4.66675C9.66602 5.58722 10.4122 6.33342 11.3327 6.33342ZM1.99935 5.33341C1.99935 3.49247 3.49174 2.00008 5.33268 2.00008H7.33268V3.33341H5.33268C4.22812 3.33341 3.33268 4.22885 3.33268 5.33341V7.33342H1.99935V5.33341ZM13.9993 8.66675H12.666V10.6667C12.666 11.7713 11.7706 12.6667 10.666 12.6667H8.66602V14.0001H10.666C12.5069 14.0001 13.9993 12.5077 13.9993 10.6667V8.66675Z",
+					"fill": "currentColor"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "Exchange02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/others/Exchange02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Exchange02.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Exchange02'
+
+export default Icon

+ 26 - 0
web/app/components/base/icons/src/vender/line/others/FileCode.json

@@ -0,0 +1,26 @@
+{
+	"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": "M10 2.66659H3.33333V13.3333H12.6667V5.33325H10V2.66659ZM2 1.99445C2 1.62929 2.29833 1.33325 2.66567 1.33325H10.6667L13.9998 4.66658L14 13.9949C14 14.3659 13.7034 14.6666 13.3377 14.6666H2.66227C2.29651 14.6666 2 14.3631 2 14.0054V1.99445ZM11.7713 7.99992L9.4142 10.3569L8.4714 9.41412L9.8856 7.99992L8.4714 6.58571L9.4142 5.6429L11.7713 7.99992ZM4.22877 7.99992L6.58579 5.6429L7.5286 6.58571L6.11438 7.99992L7.5286 9.41412L6.58579 10.3569L4.22877 7.99992Z",
+					"fill": "currentColor"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "FileCode"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/others/FileCode.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './FileCode.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'FileCode'
+
+export default Icon

+ 119 - 0
web/app/components/base/icons/src/vender/line/others/Tools.json

@@ -0,0 +1,119 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "17",
+			"viewBox": "0 0 16 17",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Tools",
+					"clip-path": "url(#clip0_5381_39479)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "vector",
+							"d": "M13.4375 14.4375V6.8125H2.5625V14.4375C2.5625 14.9898 3.01022 15.4375 3.5625 15.4375H12.4375C12.9898 15.4375 13.4375 14.9898 13.4375 14.4375Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "vector_2",
+							"d": "M13.6254 2.875L11.1738 6.47327",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "vector_3",
+							"d": "M6.3125 9.8125H9.6875",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "vector_4",
+							"d": "M8.63355 1.88044L8.75 1.64754L8.86645 1.88044C8.97531 2.09816 9.15184 2.27469 9.36956 2.38355L9.60246 2.5L9.36956 2.61645C9.15184 2.72531 8.97531 2.90184 8.86645 3.11956L8.75 3.35246L8.63355 3.11956C8.52469 2.90184 8.34816 2.72531 8.13044 2.61645L7.89754 2.5L8.13044 2.38355C8.34816 2.27469 8.52469 2.09816 8.63355 1.88044Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "square",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "vector_5",
+							"d": "M4.625 3.14754L4.61865 3.16025C4.51946 3.35862 4.35862 3.51946 4.16025 3.61865L4.14754 3.625L4.16025 3.63135C4.35862 3.73054 4.51946 3.89138 4.61865 4.08975L4.625 4.10246L4.63135 4.08975C4.73054 3.89138 4.89138 3.73054 5.08975 3.63135L5.10246 3.625L5.08975 3.61865C4.89138 3.51946 4.73054 3.35862 4.63135 3.16025L4.625 3.14754ZM4.625 3.14754L4.63135 3.16025L4.625 3.14754Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "square",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_5381_39479"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "16",
+									"height": "16",
+									"fill": "white",
+									"transform": "translate(0 0.5)"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Tools"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/others/Tools.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Tools.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Tools'
+
+export default Icon

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

@@ -1 +1,6 @@
+export { default as Apps02 } from './Apps02'
+export { default as Colors } from './Colors'
 export { default as DragHandle } from './DragHandle'
+export { default as Exchange02 } from './Exchange02'
+export { default as FileCode } from './FileCode'
+export { default as Tools } from './Tools'

File diff suppressed because it is too large
+ 25 - 0
web/app/components/base/icons/src/vender/workflow/Iteration.json


+ 16 - 0
web/app/components/base/icons/src/vender/workflow/Iteration.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Iteration.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Iteration'
+
+export default Icon

+ 36 - 0
web/app/components/base/icons/src/vender/workflow/IterationStart.json

@@ -0,0 +1,36 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "12",
+			"height": "12",
+			"viewBox": "0 0 12 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "icons/block-start"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector",
+							"d": "M6.8498 1.72732C6.3379 1.3754 5.6621 1.3754 5.1502 1.72732L2.1502 3.78982C1.74317 4.06965 1.5 4.53193 1.5 5.02588V8.99983C1.5 9.82828 2.17158 10.4998 3 10.4998H4.25C4.52614 10.4998 4.75 10.276 4.75 9.99983V8.24983C4.75 7.55948 5.30965 6.99983 6 6.99983C6.69035 6.99983 7.25 7.55948 7.25 8.24983V9.99983C7.25 10.276 7.47385 10.4998 7.75 10.4998H9C9.82845 10.4998 10.5 9.82828 10.5 8.99983V5.02588C10.5 4.53193 10.2568 4.06965 9.8498 3.78982L6.8498 1.72732Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "IterationStart"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/workflow/IterationStart.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './IterationStart.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'IterationStart'
+
+export default Icon

+ 266 - 0
web/app/components/base/icons/src/vender/workflow/ParameterExtractor.json

@@ -0,0 +1,266 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "icons/parma-extractor"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector",
+							"d": "M7.58398 10.3543C7.58398 10.0322 7.84514 9.771 8.16732 9.771C8.48949 9.771 8.75065 10.0322 8.75065 10.3543C8.75065 10.6765 8.48949 10.9377 8.16732 10.9377C7.84514 10.9377 7.58398 10.6765 7.58398 10.3543Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_2",
+							"d": "M9.625 10.3543C9.625 10.0322 9.88616 9.771 10.2083 9.771C10.5305 9.771 10.7917 10.0322 10.7917 10.3543C10.7917 10.6765 10.5305 10.9377 10.2083 10.9377C9.88616 10.9377 9.625 10.6765 9.625 10.3543Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_3",
+							"d": "M7.58398 3.64583C7.58398 3.32366 7.84514 3.0625 8.16732 3.0625C8.48949 3.0625 8.75065 3.32366 8.75065 3.64583C8.75065 3.968 8.48949 4.22917 8.16732 4.22917C7.84514 4.22917 7.58398 3.968 7.58398 3.64583Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_4",
+							"d": "M7.72852 12.104C7.72852 11.8624 7.9244 11.6665 8.16602 11.6665C8.40763 11.6665 8.60352 11.8624 8.60352 12.104C8.60352 12.3456 8.40763 12.5415 8.16602 12.5415C7.9244 12.5415 7.72852 12.3456 7.72852 12.104Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_5",
+							"d": "M11.375 8.1665C11.375 7.92489 11.5709 7.729 11.8125 7.729C12.0541 7.729 12.25 7.92489 12.25 8.1665C12.25 8.40812 12.0541 8.604 11.8125 8.604C11.5709 8.604 11.375 8.40812 11.375 8.1665Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_6",
+							"d": "M11.375 5.8335C11.375 5.59187 11.5709 5.396 11.8125 5.396C12.0541 5.396 12.25 5.59187 12.25 5.8335C12.25 6.07511 12.0541 6.271 11.8125 6.271C11.5709 6.271 11.375 6.07511 11.375 5.8335Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_7",
+							"d": "M7.72852 1.896C7.72852 1.65437 7.9244 1.4585 8.16602 1.4585C8.40763 1.4585 8.60352 1.65437 8.60352 1.896C8.60352 2.13762 8.40763 2.3335 8.16602 2.3335C7.9244 2.3335 7.72852 2.13762 7.72852 1.896Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_8",
+							"d": "M7.29102 8.1665C7.29102 7.68327 7.68278 7.2915 8.16602 7.2915C8.64925 7.2915 9.04102 7.68327 9.04102 8.1665C9.04102 8.64974 8.64925 9.0415 8.16602 9.0415C7.68278 9.0415 7.29102 8.64974 7.29102 8.1665Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_9",
+							"d": "M7.29102 5.8335C7.29102 5.35025 7.68278 4.9585 8.16602 4.9585C8.64925 4.9585 9.04102 5.35025 9.04102 5.8335C9.04102 6.31673 8.64925 6.7085 8.16602 6.7085C7.68278 6.7085 7.29102 6.31673 7.29102 5.8335Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_10",
+							"d": "M9.625 8.16683C9.625 7.84465 9.88616 7.5835 10.2083 7.5835C10.5305 7.5835 10.7917 7.84465 10.7917 8.16683C10.7917 8.489 10.5305 8.75016 10.2083 8.75016C9.88616 8.75016 9.625 8.489 9.625 8.16683Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_11",
+							"d": "M9.625 5.83333C9.625 5.51116 9.88616 5.25 10.2083 5.25C10.5305 5.25 10.7917 5.51116 10.7917 5.83333C10.7917 6.15551 10.5305 6.41667 10.2083 6.41667C9.88616 6.41667 9.625 6.15551 9.625 5.83333Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_12",
+							"d": "M9.625 3.64583C9.625 3.32366 9.88616 3.0625 10.2083 3.0625C10.5305 3.0625 10.7917 3.32366 10.7917 3.64583C10.7917 3.968 10.5305 4.22917 10.2083 4.22917C9.88616 4.22917 9.625 3.968 9.625 3.64583Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_13",
+							"d": "M6.41667 3.64583C6.41667 3.968 6.15551 4.22917 5.83333 4.22917C5.51117 4.22917 5.25 3.968 5.25 3.64583C5.25 3.32367 5.51117 3.0625 5.83333 3.0625C6.15551 3.0625 6.41667 3.32367 6.41667 3.64583Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_14",
+							"d": "M4.37565 3.64583C4.37565 3.968 4.11448 4.22917 3.79232 4.22917C3.47015 4.22917 3.20898 3.968 3.20898 3.64583C3.20898 3.32367 3.47015 3.0625 3.79232 3.0625C4.11448 3.0625 4.37565 3.32367 4.37565 3.64583Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_15",
+							"d": "M6.41667 10.3543C6.41667 10.6765 6.15551 10.9377 5.83333 10.9377C5.51117 10.9377 5.25 10.6765 5.25 10.3543C5.25 10.0322 5.51117 9.771 5.83333 9.771C6.15551 9.771 6.41667 10.0322 6.41667 10.3543Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_16",
+							"d": "M6.27148 1.896C6.27148 2.13762 6.0756 2.3335 5.83398 2.3335C5.59236 2.3335 5.39648 2.13762 5.39648 1.896C5.39648 1.65437 5.59236 1.4585 5.83398 1.4585C6.0756 1.4585 6.27148 1.65437 6.27148 1.896Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_17",
+							"d": "M2.625 5.8335C2.625 6.07511 2.42912 6.271 2.1875 6.271C1.94588 6.271 1.75 6.07511 1.75 5.8335C1.75 5.59187 1.94588 5.396 2.1875 5.396C2.42912 5.396 2.625 5.59187 2.625 5.8335Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_18",
+							"d": "M2.625 8.1665C2.625 8.40812 2.42912 8.604 2.1875 8.604C1.94588 8.604 1.75 8.40812 1.75 8.1665C1.75 7.92489 1.94588 7.729 2.1875 7.729C2.42912 7.729 2.625 7.92489 2.625 8.1665Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_19",
+							"d": "M6.27148 12.104C6.27148 12.3456 6.0756 12.5415 5.83398 12.5415C5.59236 12.5415 5.39648 12.3456 5.39648 12.104C5.39648 11.8624 5.59236 11.6665 5.83398 11.6665C6.0756 11.6665 6.27148 11.8624 6.27148 12.104Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_20",
+							"d": "M6.70898 5.8335C6.70898 6.31673 6.31722 6.7085 5.83398 6.7085C5.35073 6.7085 4.95898 6.31673 4.95898 5.8335C4.95898 5.35025 5.35073 4.9585 5.83398 4.9585C6.31722 4.9585 6.70898 5.35025 6.70898 5.8335Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_21",
+							"d": "M6.70898 8.1665C6.70898 8.64974 6.31722 9.0415 5.83398 9.0415C5.35073 9.0415 4.95898 8.64974 4.95898 8.1665C4.95898 7.68327 5.35073 7.2915 5.83398 7.2915C6.31722 7.2915 6.70898 7.68327 6.70898 8.1665Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_22",
+							"d": "M4.37565 5.83333C4.37565 6.15551 4.11448 6.41667 3.79232 6.41667C3.47015 6.41667 3.20898 6.15551 3.20898 5.83333C3.20898 5.51117 3.47015 5.25 3.79232 5.25C4.11448 5.25 4.37565 5.51117 4.37565 5.83333Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_23",
+							"d": "M4.37565 8.16683C4.37565 8.489 4.11448 8.75016 3.79232 8.75016C3.47015 8.75016 3.20898 8.489 3.20898 8.16683C3.20898 7.84465 3.47015 7.5835 3.79232 7.5835C4.11448 7.5835 4.37565 7.84465 4.37565 8.16683Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_24",
+							"d": "M4.37565 10.3543C4.37565 10.6765 4.11448 10.9377 3.79232 10.9377C3.47015 10.9377 3.20898 10.6765 3.20898 10.3543C3.20898 10.0322 3.47015 9.771 3.79232 9.771C4.11448 9.771 4.37565 10.0322 4.37565 10.3543Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "ParameterExtractor"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/workflow/ParameterExtractor.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ParameterExtractor.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ParameterExtractor'
+
+export default Icon

+ 3 - 0
web/app/components/base/icons/src/vender/workflow/index.ts

@@ -4,9 +4,12 @@ export { default as End } from './End'
 export { default as Home } from './Home'
 export { default as Http } from './Http'
 export { default as IfElse } from './IfElse'
+export { default as IterationStart } from './IterationStart'
+export { default as Iteration } from './Iteration'
 export { default as Jinja } from './Jinja'
 export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
 export { default as Llm } from './Llm'
+export { default as ParameterExtractor } from './ParameterExtractor'
 export { default as QuestionClassifier } from './QuestionClassifier'
 export { default as TemplatingTransform } from './TemplatingTransform'
 export { default as VariableX } from './VariableX'

+ 39 - 7
web/app/components/base/message-log-modal/index.tsx

@@ -1,20 +1,24 @@
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
-import { useEffect, useRef, useState } from 'react'
-import { useClickAway } from 'ahooks'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useBoolean, useClickAway } from 'ahooks'
+import IterationResultPanel from '../../workflow/run/iteration-result-panel'
 import { XClose } from '@/app/components/base/icons/src/vender/line/general'
 import type { IChatItem } from '@/app/components/app/chat/type'
 import Run from '@/app/components/workflow/run'
+import type { NodeTracing } from '@/types/workflow'
 
 type MessageLogModalProps = {
   currentLogItem?: IChatItem
+  defaultTab?: string
   width: number
   fixedWidth?: boolean
   onCancel: () => void
 }
 const MessageLogModal: FC<MessageLogModalProps> = ({
   currentLogItem,
+  defaultTab = 'DETAIL',
   width,
   fixedWidth,
   onCancel,
@@ -32,6 +36,17 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
     setMounted(true)
   }, [])
 
+  const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
+  const [isShowIterationDetail, {
+    setTrue: doShowIterationDetail,
+    setFalse: doHideIterationDetail,
+  }] = useBoolean(false)
+
+  const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => {
+    setIterationRunResult(detail)
+    doShowIterationDetail()
+  }, [doShowIterationDetail])
+
   if (!currentLogItem || !currentLogItem.workflow_run_id)
     return null
 
@@ -53,11 +68,28 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
       }}
       ref={ref}
     >
-      <h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.title')}</h1>
-      <span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onCancel}>
-        <XClose className='w-4 h-4 text-gray-500' />
-      </span>
-      <Run hideResult activeTab='DETAIL' runID={currentLogItem.workflow_run_id}/>
+      {isShowIterationDetail
+        ? (
+          <IterationResultPanel
+            list={iterationRunResult}
+            onHide={doHideIterationDetail}
+            onBack={doHideIterationDetail}
+          />
+        )
+        : (
+          <>
+            <h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.title')}</h1>
+            <span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onCancel}>
+              <XClose className='w-4 h-4 text-gray-500' />
+            </span>
+            <Run
+              hideResult activeTab={defaultTab as any}
+              runID={currentLogItem.workflow_run_id}
+              onShowIterationDetail={handleShowIterationDetail}
+            />
+          </>
+        )}
+
     </div>
   )
 }

+ 0 - 78
web/app/components/base/panel/index.tsx

@@ -1,78 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useEffect } from 'react'
-import cn from 'classnames'
-import { useBoolean } from 'ahooks'
-import { ChevronRightIcon } from '@heroicons/react/24/outline'
-
-export type IPanelProps = {
-  className?: string
-  headerIcon: React.ReactNode
-  title: React.ReactNode
-  headerRight?: React.ReactNode
-  bodyClassName?: string
-  children: React.ReactNode
-  keepUnFold?: boolean
-  foldDisabled?: boolean
-  onFoldChange?: (fold: boolean) => void
-  controlUnFold?: number
-  controlFold?: number
-}
-
-const Panel: FC<IPanelProps> = ({
-  className,
-  headerIcon,
-  title,
-  headerRight,
-  bodyClassName,
-  children,
-  keepUnFold,
-  foldDisabled = false,
-  onFoldChange,
-  controlUnFold,
-  controlFold,
-}) => {
-  const [fold, { setTrue: setFold, setFalse: setUnFold, toggle: toggleFold }] = useBoolean(!keepUnFold)
-  useEffect(() => {
-    onFoldChange?.(fold)
-  }, [fold])
-
-  useEffect(() => {
-    if (controlUnFold)
-      setUnFold()
-  }, [controlUnFold])
-
-  useEffect(() => {
-    if (controlFold)
-      setFold()
-  }, [controlFold])
-
-  // overflow-hidden
-  return (
-    <div className={cn(className, 'w-full rounded-xl border border-gray-100 overflow-hidden select-none')}>
-      {/* Header */}
-      <div
-        onClick={() => (!foldDisabled && !keepUnFold) && toggleFold()}
-        className={cn(!fold && 'border-b border-gray-100', 'flex justify-between items-center h-12 bg-gray-50 pl-4 pr-2')}>
-        <div className='flex items-center gap-2'>
-          {headerIcon}
-          <div className='text-gray-900 text-sm'>{title}</div>
-        </div>
-        {(fold && headerRight) ? headerRight : ''}
-        {!headerRight && !keepUnFold && (
-          <ChevronRightIcon className={cn(!fold && 'rotate-90', 'mr-2 cursor-pointer')} width="16" height="16">
-          </ChevronRightIcon>
-        )}
-      </div>
-
-      {/* Main Content */}
-
-      {!fold && !foldDisabled && (
-        <div className={cn(bodyClassName)}>
-          {children}
-        </div>
-      )}
-    </div>
-  )
-}
-export default React.memo(Panel)

+ 3 - 1
web/app/components/base/portal-to-follow-elem/index.tsx

@@ -150,8 +150,10 @@ React.HTMLProps<HTMLDivElement>
   if (!context.open)
     return null
 
+  const body = document.body
+
   return (
-    <FloatingPortal>
+    <FloatingPortal root={body}>
       <div
         ref={ref}
         style={{

+ 8 - 2
web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx

@@ -39,7 +39,13 @@ const WorkflowVariableBlockComponent = ({
   const [editor] = useLexicalComposerContext()
   const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
   const variablesLength = variables.length
-  const lastVariable = isSystemVar(variables) ? variables.join('.') : variables[variablesLength - 1]
+  const varName = (
+    () => {
+      const isSystem = isSystemVar(variables)
+      const varName = variablesLength >= 3 ? (variables).slice(-2).join('.') : variables[variablesLength - 1]
+      return `${isSystem ? 'sys.' : ''}${varName}`
+    }
+  )()
   const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
   const node = localWorkflowNodesMap![variables[0]]
 
@@ -86,7 +92,7 @@ const WorkflowVariableBlockComponent = ({
       </div>
       <div className='flex items-center text-primary-600'>
         <Variable02 className='w-3.5 h-3.5' />
-        <div className='shrink-0 ml-0.5 text-xs font-medium truncate' title={lastVariable}>{lastVariable}</div>
+        <div className='shrink-0 ml-0.5 text-xs font-medium truncate' title={varName}>{varName}</div>
         {
           !node && (
             <AlertCircle className='ml-0.5 w-3 h-3 text-[#D92D20]' />

+ 4 - 1
web/app/components/base/select/index.tsx

@@ -37,6 +37,7 @@ export type ISelectProps = {
   bgClassName?: string
   placeholder?: string
   overlayClassName?: string
+  optionClassName?: string
 }
 const Select: FC<ISelectProps> = ({
   className,
@@ -47,6 +48,7 @@ const Select: FC<ISelectProps> = ({
   allowSearch = true,
   bgClassName = 'bg-gray-100',
   overlayClassName,
+  optionClassName,
 }) => {
   const [query, setQuery] = useState('')
   const [open, setOpen] = useState(false)
@@ -97,7 +99,7 @@ const Select: FC<ISelectProps> = ({
                 if (!disabled)
                   setOpen(!open)
               }
-            } className={`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`}>
+            } className={classNames(optionClassName, `flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`)}>
               <div className='w-0 grow text-left truncate' title={selectedItem?.name}>{selectedItem?.name}</div>
             </Combobox.Button>}
           <Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={
@@ -118,6 +120,7 @@ const Select: FC<ISelectProps> = ({
                 value={item}
                 className={({ active }: { active: boolean }) =>
                   classNames(
+                    optionClassName,
                     'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700',
                     active ? 'bg-gray-100' : '',
                   )

+ 1 - 1
web/app/components/header/HeaderWrapper.tsx

@@ -11,7 +11,7 @@ const HeaderWrapper = ({
   children,
 }: HeaderWrapperProps) => {
   const pathname = usePathname()
-  const isBordered = ['/apps', '/datasets', '/datasets/create'].includes(pathname)
+  const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools'].includes(pathname)
 
   return (
     <div className={classNames(

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

@@ -12,6 +12,7 @@ export enum FormTypeEnum {
   secretInput = 'secret-input',
   select = 'select',
   radio = 'radio',
+  files = 'files',
 }
 
 export type FormOption = {

+ 1 - 1
web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx

@@ -239,7 +239,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
                       key={`${modelId}-${parameter.name}`}
                       className='mb-4'
                       parameterRule={parameter}
-                      value={completionParams[parameter.name]}
+                      value={completionParams?.[parameter.name]}
                       onChange={v => handleParamChange(parameter.name, v)}
                       onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
                       isInWorkflow={isInWorkflow}

+ 32 - 0
web/app/components/share/text-generation/result/index.tsx

@@ -192,6 +192,8 @@ const Result: FC<IResultProps> = ({
     })()
 
     if (isWorkflow) {
+      let isInIteration = false
+
       sendWorkflowMessage(
         data,
         {
@@ -205,7 +207,34 @@ const Result: FC<IResultProps> = ({
             })
             setRespondingFalse()
           },
+          onIterationStart: ({ data }) => {
+            setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
+              draft.expand = true
+              draft.tracing!.push({
+                ...data,
+                status: NodeRunningStatus.Running,
+                expand: true,
+              } as any)
+            }))
+            isInIteration = true
+          },
+          onIterationNext: () => {
+          },
+          onIterationFinish: ({ data }) => {
+            setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
+              draft.expand = true
+              // const iteration = draft.tracing![draft.tracing!.length - 1]
+              draft.tracing![draft.tracing!.length - 1] = {
+                ...data,
+                expand: !!data.error,
+              } as any
+            }))
+            isInIteration = false
+          },
           onNodeStarted: ({ data }) => {
+            if (isInIteration)
+              return
+
             setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
               draft.expand = true
               draft.tracing!.push({
@@ -216,6 +245,9 @@ const Result: FC<IResultProps> = ({
             }))
           },
           onNodeFinished: ({ data }) => {
+            if (isInIteration)
+              return
+
             setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
               const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
               if (currentIndex > -1 && draft.tracing) {

BIN
web/app/components/tools/add-tool-modal/D.png


+ 70 - 0
web/app/components/tools/add-tool-modal/category.tsx

@@ -0,0 +1,70 @@
+'use client'
+import { useRef } from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import { useMount } from 'ahooks'
+import { Apps02 } from '@/app/components/base/icons/src/vender/line/others'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
+import { fetchLabelList } from '@/service/tools'
+
+type Props = {
+  value: string
+  onSelect: (type: string) => void
+}
+
+const Icon = ({ svgString, active }: { svgString: string; active: boolean }) => {
+  const svgRef = useRef<SVGSVGElement | null>(null)
+  const SVGParsor = (svg: string) => {
+    if (!svg)
+      return null
+    const parser = new DOMParser()
+    const doc = parser.parseFromString(svg, 'image/svg+xml')
+    console.log(doc.documentElement)
+    return doc.documentElement
+  }
+  useMount(() => {
+    const svgElement = SVGParsor(svgString)
+    if (svgRef.current && svgElement)
+      svgRef.current.appendChild(svgElement)
+  })
+  return <svg className={cn('w-4 h-4 text-gray-700', active && '!text-primary-600')} ref={svgRef} />
+}
+
+const Category = ({
+  value,
+  onSelect,
+}: Props) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const labelList = useLabelStore(s => s.labelList)
+  const setLabelList = useLabelStore(s => s.setLabelList)
+
+  useMount(() => {
+    fetchLabelList().then((res) => {
+      setLabelList(res)
+    })
+  })
+
+  return (
+    <div className='mb-3'>
+      <div className='px-3 py-0.5 text-gray-500 text-xs leading-[18px] font-medium'>{t('tools.addToolModal.category').toLocaleUpperCase()}</div>
+      <div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === '' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('')}>
+        <Apps02 className='shrink-0 w-4 h-4 mr-2' />
+        {t('tools.type.all')}
+      </div>
+      {labelList.map(label => (
+        <div key={label.name} title={label.label[language]} className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white truncate overflow-hidden', value === label.name && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect(label.name)}>
+          <div className='shrink-0 w-4 h-4 mr-2'>
+            <Icon active={value === label.name} svgString={label.icon} />
+          </div>
+          {label.label[language]}
+        </div>
+      ))}
+    </div>
+  )
+}
+export default Category

BIN
web/app/components/tools/add-tool-modal/empty.png


+ 15 - 0
web/app/components/tools/add-tool-modal/empty.tsx

@@ -0,0 +1,15 @@
+import { useTranslation } from 'react-i18next'
+
+const Empty = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='flex flex-col items-center'>
+      <div className="shrink-0 w-[163px] h-[149px] bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/empty.png')]"></div>
+      <div className='mb-1 text-[13px] font-medium text-gray-700 leading-[18px]'>{t('tools.addToolModal.emptyTitle')}</div>
+      <div className='text-[13px] text-gray-500 leading-[18px]'>{t('tools.addToolModal.emptyTip')}</div>
+    </div>
+  )
+}
+
+export default Empty

+ 235 - 0
web/app/components/tools/add-tool-modal/index.tsx

@@ -0,0 +1,235 @@
+'use client'
+import type { FC } from 'react'
+import React, { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import produce from 'immer'
+import cn from 'classnames'
+import { useMount } from 'ahooks'
+import type { Collection, CustomCollectionBackend, Tool } from '../types'
+import Type from './type'
+import Category from './category'
+import Tools from './tools'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import Drawer from '@/app/components/base/drawer'
+import Button from '@/app/components/base/button'
+import Loading from '@/app/components/base/loading'
+import SearchInput from '@/app/components/base/search-input'
+import { Plus, XClose } from '@/app/components/base/icons/src/vender/line/general'
+import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
+import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
+import {
+  createCustomCollection,
+  fetchAllBuiltInTools,
+  fetchAllCustomTools,
+  fetchAllWorkflowTools,
+  removeBuiltInToolCredential,
+  updateBuiltInToolCredential,
+} from '@/service/tools'
+import type { ToolWithProvider } from '@/app/components/workflow/types'
+import Toast from '@/app/components/base/toast'
+import ConfigContext from '@/context/debug-configuration'
+import type { ModelConfig } from '@/models/debug'
+
+type Props = {
+  onHide: () => void
+}
+// Add and Edit
+const AddToolModal: FC<Props> = ({
+  onHide,
+}) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const [currentType, setCurrentType] = useState('builtin')
+  const [currentCategory, setCurrentCategory] = useState('')
+  const [keywords, setKeywords] = useState<string>('')
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+  }
+  const [toolList, setToolList] = useState<ToolWithProvider[]>([])
+  const [listLoading, setListLoading] = useState(true)
+  const getAllTools = async () => {
+    setListLoading(true)
+    const buildInTools = await fetchAllBuiltInTools()
+    const customTools = await fetchAllCustomTools()
+    const workflowTools = await fetchAllWorkflowTools()
+    const mergedToolList = [
+      ...buildInTools,
+      ...customTools,
+      ...workflowTools.filter((toolWithProvider) => {
+        return !toolWithProvider.tools.some((tool) => {
+          return !!tool.parameters.find(item => item.name === '__image')
+        })
+      }),
+    ]
+    setToolList(mergedToolList)
+    setListLoading(false)
+  }
+  const filteredList = useMemo(() => {
+    return toolList.filter((toolWithProvider) => {
+      if (currentType === 'all')
+        return true
+      else
+        return toolWithProvider.type === currentType
+    }).filter((toolWithProvider) => {
+      if (!currentCategory)
+        return true
+      else
+        return toolWithProvider.labels.includes(currentCategory)
+    }).filter((toolWithProvider) => {
+      return toolWithProvider.tools.some((tool) => {
+        return tool.label[language].toLowerCase().includes(keywords.toLowerCase())
+      })
+    })
+  }, [currentType, currentCategory, toolList, keywords, language])
+
+  const {
+    modelConfig,
+    setModelConfig,
+  } = useContext(ConfigContext)
+
+  const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
+  const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
+    await createCustomCollection(data)
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditCustomCollectionModal(false)
+    getAllTools()
+  }
+  const [showSettingAuth, setShowSettingAuth] = useState(false)
+  const [collection, setCollection] = useState<Collection>()
+  const toolSelectHandle = (collection: Collection, tool: Tool) => {
+    const parameters: Record<string, string> = {}
+    if (tool.parameters) {
+      tool.parameters.forEach((item) => {
+        parameters[item.name] = ''
+      })
+    }
+
+    const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => {
+      draft.agentConfig.tools.push({
+        provider_id: collection.id || collection.name,
+        provider_type: collection.type,
+        provider_name: collection.name,
+        tool_name: tool.name,
+        tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')],
+        tool_parameters: parameters,
+        enabled: true,
+      })
+    })
+    setModelConfig(nexModelConfig)
+  }
+  const authSelectHandle = (provider: Collection) => {
+    setCollection(provider)
+    setShowSettingAuth(true)
+  }
+  const updateBuiltinAuth = async (value: Record<string, any>) => {
+    if (!collection)
+      return
+    await updateBuiltInToolCredential(collection.name, value)
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    await getAllTools()
+    setShowSettingAuth(false)
+  }
+  const removeBuiltinAuth = async () => {
+    if (!collection)
+      return
+    await removeBuiltInToolCredential(collection.name)
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    await getAllTools()
+    setShowSettingAuth(false)
+  }
+
+  useMount(() => {
+    getAllTools()
+  })
+
+  return (
+    <>
+      <Drawer
+        isOpen
+        mask
+        clickOutsideNotOpen
+        onClose={onHide}
+        footer={null}
+        panelClassname={cn('mt-16 mx-2 sm:mr-2 mb-3 !p-0 rounded-xl', 'mt-2 !w-[640px]', '!max-w-[640px]')}
+      >
+        <div
+          className='w-full flex bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl'
+          style={{
+            height: 'calc(100vh - 16px)',
+          }}
+        >
+          <div className='relative shrink-0 w-[200px] pb-3 bg-gray-100 rounded-l-xl border-r-[0.5px] border-black/2 overflow-y-auto'>
+            <div className='sticky top-0 left-0 right-0'>
+              <div className='sticky top-0 left-0 right-0 px-5 py-3 text-md font-semibold text-gray-900'>{t('tools.addTool')}</div>
+              <div className='px-3 pt-2 pb-4'>
+                <Button type='primary' className='w-[176px] text-[13px] leading-[18px] font-medium' onClick={() => setIsShowEditCustomCollectionModal(true)}>
+                  <Plus className='w-4 h-4 mr-1'/>
+                  {t('tools.createCustomTool')}
+                </Button>
+              </div>
+            </div>
+            <div className='px-2 py-1'>
+              <Type value={currentType} onSelect={setCurrentType}/>
+              <Category value={currentCategory} onSelect={setCurrentCategory}/>
+            </div>
+          </div>
+          <div className='relative grow bg-white rounded-r-xl overflow-y-auto'>
+            <div className='z-10 sticky top-0 left-0 right-0 p-2 flex items-center gap-1 bg-white'>
+              <div className='grow'>
+                <SearchInput className='w-full' value={keywords} onChange={handleKeywordsChange} />
+              </div>
+              <div className='ml-2 mr-1 w-[1px] h-4 bg-gray-200'></div>
+              <div className='p-2 cursor-pointer' onClick={onHide}>
+                <XClose className='w-4 h-4 text-gray-500' />
+              </div>
+            </div>
+            {listLoading && (
+              <div className='flex h-[200px] items-center justify-center bg-white'>
+                <Loading />
+              </div>
+            )}
+            {!listLoading && (
+              <Tools
+                showWorkflowEmpty={currentType === 'workflow'}
+                tools={filteredList}
+                addedTools={(modelConfig?.agentConfig?.tools as any) || []}
+                onSelect={toolSelectHandle}
+                onAuthSetup={authSelectHandle}
+              />
+            )}
+          </div>
+        </div>
+      </Drawer>
+      {isShowEditCollectionToolModal && (
+        <EditCustomToolModal
+          positionLeft
+          payload={null}
+          onHide={() => setIsShowEditCustomCollectionModal(false)}
+          onAdd={doCreateCustomToolCollection}
+        />
+      )}
+      {showSettingAuth && collection && (
+        <ConfigCredential
+          collection={collection}
+          onCancel={() => setShowSettingAuth(false)}
+          onSaved={updateBuiltinAuth}
+          onRemove={removeBuiltinAuth}
+        />
+      )}
+    </>
+
+  )
+}
+export default React.memo(AddToolModal)

+ 146 - 0
web/app/components/tools/add-tool-modal/tools.tsx

@@ -0,0 +1,146 @@
+import {
+  memo,
+  useCallback,
+} from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Check, Plus } from '@/app/components/base/icons/src/vender/line/general'
+import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
+import type { ToolWithProvider } from '@/app/components/workflow/types'
+import { BlockEnum } from '@/app/components/workflow/types'
+import BlockIcon from '@/app/components/workflow/block-icon'
+import Tooltip from '@/app/components/base/tooltip'
+import Button from '@/app/components/base/button'
+import { useGetLanguage } from '@/context/i18n'
+import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
+import Empty from '@/app/components/tools/add-tool-modal/empty'
+import type { Tool } from '@/app/components/tools/types'
+import { CollectionType } from '@/app/components/tools/types'
+import type { AgentTool } from '@/types/app'
+import { MAX_TOOLS_NUM } from '@/config'
+
+type ToolsProps = {
+  showWorkflowEmpty: boolean
+  tools: ToolWithProvider[]
+  addedTools: AgentTool[]
+  onSelect: (provider: ToolWithProvider, tool: Tool) => void
+  onAuthSetup: (provider: ToolWithProvider) => void
+}
+const Blocks = ({
+  showWorkflowEmpty,
+  tools,
+  addedTools,
+  onSelect,
+  onAuthSetup,
+}: ToolsProps) => {
+  const { t } = useTranslation()
+  const language = useGetLanguage()
+  const labelList = useLabelStore(s => s.labelList)
+  const addable = addedTools.length < MAX_TOOLS_NUM
+
+  const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
+    const list = toolWithProvider.tools
+    const needAuth = toolWithProvider.allow_delete && !toolWithProvider.is_team_authorization && toolWithProvider.type === CollectionType.builtIn
+
+    return (
+      <div
+        key={toolWithProvider.id}
+        className='group mb-1 last-of-type:mb-0'
+      >
+        <div className='flex items-center justify-between w-full pl-3 pr-1 h-[22px] text-xs font-medium text-gray-500'>
+          {toolWithProvider.label[language]}
+          <a className='hidden cursor-pointer items-center group-hover:flex' href={`/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 w-3 h-3' /></a>
+        </div>
+        {list.map((tool) => {
+          const labelContent = (() => {
+            if (!tool.labels)
+              return ''
+            return tool.labels.map((name) => {
+              const label = labelList.find(item => item.name === name)
+              return label?.label[language]
+            }).filter(Boolean).join(', ')
+          })()
+          const added = !!addedTools?.find(v => v.provider_id === toolWithProvider.id && v.provider_type === toolWithProvider.type && v.tool_name === tool.name)
+          return (
+            <Tooltip
+              key={tool.name}
+              selector={`workflow-block-tool-${tool.name}`}
+              position='bottom'
+              className='!p-0 !px-3 !py-2.5 !w-[210px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg translate-x-[108px]'
+              htmlContent={(
+                <div>
+                  <BlockIcon
+                    size='md'
+                    className='mb-2'
+                    type={BlockEnum.Tool}
+                    toolIcon={toolWithProvider.icon}
+                  />
+                  <div className='mb-1 text-sm leading-5 text-gray-900'>{tool.label[language]}</div>
+                  <div className='text-xs text-gray-700 leading-[18px]'>{tool.description[language]}</div>
+                  {tool.labels?.length > 0 && (
+                    <div className='flex items-center shrink-0 mt-1'>
+                      <div className='relative w-full flex items-center gap-1 py-1 rounded-md text-gray-500' title={labelContent}>
+                        <Tag01 className='shrink-0 w-3 h-3 text-gray-500' />
+                        <div className='grow text-xs text-start leading-[18px] font-normal truncate'>{labelContent}</div>
+                      </div>
+                    </div>
+                  )}
+                </div>
+              )}
+              noArrow
+            >
+              <div className='group/item flex items-center w-full pl-3 pr-1 h-8 rounded-lg hover:bg-gray-50 cursor-pointer'>
+                <BlockIcon
+                  className={cn('mr-2 shrink-0', needAuth && 'opacity-30')}
+                  type={BlockEnum.Tool}
+                  toolIcon={toolWithProvider.icon}
+                />
+                <div className={cn('grow text-sm text-gray-900 truncate', needAuth && 'opacity-30')}>{tool.label[language]}</div>
+                {!needAuth && added && (
+                  <div className='flex items-center gap-1 rounded-[6px] border border-gray-100 px-2 py-[3px] bg-white text-gray-300 text-xs font-medium leading-[18px]'>
+                    <Check className='w-3 h-3'/>
+                    {t('tools.addToolModal.added').toLocaleUpperCase()}
+                  </div>
+                )}
+                {!needAuth && !added && addable && (
+                  <Button
+                    type='default'
+                    className={cn('hidden shrink-0 items-center !h-6 px-2 py-1 bg-white text-xs font-medium leading-[18px] text-primary-600 group-hover/item:flex')}
+                    onClick={() => onSelect(toolWithProvider, tool)}
+                  >
+                    <Plus className='w-3 h-3'/>
+                    {t('tools.addToolModal.add').toLocaleUpperCase()}
+                  </Button>
+                )}
+                {needAuth && (
+                  <Button
+                    type='default'
+                    className={cn('hidden shrink-0 items-center !h-6 px-2 py-1 bg-white text-xs font-medium leading-[18px] text-primary-600 group-hover/item:flex')}
+                    onClick={() => onAuthSetup(toolWithProvider)}
+                  >{t('tools.auth.setup')}</Button>
+                )}
+              </div>
+            </Tooltip>
+          )
+        })}
+      </div>
+    )
+  }, [addable, language, t, labelList, addedTools, onAuthSetup, onSelect])
+
+  return (
+    <div className='p-1 pb-6 max-w-[440px]'>
+      {!tools.length && !showWorkflowEmpty && (
+        <div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
+      )}
+      {!tools.length && showWorkflowEmpty && (
+        <div className='pt-[280px]'>
+          <Empty/>
+        </div>
+      )}
+      {!!tools.length && tools.map(renderGroup)}
+    </div>
+  )
+}
+
+export default memo(Blocks)

+ 34 - 0
web/app/components/tools/add-tool-modal/type.tsx

@@ -0,0 +1,34 @@
+'use client'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { Exchange02, FileCode } from '@/app/components/base/icons/src/vender/line/others'
+
+type Props = {
+  value: string
+  onSelect: (type: string) => void
+}
+
+const Types = ({
+  value,
+  onSelect,
+}: Props) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='mb-3'>
+      <div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-sm leading-5 rounded-lg hover:bg-white', value === 'builtin' && '!bg-white font-medium')} onClick={() => onSelect('builtin')}>
+        <div className="shrink-0 w-4 h-4 mr-2 bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/D.png')]" />
+        <span className={cn('text-gray-700', value === 'builtin' && '!text-primary-600')}>{t('tools.type.builtIn')}</span>
+      </div>
+      <div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === 'api' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('api')}>
+        <FileCode className='shrink-0 w-4 h-4 mr-2' />
+        {t('tools.type.custom')}
+      </div>
+      <div className={cn('mb-0.5 p-1 pl-3 flex items-center cursor-pointer text-gray-700 text-sm leading-5 rounded-lg hover:bg-white', value === 'workflow' && '!bg-white !text-primary-600 font-medium')} onClick={() => onSelect('workflow')}>
+        <Exchange02 className='shrink-0 w-4 h-4 mr-2' />
+        {t('tools.type.workflow')}
+      </div>
+    </div>
+  )
+}
+export default Types

+ 0 - 31
web/app/components/tools/contribute.tsx

@@ -1,31 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { Heart02 } from '../base/icons/src/vender/solid/education'
-import { BookOpen01 } from '../base/icons/src/vender/line/education'
-
-const Contribute: FC = () => {
-  const { t } = useTranslation()
-
-  return (
-    <div className='shrink-0 p-2'>
-      <div className='inline-block p-2 bg-white shadow-lg rounded-lg'>
-        <Heart02 className='w-3 h-3 text-[#EE46BC]' />
-      </div>
-      <div className='mt-2'>
-        <div className='text-gradient'>
-          {t('tools.contribute.line1')}
-        </div>
-        <div className='text-gradient'>
-          {t('tools.contribute.line2')}
-        </div>
-      </div>
-      <a href='https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md' target='_blank' rel='noopener noreferrer' className='mt-1 flex items-center space-x-1 text-[#155EEF]'>
-        <BookOpen01 className='w-3 h-3' />
-        <div className='leading-[18px] text-xs font-normal'>{t('tools.contribute.viewGuide')}</div>
-      </a>
-    </div>
-  )
-}
-export default React.memo(Contribute)

+ 3 - 0
web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx

@@ -12,6 +12,7 @@ import Radio from '@/app/components/base/radio/ui'
 import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
 
 type Props = {
+  positionCenter?: boolean
   credential: Credential
   onChange: (credential: Credential) => void
   onHide: () => void
@@ -38,6 +39,7 @@ const SelectItem: FC<ItemProps> = ({ text, value, isChecked, onClick }) => {
 }
 
 const ConfigCredential: FC<Props> = ({
+  positionCenter,
   credential,
   onChange,
   onHide,
@@ -48,6 +50,7 @@ const ConfigCredential: FC<Props> = ({
   return (
     <Drawer
       isShow
+      positionCenter={positionCenter}
       onHide={onHide}
       title={t('tools.createTool.authMethod.title')!}
       panelClassName='mt-2 !w-[520px]'

+ 23 - 3
web/app/components/tools/edit-custom-collection-modal/index.tsx

@@ -16,9 +16,11 @@ import Button from '@/app/components/base/button'
 import EmojiPicker from '@/app/components/base/emoji-picker'
 import AppIcon from '@/app/components/base/app-icon'
 import { parseParamsSchema } from '@/service/tools'
+import LabelSelector from '@/app/components/tools/labels/selector'
 
 const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
 type Props = {
+  positionLeft?: boolean
   payload: any
   onHide: () => void
   onAdd?: (payload: CustomCollectionBackend) => void
@@ -27,6 +29,7 @@ type Props = {
 }
 // Add and Edit
 const EditCustomCollectionModal: FC<Props> = ({
+  positionLeft,
   payload,
   onHide,
   onAdd,
@@ -114,6 +117,11 @@ const EditCustomCollectionModal: FC<Props> = ({
   const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null)
   const [isShowTestApi, setIsShowTestApi] = useState(false)
 
+  const [labels, setLabels] = useState<string[]>(payload?.labels || [])
+  const handleLabelSelect = (value: string[]) => {
+    setLabels(value)
+  }
+
   const handleSave = () => {
     // const postData = clone(customCollection)
     const postData = produce(customCollection, (draft) => {
@@ -124,6 +132,8 @@ const EditCustomCollectionModal: FC<Props> = ({
         delete draft.credentials.api_key_header_prefix
         delete draft.credentials.api_key_value
       }
+
+      draft.labels = labels
     })
 
     if (isAdd) {
@@ -154,10 +164,11 @@ const EditCustomCollectionModal: FC<Props> = ({
     <>
       <Drawer
         isShow
+        positionCenter={isAdd && !positionLeft}
         onHide={onHide}
         title={t(`tools.createTool.${isAdd ? 'title' : 'editTitle'}`)!}
-        panelClassName='mt-2 !w-[640px]'
-        maxWidthClassName='!max-w-[640px]'
+        panelClassName='mt-2 !w-[630px]'
+        maxWidthClassName='!max-w-[630px]'
         height='calc(100vh - 16px)'
         headerClassName='!border-b-black/5'
         body={
@@ -254,6 +265,13 @@ const EditCustomCollectionModal: FC<Props> = ({
                 </div>
               </div>
 
+              {/* Labels */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
+                <LabelSelector value={labels} onChange={handleLabelSelect} />
+              </div>
+
+              {/* Privacy Policy */}
               <div>
                 <div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div>
                 <input
@@ -288,7 +306,7 @@ const EditCustomCollectionModal: FC<Props> = ({
                 )
               }
               <div className='flex space-x-2 '>
-                <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
+                <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700 bg-white' onClick={onHide}>{t('common.operation.cancel')}</Button>
                 <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
               </div>
             </div>
@@ -308,6 +326,7 @@ const EditCustomCollectionModal: FC<Props> = ({
       />}
       {credentialsModalShow && (
         <ConfigCredentials
+          positionCenter={isAdd}
           credential={credential}
           onChange={setCredential}
           onHide={() => setCredentialsModalShow(false)}
@@ -315,6 +334,7 @@ const EditCustomCollectionModal: FC<Props> = ({
       }
       {isShowTestApi && (
         <TestApi
+          positionCenter={isAdd}
           tool={currTool as CustomParamSchema}
           customCollection={customCollection}
           onHide={() => setIsShowTestApi(false)}

+ 4 - 0
web/app/components/tools/edit-custom-collection-modal/test-api.tsx

@@ -13,6 +13,7 @@ import { testAPIAvailable } from '@/service/tools'
 import { getLanguage } from '@/i18n/language'
 
 type Props = {
+  positionCenter?: boolean
   customCollection: CustomCollectionBackend
   tool: CustomParamSchema
   onHide: () => void
@@ -21,6 +22,7 @@ type Props = {
 const keyClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900'
 
 const TestApi: FC<Props> = ({
+  positionCenter,
   customCollection,
   tool,
   onHide,
@@ -57,6 +59,7 @@ const TestApi: FC<Props> = ({
     <>
       <Drawer
         isShow
+        positionCenter={positionCenter}
         onHide={onHide}
         title={`${t('tools.test.title')}  ${toolName}`}
         panelClassName='mt-2 !w-[600px]'
@@ -119,6 +122,7 @@ const TestApi: FC<Props> = ({
       />
       {credentialsModalShow && (
         <ConfigCredentials
+          positionCenter={positionCenter}
           credential={tempCredential}
           onChange={setTempCredential}
           onHide={() => setCredentialsModalShow(false)}

+ 0 - 259
web/app/components/tools/index.tsx

@@ -1,259 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import cn from 'classnames'
-import Button from '../base/button'
-import { Plus } from '../base/icons/src/vender/line/general'
-import Toast from '../base/toast'
-import type { Collection, CustomCollectionBackend, Tool } from './types'
-import { CollectionType, LOC } from './types'
-import ToolNavList from './tool-nav-list'
-import Search from './search'
-import Contribute from './contribute'
-import ToolList from './tool-list'
-import EditCustomToolModal from './edit-custom-collection-modal'
-import NoCustomTool from './info/no-custom-tool'
-import NoSearchRes from './info/no-search-res'
-import NoCustomToolPlaceholder from './no-custom-tool-placeholder'
-import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
-import TabSlider from '@/app/components/base/tab-slider'
-import { createCustomCollection, fetchCollectionList as doFetchCollectionList, fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList } from '@/service/tools'
-import type { AgentTool } from '@/types/app'
-
-type Props = {
-  loc: LOC
-  addedTools?: AgentTool[]
-  onAddTool?: (collection: Collection, payload: Tool) => void
-  selectedProviderId?: string
-}
-
-const Tools: FC<Props> = ({
-  loc,
-  addedTools,
-  onAddTool,
-  selectedProviderId,
-}) => {
-  const { t } = useTranslation()
-  const isInToolsPage = loc === LOC.tools
-  const isInDebugPage = !isInToolsPage
-
-  const [collectionList, setCollectionList] = useState<Collection[]>([])
-  const [currCollectionIndex, setCurrCollectionIndex] = useState<number | null>(null)
-
-  const [isDetailLoading, setIsDetailLoading] = useState(false)
-
-  const fetchCollectionList = async () => {
-    const list = await doFetchCollectionList()
-    setCollectionList(list)
-    if (list.length > 0 && currCollectionIndex === null) {
-      let index = 0
-      if (selectedProviderId)
-        index = list.findIndex(item => item.id === selectedProviderId)
-
-      setCurrCollectionIndex(index || 0)
-    }
-  }
-  useEffect(() => {
-    fetchCollectionList()
-  }, [])
-
-  const collectionTypeOptions = (() => {
-    const res = [
-      { value: CollectionType.builtIn, text: t('tools.type.builtIn') },
-      { value: CollectionType.custom, text: t('tools.type.custom') },
-    ]
-    if (!isInToolsPage)
-      res.unshift({ value: CollectionType.all, text: t('tools.type.all') })
-    return res
-  })()
-
-  const [query, setQuery] = useState('')
-  const [toolPageCollectionType, setToolPageCollectionType] = useTabSearchParams({
-    defaultTab: collectionTypeOptions[0].value,
-  })
-  const [appPageCollectionType, setAppPageCollectionType] = useState(collectionTypeOptions[0].value)
-  const { collectionType, setCollectionType } = (() => {
-    if (isInToolsPage) {
-      return {
-        collectionType: toolPageCollectionType,
-        setCollectionType: setToolPageCollectionType,
-      }
-    }
-    return {
-      collectionType: appPageCollectionType,
-      setCollectionType: setAppPageCollectionType,
-    }
-  })()
-
-  const showCollectionList = (() => {
-    let typeFilteredList: Collection[] = []
-    if (collectionType === CollectionType.all)
-      typeFilteredList = collectionList.filter(item => item.type !== CollectionType.model)
-    else if (collectionType === CollectionType.builtIn)
-      typeFilteredList = collectionList.filter(item => item.type === CollectionType.builtIn)
-    else if (collectionType === CollectionType.custom)
-      typeFilteredList = collectionList.filter(item => item.type === CollectionType.custom)
-    if (query)
-      return typeFilteredList.filter(item => item.name.includes(query))
-
-    return typeFilteredList
-  })()
-
-  const hasNoCustomCollection = !collectionList.find(item => item.type === CollectionType.custom)
-
-  useEffect(() => {
-    setCurrCollectionIndex(0)
-  }, [collectionType])
-
-  const currCollection = (() => {
-    if (currCollectionIndex === null)
-      return null
-    return showCollectionList[currCollectionIndex]
-  })()
-
-  const [currTools, setCurrentTools] = useState<Tool[]>([])
-  useEffect(() => {
-    if (!currCollection)
-      return
-
-    (async () => {
-      setIsDetailLoading(true)
-      try {
-        if (currCollection.type === CollectionType.builtIn) {
-          const list = await fetchBuiltInToolList(currCollection.name)
-          setCurrentTools(list)
-        }
-        else if (currCollection.type === CollectionType.model) {
-          const list = await fetchModelToolList(currCollection.name)
-          setCurrentTools(list)
-        }
-        else {
-          const list = await fetchCustomToolList(currCollection.name)
-          setCurrentTools(list)
-        }
-      }
-      catch (e) { }
-      setIsDetailLoading(false)
-    })()
-  }, [currCollection?.name, currCollection?.type])
-
-  const [isShowEditCollectionToolModal, setIsShowEditCollectionToolModal] = useState(false)
-  const handleCreateToolCollection = () => {
-    setIsShowEditCollectionToolModal(true)
-  }
-
-  const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
-    await createCustomCollection(data)
-    Toast.notify({
-      type: 'success',
-      message: t('common.api.actionSuccess'),
-    })
-    await fetchCollectionList()
-    setIsShowEditCollectionToolModal(false)
-  }
-
-  return (
-    <>
-      <div className='flex h-full'>
-        {/* sidebar */}
-        <div className={cn(isInToolsPage ? 'sm:w-[216px] px-4' : 'sm:w-[256px] px-3', 'flex flex-col  w-16 shrink-0 pb-2')}>
-          {isInToolsPage && (
-            <Button className='mt-6 flex items-center !h-8 pl-4' type='primary' onClick={handleCreateToolCollection}>
-              <Plus className='w-4 h-4 mr-1' />
-              <div className='leading-[18px] text-[13px] font-medium truncate'>{t('tools.createCustomTool')}</div>
-            </Button>
-          )}
-
-          {isInDebugPage && (
-            <div className='mt-6 flex space-x-1 items-center'>
-              <Search
-                className='grow'
-                value={query}
-                onChange={setQuery}
-              />
-              <Button className='flex items-center justify-center !w-8 !h-8 !p-0' type='primary'>
-                <Plus className='w-4 h-4' onClick={handleCreateToolCollection} />
-              </Button>
-            </div>
-          )}
-
-          <TabSlider
-            className='mt-3'
-            itemWidth={isInToolsPage ? 89 : 75}
-            value={collectionType}
-            onChange={v => setCollectionType(v as CollectionType)}
-            options={collectionTypeOptions}
-          />
-          {isInToolsPage && (
-            <Search
-              className='mt-5'
-              value={query}
-              onChange={setQuery}
-            />
-          )}
-
-          {(collectionType === CollectionType.custom && hasNoCustomCollection)
-            ? (
-              <div className='grow h-0 p-2 pt-8'>
-                <NoCustomTool onCreateTool={handleCreateToolCollection} />
-              </div>
-            )
-            : (
-              (showCollectionList.length > 0 || !query)
-                ? <ToolNavList
-                  className='mt-2 grow height-0 overflow-y-auto'
-                  currentIndex={currCollectionIndex || 0}
-                  list={showCollectionList}
-                  onChosen={setCurrCollectionIndex}
-                />
-                : (
-                  <div className='grow h-0 p-2 pt-8'>
-                    <NoSearchRes
-                      onReset={() => { setQuery('') }}
-                    />
-                  </div>
-                )
-            )}
-
-          {loc === LOC.tools && (
-            <Contribute />
-          )}
-        </div>
-
-        {/* tools */}
-        <div className={cn('grow h-full overflow-hidden p-2')}>
-          <div className='h-full bg-white rounded-2xl'>
-            {!(collectionType === CollectionType.custom && hasNoCustomCollection) && showCollectionList.length > 0 && (
-              <ToolList
-                collection={currCollection}
-                list={currTools}
-                loc={loc}
-                addedTools={addedTools}
-                onAddTool={onAddTool}
-                onRefreshData={fetchCollectionList}
-                onCollectionRemoved={() => {
-                  setCurrCollectionIndex(0)
-                  fetchCollectionList()
-                }}
-                isLoading={isDetailLoading}
-              />
-            )}
-
-            {collectionType === CollectionType.custom && hasNoCustomCollection && (
-              <NoCustomToolPlaceholder />
-            )}
-          </div>
-        </div>
-      </div>
-      {isShowEditCollectionToolModal && (
-        <EditCustomToolModal
-          payload={null}
-          onHide={() => setIsShowEditCollectionToolModal(false)}
-          onAdd={doCreateCustomToolCollection}
-        />
-      )}
-    </>
-  )
-}
-export default React.memo(Tools)

+ 0 - 38
web/app/components/tools/info/no-custom-tool.tsx

@@ -1,38 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { Icon3Dots } from '../../base/icons/src/public/other'
-import { Tools } from '@/app/components/base/icons/src/public/header-nav/tools'
-type Props = {
-  onCreateTool: () => void
-}
-
-const NoCustomTool: FC<Props> = ({
-  onCreateTool,
-}) => {
-  const { t } = useTranslation()
-
-  return (
-    <div>
-      <div className='inline-flex p-3 rounded-lg  bg-gray-50 border border-[#EAECF5]'>
-        <Tools className='w-5 h-5 text-gray-500' />
-      </div>
-      <div className='mt-2'>
-        <div className='leading-5 text-sm font-medium text-gray-500'>
-          {t('tools.noCustomTool.title')}<Icon3Dots className='inline relative -top-3 -left-1.5' />
-        </div>
-        <div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
-          {t('tools.noCustomTool.content')}
-        </div>
-        <div
-          className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
-          onClick={onCreateTool}
-        >
-          {t('tools.noCustomTool.createTool')}
-        </div>
-      </div>
-    </div>
-  )
-}
-export default React.memo(NoCustomTool)

+ 0 - 38
web/app/components/tools/info/no-search-res.tsx

@@ -1,38 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { SearchMd } from '../../base/icons/src/vender/solid/general'
-
-type Props = {
-  onReset: () => void
-}
-
-const NoSearchRes: FC<Props> = ({
-  onReset,
-}) => {
-  const { t } = useTranslation()
-
-  return (
-    <div>
-      <div className='inline-flex p-3 rounded-lg  bg-gray-50 border border-[#EAECF5]'>
-        <SearchMd className='w-5 h-5 text-gray-500' />
-      </div>
-      <div className='mt-2'>
-        <div className='leading-5 text-sm font-medium text-gray-500'>
-          {t('tools.noSearchRes.title')}
-        </div>
-        <div className='mt-1 leading-[18px] text-xs font-normal text-gray-500'>
-          {t('tools.noSearchRes.content')}
-        </div>
-        <div
-          className='mt-2 leading-[18px] text-xs font-medium text-[#155EEF] uppercase cursor-pointer'
-          onClick={onReset}
-        >
-          {t('tools.noSearchRes.reset')}
-        </div>
-      </div>
-    </div>
-  )
-}
-export default React.memo(NoSearchRes)

+ 6 - 0
web/app/components/tools/labels/constant.ts

@@ -0,0 +1,6 @@
+import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
+export type Label = {
+  name: string
+  icon: string
+  label: TypeWithI18N
+}

+ 144 - 0
web/app/components/tools/labels/filter.tsx

@@ -0,0 +1,144 @@
+import type { FC } from 'react'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import { useDebounceFn, useMount } from 'ahooks'
+import cn from 'classnames'
+import { useStore as useLabelStore } from './store'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import SearchInput from '@/app/components/base/search-input'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
+import { Check } from '@/app/components/base/icons/src/vender/line/general'
+import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
+import type { Label } from '@/app/components/tools/labels/constant'
+import { fetchLabelList } from '@/service/tools'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+
+type LabelFilterProps = {
+  value: string[]
+  onChange: (v: string[]) => void
+}
+const LabelFilter: FC<LabelFilterProps> = ({
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const [open, setOpen] = useState(false)
+
+  const labelList = useLabelStore(s => s.labelList)
+  const setLabelList = useLabelStore(s => s.setLabelList)
+
+  const [keywords, setKeywords] = useState('')
+  const [searchKeywords, setSearchKeywords] = useState('')
+  const { run: handleSearch } = useDebounceFn(() => {
+    setSearchKeywords(keywords)
+  }, { wait: 500 })
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+    handleSearch()
+  }
+
+  const filteredLabelList = useMemo(() => {
+    return labelList.filter(label => label.name.includes(searchKeywords))
+  }, [labelList, searchKeywords])
+
+  const currentLabel = useMemo(() => {
+    return labelList.find(label => label.name === value[0])
+  }, [value, labelList])
+
+  const selectLabel = (label: Label) => {
+    if (value.includes(label.name))
+      onChange(value.filter(v => v !== label.name))
+    else
+      onChange([...value, label.name])
+  }
+
+  useMount(() => {
+    fetchLabelList().then((res) => {
+      setLabelList(res)
+    })
+  })
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-start'
+      offset={4}
+    >
+      <div className='relative'>
+        <PortalToFollowElemTrigger
+          onClick={() => setOpen(v => !v)}
+          className='block'
+        >
+          <div className={cn(
+            'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
+            open && !value.length && '!bg-gray-300 hover:bg-gray-300',
+            !open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
+            open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
+          )}>
+            <div className='p-[1px]'>
+              <Tag01 className='h-3.5 w-3.5 text-gray-700' />
+            </div>
+            <div className='text-[13px] leading-[18px] text-gray-700'>
+              {!value.length && t('common.tag.placeholder')}
+              {!!value.length && currentLabel?.label[language]}
+            </div>
+            {value.length > 1 && (
+              <div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
+            )}
+            {!value.length && (
+              <div className='p-[1px]'>
+                <ChevronDown className='h-3.5 w-3.5 text-gray-700'/>
+              </div>
+            )}
+            {!!value.length && (
+              <div className='p-[1px] cursor-pointer group/clear' onClick={(e) => {
+                e.stopPropagation()
+                onChange([])
+              }}>
+                <XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600'/>
+              </div>
+            )}
+          </div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent className='z-[1002]'>
+          <div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200  shadow-lg'>
+            <div className='p-2 border-b-[0.5px] border-black/5'>
+              <SearchInput white value={keywords} onChange={handleKeywordsChange} />
+            </div>
+            <div className='p-1'>
+              {filteredLabelList.map(label => (
+                <div
+                  key={label.name}
+                  className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
+                  onClick={() => selectLabel(label)}
+                >
+                  <div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div>
+                  {value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
+                </div>
+              ))}
+              {!filteredLabelList.length && (
+                <div className='p-3 flex flex-col items-center gap-1'>
+                  <Tag03 className='h-6 w-6 text-gray-300' />
+                  <div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
+                </div>
+              )}
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </div>
+    </PortalToFollowElem>
+
+  )
+}
+
+export default LabelFilter

+ 128 - 0
web/app/components/tools/labels/selector.tsx

@@ -0,0 +1,128 @@
+import type { FC } from 'react'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import { useDebounceFn, useMount } from 'ahooks'
+import cn from 'classnames'
+import { useStore as useLabelStore } from './store'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import SearchInput from '@/app/components/base/search-input'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
+import Checkbox from '@/app/components/base/checkbox'
+import type { Label } from '@/app/components/tools/labels/constant'
+import { fetchLabelList } from '@/service/tools'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+
+type LabelSelectorProps = {
+  value: string[]
+  onChange: (v: string[]) => void
+}
+const LabelSelector: FC<LabelSelectorProps> = ({
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const [open, setOpen] = useState(false)
+
+  const labelList = useLabelStore(s => s.labelList)
+  const setLabelList = useLabelStore(s => s.setLabelList)
+
+  const [keywords, setKeywords] = useState('')
+  const [searchKeywords, setSearchKeywords] = useState('')
+  const { run: handleSearch } = useDebounceFn(() => {
+    setSearchKeywords(keywords)
+  }, { wait: 500 })
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+    handleSearch()
+  }
+
+  const filteredLabelList = useMemo(() => {
+    return labelList.filter(label => label.name.includes(searchKeywords))
+  }, [labelList, searchKeywords])
+
+  const selectedLabels = useMemo(() => {
+    return value.map(v => labelList.find(l => l.name === v)?.label[language]).join(', ')
+  }, [value, labelList, language])
+
+  const selectLabel = (label: Label) => {
+    if (value.includes(label.name))
+      onChange(value.filter(v => v !== label.name))
+    else
+      onChange([...value, label.name])
+  }
+
+  useMount(() => {
+    fetchLabelList().then((res) => {
+      setLabelList(res)
+    })
+  })
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-start'
+      offset={4}
+    >
+      <div className='relative'>
+        <PortalToFollowElemTrigger
+          onClick={() => setOpen(v => !v)}
+          className='block'
+        >
+          <div className={cn(
+            'flex items-center gap-1 px-3 h-9 rounded-lg border-[0.5px] border-transparent bg-gray-100 cursor-pointer hover:bg-gray-200',
+            open && '!bg-gray-200 hover:bg-gray-200',
+          )}>
+            <div title={value.length > 0 ? selectedLabels : ''} className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate', !value.length && '!text-gray-400')}>
+              {!value.length && t('tools.createTool.toolInput.labelPlaceholder')}
+              {!!value.length && selectedLabels}
+            </div>
+            <div className='shrink-0 ml-1 text-gray-700 opacity-60'>
+              <ChevronDown className='h-4 w-4'/>
+            </div>
+          </div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent className='z-[1040]'>
+          <div className='relative w-[591px] bg-white rounded-lg border-[0.5px] border-gray-200  shadow-lg'>
+            <div className='p-2 border-b-[0.5px] border-black/5'>
+              <SearchInput white value={keywords} onChange={handleKeywordsChange} />
+            </div>
+            <div className='p-1 max-h-[264px] overflow-y-auto'>
+              {filteredLabelList.map(label => (
+                <div
+                  key={label.name}
+                  className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
+                  onClick={() => selectLabel(label)}
+                >
+                  <Checkbox
+                    className='shrink-0'
+                    checked={value.includes(label.name)}
+                    onCheck={() => {}}
+                  />
+                  <div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div>
+                </div>
+              ))}
+              {!filteredLabelList.length && (
+                <div className='p-3 flex flex-col items-center gap-1'>
+                  <Tag03 className='h-6 w-6 text-gray-300' />
+                  <div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
+                </div>
+              )}
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </div>
+    </PortalToFollowElem>
+  )
+}
+
+export default LabelSelector

+ 15 - 0
web/app/components/tools/labels/store.ts

@@ -0,0 +1,15 @@
+import { create } from 'zustand'
+import type { Label } from './constant'
+
+type State = {
+  labelList: Label[]
+}
+
+type Action = {
+  setLabelList: (labelList?: Label[]) => void
+}
+
+export const useStore = create<State & Action>(set => ({
+  labelList: [],
+  setLabelList: labelList => set(() => ({ labelList })),
+}))

+ 0 - 26
web/app/components/tools/no-custom-tool-placeholder.tsx

@@ -1,26 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { BookOpen01 } from '../base/icons/src/vender/line/education'
-import { Icon3Dots } from '../base/icons/src/public/other'
-
-const NoCustomToolPlaceHolder: FC = () => {
-  const { t } = useTranslation()
-
-  return (
-    <div className='h-full flex items-center justify-center'>
-      <div className='p-6 rounded-xl bg-gray-50'>
-        <div className='inline-flex p-2 border border-gray-200 rounded-md'>
-          <BookOpen01 className='w-4 h-4 text-primary-600' />
-        </div>
-        <div className='mt-3 leading-6 text-base font-medium text-gray-700'>
-          {t('tools.noCustomTool.title')}
-          <Icon3Dots className='inline relative -top-3 -left-1.5' />
-        </div>
-        <div className='mt-2 leading-5 text-sm font-normal text-gray-700'>{t('tools.noCustomTool.content')}</div>
-      </div>
-    </div>
-  )
-}
-export default React.memo(NoCustomToolPlaceHolder)

+ 117 - 0
web/app/components/tools/provider-list.tsx

@@ -0,0 +1,117 @@
+'use client'
+import { useEffect, useMemo, useState } from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import type { Collection } from './types'
+import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
+import TabSliderNew from '@/app/components/base/tab-slider-new'
+import LabelFilter from '@/app/components/tools/labels/filter'
+import SearchInput from '@/app/components/base/search-input'
+import { DotsGrid, XClose } from '@/app/components/base/icons/src/vender/line/general'
+import { Colors } from '@/app/components/base/icons/src/vender/line/others'
+import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
+import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
+import ContributeCard from '@/app/components/tools/provider/contribute'
+import ProviderCard from '@/app/components/tools/provider/card'
+import ProviderDetail from '@/app/components/tools/provider/detail'
+import Empty from '@/app/components/tools/add-tool-modal/empty'
+import { fetchCollectionList } from '@/service/tools'
+
+const ProviderList = () => {
+  const { t } = useTranslation()
+
+  const [activeTab, setActiveTab] = useTabSearchParams({
+    defaultTab: 'builtin',
+  })
+  const options = [
+    { value: 'builtin', text: t('tools.type.builtIn'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
+    { value: 'api', text: t('tools.type.custom'), icon: <Colors className='w-[14px] h-[14px] mr-1'/> },
+    { value: 'workflow', text: t('tools.type.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
+  ]
+  const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
+  const handleTagsChange = (value: string[]) => {
+    setTagFilterValue(value)
+  }
+  const [keywords, setKeywords] = useState<string>('')
+  const handleKeywordsChange = (value: string) => {
+    setKeywords(value)
+  }
+
+  const [collectionList, setCollectionList] = useState<Collection[]>([])
+  const filteredCollectionList = useMemo(() => {
+    return collectionList.filter((collection) => {
+      if (collection.type !== activeTab)
+        return false
+      if (tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
+        return false
+      if (keywords)
+        return collection.name.toLowerCase().includes(keywords.toLowerCase())
+      return true
+    })
+  }, [activeTab, tagFilterValue, keywords, collectionList])
+  const getProviderList = async () => {
+    const list = await fetchCollectionList()
+    setCollectionList([...list])
+  }
+  useEffect(() => {
+    getProviderList()
+  }, [])
+
+  const [currentProvider, setCurrentProvider] = useState<Collection | undefined>()
+  useEffect(() => {
+    if (currentProvider && collectionList.length > 0) {
+      const newCurrentProvider = collectionList.find(collection => collection.id === currentProvider.id)
+      setCurrentProvider(newCurrentProvider)
+    }
+  }, [collectionList, currentProvider])
+
+  return (
+    <div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'>
+      <div className='relative flex flex-col overflow-y-auto bg-gray-100 grow'>
+        <div className={cn(
+          'z-20 sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 flex-wrap gap-y-2',
+          currentProvider && 'pr-6',
+        )}>
+          <TabSliderNew
+            value={activeTab}
+            onChange={(state) => {
+              setActiveTab(state)
+              if (state !== activeTab)
+                setCurrentProvider(undefined)
+            }}
+            options={options}
+          />
+          <div className='flex items-center gap-2'>
+            <LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
+            <SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
+          </div>
+        </div>
+        <div className={cn(
+          'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
+          currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
+        )}>
+          {activeTab === 'builtin' && <ContributeCard />}
+          {activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList}/>}
+          {filteredCollectionList.map(collection => (
+            <ProviderCard
+              active={currentProvider?.id === collection.id}
+              onSelect={() => setCurrentProvider(collection)}
+              key={collection.id}
+              collection={collection}
+            />
+          ))}
+          {!filteredCollectionList.length && <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'><Empty/></div>}
+        </div>
+      </div>
+      <div className={cn(
+        'shrink-0 w-0 border-l-[0.5px] border-black/8 overflow-y-auto transition-all duration-200 ease-in-out',
+        currentProvider && 'w-[420px]',
+      )}>
+        {currentProvider && <ProviderDetail collection={currentProvider} onRefreshData={getProviderList} />}
+      </div>
+      <div className='absolute top-5 right-5 p-1 cursor-pointer' onClick={() => setCurrentProvider(undefined)}><XClose className='w-4 h-4'/></div>
+    </div>
+  )
+}
+ProviderList.displayName = 'ToolProviderList'
+export default ProviderList

+ 83 - 0
web/app/components/tools/provider/card.tsx

@@ -0,0 +1,83 @@
+'use client'
+import { useMemo } from 'react'
+import cn from 'classnames'
+import { useContext } from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import type { Collection } from '../types'
+import AppIcon from '@/app/components/base/app-icon'
+import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
+
+type Props = {
+  active: boolean
+  collection: Collection
+  onSelect: () => void
+}
+
+const ProviderCard = ({
+  active,
+  collection,
+  onSelect,
+}: Props) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const labelList = useLabelStore(s => s.labelList)
+
+  const labelContent = useMemo(() => {
+    if (!collection.labels)
+      return ''
+    return collection.labels.map((name) => {
+      const label = labelList.find(item => item.name === name)
+      return label?.label[language]
+    }).filter(Boolean).join(', ')
+  }, [collection.labels, labelList, language])
+
+  return (
+    <div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg', active && '!border-primary-400')} onClick={onSelect}>
+      <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
+        <div className='relative shrink-0'>
+          {typeof collection.icon === 'string' && (
+            <div className='w-10 h-10 bg-center bg-cover bg-no-repeat rounded-md' style={{ backgroundImage: `url(${collection.icon})` }}/>
+          )}
+          {typeof collection.icon !== 'string' && (
+            <AppIcon
+              size='large'
+              icon={collection.icon.content}
+              background={collection.icon.background}
+            />
+          )}
+        </div>
+        <div className='grow w-0 py-[1px]'>
+          <div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
+            <div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
+          </div>
+          <div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
+            <div className='truncate'>{t('tools.author')}&nbsp;{collection.author}</div>
+          </div>
+        </div>
+      </div>
+      <div
+        className={cn(
+          'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500',
+          collection.labels?.length ? 'line-clamp-2' : 'line-clamp-4',
+          collection.labels?.length > 0 && 'group-hover:line-clamp-2 group-hover:max-h-[36px]',
+        )}
+        title={collection.description[language]}
+      >
+        {collection.description[language]}
+      </div>
+      {collection.labels?.length > 0 && (
+        <div className='flex items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]'>
+          <div className='relative w-full flex items-center gap-1 py-[7px] rounded-md text-gray-500' title={labelContent}>
+            <Tag01 className='shrink-0 w-3 h-3' />
+            <div className='grow text-xs text-start leading-[18px] font-normal truncate'>{labelContent}</div>
+          </div>
+        </div>
+      )}
+    </div>
+  )
+}
+export default ProviderCard

+ 38 - 0
web/app/components/tools/provider/contribute.tsx

@@ -0,0 +1,38 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { ToolsActive } from '@/app/components/base/icons/src/public/header-nav/tools'
+import { Heart02 } from '@/app/components/base/icons/src/vender/solid/education'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+
+const Contribute: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <a
+      href='https://github.com/langgenius/dify/blob/main/api/core/tools/README.md'
+      target='_blank'
+      rel='noopener noreferrer'
+      className="group flex col-span-1 bg-white bg-cover bg-no-repeat bg-[url('~@/app/components/tools/provider/grid_bg.svg')] border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg"
+    >
+      <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
+        <div className='relative shrink-0 flex items-center'>
+          <div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><ToolsActive className='w-4 h-4 text-primary-600'/></div>
+          <div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]'/></div>
+        </div>
+      </div>
+      <div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'>
+        <div className='text-gradient'>{t('tools.contribute.line1')}</div>
+        <div className='text-gradient'>{t('tools.contribute.line2')}</div>
+      </div>
+      <div className='px-4 py-3 border-t-[0.5px] border-black/5 flex items-center space-x-1 text-[#155EEF]'>
+        <BookOpen01 className='w-3 h-3' />
+        <div className='grow leading-[18px] text-xs font-normal'>{t('tools.contribute.viewGuide')}</div>
+        <ArrowUpRight className='w-3 h-3' />
+      </div>
+    </a>
+  )
+}
+export default React.memo(Contribute)

+ 70 - 0
web/app/components/tools/provider/custom-create-card.tsx

@@ -0,0 +1,70 @@
+'use client'
+import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import type { CustomCollectionBackend } from '../types'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import { Plus } from '@/app/components/base/icons/src/vender/line/general'
+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'
+
+type Props = {
+  onRefreshData: () => void
+}
+
+const Contribute = ({ onRefreshData }: Props) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+
+  const linkUrl = useMemo(() => {
+    if (language.startsWith('zh_'))
+      return 'https://docs.dify.ai/v/zh-hans/guides/gong-ju/quick-tool-integration'
+    return 'https://docs.dify.ai/tutorials/quick-tool-integration'
+  }, [language])
+
+  const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
+  const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
+    await createCustomCollection(data)
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditCustomCollectionModal(false)
+    onRefreshData()
+  }
+
+  return (
+    <>
+      <div className='flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-gray-50 hover:shadow-lg'>
+        <div className='group grow rounded-t-xl hover:bg-white' onClick={() => setIsShowEditCustomCollectionModal(true)}>
+          <div className='shrink-0 flex items-center p-4 pb-3'>
+            <div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg group-hover:border-primary-100 group-hover:bg-primary-50'>
+              <Plus className='w-4 h-4 text-gray-500 group-hover:text-primary-600'/>
+            </div>
+            <div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('tools.createCustomTool')}</div>
+          </div>
+        </div>
+        <div className='px-4 py-3 rounded-b-xl border-t-[0.5px] border-black/5 text-gray-500 hover:text-[#155EEF] hover:bg-white'>
+          <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'>
+            <BookOpen01 className='shrink-0 w-3 h-3' />
+            <div className='grow leading-[18px] text-xs font-normal truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div>
+            <ArrowUpRight className='shrink-0 w-3 h-3' />
+          </a>
+        </div>
+      </div>
+      {isShowEditCollectionToolModal && (
+        <EditCustomToolModal
+          payload={null}
+          onHide={() => setIsShowEditCustomCollectionModal(false)}
+          onAdd={doCreateCustomToolCollection}
+        />
+      )}
+    </>
+  )
+}
+export default Contribute

+ 343 - 0
web/app/components/tools/provider/detail.tsx

@@ -0,0 +1,343 @@
+'use client'
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import cn from 'classnames'
+import { AuthHeaderPrefix, AuthType, CollectionType } from '../types'
+import type { Collection, CustomCollectionBackend, Tool, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '../types'
+import ToolItem from './tool-item'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import AppIcon from '@/app/components/base/app-icon'
+import Button from '@/app/components/base/button'
+import Indicator from '@/app/components/header/indicator'
+import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general'
+import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
+import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
+import WorkflowToolModal from '@/app/components/tools/workflow-tool'
+import Toast from '@/app/components/base/toast'
+import {
+  deleteWorkflowTool,
+  fetchBuiltInToolList,
+  fetchCustomCollection,
+  fetchCustomToolList,
+  fetchModelToolList,
+  fetchWorkflowToolDetail,
+  removeBuiltInToolCredential,
+  removeCustomCollection,
+  saveWorkflowToolProvider,
+  updateBuiltInToolCredential,
+  updateCustomCollection,
+} from '@/service/tools'
+import { useModalContext } from '@/context/modal-context'
+import { useProviderContext } from '@/context/provider-context'
+import { ConfigurateMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import Loading from '@/app/components/base/loading'
+
+type Props = {
+  collection: Collection
+  onRefreshData: () => void
+}
+
+const ProviderDetail = ({
+  collection,
+  onRefreshData,
+}: Props) => {
+  const { t } = useTranslation()
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+
+  const needAuth = collection.allow_delete || collection.type === CollectionType.model
+  const isAuthed = collection.is_team_authorization
+  const isBuiltIn = collection.type === CollectionType.builtIn
+  const isModel = collection.type === CollectionType.model
+
+  const [isDetailLoading, setIsDetailLoading] = useState(false)
+
+  // built in provider
+  const [showSettingAuth, setShowSettingAuth] = useState(false)
+  const { setShowModelModal } = useModalContext()
+  const { modelProviders: providers } = useProviderContext()
+  const showSettingAuthModal = () => {
+    if (isModel) {
+      const provider = providers.find(item => item.provider === collection?.id)
+      if (provider) {
+        setShowModelModal({
+          payload: {
+            currentProvider: provider,
+            currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
+            currentCustomConfigrationModelFixedFields: undefined,
+          },
+          onSaveCallback: () => {
+            onRefreshData()
+          },
+        })
+      }
+    }
+    else {
+      setShowSettingAuth(true)
+    }
+  }
+  // custom provider
+  const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | WorkflowToolProviderResponse | null>(null)
+  const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
+  const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
+    await updateCustomCollection(data)
+    onRefreshData()
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditCustomCollectionModal(false)
+  }
+  const doRemoveCustomToolCollection = async () => {
+    await removeCustomCollection(collection?.name as string)
+    onRefreshData()
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditCustomCollectionModal(false)
+  }
+  const getCustomProvider = useCallback(async () => {
+    setIsDetailLoading(true)
+    const res = await fetchCustomCollection(collection.name)
+    if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
+      if (res.credentials.api_key_value)
+        res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
+    }
+    setCustomCollection({
+      ...res,
+      labels: collection.labels,
+      provider: collection.name,
+    })
+    setIsDetailLoading(false)
+  }, [collection.name])
+  // workflow provider
+  const [isShowEditWorkflowToolModal, setIsShowEditWorkflowToolModal] = useState(false)
+  const getWorkflowToolProvider = useCallback(async () => {
+    setIsDetailLoading(true)
+    const res = await fetchWorkflowToolDetail(collection.id)
+    const payload = {
+      ...res,
+      parameters: res.tool?.parameters.map((item) => {
+        return {
+          name: item.name,
+          description: item.llm_description,
+          form: item.form,
+          required: item.required,
+          type: item.type,
+        }
+      }) || [],
+      labels: res.tool?.labels || [],
+    }
+    setCustomCollection(payload)
+    setIsDetailLoading(false)
+  }, [collection.id])
+  const removeWorkflowToolProvider = async () => {
+    await deleteWorkflowTool(collection.id)
+    onRefreshData()
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditWorkflowToolModal(false)
+  }
+  const updateWorkflowToolProvider = async (data: WorkflowToolProviderRequest & Partial<{
+    workflow_app_id: string
+    workflow_tool_id: string
+  }>) => {
+    await saveWorkflowToolProvider(data)
+    onRefreshData()
+    getWorkflowToolProvider()
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.actionSuccess'),
+    })
+    setIsShowEditWorkflowToolModal(false)
+  }
+
+  // ToolList
+  const [toolList, setToolList] = useState<Tool[]>([])
+  const getProviderToolList = useCallback(async () => {
+    setIsDetailLoading(true)
+    try {
+      if (collection.type === CollectionType.builtIn) {
+        const list = await fetchBuiltInToolList(collection.name)
+        setToolList(list)
+      }
+      else if (collection.type === CollectionType.model) {
+        const list = await fetchModelToolList(collection.name)
+        setToolList(list)
+      }
+      else if (collection.type === CollectionType.workflow) {
+        setToolList([])
+      }
+      else {
+        const list = await fetchCustomToolList(collection.name)
+        setToolList(list)
+      }
+    }
+    catch (e) { }
+    setIsDetailLoading(false)
+  }, [collection.name, collection.type])
+
+  useEffect(() => {
+    if (collection.type === CollectionType.custom)
+      getCustomProvider()
+    if (collection.type === CollectionType.workflow)
+      getWorkflowToolProvider()
+    getProviderToolList()
+  }, [collection.name, collection.type, getCustomProvider, getProviderToolList, getWorkflowToolProvider])
+
+  return (
+    <div className='px-6 py-3'>
+      <div className='flex items-center py-1 gap-2'>
+        <div className='relative shrink-0'>
+          {typeof collection.icon === 'string' && (
+            <div className='w-8 h-8 bg-center bg-cover bg-no-repeat rounded-md' style={{ backgroundImage: `url(${collection.icon})` }}/>
+          )}
+          {typeof collection.icon !== 'string' && (
+            <AppIcon
+              size='small'
+              icon={collection.icon.content}
+              background={collection.icon.background}
+            />
+          )}
+        </div>
+        <div className='grow w-0 py-[1px]'>
+          <div className='flex items-center text-md leading-6 font-semibold text-gray-900'>
+            <div className='truncate' title={collection.label[language]}>{collection.label[language]}</div>
+          </div>
+        </div>
+      </div>
+      <div className='mt-2 min-h-[36px] text-gray-500 text-sm leading-[18px]'>{collection.description[language]}</div>
+      <div className='flex gap-1 border-b-[0.5px] border-black/5'>
+        {(collection.type === CollectionType.builtIn) && needAuth && (
+          <Button
+            type={isAuthed ? 'default' : 'primary'}
+            className={cn('shrink-0 my-3 w-full flex items-center', isAuthed && 'bg-white')}
+            onClick={() => {
+              if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
+                showSettingAuthModal()
+            }}
+          >
+            {isAuthed && <Indicator className='mr-2' color={'green'} />}
+            <div className={cn('text-white leading-[18px] text-[13px] font-medium', isAuthed && '!text-gray-700')}>
+              {isAuthed ? t('tools.auth.authorized') : t('tools.auth.unauthorized')}
+            </div>
+          </Button>
+        )}
+        {collection.type === CollectionType.custom && !isDetailLoading && (
+          <Button
+            className={cn('shrink-0 my-3 w-full flex items-center bg-white')}
+            onClick={() => setIsShowEditCustomCollectionModal(true)}
+          >
+            <Settings01 className='mr-1 w-4 h-4 text-gray-500' />
+            <div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
+          </Button>
+        )}
+        {collection.type === CollectionType.workflow && !isDetailLoading && customCollection && (
+          <>
+            <Button
+              type='primary'
+              className={cn('shrink-0 my-3 w-[183px] flex items-center')}
+            >
+              <a className='flex items-center text-white' href={`/app/${(customCollection as WorkflowToolProviderResponse).workflow_app_id}/workflow`} rel='noreferrer' target='_blank'>
+                <div className='leading-5 text-sm font-medium'>{t('tools.openInStudio')}</div>
+                <LinkExternal02 className='ml-1 w-4 h-4' />
+              </a>
+            </Button>
+            <Button
+              className={cn('shrink-0 my-3 w-[183px] flex items-center bg-white')}
+              onClick={() => setIsShowEditWorkflowToolModal(true)}
+            >
+              <div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
+            </Button>
+          </>
+        )}
+      </div>
+      {/* Tools */}
+      <div className='pt-3'>
+        {isDetailLoading && <div className='flex h-[200px]'><Loading type='app'/></div>}
+        {!isDetailLoading && (
+          <div className='text-xs font-medium leading-6 text-gray-500'>
+            {collection.type === CollectionType.workflow && <span className=''>{t('tools.createTool.toolInput.title').toLocaleUpperCase()}</span>}
+            {collection.type !== CollectionType.workflow && <span className=''>{t('tools.includeToolNum', { num: toolList.length }).toLocaleUpperCase()}</span>}
+            {needAuth && (isBuiltIn || isModel) && !isAuthed && (
+              <>
+                <span className='px-1'>·</span>
+                <span className='text-[#DC6803]'>{t('tools.auth.setup').toLocaleUpperCase()}</span>
+              </>
+            )}
+          </div>
+        )}
+        {!isDetailLoading && (
+          <div className='mt-1'>
+            {collection.type !== CollectionType.workflow && toolList.map(tool => (
+              <ToolItem
+                key={tool.name}
+                disabled={needAuth && (isBuiltIn || isModel) && !isAuthed}
+                collection={collection}
+                tool={tool}
+                isBuiltIn={isBuiltIn}
+                isModel={isModel}
+              />
+            ))}
+            {collection.type === CollectionType.workflow && (customCollection as WorkflowToolProviderResponse)?.tool?.parameters.map(item => (
+              <div key={item.name} className='mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gray-200'>
+                <div className='flex items-center gap-2'>
+                  <span className='font-medium text-sm text-gray-900'>{item.name}</span>
+                  <span className='text-xs leading-[18px] text-gray-500'>{item.type}</span>
+                  <span className='font-medium text-xs leading-[18px] text-[#ec4a0a]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
+                </div>
+                <div className='h-[18px] leading-[18px] text-gray-500 text-xs'>{item.llm_description}</div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+      {showSettingAuth && (
+        <ConfigCredential
+          collection={collection}
+          onCancel={() => setShowSettingAuth(false)}
+          onSaved={async (value) => {
+            await updateBuiltInToolCredential(collection.name, value)
+            Toast.notify({
+              type: 'success',
+              message: t('common.api.actionSuccess'),
+            })
+            await onRefreshData()
+            setShowSettingAuth(false)
+          }}
+          onRemove={async () => {
+            await removeBuiltInToolCredential(collection.name)
+            Toast.notify({
+              type: 'success',
+              message: t('common.api.actionSuccess'),
+            })
+            await onRefreshData()
+            setShowSettingAuth(false)
+          }}
+        />
+      )}
+      {isShowEditCollectionToolModal && (
+        <EditCustomToolModal
+          payload={customCollection}
+          onHide={() => setIsShowEditCustomCollectionModal(false)}
+          onEdit={doUpdateCustomToolCollection}
+          onRemove={doRemoveCustomToolCollection}
+        />
+      )}
+      {isShowEditWorkflowToolModal && (
+        <WorkflowToolModal
+          payload={customCollection}
+          onHide={() => setIsShowEditWorkflowToolModal(false)}
+          onRemove={removeWorkflowToolProvider}
+          onSave={updateWorkflowToolProvider}
+        />
+      )}
+    </div>
+  )
+}
+export default ProviderDetail

File diff suppressed because it is too large
+ 5 - 0
web/app/components/tools/provider/grid_bg.svg


+ 53 - 0
web/app/components/tools/provider/tool-item.tsx

@@ -0,0 +1,53 @@
+'use client'
+import React, { useState } from 'react'
+import cn from 'classnames'
+import { useContext } from 'use-context-selector'
+import type { Collection, Tool } from '../types'
+import I18n from '@/context/i18n'
+import { getLanguage } from '@/i18n/language'
+import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
+
+type Props = {
+  disabled?: boolean
+  collection: Collection
+  tool: Tool
+  isBuiltIn: boolean
+  isModel: boolean
+}
+
+const ToolItem = ({
+  disabled,
+  collection,
+  tool,
+  isBuiltIn,
+  isModel,
+}: Props) => {
+  const { locale } = useContext(I18n)
+  const language = getLanguage(locale)
+  const [showDetail, setShowDetail] = useState(false)
+
+  return (
+    <>
+      <div
+        className={cn('mb-2 px-4 py-3 rounded-xl bg-gray-25 border-[0.5px] border-gary-200  shadow-xs cursor-pointer', disabled && 'opacity-50 !cursor-not-allowed')}
+        onClick={() => !disabled && setShowDetail(true)}
+      >
+        <div className='text-gray-800 font-semibold text-sm leading-5'>{tool.label[language]}</div>
+        <div className='mt-0.5 text-xs leading-[18px] text-gray-500 line-clamp-2' title={tool.description[language]}>{tool.description[language]}</div>
+      </div>
+      {showDetail && (
+        <SettingBuiltInTool
+          collection={collection}
+          toolName={tool.name}
+          readonly
+          onHide={() => {
+            setShowDetail(false)
+          }}
+          isBuiltIn={isBuiltIn}
+          isModel={isModel}
+        />
+      )}
+    </>
+  )
+}
+export default ToolItem

+ 0 - 41
web/app/components/tools/search.tsx

@@ -1,41 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import cn from 'classnames'
-import {
-  MagnifyingGlassIcon,
-} from '@heroicons/react/24/solid'
-import { useTranslation } from 'react-i18next'
-
-type Props = {
-  className?: string
-  value: string
-  onChange: (v: string) => void
-}
-
-const Search: FC<Props> = ({
-  className,
-  value,
-  onChange,
-}) => {
-  const { t } = useTranslation()
-
-  return (
-    <div className={cn(className, 'flex relative')}>
-      <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
-        <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
-      </div>
-      <input
-        type="text"
-        name="query"
-        className="block w-0 grow bg-gray-200 shadow-sm rounded-md border-0 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-8"
-        placeholder={t('common.operation.search')!}
-        value={value}
-        onChange={(e) => {
-          onChange(e.target.value)
-        }}
-      />
-    </div>
-  )
-}
-export default React.memo(Search)

+ 3 - 3
web/app/components/tools/setting/build-in/config-credentials.tsx

@@ -48,8 +48,8 @@ const ConfigCredential: FC<Props> = ({
       onHide={onCancel}
       title={t('tools.auth.setupModalTitle') as string}
       titleDescription={t('tools.auth.setupModalTitleDescription') as string}
-      panelClassName='mt-2 !w-[480px]'
-      maxWidthClassName='!max-w-[480px]'
+      panelClassName='mt-2 !w-[405px]'
+      maxWidthClassName='!max-w-[405px]'
       height='calc(100vh - 16px)'
       contentClassName='!bg-gray-100'
       headerClassName='!border-b-black/5'
@@ -88,7 +88,7 @@ const ConfigCredential: FC<Props> = ({
                     )
                   }
                   < div className='flex space-x-2'>
-                    <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onCancel}>{t('common.operation.cancel')}</Button>
+                    <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700 bg-white' onClick={onCancel}>{t('common.operation.cancel')}</Button>
                     <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => onSaved(tempCredential)}>{t('common.operation.save')}</Button>
                   </div>
                 </div>

+ 0 - 77
web/app/components/tools/tool-list/header.tsx

@@ -1,77 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useContext } from 'use-context-selector'
-import cn from 'classnames'
-import { useTranslation } from 'react-i18next'
-import type { Collection } from '../types'
-import { CollectionType, LOC } from '../types'
-import { Settings01 } from '../../base/icons/src/vender/line/general'
-import I18n from '@/context/i18n'
-import { getLanguage } from '@/i18n/language'
-type Props = {
-  icon: JSX.Element
-  collection: Collection
-  loc: LOC
-  onShowAuth: () => void
-  onShowEditCustomCollection: () => void
-}
-
-const Header: FC<Props> = ({
-  icon,
-  collection,
-  loc,
-  onShowAuth,
-  onShowEditCustomCollection,
-}) => {
-  const { locale } = useContext(I18n)
-  const language = getLanguage(locale)
-  const { t } = useTranslation()
-  const isInToolsPage = loc === LOC.tools
-  const isInDebugPage = !isInToolsPage
-
-  const needAuth = collection?.allow_delete || collection?.type === CollectionType.model
-  const isAuthed = collection.is_team_authorization
-  return (
-    <div className={cn(isInToolsPage ? 'py-4 px-6' : 'py-[11px] pl-4 pr-3', 'flex justify-between items-start border-b border-gray-200')}>
-      <div className='flex items-start w-full'>
-        {icon}
-        <div className='ml-3 grow w-0'>
-          <div className='flex items-center h-6 space-x-1'>
-            <div className={cn(isInDebugPage && 'truncate', 'text-base font-semibold text-gray-900')}>{collection.label[language]}</div>
-            <div className='text-xs font-normal text-gray-500'>·</div>
-            <div className='text-xs font-normal text-gray-500'>{t('tools.author')}&nbsp;{collection.author}</div>
-          </div>
-          {collection.description && (
-            <div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
-              {collection.description[language]}
-            </div>
-          )}
-        </div>
-      </div>
-      {(collection.type === CollectionType.builtIn || collection.type === CollectionType.model) && needAuth && (
-        <div
-          className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
-          onClick={() => {
-            if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model)
-              onShowAuth()
-          }}
-        >
-          <div className={cn(isAuthed ? 'border-[#12B76A] bg-[#32D583]' : 'border-gray-400 bg-gray-300', 'rounded h-2 w-2 border')}></div>
-          <div className='leading-5 text-sm font-medium text-gray-700'>{t(`tools.auth.${isAuthed ? 'authorized' : 'unauthorized'}`)}</div>
-        </div>
-      )}
-
-      {collection.type === CollectionType.custom && (
-        <div
-          className={cn('cursor-pointer', 'ml-1 shrink-0 flex items-center h-8 border border-gray-200 rounded-lg px-3 space-x-2 shadow-xs')}
-          onClick={() => onShowEditCustomCollection()}
-        >
-          <Settings01 className='w-4 h-4 text-gray-700' />
-          <div className='leading-5 text-sm font-medium text-gray-700'>{t('tools.createTool.editAction')}</div>
-        </div>
-      )}
-    </div >
-  )
-}
-export default React.memo(Header)

+ 0 - 220
web/app/components/tools/tool-list/index.tsx

@@ -1,220 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import cn from 'classnames'
-import { AuthHeaderPrefix, AuthType, CollectionType, LOC } from '../types'
-import type { Collection, CustomCollectionBackend, Tool } from '../types'
-import Loading from '../../base/loading'
-import { ArrowNarrowRight } from '../../base/icons/src/vender/line/arrows'
-import Toast from '../../base/toast'
-import { ConfigurateMethodEnum } from '../../header/account-setting/model-provider-page/declarations'
-import Header from './header'
-import Item from './item'
-import AppIcon from '@/app/components/base/app-icon'
-import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
-import { fetchCustomCollection, removeBuiltInToolCredential, removeCustomCollection, updateBuiltInToolCredential, updateCustomCollection } from '@/service/tools'
-import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
-import type { AgentTool } from '@/types/app'
-import { MAX_TOOLS_NUM } from '@/config'
-import { useModalContext } from '@/context/modal-context'
-import { useProviderContext } from '@/context/provider-context'
-
-type Props = {
-  collection: Collection | null
-  list: Tool[]
-  // onToolListChange: () => void // custom tools change
-  loc: LOC
-  addedTools?: AgentTool[]
-  onAddTool?: (collection: Collection, payload: Tool) => void
-  onRefreshData: () => void
-  onCollectionRemoved: () => void
-  isLoading: boolean
-}
-
-const ToolList: FC<Props> = ({
-  collection,
-  list,
-  loc,
-  addedTools,
-  onAddTool,
-  onRefreshData,
-  onCollectionRemoved,
-  isLoading,
-}) => {
-  const { t } = useTranslation()
-  const isInToolsPage = loc === LOC.tools
-  const isBuiltIn = collection?.type === CollectionType.builtIn
-  const isModel = collection?.type === CollectionType.model
-  const needAuth = collection?.allow_delete
-
-  const { setShowModelModal } = useModalContext()
-  const [showSettingAuth, setShowSettingAuth] = useState(false)
-  const { modelProviders: providers } = useProviderContext()
-  const showSettingAuthModal = () => {
-    if (isModel) {
-      const provider = providers.find(item => item.provider === collection?.id)
-      if (provider) {
-        setShowModelModal({
-          payload: {
-            currentProvider: provider,
-            currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
-            currentCustomConfigrationModelFixedFields: undefined,
-          },
-          onSaveCallback: () => {
-            onRefreshData()
-          },
-        })
-      }
-    }
-    else {
-      setShowSettingAuth(true)
-    }
-  }
-
-  const [customCollection, setCustomCollection] = useState<CustomCollectionBackend | null>(null)
-  useEffect(() => {
-    if (!collection)
-      return
-    (async () => {
-      if (collection.type === CollectionType.custom) {
-        const res = await fetchCustomCollection(collection.name)
-        if (res.credentials.auth_type === AuthType.apiKey && !res.credentials.api_key_header_prefix) {
-          if (res.credentials.api_key_value)
-            res.credentials.api_key_header_prefix = AuthHeaderPrefix.custom
-        }
-        setCustomCollection({
-          ...res,
-          provider: collection.name,
-        })
-      }
-    })()
-  }, [collection])
-  const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
-
-  const doUpdateCustomToolCollection = async (data: CustomCollectionBackend) => {
-    await updateCustomCollection(data)
-    onRefreshData()
-    Toast.notify({
-      type: 'success',
-      message: t('common.api.actionSuccess'),
-    })
-    setIsShowEditCustomCollectionModal(false)
-  }
-
-  const doRemoveCustomToolCollection = async () => {
-    await removeCustomCollection(collection?.name as string)
-    onCollectionRemoved()
-    Toast.notify({
-      type: 'success',
-      message: t('common.api.actionSuccess'),
-    })
-    setIsShowEditCustomCollectionModal(false)
-  }
-
-  if (!collection || isLoading)
-    return <Loading type='app' />
-
-  const icon = <>{typeof collection.icon === 'string'
-    ? (
-      <div
-        className='p-2 bg-cover bg-center border border-gray-100 rounded-lg'
-      >
-        <div className='w-6 h-6 bg-center bg-contain rounded-md'
-          style={{
-            backgroundImage: `url(${collection.icon})`,
-          }}
-        ></div>
-      </div>
-    )
-    : (
-      <AppIcon
-        size='large'
-        icon={collection.icon.content}
-        background={collection.icon.background}
-      />
-    )}
-  </>
-
-  return (
-    <div className='flex flex-col h-full pb-4'>
-      <Header
-        icon={icon}
-        collection={collection}
-        loc={loc}
-        onShowAuth={() => showSettingAuthModal()}
-        onShowEditCustomCollection={() => setIsShowEditCustomCollectionModal(true)}
-      />
-      <div className={cn(isInToolsPage ? 'px-6 pt-4' : 'px-4 pt-3')}>
-        <div className='flex items-center h-[4.5] space-x-2  text-xs font-medium text-gray-500'>
-          <div className=''>{t('tools.includeToolNum', {
-            num: list.length,
-          })}</div>
-          {needAuth && (isBuiltIn || isModel) && !collection.is_team_authorization && (
-            <>
-              <div>·</div>
-              <div
-                className='flex items-center text-[#155EEF] cursor-pointer'
-                onClick={() => showSettingAuthModal()}
-              >
-                <div>{t('tools.auth.setup')}</div>
-                <ArrowNarrowRight className='ml-0.5 w-3 h-3' />
-              </div>
-            </>
-          )}
-        </div>
-      </div>
-      <div className={cn(isInToolsPage ? 'px-6' : 'px-4', 'grow h-0 pt-2 overflow-y-auto')}>
-        {/* list */}
-        <div className={cn(isInToolsPage ? 'grid-cols-3 gap-4' : 'grid-cols-1 gap-2', 'grid')}>
-          {list.map(item => (
-            <Item
-              key={item.name}
-              icon={icon}
-              payload={item}
-              collection={collection}
-              isInToolsPage={isInToolsPage}
-              isToolNumMax={(addedTools?.length || 0) >= MAX_TOOLS_NUM}
-              added={!!addedTools?.find(v => v.provider_id === collection.id && v.provider_type === collection.type && v.tool_name === item.name)}
-              onAdd={!isInToolsPage ? tool => onAddTool?.(collection as Collection, tool) : undefined}
-            />
-          ))}
-        </div>
-      </div>
-      {showSettingAuth && (
-        <ConfigCredential
-          collection={collection}
-          onCancel={() => setShowSettingAuth(false)}
-          onSaved={async (value) => {
-            await updateBuiltInToolCredential(collection.name, value)
-            Toast.notify({
-              type: 'success',
-              message: t('common.api.actionSuccess'),
-            })
-            await onRefreshData()
-            setShowSettingAuth(false)
-          }}
-          onRemove={async () => {
-            await removeBuiltInToolCredential(collection.name)
-            Toast.notify({
-              type: 'success',
-              message: t('common.api.actionSuccess'),
-            })
-            await onRefreshData()
-            setShowSettingAuth(false)
-          }}
-        />
-      )}
-
-      {isShowEditCollectionToolModal && (
-        <EditCustomToolModal
-          payload={customCollection}
-          onHide={() => setIsShowEditCustomCollectionModal(false)}
-          onEdit={doUpdateCustomToolCollection}
-          onRemove={doRemoveCustomToolCollection}
-        />
-      )}
-    </div>
-  )
-}
-export default React.memo(ToolList)

+ 0 - 84
web/app/components/tools/tool-list/item.tsx

@@ -1,84 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useState } from 'react'
-import { useContext } from 'use-context-selector'
-import cn from 'classnames'
-import { useTranslation } from 'react-i18next'
-import type { Collection, Tool } from '../types'
-import Button from '../../base/button'
-import { CollectionType } from '../types'
-import TooltipPlus from '../../base/tooltip-plus'
-import I18n from '@/context/i18n'
-import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
-import { getLanguage } from '@/i18n/language'
-type Props = {
-  collection: Collection
-  icon: JSX.Element
-  payload: Tool
-  isInToolsPage: boolean
-  isToolNumMax: boolean
-  added?: boolean
-  onAdd?: (payload: Tool) => void
-}
-
-const Item: FC<Props> = ({
-  collection,
-  icon,
-  payload,
-  isInToolsPage,
-  isToolNumMax,
-  added,
-  onAdd,
-}) => {
-  const { t } = useTranslation()
-  const { locale } = useContext(I18n)
-  const language = getLanguage(locale)
-
-  const isBuiltIn = collection.type === CollectionType.builtIn
-  const isModel = collection.type === CollectionType.model
-  const canShowDetail = isInToolsPage
-  const [showDetail, setShowDetail] = useState(false)
-  const addBtn = <Button className='shrink-0 flex items-center h-7 !px-3 !text-xs !font-medium !text-gray-700' disabled={added || !collection.is_team_authorization} onClick={() => onAdd?.(payload)}>{t(`common.operation.${added ? 'added' : 'add'}`)}</Button>
-
-  return (
-    <>
-      <div
-        className={cn(canShowDetail && 'cursor-pointer', 'flex justify-between items-start p-4 rounded-xl border border-gray-200 bg-gray-50 shadow-xs')}
-        onClick={() => canShowDetail && setShowDetail(true)}
-      >
-        <div className='flex items-start w-full'>
-          {icon}
-          <div className='ml-3 w-0 grow'>
-            <div className={cn('text-base font-semibold text-gray-900 truncate')}>{payload.label[language]}</div>
-            <div className={cn('leading-[18px] text-[13px] font-normal text-gray-500')}>
-              {payload.description[language]}
-            </div>
-          </div>
-        </div>
-        <div className='shrink-0'>
-          {!isToolNumMax && onAdd && (
-            !collection.is_team_authorization
-              ? <TooltipPlus popupContent={t('tools.auth.unauthorized')}>
-                {addBtn}
-              </TooltipPlus>
-              : addBtn
-          )}
-        </div>
-      </div>
-      {showDetail && (
-        <SettingBuiltInTool
-          collection={collection}
-          toolName={payload.name}
-          readonly
-          onHide={() => {
-            setShowDetail(false)
-          }}
-          isBuiltIn={isBuiltIn}
-          isModel={isModel}
-        />
-      )}
-    </>
-
-  )
-}
-export default React.memo(Item)

+ 0 - 28
web/app/components/tools/tool-nav-list/index.tsx

@@ -1,28 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import cn from 'classnames'
-import Item from './item'
-import type { Collection } from '@/app/components/tools/types'
-type Props = {
-  className?: string
-  currentIndex: number
-  list: Collection[]
-  onChosen: (index: number) => void
-}
-
-const ToolNavList: FC<Props> = ({
-  className,
-  currentIndex,
-  list,
-  onChosen,
-}) => {
-  return (
-    <div className={cn(className)}>
-      {list.map((item, index) => (
-        <Item isCurrent={index === currentIndex} key={index} payload={item} onClick={() => onChosen(index)}></Item>
-      ))}
-    </div>
-  )
-}
-export default React.memo(ToolNavList)

+ 0 - 50
web/app/components/tools/tool-nav-list/item.tsx

@@ -1,50 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useContext } from 'use-context-selector'
-import cn from 'classnames'
-import AppIcon from '../../base/app-icon'
-import type { Collection } from '@/app/components/tools/types'
-import I18n from '@/context/i18n'
-import { getLanguage } from '@/i18n/language'
-
-type Props = {
-  isCurrent: boolean
-  payload: Collection
-  onClick: () => void
-}
-
-const Item: FC<Props> = ({
-  isCurrent,
-  payload,
-  onClick,
-}) => {
-  const { locale } = useContext(I18n)
-  const language = getLanguage(locale)
-  return (
-    <div
-      className={cn(isCurrent && 'bg-white shadow-xs rounded-lg', 'mt-1 flex h-9 items-center px-2 space-x-2 cursor-pointer')}
-      onClick={() => !isCurrent && onClick()}
-    >
-      {typeof payload.icon === 'string'
-        ? (
-          <div
-            className='w-6 h-6 bg-cover bg-center rounded-md'
-            style={{
-              backgroundImage: `url(${payload.icon})`,
-            }}
-          ></div>
-        )
-        : (
-          <AppIcon
-            size='tiny'
-            icon={payload.icon.content}
-            background={payload.icon.background}
-          />
-        )}
-      <div className={cn(isCurrent && 'text-primary-600 font-semibold', 'leading-5 text-sm font-normal truncate')}>{payload.label[language]}</div>
-
-    </div>
-  )
-}
-export default React.memo(Item)

+ 48 - 0
web/app/components/tools/types.ts

@@ -27,6 +27,7 @@ export enum CollectionType {
   builtIn = 'builtin',
   custom = 'api',
   model = 'model',
+  workflow = 'workflow',
 }
 
 export type Emoji = {
@@ -45,6 +46,7 @@ export type Collection = {
   team_credentials: Record<string, any>
   is_team_authorization: boolean
   allow_delete: boolean
+  labels: string[]
 }
 
 export type ToolParameter = {
@@ -52,19 +54,25 @@ export type ToolParameter = {
   label: TypeWithI18N
   human_description: TypeWithI18N
   type: string
+  form: string
+  llm_description: string
   required: boolean
   default: string
   options?: {
     label: TypeWithI18N
     value: string
   }[]
+  min?: number
+  max?: number
 }
 
 export type Tool = {
   name: string
+  author: string
   label: TypeWithI18N
   description: any
   parameters: ToolParameter[]
+  labels: string[]
 }
 
 export type ToolCredential = {
@@ -91,13 +99,17 @@ export type CustomCollectionBackend = {
   privacy_policy: string
   custom_disclaimer: string
   tools?: ParamItem[]
+  id: string
+  labels: string[]
 }
 
 export type ParamItem = {
   name: string
   label: TypeWithI18N
   human_description: TypeWithI18N
+  llm_description: string
   type: string
+  form: string
   required: boolean
   default: string
   min?: number
@@ -115,3 +127,39 @@ export type CustomParamSchema = {
   method: string
   parameters: ParamItem[]
 }
+
+export type WorkflowToolProviderParameter = {
+  name: string
+  form: string
+  description: string
+  required?: boolean
+  type?: string
+}
+
+export type WorkflowToolProviderRequest = {
+  name: string
+  icon: Emoji
+  description: string
+  parameters: WorkflowToolProviderParameter[]
+  labels: string[]
+  privacy_policy: string
+}
+
+export type WorkflowToolProviderResponse = {
+  workflow_app_id: string
+  workflow_tool_id: string
+  label: string
+  name: string
+  icon: Emoji
+  description: string
+  synced: boolean
+  tool: {
+    author: string
+    name: string
+    label: TypeWithI18N
+    description: TypeWithI18N
+    labels: string[]
+    parameters: ParamItem[]
+  }
+  privacy_policy: string
+}

+ 225 - 0
web/app/components/tools/workflow-tool/configure-button.tsx

@@ -0,0 +1,225 @@
+'use client'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useRouter } from 'next/navigation'
+import Button from '@/app/components/base/button'
+import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Tools } from '@/app/components/base/icons/src/vender/line/others'
+import Indicator from '@/app/components/header/indicator'
+import WorkflowToolModal from '@/app/components/tools/workflow-tool'
+import Loading from '@/app/components/base/loading'
+import Toast from '@/app/components/base/toast'
+import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools'
+import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
+import type { InputVar } from '@/app/components/workflow/types'
+
+type Props = {
+  disabled: boolean
+  published: boolean
+  detailNeedUpdate: boolean
+  workflowAppId: string
+  icon: Emoji
+  name: string
+  description: string
+  inputs?: InputVar[]
+  handlePublish: () => void
+  onRefreshData?: () => void
+}
+
+const WorkflowToolConfigureButton = ({
+  disabled,
+  published,
+  detailNeedUpdate,
+  workflowAppId,
+  icon,
+  name,
+  description,
+  inputs,
+  handlePublish,
+  onRefreshData,
+}: Props) => {
+  const { t } = useTranslation()
+  const router = useRouter()
+  const [showModal, setShowModal] = useState(false)
+  const [isLoading, setIsLoading] = useState(false)
+  const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
+
+  const outdated = useMemo(() => {
+    if (!detail)
+      return false
+    if (detail.tool.parameters.length !== inputs?.length) {
+      return true
+    }
+    else {
+      for (const item of inputs || []) {
+        const param = detail.tool.parameters.find(toolParam => toolParam.name === item.variable)
+        if (!param) {
+          return true
+        }
+        else if (param.required !== item.required) {
+          return true
+        }
+        else {
+          if (item.type === 'paragraph' && param.type !== 'string')
+            return true
+          if (param.type !== item.type && !(param.type === 'string' && item.type === 'paragraph'))
+            return true
+        }
+      }
+    }
+    return false
+  }, [detail, inputs])
+
+  const payload = useMemo(() => {
+    let parameters: WorkflowToolProviderParameter[] = []
+    if (!published) {
+      parameters = (inputs || []).map((item) => {
+        return {
+          name: item.variable,
+          description: '',
+          form: 'llm',
+          required: item.required,
+          type: item.type,
+        }
+      })
+    }
+    else if (detail && detail.tool) {
+      parameters = (inputs || []).map((item) => {
+        return {
+          name: item.variable,
+          required: item.required,
+          type: item.type === 'paragraph' ? 'string' : item.type,
+          description: detail.tool.parameters.find(param => param.name === item.variable)?.llm_description || '',
+          form: detail.tool.parameters.find(param => param.name === item.variable)?.form || 'llm',
+        }
+      })
+    }
+    return {
+      icon: detail?.icon || icon,
+      label: detail?.label || name,
+      name: detail?.name || '',
+      description: detail?.description || description,
+      parameters,
+      labels: detail?.tool?.labels || [],
+      privacy_policy: detail?.privacy_policy || '',
+      ...(published
+        ? {
+          workflow_tool_id: detail?.workflow_tool_id,
+        }
+        : {
+          workflow_app_id: workflowAppId,
+        }),
+    }
+  }, [detail, published, workflowAppId, icon, name, description, inputs])
+
+  const getDetail = useCallback(async (workflowAppId: string) => {
+    setIsLoading(true)
+    const res = await fetchWorkflowToolDetailByAppID(workflowAppId)
+    setDetail(res)
+    setIsLoading(false)
+  }, [])
+
+  useEffect(() => {
+    if (published)
+      getDetail(workflowAppId)
+  }, [getDetail, published, workflowAppId])
+
+  useEffect(() => {
+    if (detailNeedUpdate)
+      getDetail(workflowAppId)
+  }, [detailNeedUpdate, getDetail, workflowAppId])
+
+  const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
+    try {
+      await createWorkflowToolProvider(data)
+      onRefreshData?.()
+      getDetail(workflowAppId)
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.actionSuccess'),
+      })
+      setShowModal(false)
+    }
+    catch (e) {
+      Toast.notify({ type: 'error', message: (e as Error).message })
+    }
+  }
+
+  const updateWorkflowToolProvider = async (data: WorkflowToolProviderRequest & Partial<{
+    workflow_app_id: string
+    workflow_tool_id: string
+  }>) => {
+    try {
+      await handlePublish()
+      await saveWorkflowToolProvider(data)
+      onRefreshData?.()
+      getDetail(workflowAppId)
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.actionSuccess'),
+      })
+      setShowModal(false)
+    }
+    catch (e) {
+      Toast.notify({ type: 'error', message: (e as Error).message })
+    }
+  }
+
+  return (
+    <>
+      <div className='mt-2 pt-2 border-t-[0.5px] border-t-black/5'>
+        {(!published || !isLoading) && (
+          <div className={cn(
+            'group bg-gray-100 rounded-lg transition-colors',
+            disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer',
+            !published && 'hover:bg-primary-50',
+          )}>
+            <div
+              className='flex justify-start items-center gap-2 px-2.5 py-2'
+              onClick={() => !published && setShowModal(true)}
+            >
+              <Tools className={cn('relative w-4 h-4', !published && 'group-hover:text-primary-600')}/>
+              <div title={t('workflow.common.workflowAsTool') || ''} className={cn('grow shrink basis-0 text-[13px] font-medium leading-[18px] truncate', !published && 'group-hover:text-primary-600')}>{t('workflow.common.workflowAsTool')}</div>
+              {!published && (
+                <span className='shrink-0 px-1 border border-black/8 rounded-[5px] bg-white text-[10px] font-medium leading-[18px] text-gray-500'>{t('workflow.common.configureRequired').toLocaleUpperCase()}</span>
+              )}
+            </div>
+            {published && (
+              <div className='px-2.5 py-2 border-t-[0.5px] border-black/5'>
+                <div className='flex justify-between'>
+                  <Button
+                    className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200'
+                    onClick={() => setShowModal(true)}
+                  >
+                    {t('workflow.common.configure')}
+                    {outdated && <Indicator className='ml-1' color={'yellow'} />}
+                  </Button>
+                  <Button
+                    className='px-2 w-[140px] py-0 h-6 shadow-xs rounded-md text-xs font-medium text-gray-700 border-[0.5px] bg-white border-gray-200'
+                    onClick={() => router.push('/tools?category=workflow')}
+                  >
+                    {t('workflow.common.manageInTools')}
+                    <ArrowUpRight className='ml-1' />
+                  </Button>
+                </div>
+                {outdated && <div className='mt-1 text-xs leading-[18px] text-[#dc6803]'>{t('workflow.common.workflowAsToolTip')}</div>}
+              </div>
+            )}
+          </div>
+        )}
+        {published && isLoading && <div className='pt-2'><Loading type='app'/></div>}
+      </div>
+      {showModal && (
+        <WorkflowToolModal
+          isAdd={!published}
+          payload={payload}
+          onHide={() => setShowModal(false)}
+          onCreate={createHandle}
+          onSave={updateWorkflowToolProvider}
+        />
+      )}
+    </>
+  )
+}
+export default WorkflowToolConfigureButton

+ 47 - 0
web/app/components/tools/workflow-tool/confirm-modal/index.tsx

@@ -0,0 +1,47 @@
+'use client'
+
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import s from './style.module.css'
+import Button from '@/app/components/base/button'
+import Modal from '@/app/components/base/modal'
+import { XClose } from '@/app/components/base/icons/src/vender/line/general'
+import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
+
+type ConfirmModalProps = {
+  show: boolean
+  onConfirm?: () => void
+  onClose: () => void
+}
+
+const ConfirmModal = ({ show, onConfirm, onClose }: ConfirmModalProps) => {
+  const { t } = useTranslation()
+
+  return (
+    <Modal
+      wrapperClassName='!z-[1020]'
+      className={cn('p-8 max-w-[600px] w-[600px]', s.bg)}
+      isShow={show}
+      onClose={() => {}}
+    >
+      <div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
+        <XClose className='w-4 h-4 text-gray-500' />
+      </div>
+      <div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
+        <AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
+      </div>
+      <div className='relative mt-3 text-xl font-semibold leading-[30px] text-gray-900'>{t('tools.createTool.confirmTitle')}</div>
+      <div className='my-1 text-gray-500 text-sm leading-5'>
+        {t('tools.createTool.confirmTip')}
+      </div>
+      <div className='pt-6 flex justify-end items-center'>
+        <div className='flex items-center'>
+          <Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
+          <Button className='text-sm font-medium border-red-700 border-[0.5px]' type="warning" onClick={onConfirm}>{t('common.operation.confirm')}</Button>
+        </div>
+      </div>
+    </Modal>
+  )
+}
+
+export default ConfirmModal

+ 3 - 0
web/app/components/tools/workflow-tool/confirm-modal/style.module.css

@@ -0,0 +1,3 @@
+.bg {
+  background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
+}

+ 282 - 0
web/app/components/tools/workflow-tool/index.tsx

@@ -0,0 +1,282 @@
+'use client'
+import type { FC } from 'react'
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import produce from 'immer'
+import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types'
+import Drawer from '@/app/components/base/drawer-plus'
+import Button from '@/app/components/base/button'
+import Toast from '@/app/components/base/toast'
+import EmojiPicker from '@/app/components/base/emoji-picker'
+import AppIcon from '@/app/components/base/app-icon'
+import MethodSelector from '@/app/components/tools/workflow-tool/method-selector'
+import LabelSelector from '@/app/components/tools/labels/selector'
+import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal'
+import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
+import Tooltip from '@/app/components/base/tooltip'
+
+type Props = {
+  isAdd?: boolean
+  payload: any
+  onHide: () => void
+  onRemove?: () => void
+  onCreate?: (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => void
+  onSave?: (payload: WorkflowToolProviderRequest & Partial<{
+    workflow_app_id: string
+    workflow_tool_id: string
+  }>) => void
+}
+// Add and Edit
+const WorkflowToolAsModal: FC<Props> = ({
+  isAdd,
+  payload,
+  onHide,
+  onRemove,
+  onSave,
+  onCreate,
+}) => {
+  const { t } = useTranslation()
+
+  const [showEmojiPicker, setShowEmojiPicker] = useState<Boolean>(false)
+  const [emoji, setEmoji] = useState<Emoji>(payload.icon)
+  const [label, setLabel] = useState<string>(payload.label)
+  const [name, setName] = useState(payload.name)
+  const [description, setDescription] = useState(payload.description)
+  const [parameters, setParameters] = useState<WorkflowToolProviderParameter[]>(payload.parameters)
+  const handleParameterChange = (key: string, value: string, index: number) => {
+    const newData = produce(parameters, (draft: WorkflowToolProviderParameter[]) => {
+      if (key === 'description')
+        draft[index].description = value
+      else
+        draft[index].form = value
+    })
+    setParameters(newData)
+  }
+  const [labels, setLabels] = useState<string[]>(payload.labels)
+  const handleLabelSelect = (value: string[]) => {
+    setLabels(value)
+  }
+  const [privacyPolicy, setPrivacyPolicy] = useState(payload.privacy_policy)
+  const [showModal, setShowModal] = useState(false)
+
+  const isNameValid = (name: string) => {
+    return /^[a-zA-Z0-9_]+$/.test(name)
+  }
+
+  const onConfirm = () => {
+    if (!label) {
+      return Toast.notify({
+        type: 'error',
+        message: 'Please enter the tool name',
+      })
+    }
+    if (!name) {
+      return Toast.notify({
+        type: 'error',
+        message: 'Please enter the name for tool call',
+      })
+    }
+    else if (!isNameValid(name)) {
+      return Toast.notify({
+        type: 'error',
+        message: 'Name for tool call can only contain numbers, letters, and underscores',
+      })
+    }
+    const requestParams = {
+      name,
+      description,
+      icon: emoji,
+      label,
+      parameters: parameters.map(item => ({
+        name: item.name,
+        description: item.description,
+        form: item.form,
+      })),
+      labels,
+      privacy_policy: privacyPolicy,
+    }
+    if (!isAdd) {
+      onSave?.({
+        ...requestParams,
+        workflow_tool_id: payload.workflow_tool_id,
+      })
+    }
+    else {
+      onCreate?.({
+        ...requestParams,
+        workflow_app_id: payload.workflow_app_id,
+      })
+    }
+  }
+
+  return (
+    <>
+      <Drawer
+        isShow
+        onHide={onHide}
+        title={t('workflow.common.workflowAsTool')!}
+        panelClassName='mt-2 !w-[640px]'
+        maxWidthClassName='!max-w-[640px]'
+        height='calc(100vh - 16px)'
+        headerClassName='!border-b-black/5'
+        body={
+          <div className='flex flex-col h-full'>
+            <div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'>
+              {/* name & icon */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.name')}</div>
+                <div className='flex items-center justify-between gap-3'>
+                  <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} />
+                  <input
+                    type='text'
+                    className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
+                    placeholder={t('tools.createTool.toolNamePlaceHolder')!}
+                    value={label}
+                    onChange={e => setLabel(e.target.value)}
+                  />
+                </div>
+              </div>
+              {/* name for tool call */}
+              <div>
+                <div className='flex items-center py-2 leading-5 text-sm font-medium text-gray-900'>
+                  {t('tools.createTool.nameForToolCall')}
+                  <Tooltip
+                    htmlContent={
+                      <div className='w-[180px]'>
+                        {t('tools.createTool.nameForToolCallPlaceHolder')}
+                      </div>
+                    }
+                    selector='workflow-tool-modal-tooltip'
+                  >
+                    <HelpCircle className='ml-2 w-[14px] h-[14px] text-gray-400' />
+                  </Tooltip>
+                </div>
+                <input
+                  type='text'
+                  className='w-full h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
+                  placeholder={t('tools.createTool.nameForToolCallPlaceHolder')!}
+                  value={name}
+                  onChange={e => setName(e.target.value)}
+                />
+                {!isNameValid(name) && (
+                  <div className='text-xs leading-[18px] text-[#DC6803]'>{t('tools.createTool.nameForToolCallTip')}</div>
+                )}
+              </div>
+              {/* description */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.description')}</div>
+                <textarea
+                  className='w-full h-10 px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
+                  placeholder={t('tools.createTool.descriptionPlaceholder') || ''}
+                  value={description}
+                  onChange={e => setDescription(e.target.value)}
+                />
+              </div>
+              {/* Tool Input  */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.title')}</div>
+                <div className='rounded-lg border border-gray-200 w-full overflow-x-auto'>
+                  <table className='w-full leading-[18px] text-xs text-gray-700 font-normal'>
+                    <thead className='text-gray-500 uppercase'>
+                      <tr className='border-b border-gray-200'>
+                        <th className="p-2 pl-3 font-medium w-[156px]">{t('tools.createTool.toolInput.name')}</th>
+                        <th className="p-2 pl-3 font-medium w-[102px]">{t('tools.createTool.toolInput.method')}</th>
+                        <th className="p-2 pl-3 font-medium">{t('tools.createTool.toolInput.description')}</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      {parameters.map((item, index) => (
+                        <tr key={index} className='border-b last:border-0 border-gray-200'>
+                          <td className="p-2 pl-3 max-w-[156px]">
+                            <div className='text-[13px] leading-[18px]'>
+                              <div title={item.name} className='flex'>
+                                <span className='font-medium text-gray-900 truncate'>{item.name}</span>
+                                <span className='shrink-0 pl-1 text-[#ec4a0a] text-xs leading-[18px]'>{item.required ? t('tools.createTool.toolInput.required') : ''}</span>
+                              </div>
+                              <div className='text-gray-500'>{item.type}</div>
+                            </div>
+                          </td>
+                          <td>
+                            {item.name === '__image' && (
+                              <div className={cn(
+                                'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-default',
+                              )}>
+                                <div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
+                                  {t('tools.createTool.toolInput.methodParameter')}
+                                </div>
+                              </div>
+                            )}
+                            {item.name !== '__image' && (
+                              <MethodSelector value={item.form} onChange={value => handleParameterChange('form', value, index)}/>
+                            )}
+                          </td>
+                          <td className="p-2 pl-3 text-gray-500 w-[236px]">
+                            <input
+                              type='text'
+                              className='grow text-gray-700 text-[13px] leading-[18px] font-normal bg-white outline-none appearance-none caret-primary-600 placeholder:text-gray-300'
+                              placeholder={t('tools.createTool.toolInput.descriptionPlaceholder')!}
+                              value={item.description}
+                              onChange={e => handleParameterChange('description', e.target.value, index)}
+                            />
+                          </td>
+                        </tr>
+                      ))}
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+              {/* Tags */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div>
+                <LabelSelector value={labels} onChange={handleLabelSelect} />
+              </div>
+              {/* Privacy Policy */}
+              <div>
+                <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.privacyPolicy')}</div>
+                <input
+                  value={privacyPolicy}
+                  onChange={e => setPrivacyPolicy(e.target.value)}
+                  className='grow w-full h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} />
+              </div>
+            </div>
+            <div className={cn((!isAdd && onRemove) ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} >
+              {!isAdd && onRemove && (
+                <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onRemove}>{t('common.operation.remove')}</Button>
+              )}
+              <div className='flex space-x-2 '>
+                <Button className='flex items-center h-8 !px-3 !text-[13px] font-medium !text-gray-700' onClick={onHide}>{t('common.operation.cancel')}</Button>
+                <Button disabled={!label || !name || !isNameValid(name)} className='flex items-center h-8 !px-3 !text-[13px] font-medium' type='primary' onClick={() => {
+                  if (isAdd)
+                    onConfirm()
+                  else
+                    setShowModal(true)
+                }}>{t('common.operation.save')}</Button>
+              </div>
+            </div>
+          </div>
+        }
+        isShowMask={true}
+        clickOutsideNotOpen={true}
+      />
+      {showEmojiPicker && <EmojiPicker
+        onSelect={(icon, icon_background) => {
+          setEmoji({ content: icon, background: icon_background })
+          setShowEmojiPicker(false)
+        }}
+        onClose={() => {
+          setShowEmojiPicker(false)
+        }}
+      />}
+      {showModal && (
+        <ConfirmModal
+          show={showModal}
+          onClose={() => setShowModal(false)}
+          onConfirm={onConfirm}
+        />
+      )}
+    </>
+
+  )
+}
+export default React.memo(WorkflowToolAsModal)

+ 77 - 0
web/app/components/tools/workflow-tool/method-selector.tsx

@@ -0,0 +1,77 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Check } from '@/app/components/base/icons/src/vender/line/general'
+
+type MethodSelectorProps = {
+  value?: string
+  onChange: (v: string) => void
+}
+const MethodSelector: FC<MethodSelectorProps> = ({
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-start'
+      offset={4}
+    >
+      <div className='relative'>
+        <PortalToFollowElemTrigger
+          onClick={() => setOpen(v => !v)}
+          className='block'
+        >
+          <div className={cn(
+            'flex items-center gap-1 min-h-[56px] px-3 py-2 h-9 bg-white cursor-pointer hover:bg-gray-100',
+            open && '!bg-gray-100 hover:bg-gray-100',
+          )}>
+            <div className={cn('grow text-[13px] leading-[18px] text-gray-700 truncate')}>
+              {value === 'llm' ? t('tools.createTool.toolInput.methodParameter') : t('tools.createTool.toolInput.methodSetting')}
+            </div>
+            <div className='shrink-0 ml-1 text-gray-700 opacity-60'>
+              <ChevronDown className='h-4 w-4'/>
+            </div>
+          </div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent className='z-[1040]'>
+          <div className='relative w-[320px] bg-white rounded-lg border-[0.5px] border-gray-200  shadow-lg'>
+            <div className='p-1'>
+              <div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('llm')}>
+                <div className='flex item-center gap-1'>
+                  <div className='shrink-0 w-4 h-4'>
+                    {value === 'llm' && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
+                  </div>
+                  <div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodParameter')}</div>
+                </div>
+                <div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodParameterTip')}</div>
+              </div>
+              <div className='pl-3 pr-2 py-2.5 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={() => onChange('form')}>
+                <div className='flex item-center gap-1'>
+                  <div className='shrink-0 w-4 h-4'>
+                    {value === 'form' && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
+                  </div>
+                  <div className='text-[13px] text-gray-700 font-medium leading-[18px]'>{t('tools.createTool.toolInput.methodSetting')}</div>
+                </div>
+                <div className='pl-5 text-gray-500 text-[13px] leading-[18px]'>{t('tools.createTool.toolInput.methodSettingTip')}</div>
+              </div>
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </div>
+    </PortalToFollowElem>
+  )
+}
+
+export default MethodSelector

+ 8 - 0
web/app/components/workflow/block-icon.tsx

@@ -8,8 +8,10 @@ import {
   Home,
   Http,
   IfElse,
+  Iteration,
   KnowledgeRetrieval,
   Llm,
+  ParameterExtractor,
   QuestionClassifier,
   TemplatingTransform,
   VariableX,
@@ -40,7 +42,10 @@ const getIcon = (type: BlockEnum, className: string) => {
     [BlockEnum.QuestionClassifier]: <QuestionClassifier className={className} />,
     [BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
     [BlockEnum.VariableAssigner]: <VariableX className={className} />,
+    [BlockEnum.VariableAggregator]: <VariableX className={className} />,
     [BlockEnum.Tool]: <VariableX className={className} />,
+    [BlockEnum.Iteration]: <Iteration className={className} />,
+    [BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
   }[type]
 }
 const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
@@ -49,12 +54,15 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
   [BlockEnum.Code]: 'bg-[#2E90FA]',
   [BlockEnum.End]: 'bg-[#F79009]',
   [BlockEnum.IfElse]: 'bg-[#06AED4]',
+  [BlockEnum.Iteration]: 'bg-[#06AED4]',
   [BlockEnum.HttpRequest]: 'bg-[#875BF7]',
   [BlockEnum.Answer]: 'bg-[#F79009]',
   [BlockEnum.KnowledgeRetrieval]: 'bg-[#16B364]',
   [BlockEnum.QuestionClassifier]: 'bg-[#16B364]',
   [BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
   [BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
+  [BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
+  [BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
 }
 const BlockIcon: FC<BlockIconProps> = ({
   type,

+ 76 - 0
web/app/components/workflow/block-selector/all-tools.tsx

@@ -0,0 +1,76 @@
+import {
+  useMemo,
+  useState,
+} from 'react'
+import cn from 'classnames'
+import type {
+  OnSelectBlock,
+  ToolWithProvider,
+} from '../types'
+import { useStore } from '../store'
+import { ToolTypeEnum } from './types'
+import Tools from './tools'
+import { useToolTabs } from './hooks'
+import { useGetLanguage } from '@/context/i18n'
+
+type AllToolsProps = {
+  searchText: string
+  onSelect: OnSelectBlock
+}
+const AllTools = ({
+  searchText,
+  onSelect,
+}: AllToolsProps) => {
+  const language = useGetLanguage()
+  const tabs = useToolTabs()
+  const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
+  const buildInTools = useStore(s => s.buildInTools)
+  const customTools = useStore(s => s.customTools)
+  const workflowTools = useStore(s => s.workflowTools)
+
+  const tools = useMemo(() => {
+    let mergedTools: ToolWithProvider[] = []
+    if (activeTab === ToolTypeEnum.All)
+      mergedTools = [...buildInTools, ...customTools]
+    if (activeTab === ToolTypeEnum.BuiltIn)
+      mergedTools = buildInTools
+    if (activeTab === ToolTypeEnum.Custom)
+      mergedTools = customTools
+    if (activeTab === ToolTypeEnum.Workflow)
+      mergedTools = workflowTools
+
+    return mergedTools.filter((toolWithProvider) => {
+      return toolWithProvider.tools.some((tool) => {
+        return tool.label[language].toLowerCase().includes(searchText.toLowerCase())
+      })
+    })
+  }, [activeTab, buildInTools, customTools, workflowTools, searchText, language])
+  return (
+    <div>
+      <div className='flex items-center px-3 h-8 space-x-1 bg-gray-25 border-b-[0.5px] border-black/[0.08] shadow-xs'>
+        {
+          tabs.map(tab => (
+            <div
+              className={cn(
+                'flex items-center px-2 h-6 rounded-md hover:bg-gray-100 cursor-pointer',
+                'text-xs font-medium text-gray-700',
+                activeTab === tab.key && 'bg-gray-200',
+              )}
+              key={tab.key}
+              onClick={() => setActiveTab(tab.key)}
+            >
+              {tab.name}
+            </div>
+          ))
+        }
+      </div>
+      <Tools
+        showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
+        tools={tools}
+        onSelect={onSelect}
+      />
+    </div>
+  )
+}
+
+export default AllTools

+ 12 - 2
web/app/components/workflow/block-selector/constants.tsx

@@ -39,6 +39,11 @@ export const BLOCKS: Block[] = [
     type: BlockEnum.IfElse,
     title: 'IF/ELSE',
   },
+  {
+    classification: BlockClassificationEnum.Logic,
+    type: BlockEnum.Iteration,
+    title: 'Iteration',
+  },
   {
     classification: BlockClassificationEnum.Transform,
     type: BlockEnum.Code,
@@ -51,8 +56,13 @@ export const BLOCKS: Block[] = [
   },
   {
     classification: BlockClassificationEnum.Transform,
-    type: BlockEnum.VariableAssigner,
-    title: 'Variable Assigner',
+    type: BlockEnum.VariableAggregator,
+    title: 'Variable Aggregator',
+  },
+  {
+    classification: BlockClassificationEnum.Transform,
+    type: BlockEnum.ParameterExtractor,
+    title: 'Parameter Extractor',
   },
   {
     classification: BlockClassificationEnum.Utilities,

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