diff --git a/web/src/views/ModelManagement/index.tsx b/web/src/views/ModelManagement/index.tsx
index 461f7cd5..0dcef68f 100644
--- a/web/src/views/ModelManagement/index.tsx
+++ b/web/src/views/ModelManagement/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:05
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 12:28:07
+ * @Last Modified time: 2026-03-26 15:51:08
*/
/**
* Model Management Main Page
@@ -84,8 +84,8 @@ const tabKeys = ['group', 'list', 'square']
}
return (
-
-
+ <>
+
-
+
{activeTab === 'group' &&
}
{activeTab === 'list' &&
customModelModalRef.current?.handleClose() } />}
{activeTab === 'square' && }
@@ -145,7 +145,7 @@ const tabKeys = ['group', 'list', 'square']
ref={customModelModalRef}
refresh={handleRefresh}
/>
-
+ >
)
}
diff --git a/web/src/views/ModelManagement/utils.ts b/web/src/views/ModelManagement/utils.ts
index 23e1e4f3..8180e194 100644
--- a/web/src/views/ModelManagement/utils.ts
+++ b/web/src/views/ModelManagement/utils.ts
@@ -2,18 +2,18 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:50:22
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 14:03:13
+ * @Last Modified time: 2026-03-25 18:35:13
*/
/**
* Utility functions for Model Management
*/
-import bedrockIcon from '@/assets/images/model/bedrock.svg'
+import bedrockIcon from '@/assets/images/model/bedrock.png'
import dashscopeIcon from '@/assets/images/model/dashscope.png'
import gpustackIcon from '@/assets/images/model/gpustack.png'
-import ollamaIcon from '@/assets/images/model/ollama.svg'
-import openaiIcon from '@/assets/images/model/openai.svg'
-import xinferenceIcon from '@/assets/images/model/xinference.svg'
+import ollamaIcon from '@/assets/images/model/ollama.png'
+import openaiIcon from '@/assets/images/model/openai.png'
+import xinferenceIcon from '@/assets/images/model/xinference.png'
import volcanoIcon from '@/assets/images/model/volcano.png'
/**
diff --git a/web/src/views/Ontology/index.tsx b/web/src/views/Ontology/index.tsx
index cd599b97..7b98efea 100644
--- a/web/src/views/Ontology/index.tsx
+++ b/web/src/views/Ontology/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:15
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 13:38:59
+ * @Last Modified time: 2026-03-27 15:03:09
*/
import { type FC, useState, useRef } from 'react';
import type { MenuInfo } from 'rc-menu/lib/interface';
@@ -150,13 +150,13 @@ const Ontology: FC = () => {
items: [
{
key: 'edit',
- icon: ,
+ icon: ,
label: t('common.edit'),
onClick: (e: MenuInfo) => handleEdit(item, e),
},
{
key: 'delete',
- icon: ,
+ icon: ,
label: t('common.delete'),
onClick: (e: MenuInfo) => handleDelete(item, e),
},
@@ -164,7 +164,7 @@ const Ontology: FC = () => {
}}
placement="bottomRight"
>
- e.stopPropagation()} className="rb:cursor-pointer rb:size-6 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]">
+ e.stopPropagation()} className="rb:cursor-pointer rb:size-5.5 rb:bg-[url('@/assets/images/common/more.svg')] rb:hover:bg-[url('@/assets/images/common/more_hover.svg')]">
}
diff --git a/web/src/views/Ontology/pages/Detail.tsx b/web/src/views/Ontology/pages/Detail.tsx
index fdf33105..cf26867e 100644
--- a/web/src/views/Ontology/pages/Detail.tsx
+++ b/web/src/views/Ontology/pages/Detail.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 14:10:20
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 12:16:23
+ * @Last Modified time: 2026-03-26 18:55:37
*/
import { type FC, useEffect, useState, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom';
@@ -100,58 +100,60 @@ const Detail: FC = () => {
return (
<>
-
- {data.scene_name}
- {data.is_system_default ? {t('common.default')} : undefined}
-
-
-
- }
- extra={
- {data.is_system_default ? undefined : (
-
-
- )}
- navigate(-1)}>
-
- {t('common.return')}
-
- }
- />
+
+
+ {data.scene_name}
+ {data.is_system_default ? {t('common.default')} : undefined}
+
+
+
+ }
+ extra={
+ {data.is_system_default ? undefined : (
+
+
+ )}
+ navigate(-1)}>
+
+ {t('common.return')}
+
+ }
+ />
-
-
-
- setQuery({ class_name: value })}
- className="rb:w-full!"
- />
-
-
-
-
- {data.items?.map(item => (
-
- handleDelete(item)}
- >
)}
- >
-
- {item.class_description}
-
-
-
- ))}
+
+
+
+ setQuery({ class_name: value })}
+ className="rb:w-full!"
+ />
+
-
-
+
+
+ {data.items?.map(item => (
+
+ handleDelete(item)}
+ >
)}
+ >
+
+ {item.class_description}
+
+
+
+ ))}
+
+
+
+
= ({
return (
{title}
-
{desc}
+
{desc}
)
}
diff --git a/web/src/views/Prompt/index.tsx b/web/src/views/Prompt/index.tsx
index 98718b12..095a8daa 100644
--- a/web/src/views/Prompt/index.tsx
+++ b/web/src/views/Prompt/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:44:15
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-20 13:52:09
+ * @Last Modified time: 2026-03-27 15:14:58
*/
/**
* Prompt Editor Component
@@ -188,12 +188,12 @@ const Prompt: FC = () => {
}
data={chatList || []}
@@ -249,7 +249,7 @@ const Prompt: FC = () => {
{
{values?.current_prompt
? form.setFieldValue('current_prompt', value)}
/>
- :
+ :
}
diff --git a/web/src/views/Prompt/pages/History.tsx b/web/src/views/Prompt/pages/History.tsx
index 7953af0f..573b4a90 100644
--- a/web/src/views/Prompt/pages/History.tsx
+++ b/web/src/views/Prompt/pages/History.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:44:04
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-09 12:18:09
+ * @Last Modified time: 2026-03-27 15:52:44
*/
/**
* Prompt History Component
@@ -12,8 +12,7 @@
import React, { useRef, type MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
-import { Tooltip, App, Flex, Form, Dropdown } from 'antd';
-import { DashOutlined } from '@ant-design/icons';
+import { Space, App, Flex, Form } from 'antd';
import type { HistoryQuery, HistoryItem, PromptDetailRef } from '../types';
import RbCard from '@/components/RbCard/Card'
@@ -104,28 +103,33 @@ const History: React.FC = () => {
renderItem={(item) => (
{item.title}}
- headerClassName='rb:h-[38px]! rb:pt-3!'
+ title={item.title}
+ headerClassName='rb:min-h-[46px]!'
headerType="borderless"
+ bodyClassName="rb:px-3! rb:py-0!"
>
- handleClick(key, item)
- }}
- >
-
-
- {formatDateTime(item.created_at, 'YYYY/MM/DD HH:mm')}
-
+
+
+
+ {formatDateTime(item.created_at, 'YYYY/MM/DD HH:mm')}
+
+
+ handleClick('detail', item)}
+ >
+ handleClick('edit', item)}
+ >
+ handleClick('delete', item)}
+ >
+
+
)}
+ heightClass="rb:h-[calc(100vh-126px)]!"
/>
{
}
return (
-
-
+
+
@@ -179,7 +179,7 @@ const SelfReflectionEngine: React.FC = () => {
}
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
- className="rb:h-[calc(100vh-76px)]!"
+ className="rb:h-full!"
bodyClassName="rb:h-[calc(100%-54px)] rb:overflow-y-auto! rb:p-4! rb:pt-0!"
>
-
-
+
+
@@ -346,7 +346,7 @@ const SelfReflectionEngine: React.FC = () => {
)}
>}
-
+
);
diff --git a/web/src/views/SpaceConfig/index.tsx b/web/src/views/SpaceConfig/index.tsx
index 86fef42a..f2cbe05c 100644
--- a/web/src/views/SpaceConfig/index.tsx
+++ b/web/src/views/SpaceConfig/index.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-02-03 17:48:03
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-24 17:01:59
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-30 11:36:24
*/
/**
* Space Configuration Page
@@ -15,9 +15,7 @@ import { useTranslation } from 'react-i18next';
import type { SpaceConfigData } from './types'
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
-import { getModelListUrl } from '@/api/models'
-import CustomSelect from '@/components/CustomSelect'
-import RbAlert from '@/components/RbAlert';
+import ModelSelect from '@/components/ModelSelect';
const SpaceConfig: FC = () => {
const { t } = useTranslation();
@@ -63,7 +61,9 @@ const SpaceConfig: FC = () => {
}
return (
-
+
+
{t('menu.spaceConfig')}
+
{t('space.configAlert')}
{pageLoading
?
:
-
-
-
-
{t('space.configAlert')}
-
-
-
-
+
}
diff --git a/web/src/views/SpaceManagement/index.tsx b/web/src/views/SpaceManagement/index.tsx
index 0df3c653..781ee903 100644
--- a/web/src/views/SpaceManagement/index.tsx
+++ b/web/src/views/SpaceManagement/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:48:59
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 15:33:38
+ * @Last Modified time: 2026-03-26 14:43:20
*/
/**
* Space Management Page
@@ -12,7 +12,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import { List, Button, Flex, Space as AntSpace, Tooltip } from 'antd';
+import { Button, Flex, Space as AntSpace, Tooltip, Row, Col } from 'antd';
import type { Space, SpaceModalRef } from './types';
import SpaceModal from './components/SpaceModal';
@@ -68,11 +68,12 @@ const SpaceManagement: React.FC = () => {
{t('space.createSpace')}
- (
-
+
+ {data.map(item => (
+
{
}
>
-
- )}
- className="rb:h-[calc(100vh-124px)] rb:overflow-y-auto rb:overflow-x-hidden"
- />
+
+ ))}
+
ReactNo
return (
<>
- (
-
+
+ {data.map((item) => (
+
@@ -88,14 +88,14 @@ const Custom = forwardRef ReactNo
items: [
{
key: 'edit',
- icon: ,
+ icon: ,
label: t('common.edit'),
onClick: () => handleEdit(item),
},
{
key: 'delete',
className: 'rb:text-[#FF5D34]!',
- icon: ,
+ icon: ,
label: t('common.delete'),
onClick: () => handleDeleteService(item),
},
@@ -103,13 +103,12 @@ const Custom = forwardRef ReactNo
}}
placement="bottomRight"
>
-
+
}
isNeedTooltip={false}
>
-
{item.tags?.length > 0
?
@@ -142,10 +141,9 @@ const Custom = forwardRef ReactNo
-
- )}
- className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden"
- />
+
+ ))}
+
{/* 添加服务弹窗组件 */}
diff --git a/web/src/views/ToolManagement/Inner.tsx b/web/src/views/ToolManagement/Inner.tsx
index 689ce43e..b88428b0 100644
--- a/web/src/views/ToolManagement/Inner.tsx
+++ b/web/src/views/ToolManagement/Inner.tsx
@@ -1,6 +1,5 @@
import React, { useState, useRef, useEffect, type ReactNode } from 'react';
import {
- List,
Flex,
Space,
Tooltip,
@@ -68,13 +67,14 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode; keyword?: s
}
return (
-
+ <>
- (
-
+
+ {data.map((item) => (
+
@@ -86,7 +86,7 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode; keyword?: s
handleEdit(item)}
/>
@@ -130,21 +130,20 @@ const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode; keyword?: s
>
: item.config_data.tool_class === 'JsonTool'
- ?
+ ?
{t('tool.jsonEg')}
{InnerConfigData[item.config_data.tool_class].eg}
- :
-
{t('configStatus')}
- {t(`tool.${item.status}_desc`)}
-
+ :
+
{t('configStatus')}
+ {t(`tool.${item.status}_desc`)}
+
}
-
- )}
- className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden"
- />
+
+ ))}
+
ReactNode; keyword?: s
ref={innerToolModalRef}
refreshTable={getData}
/>
-
+ >
);
};
diff --git a/web/src/views/ToolManagement/Market.tsx b/web/src/views/ToolManagement/Market.tsx
index 3e2ca456..2c2cb6a9 100644
--- a/web/src/views/ToolManagement/Market.tsx
+++ b/web/src/views/ToolManagement/Market.tsx
@@ -434,7 +434,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
footer={
{mcp.publisher && {mcp.publisher.startsWith('@') ? mcp.publisher : `@${mcp.publisher}`}}
{mcp.view_count &&
-
+
{mcp.view_count.toLocaleString()}
}
}
diff --git a/web/src/views/ToolManagement/Mcp.tsx b/web/src/views/ToolManagement/Mcp.tsx
index eb9b45ad..d24eee3b 100644
--- a/web/src/views/ToolManagement/Mcp.tsx
+++ b/web/src/views/ToolManagement/Mcp.tsx
@@ -1,11 +1,11 @@
import { useState, useRef, useEffect, forwardRef, useImperativeHandle, type ReactNode } from 'react';
import {
App,
- List,
Space,
Tooltip,
Dropdown,
Flex,
+ Row, Col,
} from 'antd';
import { useTranslation } from 'react-i18next';
@@ -84,11 +84,12 @@ const Mcp = forwardRef ReactNode; ke
return (
<>
- (
-
+
+ {data.map((item) => (
+
@@ -103,20 +104,20 @@ const Mcp = forwardRef ReactNode; ke
items: [
{
key: 'edit',
- icon: ,
+ icon: ,
label: t('common.edit'),
onClick: () => handleEdit(item),
},
{
key: 'link',
- icon: ,
+ icon: ,
label: t('tool.testLink'),
onClick: () => handleTestConnection(item),
},
{
key: 'delete',
className: 'rb:text-[#FF5D34]!',
- icon: ,
+ icon: ,
label: t('common.delete'),
onClick: () => handleDeleteService(item),
},
@@ -124,7 +125,7 @@ const Mcp = forwardRef ReactNode; ke
}}
placement="bottomRight"
>
-
+
}
@@ -137,12 +138,12 @@ const Mcp = forwardRef ReactNode; ke
{t('tool.last_health_check')}: {formatDateTime(item.config_data?.last_health_check)}
-
+
-
- )}
- className="rb:h-[calc(100vh-124px)] rb:overflow-y-auto rb:overflow-x-hidden"
- />
+
+ ))}
+
+
{/* 添加服务弹窗组件 */}
diff --git a/web/src/views/UserManagement/index.tsx b/web/src/views/UserManagement/index.tsx
index 4f7aea92..ed09994f 100644
--- a/web/src/views/UserManagement/index.tsx
+++ b/web/src/views/UserManagement/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:51:08
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 16:45:18
+ * @Last Modified time: 2026-03-26 14:53:41
*/
/**
* User Management Page
@@ -142,9 +142,9 @@ const UserManagement: React.FC = () => {
];
return (
-
+
- {t('user.userList')}
+ {t('user.userList')}
@@ -159,7 +159,7 @@ const UserManagement: React.FC = () => {
columns={columns}
rowKey="id"
isScroll={true}
- scrollY="calc(100vh - 256px)"
+ scrollY="calc(100vh - 248px)"
/>
(false);
- const [data, setData] = useState([]);
const [form] = Form.useForm()
- const search = Form.useWatch(['search'], form)
+ const keyword = Form.useWatch(['keyword'], form)
- /** Fetch user memory list */
- useEffect(() => {
- getData()
- }, []);
+ const scrollListRef = useRef(null)
- /** Get data from API */
- const getData = () => {
- setLoading(true)
- getUserMemoryList().then((res) => {
- setData(res as Data[] || [])
- })
- .finally(() => {
- setLoading(false)
- })
- }
/** Navigate to user memory detail */
const handleViewDetail = (id: string | number) => {
switch (storageType) {
@@ -64,25 +49,12 @@ export default function UserMemory() {
navigate(`/memory`)
}
- /** Filter data by search term */
- const filterData = useMemo(() => {
- if (search && search.trim() !== '') {
- return data.filter((item) => {
- const { end_user } = item as Data;
- const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
- return name?.includes(search)
- })
- }
-
- return data
- }, [search, data])
-
return (
- {loading ?
-
- : filterData.length > 0 ? (
-
- {filterData.map((item, index) => {
- const { end_user, memory_num, memory_config } = item as Data;
- const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
- return (
-
-
- {name[0]}
-
- {name || '-'}
- }
- headerType="border"
- headerClassName="rb:h-[48px]! rb:mx-4!"
- bodyClassName="rb:py-3! rb:px-4!"
- className="rb:cursor-pointer"
- onClick={() => handleViewDetail(end_user.id)}
- >
-
-
-
-
-
-
-
-
-
-
- {t('userMemory.memory_config_name')}
-
-
-
{memory_config?.memory_config_name || '-'}
-
-
+
+
+ ref={scrollListRef}
+ url={userMemoryListUrl}
+ query={{ keyword }}
+ column={3}
+ renderItem={(item) => {
+ const { end_user, memory_num, memory_config } = item as Data;
+ const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
+ return (
+
+ {name[0]}
+
+ {name || '-'}
+ }
+ headerType="border"
+ headerClassName="rb:h-[48px]! rb:mx-4!"
+ bodyClassName="rb:py-3! rb:px-4!"
+ className="rb:cursor-pointer"
+ onClick={() => handleViewDetail(end_user.id)}
+ >
+
+
+
- )
- })}
-
- ) :
- }
+
+
+
+
+
+
+
+ {t('userMemory.memory_config_name')}
+
+
+
{memory_config?.memory_config_name || '-'}
+
+
+ )
+ }}
+ />
);
}
\ No newline at end of file
diff --git a/web/src/views/UserMemoryDetail/Neo4j.tsx b/web/src/views/UserMemoryDetail/Neo4j.tsx
index 6dde8d69..51be7c8d 100644
--- a/web/src/views/UserMemoryDetail/Neo4j.tsx
+++ b/web/src/views/UserMemoryDetail/Neo4j.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 17:57:26
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-09 14:28:34
+ * @Last Modified time: 2026-03-26 18:59:53
*/
/**
* Neo4j User Memory Detail View
@@ -75,7 +75,7 @@ const Neo4j: FC = () => {
return (
setSelectedKey(null)}>
-
+
{
})
}
return (
-
+
{name?.[0]}
diff --git a/web/src/views/UserMemoryDetail/components/AboutMe.tsx b/web/src/views/UserMemoryDetail/components/AboutMe.tsx
index f38ef30a..72b9af61 100644
--- a/web/src/views/UserMemoryDetail/components/AboutMe.tsx
+++ b/web/src/views/UserMemoryDetail/components/AboutMe.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:34:23
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-11 15:03:05
+ * @Last Modified time: 2026-03-27 11:09:52
*/
/**
* About Me Component
@@ -67,12 +67,12 @@ const AboutMe = forwardRef(({ className },
title={t('userMemory.aboutMe')}
headerClassName="rb:min-h-[46px]!! rb:font-medium!"
className={clsx("rb:bg-[#FFFFFF]! rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.13)]! rb:absolute! rb:w-100 rb:top-29 rb:left-26", className)}
- bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-176px)] rb:overflow-y-auto!"
+ bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-186px)]! rb:overflow-y-auto!"
>
{loading
?
: Object.keys(data).filter(key => data[key] !== null).length > 0
- ? <>
+ ?
{data.user_summary &&
{data.user_summary}
@@ -95,7 +95,7 @@ const AboutMe = forwardRef
(({ className },
{data.one_sentence &&
{data.one_sentence}
}
- >
+
:
}
diff --git a/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx
index 759a4bd7..211d5968 100644
--- a/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx
+++ b/web/src/views/UserMemoryDetail/components/ActivationMetricsPieCard.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:34:16
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 11:36:02
+ * @Last Modified time: 2026-03-27 11:22:10
*/
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
@@ -32,7 +32,7 @@ const ActivationMetricsPieCard: FC
= ({ chartData
className="rb:h-full!"
>
{loading
- ?
+ ?
: = ({ src, fileName, fileSize }) => {
>
-
+
diff --git a/web/src/views/UserMemoryDetail/components/CommunityNetwork.tsx b/web/src/views/UserMemoryDetail/components/CommunityNetwork.tsx
index b8322c87..33da0b04 100644
--- a/web/src/views/UserMemoryDetail/components/CommunityNetwork.tsx
+++ b/web/src/views/UserMemoryDetail/components/CommunityNetwork.tsx
@@ -66,7 +66,7 @@ const CommunityNetwork: FC<{ onSelectCommunity?: (node: RawCommunityNode) => voi
if (loading) {
return
-
+
diff --git a/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx b/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx
index 641a8af6..f956eca4 100644
--- a/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx
+++ b/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:34:04
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-20 11:04:52
+ * @Last Modified time: 2026-03-27 10:28:53
*/
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
@@ -20,8 +20,10 @@ const ConversationMemory: FC = () => {
return (
url={getRagContentUrl}
diff --git a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
index 0533efc8..c689bf72 100644
--- a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
+++ b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:33:30
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-24 17:55:02
+ * @Last Modified time: 2026-03-27 11:11:09
*/
/**
* End User Profile Component
@@ -85,7 +85,7 @@ const EndUserProfile = forwardRef(({ cla
}
headerClassName="rb:min-h-[46px]!! rb:font-medium!"
className={clsx("rb:bg-[#FFFFFF]! rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.13)]! rb:absolute! rb:w-80 rb:top-29 rb:left-26", className)}
- bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-176px)] rb:overflow-auto"
+ bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-186px)] rb:overflow-auto"
>
{loading
?
diff --git a/web/src/views/UserMemoryDetail/components/Habits.tsx b/web/src/views/UserMemoryDetail/components/Habits.tsx
index f5ccab03..90b687ff 100644
--- a/web/src/views/UserMemoryDetail/components/Habits.tsx
+++ b/web/src/views/UserMemoryDetail/components/Habits.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:33:06
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 14:05:10
+ * @Last Modified time: 2026-03-27 11:18:47
*/
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
import { useTranslation } from 'react-i18next'
@@ -83,7 +83,7 @@ const Habits = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)] rb:overflow-y-auto!"
- className="rb:h-[calc(100vh-88px)]!"
+ className="rb:h-full!"
>
{loading
?
diff --git a/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx b/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx
index 8d5457cd..c10719de 100644
--- a/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx
+++ b/web/src/views/UserMemoryDetail/components/InterestDistribution.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:47
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-05 18:29:29
+ * @Last Modified time: 2026-03-27 11:11:35
*/
/**
* Interest Distribution Component
@@ -77,7 +77,7 @@ const InterestDistribution: FC<{ className?: string; }> = ({ className }) => {
title={t('userMemory.interestDistribution')}
headerClassName="rb:min-h-[46px]!! rb:font-medium!"
className={clsx("rb:bg-[#FFFFFF]! rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.13)]! rb:absolute! rb:w-100 rb:top-29 rb:left-26", className)}
- bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-176px)] rb:overflow-auto"
+ bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-186px)] rb:overflow-auto"
>
{loading
?
diff --git a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx
index 578c8823..4b7274b1 100644
--- a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx
+++ b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:41
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-02-05 18:35:01
+ * @Last Modified time: 2026-03-27 11:11:46
*/
/**
* Memory Insight Component
@@ -66,7 +66,7 @@ const MemoryInsight = forwardRef(({ c
title={t('userMemory.memoryInsight')}
headerClassName="rb:min-h-[46px]!! rb:font-medium!"
className={clsx("rb:bg-[#FFFFFF]! rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.13)]! rb:absolute! rb:w-100 rb:top-29 rb:left-26", className)}
- bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-176px)] rb:overflow-auto"
+ bodyClassName="rb:px-5! rb:pb-5! rb:pt-3.75! rb:max-h-[calc(100vh-186px)] rb:overflow-auto"
>
{loading
?
diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
index 3330f5bc..69f252e0 100644
--- a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
+++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:23
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 12:09:53
+ * @Last Modified time: 2026-03-27 14:57:34
*/
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -92,7 +92,7 @@ const PerceptualLastInfo: FC = () => {
setData(response)
setLoading(false)
if (response.file_path) {
- fetch(response.file_path, { method: 'HEAD' })
+ fetch(response.file_path, { method: 'GET' })
.then(r => {
const bytes = Number(r.headers.get('content-length'))
if (!bytes) return
@@ -119,7 +119,7 @@ const PerceptualLastInfo: FC = () => {
headerType="borderless"
headerClassName="rb:min-h-[50px]! rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-4! rb:pt-0! rb:h-[calc(100%-50px)] rb:overflow-y-auto"
- className="rb:h-[calc(100vh-88px)]! rb:w-full!"
+ className="rb:h-full! rb:w-full!"
>
{Object.keys(KEYS).map(key => (
diff --git a/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx b/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx
index d3170114..18de43a0 100644
--- a/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx
+++ b/web/src/views/UserMemoryDetail/components/RecentTrendsLineCard.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 18:32:07
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 11:49:29
+ * @Last Modified time: 2026-03-27 11:23:11
*/
import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@@ -72,7 +72,7 @@ const RecentTrendsLineCard: FC = ({ chartData, series
className="rb:h-full!"
>
{loading
- ?
+ ?
: !chartData || chartData.length === 0
?
: {
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:pl-5! rb:pt-0! rb:pr-3! rb:pb-4! rb:h-[calc(100%-54px)] rb:overflow-y-auto"
- className="rb:h-[calc(100vh-88px)]!"
+ className="rb:h-full!"
>
{loading
?
diff --git a/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
index ca6d456e..647cc104 100644
--- a/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-01-08 19:46:02
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 15:03:50
+ * @Last Modified time: 2026-03-27 11:20:40
*/
import { type FC, useEffect, useState } from 'react'
import clsx from 'clsx'
@@ -148,15 +148,15 @@ const EpisodicDetail: FC = () => {
}
return (
-
-
+
+
{t('episodicDetail.curResult')}
({data.total || 0}{t('episodicDetail.unix')})
}
headerType="borderless"
- className="rb:h-[calc(100vh-88px)]!"
+ className="rb:h-full!"
headerClassName="rb:min-h-[38px]! rb:pt-3! rb:mb-0!"
bodyClassName="rb:p-3! rb:pb-0! rb:h-[calc(100%-38px)]!"
>
@@ -231,11 +231,11 @@ const EpisodicDetail: FC = () => {
}
-
+
diff --git a/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx
index 4795ef45..185ed02d 100644
--- a/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/ExplicitDetail.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-01-10 17:35:17
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 15:05:06
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-27 11:19:38
*/
import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@@ -132,14 +132,14 @@ const ExplicitDetail: FC = () => {
return () => { chartInstance.current?.dispose(); chartInstance.current = null }
}, [data.semantic_memories])
return (
-
-
+
+
{loading ?
@@ -163,13 +163,13 @@ const ExplicitDetail: FC = () => {
}
-
+
{loading ?
diff --git a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx
index f8282687..19f49ff0 100644
--- a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx
@@ -113,13 +113,13 @@ const GraphDetail = forwardRef((_props, ref) => {
}
/>
-
+
@@ -127,13 +127,13 @@ const GraphDetail = forwardRef((_props, ref) => {
-
+
((_props, ref) => {
}))}
onChange={(key: string) => setActiveTab(key)}
/>
-
+
{timelineLoading
?
: !activeContent || activeContent.length === 0
diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
index 2448ff90..8dd71dc2 100644
--- a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-01-08 19:46:02
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 11:56:55
+ * @Last Modified time: 2026-03-27 11:18:50
*/
import { forwardRef, useImperativeHandle, useRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -83,41 +83,39 @@ const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }, { refresh: ()
}
return (
-
-
-
-
-
+
+
+
+
-
- {activeTab === 'preferences'
- ?
- :
- }
-
-
-
-
-
-
-
-
+
+ {activeTab === 'preferences'
+ ?
+ :
+ }
+
+
+
+
+
+
+
)
})
export default ImplicitDetail
\ No newline at end of file
diff --git a/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
index 8871d9c3..ce76c899 100644
--- a/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-01-08 19:46:02
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 15:09:12
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-27 11:13:19
*/
import { type FC } from 'react'
import { Row, Col } from 'antd'
@@ -22,11 +22,11 @@ import Timeline from '../components/Timeline'
const PerceptualDetail: FC = () => {
return (
-
-
+
+
-
+
diff --git a/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
index 8b38aa54..07a27720 100644
--- a/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2026-01-08 19:46:02
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 15:09:49
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-27 11:17:22
*/
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -87,8 +87,8 @@ const ShortTermDetail: FC = () => {
}
return (
-
-
+
+
{(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => (
@@ -115,10 +115,10 @@ const ShortTermDetail: FC = () => {
)}
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
- bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)] rb:overflow-y-auto!"
- className="rb:h-[calc(100vh-183px)]!"
+ bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)]"
+ className="rb:h-[calc(100%-94px)]!"
>
-
+
{loading
?
: !data.short_term || data.short_term.length === 0
@@ -189,7 +189,7 @@ const ShortTermDetail: FC = () => {
-
+
(
{t('shortTermDetail.longTermTitle')}
@@ -200,7 +200,7 @@ const ShortTermDetail: FC = () => {
headerType="borderless"
headerClassName="rb:min-h-[54px]! rb:font-[MiSans-Bold] rb:font-bold"
bodyClassName="rb:p-3! rb:pt-0! rb:h-[calc(100%-54px)] rb:overflow-y-auto!"
- className="rb:h-[calc(100vh-88px)]!"
+ className="rb:h-full!"
>
{loading
diff --git a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx
index d8feab1f..419cf3b1 100644
--- a/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/StatementDetail.tsx
@@ -1,8 +1,8 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-19 16:54:52
- * @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-16 15:06:29
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-27 11:35:37
*/
import { forwardRef, useImperativeHandle, useRef } from 'react'
import { Row, Col } from 'antd';
@@ -46,8 +46,8 @@ const StatementDetail = forwardRef<{ handleRefresh: () => void },{ refresh: () =
handleRefresh
}));
return (
-
-
+
+
@@ -60,7 +60,7 @@ const StatementDetail = forwardRef<{ handleRefresh: () => void },{ refresh: () =
-
+
diff --git a/web/src/views/UserMemoryDetail/pages/WorkingDetail.tsx b/web/src/views/UserMemoryDetail/pages/WorkingDetail.tsx
index bfe928be..2e288988 100644
--- a/web/src/views/UserMemoryDetail/pages/WorkingDetail.tsx
+++ b/web/src/views/UserMemoryDetail/pages/WorkingDetail.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-01-12 14:42:02
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 11:55:36
+ * @Last Modified time: 2026-03-27 11:15:05
*/
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import clsx from 'clsx'
@@ -155,14 +155,14 @@ const WorkingDetail: FC = () => {
: data.length === 0
?
:(
-
-
+
+
{
{selected && <>
-
+
{timeRange}
@@ -226,13 +226,13 @@ const WorkingDetail: FC = () => {
}
-
+
{detailLoading
?
diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx
index da2d9620..dad34fc9 100644
--- a/web/src/views/Workflow/components/Chat/Chat.tsx
+++ b/web/src/views/Workflow/components/Chat/Chat.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 13:57:40
+ * @Last Modified time: 2026-03-27 17:30:47
*/
/**
* Workflow Chat Component
@@ -41,7 +41,9 @@ import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
import Runtime from './Runtime';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
-const Chat = forwardRef(({ appId, graphRef, data }, ref) => {
+const Chat = forwardRef(({
+ appId, graphRef, features
+}, ref) => {
const { t } = useTranslation()
const { message: messageApi } = App.useApp()
const toolbarRef = useRef(null)
@@ -58,7 +60,6 @@ const Chat = forwardRef(null)
const [fileList, setFileList] = useState([])
const [message, setMessage] = useState(undefined)
- const [features, setFeatures] = useState({} as FeaturesConfigForm)
/**
* Opens the chat drawer and loads workflow variables from the start node
@@ -67,10 +68,6 @@ const Chat = forwardRef {
- if (data?.features && open) setFeatures(data.features)
- }, [open, data?.features])
-
useEffect(() => {
if (open && toolbarReady) {
getVariables()
@@ -434,7 +431,7 @@ const Chat = forwardRef
diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx
index b825650a..a7b06fd1 100644
--- a/web/src/views/Workflow/components/NodeLibrary.tsx
+++ b/web/src/views/Workflow/components/NodeLibrary.tsx
@@ -10,7 +10,7 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
const { t } = useTranslation()
return (
-
@@ -27,9 +27,9 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
'rb:min-h-[52px]!': collapsed
})}
className="rb:h-full! rb:hover:shadow-none!"
- bodyClassName={clsx('rb:overflow-y-auto! rb:h-[calc(100vh-126px)]! rb:pt-0! rb:pb-3!', {
- 'rb:px-0!': collapsed,
- 'rb:px-3!': !collapsed
+ bodyClassName={clsx('rb:overflow-y-auto! rb:pt-0! rb:pb-3!', {
+ 'rb:px-0! rb:h-[calc(100%-52px)]!': collapsed,
+ 'rb:px-3! rb:h-[calc(100%-42px)]!': !collapsed
})}
>
@@ -70,7 +70,7 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
key={nodeIndex}
align="center"
gap={8}
- className="rb:rounded-xl rb:p-2! rb-border rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
+ className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('application/reactflow', node.type);
@@ -87,8 +87,6 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
}
-
-
);
};
diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx
index 9b9d2236..dd0ab23d 100644
--- a/web/src/views/Workflow/components/Nodes/AddNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-09 18:31:30
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-06 11:43:58
+ * @Last Modified time: 2026-03-30 11:55:10
*/
import { useState } from 'react';
import { Popover, Flex } from 'antd';
@@ -173,7 +173,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
align="center"
justify="center"
gap={4}
- className={clsx('rb:text-[#212332] rb:font-medium rb:text-[12px] rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:border rb:rounded-lg rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:border-[#DFE4ED] rb:flex rb:items-center rb:justify-center', {
+ className={clsx('rb:text-[#212332] rb:font-medium rb:text-[12px] rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:border rb:rounded-lg rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:border-[#FCFCFD] rb:flex rb:items-center rb:justify-center', {
'rb:border-orange-500 rb:border-[3px] rb:bg-[#FCFCFD] rb:text-[#475467]': data.isSelected,
'rb:border-[#d1d5db] rb:bg-[#FCFCFD] rb:text-[#374151]': !data.isSelected
})}
diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx
index 12ae6ca0..516b5125 100644
--- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx
@@ -48,7 +48,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
return (
diff --git a/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx
index 0f963adc..4a29531f 100644
--- a/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx
@@ -3,7 +3,7 @@ import type { ReactShapeConfig } from '@antv/x6-react-shape';
const GroupStartNode: ReactShapeConfig['component'] = () => {
return (
-
+
);
diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
index b8c2ea0c..29c683cc 100644
--- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
@@ -122,7 +122,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
return (
diff --git a/web/src/views/Workflow/components/Nodes/NormalNode.tsx b/web/src/views/Workflow/components/Nodes/NormalNode.tsx
index 340e95dc..12e89cca 100644
--- a/web/src/views/Workflow/components/Nodes/NormalNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/NormalNode.tsx
@@ -12,7 +12,7 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
return (
diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx
index 2cc0c3c5..13ad6b98 100644
--- a/web/src/views/Workflow/components/PortClickHandler.tsx
+++ b/web/src/views/Workflow/components/PortClickHandler.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-09 18:30:28
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-24 11:11:56
+ * @Last Modified time: 2026-03-30 15:14:02
*/
import { useEffect, useState } from 'react';
import { Popover } from 'antd';
@@ -20,13 +20,15 @@ const PortClickHandler: React.FC = ({ graph }) => {
const [sourceNode, setSourceNode] = useState(null);
const [sourcePort, setSourcePort] = useState('');
const [tempElement, setTempElement] = useState(null);
+ const [edgeInsertion, setEdgeInsertion] = useState(null);
useEffect(() => {
const handlePortClick = (event: CustomEvent) => {
- const { node, port, element, rect } = event.detail;
+ const { node, port, element, rect, edgeInsertion } = event.detail;
setSourceNode(node);
setSourcePort(port);
setTempElement(element);
+ setEdgeInsertion(edgeInsertion || null);
setPopoverPosition({ x: rect.left, y: rect.top });
setPopoverVisible(true);
};
@@ -72,15 +74,47 @@ const PortClickHandler: React.FC = ({ graph }) => {
const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort);
const sourcePortGroup = sourcePortInfo?.group || sourcePort;
- // If add-node position exists, use it; otherwise calculate new position
+ // Calculate new node position
let newX, newY;
- if (addNodePosition) {
+ if (edgeInsertion) {
+ // Edge insertion: place new node on the same row as target, between source and target
+ const targetBBox = edgeInsertion.targetCell.getBBox();
+ const gap = targetBBox.x - (sourceBBox.x + sourceBBox.width);
+ const requiredSpace = nodeWidth + horizontalSpacing * 4;
+
+ // New node x: right after source + spacing
+ newX = sourceBBox.x + sourceBBox.width + horizontalSpacing;
+ // Same row as target node
+ newY = targetBBox.y + (targetBBox.height - nodeHeight) / 2;
+
+ // If not enough space, shift target and all downstream nodes to the right
+ if (gap < requiredSpace) {
+ const shiftX = requiredSpace - gap;
+ const visited = new Set();
+ const shiftDownstream = (cell: any) => {
+ const cellId = cell.id;
+ if (visited.has(cellId)) return;
+ visited.add(cellId);
+ const pos = cell.getPosition();
+ cell.setPosition(pos.x + shiftX, pos.y);
+ // Recursively shift nodes connected from right ports
+ graph.getConnectedEdges(cell, { outgoing: true }).forEach((e: any) => {
+ const tId = e.getTargetCellId();
+ if (tId && !visited.has(tId)) {
+ const tCell = graph.getCellById(tId);
+ if (tCell?.isNode()) shiftDownstream(tCell);
+ }
+ });
+ };
+ shiftDownstream(edgeInsertion.targetCell);
+ }
+ } else if (addNodePosition) {
newX = addNodePosition.x;
newY = addNodePosition.y;
} else {
// Determine node placement direction based on port position
if (sourcePortGroup === 'left') {
- // Left port: add node to the left
+ // Left port: add node to the left
newX = sourceBBox.x - nodeWidth*2 - horizontalSpacing;
newY = sourceBBox.y;
} else {
@@ -91,7 +125,7 @@ const PortClickHandler: React.FC = ({ graph }) => {
// Check if position overlaps with existing nodes (only consider connected nodes)
const checkOverlap = (x: number, y: number) => {
- // Get nodes connected to the source node
+ // Get nodes connected to the source node
const connectedNodes = new Set();
graph.getConnectedEdges(sourceNode).forEach((edge: any) => {
const sourceId = edge.getSourceCellId();
@@ -108,7 +142,7 @@ const PortClickHandler: React.FC = ({ graph }) => {
y + nodeHeight < bbox.y || y > bbox.y + bbox.height);
});
};
-
+
// If position is occupied, search downward for empty space
while (checkOverlap(newX, newY)) {
newY += nodeHeight + verticalSpacing;
@@ -140,28 +174,51 @@ const PortClickHandler: React.FC = ({ graph }) => {
}
}
+ // Edge insertion: remove old edge immediately before creating new edges
+ if (edgeInsertion) {
+ const { edge: oldEdge } = edgeInsertion;
+ if (oldEdge.id && graph.getCellById(oldEdge.id)) {
+ graph.removeCell(oldEdge.id);
+ } else {
+ graph.removeEdge(oldEdge);
+ }
+ }
+
// Create edge connection
setTimeout(() => {
- const targetPorts = newNode.getPorts();
- let targetPort;
-
- if (sourcePortGroup === 'left') {
+ const newPorts = newNode.getPorts();
+
+ if (edgeInsertion) {
+ // Edge insertion: create source→new and new→target edges
+ const { targetCell, targetPort: origTargetPort } = edgeInsertion;
+ const newLeftPort = newPorts.find((p: any) => p.group === 'left')?.id || 'left';
+ const newRightPort = newPorts.find((p: any) => p.group === 'right')?.id || 'right';
+ graph.addEdge({
+ source: { cell: sourceNode.id, port: sourcePort },
+ target: { cell: newNode.id, port: newLeftPort },
+ ...edgeAttrs
+ });
+ graph.addEdge({
+ source: { cell: newNode.id, port: newRightPort },
+ target: { cell: targetCell.id, port: origTargetPort },
+ ...edgeAttrs
+ });
+ setEdgeInsertion(null);
+ } else if (sourcePortGroup === 'left') {
// Connect from left port to new node's right side
- targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right';
+ const targetPort = newPorts.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';
+ const targetPort = newPorts.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
});
}
diff --git a/web/src/views/Workflow/components/Properties/VariableList/index.tsx b/web/src/views/Workflow/components/Properties/VariableList/index.tsx
index 30b96505..6ff545eb 100644
--- a/web/src/views/Workflow/components/Properties/VariableList/index.tsx
+++ b/web/src/views/Workflow/components/Properties/VariableList/index.tsx
@@ -95,7 +95,7 @@ const VariableList: FC = ({
{config.sys?.map((vo, index) =>
-
+
sys.{vo.name}
{vo.type}
diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx
index bb80ed33..66b59075 100644
--- a/web/src/views/Workflow/components/Properties/index.tsx
+++ b/web/src/views/Workflow/components/Properties/index.tsx
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-25 15:08:02
+ * @Last Modified time: 2026-03-27 11:30:44
*/
import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx'
@@ -431,7 +431,7 @@ const Properties: FC = ({
}
return (
-
+
@@ -452,7 +452,7 @@ const Properties: FC = ({
headerType="borderless"
headerClassName={clsx("rb:font-[MiSans-Bold] rb:font-bold rb:min-h-[48px]!")}
className="rb:h-full! rb:hover:shadow-none!"
- bodyClassName={clsx('rb:overflow-y-auto! rb:h-[calc(100vh-131px)]! rb:px-3! rb:pt-0! rb:pb-3!')}
+ bodyClassName={clsx('rb:overflow-y-auto! rb:h-[calc(100%-48px)]! rb:px-3! rb:pt-0! rb:pb-3!')}
>
diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts
index cadff647..92773191 100644
--- a/web/src/views/Workflow/constant.ts
+++ b/web/src/views/Workflow/constant.ts
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:06:18
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-24 11:11:46
+ * @Last Modified time: 2026-03-31 10:08:26
*/
import LoopNode from './components/Nodes/LoopNode';
import NormalNode from './components/Nodes/NormalNode';
@@ -33,6 +33,7 @@ import assignerIcon from '@/assets/images/workflow/assigner.svg'
import memoryReadIcon from '@/assets/images/workflow/memory-read.svg'
import memoryWriteIcon from '@/assets/images/workflow/memory-write.svg'
import unknownIcon from '@/assets/images/workflow/unknown.svg'
+import documentExtractorIcon from '@/assets/images/workflow/document-extractor.svg'
import { memoryConfigListUrl } from '@/api/memory'
import type { NodeLibrary } from './types'
@@ -473,8 +474,7 @@ export const nodeLibrary: NodeLibrary[] = [
},
}
},
- {
- type: "document-extractor", icon: codeExecutionIcon,
+ { type: "document-extractor", icon: documentExtractorIcon,
config: {
file_selector: {
type: 'variableList',
@@ -693,8 +693,59 @@ export const portArgs = { x: nodeWidth, y: portItemArgsY }
const defaultPortGroup = {
position: { name: 'absolute' },
- markup: portMarkup,
- attrs: portAttrs
+ markup: [
+ { tagName: 'rect', selector: 'body' },
+ { tagName: 'circle', selector: 'hoverBody' },
+ { tagName: 'text', selector: 'label' },
+ ],
+ attrs: {
+ body: {
+ width: 1,
+ height: 8,
+ x: -1,
+ magnet: true,
+ stroke: port_color,
+ strokeWidth: edge_width,
+ fill: port_color,
+ },
+ hoverBody: {
+ r: 6,
+ cy: 2,
+ magnet: true,
+ stroke: port_color,
+ strokeWidth: edge_width,
+ fill: port_color,
+ opacity: 0,
+ },
+ label: {
+ text: '+',
+ fontSize: 12,
+ fontWeight: 'bold',
+ fill: '#FFFFFF',
+ textAnchor: 'middle',
+ textVerticalAnchor: 'middle',
+ pointerEvents: 'none',
+ y: '0.15em',
+ opacity: 0,
+ },
+ },
+}
+
+const leftPortGroup = {
+ position: { name: 'absolute' },
+ markup: [{ tagName: 'rect', selector: 'body' }],
+ attrs: {
+ body: {
+ width: 1,
+ height: 8,
+ x: -1,
+ y: -4,
+ magnet: true,
+ stroke: port_color,
+ strokeWidth: edge_width,
+ fill: port_color,
+ },
+ },
}
/**
@@ -703,7 +754,7 @@ const defaultPortGroup = {
*/
export const defaultAbsolutePortGroups = {
right: defaultPortGroup,
- left: defaultPortGroup,
+ left: leftPortGroup,
}
/**
* Default port items for standard nodes
@@ -797,7 +848,7 @@ export const graphNodeLibrary: Record = {
height: 28,
shape: 'add-node',
ports: {
- groups: { left: defaultPortGroup },
+ groups: { left: leftPortGroup },
items: [{ group: 'left', args: { x: 0, y: 18 }}],
},
},
@@ -824,7 +875,7 @@ export const graphNodeLibrary: Record = {
height: 28,
shape: 'add-node',
ports: {
- groups: { left: defaultPortGroup },
+ groups: { left: leftPortGroup },
items: [{ group: 'left', args: { x: 0, y: 14 } }],
},
},
@@ -833,7 +884,7 @@ export const graphNodeLibrary: Record = {
height: 76,
shape: 'normal-node',
ports: {
- groups: { left: defaultPortGroup },
+ groups: { left: leftPortGroup },
items: [defaultPortItems[0]],
},
},
@@ -877,11 +928,74 @@ export const edgeAttrs = {
line: {
stroke: edge_color,
strokeWidth: edge_width,
- targetMarker: {
- name: 'block',
- width: 4,
- height: 4,
+ targetMarker: null,
+ sourceMarker: null,
+ },
+ },
+}
+
+/**
+ * Edge hover tool: circular "+" button shown at midpoint on hover
+ */
+export const edgeHoverTool = {
+ name: 'button',
+ args: {
+ markup: [
+ {
+ tagName: 'circle',
+ selector: 'button',
+ attrs: {
+ r: 6,
+ stroke: port_color,
+ strokeWidth: edge_width,
+ fill: port_color,
+ cursor: 'pointer',
+ },
},
+ {
+ tagName: 'text',
+ textContent: '+',
+ selector: 'icon',
+ attrs: {
+ fontSize: 12,
+ fontWeight: 'bold',
+ fill: '#FFFFFF',
+ textAnchor: 'middle',
+ textVerticalAnchor: 'middle',
+ pointerEvents: 'none',
+ y: '0.3em',
+ },
+ },
+ ],
+ distance: 0.5,
+ offset: { x: 0, y: 0 },
+ onClick({ e, cell: edge }: any) {
+ e.stopPropagation();
+ const graph = edge.model?.graph;
+ if (!graph) return;
+ const sourceCell = graph.getCellById(edge.getSourceCellId());
+ const targetCell = graph.getCellById(edge.getTargetCellId());
+ const sourcePort = edge.getSourcePortId();
+ const targetPort = edge.getTargetPortId();
+ if (!sourceCell || !targetCell) return;
+ const rect = (e.target as HTMLElement).getBoundingClientRect();
+ const tempDiv = document.createElement('div');
+ tempDiv.style.position = 'fixed';
+ tempDiv.style.left = rect.left + 'px';
+ tempDiv.style.top = rect.top + 'px';
+ tempDiv.style.width = '1px';
+ tempDiv.style.height = '1px';
+ tempDiv.style.zIndex = '9999';
+ document.body.appendChild(tempDiv);
+ window.dispatchEvent(new CustomEvent('port:click', {
+ detail: {
+ node: sourceCell,
+ port: sourcePort,
+ element: tempDiv,
+ rect,
+ edgeInsertion: { edge, sourceCell, targetCell, sourcePort, targetPort }
+ }
+ }));
},
},
}
\ No newline at end of file
diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
index 197e4e4b..c427788b 100644
--- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts
+++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-24 15:01:52
+ * @Last Modified time: 2026-03-31 11:13:23
*/
import { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -12,7 +12,7 @@ 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, defaultPortItems, portItemArgsY, edge_width, conditionNodePortItemArgsY, conditionNodeItemHeight, conditionNodeHeight, notesConfig } from '../constant';
+import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edgeHoverTool, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode, defaultPortItems, portItemArgsY, edge_width, conditionNodePortItemArgsY, conditionNodeItemHeight, conditionNodeHeight, notesConfig } from '../constant';
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
import { useUser } from '@/store/user';
@@ -72,6 +72,7 @@ export interface UseWorkflowGraphReturn {
handleAddNotes: () => void;
handleSaveFeaturesConfig: (value: FeaturesConfigForm) => void;
+ features?: FeaturesConfigForm;
}
/**
@@ -100,6 +101,7 @@ export const useWorkflowGraph = ({
const [isHandMode, setIsHandMode] = useState(true);
const [config, setConfig] = useState(null);
const [chatVariables, setChatVariables] = useState([])
+ const featuresRef = useRef(undefined)
useEffect(() => {
getConfig()
@@ -121,6 +123,7 @@ export const useWorkflowGraph = ({
})
setChatVariables(initChatVariables)
setConfig({ ...rest, variables: initChatVariables })
+ featuresRef.current = rest.features
onFeaturesLoad?.(rest.features)
})
}
@@ -438,6 +441,7 @@ export const useWorkflowGraph = ({
setTimeout(() => {
if (graphRef.current) {
graphRef.current.centerContent()
+ graphRef.current.getNodes().forEach(node => node.toFront());
}
}, 200)
}
@@ -519,7 +523,9 @@ export const useWorkflowGraph = ({
* @param edge - Clicked edge
*/
const edgeClick = ({ edge }: { edge: Edge }) => {
+ clearEdgeSelect();
edge.setAttrByPath('line/stroke', edge_selected_color);
+ edge.setData({ ...edge.getData(), isSelected: true });
clearNodeSelect();
};
/**
@@ -544,6 +550,7 @@ export const useWorkflowGraph = ({
*/
const clearEdgeSelect = () => {
graphRef.current?.getEdges().forEach(e => {
+ e.setData({ ...e.getData(), isSelected: false, isNodeHover: false });
e.setAttrByPath('line/stroke', edge_color);
e.setAttrByPath('line/strokeWidth', edge_width);
});
@@ -716,6 +723,7 @@ export const useWorkflowGraph = ({
};
const nodePortClickEvent = ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => {
e.stopPropagation();
+ e.preventDefault();
const portElement = e.target as HTMLElement;
const rect = portElement.getBoundingClientRect();
@@ -835,15 +843,25 @@ export const useWorkflowGraph = ({
// 1. If both nodes have parent IDs, they must be same to connect
// 2. If both have no parent ID, can connect normally
// 3. If one has parent, one doesn't, cannot connect
- console.log('sourceParentId', sourceParentId, targetParentId)
if (sourceParentId && targetParentId) {
// Child nodes under same parent can connect to each other
- return sourceParentId === targetParentId;
+ if (sourceParentId !== targetParentId) return false;
} else if (sourceParentId || targetParentId) {
// One has parent, one doesn't, cannot connect
return false;
}
-
+
+ // Prevent duplicate connections between same ports
+ const sourcePortId = sourceMagnet?.getAttribute('port') ?? sourceMagnet?.closest('[port]')?.getAttribute('port');
+ const targetPortId = targetMagnet?.getAttribute('port') ?? targetMagnet?.closest('[port]')?.getAttribute('port');
+ const duplicate = graphRef.current?.getEdges().some(e =>
+ e.getSourceCellId() === sourceCell?.id &&
+ e.getTargetCellId() === targetCell?.id &&
+ e.getSourcePortId() === sourcePortId &&
+ e.getTargetPortId() === targetPortId
+ );
+ if (duplicate) return false;
+
return true;
},
},
@@ -878,12 +896,24 @@ export const useWorkflowGraph = ({
});
// Use plugins
setupPlugins();
- // Listen to edge mouseleave event
+ // Listen to edge mouseenter event: show hover style and add button
+ graphRef.current.on('edge:mouseenter', ({ edge }: { edge: Edge }) => {
+ setTimeout(() => {
+ edge.addTools([edgeHoverTool]);
+ }, 0)
+ });
+ // Listen to edge mouseleave event: revert style and remove add button
graphRef.current.on('edge:mouseleave', ({ edge }: { edge: Edge }) => {
- if (edge.getAttrByPath('line/stroke') !== edge_selected_color) {
- edge.setAttrByPath('line/stroke', edge_color);
- edge.setAttrByPath('line/strokeWidth', edge_width);
+ const data = edge.getData();
+ if (!data?.isSelected) {
+ if (data?.isNodeHover) {
+ edge.setAttrByPath('line/stroke', edge_selected_color);
+ } else {
+ edge.setAttrByPath('line/stroke', edge_color);
+ edge.setAttrByPath('line/strokeWidth', edge_width);
+ }
}
+ edge.removeTools();
});
// Listen to node selection event
graphRef.current.on('node:click', nodeClick);
@@ -891,13 +921,134 @@ export const useWorkflowGraph = ({
graphRef.current.on('edge:click', edgeClick);
// Listen to port click event
graphRef.current.on('node:port:click', nodePortClickEvent);
+ // Port hover: show circle style on right ports
+ graphRef.current.on('node:port:mouseenter', ({ node, port }) => {
+ console.log('node:port:mouseenter', port)
+ if (!port) return;
+ const portData = node.getPort(port);
+ if (portData?.group !== 'right') return;
+ node.toFront();
+ node.setPortProp(port, 'attrs/body/opacity', 0);
+ node.setPortProp(port, 'attrs/hoverBody/opacity', 1);
+ node.setPortProp(port, 'attrs/label/opacity', 1);
+ });
+ graphRef.current.on('node:port:mouseleave', ({ node, port }) => {
+ if (!port) return;
+ const portData = node.getPort(port);
+ if (portData?.group !== 'right') return;
+ node.setPortProp(port, 'attrs/body/opacity', 1);
+ node.setPortProp(port, 'attrs/hoverBody/opacity', 0);
+ node.setPortProp(port, 'attrs/label/opacity', 0);
+ });
// Listen to canvas click event, cancel selection
graphRef.current.on('blank:click', blankClick);
+ // Node hover: highlight connected edges
+ graphRef.current.on('node:mouseenter', ({ node }) => {
+ graphRef.current?.getEdges().forEach(edge => {
+ const view = graphRef.current?.findViewByCell(edge);
+ view?.removeTools();
+ if (!edge.getData()?.isSelected && edge.getAttrByPath('line/stroke') === edge_selected_color) {
+ edge.setAttrByPath('line/stroke', edge_color);
+ }
+ });
+ graphRef.current?.getConnectedEdges(node).forEach(edge => {
+ if (!edge.getData()?.isSelected) {
+ edge.setAttrByPath('line/stroke', edge_selected_color);
+ edge.setData({ ...edge.getData(), isNodeHover: true });
+ }
+ });
+ node.getPorts().filter(p => p.group === 'right').forEach(p => {
+ node.setPortProp(p.id!, 'attrs/body/opacity', 0);
+ node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 1);
+ node.setPortProp(p.id!, 'attrs/label/opacity', 1);
+ });
+ });
+ graphRef.current.on('node:mouseleave', ({ node }) => {
+ graphRef.current?.getConnectedEdges(node).forEach(edge => {
+ if (!edge.getData()?.isSelected) {
+ edge.setAttrByPath('line/stroke', edge_color);
+ edge.setData({ ...edge.getData(), isNodeHover: false });
+ }
+ });
+ node.getPorts().filter(p => p.group === 'right').forEach(p => {
+ node.setPortProp(p.id!, 'attrs/body/opacity', 1);
+ node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0);
+ node.setPortProp(p.id!, 'attrs/label/opacity', 0);
+ });
+ });
// Listen to zoom event
graphRef.current.on('scale', scaleEvent);
// Listen to node move event
graphRef.current.on('node:moved', nodeMoved);
graphRef.current.on('node:removed', blankClick)
+ // When edge connected, bring connected nodes' ports to front
+ graphRef.current.on('edge:connected', ({ isNew }) => {
+ graphRef.current?.getNodes().forEach(node => node.toFront());
+ // Reset any port hover state left from dragging
+ if (isNew) {
+ graphRef.current?.getNodes().forEach(node => {
+ node.getPorts().filter(p => p.group === 'right').forEach(p => {
+ node.setPortProp(p.id!, 'attrs/body/opacity', 1);
+ node.setPortProp(p.id!, 'attrs/hoverBody/opacity', 0);
+ node.setPortProp(p.id!, 'attrs/label/opacity', 0);
+ });
+ });
+ }
+ });
+
+ // During edge dragging, manually detect port hover since the dragging edge blocks mouse events
+ let lastHoveredPort: { node: Node; portId: string } | null = null;
+ graphRef.current.on('edge:mousemove', ({ e }: { e: MouseEvent }) => {
+ if (!graphRef.current) return;
+ const { clientX, clientY } = e;
+ let found: { node: Node; portId: string } | null = null;
+
+ for (const node of graphRef.current.getNodes()) {
+ for (const port of node.getPorts().filter(p => p.group === 'right')) {
+ const portView = graphRef.current.findViewByCell(node);
+ if (!portView) continue;
+ const portEl = (portView as any).findPortElem(port.id!, 'body') as SVGElement | null;
+ if (!portEl) continue;
+ const rect = portEl.getBoundingClientRect();
+ const hitRadius = 16;
+ const cx = rect.left + rect.width / 2;
+ const cy = rect.top + rect.height / 2;
+ if (Math.abs(clientX - cx) <= hitRadius && Math.abs(clientY - cy) <= hitRadius) {
+ found = { node, portId: port.id! };
+ break;
+ }
+ }
+ if (found) break;
+ }
+
+ if (found?.node.id !== lastHoveredPort?.node.id || found?.portId !== lastHoveredPort?.portId) {
+ // Leave previous
+ if (lastHoveredPort) {
+ const { node, portId } = lastHoveredPort;
+ node.setPortProp(portId, 'attrs/body/opacity', 1);
+ node.setPortProp(portId, 'attrs/hoverBody/opacity', 0);
+ node.setPortProp(portId, 'attrs/label/opacity', 0);
+ }
+ // Enter new
+ if (found) {
+ const { node, portId } = found;
+ node.toFront();
+ node.setPortProp(portId, 'attrs/body/opacity', 0);
+ node.setPortProp(portId, 'attrs/hoverBody/opacity', 1);
+ node.setPortProp(portId, 'attrs/label/opacity', 1);
+ }
+ lastHoveredPort = found;
+ }
+ });
+ graphRef.current.on('edge:mouseup', () => {
+ if (lastHoveredPort) {
+ const { node, portId } = lastHoveredPort;
+ node.setPortProp(portId, 'attrs/body/opacity', 1);
+ node.setPortProp(portId, 'attrs/hoverBody/opacity', 0);
+ node.setPortProp(portId, 'attrs/label/opacity', 0);
+ lastHoveredPort = null;
+ }
+ });
// Listen to copy keyboard event
graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent);
// Listen to paste keyboard event
@@ -1016,6 +1167,7 @@ export const useWorkflowGraph = ({
const params = {
...config,
+ features: featuresRef.current,
variables: chatVariables.map(v => {
const { defaultValue, ...cleanV } = v
return {
@@ -1173,7 +1325,7 @@ export const useWorkflowGraph = ({
saveWorkflowConfig(config.app_id, params as WorkflowConfig)
.then((res) => {
if (flag) {
- message.success(t('common.saveSuccess'))
+ message.success({ content: t('common.saveSuccess'), duration: 1 })
}
resolve(res)
}).catch(error => {
@@ -1208,7 +1360,8 @@ export const useWorkflowGraph = ({
});
}
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
- setConfig(prev => prev ? { ...prev, features: value } as WorkflowConfig : prev)
+ featuresRef.current = value
+ onFeaturesLoad?.(value)
}
return {
@@ -1230,6 +1383,7 @@ export const useWorkflowGraph = ({
chatVariables,
setChatVariables,
handleAddNotes,
- handleSaveFeaturesConfig
+ handleSaveFeaturesConfig,
+ features: featuresRef.current,
};
};
diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx
index d5d46a55..b698e857 100644
--- a/web/src/views/Workflow/index.tsx
+++ b/web/src/views/Workflow/index.tsx
@@ -34,7 +34,8 @@ const Workflow = forwardRef {
@@ -55,12 +56,13 @@ const Workflow = forwardRef
+
{/* 左侧节点面板 */}
@@ -99,6 +101,7 @@ const Workflow = forwardRef