From 90c8ff35d1891f5b3b860da4db5fb75e538a297c Mon Sep 17 00:00:00 2001 From: zhaoying Date: Fri, 13 Mar 2026 17:27:52 +0800 Subject: [PATCH] feat(web): app share --- web/src/api/application.ts | 32 +- web/src/api/workspaces.ts | 8 +- web/src/assets/images/file/audio.svg | 11 + web/src/assets/images/file/csv.svg | 16 + web/src/assets/images/file/excel.svg | 15 + web/src/assets/images/file/html.svg | 15 + web/src/assets/images/file/image.svg | 15 + web/src/assets/images/file/json.svg | 12 + web/src/assets/images/file/md.svg | 17 + web/src/assets/images/file/pdf.svg | 18 + web/src/assets/images/file/ppt.svg | 12 + web/src/assets/images/file/txt.svg | 12 + web/src/assets/images/file/video.svg | 14 + web/src/assets/images/file/word.svg | 15 + web/src/components/AudioRecorder/index.tsx | 19 +- web/src/components/ButtonCheckbox/index.tsx | 21 +- web/src/components/Chat/index.tsx | 8 +- web/src/components/Chat/types.ts | 3 +- web/src/i18n/en.ts | 54 +- web/src/i18n/zh.ts | 55 +- web/src/routes/routes.json | 1 + web/src/views/ApplicationConfig/Agent.tsx | 33 +- web/src/views/ApplicationConfig/Cluster.tsx | 7 +- .../views/ApplicationConfig/ReleasePage.tsx | 14 +- .../ApplicationConfig/TestChat/index.tsx | 642 ++++++++++++++++++ .../views/ApplicationConfig/TestChat/type.ts | 8 + .../components/AppSharingModal.tsx | 183 +++++ .../ApplicationConfig/components/Chat.tsx | 24 +- .../components/ConfigHeader.tsx | 37 +- .../FunConfig/FileUploadSettingModal.tsx | 182 +++++ .../components/FunConfig/FunConfigModal.tsx | 140 ++++ .../components/FunConfig/index.tsx | 50 ++ web/src/views/ApplicationConfig/index.tsx | 38 +- web/src/views/ApplicationConfig/types.ts | 40 +- .../views/ApplicationManagement/MySharing.tsx | 157 +++++ web/src/views/ApplicationManagement/index.tsx | 235 ++++--- web/src/views/ApplicationManagement/types.ts | 24 +- .../Workflow/components/Chat/Runtime.tsx | 13 +- .../components/Chat/VariableConfigModal.tsx | 1 - web/src/views/Workflow/index.tsx | 3 +- web/src/views/Workflow/types.ts | 3 + 41 files changed, 2044 insertions(+), 163 deletions(-) create mode 100644 web/src/assets/images/file/audio.svg create mode 100644 web/src/assets/images/file/csv.svg create mode 100644 web/src/assets/images/file/excel.svg create mode 100644 web/src/assets/images/file/html.svg create mode 100644 web/src/assets/images/file/image.svg create mode 100644 web/src/assets/images/file/json.svg create mode 100644 web/src/assets/images/file/md.svg create mode 100644 web/src/assets/images/file/pdf.svg create mode 100644 web/src/assets/images/file/ppt.svg create mode 100644 web/src/assets/images/file/txt.svg create mode 100644 web/src/assets/images/file/video.svg create mode 100644 web/src/assets/images/file/word.svg create mode 100644 web/src/views/ApplicationConfig/TestChat/index.tsx create mode 100644 web/src/views/ApplicationConfig/TestChat/type.ts create mode 100644 web/src/views/ApplicationConfig/components/AppSharingModal.tsx create mode 100644 web/src/views/ApplicationConfig/components/FunConfig/FileUploadSettingModal.tsx create mode 100644 web/src/views/ApplicationConfig/components/FunConfig/FunConfigModal.tsx create mode 100644 web/src/views/ApplicationConfig/components/FunConfig/index.tsx create mode 100644 web/src/views/ApplicationManagement/MySharing.tsx diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 71048454..6035afe2 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -2,11 +2,11 @@ * @Author: ZhaoYing * @Date: 2026-02-03 13:59:45 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-03 12:08:42 + * @Last Modified time: 2026-03-13 17:07:54 */ import { request } from '@/utils/request' import type { ApplicationModalData } from '@/views/ApplicationManagement/types' -import type { Config } from '@/views/ApplicationConfig/types' +import type { Config, AppSharingForm } from '@/views/ApplicationConfig/types' import { handleSSE, type SSEMessage } from '@/utils/stream' import type { QueryParams } from '@/views/Conversation/types' import type { WorkflowConfig } from '@/views/Workflow/types' @@ -113,8 +113,8 @@ export const getShareToken = (share_token: string, user_id: string) => { return request.post(`/public/share/${share_token}/token`, { user_id }) } // Copy application -export const copyApplication = (app_id: string, new_name: string) => { - return request.post(`/apps/${app_id}/copy?new_name=${new_name}`) +export const copyApplication = (app_id: string, new_name?: string) => { + return request.post(`/apps/${app_id}/copy`, { new_name }) } // Data statistics export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => { @@ -143,4 +143,26 @@ export const appExport = (app_id: string, appName: string, data?: { release_vers // Import application export const appImport = (formData: FormData) => { return request.uploadFile(`/apps/import`, formData) -} \ No newline at end of file +} + +// Share application +export const appSharing = (app_id: string, data: AppSharingForm) => { + return request.post(`/apps/${app_id}/share`, data) +} +// Get my shared application records +export const mySharedOutList = () => { + return request.get(`/apps/my-shared-out`) +} +// Get sharing records for a specific application +export const getAppShares = (app_id: string) => { + return request.get(`/apps/${app_id}/shares`) +} +// Cancel a single share (source side operation) +export const cancelShare = (app_id: string, target_workspace_id?: string) => { + return request.delete(`/apps/${app_id}/share/${target_workspace_id}`) +} +// Cancel all shares under a workspace (source side operation) +export const cancelSpaceShare = (target_workspace_id?: string) => { + return request.delete(`/apps/share/${target_workspace_id}`) +} + diff --git a/web/src/api/workspaces.ts b/web/src/api/workspaces.ts index 01f3be72..5c62489d 100644 --- a/web/src/api/workspaces.ts +++ b/web/src/api/workspaces.ts @@ -1,16 +1,16 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 14:00:26 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 14:00:26 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 15:29:03 */ import { request } from '@/utils/request' import type { SpaceModalData } from '@/views/SpaceManagement/types' import type { SpaceConfigData } from '@/views/SpaceConfig/types' // Workspace list -export const getWorkspaces = () => { - return request.get('/workspaces') +export const getWorkspaces = (data?: { include_current?: boolean }) => { + return request.get('/workspaces', data) } // Create workspace export const createWorkspace = (values: SpaceModalData) => { diff --git a/web/src/assets/images/file/audio.svg b/web/src/assets/images/file/audio.svg new file mode 100644 index 00000000..0826c7f8 --- /dev/null +++ b/web/src/assets/images/file/audio.svg @@ -0,0 +1,11 @@ + + + 音乐 + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/csv.svg b/web/src/assets/images/file/csv.svg new file mode 100644 index 00000000..1b8fc721 --- /dev/null +++ b/web/src/assets/images/file/csv.svg @@ -0,0 +1,16 @@ + + + 编组 57 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/excel.svg b/web/src/assets/images/file/excel.svg new file mode 100644 index 00000000..cd09cc8c --- /dev/null +++ b/web/src/assets/images/file/excel.svg @@ -0,0 +1,15 @@ + + + Excel + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/html.svg b/web/src/assets/images/file/html.svg new file mode 100644 index 00000000..641f97a2 --- /dev/null +++ b/web/src/assets/images/file/html.svg @@ -0,0 +1,15 @@ + + + Word + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/image.svg b/web/src/assets/images/file/image.svg new file mode 100644 index 00000000..f81baa50 --- /dev/null +++ b/web/src/assets/images/file/image.svg @@ -0,0 +1,15 @@ + + + 编组 58 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/json.svg b/web/src/assets/images/file/json.svg new file mode 100644 index 00000000..4ced0745 --- /dev/null +++ b/web/src/assets/images/file/json.svg @@ -0,0 +1,12 @@ + + + JSON + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/md.svg b/web/src/assets/images/file/md.svg new file mode 100644 index 00000000..c2cb9619 --- /dev/null +++ b/web/src/assets/images/file/md.svg @@ -0,0 +1,17 @@ + + + PDF + + + + + + + + + MD + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/pdf.svg b/web/src/assets/images/file/pdf.svg new file mode 100644 index 00000000..10c3020b --- /dev/null +++ b/web/src/assets/images/file/pdf.svg @@ -0,0 +1,18 @@ + + + PDF + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/ppt.svg b/web/src/assets/images/file/ppt.svg new file mode 100644 index 00000000..eb3d4d8d --- /dev/null +++ b/web/src/assets/images/file/ppt.svg @@ -0,0 +1,12 @@ + + + file-ppt-2-fill + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/txt.svg b/web/src/assets/images/file/txt.svg new file mode 100644 index 00000000..141d2bfb --- /dev/null +++ b/web/src/assets/images/file/txt.svg @@ -0,0 +1,12 @@ + + + txt + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/video.svg b/web/src/assets/images/file/video.svg new file mode 100644 index 00000000..08c0b262 --- /dev/null +++ b/web/src/assets/images/file/video.svg @@ -0,0 +1,14 @@ + + + 编组 59 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/file/word.svg b/web/src/assets/images/file/word.svg new file mode 100644 index 00000000..dc37637d --- /dev/null +++ b/web/src/assets/images/file/word.svg @@ -0,0 +1,15 @@ + + + Word + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/AudioRecorder/index.tsx b/web/src/components/AudioRecorder/index.tsx index d31746f6..10b8eca9 100644 --- a/web/src/components/AudioRecorder/index.tsx +++ b/web/src/components/AudioRecorder/index.tsx @@ -1,13 +1,23 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-06 21:11:51 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 17:11:14 + */ import { type FC, useRef, useState } from 'react' import RecordRTC from 'recordrtc' import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage' import { request } from '@/utils/request' +/** Props for the AudioRecorder component */ interface AudioRecorderProps { + /** Callback fired when recording is complete, receives uploaded file info and raw blob */ onRecordingComplete?: (file: { file_id: string; file_key: string; url: string; type?: string; }, blob?: Blob) => void className?: string; + /** Upload endpoint URL, defaults to fileUploadUrlWithoutApiPrefix */ action?: string; + /** Additional config passed to the upload request */ requestConfig?: Record; } @@ -17,9 +27,12 @@ const AudioRecorder: FC = ({ action = fileUploadUrlWithoutApiPrefix, requestConfig = {} }) => { + // Whether the recorder is currently capturing audio const [isRecording, setIsRecording] = useState(false) + // Holds the RecordRTC instance across renders const recorderRef = useRef(null) + /** Request microphone access and start recording */ const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) @@ -34,6 +47,7 @@ const AudioRecorder: FC = ({ } } + /** Stop recording, upload the audio blob, then invoke the completion callback */ const stopRecording = () => { if (recorderRef.current) { recorderRef.current.stopRecording(() => { @@ -49,6 +63,7 @@ const AudioRecorder: FC = ({ type: blob.type, url }, blob) + // Release recorder resources after upload recorderRef.current?.destroy() recorderRef.current = null }) @@ -57,12 +72,14 @@ const AudioRecorder: FC = ({ } } + // Toggle between recording/idle states on click; + // swap background image to reflect current state return (
diff --git a/web/src/components/ButtonCheckbox/index.tsx b/web/src/components/ButtonCheckbox/index.tsx index 4b43f18a..81396648 100644 --- a/web/src/components/ButtonCheckbox/index.tsx +++ b/web/src/components/ButtonCheckbox/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:01:59 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:46:05 + * @Last Modified time: 2026-03-12 14:59:38 */ /** @@ -15,7 +15,7 @@ */ import { type FC, type ReactNode, useEffect } from 'react'; -import { type RadioGroupProps } from 'antd'; +import { type RadioGroupProps, Flex } from 'antd'; import clsx from 'clsx' // Button checkbox component props @@ -32,6 +32,7 @@ interface ButtonCheckboxProps extends Omit { checkedIcon?: string; /** Button content */ children?: ReactNode + cicle?: boolean; } const ButtonCheckbox: FC = ({ @@ -41,6 +42,7 @@ const ButtonCheckbox: FC = ({ icon, checkedIcon, children, + cicle = false }) => { // Listen to value changes and trigger side effects via onValueChange callback useEffect(() => { @@ -57,21 +59,26 @@ const ButtonCheckbox: FC = ({ } return ( -
{/* Display unchecked icon when not checked */} - {icon && !checked && } + {icon && !checked && } {/* Display checked icon when checked */} {checkedIcon && checked && } {children} -
+ ); }; diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx index 9a60918a..9a49b0f7 100644 --- a/web/src/components/Chat/index.tsx +++ b/web/src/components/Chat/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2025-12-10 16:46:09 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-06 21:05:09 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-12 13:57:49 */ import { type FC } from 'react' import ChatInput from './ChatInput' @@ -25,7 +25,8 @@ const Chat: FC = ({ labelFormat, errorDesc, fileList, - fileChange + fileChange, + renderRuntime }) => { return (
@@ -37,6 +38,7 @@ const Chat: FC = ({ empty={empty} labelFormat={labelFormat} errorDesc={errorDesc} + renderRuntime={renderRuntime} /> {/* Chat input area */} diff --git a/web/src/components/Chat/types.ts b/web/src/components/Chat/types.ts index 0cf1b130..e8e00bd9 100644 --- a/web/src/components/Chat/types.ts +++ b/web/src/components/Chat/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:45:54 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-06 21:05:09 + * @Last Modified time: 2026-03-12 13:57:51 */ import { type ReactNode } from 'react' @@ -53,6 +53,7 @@ export interface ChatProps { fileList?: any[]; /** Attachment update */ fileChange?: (fileList: any[]) => void; + renderRuntime?: (item: ChatItem, index: number) => ReactNode; } /** diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 62f404aa..26320367 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -458,6 +458,7 @@ export const en = { imageSquareRequired: 'Please upload a square image', nameInvalid: 'Name cannot start or end with a space', notAllSpaces: 'Cannot be all spaces', + view: 'View', }, model: { searchPlaceholder: 'search model…', @@ -1370,7 +1371,58 @@ export const en = { gotoList: 'Return to Application List', gotoDetail: 'View Details', dify: 'Dify', - pleaseUploadFile: 'Please upload workflow file', + setting: 'Settings', + funConfig: 'Features', + fileUpload: 'File Upload', + fileUploadDesc: 'The chat input box supports file uploads. Types include images, documents, and other types', + settings: 'File Upload Settings', + uploadType: 'Upload Type', + local: 'Local Upload', + both: 'Both', + maxSize: 'Maximum Upload', + maxSizeDesc: 'Documents < 200.00MB, Images < 10.00MB, Audio < 50.00MB, Video < 100.00MB', + supportedTypes: 'Supported File Types', + document: 'Document', + image: 'Image', + audio: 'Audio', + video: 'Video', + other: 'Other File Types', + otherFormats: 'Specify other file types', + maxCount: 'Max Files', + singleMaxSize: 'Max Size', + unix: 'items', + textTranfer: 'Text to Speech', + textTranferDesc: 'Text can be converted to speech', + + apps: 'My Apps', + sharing: 'Sharing', + sharingApp: 'Shared Apps', + myShare: 'My Shares', + selectTargetSpace: 'Select Target Space', + alreadyShared: 'Already Shared', + permissionMode: 'Permission Mode', + readonlyMode: 'Use Shared', + readonlyModeDesc: 'Can test and run, cannot view internals', + editableMode: 'Copy Shared', + editableModeDesc: 'Copy full replica, free to edit', + confirmSharing: 'Confirm Sharing', + selectAtLeastOneSpace: 'Please select at least one target space', + test: 'Conversation Test', + log: 'Logs', + testChatEmpty: 'Send a message to test the shared app', + allCancel: 'Cancel All', + cancelShare: 'Cancel Sharing', + confirmAppCancelShareDesc: 'Are you sure to cancel sharing of 【{{app}}】 app with 【{{workspace}}】 space? The other party will no longer have access after cancellation.', + confirmWorkspaceCancelShareDesc: 'Are you sure to cancel all apps shared with 【{{workspace}}】? This action cannot be undone.', + sourceActive: 'Active', + sourceInactive: 'Inactive', + readonly: 'Use Only', + editable: 'Copyable', + version: 'Version', + permission: 'Permission', + souceStatus: 'Source App Status', + confirmCopyDesc: 'Are you sure to copy 【{{app}}】 app?', + noShareAuth: 'No permission to share apps', }, userMemory: { userMemory: 'User Memory', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 387c67c3..63074c59 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -755,6 +755,58 @@ export const zh = { gotoDetail: '查看详情', dify: 'Dify', pleaseUploadFile: '请上传工作流文件', + setting: '设置', + funConfig: '功能', + fileUpload: '文件上传', + fileUploadDesc: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型', + settings: '文件上传设置', + uploadType: '上传类型', + local: '本地上传', + both: '两者皆可', + maxSize: '最大上传大小', + maxSizeDesc: '文档 < 200.00MB,图片 < 10.00MB,音频 < 50.00MB,视频 < 100.00MB', + supportedTypes: '支持的文件类型', + document: '文档', + image: '图片', + audio: '音频', + video: '视频', + other: '其他文件类型', + otherFormats: '指定其他文件类型', + maxCount: '最大文件数', + singleMaxSize: '单文件最大大小', + unix: '个', + textTranfer: '文字转语音', + textTranferDesc: '文本可以转换成语言', + + apps: '我的应用', + sharing: '共享', + sharingApp: '共享应用', + myShare: '我的共享', + selectTargetSpace: '选择目标空间', + alreadyShared: '已共享', + permissionMode: '权限模式', + readonlyMode: '使用共享', + readonlyModeDesc: '可测试运行,不可查看内部', + editableMode: '复制共享', + editableModeDesc: '复制完整副本,自由编辑', + confirmSharing: '确认共享', + selectAtLeastOneSpace: '请选择至少一个目标空间', + test: '对话测试', + log: '日志', + testChatEmpty: '发送消息测试共享应用效果', + allCancel: '全部取消', + cancelShare: '取消共享', + confirmAppCancelShareDesc: '确定取消该【{{app}}】应用对【{{workspace}}】空间的共享?取消后对方将无法访问。', + confirmWorkspaceCancelShareDesc: '确定取消所有共享给【{{workspace}}】的应用?此操作不可恢复。', + sourceActive: '生效中', + sourceInactive: '已失效', + readonly: '仅使用', + editable: '可复制', + version: '版本号', + permission: '权限', + souceStatus: '源应用状态', + confirmCopyDesc: '确定复制【{{app}}】应用?', + noShareAuth: '无共享应用的权限', }, table: { totalRecords: '共 {{total}} 条记录' @@ -1038,6 +1090,7 @@ export const zh = { imageSquareRequired: '请上传正方形比例图片', nameInvalid: '不能是空格开头或结尾', notAllSpaces: '不能是纯空格', + view: '查看', }, model: { searchPlaceholder: '搜索模型…', @@ -2544,7 +2597,7 @@ export const zh = { memoryHealthVisualization: '记忆健康可视化', activationValueDistribution: '激活值分布', forgettingTrend: '遗忘趋势(近7天)', - + nodes_without_activation: '观察区', low_activation_nodes: '遗忘区', health_nodes: '健康区', diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index ea137bd4..78205491 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -45,6 +45,7 @@ "element": "BasicLayout", "children": [ { "path": "/application/config/:id", "element": "ApplicationConfig" }, + { "path": "/application/config/:id/:source", "element": "ApplicationConfig" }, { "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }, { "path": "/statement/:id", "element": "StatementDetail" }, { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }, diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 237c3373..add3e147 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:21 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-03 14:24:34 + * @Last Modified time: 2026-03-13 16:58:15 */ import { type FC, type ReactNode, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'; import clsx from 'clsx' @@ -23,7 +23,8 @@ import type { MemoryConfig, AiPromptModalRef, Source, - ChatVariableConfigModalRef + ChatVariableConfigModalRef, + FunConfigForm } from './types' import type { Variable } from './components/VariableList/types' import type { KnowledgeConfig } from './components/Knowledge/types' @@ -41,6 +42,7 @@ import ToolList from './components/ToolList/ToolList' import SkillList from './components/Skill' import ChatVariableConfigModal from './components/ChatVariableConfigModal'; import type { Skill } from '@/views/Skills/types' +import FunConfig from './components/FunConfig' /** * Description wrapper component @@ -99,7 +101,7 @@ const SwitchWrapper: FC<{ title: string, desc?: string, name: string | string[]; * @param name - Form field name * @param url - API URL for options */ -const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string }> = ({ title, desc, name, url }) => { +const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], url: string; disabled?: boolean }> = ({ title, desc, name, url, disabled }) => { const { t } = useTranslation(); return ( <> @@ -115,6 +117,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[], hasAll={false} valueKey='config_id' labelKey="config_name" + disabled={disabled} /> @@ -352,7 +355,8 @@ const Agent = forwardRef((_props, ref) => { }, [modelList, values?.default_model_config_id]) useImperativeHandle(ref, () => ({ - handleSave + handleSave, + funConfig: values?.funConfig })) const aiPromptModalRef = useRef(null) @@ -406,7 +410,11 @@ const Agent = forwardRef((_props, ref) => { useEffect(() => { setChatVariables(values?.variables || []) }, [values?.variables]) - console.log('values', values) + + const handleSaveFunConfig = (value: FunConfigForm) => { + form.setFieldValue('funConfig', value) + } + console.log('agent', values) return ( <> {loading && } @@ -418,6 +426,7 @@ const Agent = forwardRef((_props, ref) => { {defaultModel?.name ?
: null} {defaultModel?.name || t('application.chooseModel')} + {/* */} @@ -426,6 +435,7 @@ const Agent = forwardRef((_props, ref) => {
+
@@ -464,11 +474,12 @@ const Agent = forwardRef((_props, ref) => { - @@ -493,11 +504,6 @@ const Agent = forwardRef((_props, ref) => { {t('application.debuggingAndPreview')} - {chatVariables.length > 0 && - - } @@ -511,6 +517,7 @@ const Agent = forwardRef((_props, ref) => { updateChatList={setChatList} handleSave={handleSave} chatVariables={chatVariables} + handleEditVariables={handleOpenVariableConfig} /> diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 2688eaae..e40934a0 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:29:33 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:29:33 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-05 13:47:23 */ import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' @@ -165,7 +165,8 @@ const Cluster = forwardRef((_props, ref) => { setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id)) } useImperativeHandle(ref, () => ({ - handleSave + handleSave, + funConfig: data?.funConfig })) const modelConfigModalRef = useRef(null) diff --git a/web/src/views/ApplicationConfig/ReleasePage.tsx b/web/src/views/ApplicationConfig/ReleasePage.tsx index b44252d6..ab9225f6 100644 --- a/web/src/views/ApplicationConfig/ReleasePage.tsx +++ b/web/src/views/ApplicationConfig/ReleasePage.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:29:41 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:29:41 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-11 17:44:24 */ import { type FC, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,8 @@ import RbCard from '@/components/RbCard/Card' import { getReleaseList, rollbackRelease, appExport } from '@/api/application' import ReleaseModal from './components/ReleaseModal' import ReleaseShareModal from './components/ReleaseShareModal' -import type { Release, ReleaseModalRef, ReleaseShareModalRef } from './types' +import AppSharingModal from './components/AppSharingModal' +import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types' import type { Application } from '@/views/ApplicationManagement/types' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; @@ -39,6 +40,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres const { message } = App.useApp() const releaseModalRef = useRef(null) const releaseShareModalRef = useRef(null) + const appSharingModalRef = useRef(null) const [selectedVersion, setSelectedVersion] = useState(null); const [releaseList, setReleaseList] = useState([]) @@ -129,6 +131,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres {data?.type !== 'multi_agent' && } {data.current_release_id !== selectedVersion.id && } + } @@ -178,6 +181,11 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres ref={releaseShareModalRef} version={selectedVersion} /> +
); } diff --git a/web/src/views/ApplicationConfig/TestChat/index.tsx b/web/src/views/ApplicationConfig/TestChat/index.tsx new file mode 100644 index 00000000..891259f5 --- /dev/null +++ b/web/src/views/ApplicationConfig/TestChat/index.tsx @@ -0,0 +1,642 @@ +import { type FC, useState, useRef, useEffect, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { App, Flex, Dropdown, type MenuProps, Divider, Form, Space } from 'antd' +import { SettingOutlined } from '@ant-design/icons' +import clsx from 'clsx' +import dayjs from 'dayjs' + +import ChatIcon from '@/assets/images/application/chat.png' + +import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal' +import { draftRun } from '@/api/application'; + +import Empty from '@/components/Empty' +import Chat from '@/components/Chat' +import AudioRecorder from '@/components/AudioRecorder' +import RbCard from '@/components/RbCard/Card' +import UploadFiles from '@/views/Conversation/components/FileUpload' +import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' +import Runtime from '@/views/Workflow/components/Chat/Runtime'; +import { nodeLibrary } from '@/views/Workflow/constant' +// import ButtonCheckbox from '@/components/ButtonCheckbox'; + +// import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg' +// 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 { ChatItem } from '@/components/Chat/types' +import type { VariableConfigModalRef, WorkflowConfig } from '@/views/Workflow/types' +import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types' +import type { TestChatProps } from './type'; +import type { UploadFileListModalRef } from '@/views/Conversation/types' +import type { SSEMessage } from '@/utils/stream' + +const formatParams = (message: string, conversation_id: string | null, files: any[] = [], variables: Record) => { + return { + message, + conversation_id, + stream: true, + 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 + } + } + }), + variables: Object.keys(variables).length > 0 ? variables : undefined + } +} + +interface NodeData { + content: string; + conversation_id: string | null; + cycle_id: string; + cycle_idx: number; + node_id: string; + node_name?: string; + node_type?: string; + input?: any; + output?: any; + elapsed_time?: string; + error?: any; + state: Record; + status?: 'completed' | 'failed' +} + +interface FormData { + files: any[]; + variables: Variable[] +} +const TestChat: FC = ({ + application, + config +}) => { + const { t } = useTranslation() + const { message: messageApi } = App.useApp() + const variableConfigModalRef = useRef(null) + const uploadFileListModalRef = useRef(null) + + const [loading, setLoading] = useState(false) // Send button loading state + const [chatList, setChatList] = useState([]) // Chat message history + const [streamLoading, setStreamLoading] = useState(false) // SSE streaming state + const [conversationId, setConversationId] = useState(null) // Current conversation ID + const [message, setMessage] = useState(undefined) // Current input message + const [form] = Form.useForm() + const queryValues = Form.useWatch([], form) + + useEffect(() => { + getVariables() + }, [application, config]) + + const getVariables = () => { + if (!application || !config) return + + let initVariables: Variable[] = [] + + switch (application.type) { + case 'workflow': + const { nodes } = config as WorkflowConfig; + const startNodes = nodes.filter(vo => vo.type === 'start') + if (startNodes.length) { + const curVariables = startNodes[0].config.variables as Variable[] + + curVariables.forEach((vo) => { + if (typeof vo.default !== 'undefined') { + vo.value = vo.default + } + const lastVo = curVariables.find(item => item.name === vo.name) + if (lastVo?.value) { + vo.value = lastVo.value + } + }) + initVariables = curVariables + } + break + case 'agent': + initVariables = config.variables as Variable[] + break + } + + form.setFieldValue('variables', [...initVariables]) + } + + /** + * Opens the variable configuration modal + */ + const handleEditVariables = () => { + variableConfigModalRef.current?.handleOpen(queryValues.variables) + } + /** + * Saves updated variable values from the modal + */ + const handleSave = (values: Variable[]) => { + form.setFieldValue('variables', [...values]) + } + /** + * Handles file upload from local device + */ + const fileChange = (file?: any) => { + form.setFieldValue('files', [...(queryValues.files || []), file]) + } + const handleRecordingComplete = async (file: any) => { + form.setFieldValue('files', [...(queryValues.files || []), 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 + form.setFieldValue('files', [...(queryValues.files || []), ...(list || [])]) + } + /** + * Updates the entire file list (used when removing files) + */ + const updateFileList = (list?: any[]) => { + form.setFieldValue('files', [...list || []]) + } + const isNeedVariableConfig = useMemo(() => { + return queryValues?.variables.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === '')) + }, [queryValues?.variables]) + + const addUserMessage = (message: string, files: any[]) => { + const newUserMessage: ChatItem = { + role: 'user', + content: message, + created_at: Date.now(), + files + }; + setChatList(prev => [...prev, newUserMessage]) + } + const addAssistantMessage = () => { + const { type } = application || {} + setChatList(prev => [...prev, { + role: 'assistant', + content: '', + created_at: Date.now(), + subContent: type === 'workflow' ? [] : undefined, + }]) + } + + const updateAssistantMessage = (content: string) => { + setChatList(prev => { + let newList = [...prev] + const lastMsg = newList[newList.length - 1] + if (lastMsg.role === 'assistant') { + lastMsg.content += content + } + return newList + }) + } + const updateErrorAssistantMessage = (message_length: number) => { + if (message_length > 0) return + setChatList(prev => { + let newList = [...prev] + const lastMsg = newList[newList.length - 1] + if (lastMsg.role === 'assistant') { + lastMsg.content = null + } + return newList + }) + } + const handleSend = () => { + if (loading || !application || !message || !message?.trim()) return + // Validate required variables before sending + const { variables, files } = queryValues; + let isCanSend = true + const params: Record = {} + if (variables && variables.length > 0) { + const needRequired: string[] = [] + variables.forEach(vo => { + params[vo.name] = vo.value + + if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) { + isCanSend = false + needRequired.push(vo.name) + } + }) + + if (needRequired.length) { + messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`) + } + } + if (!isCanSend) { + setLoading(false) + return + } + addUserMessage(message, files) + setMessage(undefined) + form.setFieldValue('files', []) + addAssistantMessage() + setStreamLoading(true) + setLoading(true) + + draftRun( + application.id, + formatParams(message, conversationId, files, params), + handleStreamMessage + ) + .catch(() => { + setLoading(false) + }) + .finally(() => { + setLoading(false) + setStreamLoading(false) + }) + } + const handleStreamMessage = (data: SSEMessage[]) => { + data.map(item => { + const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number }; + + switch (item.event) { + case 'start': + if (conversation_id && conversationId !== conversation_id) { + setConversationId(conversation_id); + } + break + case 'message': + updateAssistantMessage(content) + if (conversation_id && conversationId !== conversation_id) { + setConversationId(conversation_id); + } + break; + case 'end': + updateErrorAssistantMessage(message_length) + setStreamLoading(false) + break; + } + }) + }; + + const handleWorkflowSend = () => { + if (loading || !application || !message || !message?.trim()) return + + // Validate required variables before sending + const { variables, files } = queryValues; + let isCanSend = true + const params: Record = {} + if (variables.length > 0) { + const needRequired: string[] = [] + variables.forEach(vo => { + params[vo.name] = vo.value ?? vo.defaultValue + + if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) { + isCanSend = false + needRequired.push(vo.name) + } + }) + + if (needRequired.length) { + messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`) + } + } + if (!isCanSend) { + return + } + + setLoading(true) + addUserMessage(message, files) + addAssistantMessage() + form.setFieldsValue({ + files: [], + }) + + setMessage(undefined) + setStreamLoading(true) + draftRun( + application.id, + formatParams(message, conversationId, files, params), + handleWorkflowStreamMessage + ) + .catch((error) => { + console.log('draftRun error', error) + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + newList[lastIndex] = { + ...newList[lastIndex], + status: 'failed', + content: null, + subContent: error.error + } + } + return newList + }) + }).finally(() => { + setLoading(false) + setStreamLoading(false) + }) + } + const handleWorkflowStreamMessage = (data: SSEMessage[]) => { + data.forEach(item => { + const { content, conversation_id } = item.data as NodeData; + + switch (item.event) { + // Append streaming text chunks to assistant message + case 'message': + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + newList[lastIndex] = { + ...newList[lastIndex], + content: newList[lastIndex].content + content + } + } + return newList + }) + break + // Track node execution start + case 'node_start': + addWorkflowNodeStartMessage(item.data as NodeData) + break + // Update node with execution results or errors + case 'node_end': + case 'node_error': + updateWorkflowNodeEndMessage(item.data as NodeData) + break + // Update node with subContent + case 'cycle_item': + updateWorkflowCycleMessage(item.data as NodeData) + break + // Mark workflow as complete + case 'workflow_end': + updateWorkflowEndMessage(item.data as NodeData) + setStreamLoading(false) + setLoading(false) + break + } + + if (conversation_id && conversationId !== conversation_id) { + setConversationId(conversation_id) + } + }) + } + const addWorkflowNodeStartMessage = (data: NodeData) => { + const { node_id } = data; + const { nodes } = config as WorkflowConfig + + const node = nodes.find(n => n.id === node_id); + const { name, type } = node || {} + const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + const newSubContent = newList[lastIndex].subContent || [] + const filterIndex = newSubContent.findIndex(vo => vo.id === node_id) + if (filterIndex > -1) { + newSubContent[filterIndex] = { + ...newSubContent[filterIndex], + node_id: node_id, + node_name: name, + node_type: type, + icon, + content: {}, + } + } else { + newSubContent.push({ + id: node_id, + node_id: node_id, + node_name: name, + node_type: type, + icon, + content: {}, + }) + } + newList[lastIndex] = { + ...newList[lastIndex], + subContent: newSubContent + } + } + return newList + }) + } + const updateWorkflowNodeEndMessage = (data: NodeData) => { + const { node_id, input, output, error, elapsed_time, status } = data; + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + const newSubContent = newList[lastIndex].subContent || [] + const filterIndex = newSubContent.findIndex(vo => vo.node_id === node_id) + if (filterIndex > -1 && newSubContent[filterIndex].content) { + newSubContent[filterIndex] = { + ...newSubContent[filterIndex], + content: { + input, + output, + error, + }, + status: status || 'completed', + elapsed_time + } + } + newList[lastIndex] = { + ...newList[lastIndex], + subContent: newSubContent + } + } + return newList + }) + } + const updateWorkflowCycleMessage = (data: NodeData) => { + const { node_id, cycle_id, cycle_idx, input, output, error, elapsed_time, status } = data; + const { nodes } = config as WorkflowConfig + + const node = nodes.find(n => n.id === node_id); + const { name, type } = node || {} + const icon = nodeLibrary.flatMap(g => g.nodes).find(n => n.type === type)?.icon + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + const newSubContent = newList[lastIndex].subContent || [] + const filterIndex = newSubContent.findIndex(vo => vo.id === cycle_id) + if (filterIndex > -1) { + const items = newSubContent[filterIndex].subContent || [] + items.push({ + cycle_id, + cycle_idx, + node_id, + node_name: name, + node_type: type, + icon, + content: { + cycle_idx, + input, + output, + error, + }, + status: status || 'completed', + elapsed_time + }) + newSubContent[filterIndex] = { + ...newSubContent[filterIndex], + subContent: [...items] + } + newList[lastIndex] = { + ...newList[lastIndex], + subContent: newSubContent + } + } + } + return newList + }) + } + const updateWorkflowEndMessage = (data: NodeData) => { + const { error, status } = data as { + content: string; + conversation_id: string | null; + cycle_id: string; + cycle_idx: number; + node_id: string; + node_name?: string; + node_type?: string; + input?: any; + output?: any; + elapsed_time?: string; + error?: any; + state: Record; + status?: 'completed' | 'failed' + }; + setChatList(prev => { + const newList = [...prev] + const lastIndex = newList.length - 1 + if (lastIndex >= 0) { + newList[lastIndex] = { + ...newList[lastIndex], + status, + error, + content: newList[lastIndex].content === '' ? null : newList[lastIndex].content, + } + } + return newList + }) + } + + console.log('queryValues', queryValues) + return ( +
+ + } + contentClassName={clsx(`rb:mx-[16px] rb:pt-[24px]`, { + 'rb:h-[calc(100%-140px)]': !queryValues?.files?.length, + 'rb:h-[calc(100%-208px)]': !!queryValues?.files?.length, + })} + data={chatList} + streamLoading={streamLoading} + loading={loading} + onChange={setMessage} + onSend={application?.type === 'workflow' ? handleWorkflowSend : handleSend} + fileList={queryValues?.files || []} + fileChange={updateFileList} + labelFormat={(item) => item.role === 'user' ? t('application.you') : dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')} + errorDesc={t('application.ReplyException')} + renderRuntime={application?.type === 'workflow' ? (item, index) => { + return + } : undefined} + > + + + + + + ) + }, + ], + onClick: handleShowUpload + }} + > + +
+
+
+
+ {/* + + {t(`memoryConversation.web_search`)} + + + + + + + */} + +
+ + + + +
+ +
+ + + + +
+
+ ) +} + +export default TestChat diff --git a/web/src/views/ApplicationConfig/TestChat/type.ts b/web/src/views/ApplicationConfig/TestChat/type.ts new file mode 100644 index 00000000..15829e28 --- /dev/null +++ b/web/src/views/ApplicationConfig/TestChat/type.ts @@ -0,0 +1,8 @@ +import type { Application } from '@/views/ApplicationManagement/types' +import type { Config } from '../types'; +import type { WorkflowConfig } from '@/views/Workflow/types'; + +export interface TestChatProps { + application?: Application | null; + config: Config | WorkflowConfig | null +} \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/AppSharingModal.tsx b/web/src/views/ApplicationConfig/components/AppSharingModal.tsx new file mode 100644 index 00000000..39b2a77e --- /dev/null +++ b/web/src/views/ApplicationConfig/components/AppSharingModal.tsx @@ -0,0 +1,183 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-13 17:19:13 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 17:26:57 + */ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Checkbox, App, Form } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import RbModal from '@/components/RbModal'; +import { appSharing, getAppShares } from '@/api/application'; +import { formatDateTime } from '@/utils/format'; +import type { AppSharingModalRef, Release } from '../types'; +import type { SpaceItem } from '@/views/KnowledgeBase/types'; +import { getWorkspaces } from '@/api/workspaces'; +import RadioGroupCard from '@/components/RadioGroupCard'; + +/** Props for the AppSharingModal component */ +interface AppSharingModalProps { + /** ID of the application being shared */ + appId: string; + /** The release version to share */ + version: Release | null; +} + +const AppSharingModal = forwardRef(({ appId, version }, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + + const [visible, setVisible] = useState(false); + const [loading, setLoading] = useState(false); + // All workspaces available to share with (excluding the current one) + const [spaceList, setSpaceList] = useState([]); + // IDs of workspaces that already have access to this app + const [sharedIds, setSharedIds] = useState([]); + + const [form] = Form.useForm<{ target_workspace_ids: string[]; permission: 'readonly' | 'editable' }>(); + // Reactively track the currently selected workspace IDs in the form + const selectedIds: string[] = Form.useWatch('target_workspace_ids', form) ?? []; + + /** + * Fetch workspaces and existing share records in parallel, + * sort already-shared spaces to the top, then open the modal. + * Shows a warning if the user has no shareable workspaces. + */ + const handleOpen = () => { + Promise.all([getWorkspaces({ include_current: false }), getAppShares(appId)]).then(([spaces, shared]) => { + // Normalise the shared workspace ID field across different API response shapes + const ids = ((shared as any[]) || []).map((s: any) => s.workspace_id || s.target_workspace_id || s.id); + // Sort: already-shared workspaces appear first + const sorted = (spaces as SpaceItem[]).sort((a, b) => + ids.includes(b.id) ? 1 : ids.includes(a.id) ? -1 : 0 + ); + setSpaceList(sorted); + setSharedIds(ids); + + if (sorted.length > 0) { + setVisible(true); + } else { + message.warning(t('application.noShareAuth')); + } + }); + }; + + /** Close the modal and reset form fields */ + const handleClose = () => { + setVisible(false); + form.resetFields(); + }; + + // Expose open/close handlers to the parent via ref + useImperativeHandle(ref, () => ({ handleOpen, handleClose })); + + /** + * Toggle a workspace in the selected list. + * Already-shared workspaces are read-only and cannot be toggled. + */ + const handleToggle = (id: string, isShared: boolean) => { + if (isShared) return; + const prev = form.getFieldValue('target_workspace_ids') as string[] ?? []; + form.setFieldValue( + 'target_workspace_ids', + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + }; + + /** Validate the form then submit the sharing request */ + const handleConfirm = () => { + form.validateFields().then(values => { + setLoading(true); + appSharing(appId, values) + .then(() => { + message.success(t('common.operateSuccess')); + handleClose(); + }) + .finally(() => setLoading(false)); + }); + }; + + // Normalise the version label to always start with "v" + const versionLabel = version?.version_name + ? (version.version_name[0].toLowerCase() === 'v' ? version.version_name : `v${version.version_name}`) + : `v${version?.version}`; + + return ( + {t('application.confirmSharing')}({selectedIds.length})} + onOk={handleConfirm} + confirmLoading={loading} + width={600} + > +
+ {/* Version info: displays version number, release time and publisher */} +
+
{t('application.VersionInformation')}
+
+
+
{t('application.versionList').replace('列表', '号')}
+
{versionLabel}
+
+
+
{t('application.releaseTime')}
+
{formatDateTime(version?.published_at || 0, 'YYYY-MM-DD HH:mm:ss')}
+
+
+
{t('application.publisher')}
+
{version?.publisher_name}
+
+
+
+ + {/* Target space: scrollable list of workspaces with checkbox selection */} + +
+ {spaceList.map(space => { + const isShared = sharedIds.includes(space.id); + return ( +
handleToggle(space.id, isShared)}> + handleToggle(space.id, isShared)} + /> + {space.name} + {/* Badge shown when the app is already shared with this workspace */} + {isShared && ( + {t('application.alreadyShared')} + )} +
+ ); + })} +
+
+ + {/* Permission mode: readonly (use only) or editable (full copy) */} + + ({ + value: type, + label: t(`application.${type}Mode`), + labelDesc: t(`application.${type}ModeDesc`), + }))} + /> + +
+
+ ); +}); + +export default AppSharingModal; diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 17af7613..6e98f705 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:39 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-05 17:03:46 + * @Last Modified time: 2026-03-13 15:20:32 */ /** * Chat debugging component for application testing @@ -13,7 +13,8 @@ import { type FC, useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx' -import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd' +import { Flex, Dropdown, type MenuProps, App, Divider } from 'antd'; +import { SettingOutlined } from '@ant-design/icons' import ChatIcon from '@/assets/images/application/chat.png' import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png' @@ -45,13 +46,17 @@ interface ChatProps { /** Source type: multi-agent cluster or single agent */ source?: 'multi_agent' | 'agent'; chatVariables?: Variable[]; // Add chatVariables prop + handleEditVariables?: () => void; } /** * Chat debugging component * Allows testing application with different model configurations side-by-side */ -const Chat: FC = ({ chatList, data, updateChatList, handleSave, source = 'agent', chatVariables }) => { +const Chat: FC = ({ + chatList, data, updateChatList, handleSave, source = 'agent', chatVariables, + handleEditVariables +}) => { const { t } = useTranslation(); const { message: messageApi } = App.useApp() const [loading, setLoading] = useState(false) @@ -434,6 +439,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc const updateFileList = (list?: any[]) => { setFileList([...list || []]) } + const isNeedVariableConfig = chatVariables?.some(vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === '')) return (
@@ -521,6 +527,18 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc className="rb:size-6 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/conversation/link.svg')] rb:hover:bg-[url('@/assets/images/conversation/link_hover.svg')]" >
+ {chatVariables && chatVariables.length > 0 && ( +
+ + {t(`memoryConversation.variableConfig`)} +
+ )} diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 8131a819..3350780c 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -2,9 +2,9 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:52 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-28 16:48:52 + * @Last Modified time: 2026-03-13 17:12:59 */ -import { type FC, useRef, useMemo } from 'react'; +import { type FC, useRef, useMemo, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Layout, Tabs, Dropdown, Button, Flex } from 'antd'; import type { MenuProps } from 'antd'; @@ -18,9 +18,10 @@ import exportIcon from '@/assets/images/export_hover.svg' import deleteIcon from '@/assets/images/delete_hover.svg' import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types'; import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal' -import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef } from '../types' +import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FunConfigForm } from '../types' import { deleteApplication, appExport } from '@/api/application' import CopyModal from './CopyModal' +import FunConfig from './FunConfig' const { Header } = Layout; @@ -28,6 +29,11 @@ const { Header } = Layout; * Tab keys for application configuration */ const tabKeys = ['arrangement', 'api', 'release', 'statistics'] +const sharingTabKeys = [ + 'test', + // 'log', + 'api' +] /** * Menu icon mapping @@ -64,22 +70,23 @@ interface ConfigHeaderProps { const ConfigHeader: FC = ({ application, activeTab, handleChangeTab, refresh, workflowRef, + appRef, }) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { id } = useParams(); + const { id, source } = useParams(); const applicationModalRef = useRef(null); const copyModalRef = useRef(null); /** * Format tab items for display */ - const formatTabItems = () => { - return tabKeys.map(key => ({ + const formatTabItems = useMemo(() => { + return (source === 'sharing' ? sharingTabKeys : tabKeys).map(key => ({ key, label: t(`application.${key}`), })) - } + }, [source, sharingTabKeys, tabKeys]) /** * Handle menu item click */ @@ -160,6 +167,13 @@ const ConfigHeader: FC = ({ return items }, [t, handleClick, application]) + const funConfig = useMemo(() => { + return (appRef?.current?.funConfig || { file_type: [] }) as FunConfigForm + }, [appRef]) + const handleSaveFunConfig = useCallback((value: FunConfigForm) => { + appRef?.current?.handleSaveFunConfig?.(value) + }, [appRef]) + console.log('formatMenuItems', formatMenuItems) return ( <> @@ -170,7 +184,7 @@ const ConfigHeader: FC = ({
{application?.name}
- = ({
-
+ }
{application?.type === 'workflow' - ?
+ ?
+ {/* */} diff --git a/web/src/views/ApplicationConfig/components/FunConfig/FileUploadSettingModal.tsx b/web/src/views/ApplicationConfig/components/FunConfig/FileUploadSettingModal.tsx new file mode 100644 index 00000000..3d114600 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/FunConfig/FileUploadSettingModal.tsx @@ -0,0 +1,182 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-05 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-11 15:42:13 + */ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Radio, InputNumber, Flex, Switch, Row, Col } from 'antd'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; + +import RbModal from '@/components/RbModal'; +import type { FunConfigForm } from '../../types' + +interface FileUploadSettingModalRef { + handleOpen: (values?: FileUploadSettings) => void; + handleClose: () => void; +} + +interface FileUploadSettings extends Omit {} + +interface FileUploadSettingModalProps { + onSave: (values: FileUploadSettings) => void; +} + +const fileTypeOptions = [ + { + type: 'document', + icon:
, + formats: 'TXT, MD, MDX, MARKDOWN, PDF, DOC, DOCX', + defaultMaxCount: 1, + defaultMaxSize: 2 + }, + { + type: 'image', + icon:
, + formats: 'JPG, JPEG, PNG, GIF, WEBP, SVG', + defaultMaxCount: 1, + defaultMaxSize: 2 + }, + { + type: 'audio', + icon:
, + formats: 'MP3, M4A, WAV, AMR, MPGA', + defaultMaxCount: 1, + defaultMaxSize: 2 + }, + { + type: 'video', + icon:
, + formats: 'MP4, MOV, MPEG, WEBM', + defaultMaxCount: 1, + defaultMaxSize: 2 + }, +]; + +const FileUploadSettingModal = forwardRef(({ + onSave, +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const values = Form.useWatch([], form) + + const handleClose = () => { + setVisible(false); + form.resetFields(); + }; + + const handleOpen = (values?: FileUploadSettings) => { + setVisible(true); + // if (values) { + // form.setFieldsValue(values); + // } + }; + + const handleSave = async () => { + const values = await form.validateFields(); + onSave(values); + handleClose(); + }; + + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + + return ( + +
({ + type: opt.type, + enabled: false, + maxCount: opt.defaultMaxCount, + maxSize: opt.defaultMaxSize + })) + }} + > + + + {t('application.local')} + URL + {t('application.both')} + + +
{t('application.maxCount')}
+ + + + + + + {(fields) => ( + + {fields.map((field, index) => { + const option = fileTypeOptions[index]; + const isEnabled = values?.fileTypes?.[index]?.enabled; + + return ( +
+ + + {option.icon} + + + + +
{t(`application.${option.type}`)}
+
{option.formats}
+
+ + + +
+ +
+ {isEnabled && ( + +
{t('application.singleMaxSize')}:
+ + + +
+ )} + +
+ ); + })} +
+ )} +
+
+
+
+ ); +}); + +export default FileUploadSettingModal; diff --git a/web/src/views/ApplicationConfig/components/FunConfig/FunConfigModal.tsx b/web/src/views/ApplicationConfig/components/FunConfig/FunConfigModal.tsx new file mode 100644 index 00000000..affa4e63 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/FunConfig/FunConfigModal.tsx @@ -0,0 +1,140 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-03 16:27:56 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 17:20:30 + */ +/** + * Copy Application Modal + * Allows users to duplicate an existing application with a new name + */ + +import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; +import { Form, Button, Flex } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { FunConfigModalRef } from '../../types' +import RbModal from '@/components/RbModal' +import type { FunConfigForm } from '../../types' +import SwitchFormItem from '@/components/FormItem/SwitchFormItem' +import FileUploadSettingModal from './FileUploadSettingModal' + +const FormItem = Form.Item; + +interface FunConfigModalProps { + refresh: (value: FunConfigForm) => void; +} + +/** + * Modal for copying applications + */ +const FunConfigModal = forwardRef(({ + refresh, +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const values = Form.useWatch([], form) + const fileUploadSettingModalRef = useRef(null) + + /** Close modal and reset form */ + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + /** Open modal */ + const handleOpen = (initValue: FunConfigForm) => { + setVisible(true); + form.setFieldsValue(initValue) + }; + /** Copy application with new name */ + const handleSave = () => { + setVisible(false); + setLoading(true) + const values = form.getFieldsValue() + refresh(values) + } + + const handleOpenSettings = () => { + fileUploadSettingModalRef.current?.handleOpen(values) + } + + const handleSaveSettings = (settings: any) => { + form.setFieldsValue(settings) + } + + /** Expose methods to parent component */ + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + return ( + <> + +
+ +
+ +
+ +
+ +
+ +
+ + {values?.fileUpload?.enabled && values?.fileTypes?.length > 0 ? <> +
+
{t(`application.supportedTypes`)}
+
{t('application.maxCount')}
+
{t('application.singleMaxSize')}
+
+ {values?.fileTypes?.filter(item => item.enabled).map(item => ( +
+
{t(`application.${item.type}`)}
+
{item.maxCount} {t('application.unix')}
+
{item.maxSize} MB
+
+ ))} + + : null} + + +
+
+
+
+ + + + ); +}); + +export default FunConfigModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/FunConfig/index.tsx b/web/src/views/ApplicationConfig/components/FunConfig/index.tsx new file mode 100644 index 00000000..7242acee --- /dev/null +++ b/web/src/views/ApplicationConfig/components/FunConfig/index.tsx @@ -0,0 +1,50 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-13 17:20:21 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 17:20:21 + */ +import { type FC, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from 'antd'; + +import FunConfigModal from './FunConfigModal' +import type { FunConfigModalRef, FunConfigForm } from '../../types' + +/** Props for the FunConfig component */ +interface FunConfigProps { + /** Current feature configuration values */ + value: FunConfigForm; + /** Callback to propagate updated config back to the parent */ + refresh: (value: FunConfigForm) => void; +} + +const FunConfig: FC = ({ + value, + refresh +}) => { + const { t } = useTranslation(); + // Ref used to imperatively open the config modal + const funConfigModalRef = useRef(null) + + /** Open the feature config modal pre-populated with the current values */ + const handleFunConfig = () => { + console.log('funConfig', value) + funConfigModalRef.current?.handleOpen(value) + } + + return ( + <> + {/* Button that triggers the feature configuration modal */} + + + {/* Modal for editing feature settings; calls refresh on save */} + + + ) +} + +export default FunConfig diff --git a/web/src/views/ApplicationConfig/index.tsx b/web/src/views/ApplicationConfig/index.tsx index df5dbd59..fb6c7b54 100644 --- a/web/src/views/ApplicationConfig/index.tsx +++ b/web/src/views/ApplicationConfig/index.tsx @@ -1,22 +1,24 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 16:29:37 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 16:29:37 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-12 10:23:18 */ import React, { useEffect, useState, useRef } from 'react'; import { useParams } from 'react-router-dom'; import ConfigHeader from './components/ConfigHeader' -import type { AgentRef, ClusterRef, WorkflowRef } from './types' +import type { AgentRef, ClusterRef, WorkflowRef, Config } from './types' import type { Application } from '@/views/ApplicationManagement/types' import Agent from './Agent' import Api from './Api' import ReleasePage from './ReleasePage' import Cluster from './Cluster' -import { getApplication } from '@/api/application' +import { getApplication, getApplicationConfig, getMultiAgentConfig, getWorkflowConfig } from '@/api/application' import Workflow from '@/views/Workflow'; import Statistics from './Statistics' +import TestChat from './TestChat' +import type { WorkflowConfig } from '@/views/Workflow/types'; /** * Application configuration page component @@ -25,7 +27,7 @@ import Statistics from './Statistics' */ const ApplicationConfig: React.FC = () => { // Hooks - const { id } = useParams(); + const { id, source } = useParams(); // Refs for different application types const agentRef = useRef(null) @@ -36,6 +38,31 @@ const ApplicationConfig: React.FC = () => { const [application, setApplication] = useState(null); const [activeTab, setActiveTab] = useState('arrangement'); + useEffect(() => { + setActiveTab(source === 'sharing' ? 'test' : 'arrangement') + }, [source]) + + const [config, setConfig] = useState(null) + useEffect(() => { + if (source === 'sharing' && application?.type) { + getAppConfig() + } + }, [source, application?.type]) + + const getAppConfig = () => { + if (!id || !source || !application?.type) { + return + } + const request = application?.type === 'agent' + ? getApplicationConfig + : application?.type === 'multi_agent' + ? getMultiAgentConfig + : getWorkflowConfig + request(id as string).then(res => { + setConfig(res as Config | WorkflowConfig | null) + }) + } + /** * Handle tab change with auto-save for arrangement tab * @param key - New tab key @@ -94,6 +121,7 @@ const ApplicationConfig: React.FC = () => { {activeTab === 'api' && } {activeTab === 'release' && } {activeTab === 'statistics' && } + {activeTab === 'test' && } ); }; diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 36d40a40..859d6fdf 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:49 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-28 16:40:30 + * @Last Modified time: 2026-03-13 17:01:04 */ import type { KnowledgeConfig } from './components/Knowledge/types' import type { Variable } from './components/VariableList/types' @@ -77,6 +77,8 @@ export interface Config extends MultiAgentConfig { /** Last update timestamp */ updated_at: number; skills?: SkillConfigForm | null; + + funConfig?: FunConfigForm; } /** @@ -127,6 +129,8 @@ export interface AgentRef { * @param flag - Whether to show success message */ handleSave: (flag?: boolean) => Promise; + funConfig: Config['funConfig']; + handleSaveFunConfig?: (value: FunConfigForm) => void; } /** @@ -138,6 +142,8 @@ export interface ClusterRef { * @param flag - Whether to show success message */ handleSave: (flag?: boolean) => Promise; + funConfig: Config['funConfig']; + handleSaveFunConfig?: (value: FunConfigForm) => void; } /** @@ -156,6 +162,8 @@ export interface WorkflowRef { /** Add variable */ addVariable: () => void; config: WorkflowConfig | null; + funConfig: WorkflowConfig['funConfig']; + handleSaveFunConfig?: (value: FunConfigForm) => void; } /** @@ -400,4 +408,34 @@ export interface StatisticsData { total_api_calls: number; /** Total tokens used */ total_tokens: number; +} + +export interface FileTypeConfig { + type: string; + enabled: boolean; + maxCount: number; + maxSize: number; +} +export interface FunConfigForm { + enabled: boolean; + fileTypes: FileTypeConfig[] + uploadType: 'local' | 'url' | 'both'; +} +/** + * Function config modal ref methods + */ +export interface FunConfigModalRef { + /** Open function config modal */ + handleOpen: (value: FunConfigForm) => void; +} + +/** + * App sharing modal ref methods + */ +export interface AppSharingModalRef { + handleOpen: () => void; +} +export interface AppSharingForm { + target_workspace_ids: string[]; + permission: 'readonly' | 'editable' } \ No newline at end of file diff --git a/web/src/views/ApplicationManagement/MySharing.tsx b/web/src/views/ApplicationManagement/MySharing.tsx new file mode 100644 index 00000000..ce4ffc1c --- /dev/null +++ b/web/src/views/ApplicationManagement/MySharing.tsx @@ -0,0 +1,157 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-03 16:34:12 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-13 16:19:37 + */ +import React, { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, App, Flex, Row, Col, Collapse, Tag } from 'antd'; +import clsx from 'clsx'; + +import type { MySharedOutItem } from './types'; +import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application' + +const MySharing: React.FC = () => { + const { t } = useTranslation(); + const { modal } = App.useApp(); + const [data, setData] = useState([]) + + useEffect(() => { getList() }, []) + + const getList = () => { + mySharedOutList().then(res => setData(res as MySharedOutItem[])) + } + + /** Group items by target_workspace_id */ + const grouped = useMemo(() => { + const map = new Map, items: MySharedOutItem[] }>(); + data.forEach(item => { + if (!map.has(item.target_workspace_id)) { + map.set(item.target_workspace_id, { + workspace: { + target_workspace_id: item.target_workspace_id, + target_workspace_name: item.target_workspace_name, + target_workspace_icon: item.target_workspace_icon, + }, + items: [], + }); + } + map.get(item.target_workspace_id)!.items.push(item); + }); + return Array.from(map.values()); + }, [data]); + + const handleAllCancel = (workspace: { target_workspace_name: string; target_workspace_id: string; }) => { + modal.confirm({ + title: t('application.confirmWorkspaceCancelShareDesc', { workspace: workspace.target_workspace_name }), + okText: t('common.confirm'), + cancelText: t('common.cancel'), + okType: 'danger', + onOk: () => { + cancelSpaceShare(workspace.target_workspace_id) + .then(() => { + getList(); + }) + } + }); + }; + + const handleCancelOne = (item: MySharedOutItem) => { + modal.confirm({ + title: t('application.confirmAppCancelShareDesc', { app: item.source_app_name, workspace: item.target_workspace_name }), + okText: t('common.confirm'), + cancelText: t('common.cancel'), + okType: 'danger', + onOk: () => { + cancelShare(item.source_app_id, item.target_workspace_id) + .then(() => { + getList(); + }) + } + }); + }; + + return ( + + {grouped.map(({ workspace, items }) => ( + + {workspace.target_workspace_icon + ? + :
+ {workspace.target_workspace_name[0]} +
+ } + {workspace.target_workspace_name} + {t('application.appCount', { count: items.length })} +
+ ), + extra: ( + + ), + children: ( + + {items.map(item => ( + +
handleCancelOne(item)} + /> + +
+ {item.source_app_name[0]} +
+
{item.source_app_name}
+
+ + + {t('application.type')} + + {t(`application.${item.source_app_type}`)} + + + + {t('application.version')} + {item.source_app_version} + + + {t('application.permission')} + + {t(`application.${item.permission}`)} + + + + {t('application.souceStatus')} + {item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')} + + + + ))} + + ), + }]} + /> + ))} + + ); +}; + +export default MySharing; diff --git a/web/src/views/ApplicationManagement/index.tsx b/web/src/views/ApplicationManagement/index.tsx index 62c02574..8fcc382b 100644 --- a/web/src/views/ApplicationManagement/index.tsx +++ b/web/src/views/ApplicationManagement/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:34:12 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-02 17:48:51 + * @Last Modified time: 2026-03-13 17:03:40 */ /** * Application Management Page @@ -10,9 +10,9 @@ * Supports creating, editing, and deleting applications */ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Row, Col, App, Select, Space, Dropdown } from 'antd'; +import { Button, App, Select, Space, Dropdown, type SegmentedProps, Flex, Form } from 'antd'; import clsx from 'clsx'; import { DeleteOutlined } from '@ant-design/icons'; import { useSearchParams } from 'react-router-dom' @@ -21,12 +21,16 @@ 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 { getApplicationListUrl, deleteApplication, copyApplication } from '@/api/application' import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' import { formatDateTime } from '@/utils/format'; import UploadWorkflowModal from './components/UploadWorkflowModal' import UploadModal from './components/UploadModal' +import PageTabs from '@/components/PageTabs' +import MySharing from './MySharing' + +const tabKeys = ['apps', 'sharing', 'myShare'] /** * Application management main component */ @@ -34,20 +38,19 @@ const ApplicationManagement: React.FC = () => { const { t } = useTranslation(); const { modal } = App.useApp(); const [searchParams] = useSearchParams() - const [query, setQuery] = useState({} as Query); const applicationModalRef = useRef(null); const scrollListRef = useRef(null) const uploadWorkflowModalRef = useRef(null); const uploadModalRef = useRef(null); + const [form] = Form.useForm() + const query = Form.useWatch([], form) + const [activeTab, setActiveTab] = useState('apps'); useEffect(() => { // Convert URLSearchParams to a plain object for easier access const data = Object.fromEntries(searchParams) const { type } = data - setQuery(prev => ({ - ...prev, - type: type || undefined - })) + form.setFieldValue('type', type || undefined) }, [searchParams]) /** Refresh application list */ @@ -61,7 +64,11 @@ const ApplicationManagement: React.FC = () => { } /** Navigate to application configuration page */ const handleEdit = (item: Application) => { - window.open(`/#/application/config/${item.id}`); + let url = `/#/application/config/${item.id}` + if (item.is_shared) { + url += `/${activeTab}` + } + window.open(url); } /** Delete application with confirmation */ const handleDelete = (item: Application) => { @@ -81,9 +88,6 @@ const ApplicationManagement: React.FC = () => { } }) } - const handleChangeType = (value?: string) => { - setQuery(prev => ({...prev, type: value})) - } const handleImport = () => { uploadWorkflowModalRef.current?.handleOpen() @@ -97,90 +101,137 @@ const ApplicationManagement: React.FC = () => { uploadModalRef.current?.handleOpen() } } + const formatTabItems = useMemo(() => { + return tabKeys.map(value => ({ + value, + label: t(`application.${value}`), + })) + }, [tabKeys, t]) + /** Handle tab change */ + const handleChangeTab = (value: SegmentedProps['value']) => { + setActiveTab(value as string); + form.resetFields() + } + const handleCopy = (item: Application) => { + modal.confirm({ + title: t('application.confirmCopyDesc', { app: item.name }), + okText: t('common.copy'), + cancelText: t('common.cancel'), + onOk: () => { + copyApplication(item.id) + .then(() => { + setActiveTab('apps') + }) + } + }); + } return ( <> - - - ({ + value: type, + label: t(`application.${type}`), + }))} + allowClear + className="rb:w-30" + /> + + + + + + {activeTab === 'apps' && <> + + + + + } + + } + + + + {(activeTab === 'apps' || activeTab === 'sharing') && + + ref={scrollListRef} + url={getApplicationListUrl} + query={{ ...query, include_shared: activeTab === 'sharing' }} + renderItem={(item) => ( + + {item.name[0]} +
+ } > - - - - - -
- - - ref={scrollListRef} - url={getApplicationListUrl} - query={query} - renderItem={(item) => ( - - {item.name[0]} -
- } - > - {['type', 'source', 'created_at'].map((key, index) => ( -
- {t(`application.${key}`)} - ( +
- {key === 'source' && item.is_shared - ? t('application.shared') - : key === 'source' && !item.is_shared - ? t('application.configuration') - : key === 'created_at' - ? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss') - : t(`application.${item[key as keyof Application]}`) - } - -
- ))} + {t(`application.${key}`)} + + {key === 'source' && item.is_shared + ? item.source_workspace_name + : key === 'source' && !item.is_shared + ? t('application.configuration') + : key === 'created_at' + ? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss') + : t(`application.${item[key as keyof Application]}`) + } + +
+ ))} -
- - -
- - )} - /> + {item.is_shared + ?
+ + {item.share_permission === 'editable' && } +
+ :
+ + +
+ } + + )} + /> + } + {activeTab === 'myShare' && } + void; +} +export interface MySharedOutItem { + id: string; + source_app_id: string; + source_workspace_id: string; + target_workspace_id: string; + shared_by: string; + permission: 'readonly' | 'editable'; + created_at: number; + updated_at: number; + source_app_name: string; + source_app_type: string; + source_app_version: string; + source_app_is_active: boolean; + target_workspace_name: string; + target_workspace_icon: string; } \ No newline at end of file diff --git a/web/src/views/Workflow/components/Chat/Runtime.tsx b/web/src/views/Workflow/components/Chat/Runtime.tsx index e41531b0..142b7e1d 100644 --- a/web/src/views/Workflow/components/Chat/Runtime.tsx +++ b/web/src/views/Workflow/components/Chat/Runtime.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-24 17:57:08 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-28 16:48:09 + * @Last Modified time: 2026-03-12 13:39:24 */ /* * Runtime Component @@ -225,12 +225,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
) : <> - {item.error - ?
+ {item.error && +
-
- : renderChild(item.subContent) - } +
+ } + {renderChild(item.subContent)} + ) }]} /> diff --git a/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx index 66491ab7..6bfe26e4 100644 --- a/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx +++ b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx @@ -8,7 +8,6 @@ import RbModal from '@/components/RbModal' interface VariableEditModalProps { refresh: (values: Variable[]) => void; - variables: Variable[] } const VariableConfigModal = forwardRef(({ diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx index 1346a9a6..085df878 100644 --- a/web/src/views/Workflow/index.tsx +++ b/web/src/views/Workflow/index.tsx @@ -60,7 +60,8 @@ const Workflow = forwardRef((_props, ref) => { handleRun, graphRef, addVariable, - config + config, + funConfig: config?.funConfig })) return (
diff --git a/web/src/views/Workflow/types.ts b/web/src/views/Workflow/types.ts index 909c30e4..a43e6680 100644 --- a/web/src/views/Workflow/types.ts +++ b/web/src/views/Workflow/types.ts @@ -2,6 +2,7 @@ import { Graph } from '@antv/x6'; import type { KnowledgeConfig } from './components/Properties/Knowledge/types' import type { Variable } from './components/Properties/VariableList/types' +import type { FunConfigForm } from '@/views/ApplicationConfig/types' export interface NodeConfig { type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string; placeholder?: string; @@ -89,6 +90,8 @@ export interface WorkflowConfig { is_active: boolean; created_at: number; updated_at: number; + + funConfig?: FunConfigForm; } export interface ChatRef {