diff --git a/web/src/assets/images/workflow/list-operator.svg b/web/src/assets/images/workflow/list-operator.svg new file mode 100644 index 00000000..8091c04b --- /dev/null +++ b/web/src/assets/images/workflow/list-operator.svg @@ -0,0 +1,19 @@ + + + 编组 13 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index 21f7fd36..c85f3c9f 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -241,7 +241,7 @@ const Menu: FC<{ const findMatchingKey = (menuList: MenuItem[], parentPaths: string[] = []): { key: string | null; } => { for (const menu of menuList) { if (menu.path) { - const menuPath = menu.path[0] !== '/' ? '/' + menu.path : menu.path; + const menuPath = menu.path?.[0] !== '/' ? '/' + menu.path : menu.path; /** Exact match or path prefix match (ensure complete path segment match) */ const isExactMatch = menuPath === currentPath; diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index ac917bae..caaaad89 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2179,6 +2179,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re unknown: 'Unknown Node', notes: 'Sticky Note', 'document-extractor': 'Document Extractor', + 'list-operator': 'List Operator', clickToConfigure: 'Click to configure node parameters', nodeProperties: 'Node Properties', @@ -2252,6 +2253,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'string': 'String', 'number': 'Number', 'boolean': 'Boolean', + 'file': 'File', + 'array[file]': 'Array[File]', 'array[string]': 'Array[String]', 'array[number]': 'Array[Number]', 'array[boolean]': 'Array[Boolean]', @@ -2380,6 +2383,20 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'document-extractor': { file_selector: 'Input Variable', }, + 'list-operator': { + variable: 'Input Variable', + filter_by: 'Filter Conditions', + addCondition: 'Add Filter Condition', + order_by: 'Sort', + asc: 'asc', + desc: 'desc', + extract_by: 'Extract Nth Item', + limit: 'Take First N Items', + type: { + eq: 'In', + ne: 'Not In', + } + }, name: 'Key', type: 'Type', value: 'Value', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index f862c64b..50f35813 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2175,6 +2175,7 @@ export const zh = { unknown: '未知节点', notes: '便签', 'document-extractor': '文档提取器', + 'list-operator': '列表操作', clickToConfigure: '点击配置节点参数', nodeProperties: '节点属性', @@ -2248,6 +2249,8 @@ export const zh = { 'string': 'String', 'number': 'Number', 'boolean': 'Boolean', + 'file': 'File', + 'array[file]': 'Array[File]', 'array[string]': 'Array[String]', 'array[number]': 'Array[Number]', 'array[boolean]': 'Array[Boolean]', @@ -2379,6 +2382,20 @@ export const zh = { 'document-extractor': { file_selector: '输入变量', }, + 'list-operator': { + variable: '输入变量', + filter_by: '过滤条件', + addCondition: '添加过滤条件', + order_by: '排序', + asc: 'asc', + desc: 'desc', + extract_by: '取第 N 项', + limit: '取前 N 项', + type: { + eq: '在', + ne: '不在', + } + }, name: '键', type: '类型', value: '值', diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx index d30e33a5..5c17aa53 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal.tsx @@ -26,7 +26,7 @@ interface FileUploadSettingModalProps { capability?: Capability[]; source?: Application['type'] } -const documentType = { +export const documentType = { type: 'document', icon:
, formats: [ @@ -41,7 +41,7 @@ const documentType = { "md", ], } -const imageType = { +export const imageType = { type: 'image', icon:
, formats: [ @@ -50,7 +50,7 @@ const imageType = { "jpeg" ], } -const audioType = { +export const audioType = { type: 'audio', icon:
, formats: [ @@ -59,7 +59,7 @@ const audioType = { "m4a", ], } -const videoType = { +export const videoType = { type: 'video', icon:
, formats: [ @@ -68,7 +68,7 @@ const videoType = { ], } -const defaultValues: FileUpload = { +export const defaultValues: FileUpload = { enabled: false, image_enabled: false, image_max_size_mb: 20, diff --git a/web/src/views/ApplicationManagement/MySharing.tsx b/web/src/views/ApplicationManagement/MySharing.tsx index 434bf465..e24cce0b 100644 --- a/web/src/views/ApplicationManagement/MySharing.tsx +++ b/web/src/views/ApplicationManagement/MySharing.tsx @@ -92,7 +92,7 @@ const MySharing: React.FC = () => { label: ( {workspace.target_workspace_icon - ? + ? {workspace.target_workspace_icon} :
{workspace.target_workspace_name[0]}
diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index 3c494437..67fff913 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-27 18:23:04 + * @Last Modified time: 2026-04-02 18:29:48 */ /** * File Upload Component @@ -21,7 +21,7 @@ * @component */ import { useState, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react'; -import { Upload, Progress, App, Flex } from 'antd'; +import { Upload, Progress, App, Flex, Button } from 'antd'; import type { UploadProps, UploadFile } from 'antd'; import type { UploadProps as RcUploadProps, RcFile, UploadFileStatus } from 'antd/es/upload/interface'; import { useTranslation } from 'react-i18next'; @@ -56,7 +56,9 @@ interface UploadFilesProps extends Omit { /** Custom file removal callback */ onRemove?: (file: UploadFile) => boolean | void | Promise; - featureConfig: FeaturesConfigForm['file_upload'] + featureConfig: FeaturesConfigForm['file_upload']; + textType?: 'button' | 'text'; + block?: boolean; } export const transform_file_type: Record = { @@ -149,6 +151,8 @@ const UploadFiles = forwardRef(({ onRemove: customOnRemove, requestConfig, featureConfig, + textType = 'text', + block, ...props }, ref) => { const { t } = useTranslation(); @@ -159,11 +163,11 @@ const UploadFiles = forwardRef(({ const fileType = useMemo(() => { let types: string[] = []; ['image', 'document', 'video', 'audio'].forEach(type => { - if (featureConfig[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]) { - types = types.concat(featureConfig[`${type}_allowed_extensions` as keyof FeaturesConfigForm['file_upload']] as string[]) + if (featureConfig?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]) { + const exts = featureConfig[`${type}_allowed_extensions` as keyof FeaturesConfigForm['file_upload']] as string[]; + if (Array.isArray(exts)) types = types.concat(exts.filter(Boolean)); } }) - return types }, [featureConfig]) @@ -205,6 +209,7 @@ const UploadFiles = forwardRef(({ return Upload.LIST_IGNORE; } } + console.log('onChange', isAutoUpload) if (!isAutoUpload) { const newFileList = [...fileList, file as UploadFile]; @@ -238,11 +243,11 @@ const UploadFiles = forwardRef(({ request.uploadFile(action, formData, requestConfig) .then(res => { onSuccess?.({ data: res }); + onChange?.({ ...fileVo, status: 'done', response: { data: res } }) }) .catch((error) => { onError?.(error as Error); - fileVo.status = 'error' - onChange?.(fileVo) + onChange?.({ ...fileVo, status: 'error' }) }) }; @@ -327,7 +332,10 @@ const UploadFiles = forwardRef(({ -
{t('memoryConversation.uploadFile')}
+ {textType === 'text' + ?
{t('memoryConversation.uploadFile')}
+ : + }
); }); diff --git a/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx index 15933d4a..18495295 100644 --- a/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx +++ b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx @@ -2,107 +2,203 @@ * @Author: ZhaoYing * @Date: 2025-12-30 13:59:36 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-28 16:19:26 + * @Last Modified time: 2026-04-02 19:01:12 */ -/** - * ChatVariableModal Component - * - * This component provides a modal for adding or editing chat variables in workflows. - * It supports various variable types and provides appropriate input fields based on the selected type. - */ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Form, Input, Select, InputNumber } from 'antd'; +import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react'; +import { Form, Input, Select, InputNumber, Button, Row, Col, Flex, Spin } from 'antd'; +import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import type { ChatVariableModalRef } from './types' import type { ChatVariable } from '../../types'; import RbModal from '@/components/RbModal' +import { defaultValues as defaultFileUploadValues } from '@/views/ApplicationConfig/components/FeaturesConfig/FileUploadSettingModal' +import UploadFiles from '@/views/Conversation/components/FileUpload' +import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' +import type { UploadFileListModalRef } from '@/views/Conversation/types' +import { getFileInfoByUrl } from '@/api/fileStorage' +import { transform_file_type } from '@/views/Conversation/components/FileUpload' const FormItem = Form.Item; -/** - * Props for ChatVariableModal component - */ interface ChatVariableModalProps { - /** - * Callback function to refresh variable list - * @param {ChatVariable} value - The variable data - * @param {number} [editIndex] - Optional index for editing existing variable - */ refresh: (value: ChatVariable, editIndex?: number) => void; } -/** - * Supported variable types - */ const types = [ - 'string', // String type - 'number', // Number type - 'boolean', // Boolean type - 'object', // Object type - 'array[string]', // Array of strings - 'array[number]', // Array of numbers - 'array[boolean]', // Array of booleans - 'array[object]', // Array of objects + 'string', + 'number', + 'boolean', + 'object', + 'file', + 'array[file]', + 'array[string]', + 'array[number]', + 'array[boolean]', + 'array[object]', ] -/** - * ChatVariableModal component - */ const ChatVariableModal = forwardRef(({ refresh }, ref) => { const { t } = useTranslation(); + const uploadFileListModalRef = useRef(null); - // State management - const [visible, setVisible] = useState(false); // Modal visibility - const [form] = Form.useForm(); // Form instance - const [loading, setLoading] = useState(false); // Loading state - const [editIndex, setEditIndex] = useState(undefined); // Index of variable being edited - const type = Form.useWatch('type', form); // Current selected type + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + const [editIndex, setEditIndex] = useState(undefined); + + const type = Form.useWatch('type', form); + const allowed_transfer_methods = Form.useWatch('allowed_transfer_methods', form); + const image_enabled = Form.useWatch('image_enabled', form); + const audio_enabled = Form.useWatch('audio_enabled', form); + const document_enabled = Form.useWatch('document_enabled', form); + const video_enabled = Form.useWatch('video_enabled', form); + const image_max_size_mb = Form.useWatch('image_max_size_mb', form); + const audio_max_size_mb = Form.useWatch('audio_max_size_mb', form); + const document_max_size_mb = Form.useWatch('document_max_size_mb', form); + const video_max_size_mb = Form.useWatch('video_max_size_mb', form); + const image_allowed_extensions = Form.useWatch('image_allowed_extensions', form); + const audio_allowed_extensions = Form.useWatch('audio_allowed_extensions', form); + const document_allowed_extensions = Form.useWatch('document_allowed_extensions', form); + const video_allowed_extensions = Form.useWatch('video_allowed_extensions', form); + const max_file_count = Form.useWatch('max_file_count', form); + + const hasEnabledFileType = !!(image_enabled || audio_enabled || document_enabled || video_enabled); + + const featureConfig = useMemo(() => ({ + enabled: hasEnabledFileType, + allowed_transfer_methods, + max_file_count, + image_enabled, image_max_size_mb, image_allowed_extensions, + audio_enabled, audio_max_size_mb, audio_allowed_extensions, + document_enabled, document_max_size_mb, document_allowed_extensions, + video_enabled, video_max_size_mb, video_allowed_extensions, + }), [ + hasEnabledFileType, allowed_transfer_methods, max_file_count, + image_enabled, image_max_size_mb, image_allowed_extensions, + audio_enabled, audio_max_size_mb, audio_allowed_extensions, + document_enabled, document_max_size_mb, document_allowed_extensions, + video_enabled, video_max_size_mb, video_allowed_extensions, + ]); - /** - * Handle modal close - */ const handleClose = () => { + setFileList([]); setVisible(false); form.resetFields(); setLoading(false); setEditIndex(undefined); }; - /** - * Handle modal open - */ const handleOpen = (variable?: ChatVariable, index?: number) => { setVisible(true); if (variable) { - // Exclude 'default' property and set form values const { default: _, ...rest } = variable; form.setFieldsValue({ ...rest }); setEditIndex(index); + if (variable.type === 'file' || variable.type === 'array[file]') { + const defaultVal = variable.defaultValue; + if (defaultVal) { + const list = Array.isArray(defaultVal) ? defaultVal : [defaultVal]; + setFileList(list); + } + } } else { - // Reset form for new variable form.resetFields(); setEditIndex(undefined); } }; - /** - * Handle save/submit action - */ const handleSave = () => { form.validateFields().then((values) => { - // Create variable with 'default' property mapped from 'defaultValue' refresh({ ...values, default: values.defaultValue }, editIndex); handleClose(); }); }; - // Expose handleOpen method to parent component via ref - useImperativeHandle(ref, () => ({ - handleOpen - })); + useImperativeHandle(ref, () => ({ handleOpen })); + + const setFormFileValue = (updated: any[]) => { + const isSingle = form.getFieldValue('type') === 'file'; + form.setFieldValue('defaultValue', isSingle ? (updated[0] ?? null) : updated); + }; + + const fileChange = (file?: any) => { + const fileObj = file ? { + ...file, + type: file.type, + transfer_method: "local_file", + upload_file_id: file.response?.data?.file_id, + } : undefined + if (form.getFieldValue('type') === 'file') { + const updated = [fileObj]; + setFileList(updated); + setTimeout(() => setFormFileValue(updated), 0); + return; + } + setFileList(prev => { + const index = prev.findIndex((item: any) => item.uid === fileObj.uid); + const updated = index > -1 + ? prev.map((item, i) => i === index ? fileObj : item) + : [...prev, fileObj]; + setTimeout(() => setFormFileValue(updated), 0); + return updated; + }); + }; + + const addFileList = (list?: any[]) => { + if (!list?.length) return; + const uploadingList = list.map(f => ({ ...f, status: 'uploading' })); + setFileList(prev => { + const isSingle = form.getFieldValue('type') === 'file'; + const updated = isSingle ? [uploadingList[0]] : [...prev, ...uploadingList]; + setTimeout(() => setFormFileValue(updated), 0); + return updated; + }); + const isSingle = form.getFieldValue('type') === 'file'; + (isSingle ? [uploadingList[0]] : 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 }; + setFileList(prev => { + const updated = prev.map(f => + f.uid === file.uid + ? { ...f, status: 'done', name: file_name, size: file_size, type: transform_file_type[content_type] || content_type } + : f + ); + setFormFileValue(updated); + return updated; + }); + }) + .catch(() => { + setFileList(prev => { + const updated = prev.map(f => f.uid === file.uid ? { ...f, status: 'error' } : f); + setFormFileValue(updated); + return updated; + }); + }); + }); + }; + + + const previewFileList = useMemo(() => { + return fileList.map(file => ({ + ...file, + url: file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) + })); + }, [fileList]); + + const handleDelete = (file: any) => { + const updated = fileList.filter(item => + item.thumbUrl && file.thumbUrl ? item.thumbUrl !== file.thumbUrl + : item.url && file.url ? item.url !== file.url + : item.uid !== file.uid + ); + setFileList(updated); + setFormFileValue(updated); + }; return ( - {/* Variable name field */} - - {/* Variable type field */} + - : - } - - - {/* Variable description field */} - +