+
{chatList.map((chat, index) => (
-
1,
})}>
{chat.label &&
@@ -370,8 +425,8 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc
= ({ chatList, data, updateChatList, handleSave, sourc
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label}
errorDesc={t('application.ReplyException')}
/>
-
))}
-
-
-
-
-
-

+
+
+
+
+
+ )
+ },
+ ],
+ onClick: handleShowUpload
+ }}
+ >
+
+
+
+ {/*
+
+
+ */}
+
+
>
}
+
+
)
}
diff --git a/web/src/views/ApplicationManagement/components/UploadWorkflowModal.tsx b/web/src/views/ApplicationManagement/components/UploadWorkflowModal.tsx
new file mode 100644
index 00000000..2f2f56b2
--- /dev/null
+++ b/web/src/views/ApplicationManagement/components/UploadWorkflowModal.tsx
@@ -0,0 +1,267 @@
+import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
+import { Form, Select, Steps, Flex, Alert, Row, Col, Statistic, Input, Button } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import type { UploadWorkflowModalData, UploadWorkflowModalRef } from '../types'
+import RbModal from '@/components/RbModal'
+import UploadFiles from '@/components/Upload/UploadFiles'
+import { fileUploadUrl } from '@/api/fileStorage'
+import RbCard from '@/components/RbCard/Card'
+
+interface UploadWorkflowModalProps {
+ refresh: () => void;
+}
+const steps = [
+ 'upload',
+ 'complex',
+ 'node',
+ 'configCheck',
+ 'sureInfo',
+ 'completed'
+]
+const UploadWorkflowModal = forwardRef
(({
+ refresh
+}, ref) => {
+ const { t } = useTranslation();
+ const [visible, setVisible] = useState(false);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false)
+ const [current, setCurrent] = useState(5);
+
+ // 封装取消方法,添加关闭弹窗逻辑
+ const handleClose = () => {
+ setVisible(false);
+ form.resetFields();
+ setLoading(false)
+ };
+
+ const handleOpen = () => {
+ form.resetFields();
+ setVisible(true);
+ };
+ // 封装保存方法,添加提交逻辑
+ const handleSave = () => {
+ switch(current) {
+ case 0:
+ setCurrent(1)
+ break;
+ case 1:
+ setCurrent(2)
+ break;
+ case 2:
+ setCurrent(3)
+ break;
+ case 3:
+ setCurrent(4)
+ break;
+ case 4:
+ setCurrent(5)
+ break;
+ case 5:
+ break;
+ default:
+ setCurrent(prev => prev + 1)
+ break;
+ }
+ // form
+ // .validateFields()
+ // .then(() => {
+ // })
+ // .catch((err) => {
+ // console.log('err', err)
+ // });
+ }
+
+ // 暴露给父组件的方法
+ useImperativeHandle(ref, () => ({
+ handleOpen,
+ handleClose
+ }));
+
+ const handleLastStep = () => {
+ setCurrent(prev => prev - 1)
+ }
+ const handleJump = (type: string) => {
+ switch(type) {
+ case 'detail':
+ break;
+ default:
+ break;
+ }
+ }
+
+ const getFooter = useMemo(() => {
+ switch(current) {
+ case 0:
+ return [
+ ,
+
+ ]
+ case 5:
+ return [
+ ,
+
+ ]
+ default:
+ return [
+ ,
+ ,
+
+ ]
+ }
+ }, [current])
+
+ return (
+
+
+ ({ title: t(`application.${key}`) }))}
+ />
+
+ {current === 0 &&
+
+
+
+
+ {
+ console.log('文件列表变化:', fileList);
+ }}
+ />
+
+
+ }
+
+ {current === 1 &&
+
+ {['fileType', 'parse', 'nodes', 'variable'].map(key => (
+
+ ))}
+
+
+ {['complex', 'nodes', 'task'].map(key => (
+
+
+
+ ))}
+
+
+ }
+
+ {/* 节点映射 */}
+ {current === 2 &&
+
+
+
+ Left Node
+ →
+
+
+
+
+
+
+ }
+ {current === 3 &&
+
+
+
+ }
+ {current === 4 &&
+
+
+
+
+ source
+
+
+ fileName
+
+
+ fileSize
+
+
+
+
+
+ {t('application.importStatistic')}
+
+ {['complex', 'nodes', 'task'].map(key => (
+
+
+
+ ))}
+
+
+ }
+ {current === 5 &&
+
+ 导入成功
+ 您的工作流已成功导入,可以在应用管理中查看和管理
+
+ }
+
+ );
+});
+
+export default UploadWorkflowModal;
\ No newline at end of file
diff --git a/web/src/views/ApplicationManagement/index.tsx b/web/src/views/ApplicationManagement/index.tsx
index d5335679..264b72d3 100644
--- a/web/src/views/ApplicationManagement/index.tsx
+++ b/web/src/views/ApplicationManagement/index.tsx
@@ -12,18 +12,19 @@
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { Button, Row, Col, App, Select } from 'antd';
+import { Button, Row, Col, App, Select, Space } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom'
-import type { Application, ApplicationModalRef, Query } from './types';
import ApplicationModal, { types } from './components/ApplicationModal';
+import type { Application, ApplicationModalRef, Query, UploadWorkflowModalRef } from './types';
import SearchInput from '@/components/SearchInput'
import RbCard from '@/components/RbCard/Card'
import { getApplicationListUrl, deleteApplication } from '@/api/application'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format';
+import UploadWorkflowModal from './components/UploadWorkflowModal'
/**
* Application management main component
@@ -35,6 +36,7 @@ const ApplicationManagement: React.FC = () => {
const [query, setQuery] = useState({} as Query);
const applicationModalRef = useRef(null);
const scrollListRef = useRef(null)
+ const uploadWorkflowModalRef = useRef(null);
useEffect(() => {
// Convert URLSearchParams to a plain object for easier access
@@ -80,6 +82,10 @@ const ApplicationManagement: React.FC = () => {
const handleChangeType = (value?: string) => {
setQuery(prev => ({...prev, type: value}))
}
+
+ const handleImport = () => {
+ uploadWorkflowModalRef.current?.handleOpen()
+ }
return (
<>
@@ -104,9 +110,14 @@ const ApplicationManagement: React.FC = () => {
/>
-
+
+
+
+
@@ -156,8 +167,13 @@ const ApplicationManagement: React.FC = () => {
ref={applicationModalRef}
refresh={refresh}
/>
+
+
>
);
};
-export default ApplicationManagement;
\ No newline at end of file
+export default ApplicationManagement
\ No newline at end of file
diff --git a/web/src/views/ApplicationManagement/types.ts b/web/src/views/ApplicationManagement/types.ts
index 5589206a..ccc4f114 100644
--- a/web/src/views/ApplicationManagement/types.ts
+++ b/web/src/views/ApplicationManagement/types.ts
@@ -173,3 +173,10 @@ export interface ApiExtensionModalRef {
/** Open API extension modal */
handleOpen: () => void;
}
+
+
+export interface UploadWorkflowModalData {
+}
+export interface UploadWorkflowModalRef {
+ handleOpen: () => void;
+}
\ No newline at end of file
diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx
new file mode 100644
index 00000000..f7620f3b
--- /dev/null
+++ b/web/src/views/Conversation/components/FileUpload.tsx
@@ -0,0 +1,251 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-06 21:09:42
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-06 21:09:42
+ */
+/**
+ * File Upload Component
+ *
+ * A reusable file upload component based on Ant Design Upload.
+ * Supports single/multiple file uploads, drag-and-drop, file validation, and preview.
+ *
+ * Features:
+ * - File type validation (images, documents, etc.)
+ * - File size validation
+ * - Auto-upload or manual upload modes
+ * - Progress tracking
+ * - Custom upload actions and headers
+ * - File list management
+ *
+ * @component
+ */
+import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
+import { Upload, Progress, App } from 'antd';
+import type { UploadProps, UploadFile } from 'antd';
+import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
+import { useTranslation } from 'react-i18next';
+import { cookieUtils } from '@/utils/request'
+import { fileUploadUrl } from '@/api/fileStorage'
+
+interface UploadFilesProps extends Omit {
+ /** Upload API endpoint */
+ action?: string;
+ /** Enable multiple file selection */
+ multiple?: boolean;
+ /** List of uploaded files */
+ fileList?: UploadFile[];
+ /** Callback when file list changes */
+ onChange?: (fileList: UploadFile | UploadFile[]) => void;
+ customRequest?: RcUploadProps['customRequest'];
+ /** Custom upload request configuration */
+ requestConfig?: {
+ data?: Record;
+ headers?: Record;
+ };
+ /** Disable upload */
+ disabled?: boolean;
+ /** File size limit in MB */
+ fileSize?: number;
+ /** Allowed file types ['doc', 'xls', 'ppt', 'pdf'] */
+ fileType?: string[];
+ /** Auto-upload on file selection, default is true */
+ isAutoUpload?: boolean;
+ /** Maximum number of files allowed */
+ maxCount?: number;
+ /** Custom file removal callback */
+ onRemove?: (file: UploadFile) => boolean | void | Promise;
+ /** Trigger to reset file list */
+ update?: boolean;
+}
+// Mapping of file extensions to MIME types
+const ALL_FILE_TYPE: {
+ [key: string]: string;
+} = {
+ // txt: 'text/plain',
+ pdf: 'application/pdf',
+
+ doc: 'application/msword',
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+
+ xls: 'application/vnd.ms-excel',
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ csv: 'text/csv',
+
+ ppt: 'application/vnd.ms-powerpoint',
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+
+ // md: 'text/markdown',
+ // htm: 'text/html',
+ // html: 'text/html',
+ // json: 'application/json',
+ jpg: 'image/jpeg',
+ jpeg: 'image/jpeg',
+ png: 'image/png',
+ gif: 'image/gif',
+ bmp: 'image/bmp',
+ webp: 'image/webp',
+ svg: 'image/svg+xml',
+}
+export interface UploadFilesRef {
+ /** Current file list */
+ fileList: UploadFile[];
+ /** Clear all uploaded files */
+ clearFiles: () => void;
+}
+
+/**
+ * Common upload component based on Ant Design Upload
+ * Supports single/multiple file uploads, drag-and-drop, file validation, and preview
+ */
+const UploadFiles = forwardRef(({
+ action = fileUploadUrl,
+ multiple = false,
+ fileList: propFileList = [],
+ onChange,
+ disabled = false,
+ fileSize = 5,
+ fileType = Object.entries(ALL_FILE_TYPE).map(([key]) => key),
+ isAutoUpload = true,
+ maxCount = 1,
+ onRemove: customOnRemove,
+ update,
+ ...props
+}, ref) => {
+ const { t } = useTranslation();
+ const { message } = App.useApp()
+ const [fileList, setFileList] = useState(propFileList);
+ const [accept, setAccept] = useState();
+
+ // Reset file list when update prop changes
+ useEffect(() => {
+ setFileList([])
+ }, [update])
+
+ /**
+ * Validates file type and size before upload
+ * @returns Upload.LIST_IGNORE to prevent upload, or true to proceed
+ */
+ const beforeUpload: RcUploadProps['beforeUpload'] = (file) => {
+ // Validate file size
+ if (fileSize) {
+ const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
+ if (!isLtMaxSize) {
+ message.error(t('common.fileSizeTip', { size: fileSize }));
+ return Upload.LIST_IGNORE;
+ }
+ }
+ // Validate file type
+ if (fileType && fileType.length > 0) {
+ // 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;
+ }
+ }
+
+ if (!isAutoUpload) {
+ const newFileList = [...fileList, file as UploadFile];
+ setFileList(newFileList);
+ onChange?.(newFileList);
+ return Upload.LIST_IGNORE; // Prevent auto-upload
+ }
+
+ return isAutoUpload;
+ };
+
+ /**
+ * Handles upload state changes
+ */
+ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList, event }) => {
+ console.log('event', event)
+ setFileList(newFileList);
+ if (onChange) {
+ onChange(maxCount === 1 ? newFileList[0] : newFileList);
+ }
+ };
+
+ /**
+ * Clears all uploaded files
+ */
+ const clearFiles = () => {
+ setFileList([]);
+ if (onChange) {
+ onChange([]);
+ }
+ }
+
+ // Build accept string from file types (includes both MIME types and extensions)
+ useEffect(() => {
+ if (fileType && fileType.length > 0) {
+ // Include both MIME types and file extensions
+ const acceptArray: string[] = [];
+ fileType.forEach((type: string) => {
+ const lowerType = type.toLowerCase();
+ // Add MIME type (if exists)
+ const mimeType = ALL_FILE_TYPE[lowerType];
+ if (mimeType) {
+ acceptArray.push(mimeType);
+ }
+ // Add file extension (.md, .html, etc.)
+ acceptArray.push(`.${lowerType}`);
+ });
+ setAccept(acceptArray.join(','));
+ } else {
+ setAccept(undefined);
+ }
+ }, [fileType])
+
+ // Generate upload component configuration
+ const uploadProps: UploadProps = {
+ action,
+ multiple: multiple && maxCount > 1,
+ fileList,
+ beforeUpload,
+ headers: {
+ authorization: `Bearer ${cookieUtils.get('authToken')}`,
+ },
+ onChange: handleChange,
+ accept,
+ disabled,
+ showUploadList: false,
+ itemRender: (_, file, __, actions) => {
+ return (
+
+
+ {file.name}
+ actions?.remove()}>Cancel
+
+
+
+ );
+ },
+ className: 'rb:-mb-1.5!',
+ ...props,
+ };
+
+ // Expose methods to parent component via ref
+ useImperativeHandle(ref, () => ({
+ fileList,
+ clearFiles
+ }));
+
+ return (
+
+ {t('memoryConversation.uploadFile')}
+
+ );
+});
+
+export default UploadFiles;
\ No newline at end of file
diff --git a/web/src/views/Conversation/components/UploadFileListModal.tsx b/web/src/views/Conversation/components/UploadFileListModal.tsx
new file mode 100644
index 00000000..a14b0e38
--- /dev/null
+++ b/web/src/views/Conversation/components/UploadFileListModal.tsx
@@ -0,0 +1,135 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-06 21:09:47
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-06 21:09:47
+ */
+/**
+ * Upload File List Modal Component
+ *
+ * A modal dialog for adding remote files via URL.
+ * Allows users to specify file type and URL for files hosted externally.
+ *
+ * Features:
+ * - Dynamic form fields for multiple file URLs
+ * - File type selection (currently supports images)
+ * - Form validation
+ * - Add/remove file entries
+ *
+ * @component
+ */
+import { forwardRef, useImperativeHandle, useState } from 'react';
+import { Form, Input, Select, Button, Space } from 'antd';
+import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';
+import { useTranslation } from 'react-i18next';
+
+import type { UploadFileListModalRef } from '../types'
+import RbModal from '@/components/RbModal'
+
+const FormItem = Form.Item;
+
+interface UploadFileListModalProps {
+ /** Callback to refresh parent component with new file list */
+ refresh: (fileList?: any[]) => void;
+}
+
+/**
+ * Modal for adding remote files via URL
+ */
+const UploadFileListModal = forwardRef(({
+ refresh
+}, ref) => {
+ const { t } = useTranslation();
+ const [visible, setVisible] = useState(false);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false)
+
+ /**
+ * Closes the modal and resets loading state
+ */
+ const handleClose = () => {
+ setVisible(false);
+ setLoading(false)
+ };
+
+ /**
+ * Opens the modal and resets form fields
+ */
+ const handleOpen = () => {
+ setVisible(true);
+ form.resetFields();
+ };
+ /**
+ * Validates and saves the file list
+ * Transforms form values into file objects with transfer_method: 'remote_url'
+ */
+ const handleSave = () => {
+ form.validateFields().then((values) => {
+ const fileList = values.files?.map((file: any) => ({
+ ...file,
+ uid: Math.random().toString(36).substr(2, 9),
+ transfer_method: 'remote_url'
+ })) || [];
+ refresh(fileList)
+ handleClose()
+ })
+ }
+
+ // Expose methods to parent component via ref
+ useImperativeHandle(ref, () => ({
+ handleOpen
+ }));
+
+ return (
+
+
+ {(fields, { add, remove }) => (
+ <>
+ {/* Render each file entry with type selector and URL input */}
+ {fields.map(({ key, name, ...restField }) => (
+
+
+
+
+
+
+
+ remove(name)} style={{ marginTop: 30 }} />
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+ );
+});
+
+export default UploadFileListModal;
\ No newline at end of file
diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx
index 30b2a18a..0d09bcc4 100644
--- a/web/src/views/Conversation/index.tsx
+++ b/web/src/views/Conversation/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:58:35
+ * @Last Modified time: 2026-02-06 21:11:23
*/
/**
* Conversation Page
@@ -14,12 +14,12 @@ import { type FC, useState, useEffect, useRef } from 'react'
import { useParams, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component';
-import { Flex, Skeleton, Form } from 'antd'
+import { Flex, Skeleton, Form, Dropdown, type MenuProps } from 'antd'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
-import type { HistoryItem, QueryParams } from './types'
+import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types'
import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format';
import { randomString } from '@/utils/common'
@@ -33,6 +33,10 @@ import OnlineIcon from '@/assets/images/conversation/online.svg'
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
import { type SSEMessage } from '@/utils/stream'
+import UploadFiles from './components/FileUpload'
+// import AudioRecorder from '@/components/AudioRecorder'
+import { shareFileUploadUrl } from '@/api/fileStorage'
+import UploadFileListModal from './components/UploadFileListModal'
/**
* Conversation component for shared applications
@@ -58,6 +62,8 @@ const Conversation: FC = () => {
const [form] = Form.useForm()
const queryValues = Form.useWatch([], form)
+
+ const uploadFileListModalRef = useRef(null)
useEffect(() => {
const shareToken = localStorage.getItem(`shareToken_${token}`)
setShareToken(shareToken)
@@ -142,12 +148,13 @@ const Conversation: FC = () => {
}, [conversation_id])
/** Add user message to chat */
- const addUserMessage = (message: string = '') => {
+ const addUserMessage = (message: string = '', files?: any[]) => {
const newUserMessage: ChatItem = {
conversation_id,
role: 'user',
content: message,
- created_at: Date.now()
+ created_at: Date.now(),
+ files
};
setChatList(prev => [...prev, newUserMessage])
}
@@ -189,9 +196,10 @@ const Conversation: FC = () => {
if (!token || !shareToken) {
return
}
+ const { files = [], ...rest } = queryValues || {}
setLoading(true)
setStreamLoading(true)
- addUserMessage(message)
+ addUserMessage(message, files)
addAssistantMessage()
let currentConversationId: string | null = null
@@ -222,18 +230,54 @@ const Conversation: FC = () => {
}
})
};
-
+
+ form.setFieldValue('files', [])
sendConversation({
- ...queryValues,
+ ...rest,
message: message || '',
stream: true,
conversation_id: conversation_id || null,
+ files: files.map(file => {
+ if (file.url) {
+ return file
+ } else {
+ return {
+ type: file.type,
+ transfer_method: 'local_file',
+ upload_file_id: file.response.data.file_id
+ }
+ }
+ })
}, handleStreamMessage, shareToken)
.finally(() => {
setLoading(false)
})
}
+ const [update, setUpdate] = useState(false)
+ const fileChange = (file?: any) => {
+ form.setFieldValue('files', [...(queryValues.files || []), file])
+ setUpdate(prev => !prev)
+ }
+ // const handleRecordingComplete = async (file: any) => {
+ // console.log('file', file)
+ // }
+
+ const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
+ switch(key) {
+ case 'define':
+ uploadFileListModalRef.current?.handleOpen()
+ break
+ }
+ }
+ const addFileList = (fileList?: any[]) => {
+ if (!fileList || fileList.length <= 0) return
+ form.setFieldValue('files', [...(queryValues.files || []), ...fileList])
+ }
+ const updateFileList = (fileList?: any[]) => {
+ form.setFieldValue('files', [...(fileList || [])])
+ }
+
return (
@@ -285,37 +329,75 @@ const Conversation: FC = () => {
}
- contentClassName="rb:h-[calc(100%-152px)] "
+ contentClassName="rb:h-[calc(100%-180px)]"
data={chatList}
streamLoading={streamLoading}
loading={loading}
onChange={setMessage}
onSend={handleSend}
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
+ fileList={queryValues?.files || []}
+ fileChange={updateFileList}
>
+
+
)
}
diff --git a/web/src/views/Conversation/types.ts b/web/src/views/Conversation/types.ts
index fd962ef5..deb14d1f 100644
--- a/web/src/views/Conversation/types.ts
+++ b/web/src/views/Conversation/types.ts
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 16:57:46
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-03 16:57:46
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-06 21:11:19
*/
/**
* Type definitions for Conversation
@@ -50,4 +50,9 @@ export interface QueryParams {
stream: boolean;
/** Current conversation ID */
conversation_id?: string | null;
+ files?: any[];
+}
+
+export interface UploadFileListModalRef {
+ handleOpen: (fileList?: any[]) => void;
}
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx
index 4a1ac5a7..9b648505 100644
--- a/web/src/views/Workflow/components/Chat/Chat.tsx
+++ b/web/src/views/Workflow/components/Chat/Chat.tsx
@@ -1,7 +1,30 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-02-06 21:10:56
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-02-06 21:10:56
+ */
+/**
+ * Workflow Chat Component
+ *
+ * A drawer-based chat interface for testing and debugging workflow executions.
+ * Provides real-time streaming of workflow node execution status, input/output data,
+ * and error messages. Supports variable configuration and file attachments.
+ *
+ * Key Features:
+ * - Real-time workflow execution monitoring with SSE streaming
+ * - Node-level execution tracking (start, end, error states)
+ * - Variable configuration for workflow inputs
+ * - File upload support (images and documents)
+ * - Collapsible node execution details with input/output inspection
+ * - Error handling and display
+ *
+ * @component
+ */
import { forwardRef, useImperativeHandle, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import clsx from 'clsx'
-import { Input, Form, App, Space, Button, Collapse } from 'antd'
+import { App, Space, Button, Collapse, Flex, Dropdown, type MenuProps } from 'antd'
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
import CodeBlock from '@/components/Markdown/CodeBlock'
@@ -12,30 +35,43 @@ import { draftRun } from '@/api/application';
import Empty from '@/components/Empty'
import ChatContent from '@/components/Chat/ChatContent'
import type { ChatItem } from '@/components/Chat/types'
-import ChatSendIcon from '@/assets/images/application/chatSend.svg'
import dayjs from 'dayjs'
import type { ChatRef, VariableConfigModalRef, GraphRef } from '../../types'
import { type SSEMessage } from '@/utils/stream'
import type { Variable } from '../Properties/VariableList/types'
import styles from './chat.module.css'
import Markdown from '@/components/Markdown'
+import ChatInput from '@/components/Chat/ChatInput'
+import UploadFiles from '@/views/Conversation/components/FileUpload'
+// import AudioRecorder from '@/components/AudioRecorder'
+import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal'
+import type { UploadFileListModalRef } from '@/views/Conversation/types'
const Chat = forwardRef(({ appId, graphRef }, ref) => {
const { t } = useTranslation()
const { message: messageApi } = App.useApp()
- const [form] = Form.useForm<{ message: string }>()
const variableConfigModalRef = useRef(null)
- const [open, setOpen] = useState(false)
- const [loading, setLoading] = useState(false)
- const [chatList, setChatList] = useState([])
- const [variables, setVariables] = useState([])
- const [streamLoading, setStreamLoading] = useState(false)
- const [conversationId, setConversationId] = useState(null)
+ // State management
+ const [open, setOpen] = useState(false) // Drawer visibility
+ const [loading, setLoading] = useState(false) // Send button loading state
+ const [chatList, setChatList] = useState([]) // Chat message history
+ const [variables, setVariables] = useState([]) // Workflow input variables
+ const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state
+ const [conversationId, setConversationId] = useState(null) // Current conversation ID
+ const [fileList, setFileList] = useState([]) // Uploaded files
+ const [message, setMessage] = useState(undefined) // Current input message
+ const uploadFileListModalRef = useRef(null)
+ /**
+ * Opens the chat drawer and loads workflow variables from the start node
+ */
const handleOpen = () => {
setOpen(true)
getVariables()
}
+ /**
+ * Extracts variables from the workflow's start node and merges with previous values
+ */
const getVariables = () => {
const nodes = graphRef.current?.getNodes()
const list = nodes?.map(node => node.getData()) || []
@@ -55,20 +91,42 @@ const Chat = forwardRef(({ appId
setVariables(curVariables)
}
}
+ /**
+ * Closes the drawer and resets all state
+ */
const handleClose = () => {
setOpen(false)
setChatList([])
setVariables([])
setConversationId(null)
}
+ /**
+ * Opens the variable configuration modal
+ */
const handleEditVariables = () => {
variableConfigModalRef.current?.handleOpen(variables)
}
+ /**
+ * Saves updated variable values from the modal
+ */
const handleSave = (values: Variable[]) => {
setVariables([...values])
}
- const handleSend = () => {
+ /**
+ * Sends a message to execute the workflow
+ *
+ * Process:
+ * 1. Validates required variables
+ * 2. Adds user message to chat
+ * 3. Initiates SSE stream for workflow execution
+ * 4. Handles real-time node execution updates
+ * 5. Updates chat with results or errors
+ *
+ * @param msg - Optional message to send (uses state if not provided)
+ */
+ const handleSend = async (msg?: string) => {
if (loading || !appId) return
+ // Validate required variables before sending
let isCanSend = true
const params: Record = {}
if (variables.length > 0) {
@@ -90,8 +148,8 @@ const Chat = forwardRef(({ appId
return
}
- setLoading(true)
- const message = form.getFieldValue('message')
+ // setLoading(true)
+ const message = msg
setChatList(prev => [...prev, {
role: 'user',
content: message,
@@ -104,6 +162,16 @@ const Chat = forwardRef(({ appId
subContent: [],
}])
+ /**
+ * Handles SSE stream messages from workflow execution
+ *
+ * Events:
+ * - message: Streaming text chunks for final output
+ * - node_start: Node execution begins
+ * - node_end: Node execution completes successfully
+ * - node_error: Node execution fails
+ * - workflow_end: Entire workflow completes
+ */
const handleStreamMessage = (data: SSEMessage[]) => {
data.forEach(item => {
const { chunk, conversation_id, node_id, input, output, error, elapsed_time, status } = item.data as {
@@ -125,6 +193,7 @@ const Chat = forwardRef(({ appId
console.log('node', node?.getData())
switch(item.event) {
+ // Append streaming text chunks to assistant message
case 'message':
setChatList(prev => {
const newList = [...prev]
@@ -138,6 +207,7 @@ const Chat = forwardRef(({ appId
return newList
})
break
+ // Track node execution start
case 'node_start':
setChatList(prev => {
const newList = [...prev]
@@ -170,6 +240,7 @@ const Chat = forwardRef(({ appId
return newList
})
break
+ // Update node with execution results or errors
case 'node_end':
case 'node_error':
setChatList(prev => {
@@ -198,6 +269,7 @@ const Chat = forwardRef(({ appId
return newList
})
break
+ // Mark workflow as complete
case 'workflow_end':
setChatList(prev => {
const newList = [...prev]
@@ -221,14 +293,27 @@ const Chat = forwardRef(({ appId
})
}
- form.setFieldValue('message', undefined)
- setStreamLoading(true)
- draftRun(appId, {
+ setMessage(undefined)
+ setFileList([])
+ const data = {
message: message,
variables: params,
stream: true,
- conversation_id: conversationId
- }, handleStreamMessage)
+ conversation_id: conversationId,
+ files: fileList.map(file => {
+ if (file.url) {
+ return file
+ } else {
+ return {
+ type: file.type,
+ transfer_method: 'local_file',
+ upload_file_id: file.response.data.file_id
+ }
+ }
+ })
+ }
+ setStreamLoading(true)
+ draftRun(appId, data, handleStreamMessage)
.catch((error) => {
setChatList(prev => {
const newList = [...prev]
@@ -243,29 +328,72 @@ const Chat = forwardRef(({ appId
}
return newList
})
- })
- .finally(() => {
+ }).finally(() => {
setLoading(false)
setStreamLoading(false)
})
}
- // 暴露给父组件的方法
+
+ /**
+ * Updates the current input message
+ */
+ const handleMessageChange = (message: string) => {
+ setMessage(message)
+ }
+ const [update, setUpdate] = useState(false)
+ /**
+ * Handles file upload from local device
+ */
+ const fileChange = (file?: any) => {
+ setFileList([...fileList, file])
+ setUpdate(prev => !prev)
+ }
+ // const handleRecordingComplete = async (file: any) => {
+ // console.log('file', file)
+ // }
+
+ /**
+ * Handles dropdown menu actions for file upload
+ */
+ const handleShowUpload: MenuProps['onClick'] = ({ key }) => {
+ switch(key) {
+ case 'define':
+ uploadFileListModalRef.current?.handleOpen()
+ break
+ }
+ }
+ /**
+ * Adds files from remote URL modal
+ */
+ const addFileList = (list?: any[]) => {
+ if (!list || list.length <= 0) return
+ setFileList([...fileList, ...(list || [])])
+ }
+ /**
+ * Updates the entire file list (used when removing files)
+ */
+ const updateFileList = (list?: any[]) => {
+ setFileList([...list || []])
+ }
+
+ // Expose methods to parent component via ref
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
+ /**
+ * Returns CSS class for status-based text color
+ */
const getStatus = (status?: string) => {
return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]'
}
-
- console.log('chatList', chatList)
return (
{t('workflow.run')}
{variables.length > 0 &&
-
+
}
}
classNames={{
@@ -275,7 +403,7 @@ const Chat = forwardRef