file-uploader

This commit is contained in:
StyleZhang 2024-09-25 16:23:34 +08:00
parent d01e97c1fc
commit 6fdcf6ee21
12 changed files with 100 additions and 14 deletions

View File

@ -21,6 +21,7 @@ import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal
import type { AppData } from '@/models/share' import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { FileList } from '@/app/components/base/file-uploader'
type AnswerProps = { type AnswerProps = {
item: ChatItem item: ChatItem
@ -56,6 +57,7 @@ const Answer: FC<AnswerProps> = ({
more, more,
annotation, annotation,
workflowProcess, workflowProcess,
allFiles,
} = item } = item
const hasAgentThoughts = !!agent_thoughts?.length const hasAgentThoughts = !!agent_thoughts?.length
@ -153,6 +155,15 @@ const Answer: FC<AnswerProps> = ({
/> />
) )
} }
{
allFiles?.length && (
<FileList
files={allFiles}
showDeleteAction={false}
showDownloadAction
/>
)
}
{ {
annotation?.id && annotation.authorName && ( annotation?.id && annotation.authorName && (
<EditTitle <EditTitle

View File

@ -61,6 +61,7 @@ export type ChatItem = IChatItem & {
isError?: boolean isError?: boolean
workflowProcess?: WorkflowProcess workflowProcess?: WorkflowProcess
conversationId?: string conversationId?: string
allFiles?: FileEntity[]
} }
export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void export type OnSend = (message: string, files?: FileEntity[], last_answer?: ChatItem | null) => void

View File

@ -81,7 +81,7 @@ const FileInAttachmentItem = ({
</div> </div>
<div className='shrink-0 flex items-center'> <div className='shrink-0 flex items-center'>
{ {
progress > 0 && progress < 100 && ( progress >= 0 && !file.uploadedId && (
<ProgressCircle <ProgressCircle
className='mr-2.5' className='mr-2.5'
percentage={progress} percentage={progress}

View File

@ -111,15 +111,20 @@ const FileUploaderInAttachment = ({
} }
type FileUploaderInAttachmentWrapperProps = { type FileUploaderInAttachmentWrapperProps = {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void onChange: (files: FileEntity[]) => void
fileConfig: FileUpload fileConfig: FileUpload
} }
const FileUploaderInAttachmentWrapper = ({ const FileUploaderInAttachmentWrapper = ({
value,
onChange, onChange,
fileConfig, fileConfig,
}: FileUploaderInAttachmentWrapperProps) => { }: FileUploaderInAttachmentWrapperProps) => {
return ( return (
<FileContextProvider onChange={onChange}> <FileContextProvider
value={value}
onChange={onChange}
>
<FileUploaderInAttachment fileConfig={fileConfig} /> <FileUploaderInAttachment fileConfig={fileConfig} />
</FileContextProvider> </FileContextProvider>
) )

View File

@ -76,13 +76,14 @@ const FileItem = ({
showDownloadAction && ( showDownloadAction && (
<ActionButton <ActionButton
size='xs' size='xs'
onClick={() => window.open(file.url, '_blank')}
> >
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' /> <RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton> </ActionButton>
) )
} }
{ {
progress > 0 && progress < 100 && ( progress >= 0 && !file.uploadedId && (
<ProgressCircle <ProgressCircle
percentage={progress} percentage={progress}
size={12} size={12}

View File

@ -16,9 +16,12 @@ type Shape = {
setFiles: (files: FileEntity[]) => void setFiles: (files: FileEntity[]) => void
} }
export const createFileStore = (onChange?: (files: FileEntity[]) => void) => { export const createFileStore = (
value: FileEntity[] = [],
onChange?: (files: FileEntity[]) => void,
) => {
return create<Shape>(set => ({ return create<Shape>(set => ({
files: [], files: [...value],
setFiles: (files) => { setFiles: (files) => {
set({ files }) set({ files })
onChange?.(files) onChange?.(files)
@ -43,16 +46,18 @@ export const useFileStore = () => {
type FileProviderProps = { type FileProviderProps = {
children: React.ReactNode children: React.ReactNode
value?: FileEntity[]
onChange?: (files: FileEntity[]) => void onChange?: (files: FileEntity[]) => void
} }
export const FileContextProvider = ({ export const FileContextProvider = ({
children, children,
value,
onChange, onChange,
}: FileProviderProps) => { }: FileProviderProps) => {
const storeRef = useRef<FileStore>() const storeRef = useRef<FileStore>()
if (!storeRef.current) if (!storeRef.current)
storeRef.current = createFileStore(onChange) storeRef.current = createFileStore(value, onChange)
return ( return (
<FileContext.Provider value={storeRef.current}> <FileContext.Provider value={storeRef.current}>

View File

@ -4,6 +4,7 @@ import type { FileEntity } from './types'
import { upload } from '@/service/base' import { upload } from '@/service/base'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileResponse } from '@/types/workflow'
type FileUploadParams = { type FileUploadParams = {
file: File file: File
@ -116,6 +117,22 @@ export const getProcessedFiles = (files: FileEntity[]) => {
})) }))
} }
export const getProcessedFilesFromResponse = (files: FileResponse[]) => {
return files.map((fileItem) => {
return {
id: fileItem.related_id,
name: fileItem.filename,
size: 0,
type: fileItem.mime_type,
progress: 100,
transferMethod: fileItem.transfer_method,
supportFileType: fileItem.type,
uploadedId: fileItem.related_id,
url: fileItem.url,
}
})
}
export const getFileNameFromUrl = (url: string) => { export const getFileNameFromUrl = (url: string) => {
const urlParts = url.split('/') const urlParts = url.split('/')
return urlParts[urlParts.length - 1] || '' return urlParts[urlParts.length - 1] || ''

View File

@ -2,9 +2,13 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import { useWorkflowStore } from '@/app/components/workflow/store' import { useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types' import {
BlockEnum,
InputVarType,
} from '@/app/components/workflow/types'
import { useToastContext } from '@/app/components/base/toast' 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'
export const useCheckStartNodeForm = () => { export const useCheckStartNodeForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -40,7 +44,27 @@ export const useCheckStartNodeForm = () => {
return true return true
}, [storeApi, workflowStore, notify, t]) }, [storeApi, workflowStore, notify, t])
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)
processedInputs[variable.variable] = getProcessedFiles(inputs[variable.variable])
if (variable.type === InputVarType.singleFile)
processedInputs[variable.variable] = getProcessedFiles([inputs[variable.variable]])[0]
})
return processedInputs
}, [storeApi])
return { return {
checkStartNodeForm, checkStartNodeForm,
getProcessedInputs,
} }
} }

View File

@ -23,7 +23,6 @@ import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
type Props = { type Props = {
payload: InputVar payload: InputVar
@ -161,9 +160,10 @@ const FormItem: FC<Props> = ({
} }
{(type === InputVarType.singleFile) && ( {(type === InputVarType.singleFile) && (
<FileUploaderInAttachmentWrapper <FileUploaderInAttachmentWrapper
value={value ? [value] : []}
onChange={(files) => { onChange={(files) => {
if (files.length) if (files.length)
onChange(getProcessedFiles(files)[0]) onChange(files[0])
}} }}
fileConfig={{ fileConfig={{
allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types, allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types,
@ -175,7 +175,8 @@ const FormItem: FC<Props> = ({
)} )}
{(type === InputVarType.multiFiles) && ( {(type === InputVarType.multiFiles) && (
<FileUploaderInAttachmentWrapper <FileUploaderInAttachmentWrapper
onChange={files => onChange(getProcessedFiles(files))} value={value}
onChange={files => onChange(files)}
fileConfig={{ fileConfig={{
allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types, allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types,
allowed_file_extensions: inStepRun ? [] : payload.allowed_file_extensions, allowed_file_extensions: inStepRun ? [] : payload.allowed_file_extensions,

View File

@ -62,7 +62,10 @@ 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 { checkStartNodeForm } = useCheckStartNodeForm() const {
checkStartNodeForm,
getProcessedInputs,
} = useCheckStartNodeForm()
const { const {
conversationId, conversationId,
@ -89,7 +92,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
{ {
query, query,
files, files,
inputs: workflowStore.getState().inputs, inputs: getProcessedInputs(workflowStore.getState().inputs),
conversation_id: conversationId, conversation_id: conversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null, parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
}, },
@ -97,7 +100,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController), onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
}, },
) )
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail]) }, [chatListRef, conversationId, handleSend, workflowStore, appDetail, getProcessedInputs])
const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const index = chatList.findIndex(item => item.id === chatItem.id)

View File

@ -6,6 +6,7 @@ import {
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { produce, setAutoFreeze } from 'immer' import { produce, setAutoFreeze } from 'immer'
import { uniqBy } from 'lodash-es'
import { useWorkflowRun } from '../../hooks' import { useWorkflowRun } from '../../hooks'
import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' import { NodeRunningStatus, WorkflowRunningStatus } from '../../types'
import type { import type {
@ -16,7 +17,10 @@ import type {
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 { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel/utils'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' import {
getProcessedFiles,
getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
type GetAbortController = (abortController: AbortController) => void type GetAbortController = (abortController: AbortController) => void
@ -378,6 +382,8 @@ export const useChat = (
: {}), : {}),
...data, ...data,
} as any } as any
const processedFilesFromResponse = getProcessedFilesFromResponse(data.files || [])
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
handleUpdateChatList(produce(chatListRef.current, (draft) => { handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id) const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = { draft[currentIndex] = {

View File

@ -6,6 +6,7 @@ import type {
EnvironmentVariable, EnvironmentVariable,
Node, Node,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import type { TransferMethod } from '@/types/app'
export type NodeTracing = { export type NodeTracing = {
id: string id: string
@ -128,6 +129,16 @@ export type NodeStartedResponse = {
} }
} }
export type FileResponse = {
related_id: string
extension: string
filename: string
mime_type: string
transfer_method: TransferMethod
type: string
url: string
}
export type NodeFinishedResponse = { export type NodeFinishedResponse = {
task_id: string task_id: string
workflow_run_id: string workflow_run_id: string
@ -155,6 +166,7 @@ export type NodeFinishedResponse = {
iteration_id?: string iteration_id?: string
} }
created_at: number created_at: number
files?: FileResponse[]
} }
} }