feat: add api-based extension & external data tool & moderation (#1459)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answerWrap:hover .copyBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
>
|
||||
{t('common.settings.provider')}
|
||||
</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
|
||||
@@ -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
web/app/components/app/configuration/tools/index.tsx
Normal file
185
web/app/components/app/configuration/tools/index.tsx
Normal file
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user