diff --git a/web/src/api/fileStorage.ts b/web/src/api/fileStorage.ts index ce133565..a992d460 100644 --- a/web/src/api/fileStorage.ts +++ b/web/src/api/fileStorage.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 13:59:56 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-09 16:24:05 + * @Last Modified time: 2026-03-23 17:48:40 */ import { request, API_PREFIX } from '@/utils/request' @@ -32,4 +32,9 @@ export const deleteFile = (fileId: string) => { } export const shareFileUploadUrlWithoutApiPrefix = `/storage/share/files` -export const shareFileUploadUrl = `${API_PREFIX}${shareFileUploadUrlWithoutApiPrefix}` \ No newline at end of file +export const shareFileUploadUrl = `${API_PREFIX}${shareFileUploadUrlWithoutApiPrefix}` + +// Get file info +export const getFileInfoByUrl = (url: string) => { + return request.get('/storage/files/info-by-url', {url}) +} \ No newline at end of file diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx index aab75426..e2fcb82f 100644 --- a/web/src/components/Chat/ChatInput.tsx +++ b/web/src/components/Chat/ChatInput.tsx @@ -2,10 +2,10 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:14 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-20 15:39:33 + * @Last Modified time: 2026-03-23 17:46:25 */ import { type FC, useEffect, useMemo, useState } from 'react' -import { Flex, Input, Form } from 'antd' +import { Flex, Input, Form, Spin } from 'antd' import clsx from 'clsx' import type { ChatInputProps } from './types' @@ -37,7 +37,7 @@ const ChatInput: FC = ({ }) } }, [form, message]) - + // Clear input when loading useEffect(() => { if (loading) { @@ -52,7 +52,7 @@ const ChatInput: FC = ({ fileChange?.(fileList?.filter(item => { return item.thumbUrl && file.thumbUrl ? item.thumbUrl !== file.thumbUrl : item.url && file.url ? item.url !== file.url - : item.uid !== file.uid + : item.uid !== file.uid }) || []) } // Convert file object to preview URL @@ -81,84 +81,98 @@ const ChatInput: FC = ({ ' rb:border-[#171719]!': isFocus })}> {previewFileList.length > 0 &&
- - {previewFileList.map((file) => { - if (file.type.includes('image')) { - return ( -
+ + {previewFileList.map((file) => { + if (file.type?.includes('image')) { + return ( + +
{file.name}
handleDelete(file)} >
- ) - } - if (file.type.includes('video')) { - return ( -
+ + ) + } + if (file.type?.includes('video')) { + return ( + +
- ) - } - if (file.type.includes('audio')) { - return ( -
+ + ) + } + if (file.type?.includes('audio')) { + return ( + +
- ) - } - return ( +
+ ) + } + return ( + + className={clsx("rb:w-45 rb:text-[12px] rb:group rb:relative rb:rounded-lg rb:bg-[#F6F6F6] rb:py-2! rb:px-2.5! rb:border rb:border-[#F6F6F6]", { + 'rb:border-[#FF5D34]': file.status === 'error' + })}>
{file.name}
-
{file.type.split('/')[file.type.split('/').length - 1]} · {file.size}
+
{file.type?.split('/')[file.type?.split('/').length - 1]} · {file.size}
handleDelete(file)} >
- ) - })} - +
+ ) + })} +
} {/* Message input form */}
diff --git a/web/src/components/Chat/ChatToolbar.tsx b/web/src/components/Chat/ChatToolbar.tsx index 64c3f03e..a55e088d 100644 --- a/web/src/components/Chat/ChatToolbar.tsx +++ b/web/src/components/Chat/ChatToolbar.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-17 14:22:25 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-20 15:44:48 + * @Last Modified time: 2026-03-23 17:42:38 */ // Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react' @@ -18,6 +18,7 @@ import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types' import type { UploadFileListModalRef } from '@/views/Conversation/types' import type { VariableConfigModalRef } from '@/views/Workflow/types' import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types' +import { getFileInfoByUrl } from '@/api/fileStorage'; // Exposed methods via ref for parent components to access/set form state export interface ChatToolbarRef { @@ -103,9 +104,33 @@ const ChatToolbar = forwardRef(({ // Merge a batch of files (e.g. from remote URL modal) into the file list const addFileList = (list?: any[]) => { if (!list?.length) return - const files = [...(queryValues?.files || []), ...list] + const uploadingList = list.map(f => ({ ...f, status: 'uploading' })) + const files = [...(queryValues?.files || []), ...uploadingList] form.setFieldValue('files', files) onFilesChange?.(files) + + uploadingList.forEach(file => { + getFileInfoByUrl(file.url) + .then((res) => { + const { file_name, file_size, content_type } = res as { file_name: string; file_size: number; content_type: string; } + const current: any[] = form.getFieldValue('files') || [] + const updated = current.map(f => f.uid === file.uid ? { + ...f, + status: 'done', + name: file_name, + size: file_size, + type: content_type, + } : f) + form.setFieldValue('files', updated) + onFilesChange?.(updated) + }) + .catch(() => { + const current: any[] = form.getFieldValue('files') || [] + const updated = current.map(f => f.uid === file.uid ? { ...f, status: 'error' } : f) + form.setFieldValue('files', updated) + onFilesChange?.(updated) + }) + }) } // Persist variable values from the config modal and notify parent diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index 166b00c8..88cdf83b 100644 --- a/web/src/views/Conversation/components/FileUpload.tsx +++ b/web/src/views/Conversation/components/FileUpload.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-19 21:32:34 + * @Last Modified time: 2026-03-23 17:19:58 */ /** * File Upload Component @@ -19,11 +19,11 @@ * - File list management * * @component -*/ + */ import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react'; import { Upload, Progress, App, Flex } 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 { request } from '@/utils/request' @@ -193,13 +193,13 @@ const UploadFiles = forwardRef(({ // Get file extension const fileName = file.name.toLowerCase(); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); - + // Check if extension is in allowed types list const isValidExtension = fileType.some(type => type.toLowerCase() === fileExtension); - + // Also check MIME type if available (as fallback validation) const isValidMimeType = file.type && accept ? accept.includes(file.type) : true; - + if (!isValidExtension && !isValidMimeType) { message.error(`${t('common.fileAcceptTip')} ${fileExtension || file.type}`); return Upload.LIST_IGNORE; @@ -221,17 +221,29 @@ const UploadFiles = forwardRef(({ */ const handleCustomRequest: RcUploadProps['customRequest'] = async (options) => { const { file, onSuccess, onError } = options; - - try { - const formData = new FormData(); - formData.append('file', file); - - const response = await request.uploadFile(action, formData, requestConfig); - - onSuccess?.({data: response}); - } catch (error) { - onError?.(error as Error); + if (typeof file === 'string') return; + const rcFile = file as RcFile; + const formData = new FormData(); + formData.append('file', rcFile); + const fileVo: UploadFile = { + uid: rcFile.uid, + name: rcFile.name, + status: 'uploading' as UploadFileStatus, + percent: 0, + 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) + }) }; /** diff --git a/web/src/views/Conversation/components/UploadFileListModal.tsx b/web/src/views/Conversation/components/UploadFileListModal.tsx index ce71066d..ac151fe8 100644 --- a/web/src/views/Conversation/components/UploadFileListModal.tsx +++ b/web/src/views/Conversation/components/UploadFileListModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:47 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-18 21:10:01 + * @Last Modified time: 2026-03-23 17:46:56 */ /** * Upload File List Modal Component @@ -18,8 +18,8 @@ * * @component */ -import { forwardRef, useImperativeHandle, useState, useMemo } from 'react'; -import { Form, Input, Select, Button, Flex } from 'antd'; +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input, Button, Flex } from 'antd'; import { useTranslation } from 'react-i18next'; import type { UploadFileListModalRef } from '../types' @@ -39,7 +39,6 @@ interface UploadFileListModalProps { */ const UploadFileListModal = forwardRef(({ refresh, - featureConfig }, ref) => { const { t } = useTranslation(); const [visible, setVisible] = useState(false); @@ -82,20 +81,6 @@ const UploadFileListModal = forwardRef { - const options = []; - if (featureConfig?.image_enabled) { - options.push({ label: t('memoryConversation.image'), value: 'image' }); - } - if (featureConfig?.audio_enabled) { - options.push({ label: t('memoryConversation.audio'), value: 'audio' }); - } - if (featureConfig?.video_enabled) { - options.push({ label: t('memoryConversation.video'), value: 'video' }); - } - return options; - }, [featureConfig, t]) - return ( ( - - = ({ { title: '', dataIndex: 'actions', + width: 20, render: (_: any, __: TableRow, index: number) => (