Merge branch 'develop' into feature/history_zy
@@ -174,4 +174,8 @@ export const getAppLogsUrl = (app_id: string) => `/apps/${app_id}/logs`
|
||||
// Get full conversation message history
|
||||
export const getAppLogDetail = (app_id: string, conversation_id: string) => {
|
||||
return request.get(`/apps/${app_id}/logs/${conversation_id}`)
|
||||
}
|
||||
// Reset agent model config to default
|
||||
export const resetAppModelConfig = (app_id: string) => {
|
||||
return request.get(`/apps/${app_id}/model/parameters/default`)
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
import type { Package } from '@/views/Package/types'
|
||||
|
||||
export const SYS_API_PREFIX = '/sys';
|
||||
// 套餐列表
|
||||
export const getPackageListUrl = `${SYS_API_PREFIX}/package-plans`
|
||||
export const getPackageList = (query: { category: Package['category']; status: boolean; }) => {
|
||||
export const getPackageListUrl = `/package-plans`
|
||||
export const getPackageList = (query?: { category?: Package['category']; status?: boolean; }) => {
|
||||
return request.get(getPackageListUrl, query)
|
||||
}
|
||||
// 获取套餐详情
|
||||
export const getPackageDetail = (package_plan_id: string) => {
|
||||
return request.get(`${SYS_API_PREFIX}/package-plans/${package_plan_id}`)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-25 11:17:44
|
||||
* @Last Modified time: 2026-04-14 18:36:01
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { CreateModalData, ChangeEmailModalForm } from '@/views/UserManagement/types'
|
||||
@@ -56,4 +56,9 @@ export const sendEmailCode = (data: { email: string }) => {
|
||||
// Verify code and change email
|
||||
export const changeEmail = (data: ChangeEmailModalForm) => {
|
||||
return request.put('/users/change-email', data)
|
||||
}
|
||||
|
||||
// 获取租户套餐信息
|
||||
export const getTenantSubscription = () => {
|
||||
return request.get('/tenant/subscription')
|
||||
}
|
||||
17
web/src/assets/images/application/export.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>导出</title>
|
||||
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<g id="记忆库-个人记忆-感知记忆-文本" transform="translate(-573, -158)" stroke="#171719">
|
||||
<g id="导出" transform="translate(573, 158)">
|
||||
<g id="编组-54" transform="translate(3, 3)">
|
||||
<path d="M10,6 L10,7.5 C10,8.88071187 8.88071187,10 7.5,10 L2.5,10 C1.11928813,10 0,8.88071187 0,7.5 L0,6 L0,6" id="路径"></path>
|
||||
<g id="编组-11" transform="translate(2, 0)">
|
||||
<line x1="3" y1="0.08499952" x2="3" y2="6.99635859" id="路径-24"></line>
|
||||
<polyline id="路径-25" stroke-linejoin="round" points="0 3 2.98005548 6.08298138e-18 6 3"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
17
web/src/assets/images/application/import.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>导入</title>
|
||||
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<g id="记忆库-个人记忆-感知记忆-文本" transform="translate(-555, -158)" stroke="#171719">
|
||||
<g id="导入" transform="translate(555, 158)">
|
||||
<g id="编组-54" transform="translate(3, 3)">
|
||||
<path d="M10,6 L10,7.5 C10,8.88071187 8.88071187,10 7.5,10 L2.5,10 C1.11928813,10 0,8.88071187 0,7.5 L0,6 L0,6" id="路径"></path>
|
||||
<g id="编组-11" transform="translate(5, 3.4982) scale(1, -1) translate(-5, -3.4982)translate(2, 0)">
|
||||
<line x1="3" y1="0.08499952" x2="3" y2="6.99635859" id="路径-24"></line>
|
||||
<polyline id="路径-25" stroke-linejoin="round" points="0 3 2.98005548 6.08298138e-18 6 3"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
15
web/src/assets/images/common/close_grey.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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>关闭</title>
|
||||
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="应用管理-My-Shares" transform="translate(-1396, -127)" fill="#5B6167" fill-rule="nonzero">
|
||||
<g id="卡片1备份-2" transform="translate(1044, 108)">
|
||||
<g id="编组-12" transform="translate(349, 16)">
|
||||
<g id="关闭" transform="translate(3, 3)">
|
||||
<polygon id="路径" points="9.00000098 8 13.3333333 12.3333324 12.3333324 13.3333333 8 9.00000098 3.66666764 13.3333333 2.66666667 12.3333324 6.99999902 8 2.66666667 3.66666764 3.66666764 2.66666667 8 6.99999902 12.3333324 2.66666667 13.3333333 3.66666764"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1005 B |
16
web/src/assets/images/index/arrow_right_dark.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 |
@@ -1,17 +0,0 @@
|
||||
<?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>退出</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="应用管理-编排-默认状态" transform="translate(-1262, -24)" stroke="#5B6167">
|
||||
<g id="返回空间" transform="translate(1262, 24)">
|
||||
<g id="退出" transform="translate(8, 8) scale(-1, 1) translate(-8, -8)">
|
||||
<g id="编组-7" transform="translate(2.5, 2)">
|
||||
<path d="M6,12 L1,12 C0.44771525,12 0,11.5522847 0,11 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L6,0 L6,0" id="路径"></path>
|
||||
<line x1="11" y1="6" x2="3" y2="6" id="路径-6"></line>
|
||||
<polyline id="路径" points="8 3 11 6 8 9"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
19
web/src/assets/images/logout_grey.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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>退出</title>
|
||||
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="空间配置" transform="translate(-22, -763)" stroke="#5B6167" stroke-width="1.2">
|
||||
<g id="退出" transform="translate(0, 742)">
|
||||
<g id="返回空间" transform="translate(12, 10)">
|
||||
<g id="退出" transform="translate(10, 11)">
|
||||
<g id="编组-7" transform="translate(2.5, 2)">
|
||||
<path d="M6,12 L1,12 C0.44771525,12 0,11.5522847 0,11 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L6,0 L6,0" id="路径"></path>
|
||||
<line x1="11" y1="6" x2="3" y2="6" id="路径-6"></line>
|
||||
<polyline id="路径" points="8 3 11 6 8 9"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,17 +0,0 @@
|
||||
<?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>退出</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="应用管理-编排-默认状态" transform="translate(-1262, -24)" stroke="#155EEF">
|
||||
<g id="返回空间" transform="translate(1262, 24)">
|
||||
<g id="退出" transform="translate(8, 8) scale(-1, 1) translate(-8, -8)">
|
||||
<g id="编组-7" transform="translate(2.5, 2)">
|
||||
<path d="M6,12 L1,12 C0.44771525,12 0,11.5522847 0,11 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L6,0 L6,0" id="路径"></path>
|
||||
<line x1="11" y1="6" x2="3" y2="6" id="路径-6"></line>
|
||||
<polyline id="路径" points="8 3 11 6 8 9"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
BIN
web/src/assets/images/menuNew/package_bg.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
17
web/src/assets/images/package/api_ops.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>频次</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -750)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="频次">
|
||||
<g transform="translate(6, 442)">
|
||||
<path d="M8.32397431,14.7174176 L13.3908989,7.29436898 C13.5898421,7.00271091 13.5091666,6.60853935 13.2103296,6.41423815 C13.1037093,6.3447846 12.9784064,6.30774783 12.8502468,6.30780564 L8.86631603,6.30780564 L8.86631603,1.63467602 C8.86631603,1.28423255 8.57550429,1 8.21668864,1 C7.99937181,1 7.79662724,1.10601998 7.67603646,1.28258243 L2.60911183,8.70563102 C2.41016872,8.99728909 2.49084416,9.39125438 2.78947001,9.58576185 C2.89614942,9.65527988 3.02151846,9.69238624 3.149764,9.69240062 L7.13369475,9.69240062 L7.13369475,14.365324 C7.13369475,14.7157675 7.42450649,15 7.78332213,15 C8.00063896,15 8.20359472,14.89398 8.32397431,14.7174176 Z" id="路径"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
17
web/src/assets/images/package/app.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>应用</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -414)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="应用">
|
||||
<g transform="translate(6, 106)">
|
||||
<path d="M5.73221919,1.5 L2.70920955,1.5 C2.04142437,1.5 1.5,2.0410081 1.5,2.70827986 L1.5,5.72897951 C1.5,6.39623705 2.04142437,6.93725937 2.70920955,6.93725937 L5.73223342,6.93725937 C6.40000437,6.93725937 6.94144297,6.39625128 6.94144297,5.72897951 L6.94144297,2.70826564 C6.94144297,2.0410081 6.40000437,1.5 5.73221919,1.5 L5.73221919,1.5 Z M12.7040542,1.5 L9.68104456,1.5 C9.01325938,1.5 8.47183501,2.0410081 8.47183501,2.70827986 L8.47183501,5.72897951 C8.47183501,6.39623705 9.01325938,6.93725937 9.68104456,6.93725937 L12.7040684,6.93725937 C13.3718536,6.93725937 13.913278,6.39625128 13.913278,5.72897951 L13.913278,2.70826564 C13.913278,2.0410081 13.3718394,1.5 12.7040542,1.5 L12.7040542,1.5 Z M5.73221919,8.4711823 L2.70920955,8.4711823 C2.04142437,8.4711823 1.5,9.01220462 1.5,9.67946216 L1.5,12.7001618 C1.5,13.3674336 2.04142437,13.9084417 2.70920955,13.9084417 L5.73223342,13.9084417 C6.40000437,13.9084417 6.94144297,13.3674336 6.94144297,12.7001618 L6.94144297,9.67946216 C6.94144297,9.01220462 6.40000437,8.4711823 5.73221919,8.4711823 L5.73221919,8.4711823 Z M14.1766032,10.5791939 L12.1883205,8.5402163 C11.7490465,8.08947578 11.0275174,8.07989009 10.5761312,8.51898273 L8.53500119,10.5057368 C8.08465397,10.944673 8.07520324,11.6656474 8.51434907,12.1163879 L10.5029307,14.1556499 C10.9422047,14.6063905 11.6640184,14.6158339 12.1148069,14.1768835 L14.1556522,12.1898308 C14.6067395,11.7505959 14.6155925,11.0296358 14.1766032,10.5791939 L14.1766032,10.5791939 Z" id="形状" transform="translate(8, 8) scale(1, -1) translate(-8, -8)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
13
web/src/assets/images/package/arrow.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 49</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="平台管理-套餐管理-个人版" transform="translate(-252, -435)" stroke="currentColor" stroke-width="2">
|
||||
<g id="编组-15" transform="translate(252, 398)">
|
||||
<g id="编组-49" transform="translate(0, 37)">
|
||||
<polyline id="路径" points="15 18 9 12 15 6"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 759 B |
18
web/src/assets/images/package/disable.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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>编组</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-套餐管理-个人版" transform="translate(-506, -748)" stroke="#171719" stroke-width="1.1">
|
||||
<g id="编组-5" transform="translate(288, 112)">
|
||||
<g id="编组-23" transform="translate(0, 616)">
|
||||
<g id="编组-19" transform="translate(186, 12)">
|
||||
<g id="编组" transform="translate(32, 8)">
|
||||
<circle id="椭圆形" cx="8" cy="8" r="6.45"></circle>
|
||||
<line x1="3.25956213" y1="3.2473795" x2="12.6934486" y2="12.6812659" id="路径-12"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
web/src/assets/images/package/enable.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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>编组</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-套餐管理-商业版-2" transform="translate(-1077, -748)" fill-rule="nonzero">
|
||||
<g id="编组-5备份-2" transform="translate(846, 112)">
|
||||
<g id="编组-23" transform="translate(0, 616)">
|
||||
<g id="编组-19" transform="translate(186, 12)">
|
||||
<g id="编组" transform="translate(45, 8)">
|
||||
<path d="M8,1.55 C9.78111832,1.55 11.3936183,2.27194084 12.5608387,3.43916126 C13.7280592,4.60638168 14.45,6.21888168 14.45,8 C14.45,9.78111832 13.7280592,11.3936183 12.5608387,12.5608387 C11.3936183,13.7280592 9.78111832,14.45 8,14.45 C6.21888168,14.45 4.60638168,13.7280592 3.43916126,12.5608387 C2.27194084,11.3936183 1.55,9.78111832 1.55,8 C1.55,6.21888168 2.27194084,4.60638168 3.43916126,3.43916126 C4.60638168,2.27194084 6.21888168,1.55 8,1.55 Z" id="路径" stroke="#171719" stroke-width="1.1"></path>
|
||||
<path d="M6.94857143,10.7314706 C6.78136231,10.7333786 6.62051945,10.6673918 6.50285714,10.5485714 L4.4,8.44571429 C4.1538388,8.19955308 4.15383881,7.80044692 4.40000001,7.55428572 C4.64616121,7.30812452 5.04526737,7.30812452 5.29142857,7.55428571 L6.94857143,9.21142857 L10.7085714,5.45142857 C10.9547326,5.20526739 11.3538388,5.20526739 11.6,5.45142859 C11.8461612,5.69758978 11.8461612,6.09669594 11.6,6.34285714 L7.39428571,10.5485714 C7.27662341,10.6673918 7.11578054,10.7333786 6.94857143,10.7314706 Z" id="路径" fill="#171719"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
19
web/src/assets/images/package/end_user.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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>终端</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -558)" fill="currentColor">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="终端">
|
||||
<g transform="translate(6, 250)">
|
||||
<g id="编组-12" transform="translate(1, 1.5)">
|
||||
<path d="M13,0 C13.5522847,0 14,0.44771525 14,1 L14,3 C14,3.27614237 13.7761424,3.5 13.5,3.5 L0.5,3.5 C0.223857625,3.5 0,3.27614237 0,3 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L13,0 Z M2.1,1.2 C1.76862915,1.2 1.5,1.46862915 1.5,1.8 C1.5,2.13137085 1.76862915,2.4 2.1,2.4 C2.43137085,2.4 2.7,2.13137085 2.7,1.8 C2.7,1.46862915 2.43137085,1.2 2.1,1.2 Z M3.9,1.2 C3.56862915,1.2 3.3,1.46862915 3.3,1.8 C3.3,2.13137085 3.56862915,2.4 3.9,2.4 C4.23137085,2.4 4.5,2.13137085 4.5,1.8 C4.5,1.46862915 4.23137085,1.2 3.9,1.2 Z M5.7,1.2 C5.36862915,1.2 5.1,1.46862915 5.1,1.8 C5.1,2.13137085 5.36862915,2.4 5.7,2.4 C6.03137085,2.4 6.3,2.13137085 6.3,1.8 C6.3,1.46862915 6.03137085,1.2 5.7,1.2 Z M13.5,4.5 C13.7761424,4.5 14,4.72385763 14,5 L14,11.8 C14,12.4627417 13.4627417,13 12.8,13 L1.20000005,13 C0.537258322,13 0,12.4627417 0,11.8 L0,5 C0,4.72385763 0.223857625,4.5 0.5,4.5 Z M8,10 L6,10 C5.72385763,10 5.5,10.2238576 5.5,10.5 C5.5,10.7761424 5.72385763,11 6,11 L8,11 C8.27614237,11 8.5,10.7761424 8.5,10.5 C8.5,10.2238576 8.27614237,10 8,10 Z M2.85245368,7.14535031 C2.65658607,6.95069552 2.3400051,6.9516787 2.14535031,7.14754632 L2.0877102,7.21697355 C1.95331998,7.41226 1.97344177,7.68162321 2.14754632,7.85464969 L3.293,8.993 L2.14535707,10.1475395 C1.95069852,10.3434034 1.95167564,10.6599844 2.14753952,10.8546429 C2.3434034,11.0493015 2.65998438,11.0483244 2.85464293,10.8524605 L4.35464293,9.34317241 C4.54930412,9.14730587 4.54832395,8.83071967 4.35245368,8.63606224 Z" id="形状结合"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
17
web/src/assets/images/package/knowledge.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>知识库容量</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -462)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="知识库容量">
|
||||
<g transform="translate(6, 154)">
|
||||
<path d="M2,13.2659458 L2,13.2763169 C2.02,14.2304616 2.751,15.0000005 3.672,15.0000005 L13,15.0000005 C13.552,15.0000005 14,14.5426328 14,13.9691089 L14,2.03607771 C14,1.7611133 13.8946114,1.49742586 13.7070462,1.30309395 C13.519481,1.10876204 13.2651245,0.999725798 13,1.00000052 L4,1.00000052 C2.895,1.00000052 2,1.92095808 2,3.06696881 L2,13.2669829 L2,13.2659458 Z M4.79999988,6.99763005 L4.79999988,3.0213358 C4.79999988,2.79317077 4.98,2.60649029 5.2,2.60649029 L7.6,2.60649029 C7.821,2.60649029 8.00000012,2.79317077 8.00000012,3.0213358 L8.00000012,6.99763005 C8.0001274,7.1655208 7.90266905,7.31693863 7.75311605,7.38120551 C7.60356305,7.4454724 7.43140463,7.40991604 7.317,7.29113325 L6.683,6.63360311 C6.6079727,6.55570465 6.5061637,6.51193446 6.4,6.51193446 C6.2938363,6.51193446 6.1920273,6.55570465 6.117,6.63360311 L5.483,7.29113325 C5.36859537,7.40991604 5.19643695,7.4454724 5.04688395,7.38120551 C4.89733095,7.31693863 4.7998726,7.1655208 4.79999988,6.99763005 L4.79999988,6.99763005 Z M3.576,13.5833026 C3.45866667,13.4629974 3.4,13.3164186 3.4,13.1435663 C3.4,12.9720968 3.45866667,12.8255181 3.576,12.7038301 C3.69266667,12.5821421 3.834,12.5212981 4,12.5212981 L12.3,12.5212981 C12.4656854,12.5212981 12.6,12.6556126 12.6,12.8212981 L12.6,13.4658346 C12.6,13.63152 12.4656854,13.7658346 12.3,13.7658346 L4,13.7658346 L4,13.7658346 C3.834,13.7658346 3.69266667,13.7049906 3.576,13.5833026 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
20
web/src/assets/images/package/memory_config.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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>记忆引擎</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -510)" fill="currentColor">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="记忆引擎">
|
||||
<g transform="translate(6, 202)">
|
||||
<g id="编组-25" transform="translate(1, 1.5)">
|
||||
<path d="M4.33333333,0 C5.52995029,0 6.5,0.970564798 6.5,2.16781715 L6.499,2.193 L6.5,10.8390858 L6.47808338,10.8391196 C6.31578908,12.0589502 5.27189987,13 4.00833333,13 C2.63222383,13 1.51666667,11.8838505 1.51666667,10.5070103 C1.51666667,10.332196 1.53465035,10.1615842 1.56886767,9.99692601 C0.663423478,9.73754175 0,8.90246334 0,7.9123518 C0,7.36506373 0.202700377,6.86514401 0.537084253,6.48364722 C0.203054284,6.10374909 0,5.60343451 0,5.05566888 C0,3.85841652 0.970049709,2.88785173 2.16666667,2.88785173 C2.20862378,2.88785173 2.25030235,2.88904496 2.29167033,2.89139936 C2.210453,2.6663156 2.16666667,2.42218251 2.16666667,2.16781715 C2.16666667,0.970564798 3.13671638,0 4.33333333,0 Z" id="形状结合"></path>
|
||||
<path d="M11.8333333,0 C13.0299503,0 14,0.970564798 14,2.16781715 L13.999,2.193 L14,10.8390858 L13.9780834,10.8391196 C13.8157891,12.0589502 12.7718999,13 11.5083333,13 C10.1322238,13 9.01666667,11.8838505 9.01666667,10.5070103 C9.01666667,10.332196 9.03465035,10.1615842 9.06886767,9.99692601 C8.16342348,9.73754175 7.5,8.90246334 7.5,7.9123518 C7.5,7.36506373 7.70270038,6.86514401 8.03708425,6.48364722 C7.70305428,6.10374909 7.5,5.60343451 7.5,5.05566888 C7.5,3.85841652 8.47004971,2.88785173 9.66666667,2.88785173 C9.70862378,2.88785173 9.75030235,2.88904496 9.79167033,2.89139936 C9.710453,2.6663156 9.66666667,2.42218251 9.66666667,2.16781715 C9.66666667,0.970564798 10.6367164,0 11.8333333,0 Z" id="形状结合" transform="translate(10.75, 6.5) scale(-1, 1) translate(-10.75, -6.5)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
17
web/src/assets/images/package/model.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>模型</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -654)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="模型">
|
||||
<g transform="translate(6, 346)">
|
||||
<path d="M8.9375,2.45132371 C8.9375,2.73288225 8.816875,2.98590446 8.625,3.1602932 L8.625,4.35374628 L11.75,4.35374628 C12.7855339,4.35374628 13.625,5.20548988 13.625,6.25616886 L13.625,12.5975774 C13.625,13.6482564 12.7855339,14.5 11.75,14.5 L4.25,14.5 C3.21446609,14.5 2.375,13.6482564 2.375,12.5975774 L2.375,6.25616886 C2.375,5.20548988 3.21446609,4.35374628 4.25,4.35374628 L7.375,4.35374628 L7.375,3.1602932 C7.05390217,2.86889419 6.9698603,2.39296943 7.17127056,2.00658393 C7.37268081,1.62019842 7.80769695,1.42281399 8.22581154,1.52809601 C8.64392612,1.63337802 8.93750975,2.0142252 8.9375,2.45132371 Z M0.5,7.52445058 L1.75,7.52445058 L1.75,11.3292957 L0.5,11.3292957 L0.5,7.52445058 L0.5,7.52445058 Z M15.5,7.52445058 L14.25,7.52445058 L14.25,11.3292957 L15.5,11.3292957 L15.5,7.52445058 L15.5,7.52445058 Z M6.125,10.3780844 C6.64276695,10.3780844 7.0625,9.95221264 7.0625,9.42687315 C7.0625,8.90153366 6.64276695,8.47566186 6.125,8.47566186 C5.60723305,8.47566186 5.1875,8.90153366 5.1875,9.42687315 C5.1875,9.95221264 5.60723305,10.3780844 6.125,10.3780844 Z M10.8125,9.42687315 C10.8125,8.90153366 10.392767,8.47566186 9.875,8.47566186 C9.35723305,8.47566186 8.9375,8.90153366 8.9375,9.42687315 C8.9375,9.95221264 9.35723305,10.3780844 9.875,10.3780844 C10.392767,10.3780844 10.8125,9.95221264 10.8125,9.42687315 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
17
web/src/assets/images/package/ontology.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>本体工程</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -606)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="本体工程">
|
||||
<g transform="translate(6, 298)">
|
||||
<path d="M8.00029166,11.9565844 C8.55099458,11.9458014 9.06454299,12.2334279 9.34305434,12.7086345 C9.62156569,13.1838411 9.62156569,13.7724504 9.34305434,14.247657 C9.06454299,14.7228637 8.55099458,15.0104901 8.00029166,14.9997071 C7.17156632,14.9834803 6.50823134,14.3070298 6.50823134,13.4781458 C6.50823134,12.6492617 7.17156632,11.9728113 8.00029166,11.9565844 L8.00029166,11.9565844 Z M10.6998458,3.52602054 C11.3812309,3.93684948 11.9607513,4.49662702 12.3949419,5.16336869 C12.4894327,5.30835024 12.5214141,5.48532999 12.4836406,5.65421257 C12.4458671,5.82309514 12.3415359,5.96958625 12.1942836,6.06049782 C11.5289324,6.46893919 11.1240922,7.19420398 11.1256614,7.97491772 C11.1256614,8.78688374 11.5578934,9.5224363 12.2456148,9.91966968 C12.5576851,10.0993288 12.6591809,10.5029786 12.4701887,10.8109657 C12.0643977,11.4723107 11.5176721,12.0359657 10.8690055,12.46173 C10.7204191,12.5590552 10.5377976,12.589527 10.3656617,12.5457166 C10.1935258,12.5019063 10.047691,12.387839 9.96370985,12.2313229 C9.57624512,11.5120945 8.82541474,11.0634332 8.00845798,11.0629552 C7.19433681,11.0636561 6.44575953,11.5094456 6.05728927,12.2249066 C5.8782134,12.5515596 5.46114745,12.6565552 5.15082705,12.4512304 C4.48752495,12.0126641 3.93197322,11.4297935 3.52572809,10.7462184 C3.35319815,10.4559047 3.43473788,10.0815865 3.71238698,9.88933762 C4.32253406,9.47072463 4.68664967,8.77785728 4.68534645,8.03791508 C4.686623,7.30050165 4.32496025,6.60967794 3.71822007,6.19057571 C3.4410501,5.99756611 3.36052505,5.62297181 3.53389441,5.3331116 C3.97422818,4.59704162 4.58752564,3.97953949 5.32056998,3.53418686 C5.60872463,3.35919418 5.98262572,3.44085743 6.17278447,3.72026241 C6.5863185,4.32982165 7.27545367,4.69435748 8.01204828,4.69318847 C8.74864289,4.69201945 9.43661752,4.32529808 9.84821467,3.71442932 C10.0377901,3.43444104 10.4111079,3.35219449 10.6998458,3.52602054 Z M2.52185326,6.47756371 C3.3623496,6.47756371 4.04370651,7.1589205 4.04370651,7.99941669 C4.04370651,8.83991289 3.3623496,9.52126968 2.52185326,9.52126968 C1.68135691,9.52126968 1,8.83991289 1,7.99941669 C1,7.1589205 1.68135691,6.47756371 2.52185326,6.47756371 L2.52185326,6.47756371 Z M13.4781467,6.47756371 C14.3186431,6.47756371 15,7.1589205 15,7.99941669 C15,8.83991289 14.3186431,9.52126968 13.4781467,9.52126968 C12.6376504,9.52126968 11.9562935,8.83991289 11.9562935,7.99941669 C11.9562935,7.1589205 12.6376504,6.47756371 13.4781467,6.47756371 L13.4781467,6.47756371 Z M8.00029166,1.0002929 C8.55099467,0.989509733 9.0645432,1.27713619 9.34305463,1.75234287 C9.62156606,2.22754954 9.62156606,2.81615891 9.34305463,3.29136558 C9.0645432,3.76657226 8.55099467,4.05419872 8.00029166,4.04341555 C7.17156646,4.02718851 6.50823169,3.35073813 6.50823169,2.52185423 C6.50823169,1.69297032 7.17156646,1.01651994 8.00029166,1.0002929 L8.00029166,1.0002929 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
17
web/src/assets/images/package/skill.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>技能</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -366)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="技能">
|
||||
<g transform="translate(6, 58)">
|
||||
<path d="M13.1093988,2.59578193 L13.4555609,2.94978293 C13.8735923,3.3772807 13.8735923,4.06040714 13.4555609,4.48790491 L4.31301403,13.8374857 L4.2959692,13.8545385 C3.86181051,14.2794839 3.16536908,14.272015 2.74042373,13.8378563 L2.3940984,13.4840222 C1.97572454,13.0565775 1.97556347,12.3731715 2.3937358,11.9455296 L11.5364459,2.59578193 L11.5538614,2.57836647 C11.9882203,2.15362581 12.6846581,2.16142298 13.1093988,2.59578193 Z M12.0437778,8.13777778 C12.1548235,8.14157777 12.2459151,8.22925253 12.2553906,8.3422989 C12.3375526,9.32051934 13.0757988,10.1076496 14.0280997,10.2323906 C14.140251,10.2448863 14.2244871,10.3429276 14.2221758,10.458273 C14.2198645,10.5736184 14.1317717,10.6680483 14.019214,10.6758351 C13.0620685,10.7592707 12.2919283,11.5150446 12.1709758,12.4895962 C12.154941,12.6005116 12.0607522,12.6819263 11.9511371,12.6796198 C11.841522,12.6773133 11.7506877,12.5920053 11.7391279,12.4805092 C11.6579275,11.5026771 10.9208042,10.715182 9.96908446,10.5895088 C9.85693321,10.5770131 9.77269711,10.4789718 9.77500841,10.3636264 C9.7773197,10.248281 9.86541254,10.1538511 9.97797022,10.1460643 C10.9344274,10.0618167 11.7036291,9.30623726 11.8244312,8.33230322 C11.836223,8.24007169 11.9028425,8.16505835 11.9914835,8.14420276 L12.0437778,8.13777778 Z M4.04677778,1.77777778 C4.1576393,1.78069238 4.24873095,1.86836714 4.25820637,1.98141351 C4.34036837,2.95963395 5.07861462,3.74676424 6.03091554,3.87150517 C6.14306679,3.88400094 6.22730289,3.98204216 6.22499159,4.09738759 C6.2226803,4.21273302 6.13458746,4.30716294 6.02202978,4.31494975 C5.06488427,4.39838528 4.29474414,5.15415919 4.17379165,6.12871078 C4.15775682,6.23962624 4.06356797,6.32104094 3.95395289,6.31873441 C3.8443378,6.31642788 3.7535035,6.23111991 3.7419437,6.11962381 C3.66074329,5.14179167 2.92362,4.35429664 1.97190027,4.22862345 C1.85974901,4.21612767 1.77551292,4.11808646 1.77782421,4.00274103 C1.78013551,3.8873956 1.86822835,3.79296568 1.98078603,3.78517887 C2.93724323,3.70093132 3.70644486,2.94535187 3.827247,1.97141783 C3.83903882,1.8791863 3.90565827,1.80417296 3.99429929,1.78331737 L4.04677778,1.77777778 Z M7.97077778,1.77777778 C8.0434983,1.77530067 8.10642776,1.82945264 8.11640346,1.90326549 C8.16260941,2.23494023 8.42740507,2.48846695 8.75440104,2.51300179 C8.83292056,2.51049249 8.89856235,2.57355237 8.90101608,2.65384997 C8.90346982,2.73414756 8.84180632,2.80127582 8.7632868,2.80378512 C8.43951745,2.85069166 8.19138283,3.12184124 8.16705229,3.45532153 C8.16950603,3.53561913 8.10784253,3.60274738 8.02932301,3.60525668 C7.95080349,3.60776598 7.8851617,3.5447061 7.88270797,3.4644085 C7.83684011,3.13330736 7.57169477,2.87955371 7.24559896,2.85467221 C7.17114496,2.85152757 7.11169823,2.79012738 7.10936341,2.71395988 C7.10702859,2.63779237 7.16259799,2.57270502 7.2367132,2.56479757 C7.56078738,2.51781428 7.80901639,2.24618323 7.83294771,1.91235247 C7.83703762,1.85408063 7.87483694,1.80397192 7.92891392,1.78513476 L7.97077778,1.77777778 Z" id="形状结合"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
19
web/src/assets/images/package/sla.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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>SLA</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-套餐管理-个人版" transform="translate(-1058, -694)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5备份-4" transform="translate(1032, 112)">
|
||||
<g id="编组-13" transform="translate(0, 172)">
|
||||
<g transform="translate(20, 16)" id="SLA">
|
||||
<g transform="translate(6, 394)">
|
||||
<g id="形状" transform="translate(2, 1)">
|
||||
<path d="M10.8664675,0 L1.13077453,0 C0.507469547,0 0,0.500390625 0,1.12109375 L0,12.8789062 C0,13.496875 0.504711561,14 1.13077453,14 L10.8692255,14 C11.4925305,14 12,13.4996094 12,12.8789062 L12,1.12109375 C11.997242,0.500390625 11.4925305,0 10.8664675,0 Z M3.41714548,4.16171875 L4.69960929,3.97578125 C4.84302459,3.95390625 4.96713399,3.86640625 5.03056769,3.73789062 L5.60422891,2.58671875 C5.76419214,2.2640625 6.23029189,2.2640625 6.3930131,2.58671875 L6.96667433,3.73789062 C7.03010802,3.86640625 7.15421742,3.95664062 7.29763273,3.97578125 L8.58009653,4.16171875 C8.94139278,4.21367187 9.08480809,4.65390625 8.82279936,4.90546875 L7.89335785,5.80234375 C7.78855436,5.90351562 7.74166858,6.04570312 7.76649046,6.18789062 L7.98437141,7.45390625 C8.04504712,7.809375 7.66996093,8.08007812 7.34727649,7.91328125 L6.19995403,7.31445312 C6.07308665,7.24882812 5.91863939,7.24882812 5.79177201,7.31445312 L4.64720754,7.91328125 C4.3245231,8.08007812 3.94943691,7.809375 4.01011262,7.45390625 L4.22799356,6.18789062 C4.25281544,6.04570312 4.20592967,5.90351562 4.10112618,5.80234375 L3.17168467,4.90546875 C2.91519191,4.65390625 3.05584923,4.21367187 3.41714548,4.16171875 Z M9.00482648,11.4597656 L2.99241554,11.4597656 C2.65594116,11.4597656 2.38565847,11.1917969 2.38565847,10.8582031 C2.38565847,10.5246094 2.65594116,10.2566406 2.99241554,10.2566406 L9.00482648,10.2566406 C9.34130085,10.2566406 9.61158354,10.5273438 9.61158354,10.8582031 C9.61158354,11.1917969 9.34130085,11.4597656 9.00482648,11.4597656 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
17
web/src/assets/images/package/space.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>空间</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -318)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="空间">
|
||||
<g transform="translate(6, 10)">
|
||||
<path d="M13.6,5.75002346 L9.16,8.25495925 C8.75,8.48362559 8.5,8.94095826 8.5,9.45026056 L8.5,14.2938293 C8.5,14.8135256 8.99,15.1565251 9.4,14.9174648 L13.84,12.412529 C14.24,12.1838627 14.5,11.72653 14.5,11.2172277 L14.5,6.38405285 C14.5,5.85396271 14.01,5.52135713 13.6,5.75002346 Z M6.84,8.24456533 L2.4,5.75002346 C1.99,5.52135713 1.5,5.85396271 1.5,6.37365893 L1.5,11.2380156 C1.5,11.7473179 1.75,12.2046505 2.16,12.4333169 L6.6,14.9278587 C7.01,15.1565251 7.5,14.8135256 7.5,14.2938293 L7.5,9.43986663 C7.5,8.93056434 7.25,8.47323166 6.84,8.24456533 L6.84,8.24456533 Z M13.19,4.6170857 C13.4,4.50275253 13.5,4.30526797 13.48,4.11817733 C13.5,3.92069276 13.4,3.73360212 13.19,3.61926896 L8.67,1.16630279 C8.47,1.05196962 8.23,1 7.99,1 C7.75,1 7.51,1.05196962 7.31,1.16630279 L2.79,3.60887503 C2.58,3.7232082 2.49,3.92069276 2.5,4.1077834 C2.49,4.30526797 2.58,4.50275253 2.79,4.6170857 L7.31,7.05965794 C7.51,7.17399111 7.75,7.22596073 7.99,7.22596073 C8.23,7.22596073 8.47,7.17399111 8.67,7.05965794 L13.19,4.6170857 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
17
web/src/assets/images/package/technical_support.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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>合规</title>
|
||||
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="平台管理-收费管理" transform="translate(-314, -702)" fill="currentColor" fill-rule="nonzero">
|
||||
<g id="编组-5" transform="translate(288, 64)">
|
||||
<g id="编组-13" transform="translate(0, 228)">
|
||||
<g transform="translate(20, 16)" id="合规">
|
||||
<g transform="translate(6, 394)">
|
||||
<path d="M8,1 C10.1666667,2.37375001 12.3333333,3.47275 14.5,4.294375 C14.5,9.68437502 12.3333333,13.252625 8,15 L7.98618857,14.99475 L7.97410359,15 C3.65803454,13.252625 1.5,9.68437499 1.5,4.294375 C3.65803454,3.471875 5.81606905,2.37462499 7.97410359,1 L7.98705179,1.00874999 L8,1 Z M8,2.74999999 L8,13.25 C11.25,11.93925 12.8745684,9.262625 12.8745684,5.221 C11.25,4.604125 9.6245684,3.78074999 8,2.74999999 Z" id="形状"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-04 17:20:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-04 17:20:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 11:46:39
|
||||
*/
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
@@ -35,7 +35,7 @@ interface CodeMirrorEditorProps {
|
||||
height?: string;
|
||||
size?: 'default' | 'small';
|
||||
placeholder?: string;
|
||||
variant?: 'outlined' | 'borderless';
|
||||
variant?: 'outlined' | 'borderless' | 'filled';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +156,7 @@ const CodeMirrorEditor = ({
|
||||
<div
|
||||
ref={editorRef}
|
||||
style={{ minHeight, fontSize, lineHeight }}
|
||||
className={variant === 'borderless' ? '' : 'rb-border rb:rounded-[8px]'}
|
||||
className={variant === 'outlined' ? 'rb-border rb:rounded-lg' : variant === 'filled' ? 'cm-editor-filled rb:rounded-lg' : ''}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
.breadcrumbTitle {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.header :global(.ant-breadcrumb) {
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:07:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 12:18:58
|
||||
* @Last Modified time: 2026-04-16 10:31:21
|
||||
*/
|
||||
/**
|
||||
* AppHeader Component
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { type FC, useRef, useState } from 'react';
|
||||
import { Layout, Dropdown, Breadcrumb, Flex } from 'antd';
|
||||
import { Layout, Dropdown, Breadcrumb, Flex, Tooltip } from 'antd';
|
||||
import type { MenuProps, BreadcrumbProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -31,7 +31,7 @@ const { Header } = Layout;
|
||||
/**
|
||||
* @param source - Breadcrumb source type ('space' or 'manage'), defaults to 'manage'
|
||||
*/
|
||||
const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
const AppHeader: FC<{ source?: 'space' | 'manage'; }> = ({ source = 'manage' }) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const settingModalRef = useRef<SettingModalRef>(null)
|
||||
@@ -39,7 +39,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
|
||||
const { user, logout } = useUser();
|
||||
const { allBreadcrumbs } = useMenu();
|
||||
|
||||
|
||||
/**
|
||||
* Dynamically select breadcrumb source based on current route
|
||||
* - Knowledge base list: uses 'space' breadcrumb
|
||||
@@ -48,24 +48,24 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
*/
|
||||
const getBreadcrumbSource = () => {
|
||||
const pathname = location.pathname;
|
||||
|
||||
|
||||
// Knowledge base list page uses default space breadcrumb
|
||||
if (pathname === '/knowledge-base') {
|
||||
return 'space';
|
||||
}
|
||||
|
||||
|
||||
// Knowledge base detail pages use independent breadcrumb
|
||||
if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') {
|
||||
return 'space-detail';
|
||||
}
|
||||
|
||||
|
||||
// Other pages use the passed source
|
||||
return source;
|
||||
};
|
||||
|
||||
|
||||
const breadcrumbSource = getBreadcrumbSource();
|
||||
const breadcrumbs = allBreadcrumbs[breadcrumbSource] || [];
|
||||
|
||||
|
||||
|
||||
/** Handle user logout */
|
||||
const handleLogout = () => {
|
||||
@@ -76,9 +76,11 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
icon: <Flex align="center" justify="center" className="rb:size-10 rb:rounded-xl rb:bg-[#155EEF] rb:text-white">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(0, 2) : user.username?.[0]}
|
||||
</Flex>,
|
||||
icon: user.username
|
||||
? <Flex align="center" justify="center" className="rb:size-10 rb:rounded-xl rb:bg-[#155EEF] rb:text-white">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(-2) : user.username[0]}
|
||||
</Flex>
|
||||
: null,
|
||||
label: (<>
|
||||
<div className="rb:text-[#212332] rb:leading-5">{user.username}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#7B8085] rb:leading-4.5 rb:mt-0.5 rb:mr-2">{user.email}</div>
|
||||
@@ -127,7 +129,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Format breadcrumb items with proper titles, paths, and click handlers
|
||||
* - Translates i18n keys to display text
|
||||
@@ -135,32 +137,34 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
* - Disables navigation for the last breadcrumb item
|
||||
*/
|
||||
const formatBreadcrumbNames = () => {
|
||||
return breadcrumbs.filter(item => item.type !== 'group').map((menu, index) => {
|
||||
const filtered = breadcrumbs.filter(item => item.type !== 'group');
|
||||
return filtered.map((menu, index) => {
|
||||
const label = menu.i18nKey ? t(menu.i18nKey) : menu.label;
|
||||
const isLast = index === filtered.length - 1;
|
||||
const item: any = {
|
||||
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
|
||||
title: (
|
||||
<Tooltip title={label} placement="bottom">
|
||||
<span className={styles.breadcrumbTitle}>{label}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
|
||||
// If it's the last item, don't set path
|
||||
if (index === breadcrumbs.length - 1) {
|
||||
return item;
|
||||
|
||||
if (!isLast) {
|
||||
if ((menu as any).onClick) {
|
||||
item.onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
(menu as any).onClick(e);
|
||||
};
|
||||
item.href = '#';
|
||||
} else if (menu.path && menu.path !== '#') {
|
||||
item.path = menu.path;
|
||||
}
|
||||
}
|
||||
|
||||
// If has custom onClick, use onClick and set href to '#' to show pointer cursor
|
||||
if ((menu as any).onClick) {
|
||||
item.onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
(menu as any).onClick(e);
|
||||
};
|
||||
item.href = '#';
|
||||
} else if (menu.path && menu.path !== '#') {
|
||||
// Only set path when path is not '#'
|
||||
item.path = menu.path;
|
||||
}
|
||||
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
@@ -179,9 +183,9 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
overlayClassName={styles.userDropdown}
|
||||
>
|
||||
<Flex align="center" className="rb:cursor-pointer rb:font-medium">
|
||||
<Flex align="center" justify="center" className="rb:size-8 rb:rounded-xl rb:bg-[#155EEF] rb:text-white rb:mr-2!">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(user.username.length, -2) : user.username[0]}
|
||||
</Flex>
|
||||
{user.username && <Flex align="center" justify="center" className="rb:size-8 rb:rounded-xl rb:bg-[#155EEF] rb:text-white rb:mr-2!">
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(-2) : user.username[0]}
|
||||
</Flex>}
|
||||
<span className="rb:text-[#212332] rb:text-[12px] rb:leading-4 rb:mr-1">{user.username}</span>
|
||||
<div className={clsx("rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')]", {
|
||||
'rb:rotate-180': !open,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-07 16:49:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-25 11:21:59
|
||||
* @Last Modified time: 2026-04-17 10:11:54
|
||||
*/
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { Select, Flex, Space } from 'antd';
|
||||
import type { SelectProps } from 'antd/es/select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -22,16 +22,10 @@ interface ModelSelectProps extends SelectProps {
|
||||
fontClassName?: string;
|
||||
isAutoFetch?: boolean;
|
||||
initialData?: Model[];
|
||||
updateOptions?: (options: Model[]) => void;
|
||||
}
|
||||
|
||||
const ModelSelect: FC<ModelSelectProps> = ({
|
||||
params,
|
||||
placeholder,
|
||||
fontClassName,
|
||||
isAutoFetch = true,
|
||||
initialData = [],
|
||||
...props
|
||||
}) => {
|
||||
const ModelSelect: FC<ModelSelectProps> = ({ params, placeholder, fontClassName, isAutoFetch = true, initialData = [], updateOptions, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<Model[]>([]);
|
||||
|
||||
@@ -60,6 +54,10 @@ const ModelSelect: FC<ModelSelectProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (updateOptions) updateOptions([...options, ...initialData]);
|
||||
}, [options, initialData])
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder={placeholder ?? t('common.pleaseSelect')}
|
||||
|
||||
@@ -126,7 +126,6 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
||||
// 内容不足以填满容器时,主动继续加载
|
||||
setTimeout(() => {
|
||||
const el = scrollRef.current;
|
||||
console.log(el, el?.scrollHeight, el?.clientHeight, hasMoreRef.current)
|
||||
if (el && hasMoreRef.current && el.scrollHeight <= el.clientHeight) {
|
||||
loadMoreData();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.page-tabs:global(.ant-segmented) {
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
|
||||
line-height: 24px;
|
||||
|
||||
@@ -44,6 +44,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
className = '',
|
||||
prefix,
|
||||
inputClassName,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const [curValue, setCurValue] = useState<SliderSingleProps['value']>(0)
|
||||
@@ -83,6 +84,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
max={max}
|
||||
step={step}
|
||||
value={curValue}
|
||||
disabled={disabled}
|
||||
onChange={handleSliderChange}
|
||||
classNames={size === 'small' ? {
|
||||
rail: 'rb:w-[calc(100%-6px)]!'
|
||||
@@ -96,6 +98,7 @@ const RbSlider: FC<RbSliderProps> = ({
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
disabled={disabled}
|
||||
onChange={handleInputChange}
|
||||
prefix={prefix}
|
||||
className={`${inputClassName || '' } rb:w-20!`}
|
||||
|
||||
119
web/src/components/SiderMenu/SubscriptionDetailModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 12:28:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 17:34:02
|
||||
*/
|
||||
|
||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Flex, Tooltip, Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RbModal from '@/components/RbModal';
|
||||
import type { Subscription } from './index'
|
||||
import { billingUnits } from '@/views/Package/constant'
|
||||
import { useI18n } from '@/store/locale'
|
||||
import { UnitWrapper } from '@/views/Package'
|
||||
|
||||
export interface SubscriptionDetailModalRef {
|
||||
handleOpen: (subscription: Subscription | null) => void;
|
||||
}
|
||||
|
||||
const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { language } = useI18n()
|
||||
const [detail, setDetail] = useState<Subscription | null>(null);
|
||||
|
||||
const handleOpen = (subscription: Subscription | null) => {
|
||||
setOpen(true)
|
||||
setDetail(subscription);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
const getKeyWithLanguage = (key: string) => {
|
||||
return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan']
|
||||
}
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={[t('package.packageDetail'), detail?.package_plan?.[getKeyWithLanguage('name')]].filter(item => item).join(' - ')}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
>
|
||||
{/* Header */}
|
||||
<h3 className="rb:text-[18px] rb:font-bold rb:text-[MiSans-Bold]" style={{ color: detail?.package_plan?.theme_color }}>
|
||||
{String(detail?.package_plan?.[getKeyWithLanguage('name')] ?? '')}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="rb:text-[#5B6167] rb:mb-3">
|
||||
{String(detail?.package_plan?.[getKeyWithLanguage('core_value')] ?? '')}
|
||||
</p>
|
||||
|
||||
{/* Price */}
|
||||
<div className="rb:h-10">
|
||||
{detail?.package_plan?.billing_cycle !== 'permanent_free' && <>
|
||||
<span className="rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:mr-1">¥</span>
|
||||
<span className="rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10">{detail?.package_plan?.price}</span>
|
||||
</>}
|
||||
{detail?.package_plan?.billing_cycle && (
|
||||
<span className={clsx({
|
||||
'rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10': detail?.package_plan?.billing_cycle === 'permanent_free',
|
||||
'rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:ml-1': detail?.package_plan?.billing_cycle !== 'permanent_free'
|
||||
})}>
|
||||
{detail?.package_plan?.billing_cycle !== 'permanent_free' && ' /'}
|
||||
{t(`package.${detail?.package_plan?.billing_cycle}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="rb:my-4" />
|
||||
|
||||
{/* Features */}
|
||||
<Flex gap={12} vertical className="rb:space-y-3 rb:mb-4 rb:h-[calc(100vh-341px)]! rb:overflow-y-auto">
|
||||
{billingUnits.map(({ key, unit, icon }) => {
|
||||
const value = detail?.quota[key as keyof Subscription['quota']];
|
||||
if (value === undefined || value === null) return null;
|
||||
return (
|
||||
<UnitWrapper
|
||||
key={key}
|
||||
titleKey={key}
|
||||
value={value}
|
||||
unit={unit}
|
||||
icon={icon}
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{detail?.package_plan?.tech_support && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
icon="technical_support"
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)}
|
||||
{detail?.package_plan?.sla_compliance && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
icon="sla"
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default SubscriptionDetailModal;
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:25:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-27 19:11:43
|
||||
* @Last Modified time: 2026-04-16 17:35:38
|
||||
*/
|
||||
/**
|
||||
* SiderMenu Component
|
||||
@@ -18,7 +18,7 @@
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useState, useEffect, type FC } from 'react';
|
||||
import { useState, useEffect, useRef, type FC } from 'react';
|
||||
import { Menu as AntMenu, Layout, Flex } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -30,6 +30,9 @@ import { useMenu, type MenuItem } from '@/store/menu';
|
||||
import styles from './index.module.css'
|
||||
import logo from '@/assets/images/logo.png'
|
||||
import { useUser } from '@/store/user';
|
||||
import { getTenantSubscription } from '@/api/user';
|
||||
import { useI18n } from '@/store/locale'
|
||||
import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal'
|
||||
|
||||
// Import SVG files
|
||||
// space
|
||||
@@ -70,7 +73,51 @@ import pricingActiveIcon from '@/assets/images/menuNew/pricing_active.svg'
|
||||
import skillsIcon from '@/assets/images/menuNew/skills.svg'
|
||||
import skillsActiveIcon from '@/assets/images/menuNew/skills_active.svg'
|
||||
|
||||
export interface PackagePlan {
|
||||
id: string
|
||||
name: string
|
||||
name_en?: string
|
||||
version: string
|
||||
category: string
|
||||
tier_level: number
|
||||
price: number
|
||||
billing_cycle: string
|
||||
core_value?: string
|
||||
core_value_en?: string
|
||||
tech_support?: string
|
||||
tech_support_en?: string
|
||||
sla_compliance?: string
|
||||
sla_compliance_en?: string
|
||||
page_customization?: string
|
||||
page_customization_en?: string
|
||||
theme_color?: string
|
||||
}
|
||||
|
||||
export interface SubscriptionQuota {
|
||||
app_quota: number
|
||||
model_quota: number
|
||||
skill_quota: number
|
||||
end_user_quota: number
|
||||
workspace_quota: number
|
||||
api_ops_rate_limit: number
|
||||
memory_engine_quota: number
|
||||
ontology_project_quota: number
|
||||
knowledge_capacity_quota: number
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
subscription_id: string | null
|
||||
tenant_id: string
|
||||
package_plan_id: string
|
||||
package_version: string
|
||||
package_plan: PackagePlan
|
||||
started_at: number | null
|
||||
expired_at: number | null
|
||||
status: string
|
||||
quota: SubscriptionQuota
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
/** Icon path mapping table for menu items (normal and active states) */
|
||||
const iconPathMap: Record<string, string> = {
|
||||
'dashboard': dashboardIcon,
|
||||
@@ -121,10 +168,12 @@ const Menu: FC<{
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { language } = useI18n()
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const { allMenus, collapsed, loadMenus, toggleSider } = useMenu()
|
||||
const [menus, setMenus] = useState<MenuItem[]>([])
|
||||
const { user, storageType } = useUser()
|
||||
const subscriptionDetailRef = useRef<SubscriptionDetailModalRef>(null)
|
||||
|
||||
/** Filter menus based on user role and source */
|
||||
useEffect(() => {
|
||||
@@ -279,6 +328,25 @@ const Menu: FC<{
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null)
|
||||
useEffect(() => {
|
||||
if (source === 'manage') {
|
||||
getTenantSubscription()
|
||||
.then(res => {
|
||||
setSubscription(res as Subscription)
|
||||
})
|
||||
} else {
|
||||
setSubscription(null)
|
||||
}
|
||||
}, [source])
|
||||
|
||||
const getKeyWithLanguage = (key: string) => {
|
||||
return (language === 'en' ? `${key}_en` : key) as keyof Subscription['package_plan']
|
||||
}
|
||||
const handleViewDetail = () => {
|
||||
subscriptionDetailRef.current?.handleOpen(subscription)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider
|
||||
width={240}
|
||||
@@ -325,7 +393,8 @@ const Menu: FC<{
|
||||
inlineIndent={10}
|
||||
className={clsx("rb:overflow-y-auto", {
|
||||
'rb:max-h-[calc(100vh-136px)]': user?.is_superuser && source === 'space',
|
||||
'rb:max-h-[calc(100vh-76px)]': !(user?.is_superuser && source === 'space')
|
||||
'rb:max-h-[calc(100vh-76px)]': !(user?.is_superuser && source === 'space') && !(source === 'manage' && subscription && !collapsed),
|
||||
'rb:max-h-[calc(100vh-228px)]': source === 'manage' && subscription && !collapsed,
|
||||
})}
|
||||
/>
|
||||
{/* Return to space button for superusers */}
|
||||
@@ -337,10 +406,34 @@ const Menu: FC<{
|
||||
onClick={goToSpace}
|
||||
className="rb-border-t rb:pt-5! rb:pb-2.5! rb:absolute rb:bottom-2.5 rb:right-5 rb:left-5 rb:text-[13px] rb:text-[#5B6167] rb:hover:text-[#212332] rb:leading-4.5 rb:font-regular rb:text-center rb:mt-2.25 rb:cursor-pointer"
|
||||
>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"></div>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout_grey.svg')]"></div>
|
||||
{collapsed ? null : t('common.returnToSpace')}
|
||||
</Flex>
|
||||
}
|
||||
{source === 'manage' && subscription && !collapsed &&
|
||||
<div className="rb:absolute rb:bottom-3 rb:left-3 rb:right-3 rb:py-3 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/package_bg.png')] rb:overflow-hidden rb:rounded-xl">
|
||||
<div className="rb:h-4.5 rb:flex-1 rb:truncate rb:px-3 rb:text-[13px] rb:font-medium rb:leading-4.5">{subscription.package_plan?.[getKeyWithLanguage('name')]}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-4 rb:mt-4">
|
||||
{['workspace_quota', 'skill_quota', 'app_quota', 'model_quota'].map(key => (
|
||||
<div key={key} className="rb:text-center">
|
||||
<div className="rb:text-[13px] rb:font-[MiSans-Semibold] rb:font-semibold">{subscription.quota?.[key as keyof typeof subscription.quota]}</div>
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t(`index.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Flex align="center" justify="center" className="rb:mt-4! rb:border rb:p-2! rb:text-[12px] rb:leading-4 rb:mx-3! rb:rounded-lg rb:cursor-pointer"
|
||||
onClick={handleViewDetail}
|
||||
>
|
||||
{t('package.viewDetail')}
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/index/arrow_right_dark.svg')]"></div>
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
|
||||
<SubscriptionDetailModal
|
||||
ref={subscriptionDetailRef}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:29:46
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-26 14:52:23
|
||||
* @Last Modified time: 2026-04-14 17:55:15
|
||||
*/
|
||||
/**
|
||||
* RbTable Component
|
||||
@@ -27,7 +27,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { request } from '@/utils/request';
|
||||
import Empty from '@/components/Empty';
|
||||
|
||||
interface TablePaginationConfig { pagesize: number; page: number; }
|
||||
interface TablePaginationConfig { pagesize?: number; page?: number; }
|
||||
|
||||
/** Props interface for Table component */
|
||||
interface TableComponentProps<T = Record<string, unknown>, Q = Record<string, unknown>> extends Omit<TableProps<T>, 'pagination'> {
|
||||
@@ -102,7 +102,7 @@ const RbTable = forwardRef(<T = Record<string, unknown>, Q = Record<string, unkn
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentPagination, setCurrentPagination] = useState({
|
||||
page: 1,
|
||||
pagesize: 20,
|
||||
pagesize: typeof pagination === 'object' ? (pagination.pagesize || 20) : 20,
|
||||
});
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { type FC, type ReactNode } from 'react'
|
||||
/** Props interface for Tag component */
|
||||
export interface TagProps {
|
||||
/** Color theme for the tag */
|
||||
color?: 'processing' | 'error' | 'success' | 'warning' | 'default',
|
||||
color?: 'processing' | 'error' | 'success' | 'warning' | 'default' | 'purple' | 'dark',
|
||||
/** Tag content */
|
||||
children: ReactNode;
|
||||
/** Additional CSS classes */
|
||||
@@ -32,6 +32,8 @@ const colors = {
|
||||
success: 'rb:text-[#369F21] rb:border-[rgba(54,159,33,0.25)] rb:bg-[rgba(54,159,33,0.06)]',
|
||||
warning: 'rb:text-[#FF5D34] rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)]',
|
||||
default: 'rb:text-[#5B6167] rb:border-[rgba(91,97,103,0.30)] rb:bg-[rgba(91,97,103,0.08)]',
|
||||
purple: 'rb:text-[#9C6FFF] rb:border-[rgba(156,111,255,0.25)] rb:bg-[rgba(156,111,255,0.06)]',
|
||||
dark: 'rb:text-[#171719] rb:border-[rgba(23,23,25,0.25)] rb:bg-[rgba(23,23,25,0.06)]'
|
||||
}
|
||||
|
||||
/** Custom tag component with color themes */
|
||||
|
||||
@@ -15,6 +15,10 @@ export const en = {
|
||||
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',
|
||||
workspace_quota: 'Spaces',
|
||||
skill_quota: 'Skills',
|
||||
app_quota: 'Apps',
|
||||
model_quota: 'Models',
|
||||
},
|
||||
version:{
|
||||
releaseDate: 'Release Date',
|
||||
@@ -116,7 +120,7 @@ export const en = {
|
||||
prompt: 'Prompt Engineering',
|
||||
skills: 'Skill Library',
|
||||
workbench: 'Workbench',
|
||||
memoryRelated: 'Memory-Related',
|
||||
memoryRelated: 'Memory Hub',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
promptHistory: 'My history',
|
||||
platformManagement: 'Platform Management',
|
||||
@@ -629,6 +633,7 @@ export const en = {
|
||||
video: 'Video',
|
||||
thinking: 'Deep Thinking',
|
||||
is_thinking: 'Deep Thinking Support',
|
||||
json_output: 'Support JSON formatted output',
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: 'Home',
|
||||
@@ -1524,6 +1529,7 @@ export const en = {
|
||||
}`,
|
||||
uploadCover: 'Import and Overwrite',
|
||||
refresh: 'Refresh Current Page',
|
||||
json_output: 'Support JSON formatted output',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -1595,7 +1601,6 @@ export const en = {
|
||||
domain: 'Domain',
|
||||
expertise: 'Expertise',
|
||||
interests: 'Interests',
|
||||
knowledge_tags: 'Knowledge Tags',
|
||||
|
||||
memoryWindow: "{{name}}'s Memory Overview",
|
||||
memory_insight: 'Overall Overview',
|
||||
@@ -2287,6 +2292,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
messagesPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
vision: 'Vision',
|
||||
parameterSettings: 'Parameter Settings',
|
||||
json_output: 'Support JSON formatted output',
|
||||
},
|
||||
start: {
|
||||
variables: 'Input Fields',
|
||||
@@ -2383,6 +2389,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
else_desc: 'Used to define the logic that should be executed when the if condition is not met.',
|
||||
unset: 'Condition Not Set',
|
||||
set: 'Set',
|
||||
addSubVariable: 'Add Sub Variable',
|
||||
},
|
||||
'http-request': {
|
||||
auth: 'Authentication',
|
||||
@@ -2560,6 +2567,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'list-operator.input_list': 'Input list',
|
||||
},
|
||||
checkListHasErrors: 'Please resolve all issues in the checklist before publishing',
|
||||
variableSelect: {
|
||||
empty: 'No variables available',
|
||||
},
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
@@ -2892,8 +2902,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
context_details: 'Preference Details',
|
||||
supporting_evidence: 'Preference Source',
|
||||
specific_examples: 'Source',
|
||||
preferencesTip: 'Reminder: Click on the preferences above to view the corresponding Lenovo network',
|
||||
wordEmpty: 'There is currently no Lenovo network available',
|
||||
preferencesTip: 'Reminder: Click on the preferences above to view the corresponding association network',
|
||||
wordEmpty: 'There is currently no association network available',
|
||||
noData: 'Portrait data does not exist, please click the refresh button to initialize',
|
||||
},
|
||||
shortTermDetail: {
|
||||
@@ -3080,6 +3090,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
ontology_project_quota: 'Ontology Project',
|
||||
model_quota: 'Model Quota',
|
||||
editPackage: 'Edit Package',
|
||||
|
||||
viewDetail: 'View full package details',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,6 +15,10 @@ export const zh = {
|
||||
startedDesc: '了解该平台的核心功能,并通过图形指引和视频教程快速上手。包含从创建空间到发布应用程序的整个操作流程演示。',
|
||||
spaceTitle:'记忆熊智能空间管理平台',
|
||||
spaceSubTitle: '使智能模型的实施变得更加容易——一个集模型管理、知识构建、工作流程编排以及空间操作于一体的综合性平台',
|
||||
workspace_quota: '空间',
|
||||
skill_quota: '技能',
|
||||
app_quota: '应用',
|
||||
model_quota: '模型',
|
||||
},
|
||||
version:{
|
||||
releaseDate: '发布日',
|
||||
@@ -116,7 +120,7 @@ export const zh = {
|
||||
prompt: '提示词工程',
|
||||
skills: '技能库',
|
||||
workbench: '工作台',
|
||||
memoryRelated: '记忆相关',
|
||||
memoryRelated: '记忆中枢',
|
||||
advancedSettings: '高级设置',
|
||||
promptHistory: '我的历史',
|
||||
platformManagement: '平台管理',
|
||||
@@ -859,6 +863,7 @@ export const zh = {
|
||||
}`,
|
||||
uploadCover: '导入并覆盖',
|
||||
refresh: '刷新当前页',
|
||||
json_output: '支持JSON格式化输出',
|
||||
},
|
||||
table: {
|
||||
totalRecords: '共 {{total}} 条记录'
|
||||
@@ -1307,6 +1312,7 @@ export const zh = {
|
||||
video: '视频',
|
||||
thinking: '深度思考',
|
||||
is_thinking: '支持深度思考',
|
||||
json_output: '支持JSON格式化输出',
|
||||
},
|
||||
timezones: {
|
||||
'Asia/Shanghai': '中国标准时间 (UTC+8)',
|
||||
@@ -1556,7 +1562,6 @@ export const zh = {
|
||||
domain: '领域',
|
||||
expertise: '专业擅长',
|
||||
interests: '兴趣爱好',
|
||||
knowledge_tags: '知识标签',
|
||||
|
||||
memoryWindow: "{{name}} 的记忆之窗",
|
||||
memory_insight: '总体概述',
|
||||
@@ -2248,6 +2253,7 @@ export const zh = {
|
||||
messagesPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
vision: '视觉',
|
||||
parameterSettings: '参数设置',
|
||||
json_output: '支持JSON格式化输出',
|
||||
},
|
||||
start: {
|
||||
variables: '输入字段',
|
||||
@@ -2344,6 +2350,7 @@ export const zh = {
|
||||
else_desc: '用于定义当 if 条件不满足时应执行的逻辑。',
|
||||
unset: '条件未设置',
|
||||
set: '已设置',
|
||||
addSubVariable: '添加子变量',
|
||||
},
|
||||
'http-request': {
|
||||
auth: '鉴权',
|
||||
@@ -2524,6 +2531,9 @@ export const zh = {
|
||||
'list-operator.input_list': '输入变量',
|
||||
},
|
||||
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
|
||||
variableSelect: {
|
||||
empty: '暂无变量',
|
||||
},
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
@@ -3044,6 +3054,8 @@ export const zh = {
|
||||
ontology_project_quota: '本体工程',
|
||||
model_quota: '可负载模型数量',
|
||||
editPackage: '编辑套餐',
|
||||
|
||||
viewDetail: '查看完整套餐详情',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,22 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-02 20:28:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 14:19:14
|
||||
*/
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '@/styles/index.css'
|
||||
import App from '@/App.tsx'
|
||||
|
||||
// 同步导入i18n配置以确保在组件渲染前初始化完成
|
||||
// Synchronously import i18n config to ensure initialization before component rendering
|
||||
import './i18n'
|
||||
|
||||
// After a new release, old dynamic chunk files are deleted; force a page reload on preload error
|
||||
window.addEventListener('vite:preloadError', () => {
|
||||
console.warn('New version detected, reloading page to load latest assets...')
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
createRoot(document.getElementById('root')!)
|
||||
.render(
|
||||
<App />
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
*/
|
||||
import { create } from 'zustand'
|
||||
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
|
||||
interface WorkflowState {
|
||||
checkResults: Record<string, NodeCheckResult[]>
|
||||
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
|
||||
getCheckResults: (appId: string) => NodeCheckResult[]
|
||||
chatHistoryMap: Record<string, ChatItem[]>
|
||||
setChatHistory: (conversationId: string, history: ChatItem[]) => void
|
||||
getChatHistory: (conversationId: string) => ChatItem[]
|
||||
}
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
@@ -18,4 +22,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
setCheckResults: (appId, results) =>
|
||||
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
|
||||
getCheckResults: (appId) => get().checkResults[appId] ?? [],
|
||||
chatHistoryMap: {},
|
||||
setChatHistory: (conversationId, history) =>
|
||||
set(state => ({ chatHistoryMap: { ...state.chatHistoryMap, [conversationId]: history } })),
|
||||
getChatHistory: (conversationId) => get().chatHistoryMap[conversationId] ?? [],
|
||||
}))
|
||||
|
||||
@@ -353,6 +353,26 @@ body {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.cm-editor-filled {
|
||||
background: #F6F6F6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.cm-editor-filled .ͼ1 .cm-lineNumbers .cm-gutterElement {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
.cm-editor-filled .ͼ4 .cm-line {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
.cm-editor-filled .ͼ2 .cm-activeLineGutter,
|
||||
.cm-editor-filled .ͼ2 .cm-activeLine {
|
||||
background: transparent;
|
||||
}
|
||||
.cm-editor-filled .ͼ1 .cm-placeholder {
|
||||
color: rgba(23, 23, 25, 0.25);
|
||||
}
|
||||
.cm-editor-filled .ͼ1 .cm-lineNumbers .cm-gutterElement {
|
||||
color: #212332;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 8px;
|
||||
|
||||
5
web/src/svg.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '*.svg?react' {
|
||||
import type { FC, SVGProps } from 'react'
|
||||
const ReactComponent: FC<SVGProps<SVGSVGElement>>
|
||||
export default ReactComponent
|
||||
}
|
||||
@@ -43,7 +43,8 @@ export const maskApiKeys = (text: string): string => {
|
||||
result = result.replace(API_KEY_PATTERNS[key as keyof typeof API_KEY_PREFIX], (match) => {
|
||||
const prefixLength = API_KEY_PREFIX[key].length
|
||||
const prefix = match.substring(0, prefixLength)
|
||||
return prefix + '*'.repeat(match.length - prefixLength)
|
||||
const suffix = match.slice(-4)
|
||||
return prefix + '*'.repeat(match.length - prefixLength - 4) + suffix
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import { clearAuthData } from './auth';
|
||||
import { message } from 'antd';
|
||||
import { refreshTokenUrl, refreshToken, loginUrl, logoutUrl } from '@/api/user'
|
||||
import i18n from '@/i18n'
|
||||
import { SYS_API_PREFIX } from '@/api/package'
|
||||
|
||||
/**
|
||||
* Standard API response structure
|
||||
@@ -75,10 +74,6 @@ let requests: RequestQueueItem[] = [];
|
||||
// Request interceptor
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('config', config, config.url?.startsWith(SYS_API_PREFIX))
|
||||
if (config.url?.startsWith(SYS_API_PREFIX)) {
|
||||
config.baseURL = '';
|
||||
}
|
||||
if (!config.headers.Authorization) {
|
||||
const token = cookieUtils.get('authToken');
|
||||
if (token) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:27:52
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-13 18:19:27
|
||||
* @Last Modified time: 2026-04-17 14:53:21
|
||||
*/
|
||||
import { type FC, useRef, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -37,7 +37,8 @@ const sharingTabKeys = [
|
||||
const menuIcons: Record<string, string> = {
|
||||
edit: "rb:bg-[url('@/assets/images/common/edit_bold.svg')]",
|
||||
copy: "rb:bg-[url('@/assets/images/copy_hover.svg')]",
|
||||
export: "rb:bg-[url('@/assets/images/export_hover.svg')]",
|
||||
export: "rb:bg-[url('@/assets/images/application/export.svg')]",
|
||||
uploadCover: "rb:bg-[url('@/assets/images/application/import.svg')]",
|
||||
delete: "rb:bg-[url('@/assets/images/common/delete_red_big.svg')]"
|
||||
}
|
||||
|
||||
@@ -253,7 +254,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
: <Flex justify="flex-end">
|
||||
<Flex align="center" gap={8} className="rb:leading-5 rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout_grey.svg')]"
|
||||
></div>
|
||||
{t('common.return')}
|
||||
</Flex>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 16:58:10
|
||||
* @Last Modified time: 2026-04-13 15:13:36
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Button, Form, Input, Flex, App } from 'antd';
|
||||
@@ -36,8 +36,6 @@ const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenS
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<FeaturesConfigForm['opening_statement']>();
|
||||
|
||||
console.log('chatVariables', chatVariables)
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:28:07
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-31 16:56:57
|
||||
* @Last Modified time: 2026-04-16 18:51:01
|
||||
*/
|
||||
/**
|
||||
* Model Configuration Modal
|
||||
@@ -11,14 +11,16 @@
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, type SelectProps, Checkbox } from 'antd';
|
||||
import { Form, type SelectProps, Checkbox, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { ModelConfig, ModelConfigModalRef, Config, Source } from '../types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
import ModelSelect from '@/components/ModelSelect'
|
||||
import { resetAppModelConfig } from '@/api/application';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@@ -52,6 +54,7 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
||||
data,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ModelConfig>();
|
||||
const [source, setSource] = useState<Source>('model')
|
||||
@@ -102,14 +105,15 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
||||
}
|
||||
/** Handle model selection change */
|
||||
const handleChange: SelectProps['onChange'] = (_value, option) => {
|
||||
if (source === 'chat') {
|
||||
form.setFieldValue('label', (option as Model).name)
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
const newValues: ModelConfig = {
|
||||
capability: (option as Model).capability,
|
||||
deep_thinking: false,
|
||||
})
|
||||
json_output: false
|
||||
}
|
||||
if (source === 'chat') {
|
||||
newValues.label = (option as Model).name
|
||||
}
|
||||
form.setFieldsValue(newValues)
|
||||
}
|
||||
|
||||
/** Expose methods to parent component */
|
||||
@@ -119,20 +123,27 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const { deep_thinking: _, ...rest } = data?.model_parameters || {}
|
||||
const { deep_thinking: _, json_output: __, ...rest } = data?.model_parameters || {}
|
||||
form.setFieldsValue(rest)
|
||||
}, [values?.default_model_config_id])
|
||||
|
||||
const handleReset = () => {
|
||||
if (!id) return
|
||||
resetAppModelConfig(id).then((res) => {
|
||||
const { deep_thinking: _, json_output: __, ...rest } = (res || {}) as Config['model_parameters']
|
||||
form.setFieldsValue(rest)
|
||||
})
|
||||
}
|
||||
|
||||
console.log('handleChange values', values)
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.modelConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
cancelText={t('application.resetDefault')}
|
||||
okText={t('application.apply')}
|
||||
onOk={handleSave}
|
||||
footer={[
|
||||
<Button onClick={handleReset}>{t('application.resetDefault')}</Button>,
|
||||
<Button type="primary" onClick={handleSave}>{t('application.apply')}</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
@@ -159,6 +170,9 @@ const ModelConfigModal = forwardRef<ModelConfigModalRef, ModelConfigModalProps>(
|
||||
<FormItem name="deep_thinking" valuePropName="checked" hidden={!['model', 'chat'].includes(source) || !(values?.deep_thinking || values?.capability?.includes('thinking'))}>
|
||||
<Checkbox>{t('application.deep_thinking')}</Checkbox>
|
||||
</FormItem>
|
||||
<FormItem name="json_output" valuePropName="checked" hidden={!(values?.capability?.includes('json_output'))}>
|
||||
<Checkbox>{t('application.json_output')}</Checkbox>
|
||||
</FormItem>
|
||||
{source === 'chat' && <FormItem name="label" hidden />}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:29:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 15:46:19
|
||||
* @Last Modified time: 2026-04-16 18:20:14
|
||||
*/
|
||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||
import type { Variable } from './components/VariableList/types'
|
||||
@@ -24,20 +24,21 @@ export interface ModelConfig {
|
||||
default_model_config_id?: string;
|
||||
capability?: Capability[];
|
||||
/** Temperature for response randomness (0-2) */
|
||||
temperature: number;
|
||||
temperature?: number;
|
||||
/** Maximum tokens in response */
|
||||
max_tokens: number;
|
||||
max_tokens?: number;
|
||||
/** Top-p sampling parameter */
|
||||
top_p: number;
|
||||
top_p?: number;
|
||||
/** Frequency penalty */
|
||||
frequency_penalty: number;
|
||||
frequency_penalty?: number;
|
||||
/** Presence penalty */
|
||||
presence_penalty: number;
|
||||
presence_penalty?: number;
|
||||
/** Number of completions to generate */
|
||||
n: number;
|
||||
n?: number;
|
||||
/** Stop sequences */
|
||||
stop?: string;
|
||||
deep_thinking?: boolean;
|
||||
json_output?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,27 +2,33 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:34:12
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-26 14:39:18
|
||||
* @Last Modified time: 2026-04-16 11:19:20
|
||||
*/
|
||||
import React, { useState, useEffect, useMemo, type MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, App, Flex, Collapse } from 'antd';
|
||||
import { App, Flex, Row, Col, Space } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { MySharedOutItem } from './types';
|
||||
import { mySharedOutList, cancelShare, cancelSpaceShare } from '@/api/application'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RbDescriptions from '@/components/RbDescriptions'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
const MySharing: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { modal } = App.useApp();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<MySharedOutItem[]>([])
|
||||
|
||||
useEffect(() => { getList() }, [])
|
||||
|
||||
const getList = () => {
|
||||
setLoading(true)
|
||||
mySharedOutList()
|
||||
.then(res => setData(res as MySharedOutItem[]))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
/** Group items by target_workspace_id */
|
||||
@@ -80,89 +86,114 @@ const MySharing: React.FC = () => {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null)
|
||||
const [appList, setAppList] = useState<MySharedOutItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (grouped.length === 0) {
|
||||
setSelectedWorkspace(null)
|
||||
setAppList([])
|
||||
return
|
||||
}
|
||||
const current = grouped.find(g => g.workspace.target_workspace_id === selectedWorkspace)
|
||||
if (current) {
|
||||
setAppList(current.items)
|
||||
} else {
|
||||
setSelectedWorkspace(grouped[0].workspace.target_workspace_id)
|
||||
setAppList(grouped[0].items)
|
||||
}
|
||||
}, [grouped, selectedWorkspace])
|
||||
|
||||
const handleSelectWorkspace = async (target_workspace_id: string) => {
|
||||
if (target_workspace_id === selectedWorkspace) return
|
||||
setSelectedWorkspace(target_workspace_id);
|
||||
const filterWorkspace = grouped.find(item => item.workspace.target_workspace_id === target_workspace_id);
|
||||
|
||||
setAppList(filterWorkspace?.items || [])
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex vertical gap={12} className="rb:max-h-[calc(100%-48px)]! rb:overflow-y-auto!">
|
||||
<BodyWrapper loading={false} empty={data.length === 0}>
|
||||
{grouped.map(({ workspace, items }) => (
|
||||
<Collapse
|
||||
key={workspace.target_workspace_id}
|
||||
defaultActiveKey={[workspace.target_workspace_id]}
|
||||
items={[{
|
||||
key: workspace.target_workspace_id,
|
||||
label: (
|
||||
<BodyWrapper loading={loading} empty={data.length === 0}>
|
||||
<Row gutter={12}>
|
||||
<Col flex="384px">
|
||||
<Flex vertical gap={12}>
|
||||
{grouped.map(({ workspace, items }) => (
|
||||
<Flex
|
||||
key={workspace.target_workspace_id}
|
||||
gap={8}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
className={clsx("rb:cursor-pointer rb:bg-white rb:py-3! rb:px-4! rb:rounded-2xl rb:border rb:border-white rb:group", {
|
||||
'rb:border-[#171719]!': selectedWorkspace === workspace.target_workspace_id
|
||||
})}
|
||||
onClick={() => handleSelectWorkspace(workspace.target_workspace_id)}
|
||||
>
|
||||
<Flex align="center" gap={12}>
|
||||
{workspace.target_workspace_icon
|
||||
? <img src={workspace.target_workspace_icon} alt={workspace.target_workspace_icon} className="rb:w-8 rb:h-8 rb:rounded-lg rb:object-cover" />
|
||||
: <div className="rb:w-8 rb:h-8 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
|
||||
{workspace.target_workspace_name[0]}
|
||||
</div>
|
||||
? <img src={workspace.target_workspace_icon} alt={workspace.target_workspace_icon} className="rb:size-8.5 rb:rounded-lg rb:object-cover" />
|
||||
: <div className="rb:size-8.5 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
|
||||
{workspace.target_workspace_name[0]}
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<span className="rb:font-medium">{workspace.target_workspace_name}</span>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px]">{t('application.appCount', { count: items.length })}</div>
|
||||
<span className="rb:font-medium rb:text-[16px] rb:leading-5.5">{workspace.target_workspace_name}</span>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-0.5">{t('application.appCount', { count: items.length })}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
),
|
||||
extra: (
|
||||
<Button
|
||||
size="small"
|
||||
<div
|
||||
className="rb:hidden rb:group-hover:block rb:size-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/delete.svg')] rb:hover:bg-[url('@/assets/images/common/delete_hover.svg')]"
|
||||
onClick={e => { e.stopPropagation(); handleAllCancel(workspace); }}
|
||||
>
|
||||
{t('application.allCancel')}
|
||||
</Button>
|
||||
),
|
||||
children: (
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-3">
|
||||
{items.map(item => (
|
||||
<div key={item.id} className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-3! rb:px-4! rb:relative rb:cursor-pointer" onClick={() => handleEdit(item)}>
|
||||
<div
|
||||
className="rb:absolute rb:top-3 rb:right-3 rb:cursor-pointer rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')]"
|
||||
onClick={(e) => handleCancelOne(item, e)}
|
||||
/>
|
||||
<Flex gap={8} align="center">
|
||||
<div className="rb:size-7 rb:rounded-lg rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[14px] rb:text-white">
|
||||
{item.source_app_name[0]}
|
||||
</div>
|
||||
<div className="rb:font-medium">{item.source_app_name}</div>
|
||||
</Flex>
|
||||
<Flex vertical gap={4} className="rb:mt-3! rb:text-[12px]!">
|
||||
<Flex gap={5} justify="space-between">
|
||||
<span className="rb:text-[#5B6167]">{t('application.type')}</span>
|
||||
<span className={clsx({
|
||||
'rb:text-[#155EEF] rb:font-medium': item.source_app_type === 'agent',
|
||||
'rb:text-[#369F21] rb:font-medium': item.source_app_type === 'multi_agent',
|
||||
})}>
|
||||
{t(`application.${item.source_app_type}`)}
|
||||
</span>
|
||||
</Flex>
|
||||
<Flex gap={5} justify="space-between">
|
||||
<span className="rb:text-[#5B6167]">{t('application.version')}</span>
|
||||
<span>{item.source_app_version}</span>
|
||||
</Flex>
|
||||
<Flex gap={5} justify="space-between">
|
||||
<span className="rb:text-[#5B6167]">{t('application.permission')}</span>
|
||||
<span className={clsx({
|
||||
'rb:text-[#369F21] rb:font-medium': item.permission === 'editable',
|
||||
'rb:text-[#5B6167] rb:font-medium': item.permission === 'readonly',
|
||||
})}>
|
||||
{t(`application.${item.permission}`)}
|
||||
</span>
|
||||
</Flex>
|
||||
<Flex gap={5} justify="space-between">
|
||||
<span className="rb:text-[#5B6167]">{t('application.souceStatus')}</span>
|
||||
<span>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</span>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}]}
|
||||
/>
|
||||
))}
|
||||
</BodyWrapper>
|
||||
</Flex>
|
||||
);
|
||||
></div>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-3">
|
||||
{appList.map(item => (
|
||||
<RbCard
|
||||
key={item.source_app_id}
|
||||
title={item.source_app_name}
|
||||
avatar={<Flex align="center" justify="center" className={clsx("rb:size-12 rb:rounded-lg rb:text-[24px] rb:text-[#ffffff] rb:bg-[#155EEF]", {
|
||||
'rb:bg-[#155EEF]': item.source_app_type === 'agent',
|
||||
'rb:bg-[#9C6FFF]!': item.source_app_type === 'multi_agent',
|
||||
'rb:bg-[#171719]': item.source_app_type === 'workflow',
|
||||
})}>{item.source_app_name.trim()[0]}</Flex>}
|
||||
subTitle={<Space size={6}>
|
||||
<Tag color={item.source_app_type === 'agent' ? 'processing' : item.source_app_type === 'multi_agent' ? 'dark' : 'purple'}>{t(`application.${item.source_app_type}`)}</Tag>
|
||||
<Tag color={item.source_app_is_active ? 'success' : 'error'}>{item.source_app_is_active ? t('application.sourceActive') : t('application.sourceInactive')}</Tag>
|
||||
</Space>}
|
||||
extra={<div
|
||||
className="rb:-mt-6 rb:cursor-pointer rb:size-5.5 rb:rounded-lg rb:hover:bg-[#F6F6F6] rb:bg-[url('@/assets/images/common/close_grey.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
||||
onClick={(e) => handleCancelOne(item, e)}
|
||||
></div>}
|
||||
bodyClassName="rb:py-6! rb:px-4!"
|
||||
className="rb:cursor-pointer"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<RbDescriptions
|
||||
items={[
|
||||
{
|
||||
key: 'version',
|
||||
label: t(`application.version`),
|
||||
children: item.source_app_version
|
||||
},
|
||||
{
|
||||
key: 'permission',
|
||||
label: t(`application.permission`),
|
||||
children: <span className={clsx('rb:font-medium', {
|
||||
'rb:text-[#369F21]': item.permission === 'editable',
|
||||
})}>{t(`application.${item.permission}`)}</span>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</RbCard>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</BodyWrapper>
|
||||
)
|
||||
};
|
||||
|
||||
export default MySharing;
|
||||
|
||||
@@ -61,6 +61,7 @@ const TopCardList: FC<{data?: DashboardData}> = ({ data }) => {
|
||||
<Flex align="center" className={clsx('rb:font-medium rb:mt-7.5!', {
|
||||
'rb:text-[#FF5D34]': data?.[`${item.key}_change` as keyof DashboardData] && data?.[`${item.key}_change` as keyof DashboardData] < 0,
|
||||
'rb:text-[#369F21]': !data?.[`${item.key}_change` as keyof DashboardData] || data?.[`${item.key}_change` as keyof DashboardData] >= 0,
|
||||
'rb:text-[#FFFFFF]': item.key === 'total_memory'
|
||||
})}>
|
||||
{data?.[`${item.key}_change` as keyof DashboardData] && typeof data?.[item.key as keyof DashboardData] === 'number'
|
||||
? (100 * data?.[`${item.key}_change` as keyof DashboardData]).toFixed(2)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Space, Button, Row, Col, Flex } from 'antd';
|
||||
import { Space, Button, Flex } from 'antd';
|
||||
|
||||
import TopCardList from './components/TopCardList';
|
||||
import GuideCard from './components/GuideCard';
|
||||
@@ -120,6 +120,7 @@ const Index = () => {
|
||||
rowKey="id"
|
||||
bordered={false}
|
||||
scrollY="100%"
|
||||
pagination={{pagesize: 10}}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-31 13:56:18
|
||||
* @Last Modified time: 2026-04-16 18:03:53
|
||||
*/
|
||||
/**
|
||||
* Custom Model Modal
|
||||
@@ -14,7 +14,7 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Checkbox, Button, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CustomModelForm, ModelListItem, CustomModelModalRef, CustomModelModalProps } from '../types';
|
||||
import type { CustomModelForm, ModelListItem, CustomModelModalRef, CustomModelModalProps, Capability } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import UploadImages from '@/components/Upload/UploadImages'
|
||||
@@ -73,6 +73,7 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
is_video: capability?.includes('video') || false,
|
||||
is_audio: capability?.includes('audio') || false,
|
||||
is_thinking: capability?.includes('thinking') || false,
|
||||
json_output: capability?.includes('json_output') || false,
|
||||
});
|
||||
} else {
|
||||
setIsEdit(false);
|
||||
@@ -102,13 +103,13 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { logo, type, is_vision, is_video, is_audio, is_omni, is_thinking, ...rest } = values;
|
||||
const { logo, type, is_vision, is_video, is_audio, is_omni, is_thinking, json_output, ...rest } = values;
|
||||
const formData: CustomModelForm = {
|
||||
...rest,
|
||||
type,
|
||||
}
|
||||
if (!['embedding', 'rerank'].includes(type as string)) {
|
||||
let capability = is_omni ? ["vision", "audio", 'video'] : []
|
||||
let capability: Capability[] = is_omni ? ["vision", "audio", 'video'] : []
|
||||
|
||||
if (!is_omni) {
|
||||
if (is_vision) {
|
||||
@@ -124,6 +125,9 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
if (is_thinking) {
|
||||
capability.push('thinking')
|
||||
}
|
||||
if (json_output) {
|
||||
capability.push('json_output')
|
||||
}
|
||||
|
||||
formData.capability = capability
|
||||
formData.is_omni = is_omni
|
||||
@@ -269,6 +273,11 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
<Checkbox>{t('modelNew.is_thinking')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="json_output" valuePropName="checked" className="rb:mb-0!">
|
||||
<Checkbox>{t('modelNew.json_output')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Form>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:50:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-31 15:48:02
|
||||
* @Last Modified time: 2026-04-16 18:04:46
|
||||
*/
|
||||
/**
|
||||
* Type definitions for Model Management
|
||||
@@ -296,6 +296,7 @@ export interface CustomModelForm {
|
||||
is_audio?: boolean;
|
||||
is_omni?: boolean;
|
||||
is_thinking?: boolean;
|
||||
json_output?: boolean;
|
||||
capability?: Capability[];
|
||||
}
|
||||
|
||||
@@ -325,7 +326,7 @@ export interface BaseRef {
|
||||
modelListDetailRefresh?: () => void;
|
||||
}
|
||||
|
||||
export type Capability = 'vision' | 'audio' | 'video' | 'thinking';
|
||||
export type Capability = 'vision' | 'audio' | 'video' | 'thinking' | 'json_output';
|
||||
export interface Model {
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:24
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-06 11:25:59
|
||||
*/
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Layout, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logoutIcon from '@/assets/images/logout_hover.svg'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
/**
|
||||
* Props for PageHeader component
|
||||
*/
|
||||
interface ConfigHeaderProps {
|
||||
/** Page title/name */
|
||||
name?: string | ReactNode;
|
||||
/** Subtitle content displayed below the title */
|
||||
subTitle?: ReactNode | string;
|
||||
/** Extra content displayed on the right side */
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page header component for ontology pages
|
||||
* Displays title, subtitle, back button and extra actions
|
||||
* @param props - Component props
|
||||
*/
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
subTitle,
|
||||
extra
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Navigate back to previous page
|
||||
*/
|
||||
const goBack = () => {
|
||||
navigate(-1)
|
||||
}
|
||||
return (
|
||||
<Header className="rb:w-full rb:h-16 rb:flex rb:justify-between rb:p-[0_16px_0_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
|
||||
<div className="rb:flex rb:flex-col rb:justify-center rb:gap-1 rb:mr-4 rb:max-w-[calc(100%-300px)]">
|
||||
<div className="rb:text-[16px] rb:leading-6 rb:font-medium">
|
||||
{name}
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4">{subTitle}</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:gap-3">
|
||||
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={goBack}>
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/logout_hover.svg')]" />
|
||||
{t('common.return')}
|
||||
</Button>
|
||||
{extra}
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
@@ -122,7 +122,7 @@ const Detail: FC = () => {
|
||||
</Space>)}
|
||||
<Flex align="center" className="rb:leading-5 rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={() => navigate(-1)}>
|
||||
<div
|
||||
className="rb:mr-2 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout.svg')]"
|
||||
className="rb:mr-2 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/logout_grey.svg')]"
|
||||
></div>
|
||||
{t('common.return')}
|
||||
</Flex>
|
||||
|
||||
@@ -2,39 +2,52 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 11:43:57
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-14 14:55:20
|
||||
* @Last Modified time: 2026-04-14 11:44:40
|
||||
*/
|
||||
export const billingUnits = [
|
||||
{
|
||||
key: 'workspace_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'space',
|
||||
},
|
||||
{
|
||||
key: 'skill_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'skill',
|
||||
},
|
||||
{
|
||||
key: 'app_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'app',
|
||||
},
|
||||
{
|
||||
key: 'knowledge_capacity_quota',
|
||||
unit: 'GB', placeholder: 'numberPlaceholder',
|
||||
icon: 'knowledge',
|
||||
},
|
||||
{
|
||||
key: 'memory_engine_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'memory_config',
|
||||
},
|
||||
{
|
||||
key: 'end_user_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'end_user',
|
||||
},
|
||||
{
|
||||
key: 'ontology_project_quota',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'ontology',
|
||||
},
|
||||
{
|
||||
key: 'model_quota',
|
||||
unit: 'ops', placeholder: 'numberPlaceholder',
|
||||
icon: 'model',
|
||||
},
|
||||
]
|
||||
{
|
||||
key: 'api_ops_rate_limit',
|
||||
unit: 'ops', placeholder: 'numberPlaceholder',
|
||||
icon: 'api_ops',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-25
|
||||
* @Date: 2026-04-14 11:34:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-14 14:59:11
|
||||
* @Last Modified time: 2026-04-16 17:23:49
|
||||
*/
|
||||
/**
|
||||
* Package Component
|
||||
@@ -15,11 +15,11 @@
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useMemo, useState, useEffect, type FC } from 'react';
|
||||
import { useRef, useMemo, useState, useEffect, type FC, type ComponentType, type SVGProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Flex, Row, Col, type SegmentedProps } from 'antd';
|
||||
import { Flex, Tooltip, Divider, Button, type SegmentedProps } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import Icon from '@ant-design/icons'
|
||||
|
||||
import type { Package } from './types'
|
||||
import { getPackageList } from '@/api/package';
|
||||
@@ -28,115 +28,282 @@ import { billingUnits } from './constant'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
import { useI18n } from '@/store/locale'
|
||||
import RbButton from '@/components/RbButton'
|
||||
|
||||
import SpaceSvg from '@/assets/images/package/space.svg?react'
|
||||
import SkillSvg from '@/assets/images/package/skill.svg?react'
|
||||
import AppSvg from '@/assets/images/package/app.svg?react'
|
||||
import KnowledgeSvg from '@/assets/images/package/knowledge.svg?react'
|
||||
import MemoryConfigSvg from '@/assets/images/package/memory_config.svg?react'
|
||||
import EndUserSvg from '@/assets/images/package/end_user.svg?react'
|
||||
import OntologySvg from '@/assets/images/package/ontology.svg?react'
|
||||
import ModelSvg from '@/assets/images/package/model.svg?react'
|
||||
import TechnicalSupportSvg from '@/assets/images/package/technical_support.svg?react'
|
||||
import ApiOpsSvg from '@/assets/images/package/api_ops.svg?react'
|
||||
import arrowSvg from '@/assets/images/package/arrow.svg?react'
|
||||
import slaSvg from '@/assets/images/package/sla.svg?react';
|
||||
|
||||
const iconMap: Record<string, ComponentType<SVGProps<SVGSVGElement>>> = {
|
||||
space: SpaceSvg,
|
||||
skill: SkillSvg,
|
||||
app: AppSvg,
|
||||
knowledge: KnowledgeSvg,
|
||||
memory_config: MemoryConfigSvg,
|
||||
end_user: EndUserSvg,
|
||||
ontology: OntologySvg,
|
||||
model: ModelSvg,
|
||||
technical_support: TechnicalSupportSvg,
|
||||
api_ops: ApiOpsSvg,
|
||||
sla: slaSvg,
|
||||
}
|
||||
const btnClassNames = {
|
||||
permanent_free: 'rb:h-10! rb:rounded-[8px]!',
|
||||
default: 'rb:h-10! rb:rounded-[8px]! rb:bg-[#212332]! rb:text-white! rb:border-0! rb:hover:border-0! rb:hover:opacity-[0.8]',
|
||||
}
|
||||
|
||||
export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#171719' }: { titleKey: string; value: number | string; icon: string; unit?: string; theme_color?: string; }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderFeatureIcon = (iconKey: string, color: string) => {
|
||||
const SvgComponent = iconMap[iconKey]
|
||||
if (!SvgComponent) return null
|
||||
return <Icon component={SvgComponent} style={{ color, fontSize: 16 }} />
|
||||
}
|
||||
return (
|
||||
<Flex key={titleKey} align="start" gap={16}>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className="rb:mt-1! rb:shrink-0 rb:rounded-lg rb:size-7"
|
||||
style={{ backgroundColor: `${theme_color}14` }}
|
||||
>{renderFeatureIcon(icon, theme_color)}</Flex>
|
||||
<div className="rb:text-[13px] rb:leading-4.5">
|
||||
<div className="rb:text-[#5F6266]">{t(`package.${titleKey}`)}</div>
|
||||
<div>{value} {unit ? t(`package.${unit}`) : ''}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const Package: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { language } = useI18n()
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<Package[]>([])
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const CARD_WIDTH = 360
|
||||
const GAP = 12
|
||||
const [visibleCount, setVisibleCount] = useState(3)
|
||||
|
||||
useEffect(() => {
|
||||
const calcVisible = () => {
|
||||
if (!scrollRef.current) return
|
||||
const w = scrollRef.current.offsetWidth
|
||||
setVisibleCount(Math.floor((w + GAP) / (CARD_WIDTH + GAP)))
|
||||
}
|
||||
calcVisible()
|
||||
window.addEventListener('resize', calcVisible)
|
||||
return () => window.removeEventListener('resize', calcVisible)
|
||||
}, [])
|
||||
|
||||
const [activeTab, setActiveTab] = useState('saas_personal');
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const cats = [...new Set(data.map(p => p.category))]
|
||||
return cats
|
||||
}, [data])
|
||||
|
||||
const formatTabItems = useMemo(() => {
|
||||
return ['saas_personal', 'commercial_deployment'].map(value => ({
|
||||
value,
|
||||
label: t(`package.${value}`),
|
||||
}))
|
||||
}, [t])
|
||||
/** Handle tab change */
|
||||
return (['saas_personal', 'commercial_deployment'] as const)
|
||||
.filter(v => categories.includes(v))
|
||||
.map(value => ({ value, label: t(`package.${value}`) }))
|
||||
}, [t, categories])
|
||||
|
||||
const showTabs = categories.length > 1
|
||||
|
||||
const handleChangeTab = (value: SegmentedProps['value']) => {
|
||||
setActiveTab(value as string);
|
||||
}
|
||||
|
||||
const getList = () => {
|
||||
getPackageList({ category: activeTab as Package['category'], status: true })
|
||||
.then(res => {
|
||||
setData(res as Package[] || [])
|
||||
})
|
||||
getPackageList({ status: true }).then(res => {
|
||||
setData(res as Package[] || [])
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getList()
|
||||
}, [activeTab])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (categories.length > 0 && !categories.includes(activeTab as Package['category'])) {
|
||||
setActiveTab(categories[0])
|
||||
}
|
||||
}, [categories])
|
||||
|
||||
const getKeyWithLanguage = (key: string) => {
|
||||
return (language === 'en' ? `${key}_en` : key) as keyof Package
|
||||
}
|
||||
/** Navigate to order history */
|
||||
const goToHistory = () => {
|
||||
navigate('/orders');
|
||||
}
|
||||
|
||||
const filteredData = useMemo(() => data.filter(p => p.category === activeTab), [data, activeTab])
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const totalPages = visibleCount > 0 ? Math.ceil(filteredData.length / visibleCount) : 1
|
||||
const showArrows = totalPages > 1
|
||||
const pageData = filteredData.slice(currentPage * visibleCount, (currentPage + 1) * visibleCount)
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(0)
|
||||
}, [activeTab, visibleCount, filteredData])
|
||||
|
||||
const handleChoosePlan = () => {
|
||||
window.open(`https://docs.redbearai.com/s/${language || 'en'}-memorybear`, '_blank')
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="space-between" className="rb:mb-4!">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
<RbButton className="rb:text-[#212332] rb:font-medium!" onClick={goToHistory}>
|
||||
<div
|
||||
className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/order/order.svg')]"
|
||||
></div>
|
||||
{t('pricing.orderHistory')}
|
||||
</RbButton>
|
||||
</Flex>
|
||||
<BodyWrapper empty={data.length < 1}>
|
||||
<Row gutter={[12, 12]} className="rb:max-h-[calc(100%-48px)]! rb:overflow-y-auto">
|
||||
{data.map((pkg) => (
|
||||
<Col key={pkg.id} span={8}>
|
||||
<RbCard
|
||||
className="rb:h-full! rb:shadow-md hover:rb:shadow-lg rb:transition-shadow"
|
||||
bodyClassName="rb:p-6! rb:h-full!"
|
||||
headerClassName="rb:min-h-0!"
|
||||
>
|
||||
<Flex vertical justify="space-between" className="rb:h-full!">
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="rb:text-center rb:mb-6">
|
||||
<h3 className="rb:text-xl rb:font-bold rb:mb-2 rb:min-h-7" style={{ color: pkg.theme_color }}>
|
||||
{String(pkg[getKeyWithLanguage('name')] ?? '')}
|
||||
</h3>
|
||||
<p className="rb:text-sm rb:text-gray-500 rb:mb-4 rb:min-h-5">{String(pkg[getKeyWithLanguage('core_value')] ?? '')}</p>
|
||||
<div className="rb:text-4xl rb:font-bold rb:mb-2">
|
||||
{pkg.billing_cycle !== 'permanent_free' && <>¥{pkg.price}</>}
|
||||
{pkg.billing_cycle && <span className={clsx("", {
|
||||
'rb:text-base rb:font-normal rb:text-gray-500': pkg.billing_cycle !== 'permanent_free'
|
||||
})}>{pkg.billing_cycle !== 'permanent_free' && '/'}{t(`package.${pkg.billing_cycle}`)}</span>}
|
||||
</div>
|
||||
{showTabs && (
|
||||
<Flex justify="space-between" className="rb:mb-4!">
|
||||
<PageTabs
|
||||
value={activeTab}
|
||||
options={formatTabItems}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<BodyWrapper empty={filteredData.length < 1}>
|
||||
<div ref={scrollRef} className="rb:relative rb:mx-9">
|
||||
{showArrows && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={clsx("rb:absolute rb:-left-6 rb:top-1/2 rb:-translate-y-1/2 rb:-translate-x-3 rb:z-10 rb:h-25 rb:rounded-lg rb:w-6 rb:bg-[rgba(255,255,255,0.6)] rb:border rb:border-[rgba(255,255,255,0.6)]", {
|
||||
'rb:hover:border-[#171719] rb:cursor-pointer': currentPage > 0,
|
||||
'rb:cursor-not-allowed': currentPage === 0
|
||||
})}
|
||||
onClick={() => {
|
||||
if (currentPage === 0) return
|
||||
setCurrentPage(p => p - 1)
|
||||
}}
|
||||
>
|
||||
<Icon component={arrowSvg} style={{ color: currentPage === 0 ? '#E1E2E7' : '#171719', fontSize: 24 }} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex gap={GAP} justify="center">
|
||||
{pageData.map((pkg) => (
|
||||
<div key={pkg.id} style={{ width: CARD_WIDTH, flexShrink: 0 }}>
|
||||
<RbCard
|
||||
className="rb:h-full! rb:hover:shadow-[0px_4px_10px_0px_rgba(0,0,0,0.12)]!"
|
||||
bodyClassName="rb:p-0! rb:pb-4! rb:h-full!"
|
||||
headerClassName="rb:min-h-0!"
|
||||
>
|
||||
<div className="rb:px-5 rb:pt-4">
|
||||
<div className="rb:h-25!">
|
||||
{/* Header */}
|
||||
<Flex justify="space-between" align="start" className="rb:mb-1!">
|
||||
<Tooltip title={String(pkg[getKeyWithLanguage('name')] ?? '')}>
|
||||
<h3 className="rb:text-[18px] rb:font-bold rb:text-[MiSans-Bold] rb:w-54.5 rb:line-clamp-2" style={{ color: pkg.theme_color }}>
|
||||
{String(pkg[getKeyWithLanguage('name')] ?? '')}
|
||||
</h3>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
{/* Subtitle */}
|
||||
<Tooltip title={String(pkg[getKeyWithLanguage('core_value')] ?? '')}>
|
||||
<p className="rb:text-[#5B6167] rb:mb-4 rb:line-clamp-1">
|
||||
{String(pkg[getKeyWithLanguage('core_value')] ?? '')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="rb:h-10 rb:mb-4">
|
||||
{pkg.billing_cycle !== 'permanent_free' && <>
|
||||
<span className="rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:mr-1">¥</span>
|
||||
<span className="rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10">{pkg.price}</span>
|
||||
</>}
|
||||
{pkg.billing_cycle && (
|
||||
<span className={clsx({
|
||||
'rb:text-[28px] rb:text-[MiSans-Bold] rb:font-bold rb:leading-10': pkg.billing_cycle === 'permanent_free',
|
||||
'rb:text-[#5B6167] rb:inline-block rb:leading-5 rb:pt-3.25 rb:pb-1.75 rb:ml-1': pkg.billing_cycle !== 'permanent_free'
|
||||
})}>
|
||||
{pkg.billing_cycle !== 'permanent_free' && ' /'}
|
||||
{t(`package.${pkg.billing_cycle}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type={pkg.billing_cycle !== 'permanent_free' ? 'primary' : 'default'}
|
||||
block
|
||||
className={btnClassNames[pkg.billing_cycle === 'permanent_free' ? 'permanent_free' : 'default']}
|
||||
onClick={handleChoosePlan}
|
||||
>
|
||||
{t('pricing.contactBtn')}
|
||||
</Button>
|
||||
|
||||
<Divider className="rb:my-4" />
|
||||
|
||||
{/* Features */}
|
||||
<div className="rb:space-y-3">
|
||||
{billingUnits.map(({ key, unit }) => {
|
||||
if (typeof pkg.quotas[key as keyof Package['quotas']] === 'number') {
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||
<span className="rb:text-gray-500">{t(`package.${key}`)}</span>
|
||||
<span>{pkg.quotas[key as keyof Package['quotas']]}{t(`package.${unit}`)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Flex gap={12} vertical
|
||||
className={clsx("rb:space-y-3 rb:mb-4 rb:overflow-y-auto", {
|
||||
'rb:h-[calc(100vh-401px)]!': showTabs,
|
||||
'rb:h-[calc(100vh-346px)]!': !showTabs
|
||||
})}
|
||||
{pkg.api_ops_rate_limit &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||
<span className="rb:text-gray-500">{t(`package.api_ops_rate_limit`)}</span>
|
||||
<span>{pkg.api_ops_rate_limit}{t('package.ops')}</span>
|
||||
</div>
|
||||
}
|
||||
{pkg.tech_support &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-sm">
|
||||
<span className="rb:text-gray-500">{t(`package.tech_support`)}</span>
|
||||
<span>{String(pkg[getKeyWithLanguage('tech_support')] ?? '')}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
>
|
||||
{billingUnits.map(({ key, unit, icon }) => {
|
||||
const value = pkg?.quotas?.[key as keyof Package['quotas']];
|
||||
if (value === undefined || value === null) return null;
|
||||
return (
|
||||
<UnitWrapper
|
||||
key={key}
|
||||
titleKey={key}
|
||||
value={value}
|
||||
unit={unit}
|
||||
icon={icon}
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{pkg.tech_support && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(pkg[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
icon="technical_support"
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)}
|
||||
{pkg.sla_compliance && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(pkg[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
icon="sla"
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
</RbCard>
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
</RbCard>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
{showArrows && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
className={clsx("rb:absolute rb:-right-12 rb:top-1/2 rb:-translate-y-1/2 rb:-translate-x-3 rb:z-10 rb:h-25 rb:rounded-lg rb:w-6 rb:bg-[rgba(255,255,255,0.6)] rb:border rb:border-[rgba(255,255,255,0.6)]", {
|
||||
'rb:hover:border-[#171719] rb:cursor-pointer': currentPage < totalPages - 1,
|
||||
'rb:cursor-not-allowed': currentPage >= totalPages - 1
|
||||
})}
|
||||
onClick={() => {
|
||||
if (currentPage >= totalPages - 1) return
|
||||
setCurrentPage(p => p + 1)
|
||||
}}
|
||||
>
|
||||
<Icon component={arrowSvg} className="rb:rotate-180" style={{ color: currentPage >= totalPages - 1 ? '#E1E2E7' : '#171719', fontSize: 24 }} />
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
</BodyWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,60 +2,60 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 11:35:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-14 14:28:46
|
||||
* @Last Modified time: 2026-04-16 16:44:19
|
||||
*/
|
||||
export interface Package {
|
||||
id: string;
|
||||
// 名称
|
||||
name: string;
|
||||
name_en: string;
|
||||
// 类型
|
||||
category: "saas_personal" | "commercial_deployment";
|
||||
tier_level: number;
|
||||
// 版本
|
||||
version: string;
|
||||
// 状态
|
||||
status: boolean;
|
||||
// 价格
|
||||
price: string;
|
||||
// 计费周期
|
||||
billing_cycle: "monthly" | "yearly" | "permanent_free" | "local_deployment";
|
||||
// 核心价值
|
||||
core_value: string;
|
||||
core_value_en: string;
|
||||
// 技术支持
|
||||
tech_support: string;
|
||||
tech_support_en: string;
|
||||
// SLA与合规
|
||||
sla_compliance: string;
|
||||
sla_compliance_en: string;
|
||||
// 对话页面个性化配置
|
||||
page_customization: string;
|
||||
page_customization_en: string;
|
||||
// API OPS 频次(次/秒)
|
||||
api_ops_rate_limit: number;
|
||||
// 主题色
|
||||
theme_color: string;
|
||||
quotas: {
|
||||
// 空间数量
|
||||
workspace_quota: number;
|
||||
// 技能库数量
|
||||
skill_quota: number;
|
||||
// 应用数量
|
||||
app_quota: number;
|
||||
// 知识库容量
|
||||
knowledge_capacity_quota: string;
|
||||
// 记忆引擎数量
|
||||
memory_engine_quota: number;
|
||||
// 可记忆终端用户数
|
||||
end_user_quota: number;
|
||||
// 本体工程
|
||||
ontology_project_quota: number;
|
||||
// 可负载模型数量
|
||||
model_quota: number;
|
||||
},
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
created_by: string;
|
||||
updated_by: string | null;
|
||||
id: string;
|
||||
// 名称
|
||||
name: string | null;
|
||||
name_en: string | null;
|
||||
// 类型
|
||||
category: "saas_personal" | "commercial_deployment";
|
||||
tier_level: number;
|
||||
// 版本
|
||||
version: string;
|
||||
// 状态
|
||||
status: boolean;
|
||||
// 价格
|
||||
price: string | null;
|
||||
// 计费周期
|
||||
billing_cycle: "monthly" | "yearly" | "permanent_free" | "local_deployment";
|
||||
// 核心价值
|
||||
core_value: string | null;
|
||||
core_value_en: string | null;
|
||||
// 技术支持
|
||||
tech_support: string | null;
|
||||
tech_support_en: string | null;
|
||||
// SLA与合规
|
||||
sla_compliance: string | null;
|
||||
sla_compliance_en: string | null;
|
||||
// 对话页面个性化配置
|
||||
page_customization: string | null;
|
||||
page_customization_en: string | null;
|
||||
// 主题色
|
||||
theme_color: string;
|
||||
quotas: {
|
||||
// API OPS 频次(次/秒)
|
||||
api_ops_rate_limit: number | null;
|
||||
// 空间数量
|
||||
workspace_quota: number | null;
|
||||
// 技能库数量
|
||||
skill_quota: number | null;
|
||||
// 应用数量
|
||||
app_quota: number | null;
|
||||
// 知识库容量
|
||||
knowledge_capacity_quota: number | null;
|
||||
// 记忆引擎数量
|
||||
memory_engine_quota: number | null;
|
||||
// 可记忆终端用户数
|
||||
end_user_quota: number | null;
|
||||
// 本体工程
|
||||
ontology_project_quota: number | null;
|
||||
// 可负载模型数量
|
||||
model_quota: number | null;
|
||||
},
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
created_by: string | null;
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:33:30
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-10 18:40:52
|
||||
* @Last Modified time: 2026-04-17 17:57:15
|
||||
*/
|
||||
/**
|
||||
* End User Profile Component
|
||||
* Displays and manages end user profile information
|
||||
*/
|
||||
|
||||
import { forwardRef, useImperativeHandle, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Flex } from 'antd';
|
||||
@@ -22,17 +22,16 @@ import {
|
||||
} from '@/api/memory'
|
||||
import EndUserProfileModal from './EndUserProfileModal'
|
||||
import type { EndUser, EndUserProfileModalRef, EndUserProfileRef } from '../types'
|
||||
import Tag from '@/components/Tag';
|
||||
|
||||
/**
|
||||
* Component props
|
||||
*/
|
||||
interface EndUserProfileProps {
|
||||
onDataLoaded?: (data: { other_name?: string; id: string }) => void;
|
||||
onDataLoaded?: (data?: EndUser) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ className }, ref) => {
|
||||
const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ className, onDataLoaded }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const endUserProfileModalRef = useRef<EndUserProfileModalRef>(null)
|
||||
@@ -52,6 +51,7 @@ const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ cla
|
||||
const userData = res as EndUser
|
||||
setData(userData)
|
||||
setLoading(false)
|
||||
onDataLoaded?.(userData as EndUser)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
@@ -89,11 +89,11 @@ const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ cla
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.role')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.role || '-'}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.role?.join(' | ') || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.domain')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.domain || '-'}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.domain?.join(' | ') || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.expertise')}</div>
|
||||
@@ -103,14 +103,10 @@ const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ cla
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.interests')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.interests?.join(' | ') || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.knowledge_tags')}</div>
|
||||
<Flex wrap gap={4} className="rb:mt-0.5!">{data?.knowledge_tags?.map((tag: string) => <Tag>{tag}</Tag>) || '-'}</Flex>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#7B8085] rb:text-[12px] rb:leading-4.5">
|
||||
<div className="rb:text-[#7B8085] rb:text-[12px] rb:leading-4.5">
|
||||
{t('userMemory.updated_at')}: {data?.updated_at ? dayjs(data?.updated_at).format('YYYY/MM/DD HH:mm:ss') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
<EndUserProfileModal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 18:32:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-16 14:27:12
|
||||
* @Last Modified time: 2026-04-13 13:37:43
|
||||
*/
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -93,7 +93,7 @@ const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) =
|
||||
ref={chartRef}
|
||||
option={{
|
||||
color: Colors,
|
||||
grid: { top: 8, left: 38, right: 8, bottom: 24 },
|
||||
grid: { top: 14, left: 38, right: 8, bottom: 24 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: keys.map(k => t(`implicitDetail.${k}`)),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:57:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-10 18:38:49
|
||||
* @Last Modified time: 2026-04-17 17:57:00
|
||||
*/
|
||||
/**
|
||||
* User Memory Detail Types
|
||||
@@ -172,13 +172,14 @@ export interface EndUser {
|
||||
other_name: string;
|
||||
aliases: string | null;
|
||||
meta_data: Record<string, string>;
|
||||
id?: string;
|
||||
end_user_info_id: string;
|
||||
end_user_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
profile: {
|
||||
role: string;
|
||||
domain: string;
|
||||
role: string[];
|
||||
domain: string[];
|
||||
expertise: string[];
|
||||
interests: string[];
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-30 13:59:36
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-08 11:05:34
|
||||
* @Last Modified time: 2026-04-13 15:26:33
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Button, Row, Col, Flex } from 'antd';
|
||||
@@ -136,7 +136,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
form.validateFields().then((values) => {
|
||||
const defaultValue = Array.isArray(values.defaultValue)
|
||||
? values.defaultValue.filter((v: any) => v !== undefined && v !== null && v !== '')
|
||||
: values.type.includes('object')
|
||||
: values.type.includes('object') && values.defaultValue
|
||||
? JSON.parse(values.defaultValue)
|
||||
: values.defaultValue;
|
||||
refresh({ ...values, defaultValue }, editIndex);
|
||||
@@ -345,15 +345,16 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
<Form.Item
|
||||
name="defaultValue"
|
||||
label={t('workflow.config.parameter-extractor.default')}
|
||||
rules={[
|
||||
(type === 'object' || type === 'array[object]') ? {
|
||||
rules={(type === 'object' || type === 'array[object]')
|
||||
? [{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
try { JSON.parse(value); return Promise.resolve(); }
|
||||
catch { return Promise.reject(t('workflow.invalidJSON')); }
|
||||
}
|
||||
} : {}
|
||||
]}
|
||||
}]
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{type === 'number'
|
||||
? <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:10:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 18:07:38
|
||||
* @Last Modified time: 2026-04-15 15:57:35
|
||||
*/
|
||||
/**
|
||||
* Workflow Chat Component
|
||||
@@ -41,12 +41,15 @@ import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||
import Runtime from './Runtime';
|
||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
import { replaceVariables } from '@/views/ApplicationConfig/Agent';
|
||||
import { useWorkflowStore } from '@/store/workflow';
|
||||
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({
|
||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({ // eslint-disable-line
|
||||
appId, graphRef, features
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { message: messageApi } = App.useApp()
|
||||
const { setChatHistory } = useWorkflowStore()
|
||||
const conversationIdRef = useRef<string>('draft')
|
||||
const toolbarRef = useRef<ChatToolbarRef>(null)
|
||||
const [toolbarReady, setToolbarReady] = useState(false)
|
||||
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
|
||||
@@ -118,6 +121,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
setChatList([])
|
||||
setVariables([])
|
||||
setConversationId(null)
|
||||
conversationIdRef.current = 'draft'
|
||||
setMessage(undefined)
|
||||
toolbarRef.current?.setFiles([])
|
||||
toolbarRef.current?.setVariables([])
|
||||
@@ -189,7 +193,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
elapsed_time?: string;
|
||||
error?: any;
|
||||
state: Record<string, any>;
|
||||
status?: 'completed' | 'failed',
|
||||
status?: 'completed' | 'failed' | 'running',
|
||||
citations?: {
|
||||
document_id: string;
|
||||
file_name: string;
|
||||
@@ -231,6 +235,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
node_name: name,
|
||||
node_type: type,
|
||||
icon,
|
||||
status: 'running',
|
||||
content: {},
|
||||
}
|
||||
} else {
|
||||
@@ -240,6 +245,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
node_name: name,
|
||||
node_type: type,
|
||||
icon,
|
||||
status: 'running',
|
||||
content: {},
|
||||
})
|
||||
}
|
||||
@@ -344,6 +350,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
}
|
||||
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
conversationIdRef.current = conversation_id
|
||||
setConversationId(conversation_id)
|
||||
}
|
||||
})
|
||||
@@ -440,6 +447,10 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
||||
}
|
||||
}, [chatList.length, features?.opening_statement, variables])
|
||||
|
||||
useEffect(() => {
|
||||
setChatHistory(conversationIdRef.current, chatList)
|
||||
}, [chatList])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<Flex align="center" gap={10}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-24 17:57:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 14:05:50
|
||||
* @Last Modified time: 2026-04-14 16:33:33
|
||||
*/
|
||||
/*
|
||||
* Runtime Component
|
||||
@@ -161,8 +161,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
children: (
|
||||
<Flex gap={8} vertical>
|
||||
{/* Display error message for failed nodes */}
|
||||
|
||||
{item.error &&
|
||||
{vo.content?.error && vo.content?.error !== '' &&
|
||||
<RbAlert color="orange" className="rb:pb-0!">
|
||||
<Flex vertical className="rb:w-full!">
|
||||
<Flex align="center" justify="space-between">
|
||||
@@ -219,11 +218,11 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
</div>
|
||||
}
|
||||
|
||||
/** Copy value to clipboard and show success message */
|
||||
const handleCopy = (value: string) => {
|
||||
copy(value)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
/** Copy value to clipboard and show success message */
|
||||
const handleCopy = (value: string) => {
|
||||
copy(value)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -269,7 +268,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
</div>
|
||||
)
|
||||
: <div className="rb:mb-4">
|
||||
{item.error &&
|
||||
{item.error && item.error !== '' &&
|
||||
<RbAlert color="orange" className="rb:pb-0! rb:mb-2!"><Markdown content={item.error} /></RbAlert>
|
||||
}
|
||||
{renderChild(item.subContent)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Node } from '@antv/x6';
|
||||
|
||||
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
||||
import { nodeLibrary } from '../../constant'
|
||||
import { isSubExprSet } from '../../utils'
|
||||
import { getToolMethods } from '@/api/tools'
|
||||
import RbDrawer from '@/components/RbDrawer'
|
||||
import { useWorkflowStore } from '@/store/workflow'
|
||||
@@ -44,14 +45,13 @@ const specialValidators: Record<string, (val: any) => boolean> = {
|
||||
// if-else.cases: every case must have at least one expression, and every expression must be fully set
|
||||
'if-else.cases': (val: any[]) => {
|
||||
if (!Array.isArray(val) || !val.length) return true
|
||||
return val.some(c => {
|
||||
if (!c?.expressions?.length) return true
|
||||
return c.expressions.some((expr: any) => {
|
||||
if (!expr?.left) return true
|
||||
if (['not_empty', 'empty'].includes(expr.operator)) return false
|
||||
return !(!!expr.left && (!!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number'))
|
||||
})
|
||||
})
|
||||
const isExprSet = (expr: any) => {
|
||||
if (expr?.sub_variable_condition?.conditions?.length > 0) return expr.sub_variable_condition?.conditions.every(isSubExprSet)
|
||||
if (!expr.left) return false
|
||||
if (['not_empty', 'empty'].includes(expr.operator)) return true
|
||||
return !!expr.left && (!!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number')
|
||||
}
|
||||
return val.some(c => !c?.expressions?.length || c.expressions.some((expr: any) => !isExprSet(expr)))
|
||||
},
|
||||
// question-classifier.categories: every category must have a value
|
||||
'question-classifier.categories': (val: any[]) => !Array.isArray(val) || !val.some(c => c?.class_name && String(c.class_name).trim()),
|
||||
@@ -79,7 +79,6 @@ const specialValidators: Record<string, (val: any) => boolean> = {
|
||||
}
|
||||
|
||||
function isEmpty(val: any): boolean {
|
||||
console.log('validateNode isEmpty', val, val === undefined || val === null || val === '')
|
||||
if (val === undefined || val === null || val === '') return true
|
||||
if (Array.isArray(val)) return val.length === 0
|
||||
return false
|
||||
@@ -98,7 +97,6 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
|
||||
const specialKey = `${type}.${field}`
|
||||
const specialValidator = specialValidators[specialKey]
|
||||
const isInvalid = specialValidator ? specialValidator(val) : isEmpty(val)
|
||||
console.log('validateNode', val, specialKey, specialValidator, isEmpty(val))
|
||||
if (isInvalid) errors.push({ key: specialKey, message: '' })
|
||||
})
|
||||
|
||||
@@ -114,68 +112,13 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
export async function runCheckOnGraph(
|
||||
graph: import('@antv/x6').Graph,
|
||||
t: (key: string) => string
|
||||
): Promise<NodeCheckResult[]> {
|
||||
const nodes = graph.getNodes()
|
||||
const edges = graph.getEdges()
|
||||
const targetIds = new Set<string>()
|
||||
const childTargetIds = new Set<string>()
|
||||
edges.forEach(e => {
|
||||
targetIds.add(e.getTargetCellId())
|
||||
const srcData = graph.getCellById(e.getSourceCellId())?.getData()
|
||||
const tgtData = graph.getCellById(e.getTargetCellId())?.getData()
|
||||
if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) {
|
||||
childTargetIds.add(e.getTargetCellId())
|
||||
}
|
||||
})
|
||||
|
||||
const checked: NodeCheckResult[] = []
|
||||
for (const node of nodes) {
|
||||
const data = node.getData()
|
||||
if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue
|
||||
|
||||
const errors: CheckError[] = []
|
||||
const isChildNode = !!data.cycle
|
||||
const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true
|
||||
if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') })
|
||||
|
||||
const configErrors = validateNode(data.type, data.config ?? {})
|
||||
configErrors.forEach(e => {
|
||||
errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() })
|
||||
})
|
||||
|
||||
if (data.type === 'tool') {
|
||||
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
|
||||
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
|
||||
if (toolId) {
|
||||
try {
|
||||
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
|
||||
const operation = toolParameters?.operation
|
||||
const method = operation ? methods.find(m => m.name === operation) : methods[0]
|
||||
if (method) {
|
||||
method.parameters
|
||||
.filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === ''))
|
||||
.forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` }))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors })
|
||||
}
|
||||
}
|
||||
return checked
|
||||
}
|
||||
|
||||
const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { setCheckResults, getCheckResults } = useWorkflowStore()
|
||||
const results = getCheckResults(appId)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const toolMethodsCacheRef = useRef<Record<string, Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>>>({})
|
||||
|
||||
const runCheck = useCallback(async () => {
|
||||
const graph = workflowRef.current?.graphRef?.current
|
||||
@@ -222,9 +165,13 @@ const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
|
||||
if (data.type === 'tool') {
|
||||
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
|
||||
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
|
||||
if (toolId) {
|
||||
|
||||
if (typeof toolId === 'string') {
|
||||
try {
|
||||
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
|
||||
if (!toolMethodsCacheRef.current[toolId]) {
|
||||
toolMethodsCacheRef.current[toolId] = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
|
||||
}
|
||||
const methods = toolMethodsCacheRef.current[toolId]
|
||||
const operation = toolParameters?.operation
|
||||
const method = operation ? methods.find(m => m.name === operation) : methods[0]
|
||||
if (method) {
|
||||
@@ -251,21 +198,27 @@ const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
|
||||
return checked
|
||||
}, [workflowRef.current?.graphRef?.current, t])
|
||||
|
||||
const scheduleCheckRef = useRef<() => void>()
|
||||
|
||||
const scheduleCheck = useCallback(() => {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(async () => {
|
||||
setCheckResults(appId, await runCheck())
|
||||
}, 500)
|
||||
}, 300)
|
||||
}, [runCheck])
|
||||
|
||||
scheduleCheckRef.current = scheduleCheck
|
||||
|
||||
useEffect(() => {
|
||||
const graph = workflowRef.current?.graphRef?.current
|
||||
console.log('graph')
|
||||
if (!graph) return
|
||||
const events = ['node:added', 'node:removed', 'node:change:data', 'edge:added', 'edge:removed']
|
||||
events.forEach(e => graph.on(e, scheduleCheck))
|
||||
scheduleCheck()
|
||||
const handler = () => scheduleCheckRef.current?.()
|
||||
const events = ['node:added', 'node:removed', 'node:change:data', 'edge:added', 'edge:removed', 'edge:connected', 'edge:changed']
|
||||
events.forEach(e => graph.on(e, handler))
|
||||
scheduleCheckRef.current?.()
|
||||
return () => {
|
||||
events.forEach(e => graph.off(e, scheduleCheck))
|
||||
events.forEach(e => graph.off(e, handler))
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [workflowRef.current?.graphRef?.current])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-02 15:15:36
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 14:48:00
|
||||
* @Last Modified time: 2026-04-16 11:34:41
|
||||
*/
|
||||
import { type FC, useEffect, useMemo } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
@@ -81,7 +81,7 @@ export interface Jinja2EditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
options?: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
variant?: 'outlined' | 'borderless' | 'filled';
|
||||
height?: number;
|
||||
size?: 'default' | 'small';
|
||||
className?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-23 16:22:51
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 16:29:36
|
||||
* @Last Modified time: 2026-04-16 12:04:37
|
||||
*/
|
||||
import { type FC, useState, useMemo } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
@@ -25,7 +25,7 @@ export interface LexicalEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
options?: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
variant?: 'outlined' | 'borderless' | 'filled';
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
@@ -60,6 +60,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
}) => {
|
||||
console.log('Editor value', value)
|
||||
const [_count, setCount] = useState(0);
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
if (language === 'jinja2') {
|
||||
return (
|
||||
@@ -90,7 +91,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
// Calculate minimum height based on type and size
|
||||
const minheight = useMemo(() => {
|
||||
if (type === 'input') {
|
||||
return `${height ? height : size === 'small' && variant === 'borderless' ? 18 : size === 'small' ? 26 : 30}px`
|
||||
return `${height ? height : size === 'small' && ['borderless', 'filled'].includes(variant) ? 18 : size === 'small' ? 26 : 30}px`
|
||||
}
|
||||
return `${height ? height : size === 'small' ? 60 : 120}px`
|
||||
}, [type, size, height, variant])
|
||||
@@ -103,7 +104,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
// Calculate line height based on size prop
|
||||
const lineHeight = useMemo(() => {
|
||||
return `${height ? height - 10 : size === 'small' && variant === 'borderless' ? 18 : size === 'small' ? 16 : 20}px`
|
||||
}, [size])
|
||||
}, [size, height, variant])
|
||||
|
||||
// Calculate placeholder minimum height
|
||||
const placeHolderMinheight = useMemo(() => {
|
||||
@@ -112,20 +113,24 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<div style={{ position: 'relative' }} className={className}>
|
||||
<div style={{ position: 'relative', borderRadius: '8px', background: variant === 'filled' ? '#F6F6F6': 'transparent' }} className={className}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: minheight,
|
||||
padding: height ? '4px 6px' : variant === 'borderless' ? '0' : '6px 8px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #EBEBEB',
|
||||
padding: height ? '4px 6px' : variant === 'outlined' ? '6px 8px': '0',
|
||||
border: type === 'input' && focused
|
||||
? '1px solid #171719'
|
||||
: variant === 'outlined' ? '1px solid #EBEBEB' : 'none',
|
||||
borderRadius: '8px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: fontSize,
|
||||
lineHeight: lineHeight,
|
||||
}}
|
||||
onFocus={() => type === 'input' && setFocused(true)}
|
||||
onBlur={() => type === 'input' && setFocused(false)}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
@@ -133,12 +138,13 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
style={{
|
||||
minHeight: placeHolderMinheight,
|
||||
position: 'absolute',
|
||||
top: variant === 'borderless' ? '2px' : '6px',
|
||||
left: variant === 'borderless' ? '0' : '11px',
|
||||
color: '#A8A9AA',
|
||||
top: variant === 'outlined' ? '6px' : type === 'input' ? '6px' : '2px',
|
||||
left: variant === 'outlined' ? '11px' : type === 'input' ? '8px' : '0',
|
||||
color: 'rgba(23,23,25,0.25)',
|
||||
fontSize: fontSize,
|
||||
lineHeight: placeHolderMinheight,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
|
||||
@@ -48,17 +48,13 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
||||
return (
|
||||
<span
|
||||
onClick={handleClick}
|
||||
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[10px] rb:inline-flex rb:items-center rb:py-0 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
|
||||
'rb:border-[#171719]': isSelected,
|
||||
'rb:border-[#DFE4ED]': !isSelected
|
||||
})}
|
||||
className="rb-border rb:rounded-md rb:bg-white rb:text-[10px] rb:text-[#212332] rb:h-5! rb:inline-flex rb:items-center rb:p-1 rb:mx-px rb:cursor-pointer"
|
||||
contentEditable={false}
|
||||
>
|
||||
{data.isContext ? (
|
||||
<span style={{ fontSize: '12px', marginRight: '4px' }}>📄</span>
|
||||
) : data.group !== 'CONVERSATION' && !data.value.includes('conv') ? (
|
||||
<span className={`rb:size-4 rb:mr-1 rb:bg-cover rb:inline-block rb:flex-shrink-0 ${data.nodeData?.icon}`} />
|
||||
) : <span className="rb:inline-block rb:h-4"></span>}
|
||||
{!data.isContext && data.group !== 'CONVERSATION' && !data.value.includes('conv')
|
||||
? <div className={`rb:size-3 rb:mr-1 rb:bg-cover ${data.nodeData?.icon}`} />
|
||||
: null
|
||||
}
|
||||
{!data.isContext && data.group !== 'CONVERSATION' && (
|
||||
<>
|
||||
{!data.value.includes('conv') && <>
|
||||
@@ -73,7 +69,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<span className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1 rb:text-[#171719]">{data.label}</span>
|
||||
<span className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{data.label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-23 16:22:51
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 16:51:04
|
||||
* @Last Modified time: 2026-04-13 14:00:07
|
||||
*/
|
||||
import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical';
|
||||
import { Space, Flex } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { INSERT_VARIABLE_COMMAND, CLOSE_AUTOCOMPLETE_COMMAND } from '../commands';
|
||||
import type { NodeProperties } from '../../../types'
|
||||
@@ -34,61 +36,62 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, anchorBottom: 0 });
|
||||
const [expandedParent, setExpandedParent] = useState<Suggestion | null>(null);
|
||||
const [childPanelTop, setChildPanelTop] = useState(0);
|
||||
const [childPanelPos, setChildPanelPos] = useState({ top: 0, right: 0 });
|
||||
const [activePanel, setActivePanel] = useState<'main' | 'child'>('main');
|
||||
const [childActiveIndex, setChildActiveIndex] = useState(-1);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const childItemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
|
||||
// Adjust popup position after render based on actual height
|
||||
// Adjust popup position after render based on actual size
|
||||
useLayoutEffect(() => {
|
||||
if (!popupRef.current || !showSuggestions) return;
|
||||
const { top, anchorBottom } = popupPosition;
|
||||
const { top, left, anchorBottom } = popupPosition;
|
||||
const popupHeight = popupRef.current.offsetHeight;
|
||||
const popupWidth = popupRef.current.offsetWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const MARGIN = 10;
|
||||
|
||||
let finalTop: number;
|
||||
if (top - popupHeight - MARGIN >= 0) {
|
||||
// Enough space above: show above cursor
|
||||
finalTop = top - popupHeight - MARGIN;
|
||||
} else {
|
||||
// Not enough space above: show below cursor
|
||||
finalTop = anchorBottom + MARGIN;
|
||||
if (finalTop + popupHeight > viewportHeight - MARGIN) {
|
||||
finalTop = viewportHeight - popupHeight - MARGIN;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalTop !== top) {
|
||||
setPopupPosition(prev => ({ ...prev, top: finalTop }));
|
||||
let finalLeft = left;
|
||||
if (finalLeft + popupWidth > viewportWidth - MARGIN) {
|
||||
finalLeft = viewportWidth - popupWidth - MARGIN;
|
||||
}
|
||||
if (finalLeft < MARGIN) finalLeft = MARGIN;
|
||||
|
||||
if (finalTop !== top || finalLeft !== left) {
|
||||
setPopupPosition(prev => ({ ...prev, top: finalTop, left: finalLeft }));
|
||||
}
|
||||
}, [showSuggestions, popupPosition.anchorBottom]);
|
||||
|
||||
const CHILD_PANEL_HEIGHT = 280; // max-h-60 (240) + header (~40)
|
||||
const CHILD_PANEL_HEIGHT = 280;
|
||||
|
||||
const calcChildPanelTop = (elRect: DOMRect, popupRect: DOMRect) => {
|
||||
const relativeTop = elRect.top - popupRect.top;
|
||||
const absoluteBottom = popupRect.top + relativeTop + CHILD_PANEL_HEIGHT;
|
||||
const overflow = absoluteBottom - (window.innerHeight - 10);
|
||||
return overflow > 0 ? relativeTop - overflow : relativeTop;
|
||||
const calcChildPanelPos = (key: string) => {
|
||||
const el = itemRefs.current.get(key);
|
||||
if (!el || !popupRef.current) return;
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const popupRect = popupRef.current.getBoundingClientRect();
|
||||
const actualChildHeight = Math.min(CHILD_PANEL_HEIGHT, popupRect.height);
|
||||
const top = Math.max(10, popupRect.bottom - actualChildHeight);
|
||||
setChildPanelPos({ top, right: window.innerWidth - elRect.left + 8 });
|
||||
};
|
||||
|
||||
const scrollSelectedIntoView = () => {
|
||||
if (!popupRef.current) return;
|
||||
|
||||
const selectedElement = popupRef.current.querySelector('[data-selected="true"]');
|
||||
if (!selectedElement) return;
|
||||
|
||||
const container = popupRef.current;
|
||||
const element = selectedElement as HTMLElement;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
if (elementRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += elementRect.bottom - containerRect.bottom;
|
||||
} else if (elementRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - elementRect.top;
|
||||
}
|
||||
const resetState = () => {
|
||||
setShowSuggestions(false);
|
||||
setExpandedParent(null);
|
||||
setChildPanelPos({ top: 0, right: 0 });
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
};
|
||||
|
||||
// Listen to editor updates and show suggestions when '/' is typed
|
||||
@@ -104,11 +107,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const anchorOffset = selection.anchor.offset;
|
||||
|
||||
// Get the text content of the current node
|
||||
const nodeText = anchorNode.getTextContent();
|
||||
|
||||
// Check if we have a '/' at the current position or after line break
|
||||
const textBeforeCursor = nodeText.substring(0, anchorOffset);
|
||||
const shouldShow = textBeforeCursor.endsWith('/') ||
|
||||
(textBeforeCursor === '/' && anchorOffset === 1);
|
||||
@@ -117,10 +116,11 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
if (!shouldShow) {
|
||||
setSelectedIndex(0);
|
||||
setExpandedParent(null);
|
||||
setChildPanelTop(0);
|
||||
setChildPanelPos({ top: 0, right: 0 });
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
}
|
||||
|
||||
// Calculate popup position to keep it within viewport bounds
|
||||
if (shouldShow) {
|
||||
const domSelection = window.getSelection();
|
||||
if (domSelection && domSelection.rangeCount > 0) {
|
||||
@@ -148,9 +148,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
return editor.registerCommand(
|
||||
CLOSE_AUTOCOMPLETE_COMMAND,
|
||||
() => {
|
||||
setShowSuggestions(false);
|
||||
setExpandedParent(null);
|
||||
setChildPanelTop(0);
|
||||
resetState();
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH
|
||||
@@ -160,9 +158,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
// Insert selected suggestion into editor
|
||||
const insertMention = (suggestion: Suggestion) => {
|
||||
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
||||
setShowSuggestions(false);
|
||||
setExpandedParent(null);
|
||||
setChildPanelTop(0);
|
||||
resetState();
|
||||
};
|
||||
|
||||
// Group suggestions by node ID
|
||||
@@ -176,13 +172,28 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
// Flat list for keyboard navigation
|
||||
const flatOptions = Object.values(groupedSuggestions).flat().flatMap(option => {
|
||||
if (option.key === expandedParent?.key && option.children?.length) {
|
||||
return [option, ...option.children];
|
||||
// Flat list of main-panel items for keyboard navigation
|
||||
const flatOptions = Object.values(groupedSuggestions).flat();
|
||||
|
||||
// Sync child panel position when keyboard navigates to a parent with children
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0 || selectedIndex >= flatOptions.length) return;
|
||||
const s = flatOptions[selectedIndex];
|
||||
if (s.children?.length) {
|
||||
calcChildPanelPos(s.key);
|
||||
setExpandedParent(s);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
}
|
||||
return [option];
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedIndex]);
|
||||
|
||||
// Scroll child active item into view
|
||||
useEffect(() => {
|
||||
if (!expandedParent?.children?.length || childActiveIndex < 0) return;
|
||||
const child = expandedParent.children[childActiveIndex];
|
||||
if (child) childItemRefs.current.get(child.key)?.scrollIntoView({ block: 'nearest' });
|
||||
}, [childActiveIndex, expandedParent]);
|
||||
|
||||
// Handle Enter key to select suggestion
|
||||
useEffect(() => {
|
||||
@@ -191,7 +202,15 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
return editor.registerCommand(
|
||||
KEY_ENTER_COMMAND,
|
||||
(event) => {
|
||||
if (showSuggestions && flatOptions.length > 0) {
|
||||
if (!showSuggestions) return false;
|
||||
if (activePanel === 'child' && expandedParent?.children?.length) {
|
||||
const child = expandedParent.children[childActiveIndex];
|
||||
if (child && !child.disabled) {
|
||||
event?.preventDefault();
|
||||
insertMention(child);
|
||||
return true;
|
||||
}
|
||||
} else if (flatOptions.length > 0) {
|
||||
const selectedOption = flatOptions[selectedIndex];
|
||||
if (selectedOption && !selectedOption.disabled) {
|
||||
event?.preventDefault();
|
||||
@@ -203,57 +222,56 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH
|
||||
);
|
||||
}, [showSuggestions, selectedIndex, flatOptions, insertMention, editor]);
|
||||
}, [showSuggestions, selectedIndex, flatOptions, insertMention, editor, activePanel, childActiveIndex, expandedParent]);
|
||||
|
||||
// Handle keyboard navigation (Arrow Up/Down, Escape)
|
||||
// Handle keyboard navigation (Arrow Up/Down/Left/Right, Escape)
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
|
||||
// Navigate down through suggestions, skip disabled items
|
||||
const unregisterArrowDown = editor.registerCommand(
|
||||
KEY_ARROW_DOWN_COMMAND,
|
||||
(event) => {
|
||||
if (showSuggestions && flatOptions.length > 0) {
|
||||
event?.preventDefault();
|
||||
if (!showSuggestions) return false;
|
||||
event?.preventDefault();
|
||||
if (activePanel === 'child' && expandedParent?.children) {
|
||||
setChildActiveIndex(i => Math.min(i + 1, expandedParent.children!.length - 1));
|
||||
} else {
|
||||
setSelectedIndex(prev => {
|
||||
let nextIndex = prev + 1;
|
||||
while (nextIndex < flatOptions.length && flatOptions[nextIndex].disabled) {
|
||||
nextIndex++;
|
||||
}
|
||||
const newIndex = nextIndex >= flatOptions.length ? prev : nextIndex;
|
||||
setTimeout(() => scrollSelectedIntoView(), 0);
|
||||
let next = prev + 1;
|
||||
// skip items that are disabled AND have no children
|
||||
while (next < flatOptions.length && flatOptions[next].disabled && !flatOptions[next].children?.length) next++;
|
||||
const newIndex = next >= flatOptions.length ? prev : next;
|
||||
setTimeout(() => itemRefs.current.get(flatOptions[newIndex]?.key)?.scrollIntoView({ block: 'nearest' }), 0);
|
||||
return newIndex;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH
|
||||
);
|
||||
|
||||
// Navigate up through suggestions, skip disabled items
|
||||
const unregisterArrowUp = editor.registerCommand(
|
||||
KEY_ARROW_UP_COMMAND,
|
||||
(event) => {
|
||||
if (showSuggestions && flatOptions.length > 0) {
|
||||
event?.preventDefault();
|
||||
if (!showSuggestions) return false;
|
||||
event?.preventDefault();
|
||||
if (activePanel === 'child' && expandedParent?.children) {
|
||||
setChildActiveIndex(i => Math.max(i - 1, 0));
|
||||
} else {
|
||||
setSelectedIndex(prev => {
|
||||
let prevIndex = prev - 1;
|
||||
while (prevIndex >= 0 && flatOptions[prevIndex].disabled) {
|
||||
prevIndex--;
|
||||
}
|
||||
const newIndex = prevIndex < 0 ? prev : prevIndex;
|
||||
setTimeout(() => scrollSelectedIntoView(), 0);
|
||||
let prevIdx = prev - 1;
|
||||
// skip items that are disabled AND have no children
|
||||
while (prevIdx >= 0 && flatOptions[prevIdx].disabled && !flatOptions[prevIdx].children?.length) prevIdx--;
|
||||
const newIndex = prevIdx < 0 ? prev : prevIdx;
|
||||
setTimeout(() => itemRefs.current.get(flatOptions[newIndex]?.key)?.scrollIntoView({ block: 'nearest' }), 0);
|
||||
return newIndex;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH
|
||||
);
|
||||
|
||||
// Close suggestions on Escape key
|
||||
const unregisterEscape = editor.registerCommand(
|
||||
KEY_ESCAPE_COMMAND,
|
||||
(event) => {
|
||||
@@ -272,130 +290,156 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
unregisterArrowUp();
|
||||
unregisterEscape();
|
||||
};
|
||||
}, [showSuggestions, selectedIndex, flatOptions, editor]);
|
||||
}, [showSuggestions, selectedIndex, flatOptions, editor, activePanel, childActiveIndex, expandedParent]);
|
||||
|
||||
// Handle ArrowLeft/Right for panel switching via native keydown (lexical doesn't expose these commands)
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
const current = flatOptions[selectedIndex];
|
||||
if (activePanel === 'main' && current?.children?.length) {
|
||||
e.preventDefault();
|
||||
setActivePanel('child');
|
||||
setChildActiveIndex(0);
|
||||
}
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
if (activePanel === 'child') {
|
||||
e.preventDefault();
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler, true);
|
||||
return () => document.removeEventListener('keydown', handler, true);
|
||||
}, [showSuggestions, activePanel, selectedIndex, flatOptions]);
|
||||
|
||||
if (!showSuggestions) return null;
|
||||
if (Object.entries(groupedSuggestions).length === 0) return null;
|
||||
|
||||
if (Object.entries(groupedSuggestions).length === 0) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={popupRef}
|
||||
data-autocomplete-popup="true"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:fixed rb:z-1000 rb:bg-white rb:rounded-xl rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
|
||||
style={{
|
||||
top: popupPosition.top,
|
||||
left: popupPosition.left,
|
||||
}}
|
||||
>
|
||||
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
|
||||
<Flex vertical gap={12}>
|
||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
|
||||
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
||||
const nodeIcon = nodeOptions[0]?.nodeData?.icon;
|
||||
return (
|
||||
<div key={nodeId}>
|
||||
{nodeName !== 'undefined' && <Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
||||
{nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />}
|
||||
{nodeName}
|
||||
</Flex>}
|
||||
{nodeOptions.map((option) => {
|
||||
const globalIndex = flatOptions.indexOf(option);
|
||||
const isExpanded = expandedParent?.key === option.key;
|
||||
const hasChildren = !!option.children?.length;
|
||||
return (
|
||||
<Flex
|
||||
key={option.key}
|
||||
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
||||
data-selected={selectedIndex === globalIndex}
|
||||
className="rb:pl-6! rb:pr-3! rb:py-2!"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
|
||||
opacity: option.disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (option.disabled) return;
|
||||
insertMention(option);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setSelectedIndex(globalIndex);
|
||||
if (hasChildren) {
|
||||
const el = itemRefs.current.get(option.key);
|
||||
if (el && popupRef.current) {
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const popupRect = popupRef.current.getBoundingClientRect();
|
||||
setChildPanelTop(calcChildPanelTop(elRect, popupRect));
|
||||
<>
|
||||
<div
|
||||
ref={popupRef}
|
||||
data-autocomplete-popup="true"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{
|
||||
top: popupPosition.top,
|
||||
left: popupPosition.left,
|
||||
}}
|
||||
>
|
||||
<div className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto">
|
||||
<Flex vertical gap={12}>
|
||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
|
||||
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
||||
return (
|
||||
<div key={nodeId} className="rb:text-[12px]">
|
||||
{nodeName !== 'undefined' &&
|
||||
<div className="rb:px-2 rb:leading-4.25 rb:mb-1.25 rb:font-medium rb:text-[#5B6167]">
|
||||
{nodeName}
|
||||
</div>
|
||||
}
|
||||
<Flex vertical gap={2}>
|
||||
{nodeOptions.map((option) => {
|
||||
const globalIndex = flatOptions.indexOf(option);
|
||||
const isExpanded = expandedParent?.key === option.key;
|
||||
const hasChildren = !!option.children?.length;
|
||||
const isActive = activePanel === 'main' && selectedIndex === globalIndex;
|
||||
return (
|
||||
<Flex
|
||||
key={option.key}
|
||||
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
||||
className={clsx("rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:bg-[#F6F6F6]': isActive || isExpanded,
|
||||
'rb:cursor-not-allowed rb:opacity-65': option.disabled,
|
||||
'rb:cursor-pointer': !option.disabled,
|
||||
})}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
onClick={() => {
|
||||
if (option.disabled && !hasChildren) return;
|
||||
if (!option.disabled) insertMention(option);
|
||||
if (hasChildren) {
|
||||
calcChildPanelPos(option.key);
|
||||
setExpandedParent(option);
|
||||
}
|
||||
setExpandedParent(option);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setSelectedIndex(globalIndex);
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
if (hasChildren) {
|
||||
calcChildPanelPos(option.key);
|
||||
setExpandedParent(option);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label &&
|
||||
<div className="rb:font-medium">
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span> {option.label}
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label && <Space size={4}>
|
||||
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : `{x}`}</span>
|
||||
<span>{option.label}</span>
|
||||
</Space>}
|
||||
<Space size={4}>
|
||||
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
|
||||
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1">›</span>}
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<Space size={2}>
|
||||
{option.dataType && <span>{option.dataType}</span>}
|
||||
{hasChildren && <div className="rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:rotate-90"></div>}
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
{/* Child variables panel - floats to the left */}
|
||||
{expandedParent?.children?.length && (
|
||||
|
||||
{/* Child variables panel - fixed positioned via portal to avoid clipping */}
|
||||
{expandedParent?.children?.length && createPortal(
|
||||
<div
|
||||
className="rb:absolute rb:bg-white rb:rounded-xl rb:py-1 rb:min-w-60 rb:max-h-60 rb:overflow-y-auto rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
|
||||
style={{
|
||||
top: childPanelTop,
|
||||
right: 'calc(100% + 8px)',
|
||||
transform: 'translateY(-8px)',
|
||||
}}
|
||||
onMouseEnter={() => setExpandedParent(expandedParent)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto rb:text-[12px] rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{ top: childPanelPos.top, right: childPanelPos.right }}
|
||||
onMouseEnter={() => setActivePanel('child')}
|
||||
onMouseLeave={() => { setActivePanel('main'); setChildActiveIndex(-1); }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="rb:px-3 rb:py-2 rb:text-[12px] rb:font-medium rb:text-[#5B6167] rb:border-b rb:border-[#F0F0F0]">
|
||||
<Flex justify="space-between" align="center">
|
||||
<div className="rb:pb-2 rb:mb-1 rb:font-medium rb:text-[#5B6167] rb-border-b">
|
||||
<Flex justify="space-between" align="center" gap={8}>
|
||||
<span>{expandedParent.nodeData.name}.{expandedParent.label}</span>
|
||||
<span>{expandedParent.dataType}</span>
|
||||
</Flex>
|
||||
</div>
|
||||
{expandedParent.children.map((child) => {
|
||||
const childIndex = flatOptions.indexOf(child);
|
||||
{expandedParent.children.map((child, ci) => {
|
||||
const isChildActive = activePanel === 'child' && ci === childActiveIndex;
|
||||
return (
|
||||
<Flex
|
||||
key={child.key}
|
||||
data-selected={selectedIndex === childIndex}
|
||||
className="rb:px-3! rb:py-2!"
|
||||
ref={(el) => { if (el) childItemRefs.current.set(child.key, el); }}
|
||||
className={clsx("rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:bg-[#F6F6F6]': isChildActive,
|
||||
'rb:cursor-not-allowed rb:opacity-65': child.disabled,
|
||||
'rb:cursor-pointer': !child.disabled,
|
||||
})}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
cursor: child.disabled ? 'not-allowed' : 'pointer',
|
||||
background: selectedIndex === childIndex ? '#f0f8ff' : 'white',
|
||||
opacity: child.disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !child.disabled && insertMention(child)}
|
||||
onMouseEnter={() => setSelectedIndex(childIndex)}
|
||||
onMouseEnter={() => { setActivePanel('child'); setChildActiveIndex(ci); }}
|
||||
>
|
||||
<span>{child.label}</span>
|
||||
{child.dataType && <span className="rb:text-[#5B6167]">{child.dataType}</span>}
|
||||
<span className="rb:font-medium">
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span> {child.label}
|
||||
</span>
|
||||
{child.dataType && <span>{child.dataType}</span>}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default AutocompletePlugin
|
||||
export default AutocompletePlugin
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* @Last Modified time: 2026-04-07 14:50:14
|
||||
*/
|
||||
import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import {
|
||||
$getSelection, $isRangeSelection, $isTextNode,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND,
|
||||
} from 'lexical';
|
||||
import { Space, Flex } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { CLOSE_AUTOCOMPLETE_COMMAND } from '../commands';
|
||||
import type { Suggestion } from './AutocompletePlugin';
|
||||
@@ -22,17 +24,22 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, anchorBottom: 0 });
|
||||
const [expandedParent, setExpandedParent] = useState<Suggestion | null>(null);
|
||||
const [childPanelTop, setChildPanelTop] = useState(0);
|
||||
const [childPanelPos, setChildPanelPos] = useState({ top: 0, right: 0 });
|
||||
const [activePanel, setActivePanel] = useState<'main' | 'child'>('main');
|
||||
const [childActiveIndex, setChildActiveIndex] = useState(-1);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const childItemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
|
||||
const CHILD_PANEL_HEIGHT = 280;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!popupRef.current || !showSuggestions) return;
|
||||
const { top, anchorBottom } = popupPosition;
|
||||
const { top, left, anchorBottom } = popupPosition;
|
||||
const popupHeight = popupRef.current.offsetHeight;
|
||||
const popupWidth = popupRef.current.offsetWidth;
|
||||
const MARGIN = 10;
|
||||
|
||||
let finalTop: number;
|
||||
if (top - popupHeight - MARGIN >= 0) {
|
||||
finalTop = top - popupHeight - MARGIN;
|
||||
@@ -41,51 +48,57 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
||||
if (finalTop + popupHeight > window.innerHeight - MARGIN)
|
||||
finalTop = window.innerHeight - popupHeight - MARGIN;
|
||||
}
|
||||
if (finalTop !== top) setPopupPosition(prev => ({ ...prev, top: finalTop }));
|
||||
|
||||
let finalLeft = left;
|
||||
if (finalLeft + popupWidth > window.innerWidth - MARGIN)
|
||||
finalLeft = window.innerWidth - popupWidth - MARGIN;
|
||||
if (finalLeft < MARGIN) finalLeft = MARGIN;
|
||||
|
||||
if (finalTop !== top || finalLeft !== left)
|
||||
setPopupPosition(prev => ({ ...prev, top: finalTop, left: finalLeft }));
|
||||
}, [showSuggestions, popupPosition.anchorBottom]);
|
||||
|
||||
const calcChildPanelTop = (elRect: DOMRect, popupRect: DOMRect) => {
|
||||
const relativeTop = elRect.top - popupRect.top;
|
||||
const overflow = popupRect.top + relativeTop + CHILD_PANEL_HEIGHT - (window.innerHeight - 10);
|
||||
return overflow > 0 ? relativeTop - overflow : relativeTop;
|
||||
const calcChildPanelPos = (key: string) => {
|
||||
const el = itemRefs.current.get(key);
|
||||
if (!el || !popupRef.current) return;
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const popupRect = popupRef.current.getBoundingClientRect();
|
||||
const actualChildHeight = Math.min(CHILD_PANEL_HEIGHT, popupRect.height);
|
||||
const top = Math.max(10, popupRect.bottom - actualChildHeight);
|
||||
setChildPanelPos({ top, right: window.innerWidth - elRect.left + 8 });
|
||||
};
|
||||
|
||||
const scrollSelectedIntoView = () => {
|
||||
if (!popupRef.current) return;
|
||||
const selectedElement = popupRef.current.querySelector('[data-selected="true"]');
|
||||
if (!selectedElement) return;
|
||||
const container = popupRef.current;
|
||||
const element = selectedElement as HTMLElement;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (elementRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += elementRect.bottom - containerRect.bottom;
|
||||
} else if (elementRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - elementRect.top;
|
||||
}
|
||||
const resetState = () => {
|
||||
setShowSuggestions(false);
|
||||
setExpandedParent(null);
|
||||
setChildPanelPos({ top: 0, right: 0 });
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const selection = $getSelection();
|
||||
if (!selection || !$isRangeSelection(selection)) {
|
||||
setShowSuggestions(false);
|
||||
return;
|
||||
}
|
||||
if (!selection || !$isRangeSelection(selection)) { setShowSuggestions(false); return; }
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const anchorOffset = selection.anchor.offset;
|
||||
const textBeforeCursor = anchorNode.getTextContent().substring(0, anchorOffset);
|
||||
const shouldShow = textBeforeCursor.endsWith('/');
|
||||
setShowSuggestions(shouldShow);
|
||||
if (!shouldShow) { setSelectedIndex(0); setExpandedParent(null); setChildPanelTop(0); return; }
|
||||
|
||||
if (!shouldShow) {
|
||||
setSelectedIndex(0);
|
||||
setExpandedParent(null);
|
||||
setChildPanelPos({ top: 0, right: 0 });
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
return;
|
||||
}
|
||||
const domSelection = window.getSelection();
|
||||
if (domSelection && domSelection.rangeCount > 0) {
|
||||
const rect = domSelection.getRangeAt(0).getBoundingClientRect();
|
||||
const popupWidth = 280;
|
||||
let left = rect.left;
|
||||
if (left + popupWidth > window.innerWidth) left = window.innerWidth - popupWidth - 10;
|
||||
if (left + 280 > window.innerWidth) left = window.innerWidth - 280 - 10;
|
||||
if (left < 10) left = 10;
|
||||
setPopupPosition({ top: rect.top, left, anchorBottom: rect.bottom });
|
||||
}
|
||||
@@ -96,7 +109,7 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
CLOSE_AUTOCOMPLETE_COMMAND,
|
||||
() => { setShowSuggestions(false); setExpandedParent(null); setChildPanelTop(0); return true; },
|
||||
() => { resetState(); return true; },
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
);
|
||||
}, [editor]);
|
||||
@@ -119,9 +132,7 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('jinja2-variable-inserted', { detail: { value: suggestion.value } }));
|
||||
setShowSuggestions(false);
|
||||
setExpandedParent(null);
|
||||
setChildPanelTop(0);
|
||||
resetState();
|
||||
};
|
||||
|
||||
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, s) => {
|
||||
@@ -131,152 +142,227 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const allOptions = Object.values(groupedSuggestions).flat().flatMap(o =>
|
||||
o.key === expandedParent?.key && o.children?.length ? [o, ...o.children] : [o]
|
||||
);
|
||||
// Flat list of main-panel items for keyboard navigation
|
||||
const flatOptions = Object.values(groupedSuggestions).flat();
|
||||
|
||||
// Sync child panel position when keyboard navigates to a parent with children
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0 || selectedIndex >= flatOptions.length) return;
|
||||
const s = flatOptions[selectedIndex];
|
||||
if (s.children?.length) {
|
||||
calcChildPanelPos(s.key);
|
||||
setExpandedParent(s);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedIndex]);
|
||||
|
||||
// Scroll child active item into view
|
||||
useEffect(() => {
|
||||
if (!expandedParent?.children?.length || childActiveIndex < 0) return;
|
||||
const child = expandedParent.children[childActiveIndex];
|
||||
if (child) childItemRefs.current.get(child.key)?.scrollIntoView({ block: 'nearest' });
|
||||
}, [childActiveIndex, expandedParent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
return editor.registerCommand(
|
||||
KEY_ENTER_COMMAND,
|
||||
(event) => {
|
||||
const opt = allOptions[selectedIndex];
|
||||
if (opt && !opt.disabled) { event?.preventDefault(); insertMention(opt); return true; }
|
||||
if (!showSuggestions) return false;
|
||||
if (activePanel === 'child' && expandedParent?.children?.length) {
|
||||
const child = expandedParent.children[childActiveIndex];
|
||||
if (child && !child.disabled) { event?.preventDefault(); insertMention(child); return true; }
|
||||
} else if (flatOptions.length > 0) {
|
||||
const opt = flatOptions[selectedIndex];
|
||||
if (opt && !opt.disabled) { event?.preventDefault(); insertMention(opt); return true; }
|
||||
}
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
);
|
||||
}, [showSuggestions, selectedIndex, allOptions]);
|
||||
}, [showSuggestions, selectedIndex, flatOptions, activePanel, childActiveIndex, expandedParent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
const down = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e) => {
|
||||
if (!showSuggestions) return false;
|
||||
e?.preventDefault();
|
||||
setSelectedIndex(prev => {
|
||||
let next = prev + 1;
|
||||
while (next < allOptions.length && allOptions[next].disabled) next++;
|
||||
setTimeout(scrollSelectedIntoView, 0);
|
||||
return next >= allOptions.length ? prev : next;
|
||||
});
|
||||
if (activePanel === 'child' && expandedParent?.children) {
|
||||
setChildActiveIndex(i => Math.min(i + 1, expandedParent.children!.length - 1));
|
||||
} else {
|
||||
setSelectedIndex(prev => {
|
||||
let next = prev + 1;
|
||||
while (next < flatOptions.length && flatOptions[next].disabled && !flatOptions[next].children?.length) next++;
|
||||
const newIndex = next >= flatOptions.length ? prev : next;
|
||||
setTimeout(() => itemRefs.current.get(flatOptions[newIndex]?.key)?.scrollIntoView({ block: 'nearest' }), 0);
|
||||
return newIndex;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}, COMMAND_PRIORITY_HIGH);
|
||||
|
||||
const up = editor.registerCommand(KEY_ARROW_UP_COMMAND, (e) => {
|
||||
if (!showSuggestions) return false;
|
||||
e?.preventDefault();
|
||||
setSelectedIndex(prev => {
|
||||
let p = prev - 1;
|
||||
while (p >= 0 && allOptions[p].disabled) p--;
|
||||
setTimeout(scrollSelectedIntoView, 0);
|
||||
return p < 0 ? prev : p;
|
||||
});
|
||||
if (activePanel === 'child' && expandedParent?.children) {
|
||||
setChildActiveIndex(i => Math.max(i - 1, 0));
|
||||
} else {
|
||||
setSelectedIndex(prev => {
|
||||
let p = prev - 1;
|
||||
while (p >= 0 && flatOptions[p].disabled && !flatOptions[p].children?.length) p--;
|
||||
const newIndex = p < 0 ? prev : p;
|
||||
setTimeout(() => itemRefs.current.get(flatOptions[newIndex]?.key)?.scrollIntoView({ block: 'nearest' }), 0);
|
||||
return newIndex;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}, COMMAND_PRIORITY_HIGH);
|
||||
|
||||
const esc = editor.registerCommand(KEY_ESCAPE_COMMAND, (e) => {
|
||||
e?.preventDefault(); setShowSuggestions(false); return true;
|
||||
}, COMMAND_PRIORITY_HIGH);
|
||||
|
||||
return () => { down(); up(); esc(); };
|
||||
}, [showSuggestions, selectedIndex, allOptions, editor]);
|
||||
}, [showSuggestions, selectedIndex, flatOptions, editor, activePanel, childActiveIndex, expandedParent]);
|
||||
|
||||
// ArrowLeft/Right for panel switching
|
||||
useEffect(() => {
|
||||
if (!showSuggestions) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
const current = flatOptions[selectedIndex];
|
||||
if (activePanel === 'main' && current?.children?.length) {
|
||||
e.preventDefault();
|
||||
setActivePanel('child');
|
||||
setChildActiveIndex(0);
|
||||
}
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
if (activePanel === 'child') {
|
||||
e.preventDefault();
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler, true);
|
||||
return () => document.removeEventListener('keydown', handler, true);
|
||||
}, [showSuggestions, activePanel, selectedIndex, flatOptions]);
|
||||
|
||||
if (!showSuggestions || Object.keys(groupedSuggestions).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={popupRef}
|
||||
data-autocomplete-popup="true"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:fixed rb:z-1000 rb:bg-white rb:rounded-xl rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
|
||||
style={{ top: popupPosition.top, left: popupPosition.left }}
|
||||
>
|
||||
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
|
||||
<Flex vertical gap={12}>
|
||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => (
|
||||
<div key={nodeId}>
|
||||
<Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
||||
{nodeOptions[0]?.nodeData?.icon && <div className={`rb:size-3 rb:bg-cover ${nodeOptions[0].nodeData.icon}`} />}
|
||||
{nodeOptions[0]?.nodeData?.name || nodeId}
|
||||
</Flex>
|
||||
{nodeOptions.map((option) => {
|
||||
const globalIndex = allOptions.indexOf(option);
|
||||
const hasChildren = !!option.children?.length;
|
||||
const isExpanded = expandedParent?.key === option.key;
|
||||
<>
|
||||
<div
|
||||
ref={popupRef}
|
||||
data-autocomplete-popup="true"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{ top: popupPosition.top, left: popupPosition.left }}
|
||||
>
|
||||
<div className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto">
|
||||
<Flex vertical gap={12}>
|
||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
|
||||
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
||||
return (
|
||||
<Flex
|
||||
key={option.key}
|
||||
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
||||
data-selected={selectedIndex === globalIndex}
|
||||
className="rb:pl-6! rb:pr-3! rb:py-2!"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
|
||||
opacity: option.disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => { if (option.disabled || hasChildren) return; insertMention(option); }}
|
||||
onMouseEnter={() => {
|
||||
setSelectedIndex(globalIndex);
|
||||
if (hasChildren) {
|
||||
const el = itemRefs.current.get(option.key);
|
||||
if (el && popupRef.current) {
|
||||
setChildPanelTop(calcChildPanelTop(el.getBoundingClientRect(), popupRef.current.getBoundingClientRect()));
|
||||
}
|
||||
setExpandedParent(option);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Space size={4}>
|
||||
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : '{x}'}</span>
|
||||
<span>{option.label}</span>
|
||||
</Space>
|
||||
<Space size={4}>
|
||||
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
|
||||
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1">›</span>}
|
||||
</Space>
|
||||
</Flex>
|
||||
<div key={nodeId} className="rb:text-[12px]">
|
||||
{nodeName !== 'undefined' &&
|
||||
<div className="rb:px-2 rb:leading-4.25 rb:mb-1.25 rb:font-medium rb:text-[#5B6167]">
|
||||
{nodeName}
|
||||
</div>
|
||||
}
|
||||
<Flex vertical gap={2}>
|
||||
{nodeOptions.map((option) => {
|
||||
const globalIndex = flatOptions.indexOf(option);
|
||||
const hasChildren = !!option.children?.length;
|
||||
const isExpanded = expandedParent?.key === option.key;
|
||||
const isActive = activePanel === 'main' && selectedIndex === globalIndex;
|
||||
return (
|
||||
<Flex
|
||||
key={option.key}
|
||||
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
||||
className={clsx('rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]', {
|
||||
'rb:bg-[#F6F6F6]': isActive || isExpanded,
|
||||
'rb:cursor-not-allowed rb:opacity-65': option.disabled && !hasChildren,
|
||||
'rb:cursor-pointer': !option.disabled || hasChildren,
|
||||
})}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
onClick={() => {
|
||||
if (option.disabled && !hasChildren) return;
|
||||
if (!option.disabled) insertMention(option);
|
||||
if (hasChildren) { calcChildPanelPos(option.key); setExpandedParent(option); }
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setSelectedIndex(globalIndex);
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
if (hasChildren) { calcChildPanelPos(option.key); setExpandedParent(option); }
|
||||
else setExpandedParent(null);
|
||||
}}
|
||||
>
|
||||
{option.label &&
|
||||
<div className="rb:font-medium">
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span> {option.label}
|
||||
</div>
|
||||
}
|
||||
<Space size={2}>
|
||||
{option.dataType && <span>{option.dataType}</span>}
|
||||
{hasChildren && <div className="rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:rotate-90"></div>}
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
{expandedParent?.children?.length && (
|
||||
|
||||
{expandedParent?.children?.length && createPortal(
|
||||
<div
|
||||
className="rb:absolute rb:bg-white rb:rounded-xl rb:py-1 rb:min-w-60 rb:max-h-60 rb:overflow-y-auto rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
|
||||
style={{ top: childPanelTop, right: 'calc(100% + 8px)', transform: 'translateY(-8px)' }}
|
||||
onMouseEnter={() => setExpandedParent(expandedParent)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto rb:text-[12px] rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{ top: childPanelPos.top, right: childPanelPos.right }}
|
||||
onMouseEnter={() => setActivePanel('child')}
|
||||
onMouseLeave={() => { setActivePanel('main'); setChildActiveIndex(-1); }}
|
||||
>
|
||||
<div className="rb:px-3 rb:py-2 rb:text-[12px] rb:font-medium rb:text-[#5B6167] rb:border-b rb:border-[#F0F0F0]">
|
||||
<Flex justify="space-between" align="center">
|
||||
<div className="rb:pb-2 rb:mb-1 rb:font-medium rb:text-[#5B6167] rb-border-b">
|
||||
<Flex justify="space-between" align="center" gap={8}>
|
||||
<span>{expandedParent.nodeData.name}.{expandedParent.label}</span>
|
||||
<span>{expandedParent.dataType}</span>
|
||||
</Flex>
|
||||
</div>
|
||||
{expandedParent.children.map((child) => {
|
||||
const childIndex = allOptions.indexOf(child);
|
||||
{expandedParent.children.map((child, ci) => {
|
||||
const isChildActive = activePanel === 'child' && ci === childActiveIndex;
|
||||
return (
|
||||
<Flex
|
||||
key={child.key}
|
||||
data-selected={selectedIndex === childIndex}
|
||||
className="rb:px-3! rb:py-2!"
|
||||
ref={(el) => { if (el) childItemRefs.current.set(child.key, el); }}
|
||||
className={clsx('rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]', {
|
||||
'rb:bg-[#F6F6F6]': isChildActive,
|
||||
'rb:cursor-not-allowed rb:opacity-65': child.disabled,
|
||||
'rb:cursor-pointer': !child.disabled,
|
||||
})}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
cursor: child.disabled ? 'not-allowed' : 'pointer',
|
||||
background: selectedIndex === childIndex ? '#f0f8ff' : 'white',
|
||||
opacity: child.disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !child.disabled && insertMention(child)}
|
||||
onMouseEnter={() => setSelectedIndex(childIndex)}
|
||||
onMouseEnter={() => { setActivePanel('child'); setChildActiveIndex(ci); }}
|
||||
>
|
||||
<span>{child.label}</span>
|
||||
{child.dataType && <span className="rb:text-[#5B6167]">{child.dataType}</span>}
|
||||
<span className="rb:font-medium">
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span> {child.label}
|
||||
</span>
|
||||
{child.dataType && <span>{child.dataType}</span>}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -34,29 +34,24 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
|
||||
>
|
||||
<Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}>
|
||||
{collapsed
|
||||
? <>
|
||||
{nodeLibrary.map(category => (
|
||||
<>
|
||||
{category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map((node, nodeIndex) => (
|
||||
<Tooltip key={nodeIndex} title={t(`workflow.${node.type}`)} placement="right">
|
||||
<div
|
||||
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('application/reactflow', node.type);
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
||||
}}
|
||||
>
|
||||
<div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
? nodeLibrary.flatMap(category =>
|
||||
category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map(node => (
|
||||
<Tooltip key={node.type} title={t(`workflow.${node.type}`)} placement="right">
|
||||
<div
|
||||
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('application/reactflow', node.type);
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
||||
}}
|
||||
>
|
||||
<div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
)
|
||||
: nodeLibrary.map(category => (
|
||||
<div
|
||||
key={category.category}
|
||||
@@ -65,9 +60,9 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col
|
||||
<Flex gap={6} vertical>
|
||||
{category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map((node, nodeIndex) => (
|
||||
.map((node) => (
|
||||
<Flex
|
||||
key={nodeIndex}
|
||||
key={node.type}
|
||||
align="center"
|
||||
gap={8}
|
||||
className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
|
||||
|
||||
@@ -3,15 +3,21 @@ import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
import NodeTools from './NodeTools'
|
||||
import { useVariableList } from '../Properties/hooks/useVariableList'
|
||||
import { isSubExprSet } from '../../utils'
|
||||
import { fileSubFieldOperators } from '../Properties/CaseList'
|
||||
|
||||
const caculateIsSet = (item: any, type: string) => {
|
||||
switch (type) {
|
||||
case 'categories':
|
||||
return typeof item?.class_name === 'string' && item?.class_name !== ''
|
||||
case 'cases': {
|
||||
if (item?.sub_variable_condition !== undefined) {
|
||||
return !!item.left && !!item.operator
|
||||
}
|
||||
if (!item.left) return false
|
||||
if (['not_empty', 'empty'].includes(item.operator)) return true
|
||||
return !!item.left && (!!item.right || typeof item.right === 'boolean' || typeof item.right === 'number')
|
||||
@@ -25,12 +31,24 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const variableList = useVariableList(node ?? null, graphRef, data.chatVariables ?? [])
|
||||
|
||||
const getLocaleField = (field: string, filedType: string) => {
|
||||
const key = filedType === 'boolean' ? `workflow.config.if-else..boolean.${field}` : filedType === 'number' ? `workflow.config.if-else.num.${field}` : `workflow.config.if-else.${field}`
|
||||
const key = filedType === 'boolean'
|
||||
? `workflow.config.if-else..boolean.${field}`
|
||||
: filedType === 'number'
|
||||
? `workflow.config.if-else.num.${field}`
|
||||
: `workflow.config.if-else.${field}`
|
||||
const value = t(key)
|
||||
return value !== key ? value : t(`workflow.config.if-else.num.${field}`)
|
||||
};
|
||||
const getSubLocaleField = (field: string, fieldKey: string) => {
|
||||
const operators = fileSubFieldOperators[fieldKey] ?? fileSubFieldOperators.default
|
||||
const match = operators?.find(op => op.value === field)
|
||||
return match?.label ? t(match.label as string) : field
|
||||
}
|
||||
|
||||
const labelRender = (value: string) => {
|
||||
const filterOption = variableList.find(vo => `{{${vo.value}}}` === value)
|
||||
?? variableList.flatMap(vo => vo.children ?? []).find(child => `{{${child.value}}}` === value)
|
||||
?? variableList.flatMap(vo => vo.children ?? []).flatMap((child: any) => child.children ?? []).find((grandchild: any) => `{{${grandchild.value}}}` === value)
|
||||
|
||||
if (filterOption) {
|
||||
return (
|
||||
@@ -47,13 +65,23 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#FCFCFD]': !data.isSelected
|
||||
'rb:border-[#171719]!': data.isSelected,
|
||||
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||
})}>
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
{data.executionStatus === 'completed'
|
||||
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||
: data.executionStatus === 'failed'
|
||||
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||
: data.executionStatus === 'running'
|
||||
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||
: null
|
||||
}
|
||||
</Flex>
|
||||
|
||||
{data.type === 'question-classifier' &&
|
||||
@@ -80,7 +108,8 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
{item.expressions.map((expression: any, eIndex: number) => (
|
||||
<div key={eIndex} className="rb:relative">
|
||||
{item.expressions.length > 1 && eIndex > 0 && <div className="rb:absolute rb:-top-2 rb:right-2 rb:text-[10px] rb:text-[#155EEF] rb:font-medium rb:leading-3.5 rb:text-right rb:pr-0.5">{item.logical_operator?.toLocaleUpperCase()}</div>}
|
||||
<Flex align="center" className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
<Flex vertical gap={2} className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
<Flex align="center">
|
||||
{caculateIsSet(expression, 'cases')
|
||||
? <>
|
||||
{labelRender(expression.left)}
|
||||
@@ -89,6 +118,33 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
</>
|
||||
: t(`workflow.config.${data.type}.unset`)
|
||||
}
|
||||
</Flex>
|
||||
{expression.sub_variable_condition?.conditions?.length > 0 && expression.sub_variable_condition?.conditions.every(isSubExprSet)
|
||||
? <div className="rb-border-l rb:ml-2 rb:mt-1.5">
|
||||
{expression.sub_variable_condition?.conditions.map((sub: any, sIndex: number) => (
|
||||
<div key={sIndex} className="rb:relative">
|
||||
{expression.sub_variable_condition?.conditions.length > 1 && sIndex > 0 && <div className="rb:absolute rb:-top-2 rb:right-2 rb:text-[10px] rb:text-[#155EEF] rb:font-medium rb:leading-3.5 rb:text-right rb:pr-0.5">{expression.sub_variable_condition?.logical_operator?.toLocaleUpperCase()}</div>}
|
||||
<Flex align="center" className=" rb:py-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
<span className="rb:text-[#155EEF]">{sub.key}</span>
|
||||
<span className="rb:mx-1">{getSubLocaleField(sub.operator, sub.key)}</span>
|
||||
<span className="rb:break-all rb:line-clamp-1">
|
||||
{sub.key === 'type'
|
||||
? t(`application.${sub.value}`)
|
||||
:!['not_empty', 'empty'].includes(sub.operator)
|
||||
? <span>{typeof sub.value === 'boolean' ? String(sub.value).charAt(0).toUpperCase() + String(sub.value).slice(1) : sub.value}</span>
|
||||
: null
|
||||
}
|
||||
</span>
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
: expression.sub_variable_condition?.conditions?.length > 0
|
||||
? <Flex align="center" className="rb:mt-1! rb:pl-2! rb:rounded-md rb:py-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
{t(`workflow.config.${data.type}.unset`)}
|
||||
</Flex>
|
||||
: null
|
||||
}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
import { graphNodeLibrary, edgeAttrs } from '../../constant';
|
||||
import NodeTools from './NodeTools'
|
||||
@@ -131,12 +132,22 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#FCFCFD]': !data.isSelected
|
||||
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||
})}>
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
{data.executionStatus === 'completed'
|
||||
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||
: data.executionStatus === 'failed'
|
||||
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||
: data.executionStatus === 'running'
|
||||
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||
: null
|
||||
}
|
||||
</Flex>
|
||||
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
import NodeTools from './NodeTools'
|
||||
|
||||
@@ -11,13 +12,23 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#FCFCFD]': !data.isSelected
|
||||
'rb:border-[#171719]!': data.isSelected,
|
||||
'rb:border-[#FCFCFD]': !data.isSelected,
|
||||
'rb:border-[#369F21]!': !data.isSelected && data.executionStatus === 'completed',
|
||||
'rb:border-[#FF5D34]!': !data.isSelected && data.executionStatus === 'failed',
|
||||
})}>
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<div className={`rb:size-6 rb:bg-cover ${data.icon}`} />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1 rb:flex-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
{data.executionStatus === 'completed'
|
||||
? <CheckCircleFilled style={{ color: '#369F21', fontSize: 16 }} />
|
||||
: data.executionStatus === 'failed'
|
||||
? <CloseCircleFilled style={{ color: '#FF5D34', fontSize: 16 }} />
|
||||
: data.executionStatus === 'running'
|
||||
? <LoadingOutlined style={{ color: '#5B6167', fontSize: 16 }} />
|
||||
: null
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div>
|
||||
|
||||
@@ -30,6 +30,25 @@ const operationsObj = {
|
||||
],
|
||||
}
|
||||
|
||||
const filterByDataType = (options: Suggestion[], dataType: string): Suggestion[] =>
|
||||
options.reduce<Suggestion[]>((acc, vo) => {
|
||||
if (vo.children?.length) {
|
||||
const children = vo.children.reduce<Suggestion[]>((cacc, child) => {
|
||||
if (child.children?.length) {
|
||||
const grandchildren = child.children.filter(gc => gc.dataType === dataType);
|
||||
if (grandchildren.length) cacc.push({ ...child, children: grandchildren });
|
||||
} else if (child.dataType === dataType) {
|
||||
cacc.push(child);
|
||||
}
|
||||
return cacc;
|
||||
}, []);
|
||||
if (children.length) acc.push({ ...vo, children });
|
||||
} else if (vo.dataType === dataType) {
|
||||
acc.push(vo);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const AssignmentList: FC<AssignmentListProps> = ({
|
||||
parentName,
|
||||
options = [],
|
||||
@@ -59,7 +78,9 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
<Flex gap={10} vertical>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector)
|
||||
?? options.flatMap(o => o.children ?? []).find(child => `{{${child.value}}}` === variableSelector)
|
||||
?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
@@ -119,7 +140,7 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
options={dataType ? filterByDataType(options, dataType) : options}
|
||||
size={size}
|
||||
className="rb:flex-1!"
|
||||
variant="filled"
|
||||
@@ -150,7 +171,7 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
options={dataType ? filterByDataType(options, dataType) : options}
|
||||
size={size}
|
||||
className="rb:flex-1!"
|
||||
variant="filled"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:24:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-25 15:23:45
|
||||
* @Last Modified time: 2026-04-17 20:47:49
|
||||
*/
|
||||
import { useMemo, type FC } from 'react'
|
||||
import { useEffect, useMemo, type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd'
|
||||
@@ -15,11 +15,37 @@ import Editor from '../../Editor'
|
||||
import { edgeAttrs, nodeWidth } from '../../../constant'
|
||||
import RbButton from '@/components/RbButton';
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils';
|
||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY, isSubExprSet } from '../../../utils';
|
||||
import { typeOptions } from '../ListOperator/FilterConditions'
|
||||
|
||||
|
||||
interface SubCondition {
|
||||
key: string;
|
||||
operator: string;
|
||||
value?: string | number;
|
||||
input_type?: string;
|
||||
}
|
||||
|
||||
interface SubVariableCondition {
|
||||
logical_operator: 'and' | 'or';
|
||||
conditions: SubCondition[];
|
||||
}
|
||||
|
||||
interface Expression {
|
||||
left?: string;
|
||||
operator?: string;
|
||||
right?: string | number;
|
||||
input_type?: string;
|
||||
sub_variable_condition?: SubVariableCondition;
|
||||
}
|
||||
|
||||
interface CaseItem {
|
||||
logical_operator: 'and' | 'or';
|
||||
expressions: Expression[];
|
||||
}
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
||||
value?: CaseItem[];
|
||||
onChange?: (value: CaseItem[]) => void;
|
||||
options: Suggestion[];
|
||||
name: string;
|
||||
selectedNode?: any;
|
||||
@@ -60,13 +86,12 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
{ value: 'empty', label: 'workflow.config.if-else.file.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.file.not_empty' },
|
||||
],
|
||||
// TODO:包含、不包含、全都是
|
||||
'array[file]': [
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'eq', label: 'workflow.config.if-else.file.eq' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
// { value: 'eq', label: 'workflow.config.if-else.eq' },
|
||||
// { value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
// { value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
],
|
||||
'array': [
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
@@ -80,6 +105,218 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
]
|
||||
}
|
||||
|
||||
const fileSubFieldOptions = [
|
||||
{ value: 'type', label: 'type' },
|
||||
{ value: 'size', label: 'size' },
|
||||
{ value: 'name', label: 'name' },
|
||||
{ value: 'url', label: 'url' },
|
||||
{ value: 'extension', label: 'extension' },
|
||||
{ value: 'mime_type', label: 'mime_type' },
|
||||
]
|
||||
|
||||
export const fileSubFieldOperators: { [key: string]: SelectProps['options'] } = {
|
||||
type: [
|
||||
{ value: 'eq', label: 'workflow.config.list-operator.type.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.list-operator.type.ne' },
|
||||
],
|
||||
size: [
|
||||
{ value: 'lt', label: 'workflow.config.if-else.num.lt' },
|
||||
{ value: 'le', label: 'workflow.config.if-else.num.le' },
|
||||
{ value: 'gt', label: 'workflow.config.if-else.num.gt' },
|
||||
{ value: 'ge', label: 'workflow.config.if-else.num.ge' },
|
||||
],
|
||||
default: [
|
||||
{ value: 'contains', label: 'workflow.config.if-else.contains' },
|
||||
{ value: 'not_contains', label: 'workflow.config.if-else.not_contains' },
|
||||
{ value: 'startwith', label: 'workflow.config.if-else.startwith' },
|
||||
{ value: 'endwith', label: 'workflow.config.if-else.endwith' },
|
||||
{ value: 'eq', label: 'workflow.config.if-else.eq' },
|
||||
{ value: 'ne', label: 'workflow.config.if-else.ne' },
|
||||
{ value: 'empty', label: 'workflow.config.if-else.empty' },
|
||||
{ value: 'not_empty', label: 'workflow.config.if-else.not_empty' },
|
||||
],
|
||||
}
|
||||
|
||||
interface ArrayFileSubConditionsProps {
|
||||
conditionFieldName: number;
|
||||
caseIndex: number;
|
||||
conditionIndex: number;
|
||||
name: string;
|
||||
filterNumberOptions: Suggestion[];
|
||||
options: Suggestion[];
|
||||
updateNodeLayout: (cases: any[]) => void;
|
||||
updateNodePorts: (caseCount: number, removedCaseIndex?: number) => void;
|
||||
}
|
||||
|
||||
const ArrayFileSubConditions: FC<ArrayFileSubConditionsProps> = ({ conditionFieldName, caseIndex, conditionIndex, name, filterNumberOptions, options, updateNodeLayout, updateNodePorts }) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const subValues = Form.useWatch([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions'], form)
|
||||
|
||||
const handleChangeSubLogicalOperator = () => {
|
||||
const current = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator']) || 'and';
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator'], current === 'and' ? 'or' : 'and');
|
||||
};
|
||||
const handleInputTypeChange = (caseIndex: number, conditionIndex: number, subIndex: number) => {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex, 'value'], undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('subValues', subValues)
|
||||
if (!subValues) return
|
||||
const cases = form.getFieldValue(name) || [];
|
||||
setTimeout(() => {
|
||||
updateNodeLayout(cases);
|
||||
const allSet = (subValues ?? []).every(isSubExprSet);
|
||||
console.log('allSet', allSet)
|
||||
updateNodePorts(cases.length);
|
||||
}, 100);
|
||||
}, [subValues])
|
||||
|
||||
return (
|
||||
<div className="rb:bg-white rb:rounded-lg rb:p-1 rb:w-62">
|
||||
<Form.List name={[conditionFieldName, 'sub_variable_condition', 'conditions']}>
|
||||
{(subFields, { add: addSub, remove: removeSub }) => {
|
||||
const subLogicalOperator = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator']) || 'and';
|
||||
return (
|
||||
<>
|
||||
<div className={clsx("rb:relative", {
|
||||
'rb:ml-11': subFields.length > 1,
|
||||
})}>
|
||||
{subFields.length > 1 && (
|
||||
<div className="rb:absolute rb:-left-8 rb:top-4 rb:bottom-4 rb:w-6 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2 rb:h-[calc(50%-20px)] rb:left-5 rb:top-0 rb:z-10 rb:border-l rb:border-t rb:border-[#EBEBEB] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:-right-1.25 rb:top-[calc(50%-10px)]">
|
||||
<Space size={2} className="rb:cursor-pointer rb:text-[#155EEF] rb:text-[10px] rb:leading-4.5 rb:font-medium rb-border rb:py-px! rb:px-1! rb:rounded-sm" onClick={handleChangeSubLogicalOperator}>
|
||||
{subLogicalOperator}
|
||||
<div className="rb:size-2.5 rb:bg-cover rb:bg-[url('@/assets/images/workflow/refresh_active.svg')]"></div>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="rb:absolute rb:w-2 rb:h-[calc(50%-20px)] rb:left-5 rb:bottom-0 rb:z-10 rb:border-l rb:border-b rb:border-[#EBEBEB] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>
|
||||
)}
|
||||
{subFields.map((subField, subIndex) => {
|
||||
const subExpr = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex]) || {};
|
||||
const subOperator = subExpr.operator;
|
||||
const subLeft = subExpr.key;
|
||||
const subOperatorList = subLeft === 'type' ? fileSubFieldOperators.type : subLeft === 'size' ? fileSubFieldOperators.size : fileSubFieldOperators.default;
|
||||
const hideSubRight = subOperator === 'empty' || subOperator === 'not_empty';
|
||||
const subInputType = subExpr.input_type
|
||||
return (
|
||||
<Flex key={subField.key} gap={4} align="start" className="rb:mb-1.5!">
|
||||
<div className={clsx("rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg rb:border rb:border-[#EBEBEB]", {
|
||||
'rb:w-43.5': subFields.length > 1,
|
||||
'rb:w-54.5': subFields.length === 1
|
||||
})}>
|
||||
<Row className={clsx('rb:p-1!', { 'rb-border-b': !hideSubRight })}>
|
||||
<Col flex="100px">
|
||||
<Form.Item name={[subField.name, 'key']} noStyle>
|
||||
<Select
|
||||
options={fileSubFieldOptions}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value: string) => {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex], {
|
||||
key: value,
|
||||
input_type: value === 'size' ? 'constant' : undefined,
|
||||
value: undefined,
|
||||
operator: value === 'size' ? 'ge' : 'eq',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Form.Item name={[subField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={(subOperatorList ?? []).map(vo => ({ ...vo, label: t(String(vo?.label || '')) }))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{!hideSubRight && (
|
||||
<div>
|
||||
{subLeft === 'size'
|
||||
? <Flex align="center">
|
||||
<Form.Item name={[subField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
onChange={() => { handleInputTypeChange(caseIndex, conditionIndex, subIndex); }}
|
||||
className="rb:w-20!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" className="rb:mx-0!" />
|
||||
<Form.Item name={[subField.name, 'value']} noStyle>
|
||||
{subInputType === 'variable'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={filterNumberOptions}
|
||||
allowClear={true}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
className="rb:flex-1!"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
suffix="Byte"
|
||||
size="small"
|
||||
onChange={(value) => { form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value); }}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
: <Form.Item name={[subField.name, 'value']} noStyle>
|
||||
{subLeft === 'type'
|
||||
? <Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={typeOptions.map(vo => ({ value: vo, label: t(`application.${vo}`) }))}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
: <Editor options={options} size="small" type="input" variant='borderless' height={28} className="rb:w-full!" />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => { removeSub(subField.name); }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => { addSub({ key: undefined, operator: undefined, value: undefined, input_type: undefined }); }}
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.if-else.addSubVariable')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CaseList: FC<CaseListProps> = ({
|
||||
options,
|
||||
name,
|
||||
@@ -251,7 +488,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: undefined
|
||||
input_type: 'constant'
|
||||
});
|
||||
};
|
||||
|
||||
@@ -303,7 +540,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
|
||||
return (
|
||||
<Row className="rb:text-[12px] rb:mb-4!">
|
||||
<Col flex="48px">
|
||||
<Col flex="44px">
|
||||
<div className="rb:font-medium rb:leading-4.5">{caseIndex === 0 ? 'IF' : 'ELIF'}</div>
|
||||
{caseFields.length > 1 && <div className="rb:text-[10px] rb:text-[#5B6167] rb:leading-2.5"> {`CASE ${caseIndex + 1}`}</div>}
|
||||
</Col>
|
||||
@@ -314,9 +551,9 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<div className="rb:absolute rb:w-3 rb:h-[calc(50%-20px)] rb:left-5 rb:top-0 rb:z-10 rb:border-l rb:border-t rb:border-[#EBEBEB] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:-right-1.25 rb:top-[calc(50%-10px)]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Space size={2} className="rb:cursor-pointer rb:text-[#155EEF] rb:leading-4.5 rb:font-medium rb-border rb:py-px! rb:px-1! rb:rounded-sm" onClick={() => handleChangeLogicalOperator(caseIndex)}>
|
||||
<Space size={2} className="rb:cursor-pointer rb:text-[#155EEF] rb:text-[10px] rb:leading-4.5 rb:font-medium rb-border rb:py-px! rb:px-1! rb:rounded-sm" onClick={() => handleChangeLogicalOperator(caseIndex)}>
|
||||
{logicalOperator}
|
||||
<div className="rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/workflow/refresh_active.svg')]"></div>
|
||||
<div className="rb:size-2.5 rb:bg-cover rb:bg-[url('@/assets/images/workflow/refresh_active.svg')]"></div>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -329,9 +566,11 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const currentExpression = currentCase.expressions?.[conditionIndex] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue)
|
||||
?? options.flatMap(o => o.children ?? []).find(child => `{{${child.value}}}` === leftFieldValue)
|
||||
?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]' || leftFieldType === 'array[file]';
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]';
|
||||
const operatorList = leftFieldType && operatorsObj[leftFieldType]
|
||||
? operatorsObj[leftFieldType]
|
||||
: leftFieldType && leftFieldType?.includes('array')
|
||||
@@ -341,7 +580,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
return (
|
||||
<Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||
<Row className={clsx("rb:p-1!", {
|
||||
<Row className={clsx("rb:px-1!", {
|
||||
'rb-border-b': !hideRightField
|
||||
})}>
|
||||
<Col flex="144px">
|
||||
@@ -375,22 +614,40 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</Row>
|
||||
|
||||
{!hideRightField && (
|
||||
<div className="rb:py-1 rb:px-1.5">
|
||||
<div
|
||||
className={clsx({
|
||||
"rb:py-1 rb:px-1.5": ['boolean', 'array[boolean]', 'array[file]'].includes(leftFieldType as string)
|
||||
})}
|
||||
>
|
||||
{leftFieldType === 'array[file]'
|
||||
? <>TODO</>
|
||||
? <>
|
||||
<Form.Item name={[conditionField.name, 'sub_variable_condition', 'logical_operator']} initialValue="and" noStyle>
|
||||
<span />
|
||||
</Form.Item>
|
||||
<ArrayFileSubConditions
|
||||
conditionFieldName={conditionField.name}
|
||||
caseIndex={caseIndex}
|
||||
conditionIndex={conditionIndex}
|
||||
name={name}
|
||||
options={options}
|
||||
filterNumberOptions={filterNumberOptions}
|
||||
updateNodeLayout={updateNodeLayout}
|
||||
updateNodePorts={updateNodePorts}
|
||||
/>
|
||||
</>
|
||||
: leftFieldType === 'number'
|
||||
? <Flex align="center">
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'Constant', label: 'constant' }]}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
className="rb:w-20!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Divider type="vertical" className="rb:mx-0!" />
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'variable'
|
||||
? <VariableSelect
|
||||
@@ -413,7 +670,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{['boolean', 'array[boolean]'].includes(leftFieldType as string)
|
||||
? <RadioGroupBtn options={[{ value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
: <Editor options={options} size="small" type="input" />
|
||||
: <Editor options={options} size="small" type="input" variant='borderless' height={28} />
|
||||
}
|
||||
</Form.Item>
|
||||
)
|
||||
|
||||
@@ -94,7 +94,7 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
className={`rb:font-medium! rb:w-25! rb:h-4! rb:p-0! ${styles.editor}`}
|
||||
className={`rb:font-medium! rb:w-25! rb:h-4! rb:py-0! rb:px-2! ${styles.editor}`}
|
||||
onChange={handleChangeLanguage}
|
||||
variant="borderless"
|
||||
/>
|
||||
|
||||
@@ -155,7 +155,9 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue)
|
||||
?? options.flatMap(o => o.children ?? []).find(child => `{{${child.value}}}` === leftFieldValue)
|
||||
?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || ['array[object]', 'object'].includes(leftFieldType as string);
|
||||
const operatorList = leftFieldType && ['array[object]', 'object'].includes(leftFieldType)
|
||||
@@ -176,7 +178,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||
<Row className={clsx("rb:p-1!", {
|
||||
<Row className={clsx("rb:px-1!", {
|
||||
'rb-border-b': !hideRightField
|
||||
})}>
|
||||
<Col flex="1">
|
||||
@@ -216,7 +218,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
</Row>
|
||||
|
||||
{!hideRightField && (
|
||||
<div className="rb:py-1 rb:px-1.5">
|
||||
<div className={leftFieldType === 'boolean' ? "rb:py-1 rb:px-1.5" : ''}>
|
||||
{leftFieldType === 'number'
|
||||
? (
|
||||
<Flex align="center">
|
||||
|
||||
@@ -155,7 +155,7 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
? <CodeMirrorEditor
|
||||
language="json"
|
||||
placeholder={object_placeholder}
|
||||
variant="outlined"
|
||||
variant="filled"
|
||||
size="small"
|
||||
/>
|
||||
: (
|
||||
|
||||
@@ -62,14 +62,18 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isCanAdd && value[0]) {
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === value[0]);
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === value[0])
|
||||
?? options.flatMap(o => o.children ?? []).find(c => `{{${c.value}}}` === value[0])
|
||||
?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === value[0]);
|
||||
if (firstVariable) {
|
||||
form.setFieldValue(['group_type', 'output'], firstVariable.dataType);
|
||||
}
|
||||
} else if (isCanAdd) {
|
||||
value.forEach((item: any, index: number) => {
|
||||
if (item?.value?.[0]) {
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === item.value[0]);
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === item.value[0])
|
||||
?? options.flatMap(o => o.children ?? []).find(c => `{{${c.value}}}` === item.value[0])
|
||||
?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === item.value[0]);
|
||||
if (firstVariable) {
|
||||
form.setFieldValue(['group_type', index], firstVariable.dataType);
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
return [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
dataIndex: 'key',
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} className={formClassName}>
|
||||
<Form.Item name={[index, 'key']} className={formClassName}>
|
||||
<Editor
|
||||
options={namefilterOptions}
|
||||
type="input"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:35:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-02 17:17:06
|
||||
* @Last Modified time: 2026-04-14 17:36:53
|
||||
*/
|
||||
import { type FC, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -35,9 +35,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
form.setFieldsValue({ auth })
|
||||
}
|
||||
|
||||
const handleChangeBodyContentType = (e: any) => {
|
||||
const value = e.target.value || e.target.value
|
||||
form.setFieldValue(['body', 'data'], ['form-data', 'x-www-form-urlencoded'].includes(value) ? [{}] : undefined)
|
||||
const handleChangeBodyContentType = () => {
|
||||
form.setFieldValue(['body', 'data'], undefined)
|
||||
}
|
||||
|
||||
// Handle error handling method change and update node ports accordingly
|
||||
|
||||
@@ -56,7 +56,7 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = {
|
||||
]
|
||||
}
|
||||
|
||||
const typeOptions = ['image', 'document', 'video', 'audio']
|
||||
export const typeOptions = ['image', 'document', 'video', 'audio']
|
||||
|
||||
const FilterConditions: FC<FilterConditionsProps> = ({
|
||||
options,
|
||||
@@ -101,24 +101,20 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
||||
align="start"
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||
<div className="rb:flex-1">
|
||||
{variableType === 'array[file]' &&
|
||||
<Row className="rb:p-1! rb-border-b">
|
||||
<Col span={24}>
|
||||
<Form.Item name={[field.name, 'key']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={fileSubVariable}
|
||||
fieldNames={{ value: 'filed', label: 'label' }}
|
||||
onChange={(value) => handleKeyFieldChange(index, value)}
|
||||
variant="borderless"
|
||||
className="rb:w-full! rb:h-7!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={[field.name, 'key']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={fileSubVariable}
|
||||
fieldNames={{ value: 'filed', label: 'label' }}
|
||||
onChange={(value) => handleKeyFieldChange(index, value)}
|
||||
className="rb:w-full! select rb:mb-1!"
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
<Row>
|
||||
<Row gutter={8}>
|
||||
<Col flex={hideValueField ? '1' : "96px"}>
|
||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||
<Select
|
||||
@@ -129,28 +125,27 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
className="rb:w-full! select"
|
||||
variant="borderless"
|
||||
className="rb:w-full! rb:h-7!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!hideValueField && (
|
||||
<Col flex="1">
|
||||
<Form.Item name={[field.name, 'value']} className="rb:pt-0.5! rb:mb-0! rb:pl-2!">
|
||||
<Form.Item name={[field.name, 'value']} noStyle>
|
||||
{innerType === 'boolean'
|
||||
? <RadioGroupBtn options={[{ value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
: keyFieldValue === 'type'
|
||||
? <Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={typeOptions.map(vo => ({ value: vo, label: t(`application.${vo}`) } ))}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
variant="filled"
|
||||
/>
|
||||
: <Editor
|
||||
variant="borderless"
|
||||
variant="filled"
|
||||
type="input"
|
||||
size="small"
|
||||
height={24}
|
||||
height={28}
|
||||
options={keyFieldType ? options.flatMap(vo => {
|
||||
if (vo.dataType === keyFieldType) return [vo];
|
||||
const filteredChildren = vo.children?.filter(sub => sub.dataType === keyFieldType);
|
||||
@@ -167,7 +162,7 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
||||
</Row>
|
||||
</div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:mt-1.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(field.name)}
|
||||
></div>
|
||||
</Flex>
|
||||
|
||||
@@ -58,7 +58,7 @@ const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueK
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
className="rb:w-51!"
|
||||
className="rb:flex-1!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
import { type FC } from "react";
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-07 14:55:04
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 10:05:32
|
||||
*/
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form } from 'antd'
|
||||
import { Form, Switch } from 'antd'
|
||||
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ModelSelect from '@/components/ModelSelect'
|
||||
import type { Model } from '@/views/ModelManagement/types';
|
||||
|
||||
const ModelConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const model_id = Form.useWatch(['model_id'], form)
|
||||
console.log('ModelConfig', model_id)
|
||||
const [selectedModel, setSelectedModel] = useState<Model | null>(null)
|
||||
const [options, setOptions] = useState<Model[]>([])
|
||||
|
||||
const updateOptions = (options: Model[]) => {
|
||||
setOptions(options)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (model_id && options) {
|
||||
const model = options.find(item => item.id === model_id)
|
||||
setSelectedModel(model || null)
|
||||
form.setFieldValue('json_output', false)
|
||||
} else {
|
||||
setSelectedModel(null)
|
||||
}
|
||||
}, [model_id, options])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -25,6 +47,7 @@ const ModelConfig: FC = () => {
|
||||
params={{ type: 'llm,chat' }}
|
||||
className="rb:w-full!"
|
||||
size="small"
|
||||
updateOptions={updateOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
{model_id && (
|
||||
@@ -52,7 +75,7 @@ const ModelConfig: FC = () => {
|
||||
<Form.Item
|
||||
name="max_tokens"
|
||||
label={t('workflow.config.llm.max_tokens')}
|
||||
className="rb:mb-0!"
|
||||
className="rb:mb-1.5!"
|
||||
>
|
||||
<RbSlider
|
||||
min={256}
|
||||
@@ -63,6 +86,16 @@ const ModelConfig: FC = () => {
|
||||
className="rb:-mt-2!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="json_output"
|
||||
valuePropName="checked"
|
||||
label={t('workflow.config.llm.json_output')}
|
||||
layout="horizontal"
|
||||
className="rb:mb-0!"
|
||||
hidden={!(selectedModel?.capability?.includes('json_output'))}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</RbCard>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:40:13
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-08 10:48:21
|
||||
* @Last Modified time: 2026-04-16 13:57:30
|
||||
*/
|
||||
import { useState, useRef, useEffect, useLayoutEffect, type FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@@ -40,15 +40,34 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [expandedParent, setExpandedParent] = useState<Suggestion | null>(null);
|
||||
const [expandedParentKey, setExpandedParentKey] = useState<string | null>(null);
|
||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||
const [activePanel, setActivePanel] = useState<'main' | 'child'>('main');
|
||||
const [childActiveIndex, setChildActiveIndex] = useState<number>(-1);
|
||||
const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0, width: 0 });
|
||||
const [childPanelPos, setChildPanelPos] = useState({ top: 0, right: 0 });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const childItemRefs = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const activeKeyRef = useRef<string | null>(null);
|
||||
|
||||
const CHILD_PANEL_HEIGHT = 280; // max-h-60 (240) + header (~40)
|
||||
|
||||
const calcChildPos = (key: string) => {
|
||||
const el = itemRefs.current.get(key);
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const dropdownEl = dropdownRef.current;
|
||||
if (!dropdownEl) return;
|
||||
const dropdownRect = dropdownEl.getBoundingClientRect();
|
||||
const dropdownBottom = dropdownRect.bottom;
|
||||
const actualChildHeight = Math.min(CHILD_PANEL_HEIGHT, dropdownRect.height);
|
||||
// Bottom-align child panel with main panel
|
||||
const top = Math.max(10, dropdownBottom - actualChildHeight);
|
||||
setChildPanelPos({ top, right: window.innerWidth - rect.left + 8 });
|
||||
};
|
||||
|
||||
// Calculate dropdown position (runs synchronously after DOM paint to avoid flicker)
|
||||
useLayoutEffect(() => {
|
||||
if (!open || !containerRef.current) return;
|
||||
@@ -69,7 +88,9 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
? triggerRect.bottom + MARGIN
|
||||
: Math.max(MARGIN, triggerRect.top - dropdownHeight - MARGIN);
|
||||
setDropdownPos({ top, left, width });
|
||||
}, [open, search, Array.isArray(value) ? value.length : 0]);
|
||||
// Re-calculate child panel position if expanded
|
||||
if (expandedParentKey) calcChildPos(expandedParentKey);
|
||||
}, [open, search, Array.isArray(value) ? value.length : 0, options.length, expandedParentKey]);
|
||||
|
||||
const filteredOptions = filterBooleanType
|
||||
? options.filter(o => o.dataType !== 'boolean')
|
||||
@@ -84,6 +105,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
? filteredOptions.find(o => o.children?.some(c => `{{${c.value}}}` === value))
|
||||
: undefined;
|
||||
|
||||
const expandedParent = expandedParentKey
|
||||
? filteredOptions.find(o => o.key === expandedParentKey) ?? null
|
||||
: null;
|
||||
|
||||
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, Suggestion[]>, s) => {
|
||||
const nodeId = s.nodeData.id as string;
|
||||
if (!groups[nodeId]) groups[nodeId] = [];
|
||||
@@ -103,6 +128,12 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}, {})
|
||||
: groupedSuggestions;
|
||||
|
||||
useEffect(() => {
|
||||
if (!expandedParentKey) return;
|
||||
calcChildPos(expandedParentKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dropdownPos, expandedParentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const updatePos = () => {
|
||||
@@ -139,7 +170,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
) {
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
setExpandedParent(null);
|
||||
setExpandedParentKey(null);
|
||||
setChildPanelPos({ top: 0, right: 0 });
|
||||
}
|
||||
};
|
||||
@@ -147,6 +178,87 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
// Flat list of all visible selectable items (main panel only, no children expanded inline)
|
||||
const flatItems = Object.values(filteredGroups).flat();
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(-1);
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
}, [open, search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex < 0 || activeIndex >= flatItems.length) {
|
||||
setExpandedParentKey(null);
|
||||
return;
|
||||
}
|
||||
const s = flatItems[activeIndex];
|
||||
activeKeyRef.current = s.key;
|
||||
itemRefs.current.get(s.key)?.scrollIntoView({ block: 'nearest' });
|
||||
if (s.children?.length) {
|
||||
calcChildPos(s.key);
|
||||
setExpandedParentKey(s.key);
|
||||
} else {
|
||||
setExpandedParentKey(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!expandedParent?.children?.length || childActiveIndex < 0) return;
|
||||
const child = expandedParent.children[childActiveIndex];
|
||||
if (child) childItemRefs.current.get(child.key)?.scrollIntoView({ block: 'nearest' });
|
||||
}, [childActiveIndex, expandedParent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const children = expandedParent?.children ?? [];
|
||||
if (activePanel === 'child') {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setChildActiveIndex(i => Math.min(i + 1, children.length - 1));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setChildActiveIndex(i => Math.max(i - 1, 0));
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
setActivePanel('main');
|
||||
setChildActiveIndex(-1);
|
||||
} else if (e.key === 'Enter' && childActiveIndex >= 0 && childActiveIndex < children.length) {
|
||||
e.preventDefault();
|
||||
const child = children[childActiveIndex];
|
||||
if (!child.disabled) handleSelect(child);
|
||||
} else if (e.key === 'Escape') {
|
||||
setOpen(false);
|
||||
}
|
||||
} else {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setActiveIndex(i => Math.min(i + 1, flatItems.length - 1));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setActiveIndex(i => Math.max(i - 1, 0));
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
if (expandedParent?.children?.length) {
|
||||
setActivePanel('child');
|
||||
setChildActiveIndex(0);
|
||||
}
|
||||
} else if (e.key === 'Enter' && activeIndex >= 0 && activeIndex < flatItems.length) {
|
||||
e.preventDefault();
|
||||
const s = flatItems[activeIndex];
|
||||
if (!s.disabled) handleSelect(s);
|
||||
} else if (e.key === 'Escape') {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, activeIndex, activePanel, childActiveIndex, flatItems, expandedParent]);
|
||||
|
||||
const handleSelect = (suggestion: Suggestion) => {
|
||||
if (multiple) {
|
||||
const key = `{{${suggestion.value}}}`;
|
||||
@@ -159,7 +271,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
onChange?.(`{{${suggestion.value}}}`, suggestion);
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
setExpandedParent(null);
|
||||
setExpandedParentKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,19 +279,6 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
e.stopPropagation();
|
||||
onChange?.(multiple ? [] : '', multiple ? [] : undefined);
|
||||
};
|
||||
|
||||
const updateChildPos = (key: string) => {
|
||||
const el = itemRefs.current.get(key);
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.top - 10;
|
||||
const top = spaceBelow >= CHILD_PANEL_HEIGHT
|
||||
? rect.top
|
||||
: Math.max(10, window.innerHeight - CHILD_PANEL_HEIGHT - 10);
|
||||
setChildPanelPos({ top, right: window.innerWidth - rect.left + 8 });
|
||||
}
|
||||
};
|
||||
|
||||
const sep = <span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>;
|
||||
const isConversation = (parentOfSelected ?? selectedSuggestion)?.group === 'CONVERSATION' ||
|
||||
(selectedSuggestion ? filteredOptions.some(o => o.group === 'CONVERSATION' && o.children?.some(c => `{{${c.value}}}` === value)) : false);
|
||||
@@ -190,20 +289,30 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
{/* Trigger */}
|
||||
<div
|
||||
className={clsx(
|
||||
'rb:w-full rb:flex rb:items-center rb:justify-between rb:cursor-pointer rb:rounded-md rb:px-2 rb:transition-colors',
|
||||
variant === 'filled' && 'rb:bg-[#F6F6F6] rb:border-none rb:shadow-none',
|
||||
variant === 'outlined' && 'rb:border rb:border-[#d9d9d9] hover:rb:border-[#4096ff] rb:bg-white',
|
||||
variant === 'outlined' && open && 'rb:border-[#4096ff] rb:shadow-[0_0_0_2px_rgba(5,145,255,0.1)]',
|
||||
variant === 'borderless' && 'rb:border-none rb:shadow-none rb:bg-transparent',
|
||||
multiple && size === 'small' ? 'rb:min-h-7 rb:py-0.75' : multiple ? 'rb:min-h-8 rb:py-1' : size === 'small' ? 'rb:h-7 rb:text-[10px]' : size === 'large' ? 'rb:h-10' : 'rb:h-8 rb:text-[12px]',
|
||||
!multiple && (size === 'small' ? 'rb:text-[12px]' : 'rb:text-[12px]'),
|
||||
'rb:w-full rb:flex rb:items-center rb:justify-between rb:cursor-pointer rb:rounded-lg rb:px-2 rb:transition-colors', {
|
||||
'rb:bg-[#F6F6F6] rb:border-none rb:shadow-none': variant === 'filled',
|
||||
'rb:border rb:border-[#d9d9d9] hover:rb:border-[#4096ff] rb:bg-white': variant === 'outlined',
|
||||
'rb:border-[#171719]!': variant === 'outlined' && open,
|
||||
'rb:border-none rb:shadow-none rb:bg-transparent': variant === 'borderless',
|
||||
'rb:text-[12px]': size === 'small',
|
||||
'rb:text-[14px]': size !== 'small',
|
||||
},
|
||||
multiple && size === 'small'
|
||||
? 'rb:min-h-7 rb:py-0.75'
|
||||
: multiple
|
||||
? 'rb:min-h-8 rb:py-1'
|
||||
: size === 'small'
|
||||
? 'rb:h-7 rb:text-[10px]'
|
||||
: size === 'large'
|
||||
? 'rb:h-10'
|
||||
: 'rb:h-8 rb:text-[12px]',
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(o => !o)}
|
||||
>
|
||||
{multiple ? (
|
||||
selectedValues.length > 0 ? (
|
||||
<span className="rb:flex rb:flex-wrap rb:gap-1 rb:flex-1 rb:min-w-0">
|
||||
<Flex wrap gap={4} className="rb:flex-1! rb:min-w-0">
|
||||
{selectedValues.map(v => {
|
||||
const s = suggestionMap.get(v);
|
||||
if (!s) return null;
|
||||
@@ -214,11 +323,11 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
return (
|
||||
<span
|
||||
key={v}
|
||||
className="rb:inline-flex rb:items-center rb:gap-0.5 rb:bg-[#f0f8ff] rb:rounded rb:px-1 rb:py-0.5 rb:text-[11px] rb:max-w-full"
|
||||
className="rb-border rb:rounded-md rb:bg-white rb:text-[10px] rb:text-[#212332] rb:h-5! rb:inline-flex rb:items-center rb:p-1 rb:cursor-pointer"
|
||||
>
|
||||
{!isConv && nd?.icon && <div className={`rb:size-3 rb:shrink-0 rb:bg-cover ${nd.icon}`} />}
|
||||
{!isConv && nd?.icon && <div className={`rb:size-3 rb:bg-cover ${nd.icon}`} />}
|
||||
{!isConv && nd?.name && <span className="rb:text-[#5B6167]">{nd.name}{sep}</span>}
|
||||
<span className="rb:text-[#171719]">
|
||||
<span>
|
||||
{parent ? <>{parent.label}{sep}{s.label}</> : s.label}
|
||||
</span>
|
||||
<span
|
||||
@@ -228,23 +337,25 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</Flex>
|
||||
) : (
|
||||
<span className="rb:text-[#bfbfbf] rb:flex-1 rb:text-[12px]">{placeholder}</span>
|
||||
<span className="rb:text-[rgba(23,23,25,0.25)] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{placeholder}</span>
|
||||
)
|
||||
) : selectedSuggestion ? (
|
||||
<div className="rb:flex rb:flex-1 rb:min-w-0 rb:max-w-full">
|
||||
<span className="rb:inline-flex rb:items-center rb:gap-0.5 rb:bg-[#f0f8ff] rb:rounded rb:px-1 rb:py-0.5 rb:text-[11px] rb:max-w-full rb:overflow-hidden">
|
||||
{!isConversation && nodeData?.icon && <div className={`rb:size-3 rb:shrink-0 rb:bg-cover ${nodeData.icon}`} />}
|
||||
{!isConversation && nodeData?.name && <span className="rb:text-[#5B6167] rb:shrink rb:min-w-0 rb:truncate rb:max-w-[40%]">{nodeData.name}</span>}
|
||||
{!isConversation && nodeData?.name && <span className="rb:text-[#5B6167]">{sep}</span>}
|
||||
<span className="rb:text-[#171719] rb:shrink rb:min-w-0 rb:truncate">
|
||||
<span
|
||||
className="rb-border rb:rounded-md rb:bg-white rb:text-[10px] rb:text-[#212332] rb:h-5! rb:inline-flex rb:items-center rb:p-1 rb:cursor-pointer"
|
||||
>
|
||||
{!isConversation && nodeData?.icon && <div className={`rb:size-3 rb:bg-cover rb:mr-1 ${nodeData.icon}`} />}
|
||||
{!isConversation && nodeData?.name && <span className="rb:shrink rb:min-w-0 rb:truncate rb:max-w-[40%]">{nodeData.name}</span>}
|
||||
{!isConversation && nodeData?.name && <span>{sep}</span>}
|
||||
<span className="rb:shrink rb:min-w-0 rb:truncate">
|
||||
{parentOfSelected ? <>{parentOfSelected.label}{sep}{selectedSuggestion.label}</> : selectedSuggestion.label}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="rb:text-[#bfbfbf] rb:flex-1">{placeholder}</span>
|
||||
<span className="rb:text-[rgba(23,23,25,0.25)] rb:flex-1">{placeholder}</span>
|
||||
)}
|
||||
<Space size={4} className="rb:shrink-0 rb:ml-1">
|
||||
{allowClear && (
|
||||
@@ -266,18 +377,19 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
{open && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="rb:fixed rb:z-9999 rb:bg-white rb:text-[14px] rb:rounded-lg rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)] rb:p-1"
|
||||
className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{ top: dropdownPos.top, left: dropdownPos.left, minWidth: dropdownPos.width }}
|
||||
>
|
||||
<div className="rb:min-w-70 rb:max-h-60 rb:overflow-y-auto rb:py-1">
|
||||
{Object.entries(filteredGroups).map(([nodeId, suggestions]) => {
|
||||
<div className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto">
|
||||
{Object.entries(filteredGroups).map(([nodeId, suggestions], index) => {
|
||||
const nd = suggestions[0].nodeData;
|
||||
return (
|
||||
<div key={nodeId}>
|
||||
<Flex align="center" gap={4} className="rb:px-3! rb:py-1.25! rb:text-[12px] rb:text-[#5B6167]">
|
||||
{nd.icon && <div className={`rb:size-4 rb:bg-cover ${nd.icon}`} />}
|
||||
<div key={nodeId} className={clsx("rb:text-[12px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<div className="rb:px-2 rb:leading-4.25 rb:mb-1.25 rb:font-medium rb:text-[#5B6167]">
|
||||
{nd.name}
|
||||
</Flex>
|
||||
</div>
|
||||
{suggestions.map(s => {
|
||||
const isSelected = multiple
|
||||
? selectedValues.includes(`{{${s.value}}}`)
|
||||
@@ -288,11 +400,9 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
<Flex
|
||||
key={s.key}
|
||||
ref={(el) => { if (el) itemRefs.current.set(s.key, el); }}
|
||||
className={clsx("rb:pl-6! rb:pr-3! rb:py-1.25! rb:rounded-lg!", {
|
||||
'rb:bg-[#e6f4ff]': isSelected || isExpanded,
|
||||
'rb:bg-white rb:hover:bg-[#F6F6F6]!': !(isSelected || isExpanded),
|
||||
'rb:opacity-60': s.disabled,
|
||||
'rb:cursor-not-allowed': s.disabled,
|
||||
className={clsx("rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:bg-[#F6F6F6]': isSelected || isExpanded || flatItems.indexOf(s) === activeIndex,
|
||||
'rb:cursor-not-allowed rb:opacity-65': s.disabled,
|
||||
'rb:cursor-pointer': !s.disabled,
|
||||
})}
|
||||
align="center"
|
||||
@@ -300,30 +410,29 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
onClick={() => {
|
||||
if (s.disabled) return;
|
||||
if (hasChildren) {
|
||||
updateChildPos(s.key);
|
||||
setExpandedParent(prev => prev?.key === s.key ? null : s);
|
||||
calcChildPos(s.key);
|
||||
setExpandedParentKey(prev => prev === s.key ? null : s.key);
|
||||
}
|
||||
handleSelect(s);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (hasChildren) {
|
||||
updateChildPos(s.key);
|
||||
setExpandedParent(s);
|
||||
calcChildPos(s.key);
|
||||
setExpandedParentKey(s.key);
|
||||
} else {
|
||||
setExpandedParent(null);
|
||||
setExpandedParentKey(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Space size={4}>
|
||||
<div className="rb:font-medium">
|
||||
{multiple && (
|
||||
<Checkbox checked={isSelected} />
|
||||
<Checkbox checked={isSelected} className="rb:mr-2!" />
|
||||
)}
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span>
|
||||
<span>{s.label}</span>
|
||||
</Space>
|
||||
<Space size={4} className="rb:text-[#5B6167] rb:text-[12px]">
|
||||
{s.dataType && <span>{s.dataType}</span>}
|
||||
<span className="rb:text-[#155EEF]">{`{x}`}</span> {s.label}
|
||||
</div>
|
||||
|
||||
<Space size={2}>
|
||||
{s.dataType && <span>{s.dataType}</span>}
|
||||
{hasChildren && <div className="rb:size-3 rb:bg-cover rb:bg-[url('@/assets/images/common/arrow_up.svg')] rb:rotate-90"></div>}
|
||||
</Space>
|
||||
</Flex>
|
||||
@@ -334,7 +443,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
})}
|
||||
{Object.keys(filteredGroups).length === 0 && (
|
||||
<div className="rb:px-3 rb:py-4 rb:text-center rb:text-[#bfbfbf] rb:text-[14px]">
|
||||
{t('workflow.variableSelect.empty', '暂无变量')}
|
||||
{t('workflow.variableSelect.empty')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -346,51 +455,43 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
{open && expandedParent?.children?.length && createPortal(
|
||||
<div
|
||||
id="variable-select-child-panel"
|
||||
className="rb:fixed rb:z-9999 rb:bg-white rb:rounded-xl rb:py-1 rb:min-w-60 rb:max-h-60 rb:overflow-y-auto rb:text-[14px] rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]"
|
||||
className="rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto rb:text-[12px] rb:fixed rb:z-1000 rb:bg-white rb:rounded-lg rb:border-[0.5px] rb:border-[#EBEBEB] rb:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.1)] rb:py-3 rb:px-2"
|
||||
style={{ top: childPanelPos.top, right: childPanelPos.right }}
|
||||
onMouseEnter={() => setExpandedParent(expandedParent)}
|
||||
onMouseEnter={() => setExpandedParentKey(expandedParentKey)}
|
||||
>
|
||||
<div
|
||||
className="rb:px-3 rb:py-2 rb:text-[14px] rb:font-medium rb:text-[#5B6167] rb:border-b rb:border-[#F0F0F0] rb:cursor-pointer rb:hover:bg-[#f0f8ff]"
|
||||
onClick={() => !expandedParent.disabled && handleSelect(expandedParent)}
|
||||
>
|
||||
<div className="rb:pb-2 rb:mb-1 rb:font-medium rb:text-[#5B6167] rb-border-b">
|
||||
<Flex justify="space-between" align="center" gap={8}>
|
||||
<Flex align="center" gap={6}>
|
||||
<span>{expandedParent.nodeData.name}.{expandedParent.label}</span>
|
||||
</Flex>
|
||||
<span>{expandedParent.nodeData.name}.{expandedParent.label}</span>
|
||||
<span>{expandedParent.dataType}</span>
|
||||
</Flex>
|
||||
</div>
|
||||
{expandedParent.children.map(child => {
|
||||
{expandedParent.children.map((child, ci) => {
|
||||
const isSelected = multiple
|
||||
? selectedValues.includes(`{{${child.value}}}`)
|
||||
: `{{${child.value}}}` === value;
|
||||
const hasGrandChildren = !!child.children?.length;
|
||||
const isChildActive = activePanel === 'child' && ci === childActiveIndex;
|
||||
return (
|
||||
<Flex
|
||||
key={child.key}
|
||||
className={clsx("rb:px-3! rb:py-2! rb:hover:bg-[#f0f8ff]!", {
|
||||
'rb:bg-[#f0f8ff]': isSelected,
|
||||
'rb:white': !isSelected
|
||||
ref={(el) => { if (el) childItemRefs.current.set(child.key, el); }}
|
||||
className={clsx("rb:px-2! rb:py-0.75! rb:rounded-sm rb:leading-4.5 rb:text-[#5B6167] rb:hover:bg-[#F6F6F6]", {
|
||||
'rb:bg-[#F6F6F6]': isSelected || isChildActive,
|
||||
'rb:cursor-not-allowed rb:opacity-65': child.disabled,
|
||||
'rb:cursor-pointer': !child.disabled,
|
||||
})}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style={{
|
||||
cursor: child.disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: child.disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !child.disabled && handleSelect(child)}
|
||||
>
|
||||
<Flex align="center" gap={6}>
|
||||
<Flex align="center" gap={8}>
|
||||
{multiple && (
|
||||
<Checkbox checked={isSelected} />
|
||||
)}
|
||||
<span>{child.label}</span>
|
||||
</Flex>
|
||||
<Flex align="center" gap={4}>
|
||||
{child.dataType && <span className="rb:text-[#5B6167]">{child.dataType}</span>}
|
||||
{hasGrandChildren && <span className="rb:text-[#5B6167] rb:ml-1">›</span>}
|
||||
<span className="rb:font-medium">{child.label}</span>
|
||||
</Flex>
|
||||
<Space size={2}>
|
||||
{child.dataType && <span>{child.dataType}</span>}
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-19 17:00:26
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-08 10:12:27
|
||||
* @Last Modified time: 2026-04-13 10:44:17
|
||||
*/
|
||||
/**
|
||||
* useVariableList Hook
|
||||
@@ -414,7 +414,7 @@ export const useVariableList = (
|
||||
const pd = parentLoop.getData();
|
||||
const pid = pd.id;
|
||||
if (pd.type === 'loop') {
|
||||
(pd.cycle_vars || []).forEach((cv: any) => addVariable(list, keys, `${pid}_cycle_${cv.name}`, cv.name, cv.type || 'String', `${pid}.${cv.name}`, pd));
|
||||
(pd.cycle_vars || []).forEach((cv: any) => addVariable(list, keys, `${pid}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${pid}.${cv.name}`, pd));
|
||||
} else if (pd.type === 'iteration' && pd.config.input.defaultValue) {
|
||||
let itemType = 'object';
|
||||
const iv = list.find(v => `{{${v.value}}}` === pd.config.input.defaultValue);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:39:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-10 17:24:19
|
||||
* @Last Modified time: 2026-04-13 10:44:19
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
@@ -266,7 +266,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
dataType: cycleVar.type || 'string',
|
||||
value: `${parentNodeId}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
@@ -643,7 +643,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: contextKey,
|
||||
label: 'context',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
dataType: 'string',
|
||||
value: `context`,
|
||||
nodeData: selectedNode.getData(),
|
||||
isContext: true,
|
||||
@@ -791,7 +791,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
dataType: cycleVar.type || 'string',
|
||||
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
||||
nodeData: selectedNode.getData(),
|
||||
}));
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
}
|
||||
.properties :global(.select.ant-select-single.ant-select-sm.ant-select-borderless) {
|
||||
height: 28px;
|
||||
border: 1px solid #F6F6F6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.properties :global(.select.ant-select-single.ant-select-sm.ant-select-borderless.ant-select-focused) {
|
||||
border: 1px solid #171719;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td),
|
||||
@@ -157,4 +162,7 @@
|
||||
padding-inline-start: 0px;
|
||||
border-radius: 4px;
|
||||
margin-block: 0px;
|
||||
}
|
||||
.properties :global(.ant-input-number-affix-wrapper) {
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:06:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 19:56:56
|
||||
* @Last Modified time: 2026-04-16 17:52:30
|
||||
*/
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
@@ -101,6 +101,10 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
step: 1,
|
||||
defaultValue: 2000
|
||||
},
|
||||
json_output: {
|
||||
type: 'define',
|
||||
defaultValue: false
|
||||
},
|
||||
context: {
|
||||
type: 'variableList',
|
||||
placeholder: 'workflow.config.llm.contextPlaceholder'
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||
import { conditionNodeHeight, conditionNodeItemHeight, conditionNodePortItemArgsY, defaultAbsolutePortGroups, defaultPortItems, edgeAttrs, edgeHoverTool, edge_color, edge_selected_color, edge_width, graphNodeLibrary, nodeLibrary, nodeRegisterLibrary, nodeWidth, notesConfig, portAttrs, portItemArgsY, portMarkup, portTextAttrs, unknownNode } from '../constant';
|
||||
import type { ChatVariable, NodeProperties, WorkflowConfig } from '../types';
|
||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils';
|
||||
import { useWorkflowStore } from '@/store/workflow';
|
||||
|
||||
/**
|
||||
* Props for useWorkflowGraph hook
|
||||
@@ -103,6 +104,8 @@ export const useWorkflowGraph = ({
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation()
|
||||
const { user } = useUser();
|
||||
const { chatHistoryMap } = useWorkflowStore()
|
||||
const chatHistory = Object.values(chatHistoryMap).at(-1) ?? []
|
||||
|
||||
// Refs
|
||||
const graphRef = useRef<Graph>();
|
||||
@@ -122,6 +125,7 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.getNodes().forEach(node => {
|
||||
const data = node.getData()
|
||||
if (data?.type === 'if-else' || data?.type === 'question-classifier') {
|
||||
console.log('chatVariables', chatVariables)
|
||||
node.setData({ ...data, chatVariables }, { silent: true })
|
||||
}
|
||||
})
|
||||
@@ -214,7 +218,7 @@ export const useWorkflowGraph = ({
|
||||
? Object.entries(group_variables as Record<string, any>).map(([key, value]) => ({ key, value }))
|
||||
: group_variables
|
||||
} else if (type === 'http-request' && (key === 'headers' || key === 'params') && config[key] && typeof config[key] === 'object' && !Array.isArray(config[key]) && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([key, value]) => ({ key, value }))
|
||||
} else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||
try {
|
||||
nodeLibraryConfig.config[key].defaultValue = decodeURIComponent(atob(config[key] as string))
|
||||
@@ -1051,24 +1055,39 @@ export const useWorkflowGraph = ({
|
||||
|
||||
graphRef.current.on('node:removed', blankClick)
|
||||
// When edge connected, bring connected nodes' ports to front
|
||||
graphRef.current.on('edge:connected', ({ isNew }) => {
|
||||
// Bring edge to front first, then bring child nodes above edges
|
||||
// Parent (loop/iteration) nodes stay behind to avoid covering edges
|
||||
// Reset any port hover state left from dragging
|
||||
graphRef.current.on('edge:connected', ({ isNew, edge }) => {
|
||||
if (isNew) {
|
||||
graphRef.current?.getNodes().forEach(node => {
|
||||
if (!node.getData()?.cycle) node.toFront();
|
||||
});
|
||||
graphRef.current?.getEdges().forEach(edge => {
|
||||
const sourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
if (sourceCell?.getData()?.cycle || targetCell?.getData()?.cycle) {
|
||||
edge.toFront();
|
||||
}
|
||||
});
|
||||
graphRef.current?.getNodes().forEach(node => {
|
||||
if (node.getData()?.cycle) node.toFront();
|
||||
});
|
||||
const sourceCellId = edge.getSourceCellId()
|
||||
const targetCellId = edge.getTargetCellId()
|
||||
const sourceCell = graphRef.current?.getCellById(sourceCellId);
|
||||
const targetCell = graphRef.current?.getCellById(targetCellId);
|
||||
|
||||
sourceCell?.toFront();
|
||||
targetCell?.toFront()
|
||||
if (['loop', 'iteration'].includes(sourceCell?.getData()?.type)) {
|
||||
graphRef.current?.getEdges().forEach(edge => {
|
||||
const edgeSourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||
const edgeTargetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
if (edgeSourceCell?.getData()?.cycle === sourceCellId || edgeTargetCell?.getData()?.cycle === sourceCellId) {
|
||||
edge.toFront();
|
||||
}
|
||||
});
|
||||
graphRef.current?.getNodes().forEach(node => {
|
||||
if (node.getData()?.cycle === sourceCellId) node.toFront();
|
||||
});
|
||||
}
|
||||
if (['loop', 'iteration'].includes(targetCell?.getData()?.type)) {
|
||||
graphRef.current?.getEdges().forEach(edge => {
|
||||
const edgeSourceCell = graphRef.current?.getCellById(edge.getSourceCellId());
|
||||
const edgeTargetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
if (edgeSourceCell?.getData()?.cycle === targetCellId || edgeTargetCell?.getData()?.cycle === targetCellId) {
|
||||
edge.toFront();
|
||||
}
|
||||
});
|
||||
graphRef.current?.getNodes().forEach(node => {
|
||||
if (node.getData()?.cycle === targetCellId) node.toFront();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1216,9 +1235,6 @@ export const useWorkflowGraph = ({
|
||||
}) || [];
|
||||
const edges = graphRef.current?.getEdges() || []
|
||||
|
||||
|
||||
console.log('config', config)
|
||||
|
||||
const params = {
|
||||
...config,
|
||||
features: featuresRef.current,
|
||||
@@ -1275,9 +1291,17 @@ export const useWorkflowGraph = ({
|
||||
itemConfig[key] = {}
|
||||
if (value.length > 0) {
|
||||
value.forEach((vo: any) => {
|
||||
itemConfig[key][vo.name] = vo.value
|
||||
itemConfig[key][vo.key] = vo.value
|
||||
})
|
||||
}
|
||||
} else if (data.type === 'http-request' && key === 'body' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const value = data.config[key].defaultValue
|
||||
itemConfig[key] = value
|
||||
if (value.content_type === 'json' && value.data && value.data !== '') {
|
||||
itemConfig[key].data = value.data.replace(/\u00a0/g, ' ')
|
||||
} else {
|
||||
itemConfig[key].data = value.data
|
||||
}
|
||||
} else if (data.config[key] && 'defaultValue' in data.config[key] && key !== 'knowledge_retrieval') {
|
||||
itemConfig[key] = data.config[key].defaultValue
|
||||
} else if (key === 'knowledge_retrieval' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
@@ -1460,6 +1484,31 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!graphRef.current) return;
|
||||
const nodes = graphRef.current.getNodes();
|
||||
|
||||
const lastWithSub = [...chatHistory].reverse().find(item => item.subContent?.length);
|
||||
// Reset all node execution status first
|
||||
nodes.forEach(node => {
|
||||
const data = node.getData();
|
||||
if (typeof data.status === 'string') {
|
||||
node.setData({ ...data, executionStatus: undefined });
|
||||
}
|
||||
});
|
||||
if (!lastWithSub?.subContent) return;
|
||||
// Build a nodeId -> status map first
|
||||
const statusMap: Record<string, string> = {};
|
||||
lastWithSub.subContent.forEach(sub => {
|
||||
if (typeof sub.status === 'string') {
|
||||
statusMap[sub.node_id] = sub.status;
|
||||
const node = nodes.find(n => n.getData()?.id === sub.node_id);
|
||||
if (node) {
|
||||
node.setData({ ...node.getData(), executionStatus: sub.status });
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [chatHistory, graphRef.current]);
|
||||
|
||||
return {
|
||||
config,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-24 15:07:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-24 15:07:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 20:40:47
|
||||
*/
|
||||
|
||||
import { portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from './constant'
|
||||
@@ -22,11 +22,31 @@ import { portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from '
|
||||
* @param cases - Array of case objects, each containing an `expressions` array.
|
||||
* @returns The total pixel height for the condition node.
|
||||
*/
|
||||
export const isSubExprSet = (sub: any) => {
|
||||
if (!sub?.key) return false;
|
||||
if (['not_empty', 'empty'].includes(sub?.operator)) return true;
|
||||
return !!sub.value || typeof sub.value === 'boolean' || typeof sub.value === 'number';
|
||||
};
|
||||
|
||||
const getEffectiveExprCount = (expr: any): number => {
|
||||
const subs = expr?.sub_variable_condition?.conditions;
|
||||
if (subs?.length && subs.every(isSubExprSet)) return 1 + subs.length;
|
||||
if (subs?.length > 0) {
|
||||
return 2
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
export const calcConditionNodeTotalHeight = (cases: any[]) => {
|
||||
// Total number of expressions across all cases
|
||||
const exprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length || 0), 0);
|
||||
// Sum of expression counts only for cases that have more than one expression
|
||||
const hasMultiExprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length > 1 ? c?.expressions?.length : 0), 0);
|
||||
// Total number of effective expression rows (sub_variable_condition expand height when all set)
|
||||
const exprCount = cases.reduce((acc: number, c: any) =>
|
||||
acc + (c?.expressions?.reduce((s: number, e: any) => s + getEffectiveExprCount(e), 0) || 0), 0);
|
||||
// Sum of effective expression counts only for cases that have more than one expression
|
||||
const hasMultiExprCount = cases.reduce((acc: number, c: any) => {
|
||||
if (!c?.expressions?.length || c.expressions.length <= 1) return acc;
|
||||
const effectiveCount = c.expressions.reduce((s: number, e: any) => s + getEffectiveExprCount(e), 0);
|
||||
return acc + effectiveCount;
|
||||
}, 0);
|
||||
|
||||
return conditionNodeHeight + (cases.length - 1) * 26 + exprCount * 20 + hasMultiExprCount * 3;
|
||||
};
|
||||
@@ -68,17 +88,44 @@ export const getConditionNodeCasePortY = (cases: any[], caseIndex: number) => {
|
||||
let singleExprCount = 0;
|
||||
let multiExprCount = 0;
|
||||
let extraExprs = 0;
|
||||
let portItemArgsYNum = 0;
|
||||
|
||||
for (let i = 0; i < caseIndex; i++) {
|
||||
const notHasSub = cases[i]?.expressions?.filter((e: any) => !e?.sub_variable_condition?.conditions || e?.sub_variable_condition?.conditions.length <1).length
|
||||
const n = cases[i]?.expressions?.length || 0;
|
||||
y += portItemArgsY * (n + 1);
|
||||
if (n === 1) singleExprCount++;
|
||||
else if (n >= 2) {
|
||||
let casePortItemArgsYNum = n + 1;
|
||||
// Add extra y for expressions with all sub_variable_condition set
|
||||
cases[i]?.expressions?.forEach((expr: any) => {
|
||||
const subs = expr?.sub_variable_condition?.conditions;
|
||||
if (subs?.length && subs.every(isSubExprSet)) {
|
||||
casePortItemArgsYNum += subs.length;
|
||||
} else if (subs?.length) {
|
||||
casePortItemArgsYNum += 1
|
||||
}
|
||||
});
|
||||
portItemArgsYNum += casePortItemArgsYNum;
|
||||
if (n === 1 && !cases[i]?.expressions?.some((e: any) => e?.sub_variable_condition?.conditions?.length > 0)) {
|
||||
singleExprCount++
|
||||
} else if (n >= 2 || cases[i]?.expressions?.some((e: any) => e?.sub_variable_condition?.conditions?.length > 0)) {
|
||||
multiExprCount++;
|
||||
if (n > 2) extraExprs += n - 2;
|
||||
cases[i]?.expressions?.forEach((e: any) => {
|
||||
const subs = e?.sub_variable_condition?.conditions;
|
||||
if (subs?.length && subs.every(isSubExprSet) && subs.length > 1) {
|
||||
extraExprs += subs.length + 2;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('extraExprs notHasSub', notHasSub)
|
||||
if (notHasSub > 3) {
|
||||
extraExprs += n - 2 + notHasSub/4;
|
||||
} else {
|
||||
extraExprs += n - 2 + notHasSub/4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('singleExprCount', singleExprCount, 'multiExprCount', multiExprCount, 'extraExprs', extraExprs)
|
||||
y += portItemArgsY * portItemArgsYNum
|
||||
// Correction for single-expression cases (slightly shorter rendered height)
|
||||
if (singleExprCount > 0) y -= singleExprCount * 7 + 2;
|
||||
// Correction for multi-expression cases (compact logical operator row)
|
||||
|
||||