webapp chat embedded chat

This commit is contained in:
StyleZhang 2024-09-26 13:47:36 +08:00
parent 296253a365
commit a7d53abba9
13 changed files with 117 additions and 53 deletions

View File

@ -3,17 +3,20 @@ 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 {
appParams,
inputsForms,
newConversationInputs,
handleNewConversationInputsChange,
isMobile,
} = useChatWithHistoryContext()
const handleFormChange = useCallback((variable: string, value: string) => {
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputs,
[variable]: value,
@ -48,6 +51,20 @@ const Form = () => {
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: appParams?.file_upload?.allowed_file_types,
allowed_file_extensions: appParams?.file_upload?.allowed_file_extensions,
allowed_file_upload_methods: appParams?.file_upload?.allowed_file_upload_methods,
number_limits: appParams?.file_upload?.number_limits,
}}
/>
)
}
return (
<PortalSelect

View File

@ -127,7 +127,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 +147,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',

View File

@ -6,6 +6,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { produce, setAutoFreeze } from 'immer'
import { uniqBy } from 'lodash-es'
import { useParams, usePathname } from 'next/navigation'
import { v4 as uuidV4 } from 'uuid'
import type {
@ -14,7 +15,10 @@ import type {
Inputs,
} from '../types'
import type { InputForm } from './type'
import { processOpeningStatement } from './utils'
import {
getProcessedInputs,
processOpeningStatement,
} from './utils'
import { TransferMethod } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast'
import { ssePost } from '@/service/base'
@ -23,7 +27,10 @@ import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import useTimestamp from '@/hooks/use-timestamp'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import {
getProcessedFiles,
getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils'
type GetAbortController = (abortController: AbortController) => void
type SendCallback = {
@ -206,12 +213,13 @@ export const useChat = (
handleResponding(true)
hasStopResponded.current = false
const { query, files, ...restData } = data
const { query, files, inputs, ...restData } = data
const bodyParams = {
response_mode: 'streaming',
conversation_id: conversationId.current,
files: getProcessedFiles(files || []),
query,
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
...restData,
}
if (bodyParams?.files?.length) {
@ -512,6 +520,8 @@ export const useChat = (
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id)
})
responseItem.workflowProcess!.tracing[currentIndex] = data as any
const processedFilesFromResponse = getProcessedFilesFromResponse(data.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {

View File

@ -1,4 +1,6 @@
import type { InputForm } from './type'
import { InputVarType } from '@/app/components/workflow/types'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
export const processOpeningStatement = (openingStatement: string, inputs: Record<string, any>, inputsForm: InputForm[]) => {
if (!openingStatement)
@ -14,3 +16,17 @@ export const processOpeningStatement = (openingStatement: string, inputs: Record
return valueObj ? `{{${valueObj.label}}}` : match
})
}
export const getProcessedInputs = (inputs: Record<string, any>, inputsForm: InputForm[]) => {
const processedInputs = { ...inputs }
inputsForm.forEach((item) => {
if (item.type === InputVarType.multiFiles && inputs[item.variable])
processedInputs[item.variable] = getProcessedFiles(inputs[item.variable])
if (item.type === InputVarType.singleFile && inputs[item.variable])
processedInputs[item.variable] = getProcessedFiles([inputs[item.variable]])[0]
})
return processedInputs
}

View File

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

View File

@ -3,17 +3,20 @@ import { useTranslation } from 'react-i18next'
import { useEmbeddedChatbotContext } 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 {
appParams,
inputsForms,
newConversationInputs,
handleNewConversationInputsChange,
isMobile,
} = useEmbeddedChatbotContext()
const handleFormChange = useCallback((variable: string, value: string) => {
const handleFormChange = useCallback((variable: string, value: any) => {
handleNewConversationInputsChange({
...newConversationInputs,
[variable]: value,
@ -49,6 +52,32 @@ const Form = () => {
)
}
if (form.type === 'number') {
return (
<input
className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
type="number"
value={newConversationInputs[variable] || ''}
onChange={e => handleFormChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: appParams?.file_upload?.allowed_file_types,
allowed_file_extensions: appParams?.file_upload?.allowed_file_extensions,
allowed_file_upload_methods: appParams?.file_upload?.allowed_file_upload_methods,
number_limits: appParams?.file_upload?.number_limits,
}}
/>
)
}
return (
<PortalSelect
popupClassName='w-[200px]'

View File

@ -123,6 +123,20 @@ export const useEmbeddedChatbot = () => {
}
}
if (item['file-list']) {
return {
...item['file-list'],
type: 'file-list',
}
}
if (item.file) {
return {
...item.file,
type: 'file',
}
}
let value = initInputs[item['text-input'].variable]
if (value && item['text-input'].max_length && value.length > item['text-input'].max_length)
value = value.slice(0, item['text-input'].max_length)

View File

@ -16,4 +16,3 @@ export * from './use-workflow-variables'
export * from './use-shortcuts'
export * from './use-workflow-interactions'
export * from './use-workflow-mode'
export * from './use-check-start-node-form'

View File

@ -1,35 +0,0 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import {
BlockEnum,
InputVarType,
} from '@/app/components/workflow/types'
import type { InputVar } from '@/app/components/workflow/types'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
export const useCheckStartNodeForm = () => {
const storeApi = useStoreApi()
const getProcessedInputs = useCallback((inputs: Record<string, any>) => {
const { getNodes } = storeApi.getState()
const nodes = getNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables: InputVar[] = startNode?.data.variables || []
const processedInputs = { ...inputs }
variables.forEach((variable) => {
if (variable.type === InputVarType.multiFiles && inputs[variable.variable])
processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable])
if (variable.type === InputVarType.singleFile && inputs[variable.variable])
processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0]
})
return processedInputs
}, [storeApi])
return {
getProcessedInputs,
}
}

View File

@ -12,7 +12,6 @@ import {
useStore,
useWorkflowStore,
} from '../../store'
import { useCheckStartNodeForm } from '../../hooks'
import type { StartNodeType } from '../../nodes/start/types'
import Empty from './empty'
import UserInput from './user-input'
@ -62,7 +61,6 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
}
}, [features.opening, features.suggested, features.text2speech, features.speech2text, features.citation, features.moderation, features.file])
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
const { getProcessedInputs } = useCheckStartNodeForm()
const {
conversationId,
@ -89,7 +87,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
{
query,
files,
inputs: getProcessedInputs(workflowStore.getState().inputs),
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
},
@ -97,7 +95,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
},
)
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail, getProcessedInputs])
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id)

View File

@ -15,7 +15,7 @@ import {
useStore,
useWorkflowStore,
} from '../store'
import { useCheckStartNodeForm, useWorkflowRun } from '../hooks'
import { useWorkflowRun } from '../hooks'
import type { StartNodeType } from '../nodes/start/types'
import { TransferMethod } from '../../base/text-generation/types'
import Button from '@/app/components/base/button'
@ -56,8 +56,6 @@ const InputsPanel = ({ onRun }: Props) => {
return data
}, [fileSettings?.image?.enabled, startVariables])
const { getProcessedInputs } = useCheckStartNodeForm()
const handleValueChange = (variable: string, v: any) => {
const {
inputs,
@ -78,8 +76,8 @@ const InputsPanel = ({ onRun }: Props) => {
const doRun = useCallback(() => {
onRun()
handleRun({ inputs: getProcessedInputs(inputs), files })
}, [files, getProcessedInputs, handleRun, inputs, onRun])
handleRun({ inputs, files })
}, [files, handleRun, inputs, onRun])
const canRun = useMemo(() => {
if (files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id))

View File

@ -368,6 +368,7 @@ export type UploadFileSetting = {
allowed_file_types: SupportUploadFileTypes[]
allowed_file_extensions?: string[]
max_length: number
number_limits?: number
}
export type VisionSetting = {

View File

@ -6,6 +6,7 @@ import type {
RerankingModeEnum,
WeightedScoreEnum,
} from '@/models/datasets'
import type { UploadFileSetting } from '@/app/components/workflow/types'
export enum Theme {
light = 'light',
@ -242,7 +243,7 @@ export type ModelConfig = {
dataset_configs: DatasetConfigs
file_upload?: {
image: VisionSettings
}
} & UploadFileSetting
files?: VisionFile[]
created_at?: number
updated_at?: number