fix: chat check inputs form

This commit is contained in:
StyleZhang 2024-09-26 11:06:51 +08:00
parent 9ce9a52a86
commit 1d027fa065
11 changed files with 125 additions and 116 deletions

View File

@ -55,7 +55,7 @@ const ChatWrapper = () => {
appConfig, appConfig,
{ {
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
promptVariables: inputsForms, inputsForm: inputsForms,
}, },
appPrevChatList, appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
@ -165,6 +165,8 @@ const ChatWrapper = () => {
chatFooterClassName='pb-4' chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`} chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
onSend={doSend} onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
onStopResponding={handleStop} onStopResponding={handleStop}
chatNode={chatNode} chatNode={chatNode}

View File

@ -11,6 +11,8 @@ import type {
OnSend, OnSend,
} from '../../types' } from '../../types'
import type { Theme } from '../../embedded-chatbot/theme/theme-context' import type { Theme } from '../../embedded-chatbot/theme/theme-context'
import type { InputForm } from '../type'
import { useCheckInputsForms } from '../check-input-forms-hooks'
import { useTextAreaHeight } from './hooks' import { useTextAreaHeight } from './hooks'
import Operation from './operation' import Operation from './operation'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -18,7 +20,7 @@ import { FileListInChatInput } from '@/app/components/base/file-uploader'
import { useFile } from '@/app/components/base/file-uploader/hooks' import { useFile } from '@/app/components/base/file-uploader/hooks'
import { import {
FileContextProvider, FileContextProvider,
useStore, useFileStore,
} from '@/app/components/base/file-uploader/store' } from '@/app/components/base/file-uploader/store'
import VoiceInput from '@/app/components/base/voice-input' import VoiceInput from '@/app/components/base/voice-input'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
@ -34,7 +36,8 @@ type ChatInputAreaProps = {
visionConfig?: FileUpload visionConfig?: FileUpload
speechToTextConfig?: EnableType speechToTextConfig?: EnableType
onSend?: OnSend onSend?: OnSend
onSendCheck?: () => boolean inputs?: Record<string, any>
inputsForm?: InputForm[]
theme?: Theme | null theme?: Theme | null
} }
const ChatInputArea = ({ const ChatInputArea = ({
@ -45,7 +48,8 @@ const ChatInputArea = ({
visionConfig, visionConfig,
speechToTextConfig = { enabled: true }, speechToTextConfig = { enabled: true },
onSend, onSend,
onSendCheck = () => true, inputs = {},
inputsForm = [],
// theme, // theme,
}: ChatInputAreaProps) => { }: ChatInputAreaProps) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -61,8 +65,7 @@ const ChatInputArea = ({
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const isUseInputMethod = useRef(false) const isUseInputMethod = useRef(false)
const [showVoiceInput, setShowVoiceInput] = useState(false) const [showVoiceInput, setShowVoiceInput] = useState(false)
const files = useStore(s => s.files) const filesStore = useFileStore()
const setFiles = useStore(s => s.setFiles)
const { const {
handleDragFileEnter, handleDragFileEnter,
handleDragFileLeave, handleDragFileLeave,
@ -71,9 +74,11 @@ const ChatInputArea = ({
handleClipboardPasteFile, handleClipboardPasteFile,
isDragActive, isDragActive,
} = useFile(visionConfig!) } = useFile(visionConfig!)
const { checkInputsForm } = useCheckInputsForms()
const handleSend = () => { const handleSend = () => {
if (onSend) { if (onSend) {
const { files, setFiles } = filesStore.getState()
if (files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) { if (files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
return return
@ -82,7 +87,7 @@ const ChatInputArea = ({
notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') }) notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') })
return return
} }
if (onSendCheck()) { if (checkInputsForm(inputs, inputsForm)) {
onSend(query, files) onSend(query, files)
setQuery('') setQuery('')
setFiles([]) setFiles([])

View File

@ -0,0 +1,51 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { InputForm } from './type'
import { useToastContext } from '@/app/components/base/toast'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
export const useCheckInputsForms = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const checkInputsForm = useCallback((inputs: Record<string, any>, inputsForm: InputForm[]) => {
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForm.filter(({ required }) => required)
if (requiredVars?.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return
if (!inputs[variable])
hasEmptyInput = label as string
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputs[variable]) {
const files = inputs[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transfer_method === TransferMethod.local_file && !files.uploadedId
}
})
}
if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}
if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
return
}
return true
}, [notify, t])
return {
checkInputsForm,
}
}

View File

@ -12,12 +12,12 @@ import type {
ChatConfig, ChatConfig,
ChatItem, ChatItem,
Inputs, Inputs,
PromptVariable,
} from '../types' } from '../types'
import type { InputForm } from './type'
import { processOpeningStatement } from './utils'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { ssePost } from '@/service/base' import { ssePost } from '@/service/base'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils'
import type { Annotation } from '@/models/log' import type { Annotation } from '@/models/log'
import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
@ -33,50 +33,11 @@ type SendCallback = {
isPublicAPI?: boolean isPublicAPI?: boolean
} }
export const useCheckPromptVariables = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const checkPromptVariables = useCallback((promptVariablesConfig: {
inputs: Inputs
promptVariables: PromptVariable[]
}) => {
const {
promptVariables,
inputs,
} = promptVariablesConfig
let hasEmptyInput = ''
const requiredVars = promptVariables.filter(({ key, name, required, type }) => {
if (type !== 'string' && type !== 'paragraph' && type !== 'select')
return false
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
})
if (requiredVars?.length) {
requiredVars.forEach(({ key, name }) => {
if (hasEmptyInput)
return
if (!inputs[key])
hasEmptyInput = name
})
}
if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}
}, [notify, t])
return checkPromptVariables
}
export const useChat = ( export const useChat = (
config?: ChatConfig, config?: ChatConfig,
promptVariablesConfig?: { formSettings?: {
inputs: Inputs inputs: Inputs
promptVariables: PromptVariable[] inputsForm: InputForm[]
}, },
prevChatList?: ChatItem[], prevChatList?: ChatItem[],
stopChat?: (taskId: string) => void, stopChat?: (taskId: string) => void,
@ -94,7 +55,6 @@ export const useChat = (
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null) const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
const checkPromptVariables = useCheckPromptVariables()
const params = useParams() const params = useParams()
const pathname = usePathname() const pathname = usePathname()
useEffect(() => { useEffect(() => {
@ -114,8 +74,8 @@ export const useChat = (
}, []) }, [])
const getIntroduction = useCallback((str: string) => { const getIntroduction = useCallback((str: string) => {
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) }, [formSettings?.inputs, formSettings?.inputsForm])
useEffect(() => { useEffect(() => {
if (config?.opening_statement) { if (config?.opening_statement) {
handleUpdateChatList(produce(chatListRef.current, (draft) => { handleUpdateChatList(produce(chatListRef.current, (draft) => {
@ -216,9 +176,6 @@ export const useChat = (
return false return false
} }
if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables)
checkPromptVariables(promptVariablesConfig)
const questionId = `question-${Date.now()}` const questionId = `question-${Date.now()}`
const questionItem = { const questionItem = {
id: questionId, id: questionId,
@ -575,15 +532,16 @@ export const useChat = (
}) })
return true return true
}, [ }, [
checkPromptVariables,
config?.suggested_questions_after_answer, config?.suggested_questions_after_answer,
updateCurrentQA, updateCurrentQA,
t, t,
notify, notify,
promptVariablesConfig,
handleUpdateChatList, handleUpdateChatList,
handleResponding, handleResponding,
formatTime, formatTime,
params.token,
params.appId,
pathname,
]) ])
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {

View File

@ -25,6 +25,7 @@ import Answer from './answer'
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'
import type { InputForm } from './type'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { Emoji } from '@/app/components/tools/types' import type { Emoji } from '@/app/components/tools/types'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
@ -43,7 +44,8 @@ export type ChatProps = {
onStopResponding?: () => void onStopResponding?: () => void
noChatInput?: boolean noChatInput?: boolean
onSend?: OnSend onSend?: OnSend
onSendCheck?: () => boolean inputs?: Record<string, any>
inputsForm?: InputForm[]
onRegenerate?: OnRegenerate onRegenerate?: OnRegenerate
chatContainerClassName?: string chatContainerClassName?: string
chatContainerInnerClassName?: string chatContainerInnerClassName?: string
@ -73,7 +75,8 @@ const Chat: FC<ChatProps> = ({
appData, appData,
config, config,
onSend, onSend,
onSendCheck, inputs,
inputsForm,
onRegenerate, onRegenerate,
chatList, chatList,
isResponding, isResponding,
@ -283,7 +286,8 @@ const Chat: FC<ChatProps> = ({
visionConfig={config?.file_upload} visionConfig={config?.file_upload}
speechToTextConfig={config?.speech_to_text} speechToTextConfig={config?.speech_to_text}
onSend={onSend} onSend={onSend}
onSendCheck={onSendCheck} inputs={inputs}
inputsForm={inputsForm}
theme={themeBuilder?.theme} theme={themeBuilder?.theme}
/> />
) )

View File

@ -2,6 +2,7 @@ import type { TypeWithI18N } from '@/app/components/header/account-setting/model
import type { Annotation, MessageRating } from '@/models/log' import type { Annotation, MessageRating } from '@/models/log'
import type { VisionFile } from '@/types/app' import type { VisionFile } from '@/types/app'
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { InputVarType } from '@/app/components/workflow/types'
export type MessageMore = { export type MessageMore = {
time: string time: string
@ -130,3 +131,11 @@ export type AnnotationReply = {
annotation_id: string annotation_id: string
annotation_author_name: string annotation_author_name: string
} }
export type InputForm = {
type: InputVarType
label: string
variable: any
required: boolean
[key: string]: any
}

View File

@ -0,0 +1,16 @@
import type { InputForm } from './type'
export const processOpeningStatement = (openingStatement: string, inputs: Record<string, any>, inputsForm: InputForm[]) => {
if (!openingStatement)
return openingStatement
return openingStatement.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return name
}
const valueObj = inputsForm.find(v => v.variable === key)
return valueObj ? `{{${valueObj.label}}}` : match
})
}

View File

@ -74,12 +74,13 @@ const FileItem = ({
</div> </div>
{ {
showDownloadAction && ( showDownloadAction && (
<ActionButton <a href={file.url} download={true} target='_blank'>
size='xs' <ActionButton
onClick={() => window.open(file.url, '_blank')} size='xs'
> >
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' /> <RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton> </ActionButton>
</a>
) )
} }
{ {

View File

@ -1,48 +1,14 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { import {
BlockEnum, BlockEnum,
InputVarType, InputVarType,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { useToastContext } from '@/app/components/base/toast'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
export const useCheckStartNodeForm = () => { export const useCheckStartNodeForm = () => {
const { t } = useTranslation()
const storeApi = useStoreApi() const storeApi = useStoreApi()
const workflowStore = useWorkflowStore()
const { notify } = useToastContext()
const checkStartNodeForm = useCallback(() => {
const { getNodes } = storeApi.getState()
const nodes = getNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables: InputVar[] = startNode?.data.variables || []
const inputs = workflowStore.getState().inputs
let hasEmptyInput = ''
const requiredVars = variables.filter(({ required }) => required)
if (requiredVars?.length) {
requiredVars.forEach(({ variable, label }) => {
if (hasEmptyInput)
return
if (!inputs[variable])
hasEmptyInput = label as string
})
}
if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}
return true
}, [storeApi, workflowStore, notify, t])
const getProcessedInputs = useCallback((inputs: Record<string, any>) => { const getProcessedInputs = useCallback((inputs: Record<string, any>) => {
const { getNodes } = storeApi.getState() const { getNodes } = storeApi.getState()
@ -53,10 +19,10 @@ export const useCheckStartNodeForm = () => {
const processedInputs = { ...inputs } const processedInputs = { ...inputs }
variables.forEach((variable) => { variables.forEach((variable) => {
if (variable.type === InputVarType.multiFiles) if (variable.type === InputVarType.multiFiles && inputs[variable.variable])
processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable]) processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable])
if (variable.type === InputVarType.singleFile) if (variable.type === InputVarType.singleFile && inputs[variable.variable])
processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0] processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0]
}) })
@ -64,7 +30,6 @@ export const useCheckStartNodeForm = () => {
}, [storeApi]) }, [storeApi])
return { return {
checkStartNodeForm,
getProcessedInputs, getProcessedInputs,
} }
} }

View File

@ -62,10 +62,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
} }
}, [features.opening, features.suggested, features.text2speech, features.speech2text, features.citation, features.moderation, features.file]) }, [features.opening, features.suggested, features.text2speech, features.speech2text, features.citation, features.moderation, features.file])
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel) const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
const { const { getProcessedInputs } = useCheckStartNodeForm()
checkStartNodeForm,
getProcessedInputs,
} = useCheckStartNodeForm()
const { const {
conversationId, conversationId,
@ -81,7 +78,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
config, config,
{ {
inputs, inputs,
promptVariables: (startVariables as any) || [], inputsForm: (startVariables || []) as any,
}, },
[], [],
taskId => stopChatMessageResponding(appDetail!.id, taskId), taskId => stopChatMessageResponding(appDetail!.id, taskId),
@ -146,7 +143,8 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
showFeatureBar showFeatureBar
onFeatureBarClick={setShowFeaturesPanel} onFeatureBarClick={setShowFeaturesPanel}
onSend={doSend} onSend={doSend}
onSendCheck={checkStartNodeForm} inputs={inputs}
inputsForm={(startVariables || []) as any}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
onStopResponding={handleStop} onStopResponding={handleStop}
chatNode={( chatNode={(

View File

@ -12,11 +12,11 @@ import { NodeRunningStatus, WorkflowRunningStatus } from '../../types'
import type { import type {
ChatItem, ChatItem,
Inputs, Inputs,
PromptVariable,
} from '@/app/components/base/chat/types' } from '@/app/components/base/chat/types'
import type { InputForm } from '@/app/components/base/chat/chat/type'
import { processOpeningStatement } from '@/app/components/base/chat/chat/utils'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils'
import { import {
getProcessedFiles, getProcessedFiles,
getProcessedFilesFromResponse, getProcessedFilesFromResponse,
@ -29,9 +29,9 @@ type SendCallback = {
} }
export const useChat = ( export const useChat = (
config: any, config: any,
promptVariablesConfig?: { formSettings?: {
inputs: Inputs inputs: Inputs
promptVariables: PromptVariable[] inputsForm: InputForm[]
}, },
prevChatList?: ChatItem[], prevChatList?: ChatItem[],
stopChat?: (taskId: string) => void, stopChat?: (taskId: string) => void,
@ -67,8 +67,8 @@ export const useChat = (
}, []) }, [])
const getIntroduction = useCallback((str: string) => { const getIntroduction = useCallback((str: string) => {
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
}, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) }, [formSettings?.inputs, formSettings?.inputsForm])
useEffect(() => { useEffect(() => {
if (config?.opening_statement) { if (config?.opening_statement) {
handleUpdateChatList(produce(chatListRef.current, (draft) => { handleUpdateChatList(produce(chatListRef.current, (draft) => {