= ({
diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx
index 801c91bdd2..2ec07b0852 100644
--- a/web/app/components/app/configuration/index.tsx
+++ b/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({
enabled: false,
})
+ const [moderationConfig, setModerationConfig] = useState({
+ enabled: false,
+ })
+ const [externalDataToolsConfig, setExternalDataToolsConfig] = useState([])
const [formattingChanged, setFormattingChanged] = useState(false)
const [inputs, setInputs] = useState({})
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 = () => {
-
+ setShowAccountSettingModal({ payload: 'provider' })}
+ inputs={inputs}
+ />
@@ -609,22 +640,12 @@ const Configuration: FC = () => {
isShow={showUseGPT4Confirm}
onClose={() => setShowUseGPT4Confirm(false)}
onConfirm={() => {
- setShowSetAPIKeyModal(true)
+ setShowAccountSettingModal({ payload: 'provider' })
setShowUseGPT4Confirm(false)
}}
onCancel={() => setShowUseGPT4Confirm(false)}
/>
)}
- {
- showSetAPIKeyModal && (
- {
- setShowSetAPIKeyModal(false)
- }} />
- )
- }
- {isShowSetAPIKey && {
- hideSetAPIkey()
- }} />}
{isShowSelectDataSet && (
void
+ inputs: Inputs
}
const PromptValuePanel: FC = ({
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()
diff --git a/web/app/components/app/configuration/toolbox/index.tsx b/web/app/components/app/configuration/toolbox/index.tsx
index 0304b3f32e..d79b72371c 100644
--- a/web/app/components/app/configuration/toolbox/index.tsx
+++ b/web/app/components/app/configuration/toolbox/index.tsx
@@ -1,25 +1,26 @@
'use client'
-import React, { FC } from 'react'
-import GroupName from '../base/group-name'
-export interface IToolboxProps {
- searchToolConfig: any
- sensitiveWordAvoidanceConifg: any
+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 type ToolboxProps = {
+ showModerationSettings: boolean
}
-/*
-* Include
-* 1. Search Tool
-* 2. Sensitive word avoidance
-*/
-const Toolbox: FC = ({ searchToolConfig, sensitiveWordAvoidanceConifg }) => {
+const Toolbox: FC = ({ showModerationSettings }) => {
+ const { t } = useTranslation()
+
return (
-
-
-
- {searchToolConfig?.enabled &&
Search Tool
}
- {sensitiveWordAvoidanceConifg?.enabled &&
Sensitive word avoidance
}
-
+
+
+ {
+ showModerationSettings && (
+
+ )
+ }
)
}
diff --git a/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx b/web/app/components/app/configuration/toolbox/moderation/form-generation.tsx
new file mode 100644
index 0000000000..c072c46d74
--- /dev/null
+++ b/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
) => void
+}
+const FormGeneration: FC = ({
+ forms,
+ value,
+ onChange,
+}) => {
+ const { locale } = useContext(I18n)
+
+ const handleFormChange = (type: string, v: string) => {
+ onChange({ ...value, [type]: v })
+ }
+
+ return (
+ <>
+ {
+ forms.map((form, index) => (
+
+
+ {locale === 'zh-Hans' ? form.label['zh-Hans'] : form.label['en-US']}
+
+ {
+ form.type === 'text-input' && (
+
handleFormChange(form.variable, e.target.value)}
+ />
+ )
+ }
+ {
+ form.type === 'paragraph' && (
+
+
+ )
+ }
+ {
+ form.type === 'select' && (
+
{
+ return {
+ value: option,
+ name: option,
+ }
+ })}
+ onSelect={item => handleFormChange(form.variable, item.value as string)}
+ />
+ )
+ }
+
+ ))
+ }
+ >
+ )
+}
+
+export default FormGeneration
diff --git a/web/app/components/app/configuration/toolbox/moderation/index.tsx b/web/app/components/app/configuration/toolbox/moderation/index.tsx
new file mode 100644
index 0000000000..c049c2b42f
--- /dev/null
+++ b/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 (
+
+
+
+
+
+ {t('appDebug.feature.moderation.title')}
+
+
+ {renderInfo()}
+
+
+
+
+ {t('common.operation.settings')}
+
+
+ )
+}
+
+export default Moderation
diff --git a/web/app/components/app/configuration/toolbox/moderation/moderation-content.tsx b/web/app/components/app/configuration/toolbox/moderation/moderation-content.tsx
new file mode 100644
index 0000000000..7cb8114959
--- /dev/null
+++ b/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 = ({
+ 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 (
+
+
+
+
{title}
+
+ {
+ info && (
+
{info}
+ )
+ }
+
handleConfigChange('enabled', v)}
+ />
+
+
+ {
+ config.enabled && showPreset && (
+
+
+ {t('appDebug.feature.moderation.modal.content.preset')}
+ {t('appDebug.feature.moderation.modal.content.supportMarkdown')}
+
+
+
+ )
+ }
+
+
+ )
+}
+
+export default ModerationContent
diff --git a/web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx b/web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx
new file mode 100644
index 0000000000..486d61f998
--- /dev/null
+++ b/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 = ({
+ 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(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) => {
+ 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) => {
+ setLocaleData({
+ ...localeData,
+ config: {
+ ...localeData.config,
+ ...extraValue,
+ },
+ })
+ }
+
+ const formatData = (originData: ModerationConfig) => {
+ const { enabled, type, config } = originData
+ const { inputs_config, outputs_config } = config!
+ const params: Record = {}
+
+ 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 (
+ {}}
+ className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
+ >
+
+ {t('appDebug.feature.moderation.modal.title')}
+
+
+
+ {t('appDebug.feature.moderation.modal.provider.title')}
+
+
+ {
+ providers.map(provider => (
+
handleDataTypeChange(provider.key)}
+ >
+
+ {provider.name}
+
+ ))
+ }
+
+ {
+ !isLoading && !openaiProviderConfiged && localeData.type === 'openai_moderation' && (
+
+
+
+ {t('appDebug.feature.moderation.modal.openaiNotConfig.before')}
+
+ {t('common.settings.provider')}
+
+ {t('appDebug.feature.moderation.modal.openaiNotConfig.after')}
+
+
+ )
+ }
+
+ {
+ localeData.type === 'keywords' && (
+
+
{t('appDebug.feature.moderation.modal.provider.keywords')}
+
{t('appDebug.feature.moderation.modal.keywords.tip')}
+
+
+
+ {(localeData.config?.keywords || '').split('\n').filter(Boolean).length}/100 {t('appDebug.feature.moderation.modal.keywords.line')}
+
+
+
+ )
+ }
+ {
+ localeData.type === 'api' && (
+
+ )
+ }
+ {
+ systemTypes.findIndex(t => t === localeData.type) < 0
+ && currentProvider?.form_schema
+ && (
+
+ )
+ }
+
+ handleDataContentChange('inputs_config', config)}
+ info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
+ showPreset={!(localeData.type === 'api')}
+ />
+ handleDataContentChange('outputs_config', config)}
+ info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
+ showPreset={!(localeData.type === 'api')}
+ />
+ {t('appDebug.feature.moderation.modal.content.condition')}
+
+
+
+
+
+ )
+}
+
+export default ModerationSettingModal
diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx
new file mode 100644
index 0000000000..7cdd6e1f7a
--- /dev/null
+++ b/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 = ({
+ 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) => {
+ setLocaleData({
+ ...localeData,
+ config: {
+ ...localeData.config,
+ ...extraValue,
+ },
+ })
+ }
+
+ const handleValueChange = (value: Record) => {
+ 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 = {}
+
+ 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 (
+ {}}
+ className='!p-8 !pb-6 !max-w-none !w-[640px]'
+ >
+
+ {`${action} ${t('appDebug.feature.tools.modal.title')}`}
+
+
+
+ {t('appDebug.feature.tools.modal.toolType.title')}
+
+
{
+ return {
+ value: option.key,
+ name: option.name,
+ }
+ })}
+ onSelect={item => handleDataTypeChange(item.value as string)}
+ />
+
+
+
+ {t('appDebug.feature.tools.modal.name.title')}
+
+
+
+
+
+ {t('appDebug.feature.tools.modal.variableName.title')}
+
+
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') || ''}
+ />
+
+ {
+ localeData.type === 'api' && (
+
+ )
+ }
+ {
+ systemTypes.findIndex(t => t === localeData.type) < 0
+ && currentProvider?.form_schema
+ && (
+
+ )
+ }
+
+
+
+
+ {
+ showEmojiPicker && (
+ {
+ handleValueChange({ icon, icon_background })
+ setShowEmojiPicker(false)
+ }}
+ onClose={() => {
+ handleValueChange({ icon: '', icon_background: '' })
+ setShowEmojiPicker(false)
+ }}
+ />
+ )
+ }
+
+ )
+}
+
+export default ExternalDataToolModal
diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx
new file mode 100644
index 0000000000..21db5575b6
--- /dev/null
+++ b/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 (
+
+
+
+
setExpanded(v => !v)}
+ >
+ {
+ externalDataToolsConfig.length
+ ?
+ :
+ }
+ {
+ !!externalDataToolsConfig.length && (
+
+ )
+ }
+
+
+ {t('appDebug.feature.tools.title')}
+
+
{t('appDebug.feature.tools.tips')}}>
+
+
+
+ {
+ !expanded && !!externalDataToolsConfig.length && (
+ <>
+
{t('appDebug.feature.tools.toolsInUse', { count: externalDataToolsConfig.length })}
+
+ >
+ )
+ }
+
handleOpenExternalDataToolModal({}, -1)}
+ >
+
+ {t('common.operation.add')}
+
+
+ {
+ expanded && !!externalDataToolsConfig.length && (
+
+ {
+ externalDataToolsConfig.map((item, index: number) => (
+
+
+
+
{item.label}
+
+ {
+ copy(item.variable || '')
+ setCopied(true)
+ }}
+ >
+ {item.variable}
+
+
+
+
handleOpenExternalDataToolModal(item, index)}
+ >
+
+
+
setExternalDataToolsConfig([...externalDataToolsConfig.slice(0, index), ...externalDataToolsConfig.slice(index + 1)])}
+ >
+
+
+
+
handleSaveExternalDataToolModal({ ...item, enabled }, index)}
+ />
+
+ ))
+ }
+
+ )
+ }
+
+ )
+}
+
+export default Tools
diff --git a/web/app/components/app/overview/apikey-info-panel/index.tsx b/web/app/components/app/overview/apikey-info-panel/index.tsx
index 17bcb67ce8..d07c2af484 100644
--- a/web/app/components/app/overview/apikey-info-panel/index.tsx
+++ b/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 = () => {