Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
zxhlyh 2024-10-21 10:32:37 +08:00 • committed by GitHub
commit 7a1d6fe509
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
445 changed files with 11759 additions and 6922 deletions

View file

@ -15,7 +15,6 @@ import {
getUrl,
stopChatMessageResponding,
} from '@/service/share'
import AnswerIcon from '@/app/components/base/answer-icon'
const ChatWrapper = () => {
const {
@ -56,7 +55,7 @@ const ChatWrapper = () => {
appConfig,
{
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
promptVariables: inputsForms,
inputsForm: inputsForms,
},
appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
@ -65,19 +64,18 @@ const ChatWrapper = () => {
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
}
if (appConfig?.file_upload?.image.enabled && files?.length)
data.files = files
handleSend(
getUrl('chat-messages', isInstalledApp, appId || ''),
data,
@ -152,35 +150,31 @@ const ChatWrapper = () => {
isMobile,
])
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon
iconType={appData.site.icon_type}
icon={appData.site.icon}
background={appData.site.icon_background}
imageUrl={appData.site.icon_url}
/>
: null
return (
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-full ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>
<div
className='h-full bg-chatbot-bg overflow-hidden'
>
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
hideProcessDetail
themeBuilder={themeBuilder}
/>
</div>
)
}

View file

@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import Textarea from '@/app/components/base/textarea'
type InputProps = {
form: any
@ -23,9 +24,9 @@ const FormInput: FC<InputProps> = ({
if (type === 'paragraph') {
return (
<textarea
<Textarea
value={value}
className='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none resize-none'
className='resize-none'
onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>

View file

@ -1,24 +1,26 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context'
import Input from './form-input'
import { PortalSelect } from '@/app/components/base/select'
import { InputVarType } from '@/app/components/workflow/types'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
const Form = () => {
const { t } = useTranslation()
const {
inputsForms,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
isMobile,
} = useChatWithHistoryContext()
const handleFormChange = useCallback((variable: string, value: string) => {
const handleFormChange = (variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputs,
...newConversationInputsRef.current,
[variable]: value,
})
}, [newConversationInputs, handleNewConversationInputsChange])
}
const renderField = (form: any) => {
const {
@ -48,6 +50,34 @@ const Form = () => {
/>
)
}
if (form.type === InputVarType.singleFile) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
onChange={files => handleFormChange(variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
}}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
}}
/>
)
}
return (
<PortalSelect

View file

@ -30,6 +30,7 @@ export type ChatWithHistoryContextValue = {
conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean
newConversationInputs: Record<string, any>
newConversationInputsRef: RefObject<Record<string, any>>
handleNewConversationInputsChange: (v: Record<string, any>) => void
inputsForms: any[]
handleNewConversation: () => void
@ -57,6 +58,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
conversationList: [],
showConfigPanelBeforeChat: false,
newConversationInputs: {},
newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {},
inputsForms: [],
handleNewConversation: () => {},

View file

@ -37,6 +37,8 @@ import type {
import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config'
import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
@ -127,7 +129,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setNewConversationInputs(newInputs)
}, [])
const inputsForms = useMemo(() => {
return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => {
return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => {
if (item.paragraph) {
return {
...item.paragraph,
@ -147,6 +149,20 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
}
if (item['file-list']) {
return {
...item['file-list'],
type: 'file-list',
}
}
if (item.file) {
return {
...item.file,
type: 'file',
}
}
return {
...item['text-input'],
type: 'text-input',
@ -206,21 +222,38 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { notify } = useToastContext()
const checkInputsRequired = useCallback((silent?: boolean) => {
if (inputsForms.length) {
for (let i = 0; i < inputsForms.length; i += 1) {
const item = inputsForms[i]
if (item.required && !newConversationInputsRef.current[item.variable]) {
if (!silent) {
notify({
type: 'error',
message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }),
})
}
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return
if (fileIsUploading)
return
if (!newConversationInputsRef.current[variable] && !silent)
hasEmptyInput = label as string
if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
const files = newConversationInputsRef.current[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
}
}
return true
})
}
if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}
if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return
}
return true
@ -377,6 +410,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setShowConfigPanelBeforeChat,
setShowNewConversationItemInList,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,

View file

@ -125,6 +125,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
conversationList,
showConfigPanelBeforeChat,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,
@ -158,6 +159,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
conversationList,
showConfigPanelBeforeChat,
newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange,
inputsForms,
handleNewConversation,