Merge #108 into develop from develop_web
Merge #107 into develop_web from feature/20251219_zy * develop_web: (8 commits) feat(web): add question classifier node feat(web): memory insight feat(web): add loop node; add chat variable; feat(index): add homepage with dashboard cards and knowledge graph support feat(web): memory-read、memory-write、iteration、assigner、tool node Merge #107 into develop_web from feature/20251219_zy feat(dashboard): add statistics API and enhance homepage dashboard cards Merge #109 into develop_web from feature/20251219_yjp Signed-off-by: zhaoying <zhaoying@redbearai.com> Reviewed-by: zhuwenhui5566@163.com <zhuwenhui5566@163.com> Reviewed-by: vrhs@163.com <accounts_660b6454a0eb398d3f8d2c76@mail.teambition.com> Merged-by: zhuwenhui5566@163.com <zhuwenhui5566@163.com> CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/108
33
web/src/api/common.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { request } from "@/utils/request";
|
||||
// 列表查询参数
|
||||
export interface Query {
|
||||
page?: number;
|
||||
pagesize?: number;
|
||||
orderby?: string;
|
||||
desc?: boolean;
|
||||
keywords?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface DataResponse {
|
||||
total_models: Number;
|
||||
total_llm: Number;
|
||||
total_embedding: Number;
|
||||
model_week_growth_rate: Number;
|
||||
active_workspaces: Number;
|
||||
new_workspaces_this_week: Number;
|
||||
workspace_week_growth_rate: Number;
|
||||
total_users: Number;
|
||||
new_users_this_week: Number;
|
||||
user_week_growth_rate: Number;
|
||||
running_apps: Number;
|
||||
new_apps_this_week: Number;
|
||||
app_week_growth_rate: Number
|
||||
}
|
||||
// 首页数据统计
|
||||
export const getDashboardData = `/home-page/workspaces`
|
||||
|
||||
// 首页数据看板统计
|
||||
export const getDashboardStatistics = async () => {
|
||||
const response = await request.get(`/home-page/statistics`);
|
||||
return response as DataResponse;
|
||||
};
|
||||
@@ -285,3 +285,14 @@ export const getRetrievalModeType = async () => {
|
||||
const response = await request.get(`${apiPrefix}/chunks/retrieve_type`);
|
||||
return response as any;
|
||||
};
|
||||
|
||||
// 获取知识库图谱
|
||||
export const getKnowledgeGraph = async (kb_id: string) => {
|
||||
const response = await request.get(`${apiPrefix}/knowledges/${kb_id}/knowledge_graph`);
|
||||
return response;
|
||||
};
|
||||
// 获取知识库图谱实体类型
|
||||
export const getKnowledgeGraphEntityTypes = async (query: any) => {
|
||||
const response = await request.get(`${apiPrefix}/knowledges/knowledge_graph_entity_types`,query);
|
||||
return response ;
|
||||
};
|
||||
@@ -27,5 +27,10 @@ export const execute = (data: ExecuteData) => {
|
||||
}
|
||||
export const parseSchema = (data: Record<string, any>) => {
|
||||
return request.post(`/tools/parse_schema`, data)
|
||||
|
||||
}
|
||||
export const getToolDetail = (tool_id: string) => {
|
||||
return request.get(`/tools/${tool_id}`)
|
||||
}
|
||||
export const getToolMethods = (tool_id: string) => {
|
||||
return request.get(`/tools/${tool_id}/methods`)
|
||||
}
|
||||
18
web/src/assets/images/index/apps.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 34</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1056, -232)">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份-3" transform="translate(648, 0)">
|
||||
<g id="编组-34" transform="translate(152, 16)">
|
||||
<rect id="矩形" fill="#9C6FFF" x="0" y="0" width="32" height="32" rx="8"></rect>
|
||||
<g id="icon-GIS_运行管理" transform="translate(7, 8)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M7.16762406,14.1962904 C6.34809084,14.1646209 5.54326395,13.9741465 4.80020028,13.636004 C3.71131238,13.1260904 3.00897969,12.3104053 2.71316514,11.2234143 L2.87831314,11.2234143 C3.04376149,11.2238267 3.19695148,11.1385171 3.28050226,10.9994401 C3.36405304,10.8603631 3.3653563,10.6885064 3.28392388,10.5482427 L2.07888794,8.54040242 C1.99542259,8.40040876 1.84176478,8.31425552 1.67554571,8.31425552 C1.50932664,8.31425552 1.35566883,8.40040876 1.27220348,8.54040242 L0.0617231013,10.5500101 C-0.0210158141,10.6903056 -0.0205423886,10.8629501 0.0629647773,11.0028132 C0.146471943,11.1426763 0.300303569,11.2284717 0.466426438,11.227833 L0.804889093,11.227833 C1.23681463,13.5308399 2.77123916,14.6911806 3.98534917,15.2567694 C4.98325891,15.7122175 6.06611522,15.9645167 7.16762406,15.998225 C7.68009348,15.9996891 8.09703053,15.596793 8.09953973,15.0976976 C8.100628,14.8580057 8.00249628,14.627997 7.82730865,14.4596427 C7.65206093,14.2900046 7.4147074,14.1952508 7.16762406,14.1962904 L7.16762406,14.1962904 Z M17.9430772,5.00547242 C17.8606071,4.86230637 17.7051137,4.77374302 17.5365591,4.77393451 L17.1980964,4.77393451 C16.7661709,2.47092761 15.230839,1.31058684 14.0176364,0.742346841 C13.0197435,0.28685171 11.9368771,0.0345501233 10.8353615,0.000895273713 C10.4982732,-0.0093928287 10.18217,0.159876081 10.0105024,0.442595865 C9.83883478,0.725315649 9.83883478,1.076638 10.0105024,1.35935779 C10.18217,1.64207757 10.4982732,1.81134648 10.8353615,1.80105838 C11.6548676,1.83493183 12.4595539,2.02529996 13.2036927,2.36134478 C14.2925806,2.87214217 14.9949133,3.68694353 15.2907278,4.77393451 L15.1255798,4.77393451 C14.9574006,4.77106293 14.8007624,4.85694697 14.7160555,4.99847492 C14.6313487,5.14000288 14.6318043,5.3150681 14.7172468,5.456176 L15.9213754,7.46401624 C16.0048407,7.60400991 16.1584985,7.69016315 16.3247176,7.69016315 C16.4909367,7.69016315 16.6445945,7.60400991 16.7280598,7.46401624 L17.9303735,5.4588272 C18.018434,5.32105318 18.023291,5.14771891 17.9430772,5.00547242 L17.9430772,5.00547242 Z M16.1563937,7.99424995 L10.1375658,7.99424995 C9.50717772,7.99229725 8.99410285,8.48763753 8.99060389,9.10157256 L8.99060389,14.8926716 C8.99410285,15.5066067 9.50717772,16.0019469 10.1375658,15.9999942 L16.1563937,15.9999942 C16.7867818,16.0019469 17.2998566,15.5066067 17.3033556,14.8926716 L17.3033556,9.10157256 C17.2998566,8.48763753 16.7867818,7.99229725 16.1563937,7.99424995 L16.1563937,7.99424995 Z M15.4431721,9.79530257 L15.4431721,14.1962904 L10.854417,14.1962904 L10.854417,9.79530257 L15.4431721,9.79530257 Z M9.00058536,1.10732836 C8.99708639,0.493393328 8.48401152,-0.0019469461 7.85362344,5.75355807e-06 L1.83207335,5.75355807e-06 C1.20203901,-0.00145886726 0.689517223,0.493739212 0.686018831,1.10732836 L0.686018831,6.89842744 C0.686499539,7.19390032 0.807543503,7.47707233 1.02249671,7.68558868 C1.23744991,7.89410503 1.5286856,8.0108668 1.83207335,8.01018014 L7.85090122,8.01018014 C8.15491658,8.01180822 8.44708623,7.89546249 8.66282574,7.68684408 C8.87856526,7.47822567 9.00010748,7.1945153 9.00058536,6.89842744 L9.00058536,1.10732836 Z M7.13677223,1.80105838 L7.13677223,6.20558116 L2.54983195,6.20558116 L2.54983195,1.80105838 L7.13677223,1.80105838 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
17
web/src/assets/images/index/arrow_down.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>箭头_向上</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-928, -367)" fill="#FF5D34" fill-rule="nonzero">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份-3" transform="translate(648, 0)">
|
||||
<g id="标签1" transform="translate(16, 148)">
|
||||
<g id="箭头_向上" transform="translate(15, 10) scale(1, -1) translate(-15, -10)translate(8, 3)">
|
||||
<polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
18
web/src/assets/images/index/arrow_down_d.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 30</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="首页" transform="translate(-1006, -326)" stroke="#FF5D34" stroke-width="1.2">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份-3" transform="translate(648, 0)">
|
||||
<g id="编组-30" transform="translate(108, 116) scale(1, -1) translate(-108, -116)translate(102, 110)">
|
||||
<g id="编组-31" transform="translate(2.5, 2.5)">
|
||||
<polyline id="路径" points="0 3 3.5 0 7 3"></polyline>
|
||||
<polyline id="路径备份" points="0 7 3.5 4 7 7"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
16
web/src/assets/images/index/arrow_right.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 5</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1229, -446)" stroke="#212332">
|
||||
<g id="编组-13" transform="translate(1120, 300)">
|
||||
<g id="编组-6" transform="translate(16, 138)">
|
||||
<g id="编组-5" transform="translate(93, 8)">
|
||||
<polyline id="路径" points="10 6 12 8 10 10"></polyline>
|
||||
<line x1="12" y1="8" x2="2" y2="8" id="路径-2"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 820 B |
16
web/src/assets/images/index/arrow_right_blue.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 5</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1226, -244)" stroke="#155EEF">
|
||||
<g id="新手指引" transform="translate(1120, 80)">
|
||||
<g id="编组-6" transform="translate(16, 156)">
|
||||
<g id="编组-5" transform="translate(90, 8)">
|
||||
<polyline id="路径" points="10 6 12 8 10 10"></polyline>
|
||||
<line x1="12" y1="8" x2="2" y2="8" id="路径-2"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
17
web/src/assets/images/index/arrow_up.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>箭头_向上</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-712, -367)" fill="#369F21" fill-rule="nonzero">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份-2" transform="translate(432, 0)">
|
||||
<g id="标签1" transform="translate(16, 148)">
|
||||
<g id="箭头_向上" transform="translate(8, 3)">
|
||||
<polygon id="路径" points="7.58333789 4.21666211 7.58333789 12.4416621 6.41666211 12.4416621 6.41666211 4.21666211 4.60833789 6.025 3.79166211 5.20832422 7 2 10.2083379 5.20832422 9.39166211 6.025"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
web/src/assets/images/index/arrow_up_d.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 30</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="首页" transform="translate(-488, -326)" stroke="#369F21" stroke-width="1.2">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份" transform="translate(216, 0)">
|
||||
<g id="编组-30" transform="translate(16, 110)">
|
||||
<g id="编组-31" transform="translate(2.5, 2.5)">
|
||||
<polyline id="路径" points="0 3 3.5 0 7 3"></polyline>
|
||||
<polyline id="路径备份" points="0 7 3.5 4 7 7"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1013 B |
22
web/src/assets/images/index/data_export.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 23</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1252, -664)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-23" transform="translate(130, 124)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="编组-12" transform="translate(11.5, 11)" stroke="#212332" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7">
|
||||
<path d="M17,7.30064813 L17,14 C17,15.6568542 15.6568542,17 14,17 L3,17 C1.34314575,17 8.8817842e-16,15.6568542 8.8817842e-16,14 L-4.4408921e-16,5.42857143 C-4.4408921e-16,3.77171718 1.34314575,2.42857143 3,2.42857143 L12.7248521,2.42857143 L12.7248521,2.42857143" id="路径"></path>
|
||||
<polyline id="路径" points="10.9285714 0 13.3571429 2.42857143 10.9285714 4.85714286"></polyline>
|
||||
<line x1="4.86821714" y1="9.71428571" x2="4.86821714" y2="13.3571429" id="路径-13"></line>
|
||||
<line x1="12.1428571" y1="9.71428571" x2="12.1428571" y2="13.3571429" id="路径-13"></line>
|
||||
<line x1="8.51107429" y1="7.29747399" x2="8.51107429" y2="13.3689026" id="路径-13备份"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
web/src/assets/images/index/guide_bg@2x.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
21
web/src/assets/images/index/help_center.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 17</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1252, -772)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-17" transform="translate(130, 232)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="使用帮助" transform="translate(8, 8)" stroke="#212332" stroke-width="1.7">
|
||||
<g id="编组-16" transform="translate(2.7, 1.2)">
|
||||
<rect id="矩形" x="0" y="3.20000312" width="18.6000182" height="16.2000158" rx="3.5"></rect>
|
||||
<path d="M6.43068654,1.74093758 L9.44000922,3.20000313 L9.44000922,3.20000313 L9.44000922,19.2000188 L4.24931901,16.6833205 C3.38736738,16.2654045 2.84000277,15.3917071 2.84000277,14.4337852 L2.84000277,3.99047285 C2.84000277,2.60976098 3.9592909,1.49047285 5.34000277,1.49047285 C5.71781927,1.49047285 6.09072193,1.57610626 6.43068654,1.74093758 Z" id="矩形" fill="#FFFFFF" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
web/src/assets/images/index/index_bg@2x.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
22
web/src/assets/images/index/log_mgt.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 21</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1352, -664)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-21" transform="translate(230, 124)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="编组-15" transform="translate(12, 11)" stroke="#212332" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7">
|
||||
<path d="M6.98059242,17 L2,17 C0.8954305,17 -8.8817842e-16,16.1045695 -8.8817842e-16,15 L0,2 C0,0.8954305 0.8954305,2.22044605e-16 2,0 L14,0 C15.1045695,0 16,0.8954305 16,2 L16,6.06912749 L16,6.06912749" id="路径"></path>
|
||||
<line x1="3" y1="4.5" x2="12.8160264" y2="4.5" id="路径-17"></line>
|
||||
<line x1="3" y1="10.0001" x2="6.37326888" y2="10.0001" id="路径-18"></line>
|
||||
<circle id="椭圆形" cx="13" cy="13" r="4"></circle>
|
||||
<line x1="16" y1="16" x2="18" y2="18" id="路径-20"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
30
web/src/assets/images/index/model_mgt.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 25</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1152, -556)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-25" transform="translate(30, 16)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="模型管理" transform="translate(9, 9)">
|
||||
<rect id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" x="2.2" y="7.7" width="12.1" height="12.1"></rect>
|
||||
<polygon id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="7.7 2.2 19.8 2.2 14.3 7.7 2.2 7.7"></polygon>
|
||||
<polygon id="矩形" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="7.7 14.3 19.8 14.3 14.3 19.8 2.2 19.8"></polygon>
|
||||
<polygon id="矩形备份-9" stroke="#212332" stroke-width="1.7" stroke-linejoin="round" points="14.3 7.7 19.8 2.2 19.8 14.3 14.3 19.8"></polygon>
|
||||
<line x1="7.7" y1="2.2" x2="7.7" y2="14.3" id="路径-10" stroke="#212332" stroke-width="1.7"></line>
|
||||
<path d="M7.7,4.4 C8.91502645,4.4 9.9,3.41502645 9.9,2.2 C9.9,0.98497355 8.91502645,0 7.7,0 C6.48497355,0 5.5,0.98497355 5.5,2.2 C5.5,3.41502645 6.48497355,4.4 7.7,4.4 Z" id="椭圆形" fill="#212332"></path>
|
||||
<path d="M8.25899503,16.5 C9.47402148,16.5 10.458995,15.5150264 10.458995,14.3 C10.458995,13.0849736 9.47402148,12.1 8.25899503,12.1 C7.04396858,12.1 6.05899503,13.0849736 6.05899503,14.3 C6.05899503,15.5150264 7.04396858,16.5 8.25899503,16.5 Z" id="椭圆形" fill="#212332"></path>
|
||||
<path d="M2.2,9.9 C3.41502645,9.9 4.4,8.91502645 4.4,7.7 C4.4,6.48497355 3.41502645,5.5 2.2,5.5 C0.98497355,5.5 0,6.48497355 0,7.7 C0,8.91502645 0.98497355,9.9 2.2,9.9 Z" id="椭圆形备份-2" fill="#212332"></path>
|
||||
<path d="M2.2,22 C3.41502645,22 4.4,21.0150264 4.4,19.8 C4.4,18.5849736 3.41502645,17.6 2.2,17.6 C0.98497355,17.6 0,18.5849736 0,19.8 C0,21.0150264 0.98497355,22 2.2,22 Z" id="椭圆形备份-3" fill="#212332"></path>
|
||||
<path d="M19.8,4.4 C21.0150264,4.4 22,3.41502645 22,2.2 C22,0.98497355 21.0150264,0 19.8,0 C18.5849736,0 17.6,0.98497355 17.6,2.2 C17.6,3.41502645 18.5849736,4.4 19.8,4.4 Z" id="椭圆形" fill="#212332"></path>
|
||||
<path d="M19.8,16.5 C21.0150264,16.5 22,15.5150264 22,14.3 C22,13.0849736 21.0150264,12.1 19.8,12.1 C18.5849736,12.1 17.6,13.0849736 17.6,14.3 C17.6,15.5150264 18.5849736,16.5 19.8,16.5 Z" id="椭圆形" fill="#212332"></path>
|
||||
<path d="M14.3,9.9 C15.5150264,9.9 16.5,8.91502645 16.5,7.7 C16.5,6.48497355 15.5150264,5.5 14.3,5.5 C13.0849736,5.5 12.1,6.48497355 12.1,7.7 C12.1,8.91502645 13.0849736,9.9 14.3,9.9 Z" id="椭圆形备份-2" fill="#212332"></path>
|
||||
<path d="M14.3,22 C15.5150264,22 16.5,21.0150264 16.5,19.8 C16.5,18.5849736 15.5150264,17.6 14.3,17.6 C13.0849736,17.6 12.1,18.5849736 12.1,19.8 C12.1,21.0150264 13.0849736,22 14.3,22 Z" id="椭圆形备份-3" fill="#212332"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
19
web/src/assets/images/index/models.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 14</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-408, -232)">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-14" transform="translate(152, 16)">
|
||||
<rect id="矩形" fill="#155EEF" x="0" y="0" width="32" height="32" rx="8"></rect>
|
||||
<g id="模型管理" transform="translate(8, 7)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<polygon id="路径" points="7.4583178 17.9990012 0 13.715998 0 4.56667222 7.9512983 0 15.9025966 4.56567338 15.9025966 11.1380057 13.914772 11.1380057 13.914772 5.72631929 7.9512983 2.30131513 1.98782457 5.72731813 1.98782457 12.5573498 8.44427879 16.266023 7.4583178 18"></polygon>
|
||||
<polygon id="路径" points="14.6105106 3.99533877 15.6044229 5.72631929 8.37371102 9.921425 8.33594235 9.98634926 0.993912287 5.72631929 1.98782457 3.99533877 8.2991676 7.65606792"></polygon>
|
||||
<polygon id="路径" points="7.2555597 11.9860163 9.24338427 12.984851 9.24338427 8.98951224 7.2555597 8.98951224 7.2555597 11.9860163"></polygon>
|
||||
<polygon id="路径" points="14.5091316 12.4854337 16 15.0804062 14.5091316 17.6753787 11.5273947 17.6753787 10.0365263 15.0804062 11.5273947 12.4854337"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
21
web/src/assets/images/index/note_mgt.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 20</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1152, -772)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-20" transform="translate(30, 232)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="消息提醒" transform="translate(12, 11)" stroke="#212332" stroke-width="1.7">
|
||||
<path d="M2,14.0377426 L2,7.71071968 C2,4.39701118 4.6862915,1.71071968 8,1.71071968 C11.3137085,1.71071968 14,4.39701118 14,7.71071968 L14,14.0377426 L14,14.0377426" id="路径"></path>
|
||||
<line x1="1.08244944e-14" y1="14.5" x2="16" y2="14.5" id="路径-14" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
<line x1="8" y1="-5.46307465e-14" x2="8" y2="1.43460647" id="路径-15" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
<line x1="6.5" y1="17.5" x2="9.5" y2="17.5" id="路径-16" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
20
web/src/assets/images/index/space_mgt.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 26</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1254, -556)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-26" transform="translate(132, 16)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="空间管理" transform="translate(11, 10)" stroke="#212332" stroke-linejoin="round" stroke-width="1.7">
|
||||
<polygon id="路径" points="0 5 9 0 18 5 18 15 9 20 0 15"></polygon>
|
||||
<polygon id="路径-8" points="9.00139153 1 1 15 17 15"></polygon>
|
||||
<polygon id="路径-8" transform="translate(9, 11) scale(1, -1) translate(-9, -11)" points="9.00065228 8 5.25 14 12.75 14"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
26
web/src/assets/images/index/spaces.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 32</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="96.5083812%" id="linearGradient-1">
|
||||
<stop stop-color="#369F21" offset="0%"></stop>
|
||||
<stop stop-color="#369F21" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-624, -232)">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份" transform="translate(216, 0)">
|
||||
<g id="编组-32" transform="translate(152, 16)">
|
||||
<rect id="矩形" fill="url(#linearGradient-1)" x="0" y="0" width="32" height="32" rx="8"></rect>
|
||||
<g id="空间-(1)" transform="translate(5, 7)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M21.8430883,4.38834951 C21.4863174,3.55339806 20.6408194,3.19174757 19.9932559,3.03640777 C19.3676851,2.88592233 18.5954962,2.83495146 17.6937946,2.8907767 C17.6131546,2.80582524 17.5325146,2.72087379 17.449431,2.63834951 C16.6185948,1.8131068 15.6484713,1.16262136 14.5683843,0.708737864 C13.4516427,0.240291262 12.2640357,0 11.0397741,0 C9.81795616,0 8.63034912,0.237864078 7.51116389,0.708737864 C6.43107684,1.16262136 5.46339703,1.81067961 4.6301172,2.63834951 C3.79683737,3.46601942 3.14438659,4.42718447 2.68742668,5.5 C2.21580496,6.6092233 1.973885,7.78883495 1.973885,9.00485437 C1.973885,9.1723301 1.97877228,9.33737864 1.98854682,9.50242718 C1.30188514,10.1237864 0.788721607,10.723301 0.456387128,11.288835 C0.121609013,11.8592233 -0.200950922,12.7135922 0.155819916,13.5485437 C0.519921661,14.4004854 1.39718694,14.7621359 2.06674317,14.9150485 C2.51637217,15.0169903 3.03686661,15.0679612 3.62333922,15.0679612 C3.84571008,15.0679612 4.07785549,15.0606796 4.31977544,15.0461165 C4.46150632,15.2014563 4.60812448,15.3519417 4.7596299,15.4975728 C5.02354257,15.7475728 5.3045607,15.9854369 5.59535337,16.2038835 C7.18371668,17.3786408 9.06531631,18 11.0446614,18 C12.2664793,18 13.4540863,17.7621359 14.5732716,17.2912621 C15.6533586,16.8373786 16.6210384,16.1893204 17.4543183,15.3616505 C18.2851545,14.5364078 18.9400489,13.5728155 19.3970088,12.5 C19.8686305,11.3907767 20.1105505,10.211165 20.1105505,8.99514563 C20.1105505,8.78398058 20.1032195,8.57038835 20.0885577,8.36165049 C20.7165721,7.78398058 21.1955248,7.22572816 21.5131974,6.69660194 C21.865081,6.11650485 22.2096337,5.24029126 21.8430883,4.38834951 Z M5.88125876,3.87135922 C7.2594694,2.50242718 9.09463994,1.74757282 11.0446614,1.74757282 C12.9946828,1.74757282 14.8298533,2.50242718 16.208064,3.87135922 C17.261271,4.91747573 17.9552636,6.23058252 18.2216199,7.65533981 C16.6503621,8.91504854 14.4437589,10.1747573 11.9683557,11.2208738 C9.51494531,12.2572816 7.09330216,12.9587379 5.09929529,13.2135922 C4.92824077,12.9781553 4.77429171,12.7305825 4.63256083,12.4781553 C4.56169539,12.3470874 4.49082995,12.2135922 4.42729542,12.0776699 C4.40285906,12.0242718 4.37597907,11.9684466 4.35154271,11.9150485 C3.94345552,10.9951456 3.7381901,10.0145631 3.7381901,9 C3.74307738,7.0631068 4.50304813,5.24271845 5.88125876,3.87135922 Z M1.78328141,12.8616505 C1.7417396,12.7645631 1.77350686,12.5291262 1.98610318,12.1650485 C2.08629225,11.9951456 2.21580496,11.8131068 2.37464129,11.6237864 C2.47727399,11.961165 2.60189942,12.2936893 2.74607394,12.618932 C2.77539757,12.6868932 2.80716484,12.7548544 2.8389321,12.8228155 C2.91468482,12.9830097 2.9953248,13.1432039 3.08085205,13.2985437 C2.17670678,13.2257282 1.83459776,12.9805825 1.78328141,12.8616505 L1.78328141,12.8616505 Z M16.208064,14.1286408 C14.8298533,15.5 12.9946828,16.2524272 11.0446614,16.2524272 C9.44896714,16.2524272 7.9339129,15.75 6.66322224,14.8009709 C6.63634225,14.7815534 6.60946225,14.7597087 6.58258226,14.7402913 C8.44952007,14.3762136 10.5510469,13.7160194 12.657461,12.8276699 C14.7858679,11.9296117 16.7383329,10.8713592 18.3047035,9.77669903 C18.1312054,11.4223301 17.4005583,12.9441748 16.208064,14.1286408 L16.208064,14.1286408 Z M19.6780269,6.25728155 C19.5949433,6 19.4996415,5.74757282 19.3945651,5.49757282 C19.2699397,5.2038835 19.1282088,4.91747573 18.9742598,4.63834951 C19.1990743,4.65776699 19.401896,4.68932039 19.5802815,4.73300971 C19.9932559,4.83252427 20.1838595,4.97330097 20.227845,5.07281553 C20.2742741,5.1868932 20.2180704,5.58737864 19.6780269,6.25728155 L19.6780269,6.25728155 Z" id="形状"></path>
|
||||
<path d="M6.83916403,8.87135922 C7.18127305,9.06796117 7.56981115,9.16504854 7.9681238,9.16504854 C8.28579646,9.16504854 8.61324367,9.10194175 8.9284727,8.97815534 C9.58092348,8.7184466 10.1502906,8.21601942 10.5290542,7.5631068 C10.9078178,6.91019417 11.0617668,6.16990291 10.9615777,5.47815534 C10.8516141,4.72087379 10.436196,4.08495146 9.82284343,3.73300971 C9.20949082,3.38106796 8.44707643,3.34223301 7.73109112,3.62621359 C7.07864034,3.88592233 6.50927318,4.38834951 6.13050962,5.04126214 C5.75174606,5.69417476 5.597797,6.43446602 5.69798607,7.12621359 C5.81039332,7.88349515 6.22581142,8.51941748 6.83916403,8.87135922 Z M7.65533841,5.91504854 C7.83616746,5.60436893 8.10252377,5.36165049 8.38598553,5.25 C8.51061096,5.20145631 8.74764364,5.13349515 8.94557815,5.24757282 C9.14106902,5.3592233 9.20460355,5.59708738 9.221709,5.73058252 C9.26569445,6.0315534 9.1874981,6.38106796 9.00666905,6.69174757 C8.82583999,7.00242718 8.55948368,7.24514563 8.27602192,7.35679612 C8.15139649,7.40533981 7.91436381,7.47330097 7.71642931,7.3592233 C7.39875664,7.17475728 7.2936803,6.53640777 7.65533841,5.91504854 Z" id="形状"></path>
|
||||
<path d="M14.3929157,5.78130192 C14.6699504,6.12865804 15.0730899,6.3338495 15.4504584,6.31957402 C15.8278269,6.30529853 16.1220856,6.07372518 16.2223777,5.71209515 C16.3226697,5.35046513 16.2137563,4.91372597 15.9366687,4.56641148 C15.659634,4.21905536 15.2564945,4.0138639 14.879126,4.02813938 C14.5017575,4.04241487 14.2074988,4.27398822 14.1072067,4.63561824 C14.0069147,4.99724827 14.1158281,5.43398743 14.3929157,5.78130192 L14.3929157,5.78130192 Z" id="路径"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
22
web/src/assets/images/index/user_mgt.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 24</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1152, -664)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-24" transform="translate(30, 124)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="用户管理" transform="translate(9, 10)" stroke="#212332" stroke-width="1.7">
|
||||
<path d="M10.4752216,9.8347602 C12.0826337,9.8347602 13.5411901,10.4629457 14.5971671,11.4807313 C15.6415902,12.4873807 16.2916977,13.8763311 16.2916977,15.41013 L16.2916987,17.2469287 C16.292495,17.768016 16.0699552,18.2385306 15.715897,18.5800613 C15.3496472,18.9333521 14.8431448,19.15 14.2862956,19.15 L2.85772417,19.15 C2.30025073,19.15 1.79383233,18.9336648 1.42754075,18.5806207 C1.07279965,18.2387094 0.85,17.7674542 0.85,17.2456298 L0.85,15.41013 C0.85,13.8763309 1.50010692,12.4873802 2.54452956,11.4807307 C3.60050579,10.4629455 5.05906118,9.8347602 6.6664729,9.8347602 Z" id="路径" fill-rule="nonzero"></path>
|
||||
<path d="M8.51095419,0.85 C9.31202689,0.85 10.0374296,1.17449643 10.5625442,1.69913767 C11.0873448,2.22346508 11.4120985,2.94775006 11.4120985,3.74776253 C11.4120985,4.54882217 11.0868662,5.27413048 10.5618329,5.79931671 C10.0365783,6.32472447 9.31123944,6.65 8.51095419,6.65 C7.71104886,6.65 6.98645948,6.32499629 6.46178132,5.80015797 C5.93701763,5.27523409 5.61209853,4.5502916 5.61209853,3.75 C5.61209853,2.94883778 5.93633819,2.22374798 6.46093583,1.69899021 C6.98544108,1.17432485 7.71016945,0.85 8.51095419,0.85 Z" id="路径" fill-rule="nonzero"></path>
|
||||
<path d="M16.5,8 C17.8807119,8 19,6.88071187 19,5.5 C19,4.11928813 17.8807119,3 16.5,3" id="路径" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M18,11 C20.209139,11 22,12.790861 22,15 L22,16 C22,16.5522847 21.5522847,17 21,17 L20.4553443,17 L20.4553443,17" id="路径" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<line x1="8.57084884" y1="13.4113022" x2="8.57084884" y2="15.9113022" id="路径-21" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
18
web/src/assets/images/index/users.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-840, -232)">
|
||||
<g id="核心数据" transform="translate(256, 216)">
|
||||
<g id="编组-19备份-2" transform="translate(432, 0)">
|
||||
<g id="编组-33" transform="translate(152, 16)">
|
||||
<rect id="矩形" fill="#4DA8FF" x="0" y="0" width="32" height="32" rx="8"></rect>
|
||||
<g id="用户总数总计" transform="translate(6, 8)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M9.5,8.845 C10.9,7.95 11.9,6.45999999 11.9,4.77 C11.9,2.087 9.69999999,0 7,0 C4.30000001,0 2,2.087 2,4.77 C2,6.56 3,8.05 4.4,8.845 C1.8,9.839 0,12.224 0,15.105 C0,15.602 0.4,15.901 0.8,15.901 C1.2,15.901 1.6,15.503 1.6,15.106 C1.6,12.224 4,9.93799999 6.89999999,9.93799999 C9.79999998,9.93799999 12.2,12.224 12.2,15.106 C12.2,15.602 12.6,15.901 13,15.901 C13.4,15.901 13.8,15.503 13.8,15.106 C13.9,12.323 12,9.83899998 9.5,8.845 Z M6.89999999,7.95 C5.09999999,7.95 3.69999999,6.559 3.69999999,4.77 C3.69999999,2.98099999 5.19999999,1.59 6.89999999,1.59 C8.69999999,1.59 10.1,2.98100001 10.1,4.77 C10.1,6.559 8.69999999,7.95 6.89999999,7.95 Z M18.4,6.957 C18.4,4.77 16.6,2.98099999 14.4,2.98099999 C14,2.98099999 13.2,3.09957886 12.8,3.37899999 C12.5,3.57799999 12.3,3.87599999 12.3,4.17399999 C12.3,4.67099999 12.7,5.06799999 13.2,5.068 C13.4302275,5.068 13.6352352,4.87 14.2,4.87 C15.5,4.87 16.5,5.86300001 16.5,7.05599999 C16.5,8.34799999 15.5,9.34199999 14.3,9.34199999 C13.7,9.34199999 13.3,9.73899999 13.3,10.335 C13.3,10.832 13.8,11.329 14.3,11.329 L14.4,11.329 C16.5,11.329 18.2,13.019 18.2,15.106 C18.2,15.602 18.6,16 19.1,16 C19.6,16 20,15.602 20,15.106 C20,12.919 18.8,10.932 16.8,10.136 C17.8,9.34199999 18.4,8.14899999 18.4,6.957 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
21
web/src/assets/images/index/workflow_mgt.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 29</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="首页" transform="translate(-1354, -556)">
|
||||
<g id="快捷操作" transform="translate(1120, 502)">
|
||||
<g id="编组-11" transform="translate(2, 38)">
|
||||
<g id="编组-29" transform="translate(232, 16)">
|
||||
<rect id="矩形" stroke="#DFE4ED" fill="#FFFFFF" x="0.5" y="0.5" width="39" height="39" rx="8"></rect>
|
||||
<g id="编组-9" transform="translate(11.5, 11.5)">
|
||||
<rect id="矩形" stroke="#212332" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" x="4.53333333" y="0" width="11.9" height="3.96666667" rx="0.4"></rect>
|
||||
<path d="M13.0448746,15.0166667 L16.6,15.0166667 C16.8209139,15.0166667 17,14.8375806 17,14.6166667 L17,8.9 C17,8.6790861 16.8209139,8.5 16.6,8.5 L0.4,8.5 C0.1790861,8.5 3.88578059e-16,8.3209139 3.88578059e-16,8.1 L0,2.38333333 C0,2.16241943 0.1790861,1.98333333 0.4,1.98333333 L4.55122349,1.98333333 L4.55122349,1.98333333" id="路径" stroke="#212332" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<rect id="矩形" stroke="#212332" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" x="1.13333333" y="13.0333333" width="11.9" height="3.96666667" rx="0.4"></rect>
|
||||
<path d="M8.78082738,6.23284271 L10.765142,8.21715729 C10.9213517,8.373367 10.9213517,8.626633 10.765142,8.78284271 L8.78082738,10.7671573 C8.62461766,10.923367 8.37135167,10.923367 8.21514196,10.7671573 L6.23082738,8.78284271 C6.07461766,8.626633 6.07461766,8.373367 6.23082738,8.21715729 L8.21514196,6.23284271 C8.37135167,6.076633 8.62461766,6.076633 8.78082738,6.23284271 Z" id="多边形" fill="#212332"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
web/src/assets/images/workflow/assigner.png
Normal file
|
After Width: | Height: | Size: 588 B |
BIN
web/src/assets/images/workflow/break.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
web/src/assets/images/workflow/question-classifier.png
Normal file
|
After Width: | Height: | Size: 853 B |
@@ -3,6 +3,60 @@ export const en = {
|
||||
welcome: 'Welcome to React Font CLI',
|
||||
title: 'Memory Bear.AI ',
|
||||
memoryBear: 'Memory Bear.AI',
|
||||
index:{
|
||||
viewGuide: 'View Guide',
|
||||
watchVideo: 'Watch Video',
|
||||
viewDetails: 'View Details',
|
||||
changeLog: 'Change Log',
|
||||
latestUpdate: 'Latest Update',
|
||||
appCount: 'Number of Spaces',
|
||||
userCount: 'Number of Users',
|
||||
latestUpdateDesc: 'Version v0.2.0 release: Added visual workflow editor, model performance monitoring panel, and multi tenant permission management system.',
|
||||
getStarted: 'Getting Started',
|
||||
startedDesc: 'Understand the core functions of the platform and quickly get started through graphic guidance and video tutorials. Includes a full process demonstration from creating a space to publishing an application.',
|
||||
spaceTitle:'Memory Bear Intelligent Space Management Platform',
|
||||
spaceSubTitle: 'Making it easier to implement intelligent models - a one-stop platform for model management, knowledge building, workflow orchestration, and spatial operations',
|
||||
},
|
||||
quickActions:{
|
||||
title: 'Quick Actions',
|
||||
spaceManagement: 'Space Management',
|
||||
modelManagement: 'Model Management',
|
||||
workflowOrchestration: 'Workflow Orchestration',
|
||||
userManagement: 'User Management',
|
||||
dataExport: 'Data Export',
|
||||
logQuery:'Log Query',
|
||||
notificationReminder: 'Notification Reminder',
|
||||
helpCenter: 'Help Center',
|
||||
knowledgeBase: 'Knowledge Base',
|
||||
knowledgeBaseDesc: 'Manage and maintain your knowledge base data',
|
||||
modelManagementDesc: 'Configure and optimize AI model parameters',
|
||||
userManagementDesc: 'Manage system users and permission settings',
|
||||
systemSettings: 'System Settings',
|
||||
systemSettingsDesc: 'Configure global system parameters and options',
|
||||
memoryManagement: 'Memory Management',
|
||||
memoryManagementDesc: 'Manage user memories and conversation history',
|
||||
apiManagement: 'API Management',
|
||||
apiManagementDesc: 'Manage API keys and interface configurations',
|
||||
workflowEngine: 'Workflow Engine',
|
||||
workflowEngineDesc: 'Design and manage automated workflows',
|
||||
performanceMonitor: 'Performance Monitor',
|
||||
performanceMonitorDesc: 'Monitor system performance and operational status',
|
||||
},
|
||||
guide: {
|
||||
quickStart: 'Quick Start',
|
||||
createKnowledge: 'Create Knowledge Base',
|
||||
createKnowledgeDesc: 'Build your exclusive knowledge base to help AI better understand your business',
|
||||
manageModel: 'Manage Models',
|
||||
manageModelDesc: 'Configure and manage your AI models to optimize performance',
|
||||
createSpace: 'Create Space',
|
||||
createSpaceDesc: 'Create new workspaces to organize your projects and teams',
|
||||
systemConfig: 'System Configuration',
|
||||
systemConfigDesc: 'Configure system parameters to personalize your experience',
|
||||
startCreate: 'Start Creating',
|
||||
goManage: 'Go Manage',
|
||||
createNow: 'Create Now',
|
||||
goConfig: 'Go Configure',
|
||||
},
|
||||
menu: {
|
||||
home: 'Home',
|
||||
tenantManagement: 'Tenant Management',
|
||||
@@ -43,12 +97,22 @@ export const en = {
|
||||
orderHistory: 'Order History',
|
||||
},
|
||||
dashboard: {
|
||||
total_models: 'Total number of available models',
|
||||
total_spaces: 'Number of active spaces',
|
||||
total_users: 'Total number of users',
|
||||
total_running_apps: 'Number of application runs',
|
||||
desc_models: 'Contains {{ account }} LLMs and {{ nums }} Embeddings',
|
||||
desc_spaces: 'more than last week',
|
||||
desc_users: 'New additions this week',
|
||||
desc_running_apps: "Today's success rate",
|
||||
totalMemoryCapacity: 'Total Memory Capacity',
|
||||
userMemory: 'User Memory',
|
||||
knowledgeBaseCount: 'Knowledge Base Count',
|
||||
apiCallCount: 'API Call Count',
|
||||
comparedToYesterday: 'compared to yesterday',
|
||||
thisWeek: 'this week',
|
||||
thisDay: 'day on day',
|
||||
failureRate: 'Failure Rate',
|
||||
application: 'Application Count',
|
||||
total_num: 'Total Memory Capacity',
|
||||
|
||||
@@ -630,6 +694,38 @@ export const en = {
|
||||
fileDurationExceeds: 'File duration exceeds the limit',
|
||||
fileDurationLimitError: 'The duration of the media file exceeds the limit. The maximum supported duration is 150 seconds. Current duration',
|
||||
unableReadFile:'Unable to read the information of the media file. Please check the file format.',
|
||||
// Knowledge Graph related
|
||||
knowledgeGraph: 'Knowledge Graph',
|
||||
basicConfig: 'Basic Configuration',
|
||||
enableKnowledgeGraph: 'Enable Knowledge Graph',
|
||||
enableKnowledgeGraphTips: 'Once activated, it will automatically construct an entity relationship network.',
|
||||
graphConfig: 'Graph Configuration',
|
||||
sceneName: 'Scene Name',
|
||||
sceneNamePlaceholder: 'Please enter scene name',
|
||||
entityTypes: 'Entity Types',
|
||||
entityTypesPlaceholder: 'Please enter entity types, separate multiple types with line breaks',
|
||||
entityNormalization: 'Entity Normalization',
|
||||
entityNormalizationTips: 'Merge similar entities when enabled',
|
||||
entityMethod: 'Entity Method',
|
||||
entityMethodGeneral: 'General',
|
||||
entityMethodLight: 'Light',
|
||||
communityReportGeneration: 'Community Report Generation',
|
||||
communityReportGenerationTips: 'Generate community analysis reports when enabled',
|
||||
generateEntityTypes: 'Generate Entity Types',
|
||||
regenerateEntityTypes: 'Regenerate',
|
||||
generateEntityTypesSuccess: 'Entity types generated successfully',
|
||||
generateEntityTypesFailed: 'Failed to generate entity types',
|
||||
unknownError: 'Unknown error',
|
||||
pleaseSelectLLMModel: 'Please select a LLM model in basic configuration first',
|
||||
enterScenarioName: 'Please enter scenario name',
|
||||
entityDetails: 'Entity Details',
|
||||
entityDetailEmpty: 'Click on a node in the graph to view details',
|
||||
entityDetailEmptyDesc: 'Select an entity node to view its detailed information',
|
||||
entityDescription: 'Entity Description',
|
||||
graphTitle: 'Knowledge Graph: The Network of Entity, Relationship and Attribute Associations',
|
||||
graphTips: 'Explore the entity nodes in the knowledge base and their relationship networks',
|
||||
sourceDocuments: 'Source Documents',
|
||||
rebuildGraph: 'Rebuild Graph',
|
||||
createForm:{
|
||||
name: 'Name',
|
||||
embedding_id: 'Embedding',
|
||||
@@ -1117,7 +1213,11 @@ export const en = {
|
||||
memoryContent: 'Memory Content',
|
||||
created_at: 'Created At',
|
||||
|
||||
memoryWindow: "{{name}}'s Window of Memory"
|
||||
memoryWindow: "{{name}}'s Window of Memory",
|
||||
memory_insight: 'Overall Overview',
|
||||
key_findings: 'Key Findings',
|
||||
behavior_pattern: 'Behavior Pattern',
|
||||
growth_trajectory: 'Growth Trajectory',
|
||||
},
|
||||
space: {
|
||||
createSpace: 'Create Space',
|
||||
@@ -1580,16 +1680,22 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'parameter-extractor': 'Parameter Extraction',
|
||||
flowControl: 'Flow Control',
|
||||
'if-else': 'Conditional Branch',
|
||||
'question-classifier': 'Question Classifier',
|
||||
iteration: 'Iteration',
|
||||
loop: 'Loop',
|
||||
'cycle-start': '',
|
||||
break: 'Break Loop',
|
||||
assigner: 'Variable Assignment',
|
||||
parallel: 'Parallel Execution',
|
||||
'var-aggregator': 'Variable Aggregator',
|
||||
externalInteraction: 'External Interaction',
|
||||
"http-request": 'HTTP Request',
|
||||
tools: 'Tools',
|
||||
tool: 'Tools',
|
||||
code_execution: 'Code Execution',
|
||||
"jinja-render": 'Template Rendering',
|
||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||
'memory-read': 'Memory Retrieval',
|
||||
'memory-write': 'Memory Storage',
|
||||
task_planning: 'Task Planning',
|
||||
reasoning_control: 'Reasoning Control',
|
||||
self_reflection: 'Self Reflection',
|
||||
@@ -1607,9 +1713,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
|
||||
clickToConfigure: 'Click to configure node parameters',
|
||||
nodeProperties: 'Node Properties',
|
||||
empty: "Emmm... The box is empty, nothing here~",
|
||||
empty: "Emmm…The box is empty, there's nothing here~",
|
||||
nodeName: 'Node Name',
|
||||
|
||||
addvariable: 'Chat Variables',
|
||||
addChatVariable: 'Add Chat Variable',
|
||||
editChatVariable: 'Edit Chat Variable',
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
@@ -1631,7 +1739,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
editVariable: 'Edit Variable',
|
||||
variableType: 'Variable Type',
|
||||
variableName: 'Variable Name',
|
||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
||||
invalidVariableName: 'Variable name can only start with English letters and contain English letters, numbers, and underscores',
|
||||
description: 'Display Name',
|
||||
default: 'Default Value',
|
||||
required: 'Required',
|
||||
@@ -1658,10 +1766,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
editParam: 'Edit Extract Parameter',
|
||||
|
||||
name: 'Name',
|
||||
invalidParamName: 'Parameter name must start with a letter and contain only letters, numbers, and underscores',
|
||||
invalidParamName: 'Extract parameter name can only start with English letters and contain English letters, numbers, and underscores',
|
||||
type: 'Type',
|
||||
desc: 'Description',
|
||||
required: 'Required',
|
||||
default: 'Default Value',
|
||||
|
||||
'string': 'String',
|
||||
'number': 'Number',
|
||||
@@ -1673,7 +1782,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: 'Aggregation Group',
|
||||
invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores',
|
||||
invalidVariableName: 'Variable name can only start with English letters and contain English letters, numbers, and underscores',
|
||||
addGroup: 'Add Group',
|
||||
variable: 'Variable Assignment'
|
||||
},
|
||||
@@ -1691,7 +1800,76 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
"gt": '>',
|
||||
"ge": '>=',
|
||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.'
|
||||
}
|
||||
},
|
||||
'http-request': {
|
||||
auth: 'Authentication',
|
||||
authType: 'Auth Type',
|
||||
apiKey: 'API Key',
|
||||
basic: 'Basic',
|
||||
bearer: 'Bearer',
|
||||
custom: 'Custom',
|
||||
header: 'Header',
|
||||
api_key: 'API Key',
|
||||
timeouts: 'Timeout Settings',
|
||||
"connect_timeout": 'Connection Timeout (seconds)',
|
||||
"read_timeout": 'Read Timeout (seconds)',
|
||||
"write_timeout": 'Write Timeout (seconds)',
|
||||
retry: 'Retry on Failure',
|
||||
error_handle: 'Error Handling',
|
||||
verify_ssl: 'Verify SSL Certificate',
|
||||
none: 'None',
|
||||
default: 'Default Value',
|
||||
branch: 'Error Branch',
|
||||
status_code: 'Status Code',
|
||||
max_attempts: 'Max Retry Attempts',
|
||||
retry_interval: 'Retry Interval',
|
||||
},
|
||||
'jinja-render': {
|
||||
template: 'Code',
|
||||
mapping: 'Input Variables'
|
||||
},
|
||||
'question-classifier': {
|
||||
model_id: 'Model',
|
||||
input_variable: 'Input Variable',
|
||||
categories: 'Categories',
|
||||
user_supplement_prompt: 'Instruction',
|
||||
class_name: 'Classification',
|
||||
addClassName: 'Add Classification'
|
||||
},
|
||||
loop: {
|
||||
cycle_vars: 'Loop Variables',
|
||||
condition: 'Loop Termination Condition',
|
||||
},
|
||||
assigner: {
|
||||
assignments: 'Variables',
|
||||
cover: 'Overwrite',
|
||||
assign: 'Set',
|
||||
clear: 'Clear'
|
||||
},
|
||||
iteration: {
|
||||
input: 'Input Variable',
|
||||
output: 'Output Variable',
|
||||
parallel: 'Parallel Mode',
|
||||
parallel_count: 'Max Parallelism',
|
||||
flatten: 'Flatten Output',
|
||||
},
|
||||
tool: {
|
||||
tool_id: 'Tool',
|
||||
},
|
||||
'memory-read': {
|
||||
message: 'Message',
|
||||
config_id: 'Memory Configuration',
|
||||
search_switch: 'Search Mode',
|
||||
},
|
||||
'memory-write': {
|
||||
message: 'Message',
|
||||
config_id: 'Memory Configuration',
|
||||
search_switch: 'Search Mode',
|
||||
},
|
||||
name: 'Key',
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
addCase: 'Add Condition',
|
||||
},
|
||||
|
||||
clear: 'Clear',
|
||||
|
||||
@@ -3,6 +3,60 @@ export const zh = {
|
||||
title: '记忆熊',
|
||||
memoryBear: '记忆熊',
|
||||
welcome: '欢迎使用 React Font CLI',
|
||||
index:{
|
||||
viewGuide: '查看引导',
|
||||
watchVideo: '观看视频',
|
||||
viewDetails: '查看详情',
|
||||
changeLog: '变更日志',
|
||||
getStarted:'快速开始',
|
||||
latestUpdate: '最新更新',
|
||||
appCount: '应用数量',
|
||||
userCount: '用户数量',
|
||||
latestUpdateDesc: '版本 v0.2.0 发布:新增了可视化工作流编辑器、模型性能监控面板以及多租户权限管理系统。',
|
||||
startedDesc: '了解该平台的核心功能,并通过图形指引和视频教程快速上手。包含从创建空间到发布应用程序的整个操作流程演示。',
|
||||
spaceTitle:'记忆熊智能空间管理平台',
|
||||
spaceSubTitle: '使智能模型的实施变得更加容易——一个集模型管理、知识构建、工作流程编排以及空间操作于一体的综合性平台',
|
||||
},
|
||||
quickActions:{
|
||||
title: '快速操作',
|
||||
spaceManagement: '空间管理',
|
||||
modelManagement: '模型管理',
|
||||
workflowOrchestration: '工作流编排',
|
||||
userManagement: '用户管理',
|
||||
dataExport: '数据导出',
|
||||
logQuery:'日志查询',
|
||||
notificationReminder: '通知提醒',
|
||||
helpCenter: '帮助中心',
|
||||
knowledgeBase: '知识库',
|
||||
knowledgeBaseDesc: '管理和维护您的知识库数据',
|
||||
modelManagementDesc: '配置和优化AI模型参数',
|
||||
userManagementDesc: '管理系统用户和权限设置',
|
||||
systemSettings: '系统设置',
|
||||
systemSettingsDesc: '配置系统全局参数和选项',
|
||||
memoryManagement: '记忆管理',
|
||||
memoryManagementDesc: '管理用户记忆和对话历史',
|
||||
apiManagement: 'API管理',
|
||||
apiManagementDesc: '管理API密钥和接口配置',
|
||||
workflowEngine: '工作流引擎',
|
||||
workflowEngineDesc: '设计和管理自动化工作流程',
|
||||
performanceMonitor: '性能监控',
|
||||
performanceMonitorDesc: '监控系统性能和运行状态',
|
||||
},
|
||||
guide: {
|
||||
quickStart: '快速开始',
|
||||
createKnowledge: '创建知识库',
|
||||
createKnowledgeDesc: '构建您的专属知识库,让AI更好地理解您的业务',
|
||||
manageModel: '管理模型',
|
||||
manageModelDesc: '配置和管理您的AI模型,优化性能表现',
|
||||
createSpace: '创建空间',
|
||||
createSpaceDesc: '创建新的工作空间,组织您的项目和团队',
|
||||
systemConfig: '系统配置',
|
||||
systemConfigDesc: '配置系统参数,个性化您的使用体验',
|
||||
startCreate: '开始创建',
|
||||
goManage: '去管理',
|
||||
createNow: '立即创建',
|
||||
goConfig: '去配置',
|
||||
},
|
||||
menu: {
|
||||
home: '首页',
|
||||
tenantManagement: '租户管理',
|
||||
@@ -250,6 +304,38 @@ export const zh = {
|
||||
fileDurationExceeds:'文件时长超过限制',
|
||||
fileDurationLimitError: '媒体文件时长超过限制,最大支持150秒,当前时长',
|
||||
unableReadFile:'无法读取媒体文件信息,请检查文件格式',
|
||||
// 知识图谱相关
|
||||
knowledgeGraph: '知识图谱',
|
||||
basicConfig: '基础配置',
|
||||
enableKnowledgeGraph: '启用知识图谱',
|
||||
enableKnowledgeGraphTips: '开启后将自动构建实体关系网络',
|
||||
graphConfig: '图谱配置',
|
||||
sceneName: '场景名称',
|
||||
sceneNamePlaceholder: '请输入场景名称',
|
||||
entityTypes: '实体类型',
|
||||
entityTypesPlaceholder: '请输入实体类型,多个类型用换行分隔',
|
||||
entityNormalization: '实体归一化',
|
||||
entityNormalizationTips: '开启后将对相似实体进行合并处理',
|
||||
entityMethod: '实体方法',
|
||||
entityMethodGeneral: '通用',
|
||||
entityMethodLight: '轻量',
|
||||
communityReportGeneration: '社区报告生成',
|
||||
communityReportGenerationTips: '开启后将生成社区分析报告',
|
||||
generateEntityTypes: '生成实体类型',
|
||||
regenerateEntityTypes: '重新生成',
|
||||
generateEntityTypesSuccess: '实体类型生成成功',
|
||||
generateEntityTypesFailed: '生成实体类型失败',
|
||||
unknownError: '未知错误',
|
||||
pleaseSelectLLMModel: '请先在基础配置中选择大语言模型',
|
||||
enterScenarioName: '请输入场景名称',
|
||||
entityDetails: '实体详情',
|
||||
entityDetailEmpty: '请点击图谱中的节点查看详情',
|
||||
entityDetailEmptyDesc: '选择一个实体节点来查看其详细信息',
|
||||
entityDescription: '实体描述',
|
||||
sourceDocuments: '来源文档',
|
||||
graphTitle: '知识图谱:实体、关系与属性的关联网络',
|
||||
graphTips: '探索知识库中的实体节点及其关系脉络',
|
||||
rebuildGraph: '重建图谱',
|
||||
createForm: {
|
||||
name: '名称',
|
||||
embedding_id: '嵌入模型',
|
||||
@@ -602,12 +688,22 @@ export const zh = {
|
||||
totalRecords: '共 {{total}} 条记录'
|
||||
},
|
||||
dashboard: {
|
||||
total_models: '可用模型总数',
|
||||
total_spaces: '活跃空间数量',
|
||||
total_users: '用户总数',
|
||||
total_running_apps: '应用运行次数',
|
||||
desc_models: '包含 {{ account }} 个大语言模型和 {{ nums }} 个嵌入模型',
|
||||
desc_spaces: '多于上周',
|
||||
desc_users: '本周新增',
|
||||
desc_running_apps: '今日成功率',
|
||||
totalMemoryCapacity: '总记忆容量',
|
||||
userMemory: '用户记忆',
|
||||
knowledgeBaseCount: '知识库数量',
|
||||
apiCallCount: 'API调用次数',
|
||||
comparedToYesterday: '与昨天相比',
|
||||
thisWeek: '本周',
|
||||
thisDay: '本日',
|
||||
failureRate: '故障率',
|
||||
application: '应用数量',
|
||||
total_num: '总记忆容量',
|
||||
|
||||
@@ -1198,7 +1294,11 @@ export const zh = {
|
||||
updated_at: '最后更新时间',
|
||||
fullScreen: '全屏',
|
||||
|
||||
memoryWindow: "{{name}}的记忆之窗"
|
||||
memoryWindow: "{{name}}的记忆之窗",
|
||||
memory_insight: '总体概述',
|
||||
key_findings: '关键发现',
|
||||
behavior_pattern: '行为模式',
|
||||
growth_trajectory: '成长轨迹',
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1680,16 +1780,22 @@ export const zh = {
|
||||
'parameter-extractor': '参数提取',
|
||||
flowControl: '流程控制',
|
||||
'if-else': '条件分支',
|
||||
'question-classifier': '问题分类器',
|
||||
iteration: '迭代 (Iteration)',
|
||||
loop: '循环 (Loop)',
|
||||
'cycle-start': '',
|
||||
break: '退出循环',
|
||||
assigner: '变量赋值',
|
||||
parallel: '并行执行',
|
||||
'var-aggregator': '变量聚合器',
|
||||
externalInteraction: '外部交互',
|
||||
"http-request": 'HTTP请求',
|
||||
tools: '工具 (Tools)',
|
||||
tool: '工具 (Tool)',
|
||||
code_execution: '代码执行',
|
||||
"jinja-render": '模板渲染',
|
||||
cognitiveUpgrading: '认知升级(创新)',
|
||||
'memory-read': '记忆提取',
|
||||
'memory-write': '记忆储存',
|
||||
task_planning: '任务规划',
|
||||
reasoning_control: '推理控制',
|
||||
self_reflection: '自我反思',
|
||||
@@ -1709,7 +1815,9 @@ export const zh = {
|
||||
nodeProperties: '节点属性',
|
||||
empty: "Emmm…盒子是空的,这里什么都没有~",
|
||||
nodeName: '节点名称',
|
||||
|
||||
addvariable: '会话变量',
|
||||
addChatVariable: '添加会话变量',
|
||||
editChatVariable: '编辑会话变量',
|
||||
|
||||
config: {
|
||||
llm: {
|
||||
@@ -1762,6 +1870,7 @@ export const zh = {
|
||||
type: '类型',
|
||||
desc: '描述',
|
||||
required: '必填',
|
||||
default: '默认值',
|
||||
|
||||
'string': 'String',
|
||||
'number': 'Number',
|
||||
@@ -1819,9 +1928,48 @@ export const zh = {
|
||||
template: '代码',
|
||||
mapping: '输入变量'
|
||||
},
|
||||
'question-classifier': {
|
||||
model_id: '模型',
|
||||
input_variable: '输入变量',
|
||||
categories: '分类',
|
||||
user_supplement_prompt: '指令',
|
||||
class_name: '分类',
|
||||
addClassName: '添加分类'
|
||||
},
|
||||
loop: {
|
||||
cycle_vars: '循环变量',
|
||||
condition: '循环终止条件',
|
||||
},
|
||||
assigner: {
|
||||
assignments: '变量',
|
||||
cover: '覆盖',
|
||||
assign: '设置',
|
||||
clear: '清空'
|
||||
},
|
||||
iteration: {
|
||||
input: '输入变量',
|
||||
output: '输出变量',
|
||||
parallel: '并行模式',
|
||||
parallel_count: '最大并行度',
|
||||
flatten: '扁平化输出',
|
||||
},
|
||||
tool: {
|
||||
tool_id: '工具',
|
||||
},
|
||||
'memory-read': {
|
||||
message: '消息',
|
||||
config_id: '记忆配置',
|
||||
search_switch: '检索模式',
|
||||
},
|
||||
'memory-write': {
|
||||
message: '消息',
|
||||
config_id: '记忆配置',
|
||||
search_switch: '检索模式',
|
||||
},
|
||||
name: '键',
|
||||
type: '类型',
|
||||
value: '值',
|
||||
addCase: '添加条件',
|
||||
},
|
||||
|
||||
clear: '清空',
|
||||
|
||||
@@ -35,6 +35,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
|
||||
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
|
||||
// 视图组件
|
||||
Index: lazy(() => import('@/views/Index')),
|
||||
Home: lazy(() => import('@/views/Home')),
|
||||
UserMemory: lazy(() => import('@/views/UserMemory')),
|
||||
UserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail')),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
"element": "AuthLayout",
|
||||
"children": [
|
||||
{ "path": "/index", "element": "Index" },
|
||||
{ "path": "/user-management", "element": "UserManagement" },
|
||||
{ "path": "/model", "element": "ModelManagement" },
|
||||
{ "path": "/space", "element": "SpaceManagement" },
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
{
|
||||
"manage": [
|
||||
{
|
||||
"id": 999,
|
||||
"parent": 0,
|
||||
"code": "dashboard",
|
||||
"label": "首页",
|
||||
"i18nKey": "menu.home",
|
||||
"path": "/index",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"parent": 0,
|
||||
|
||||
@@ -101,6 +101,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
const clear = () => {
|
||||
workflowRef?.current?.graphRef?.current?.clearCells()
|
||||
}
|
||||
const addvariable = () => {
|
||||
workflowRef?.current?.addVariable()
|
||||
}
|
||||
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">
|
||||
@@ -132,6 +135,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
{application?.type === 'workflow'
|
||||
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
|
||||
<Button onClick={clear}>{t('workflow.clear')}</Button>
|
||||
<Button onClick={addvariable}>{t('workflow.addvariable')}</Button>
|
||||
<Button onClick={run}>{t('workflow.run')}</Button>
|
||||
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
|
||||
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
||||
|
||||
@@ -121,7 +121,8 @@ export interface ClusterRef {
|
||||
export interface WorkflowRef {
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
handleRun: () => void;
|
||||
graphRef: GraphRef
|
||||
graphRef: GraphRef;
|
||||
addVariable: () => void;
|
||||
}
|
||||
export interface ApplicationModalRef {
|
||||
handleOpen: (application?: Config) => void;
|
||||
|
||||
32
web/src/views/Index/components/GuideCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import guideBgImg from '@/assets/images/index/guide_bg@2x.png'
|
||||
import { Button } from 'antd';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons'
|
||||
import arrowRight from '@/assets/images/index/arrow_right_blue.svg'
|
||||
const GuideCard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='rb:w-full rb:h-[204px] rb:p-4' style={{ backgroundImage: `url(${guideBgImg})`, backgroundSize: '100% 100%' }}>
|
||||
<div className='rb:flex rb:justify-start rb:text-white rb:text-base rb:font-semibold'>
|
||||
{ t('index.getStarted')}
|
||||
</div>
|
||||
<div className='rb:flex rb:text-xs rb:text-white rb:leading-[18px] rb:mt-3'>
|
||||
{ t('index.startedDesc')}
|
||||
</div>
|
||||
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
|
||||
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF] '>
|
||||
<span className='rb:text-xs'>{ t('index.viewGuide')}</span>
|
||||
<img src={arrowRight} className='rb:size-4' />
|
||||
</Button>
|
||||
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#155EEF]'>
|
||||
<span className='rb:text-xs'>{ t('index.watchVideo')}</span>
|
||||
<img src={arrowRight} className='rb:size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideCard;
|
||||
101
web/src/views/Index/components/QuickActions.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import modelIcon from '@/assets/images/index/model_mgt.svg'
|
||||
import spaceIcon from '@/assets/images/index/space_mgt.svg'
|
||||
import workflowIcon from '@/assets/images/index/workflow_mgt.svg'
|
||||
import userIcon from '@/assets/images/index/user_mgt.svg'
|
||||
import dataExportIcon from '@/assets/images/index/data_export.svg'
|
||||
import logIcon from '@/assets/images/index/log_mgt.svg'
|
||||
import noteIcon from '@/assets/images/index/note_mgt.svg'
|
||||
import helpCenterIcon from '@/assets/images/index/help_center.svg'
|
||||
interface QuickAction {
|
||||
key: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface QuickActionsProps {
|
||||
className?: string;
|
||||
onNavigate?: (path: string) => void;
|
||||
}
|
||||
|
||||
const QuickActions: FC<QuickActionsProps> = ({ onNavigate }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const quickActions: QuickAction[] = [
|
||||
{
|
||||
key: 'model-management',
|
||||
icon: modelIcon,
|
||||
title: t('quickActions.modelManagement'),
|
||||
onClick: () => onNavigate?.('/model-management')
|
||||
},
|
||||
{
|
||||
key: 'space-management',
|
||||
icon: spaceIcon,
|
||||
title: t('quickActions.spaceManagement'),
|
||||
onClick: () => onNavigate?.('/spce')
|
||||
},
|
||||
{
|
||||
key: 'workflow-orchestration',
|
||||
icon: workflowIcon,
|
||||
title: t('quickActions.workflowOrchestration'),
|
||||
onClick: () => onNavigate?.('/workflow')
|
||||
},
|
||||
{
|
||||
key: 'user-management',
|
||||
icon: userIcon,
|
||||
title: t('quickActions.userManagement'),
|
||||
onClick: () => onNavigate?.('/user-management')
|
||||
},
|
||||
{
|
||||
key: 'data-export',
|
||||
icon: dataExportIcon,
|
||||
title: t('quickActions.dataExport'),
|
||||
onClick: () => onNavigate?.('/')
|
||||
},
|
||||
{
|
||||
key: 'log-query',
|
||||
icon: logIcon,
|
||||
title: t('quickActions.logQuery'),
|
||||
onClick: () => onNavigate?.('/log')
|
||||
},
|
||||
{
|
||||
key: 'notification-reminder',
|
||||
icon: noteIcon,
|
||||
title: t('quickActions.notificationReminder'),
|
||||
onClick: () => onNavigate?.('/notification-reminder')
|
||||
},
|
||||
|
||||
{
|
||||
key: 'help-center',
|
||||
icon: helpCenterIcon,
|
||||
title: t('quickActions.helpCenter'),
|
||||
onClick: () => onNavigate?.('/help-center')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='rb:w-full rb:p-4 rb:bg-[#FBFDFF] rb:border-1 rb:border-[#DFE4ED] rb:rounded-xl'>
|
||||
<div className='rb:flex rb:justify-start rb:text-base rb:font-medium rb:text-[#212332]'>
|
||||
{ t('quickActions.title') }
|
||||
</div>
|
||||
<div className="rb:grid rb:grid-cols-3 md:rb:grid-cols-4 rb:gap-4 rb:mt-4">
|
||||
|
||||
{quickActions.map((action) => (
|
||||
<div key={action.key}
|
||||
className="rb:flex rb:flex-col rb:items-center rb:text-center rb:cursor-pointer rb:group"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<img src={action.icon} className='rb:size-10 rb:mx-auto' />
|
||||
<div className="rb:mt-2 rb:text-xs rb:max-w-[74px] rb:text-[#5B6167] rb:text-center rb:leading-[14px]">
|
||||
{action.title}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default QuickActions;
|
||||
99
web/src/views/Index/components/TopCardList/index.module.css
Normal file
@@ -0,0 +1,99 @@
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #DFE4ED;
|
||||
padding: 16px;
|
||||
}
|
||||
.header {
|
||||
line-height: 16px;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #212332;
|
||||
font-style: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.headerTitle {
|
||||
/* Add your header title styles here */
|
||||
}
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 2px 6px 0px rgba(33, 35, 50, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* margin-right: 12px; */
|
||||
}
|
||||
.avatar img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.content {
|
||||
padding: 24px 24px 8px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: Gilroy, Gilroy;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
color: #212332;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
.content-right {
|
||||
text-align: right;
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #5F6266;
|
||||
line-height: 16px;
|
||||
font-style: normal;
|
||||
row-gap: 4px;
|
||||
}
|
||||
.trend {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-style: normal;
|
||||
padding-left: 15px;
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
.trend::before {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
.trend.up {
|
||||
color: #369F21;
|
||||
}
|
||||
.trend.up::before {
|
||||
background-image: url('@/assets/images/home/arrow_up_success.svg');
|
||||
}
|
||||
.trend.down {
|
||||
color: #FF5D34;
|
||||
}
|
||||
.trend.down::before {
|
||||
background-image: url('@/assets/images/home/arrow_down.png');
|
||||
}
|
||||
|
||||
.trend-desc {
|
||||
font-family: PingFangSC, PingFang SC;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #155EEF;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
133
web/src/views/Index/components/TopCardList/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import totalModels from '@/assets/images/index/models.svg';
|
||||
import totalSpaces from '@/assets/images/index/spaces.svg';
|
||||
import totalUsers from '@/assets/images/index/users.svg';
|
||||
import totalApps from '@/assets/images/index/apps.svg';
|
||||
import arrowUpDb from '@/assets/images/index/arrow_up_d.svg'
|
||||
import arrowDownDb from '@/assets/images/index/arrow_down_d.svg'
|
||||
import arrowUp from '@/assets/images/index/arrow_up.svg'
|
||||
import arrowDown from '@/assets/images/index/arrow_down.svg'
|
||||
import styles from './index.module.css'
|
||||
import { type DataResponse } from '@/api/common'
|
||||
|
||||
const list = [
|
||||
{
|
||||
key: 'models',
|
||||
icon: totalModels,
|
||||
value: '24',
|
||||
// trendValue: '12.5%',
|
||||
trend: 'up',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
rate:"up",
|
||||
rateValue: '12%',
|
||||
background: 'linear-gradient( 136deg, rgba(21,94,239,0.06) 0%, rgba(251,253,255,0) 100%)'
|
||||
},
|
||||
{
|
||||
key: 'spaces',
|
||||
icon: totalSpaces,
|
||||
value: '156',
|
||||
trendValue: '+8',
|
||||
trend: 'down',
|
||||
rate:"up",
|
||||
rateValue: '5.4%',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'linear-gradient( 134deg, rgba(54,159,33,0.06) 0%, rgba(251,253,255,0) 100%)',
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
icon: totalUsers,
|
||||
value: '1,248',
|
||||
trendValue: '+42',
|
||||
trend: 'up',
|
||||
rate:"up",
|
||||
rateValue: '12%',
|
||||
// trendDesc: 'thisWeek',
|
||||
background: 'linear-gradient( 136deg, rgba(77,168,255,0.06) 0%, rgba(251,253,255,0) 100%)',
|
||||
},
|
||||
{
|
||||
key: 'running_apps',
|
||||
icon: totalApps,
|
||||
value: '12.8k',
|
||||
trendValue: '98.7%',
|
||||
trend: 'up',
|
||||
rate:"down",
|
||||
rateValue: '2.1%',
|
||||
// trendDesc: 'comparedToYesterday',
|
||||
background: 'linear-gradient( 136deg, rgba(156,111,255,0.06) 0%, rgba(251,253,255,0) 100%)',
|
||||
},
|
||||
]
|
||||
const TopCardList: FC<{data?: DataResponse}> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={styles.card}
|
||||
style={{
|
||||
background: item.background,
|
||||
}}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className="rb:text-xs rb:font-medium rb:text-[#212332] rb:w-[96px]">{t(`dashboard.${'total_' + item.key}`)}</div>
|
||||
<div className={styles.avatar}><img src={item.icon} /></div>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
{item.key === 'spaces' && String(data?.active_workspaces)}
|
||||
{item.key !== 'spaces' && String(data?.[`total_${item.key}` as keyof DataResponse] || item.value || 0)}
|
||||
</div>
|
||||
<div className='rb:flex rb:flex-col rb:items-start'>
|
||||
{item.key === 'models' ? (
|
||||
<div className='rb:text-xs rb:leading-4 rb:text-[#5F6266] rb:w-[130px]'>
|
||||
{t(`dashboard.${'desc_' + item.key}`, { account: data?.total_llm, nums: data?.total_embedding })}
|
||||
</div>
|
||||
) : (<>
|
||||
<div className='rb:flex rb:items-center rb:text-xs rb:leading-4 rb:gap-1'>
|
||||
{item.key === 'spaces' && (<>
|
||||
<img src={Number(data?.new_workspaces_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/>
|
||||
<span className={Number(data?.new_workspaces_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_workspaces_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_workspaces_this_week || 0))}</span>
|
||||
</>)}
|
||||
{item.key === 'users' && (<>
|
||||
<img src={Number(data?.new_users_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/>
|
||||
<span className={Number(data?.new_users_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_users_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_users_this_week || 0))}</span>
|
||||
</>)}
|
||||
{item.key === 'running_apps' && (<>
|
||||
<img src={Number(data?.new_apps_this_week || 0) >= 0 ? arrowUpDb : arrowDownDb} className='rb:size-3'/>
|
||||
<span className={Number(data?.new_apps_this_week || 0) >= 0 ? 'rb:text-[#369F21]' : 'rb:text-[#FF5D34]'}>{Number(data?.new_apps_this_week || 0) >= 0 ? '+' : '-'}{Math.abs(Number(data?.new_apps_this_week || 0))}</span>
|
||||
</>)}
|
||||
|
||||
</div>
|
||||
<div className='rb:text-xs rb:leading-4 rb:text-[#5F6266]'>
|
||||
{t(`dashboard.${'desc_' + item.key}`)}
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{item.key === 'models' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.model_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}>
|
||||
<img src={Number(data?.model_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/>
|
||||
<span>{Math.abs(Number(data?.model_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
|
||||
</div>)}
|
||||
{item.key === 'spaces' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.workspace_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}>
|
||||
<img src={Number(data?.workspace_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/>
|
||||
<span>{Math.abs(Number(data?.workspace_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
|
||||
</div>)}
|
||||
{item.key === 'users' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.user_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}>
|
||||
<img src={Number(data?.user_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/>
|
||||
<span>{Math.abs(Number(data?.user_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
|
||||
</div>)}
|
||||
{item.key === 'running_apps' && (<div className={`rb:flex rb:max-w-40 rb:text-xs rb:mt-2 rb:items-center rb:gap-1 rb:border-1 rb:rounded rb:px-2 rb:py-0.5 ${Number(data?.app_week_growth_rate || 0) >= 0 ? 'rb:text-[#369F21] rb:border-[#369F21] rb:bg-[rgba(54, 159, 33, 0.25)]' : 'rb:text-[#FF5D34] rb:border-[#FF5D34] rb:bg-[rgba(255, 93, 52, 0.25)]'}`}>
|
||||
<img src={Number(data?.app_week_growth_rate || 0) >= 0 ? arrowUp : arrowDown} className='rb:size-3'/>
|
||||
<span>{Math.abs(Number(data?.app_week_growth_rate || 0))}% {t('dashboard.thisWeek')}</span>
|
||||
</div>)}
|
||||
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopCardList
|
||||
30
web/src/views/Index/components/VersionCard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import arrowRight from '@/assets/images/index/arrow_right.svg'
|
||||
const GuideCard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='rb:w-full rb:h-[186px] rb:p-4 rb:border-1 rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-xl'>
|
||||
<div className='rb:flex rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold'>
|
||||
{ t('index.latestUpdate')}
|
||||
</div>
|
||||
<div className='rb:flex rb:text-xs rb:text-[#5B6167] rb:leading-[18px] rb:mt-3 rb:pl-2'>
|
||||
{ t('index.latestUpdateDesc')}
|
||||
</div>
|
||||
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
|
||||
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332] '>
|
||||
<span className='rb:text-xs'>{ t('index.viewDetails')}</span>
|
||||
<img src={arrowRight} className='rb:size-4' />
|
||||
</Button>
|
||||
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332]'>
|
||||
<span className='rb:text-xs'>{ t('index.changeLog')}</span>
|
||||
<img src={arrowRight} className='rb:size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideCard;
|
||||
143
web/src/views/Index/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Row, Col, Space, Button } from 'antd';
|
||||
import TopCardList from './components/TopCardList';
|
||||
import GuideCard from './components/GuideCard';
|
||||
import VersionCard from './components/VersionCard';
|
||||
import QuickActions from './components/QuickActions';
|
||||
import bgImg from '@/assets/images/index/index_bg@2x.png'
|
||||
import Table, { type TableRef } from '@/components/Table'
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import {
|
||||
getDashboardData,
|
||||
getDashboardStatistics,
|
||||
type DataResponse } from '@/api/common';
|
||||
import { switchWorkspace } from '@/api/workspaces'
|
||||
const Index = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const [dashboardData, setDashboardData] = useState<DataResponse>();
|
||||
const tableRef = useRef<TableRef>(null);
|
||||
const tableApi = getDashboardData;
|
||||
const getDashboardCount = async () => {
|
||||
try{
|
||||
const res = await getDashboardStatistics();
|
||||
setDashboardData(res);
|
||||
}catch(e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
const handleJump = (id: string) => {
|
||||
switchWorkspace(id)
|
||||
.then(() => {
|
||||
localStorage.removeItem('user')
|
||||
navigate('/')
|
||||
})
|
||||
}
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
title: t('space.spaceName'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
|
||||
{
|
||||
title: t('space.spaceIcon'),
|
||||
dataIndex: 'icon',
|
||||
key: 'icon',
|
||||
render:(value: string, record: any) => {
|
||||
return value ? (
|
||||
<img src={value} alt="icon" className='rb:w-[24px] rb:h-[24px]' />
|
||||
) : (
|
||||
<div className='rb:w-[24px] rb:h-[24px] rb:bg-blue-500 rb:text-white rb:rounded rb:flex rb:items-center rb:justify-center rb:text-xs rb:font-medium'>
|
||||
{record.name?.charAt(0)?.toUpperCase() || '?'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('index.appCount'),
|
||||
dataIndex: 'app_count',
|
||||
key: 'app_count',
|
||||
},
|
||||
{
|
||||
title: t('index.userCount'),
|
||||
dataIndex: 'user_count',
|
||||
key: 'user_count',
|
||||
},
|
||||
{
|
||||
title: t('apiKey.createdAt'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render:(value:string) => {
|
||||
return(
|
||||
<span>{formatDateTime(Number(value) ,'YYYY-MM-DD HH:mm:ss')}</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('common.operation'),
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button onClick={() => handleJump(record.id)} color="primary" variant="text">{t('space.enterSpace')}</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
tableRef.current?.loadData();
|
||||
}, [tableApi]);
|
||||
useEffect(() => {
|
||||
getDashboardCount();
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<div className="rb:pb-[24px]">
|
||||
<Row className="rb:mt-[16px]" gutter={16}>
|
||||
<Col span={19}>
|
||||
<div className='rb:flex-col rb:w-full rb:h-[120px] rb:mb-4 rb:p-6 rb:leading-[30px]' style={{backgroundImage: `url(${bgImg})`, backgroundSize: '100% 100%'}}>
|
||||
<div className='rb:flex rb:text-[22px] rb:text-[#0041C3] rb:font-semibold'>
|
||||
{ t('index.spaceTitle' )}
|
||||
</div>
|
||||
<div className='rb:flex rb:mt-2 rb:text-xs rb:leading-[18px] rb:text-[#5F6266] rb:max-w-[560px]'>
|
||||
{ t('index.spaceSubTitle' )}
|
||||
</div>
|
||||
</div>
|
||||
{/* 统计卡片 */}
|
||||
<TopCardList data={dashboardData} />
|
||||
<div className="rb:rounded rb:max-h-[calc(100%-100px)] rb:overflow-y-auto rb:mt-4">
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={tableApi}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={5}>
|
||||
{/* 引导 */}
|
||||
<GuideCard />
|
||||
<div className='rb:w-full rb:mt-4 '>
|
||||
<VersionCard />
|
||||
</div>
|
||||
{/* 快捷操作 */}
|
||||
<div className='rb:w-full rb:mt-4'>
|
||||
<QuickActions />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index
|
||||
13
web/src/views/Index/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface TopCardListProps {
|
||||
title:string;
|
||||
description:string;
|
||||
icon: Element;
|
||||
number: number;
|
||||
label: string;
|
||||
}
|
||||
export interface DashboardData {
|
||||
total_models:number;
|
||||
total_spaces:number;
|
||||
total_users:number;
|
||||
total_apps_runs: string;
|
||||
}
|
||||
@@ -2,18 +2,18 @@
|
||||
import { useEffect, useState, useRef, useCallback, type FC } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Switch, Button, Dropdown, Space, Modal, message } from 'antd';
|
||||
import { Switch, Button, Dropdown, Space, Modal, message, Radio } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import Table, { type TableRef } from '@/components/Table'
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { MoreOutlined, DeploymentUnitOutlined, BarsOutlined } from '@ant-design/icons';
|
||||
import folderIcon from '@/assets/images/knowledgeBase/folder.png';
|
||||
import textIcon from '@/assets/images/knowledgeBase/text.png';
|
||||
import editIcon from '@/assets/images/knowledgeBase/edit.png';
|
||||
import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png';
|
||||
import imageIcon from '@/assets/images/knowledgeBase/image.png'
|
||||
// import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png';
|
||||
// import imageIcon from '@/assets/images/knowledgeBase/image.png'
|
||||
import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase';
|
||||
import {
|
||||
type CreateModalRef,
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
type CreateFolderModalRef,
|
||||
type CreateSetModalRef,
|
||||
type ShareModalRef,
|
||||
type CreateDatasetModalRef,type FolderFormData,
|
||||
type KnowledgeBaseDocumentData,
|
||||
type CreateDatasetModalRef,
|
||||
type FolderFormData,
|
||||
type KnowledgeBaseDocumentData,
|
||||
type KnowledgeBaseFormData,
|
||||
} from '@/views/KnowledgeBase/types';
|
||||
import RecallTestDrawer from '../components/RecallTestDrawer';
|
||||
import CreateFolderModal from '../components/CreateFolderModal';
|
||||
@@ -34,7 +36,7 @@ import CreateDatasetModal from '../components/CreateDatasetModal';
|
||||
import CreateImageDataset from '../components/CreateImageDataset';
|
||||
import FolderTree, { type TreeNodeData } from '../components/FolderTree';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
import KnowledgeGraphCard from '../components/KnowledgeGraphCard';
|
||||
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
|
||||
import './Private.css'
|
||||
const { confirm } = Modal
|
||||
@@ -68,7 +70,7 @@ const Private: FC = () => {
|
||||
const datasetModalRef = useRef<CreateDatasetModalRef>(null);
|
||||
const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0);
|
||||
const [autoExpandPath, setAutoExpandPath] = useState<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
const [isGraph, setIsGraph] = useState(false);
|
||||
const { updateBreadcrumbs } = useBreadcrumbManager({
|
||||
breadcrumbType: 'detail',
|
||||
// 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面)
|
||||
@@ -376,9 +378,37 @@ const Private: FC = () => {
|
||||
|
||||
// 处理开关
|
||||
const onChange = (checked: boolean) => {
|
||||
updateKnowledgeBase(knowledgeBaseId || '', {
|
||||
if (!knowledgeBase) return;
|
||||
|
||||
// 构造完整的更新数据,保留现有配置
|
||||
const updateData: KnowledgeBaseFormData = {
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description,
|
||||
embedding_id: knowledgeBase.embedding_id,
|
||||
llm_id: knowledgeBase.llm_id,
|
||||
image2text_id: knowledgeBase.image2text_id,
|
||||
reranker_id: knowledgeBase.reranker_id,
|
||||
permission_id: knowledgeBase.permission_id,
|
||||
type: knowledgeBase.type,
|
||||
status: checked ? 1 : 0,
|
||||
});
|
||||
parser_config: knowledgeBase.parser_config || {
|
||||
chunk_token_num: 512,
|
||||
delimiter: '\n',
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
graphrag: {
|
||||
use_graphrag: false,
|
||||
scene_name: '',
|
||||
entity_types: [],
|
||||
method: '',
|
||||
resolution: false,
|
||||
community: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateKnowledgeBase(knowledgeBaseId || '', updateData);
|
||||
console.log(`switch to ${checked}`);
|
||||
};
|
||||
// 处理搜索
|
||||
@@ -626,17 +656,15 @@ const Private: FC = () => {
|
||||
}
|
||||
|
||||
const handleRefreshTable = () => {
|
||||
debugger
|
||||
// 刷新表格数据
|
||||
tableRef.current?.loadData();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div className="rb:flex rb:h-full rb:gap-4">
|
||||
{folder && (
|
||||
<div className="rb:w-80 rb:flex-shrink-0 rb:h-[calc(100%+40px)] rb:mt-[-16px] rb:border-r rb:border-[#EAECEE] rb:p-4 rb:bg-transparent">
|
||||
<div className="rb:w-64 rb:flex-shrink-0 rb:h-[calc(100%+40px)] rb:mt-[-16px] rb:border-r rb:border-[#EAECEE] rb:p-4 rb:bg-transparent">
|
||||
<FolderTree
|
||||
multiple
|
||||
className="customTree"
|
||||
@@ -678,6 +706,14 @@ const Private: FC = () => {
|
||||
<div className='rb:flex rb:items-center rb:justify-between rb:mb-4'>
|
||||
<SearchInput placeholder={t('knowledgeBase.search')} onSearch={handleSearch} />
|
||||
<div className='rb:flex-1 rb:flex rb:items-center rb:justify-end rb:gap-2.5'>
|
||||
<Radio.Group value={isGraph} onChange={(e) => setIsGraph(e.target.value)}>
|
||||
<Radio.Button value={false} >
|
||||
<BarsOutlined />
|
||||
</Radio.Button>
|
||||
<Radio.Button value={true} >
|
||||
<DeploymentUnitOutlined />
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Button onClick={handleShare}>{t('knowledgeBase.share')}</Button>
|
||||
<Button onClick={handleRecallTest}>{t('knowledgeBase.recallTest')}</Button>
|
||||
<Button onClick={handleSetting}>{t('knowledgeBase.knowledgeBase')} {t('knowledgeBase.setting')}</Button>
|
||||
@@ -688,14 +724,21 @@ const Private: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:rounded rb:max-h-[calc(100%-100px)] rb:overflow-y-auto">
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={tableApi}
|
||||
apiParams={query as Record<string, unknown>}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
scrollX={1500}
|
||||
/>
|
||||
{isGraph ? (
|
||||
<KnowledgeGraphCard
|
||||
knowledgeBase={knowledgeBase}
|
||||
onRebuildGraph={() => modalRef.current?.handleOpen(knowledgeBase, 'rebuild')}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={tableApi}
|
||||
apiParams={query as Record<string, unknown>}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
scrollX={1500}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RecallTestDrawer
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import { Form, Input, Select, Modal } from 'antd';
|
||||
import { Form, Input, Select, Modal, Tabs, Switch, Radio, Button,message } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KnowledgeBaseListItem, KnowledgeBaseFormData, CreateModalRef, CreateModalRefProps } from '@/views/KnowledgeBase/types';
|
||||
import { getModelTypeList, getModelList, createKnowledgeBase, updateKnowledgeBase } from '@/api/knowledgeBase'
|
||||
import {
|
||||
getModelTypeList,
|
||||
getModelList,
|
||||
createKnowledgeBase,
|
||||
updateKnowledgeBase,
|
||||
getKnowledgeGraphEntityTypes
|
||||
} from '@/api/knowledgeBase'
|
||||
import RbModal from '@/components/RbModal'
|
||||
const { TextArea } = Input;
|
||||
const { confirm } = Modal
|
||||
@@ -14,22 +20,106 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [modelTypeList, setModelTypeList] = useState<string[]>([]);
|
||||
const [modelOptionsByType, setModelOptionsByType] = useState<Record<string, { label: string; value: string }[]>>({});
|
||||
const [datasets, setDatasets] = useState<KnowledgeBaseListItem | null>(null);
|
||||
const [currentType, setCurrentType] = useState<'General' | 'Web' | 'Third-party' | 'Folder'>('General');
|
||||
const [form] = Form.useForm<KnowledgeBaseFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
const [generatingEntityTypes, setGeneratingEntityTypes] = useState(false);
|
||||
const [isRebuildMode, setIsRebuildMode] = useState(false);
|
||||
|
||||
// 监听 parser_config.graphrag 相关字段的变化
|
||||
const parserConfig = Form.useWatch('parser_config', form);
|
||||
const graphragConfig = parserConfig?.graphrag;
|
||||
const enableKnowledgeGraph = graphragConfig?.use_graphrag || false;
|
||||
const entityTypes = graphragConfig?.entity_types || '';
|
||||
const entityNormalization = graphragConfig?.resolution || false;
|
||||
const communityReportGeneration = graphragConfig?.community || false;
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setDatasets(null);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
setActiveTab('basic');
|
||||
setIsRebuildMode(false); // 重置重建模式标识
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
// 生成实体类型的函数
|
||||
const generateEntityTypes = async () => {
|
||||
const sceneName = form.getFieldValue(['parser_config', 'graphrag', 'scene_name']);
|
||||
if (!sceneName) {
|
||||
// 可以添加提示用户输入场景名称
|
||||
messageApi.error(t('knowledgeBase.enterScenarioName'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了 LLM 模型
|
||||
const llmId = form.getFieldValue('llm_id');
|
||||
if (!llmId) {
|
||||
// 跳转到基础配置页
|
||||
setActiveTab('basic');
|
||||
messageApi.error(t('knowledgeBase.pleaseSelectLLMModel'));
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneratingEntityTypes(true);
|
||||
try {
|
||||
// 这里应该调用实际的API接口
|
||||
// const user = JSON.parse(localStorage.getItem('user') as any);
|
||||
//datasets?.id || datasets?.parent_id || user?.current_workspace_id,
|
||||
const params = {
|
||||
scenario: sceneName,
|
||||
llm_id: llmId
|
||||
};
|
||||
const response = await getKnowledgeGraphEntityTypes(params);
|
||||
// 模拟API调用
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 处理API响应数据
|
||||
console.log('API Response:', response); // 调试日志
|
||||
|
||||
// 检查响应结构 - API直接返回字符串
|
||||
if (response && typeof response === 'string' && response.trim()) {
|
||||
// 将逗号分隔的字符串转换为换行分隔的格式以便在TextArea中显示
|
||||
const entityTypesString = response.replace(/,\s*/g, '\n');
|
||||
console.log('Converted entity types:', entityTypesString); // 调试日志
|
||||
|
||||
const currentGraphrag = form.getFieldValue(['parser_config', 'graphrag']) || {};
|
||||
const updatedGraphrag = {
|
||||
...currentGraphrag,
|
||||
entity_types: entityTypesString
|
||||
};
|
||||
|
||||
console.log('Updating form with:', updatedGraphrag); // 调试日志
|
||||
|
||||
// 使用更直接的方式更新表单字段
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
|
||||
// 强制触发表单重新渲染
|
||||
form.validateFields([['parser_config', 'graphrag', 'entity_types']]);
|
||||
|
||||
// 额外的强制更新机制
|
||||
setTimeout(() => {
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
}, 100);
|
||||
|
||||
messageApi.success(t('knowledgeBase.generateEntityTypesSuccess'));
|
||||
} else {
|
||||
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + ':' + t('knowledgeBase.unknownError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('knowledgeBase.generateEntityTypesFailed') + ':', error);
|
||||
} finally {
|
||||
setGeneratingEntityTypes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const typeToFieldKey = (type: string): string => {
|
||||
switch ((type || '').toLowerCase()) {
|
||||
case 'embedding':
|
||||
@@ -89,6 +179,30 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
type: type || record.type || currentType,
|
||||
status: record.status,
|
||||
};
|
||||
|
||||
// 处理 parser_config 配置数据,如果没有则设置默认值
|
||||
baseValues.parser_config = record.parser_config || {
|
||||
graphrag: {
|
||||
use_graphrag: false,
|
||||
scene_name: '',
|
||||
entity_types: [] as any,
|
||||
method: 'general',
|
||||
resolution: false,
|
||||
community: false,
|
||||
}
|
||||
};
|
||||
|
||||
// 如果存在 entity_types,转换为换行分隔格式用于 TextArea 显示
|
||||
if (baseValues.parser_config.graphrag.entity_types) {
|
||||
if (Array.isArray(baseValues.parser_config.graphrag.entity_types)) {
|
||||
// 如果是数组格式,转换为换行分隔字符串
|
||||
(baseValues.parser_config.graphrag as any).entity_types = baseValues.parser_config.graphrag.entity_types.join('\n');
|
||||
} else if (typeof baseValues.parser_config.graphrag.entity_types === 'string') {
|
||||
// 如果是逗号分隔字符串格式,转换为换行分隔字符串(兼容旧数据)
|
||||
(baseValues.parser_config.graphrag as any).entity_types = (baseValues.parser_config.graphrag.entity_types as string).replace(/,\s*/g, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldsValue(baseValues);
|
||||
};
|
||||
|
||||
@@ -112,6 +226,15 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
setDatasets(record || null);
|
||||
const nextType = type || currentType;
|
||||
setCurrentType(nextType as any);
|
||||
setIsRebuildMode(type === 'rebuild'); // 设置重建模式标识
|
||||
|
||||
// 如果是重建模式,默认切换到知识图谱标签页
|
||||
if (type === 'rebuild') {
|
||||
setActiveTab('knowledgeGraph');
|
||||
} else {
|
||||
setActiveTab('basic');
|
||||
}
|
||||
|
||||
setBaseFields(record || null, nextType);
|
||||
getTypeList(record || null);
|
||||
setVisible(true);
|
||||
@@ -142,12 +265,26 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
const formValues = form.getFieldsValue();
|
||||
|
||||
// 处理 entity_types 格式转换:从换行分隔字符串转换为字符串数组
|
||||
if (formValues.parser_config && formValues.parser_config.graphrag && formValues.parser_config.graphrag.entity_types) {
|
||||
const entityTypesString = formValues.parser_config.graphrag.entity_types as any as string;
|
||||
const entityTypesArray = entityTypesString
|
||||
.split('\n')
|
||||
.map((item: string) => item.trim())
|
||||
.filter((item: string) => item.length > 0);
|
||||
formValues.parser_config.graphrag.entity_types = entityTypesArray;
|
||||
}
|
||||
|
||||
const payload: KnowledgeBaseFormData = {
|
||||
...formValues,
|
||||
type: formValues.type || currentType,
|
||||
permission_id: formValues.permission_id || 'Private',
|
||||
parent_id: datasets?.parent_id || undefined,
|
||||
};
|
||||
|
||||
console.log('Saving payload:', payload); // 调试日志
|
||||
|
||||
const submit = datasets?.id
|
||||
? updateKnowledgeBase(datasets.id, payload)
|
||||
: createKnowledgeBase(payload);
|
||||
@@ -194,6 +331,9 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
// 根据 type 获取标题
|
||||
const getTitle = () => {
|
||||
if (isRebuildMode) {
|
||||
return t('knowledgeBase.rebuildGraph') + ' - ' + (datasets?.name || '');
|
||||
}
|
||||
if (datasets?.id) {
|
||||
return t('knowledgeBase.edit') + ' ' + datasets.name;
|
||||
}
|
||||
@@ -205,6 +345,192 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
const dynamicTypeList = useMemo(() => modelTypeList.filter((tp) => (modelOptionsByType[tp] || []).length), [modelTypeList, modelOptionsByType]);
|
||||
|
||||
// 基础配置表单内容
|
||||
const renderBasicConfig = () => (
|
||||
<>
|
||||
{!datasets?.id && (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('knowledgeBase.createForm.name')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.createForm.name')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')}>
|
||||
<TextArea rows={2} placeholder={t('knowledgeBase.createForm.description')} />
|
||||
</Form.Item>
|
||||
|
||||
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 当 tp 为 'llm' 时,合并 llm 和 chat 的选项
|
||||
const options = tp.toLowerCase() === 'llm'
|
||||
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
|
||||
: modelOptionsByType[tp] || [];
|
||||
return (
|
||||
<Form.Item
|
||||
key={tp}
|
||||
name={fieldKey as keyof KnowledgeBaseFormData}
|
||||
label={t(`knowledgeBase.createForm.${fieldKey}`) + ' ' + 'model'}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.modelRequired') }]}
|
||||
>
|
||||
<Select
|
||||
options={options}
|
||||
placeholder={t(`knowledgeBase.createForm.${fieldKey}`)}
|
||||
allowClear={false}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChange(value, tp)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
// 知识图谱配置表单内容
|
||||
const renderKnowledgeGraphConfig = () => (
|
||||
<>
|
||||
<div className={`rb:flex rb:w-full rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
enableKnowledgeGraph
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.enableKnowledgeGraph')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.enableKnowledgeGraphTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'use_graphrag']}
|
||||
label=''
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{enableKnowledgeGraph && (
|
||||
<>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium rb:mb-4'>
|
||||
{t('knowledgeBase.graphConfig')}
|
||||
</div>
|
||||
{/* 场景名称 */}
|
||||
<div className='rb:flex rb:items-center rb:gap-2'>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'scene_name']}
|
||||
label={t('knowledgeBase.sceneName')}
|
||||
className='rb:w-full rb:min-w-[240px]'
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') + t('knowledgeBase.sceneName') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.sceneNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={generatingEntityTypes}
|
||||
onClick={generateEntityTypes}
|
||||
className='rb:mt-1'
|
||||
>
|
||||
{!(entityTypes as any as string) || (entityTypes as any as string).trim() === ''
|
||||
? t('knowledgeBase.generateEntityTypes')
|
||||
: t('knowledgeBase.regenerateEntityTypes')
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体类型 */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'entity_types']}
|
||||
label={t('knowledgeBase.entityTypes')}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={t('knowledgeBase.entityTypesPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 实体归一化 */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
entityNormalization
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.entityNormalization')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.entityNormalizationTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'resolution']}
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体方法 */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'method']}
|
||||
label={t('knowledgeBase.entityMethod')}
|
||||
initialValue="general"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="general">{t('knowledgeBase.entityMethodGeneral')}</Radio>
|
||||
<Radio value="light">{t('knowledgeBase.entityMethodLight')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 社区报告生成 */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
communityReportGeneration
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.communityReportGeneration')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.communityReportGenerationTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'community']}
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Tabs 配置
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'basic',
|
||||
label: t('knowledgeBase.basicConfig'),
|
||||
children: renderBasicConfig(),
|
||||
},
|
||||
{
|
||||
key: 'knowledgeGraph',
|
||||
label: t('knowledgeBase.knowledgeGraph'),
|
||||
children: renderKnowledgeGraphConfig(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={getTitle()}
|
||||
@@ -220,48 +546,25 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
initialValues={{
|
||||
permission_id: 'Private', // 设置 permission_id 的默认值
|
||||
type: currentType,
|
||||
parser_config: {
|
||||
graphrag: {
|
||||
use_graphrag: false, // 默认不启用知识图谱
|
||||
scene_name: '', // 场景名称
|
||||
entity_types: '' as any, // 实体类型(界面上显示为字符串,保存时转为数组)
|
||||
method: 'general', // 默认使用通用方法
|
||||
resolution: false, // 默认不启用实体归一化
|
||||
community: false, // 默认不生成社区报告
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <div className="rb:text-[14px] rb:font-medium rb:text-[#5B6167] rb:mb-[16px]">{t('model.basicParameters')}</div> */}
|
||||
{!datasets?.id && (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('knowledgeBase.createForm.name')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.createForm.name')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')}>
|
||||
<TextArea rows={2} placeholder={t('knowledgeBase.createForm.description')} />
|
||||
</Form.Item>
|
||||
|
||||
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 当 tp 为 'llm' 时,合并 llm 和 chat 的选项
|
||||
const options = tp.toLowerCase() === 'llm'
|
||||
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
|
||||
: modelOptionsByType[tp] || [];
|
||||
return (
|
||||
<Form.Item
|
||||
key={tp}
|
||||
name={fieldKey as keyof KnowledgeBaseFormData}
|
||||
label={t(`knowledgeBase.createForm.${fieldKey}`) + ' ' + 'model'}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.modelRequired') }]}
|
||||
>
|
||||
<Select
|
||||
options={options}
|
||||
placeholder={t(`knowledgeBase.createForm.${fieldKey}`)}
|
||||
allowClear={false}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChange(value, tp)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
/>
|
||||
</Form>
|
||||
{contextHolder}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
451
web/src/views/KnowledgeBase/components/KnowledgeGraph.tsx
Normal file
@@ -0,0 +1,451 @@
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Col } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import zoom from '@/assets/images/userMemory/zoom.svg'
|
||||
import drag from '@/assets/images/userMemory/drag.svg'
|
||||
import pointer from '@/assets/images/userMemory/pointer.svg'
|
||||
import empty from '@/assets/images/userMemory/empty.svg'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
// 知识图谱数据类型定义
|
||||
export interface KnowledgeNode {
|
||||
id: string
|
||||
entity_name: string
|
||||
entity_type: string
|
||||
description: string
|
||||
pagerank: number
|
||||
source_id: string[]
|
||||
// ECharts 需要的属性
|
||||
name: string
|
||||
category: number
|
||||
symbolSize: number
|
||||
itemStyle: {
|
||||
color: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface KnowledgeEdge {
|
||||
src_id: string
|
||||
tgt_id: string
|
||||
description: string
|
||||
keywords: string[]
|
||||
weight: number
|
||||
source_id: string[]
|
||||
source: string
|
||||
target: string
|
||||
// ECharts 需要的属性
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphData {
|
||||
directed: boolean
|
||||
multigraph: boolean
|
||||
graph: {
|
||||
source_id: string[]
|
||||
}
|
||||
nodes: KnowledgeNode[]
|
||||
edges: KnowledgeEdge[]
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphResponse {
|
||||
graph: KnowledgeGraphData
|
||||
mind_map: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface KnowledgeGraphProps {
|
||||
data?: KnowledgeGraphResponse
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const operations = [
|
||||
{ name: 'click', icon: pointer },
|
||||
{ name: 'drag', icon: drag },
|
||||
{ name: 'zoom', icon: zoom },
|
||||
]
|
||||
|
||||
// 预定义的颜色调色板
|
||||
const colorPalette = [
|
||||
'#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21',
|
||||
'#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6',
|
||||
'#3498DB', '#1ABC9C', '#F39C12', '#D35400', '#C0392B',
|
||||
'#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22'
|
||||
]
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
const generateEntityTypeColors = (entityTypes: string[]): Record<string, string> => {
|
||||
const colorMap: Record<string, string> = {}
|
||||
entityTypes.forEach((type, index) => {
|
||||
colorMap[type] = colorPalette[index % colorPalette.length]
|
||||
})
|
||||
return colorMap
|
||||
}
|
||||
|
||||
const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null)
|
||||
const resizeScheduledRef = useRef(false)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const [nodes, setNodes] = useState<KnowledgeNode[]>([])
|
||||
const [links, setLinks] = useState<KnowledgeEdge[]>([])
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<KnowledgeNode | null>(null)
|
||||
const [entityTypeColors, setEntityTypeColors] = useState<Record<string, string>>({})
|
||||
|
||||
// 弹框拖动相关状态
|
||||
const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
|
||||
// 拖动处理函数
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
setIsDragging(true)
|
||||
setDragStart({
|
||||
x: e.clientX - modalPosition.x,
|
||||
y: e.clientY - modalPosition.y
|
||||
})
|
||||
}, [modalPosition])
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const newX = e.clientX - dragStart.x
|
||||
const newY = e.clientY - dragStart.y
|
||||
|
||||
// 限制拖动范围,确保弹框不会超出容器
|
||||
const container = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (container && modalRef.current) {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const modalRect = modalRef.current.getBoundingClientRect()
|
||||
|
||||
const maxX = containerRect.width - modalRect.width
|
||||
const maxY = containerRect.height - modalRect.height
|
||||
|
||||
setModalPosition({
|
||||
x: Math.max(0, Math.min(newX, maxX)),
|
||||
y: Math.max(0, Math.min(newY, maxY))
|
||||
})
|
||||
}
|
||||
}, [isDragging, dragStart])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
// 添加全局鼠标事件监听
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging, handleMouseMove, handleMouseUp])
|
||||
|
||||
// 关闭弹框
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
}, [])
|
||||
|
||||
// 处理知识图谱数据
|
||||
const processGraphData = useCallback(() => {
|
||||
if (!data?.graph) {
|
||||
setNodes([])
|
||||
setLinks([])
|
||||
setCategories([])
|
||||
setSelectedNode(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { nodes: rawNodes, edges: rawEdges } = data.graph
|
||||
const processedNodes: KnowledgeNode[] = []
|
||||
const processedEdges: KnowledgeEdge[] = []
|
||||
|
||||
// 获取所有实体类型
|
||||
const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))]
|
||||
const categoryMap = entityTypes.reduce((acc, type, index) => {
|
||||
acc[type] = index
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes)
|
||||
setEntityTypeColors(dynamicEntityTypeColors)
|
||||
|
||||
// 计算每个节点的连接数
|
||||
const connectionCount: Record<string, number> = {}
|
||||
rawEdges.forEach(edge => {
|
||||
// 使用 src_id 和 tgt_id 计算连接数
|
||||
connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1
|
||||
connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1
|
||||
})
|
||||
|
||||
// 处理节点数据
|
||||
rawNodes.forEach(node => {
|
||||
const connections = connectionCount[node.id] || 0
|
||||
const categoryIndex = categoryMap[node.entity_type] || 0
|
||||
|
||||
// 根据 pagerank 和连接数计算节点大小
|
||||
let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2))
|
||||
|
||||
processedNodes.push({
|
||||
...node,
|
||||
name: node.entity_name,
|
||||
category: categoryIndex,
|
||||
symbolSize,
|
||||
itemStyle: {
|
||||
color: dynamicEntityTypeColors[node.entity_type] || colorPalette[0]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 处理边数据
|
||||
rawEdges.forEach(edge => {
|
||||
// 注意:根据数据结构,source 和 target 字段可能与 src_id 和 tgt_id 相反
|
||||
// 我们使用 src_id 和 tgt_id 作为正确的连接关系
|
||||
processedEdges.push({
|
||||
...edge, // 保留所有原始字段
|
||||
source: edge.src_id, // 使用 src_id 作为源节点
|
||||
target: edge.tgt_id, // 使用 tgt_id 作为目标节点
|
||||
value: edge.weight || 1
|
||||
})
|
||||
})
|
||||
|
||||
// 验证节点ID和边的连接
|
||||
const nodeIds = new Set(processedNodes.map(n => n.id))
|
||||
const validEdges = processedEdges.filter(edge => {
|
||||
const sourceExists = nodeIds.has(edge.source)
|
||||
const targetExists = nodeIds.has(edge.target)
|
||||
if (!sourceExists || !targetExists) {
|
||||
console.warn('Invalid edge:', edge, 'Source exists:', sourceExists, 'Target exists:', targetExists)
|
||||
}
|
||||
return sourceExists && targetExists
|
||||
})
|
||||
|
||||
// 调试信息
|
||||
console.log('Total nodes:', processedNodes.length)
|
||||
console.log('Total edges:', processedEdges.length)
|
||||
console.log('Valid edges:', validEdges.length)
|
||||
console.log('Node IDs:', Array.from(nodeIds).slice(0, 5))
|
||||
console.log('Edge sample:', validEdges.slice(0, 3))
|
||||
|
||||
// 设置分类
|
||||
const processedCategories = entityTypes.map(type => ({ name: type }))
|
||||
|
||||
setNodes(processedNodes)
|
||||
setLinks(validEdges) // 只使用有效的边
|
||||
setCategories(processedCategories)
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
processGraphData()
|
||||
}, [processGraphData])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartRef.current && !resizeScheduledRef.current) {
|
||||
resizeScheduledRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
chartRef.current?.getEchartsInstance().resize()
|
||||
resizeScheduledRef.current = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
resizeObserver.observe(chartElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<RbCard
|
||||
title={t('knowledgeBase.knowledgeGraph')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
<div className="rb:h-124 rb:relative">
|
||||
{loading ? (
|
||||
<div className="rb:h-full rb:flex rb:items-center rb:justify-center">
|
||||
<div className="rb:text-[#5B6167]">加载中...</div>
|
||||
</div>
|
||||
) : nodes.length === 0 ? (
|
||||
<Empty className="rb:h-full" />
|
||||
) : (
|
||||
<>
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
colors: Object.values(entityTypeColors),
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter: (params: any) => {
|
||||
if (params.dataType === 'node') {
|
||||
const node = params.data as KnowledgeNode
|
||||
return `
|
||||
<div>
|
||||
<div><strong>${node.entity_name}</strong></div>
|
||||
<div>类型: ${node.entity_type}</div>
|
||||
<div>重要度: ${(node.pagerank * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
`
|
||||
} else if (params.dataType === 'edge') {
|
||||
const edge = params.data as KnowledgeEdge
|
||||
return `
|
||||
<div>
|
||||
<div><strong>关系</strong></div>
|
||||
<div>权重: ${edge.weight}</div>
|
||||
<div>${edge.description}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: categories.map(cat => cat.name),
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
data: nodes,
|
||||
links: links,
|
||||
categories: categories,
|
||||
roam: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '{b}',
|
||||
fontSize: 12
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5B6167',
|
||||
curveness: 0.3,
|
||||
width: 2, // 固定线宽,避免函数问题
|
||||
opacity: 0.8
|
||||
},
|
||||
force: {
|
||||
repulsion: 300,
|
||||
edgeLength: 150,
|
||||
gravity: 0.1,
|
||||
layoutAnimation: true,
|
||||
preventOverlap: true
|
||||
},
|
||||
selectedMode: 'single',
|
||||
draggable: true,
|
||||
animationDurationUpdate: 0,
|
||||
select: {
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
shadowBlur: 10,
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'adjacency',
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '496px', width: '100%' }}
|
||||
notMerge={false}
|
||||
lazyUpdate={true}
|
||||
onEvents={{
|
||||
click: (params: { dataType: string; data: KnowledgeNode }) => {
|
||||
if (params.dataType === 'node') {
|
||||
console.log('Knowledge node clicked:', params.data)
|
||||
setSelectedNode(params.data)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 实体详情弹框 */}
|
||||
{selectedNode && (
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="rb:absolute rb:bg-white rb:border rb:border-[#EBEBEB] rb:rounded-[12px] rb:shadow-lg rb:p-4 rb:w-80 rb:z-10"
|
||||
style={{
|
||||
left: modalPosition.x,
|
||||
top: modalPosition.y,
|
||||
cursor: isDragging ? 'grabbing' : 'grab'
|
||||
}}
|
||||
>
|
||||
{/* 弹框头部 - 可拖动区域 */}
|
||||
<div
|
||||
className="rb:flex rb:items-center rb:justify-between rb:mb-3 rb:pb-2 rb:border-b rb:border-[#EBEBEB] rb:cursor-grab"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:text-[#1A1A1A]">
|
||||
{t('knowledgeBase.entityDetails')}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="rb:w-6 rb:h-6 rb:flex rb:items-center rb:justify-center rb:text-[#5B6167] hover:rb:text-[#1A1A1A] hover:rb:bg-[#F0F3F8] rb:rounded rb:transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 弹框内容 */}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:mb-4">
|
||||
<div className="rb:text-[16px] rb:mb-2">{selectedNode.entity_name}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-2">
|
||||
<span className="rb:inline-block rb:px-2 rb:py-1 rb:bg-[#F0F3F8] rb:rounded rb:mr-2">
|
||||
{selectedNode.entity_type}
|
||||
</span>
|
||||
<span>重要度: {(selectedNode.pagerank * 100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:font-medium rb:mb-4">
|
||||
{t('knowledgeBase.entityDescription')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-5">
|
||||
{selectedNode.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('knowledgeBase.sourceDocuments')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{selectedNode.source_id.length} 个文档
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-6 rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
{operations.map((item) => (
|
||||
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-5">
|
||||
<img src={item.icon} className="rb:w-5 rb:h-5 rb:mr-1" />
|
||||
{t(`userMemory.${item.name}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(KnowledgeGraph)
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-12-30 15:07:37
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2026-01-05 16:18:53
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import KnowledgeGraph, { type KnowledgeGraphResponse } from './KnowledgeGraph'
|
||||
import { getKnowledgeGraph } from '@/api/knowledgeBase';
|
||||
import { type KnowledgeBase } from '../types';
|
||||
import Empty from '@/components/Empty';
|
||||
interface KnowledgeGraphCardProps {
|
||||
knowledgeBase?: KnowledgeBase;
|
||||
onRebuildGraph?: () => void; // 添加重建图谱的回调函数
|
||||
}
|
||||
|
||||
const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase, onRebuildGraph }) => {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState<KnowledgeGraphResponse | undefined>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const handleRebuildGraph = () => {
|
||||
// 调用父组件传递的回调函数来打开CreateModal并传递重建标识
|
||||
if (onRebuildGraph) {
|
||||
onRebuildGraph();
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!knowledgeBase?.id) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getKnowledgeGraph(knowledgeBase?.id)
|
||||
setData(res as KnowledgeGraphResponse)
|
||||
} catch (error) {
|
||||
console.error('获取知识图谱数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [knowledgeBase?.id])
|
||||
|
||||
return (
|
||||
<div className='rb:flex rb:w-full rb:flex-col'>
|
||||
<div className='rb:flex rb:w-full rb:flex-col rb:p-4'>
|
||||
<div className='rb:w-full rb:text-lg rb:font-medium rb:text-[#212332] rb:leading-6'>
|
||||
{t('knowledgeBase.graphTitle')}
|
||||
</div>
|
||||
<div className='rb:w-full rb:text-xs rb:text-[#5B6167] rb:leading-4 rb:mt-2'>
|
||||
{t('knowledgeBase.graphTips')}
|
||||
</div>
|
||||
<div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:mt-4'>
|
||||
<span className='rb:text-base rb:font-medium rb:text-[#212332]'>
|
||||
{knowledgeBase?.parser_config?.graphrag?.scene_name}
|
||||
</span>
|
||||
<Button type="primary" onClick={() => handleRebuildGraph()}>
|
||||
{t('knowledgeBase.rebuildGraph')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rb:p-4 rb:pt-0'>
|
||||
{knowledgeBase?.parser_config?.graphrag?.use_graphrag ? (<KnowledgeGraph data={data} loading={loading} />) : <Empty />}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KnowledgeGraphCard
|
||||
163
web/src/views/KnowledgeBase/components/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# KnowledgeGraph 组件
|
||||
|
||||
基于 ECharts 的知识图谱可视化组件,用于展示知识库中实体之间的关系网络。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎯 **交互式图谱**: 支持节点点击、拖拽、缩放等交互操作
|
||||
- 🎨 **实体分类**: 根据实体类型自动分配颜色和图例
|
||||
- 📊 **智能布局**: 基于力导向算法的自动布局
|
||||
- 🔍 **详情展示**: 点击节点查看实体详细信息
|
||||
- 📱 **响应式设计**: 自适应容器大小变化
|
||||
- 🌐 **国际化支持**: 支持中英文切换
|
||||
|
||||
## 数据结构
|
||||
|
||||
### KnowledgeGraphResponse
|
||||
```typescript
|
||||
interface KnowledgeGraphResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
graph: KnowledgeGraphData
|
||||
mind_map: Record<string, unknown>
|
||||
}
|
||||
error: string
|
||||
time: number
|
||||
}
|
||||
```
|
||||
|
||||
### KnowledgeNode
|
||||
```typescript
|
||||
interface KnowledgeNode {
|
||||
id: string // 节点唯一标识
|
||||
entity_name: string // 实体名称
|
||||
entity_type: string // 实体类型 (ORGANIZATION, PERSON, EVENT, CATEGORY, etc.)
|
||||
description: string // 实体描述
|
||||
pagerank: number // PageRank 重要度分数
|
||||
source_id: string[] // 来源文档ID列表
|
||||
}
|
||||
```
|
||||
|
||||
### KnowledgeEdge
|
||||
```typescript
|
||||
interface KnowledgeEdge {
|
||||
src_id: string // 源节点ID
|
||||
tgt_id: string // 目标节点ID
|
||||
description: string // 关系描述
|
||||
keywords: string[] // 关键词
|
||||
weight: number // 关系权重
|
||||
source_id: string[] // 来源文档ID列表
|
||||
source: string // 源节点名称
|
||||
target: string // 目标节点名称
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```tsx
|
||||
import KnowledgeGraph from './components/KnowledgeGraph'
|
||||
|
||||
const MyComponent = () => {
|
||||
const [graphData, setGraphData] = useState<KnowledgeGraphResponse>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
return (
|
||||
<KnowledgeGraph
|
||||
data={graphData}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Row } from 'antd'
|
||||
import KnowledgeGraph from './components/KnowledgeGraph'
|
||||
|
||||
const KnowledgeBasePage = () => {
|
||||
const [data, setData] = useState()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchKnowledgeGraph = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await api.getKnowledgeGraph(knowledgeBaseId)
|
||||
setData(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch knowledge graph:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchKnowledgeGraph()
|
||||
}, [knowledgeBaseId])
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<KnowledgeGraph data={data} loading={loading} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| data | KnowledgeGraphResponse | undefined | 知识图谱数据 |
|
||||
| loading | boolean | false | 加载状态 |
|
||||
|
||||
## 实体类型颜色
|
||||
|
||||
组件内置了以下实体类型的颜色映射:
|
||||
|
||||
- `ORGANIZATION`: #155EEF (蓝色)
|
||||
- `PERSON`: #4DA8FF (浅蓝色)
|
||||
- `EVENT`: #9C6FFF (紫色)
|
||||
- `CATEGORY`: #8BAEF7 (淡蓝色)
|
||||
- `LOCATION`: #369F21 (绿色)
|
||||
- `TIME`: #FF5D34 (橙红色)
|
||||
- `CONCEPT`: #FF8A4C (橙色)
|
||||
- `OTHER`: #FFB048 (黄色)
|
||||
|
||||
## 交互功能
|
||||
|
||||
1. **节点点击**: 点击节点查看实体详细信息
|
||||
2. **拖拽**: 拖拽节点调整位置
|
||||
3. **缩放**: 鼠标滚轮缩放图谱
|
||||
4. **悬停**: 悬停显示节点和边的详细信息
|
||||
5. **高亮**: 点击节点高亮相邻节点和边
|
||||
|
||||
## 国际化
|
||||
|
||||
组件使用以下翻译键:
|
||||
|
||||
- `knowledgeBase.knowledgeGraph`: 知识图谱标题
|
||||
- `knowledgeBase.entityDetails`: 实体详情标题
|
||||
- `knowledgeBase.entityDetailEmpty`: 空状态提示
|
||||
- `knowledgeBase.entityDetailEmptyDesc`: 空状态描述
|
||||
- `knowledgeBase.entityDescription`: 实体描述标签
|
||||
- `knowledgeBase.sourceDocuments`: 来源文档标签
|
||||
- `userMemory.click/drag/zoom`: 操作说明
|
||||
|
||||
## 性能优化
|
||||
|
||||
- 使用 `React.memo` 避免不必要的重渲染
|
||||
- 使用 `ResizeObserver` 监听容器大小变化
|
||||
- 使用 `requestAnimationFrame` 优化图表重绘
|
||||
- 延迟更新和懒加载提升大数据集性能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保传入的数据结构符合 `KnowledgeGraphResponse` 接口
|
||||
2. 节点的 `pagerank` 值用于计算节点大小,建议范围在 0-1 之间
|
||||
3. 边的 `weight` 值用于计算连线粗细,建议使用正数
|
||||
4. 大数据集可能影响渲染性能,建议进行数据分页或过滤
|
||||
@@ -14,6 +14,15 @@ export interface KnowledgeBaseFormData {
|
||||
parent_id?: string; // 父ID
|
||||
type?: string; // 知识库类型
|
||||
status?: number; // 状态
|
||||
parser_config: ParserConfig; // 解析器配置
|
||||
}
|
||||
export interface GraphragConfig{
|
||||
use_graphrag:boolean; // 是否启用图谱
|
||||
scene_name: string; // 场景名称
|
||||
entity_types: Array<string>; // 实体类型
|
||||
method: string; // 方法
|
||||
resolution: boolean; // 实体归一化
|
||||
community: boolean; /// 是否生成社区报告
|
||||
}
|
||||
export interface KnowledgeBase {
|
||||
id: string;
|
||||
@@ -86,6 +95,8 @@ export interface ParserConfig {
|
||||
auto_keywords?: number; // 自动关键词
|
||||
auto_questions?: number; // 自动问题
|
||||
html4excel?: boolean; // 是否为Excel文件
|
||||
graphrag:GraphragConfig; // 知识图谱生成
|
||||
|
||||
}
|
||||
// 文件数据
|
||||
export interface KnowledgeBaseDocumentData { // 知识库文档数据
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type ToolType = 'mcp' | 'builtin' | 'custom'
|
||||
export type ToolType = 'mcp' | 'builtin' | 'custom'
|
||||
export interface Query {
|
||||
name?: string;
|
||||
tool_type: ToolType
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type FC, useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
import { Skeleton, Space } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
@@ -9,11 +10,20 @@ import {
|
||||
} from '@/api/memory'
|
||||
import type { MemoryInsightRef } from '../types'
|
||||
|
||||
interface Data {
|
||||
memory_insight?: string;
|
||||
behavior_pattern?: string;
|
||||
key_findings?: string[];
|
||||
growth_trajectory?: string;
|
||||
updated_at?: number;
|
||||
is_cached: boolean;
|
||||
}
|
||||
|
||||
const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [report, setReport] = useState<string | null>(null)
|
||||
const [data, setData] = useState<Data>({} as Data)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -25,7 +35,7 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getMemoryInsightReport(id).then((res) => {
|
||||
setReport((res as { report?: string }).report || null)
|
||||
setData((res as Data) || {})
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -43,10 +53,35 @@ const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => {
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: report
|
||||
? <div className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:px-4 rb:text-[#5B6167] rb:leading-5">
|
||||
{report || '-'}
|
||||
</div>
|
||||
: Object.keys(data).length > 0
|
||||
? <Space size={16} direction="vertical" className="rb:w-full">
|
||||
{['memory_insight', 'key_findings', 'behavior_pattern', 'growth_trajectory'].map(key => {
|
||||
if (data[key as keyof Data]) {
|
||||
return (
|
||||
<div key={key} className="rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:py-3 rb:text-[#5B6167] rb:leading-5">
|
||||
<div className={clsx(`rb:relative rb:before:content-[''] rb:before:block rb:before:h-4 rb:before:absolute rb:before:top-0.5 rb:before:left-0 rb:before:w-1 rb:pl-4 rb:mb-2 rb:font-medium rb:leading-5`, {
|
||||
'rb:before:bg-[#155EEF]': key === 'memory_insight',
|
||||
'rb:before:bg-[#369F21]': key !== 'memory_insight'
|
||||
})}>{t(`userMemory.${key}`)}</div>
|
||||
<div className="rb:px-4">
|
||||
{Array.isArray(data[key as keyof Data])
|
||||
? <>
|
||||
{(data[key as keyof Data] as string[])?.map((item: string, index: number) => (
|
||||
<div key={index}>
|
||||
- {item}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
: data[key as keyof Data] as string
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
|
||||
</Space>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, Checkbox, InputNumber } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ChatVariableModalRef } from './types'
|
||||
import type { ChatVariable } from '../../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ChatVariableModalProps {
|
||||
refresh: (value: ChatVariable, editIndex?: number) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
]
|
||||
|
||||
const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ChatVariable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
||||
const typeValue = Form.useWatch('type', form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditIndex(undefined)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: ChatVariable, index?: number) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
form.setFieldsValue(variable)
|
||||
setEditIndex(index)
|
||||
} else {
|
||||
form.resetFields();
|
||||
setEditIndex(undefined)
|
||||
}
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refresh({ ...values }, editIndex)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editIndex !== undefined ? t('workflow.editChatVariable') : t('workflow.addChatVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('workflow.config.parameter-extractor.name')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.parameter-extractor.invalidParamName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="type"
|
||||
label={t('workflow.config.parameter-extractor.type')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={() => form.setFieldValue('default', undefined)}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="default"
|
||||
label={t('workflow.config.parameter-extractor.default')}
|
||||
>
|
||||
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}>
|
||||
{({ getFieldValue }) => {
|
||||
const type = getFieldValue('type');
|
||||
if (type === 'number') {
|
||||
return <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />;
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return (
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: true, label: 'true' },
|
||||
{ value: false, label: 'false' }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Input placeholder={t('common.enter')} />;
|
||||
}}
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('workflow.config.parameter-extractor.desc')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="required"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('workflow.config.parameter-extractor.required')}</Checkbox>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ChatVariableModal;
|
||||
113
web/src/views/Workflow/components/AddChatVariable/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState, useImperativeHandle, forwardRef, useRef } from 'react';
|
||||
import { Button, Input, Space, Typography, Tooltip, message, List } from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ChatVariable, AddChatVariableRef } from '../../types';
|
||||
import type { ChatVariableModalRef } from './types'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
import Empty from '@/components/Empty';
|
||||
import ChatVariableModal from './ChatVariableModal';
|
||||
|
||||
interface AddChatVariableProps {
|
||||
variables?: ChatVariable[];
|
||||
onChange?: (variables: ChatVariable[]) => void;
|
||||
disabled?: boolean;
|
||||
maxVariables?: number;
|
||||
}
|
||||
const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||
variables = [],
|
||||
onChange,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const chatVariableRef = useRef<ChatVariableModalRef>(null);
|
||||
|
||||
const handleAddVariable = () => {
|
||||
chatVariableRef.current?.handleOpen()
|
||||
};
|
||||
|
||||
const handleEdit = (index: number) => {
|
||||
chatVariableRef.current?.handleOpen(variables[index], index)
|
||||
}
|
||||
const handleDelete = (index: number) => {
|
||||
const list = [...variables]
|
||||
list.splice(index, 1)
|
||||
onChange && onChange(list)
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
const handleSave = (value: ChatVariable, index?: number) => {
|
||||
const list = [...variables]
|
||||
if (index && index > -1) {
|
||||
list[index] = value
|
||||
} else {
|
||||
list.push(value)
|
||||
}
|
||||
onChange && onChange(list)
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={t('workflow.addvariable')}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
className="rb:mb-3"
|
||||
onClick={handleAddVariable}
|
||||
>
|
||||
+ {t('workflow.addChatVariable')}
|
||||
</Button>
|
||||
|
||||
{variables.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={variables}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.description}</div>
|
||||
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ChatVariableModal
|
||||
ref={chatVariableRef}
|
||||
refresh={handleSave}
|
||||
/>
|
||||
</RbDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddChatVariable;
|
||||
24
web/src/views/Workflow/components/AddChatVariable/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ChatVariable } from '../../types'
|
||||
|
||||
export interface AddChatVariableProps {
|
||||
variables?: ChatVariable[];
|
||||
onChange?: (variables: ChatVariable[]) => void;
|
||||
disabled?: boolean;
|
||||
maxVariables?: number;
|
||||
}
|
||||
|
||||
export interface VariableFormData {
|
||||
name: string;
|
||||
type: ChatVariable['type'];
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export interface ChatVariableModalRef {
|
||||
handleOpen: (value?: ChatVariable, index?: number) => void;
|
||||
}
|
||||
|
||||
export interface ChatVariableModalRef {
|
||||
handleOpen: (vo?: ChatVariable, index?: number) => void;
|
||||
}
|
||||
@@ -44,14 +44,14 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
||||
>
|
||||
{data.isContext ? (
|
||||
<span style={{ fontSize: '12px', marginRight: '4px' }}>📄</span>
|
||||
) : (
|
||||
) : data.group !== 'CONVERSATION' ? (
|
||||
<img
|
||||
src={data.nodeData?.icon}
|
||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
{!data.isContext && (
|
||||
) : null}
|
||||
{!data.isContext && data.group !== 'CONVERSATION' && (
|
||||
<>
|
||||
<span className="rb:wrap-break-word rb:line-clamp-1">{data.nodeData?.name}</span>
|
||||
<span style={{ color: '#DFE4ED', margin: '0 2px' }}>/</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Suggestion {
|
||||
type: string;
|
||||
dataType: string;
|
||||
value: string;
|
||||
group?: string
|
||||
nodeData: NodeProperties;
|
||||
isContext?: boolean; // 标记是否为context变量
|
||||
disabled?: boolean; // 标记是否禁用
|
||||
|
||||
@@ -23,7 +23,9 @@ const NodeLibrary: FC = () => {
|
||||
}}
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{category.nodes.map((node, nodeIndex) => (
|
||||
{category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map((node, nodeIndex) => (
|
||||
<div
|
||||
key={nodeIndex}
|
||||
className="rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]"
|
||||
|
||||
@@ -1,18 +1,167 @@
|
||||
import { useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { nodeLibrary, graphNodeLibrary } from '../../constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AddNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node?.getData() || {};
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleNodeSelect = (selectedNodeType: any) => {
|
||||
const parentBBox = node.getBBox();
|
||||
const cycleId = data.cycle;
|
||||
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: parentBBox.x,
|
||||
y: parentBBox.y,
|
||||
data: {
|
||||
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: selectedNodeType.type,
|
||||
icon: selectedNodeType.icon,
|
||||
name: t(`workflow.${selectedNodeType.type}`),
|
||||
cycle: cycleId,
|
||||
parentId: data.parentId,
|
||||
config: selectedNodeType.config || {}
|
||||
},
|
||||
});
|
||||
|
||||
// 将新节点添加为父节点的子节点
|
||||
if (cycleId) {
|
||||
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||
if (parentNode) {
|
||||
parentNode.addChild(newNode);
|
||||
}
|
||||
}
|
||||
|
||||
const incomingEdges = graph.getIncomingEdges(node);
|
||||
const outgoingEdges = graph.getOutgoingEdges(node);
|
||||
|
||||
incomingEdges?.forEach(edge => {
|
||||
graph.addEdge({
|
||||
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
|
||||
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
|
||||
attrs: edge.getAttrs()
|
||||
});
|
||||
});
|
||||
|
||||
outgoingEdges?.forEach(edge => {
|
||||
const targetCell = graph.getCellById(edge.getTargetCellId()) as any;
|
||||
const targetPortId = targetCell?.getPorts?.()?.find((port: any) => port.group === 'left')?.id || edge.getTargetPortId();
|
||||
graph.addEdge({
|
||||
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
||||
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
||||
attrs: edge.getAttrs()
|
||||
});
|
||||
});
|
||||
|
||||
// 删除所有add-node类型的节点
|
||||
graph.getNodes().forEach((n: any) => {
|
||||
if (n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId) {
|
||||
n.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// 自动调整循环节点大小
|
||||
const loopNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||
if (loopNode) {
|
||||
const adjustLoopSize = () => {
|
||||
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||
if (childNodes.length > 0) {
|
||||
const bounds = childNodes.reduce((acc, child) => {
|
||||
const bbox = child.getBBox();
|
||||
return {
|
||||
minX: Math.min(acc.minX, bbox.x),
|
||||
minY: Math.min(acc.minY, bbox.y),
|
||||
maxX: Math.max(acc.maxX, bbox.x + bbox.width),
|
||||
maxY: Math.max(acc.maxY, bbox.y + bbox.height)
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const padding = 20;
|
||||
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2);
|
||||
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||
|
||||
loopNode.prop('size', { width: newWidth, height: newHeight });
|
||||
}
|
||||
};
|
||||
|
||||
adjustLoopSize();
|
||||
|
||||
// 监听子节点移动事件
|
||||
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||
childNodes.forEach((childNode: any) => {
|
||||
childNode.on('change:position', adjustLoopSize);
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
|
||||
{nodeLibrary.map((category, categoryIndex) => {
|
||||
const filteredNodes = category.nodes.filter(nodeType =>
|
||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
|
||||
);
|
||||
|
||||
if (filteredNodes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={category.category}>
|
||||
{categoryIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||
{t(`workflow.${category.category}`)}
|
||||
</div>
|
||||
{filteredNodes.map((nodeType) => (
|
||||
<div
|
||||
key={nodeType.type}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => handleNodeSelect(nodeType)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f0f8ff';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
}}
|
||||
>
|
||||
<img src={nodeType.icon} className="rb:w-4 rb:h-4" />
|
||||
<span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-30 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
})}>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
</div>
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div
|
||||
className={clsx('rb:group rb:relative rb:h-11 rb:w-22 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border rb:cursor-pointer', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
})}
|
||||
>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-60 rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import startIcon from '@/assets/images/workflow/start.png';
|
||||
|
||||
const GroupStartNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
|
||||
const GroupStartNode: ReactShapeConfig['component'] = () => {
|
||||
return (
|
||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-20 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
})}>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)] rb:border-[#DFE4ED]')}>
|
||||
<img src={startIcon} className="rb:w-6 rb:h-6" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Dropdown } from 'antd';
|
||||
import { SmallDashOutlined } from '@ant-design/icons';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { graphNodeLibrary } from '../../constant';
|
||||
|
||||
interface NodeData {
|
||||
isSelected?: boolean;
|
||||
type?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
parentId?: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
const IterationNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node.getData() as NodeData;
|
||||
|
||||
useEffect(() => {
|
||||
initNodes()
|
||||
}, [])
|
||||
|
||||
const initNodes = () => {
|
||||
// 添加默认子节点
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
|
||||
const childNode1 = graph.addNode({
|
||||
...graphNodeLibrary.groupStart,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '开始',
|
||||
// icon: '📌',
|
||||
parentId: node.id,
|
||||
isDefault: true // 标记为默认节点,不可删除
|
||||
},
|
||||
});
|
||||
const childNode2 = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: centerX + 150,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '添加节点',
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
},
|
||||
});
|
||||
node.addChild(childNode1)
|
||||
node.addChild(childNode2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:border-2 rb:border-dashed rb:rounded-xl rb:relative rb:min-w-75 rb:min-h-50 rb:p-4', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data?.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data?.isSelected
|
||||
})}>
|
||||
{/* 标题区域 */}
|
||||
<div className="rb:absolute rb:-top-3 rb:left-4 rb:bg-[#10b981] rb:rounded-[20px] rb:p-[8px_16px] rb:flex rb:items-center rb:gap-2 rb:text-white rb:text-[14px] rb:font-bold rb:z-10">
|
||||
<div className="rb:w-5 rb:h-5 rb:bg-[#FFFFFF] rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:text-[#10b981]">
|
||||
🔁
|
||||
</div>
|
||||
迭代
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{items: [
|
||||
{
|
||||
key: '1',
|
||||
label: '删除',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '复制',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '删除',
|
||||
}
|
||||
]}}
|
||||
>
|
||||
<SmallDashOutlined
|
||||
className={clsx("rb:cursor-pointer rb:right-1 rb:top-1 rb:invisible rb:absolute rb:group-hover:visible", {
|
||||
'rb:visible': data.isSelected
|
||||
})}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
{/* 画布内容区域 */}
|
||||
<div className="rb:mt-6 rb:min-h-37.5 rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IterationNode;
|
||||
@@ -1,19 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import { Dropdown } from 'antd';
|
||||
import { SmallDashOutlined } from '@ant-design/icons';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { graphNodeLibrary } from '../../constant';
|
||||
|
||||
interface NodeData {
|
||||
isSelected?: boolean;
|
||||
type?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
parentId?: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
import { edge_color } from '../../hooks/useWorkflowGraph'
|
||||
|
||||
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node.getData() || {};
|
||||
@@ -21,63 +12,145 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
|
||||
useEffect(() => {
|
||||
initNodes()
|
||||
// 检查是否需要添加add-node
|
||||
checkAndAddAddNode()
|
||||
}, [])
|
||||
|
||||
const checkAndAddAddNode = () => {
|
||||
if (!graph) return;
|
||||
|
||||
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === data.id);
|
||||
const cycleStartNodes = childNodes.filter((n: any) => n.getData()?.type === 'cycle-start');
|
||||
|
||||
// 如果只有一个cycle-start节点且没有其他类型的子节点,则添加add-node
|
||||
if (cycleStartNodes.length === 1 && childNodes.length === 1) {
|
||||
const cycleStartNode = cycleStartNodes[0];
|
||||
const cycleStartBBox = cycleStartNode.getBBox();
|
||||
|
||||
const addNode = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: cycleStartBBox.x + 64,
|
||||
y: cycleStartBBox.y,
|
||||
data: {
|
||||
type: 'add-node',
|
||||
label: '添加节点',
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
cycle: data.id,
|
||||
},
|
||||
});
|
||||
|
||||
node.addChild(addNode);
|
||||
|
||||
// 连接cycle-start和add-node
|
||||
const sourcePorts = cycleStartNode.getPorts();
|
||||
const targetPorts = addNode.getPorts();
|
||||
const sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||
const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
||||
|
||||
graph.addEdge({
|
||||
source: { cell: cycleStartNode.id, port: sourcePort },
|
||||
target: { cell: addNode.id, port: targetPort },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: edge_color,
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initNodes = () => {
|
||||
// 检查是否存在cycle为当前节点ID的子节点,若存在则不调用initNodes,避免重复创建
|
||||
const existingCycleNodes = graph.getNodes().filter((n: any) =>
|
||||
n.getData()?.cycle === data.id
|
||||
);
|
||||
if (existingCycleNodes.length > 0) return;
|
||||
// 添加默认子节点
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
|
||||
const childNode1 = graph.addNode({
|
||||
...graphNodeLibrary.groupStart,
|
||||
const cycleStartNode = graph.addNode({
|
||||
...graphNodeLibrary.cycleStart,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '开始',
|
||||
// icon: '📌',
|
||||
type: 'cycle-start',
|
||||
parentId: node.id,
|
||||
isDefault: true // 标记为默认节点,不可删除
|
||||
isDefault: true, // 标记为默认节点,不可删除
|
||||
cycle: data.id,
|
||||
},
|
||||
});
|
||||
const childNode2 = graph.addNode({
|
||||
const addNode = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: centerX + 150,
|
||||
x: centerX + 64,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
type: 'add-node',
|
||||
label: '添加节点',
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
cycle: data.id,
|
||||
},
|
||||
});
|
||||
node.addChild(childNode1)
|
||||
node.addChild(childNode2)
|
||||
node.addChild(cycleStartNode)
|
||||
node.addChild(addNode)
|
||||
const sourcePorts = cycleStartNode.getPorts()
|
||||
const targetPorts = addNode.getPorts()
|
||||
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||
|
||||
const edgeConfig = {
|
||||
source: {
|
||||
cell: cycleStartNode.id,
|
||||
port: sourcePort
|
||||
},
|
||||
target: {
|
||||
cell: addNode.id,
|
||||
port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left'
|
||||
},
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: edge_color,
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
graph.addEdge(edgeConfig)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-16 rb:w-60 rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
<div className="rb:mt-6 rb:min-h-37.5 rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopNode;
|
||||
|
||||
@@ -7,7 +7,7 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-16 rb:w-60 rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
|
||||
222
web/src/views/Workflow/components/PortClickHandler.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { nodeLibrary, graphNodeLibrary } from '../constant';
|
||||
|
||||
interface PortClickHandlerProps {
|
||||
graph: any;
|
||||
}
|
||||
|
||||
const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
const { t } = useTranslation();
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [popoverPosition, setPopoverPosition] = useState({ x: 0, y: 0 });
|
||||
const [sourceNode, setSourceNode] = useState<any>(null);
|
||||
const [sourcePort, setSourcePort] = useState<string>('');
|
||||
const [tempElement, setTempElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePortClick = (event: CustomEvent) => {
|
||||
const { node, port, element, rect } = event.detail;
|
||||
setSourceNode(node);
|
||||
setSourcePort(port);
|
||||
setTempElement(element);
|
||||
setPopoverPosition({ x: rect.left, y: rect.top });
|
||||
setPopoverVisible(true);
|
||||
};
|
||||
|
||||
window.addEventListener('port:click', handlePortClick as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('port:click', handlePortClick as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleNodeSelect = (selectedNodeType: any) => {
|
||||
if (!sourceNode || !graph) return;
|
||||
|
||||
const sourceNodeData = sourceNode.getData();
|
||||
|
||||
// 计算新节点位置(在源节点右侧)
|
||||
const sourceBBox = sourceNode.getBBox();
|
||||
const newX = sourceBBox.x + sourceBBox.width + 50;
|
||||
const newY = sourceBBox.y;
|
||||
|
||||
// 创建新节点
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: newX,
|
||||
y: newY,
|
||||
data: {
|
||||
id: `${selectedNodeType.type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
type: selectedNodeType.type,
|
||||
icon: selectedNodeType.icon,
|
||||
name: t(`workflow.${selectedNodeType.type}`),
|
||||
cycle: sourceNodeData.cycle, // 继承源节点的cycle
|
||||
config: selectedNodeType.config || {}
|
||||
},
|
||||
});
|
||||
|
||||
// 将新节点添加为父节点的子节点
|
||||
if (sourceNodeData.cycle) {
|
||||
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle);
|
||||
if (parentNode) {
|
||||
parentNode.addChild(newNode);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建连线
|
||||
setTimeout(() => {
|
||||
const targetPorts = newNode.getPorts();
|
||||
const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
|
||||
|
||||
graph.addEdge({
|
||||
source: { cell: sourceNode.id, port: sourcePort },
|
||||
target: { cell: newNode.id, port: targetPort },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#155EEF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 循环节点内子节点通过连接桩添加时,调整循环节点大小
|
||||
const cycleId = sourceNodeData.cycle;
|
||||
if (cycleId) {
|
||||
const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === cycleId);
|
||||
|
||||
if (parentNode) {
|
||||
const adjustLoopSize = () => {
|
||||
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||
if (childNodes.length > 0) {
|
||||
const bounds = childNodes.reduce((acc: any, child: any) => {
|
||||
const bbox = child.getBBox();
|
||||
return {
|
||||
minX: Math.min(acc.minX, bbox.x),
|
||||
minY: Math.min(acc.minY, bbox.y),
|
||||
maxX: Math.max(acc.maxX, bbox.x + bbox.width),
|
||||
maxY: Math.max(acc.maxY, bbox.y + bbox.height)
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const padding = 20;
|
||||
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2);
|
||||
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||
|
||||
parentNode.prop('size', { width: newWidth, height: newHeight });
|
||||
}
|
||||
};
|
||||
|
||||
adjustLoopSize();
|
||||
|
||||
// 监听子节点移动事件
|
||||
const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId);
|
||||
childNodes.forEach((childNode: any) => {
|
||||
childNode.on('change:position', adjustLoopSize);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// 清理临时元素
|
||||
if (tempElement) {
|
||||
document.body.removeChild(tempElement);
|
||||
setTempElement(null);
|
||||
}
|
||||
|
||||
setPopoverVisible(false);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setPopoverVisible(false);
|
||||
if (tempElement) {
|
||||
document.body.removeChild(tempElement);
|
||||
setTempElement(null);
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
|
||||
{nodeLibrary.map((category, categoryIndex) => {
|
||||
const sourceNodeData = sourceNode?.getData();
|
||||
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');
|
||||
|
||||
let filteredNodes;
|
||||
if (isChildOfLoop) {
|
||||
// Use same filtering as AddNode for child nodes of loop
|
||||
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
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||
} else {
|
||||
// Original filtering for non-loop child nodes
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type));
|
||||
filteredNodes = category.nodes.filter(nodeType =>
|
||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredNodes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={category.category}>
|
||||
{categoryIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||
{t(`workflow.${category.category}`)}
|
||||
</div>
|
||||
{filteredNodes.map((nodeType) => (
|
||||
<div
|
||||
key={nodeType.type}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
onClick={() => handleNodeSelect(nodeType)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f0f8ff';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'white';
|
||||
}}
|
||||
>
|
||||
<img src={nodeType.icon} className="rb:w-4 rb:h-4" />
|
||||
<span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!tempElement) return null;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={content}
|
||||
open={popoverVisible}
|
||||
onOpenChange={(visible) => {
|
||||
if (!visible) handlePopoverClose();
|
||||
}}
|
||||
placement="right"
|
||||
overlayStyle={{
|
||||
position: 'fixed',
|
||||
left: popoverPosition.x + 10,
|
||||
top: popoverPosition.y - 10,
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default PortClickHandler;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col, Select } from 'antd'
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface AssignmentListProps {
|
||||
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||
parentName: string;
|
||||
options: Suggestion[];
|
||||
}
|
||||
|
||||
const AssignmentList: FC<AssignmentListProps> = ({
|
||||
parentName,
|
||||
options = [],
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-between">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'variable_selector']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'operation']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'cover', label: t('workflow.config.assigner.cover') },
|
||||
{ value: 'clear', label: t('workflow.config.assigner.clear') },
|
||||
{ value: 'assign', label: t('workflow.config.assigner.assign') },
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
if (operation === 'clear') return null;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
rules={[{ required: true, message: 'Missing last name' }]}
|
||||
>
|
||||
{operation === 'assign' ? (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssignmentList
|
||||
@@ -9,8 +9,8 @@ import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>) => void;
|
||||
options: Suggestion[];
|
||||
name: string;
|
||||
selectedNode?: any;
|
||||
@@ -221,7 +221,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
onClick={() => addCondition()}
|
||||
size="small"
|
||||
>
|
||||
+ 添加条件
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
{caseFields.length > 1 && <DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
@@ -229,7 +229,8 @@ const CaseList: FC<CaseListProps> = ({
|
||||
/>}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 && <>
|
||||
{conditionFields?.length > 1 &&
|
||||
<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
@@ -238,50 +239,56 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{conditionFields.map((conditionField, conditionIndex) => (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const currentOperator = value?.[caseIndex]?.expressions?.[conditionIndex]?.comparison_operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
|
||||
return (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'comparison_operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!hideRightField && (
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
placeholder="包含"
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input, Button, Form, Space } from 'antd';
|
||||
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
|
||||
interface CategoryListProps {
|
||||
parentName: string;
|
||||
selectedNode?: Node | null;
|
||||
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||
}
|
||||
|
||||
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef }) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const formValues = Form.useWatch([parentName], form);
|
||||
|
||||
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
|
||||
if (!selectedNode || !graphRef?.current) return;
|
||||
|
||||
// 保存现有连线信息(包括左侧端口连线)
|
||||
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
|
||||
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
|
||||
);
|
||||
const edgeConnections = existingEdges.map((edge: any) => ({
|
||||
edge,
|
||||
sourcePortId: edge.getSourcePortId(),
|
||||
targetCellId: edge.getTargetCellId(),
|
||||
targetPortId: edge.getTargetPortId(),
|
||||
sourceCellId: edge.getSourceCellId(),
|
||||
isIncoming: edge.getTargetCellId() === selectedNode.id
|
||||
}));
|
||||
|
||||
// 移除所有现有的右侧端口
|
||||
const existingPorts = selectedNode.getPorts();
|
||||
existingPorts.forEach((port: any) => {
|
||||
if (port.group === 'right') {
|
||||
selectedNode.removePort(port.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 计算新的节点高度:基础高度88px + 每个额外port增加30px
|
||||
const baseHeight = 88;
|
||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||
|
||||
selectedNode.prop('size', { width: 240, height: newHeight < baseHeight ? baseHeight : newHeight })
|
||||
|
||||
// 添加 分类 端口
|
||||
for (let i = 0; i < caseCount; i++) {
|
||||
selectedNode.addPort({
|
||||
id: `CASE${i + 1}`,
|
||||
group: 'right',
|
||||
args: i === 0 ? { dy: 24 } : undefined,
|
||||
attrs: { text: { text: `分类${i + 1}`, fontSize: 12, fill: '#5B6167' } }
|
||||
});
|
||||
}
|
||||
// 恢复连线
|
||||
setTimeout(() => {
|
||||
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
|
||||
graphRef.current?.removeCell(edge);
|
||||
|
||||
// 如果是进入连线(左侧端口),直接恢复
|
||||
if (isIncoming) {
|
||||
const sourceCell = graphRef.current?.getCellById(sourceCellId);
|
||||
if (sourceCell) {
|
||||
graphRef.current?.addEdge({
|
||||
source: { cell: sourceCellId, port: sourcePortId },
|
||||
target: { cell: selectedNode.id, port: targetPortId },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#155EEF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理右侧端口连线
|
||||
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
|
||||
|
||||
// 如果是被删除的端口,不重新创建连线
|
||||
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newPortId = sourcePortId;
|
||||
|
||||
// 如果删除了某个端口,需要重新映射后续端口的ID
|
||||
if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
|
||||
newPortId = `CASE${originalCaseNumber - 1}`;
|
||||
}
|
||||
|
||||
// 检查新端口是否存在
|
||||
const newPorts = selectedNode.getPorts();
|
||||
const matchingPort = newPorts.find((port: any) => port.id === newPortId);
|
||||
|
||||
if (matchingPort) {
|
||||
const targetCell = graphRef.current?.getCellById(targetCellId);
|
||||
if (targetCell) {
|
||||
graphRef.current?.addEdge({
|
||||
source: { cell: selectedNode.id, port: newPortId },
|
||||
target: { cell: targetCellId, port: targetPortId },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#155EEF',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
size: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleAddCategory = (addFunc: Function) => {
|
||||
addFunc({});
|
||||
setTimeout(() => {
|
||||
updateNodePorts((formValues?.length || 0) + 1);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRemoveCategory = (removeFunc: Function, fieldName: number, categoryIndex: number) => {
|
||||
removeFunc(fieldName);
|
||||
setTimeout(() => {
|
||||
updateNodePorts((formValues?.length || 1) - 1, categoryIndex);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
console.log('formValues', formValues)
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space direction="vertical" size={12} className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }, index) => {
|
||||
const currentItem = formValues?.[key] || {};
|
||||
const contentLength = (currentItem.class_name || '').length;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-1">
|
||||
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'class_name']}
|
||||
noStyle
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={2}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)})}
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => handleAddCategory(add)}
|
||||
className="rb:w-full"
|
||||
>
|
||||
+ {t('workflow.config.question-classifier.addClassName')}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form.List>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryList;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
expressions: Array<{ left: string; comparison_operator: string; right: string; }>
|
||||
}
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Case;
|
||||
onChange?: (value: Case) => void;
|
||||
options: Suggestion[];
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
addBtnText?: string;
|
||||
}
|
||||
const operatorList = [
|
||||
"empty",
|
||||
"not_empty",
|
||||
"contains",
|
||||
"not_contains",
|
||||
"startwith",
|
||||
"endwith",
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"le",
|
||||
"gt",
|
||||
"ge"
|
||||
]
|
||||
|
||||
const ConditionList: FC<CaseListProps> = ({
|
||||
value,
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChangeLogicalOperator = () => {
|
||||
if (!value) return;
|
||||
onChange && onChange({
|
||||
logical_operator: value.logical_operator === 'and' ? 'or' : 'and',
|
||||
expressions: value.expressions || []
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
<div className="rb:relative">
|
||||
{fields.map((field, index) => {
|
||||
const currentOperator = value?.expressions?.[index]?.comparison_operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:mb-3">
|
||||
{index > 0 && (<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{value?.logical_operator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
|
||||
<Row gutter={8} align="middle">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{!hideRightField && (
|
||||
<Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ left: '', comparison_operator: '', right: '' })}
|
||||
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||
icon={<span>+</span>}
|
||||
>
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionList
|
||||
@@ -0,0 +1,163 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, Input } from 'antd'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface CycleVar {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
input_type: string;
|
||||
}
|
||||
|
||||
interface CycleVarsListProps {
|
||||
value?: CycleVar[];
|
||||
onChange?: (value: CycleVar[]) => void;
|
||||
options: Suggestion[];
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]'
|
||||
]
|
||||
|
||||
const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
value = [],
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// 获取循环节点的子节点变量
|
||||
const getChildNodeVariables = () => {
|
||||
if (!selectedNode || !graphRef?.current || selectedNode.getData()?.type !== 'loop') {
|
||||
return options;
|
||||
}
|
||||
|
||||
const loopNodeId = selectedNode.getData()?.id;
|
||||
const childNodes = graphRef.current.getNodes().filter((node: any) =>
|
||||
node.getData()?.cycle === loopNodeId
|
||||
);
|
||||
|
||||
const childVariables: Suggestion[] = [];
|
||||
childNodes.forEach((childNode: any) => {
|
||||
const childData = childNode.getData();
|
||||
if (childData?.config) {
|
||||
Object.keys(childData.config).forEach(key => {
|
||||
if (childData.config[key]?.defaultValue) {
|
||||
childVariables.push({
|
||||
key: `${childData.id}.${key}`,
|
||||
label: `${childData.name || childData.type}.${key}`,
|
||||
type: 'output',
|
||||
dataType: 'string',
|
||||
value: `{{${childData.id}.${key}}}`,
|
||||
nodeData: childData
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...options, ...childVariables];
|
||||
};
|
||||
|
||||
const availableOptions = getChildNodeVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...field }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white">
|
||||
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
// 重置 value 字段
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
{currentInputType === 'variable' ? (
|
||||
<VariableSelect
|
||||
placeholder="选择变量"
|
||||
options={availableOptions}
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
placeholder="输入值"
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CycleVarsList
|
||||
@@ -0,0 +1,216 @@
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps } from 'antd'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import { getToolMethods, getToolDetail, getTools } from '@/api/tools'
|
||||
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||
import Editor from "../../Editor";
|
||||
|
||||
interface Option {
|
||||
value?: string | number | null;
|
||||
label?: React.ReactNode;
|
||||
children?: Option[];
|
||||
isLeaf?: boolean;
|
||||
method_id?: string;
|
||||
parameters?: Parameter[];
|
||||
}
|
||||
interface Parameter {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
default: any;
|
||||
enum: null | string[];
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
pattern: null | string;
|
||||
}
|
||||
|
||||
|
||||
const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
options,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
const values = Form.useWatch([], form) || {}
|
||||
const [optionList, setOptionList] = useState<Option[]>([
|
||||
{ value: 'mcp', label: t('tool.mcp'), isLeaf: false },
|
||||
{ value: 'builtin', label: t('tool.inner'), isLeaf: false },
|
||||
{ value: 'custom', label: t('tool.custom'), isLeaf: false },
|
||||
])
|
||||
const [parameters, setParameters] = useState<Parameter[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (values.tool_id) {
|
||||
getToolDetail(values.tool_id)
|
||||
.then(res => {
|
||||
const detail = res as { tool_type: ToolType; }
|
||||
|
||||
getTools({ tool_type: detail.tool_type })
|
||||
.then(toolsRes => {
|
||||
const tools = toolsRes as ToolItem[]
|
||||
|
||||
getToolMethods(values.tool_id)
|
||||
.then(methodsRes => {
|
||||
const response = methodsRes as Array<{ method_id: string; name: string; parameters: Parameter[] }>
|
||||
|
||||
setOptionList(prevList => {
|
||||
return prevList.map(item => {
|
||||
if (item.value === detail.tool_type) {
|
||||
return {
|
||||
...item,
|
||||
children: tools.map((vo: ToolItem) => ({
|
||||
value: vo.id,
|
||||
label: vo.name,
|
||||
isLeaf: false,
|
||||
children: vo.id === values.tool_id ? response.map(method => ({
|
||||
value: method.name,
|
||||
label: method.name,
|
||||
isLeaf: true,
|
||||
method_id: method.method_id,
|
||||
parameters: method.parameters
|
||||
})) : undefined
|
||||
}))
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
if (response.length > 1) {
|
||||
const filterTarget = response.find(vo => vo.name === values.tool_parameters?.operation)
|
||||
if (filterTarget) {
|
||||
setParameters([...filterTarget.parameters])
|
||||
} else {
|
||||
setParameters([])
|
||||
}
|
||||
} else {
|
||||
setParameters([...response[0].parameters])
|
||||
}
|
||||
|
||||
form.setFieldValue('tools', [detail.tool_type, values.tool_id, values.tool_parameters?.operation ?? response[0].name])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [values.tool_id, values.tool_parameters?.operation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (values.tools && values.tools.length === 3) {
|
||||
const [toolType, toolId, operation] = values.tools
|
||||
|
||||
// 从 optionList 中查找对应的参数
|
||||
const typeOption = optionList.find(opt => opt.value === toolType)
|
||||
if (typeOption?.children) {
|
||||
const toolOption = typeOption.children.find(opt => opt.value === toolId)
|
||||
if (toolOption?.children) {
|
||||
const methodOption = toolOption.children.find(opt => opt.value === operation)
|
||||
if (methodOption?.parameters) {
|
||||
setParameters([...methodOption.parameters])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [values.tools])
|
||||
|
||||
const loadData = (selectedOptions: Option[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
if (selectedOptions.length === 1) {
|
||||
getTools({ tool_type: targetOption.value as ToolType })
|
||||
.then(res => {
|
||||
const response = res as ToolItem[]
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.id,
|
||||
label: vo.name,
|
||||
isLeaf: response.length === 0,
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
} else {
|
||||
getToolMethods(targetOption.value as string)
|
||||
.then(res => {
|
||||
const response = res as Array<{ method_id: string; name: string }>
|
||||
targetOption.children = response.map((vo: any) => {
|
||||
return {
|
||||
value: vo.name,
|
||||
label: vo.name,
|
||||
isLeaf: true,
|
||||
method_id: vo.method_id,
|
||||
parameters: vo.parameters
|
||||
}
|
||||
})
|
||||
setOptionList([...optionList])
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: CascaderProps<Option>['onChange'] = (value, selectedOptions) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
const curParameters = [...(targetOption.parameters ?? [])]
|
||||
setParameters([...curParameters])
|
||||
const inititalValue: any = { tool_id: selectedOptions[1].value, tool_parameters: {} }
|
||||
|
||||
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
||||
inititalValue.tool_parameters.operation = value?.[2]
|
||||
} else if (value[0] === 'custom') {
|
||||
inititalValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||
}
|
||||
curParameters.forEach(vo => {
|
||||
inititalValue.tool_parameters[vo.name] = vo.default
|
||||
})
|
||||
|
||||
form.setFieldsValue(inititalValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="tools"
|
||||
label={t('workflow.config.tool.tool_id')}
|
||||
>
|
||||
<Cascader
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
changeOnSelect={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="tool_id" hidden />
|
||||
<Form.Item name={['tool_parameters', 'operation']} hidden />
|
||||
{parameters.map((parameter) => {
|
||||
return (
|
||||
<div key={parameter.name}>
|
||||
<Form.Item
|
||||
name={['tool_parameters', parameter.name]}
|
||||
label={parameter.name}
|
||||
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||
rules={[
|
||||
{ required: parameter.required, message: t('workflow.config.tool.required') }
|
||||
]}
|
||||
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
|
||||
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
|
||||
>
|
||||
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch />
|
||||
: parameter.type === 'integer' || parameter.type === 'number'
|
||||
? <InputNumber min={parameter.minimum} max={parameter.maximum} step={parameter.type === 'integer' ? 1 : 0.01} placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||
: <Editor
|
||||
height={32}
|
||||
variant="outlined"
|
||||
options={options}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
{parameter.type === 'boolean' && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mb-6">{parameter.description}</div>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ToolConfig;
|
||||
@@ -26,7 +26,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props
|
||||
const filterOption = options.find(vo => vo.value === value)
|
||||
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
||||
|
||||
if (filterOption) {
|
||||
return (
|
||||
@@ -37,13 +37,17 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
})}
|
||||
contentEditable={false}
|
||||
>
|
||||
<img
|
||||
src={filterOption.nodeData?.icon}
|
||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||
alt=""
|
||||
/>
|
||||
{filterOption.nodeData?.name}
|
||||
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
||||
{filterOption.nodeData?.icon && filterOption.nodeData?.name && (
|
||||
<>
|
||||
<img
|
||||
src={filterOption.nodeData.icon}
|
||||
style={{ width: '12px', height: '12px', marginRight: '4px' }}
|
||||
alt=""
|
||||
/>
|
||||
{filterOption.nodeData.name}
|
||||
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
||||
</>
|
||||
)}
|
||||
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
||||
</span>
|
||||
)
|
||||
@@ -62,8 +66,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({ label: s.label, value: s.value }))
|
||||
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||
}));
|
||||
|
||||
console.log('groupedOptions', groupedOptions)
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -17,6 +17,11 @@ import GroupVariableList from './GroupVariableList'
|
||||
import CaseList from './CaseList'
|
||||
import HttpRequest from './HttpRequest';
|
||||
import MappingList from './MappingList'
|
||||
import CategoryList from './CategoryList'
|
||||
import ConditionList from './ConditionList'
|
||||
import CycleVarsList from './CycleVarsList'
|
||||
import AssignmentList from './AssignmentList'
|
||||
import ToolConfig from './ToolConfig'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -26,10 +31,12 @@ interface PropertiesProps {
|
||||
deleteEvent: () => void;
|
||||
copyEvent: () => void;
|
||||
parseEvent: () => void;
|
||||
config?: any;
|
||||
}
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
config,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
@@ -178,9 +185,19 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return allPrevious;
|
||||
};
|
||||
|
||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||
// Find child nodes (nodes whose cycle field equals current node's ID)
|
||||
const getChildNodes = (nodeId: string): string[] => {
|
||||
return nodes
|
||||
.filter(node => node.getData()?.cycle === nodeId)
|
||||
.map(node => node.id);
|
||||
};
|
||||
|
||||
allPreviousNodeIds.forEach(nodeId => {
|
||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||
const childNodeIds = getChildNodes(selectedNode.id);
|
||||
console.log('childNodeIds', childNodeIds)
|
||||
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||
|
||||
allRelevantNodeIds.forEach(nodeId => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
@@ -245,7 +262,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: knowledgeKey,
|
||||
label: 'message',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
dataType: 'array[object]',
|
||||
value: `${nodeId}.message`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
@@ -254,10 +271,30 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
// Add conversation variables from global config
|
||||
const conversationVariables = config?.variables || [];
|
||||
|
||||
conversationVariables.forEach((variable: any) => {
|
||||
const key = `CONVERSATION_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `conversation.${variable.name}`,
|
||||
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||
group: 'CONVERSATION'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return variableList;
|
||||
}, [selectedNode, graphRef]);
|
||||
|
||||
console.log('values', values)
|
||||
console.log('variableList', variableList, selectedNode?.data)
|
||||
|
||||
return (
|
||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||
@@ -278,12 +315,18 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</Form.Item>
|
||||
|
||||
{selectedNode?.data?.type === 'http-request'
|
||||
? <HttpRequest
|
||||
options={variableList}
|
||||
/>
|
||||
? <HttpRequest
|
||||
options={variableList}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'tool'
|
||||
? <ToolConfig options={variableList} />
|
||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||
const config = configs[key] || {}
|
||||
|
||||
if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||
return (
|
||||
<div key={key}>
|
||||
@@ -409,14 +452,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<GroupVariableList
|
||||
name={key}
|
||||
options={variableList}
|
||||
isCanAdd={!!values?.group}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
console.log('key', key)
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
@@ -439,6 +481,94 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'cycleVarsList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (config.type === 'assignmentList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<AssignmentList
|
||||
parentName={key}
|
||||
options={(() => {
|
||||
if (config.filterLoopIterationVars) {
|
||||
// Add loop cycle variables and iteration item/index variables
|
||||
const loopIterationVars: Suggestion[] = [];
|
||||
const graph = graphRef.current;
|
||||
if (graph && selectedNode) {
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find parent loop/iteration nodes
|
||||
const findParentLoopIteration = (nodeId: string): string[] => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return [];
|
||||
|
||||
const nodeData = node.getData();
|
||||
const cycle = nodeData?.cycle;
|
||||
|
||||
if (cycle) {
|
||||
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||
if (parentNode) {
|
||||
const parentData = parentNode.getData();
|
||||
if (parentData?.type === 'loop') {
|
||||
console.log('parentData', parentData)
|
||||
// Add cycle variables from loop node
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
loopIterationVars.push({
|
||||
key: `${cycle}_cycle_${cycleVar.name}`,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${cycle}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
});
|
||||
} else if (parentData?.type === 'iteration') {
|
||||
// Add item and index variables from iteration node
|
||||
loopIterationVars.push(
|
||||
{
|
||||
key: `${cycle}_item`,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'Object',
|
||||
value: `${cycle}.item`,
|
||||
nodeData: parentData,
|
||||
},
|
||||
{
|
||||
key: `${cycle}_index`,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'Number',
|
||||
value: `${cycle}.index`,
|
||||
nodeData: parentData,
|
||||
}
|
||||
);
|
||||
}
|
||||
return [cycle, ...findParentLoopIteration(cycle)];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
findParentLoopIteration(selectedNode.id);
|
||||
}
|
||||
|
||||
return [...variableList, ...loopIterationVars];
|
||||
}
|
||||
return variableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
@@ -453,7 +583,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.options}
|
||||
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
@@ -472,10 +602,50 @@ const Properties: FC<PropertiesProps> = ({
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={variableList}
|
||||
options={(() => {
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return variableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch && variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
if (config.filterChildNodes && selectedNode) {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return [];
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find child nodes whose cycle field equals parent node's ID
|
||||
const childNodes = nodes.filter(node => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return variableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||
);
|
||||
}
|
||||
return variableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import IterationNode from './components/Nodes/IterationNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
import ConditionNode from './components/Nodes/ConditionNode';
|
||||
import GroupStartNode from './components/Nodes/GroupStartNode';
|
||||
@@ -37,6 +36,10 @@ import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection
|
||||
import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
||||
import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
||||
import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
||||
import breakIcon from '@/assets/images/workflow/break.png'
|
||||
import assignerIcon from '@/assets/images/workflow/assigner.png'
|
||||
import { memoryConfigListUrl } from '@/api/memory'
|
||||
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import type { NodeLibrary } from './types'
|
||||
@@ -168,15 +171,49 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// category: "cognitiveUpgrading",
|
||||
// nodes: [
|
||||
// { type: "task_planning", icon: taskPlanningIcon },
|
||||
// { type: "reasoning_control", icon: reasoningControlIcon },
|
||||
// { type: "self_reflection", icon: selfReflectionIcon },
|
||||
// { type: "memory_enhancement", icon: memoryEnhancementIcon }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
category: "cognitiveUpgrading",
|
||||
nodes: [
|
||||
{
|
||||
type: "memory-read", icon: memoryEnhancementIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
type: 'customSelect',
|
||||
url: memoryConfigListUrl,
|
||||
valueKey: 'config_id',
|
||||
labelKey: 'config_name'
|
||||
},
|
||||
search_switch: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '0', label: 'memoryConversation.deepThinking' },
|
||||
{ value: '1', label: 'memoryConversation.normalReply' },
|
||||
{ value: '2', label: 'memoryConversation.quickReply' },
|
||||
],
|
||||
needTranslation: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: "memory-write", icon: memoryEnhancementIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
type: 'customSelect',
|
||||
url: memoryConfigListUrl,
|
||||
valueKey: 'config_id',
|
||||
labelKey: 'config_name'
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
// {
|
||||
// category: "agentCollaborationNode",
|
||||
// nodes: [
|
||||
@@ -201,8 +238,76 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "iteration", icon: iterationIcon },
|
||||
// { type: "loop", icon: loopIcon },
|
||||
{ type: "question-classifier", icon: questionClassifierIcon,
|
||||
config: {
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat' }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
input_variable: {
|
||||
type: 'variableList',
|
||||
},
|
||||
categories: {
|
||||
type: 'categoryList',
|
||||
defaultValue: [
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
user_supplement_prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: "iteration", icon: iterationIcon,
|
||||
config: {
|
||||
input: {
|
||||
type: 'variableList',
|
||||
filterNodeTypes: ['knowledge-retrieval'],
|
||||
filterVariableNames: ['message']
|
||||
},
|
||||
parallel: {
|
||||
type: 'switch',
|
||||
},
|
||||
parallel_count: {
|
||||
type: 'slider',
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1,
|
||||
defaultValue: 10,
|
||||
dependsOn: 'parallel',
|
||||
dependsOnValue: true
|
||||
},
|
||||
flatten: { // 扁平化输出
|
||||
type: 'switch',
|
||||
},
|
||||
output: {
|
||||
type: 'variableList',
|
||||
filterChildNodes: true
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: "loop", icon: loopIcon,
|
||||
config: {
|
||||
cycle_vars: {
|
||||
type: 'cycleVarsList',
|
||||
},
|
||||
condition: {
|
||||
type: 'conditionList',
|
||||
showLabel: true,
|
||||
defaultValue: {
|
||||
logical_operator: 'and',
|
||||
expressions: []
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
{ type: "cycle-start", icon: loopIcon },
|
||||
{ type: "break", icon: breakIcon },
|
||||
// { type: "parallel", icon: parallelIcon },
|
||||
{ type: "var-aggregator", icon: aggregatorIcon,
|
||||
config: {
|
||||
@@ -215,7 +320,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
defaultValue: [{ key: 'Group1', value: []}]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "assigner", icon: assignerIcon,
|
||||
config: {
|
||||
assignments: {
|
||||
type: 'assignmentList',
|
||||
filterLoopIterationVars: true
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -278,7 +392,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "tools", icon: toolsIcon },
|
||||
{ type: "tool", icon: toolsIcon,
|
||||
config: {
|
||||
tool_id: {
|
||||
type: 'cascader'
|
||||
},
|
||||
tool_parameters: {
|
||||
type: 'define'
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "code_execution", icon: codeExecutionIcon },
|
||||
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||
config: {
|
||||
@@ -315,14 +438,14 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
{
|
||||
shape: 'loop-node',
|
||||
width: 240,
|
||||
height: 80,
|
||||
height: 120,
|
||||
component: LoopNode,
|
||||
},
|
||||
{
|
||||
shape: 'iteration-node',
|
||||
width: 200,
|
||||
height: 200,
|
||||
component: IterationNode,
|
||||
width: 240,
|
||||
height: 120,
|
||||
component: LoopNode,
|
||||
},
|
||||
{
|
||||
shape: 'normal-node',
|
||||
@@ -337,15 +460,15 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
component: ConditionNode,
|
||||
},
|
||||
{
|
||||
shape: 'group-start-node',
|
||||
shape: 'cycle-start',
|
||||
width: 44,
|
||||
height: 44,
|
||||
component: GroupStartNode,
|
||||
},
|
||||
{
|
||||
shape: 'add-node',
|
||||
width: 120,
|
||||
height: 40,
|
||||
width: 88,
|
||||
height: 44,
|
||||
component: AddNode,
|
||||
},
|
||||
];
|
||||
@@ -382,7 +505,7 @@ const defaultPortItems = [
|
||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
iteration: {
|
||||
width: 240,
|
||||
height: 200,
|
||||
height: 120,
|
||||
shape: 'iteration-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
@@ -411,6 +534,19 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
],
|
||||
},
|
||||
},
|
||||
'question-classifier': {
|
||||
width: 240,
|
||||
height: 88,
|
||||
shape: 'condition-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
{ group: 'right', id: 'CASE1', args: { dy: 24 }, attrs: { text: { text: '分类1', fontSize: 12, color: '#5B6167' } } },
|
||||
{ group: 'right', id: 'CASE2', attrs: { text: { text: '分类2', fontSize: 12, color: '#5B6167' } } }
|
||||
],
|
||||
},
|
||||
},
|
||||
start: {
|
||||
width: 240,
|
||||
height: 64,
|
||||
@@ -429,6 +565,24 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
items: [{ group: 'left' }],
|
||||
},
|
||||
},
|
||||
'cycle-start': {
|
||||
width: 44,
|
||||
height: 44,
|
||||
shape: 'cycle-start',
|
||||
ports: {
|
||||
groups: {right: { position: 'right', attrs: portAttrs }},
|
||||
items: [{ group: 'right' }],
|
||||
},
|
||||
},
|
||||
'add-node': {
|
||||
width: 88,
|
||||
height: 44,
|
||||
shape: 'add-node',
|
||||
ports: {
|
||||
groups: {left: { position: 'left', attrs: portAttrs }},
|
||||
items: [{ group: 'left' }],
|
||||
},
|
||||
},
|
||||
default: {
|
||||
width: 240,
|
||||
height: 64,
|
||||
@@ -438,18 +592,18 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
items: defaultPortItems,
|
||||
},
|
||||
},
|
||||
groupStart: {
|
||||
width: 80,
|
||||
height: 40,
|
||||
shape: 'group-start-node',
|
||||
cycleStart: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
shape: 'cycle-start',
|
||||
ports: {
|
||||
groups: {right: { position: 'right', attrs: portAttrs }},
|
||||
items: [{ group: 'right' }],
|
||||
},
|
||||
},
|
||||
addStart: {
|
||||
width: 80,
|
||||
height: 40,
|
||||
width: 88,
|
||||
height: 44,
|
||||
shape: 'add-node',
|
||||
ports: {
|
||||
groups: {left: { position: 'left', attrs: portAttrs }},
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface UseWorkflowGraphProps {
|
||||
|
||||
export interface UseWorkflowGraphReturn {
|
||||
config: WorkflowConfig | null;
|
||||
setConfig: React.Dispatch<React.SetStateAction<WorkflowConfig | null>>;
|
||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||
selectedNode: Node | null;
|
||||
setSelectedNode: React.Dispatch<React.SetStateAction<Node | null>>;
|
||||
@@ -155,9 +156,99 @@ export const useWorkflowGraph = ({
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点,根据categories动态生成端口
|
||||
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||
const categoryCount = config.categories.length;
|
||||
const baseHeight = 88;
|
||||
const newHeight = baseHeight + (categoryCount - 1) * 30;
|
||||
|
||||
const portAttrs = {
|
||||
circle: {
|
||||
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||
},
|
||||
};
|
||||
|
||||
const portItems: PortMetadata[] = [
|
||||
{ group: 'left' }
|
||||
];
|
||||
|
||||
// 添加分类端口
|
||||
config.categories.forEach((category: any, index: number) => {
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
id: `CASE${index + 1}`,
|
||||
args: index === 0 ? { dy: 24 } : undefined,
|
||||
attrs: { text: { text: category.class_name || `分类${index + 1}`, fontSize: 12, fill: '#5B6167' }}
|
||||
});
|
||||
});
|
||||
|
||||
nodeConfig.ports = {
|
||||
groups: {
|
||||
right: { position: 'right', attrs: portAttrs },
|
||||
left: { position: 'left', attrs: portAttrs },
|
||||
},
|
||||
items: portItems
|
||||
};
|
||||
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
return nodeConfig
|
||||
})
|
||||
graphRef.current?.addNodes(nodeList)
|
||||
|
||||
// 分离父节点和子节点
|
||||
const parentNodes = nodeList.filter(node => !node.data.cycle)
|
||||
const childNodes = nodeList.filter(node => node.data.cycle)
|
||||
|
||||
// 先添加父节点
|
||||
graphRef.current?.addNodes(parentNodes)
|
||||
|
||||
// 然后处理子节点,使用addChild添加到对应的父节点
|
||||
childNodes.forEach(childNode => {
|
||||
const cycleId = childNode.data.cycle
|
||||
if (cycleId) {
|
||||
const parentNode = graphRef.current?.getCellById(cycleId)
|
||||
if (parentNode) {
|
||||
const addedChild = graphRef.current?.addNode(childNode)
|
||||
if (addedChild) {
|
||||
parentNode.addChild(addedChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 调整父节点大小以适应子节点
|
||||
setTimeout(() => {
|
||||
const parentNodesWithChildren = parentNodes.filter(parentNode => {
|
||||
const parentId = parentNode.data.id
|
||||
return childNodes.some(child => child.data.cycle === parentId)
|
||||
})
|
||||
|
||||
parentNodesWithChildren.forEach(parentNodeConfig => {
|
||||
const parentNode = graphRef.current?.getCellById(parentNodeConfig.data.id)
|
||||
if (parentNode) {
|
||||
const children = parentNode.getChildren()
|
||||
if (children && children.length > 0) {
|
||||
const childBounds = children.map(child => child.getBBox())
|
||||
const minX = Math.min(...childBounds.map(b => b.x))
|
||||
const minY = Math.min(...childBounds.map(b => b.y))
|
||||
const maxX = Math.max(...childBounds.map(b => b.x + b.width))
|
||||
const maxY = Math.max(...childBounds.map(b => b.y + b.height))
|
||||
|
||||
const padding = 24
|
||||
const headerHeight = 50
|
||||
const parentBBox = parentNode.getBBox()
|
||||
|
||||
const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2)
|
||||
const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight)
|
||||
|
||||
console.log('newWidth', newHeight, newWidth)
|
||||
|
||||
parentNode.prop('size', { width: newWidth, height: newHeight })
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
if (edges.length) {
|
||||
// 去重处理:相同节点之间的连线仅连一次
|
||||
@@ -185,6 +276,14 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点且有label,根据label匹配对应的端口
|
||||
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||
if (matchingPort) {
|
||||
sourcePort = label;
|
||||
}
|
||||
}
|
||||
|
||||
const edgeConfig = {
|
||||
source: {
|
||||
cell: sourceCell.id,
|
||||
@@ -304,6 +403,12 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
// 节点选择事件
|
||||
const nodeClick = ({ node }: { node: Node }) => {
|
||||
// 忽略 add-node 类型的节点点击
|
||||
if (node.getData()?.type === 'add-node' || node.getData().type === 'break' || node.getData().type === 'cycle-start') {
|
||||
setSelectedNode(null)
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = graphRef.current?.getNodes();
|
||||
|
||||
nodes?.forEach(vo => {
|
||||
@@ -360,9 +465,9 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
// 节点移动事件
|
||||
const nodeMoved = ({ node }: { node: Node }) => {
|
||||
const parentId = node.getData()?.parentId;
|
||||
if (parentId) {
|
||||
const parentNode = graphRef.current!.getNodes().find(n => n.id === parentId);
|
||||
const cycle = node.getData()?.cycle;
|
||||
if (cycle) {
|
||||
const parentNode = graphRef.current!.getNodes().find(n => n.id === cycle);
|
||||
if (parentNode?.getData()?.isGroup) {
|
||||
// 获取父节点和子节点的边界框
|
||||
const parentBBox = parentNode.getBBox();
|
||||
@@ -465,21 +570,23 @@ export const useWorkflowGraph = ({
|
||||
nodesToDelete.forEach(nodeToDelete => {
|
||||
// 检查是否为子节点
|
||||
const nodeData = nodeToDelete.getData();
|
||||
if (nodeData.parentId) {
|
||||
if (nodeData.cycle) {
|
||||
// 找到对应的父节点
|
||||
const parentNode = nodes?.find(n => n.id === nodeData.parentId);
|
||||
const parentNode = nodes?.find(n => n.id === nodeData.cycle);
|
||||
if (parentNode) {
|
||||
// 使用removeChild方法删除子节点
|
||||
parentNode.removeChild(nodeToDelete);
|
||||
parentNodesToUpdate.push(parentNode);
|
||||
}
|
||||
// 将子节点添加到删除列表
|
||||
cells.push(nodeToDelete);
|
||||
}
|
||||
// 检查是否为 LoopNode、IterationNode 或 SubGraphNode
|
||||
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
||||
// 查找所有 parentId 为当前节点 id 的子节点
|
||||
// 查找所有 cycle 为当前节点 id 的子节点
|
||||
nodes?.forEach(node => {
|
||||
const data = node.getData();
|
||||
if (data.parentId === nodeToDelete.id) {
|
||||
if (data.cycle === nodeToDelete.id || data.cycle === nodeToDelete.getData()?.id) {
|
||||
cells.push(node);
|
||||
}
|
||||
});
|
||||
@@ -582,13 +689,14 @@ export const useWorkflowGraph = ({
|
||||
if (sourceType === 'end') return false;
|
||||
|
||||
// 获取源节点和目标节点的父节点ID
|
||||
const sourceParentId = sourceCell?.getData()?.parentId;
|
||||
const targetParentId = targetCell?.getData()?.parentId;
|
||||
const sourceParentId = sourceCell?.getData()?.cycle;
|
||||
const targetParentId = targetCell?.getData()?.cycle;
|
||||
|
||||
// 验证父子节点关系:
|
||||
// 1. 如果两个节点都有父节点ID,必须相同才能连线
|
||||
// 2. 如果一个有父节点ID,另一个没有,不能连线
|
||||
// 3. 如果两个都没有父节点ID,可以正常连线
|
||||
// 2. 如果两个都没有父节点ID,可以正常连线
|
||||
// 3. 如果一个有父节点,一个没有,不能连线
|
||||
console.log('sourceParentId', sourceParentId, targetParentId)
|
||||
if (sourceParentId && targetParentId) {
|
||||
// 同一父节点下的子节点可以互相连线
|
||||
return sourceParentId === targetParentId;
|
||||
@@ -635,6 +743,28 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.on('node:click', nodeClick);
|
||||
// 监听连线选择事件
|
||||
graphRef.current.on('edge:click', edgeClick);
|
||||
// 监听连接桩点击事件
|
||||
graphRef.current.on('node:port:click', ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => {
|
||||
e.stopPropagation();
|
||||
const portElement = e.target as HTMLElement;
|
||||
const rect = portElement.getBoundingClientRect();
|
||||
|
||||
// 创建临时的popover触发元素
|
||||
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);
|
||||
|
||||
// 触发自定义事件来显示节点选择popover
|
||||
const customEvent = new CustomEvent('port:click', {
|
||||
detail: { node, port, element: tempDiv, rect }
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
});
|
||||
// 监听画布点击事件,取消选择
|
||||
graphRef.current.on('blank:click', blankClick);
|
||||
// 监听缩放事件
|
||||
@@ -723,36 +853,23 @@ export const useWorkflowGraph = ({
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
} else {
|
||||
// 检查是否放置在群组内
|
||||
const groups = graphRef.current.getNodes().filter(node => {
|
||||
const shape = node.shape;
|
||||
return shape === 'loop-node' || shape === 'iteration-node' || shape === 'subgraph-node';
|
||||
});
|
||||
let parentGroup = null;
|
||||
|
||||
for (const group of groups) {
|
||||
const bbox = group.getBBox();
|
||||
if (point.x >= bbox.x && point.x <= bbox.x + bbox.width &&
|
||||
point.y >= bbox.y && point.y <= bbox.y + bbox.height) {
|
||||
parentGroup = group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const childNode = graphRef.current.addNode({
|
||||
// 普通节点创建,不支持拖拽到循环节点内
|
||||
graphRef.current.addNode({
|
||||
...(graphNodeLibrary[dragData.type] || graphNodeLibrary.default),
|
||||
x: point.x - 60,
|
||||
y: point.y - 20,
|
||||
data: { ...cleanNodeData, parentId: parentGroup?.id },
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
parentGroup?.addChild(childNode);
|
||||
}
|
||||
};
|
||||
// 保存workflow配置
|
||||
const handleSave = (flag = true) => {
|
||||
if (!graphRef.current || !config) return Promise.resolve()
|
||||
return new Promise((resolve, reject) => {
|
||||
const nodes = graphRef.current?.getNodes() || [];
|
||||
const nodes = graphRef.current?.getNodes().filter((node: Node) => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.type !== 'add-node';
|
||||
}) || [];
|
||||
const edges = graphRef.current?.getEdges() || []
|
||||
|
||||
const params = {
|
||||
@@ -771,7 +888,7 @@ export const useWorkflowGraph = ({
|
||||
itemConfig = {
|
||||
...itemConfig,
|
||||
...data.config[key].defaultValue,
|
||||
knowledge_bases: knowledge_bases.map((vo: any) => ({ kb_id: vo.id, ...vo.config }))
|
||||
knowledge_bases: knowledge_bases?.map((vo: any) => ({ kb_id: vo.id, ...vo.config }))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -781,6 +898,7 @@ export const useWorkflowGraph = ({
|
||||
id: data.id || node.id,
|
||||
type: data.type,
|
||||
name: data.name,
|
||||
cycle: data.cycle, // 保存cycle参数
|
||||
position: {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
@@ -793,8 +911,9 @@ export const useWorkflowGraph = ({
|
||||
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
const sourcePortId = edge.getSourcePortId();
|
||||
|
||||
// 过滤无效连线:源节点或目标节点不存在
|
||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id) {
|
||||
// 过滤无效连线:源节点或目标节点不存在,或者是add-node类型
|
||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
||||
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -807,6 +926,15 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点的右侧端口连线,添加label
|
||||
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
||||
return {
|
||||
source: sourceCell.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
label: sourcePortId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: sourceCell?.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
@@ -832,6 +960,7 @@ export const useWorkflowGraph = ({
|
||||
|
||||
return {
|
||||
config,
|
||||
setConfig,
|
||||
graphRef,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
@@ -848,6 +977,6 @@ export const useWorkflowGraph = ({
|
||||
deleteEvent,
|
||||
copyEvent,
|
||||
parseEvent,
|
||||
handleSave
|
||||
handleSave,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,20 +4,24 @@ import clsx from 'clsx';
|
||||
import NodeLibrary from './components/NodeLibrary'
|
||||
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 Chat from './components/Chat/Chat';
|
||||
import type { ChatRef } from './types'
|
||||
import type { ChatRef, AddChatVariableRef, ChatVariable } from './types'
|
||||
import arrowIcon from '@/assets/images/workflow/arrow.png'
|
||||
import AddChatVariable from './components/AddChatVariable';
|
||||
|
||||
const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const miniMapRef = useRef<HTMLDivElement>(null);
|
||||
const addChatVariableRef = useRef<AddChatVariableRef>(null)
|
||||
const chatRef = useRef<ChatRef>(null)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
// 使用自定义Hook初始化工作流图
|
||||
const {
|
||||
config,
|
||||
setConfig,
|
||||
graphRef,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
@@ -33,7 +37,7 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
deleteEvent,
|
||||
copyEvent,
|
||||
parseEvent,
|
||||
handleSave
|
||||
handleSave,
|
||||
} = useWorkflowGraph({ containerRef, miniMapRef });
|
||||
|
||||
const onDragOver = (event: React.DragEvent) => {
|
||||
@@ -45,11 +49,24 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
const handleToggle = () => {
|
||||
setCollapsed(prev => !prev)
|
||||
}
|
||||
const addVariable = () => {
|
||||
addChatVariableRef.current?.handleOpen()
|
||||
}
|
||||
const handleUpdateChatVariable = (variables: ChatVariable[]) => {
|
||||
setConfig(prev => {
|
||||
if (!prev) return null
|
||||
return {
|
||||
...prev,
|
||||
variables
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSave,
|
||||
handleRun,
|
||||
graphRef
|
||||
graphRef,
|
||||
addVariable
|
||||
}))
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
||||
@@ -97,12 +114,20 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
deleteEvent={deleteEvent}
|
||||
copyEvent={copyEvent}
|
||||
parseEvent={parseEvent}
|
||||
config={config}
|
||||
/>
|
||||
<Chat
|
||||
ref={chatRef}
|
||||
graphRef={graphRef}
|
||||
appId={config?.app_id as string}
|
||||
/>
|
||||
<PortClickHandler graph={graphRef.current} />
|
||||
|
||||
<AddChatVariable
|
||||
ref={addChatVariableRef}
|
||||
variables={config?.variables}
|
||||
onChange={handleUpdateChatVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,8 @@ export interface NodeConfig {
|
||||
|
||||
knowledge_retrieval?: KnowledgeConfig;
|
||||
|
||||
group_names?: Array<{key: string, value: string[]}>
|
||||
group_names?: Array<{ key: string, value: string[] }>
|
||||
cycle?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -34,6 +35,7 @@ export interface NodeProperties {
|
||||
name?: string;
|
||||
id?: string;
|
||||
config?: Record<string, NodeConfig>;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface NodeLibrary {
|
||||
@@ -53,6 +55,8 @@ export interface NodeItem {
|
||||
config: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
cycle?: string;
|
||||
}
|
||||
export interface EdgesItem {
|
||||
source: string;
|
||||
@@ -102,4 +106,15 @@ export interface ChatRef {
|
||||
export type GraphRef = React.MutableRefObject<Graph | undefined>
|
||||
export interface VariableConfigModalRef {
|
||||
handleOpen: (values: StartVariableItem[]) => void;
|
||||
}
|
||||
|
||||
export interface ChatVariable {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
default: string;
|
||||
}
|
||||
export interface AddChatVariableRef {
|
||||
handleOpen: (value?: ChatVariable) => void;
|
||||
}
|
||||