Merge branch 'release/v0.2.8' of github.com:SuanmoSuanyangTechnology/MemoryBear into release/v0.2.8

This commit is contained in:
yujiangping
2026-03-18 18:17:20 +08:00
44 changed files with 710 additions and 301 deletions

View File

@@ -2,10 +2,12 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:11:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 18:06:00
* @Last Modified time: 2026-03-17 18:39:09
*/
import { type FC, useRef, useState } from 'react'
import RecordRTC from 'recordrtc'
import { App } from 'antd'
import { useTranslation } from 'react-i18next';
import { fileUploadUrlWithoutApiPrefix } from '@/api/fileStorage'
import { request } from '@/utils/request'
@@ -20,6 +22,7 @@ interface AudioRecorderProps {
/** Additional config passed to the upload request */
requestConfig?: Record<string, any>;
disabled?: boolean;
maxSize?: number;
}
const AudioRecorder: FC<AudioRecorderProps> = ({
@@ -27,8 +30,11 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
className = '',
action = fileUploadUrlWithoutApiPrefix,
requestConfig = {},
disabled = false
disabled = false,
maxSize,
}) => {
const { message } = App.useApp()
const { t } = useTranslation();
// Whether the recorder is currently capturing audio
const [isRecording, setIsRecording] = useState(false)
// Holds the RecordRTC instance across renders
@@ -57,6 +63,12 @@ const AudioRecorder: FC<AudioRecorderProps> = ({
recorderRef.current.stopRecording(() => {
const blob = recorderRef.current!.getBlob()
const url = recorderRef.current!.toURL()
if (maxSize && blob.size > maxSize * 1024 * 1024) {
message.error(t('common.fileSizeTip', { size: maxSize }));
return
}
const formData = new FormData()
formData.append('file', blob, `recording_${Date.now()}.webm`)
request

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-03-17 14:22:25
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 14:22:25
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 15:55:13
*/
// Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration
import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react'
@@ -120,7 +120,10 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
// Build dropdown menu items based on allowed transfer methods
const fileMenus: MenuProps['items'] = []
if (file_upload?.allowed_transfer_methods?.includes('remote_url')) {
const enabledTypes = ['image', 'document', 'video', 'audio'].filter(
type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]
)
if (file_upload?.allowed_transfer_methods?.includes('remote_url') && enabledTypes.length > 0) {
fileMenus.push({
key: 'url',
label: t('memoryConversation.addRemoteFile'),
@@ -133,9 +136,6 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
}
})
}
const enabledTypes = ['image', 'document', 'video', 'audio'].filter(
type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']]
)
if (file_upload?.allowed_transfer_methods?.includes('local_file') && enabledTypes.length > 0) {
fileMenus.push({
key: 'upload',
@@ -151,13 +151,11 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
})
}
console.log('queryValues', queryValues)
return (
<Form form={form} initialValues={{ files: [], variables: [] }}>
<Flex justify="space-between" className="rb:flex-1">
<Flex gap={8} align="center">
<Form.Item name="files" noStyle hidden={!file_upload?.enabled}>
<Form.Item name="files" noStyle hidden={!file_upload?.enabled || fileMenus.length === 0}>
<Dropdown menu={{ items: fileMenus }}>
<div 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')]" />
</Dropdown>
@@ -183,6 +181,7 @@ const ChatToolbar = forwardRef<ChatToolbarRef, ChatToolbarProps>(({
action={uploadAction}
requestConfig={uploadRequestConfig}
onRecordingComplete={handleRecordingComplete}
maxSize={file_upload?.audio_max_size_mb}
/>
<Divider type="vertical" className="rb:ml-1.5! rb:mr-3!" />
</Flex>

View File

@@ -776,7 +776,7 @@ export const zh = {
singleMaxSize: '单文件最大大小',
unix: '个',
text_to_speech: '文字转语音',
text_to_speech_desc: '文本可以转换成语',
text_to_speech_desc: '文本可以转换成语',
apps: '我的应用',
sharing: '共享',

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-02 16:35:43
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-04 18:19:24
* @Last Modified time: 2026-03-18 14:32:40
*/
/**
* Server-Sent Events (SSE) Stream Utility Module
@@ -176,17 +176,23 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
case 500:
case 502:
const errorData = await response.json();
let errorInfo = errorData.error || i18n.t('common.serviceUpgrading')
const errorInfo = errorData.error || i18n.t('common.serviceUpgrading');
message.warning(errorInfo);
throw errorInfo;
throw new Error(errorData);
case 400:
const error = await response.json();
message.warning(error.error);
throw error.error || 'Bad Request';
const error400 = error.error || 'Bad Request';
message.warning(error400);
throw new Error(error);
case 403:
const errors = await response.json();
message.warning(i18n.t('common.permissionDenied'));
throw new Error(errors);
case 504:
const errorJson = await response.json();
message.warning(errorJson.error || i18n.t('common.serverError'));
throw errorData.error;
const errorMsg = errorJson.error || i18n.t('common.serverError');
message.warning(errorMsg);
throw new Error(errorJson);
case 401:
if (url?.includes('/public')) {
return message.warning(i18n.t('common.publicApiCannotRefreshToken'));

View File

@@ -129,7 +129,7 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string | string[],
* Agent configuration component
* Manages single agent configuration including prompts, knowledge, memory, variables, and tools
*/
const Agent = forwardRef<AgentRef>((_props, ref) => {
const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const { t } = useTranslation()
const { id } = useParams();
const { message } = App.useApp()
@@ -200,6 +200,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
...response,
tools: allTools
})
onFeaturesLoad?.(response.features)
}).finally(() => {
setLoading(false)
})

View File

@@ -38,7 +38,7 @@ const MAX_LENGTH = 5;
* Multi-agent cluster configuration component
* Manages multi-agent orchestration, sub-agents, and collaboration modes
*/
const Cluster = forwardRef<ClusterRef>((_props, ref) => {
const Cluster = forwardRef<ClusterRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const { t } = useTranslation()
const { message } = App.useApp()
const [form] = Form.useForm()
@@ -131,6 +131,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
} else {
setSubAgents(sub_agents)
}
onFeaturesLoad?.(response.features)
})
}
/**

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:29:41
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-11 17:44:24
* @Last Modified time: 2026-03-18 14:30:41
*/
import { type FC, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -70,7 +70,8 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres
})
}
const handleExport = () => {
appExport(data.id, data.name)
if (!selectedVersion) return
appExport(data.id, data.name, {release_version: selectedVersion.id})
}
return (
<div className="rb:flex rb:h-[calc(100vh-64px)]">

View File

@@ -193,7 +193,10 @@ const TestChat: FC<TestChatProps> = ({
formatParams(message, conversationId, files, params),
handleStreamMessage
)
.catch(() => setLoading(false))
.catch(() => {
updateErrorAssistantMessage(0)
setLoading(false)
})
.finally(() => {
setLoading(false)
setStreamLoading(false)
@@ -243,11 +246,12 @@ const TestChat: FC<TestChatProps> = ({
handleWorkflowStreamMessage
)
.catch((error) => {
const errorInfo = JSON.parse(error.message)
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
if (lastIndex >= 0) {
newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: error.error }
newList[lastIndex] = { ...newList[lastIndex], status: 'failed', content: null, subContent: errorInfo.error }
}
return newList
})

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-03-13 17:19:13
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:26:57
* @Last Modified time: 2026-03-18 16:03:46
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Checkbox, App, Form } from 'antd';
@@ -78,7 +78,7 @@ const AppSharingModal = forwardRef<AppSharingModalRef, AppSharingModalProps>(({
*/
const handleToggle = (id: string, isShared: boolean) => {
if (isShared) return;
const prev = form.getFieldValue('target_workspace_ids') as string[] ?? [];
const prev: string[] = form.getFieldValue('target_workspace_ids') ?? [];
form.setFieldValue(
'target_workspace_ids',
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
@@ -135,10 +135,16 @@ const AppSharingModal = forwardRef<AppSharingModalRef, AppSharingModalProps>(({
{/* Target space: scrollable list of workspaces with checkbox selection */}
<Form.Item
name="target_workspace_ids"
label={t('application.selectTargetSpace')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
required
>
<Form.Item
name="target_workspace_ids"
noStyle
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<input type="hidden" />
</Form.Item>
<div className="rb:rounded-lg rb:border rb:border-[#EBEBEB] rb:divide-y rb:divide-[#EBEBEB] rb:max-h-50 rb:overflow-y-auto">
{spaceList.map(space => {
const isShared = sharedIds.includes(space.id);
@@ -146,11 +152,11 @@ const AppSharingModal = forwardRef<AppSharingModalRef, AppSharingModalProps>(({
<div key={space.id} className="rb:flex rb:items-center rb:gap-2 rb:px-4 rb:py-3 rb:cursor-pointer" onClick={() => handleToggle(space.id, isShared)}>
<Checkbox
checked={isShared || selectedIds.includes(space.id)}
disabled={isShared} // already-shared workspaces cannot be unselected
disabled={isShared}
onClick={(e) => e.stopPropagation()}
onChange={() => handleToggle(space.id, isShared)}
/>
<span className="rb:flex-1 rb:text-sm">{space.name}</span>
{/* Badge shown when the app is already shared with this workspace */}
{isShared && (
<span className="rb:text-xs rb:text-[#5B6167]">{t('application.alreadyShared')}</span>
)}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:52
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 17:04:56
* @Last Modified time: 2026-03-18 15:40:53
*/
import { type FC, useRef, useMemo, useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -61,6 +61,10 @@ interface ConfigHeaderProps {
workflowRef: React.RefObject<WorkflowRef>
/** App component ref (Agent/Cluster/Workflow) */
appRef?: React.RefObject<AgentRef | ClusterRef | WorkflowRef>
/** Features config from parent state */
features?: FeaturesConfigForm;
/** Callback to update features in parent */
onFeaturesChange?: (value: FeaturesConfigForm) => void;
}
/**
@@ -71,6 +75,8 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh,
workflowRef,
appRef,
features,
onFeaturesChange,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -173,14 +179,10 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
return items
}, [t, handleClick, application])
const features = useMemo(() => {
return (appRef?.current?.features || { file_type: [] }) as FeaturesConfigForm
}, [appRef])
const handleSaveFeaturesConfig = useCallback((value: FeaturesConfigForm) => {
appRef?.current?.handleSaveFeaturesConfig?.(value)
}, [appRef])
console.log('formatMenuItems', formatMenuItems)
onFeaturesChange?.(value)
}, [appRef, onFeaturesChange])
return (
<>
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
@@ -211,7 +213,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
</div>
{application?.type === 'workflow'
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
<FeaturesConfig value={features} refresh={handleSaveFeaturesConfig} />
<FeaturesConfig source={application?.type} value={features} refresh={handleSaveFeaturesConfig} />
<Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 18:31:58
* @Last Modified time: 2026-03-18 15:38:14
*/
/**
* Copy Application Modal
@@ -18,9 +18,11 @@ import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
import RbModal from '@/components/RbModal'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
import FileUploadSettingModal from './FileUploadSettingModal'
import type { Application } from '@/views/ApplicationManagement/types';
interface FeaturesConfigModalProps {
refresh: (value: FeaturesConfigForm) => void;
source?: Application['type'];
}
/**
@@ -28,6 +30,7 @@ interface FeaturesConfigModalProps {
*/
const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigModalProps>(({
refresh,
source,
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
@@ -44,6 +47,7 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
/** Open modal */
const handleOpen = (initValue: FeaturesConfigForm) => {
setVisible(true);
console.log('initValue', initValue)
form.setFieldsValue(initValue)
};
/** Copy application with new name */
@@ -66,7 +70,6 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
handleClose
}));
console.log('settings values', values)
return (
<>
<RbModal
@@ -81,20 +84,22 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
layout="vertical"
>
<Flex vertical gap={12}>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t(`memoryConversation.web_search`)}
name={['web_search', "enabled"]}
/>
</div>
{source !== 'workflow' && <>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t(`memoryConversation.web_search`)}
name={['web_search', "enabled"]}
/>
</div>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.text_to_speech')}
name={['text_to_speech', "enabled"]}
desc={t('application.text_to_speech_desc')}
/>
</div>
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem
title={t('application.text_to_speech')}
name={['text_to_speech', "enabled"]}
desc={t('application.text_to_speech_desc')}
/>
</div>
</>}
<div className="rb:relative rb:border rb:border-[#DFE4ED] rb:p-3 rb:rounded-lg rb:bg-[#f5f7fc]">
<SwitchFormItem

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-03-05
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 18:36:09
* @Last Modified time: 2026-03-17 18:10:47
*/
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, InputNumber, Flex, Switch, Row, Col, Radio } from 'antd';
@@ -128,7 +128,7 @@ const FileUploadSettingModal = forwardRef<FileUploadSettingModalRef, FileUploadS
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-1">{t('application.maxCount')}</div>
<Form.Item label={t('application.maxCount')} name="max_file_count">
<InputNumber min={1} max={100} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
<InputNumber min={1} max={100} precision={0} className="rb:w-full!" placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item label={t('application.supportedTypes')}>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-03-13 17:20:21
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 18:31:43
* @Last Modified time: 2026-03-18 15:38:59
*/
import { type FC, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,6 +10,7 @@ import { Button } from 'antd';
import FeaturesConfigModal from './FeaturesConfigModal'
import type { FeaturesConfigModalRef, FeaturesConfigForm } from '../../types'
import type { Application } from '@/views/ApplicationManagement/types';
/** Props for the FeaturesConfig component */
interface FeaturesConfigProps {
@@ -17,11 +18,13 @@ interface FeaturesConfigProps {
value: FeaturesConfigForm;
/** Callback to propagate updated config back to the parent */
refresh: (value: FeaturesConfigForm) => void;
source?: Application['type'];
}
const FeaturesConfig: FC<FeaturesConfigProps> = ({
value,
refresh
refresh,
source
}) => {
const { t } = useTranslation();
// Ref used to imperatively open the config modal
@@ -29,6 +32,7 @@ const FeaturesConfig: FC<FeaturesConfigProps> = ({
/** Open the feature config modal pre-populated with the current values */
const handleFeaturesConfig = () => {
console.log('handleFeaturesConfig', value)
funConfigModalRef.current?.handleOpen(value)
}
@@ -41,6 +45,7 @@ const FeaturesConfig: FC<FeaturesConfigProps> = ({
<FeaturesConfigModal
ref={funConfigModalRef}
refresh={refresh}
source={source}
/>
</>
)

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:26:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:53:06
* @Last Modified time: 2026-03-18 14:01:13
*/
/**
* Tool List Component
@@ -107,7 +107,10 @@ const ToolList: FC<{ value?: ToolOption[]; onChange?: (config: ToolOption[]) =>
}
/** Add new tool to list */
const updateTools = (tool: ToolOption) => {
const list = [...toolList, tool]
const list = [...toolList, {
...tool,
is_active: true,
}]
setToolList(list)
onChange && onChange(list)
}

View File

@@ -37,6 +37,7 @@ const ApplicationConfig: React.FC = () => {
// State
const [application, setApplication] = useState<Application | null>(null);
const [activeTab, setActiveTab] = useState('arrangement');
const [features, setFeatures] = useState<import('./types').FeaturesConfigForm | undefined>(undefined);
useEffect(() => {
setActiveTab(source === 'sharing' ? 'test' : 'arrangement')
@@ -114,10 +115,12 @@ const ApplicationConfig: React.FC = () => {
refresh={getApplicationInfo}
appRef={application?.type === 'agent' ? agentRef : application?.type === 'multi_agent' ? clusterRef : application?.type === 'workflow' ? workflowRef : undefined}
workflowRef={workflowRef}
features={features}
onFeaturesChange={setFeatures}
/>
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} />}
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster ref={clusterRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'arrangement' && application?.type === 'workflow' && <Workflow ref={workflowRef} onFeaturesLoad={setFeatures} />}
{activeTab === 'api' && <Api application={application} />}
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
{activeTab === 'statistics' && <Statistics application={application} />}

View File

@@ -2,15 +2,16 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-13 17:36:16
* @Last Modified time: 2026-03-18 16:15:43
*/
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, type MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, App, Flex, Row, Col, Collapse } from 'antd';
import clsx from 'clsx';
import type { MySharedOutItem } from './types';
import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application'
import BodyWrapper from '@/components/Empty/BodyWrapper'
const MySharing: React.FC = () => {
const { t } = useTranslation();
@@ -20,7 +21,8 @@ const MySharing: React.FC = () => {
useEffect(() => { getList() }, [])
const getList = () => {
mySharedOutList().then(res => setData(res as MySharedOutItem[]))
mySharedOutList()
.then(res => setData(res as MySharedOutItem[]))
}
/** Group items by target_workspace_id */
@@ -57,7 +59,8 @@ const MySharing: React.FC = () => {
});
};
const handleCancelOne = (item: MySharedOutItem) => {
const handleCancelOne = (item: MySharedOutItem, e: MouseEvent) => {
e.stopPropagation()
modal.confirm({
title: t('application.confirmAppCancelShareDesc', { app: item.source_app_name, workspace: item.target_workspace_name }),
okText: t('common.confirm'),
@@ -71,87 +74,94 @@ const MySharing: React.FC = () => {
}
});
};
/** Navigate to application configuration page */
const handleEdit = (item: MySharedOutItem) => {
let url = `/#/application/config/${item.source_app_id}`
window.open(url);
}
return (
<Flex vertical gap={12}>
{grouped.map(({ workspace, items }) => (
<Collapse
key={workspace.target_workspace_id}
defaultActiveKey={[workspace.target_workspace_id]}
items={[{
key: workspace.target_workspace_id,
label: (
<Flex align="center" gap={12}>
{workspace.target_workspace_icon
? <img src={workspace.target_workspace_icon} className="rb:w-8 rb:h-8 rb:rounded-lg rb:object-cover" />
: <div className="rb:w-8 rb:h-8 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{workspace.target_workspace_name[0]}
</div>
}
<div>
<span className="rb:font-medium">{workspace.target_workspace_name}</span>
<div className="rb:text-[#5B6167] rb:text-[12px]">{t('application.appCount', { count: items.length })}</div>
</div>
</Flex>
),
extra: (
<Button
size="small"
onClick={e => { e.stopPropagation(); handleAllCancel(workspace); }}
>
{t('application.allCancel')}
</Button>
),
children: (
<Row gutter={[12, 12]}>
{items.map(item => (
<Col key={item.id} span={6} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-3! rb:px-4! rb:relative">
<div
className="rb:absolute rb:top-3 rb:right-3 rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/close.svg')]"
onClick={() => handleCancelOne(item)}
/>
<Flex gap={8} align="center">
<div className="rb:size-7 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{item.source_app_name[0]}
<Flex vertical gap={12} className="rb:h-[calc(100vh-148px)]! rb:overflow-y-auto!">
<BodyWrapper loading={false} empty={data.length === 0}>
{grouped.map(({ workspace, items }) => (
<Collapse
key={workspace.target_workspace_id}
defaultActiveKey={[workspace.target_workspace_id]}
items={[{
key: workspace.target_workspace_id,
label: (
<Flex align="center" gap={12}>
{workspace.target_workspace_icon
? <img src={workspace.target_workspace_icon} className="rb:w-8 rb:h-8 rb:rounded-lg rb:object-cover" />
: <div className="rb:w-8 rb:h-8 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{workspace.target_workspace_name[0]}
</div>
<div className="rb:font-medium">{item.source_app_name}</div>
</Flex>
<Flex vertical gap={4} className="rb:mt-3! rb:text-[12px]!">
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.type')}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': item.source_app_type === 'agent',
'rb:text-[#369F21] rb:font-medium': item.source_app_type === 'multi_agent',
})}>
{t(`application.${item.source_app_type}`)}
</span>
}
<div>
<span className="rb:font-medium">{workspace.target_workspace_name}</span>
<div className="rb:text-[#5B6167] rb:text-[12px]">{t('application.appCount', { count: items.length })}</div>
</div>
</Flex>
),
extra: (
<Button
size="small"
onClick={e => { e.stopPropagation(); handleAllCancel(workspace); }}
>
{t('application.allCancel')}
</Button>
),
children: (
<Row gutter={[12, 12]}>
{items.map(item => (
<Col key={item.id} span={6} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-3! rb:px-4! rb:relative rb:cursor-pointer" onClick={() => handleEdit(item)}>
<div
className="rb:absolute rb:top-3 rb:right-3 rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')]"
onClick={(e) => handleCancelOne(item, e)}
/>
<Flex gap={8} align="center">
<div className="rb:size-7 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
{item.source_app_name[0]}
</div>
<div className="rb:font-medium">{item.source_app_name}</div>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.version')}</span>
<span>{item.source_app_version}</span>
<Flex vertical gap={4} className="rb:mt-3! rb:text-[12px]!">
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.type')}</span>
<span className={clsx({
'rb:text-[#155EEF] rb:font-medium': item.source_app_type === 'agent',
'rb:text-[#369F21] rb:font-medium': item.source_app_type === 'multi_agent',
})}>
{t(`application.${item.source_app_type}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.version')}</span>
<span>{item.source_app_version}</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.permission')}</span>
<span className={clsx({
'rb:text-[#369F21] rb:font-medium': item.permission === 'editable',
'rb:text-[#5B6167] rb:font-medium': item.permission === 'readonly',
})}>
{t(`application.${item.permission}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.souceStatus')}</span>
<span>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</span>
</Flex>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.permission')}</span>
<span className={clsx({
'rb:text-[#369F21] rb:font-medium': item.permission === 'editable',
'rb:text-[#5B6167] rb:font-medium': item.permission === 'readonly',
})}>
{t(`application.${item.permission}`)}
</span>
</Flex>
<Flex gap={5} justify="space-between">
<span className="rb:text-[#5B6167]">{t('application.souceStatus')}</span>
<span>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</span>
</Flex>
</Flex>
</Col>
))}
</Row>
),
}]}
/>
))}
</Flex>
</Col>
))}
</Row>
),
}]}
/>
))}
</BodyWrapper>
</Flex>
);
};

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:12
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 09:56:02
* @Last Modified time: 2026-03-18 10:50:33
*/
/**
* Application Management Page
@@ -185,7 +185,7 @@ const ApplicationManagement: React.FC = () => {
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={{ ...query, shared_only: activeTab === 'sharing' }}
query={{ ...query, shared_only: activeTab === 'sharing', include_shared: activeTab !== 'apps' }}
renderItem={(item) => (
<RbCard
title={item.name}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:34:15
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-16 09:55:52
* @Last Modified time: 2026-03-18 10:50:27
*/
/**
* Type definitions for Application Management
@@ -16,6 +16,7 @@ export interface Query {
search: string;
type?: string;
shared_only?: boolean;
include_shared?: boolean;
}
/**

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:09:47
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 10:28:04
* @Last Modified time: 2026-03-18 15:50:31
*/
/**
* Upload File List Modal Component
@@ -115,7 +115,6 @@ const UploadFileListModal = forwardRef<UploadFileListModalRef, UploadFileListMod
<FormItem
{...restField}
name={[name, 'type']}
initialValue="image"
className="rb:mb-0!"
>
<Select

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:39:17
* @Last Modified time: 2026-03-18 15:35:05
*/
/**
* Conversation Page
@@ -60,6 +60,7 @@ const Conversation: FC = () => {
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
const [fileList, setFileList] = useState<any[]>([])
const [webSearch, setWebSearch] = useState(false)
const [isHasMemory, setIsHasMemory] = useState(false)
const [memory, setMemory] = useState(true)
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
@@ -85,9 +86,10 @@ const Conversation: FC = () => {
if (shareToken && token) {
getExperienceConfig(token)
.then(res => {
const response = res as { variables: Variable[]; features: FeaturesConfigForm }
const response = res as { variables: Variable[]; features: FeaturesConfigForm; app_type: string; memory?: boolean; }
toolbarRef.current?.setVariables(response.variables || [])
setFeatures(response.features)
setIsHasMemory((response.app_type === 'workflow' && response.memory) || (response.app_type !== 'workflow'))
})
} else {
setChatList([])
@@ -369,7 +371,7 @@ const Conversation: FC = () => {
}}
extra={
<>
{features.web_search?.enabled &&
{features?.web_search?.enabled &&
<ButtonCheckbox
icon={OnlineIcon}
checkedIcon={OnlineCheckedIcon}
@@ -379,14 +381,16 @@ const Conversation: FC = () => {
{t('memoryConversation.web_search')}
</ButtonCheckbox>
}
<ButtonCheckbox
icon={MemoryFunctionIcon}
checkedIcon={MemoryFunctionCheckedIcon}
checked={memory}
onChange={handleChangeMemory}
>
{t('memoryConversation.memory')}
</ButtonCheckbox>
{isHasMemory &&
<ButtonCheckbox
icon={MemoryFunctionIcon}
checkedIcon={MemoryFunctionCheckedIcon}
checked={memory}
onChange={handleChangeMemory}
>
{t('memoryConversation.memory')}
</ButtonCheckbox>
}
</>
}
/>

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:23
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 18:32:23
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 17:36:49
*/
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -90,7 +90,7 @@ const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text'
})
}
const handleDownload = () => {
const handleDownload = async () => {
if (!data.file_path) return
window.open(data.file_path, '_blank')
}

View File

@@ -10,10 +10,6 @@ interface CanvasToolbarProps {
isHandMode: boolean;
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
zoomLevel: number;
canUndo: boolean;
canRedo: boolean;
onUndo: () => void;
onRedo: () => void;
addNotes: () => void;
}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 15:05:21
* @Last Modified time: 2026-03-18 14:34:20
*/
/**
* Workflow Chat Component
@@ -359,7 +359,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
setStreamLoading(true)
draftRun(appId, data, handleStreamMessage)
.catch((error) => {
console.log('draftRun error', error)
const errorInfo = JSON.parse(error.message)
setChatList(prev => {
const newList = [...prev]
const lastIndex = newList.length - 1
@@ -368,7 +368,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
...newList[lastIndex],
status: 'failed',
content: null,
subContent: error.error
subContent: errorInfo.error
}
}
return newList

View File

@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-09 18:30:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-09 18:30:28
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 12:06:27
*/
import { useEffect, useState } from 'react';
import { Popover } from 'antd';
@@ -70,7 +70,6 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
// Get source port group information
const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort);
const sourcePortGroup = sourcePortInfo?.group || sourcePort;
console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo)
// If add-node position exists, use it; otherwise calculate new position
let newX, newY;
@@ -148,18 +147,23 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
if (sourcePortGroup === 'left') {
// Connect from left port to new node's right side
targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right';
graph.addEdge({
source: { cell: newNode.id, port: targetPort },
target: { cell: sourceNode.id, port: sourcePort },
...edgeAttrs
// zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0
});
} else {
// Connect from right port to new node's left side
targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
graph.addEdge({
source: { cell: sourceNode.id, port: sourcePort },
target: { cell: newNode.id, port: targetPort },
...edgeAttrs
// zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0
});
}
graph.addEdge({
source: { cell: sourceNode.id, port: sourcePort },
target: { cell: newNode.id, port: targetPort },
...edgeAttrs
// zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0
});
// Adjust loop node size when child node is added via port within loop node
const cycleId = sourceNodeData.cycle;
if (cycleId) {
@@ -223,20 +227,27 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop');
const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration');
const sourcePortInfo = sourceNode?.getPorts().find((p: any) => p.id === sourcePort);
const sourcePortGroup = sourcePortInfo?.group || sourcePort;
const isLeftPort = sourcePortGroup === 'left';
let filteredNodes;
if (isChildOfLoop) {
// Use same filtering as AddNode for child nodes of loop, but allow break
// Use same filtering as AddNode for child nodes of loop, but allow break
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
} else if (isChildOfIteration) {
// Filter out loop and iteration nodes for children of iteration nodes, but allow break
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
} else {
// Original filtering for non-loop child nodes
filteredNodes = category.nodes.filter(nodeType => !['start', 'break', 'cycle-start'].includes(nodeType.type));
filteredNodes = category.nodes.filter(nodeType =>
nodeType.type !== 'start' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
);
}
if (isLeftPort) {
filteredNodes = filteredNodes.filter(nodeType => nodeType.type !== 'end');
}
if (filteredNodes.length === 0) return null;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-17 10:00:10
* @Last Modified time: 2026-03-18 16:08:17
*/
import { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -12,10 +12,11 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '
import { register } from '@antv/x6-react-shape';
import type { PortMetadata } from '@antv/x6/lib/model/port';
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode, noteNode, notesConfig } from '../constant';
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode, notesConfig } from '../constant';
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
import { useUser } from '@/store/user';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
/**
* Props for useWorkflowGraph hook
@@ -25,6 +26,8 @@ export interface UseWorkflowGraphProps {
containerRef: React.RefObject<HTMLDivElement>;
/** Reference to the minimap container element */
miniMapRef: React.RefObject<HTMLDivElement>;
/** Callback when features config is loaded */
onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void;
}
/**
@@ -67,6 +70,7 @@ export interface UseWorkflowGraphReturn {
setChatVariables: React.Dispatch<React.SetStateAction<ChatVariable[]>>;
handleAddNotes: () => void;
handleSaveFeaturesConfig: (value: FeaturesConfigForm) => void;
}
/**
@@ -78,6 +82,7 @@ export interface UseWorkflowGraphReturn {
export const useWorkflowGraph = ({
containerRef,
miniMapRef,
onFeaturesLoad,
}: UseWorkflowGraphProps): UseWorkflowGraphReturn => {
// Hooks
const { id } = useParams();
@@ -115,6 +120,7 @@ export const useWorkflowGraph = ({
})
setChatVariables(initChatVariables)
setConfig({ ...rest, variables: initChatVariables })
onFeaturesLoad?.(rest.features)
})
}
@@ -132,7 +138,7 @@ export const useWorkflowGraph = ({
if (nodes.length) {
const nodeList = nodes.map(node => {
const { id, type, name, position, config = {} } = node
let nodeLibraryConfig = [...nodeLibrary, { nodes: [unknownNode, notesConfig] }]
let nodeLibraryConfig: NodeProperties | undefined = [...nodeLibrary, { nodes: [unknownNode, notesConfig] }]
.flatMap(category => category.nodes)
.find(n => n.type === type)
nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties
@@ -593,13 +599,6 @@ export const useWorkflowGraph = ({
if (!graphRef.current) return false;
const selectedNodes = graphRef.current.getNodes().filter(node => node.getData()?.isSelected);
if (selectedNodes.length) {
selectedNodes.forEach(node => {
const data = node.getData();
node.setData({
...data,
id: `${(data.type as string).replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
});
});
graphRef.current.copy(selectedNodes);
}
return false;
@@ -610,7 +609,14 @@ export const useWorkflowGraph = ({
*/
const parseEvent = () => {
if (!graphRef.current?.isClipboardEmpty()) {
graphRef.current?.paste({ offset: 32 });
const pastedNodes = graphRef.current?.paste({ offset: 32 }) ?? [];
pastedNodes.forEach(cell => {
if (cell.isNode()) {
const data = cell.getData();
const newId = `${(data.type as string).replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
cell.setData({ ...data, id: newId });
}
});
blankClick();
}
return false;
@@ -761,8 +767,23 @@ export const useWorkflowGraph = ({
createEdge() {
return graphRef.current?.createEdge(edgeAttrs);
},
validateConnection({ sourceCell, targetCell, targetMagnet }) {
validateConnection({ sourceCell, targetCell, sourceMagnet, targetMagnet }) {
if (!targetMagnet) return false;
// Only allow right port → left port connections
const getPortGroup = (magnet: Element) => {
let el: Element | null = magnet;
while (el) {
const group = el.getAttribute('port-group');
if (group) return group;
el = el.parentElement;
}
return null;
};
const sourceGroup = sourceMagnet ? getPortGroup(sourceMagnet) : null;
const targetGroup = targetMagnet ? getPortGroup(targetMagnet) : null;
if (sourceGroup === 'left' || targetGroup === 'right') return false;
// Node cannot connect to itself
if (sourceCell?.id === targetCell?.id) return false;
@@ -979,6 +1000,9 @@ export const useWorkflowGraph = ({
}) || [];
const edges = graphRef.current?.getEdges() || []
console.log('config', config)
const params = {
...config,
variables: chatVariables.map(v => {
@@ -1172,6 +1196,9 @@ export const useWorkflowGraph = ({
data: { ...cleanNodeData },
});
}
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
setConfig(prev => prev ? { ...prev, features: value } as WorkflowConfig : prev)
}
return {
config,
@@ -1191,6 +1218,7 @@ export const useWorkflowGraph = ({
handleSave,
chatVariables,
setChatVariables,
handleAddNotes
handleAddNotes,
handleSaveFeaturesConfig
};
};

View File

@@ -6,13 +6,13 @@ import Properties from './components/Properties';
import CanvasToolbar from './components/CanvasToolbar';
import PortClickHandler from './components/PortClickHandler';
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
import type { WorkflowRef, FeaturesConfigForm } from '@/views/ApplicationConfig/types'
import Chat from './components/Chat/Chat';
import type { ChatRef, AddChatVariableRef } from './types'
import arrowIcon from '@/assets/images/workflow/arrow.png'
import AddChatVariable from './components/AddChatVariable';
const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const miniMapRef = useRef<HTMLDivElement>(null);
const addChatVariableRef = useRef<AddChatVariableRef>(null)
@@ -25,12 +25,8 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
selectedNode,
setSelectedNode,
zoomLevel,
canUndo,
canRedo,
isHandMode,
setIsHandMode,
onUndo,
onRedo,
onDrop,
blankClick,
deleteEvent,
@@ -39,8 +35,9 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
handleSave,
chatVariables,
setChatVariables,
handleAddNotes
} = useWorkflowGraph({ containerRef, miniMapRef });
handleAddNotes,
handleSaveFeaturesConfig
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
const onDragOver = (event: React.DragEvent) => {
event.preventDefault();
@@ -61,7 +58,8 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
graphRef,
addVariable,
config,
features: config?.features
features: config?.features,
handleSaveFeaturesConfig
}))
return (
<div className="rb:h-[calc(100vh-64px)] rb:relative">
@@ -93,10 +91,6 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
isHandMode={isHandMode}
setIsHandMode={setIsHandMode}
zoomLevel={zoomLevel}
canUndo={canUndo}
canRedo={canRedo}
onUndo={onUndo}
onRedo={onRedo}
addNotes={handleAddNotes}
/>
</div>