mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
webapp chat embedded chat
This commit is contained in:
parent
296253a365
commit
a7d53abba9
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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] = {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -368,6 +368,7 @@ export type UploadFileSetting = {
|
|||
allowed_file_types: SupportUploadFileTypes[]
|
||||
allowed_file_extensions?: string[]
|
||||
max_length: number
|
||||
number_limits?: number
|
||||
}
|
||||
|
||||
export type VisionSetting = {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user