feat(web): file add status

This commit is contained in:
zhaoying
2026-03-19 19:00:31 +08:00
parent 27b782e12a
commit b84aba71e7
7 changed files with 109 additions and 70 deletions

View File

@@ -2,10 +2,11 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14 * @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-19 16:05:56 * @Last Modified time: 2026-03-19 18:44:51
*/ */
import { type FC, useEffect, useMemo } from 'react' import { type FC, useEffect, useMemo } from 'react'
import { Flex, Input, Form } from 'antd' import { Flex, Input, Form, Spin } from 'antd'
import clsx from 'clsx'
import SendIcon from '@/assets/images/conversation/send.svg' import SendIcon from '@/assets/images/conversation/send.svg'
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg' import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
@@ -69,6 +70,8 @@ const ChatInput: FC<ChatInputProps> = ({
onSend(values.message) onSend(values.message)
} }
console.log('previewFileList', previewFileList)
return ( return (
<div className={`rb:absolute rb:bottom-3 rb:left-0 rb:right-0 rb:w-full ${className}`}> <div className={`rb:absolute rb:bottom-3 rb:left-0 rb:right-0 rb:w-full ${className}`}>
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:min-h-30"> <Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:min-h-30">
@@ -76,39 +79,54 @@ const ChatInput: FC<ChatInputProps> = ({
{previewFileList.map((file) => { {previewFileList.map((file) => {
if (file.type.includes('image')) { if (file.type.includes('image')) {
return ( return (
<div key={file.url || file.uid} className="rb:inline-block rb:group rb:relative rb:rounded-lg"> <Spin key={`${file.url || file.uid}_${file.status}`} spinning={file.status === 'uploading'}>
<div key={file.url || file.uid} className={clsx("rb:inline-block rb:group rb:relative rb:rounded-lg", {
'rb:border rb:border-[#FF5D34]': file.status === 'error'
})}>
<img src={file.url} alt={file.name} className="rb:size-12! rb:rounded-lg rb:object-cover rb:cursor-pointer" /> <img src={file.url} alt={file.name} className="rb:size-12! rb:rounded-lg rb:object-cover rb:cursor-pointer" />
<div <div
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]" className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
onClick={() => handleDelete(file)} onClick={() => handleDelete(file)}
></div> ></div>
</div> </div>
</Spin>
) )
} }
if (file.type.includes('video')) { if (file.type.includes('video')) {
return ( return (
<div key={file.url || file.uid} className="rb:w-45 rb:h-16 rb:inline-block rb:group rb:relative rb:rounded-lg"> <Spin key={`${file.url || file.uid}_${file.status}`} spinning={file.status === 'uploading'}>
<video src={file.url} controls className="rb:w-45 rb:h-16 rb:rounded-lg rb:object-cover rb:cursor-pointer" /> <div key={file.url || file.uid} className={clsx("rb:w-45 rb:h-16 rb:inline-block rb:group rb:relative rb:rounded-lg", {
'rb:border rb:border-[#FF5D34]': file.status === 'error'
})}>
<video src={file.url} controls className="rb:w-45 rb:h-15.5 rb:rounded-lg rb:object-cover rb:cursor-pointer" />
<div <div
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]" className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
onClick={() => handleDelete(file)} onClick={() => handleDelete(file)}
></div> ></div>
</div> </div>
</Spin>
) )
} }
if (file.type.includes('audio')) { if (file.type.includes('audio')) {
return ( return (
<div key={file.url || file.uid} className="rb:w-45 rb:h-16 rb:inline-flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5 rb:gap-2"> <Spin key={`${file.url || file.uid}_${file.status}`} spinning={file.status === 'uploading'}>
<audio src={file.url} controls className="rb:w-45 rb:h-16" /> <div key={file.url || file.uid} className={clsx("rb:w-45 rb:h-16 rb:inline-flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5 rb:gap-2", {
'rb:border rb:border-[#FF5D34]': file.status === 'error'
})}>
<audio src={file.url} controls className="rb:w-45 rb:h-15.5" />
<div <div
className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]" className="rb:hidden rb:group-hover:block rb:absolute rb:-right-1 rb:-top-1 rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/delete.svg')] rb:hover:bg-[url('@/assets/images/conversation/delete_hover.svg')]"
onClick={() => handleDelete(file)} onClick={() => handleDelete(file)}
></div> ></div>
</div> </div>
</Spin>
) )
} }
return ( return (
<div key={file.url || file.uid} className="rb:w-45 rb:text-[12px] rb:gap-2.5 rb:flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5"> <Spin key={`${file.url || file.uid}_${file.status}`} spinning={file.status === 'uploading'}>
<div key={file.url || file.uid} className={clsx("rb:w-45 rb:text-[12px] rb:gap-2.5 rb:flex rb:items-center rb:group rb:relative rb:rounded-lg rb:bg-[#F0F3F8] rb:py-2 rb:px-2.5", {
'rb:border rb:border-[#FF5D34]': file.status === 'error'
})}>
{file.type.includes('pdf') {file.type.includes('pdf')
? <div ? <div
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/pdf.svg')]" className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/pdf_disabled.svg')] rb:hover:bg-[url('@/assets/images/conversation/pdf.svg')]"
@@ -132,6 +150,7 @@ const ChatInput: FC<ChatInputProps> = ({
onClick={() => handleDelete(file)} onClick={() => handleDelete(file)}
></div> ></div>
</div> </div>
</Spin>
) )
})} })}
</Flex></div>} </Flex></div>}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-03-17 14:22:25 * @Date: 2026-03-17 14:22:25
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:55:13 * @Last Modified time: 2026-03-19 18:59:37
*/ */
// Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration // Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration
import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react' import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react'
@@ -85,10 +85,18 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
// Append newly uploaded file to the file list when upload is complete // Append newly uploaded file to the file list when upload is complete
const fileChange = (file?: any) => { const fileChange = (file?: any) => {
if (file?.status !== 'done') return console.log('file', file)
const files = [...(queryValues?.files || []), file] const lastFiles = form.getFieldValue('files') || [];
form.setFieldValue('files', files) const index = lastFiles.findIndex((item: any) => item.uid === file.uid)
onFilesChange?.(files) if (index > -1) {
lastFiles[index] = file
} else {
lastFiles.push(file)
}
form.setFieldValue('files', [...lastFiles])
onFilesChange?.([...lastFiles])
console.log('lastFiles', lastFiles)
} }
// Append recorded audio file to the file list and notify parent // Append recorded audio file to the file list and notify parent

View File

@@ -183,7 +183,7 @@ const TestChat: FC<TestChatProps> = ({
const handleSend = () => { const handleSend = () => {
if (loading || !application || !message || !message?.trim()) return if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
const variables = toolbarRef.current?.getVariables() || [] const variables = toolbarRef.current?.getVariables() || []
const { isCanSend, params } = buildVariableParams(variables) const { isCanSend, params } = buildVariableParams(variables)
if (!isCanSend) return if (!isCanSend) return
@@ -235,7 +235,7 @@ const TestChat: FC<TestChatProps> = ({
const handleWorkflowSend = () => { const handleWorkflowSend = () => {
if (loading || !application || !message || !message?.trim()) return if (loading || !application || !message || !message?.trim()) return
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
const variables = toolbarRef.current?.getVariables() || [] const variables = toolbarRef.current?.getVariables() || []
const { isCanSend, params } = buildVariableParams(variables) const { isCanSend, params } = buildVariableParams(variables)
if (!isCanSend) return if (!isCanSend) return

View File

@@ -191,7 +191,7 @@ const Chat: FC<ChatProps> = ({
.then(() => { .then(() => {
const message = msg const message = msg
if (!message?.trim()) return if (!message?.trim()) return
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
// Validate required variables before sending // Validate required variables before sending
let isCanSend = true let isCanSend = true
const params: Record<string, any> = {} const params: Record<string, any> = {}
@@ -352,7 +352,7 @@ const Chat: FC<ChatProps> = ({
.then(() => { .then(() => {
const message = msg const message = msg
if (!message || message.trim() === '') return if (!message || message.trim() === '') return
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
addUserMessage(message, files) addUserMessage(message, files)
setMessage(undefined) setMessage(undefined)
toolbarRef.current?.setFiles([]) toolbarRef.current?.setFiles([])

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:09:42 * @Date: 2026-02-06 21:09:42
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 20:32:54 * @Last Modified time: 2026-03-19 18:38:41
*/ */
/** /**
* File Upload Component * File Upload Component
@@ -23,7 +23,7 @@
import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react'; import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react';
import { Upload, Progress, App } from 'antd'; import { Upload, Progress, App } from 'antd';
import type { UploadProps, UploadFile } from 'antd'; import type { UploadProps, UploadFile } from 'antd';
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface'; import type { UploadProps as RcUploadProps, RcFile, UploadFileStatus } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { request } from '@/utils/request' import { request } from '@/utils/request'
@@ -221,17 +221,29 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
*/ */
const handleCustomRequest: RcUploadProps['customRequest'] = async (options) => { const handleCustomRequest: RcUploadProps['customRequest'] = async (options) => {
const { file, onSuccess, onError } = options; const { file, onSuccess, onError } = options;
if (typeof file === 'string') return;
try { const rcFile = file as RcFile;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', rcFile);
const fileVo: UploadFile = {
const response = await request.uploadFile(action, formData, requestConfig); uid: rcFile.uid,
name: rcFile.name,
onSuccess?.({data: response}); status: 'uploading' as UploadFileStatus,
} catch (error) { percent: 0,
onError?.(error as Error); type: rcFile.type,
originFileObj: rcFile,
thumbUrl: URL.createObjectURL(rcFile)
} }
onChange?.(fileVo)
request.uploadFile(action, formData, requestConfig)
.then(res => {
onSuccess?.({ data: res });
})
.catch((error) => {
onError?.(error as Error);
fileVo.status = 'error'
onChange?.(fileVo)
})
}; };
/** /**

View File

@@ -200,7 +200,7 @@ const Conversation: FC = () => {
/** Send message and handle streaming response */ /** Send message and handle streaming response */
const handleSend = () => { const handleSend = () => {
if (!token || !shareToken) return if (!token || !shareToken) return
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
const variables = toolbarRef.current?.getVariables() || [] const variables = toolbarRef.current?.getVariables() || []
let isCanSend = true let isCanSend = true
const params: Record<string, any> = {} const params: Record<string, any> = {}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56 * @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 20:46:35 * @Last Modified time: 2026-03-19 18:41:07
*/ */
/** /**
* Workflow Chat Component * Workflow Chat Component
@@ -151,7 +151,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
setLoading(true) setLoading(true)
const message = msg const message = msg
const files = toolbarRef.current?.getFiles() || [] const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
setChatList(prev => [...prev, { setChatList(prev => [...prev, {
role: 'user', role: 'user',
content: message, content: message,