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

This commit is contained in:
zxhlyh
2023-11-06 19:36:32 +08:00
committed by GitHub
parent db43ed6f41
commit 32747641e4
84 changed files with 3327 additions and 167 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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