Explorar el Código

feat: add api-based extension & external data tool & moderation (#1459)

zxhlyh hace 1 año
padre
commit
32747641e4
Se han modificado 84 ficheros con 3275 adiciones y 166 borrados
  1. 7 4
      web/app/(commonLayout)/layout.tsx
  2. 17 2
      web/app/components/app/chat/answer/index.tsx
  3. 5 0
      web/app/components/app/chat/style.module.css
  4. 7 0
      web/app/components/app/chat/type.ts
  5. 40 1
      web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx
  6. 40 1
      web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
  7. 14 0
      web/app/components/app/configuration/config/feature/choose-feature/index.tsx
  8. 8 0
      web/app/components/app/configuration/config/feature/use-feature.tsx
  9. 39 2
      web/app/components/app/configuration/config/index.tsx
  10. 90 0
      web/app/components/app/configuration/dataset-config/card-item/item.tsx
  11. 10 3
      web/app/components/app/configuration/dataset-config/index.tsx
  12. 152 0
      web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
  13. 19 3
      web/app/components/app/configuration/debug/index.tsx
  14. 38 17
      web/app/components/app/configuration/index.tsx
  15. 4 2
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  16. 17 16
      web/app/components/app/configuration/toolbox/index.tsx
  17. 78 0
      web/app/components/app/configuration/toolbox/moderation/form-generation.tsx
  18. 81 0
      web/app/components/app/configuration/toolbox/moderation/index.tsx
  19. 72 0
      web/app/components/app/configuration/toolbox/moderation/moderation-content.tsx
  20. 362 0
      web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx
  21. 294 0
      web/app/components/app/configuration/tools/external-data-tool-modal.tsx
  22. 185 0
      web/app/components/app/configuration/tools/index.tsx
  23. 3 14
      web/app/components/app/overview/apikey-info-panel/index.tsx
  24. 1 0
      web/app/components/app/text-generate/item/index.tsx
  25. 12 0
      web/app/components/base/icons/assets/vender/line/development/webhooks.svg
  26. 6 0
      web/app/components/base/icons/assets/vender/line/education/book-open-01.svg
  27. 10 0
      web/app/components/base/icons/assets/vender/line/general/edit-02.svg
  28. 4 0
      web/app/components/base/icons/assets/vender/line/general/settings-01.svg
  29. 8 0
      web/app/components/base/icons/assets/vender/solid/files/file-search-02.svg
  30. 9 0
      web/app/components/base/icons/assets/vender/solid/general/tool-03.svg
  31. 89 0
      web/app/components/base/icons/src/vender/line/development/Webhooks.json
  32. 16 0
      web/app/components/base/icons/src/vender/line/development/Webhooks.tsx
  33. 1 0
      web/app/components/base/icons/src/vender/line/development/index.ts
  34. 49 0
      web/app/components/base/icons/src/vender/line/education/BookOpen01.json
  35. 16 0
      web/app/components/base/icons/src/vender/line/education/BookOpen01.tsx
  36. 1 0
      web/app/components/base/icons/src/vender/line/education/index.ts
  37. 66 0
      web/app/components/base/icons/src/vender/line/general/Edit02.json
  38. 16 0
      web/app/components/base/icons/src/vender/line/general/Edit02.tsx
  39. 44 0
      web/app/components/base/icons/src/vender/line/general/Settings01.json
  40. 16 0
      web/app/components/base/icons/src/vender/line/general/Settings01.tsx
  41. 2 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  42. 57 0
      web/app/components/base/icons/src/vender/solid/files/FileSearch02.json
  43. 16 0
      web/app/components/base/icons/src/vender/solid/files/FileSearch02.tsx
  44. 1 0
      web/app/components/base/icons/src/vender/solid/files/index.ts
  45. 62 0
      web/app/components/base/icons/src/vender/solid/general/Tool03.json
  46. 16 0
      web/app/components/base/icons/src/vender/solid/general/Tool03.tsx
  47. 1 0
      web/app/components/base/icons/src/vender/solid/general/index.ts
  48. 4 13
      web/app/components/base/notion-page-selector/base.tsx
  49. 8 2
      web/app/components/base/prompt-editor/index.tsx
  50. 100 5
      web/app/components/base/prompt-editor/plugins/variable-picker.tsx
  51. 4 11
      web/app/components/datasets/create/index.tsx
  52. 3 8
      web/app/components/datasets/settings/form/index.tsx
  53. 3 0
      web/app/components/datasets/settings/index-method-radio/index.tsx
  54. 1 0
      web/app/components/develop/template/template.en.mdx
  55. 1 0
      web/app/components/develop/template/template.zh.mdx
  56. 3 12
      web/app/components/explore/universal-chat/config/plugins-config/index.tsx
  57. 3 6
      web/app/components/header/account-dropdown/index.tsx
  58. 26 0
      web/app/components/header/account-setting/api-based-extension-page/empty.tsx
  59. 53 0
      web/app/components/header/account-setting/api-based-extension-page/index.tsx
  60. 75 0
      web/app/components/header/account-setting/api-based-extension-page/item.tsx
  61. 151 0
      web/app/components/header/account-setting/api-based-extension-page/modal.tsx
  62. 119 0
      web/app/components/header/account-setting/api-based-extension-page/selector.tsx
  63. 16 2
      web/app/components/header/account-setting/index.tsx
  64. 3 14
      web/app/components/header/account-setting/model-page/model-selector/index.tsx
  65. 17 1
      web/app/components/share/chat/index.tsx
  66. 12 1
      web/app/components/share/chatbot/index.tsx
  67. 2 1
      web/app/components/share/text-generation/index.tsx
  68. 8 1
      web/app/components/share/text-generation/result/index.tsx
  69. 29 1
      web/context/debug-configuration.ts
  70. 140 0
      web/context/modal-context.tsx
  71. 49 0
      web/hooks/use-moderate.ts
  72. 61 0
      web/i18n/lang/app-debug.en.ts
  73. 61 0
      web/i18n/lang/app-debug.zh.ts
  74. 36 2
      web/i18n/lang/common.en.ts
  75. 36 2
      web/i18n/lang/common.zh.ts
  76. 2 0
      web/i18n/lang/dataset-settings.en.ts
  77. 2 0
      web/i18n/lang/dataset-settings.zh.ts
  78. 58 1
      web/models/common.ts
  79. 15 0
      web/models/debug.ts
  80. 9 4
      web/service/base.ts
  81. 45 4
      web/service/common.ts
  82. 7 5
      web/service/debug.ts
  83. 7 5
      web/service/share.ts
  84. 5 0
      web/types/app.ts

+ 7 - 4
web/app/(commonLayout)/layout.tsx

@@ -7,6 +7,7 @@ import HeaderWrapper from '@/app/components/header/HeaderWrapper'
 import Header from '@/app/components/header'
 import { EventEmitterContextProvider } from '@/context/event-emitter'
 import { ProviderContextProvider } from '@/context/provider-context'
+import { ModalContextProvider } from '@/context/modal-context'
 
 const Layout = ({ children }: { children: ReactNode }) => {
   return (
@@ -16,10 +17,12 @@ const Layout = ({ children }: { children: ReactNode }) => {
         <AppContextProvider>
           <EventEmitterContextProvider>
             <ProviderContextProvider>
-              <HeaderWrapper>
-                <Header />
-              </HeaderWrapper>
-              {children}
+              <ModalContextProvider>
+                <HeaderWrapper>
+                  <Header />
+                </HeaderWrapper>
+                {children}
+              </ModalContextProvider>
             </ProviderContextProvider>
           </EventEmitterContextProvider>
         </AppContextProvider>

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

@@ -22,6 +22,7 @@ import { Markdown } from '@/app/components/base/markdown'
 import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
 import Button from '@/app/components/base/button'
 import type { DataSet } from '@/models/datasets'
+
 const Divider: FC<{ name: string }> = ({ name }) => {
   const { t } = useTranslation()
   return <div className='flex items-center my-2'>
@@ -53,7 +54,22 @@ export type IAnswerProps = {
   isShowCitationHitInfo?: boolean
 }
 // The component needs to maintain its own state to control whether to display input component
-const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing, answerIcon, thoughts, citation, isThinking, dataSets, isShowCitation, isShowCitationHitInfo = false }) => {
+const Answer: FC<IAnswerProps> = ({
+  item,
+  feedbackDisabled = false,
+  isHideFeedbackEdit = false,
+  onFeedback,
+  onSubmitAnnotation,
+  displayScene = 'web',
+  isResponsing,
+  answerIcon,
+  thoughts,
+  citation,
+  isThinking,
+  dataSets,
+  isShowCitation,
+  isShowCitationHitInfo = false,
+}) => {
   const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
   const [showEdit, setShowEdit] = useState(false)
   const [loading, setLoading] = useState(false)
@@ -62,7 +78,6 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
   const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
   const { userProfile } = useContext(AppContext)
   const { t } = useTranslation()
-
   /**
  * Render feedback results (distinguish between users and administrators)
  * User reviews cannot be cancelled in Console

+ 5 - 0
web/app/components/app/chat/style.module.css

@@ -59,6 +59,11 @@
   max-width: 100%;
 }
 
+.answer {
+  display: inline-block;
+  max-width: 100%;
+}
+
 .answerWrap:hover .copyBtn {
   display: block;
 }

+ 7 - 0
web/app/components/app/chat/type.ts

@@ -73,3 +73,10 @@ export type MessageEnd = {
   id: string
   retriever_resources?: CitationItem[]
 }
+
+export type MessageReplace = {
+  id: string
+  task_id: string
+  answer: string
+  conversation_id: string
+}

+ 40 - 1
web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx

@@ -19,6 +19,9 @@ import ConfigContext from '@/context/debug-configuration'
 import { getNewVar, getVars } from '@/utils/var'
 import { AppType } from '@/types/app'
 import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
+import { useModalContext } from '@/context/modal-context'
+import type { ExternalDataTool } from '@/models/common'
+import { useToastContext } from '@/app/components/base/toast'
 
 type Props = {
   type: PromptRole
@@ -56,7 +59,36 @@ const AdvancedPromptInput: FC<Props> = ({
     showHistoryModal,
     dataSets,
     showSelectDataSet,
+    externalDataToolsConfig,
+    setExternalDataToolsConfig,
   } = useContext(ConfigContext)
+  const { notify } = useToastContext()
+  const { setShowExternalDataToolModal } = useModalContext()
+  const handleOpenExternalDataToolModal = () => {
+    setShowExternalDataToolModal({
+      payload: {},
+      onSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+        setExternalDataToolsConfig([...externalDataToolsConfig, newExternalDataTool])
+      },
+      onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+        for (let i = 0; i < promptVariables.length; i++) {
+          if (promptVariables[i].key === newExternalDataTool.variable) {
+            notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: promptVariables[i].key }) })
+            return false
+          }
+        }
+
+        for (let i = 0; i < externalDataToolsConfig.length; i++) {
+          if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) {
+            notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: externalDataToolsConfig[i].variable }) })
+            return false
+          }
+        }
+
+        return true
+      },
+    })
+  }
   const isChatApp = mode === AppType.chat
   const [isCopied, setIsCopied] = React.useState(false)
 
@@ -76,7 +108,7 @@ const AdvancedPromptInput: FC<Props> = ({
   }
   const handleBlur = () => {
     const keys = getVars(value)
-    const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
+    const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.find(item => item.variable === key)).map(key => getNewVar(key))
     if (newPromptVariables.length > 0) {
       setNewPromptVariables(newPromptVariables)
       showConfirmAddVar()
@@ -174,6 +206,13 @@ const AdvancedPromptInput: FC<Props> = ({
                 name: item.name,
                 value: item.key,
               })),
+              externalTools: externalDataToolsConfig.map(item => ({
+                name: item.label!,
+                variableName: item.variable!,
+                icon: item.icon,
+                icon_background: item.icon_background,
+              })),
+              onAddExternalTool: handleOpenExternalDataToolModal,
             }}
             historyBlock={{
               show: !isChatMode && isChatApp,

+ 40 - 1
web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx

@@ -18,6 +18,9 @@ import type { AutomaticRes } from '@/service/debug'
 import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
 import PromptEditor from '@/app/components/base/prompt-editor'
 import ConfigContext from '@/context/debug-configuration'
+import { useModalContext } from '@/context/modal-context'
+import type { ExternalDataTool } from '@/models/common'
+import { useToastContext } from '@/app/components/base/toast'
 
 export type ISimplePromptInput = {
   mode: AppType
@@ -43,7 +46,36 @@ const Prompt: FC<ISimplePromptInput> = ({
     setIntroduction,
     hasSetBlockStatus,
     showSelectDataSet,
+    externalDataToolsConfig,
+    setExternalDataToolsConfig,
   } = useContext(ConfigContext)
+  const { notify } = useToastContext()
+  const { setShowExternalDataToolModal } = useModalContext()
+  const handleOpenExternalDataToolModal = () => {
+    setShowExternalDataToolModal({
+      payload: {},
+      onSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+        setExternalDataToolsConfig([...externalDataToolsConfig, newExternalDataTool])
+      },
+      onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
+        for (let i = 0; i < promptVariables.length; i++) {
+          if (promptVariables[i].key === newExternalDataTool.variable) {
+            notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: promptVariables[i].key }) })
+            return false
+          }
+        }
+
+        for (let i = 0; i < externalDataToolsConfig.length; i++) {
+          if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) {
+            notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: externalDataToolsConfig[i].variable }) })
+            return false
+          }
+        }
+
+        return true
+      },
+    })
+  }
   const promptVariablesObj = (() => {
     const obj: Record<string, boolean> = {}
     promptVariables.forEach((item) => {
@@ -57,7 +89,7 @@ const Prompt: FC<ISimplePromptInput> = ({
   const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
 
   const handleChange = (newTemplates: string, keys: string[]) => {
-    const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
+    const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.find(item => item.variable === key)).map(key => getNewVar(key))
     if (newPromptVariables.length > 0) {
       setNewPromptVariables(newPromptVariables)
       setNewTemplates(newTemplates)
@@ -127,6 +159,13 @@ const Prompt: FC<ISimplePromptInput> = ({
                 name: item.name,
                 value: item.key,
               })),
+              externalTools: externalDataToolsConfig.map(item => ({
+                name: item.label!,
+                variableName: item.variable!,
+                icon: item.icon,
+                icon_background: item.icon_background,
+              })),
+              onAddExternalTool: handleOpenExternalDataToolModal,
             }}
             historyBlock={{
               show: false,

+ 14 - 0
web/app/components/app/configuration/config/feature/choose-feature/index.tsx

@@ -9,12 +9,14 @@ import Modal from '@/app/components/base/modal'
 import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
 import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
 import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
+import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
 type IConfig = {
   openingStatement: boolean
   moreLikeThis: boolean
   suggestedQuestionsAfterAnswer: boolean
   speechToText: boolean
   citation: boolean
+  moderation: boolean
 }
 
 export type IChooseFeatureProps = {
@@ -114,6 +116,18 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
             </>
           </FeatureGroup>
         )}
+        <FeatureGroup title={t('appDebug.feature.toolbox.title')}>
+          <>
+            <FeatureItem
+              icon={<FileSearch02 className='w-4 h-4 text-[#039855]' />}
+              previewImgClassName=''
+              title={t('appDebug.feature.moderation.title')}
+              description={t('appDebug.feature.moderation.description')}
+              value={config.moderation}
+              onChange={value => onChange('moderation', value)}
+            />
+          </>
+        </FeatureGroup>
       </div>
 
     </Modal>

+ 8 - 0
web/app/components/app/configuration/config/feature/use-feature.tsx

@@ -11,6 +11,8 @@ function useFeature({
   setSpeechToText,
   citation,
   setCitation,
+  moderation,
+  setModeration,
 }: {
   introduction: string
   setIntroduction: (introduction: string) => void
@@ -22,6 +24,8 @@ function useFeature({
   setSpeechToText: (speechToText: boolean) => void
   citation: boolean
   setCitation: (citation: boolean) => void
+  moderation: boolean
+  setModeration: (moderation: boolean) => void
 }) {
   const [tempshowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
   useEffect(() => {
@@ -41,6 +45,7 @@ function useFeature({
     suggestedQuestionsAfterAnswer,
     speechToText,
     citation,
+    moderation,
   }
   const handleFeatureChange = (key: string, value: boolean) => {
     switch (key) {
@@ -61,6 +66,9 @@ function useFeature({
         break
       case 'citation':
         setCitation(value)
+        break
+      case 'moderation':
+        setModeration(value)
     }
   }
   return {

+ 39 - 2
web/app/components/app/configuration/config/index.tsx

@@ -5,6 +5,7 @@ import { useContext } from 'use-context-selector'
 import produce from 'immer'
 import { useBoolean, useScroll } from 'ahooks'
 import DatasetConfig from '../dataset-config'
+import Tools from '../tools'
 import ChatGroup from '../features/chat-group'
 import ExperienceEnchanceGroup from '../features/experience-enchance-group'
 import Toolbox from '../toolbox'
@@ -19,6 +20,8 @@ import ConfigVar from '@/app/components/app/configuration/config-var'
 import type { PromptVariable } from '@/models/debug'
 import { AppType, ModelModeType } from '@/types/app'
 import { useProviderContext } from '@/context/provider-context'
+import { useModalContext } from '@/context/modal-context'
+
 const Config: FC = () => {
   const {
     mode,
@@ -41,9 +44,12 @@ const Config: FC = () => {
     setSpeechToTextConfig,
     citationConfig,
     setCitationConfig,
+    moderationConfig,
+    setModerationConfig,
   } = useContext(ConfigContext)
   const isChatApp = mode === AppType.chat
   const { speech2textDefaultModel } = useProviderContext()
+  const { setShowModerationSettingModal } = useModalContext()
 
   const promptTemplate = modelConfig.configs.prompt_template
   const promptVariables = modelConfig.configs.prompt_variables
@@ -100,6 +106,35 @@ const Config: FC = () => {
         draft.enabled = value
       }))
     },
+    moderation: moderationConfig.enabled,
+    setModeration: (value) => {
+      setModerationConfig(produce(moderationConfig, (draft) => {
+        draft.enabled = value
+      }))
+      if (value && !moderationConfig.type) {
+        setShowModerationSettingModal({
+          payload: {
+            enabled: true,
+            type: 'keywords',
+            config: {
+              keywords: '',
+              inputs_config: {
+                enabled: true,
+                preset_response: '',
+              },
+            },
+          },
+          onSaveCallback: setModerationConfig,
+          onCancelCallback: () => {
+            setModerationConfig(produce(moderationConfig, (draft) => {
+              draft.enabled = false
+              showChooseFeatureTrue()
+            }))
+          },
+        })
+        showChooseFeatureFalse()
+      }
+    },
   })
 
   const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
@@ -156,6 +191,8 @@ const Config: FC = () => {
         {/* Dataset */}
         <DatasetConfig />
 
+        <Tools />
+
         {/* Chat History */}
         {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
           <HistoryPanel
@@ -189,8 +226,8 @@ const Config: FC = () => {
 
         {/* Toolbox */}
         {
-          hasToolbox && (
-            <Toolbox searchToolConfig={false} sensitiveWordAvoidanceConifg={false} />
+          moderationConfig.enabled && (
+            <Toolbox showModerationSettings />
           )
         }
       </div>

+ 90 - 0
web/app/components/app/configuration/dataset-config/card-item/item.tsx

@@ -0,0 +1,90 @@
+'use client'
+import type { FC } from 'react'
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import SettingsModal from '../settings-modal'
+import type { DataSet } from '@/models/datasets'
+import { DataSourceType } from '@/models/datasets'
+import { formatNumber } from '@/utils/format'
+import FileIcon from '@/app/components/base/file-icon'
+import { Settings01, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
+import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
+
+type ItemProps = {
+  className?: string
+  config: DataSet
+  onRemove: (id: string) => void
+  readonly?: boolean
+  onSave: (newDataset: DataSet) => void
+}
+
+const Item: FC<ItemProps> = ({
+  config,
+  onSave,
+  onRemove,
+}) => {
+  const { t } = useTranslation()
+  const [showSettingsModal, setShowSettingsModal] = useState(false)
+
+  const handleSave = (newDataset: DataSet) => {
+    onSave(newDataset)
+    setShowSettingsModal(false)
+  }
+
+  return (
+    <div className='group relative flex items-center mb-1 last-of-type:mb-0  pl-2.5 py-2 pr-3 w-full bg-white rounded-lg border-[0.5px] border-gray-200 shadow-xs'>
+      {
+        config.data_source_type === DataSourceType.FILE && (
+          <div className='shrink-0 flex items-center justify-center mr-2 w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]'>
+            <Folder className='w-4 h-4 text-[#444CE7]' />
+          </div>
+        )
+      }
+      {
+        config.data_source_type === DataSourceType.NOTION && (
+          <div className='shrink-0 flex items-center justify-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-[#EAECF5]'>
+            <FileIcon type='notion' className='w-4 h-4' />
+          </div>
+        )
+      }
+      <div className='grow'>
+        <div className='flex items-center h-[18px]'>
+          <div className='grow text-[13px] font-medium text-gray-800 truncate' title={config.name}>{config.name}</div>
+          <div className='shrink-0 text-xs text-gray-500'>
+            {formatNumber(config.word_count)} {t('appDebug.feature.dataSet.words')} · {formatNumber(config.document_count)} {t('appDebug.feature.dataSet.textBlocks')}
+          </div>
+        </div>
+        {/* {
+          config.description && (
+            <div className='text-xs text-gray-500'>{config.description}</div>
+          )
+        } */}
+      </div>
+      <div className='hidden group-hover:flex items-center justify-end absolute right-0 top-0 bottom-0 pr-2 w-[124px] bg-gradient-to-r from-white/50 to-white to-50%'>
+        <div
+          className='flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
+          onClick={() => setShowSettingsModal(true)}
+        >
+          <Settings01 className='w-4 h-4 text-gray-500' />
+        </div>
+        <div
+          className='group/action flex items-center justify-center w-6 h-6 hover:bg-[#FEE4E2] rounded-md cursor-pointer'
+          onClick={() => onRemove(config.id)}
+        >
+          <Trash03 className='w-4 h-4 text-gray-500 group-hover/action:text-[#D92D20]' />
+        </div>
+      </div>
+      {
+        showSettingsModal && (
+          <SettingsModal
+            currentDataset={config}
+            onCancel={() => setShowSettingsModal(false)}
+            onSave={handleSave}
+          />
+        )
+      }
+    </div>
+  )
+}
+
+export default Item

+ 10 - 3
web/app/components/app/configuration/dataset-config/index.tsx

@@ -6,11 +6,12 @@ import { useContext } from 'use-context-selector'
 import produce from 'immer'
 import FeaturePanel from '../base/feature-panel'
 import OperationBtn from '../base/operation-btn'
-import CardItem from './card-item'
+import CardItem from './card-item/item'
 import ParamsConfig from './params-config'
 import ContextVar from './context-var'
 import ConfigContext from '@/context/debug-configuration'
 import { AppType } from '@/types/app'
+import type { DataSet } from '@/models/datasets'
 
 const Icon = (
   <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -30,7 +31,6 @@ const DatasetConfig: FC = () => {
     setModelConfig,
     showSelectDataSet,
   } = useContext(ConfigContext)
-  const selectedIds = dataSet.map(item => item.id)
 
   const hasData = dataSet.length > 0
 
@@ -39,6 +39,13 @@ const DatasetConfig: FC = () => {
     setFormattingChanged(true)
   }
 
+  const handleSave = (newDataset: DataSet) => {
+    const index = dataSet.findIndex(item => item.id === newDataset.id)
+
+    setDataSet([...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)])
+    setFormattingChanged(true)
+  }
+
   const promptVariables = modelConfig.configs.prompt_variables
   const promptVariablesToSelect = promptVariables.map(item => ({
     name: item.name,
@@ -77,10 +84,10 @@ const DatasetConfig: FC = () => {
           <div className='flex flex-wrap mt-1 px-3 justify-between'>
             {dataSet.map(item => (
               <CardItem
-                className="mb-2"
                 key={item.id}
                 config={item}
                 onRemove={onRemove}
+                onSave={handleSave}
               />
             ))}
           </div>

+ 152 - 0
web/app/components/app/configuration/dataset-config/settings-modal/index.tsx

@@ -0,0 +1,152 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
+import type { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
+import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
+import type { DataSet } from '@/models/datasets'
+import { useToastContext } from '@/app/components/base/toast'
+import { updateDatasetSetting } from '@/service/datasets'
+import { useModalContext } from '@/context/modal-context'
+
+type SettingsModalProps = {
+  currentDataset: DataSet
+  onCancel: () => void
+  onSave: (newDataset: DataSet) => void
+}
+const SettingsModal: FC<SettingsModalProps> = ({
+  currentDataset,
+  onCancel,
+  onSave,
+}) => {
+  const { t } = useTranslation()
+  const { notify } = useToastContext()
+  const { setShowAccountSettingModal } = useModalContext()
+  const [loading, setLoading] = useState(false)
+  const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset })
+
+  const handleValueChange = (type: string, value: string) => {
+    setLocaleCurrentDataset({ ...localeCurrentDataset, [type]: value })
+  }
+
+  const handleSave = async () => {
+    if (loading)
+      return
+    if (!localeCurrentDataset.name?.trim()) {
+      notify({ type: 'error', message: t('datasetSettings.form.nameError') })
+      return
+    }
+    try {
+      setLoading(true)
+      const { id, name, description, indexing_technique } = localeCurrentDataset
+      await updateDatasetSetting({
+        datasetId: id,
+        body: {
+          name,
+          description,
+          indexing_technique,
+        },
+      })
+      notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
+      onSave(localeCurrentDataset)
+    }
+    catch (e) {
+      notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
+    }
+    finally {
+      setLoading(false)
+    }
+  }
+
+  return (
+    <Modal
+      isShow
+      onClose={() => {}}
+      className='!p-8 !pb-6 !max-w-none !w-[640px]'
+    >
+      <div className='mb-2 text-xl font-semibold text-gray-900'>
+        {t('datasetSettings.title')}
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('datasetSettings.form.name')}
+        </div>
+        <input
+          value={localeCurrentDataset.name}
+          onChange={e => handleValueChange('name', e.target.value)}
+          className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+          placeholder={t('datasetSettings.form.namePlaceholder') || ''}
+        />
+      </div>
+      <div className='py-2'>
+        <div className='flex justify-between items-center mb-1 h-5 text-sm font-medium text-gray-900'>
+          {t('datasetSettings.form.desc')}
+        </div>
+        <div className='mb-2 text-xs text-gray-500'>
+          {t('datasetSettings.form.descInfo')}<a href='/' className='text-primary-600'>{t('common.operation.learnMore')}</a>
+        </div>
+        <textarea
+          value={localeCurrentDataset.description || ''}
+          onChange={e => handleValueChange('description', e.target.value)}
+          className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
+          placeholder={t('datasetSettings.form.descPlaceholder') || ''}
+        />
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('datasetSettings.form.indexMethod')}
+        </div>
+        <div>
+          <IndexMethodRadio
+            disable={!localeCurrentDataset?.embedding_available}
+            value={localeCurrentDataset.indexing_technique}
+            onChange={v => handleValueChange('indexing_technique', v!)}
+            itemClassName='!w-[282px]'
+          />
+        </div>
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('datasetSettings.form.embeddingModel')}
+        </div>
+        <div className='w-full h-9 rounded-lg bg-gray-100 opacity-60'>
+          <ModelSelector
+            readonly
+            value={{
+              providerName: localeCurrentDataset.embedding_model_provider as ProviderEnum,
+              modelName: localeCurrentDataset.embedding_model,
+            }}
+            modelType={ModelType.embeddings}
+            onChange={() => {}}
+          />
+        </div>
+        <div className='mt-2 w-full text-xs leading-6 text-gray-500'>
+          {t('datasetSettings.form.embeddingModelTip')}
+          <span className='text-[#155eef] cursor-pointer' onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
+        </div>
+      </div>
+      <div></div>
+      <div className='flex items-center justify-end mt-6'>
+        <Button
+          onClick={onCancel}
+          className='mr-2 text-sm font-medium'
+        >
+          {t('common.operation.cancel')}
+        </Button>
+        <Button
+          type='primary'
+          className='text-sm font-medium'
+          disabled={loading}
+          onClick={handleSave}
+        >
+          {t('common.operation.save')}
+        </Button>
+      </div>
+    </Modal>
+  )
+}
+
+export default SettingsModal

+ 19 - 3
web/app/components/app/configuration/debug/index.tsx

@@ -24,14 +24,18 @@ import { promptVariablesToUserInputsForm } from '@/utils/model-config'
 import TextGeneration from '@/app/components/app/text-generate/item'
 import { IS_CE_EDITION } from '@/config'
 import { useProviderContext } from '@/context/provider-context'
+import type { Inputs } from '@/models/debug'
+
 type IDebug = {
   hasSetAPIKEY: boolean
   onSetting: () => void
+  inputs: Inputs
 }
 
 const Debug: FC<IDebug> = ({
   hasSetAPIKEY = true,
   onSetting,
+  inputs,
 }) => {
   const { t } = useTranslation()
   const {
@@ -47,9 +51,8 @@ const Debug: FC<IDebug> = ({
     suggestedQuestionsAfterAnswerConfig,
     speechToTextConfig,
     citationConfig,
+    moderationConfig,
     moreLikeThisConfig,
-    inputs,
-    // setInputs,
     formattingChanged,
     setFormattingChanged,
     conversationId,
@@ -60,6 +63,7 @@ const Debug: FC<IDebug> = ({
     completionParams,
     hasSetContextVar,
     datasetConfigs,
+    externalDataToolsConfig,
   } = useContext(ConfigContext)
   const { speech2textDefaultModel } = useProviderContext()
   const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
@@ -190,6 +194,8 @@ const Debug: FC<IDebug> = ({
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
       retriever_resource: citationConfig,
+      sensitive_word_avoidance: moderationConfig,
+      external_data_tools: externalDataToolsConfig,
       agent_mode: {
         enabled: true,
         tools: [...postDatasets],
@@ -319,6 +325,9 @@ const Debug: FC<IDebug> = ({
           })
         setChatList(newListWithAnswer)
       },
+      onMessageReplace: (messageReplace) => {
+        responseItem.content = messageReplace.answer
+      },
       onError() {
         setResponsingFalse()
         // role back placeholder answer
@@ -371,6 +380,8 @@ const Debug: FC<IDebug> = ({
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
       retriever_resource: citationConfig,
+      sensitive_word_avoidance: moderationConfig,
+      external_data_tools: externalDataToolsConfig,
       more_like_this: moreLikeThisConfig,
       agent_mode: {
         enabled: true,
@@ -397,7 +408,7 @@ const Debug: FC<IDebug> = ({
 
     setCompletionRes('')
     setMessageId('')
-    const res: string[] = []
+    let res: string[] = []
 
     setResponsingTrue()
     sendCompletionMessage(appId, data, {
@@ -406,6 +417,10 @@ const Debug: FC<IDebug> = ({
         setCompletionRes(res.join(''))
         setMessageId(messageId)
       },
+      onMessageReplace: (messageReplace) => {
+        res = [messageReplace.answer]
+        setCompletionRes(res.join(''))
+      },
       onCompleted() {
         setResponsingFalse()
       },
@@ -432,6 +447,7 @@ const Debug: FC<IDebug> = ({
         <PromptValuePanel
           appType={mode as AppType}
           onSend={sendTextCompletion}
+          inputs={inputs}
         />
       </div>
       <div className="flex flex-col grow">

+ 38 - 17
web/app/components/app/configuration/index.tsx

@@ -13,7 +13,17 @@ import Loading from '../../base/loading'
 import s from './style.module.css'
 import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
 import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
-import type { CompletionParams, DatasetConfigs, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
+import type {
+  CompletionParams,
+  DatasetConfigs,
+  Inputs,
+  ModelConfig,
+  ModerationConfig,
+  MoreLikeThisConfig,
+  PromptConfig,
+  PromptVariable,
+} from '@/models/debug'
+import type { ExternalDataTool } from '@/models/common'
 import type { DataSet } from '@/models/datasets'
 import type { ModelConfig as BackendModelConfig } from '@/types/app'
 import ConfigContext from '@/context/debug-configuration'
@@ -26,7 +36,6 @@ import { ToastContext } from '@/app/components/base/toast'
 import { fetchAppDetail, updateAppModelConfig } from '@/service/apps'
 import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from '@/utils/model-config'
 import { fetchDatasets } from '@/service/datasets'
-import AccountSetting from '@/app/components/header/account-setting'
 import { useProviderContext } from '@/context/provider-context'
 import { AppType, ModelModeType } from '@/types/app'
 import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
@@ -34,6 +43,7 @@ import { PromptMode } from '@/models/debug'
 import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
 import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
 import I18n from '@/context/i18n'
+import { useModalContext } from '@/context/modal-context'
 
 type PublichConfig = {
   modelConfig: ModelConfig
@@ -43,7 +53,7 @@ type PublichConfig = {
 const Configuration: FC = () => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
-
+  const { setShowAccountSettingModal } = useModalContext()
   const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
   const isLoading = !hasFetchedDetail
   const pathname = usePathname()
@@ -72,6 +82,10 @@ const Configuration: FC = () => {
   const [citationConfig, setCitationConfig] = useState<MoreLikeThisConfig>({
     enabled: false,
   })
+  const [moderationConfig, setModerationConfig] = useState<ModerationConfig>({
+    enabled: false,
+  })
+  const [externalDataToolsConfig, setExternalDataToolsConfig] = useState<ExternalDataTool[]>([])
   const [formattingChanged, setFormattingChanged] = useState(false)
   const [inputs, setInputs] = useState<Inputs>({})
   const [query, setQuery] = useState('')
@@ -108,6 +122,7 @@ const Configuration: FC = () => {
     suggested_questions_after_answer: null,
     speech_to_text: null,
     retriever_resource: null,
+    sensitive_word_avoidance: null,
     dataSets: [],
   })
 
@@ -214,7 +229,6 @@ const Configuration: FC = () => {
 
   const hasSetAPIKEY = hasSetCustomAPIKEY || !isTrailFinished
 
-  const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
   const [promptMode, doSetPromptMode] = useState(PromptMode.simple)
   const isAdvancedMode = promptMode === PromptMode.advanced
   const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
@@ -322,6 +336,12 @@ const Configuration: FC = () => {
       if (modelConfig.retriever_resource)
         setCitationConfig(modelConfig.retriever_resource)
 
+      if (modelConfig.sensitive_word_avoidance)
+        setModerationConfig(modelConfig.sensitive_word_avoidance)
+
+      if (modelConfig.external_data_tools)
+        setExternalDataToolsConfig(modelConfig.external_data_tools)
+
       const config = {
         modelConfig: {
           provider: model.provider,
@@ -336,6 +356,8 @@ const Configuration: FC = () => {
           suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
           speech_to_text: modelConfig.speech_to_text,
           retriever_resource: modelConfig.retriever_resource,
+          sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
+          external_data_tools: modelConfig.external_data_tools,
           dataSets: datasets || [],
         },
         completionParams: model.completion_params,
@@ -424,6 +446,8 @@ const Configuration: FC = () => {
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
       retriever_resource: citationConfig,
+      sensitive_word_avoidance: moderationConfig,
+      external_data_tools: externalDataToolsConfig,
       agent_mode: {
         enabled: true,
         tools: [...postDatasets],
@@ -469,7 +493,6 @@ const Configuration: FC = () => {
   }
 
   const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
-  const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
   const { locale } = useContext(I18n)
 
   if (isLoading) {
@@ -514,6 +537,10 @@ const Configuration: FC = () => {
       setSpeechToTextConfig,
       citationConfig,
       setCitationConfig,
+      moderationConfig,
+      setModerationConfig,
+      externalDataToolsConfig,
+      setExternalDataToolsConfig,
       formattingChanged,
       setFormattingChanged,
       inputs,
@@ -588,7 +615,11 @@ const Configuration: FC = () => {
               <Config />
             </div>
             <div className="relative w-1/2  grow h-full overflow-y-auto  py-4 px-6 bg-gray-50 flex flex-col rounded-tl-2xl border-t border-l" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
-              <Debug hasSetAPIKEY={hasSetAPIKEY} onSetting={showSetAPIKey} />
+              <Debug
+                hasSetAPIKEY={hasSetAPIKEY}
+                onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
+                inputs={inputs}
+              />
             </div>
           </div>
         </div>
@@ -609,22 +640,12 @@ const Configuration: FC = () => {
             isShow={showUseGPT4Confirm}
             onClose={() => setShowUseGPT4Confirm(false)}
             onConfirm={() => {
-              setShowSetAPIKeyModal(true)
+              setShowAccountSettingModal({ payload: 'provider' })
               setShowUseGPT4Confirm(false)
             }}
             onCancel={() => setShowUseGPT4Confirm(false)}
           />
         )}
-        {
-          showSetAPIKeyModal && (
-            <AccountSetting activeTab="provider" onCancel={async () => {
-              setShowSetAPIKeyModal(false)
-            }} />
-          )
-        }
-        {isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
-          hideSetAPIkey()
-        }} />}
 
         {isShowSelectDataSet && (
           <SelectDataSet

+ 4 - 2
web/app/components/app/configuration/prompt-value-panel/index.tsx

@@ -7,7 +7,7 @@ import {
   PlayIcon,
 } from '@heroicons/react/24/solid'
 import ConfigContext from '@/context/debug-configuration'
-import type { PromptVariable } from '@/models/debug'
+import type { Inputs, PromptVariable } from '@/models/debug'
 import { AppType, ModelModeType } from '@/types/app'
 import Select from '@/app/components/base/select'
 import { DEFAULT_VALUE_MAX_LEN } from '@/config'
@@ -18,14 +18,16 @@ import Tooltip from '@/app/components/base/tooltip-plus'
 export type IPromptValuePanelProps = {
   appType: AppType
   onSend?: () => void
+  inputs: Inputs
 }
 
 const PromptValuePanel: FC<IPromptValuePanelProps> = ({
   appType,
   onSend,
+  inputs,
 }) => {
   const { t } = useTranslation()
-  const { modelModeType, modelConfig, inputs, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
+  const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
   const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
   const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
     return key && key?.trim() && name && name?.trim()

+ 17 - 16
web/app/components/app/configuration/toolbox/index.tsx

@@ -1,25 +1,26 @@
 'use client'
-import React, { FC } from 'react'
+
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
 import GroupName from '../base/group-name'
+import Moderation from './moderation'
 
-export interface IToolboxProps {
-  searchToolConfig: any
-  sensitiveWordAvoidanceConifg: any
+export type ToolboxProps = {
+  showModerationSettings: boolean
 }
 
-/*
-* Include 
-* 1. Search Tool
-* 2. Sensitive word avoidance
-*/
-const Toolbox: FC<IToolboxProps> = ({ searchToolConfig, sensitiveWordAvoidanceConifg }) => {
+const Toolbox: FC<ToolboxProps> = ({ showModerationSettings }) => {
+  const { t } = useTranslation()
+
   return (
-    <div>
-      <GroupName name='Toolbox' />
-      <div>
-        {searchToolConfig?.enabled && <div>Search Tool</div>}
-        {sensitiveWordAvoidanceConifg?.enabled && <div>Sensitive word avoidance</div>}
-      </div>
+    <div className='mt-7'>
+      <GroupName name={t('appDebug.feature.toolbox.title')} />
+      {
+        showModerationSettings && (
+          <Moderation />
+        )
+      }
     </div>
   )
 }

+ 78 - 0
web/app/components/app/configuration/toolbox/moderation/form-generation.tsx

@@ -0,0 +1,78 @@
+import type { FC } from 'react'
+import { useContext } from 'use-context-selector'
+import type { CodeBasedExtensionForm } from '@/models/common'
+import I18n from '@/context/i18n'
+import { SimpleSelect } from '@/app/components/base/select'
+import type { ModerationConfig } from '@/models/debug'
+
+type FormGenerationProps = {
+  forms: CodeBasedExtensionForm[]
+  value: ModerationConfig['config']
+  onChange: (v: Record<string, string>) => void
+}
+const FormGeneration: FC<FormGenerationProps> = ({
+  forms,
+  value,
+  onChange,
+}) => {
+  const { locale } = useContext(I18n)
+
+  const handleFormChange = (type: string, v: string) => {
+    onChange({ ...value, [type]: v })
+  }
+
+  return (
+    <>
+      {
+        forms.map((form, index) => (
+          <div
+            key={index}
+            className='py-2'
+          >
+            <div className='flex items-center h-9 text-sm font-medium text-gray-900'>
+              {locale === 'zh-Hans' ? form.label['zh-Hans'] : form.label['en-US']}
+            </div>
+            {
+              form.type === 'text-input' && (
+                <input
+                  value={value?.[form.variable] || ''}
+                  className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+                  placeholder={form.placeholder}
+                  onChange={e => handleFormChange(form.variable, e.target.value)}
+                />
+              )
+            }
+            {
+              form.type === 'paragraph' && (
+                <div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
+                  <textarea
+                    value={value?.[form.variable] || ''}
+                    className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
+                    placeholder={form.placeholder}
+                    onChange={e => handleFormChange(form.variable, e.target.value)}
+                  />
+                </div>
+              )
+            }
+            {
+              form.type === 'select' && (
+                <SimpleSelect
+                  defaultValue={value?.[form.variable]}
+                  items={form.options.map((option) => {
+                    return {
+                      value: option,
+                      name: option,
+                    }
+                  })}
+                  onSelect={item => handleFormChange(form.variable, item.value as string)}
+                />
+              )
+            }
+          </div>
+        ))
+      }
+    </>
+  )
+}
+
+export default FormGeneration

+ 81 - 0
web/app/components/app/configuration/toolbox/moderation/index.tsx

@@ -0,0 +1,81 @@
+import { useTranslation } from 'react-i18next'
+import useSWR from 'swr'
+import { useContext } from 'use-context-selector'
+import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
+import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
+import { useModalContext } from '@/context/modal-context'
+import ConfigContext from '@/context/debug-configuration'
+import { fetchCodeBasedExtensionList } from '@/service/common'
+import I18n from '@/context/i18n'
+
+const Moderation = () => {
+  const { t } = useTranslation()
+  const { setShowModerationSettingModal } = useModalContext()
+  const { locale } = useContext(I18n)
+  const {
+    moderationConfig,
+    setModerationConfig,
+  } = useContext(ConfigContext)
+  const { data: codeBasedExtensionList } = useSWR(
+    '/code-based-extension?module=moderation',
+    fetchCodeBasedExtensionList,
+  )
+
+  const handleOpenModerationSettingModal = () => {
+    setShowModerationSettingModal({
+      payload: moderationConfig,
+      onSaveCallback: setModerationConfig,
+    })
+  }
+
+  const renderInfo = () => {
+    let prefix = ''
+    let suffix = ''
+    if (moderationConfig.type === 'openai_moderation')
+      prefix = t('appDebug.feature.moderation.modal.provider.openai')
+    else if (moderationConfig.type === 'keywords')
+      prefix = t('appDebug.feature.moderation.modal.provider.keywords')
+    else if (moderationConfig.type === 'api')
+      prefix = t('common.apiBasedExtension.selector.title')
+    else
+      prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale === 'en' ? 'en-US' : 'zh-Hans'] || ''
+
+    if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
+      suffix = t('appDebug.feature.moderation.allEnabled')
+    else if (moderationConfig.config?.inputs_config?.enabled)
+      suffix = t('appDebug.feature.moderation.inputEnabled')
+    else if (moderationConfig.config?.outputs_config?.enabled)
+      suffix = t('appDebug.feature.moderation.outputEnabled')
+
+    return `${prefix} · ${suffix}`
+  }
+
+  return (
+    <div className='flex items-center px-3 h-12 bg-gray-50 rounded-xl overflow-hidden'>
+      <div className='shrink-0 flex items-center justify-center mr-1 w-6 h-6'>
+        <FileSearch02 className='shrink-0 w-4 h-4 text-[#039855]' />
+      </div>
+      <div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
+        {t('appDebug.feature.moderation.title')}
+      </div>
+      <div
+        className='grow block w-0 text-right text-xs text-gray-500 truncate'
+        title={renderInfo()}>
+        {renderInfo()}
+      </div>
+      <div className='shrink-0 ml-4 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
+      <div
+        className={`
+          shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
+          text-xs text-gray-700 font-medium hover:bg-gray-200
+        `}
+        onClick={handleOpenModerationSettingModal}
+      >
+        <Settings01 className='mr-[5px] w-3.5 h-3.5' />
+        {t('common.operation.settings')}
+      </div>
+    </div>
+  )
+}
+
+export default Moderation

+ 72 - 0
web/app/components/app/configuration/toolbox/moderation/moderation-content.tsx

@@ -0,0 +1,72 @@
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import Switch from '@/app/components/base/switch'
+import type { ModerationContentConfig } from '@/models/debug'
+
+type ModerationContentProps = {
+  title: string
+  info?: string
+  showPreset?: boolean
+  config: ModerationContentConfig
+  onConfigChange: (config: ModerationContentConfig) => void
+}
+const ModerationContent: FC<ModerationContentProps> = ({
+  title,
+  info,
+  showPreset = true,
+  config,
+  onConfigChange,
+}) => {
+  const { t } = useTranslation()
+
+  const handleConfigChange = (field: string, value: boolean | string) => {
+    if (field === 'preset_response' && typeof value === 'string')
+      value = value.slice(0, 100)
+    onConfigChange({ ...config, [field]: value })
+  }
+
+  return (
+    <div className='py-2'>
+      <div className='rounded-lg bg-gray-50 border border-gray-200'>
+        <div className='flex items-center justify-between px-3 h-10 rounded-lg'>
+          <div className='shrink-0 text-sm font-medium text-gray-900'>{title}</div>
+          <div className='grow flex items-center justify-end'>
+            {
+              info && (
+                <div className='mr-2 text-xs text-gray-500 truncate' title={info}>{info}</div>
+              )
+            }
+            <Switch
+              size='l'
+              defaultValue={config.enabled}
+              onChange={v => handleConfigChange('enabled', v)}
+            />
+          </div>
+        </div>
+        {
+          config.enabled && showPreset && (
+            <div className='px-3 pt-1 pb-3 bg-white rounded-lg'>
+              <div className='flex items-center justify-between h-8 text-[13px] font-medium text-gray-700'>
+                {t('appDebug.feature.moderation.modal.content.preset')}
+                <span className='text-xs font-normal text-gray-500'>{t('appDebug.feature.moderation.modal.content.supportMarkdown')}</span>
+              </div>
+              <div className='relative px-3 py-2 h-20 rounded-lg bg-gray-100'>
+                <textarea
+                  value={config.preset_response || ''}
+                  className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
+                  placeholder={t('appDebug.feature.moderation.modal.content.placeholder') || ''}
+                  onChange={e => handleConfigChange('preset_response', e.target.value)}
+                />
+                <div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
+                  <span>{(config.preset_response || '').length}</span>/<span className='text-gray-500'>100</span>
+                </div>
+              </div>
+            </div>
+          )
+        }
+      </div>
+    </div>
+  )
+}
+
+export default ModerationContent

+ 362 - 0
web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx

@@ -0,0 +1,362 @@
+import type { ChangeEvent, FC } from 'react'
+import { useState } from 'react'
+import useSWR from 'swr'
+import { useContext } from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import ModerationContent from './moderation-content'
+import FormGeneration from './form-generation'
+import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
+import { useToastContext } from '@/app/components/base/toast'
+import {
+  fetchCodeBasedExtensionList,
+  fetchModelProviders,
+} from '@/service/common'
+import type { CodeBasedExtensionItem } from '@/models/common'
+import I18n from '@/context/i18n'
+import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
+import { useModalContext } from '@/context/modal-context'
+
+const systemTypes = ['openai_moderation', 'keywords', 'api']
+
+type Provider = {
+  key: string
+  name: string
+  form_schema?: CodeBasedExtensionItem['form_schema']
+}
+
+type ModerationSettingModalProps = {
+  data: ModerationConfig
+  onCancel: () => void
+  onSave: (moderationConfig: ModerationConfig) => void
+}
+
+const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
+  data,
+  onCancel,
+  onSave,
+}) => {
+  const { t } = useTranslation()
+  const { notify } = useToastContext()
+  const { locale } = useContext(I18n)
+  const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
+  const [localeData, setLocaleData] = useState<ModerationConfig>(data)
+  const { setShowAccountSettingModal } = useModalContext()
+  const handleOpenSettingsModal = () => {
+    setShowAccountSettingModal({
+      payload: 'provider',
+      onCancelCallback: () => {
+        mutate()
+      },
+    })
+  }
+  const { data: codeBasedExtensionList } = useSWR(
+    '/code-based-extension?module=moderation',
+    fetchCodeBasedExtensionList,
+  )
+  const systemOpenaiProvider = modelProviders?.openai.providers.find(item => item.provider_type === 'system')
+  const systemOpenaiProviderCanUse = systemOpenaiProvider && (((systemOpenaiProvider as any).quota_limit - (systemOpenaiProvider as any).quota_used) > 0)
+  const customOpenaiProviders = modelProviders?.openai.providers.filter(item => item.provider_type !== 'system')
+  const customOpenaiProvidersCanUse = customOpenaiProviders?.some(item => item.is_valid)
+  const openaiProviderConfiged = customOpenaiProvidersCanUse || systemOpenaiProviderCanUse
+  const providers: Provider[] = [
+    {
+      key: 'openai_moderation',
+      name: t('appDebug.feature.moderation.modal.provider.openai'),
+    },
+    {
+      key: 'keywords',
+      name: t('appDebug.feature.moderation.modal.provider.keywords'),
+    },
+    {
+      key: 'api',
+      name: t('common.apiBasedExtension.selector.title'),
+    },
+    ...(
+      codeBasedExtensionList
+        ? codeBasedExtensionList.data.map((item) => {
+          return {
+            key: item.name,
+            name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
+            form_schema: item.form_schema,
+          }
+        })
+        : []
+    ),
+  ]
+
+  const currentProvider = providers.find(provider => provider.key === localeData.type)
+
+  const handleDataTypeChange = (type: string) => {
+    setLocaleData({
+      ...localeData,
+      type,
+      config: undefined,
+    })
+  }
+
+  const handleDataKeywordsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
+    const value = e.target.value
+
+    const arr = value.split('\n').reduce((prev: string[], next: string) => {
+      if (next !== '')
+        prev.push(next.slice(0, 100))
+      if (next === '' && prev[prev.length - 1] !== '')
+        prev.push(next)
+
+      return prev
+    }, [])
+
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        keywords: arr.slice(0, 100).join('\n'),
+      },
+    })
+  }
+
+  const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        [contentType]: contentConfig,
+      },
+    })
+  }
+
+  const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        api_based_extension_id: apiBasedExtensionId,
+      },
+    })
+  }
+
+  const handleDataExtraChange = (extraValue: Record<string, string>) => {
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        ...extraValue,
+      },
+    })
+  }
+
+  const formatData = (originData: ModerationConfig) => {
+    const { enabled, type, config } = originData
+    const { inputs_config, outputs_config } = config!
+    const params: Record<string, string | undefined> = {}
+
+    if (type === 'keywords')
+      params.keywords = config?.keywords
+
+    if (type === 'api')
+      params.api_based_extension_id = config?.api_based_extension_id
+
+    if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
+      currentProvider.form_schema.forEach((form) => {
+        params[form.variable] = config?.[form.variable]
+      })
+    }
+
+    return {
+      type,
+      enabled,
+      config: {
+        inputs_config: inputs_config || { enabled: false },
+        outputs_config: outputs_config || { enabled: false },
+        ...params,
+      },
+    }
+  }
+
+  const handleSave = () => {
+    if (localeData.type === 'openai_moderation' && !openaiProviderConfiged)
+      return
+
+    if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
+      notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.condition') })
+      return
+    }
+
+    if (localeData.type === 'keywords' && !localeData.config.keywords) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale === 'en' ? 'keywords' : '关键词' }) })
+      return
+    }
+
+    if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale === 'en' ? 'API-based Extension' : '基于 API 的扩展' }) })
+      return
+    }
+
+    if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
+      for (let i = 0; i < currentProvider.form_schema.length; i++) {
+        if (!localeData.config?.[currentProvider.form_schema[i].variable]) {
+          notify({
+            type: 'error',
+            message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale === 'en' ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
+          })
+          return
+        }
+      }
+    }
+
+    if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
+      notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
+      return
+    }
+
+    if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
+      notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
+      return
+    }
+
+    onSave(formatData(localeData))
+  }
+
+  return (
+    <Modal
+      isShow
+      onClose={() => {}}
+      className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
+    >
+      <div className='mb-2 text-xl font-semibold text-[#1D2939]'>
+        {t('appDebug.feature.moderation.modal.title')}
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('appDebug.feature.moderation.modal.provider.title')}
+        </div>
+        <div className='grid gap-2.5 grid-cols-3'>
+          {
+            providers.map(provider => (
+              <div
+                key={provider.key}
+                className={`
+                  flex items-center px-3 py-2 rounded-lg text-sm text-gray-900 cursor-pointer
+                  ${localeData.type === provider.key ? 'bg-white border-[1.5px] border-primary-400 shadow-sm' : 'border border-gray-100 bg-gray-25'}
+                  ${localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !openaiProviderConfiged && 'opacity-50'}
+                `}
+                onClick={() => handleDataTypeChange(provider.key)}
+              >
+                <div className={`
+                  mr-2 w-4 h-4 rounded-full border 
+                  ${localeData.type === provider.key ? 'border-[5px] border-primary-600' : 'border border-gray-300'}`} />
+                {provider.name}
+              </div>
+            ))
+          }
+        </div>
+        {
+          !isLoading && !openaiProviderConfiged && localeData.type === 'openai_moderation' && (
+            <div className='flex items-center mt-2 px-3 py-2 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'>
+              <InfoCircle className='mr-1 w-4 h-4 text-[#F79009]' />
+              <div className='flex items-center text-xs font-medium text-gray-700'>
+                {t('appDebug.feature.moderation.modal.openaiNotConfig.before')}
+                <span
+                  className='text-primary-600 cursor-pointer'
+                  onClick={handleOpenSettingsModal}
+                >
+                  &nbsp;{t('common.settings.provider')}&nbsp;
+                </span>
+                {t('appDebug.feature.moderation.modal.openaiNotConfig.after')}
+              </div>
+            </div>
+          )
+        }
+      </div>
+      {
+        localeData.type === 'keywords' && (
+          <div className='py-2'>
+            <div className='mb-1 text-sm font-medium text-gray-900'>{t('appDebug.feature.moderation.modal.provider.keywords')}</div>
+            <div className='mb-2 text-xs text-gray-500'>{t('appDebug.feature.moderation.modal.keywords.tip')}</div>
+            <div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
+              <textarea
+                value={localeData.config?.keywords || ''}
+                onChange={handleDataKeywordsChange}
+                className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
+                placeholder={t('appDebug.feature.moderation.modal.keywords.placeholder') || ''}
+              />
+              <div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
+                <span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>/<span className='text-gray-500'>100 {t('appDebug.feature.moderation.modal.keywords.line')}</span>
+              </div>
+            </div>
+          </div>
+        )
+      }
+      {
+        localeData.type === 'api' && (
+          <div className='py-2'>
+            <div className='flex items-center justify-between h-9'>
+              <div className='text-sm font-medium text-gray-900'>{t('common.apiBasedExtension.selector.title')}</div>
+              <a
+                href={t('common.apiBasedExtension.linkUrl') || '/'}
+                target='_blank'
+                className='group flex items-center text-xs text-gray-500 hover:text-primary-600'
+              >
+                <BookOpen01 className='mr-1 w-3 h-3 text-gray-500 group-hover:text-primary-600' />
+                {t('common.apiBasedExtension.link')}
+              </a>
+            </div>
+            <ApiBasedExtensionSelector
+              value={localeData.config?.api_based_extension_id || ''}
+              onChange={handleDataApiBasedChange}
+            />
+          </div>
+        )
+      }
+      {
+        systemTypes.findIndex(t => t === localeData.type) < 0
+        && currentProvider?.form_schema
+        && (
+          <FormGeneration
+            forms={currentProvider?.form_schema}
+            value={localeData.config}
+            onChange={handleDataExtraChange}
+          />
+        )
+      }
+      <div className='my-3 h-[1px] bg-gradient-to-r from-[#F3F4F6]'></div>
+      <ModerationContent
+        title={t('appDebug.feature.moderation.modal.content.input') || ''}
+        config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
+        onConfigChange={config => handleDataContentChange('inputs_config', config)}
+        info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
+        showPreset={!(localeData.type === 'api')}
+      />
+      <ModerationContent
+        title={t('appDebug.feature.moderation.modal.content.output') || ''}
+        config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
+        onConfigChange={config => handleDataContentChange('outputs_config', config)}
+        info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
+        showPreset={!(localeData.type === 'api')}
+      />
+      <div className='mt-1 mb-8 text-xs font-medium text-gray-500'>{t('appDebug.feature.moderation.modal.content.condition')}</div>
+      <div className='flex items-center justify-end'>
+        <Button
+          onClick={onCancel}
+          className='mr-2 text-sm font-medium'
+        >
+          {t('common.operation.cancel')}
+        </Button>
+        <Button
+          type='primary'
+          className='text-sm font-medium'
+          onClick={handleSave}
+          disabled={localeData.type === 'openai_moderation' && !openaiProviderConfiged}
+        >
+          {t('common.operation.save')}
+        </Button>
+      </div>
+    </Modal>
+  )
+}
+
+export default ModerationSettingModal

+ 294 - 0
web/app/components/app/configuration/tools/external-data-tool-modal.tsx

@@ -0,0 +1,294 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import useSWR from 'swr'
+import { useContext } from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import FormGeneration from '../toolbox/moderation/form-generation'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import AppIcon from '@/app/components/base/app-icon'
+import EmojiPicker from '@/app/components/base/emoji-picker'
+import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+import { fetchCodeBasedExtensionList } from '@/service/common'
+import { SimpleSelect } from '@/app/components/base/select'
+import I18n from '@/context/i18n'
+import type {
+  CodeBasedExtensionItem,
+  ExternalDataTool,
+} from '@/models/common'
+import { useToastContext } from '@/app/components/base/toast'
+
+const systemTypes = ['api']
+type ExternalDataToolModalProps = {
+  data: ExternalDataTool
+  onCancel: () => void
+  onSave: (externalDataTool: ExternalDataTool) => void
+  onValidateBeforeSave?: (externalDataTool: ExternalDataTool) => boolean
+}
+type Provider = {
+  key: string
+  name: string
+  form_schema?: CodeBasedExtensionItem['form_schema']
+}
+const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
+  data,
+  onCancel,
+  onSave,
+  onValidateBeforeSave,
+}) => {
+  const { t } = useTranslation()
+  const { notify } = useToastContext()
+  const { locale } = useContext(I18n)
+  const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
+  const [showEmojiPicker, setShowEmojiPicker] = useState(false)
+  const { data: codeBasedExtensionList } = useSWR(
+    '/code-based-extension?module=external_data_tool',
+    fetchCodeBasedExtensionList,
+  )
+
+  const providers: Provider[] = [
+    {
+      key: 'api',
+      name: t('common.apiBasedExtension.selector.title'),
+    },
+    ...(
+      codeBasedExtensionList
+        ? codeBasedExtensionList.data.map((item) => {
+          return {
+            key: item.name,
+            name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
+            form_schema: item.form_schema,
+          }
+        })
+        : []
+    ),
+  ]
+  const currentProvider = providers.find(provider => provider.key === localeData.type)
+
+  const handleDataTypeChange = (type: string) => {
+    setLocaleData({
+      ...localeData,
+      type,
+      config: undefined,
+    })
+  }
+
+  const handleDataExtraChange = (extraValue: Record<string, string>) => {
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        ...extraValue,
+      },
+    })
+  }
+
+  const handleValueChange = (value: Record<string, string>) => {
+    setLocaleData({
+      ...localeData,
+      ...value,
+    })
+  }
+
+  const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
+    setLocaleData({
+      ...localeData,
+      config: {
+        ...localeData.config,
+        api_based_extension_id: apiBasedExtensionId,
+      },
+    })
+  }
+
+  const formatData = (originData: ExternalDataTool) => {
+    const { type, config } = originData
+    const params: Record<string, string | undefined> = {}
+
+    if (type === 'api')
+      params.api_based_extension_id = config?.api_based_extension_id
+
+    if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
+      currentProvider.form_schema.forEach((form) => {
+        params[form.variable] = config?.[form.variable]
+      })
+    }
+
+    return {
+      ...originData,
+      type,
+      enabled: data.type ? data.enabled : true,
+      config: {
+        ...params,
+      },
+    }
+  }
+
+  const handleSave = () => {
+    if (!localeData.type) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: t('appDebug.feature.tools.modal.toolType.title') }) })
+      return
+    }
+
+    if (!localeData.label) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: t('appDebug.feature.tools.modal.name.title') }) })
+      return
+    }
+
+    if (!localeData.variable) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: t('appDebug.feature.tools.modal.variableName.title') }) })
+      return
+    }
+
+    if (localeData.variable && !/[a-zA-Z_][a-zA-Z0-9_]{0,29}/g.test(localeData.variable)) {
+      notify({ type: 'error', message: t('appDebug.varKeyError.notValid', { key: t('appDebug.feature.tools.modal.variableName.title') }) })
+      return
+    }
+
+    if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale === 'en' ? 'API-based Extension' : '基于 API 的扩展' }) })
+      return
+    }
+
+    if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
+      for (let i = 0; i < currentProvider.form_schema.length; i++) {
+        if (!localeData.config?.[currentProvider.form_schema[i].variable]) {
+          notify({
+            type: 'error',
+            message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale === 'en' ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
+          })
+          return
+        }
+      }
+    }
+
+    const formatedData = formatData(localeData)
+
+    if (onValidateBeforeSave && !onValidateBeforeSave(formatedData))
+      return
+
+    onSave(formatData(formatedData))
+  }
+
+  const action = data.type ? t('common.operation.edit') : t('common.operation.add')
+
+  return (
+    <Modal
+      isShow
+      onClose={() => {}}
+      className='!p-8 !pb-6 !max-w-none !w-[640px]'
+    >
+      <div className='mb-2 text-xl font-semibold text-gray-900'>
+        {`${action} ${t('appDebug.feature.tools.modal.title')}`}
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('appDebug.feature.tools.modal.toolType.title')}
+        </div>
+        <SimpleSelect
+          defaultValue={localeData.type}
+          items={providers.map((option) => {
+            return {
+              value: option.key,
+              name: option.name,
+            }
+          })}
+          onSelect={item => handleDataTypeChange(item.value as string)}
+        />
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('appDebug.feature.tools.modal.name.title')}
+        </div>
+        <div className='flex items-center'>
+          <input
+            value={localeData.label || ''}
+            onChange={e => handleValueChange({ label: e.target.value })}
+            className='grow block mr-2 px-3 h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+            placeholder={t('appDebug.feature.tools.modal.name.placeholder') || ''}
+          />
+          <AppIcon size='large'
+            onClick={() => { setShowEmojiPicker(true) }}
+            className='!w-9 !h-9 rounded-lg border-[0.5px] border-black/5 cursor-pointer '
+            icon={localeData.icon}
+            background={localeData.icon_background}
+          />
+        </div>
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('appDebug.feature.tools.modal.variableName.title')}
+        </div>
+        <input
+          value={localeData.variable || ''}
+          onChange={e => handleValueChange({ variable: e.target.value })}
+          className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+          placeholder={t('appDebug.feature.tools.modal.variableName.placeholder') || ''}
+        />
+      </div>
+      {
+        localeData.type === 'api' && (
+          <div className='py-2'>
+            <div className='flex justify-between items-center h-9 text-sm font-medium text-gray-900'>
+              {t('common.apiBasedExtension.selector.title')}
+              <a
+                href={t('common.apiBasedExtension.linkUrl') || '/'}
+                target='_blank'
+                className='group flex items-center text-xs font-normal text-gray-500 hover:text-primary-600'
+              >
+                <BookOpen01 className='mr-1 w-3 h-3 text-gray-500 group-hover:text-primary-600' />
+                {t('common.apiBasedExtension.link')}
+              </a>
+            </div>
+            <ApiBasedExtensionSelector
+              value={localeData.config?.api_based_extension_id || ''}
+              onChange={handleDataApiBasedChange}
+            />
+          </div>
+        )
+      }
+      {
+        systemTypes.findIndex(t => t === localeData.type) < 0
+        && currentProvider?.form_schema
+        && (
+          <FormGeneration
+            forms={currentProvider?.form_schema}
+            value={localeData.config}
+            onChange={handleDataExtraChange}
+          />
+        )
+      }
+      <div className='flex items-center justify-end mt-6'>
+        <Button
+          onClick={onCancel}
+          className='mr-2 text-sm font-medium'
+        >
+          {t('common.operation.cancel')}
+        </Button>
+        <Button
+          type='primary'
+          className='text-sm font-medium'
+          onClick={handleSave}
+        >
+          {t('common.operation.save')}
+        </Button>
+      </div>
+      {
+        showEmojiPicker && (
+          <EmojiPicker
+            onSelect={(icon, icon_background) => {
+              handleValueChange({ icon, icon_background })
+              setShowEmojiPicker(false)
+            }}
+            onClose={() => {
+              handleValueChange({ icon: '', icon_background: '' })
+              setShowEmojiPicker(false)
+            }}
+          />
+        )
+      }
+    </Modal>
+  )
+}
+
+export default ExternalDataToolModal

+ 185 - 0
web/app/components/app/configuration/tools/index.tsx

@@ -0,0 +1,185 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import copy from 'copy-to-clipboard'
+import { useContext } from 'use-context-selector'
+import ConfigContext from '@/context/debug-configuration'
+import Switch from '@/app/components/base/switch'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
+import {
+  HelpCircle,
+  Plus,
+  Settings01,
+  Trash03,
+} from '@/app/components/base/icons/src/vender/line/general'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { useModalContext } from '@/context/modal-context'
+import type { ExternalDataTool } from '@/models/common'
+import AppIcon from '@/app/components/base/app-icon'
+import { useToastContext } from '@/app/components/base/toast'
+
+const Tools = () => {
+  const { t } = useTranslation()
+  const { notify } = useToastContext()
+  const { setShowExternalDataToolModal } = useModalContext()
+  const {
+    externalDataToolsConfig,
+    setExternalDataToolsConfig,
+    modelConfig,
+  } = useContext(ConfigContext)
+  const [expanded, setExpanded] = useState(true)
+  const [copied, setCopied] = useState(false)
+
+  const handleSaveExternalDataToolModal = (externalDataTool: ExternalDataTool, index: number) => {
+    if (index > -1) {
+      setExternalDataToolsConfig([
+        ...externalDataToolsConfig.slice(0, index),
+        externalDataTool,
+        ...externalDataToolsConfig.slice(index + 1),
+      ])
+    }
+    else {
+      setExternalDataToolsConfig([...externalDataToolsConfig, externalDataTool])
+    }
+  }
+  const handleValidateBeforeSaveExternalDataToolModal = (newExternalDataTool: ExternalDataTool, index: number) => {
+    const promptVariables = modelConfig?.configs?.prompt_variables || []
+    for (let i = 0; i < promptVariables.length; i++) {
+      if (promptVariables[i].key === newExternalDataTool.variable) {
+        notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: promptVariables[i].key }) })
+        return false
+      }
+    }
+
+    let existedExternalDataTools = []
+    if (index > -1) {
+      existedExternalDataTools = [
+        ...externalDataToolsConfig.slice(0, index),
+        ...externalDataToolsConfig.slice(index + 1),
+      ]
+    }
+    else {
+      existedExternalDataTools = [...externalDataToolsConfig]
+    }
+
+    for (let i = 0; i < existedExternalDataTools.length; i++) {
+      if (existedExternalDataTools[i].variable === newExternalDataTool.variable) {
+        notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: existedExternalDataTools[i].variable }) })
+        return false
+      }
+    }
+
+    return true
+  }
+  const handleOpenExternalDataToolModal = (payload: ExternalDataTool, index: number) => {
+    setShowExternalDataToolModal({
+      payload,
+      onSaveCallback: (externalDataTool: ExternalDataTool) => handleSaveExternalDataToolModal(externalDataTool, index),
+      onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => handleValidateBeforeSaveExternalDataToolModal(newExternalDataTool, index),
+    })
+  }
+
+  return (
+    <div className='mt-3 px-3 rounded-xl bg-gray-50'>
+      <div className='flex items-center h-12'>
+        <div className='grow flex items-center'>
+          <div
+            className={`
+              group flex items-center justify-center mr-1 w-6 h-6 rounded-md 
+              ${externalDataToolsConfig.length && 'hover:shadow-xs hover:bg-white'}
+            `}
+            onClick={() => setExpanded(v => !v)}
+          >
+            {
+              externalDataToolsConfig.length
+                ? <Tool03 className='group-hover:hidden w-4 h-4 text-[#444CE7]' />
+                : <Tool03 className='w-4 h-4 text-[#444CE7]' />
+            }
+            {
+              !!externalDataToolsConfig.length && (
+                <ChevronDown className={`hidden group-hover:block w-4 h-4 text-primary-600 cursor-pointer ${expanded ? 'rotate-180' : 'rotate-0'}`} />
+              )
+            }
+          </div>
+          <div className='mr-1 text-sm font-semibold text-gray-800'>
+            {t('appDebug.feature.tools.title')}
+          </div>
+          <TooltipPlus popupContent={<div className='max-w-[160px]'>{t('appDebug.feature.tools.tips')}</div>}>
+            <HelpCircle className='w-3.5 h-3.5 text-gray-400' />
+          </TooltipPlus>
+        </div>
+        {
+          !expanded && !!externalDataToolsConfig.length && (
+            <>
+              <div className='mr-3 text-xs text-gray-500'>{t('appDebug.feature.tools.toolsInUse', { count: externalDataToolsConfig.length })}</div>
+              <div className='mr-1 w-[1px] h-3.5 bg-gray-200' />
+            </>
+          )
+        }
+        <div
+          className='flex items-center h-7 px-3 text-xs font-medium text-gray-700 cursor-pointer'
+          onClick={() => handleOpenExternalDataToolModal({}, -1)}
+        >
+          <Plus className='mr-[5px] w-3.5 h-3.5 ' />
+          {t('common.operation.add')}
+        </div>
+      </div>
+      {
+        expanded && !!externalDataToolsConfig.length && (
+          <div className='pb-3'>
+            {
+              externalDataToolsConfig.map((item, index: number) => (
+                <div
+                  key={`${index}-${item.type}-${item.label}-${item.variable}`}
+                  className='group flex items-center mb-1 last-of-type:mb-0 px-2.5 py-2 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'
+                >
+                  <div className='grow flex items-center'>
+                    <AppIcon size='large'
+                      className='mr-2 !w-6 !h-6 rounded-md border-[0.5px] border-black/5'
+                      icon={item.icon}
+                      background={item.icon_background}
+                    />
+                    <div className='mr-2 text-[13px] font-medium text-gray-800'>{item.label}</div>
+                    <TooltipPlus
+                      popupContent={copied ? t('appApi.copied') : `${item.variable}, ${t('appApi.copy')}`}
+                    >
+                      <div
+                        className='text-xs text-gray-500'
+                        onClick={() => {
+                          copy(item.variable || '')
+                          setCopied(true)
+                        }}
+                      >
+                        {item.variable}
+                      </div>
+                    </TooltipPlus>
+                  </div>
+                  <div
+                    className='hidden group-hover:flex items-center justify-center mr-1 w-6 h-6 hover:bg-black/5 rounded-md cursor-pointer'
+                    onClick={() => handleOpenExternalDataToolModal(item, index)}
+                  >
+                    <Settings01 className='w-4 h-4 text-gray-500' />
+                  </div>
+                  <div
+                    className='hidden group/action group-hover:flex items-center justify-center w-6 h-6 hover:bg-[#FEE4E2] rounded-md cursor-pointer'
+                    onClick={() => setExternalDataToolsConfig([...externalDataToolsConfig.slice(0, index), ...externalDataToolsConfig.slice(index + 1)])}
+                  >
+                    <Trash03 className='w-4 h-4 text-gray-500 group-hover/action:text-[#D92D20]' />
+                  </div>
+                  <div className='hidden group-hover:block ml-2 mr-3 w-[1px] h-3.5 bg-gray-200' />
+                  <Switch
+                    size='l'
+                    defaultValue={item.enabled}
+                    onChange={(enabled: boolean) => handleSaveExternalDataToolModal({ ...item, enabled }, index)}
+                  />
+                </div>
+              ))
+            }
+          </div>
+        )
+      }
+    </div>
+  )
+}
+
+export default Tools

+ 3 - 14
web/app/components/app/overview/apikey-info-panel/index.tsx

@@ -7,23 +7,22 @@ import { useContext } from 'use-context-selector'
 import Progress from './progress'
 import Button from '@/app/components/base/button'
 import { LinkExternal02, XClose } from '@/app/components/base/icons/src/vender/line/general'
-import AccountSetting from '@/app/components/header/account-setting'
 import { IS_CE_EDITION } from '@/config'
 import { useProviderContext } from '@/context/provider-context'
 import { formatNumber } from '@/utils/format'
 import I18n from '@/context/i18n'
 import ProviderConfig from '@/app/components/header/account-setting/model-page/configs'
+import { useModalContext } from '@/context/modal-context'
 
 const APIKeyInfoPanel: FC = () => {
   const isCloud = !IS_CE_EDITION
   const { locale } = useContext(I18n)
 
   const { textGenerationModelList } = useProviderContext()
+  const { setShowAccountSettingModal } = useModalContext()
 
   const { t } = useTranslation()
 
-  const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
-
   const [isShow, setIsShow] = useState(true)
 
   const hasSetAPIKEY = !!textGenerationModelList?.find(({ model_provider: provider }) => {
@@ -101,9 +100,7 @@ const APIKeyInfoPanel: FC = () => {
       <Button
         type='primary'
         className='space-x-2'
-        onClick={() => {
-          setShowSetAPIKeyModal(true)
-        }}
+        onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
       >
         <div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div>
         <LinkExternal02 className='w-4 h-4' />
@@ -123,14 +120,6 @@ const APIKeyInfoPanel: FC = () => {
         className='absolute right-4 top-4 flex items-center justify-center w-8 h-8 cursor-pointer '>
         <XClose className='w-4 h-4 text-gray-500' />
       </div>
-
-      {
-        showSetAPIKeyModal && (
-          <AccountSetting activeTab="provider" onCancel={async () => {
-            setShowSetAPIKeyModal(false)
-          }} />
-        )
-      }
     </div>
   )
 }

+ 1 - 0
web/app/components/app/text-generate/item/index.tsx

@@ -19,6 +19,7 @@ import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
 import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
 import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
 import { fetchTextGenerationMessge } from '@/service/debug'
+
 const MAX_DEPTH = 3
 export type IGenerationItemProps = {
   className?: string

+ 12 - 0
web/app/components/base/icons/assets/vender/line/development/webhooks.svg

@@ -0,0 +1,12 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="webhooks">
+<g id="Vector">
+<path d="M12.0007 11.9999C12.5529 11.9999 13.0007 11.5522 13.0007 10.9999C13.0007 10.4476 12.5529 9.99993 12.0007 9.99993C11.4484 9.99993 11.0007 10.4476 11.0007 10.9999C11.0007 11.5522 11.4484 11.9999 12.0007 11.9999Z" fill="#155EEF"/>
+<path d="M8.00065 5.49993C8.55294 5.49993 9.00065 5.05222 9.00065 4.49993C9.00065 3.94765 8.55294 3.49993 8.00065 3.49993C7.44837 3.49993 7.00065 3.94765 7.00065 4.49993C7.00065 5.05222 7.44837 5.49993 8.00065 5.49993Z" fill="#155EEF"/>
+<path d="M4.00065 11.9999C4.55294 11.9999 5.00065 11.5522 5.00065 10.9999C5.00065 10.4476 4.55294 9.99993 4.00065 9.99993C3.44837 9.99993 3.00065 10.4476 3.00065 10.9999C3.00065 11.5522 3.44837 11.9999 4.00065 11.9999Z" fill="#155EEF"/>
+<path d="M2.40065 8.9666C2.6952 9.18751 2.7549 9.60538 2.53398 9.89993C2.35969 10.1323 2.24311 10.4028 2.19386 10.6891C2.14461 10.9754 2.16409 11.2693 2.25071 11.5466C2.33733 11.8239 2.48859 12.0766 2.69205 12.2839C2.8955 12.4913 3.14531 12.6473 3.4209 12.7392C3.69649 12.831 3.98996 12.8561 4.27713 12.8123C4.56431 12.7685 4.83696 12.6571 5.07262 12.4872C5.30828 12.3174 5.50021 12.0939 5.63258 11.8353C5.76495 11.5768 5.83398 11.2904 5.83398 10.9999C5.83398 10.6317 6.13246 10.3333 6.50065 10.3333H12.0007C12.3688 10.3333 12.6673 10.6317 12.6673 10.9999C12.6673 11.3681 12.3688 11.6666 12.0007 11.6666H7.09635C7.03846 11.9354 6.94561 12.1965 6.81944 12.4429C6.5908 12.8896 6.25929 13.2755 5.85223 13.5689C5.44518 13.8623 4.97424 14.0547 4.47821 14.1304C3.98219 14.2061 3.47528 14.1628 2.99926 14.0041C2.52325 13.8454 2.09175 13.5759 1.74033 13.2178C1.38891 12.8596 1.12763 12.4231 0.978025 11.9441C0.828415 11.4652 0.794759 10.9575 0.879828 10.463C0.964898 9.96855 1.16626 9.50134 1.46732 9.09993C1.68823 8.80538 2.1061 8.74568 2.40065 8.9666Z" fill="#155EEF"/>
+<path d="M7.22821 1.43134C7.70981 1.31005 8.21318 1.30373 8.69767 1.41291C9.18216 1.52208 9.63418 1.74367 10.0172 2.05979C10.4003 2.37591 10.7036 2.77769 10.9027 3.23268C11.0503 3.56999 10.8965 3.96309 10.5592 4.11069C10.2218 4.25828 9.82874 4.10449 9.68115 3.76718C9.56589 3.50377 9.39028 3.27116 9.16852 3.08814C8.94676 2.90512 8.68507 2.77683 8.40458 2.71363C8.12408 2.65042 7.83265 2.65408 7.55383 2.7243C7.27501 2.79452 7.01662 2.92933 6.79952 3.11785C6.58242 3.30637 6.41271 3.54331 6.30409 3.80953C6.19547 4.07575 6.15099 4.36379 6.17424 4.65038C6.19749 4.93696 6.28782 5.21406 6.43794 5.45929C6.58806 5.70452 6.79375 5.911 7.0384 6.06206C7.35127 6.25524 7.44865 6.66527 7.25605 6.9785L4.56855 11.3491C4.37569 11.6628 3.96509 11.7607 3.65145 11.5678C3.33781 11.375 3.2399 10.9644 3.43276 10.6507L5.80875 6.7867C5.61374 6.59953 5.44284 6.38752 5.30076 6.15541C5.04146 5.73184 4.88544 5.25321 4.84527 4.7582C4.80511 4.26319 4.88194 3.76567 5.06956 3.30584C5.25717 2.846 5.55031 2.43674 5.9253 2.11111C6.30029 1.78549 6.74661 1.55262 7.22821 1.43134Z" fill="#155EEF"/>
+<path d="M7.65145 3.93204C7.96509 3.73918 8.37569 3.83709 8.56855 4.15073L10.944 8.01384C11.1917 7.9264 11.4501 7.86984 11.7135 7.84608C12.2008 7.80211 12.6917 7.87167 13.1476 8.04931C13.6036 8.22695 14.0121 8.50783 14.3413 8.86991C14.6704 9.23199 14.9111 9.66542 15.0446 10.1362C15.1781 10.6069 15.2006 11.1022 15.1105 11.5832C15.0204 12.0641 14.82 12.5176 14.5252 12.9081C14.2303 13.2986 13.849 13.6155 13.4111 13.8338C12.9732 14.0522 12.4907 14.1661 12.0014 14.1666C11.6332 14.167 11.3344 13.8688 11.334 13.5006C11.3336 13.1324 11.6318 12.8337 12 12.8333C12.2832 12.833 12.5626 12.767 12.8161 12.6406C13.0696 12.5142 13.2904 12.3308 13.4611 12.1047C13.6318 11.8786 13.7478 11.616 13.8 11.3376C13.8522 11.0592 13.8391 10.7724 13.7618 10.4999C13.6846 10.2273 13.5452 9.97639 13.3546 9.76676C13.1641 9.55714 12.9276 9.39452 12.6636 9.29168C12.3996 9.18884 12.1154 9.14856 11.8333 9.17402C11.5511 9.19947 11.2787 9.28996 11.0375 9.43839C10.8868 9.53104 10.7056 9.56006 10.5336 9.51905C10.3616 9.47805 10.2129 9.37039 10.1203 9.21975L7.43276 4.84913C7.2399 4.53549 7.33781 4.12489 7.65145 3.93204Z" fill="#155EEF"/>
+</g>
+</g>
+</svg>

+ 6 - 0
web/app/components/base/icons/assets/vender/line/education/book-open-01.svg

@@ -0,0 +1,6 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="book-open-01">
+<path id="Fill" opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#667085"/>
+<path id="Icon" d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/general/edit-02.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="Left Icon" clip-path="url(#clip0_12284_22440)">
+<path id="Icon" d="M10.5007 5.83319L8.16733 3.49985M1.45898 12.5415L3.4332 12.3222C3.6744 12.2954 3.795 12.282 3.90773 12.2455C4.00774 12.2131 4.10291 12.1673 4.19067 12.1095C4.28958 12.0443 4.37539 11.9585 4.54699 11.7868L12.2507 4.08319C12.895 3.43885 12.895 2.39418 12.2507 1.74985C11.6063 1.10552 10.5617 1.10552 9.91733 1.74985L2.21366 9.45351C2.04205 9.62512 1.95625 9.71092 1.89102 9.80983C1.83315 9.89759 1.78741 9.99277 1.75503 10.0928C1.71854 10.2055 1.70514 10.3261 1.67834 10.5673L1.45898 12.5415Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_12284_22440">
+<rect width="14" height="14" fill="white"/>
+</clipPath>
+</defs>
+</svg>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
web/app/components/base/icons/assets/vender/line/general/settings-01.svg


+ 8 - 0
web/app/components/base/icons/assets/vender/solid/files/file-search-02.svg

@@ -0,0 +1,8 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="file-search-02">
+<g id="Solid">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1609 0.666748H5.83913C5.3025 0.66674 4.85958 0.666734 4.49878 0.696212C4.12405 0.726828 3.77958 0.792538 3.45603 0.957399C2.95426 1.21306 2.54631 1.62101 2.29065 2.12277C2.12579 2.44633 2.06008 2.7908 2.02946 3.16553C1.99999 3.52632 1.99999 3.96924 2 4.50587V11.4943C1.99999 12.0309 1.99999 12.4738 2.02946 12.8346C2.06008 13.2094 2.12579 13.5538 2.29065 13.8774C2.54631 14.3792 2.95426 14.7871 3.45603 15.0428C3.77958 15.2076 4.12405 15.2733 4.49878 15.304C4.85958 15.3334 5.30248 15.3334 5.83912 15.3334H7.75554C8.22798 15.3334 8.4642 15.3334 8.55219 15.2689C8.64172 15.2033 8.67645 15.1421 8.68693 15.0316C8.69724 14.9229 8.55693 14.6879 8.27632 14.2177C7.88913 13.5689 7.66667 12.8105 7.66667 12.0001C7.66667 9.60685 9.60677 7.66675 12 7.66675C12.4106 7.66675 12.8078 7.72385 13.1842 7.83055C13.5061 7.92177 13.667 7.96739 13.7581 7.94138C13.847 7.91602 13.9015 7.87486 13.9501 7.79623C14 7.71563 14 7.56892 14 7.27549V4.50587C14 3.96923 14 3.52633 13.9705 3.16553C13.9399 2.7908 13.8742 2.44633 13.7093 2.12277C13.4537 1.62101 13.0457 1.21306 12.544 0.957399C12.2204 0.792538 11.8759 0.726828 11.5012 0.696212C11.1404 0.666734 10.6975 0.66674 10.1609 0.666748ZM4.66667 3.33342C4.29848 3.33342 4 3.63189 4 4.00008C4 4.36827 4.29848 4.66675 4.66667 4.66675H10.6667C11.0349 4.66675 11.3333 4.36827 11.3333 4.00008C11.3333 3.63189 11.0349 3.33342 10.6667 3.33342H4.66667ZM4 6.66675C4 6.29856 4.29848 6.00008 4.66667 6.00008H8.66667C9.03486 6.00008 9.33333 6.29856 9.33333 6.66675C9.33333 7.03494 9.03486 7.33342 8.66667 7.33342H4.66667C4.29848 7.33342 4 7.03494 4 6.66675ZM4 9.33342C4 8.96523 4.29848 8.66675 4.66667 8.66675H6C6.36819 8.66675 6.66667 8.96523 6.66667 9.33342C6.66667 9.7016 6.36819 10.0001 6 10.0001H4.66667C4.29848 10.0001 4 9.7016 4 9.33342Z" fill="#039855"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12.0001C9 10.3432 10.3431 9.00008 12 9.00008C13.6569 9.00008 15 10.3432 15 12.0001C15 12.5871 14.8314 13.1348 14.54 13.5972L15.1381 14.1953C15.3984 14.4557 15.3984 14.8778 15.1381 15.1382C14.8777 15.3985 14.4556 15.3985 14.1953 15.1382L13.5972 14.54C13.1347 14.8315 12.587 15.0001 12 15.0001C10.3431 15.0001 9 13.6569 9 12.0001ZM12 10.3334C11.0795 10.3334 10.3333 11.0796 10.3333 12.0001C10.3333 12.9206 11.0795 13.6667 12 13.6667C12.9205 13.6667 13.6667 12.9206 13.6667 12.0001C13.6667 11.0796 12.9205 10.3334 12 10.3334Z" fill="#039855"/>
+</g>
+</g>
+</svg>

+ 9 - 0
web/app/components/base/icons/assets/vender/solid/general/tool-03.svg

@@ -0,0 +1,9 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="tool-03">
+<g id="Vector">
+<path d="M5.10516 6.61092L6.45642 5.41856C6.43816 5.25959 6.43018 5.09961 6.43253 4.93962V4.9285L2.91826 1.41365C2.89245 1.38778 2.86179 1.36725 2.82804 1.35325C2.79429 1.33924 2.75811 1.33203 2.72157 1.33203C2.68503 1.33203 2.64884 1.33924 2.61509 1.35325C2.58134 1.36725 2.55069 1.38778 2.52488 1.41365L1.41365 2.52489C1.38778 2.5507 1.36725 2.58135 1.35325 2.6151C1.33924 2.64885 1.33203 2.68504 1.33203 2.72158C1.33203 2.75812 1.33924 2.7943 1.35325 2.82806C1.36725 2.86181 1.38778 2.89246 1.41365 2.91827L5.10516 6.61092Z" fill="#444CE7"/>
+<path d="M12.5043 9.33348C12.3512 9.3848 12.1956 9.42819 12.0381 9.46349L11.9748 9.47461C11.7112 9.51388 11.4451 9.53375 11.1786 9.53406C10.9848 9.53389 10.7912 9.52314 10.5985 9.50183L8.58942 11.7604L10.8297 14.0007C11.0335 14.2097 11.2767 14.3763 11.5452 14.4907C11.8138 14.6052 12.1024 14.6652 12.3943 14.6674H12.4176C12.8604 14.6643 13.2924 14.5307 13.6596 14.2832C14.0268 14.0356 14.3128 13.6853 14.4818 13.276C14.6508 12.8667 14.6952 12.4167 14.6096 11.9822C14.524 11.5478 14.3122 11.1483 14.0006 10.8337L12.5043 9.33348Z" fill="#444CE7"/>
+<path d="M14.4606 3.79227C14.4443 3.74889 14.4174 3.71027 14.3823 3.67995C14.3472 3.64963 14.3051 3.62857 14.2599 3.61868C14.2146 3.6088 14.1675 3.6104 14.123 3.62335C14.0785 3.6363 14.0379 3.66018 14.005 3.69282L12.4132 5.27745L10.7224 3.5928L12.3132 2.00929C12.3454 1.97739 12.3692 1.93802 12.3825 1.89468C12.3957 1.85134 12.3981 1.80539 12.3893 1.76092C12.3805 1.7159 12.3606 1.67376 12.3315 1.63828C12.3024 1.60279 12.265 1.57506 12.2226 1.55757C11.7685 1.35982 11.2688 1.29063 10.778 1.35754C9.88338 1.43541 9.05173 1.8501 8.45122 2.51777C7.8507 3.18544 7.52615 4.05624 7.54319 4.95408C7.53907 5.24983 7.58317 5.54428 7.67376 5.82584L2.09204 10.7442C1.64427 11.1439 1.3735 11.7051 1.33923 12.3043C1.30495 12.9036 1.50997 13.4919 1.90924 13.9401L1.95703 13.9924C2.35812 14.411 2.90891 14.6533 3.4885 14.6662C4.06809 14.6791 4.62913 14.4616 5.04848 14.0613C5.11213 14.0008 5.17189 13.9364 5.22739 13.8685L10.1801 8.30058C10.7141 8.43272 11.2688 8.45821 11.8126 8.37559C12.4502 8.24485 13.04 7.9423 13.5182 7.50065C13.9964 7.05899 14.3447 6.49503 14.5256 5.86974C14.7321 5.18882 14.7092 4.45895 14.4606 3.79227Z" fill="#444CE7"/>
+</g>
+</g>
+</svg>

+ 89 - 0
web/app/components/base/icons/src/vender/line/development/Webhooks.json

@@ -0,0 +1,89 @@
+{
+	"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": "webhooks"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Vector"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M12.0007 11.9999C12.5529 11.9999 13.0007 11.5522 13.0007 10.9999C13.0007 10.4476 12.5529 9.99993 12.0007 9.99993C11.4484 9.99993 11.0007 10.4476 11.0007 10.9999C11.0007 11.5522 11.4484 11.9999 12.0007 11.9999Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M8.00065 5.49993C8.55294 5.49993 9.00065 5.05222 9.00065 4.49993C9.00065 3.94765 8.55294 3.49993 8.00065 3.49993C7.44837 3.49993 7.00065 3.94765 7.00065 4.49993C7.00065 5.05222 7.44837 5.49993 8.00065 5.49993Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M4.00065 11.9999C4.55294 11.9999 5.00065 11.5522 5.00065 10.9999C5.00065 10.4476 4.55294 9.99993 4.00065 9.99993C3.44837 9.99993 3.00065 10.4476 3.00065 10.9999C3.00065 11.5522 3.44837 11.9999 4.00065 11.9999Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M2.40065 8.9666C2.6952 9.18751 2.7549 9.60538 2.53398 9.89993C2.35969 10.1323 2.24311 10.4028 2.19386 10.6891C2.14461 10.9754 2.16409 11.2693 2.25071 11.5466C2.33733 11.8239 2.48859 12.0766 2.69205 12.2839C2.8955 12.4913 3.14531 12.6473 3.4209 12.7392C3.69649 12.831 3.98996 12.8561 4.27713 12.8123C4.56431 12.7685 4.83696 12.6571 5.07262 12.4872C5.30828 12.3174 5.50021 12.0939 5.63258 11.8353C5.76495 11.5768 5.83398 11.2904 5.83398 10.9999C5.83398 10.6317 6.13246 10.3333 6.50065 10.3333H12.0007C12.3688 10.3333 12.6673 10.6317 12.6673 10.9999C12.6673 11.3681 12.3688 11.6666 12.0007 11.6666H7.09635C7.03846 11.9354 6.94561 12.1965 6.81944 12.4429C6.5908 12.8896 6.25929 13.2755 5.85223 13.5689C5.44518 13.8623 4.97424 14.0547 4.47821 14.1304C3.98219 14.2061 3.47528 14.1628 2.99926 14.0041C2.52325 13.8454 2.09175 13.5759 1.74033 13.2178C1.38891 12.8596 1.12763 12.4231 0.978025 11.9441C0.828415 11.4652 0.794759 10.9575 0.879828 10.463C0.964898 9.96855 1.16626 9.50134 1.46732 9.09993C1.68823 8.80538 2.1061 8.74568 2.40065 8.9666Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M7.22821 1.43134C7.70981 1.31005 8.21318 1.30373 8.69767 1.41291C9.18216 1.52208 9.63418 1.74367 10.0172 2.05979C10.4003 2.37591 10.7036 2.77769 10.9027 3.23268C11.0503 3.56999 10.8965 3.96309 10.5592 4.11069C10.2218 4.25828 9.82874 4.10449 9.68115 3.76718C9.56589 3.50377 9.39028 3.27116 9.16852 3.08814C8.94676 2.90512 8.68507 2.77683 8.40458 2.71363C8.12408 2.65042 7.83265 2.65408 7.55383 2.7243C7.27501 2.79452 7.01662 2.92933 6.79952 3.11785C6.58242 3.30637 6.41271 3.54331 6.30409 3.80953C6.19547 4.07575 6.15099 4.36379 6.17424 4.65038C6.19749 4.93696 6.28782 5.21406 6.43794 5.45929C6.58806 5.70452 6.79375 5.911 7.0384 6.06206C7.35127 6.25524 7.44865 6.66527 7.25605 6.9785L4.56855 11.3491C4.37569 11.6628 3.96509 11.7607 3.65145 11.5678C3.33781 11.375 3.2399 10.9644 3.43276 10.6507L5.80875 6.7867C5.61374 6.59953 5.44284 6.38752 5.30076 6.15541C5.04146 5.73184 4.88544 5.25321 4.84527 4.7582C4.80511 4.26319 4.88194 3.76567 5.06956 3.30584C5.25717 2.846 5.55031 2.43674 5.9253 2.11111C6.30029 1.78549 6.74661 1.55262 7.22821 1.43134Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M7.65145 3.93204C7.96509 3.73918 8.37569 3.83709 8.56855 4.15073L10.944 8.01384C11.1917 7.9264 11.4501 7.86984 11.7135 7.84608C12.2008 7.80211 12.6917 7.87167 13.1476 8.04931C13.6036 8.22695 14.0121 8.50783 14.3413 8.86991C14.6704 9.23199 14.9111 9.66542 15.0446 10.1362C15.1781 10.6069 15.2006 11.1022 15.1105 11.5832C15.0204 12.0641 14.82 12.5176 14.5252 12.9081C14.2303 13.2986 13.849 13.6155 13.4111 13.8338C12.9732 14.0522 12.4907 14.1661 12.0014 14.1666C11.6332 14.167 11.3344 13.8688 11.334 13.5006C11.3336 13.1324 11.6318 12.8337 12 12.8333C12.2832 12.833 12.5626 12.767 12.8161 12.6406C13.0696 12.5142 13.2904 12.3308 13.4611 12.1047C13.6318 11.8786 13.7478 11.616 13.8 11.3376C13.8522 11.0592 13.8391 10.7724 13.7618 10.4999C13.6846 10.2273 13.5452 9.97639 13.3546 9.76676C13.1641 9.55714 12.9276 9.39452 12.6636 9.29168C12.3996 9.18884 12.1154 9.14856 11.8333 9.17402C11.5511 9.19947 11.2787 9.28996 11.0375 9.43839C10.8868 9.53104 10.7056 9.56006 10.5336 9.51905C10.3616 9.47805 10.2129 9.37039 10.1203 9.21975L7.43276 4.84913C7.2399 4.53549 7.33781 4.12489 7.65145 3.93204Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Webhooks"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/development/Webhooks.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Webhooks.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 = 'Webhooks'
+
+export default Icon

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

@@ -4,3 +4,4 @@ export { default as Database01 } from './Database01'
 export { default as Database03 } from './Database03'
 export { default as PuzzlePiece01 } from './PuzzlePiece01'
 export { default as Variable } from './Variable'
+export { default as Webhooks } from './Webhooks'

+ 49 - 0
web/app/components/base/icons/src/vender/line/education/BookOpen01.json

@@ -0,0 +1,49 @@
+{
+	"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": "book-open-01"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Fill",
+							"opacity": "0.12",
+							"d": "M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7",
+							"stroke": "currentColor",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "BookOpen01"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/education/BookOpen01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './BookOpen01.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 = 'BookOpen01'
+
+export default Icon

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

@@ -0,0 +1 @@
+export { default as BookOpen01 } from './BookOpen01'

+ 66 - 0
web/app/components/base/icons/src/vender/line/general/Edit02.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": "Left Icon",
+					"clip-path": "url(#clip0_12284_22440)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M10.5007 5.83319L8.16733 3.49985M1.45898 12.5415L3.4332 12.3222C3.6744 12.2954 3.795 12.282 3.90773 12.2455C4.00774 12.2131 4.10291 12.1673 4.19067 12.1095C4.28958 12.0443 4.37539 11.9585 4.54699 11.7868L12.2507 4.08319C12.895 3.43885 12.895 2.39418 12.2507 1.74985C11.6063 1.10552 10.5617 1.10552 9.91733 1.74985L2.21366 9.45351C2.04205 9.62512 1.95625 9.71092 1.89102 9.80983C1.83315 9.89759 1.78741 9.99277 1.75503 10.0928C1.71854 10.2055 1.70514 10.3261 1.67834 10.5673L1.45898 12.5415Z",
+							"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_12284_22440"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "14",
+									"height": "14",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Edit02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/general/Edit02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Edit02.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 = 'Edit02'
+
+export default Icon

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 44 - 0
web/app/components/base/icons/src/vender/line/general/Settings01.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/general/Settings01.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Settings01.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 = 'Settings01'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/line/general/index.ts

@@ -2,6 +2,7 @@ export { default as AtSign } from './AtSign'
 export { default as Bookmark } from './Bookmark'
 export { default as Check } from './Check'
 export { default as DotsHorizontal } from './DotsHorizontal'
+export { default as Edit02 } from './Edit02'
 export { default as Edit03 } from './Edit03'
 export { default as Hash02 } from './Hash02'
 export { default as HelpCircle } from './HelpCircle'
@@ -13,6 +14,7 @@ export { default as LogOut01 } from './LogOut01'
 export { default as Pin02 } from './Pin02'
 export { default as Plus } from './Plus'
 export { default as SearchLg } from './SearchLg'
+export { default as Settings01 } from './Settings01'
 export { default as Settings04 } from './Settings04'
 export { default as Target04 } from './Target04'
 export { default as Trash03 } from './Trash03'

+ 57 - 0
web/app/components/base/icons/src/vender/solid/files/FileSearch02.json

@@ -0,0 +1,57 @@
+{
+	"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": "file-search-02"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Solid"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M10.1609 0.666748H5.83913C5.3025 0.66674 4.85958 0.666734 4.49878 0.696212C4.12405 0.726828 3.77958 0.792538 3.45603 0.957399C2.95426 1.21306 2.54631 1.62101 2.29065 2.12277C2.12579 2.44633 2.06008 2.7908 2.02946 3.16553C1.99999 3.52632 1.99999 3.96924 2 4.50587V11.4943C1.99999 12.0309 1.99999 12.4738 2.02946 12.8346C2.06008 13.2094 2.12579 13.5538 2.29065 13.8774C2.54631 14.3792 2.95426 14.7871 3.45603 15.0428C3.77958 15.2076 4.12405 15.2733 4.49878 15.304C4.85958 15.3334 5.30248 15.3334 5.83912 15.3334H7.75554C8.22798 15.3334 8.4642 15.3334 8.55219 15.2689C8.64172 15.2033 8.67645 15.1421 8.68693 15.0316C8.69724 14.9229 8.55693 14.6879 8.27632 14.2177C7.88913 13.5689 7.66667 12.8105 7.66667 12.0001C7.66667 9.60685 9.60677 7.66675 12 7.66675C12.4106 7.66675 12.8078 7.72385 13.1842 7.83055C13.5061 7.92177 13.667 7.96739 13.7581 7.94138C13.847 7.91602 13.9015 7.87486 13.9501 7.79623C14 7.71563 14 7.56892 14 7.27549V4.50587C14 3.96923 14 3.52633 13.9705 3.16553C13.9399 2.7908 13.8742 2.44633 13.7093 2.12277C13.4537 1.62101 13.0457 1.21306 12.544 0.957399C12.2204 0.792538 11.8759 0.726828 11.5012 0.696212C11.1404 0.666734 10.6975 0.66674 10.1609 0.666748ZM4.66667 3.33342C4.29848 3.33342 4 3.63189 4 4.00008C4 4.36827 4.29848 4.66675 4.66667 4.66675H10.6667C11.0349 4.66675 11.3333 4.36827 11.3333 4.00008C11.3333 3.63189 11.0349 3.33342 10.6667 3.33342H4.66667ZM4 6.66675C4 6.29856 4.29848 6.00008 4.66667 6.00008H8.66667C9.03486 6.00008 9.33333 6.29856 9.33333 6.66675C9.33333 7.03494 9.03486 7.33342 8.66667 7.33342H4.66667C4.29848 7.33342 4 7.03494 4 6.66675ZM4 9.33342C4 8.96523 4.29848 8.66675 4.66667 8.66675H6C6.36819 8.66675 6.66667 8.96523 6.66667 9.33342C6.66667 9.7016 6.36819 10.0001 6 10.0001H4.66667C4.29848 10.0001 4 9.7016 4 9.33342Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M9 12.0001C9 10.3432 10.3431 9.00008 12 9.00008C13.6569 9.00008 15 10.3432 15 12.0001C15 12.5871 14.8314 13.1348 14.54 13.5972L15.1381 14.1953C15.3984 14.4557 15.3984 14.8778 15.1381 15.1382C14.8777 15.3985 14.4556 15.3985 14.1953 15.1382L13.5972 14.54C13.1347 14.8315 12.587 15.0001 12 15.0001C10.3431 15.0001 9 13.6569 9 12.0001ZM12 10.3334C11.0795 10.3334 10.3333 11.0796 10.3333 12.0001C10.3333 12.9206 11.0795 13.6667 12 13.6667C12.9205 13.6667 13.6667 12.9206 13.6667 12.0001C13.6667 11.0796 12.9205 10.3334 12 10.3334Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "FileSearch02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/files/FileSearch02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './FileSearch02.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 = 'FileSearch02'
+
+export default Icon

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

@@ -1,2 +1,3 @@
 export { default as File05 } from './File05'
+export { default as FileSearch02 } from './FileSearch02'
 export { default as Folder } from './Folder'

+ 62 - 0
web/app/components/base/icons/src/vender/solid/general/Tool03.json

@@ -0,0 +1,62 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "tool-03"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Vector"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M5.10516 6.61092L6.45642 5.41856C6.43816 5.25959 6.43018 5.09961 6.43253 4.93962V4.9285L2.91826 1.41365C2.89245 1.38778 2.86179 1.36725 2.82804 1.35325C2.79429 1.33924 2.75811 1.33203 2.72157 1.33203C2.68503 1.33203 2.64884 1.33924 2.61509 1.35325C2.58134 1.36725 2.55069 1.38778 2.52488 1.41365L1.41365 2.52489C1.38778 2.5507 1.36725 2.58135 1.35325 2.6151C1.33924 2.64885 1.33203 2.68504 1.33203 2.72158C1.33203 2.75812 1.33924 2.7943 1.35325 2.82806C1.36725 2.86181 1.38778 2.89246 1.41365 2.91827L5.10516 6.61092Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M12.5043 9.33348C12.3512 9.3848 12.1956 9.42819 12.0381 9.46349L11.9748 9.47461C11.7112 9.51388 11.4451 9.53375 11.1786 9.53406C10.9848 9.53389 10.7912 9.52314 10.5985 9.50183L8.58942 11.7604L10.8297 14.0007C11.0335 14.2097 11.2767 14.3763 11.5452 14.4907C11.8138 14.6052 12.1024 14.6652 12.3943 14.6674H12.4176C12.8604 14.6643 13.2924 14.5307 13.6596 14.2832C14.0268 14.0356 14.3128 13.6853 14.4818 13.276C14.6508 12.8667 14.6952 12.4167 14.6096 11.9822C14.524 11.5478 14.3122 11.1483 14.0006 10.8337L12.5043 9.33348Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M14.4606 3.79227C14.4443 3.74889 14.4174 3.71027 14.3823 3.67995C14.3472 3.64963 14.3051 3.62857 14.2599 3.61868C14.2146 3.6088 14.1675 3.6104 14.123 3.62335C14.0785 3.6363 14.0379 3.66018 14.005 3.69282L12.4132 5.27745L10.7224 3.5928L12.3132 2.00929C12.3454 1.97739 12.3692 1.93802 12.3825 1.89468C12.3957 1.85134 12.3981 1.80539 12.3893 1.76092C12.3805 1.7159 12.3606 1.67376 12.3315 1.63828C12.3024 1.60279 12.265 1.57506 12.2226 1.55757C11.7685 1.35982 11.2688 1.29063 10.778 1.35754C9.88338 1.43541 9.05173 1.8501 8.45122 2.51777C7.8507 3.18544 7.52615 4.05624 7.54319 4.95408C7.53907 5.24983 7.58317 5.54428 7.67376 5.82584L2.09204 10.7442C1.64427 11.1439 1.3735 11.7051 1.33923 12.3043C1.30495 12.9036 1.50997 13.4919 1.90924 13.9401L1.95703 13.9924C2.35812 14.411 2.90891 14.6533 3.4885 14.6662C4.06809 14.6791 4.62913 14.4616 5.04848 14.0613C5.11213 14.0008 5.17189 13.9364 5.22739 13.8685L10.1801 8.30058C10.7141 8.43272 11.2688 8.45821 11.8126 8.37559C12.4502 8.24485 13.04 7.9423 13.5182 7.50065C13.9964 7.05899 14.3447 6.49503 14.5256 5.86974C14.7321 5.18882 14.7092 4.45895 14.4606 3.79227Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Tool03"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/general/Tool03.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Tool03.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 = 'Tool03'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/solid/general/index.ts

@@ -3,4 +3,5 @@ export { default as CheckDone01 } from './CheckDone01'
 export { default as Download02 } from './Download02'
 export { default as MessageClockCircle } from './MessageClockCircle'
 export { default as Target04 } from './Target04'
+export { default as Tool03 } from './Tool03'
 export { default as XCircle } from './XCircle'

+ 4 - 13
web/app/components/base/notion-page-selector/base.tsx

@@ -8,11 +8,10 @@ import WorkspaceSelector from './workspace-selector'
 import SearchInput from './search-input'
 import PageSelector from './page-selector'
 import { preImportNotionPages } from '@/service/datasets'
-import AccountSetting from '@/app/components/header/account-setting'
 import { NotionConnector } from '@/app/components/datasets/create/step-one'
 import type { DataSourceNotionPageMap, DataSourceNotionWorkspace, NotionPage } from '@/models/common'
 import { ToastContext } from '@/app/components/base/toast'
-
+import { useModalContext } from '@/context/modal-context'
 
 type NotionPageSelectorProps = {
   value?: string[]
@@ -40,8 +39,8 @@ const NotionPageSelector = ({
   const { data, mutate } = useSWR({ url: '/notion/pre-import/pages', datasetId }, preImportNotionPages)
   const [prevData, setPrevData] = useState(data)
   const [searchValue, setSearchValue] = useState('')
-  const [showDataSourceSetting, setShowDataSourceSetting] = useState(false)
   const [currentWorkspaceId, setCurrentWorkspaceId] = useState('')
+  const { setShowAccountSettingModal } = useModalContext()
 
   const notionWorkspaces = useMemo(() => {
     return data?.notion_info || []
@@ -112,7 +111,7 @@ const NotionPageSelector = ({
                 <div className='mx-1 w-[1px] h-3 bg-gray-200' />
                 <div
                   className={cn(s['setting-icon'], 'w-6 h-6 cursor-pointer')}
-                  onClick={() => setShowDataSourceSetting(true)}
+                  onClick={() => setShowAccountSettingModal({ payload: 'data-source', onCancelCallback: mutate })}
                 />
                 <div className='grow' />
                 <SearchInput
@@ -135,17 +134,9 @@ const NotionPageSelector = ({
             </>
           )
           : (
-            <NotionConnector onSetting={() => setShowDataSourceSetting(true)} />
+            <NotionConnector onSetting={() => setShowAccountSettingModal({ payload: 'data-source', onCancelCallback: mutate })} />
           )
       }
-      {
-        showDataSourceSetting && (
-          <AccountSetting activeTab='data-source' onCancel={() => {
-            setShowDataSourceSetting(false)
-            mutate()
-          }} />
-        )
-      }
     </div>
   )
 }

+ 8 - 2
web/app/components/base/prompt-editor/index.tsx

@@ -35,7 +35,7 @@ import OnBlurBlock from './plugins/on-blur-block'
 import { textToEditorState } from './utils'
 import type { Dataset } from './plugins/context-block'
 import type { RoleName } from './plugins/history-block'
-import type { Option } from './plugins/variable-picker'
+import type { ExternalToolOption, Option } from './plugins/variable-picker'
 import {
   UPDATE_DATASETS_EVENT_EMITTER,
   UPDATE_HISTORY_EVENT_EMITTER,
@@ -59,6 +59,8 @@ export type PromptEditorProps = {
   variableBlock?: {
     selectable?: boolean
     variables: Option[]
+    externalTools?: ExternalToolOption[]
+    onAddExternalTool?: () => void
   }
   historyBlock?: {
     show?: boolean
@@ -166,7 +168,11 @@ const PromptEditor: FC<PromptEditorProps> = ({
           queryDisabled={!queryBlock.selectable}
           queryShow={queryBlock.show}
         />
-        <VariablePicker items={variableBlock.variables} />
+        <VariablePicker
+          items={variableBlock.variables}
+          externalTools={variableBlock.externalTools}
+          onAddExternalTool={variableBlock.onAddExternalTool}
+        />
         {
           contextBlock.show && (
             <>

+ 100 - 5
web/app/components/base/prompt-editor/plugins/variable-picker.tsx

@@ -12,10 +12,14 @@ import { useBasicTypeaheadTriggerMatch } from '../hooks'
 import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block'
 import { $createCustomTextNode } from './custom-text/node'
 import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
+import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
+import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import AppIcon from '@/app/components/base/app-icon'
 
 class VariablePickerOption extends MenuOption {
   title: string
   icon?: JSX.Element
+  extraElement?: JSX.Element
   keywords: Array<string>
   keyboardShortcut?: string
   onSelect: (queryString: string) => void
@@ -24,6 +28,7 @@ class VariablePickerOption extends MenuOption {
     title: string,
     options: {
       icon?: JSX.Element
+      extraElement?: JSX.Element
       keywords?: Array<string>
       keyboardShortcut?: string
       onSelect: (queryString: string) => void
@@ -33,6 +38,7 @@ class VariablePickerOption extends MenuOption {
     this.title = title
     this.keywords = options.keywords || []
     this.icon = options.icon
+    this.extraElement = options.extraElement
     this.keyboardShortcut = options.keyboardShortcut
     this.onSelect = options.onSelect.bind(this)
   }
@@ -82,11 +88,12 @@ const VariablePickerMenuItem: FC<VariablePickerMenuItemProps> = ({
       <div className='mr-2'>
         {option.icon}
       </div>
-      <div className='text-[13px] text-gray-900'>
+      <div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
         {before}
         <span className='text-[#2970FF]'>{middle}</span>
         {after}
       </div>
+      {option.extraElement}
     </div>
   )
 }
@@ -96,11 +103,22 @@ export type Option = {
   name: string
 }
 
+export type ExternalToolOption = {
+  name: string
+  variableName: string
+  icon?: string
+  icon_background?: string
+}
+
 type VariablePickerProps = {
   items?: Option[]
+  externalTools?: ExternalToolOption[]
+  onAddExternalTool?: () => void
 }
 const VariablePicker: FC<VariablePickerProps> = ({
   items = [],
+  externalTools = [],
+  onAddExternalTool,
 }) => {
   const { t } = useTranslation()
   const [editor] = useLexicalComposerContext()
@@ -127,6 +145,30 @@ const VariablePicker: FC<VariablePickerProps> = ({
     return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
   }, [editor, queryString, items])
 
+  const toolOptions = useMemo(() => {
+    const baseToolOptions = externalTools.map((item) => {
+      return new VariablePickerOption(item.name, {
+        icon: (
+          <AppIcon
+            className='!w-[14px] !h-[14px]'
+            icon={item.icon}
+            background={item.icon_background}
+          />
+        ),
+        extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
+        onSelect: () => {
+          editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
+        },
+      })
+    })
+    if (!queryString)
+      return baseToolOptions
+
+    const regex = new RegExp(queryString, 'i')
+
+    return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
+  }, [editor, queryString, externalTools])
+
   const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), {
     icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
     onSelect: () => {
@@ -139,6 +181,15 @@ const VariablePicker: FC<VariablePickerProps> = ({
     },
   })
 
+  const newToolOption = new VariablePickerOption(t('common.promptEditor.variable.modal.addTool'), {
+    icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
+    extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
+    onSelect: () => {
+      if (onAddExternalTool)
+        onAddExternalTool()
+    },
+  })
+
   const onSelectOption = useCallback(
     (
       selectedOption: VariablePickerOption,
@@ -157,7 +208,7 @@ const VariablePicker: FC<VariablePickerProps> = ({
     [editor],
   )
 
-  const mergedOptions = [...options, newOption]
+  const mergedOptions = [...options, ...toolOptions, newOption, newToolOption]
 
   return (
     <LexicalTypeaheadMenuPlugin
@@ -195,26 +246,70 @@ const VariablePicker: FC<VariablePickerProps> = ({
                   </>
                 )
               }
+              {
+                !!toolOptions.length && (
+                  <>
+                    <div className='p-1'>
+                      {toolOptions.map((option, i: number) => (
+                        <VariablePickerMenuItem
+                          isSelected={selectedIndex === i + options.length}
+                          onClick={() => {
+                            setHighlightedIndex(i + options.length)
+                            selectOptionAndCleanUp(option)
+                          }}
+                          onMouseEnter={() => {
+                            setHighlightedIndex(i + options.length)
+                          }}
+                          key={option.key}
+                          option={option}
+                          queryString={queryString}
+                        />
+                      ))}
+                    </div>
+                    <div className='h-[1px] bg-gray-100' />
+                  </>
+                )
+              }
               <div className='p-1'>
                 <div
                   className={`
                     flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
-                    ${selectedIndex === options.length && 'bg-primary-50'}
+                    ${selectedIndex === options.length + toolOptions.length && 'bg-primary-50'}
                   `}
                   ref={newOption.setRefElement}
                   tabIndex={-1}
                   onClick={() => {
-                    setHighlightedIndex(options.length)
+                    setHighlightedIndex(options.length + toolOptions.length)
                     selectOptionAndCleanUp(newOption)
                   }}
                   onMouseEnter={() => {
-                    setHighlightedIndex(options.length)
+                    setHighlightedIndex(options.length + toolOptions.length)
                   }}
                   key={newOption.key}
                 >
                   {newOption.icon}
                   <div className='text-[13px] text-gray-900'>{newOption.title}</div>
                 </div>
+                <div
+                  className={`
+                    flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
+                    ${selectedIndex === options.length + toolOptions.length + 1 && 'bg-primary-50'}
+                  `}
+                  ref={newToolOption.setRefElement}
+                  tabIndex={-1}
+                  onClick={() => {
+                    setHighlightedIndex(options.length + toolOptions.length + 1)
+                    selectOptionAndCleanUp(newToolOption)
+                  }}
+                  onMouseEnter={() => {
+                    setHighlightedIndex(options.length + toolOptions.length + 1)
+                  }}
+                  key={newToolOption.key}
+                >
+                  {newToolOption.icon}
+                  <div className='grow text-[13px] text-gray-900'>{newToolOption.title}</div>
+                  {newToolOption.extraElement}
+                </div>
               </div>
             </div>,
             anchorElementRef.current,

+ 4 - 11
web/app/components/datasets/create/index.tsx

@@ -1,7 +1,6 @@
 'use client'
 import React, { useCallback, useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import { useBoolean } from 'ahooks'
 import AppUnavailable from '../../base/app-unavailable'
 import StepsNavBar from './steps-nav-bar'
 import StepOne from './step-one'
@@ -13,8 +12,7 @@ import { fetchDataSource } from '@/service/common'
 import { fetchDatasetDetail } from '@/service/datasets'
 import type { NotionPage } from '@/models/common'
 import { useProviderContext } from '@/context/provider-context'
-
-import AccountSetting from '@/app/components/header/account-setting'
+import { useModalContext } from '@/context/modal-context'
 
 type DatasetUpdateFormProps = {
   datasetId?: string
@@ -22,9 +20,8 @@ type DatasetUpdateFormProps = {
 
 const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
   const { t } = useTranslation()
-  const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
+  const { setShowAccountSettingModal } = useModalContext()
   const [hasConnection, setHasConnection] = useState(true)
-  const [isShowDataSourceSetting, { setTrue: showDataSourceSetting, setFalse: hideDataSourceSetting }] = useBoolean()
   const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE)
   const [step, setStep] = useState(1)
   const [indexingTypeCache, setIndexTypeCache] = useState('')
@@ -112,7 +109,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
       <div className="grow bg-white">
         {step === 1 && <StepOne
           hasConnection={hasConnection}
-          onSetting={showDataSourceSetting}
+          onSetting={() => setShowAccountSettingModal({ payload: 'data-source' })}
           datasetId={datasetId}
           dataSourceType={dataSourceType}
           dataSourceTypeDisable={!!detail?.data_source_type}
@@ -126,7 +123,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
         />}
         {(step === 2 && (!datasetId || (datasetId && !!detail))) && <StepTwo
           hasSetAPIKEY={!!embeddingsDefaultModel}
-          onSetting={showSetAPIKey}
+          onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
           indexingType={detail?.indexing_technique}
           datasetId={datasetId}
           dataSourceType={dataSourceType}
@@ -143,10 +140,6 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
           creationCache={result}
         />}
       </div>
-      {isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
-        hideSetAPIkey()
-      }} />}
-      {isShowDataSourceSetting && <AccountSetting activeTab="data-source" onCancel={hideDataSourceSetting}/>}
     </div>
   )
 }

+ 3 - 8
web/app/components/datasets/settings/form/index.tsx

@@ -16,8 +16,8 @@ import type { DataSet, DataSetListResponse } from '@/models/datasets'
 import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
 import type { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
 import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
-import AccountSetting from '@/app/components/header/account-setting'
 import DatasetDetailContext from '@/context/dataset-detail'
+import { useModalContext } from '@/context/modal-context'
 
 const rowClass = `
   flex justify-between py-4
@@ -45,12 +45,12 @@ const Form = () => {
   const { notify } = useContext(ToastContext)
   const { mutate } = useSWRConfig()
   const { dataset: currentDataset, mutateDatasetRes: mutateDatasets } = useContext(DatasetDetailContext)
+  const { setShowAccountSettingModal } = useModalContext()
   const [loading, setLoading] = useState(false)
   const [name, setName] = useState(currentDataset?.name ?? '')
   const [description, setDescription] = useState(currentDataset?.description ?? '')
   const [permission, setPermission] = useState(currentDataset?.permission)
   const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
-  const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
   const handleSave = async () => {
     if (loading)
       return
@@ -167,7 +167,7 @@ const Form = () => {
             </div>
             <div className='mt-2 w-full text-xs leading-6 text-gray-500'>
               {t('datasetSettings.form.embeddingModelTip')}
-              <span className='text-[#155eef] cursor-pointer' onClick={() => setShowSetAPIKeyModal(true)}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
+              <span className='text-[#155eef] cursor-pointer' onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
             </div>
           </div>
         </div>
@@ -186,11 +186,6 @@ const Form = () => {
           </div>
         </div>
       )}
-      {showSetAPIKeyModal && (
-        <AccountSetting activeTab="provider" onCancel={async () => {
-          setShowSetAPIKeyModal(false)
-        }} />
-      )}
     </div>
   )
 }

+ 3 - 0
web/app/components/datasets/settings/index-method-radio/index.tsx

@@ -14,12 +14,14 @@ type IIndexMethodRadioProps = {
   value?: DataSet['indexing_technique']
   onChange: (v?: DataSet['indexing_technique']) => void
   disable?: boolean
+  itemClassName?: string
 }
 
 const IndexMethodRadio = ({
   value,
   onChange,
   disable,
+  itemClassName,
 }: IIndexMethodRadioProps) => {
   const { t } = useTranslation()
   const options = [
@@ -45,6 +47,7 @@ const IndexMethodRadio = ({
             key={option.key}
             className={classNames(
               itemClass,
+              itemClassName,
               s.item,
               option.key === value && s['item-active'],
               disable && s.disable,

+ 1 - 0
web/app/components/develop/template/template.en.mdx

@@ -212,6 +212,7 @@ For high-quality text generation, such as articles, summaries, and translations,
         },
         {
           // ...
+        }
       ]
     }
     ```

+ 1 - 0
web/app/components/develop/template/template.zh.mdx

@@ -212,6 +212,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
         },
         {
           // ...
+        }
       ]
     }
     ```

+ 3 - 12
web/app/components/explore/universal-chat/config/plugins-config/index.tsx

@@ -7,7 +7,7 @@ import FeaturePanel from '@/app/components/app/configuration/base/feature-panel'
 import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins'
 import { getToolProviders } from '@/service/explore'
 import Loading from '@/app/components/base/loading'
-import AccountSetting from '@/app/components/header/account-setting'
+import { useModalContext } from '@/context/modal-context'
 
 export type IPluginsProps = {
   readonly?: boolean
@@ -27,6 +27,7 @@ const Plugins: FC<IPluginsProps> = ({
   onChange,
 }) => {
   const { t } = useTranslation()
+  const { setShowAccountSettingModal } = useModalContext()
   const [isLoading, setIsLoading] = React.useState(!readonly)
   const [isSerpApiValid, setIsSerpApiValid] = React.useState(false)
   const checkSerpApiKey = async () => {
@@ -42,8 +43,6 @@ const Plugins: FC<IPluginsProps> = ({
     checkSerpApiKey()
   }, [])
 
-  const [showSetSerpAPIKeyModal, setShowSetAPIKeyModal] = React.useState(false)
-
   const itemConfigs = plugins.map((plugin) => {
     const res: Record<string, any> = { ...plugin }
     const { key } = plugin
@@ -56,7 +55,7 @@ const Plugins: FC<IPluginsProps> = ({
       res.more = (
         <div className='border-t border-[#FEF0C7] flex items-center h-[34px] pl-2 bg-[#FFFAEB] text-gray-700 text-xs '>
           <span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.left')}</span>
-          <span className='cursor-pointer text-[#155EEF]' onClick={() => setShowSetAPIKeyModal(true)}>{t('explore.universalChat.plugins.google_search.more.link')}</span>
+          <span className='cursor-pointer text-[#155EEF]' onClick={() => setShowAccountSettingModal({ payload: 'plugin', onCancelCallback: async () => await checkSerpApiKey() })}>{t('explore.universalChat.plugins.google_search.more.link')}</span>
           <span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.right')}</span>
         </div>
       )
@@ -98,14 +97,6 @@ const Plugins: FC<IPluginsProps> = ({
             ))}
           </div>)}
       </FeaturePanel>
-      {
-        showSetSerpAPIKeyModal && (
-          <AccountSetting activeTab="plugin" onCancel={async () => {
-            setShowSetAPIKeyModal(false)
-            await checkSerpApiKey()
-          }} />
-        )
-      }
     </>
   )
 }

+ 3 - 6
web/app/components/header/account-dropdown/index.tsx

@@ -7,7 +7,6 @@ import classNames from 'classnames'
 import Link from 'next/link'
 import { Menu, Transition } from '@headlessui/react'
 import Indicator from '../indicator'
-import AccountSetting from '../account-setting'
 import AccountAbout from '../account-about'
 import WorkplaceSelector from './workplace-selector'
 import I18n from '@/context/i18n'
@@ -16,6 +15,7 @@ import { logout } from '@/service/common'
 import { useAppContext } from '@/context/app-context'
 import { ArrowUpRight, ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
 import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
+import { useModalContext } from '@/context/modal-context'
 
 export default function AppSelector() {
   const itemClassName = `
@@ -23,12 +23,12 @@ export default function AppSelector() {
     rounded-lg font-normal hover:bg-gray-50 cursor-pointer
   `
   const router = useRouter()
-  const [settingVisible, setSettingVisible] = useState(false)
   const [aboutVisible, setAboutVisible] = useState(false)
 
   const { locale } = useContext(I18n)
   const { t } = useTranslation()
   const { userProfile, langeniusVersionInfo } = useAppContext()
+  const { setShowAccountSettingModal } = useModalContext()
 
   const handleLogout = async () => {
     await logout({
@@ -89,7 +89,7 @@ export default function AppSelector() {
                   </div>
                   <div className="px-1 py-1">
                     <Menu.Item>
-                      <div className={itemClassName} onClick={() => setSettingVisible(true)}>
+                      <div className={itemClassName} onClick={() => setShowAccountSettingModal({ payload: 'account' })}>
                         <div>{t('common.userProfile.settings')}</div>
                       </div>
                     </Menu.Item>
@@ -134,9 +134,6 @@ export default function AppSelector() {
           )
         }
       </Menu>
-      {
-        settingVisible && <AccountSetting onCancel={() => setSettingVisible(false)} />
-      }
       {
         aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langeniusVersionInfo={langeniusVersionInfo} />
       }

+ 26 - 0
web/app/components/header/account-setting/api-based-extension-page/empty.tsx

@@ -0,0 +1,26 @@
+import { useTranslation } from 'react-i18next'
+import { Webhooks } from '@/app/components/base/icons/src/vender/line/development'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+
+const Empty = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='mb-2 p-6 rounded-2xl bg-gray-50'>
+      <div className='flex items-center justify-center mb-3 w-12 h-12 rounded-[10px] border border-[#EAECF5]'>
+        <Webhooks className='w-6 h-6 text-gray-500' />
+      </div>
+      <div className='mb-2 text-sm text-gray-600'>{t('commosn.apiBasedExtension.title')}</div>
+      <a
+        className='flex items-center mb-2 h-[18px] text-xs text-primary-600'
+        href={t('common.apiBasedExtension.linkUrl') || '/'}
+        target='_blank'
+      >
+        <BookOpen01 className='mr-1 w-3 h-3' />
+        {t('common.apiBasedExtension.link')}
+      </a>
+    </div>
+  )
+}
+
+export default Empty

+ 53 - 0
web/app/components/header/account-setting/api-based-extension-page/index.tsx

@@ -0,0 +1,53 @@
+import { useTranslation } from 'react-i18next'
+import useSWR from 'swr'
+import Item from './item'
+import Empty from './empty'
+import { useModalContext } from '@/context/modal-context'
+import { Plus } from '@/app/components/base/icons/src/vender/line/general'
+import { fetchApiBasedExtensionList } from '@/service/common'
+
+const ApiBasedExtensionPage = () => {
+  const { t } = useTranslation()
+  const { setShowApiBasedExtensionModal } = useModalContext()
+  const { data, mutate, isLoading } = useSWR(
+    '/api-based-extension',
+    fetchApiBasedExtensionList,
+  )
+
+  const handleOpenApiBasedExtensionModal = () => {
+    setShowApiBasedExtensionModal({
+      payload: {},
+      onSaveCallback: () => mutate(),
+    })
+  }
+
+  return (
+    <div>
+      {
+        !isLoading && !data?.length && (
+          <Empty />
+        )
+      }
+      {
+        !isLoading && !!data?.length && (
+          data.map(item => (
+            <Item
+              key={item.id}
+              data={item}
+              onUpdate={() => mutate()}
+            />
+          ))
+        )
+      }
+      <div
+        className='flex items-center justify-center px-3 h-8 text-[13px] font-medium text-gray-700 rounded-lg bg-gray-50 cursor-pointer'
+        onClick={handleOpenApiBasedExtensionModal}
+      >
+        <Plus className='mr-2 w-4 h-4' />
+        {t('common.apiBasedExtension.add')}
+      </div>
+    </div>
+  )
+}
+
+export default ApiBasedExtensionPage

+ 75 - 0
web/app/components/header/account-setting/api-based-extension-page/item.tsx

@@ -0,0 +1,75 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Edit02, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
+import type { ApiBasedExtension } from '@/models/common'
+import { useModalContext } from '@/context/modal-context'
+import { deleteApiBasedExtension } from '@/service/common'
+import ConfirmCommon from '@/app/components/base/confirm/common'
+
+type ItemProps = {
+  data: ApiBasedExtension
+  onUpdate: () => void
+}
+const Item: FC<ItemProps> = ({
+  data,
+  onUpdate,
+}) => {
+  const { t } = useTranslation()
+  const { setShowApiBasedExtensionModal } = useModalContext()
+  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
+
+  const handleOpenApiBasedExtensionModal = () => {
+    setShowApiBasedExtensionModal({
+      payload: data,
+      onSaveCallback: () => onUpdate(),
+    })
+  }
+  const handleDeleteApiBasedExtension = async () => {
+    await deleteApiBasedExtension(`/api-based-extension/${data.id}`)
+
+    setShowDeleteConfirm(false)
+    onUpdate()
+  }
+
+  return (
+    <div className='group flex items-center mb-2 px-4 py-2 border-[0.5px] border-transparent rounded-xl bg-gray-50 hover:border-gray-200 hover:shadow-xs'>
+      <div className='grow'>
+        <div className='mb-0.5 text-[13px] font-medium text-gray-700'>{data.name}</div>
+        <div className='text-xs text-gray-500'>{data.api_endpoint}</div>
+      </div>
+      <div className='hidden group-hover:flex items-center'>
+        <div
+          className='flex items-center mr-1 px-3 h-7 bg-white text-xs font-medium text-gray-700 rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer'
+          onClick={handleOpenApiBasedExtensionModal}
+        >
+          <Edit02 className='mr-[5px] w-3.5 h-3.5' />
+          {t('common.operation.edit')}
+        </div>
+        <div
+          className='flex items-center justify-center w-7 h-7 bg-white text-gray-700 rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer'
+          onClick={() => setShowDeleteConfirm(true)}
+        >
+          <Trash03 className='w-4 h-4' />
+        </div>
+      </div>
+      {
+        showDeleteConfirm && (
+          <ConfirmCommon
+            type='danger'
+            isShow={showDeleteConfirm}
+            onCancel={() => setShowDeleteConfirm(false)}
+            title={`${t('common.operation.delete')} “${data.name}”?`}
+            onConfirm={handleDeleteApiBasedExtension}
+            desc={t('common.apiBasedExtension.confirm.desc') || ''}
+            confirmWrapperClassName='!z-30'
+            confirmText={t('common.operation.delete') || ''}
+            confirmBtnClassName='!bg-[#D92D20]'
+          />
+        )
+      }
+    </div>
+  )
+}
+
+export default Item

+ 151 - 0
web/app/components/header/account-setting/api-based-extension-page/modal.tsx

@@ -0,0 +1,151 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+import type { ApiBasedExtension } from '@/models/common'
+import {
+  addApiBasedExtension,
+  updateApiBasedExtension,
+} from '@/service/common'
+import { useToastContext } from '@/app/components/base/toast'
+
+export type ApiBasedExtensionData = {
+  name?: string
+  apiEndpoint?: string
+  apiKey?: string
+}
+
+type ApiBasedExtensionModalProps = {
+  data: ApiBasedExtension
+  onCancel: () => void
+  onSave?: (newData: ApiBasedExtension) => void
+}
+const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
+  data,
+  onCancel,
+  onSave,
+}) => {
+  const { t } = useTranslation()
+  const [localeData, setLocaleData] = useState(data)
+  const [loading, setLoading] = useState(false)
+  const { notify } = useToastContext()
+  const handleDataChange = (type: string, value: string) => {
+    setLocaleData({ ...localeData, [type]: value })
+  }
+  const handleSave = async () => {
+    setLoading(true)
+
+    if (localeData && localeData.api_key && localeData.api_key?.length < 5) {
+      notify({ type: 'error', message: t('common.apiBasedExtension.modal.apiKey.lengthError') })
+      setLoading(false)
+      return
+    }
+
+    try {
+      let res: ApiBasedExtension = {}
+      if (!data.id) {
+        res = await addApiBasedExtension({
+          url: '/api-based-extension',
+          body: localeData,
+        })
+      }
+      else {
+        res = await updateApiBasedExtension({
+          url: `/api-based-extension/${data.id}`,
+          body: {
+            ...localeData,
+            api_key: data.api_key === localeData.api_key ? '[__HIDDEN__]' : localeData.api_key,
+          },
+        })
+
+        notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
+      }
+
+      if (onSave)
+        onSave(res)
+    }
+    finally {
+      setLoading(false)
+    }
+  }
+
+  return (
+    <Modal
+      isShow
+      onClose={() => {}}
+      wrapperClassName='!z-30'
+      className='!p-8 !pb-6 !max-w-none !w-[640px]'
+    >
+      <div className='mb-2 text-xl font-semibold text-gray-900'>
+        {
+          data.name
+            ? t('common.apiBasedExtension.modal.editTitle')
+            : t('common.apiBasedExtension.modal.title')
+        }
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('common.apiBasedExtension.modal.name.title')}
+        </div>
+        <input
+          value={localeData.name || ''}
+          onChange={e => handleDataChange('name', e.target.value)}
+          className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+          placeholder={t('common.apiBasedExtension.modal.name.placeholder') || ''}
+        />
+      </div>
+      <div className='py-2'>
+        <div className='flex justify-between items-center h-9 text-sm font-medium text-gray-900'>
+          {t('common.apiBasedExtension.modal.apiEndpoint.title')}
+          <a
+            href={t('common.apiBasedExtension.linkUrl') || '/'}
+            target='_blank'
+            className='group flex items-center text-xs text-gray-500 font-normal hover:text-primary-600'
+          >
+            <BookOpen01 className='mr-1 w-3 h-3 text-gray-500 group-hover:text-primary-600' />
+            {t('common.apiBasedExtension.link')}
+          </a>
+        </div>
+        <input
+          value={localeData.api_endpoint || ''}
+          onChange={e => handleDataChange('api_endpoint', e.target.value)}
+          className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+          placeholder={t('common.apiBasedExtension.modal.apiEndpoint.placeholder') || ''}
+        />
+      </div>
+      <div className='py-2'>
+        <div className='leading-9 text-sm font-medium text-gray-900'>
+          {t('common.apiBasedExtension.modal.apiKey.title')}
+        </div>
+        <div className='flex items-center'>
+          <input
+            value={localeData.api_key || ''}
+            onChange={e => handleDataChange('api_key', e.target.value)}
+            className='block grow mr-2 px-3 h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+            placeholder={t('common.apiBasedExtension.modal.apiKey.placeholder') || ''}
+          />
+        </div>
+      </div>
+      <div className='flex items-center justify-end mt-6'>
+        <Button
+          onClick={onCancel}
+          className='mr-2 text-sm font-medium'
+        >
+          {t('common.operation.cancel')}
+        </Button>
+        <Button
+          type='primary'
+          className='text-sm font-medium'
+          disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading}
+          onClick={handleSave}
+        >
+          {t('common.operation.save')}
+        </Button>
+      </div>
+    </Modal>
+  )
+}
+
+export default ApiBasedExtensionModal

+ 119 - 0
web/app/components/header/account-setting/api-based-extension-page/selector.tsx

@@ -0,0 +1,119 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import useSWR from 'swr'
+import { useTranslation } from 'react-i18next'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import {
+  ArrowUpRight,
+  ChevronDown,
+} from '@/app/components/base/icons/src/vender/line/arrows'
+import { Plus } from '@/app/components/base/icons/src/vender/line/general'
+import { useModalContext } from '@/context/modal-context'
+import { fetchApiBasedExtensionList } from '@/service/common'
+
+type ApiBasedExtensionSelectorProps = {
+  value: string
+  onChange: (value: string) => void
+}
+
+const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+  const {
+    setShowAccountSettingModal,
+    setShowApiBasedExtensionModal,
+  } = useModalContext()
+  const { data } = useSWR(
+    '/api-based-extension',
+    fetchApiBasedExtensionList,
+  )
+  const handleSelect = (id: string) => {
+    onChange(id)
+    setOpen(false)
+  }
+
+  const currentItem = data?.find(item => item.id === value)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-start'
+      offset={4}
+    >
+      <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} className='w-full'>
+        {
+          currentItem
+            ? (
+              <div className='flex items-center justify-between pl-3 pr-2.5 h-9 bg-gray-100 rounded-lg cursor-pointer'>
+                <div className='text-sm text-gray-900'>{currentItem.name}</div>
+                <div className='flex items-center'>
+                  <div className='mr-1.5 w-[270px] text-xs text-gray-400 truncate text-right'>
+                    {currentItem.api_endpoint}
+                  </div>
+                  <ChevronDown className={`w-4 h-4 text-gray-700 ${!open && 'opacity-60'}`} />
+                </div>
+              </div>
+            )
+            : (
+              <div className='flex items-center justify-between pl-3 pr-2.5 h-9 bg-gray-100 rounded-lg text-sm text-gray-400 cursor-pointer'>
+                {t('common.apiBasedExtension.selector.placeholder')}
+                <ChevronDown className={`w-4 h-4 text-gray-700 ${!open && 'opacity-60'}`} />
+              </div>
+            )
+        }
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className='w-[576px] z-[11]'>
+        <div className='w-full rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg z-10'>
+          <div className='p-1'>
+            <div className='flex items-center justify-between px-3 pt-2 pb-1'>
+              <div className='text-xs font-medium text-gray-500'>
+                {t('common.apiBasedExtension.selector.title')}
+              </div>
+              <div
+                className='flex items-center text-xs text-primary-600 cursor-pointer'
+                onClick={() => setShowAccountSettingModal({ payload: 'api-based-extension' })}
+              >
+                {t('common.apiBasedExtension.selector.manage')}
+                <ArrowUpRight className='ml-0.5 w-3 h-3' />
+              </div>
+            </div>
+            <div className='max-h-[250px] overflow-y-auto'>
+              {
+                data?.map(item => (
+                  <div
+                    key={item.id}
+                    className='px-3 py-1.5 w-full cursor-pointer hover:bg-gray-50 rounded-md text-left'
+                    onClick={() => handleSelect(item.id!)}
+                  >
+                    <div className='text-sm text-gray-900'>{item.name}</div>
+                    <div className='text-xs text-gray-500'>{item.api_endpoint}</div>
+                  </div>
+                ))
+              }
+            </div>
+          </div>
+          <div className='h-[1px] bg-gray-100' />
+          <div className='p-1'>
+            <div
+              className='flex items-center px-3 h-8 text-sm text-primary-600 cursor-pointer'
+              onClick={() => setShowApiBasedExtensionModal({ payload: {} })}
+            >
+              <Plus className='mr-2 w-4 h-4' />
+              {t('common.operation.add')}
+            </div>
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default ApiBasedExtensionSelector

+ 16 - 2
web/app/components/header/account-setting/index.tsx

@@ -7,11 +7,16 @@ import MembersPage from './members-page'
 import IntegrationsPage from './Integrations-page'
 import LanguagePage from './language-page'
 import PluginPage from './plugin-page'
+import ApiBasedExtensionPage from './api-based-extension-page'
 import DataSourcePage from './data-source-page'
 import ModelPage from './model-page'
 import s from './index.module.css'
 import Modal from '@/app/components/base/modal'
-import { Database03, PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
+import {
+  Database03,
+  PuzzlePiece01,
+  Webhooks,
+} from '@/app/components/base/icons/src/vender/line/development'
 import { Database03 as Database03Solid, PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
 import { User01, Users01 } from '@/app/components/base/icons/src/vender/line/users'
 import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components/base/icons/src/vender/solid/users'
@@ -66,6 +71,12 @@ export default function AccountSetting({
           icon: <PuzzlePiece01 className={iconClassName} />,
           activeIcon: <PuzzlePiece01Solid className={iconClassName} />,
         },
+        {
+          key: 'api-based-extension',
+          name: t('common.settings.apiBasedExtension'),
+          icon: <Webhooks className={iconClassName} />,
+          activeIcon: <Webhooks className={iconClassName} />,
+        },
       ],
     },
     {
@@ -135,9 +146,11 @@ export default function AccountSetting({
                             flex items-center h-[37px] mb-[2px] text-sm cursor-pointer rounded-lg
                             ${activeMenu === item.key ? 'font-semibold text-primary-600 bg-primary-50' : 'font-light text-gray-700'}
                           `}
+                          title={item.name}
                           onClick={() => setActiveMenu(item.key)}
                         >
-                          {activeMenu === item.key ? item.activeIcon : item.icon}{item.name}
+                          {activeMenu === item.key ? item.activeIcon : item.icon}
+                          <div className='truncate'>{item.name}</div>
                         </div>
                       ))
                     }
@@ -162,6 +175,7 @@ export default function AccountSetting({
             {activeMenu === 'provider' && <ModelPage />}
             {activeMenu === 'data-source' && <DataSourcePage />}
             {activeMenu === 'plugin' && <PluginPage />}
+            {activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> }
           </div>
         </div>
       </div>

+ 3 - 14
web/app/components/header/account-setting/model-page/model-selector/index.tsx

@@ -19,7 +19,7 @@ import { useProviderContext } from '@/context/provider-context'
 import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
 import type { ModelModeType } from '@/types/app'
 import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
-import AccountSetting from '@/app/components/header/account-setting'
+import { useModalContext } from '@/context/modal-context'
 
 type Props = {
   value: {
@@ -59,6 +59,7 @@ const ModelSelector: FC<Props> = ({
   triggerIconSmall,
 }) => {
   const { t } = useTranslation()
+  const { setShowAccountSettingModal } = useModalContext()
   const { textGenerationModelList, embeddingsModelList, speech2textModelList, agentThoughtModelList } = useProviderContext()
   const [search, setSearch] = useState('')
   const modelList = supportAgentThought
@@ -112,8 +113,6 @@ const ModelSelector: FC<Props> = ({
     return res
   })()
 
-  const [showSettingModal, setShowSettingModal] = useState(false)
-
   return (
     <div className=''>
       <Popover className='relative'>
@@ -248,9 +247,7 @@ const ModelSelector: FC<Props> = ({
                   style={{
                     borderColor: 'rgba(0, 0, 0, 0.05)',
                   }}
-                  onClick={() => {
-                    setShowSettingModal(true)
-                  }}
+                  onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
                 >
                   <CubeOutline className='w-4 h-4 mr-2' />
                   <div>{t('common.model.addMoreModel')}</div>
@@ -260,14 +257,6 @@ const ModelSelector: FC<Props> = ({
           </Transition>
         )}
       </Popover>
-
-      {
-        showSettingModal && (
-          <AccountSetting activeTab="provider" onCancel={async () => {
-            setShowSettingModal(false)
-          }} />
-        )
-      }
     </div>
   )
 }

+ 17 - 1
web/app/components/share/chat/index.tsx

@@ -368,7 +368,7 @@ const Main: FC<IMainProps> = ({
         const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
         setAllConversationList(allConversations)
         // fetch new conversation info
-        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams
+        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, sensitive_word_avoidance }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
         if (siteInfo.default_language)
           changeLanguage(siteInfo.default_language)
@@ -555,6 +555,22 @@ const Main: FC<IMainProps> = ({
           setChatList(newListWithAnswer)
         }
         : undefined,
+      onMessageReplace: (messageReplace) => {
+        if (isInstalledApp) {
+          responseItem.content = messageReplace.answer
+        }
+        else {
+          setChatList(produce(
+            getChatList(),
+            (draft) => {
+              const current = draft.find(item => item.id === messageReplace.id)
+
+              if (current)
+                current.content = messageReplace.answer
+            },
+          ))
+        }
+      },
       onError() {
         setResponsingFalse()
         // role back placeholder answer

+ 12 - 1
web/app/components/share/chatbot/index.tsx

@@ -292,7 +292,7 @@ const Main: FC<IMainProps> = ({
         const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
         setAllConversationList(allConversations)
         // fetch new conversation info
-        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams
+        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, sensitive_word_avoidance }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
         if (siteInfo.default_language)
           changeLanguage(siteInfo.default_language)
@@ -455,6 +455,17 @@ const Main: FC<IMainProps> = ({
           setIsShowSuggestion(true)
         }
       },
+      onMessageReplace: (messageReplace) => {
+        setChatList(produce(
+          getChatList(),
+          (draft) => {
+            const current = draft.find(item => item.id === messageReplace.id)
+
+            if (current)
+              current.content = messageReplace.answer
+          },
+        ))
+      },
       onError(errorMessage, errorCode) {
         if (['provider_not_initialize', 'completion_request_error'].includes(errorCode as string))
           setShouldReload(true)

+ 2 - 1
web/app/components/share/text-generation/index.tsx

@@ -26,6 +26,7 @@ import SavedItems from '@/app/components/app/text-generate/saved-items'
 import type { InstalledApp } from '@/models/explore'
 import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
 import Toast from '@/app/components/base/toast'
+
 const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
 enum TaskStatus {
   pending = 'pending',
@@ -337,7 +338,7 @@ const TextGeneration: FC<IMainProps> = ({
       setSiteInfo(siteInfo as SiteInfo)
       changeLanguage(siteInfo.default_language)
 
-      const { user_input_form, more_like_this }: any = appParams
+      const { user_input_form, more_like_this, sensitive_word_avoidance }: any = appParams
       const prompt_variables = userInputsFormToPromptVariables(user_input_form)
       setPromptConfig({
         prompt_template: '', // placeholder for feture

+ 8 - 1
web/app/components/share/text-generation/result/index.tsx

@@ -12,6 +12,7 @@ import type { Feedbacktype } from '@/app/components/app/chat/type'
 import Loading from '@/app/components/base/loading'
 import type { PromptConfig } from '@/models/debug'
 import type { InstalledApp } from '@/models/explore'
+import type { ModerationService } from '@/models/common'
 export type IResultProps = {
   isCallBatchAPI: boolean
   isPC: boolean
@@ -29,6 +30,8 @@ export type IResultProps = {
   handleSaveMessage: (messageId: string) => void
   taskId?: number
   onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
+  enableModeration?: boolean
+  moderationService?: (text: string) => ReturnType<ModerationService>
 }
 
 const Result: FC<IResultProps> = ({
@@ -127,7 +130,7 @@ const Result: FC<IResultProps> = ({
     })
     setCompletionRes('')
 
-    const res: string[] = []
+    let res: string[] = []
     let tempMessageId = ''
 
     if (!isPC)
@@ -160,6 +163,10 @@ const Result: FC<IResultProps> = ({
         onCompleted(getCompletionRes(), taskId, true)
         clearInterval(runId)
       },
+      onMessageReplace: (messageReplace) => {
+        res = [messageReplace.answer]
+        setCompletionRes(res.join(''))
+      },
       onError() {
         if (isTimeout)
           return

+ 29 - 1
web/context/debug-configuration.ts

@@ -1,6 +1,23 @@
 import { createContext } from 'use-context-selector'
 import { PromptMode } from '@/models/debug'
-import type { BlockStatus, ChatPromptConfig, CitationConfig, CompletionParams, CompletionPromptConfig, ConversationHistoriesRole, DatasetConfigs, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptItem, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
+import type {
+  BlockStatus,
+  ChatPromptConfig,
+  CitationConfig,
+  CompletionParams,
+  CompletionPromptConfig,
+  ConversationHistoriesRole,
+  DatasetConfigs,
+  Inputs,
+  ModelConfig,
+  ModerationConfig,
+  MoreLikeThisConfig,
+  PromptConfig,
+  PromptItem,
+  SpeechToTextConfig,
+  SuggestedQuestionsAfterAnswerConfig,
+} from '@/models/debug'
+import type { ExternalDataTool } from '@/models/common'
 import type { DataSet } from '@/models/datasets'
 import { ModelModeType } from '@/types/app'
 import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
@@ -40,6 +57,10 @@ type IDebugConfiguration = {
   setSpeechToTextConfig: (speechToTextConfig: SpeechToTextConfig) => void
   citationConfig: CitationConfig
   setCitationConfig: (citationConfig: CitationConfig) => void
+  moderationConfig: ModerationConfig
+  setModerationConfig: (moderationConfig: ModerationConfig) => void
+  externalDataToolsConfig: ExternalDataTool[]
+  setExternalDataToolsConfig: (externalDataTools: ExternalDataTool[]) => void
   formattingChanged: boolean
   setFormattingChanged: (formattingChanged: boolean) => void
   inputs: Inputs
@@ -114,6 +135,12 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
     enabled: false,
   },
   setCitationConfig: () => {},
+  moderationConfig: {
+    enabled: false,
+  },
+  setModerationConfig: () => {},
+  externalDataToolsConfig: [],
+  setExternalDataToolsConfig: () => {},
   formattingChanged: false,
   setFormattingChanged: () => { },
   inputs: {},
@@ -141,6 +168,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
     suggested_questions_after_answer: null,
     speech_to_text: null,
     retriever_resource: null,
+    sensitive_word_avoidance: null,
     dataSets: [],
   },
   setModelConfig: () => { },

+ 140 - 0
web/context/modal-context.tsx

@@ -0,0 +1,140 @@
+'use client'
+
+import type { Dispatch, SetStateAction } from 'react'
+import { useState } from 'react'
+import { createContext, useContext } from 'use-context-selector'
+import AccountSetting from '@/app/components/header/account-setting'
+import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal'
+import ModerationSettingModal from '@/app/components/app/configuration/toolbox/moderation/moderation-setting-modal'
+import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal'
+import type { ModerationConfig } from '@/models/debug'
+import type {
+  ApiBasedExtension,
+  ExternalDataTool,
+} from '@/models/common'
+
+export type ModalState<T> = {
+  payload: T
+  onCancelCallback?: () => void
+  onSaveCallback?: (newPayload: T) => void
+  onValidateBeforeSaveCallback?: (newPayload: T) => boolean
+}
+
+const ModalContext = createContext<{
+  setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<string> | null>>
+  setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
+  setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
+  setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
+}>({
+  setShowAccountSettingModal: () => {},
+  setShowApiBasedExtensionModal: () => {},
+  setShowModerationSettingModal: () => {},
+  setShowExternalDataToolModal: () => {},
+})
+
+export const useModalContext = () => useContext(ModalContext)
+
+type ModalContextProviderProps = {
+  children: React.ReactNode
+}
+export const ModalContextProvider = ({
+  children,
+}: ModalContextProviderProps) => {
+  const [showAccountSettingModal, setShowAccountSettingModal] = useState<ModalState<string> | null>(null)
+  const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
+  const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
+  const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
+
+  const handleCancelAccountSettingModal = () => {
+    setShowAccountSettingModal(null)
+
+    if (showAccountSettingModal?.onCancelCallback)
+      showAccountSettingModal?.onCancelCallback()
+  }
+
+  const handleCancelModerationSettingModal = () => {
+    setShowModerationSettingModal(null)
+
+    if (showModerationSettingModal?.onCancelCallback)
+      showModerationSettingModal.onCancelCallback()
+  }
+
+  const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
+    if (showApiBasedExtensionModal?.onSaveCallback)
+      showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
+
+    setShowApiBasedExtensionModal(null)
+  }
+
+  const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
+    if (showModerationSettingModal?.onSaveCallback)
+      showModerationSettingModal.onSaveCallback(newModerationConfig)
+
+    setShowModerationSettingModal(null)
+  }
+
+  const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
+    if (showExternalDataToolModal?.onSaveCallback)
+      showExternalDataToolModal.onSaveCallback(newExternalDataTool)
+
+    setShowExternalDataToolModal(null)
+  }
+
+  const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
+    if (showExternalDataToolModal?.onValidateBeforeSaveCallback)
+      return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool)
+
+    return true
+  }
+
+  return (
+    <ModalContext.Provider value={{
+      setShowAccountSettingModal,
+      setShowApiBasedExtensionModal,
+      setShowModerationSettingModal,
+      setShowExternalDataToolModal,
+    }}>
+      <>
+        {children}
+        {
+          !!showAccountSettingModal && (
+            <AccountSetting
+              activeTab={showAccountSettingModal.payload}
+              onCancel={handleCancelAccountSettingModal}
+            />
+          )
+        }
+        {
+          !!showApiBasedExtensionModal && (
+            <ApiBasedExtensionModal
+              data={showApiBasedExtensionModal.payload}
+              onCancel={() => setShowApiBasedExtensionModal(null)}
+              onSave={handleSaveApiBasedExtension}
+            />
+          )
+        }
+        {
+          !!showModerationSettingModal && (
+            <ModerationSettingModal
+              data={showModerationSettingModal.payload}
+              onCancel={handleCancelModerationSettingModal}
+              onSave={handleSaveModeration}
+            />
+          )
+        }
+        {
+          !!showExternalDataToolModal && (
+            <ExternalDataToolModal
+              data={showExternalDataToolModal.payload}
+              onCancel={() => setShowExternalDataToolModal(null)}
+              onSave={handleSaveExternalDataTool}
+              onValidateBeforeSave={handleValidateBeforeSaveExternalDataTool}
+            />
+          )
+        }
+      </>
+    </ModalContext.Provider>
+  )
+}
+
+export default ModalContext

+ 49 - 0
web/hooks/use-moderate.ts

@@ -0,0 +1,49 @@
+import { useEffect, useRef, useState } from 'react'
+import type { ModerationService } from '@/models/common'
+
+function splitStringByLength(inputString: string, chunkLength: number) {
+  const resultArray = []
+  for (let i = 0; i < inputString.length; i += chunkLength)
+    resultArray.push(inputString.substring(i, i + chunkLength))
+
+  return resultArray
+}
+
+export const useModerate = (
+  content: string,
+  stop: boolean,
+  moderationService: (text: string) => ReturnType<ModerationService>,
+  seperateLength = 50,
+) => {
+  const moderatedContentMap = useRef<Map<number, string>>(new Map())
+  const moderatingIndex = useRef<number[]>([])
+  const [contentArr, setContentArr] = useState<string[]>([])
+
+  const handleModerate = () => {
+    const stringArr = splitStringByLength(content, seperateLength)
+
+    const lastIndex = stringArr.length - 1
+    stringArr.forEach((item, index) => {
+      if (!(index in moderatingIndex.current) && !moderatedContentMap.current.get(index)) {
+        if (index === lastIndex && !stop)
+          return
+
+        moderatingIndex.current.push(index)
+        moderationService(item).then((res) => {
+          if (res.flagged) {
+            moderatedContentMap.current.set(index, res.text)
+            setContentArr([...stringArr.slice(0, index), res.text, ...stringArr.slice(index + 1)])
+          }
+        })
+      }
+    })
+
+    setContentArr(stringArr)
+  }
+  useEffect(() => {
+    if (content)
+      handleModerate()
+  }, [content, stop])
+
+  return contentArr.map((item, index) => moderatedContentMap.current.get(index) || item).join('')
+}

+ 61 - 0
web/i18n/lang/app-debug.en.ts

@@ -98,6 +98,26 @@ const translation = {
         deleteContextVarTip: 'This variable has been set as a context query variable, and removing it will impact the normal use of the dataset. If you still need to delete it, please reselect it in the context section.',
       },
     },
+    tools: {
+      title: 'Tools',
+      tips: 'Tools provide a standard API call method, taking user input or variables as request parameters for querying external data as context.',
+      toolsInUse: '{{count}} tools in use',
+      modal: {
+        title: 'Tool',
+        toolType: {
+          title: 'Tool Type',
+          placeholder: 'Please select the tool type',
+        },
+        name: {
+          title: 'Name',
+          placeholder: 'Please enter the name',
+        },
+        variableName: {
+          title: 'Variable Name',
+          placeholder: 'Please enter the variable name',
+        },
+      },
+    },
     conversationHistory: {
       title: 'Conversation History',
       description: 'Set prefix names for conversation roles',
@@ -109,6 +129,47 @@ const translation = {
         assistantPrefix: 'Assistant prefix',
       },
     },
+    toolbox: {
+      title: 'TOOLBOX',
+    },
+    moderation: {
+      title: 'Content moderation',
+      description: 'Content moderation',
+      allEnabled: 'INPUT/OUTPUT Content Enabled',
+      inputEnabled: 'INPUT Content Enabled',
+      outputEnabled: 'OUTPUT Content Enabled',
+      modal: {
+        title: 'Content moderation settings',
+        provider: {
+          title: 'Provider',
+          openai: 'OpenAI Moderation',
+          openaiTip: {
+            prefix: 'OpenAI Moderation requires an OpenAI API key configured in the ',
+            suffix: '.',
+          },
+          keywords: 'Keywords',
+        },
+        keywords: {
+          tip: 'One per line, separated by line breaks. Up to 100 characters per line.',
+          placeholder: 'One per line, separated by line breaks',
+          line: 'Line',
+        },
+        content: {
+          input: 'Moderate INPUT Content',
+          output: 'Moderate OUTPUT Content',
+          preset: 'Preset replies',
+          placeholder: 'Preset replies content here',
+          condition: 'Moderate INPUT and OUTPUT Content enabled at least one',
+          fromApi: 'Preset replies are returned by API',
+          errorMessage: 'Preset replies cannot be empty',
+          supportMarkdown: 'Markdown supported',
+        },
+        openaiNotConfig: {
+          before: 'OpenAI Moderation requires an OpenAI API key configured in the',
+          after: '',
+        },
+      },
+    },
   },
   automatic: {
     title: 'Automated application orchestration',

+ 61 - 0
web/i18n/lang/app-debug.zh.ts

@@ -98,6 +98,26 @@ const translation = {
         deleteContextVarTip: '该变量已被设置为上下文查询变量,删除该变量将影响数据集的正常使用。 如果您仍需要删除它,请在上下文部分中重新选择它。',
       },
     },
+    tools: {
+      title: '工具',
+      tips: '工具提供了一个标准的 API 调用方式,将用户输入或变量作为 API 的请求参数,用于查询外部数据作为上下文。',
+      toolsInUse: '{{count}} 工具使用中',
+      modal: {
+        title: '工具',
+        toolType: {
+          title: '工具类型',
+          placeholder: '请选择工具类型',
+        },
+        name: {
+          title: '名称',
+          placeholder: '请填写名称',
+        },
+        variableName: {
+          title: '变量名称',
+          placeholder: '请填写变量名称',
+        },
+      },
+    },
     conversationHistory: {
       title: '对话历史',
       description: '设置对话角色的前缀名称',
@@ -109,6 +129,47 @@ const translation = {
         assistantPrefix: '助手前缀',
       },
     },
+    toolbox: {
+      title: '工具箱',
+    },
+    moderation: {
+      title: '内容审核',
+      description: '内容审核',
+      allEnabled: '审核输入/审核输出 内容已启用',
+      inputEnabled: '审核输入内容已启用',
+      outputEnabled: '审核输出内容已启用',
+      modal: {
+        title: '内容审核设置',
+        provider: {
+          title: '类别',
+          openai: 'OpenAI Moderation',
+          openaiTip: {
+            prefix: 'OpenAI Moderation 需要在',
+            suffix: '中配置 OpenAI API 密钥。',
+          },
+          keywords: '关键词',
+        },
+        keywords: {
+          tip: '每行一个,用换行符分隔。每行最多 100 个字符。',
+          placeholder: '每行一个,用换行符分隔',
+          line: '行',
+        },
+        content: {
+          input: '审核输入内容',
+          output: '审核输出内容',
+          preset: '预设回复',
+          placeholder: '这里预设回复内容',
+          condition: '审核输入内容和审核输出内容至少启用一项',
+          fromApi: '预设回复通过 API 返回',
+          errorMessage: '预设回复不能为空',
+          supportMarkdown: '支持 Markdown',
+        },
+        openaiNotConfig: {
+          before: 'OpenAI 内容审核需要在',
+          after: '中配置 OpenAI API 密钥。',
+        },
+      },
+    },
   },
   automatic: {
     title: '自动编排',

+ 36 - 2
web/i18n/lang/common.en.ts

@@ -30,6 +30,7 @@ const translation = {
     reload: 'Reload',
     ok: 'OK',
     log: 'Log',
+    learnMore: 'Learn More',
   },
   placeholder: {
     input: 'Please enter',
@@ -108,6 +109,7 @@ const translation = {
     provider: 'Model Provider',
     dataSource: 'Data Source',
     plugin: 'Plugins',
+    apiBasedExtension: 'API-based Extension',
   },
   account: {
     avatar: 'Avatar',
@@ -299,6 +301,37 @@ const translation = {
       keyFrom: 'Get your SerpAPI key from SerpAPI Account Page',
     },
   },
+  apiBasedExtension: {
+    title: 'API-based extensions provide centralized API management, simplifying configuration for easy use across Dify\'s applications.',
+    link: 'Learn how to develop your own API-based Extension.',
+    linkUrl: 'https://docs.dify.ai/advanced/api_based_extension',
+    add: 'Add API-based Extension',
+    selector: {
+      title: 'API-based Extension',
+      placeholder: 'Please select API-based extension',
+      manage: 'Manage API-based Extension',
+    },
+    modal: {
+      title: 'Add API-based Extension',
+      editTitle: 'Edit API-based Extension',
+      name: {
+        title: 'Name',
+        placeholder: 'Please enter the name',
+      },
+      apiEndpoint: {
+        title: 'API Endpoint',
+        placeholder: 'Please enter the API endpoint',
+      },
+      apiKey: {
+        title: 'API-key',
+        placeholder: 'Please enter the API-key',
+        lengthError: 'API-key length cannot be less than 5 characters',
+      },
+    },
+    confirm: {
+      desc: 'Deleting the WebHook might cause the extension points configured for this API-based Extension to fail and produce errors. Please proceed with caution.',
+    },
+  },
   about: {
     changeLog: 'Changlog',
     updateNow: 'Update now',
@@ -384,11 +417,12 @@ const translation = {
     },
     variable: {
       item: {
-        title: 'Variables',
-        desc: 'Insert variable template',
+        title: 'Variables & External Tools',
+        desc: 'Insert Variables & External Tools',
       },
       modal: {
         add: 'New variable',
+        addTool: 'New tool',
       },
     },
     query: {

+ 36 - 2
web/i18n/lang/common.zh.ts

@@ -30,6 +30,7 @@ const translation = {
     reload: '刷新',
     ok: '好的',
     log: '日志',
+    learnMore: '了解更多',
   },
   placeholder: {
     input: '请输入',
@@ -108,6 +109,7 @@ const translation = {
     provider: '模型供应商',
     dataSource: '数据来源',
     plugin: '插件',
+    apiBasedExtension: '基于 API 的扩展',
   },
   account: {
     avatar: '头像',
@@ -299,6 +301,37 @@ const translation = {
       keyFrom: '从 SerpAPI 帐户页面获取您的 SerpAPI 密钥',
     },
   },
+  apiBasedExtension: {
+    title: '基于 API 的扩展提供了一个集中式的 API 管理,在此统一添加 API 配置后,方便在 Dify 上的各类应用中直接使用。',
+    link: '了解如何开发您自己的基于 API 的扩展。',
+    linkUrl: 'https://docs.dify.ai/v/zh-hans/advanced/api_based_extension',
+    add: '新增基于 API 的扩展',
+    selector: {
+      title: '基于 API 的扩展',
+      placeholder: '请选择基于 API 的扩展',
+      manage: '管理基于 API 的扩展',
+    },
+    modal: {
+      title: '新增基于 API 的扩展',
+      editTitle: '编辑基于 API 的扩展',
+      name: {
+        title: '名称',
+        placeholder: '请输入名称',
+      },
+      apiEndpoint: {
+        title: 'API Endpoint',
+        placeholder: '请输入 API endpoint',
+      },
+      apiKey: {
+        title: 'API-key',
+        placeholder: '请输入 API-key',
+        lengthError: 'API-key 不能少于 5 位',
+      },
+    },
+    confirm: {
+      desc: '删除 WebHook 可能会导致这个基于 API 的扩展配置的扩展失败并产生错误。请谨慎删除。',
+    },
+  },
   about: {
     changeLog: '更新日志',
     updateNow: '现在更新',
@@ -384,11 +417,12 @@ const translation = {
     },
     variable: {
       item: {
-        title: '变量',
-        desc: '插入变量模板',
+        title: '变量 & 外部工具',
+        desc: '插入变量和外部工具',
       },
       modal: {
         add: '添加新变量',
+        addTool: '添加工具',
       },
     },
     query: {

+ 2 - 0
web/i18n/lang/dataset-settings.en.ts

@@ -3,8 +3,10 @@ const translation = {
   desc: 'Here you can modify the properties and working methods of the dataset.',
   form: {
     name: 'Dataset Name',
+    namePlaceholder: 'Please enter the dataset name',
     nameError: 'Name cannot be empty',
     desc: 'Dataset description',
+    descInfo: 'Please write a clear textual description to outline the content of the dataset. This description will be used as a basis for matching when selecting from multiple datasets for inference.',
     descPlaceholder: 'Describe what is in this data set. A detailed description allows AI to access the content of the data set in a timely manner. If empty, Dify will use the default hit strategy.',
     descWrite: 'Learn how to write a good dataset description.',
     permissions: 'Permissions',

+ 2 - 0
web/i18n/lang/dataset-settings.zh.ts

@@ -3,8 +3,10 @@ const translation = {
   desc: '在这里您可以修改数据集的工作方式以及其它设置。',
   form: {
     name: '数据集名称',
+    namePlaceholder: '请输入数据集名称',
     nameError: '名称不能为空',
     desc: '数据集描述',
+    descInfo: '请写出清楚的文字描述来概述数据集的内容。当从多个数据集中进行选择匹配时,该描述将用作匹配的基础。',
     descPlaceholder: '描述这个数据集中的内容。详细的描述可以让 AI 及时访问数据集的内容。如果为空,Dify 将使用默认的命中策略。',
     descWrite: '了解如何编写更好的数据集描述。',
     permissions: '可见权限',

+ 58 - 1
web/models/common.ts

@@ -92,7 +92,7 @@ export type Provider = {
     is_valid: boolean
     is_enabled: boolean
     last_used: string
-    token?: ProviderTokenType[Name]
+    token?: string | ProviderAzureToken | ProviderAnthropicToken
   }
 }[ProviderName]
 
@@ -196,3 +196,60 @@ export type InvitationResult = {
 export type InvitationResponse = CommonResponse & {
   invitation_results: InvitationResult[]
 }
+
+export type ApiBasedExtension = {
+  id?: string
+  name?: string
+  api_endpoint?: string
+  api_key?: string
+}
+
+export type I18nText = {
+  'en-US': string
+  'zh-Hans': string
+}
+
+export type CodeBasedExtensionForm = {
+  type: string
+  label: I18nText
+  variable: string
+  required: boolean
+  options: string[]
+  default: string
+  placeholder: string
+}
+
+export type CodeBasedExtensionItem = {
+  name: string
+  label: I18nText
+  form_schema: CodeBasedExtensionForm[]
+}
+export type CodeBasedExtension = {
+  module: string
+  data: CodeBasedExtensionItem[]
+}
+
+export type ExternalDataTool = {
+  type?: string
+  label?: string
+  icon?: string
+  icon_background?: string
+  variable?: string
+  enabled?: boolean
+  config?: {
+    api_based_extension_id?: string
+  } & Partial<Record<string, any>>
+}
+
+export type ModerateResponse = {
+  flagged: boolean
+  text: string
+}
+
+export type ModerationService = (
+  url: string,
+  body: {
+    app_id: string
+    text: string
+  }
+) => Promise<ModerateResponse>

+ 15 - 0
web/models/debug.ts

@@ -73,6 +73,20 @@ export type SpeechToTextConfig = MoreLikeThisConfig
 
 export type CitationConfig = MoreLikeThisConfig
 
+export type ModerationContentConfig = {
+  enabled: boolean
+  preset_response?: string
+}
+export type ModerationConfig = MoreLikeThisConfig & {
+  type?: string
+  config?: {
+    keywords?: string
+    api_based_extension_id?: string
+    inputs_config?: ModerationContentConfig
+    outputs_config?: ModerationContentConfig
+  } & Partial<Record<string, any>>
+}
+
 export type RetrieverResourceConfig = MoreLikeThisConfig
 
 // frontend use. Not the same as backend
@@ -86,6 +100,7 @@ export type ModelConfig = {
   suggested_questions_after_answer: SuggestedQuestionsAfterAnswerConfig | null
   speech_to_text: SpeechToTextConfig | null
   retriever_resource: RetrieverResourceConfig | null
+  sensitive_word_avoidance: ModerationConfig | null
   dataSets: any[]
 }
 export type DatasetConfigItem = {

+ 9 - 4
web/service/base.ts

@@ -1,6 +1,6 @@
 import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
 import Toast from '@/app/components/base/toast'
-import type { MessageEnd, ThoughtItem } from '@/app/components/app/chat/type'
+import type { MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/app/chat/type'
 
 const TIME_OUT = 100000
 
@@ -33,6 +33,7 @@ export type IOnDataMoreInfo = {
 export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
 export type IOnThought = (though: ThoughtItem) => void
 export type IOnMessageEnd = (messageEnd: MessageEnd) => void
+export type IOnMessageReplace = (messageReplace: MessageReplace) => void
 export type IOnCompleted = (hasError?: boolean) => void
 export type IOnError = (msg: string, code?: string) => void
 
@@ -44,6 +45,7 @@ type IOtherOptions = {
   onData?: IOnData // for stream
   onThought?: IOnThought
   onMessageEnd?: IOnMessageEnd
+  onMessageReplace?: IOnMessageReplace
   onError?: IOnError
   onCompleted?: IOnCompleted // for stream
   getAbortController?: (abortController: AbortController) => void
@@ -77,7 +79,7 @@ export function format(text: string) {
   return res.replaceAll('\n', '<br/>').replaceAll('```', '')
 }
 
-const handleStream = (response: Response, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought, onMessageEnd?: IOnMessageEnd) => {
+const handleStream = (response: Response, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought, onMessageEnd?: IOnMessageEnd, onMessageReplace?: IOnMessageReplace) => {
   if (!response.ok)
     throw new Error('Network response was not ok')
 
@@ -135,6 +137,9 @@ const handleStream = (response: Response, onData: IOnData, onCompleted?: IOnComp
             else if (bufferObj.event === 'message_end') {
               onMessageEnd?.(bufferObj as MessageEnd)
             }
+            else if (bufferObj.event === 'message_replace') {
+              onMessageReplace?.(bufferObj as MessageReplace)
+            }
           }
         })
         buffer = lines[lines.length - 1]
@@ -327,7 +332,7 @@ export const upload = (options: any): Promise<any> => {
   })
 }
 
-export const ssePost = (url: string, fetchOptions: FetchOptionType, { isPublicAPI = false, onData, onCompleted, onThought, onMessageEnd, onError, getAbortController }: IOtherOptions) => {
+export const ssePost = (url: string, fetchOptions: FetchOptionType, { isPublicAPI = false, onData, onCompleted, onThought, onMessageEnd, onMessageReplace, onError, getAbortController }: IOtherOptions) => {
   const abortController = new AbortController()
 
   const options = Object.assign({}, baseOptions, {
@@ -366,7 +371,7 @@ export const ssePost = (url: string, fetchOptions: FetchOptionType, { isPublicAP
           return
         }
         onData?.(str, isFirstMessage, moreInfo)
-      }, onCompleted, onThought, onMessageEnd)
+      }, onCompleted, onThought, onMessageEnd, onMessageReplace)
     }).catch((e) => {
       if (e.toString() !== 'AbortError: The user aborted a request.')
         Toast.notify({ type: 'error', message: e })

+ 45 - 4
web/service/common.ts

@@ -1,13 +1,26 @@
 import type { Fetcher } from 'swr'
 import { del, get, patch, post, put } from './base'
 import type {
-  AccountIntegrate, CommonResponse, DataSourceNotion,
+  AccountIntegrate,
+  ApiBasedExtension,
+  CodeBasedExtension,
+  CommonResponse,
+  DataSourceNotion,
   DocumentsLimitResponse,
   FileUploadConfigResponse,
   ICurrentWorkspace,
-  IWorkspace, InvitationResponse, LangGeniusVersionResponse, Member,
-  OauthResponse, PluginProvider, Provider, ProviderAnthropicToken, ProviderAzureToken,
-  SetupStatusResponse, UserProfileOriginResponse,
+  IWorkspace,
+  InvitationResponse,
+  LangGeniusVersionResponse,
+  Member,
+  ModerateResponse,
+  OauthResponse,
+  PluginProvider,
+  Provider,
+  ProviderAnthropicToken,
+  ProviderAzureToken,
+  SetupStatusResponse,
+  UserProfileOriginResponse,
 } from '@/models/common'
 import type {
   UpdateOpenAIKeyResponse,
@@ -196,3 +209,31 @@ export const fetchNotionConnection: Fetcher<{ data: string }, string> = (url) =>
 export const fetchDataSourceNotionBinding: Fetcher<{ result: string }, string> = (url) => {
   return get(url) as Promise<{ result: string }>
 }
+
+export const fetchApiBasedExtensionList: Fetcher<ApiBasedExtension[], string> = (url) => {
+  return get(url) as Promise<ApiBasedExtension[]>
+}
+
+export const fetchApiBasedExtensionDetail: Fetcher<ApiBasedExtension, string> = (url) => {
+  return get(url) as Promise<ApiBasedExtension>
+}
+
+export const addApiBasedExtension: Fetcher<ApiBasedExtension, { url: string; body: ApiBasedExtension }> = ({ url, body }) => {
+  return post(url, { body }) as Promise<ApiBasedExtension>
+}
+
+export const updateApiBasedExtension: Fetcher<ApiBasedExtension, { url: string; body: ApiBasedExtension }> = ({ url, body }) => {
+  return post(url, { body }) as Promise<ApiBasedExtension>
+}
+
+export const deleteApiBasedExtension: Fetcher<{ result: string }, string> = (url) => {
+  return del(url) as Promise<{ result: string }>
+}
+
+export const fetchCodeBasedExtensionList: Fetcher<CodeBasedExtension, string> = (url) => {
+  return get(url) as Promise<CodeBasedExtension>
+}
+
+export const moderate = (url: string, body: { app_id: string; text: string }) => {
+  return post(url, { body }) as Promise<ModerateResponse>
+}

+ 7 - 5
web/service/debug.ts

@@ -1,4 +1,4 @@
-import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base'
+import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd, IOnMessageReplace } from './base'
 import { get, post, ssePost } from './base'
 import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
 import type { ModelModeType } from '@/types/app'
@@ -9,10 +9,11 @@ export type AutomaticRes = {
   opening_statement: string
 }
 
-export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd }: {
+export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd, onMessageReplace }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onMessageEnd: IOnMessageEnd
+  onMessageReplace: IOnMessageReplace
   onError: IOnError
   getAbortController?: (abortController: AbortController) => void
 }) => {
@@ -21,24 +22,25 @@ export const sendChatMessage = async (appId: string, body: Record<string, any>,
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, onError, getAbortController, onMessageEnd })
+  }, { onData, onCompleted, onError, getAbortController, onMessageEnd, onMessageReplace })
 }
 
 export const stopChatMessageResponding = async (appId: string, taskId: string) => {
   return post(`apps/${appId}/chat-messages/${taskId}/stop`)
 }
 
-export const sendCompletionMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError }: {
+export const sendCompletionMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onError: IOnError
+  onMessageReplace: IOnMessageReplace
 }) => {
   return ssePost(`apps/${appId}/completion-messages`, {
     body: {
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, onError })
+  }, { onData, onCompleted, onError, onMessageReplace })
 }
 
 export const fetchSuggestedQuestions = (appId: string, messageId: string) => {

+ 7 - 5
web/service/share.ts

@@ -1,4 +1,4 @@
-import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base'
+import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd, IOnMessageReplace } from './base'
 import {
   del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
   delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
@@ -22,11 +22,12 @@ function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
   return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url
 }
 
-export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd }: {
+export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd, onMessageReplace }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onError: IOnError
   onMessageEnd?: IOnMessageEnd
+  onMessageReplace?: IOnMessageReplace
   getAbortController?: (abortController: AbortController) => void
 }, isInstalledApp: boolean, installedAppId = '') => {
   return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
@@ -34,24 +35,25 @@ export const sendChatMessage = async (body: Record<string, any>, { onData, onCom
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd })
+  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd, onMessageReplace })
 }
 
 export const stopChatMessageResponding = async (appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => {
   return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId))
 }
 
-export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError }: {
+export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onError: IOnError
+  onMessageReplace: IOnMessageReplace
 }, isInstalledApp: boolean, installedAppId = '') => {
   return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), {
     body: {
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError })
+  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace })
 }
 
 export const fetchAppInfo = async () => {

+ 5 - 0
web/types/app.ts

@@ -1,4 +1,5 @@
 import type { ChatPromptConfig, CompletionPromptConfig, DatasetConfigs, PromptMode } from '@/models/debug.ts'
+import type { ExternalDataTool } from '@/models/common'
 export enum ProviderType {
   openai = 'openai',
   anthropic = 'anthropic',
@@ -113,6 +114,10 @@ export type ModelConfig = {
   retriever_resource: {
     enabled: boolean
   }
+  sensitive_word_avoidance: {
+    enabled: boolean
+  }
+  external_data_tools: ExternalDataTool[]
   agent_mode: {
     enabled: boolean
     tools: ToolItem[]

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio