vision setting

This commit is contained in:
JzoNg 2024-09-12 14:13:45 +08:00
parent 5dd556b4c8
commit a10b0db102
12 changed files with 251 additions and 723 deletions

View File

@ -4,12 +4,15 @@ import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import produce from 'immer' import produce from 'immer'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import ParamConfig from './param-config'
import { Vision } from '@/app/components/base/icons/src/vender/features' import { Vision } from '@/app/components/base/icons/src/vender/features'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' // import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import { Resolution } from '@/types/app' // import { Resolution } from '@/types/app'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import Switch from '@/app/components/base/switch'
import type { FileUpload } from '@/app/components/base/features/types'
const ConfigVision: FC = () => { const ConfigVision: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -17,7 +20,7 @@ const ConfigVision: FC = () => {
const file = useFeatures(s => s.features.file) const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore() const featuresStore = useFeaturesStore()
const handleChange = useCallback((resolution: Resolution) => { const handleChange = useCallback((data: FileUpload) => {
const { const {
features, features,
setFeatures, setFeatures,
@ -26,7 +29,8 @@ const ConfigVision: FC = () => {
const newFeatures = produce(features, (draft) => { const newFeatures = produce(features, (draft) => {
draft.file = { draft.file = {
...draft.file, ...draft.file,
image: { detail: resolution }, enabled: data.enabled,
image: { detail: data.image?.detail },
} }
}) })
setFeatures(newFeatures) setFeatures(newFeatures)
@ -53,7 +57,7 @@ const ConfigVision: FC = () => {
/> />
</div> </div>
<div className='shrink-0 flex items-center'> <div className='shrink-0 flex items-center'>
<div className='mr-2 flex items-center gap-0.5'> {/* <div className='mr-2 flex items-center gap-0.5'>
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div> <div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip <Tooltip
popupContent={ popupContent={
@ -64,8 +68,8 @@ const ConfigVision: FC = () => {
</div> </div>
} }
/> />
</div> </div> */}
<div className='flex items-center gap-1'> {/* <div className='flex items-center gap-1'>
<OptionCard <OptionCard
title={t('appDebug.vision.visionSettings.high')} title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high} selected={file?.image?.detail === Resolution.high}
@ -76,7 +80,17 @@ const ConfigVision: FC = () => {
selected={file?.image?.detail === Resolution.low} selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange(Resolution.low)} onSelect={() => handleChange(Resolution.low)}
/> />
</div> </div> */}
<ParamConfig />
<div className='ml-1 mr-3 w-[1px] h-3.5 bg-divider-subtle'></div>
<Switch
defaultValue={file?.enabled}
onChange={value => handleChange({
...(file || {}),
enabled: value,
})}
size='md'
/>
</div> </div>
</div> </div>
) )

View File

@ -0,0 +1,137 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { Resolution, TransferMethod } from '@/types/app'
import ParamItem from '@/app/components/base/param-item'
import Tooltip from '@/app/components/base/tooltip'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { FileUpload } from '@/app/components/base/features/types'
const MIN = 1
const MAX = 6
const ParamConfigContent: FC = () => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const handleChange = useCallback((data: FileUpload) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
image: { detail: data.image?.detail },
allowed_file_upload_methods: data.allowed_file_upload_methods,
number_limits: data.number_limits,
}
})
setFeatures(newFeatures)
}, [featuresStore])
return (
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.high },
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.low },
})}
/>
</div>
</div>
<div>
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.both')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && !!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.localUpload')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.url')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.remote_url],
})}
/>
</div>
</div>
<div>
<ParamItem
id='upload_limit'
className=''
name={t('appDebug.vision.visionSettings.uploadLimit')}
noTooltip
{...{
default: 2,
step: 1,
min: MIN,
max: MAX,
}}
value={file?.number_limits || 3}
enable={true}
onChange={(_key: string, value: number) => {
if (!value)
return
handleChange({
...file,
number_limits: value,
})
}}
/>
</div>
</div>
</div>
)
}
export default React.memo(ParamConfigContent)

View File

@ -0,0 +1,41 @@
'use client'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ParamConfigContent from './param-config-content'
import cn from '@/utils/classnames'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
const ParamsConfig: FC = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-text-tertiary cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
<ParamConfigContent />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ParamsConfig)

View File

@ -49,7 +49,6 @@ const ChatUserInput = ({
return ( return (
<div className={cn('bg-components-panel-on-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs z-[1]')}> <div className={cn('bg-components-panel-on-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs z-[1]')}>
<div className='px-4 pt-3 pb-4'> <div className='px-4 pt-3 pb-4'>
{/* ##TODO## file_upload */}
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => ( {promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
<div <div
key={key} key={key}

View File

@ -12,16 +12,18 @@ import {
} from './context' } from './context'
import type { DebugWithMultipleModelContextType } from './context' import type { DebugWithMultipleModelContextType } from './context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area' import ChatInput from '@/app/components/base/chat/chat/chat-input'
// import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
import type { VisionFile } from '@/app/components/base/chat/types' import type { VisionFile } from '@/app/components/base/chat/types'
import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useFeatures } from '@/app/components/base/features/hooks' import { useFeatures } from '@/app/components/base/features/hooks'
import { useStore as useAppStore } from '@/app/components/app/store' // import { useStore as useAppStore } from '@/app/components/app/store'
import { Resolution, TransferMethod } from '@/types/app'
const DebugWithMultipleModel = () => { const DebugWithMultipleModel = () => {
const { const {
mode, mode,
isShowVisionConfig, // isShowVisionConfig,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const speech2text = useFeatures(s => s.features.speech2text) const speech2text = useFeatures(s => s.features.speech2text)
const file = useFeatures(s => s.features.file) const file = useFeatures(s => s.features.file)
@ -32,6 +34,15 @@ const DebugWithMultipleModel = () => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const isChatMode = mode === 'chat' || mode === 'agent-chat' const isChatMode = mode === 'chat' || mode === 'agent-chat'
const visionConfig = useMemo(() => {
return {
enabled: file?.enabled || false,
detail: file?.image?.detail || Resolution.high,
number_limits: file?.number_limits || 3,
transfer_methods: file?.allowed_file_upload_methods || [TransferMethod.local_file, TransferMethod.remote_url],
}
}, [file])
const handleSend = useCallback((message: string, files?: VisionFile[]) => { const handleSend = useCallback((message: string, files?: VisionFile[]) => {
if (checkCanSend && !checkCanSend()) if (checkCanSend && !checkCanSend())
return return
@ -95,7 +106,7 @@ const DebugWithMultipleModel = () => {
} }
}, [twoLine, threeLine, fourLine]) }, [twoLine, threeLine, fourLine])
const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal) // const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
@ -128,14 +139,19 @@ const DebugWithMultipleModel = () => {
</div> </div>
{isChatMode && ( {isChatMode && (
<div className='shrink-0 pb-0 px-6'> <div className='shrink-0 pb-0 px-6'>
<ChatInputArea <ChatInput
onSend={handleSend}
speechToTextConfig={speech2text as any}
visionConfig={visionConfig}
/>
{/* <ChatInputArea
showFeatureBar showFeatureBar
showFileUpload={isShowVisionConfig} showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal} onFeatureBarClick={setShowAppConfigureFeaturesModal}
onSend={handleSend} onSend={handleSend}
speechToTextConfig={speech2text as any} speechToTextConfig={speech2text as any}
visionConfig={file} visionConfig={file}
/> /> */}
</div> </div>
)} )}
</div> </div>

View File

@ -41,7 +41,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
inputs, inputs,
collectionList, collectionList,
completionParams, completionParams,
isShowVisionConfig, // isShowVisionConfig,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext() const { textGenerationModelList } = useProviderContext()
const features = useFeatures(s => s.features) const features = useFeatures(s => s.features)
@ -142,7 +142,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
chatContainerClassName='px-3 pt-6' chatContainerClassName='px-3 pt-6'
chatFooterClassName='px-3 pt-10 pb-0' chatFooterClassName='px-3 pt-10 pb-0'
showFeatureBar showFeatureBar
showFileUpload={isShowVisionConfig} showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal} onFeatureBarClick={setShowAppConfigureFeaturesModal}
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
onSend={doSend} onSend={doSend}

View File

@ -979,7 +979,7 @@ const Configuration: FC = () => {
<NewFeaturePanel <NewFeaturePanel
show show
inWorkflow={false} inWorkflow={false}
showFileUpload={isShowVisionConfig} showFileUpload={false}
isChatMode={mode !== 'completion'} isChatMode={mode !== 'completion'}
disabled={false} disabled={false}
onChange={handleFeaturesChange} onChange={handleFeaturesChange}

View File

@ -155,7 +155,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
</div> </div>
</div> </div>
))} ))}
{/* ##TODO## file_upload */}
{visionConfig?.enabled && ( {visionConfig?.enabled && (
<div className="mt-3 xl:flex justify-between"> <div className="mt-3 xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div> <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
@ -204,6 +203,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
</div> </div>
<div className='mx-3'> <div className='mx-3'>
<FeatureBar <FeatureBar
showFileUpload={false}
isChatMode={appType !== AppType.completion} isChatMode={appType !== AppType.completion}
onFeatureBarClick={setShowAppConfigureFeaturesModal} /> onFeatureBarClick={setShowAppConfigureFeaturesModal} />
</div> </div>

View File

@ -6,6 +6,7 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
@ -21,7 +22,7 @@ import type {
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context' import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
import Question from './question' import Question from './question'
import Answer from './answer' import Answer from './answer'
// import ChatInput from './chat-input' import ChatInput from './chat-input'
import ChatInputArea from './chat-input-area' import ChatInputArea from './chat-input-area'
import TryToAsk from './try-to-ask' import TryToAsk from './try-to-ask'
import { ChatContextProvider } from './context' import { ChatContextProvider } from './context'
@ -33,6 +34,7 @@ import AgentLogModal from '@/app/components/base/agent-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import type { AppData } from '@/models/share' import type { AppData } from '@/models/share'
import { Resolution, TransferMethod } from '@/types/app'
export type ChatProps = { export type ChatProps = {
appData?: AppData appData?: AppData
@ -182,6 +184,15 @@ const Chat: FC<ChatProps> = ({
const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
const visionConfig = useMemo(() => {
return {
enabled: (config?.file_upload as any)?.enabled || false,
detail: (config?.file_upload as any)?.image?.detail || Resolution.high,
number_limits: (config?.file_upload as any)?.number_limits || 3,
transfer_methods: (config?.file_upload as any)?.allowed_file_upload_methods || [TransferMethod.local_file, TransferMethod.remote_url],
}
}, [config?.file_upload])
return ( return (
<ChatContextProvider <ChatContextProvider
config={config} config={config}
@ -268,7 +279,7 @@ const Chat: FC<ChatProps> = ({
) )
} }
{ {
!noChatInput && ( !noChatInput && showFileUpload && (
<ChatInputArea <ChatInputArea
showFeatureBar={showFeatureBar} showFeatureBar={showFeatureBar}
showFileUpload={showFileUpload} showFileUpload={showFileUpload}
@ -278,10 +289,17 @@ const Chat: FC<ChatProps> = ({
speechToTextConfig={config?.speech_to_text} speechToTextConfig={config?.speech_to_text}
onSend={onSend} onSend={onSend}
theme={themeBuilder?.theme} theme={themeBuilder?.theme}
// noSpacing={noSpacing}
/> />
) )
} }
{!noChatInput && !showFileUpload && (
<ChatInput
onSend={onSend}
speechToTextConfig={config?.speech_to_text}
visionConfig={visionConfig}
noSpacing={noSpacing}
/>
)}
</div> </div>
</div> </div>
{showPromptLogModal && !hideLogModal && ( {showPromptLogModal && !hideLogModal && (

View File

@ -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 isOpenAIProviderConfigured = 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' && !isOpenAIProviderConfigured)
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' && !isOpenAIProviderConfigured && '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 && !isOpenAIProviderConfigured && 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' 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' && !isOpenAIProviderConfigured}
>
{t('common.operation.save')}
</Button>
</div>
</Modal>
)
}
export default memo(ModerationSettingModal)

View File

@ -1,321 +0,0 @@
/* eslint-disable multiline-ternary */
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import produce from 'immer'
import {
RiAddLine,
RiDeleteBinLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { ReactSortable } from 'react-sortablejs'
import {
useFeatures,
useFeaturesStore,
} from '../../hooks'
import type { OnFeaturesChange } from '../../types'
import cn from '@/utils/classnames'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import Button from '@/app/components/base/button'
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import { getInputKeys } from '@/app/components/base/block-input'
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
import { getNewVar } from '@/utils/var'
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
import type { PromptVariable } from '@/models/debug'
const MAX_QUESTION_NUM = 5
export type OpeningStatementProps = {
onChange?: OnFeaturesChange
readonly?: boolean
promptVariables?: PromptVariable[]
onAutoAddPromptVariable: (variable: PromptVariable[]) => void
}
// regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g
const OpeningStatement: FC<OpeningStatementProps> = ({
onChange,
readonly,
promptVariables = [],
onAutoAddPromptVariable,
}) => {
const { t } = useTranslation()
const featureStore = useFeaturesStore()
const openingStatement = useFeatures(s => s.features.opening)
const value = openingStatement?.opening_statement || ''
const suggestedQuestions = openingStatement?.suggested_questions || []
const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
const hasValue = !!(value || '').trim()
const inputRef = useRef<HTMLTextAreaElement>(null)
const [isFocus, { setTrue: didSetFocus, setFalse: setBlur }] = useBoolean(false)
const setFocus = () => {
didSetFocus()
setTimeout(() => {
const input = inputRef.current
if (input) {
input.focus()
input.setSelectionRange(input.value.length, input.value.length)
}
}, 0)
}
const [tempValue, setTempValue] = useState(value)
useEffect(() => {
setTempValue(value || '')
}, [value])
const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(suggestedQuestions || [])
const notEmptyQuestions = tempSuggestedQuestions.filter(question => !!question && question.trim())
const coloredContent = (tempValue || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />')
const handleEdit = () => {
if (readonly)
return
setFocus()
}
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
const handleCancel = () => {
setBlur()
setTempValue(value)
setTempSuggestedQuestions(suggestedQuestions)
}
const handleConfirm = () => {
const keys = getInputKeys(tempValue)
const promptKeys = promptVariables.map(item => item.key)
let notIncludeKeys: string[] = []
if (promptKeys.length === 0) {
if (keys.length > 0)
notIncludeKeys = keys
}
else {
notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
}
if (notIncludeKeys.length > 0) {
setNotIncludeKeys(notIncludeKeys)
showConfirmAddVar()
return
}
setBlur()
const { getState } = featureStore!
const {
features,
setFeatures,
} = getState()
const newFeatures = produce(features, (draft) => {
if (draft.opening) {
draft.opening.opening_statement = tempValue
draft.opening.suggested_questions = tempSuggestedQuestions
}
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
}
const cancelAutoAddVar = () => {
const { getState } = featureStore!
const {
features,
setFeatures,
} = getState()
const newFeatures = produce(features, (draft) => {
if (draft.opening)
draft.opening.opening_statement = tempValue
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
hideConfirmAddVar()
setBlur()
}
const autoAddVar = () => {
const { getState } = featureStore!
const {
features,
setFeatures,
} = getState()
const newFeatures = produce(features, (draft) => {
if (draft.opening)
draft.opening.opening_statement = tempValue
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
onAutoAddPromptVariable([...notIncludeKeys.map(key => getNewVar(key, 'string'))])
hideConfirmAddVar()
setBlur()
}
const headerRight = !readonly ? (
isFocus ? (
<div className='flex items-center space-x-1'>
<Button
variant='ghost'
size='small'
onClick={handleCancel}
>
{t('common.operation.cancel')}
</Button>
<Button size='small' onClick={handleConfirm} variant="primary">{t('common.operation.save')}</Button>
</div>
) : (
<OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpener') as string} onClick={handleEdit} />
)
) : null
const renderQuestions = () => {
return isFocus ? (
<div>
<div className='flex items-center py-2'>
<div className='shrink-0 flex space-x-0.5 leading-[18px] text-xs font-medium text-gray-500'>
<div className='uppercase'>{t('appDebug.openingStatement.openingQuestion')}</div>
<div>·</div>
<div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
</div>
<div className='ml-3 grow w-0 h-px bg-[#243, 244, 246]'></div>
</div>
<ReactSortable
className="space-y-1"
list={tempSuggestedQuestions.map((name, index) => {
return {
id: index,
name,
}
})}
setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
handle='.handle'
ghostClass="opacity-50"
animation={150}
>
{tempSuggestedQuestions.map((question, index) => {
return (
<div className='group relative rounded-lg border border-gray-200 flex items-center pl-2.5 hover:border-gray-300 hover:bg-white' key={index}>
<div className='handle flex items-center justify-center w-4 h-4 cursor-grab'>
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="#98A2B3" />
</svg>
</div>
<input
type="input"
value={question || ''}
onChange={(e) => {
const value = e.target.value
setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => {
if (index === i)
return value
return item
}))
}}
className={'w-full overflow-x-auto pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer rounded-lg'}
/>
<div
className='block absolute top-1/2 translate-y-[-50%] right-1.5 p-1 rounded-md cursor-pointer hover:bg-[#FEE4E2] hover:text-[#D92D20]'
onClick={() => {
setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
}}
>
<RiDeleteBinLine className='w-3.5 h-3.5' />
</div>
</div>
)
})}</ReactSortable>
{tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
<div
onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'>
<RiAddLine className='w-4 h-4' />
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConfig.addOption')}</div>
</div>
)}
</div>
) : (
<div className='mt-1.5 flex flex-wrap'>
{notEmptyQuestions.map((question, index) => {
return (
<div key={index} className='mt-1 mr-1 max-w-full truncate last:mr-0 shrink-0 leading-8 items-center px-2.5 rounded-lg border border-gray-200 shadow-xs bg-white text-[13px] font-normal text-gray-900 cursor-pointer'>
{question}
</div>
)
})}
</div>
)
}
return (
<Panel
className={cn(isShowConfirmAddVar && 'h-[220px]', 'relative !bg-gray-25')}
title={t('appDebug.openingStatement.title')}
headerIcon={
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.33353 1.33301C4.83572 1.33301 2.00019 4.16854 2.00019 7.66634C2.00019 8.37301 2.11619 9.05395 2.3307 9.69036C2.36843 9.80229 2.39063 9.86853 2.40507 9.91738L2.40979 9.93383L2.40729 9.93903C2.39015 9.97437 2.36469 10.0218 2.31705 10.11L1.2158 12.1484C1.14755 12.2746 1.07633 12.4064 1.02735 12.5209C0.978668 12.6348 0.899813 12.8437 0.938613 13.0914C0.984094 13.3817 1.15495 13.6373 1.40581 13.7903C1.61981 13.9208 1.843 13.9279 1.96683 13.9264C2.09141 13.925 2.24036 13.9095 2.38314 13.8947L5.81978 13.5395C5.87482 13.5338 5.9036 13.5309 5.92468 13.5292L5.92739 13.529L5.93564 13.532C5.96154 13.5413 5.99666 13.5548 6.0573 13.5781C6.76459 13.8506 7.53244 13.9997 8.33353 13.9997C11.8313 13.9997 14.6669 11.1641 14.6669 7.66634C14.6669 4.16854 11.8313 1.33301 8.33353 1.33301ZM5.9799 5.72116C6.73142 5.08698 7.73164 5.27327 8.33144 5.96584C8.93125 5.27327 9.91854 5.09365 10.683 5.72116C11.4474 6.34867 11.5403 7.41567 10.9501 8.16572C10.5845 8.6304 9.6668 9.47911 9.02142 10.0576C8.78435 10.2702 8.66582 10.3764 8.52357 10.4192C8.40154 10.456 8.26134 10.456 8.13931 10.4192C7.99706 10.3764 7.87853 10.2702 7.64147 10.0576C6.99609 9.47911 6.07839 8.6304 5.71276 8.16572C5.12259 7.41567 5.22839 6.35534 5.9799 5.72116Z" fill="#E74694" />
</svg>
}
headerRight={headerRight}
hasHeaderBottomBorder={!hasValue}
isFocus={isFocus}
>
<div className='text-gray-700 text-sm'>
{(hasValue || (!hasValue && isFocus)) ? (
<>
{isFocus
? (
<div>
<textarea
ref={inputRef}
value={tempValue}
rows={3}
onChange={e => setTempValue(e.target.value)}
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
placeholder={t('appDebug.openingStatement.placeholder') as string}
>
</textarea>
</div>
)
: (
<div dangerouslySetInnerHTML={{
__html: coloredContent,
}}></div>
)}
{renderQuestions()}
</>) : (
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
)}
{isShowConfirmAddVar && (
<ConfirmAddVar
varNameArr={notIncludeKeys}
onConfirm={autoAddVar}
onCancel={cancelAutoAddVar}
onHide={hideConfirmAddVar}
/>
)}
</div>
</Panel>
)
}
export default React.memo(OpeningStatement)

View File

@ -1,4 +1,4 @@
import type { TransferMethod, TtsAutoPlay } from '@/types/app' import type { Resolution, TransferMethod, TtsAutoPlay } from '@/types/app'
export type EnabledOrDisabled = { export type EnabledOrDisabled = {
enabled?: boolean enabled?: boolean
@ -30,13 +30,13 @@ export type SensitiveWordAvoidance = EnabledOrDisabled & {
export type FileUpload = { export type FileUpload = {
image?: EnabledOrDisabled & { image?: EnabledOrDisabled & {
detail?: 'high' | 'low' detail?: Resolution
number_limits?: number number_limits?: number
transfer_methods?: TransferMethod[] transfer_methods?: TransferMethod[]
} }
allowed_file_types?: string[] allowed_file_types?: string[]
allowed_file_extensions?: string[] allowed_file_extensions?: string[]
allowed_file_upload_methods?: string[] allowed_file_upload_methods?: TransferMethod[]
number_limits?: number number_limits?: number
} & EnabledOrDisabled } & EnabledOrDisabled