mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-19 01:15:53 +08:00
moderation
This commit is contained in:
parent
a691700b48
commit
2f658de155
@ -3,7 +3,7 @@ import { useState } from 'react'
|
|||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import FormGeneration from '../toolbox/moderation/form-generation'
|
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
|
||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import type { FC } from 'react'
|
|
||||||
import { memo } from 'react'
|
|
||||||
import { useContext } from 'use-context-selector'
|
|
||||||
import type { CodeBasedExtensionForm } from '@/models/common'
|
|
||||||
import I18n from '@/context/i18n'
|
|
||||||
import { PortalSelect } from '@/app/components/base/select'
|
|
||||||
import Textarea from '@/app/components/base/textarea'
|
|
||||||
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'>
|
|
||||||
<Textarea
|
|
||||||
value={value?.[form.variable] || ''}
|
|
||||||
className='resize-none'
|
|
||||||
placeholder={form.placeholder}
|
|
||||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
form.type === 'select' && (
|
|
||||||
<PortalSelect
|
|
||||||
value={value?.[form.variable]}
|
|
||||||
items={form.options.map((option) => {
|
|
||||||
return {
|
|
||||||
name: option.label[locale === 'zh-Hans' ? 'zh-Hans' : 'en-US'],
|
|
||||||
value: option.value,
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
onSelect={item => handleFormChange(form.variable, item.value as string)}
|
|
||||||
popupClassName='w-[576px] !z-[102]'
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(FormGeneration)
|
|
@ -1,73 +0,0 @@
|
|||||||
import type { FC } from 'react'
|
|
||||||
import { memo } 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 memo(ModerationContent)
|
|
@ -1,376 +0,0 @@
|
|||||||
import type { ChangeEvent, FC } from 'react'
|
|
||||||
import {
|
|
||||||
memo,
|
|
||||||
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 { LanguagesSupported } from '@/i18n/language'
|
|
||||||
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
|
|
||||||
import { useModalContext } from '@/context/modal-context'
|
|
||||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
|
||||||
|
|
||||||
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 openaiProvider = modelProviders?.data.find(item => item.provider === 'openai')
|
|
||||||
const systemOpenaiProviderEnabled = openaiProvider?.system_configuration.enabled
|
|
||||||
const systemOpenaiProviderQuota = systemOpenaiProviderEnabled ? openaiProvider?.system_configuration.quota_configurations.find(item => item.quota_type === openaiProvider.system_configuration.current_quota_type) : undefined
|
|
||||||
const systemOpenaiProviderCanUse = systemOpenaiProviderQuota?.is_valid
|
|
||||||
const customOpenaiProvidersCanUse = openaiProvider?.custom_configuration.status === CustomConfigurationStatusEnum.active
|
|
||||||
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) => {
|
|
||||||
let config: undefined | Record<string, any>
|
|
||||||
const currProvider = providers.find(provider => provider.key === type)
|
|
||||||
|
|
||||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
|
||||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
|
||||||
prev[next.variable] = next.default
|
|
||||||
return prev
|
|
||||||
}, {} as Record<string, any>)
|
|
||||||
}
|
|
||||||
setLocaleData({
|
|
||||||
...localeData,
|
|
||||||
type,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 !== LanguagesSupported[1] ? 'keywords' : '关键词' }) })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
|
|
||||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'API 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] && currentProvider.form_schema[i].required) {
|
|
||||||
notify({
|
|
||||||
type: 'error',
|
|
||||||
message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 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' rel='noopener noreferrer'
|
|
||||||
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'
|
|
||||||
>
|
|
||||||
{t('common.operation.cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='primary'
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={localeData.type === 'openai_moderation' && !openaiProviderConfiged}
|
|
||||||
>
|
|
||||||
{t('common.operation.save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(ModerationSettingModal)
|
|
@ -44,7 +44,7 @@ const Citation = ({
|
|||||||
}
|
}
|
||||||
title={t('appDebug.feature.citation.title')}
|
title={t('appDebug.feature.citation.title')}
|
||||||
value={!!features.citation?.enabled}
|
value={!!features.citation?.enabled}
|
||||||
description={t('appDebug.feature.citation.description')}
|
description={t('appDebug.feature.citation.description')!}
|
||||||
onChange={state => handleChange(FeatureEnum.citation, state)}
|
onChange={state => handleChange(FeatureEnum.citation, state)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -44,7 +44,7 @@ const FollowUp = ({
|
|||||||
}
|
}
|
||||||
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
|
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
|
||||||
value={!!features.suggested?.enabled}
|
value={!!features.suggested?.enabled}
|
||||||
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
|
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')!}
|
||||||
onChange={state => handleChange(FeatureEnum.suggested, state)}
|
onChange={state => handleChange(FeatureEnum.suggested, state)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,7 @@ import Switch from '@/app/components/base/switch'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
import MoreLikeThis from '@/app/components/base/features/new-feature-panel/more-like-this'
|
import MoreLikeThis from '@/app/components/base/features/new-feature-panel/more-like-this'
|
||||||
|
import Moderation from '@/app/components/base/features/new-feature-panel/moderation'
|
||||||
import SpeechToText from '@/app/components/base/features/new-feature-panel/speech-to-text'
|
import SpeechToText from '@/app/components/base/features/new-feature-panel/speech-to-text'
|
||||||
import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech'
|
import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech'
|
||||||
import FollowUp from '@/app/components/base/features/new-feature-panel/follow-up'
|
import FollowUp from '@/app/components/base/features/new-feature-panel/follow-up'
|
||||||
@ -88,6 +89,7 @@ const NewFeaturePanel = ({
|
|||||||
{!isChatMode && (
|
{!isChatMode && (
|
||||||
<MoreLikeThis onChange={onChange} />
|
<MoreLikeThis onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
|
<Moderation onChange={onChange} />
|
||||||
{isChatMode && speech2textDefaultModel && (
|
{isChatMode && speech2textDefaultModel && (
|
||||||
<SpeechToText onChange={onChange} />
|
<SpeechToText onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,170 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import produce from 'immer'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { RiEqualizer2Line } from '@remixicon/react'
|
||||||
|
import { ContentModeration } from '@/app/components/base/icons/src/vender/features'
|
||||||
|
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||||
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
|
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||||
|
import { FeatureEnum } from '@/app/components/base/features/types'
|
||||||
|
import { fetchCodeBasedExtensionList } from '@/service/common'
|
||||||
|
import { useModalContext } from '@/context/modal-context'
|
||||||
|
import I18n from '@/context/i18n'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled?: boolean
|
||||||
|
onChange?: OnFeaturesChange
|
||||||
|
}
|
||||||
|
|
||||||
|
const Moderation = ({
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
|
||||||
|
const { setShowModerationSettingModal } = useModalContext()
|
||||||
|
const { locale } = useContext(I18n)
|
||||||
|
const featuresStore = useFeaturesStore()
|
||||||
|
const moderation = useFeatures(s => s.features.moderation)
|
||||||
|
const { data: codeBasedExtensionList } = useSWR(
|
||||||
|
'/code-based-extension?module=moderation',
|
||||||
|
fetchCodeBasedExtensionList,
|
||||||
|
)
|
||||||
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
|
|
||||||
|
const handleOpenModerationSettingModal = () => {
|
||||||
|
if (disabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
const {
|
||||||
|
features,
|
||||||
|
setFeatures,
|
||||||
|
} = featuresStore!.getState()
|
||||||
|
setShowModerationSettingModal({
|
||||||
|
payload: moderation as any,
|
||||||
|
onSaveCallback: (newModeration) => {
|
||||||
|
const newFeatures = produce(features, (draft) => {
|
||||||
|
draft.moderation = newModeration
|
||||||
|
})
|
||||||
|
setFeatures(newFeatures)
|
||||||
|
setShowFeaturesPanel(true)
|
||||||
|
if (onChange)
|
||||||
|
onChange(newFeatures)
|
||||||
|
},
|
||||||
|
onCancelCallback: () => {
|
||||||
|
setShowFeaturesPanel(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
|
||||||
|
const {
|
||||||
|
features,
|
||||||
|
setFeatures,
|
||||||
|
} = featuresStore!.getState()
|
||||||
|
|
||||||
|
if (enabled && !features.moderation?.type && type === FeatureEnum.moderation) {
|
||||||
|
setShowModerationSettingModal({
|
||||||
|
payload: {
|
||||||
|
enabled: true,
|
||||||
|
type: 'keywords',
|
||||||
|
config: {
|
||||||
|
keywords: '',
|
||||||
|
inputs_config: {
|
||||||
|
enabled: true,
|
||||||
|
preset_response: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSaveCallback: (newModeration) => {
|
||||||
|
const newFeatures = produce(features, (draft) => {
|
||||||
|
draft.moderation = newModeration
|
||||||
|
})
|
||||||
|
setFeatures(newFeatures)
|
||||||
|
setShowFeaturesPanel(true)
|
||||||
|
if (onChange)
|
||||||
|
onChange(newFeatures)
|
||||||
|
},
|
||||||
|
onCancelCallback: () => {
|
||||||
|
const newFeatures = produce(features, (draft) => {
|
||||||
|
draft.moderation = { enabled: false }
|
||||||
|
})
|
||||||
|
setFeatures(newFeatures)
|
||||||
|
setShowFeaturesPanel(true)
|
||||||
|
if (onChange)
|
||||||
|
onChange(newFeatures)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [featuresStore, onChange])
|
||||||
|
|
||||||
|
const providerContent = useMemo(() => {
|
||||||
|
if (moderation?.type === 'openai_moderation')
|
||||||
|
return t('appDebug.feature.moderation.modal.provider.openai')
|
||||||
|
else if (moderation?.type === 'keywords')
|
||||||
|
return t('appDebug.feature.moderation.modal.provider.keywords')
|
||||||
|
else if (moderation?.type === 'api')
|
||||||
|
return t('common.apiBasedExtension.selector.title')
|
||||||
|
else
|
||||||
|
return codeBasedExtensionList?.data.find(item => item.name === moderation?.type)?.label[locale] || '-'
|
||||||
|
}, [codeBasedExtensionList, moderation])
|
||||||
|
|
||||||
|
const enableContent = useMemo(() => {
|
||||||
|
if (moderation?.config?.inputs_config?.enabled && moderation.config?.outputs_config?.enabled)
|
||||||
|
return t('appDebug.feature.moderation.allEnabled')
|
||||||
|
else if (moderation?.config?.inputs_config?.enabled)
|
||||||
|
return t('appDebug.feature.moderation.inputEnabled')
|
||||||
|
else if (moderation?.config?.outputs_config?.enabled)
|
||||||
|
return t('appDebug.feature.moderation.outputEnabled')
|
||||||
|
}, [moderation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureCard
|
||||||
|
icon={
|
||||||
|
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-text-success'>
|
||||||
|
<ContentModeration className='w-4 h-4 text-text-primary-on-surface' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title={t('appDebug.feature.moderation.title')}
|
||||||
|
value={!!moderation?.enabled}
|
||||||
|
onChange={state => handleChange(FeatureEnum.moderation, state)}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{!moderation?.enabled && (
|
||||||
|
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.moderation.description')}</div>
|
||||||
|
)}
|
||||||
|
{!!moderation?.enabled && (
|
||||||
|
<>
|
||||||
|
{!isHovering && (
|
||||||
|
<div className='pt-0.5 flex items-center gap-4'>
|
||||||
|
<div className=''>
|
||||||
|
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.moderation.modal.provider.title')}</div>
|
||||||
|
<div className='text-text-secondary system-xs-regular'>{providerContent}</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
|
||||||
|
<div className=''>
|
||||||
|
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.moderation.contentEnableLabel')}</div>
|
||||||
|
<div className='text-text-secondary system-xs-regular'>{enableContent}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isHovering && (
|
||||||
|
<Button className='w-full' onClick={handleOpenModerationSettingModal}>
|
||||||
|
<RiEqualizer2Line className='mr-1 w-4 h-4' />
|
||||||
|
{t('common.operation.settings')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</FeatureCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Moderation
|
@ -3,6 +3,7 @@ import { useState } from 'react'
|
|||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import ModerationContent from './moderation-content'
|
import ModerationContent from './moderation-content'
|
||||||
import FormGeneration from './form-generation'
|
import FormGeneration from './form-generation'
|
||||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||||
@ -237,10 +238,11 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
isShow
|
isShow
|
||||||
onClose={() => { }}
|
onClose={() => { }}
|
||||||
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
|
className='!p-6 !mt-14 !max-w-none !w-[600px]'
|
||||||
>
|
>
|
||||||
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
|
<div className='flex items-center justify-between'>
|
||||||
{t('appDebug.feature.moderation.modal.title')}
|
<div className='text-text-primary title-2xl-semi-bold'>{t('appDebug.feature.moderation.modal.title')}</div>
|
||||||
|
<div className='p-1 cursor-pointer' onClick={onCancel}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
|
||||||
</div>
|
</div>
|
||||||
<div className='py-2'>
|
<div className='py-2'>
|
||||||
<div className='leading-9 text-sm font-medium text-gray-900'>
|
<div className='leading-9 text-sm font-medium text-gray-900'>
|
@ -45,7 +45,7 @@ const MoreLikeThis = ({
|
|||||||
title={t('appDebug.feature.moreLikeThis.title')}
|
title={t('appDebug.feature.moreLikeThis.title')}
|
||||||
tooltip={t('appDebug.feature.moreLikeThis.tip')}
|
tooltip={t('appDebug.feature.moreLikeThis.tip')}
|
||||||
value={!!features.moreLikeThis?.enabled}
|
value={!!features.moreLikeThis?.enabled}
|
||||||
description={t('appDebug.feature.moreLikeThis.description')}
|
description={t('appDebug.feature.moreLikeThis.description')!}
|
||||||
onChange={state => handleChange(FeatureEnum.moreLikeThis, state)}
|
onChange={state => handleChange(FeatureEnum.moreLikeThis, state)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import AccountSetting from '@/app/components/header/account-setting'
|
import AccountSetting from '@/app/components/header/account-setting'
|
||||||
import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal'
|
import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal'
|
||||||
import ModerationSettingModal from '@/app/components/app/configuration/toolbox/moderation/moderation-setting-modal'
|
import ModerationSettingModal from '@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'
|
||||||
import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal'
|
import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal'
|
||||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||||
import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal'
|
import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal'
|
||||||
|
@ -163,9 +163,10 @@ const translation = {
|
|||||||
moderation: {
|
moderation: {
|
||||||
title: 'Content moderation',
|
title: 'Content moderation',
|
||||||
description: 'Secure model output by using moderation API or maintaining a sensitive word list.',
|
description: 'Secure model output by using moderation API or maintaining a sensitive word list.',
|
||||||
allEnabled: 'INPUT/OUTPUT Content Enabled',
|
contentEnableLabel: 'Enabled moderate content',
|
||||||
inputEnabled: 'INPUT Content Enabled',
|
allEnabled: 'INPUT & OUTPUT',
|
||||||
outputEnabled: 'OUTPUT Content Enabled',
|
inputEnabled: 'INPUT',
|
||||||
|
outputEnabled: 'OUTPUT',
|
||||||
modal: {
|
modal: {
|
||||||
title: 'Content moderation settings',
|
title: 'Content moderation settings',
|
||||||
provider: {
|
provider: {
|
||||||
|
@ -163,9 +163,10 @@ const translation = {
|
|||||||
moderation: {
|
moderation: {
|
||||||
title: '内容审查',
|
title: '内容审查',
|
||||||
description: '您可以调用审查 API 或者维护敏感词库来使模型更安全地输出。',
|
description: '您可以调用审查 API 或者维护敏感词库来使模型更安全地输出。',
|
||||||
allEnabled: '审查输入/审查输出 内容已启用',
|
contentEnableLabel: '启用审查内容',
|
||||||
inputEnabled: '审查输入内容已启用',
|
allEnabled: '输入内容和输出内容',
|
||||||
outputEnabled: '审查输出内容已启用',
|
inputEnabled: '输入内容',
|
||||||
|
outputEnabled: '输出内容',
|
||||||
modal: {
|
modal: {
|
||||||
title: '内容审查设置',
|
title: '内容审查设置',
|
||||||
provider: {
|
provider: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user