Merge branch 'release/v0.3.1' into develop
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-07 16:49:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 10:11:54
|
||||
* @Last Modified time: 2026-04-20 18:14:34
|
||||
*/
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { Select, Flex, Space } from 'antd';
|
||||
@@ -56,7 +56,7 @@ const ModelSelect: FC<ModelSelectProps> = ({ params, placeholder, fontClassName,
|
||||
|
||||
useEffect(() => {
|
||||
if (updateOptions) updateOptions([...options, ...initialData]);
|
||||
}, [options, initialData])
|
||||
}, [JSON.stringify(options), JSON.stringify(initialData)])
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 12:28:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 17:34:02
|
||||
* @Last Modified time: 2026-04-21 15:46:35
|
||||
*/
|
||||
|
||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import { Flex, Tooltip, Divider } from 'antd';
|
||||
import { Flex, Divider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -82,8 +82,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
||||
{/* 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;
|
||||
const value = detail?.quotas?.[key as keyof Subscription['quotas']];
|
||||
return (
|
||||
<UnitWrapper
|
||||
key={key}
|
||||
@@ -95,7 +94,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{detail?.package_plan?.tech_support && (
|
||||
{detail?.package_plan?.tech_support && detail?.package_plan?.[getKeyWithLanguage('tech_support')] && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
@@ -103,7 +102,7 @@ const SubscriptionDetailModal = forwardRef<SubscriptionDetailModalRef>((_props,
|
||||
theme_color={detail?.package_plan?.theme_color}
|
||||
/>
|
||||
)}
|
||||
{detail?.package_plan?.sla_compliance && (
|
||||
{detail?.package_plan?.sla_compliance && detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(detail?.package_plan?.[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
|
||||
@@ -18,61 +18,61 @@
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, type FC } from 'react';
|
||||
import { Menu as AntMenu, Layout, Flex, Divider } from 'antd';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu as AntMenu, Divider, Flex, Layout } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useRef, useState, type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
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 logo from '@/assets/images/logo.png';
|
||||
import { useI18n } from '@/store/locale';
|
||||
import { useMenu, type MenuItem } from '@/store/menu';
|
||||
import { useUser } from '@/store/user';
|
||||
import styles from './index.module.css';
|
||||
import SubscriptionDetailModal, { type SubscriptionDetailModalRef } from './SubscriptionDetailModal';
|
||||
import SwitchSpaceModal, { type SwitchSpaceModalRef } from './SwitchSpaceModal';
|
||||
|
||||
// Import SVG files
|
||||
// space
|
||||
import dashboardIcon from '@/assets/images/menuNew/dashboard.svg';
|
||||
import dashboardActiveIcon from '@/assets/images/menuNew/dashboard_active.svg';
|
||||
import applicationIcon from '@/assets/images/menuNew/application.svg';
|
||||
import applicationActiveIcon from '@/assets/images/menuNew/application_active.svg';
|
||||
import knowledgeIcon from '@/assets/images/menuNew/knowledge.svg';
|
||||
import knowledgeActiveIcon from '@/assets/images/menuNew/knowledge_active.svg';
|
||||
import memoryIcon from '@/assets/images/menuNew/memory.svg';
|
||||
import memoryActiveIcon from '@/assets/images/menuNew/memory_active.svg';
|
||||
import userMemoryIcon from '@/assets/images/menuNew/userMemory.svg';
|
||||
import userMemoryActiveIcon from '@/assets/images/menuNew/userMemory_active.svg';
|
||||
import memoryConversationIcon from '@/assets/images/menuNew/memoryConversation.svg';
|
||||
import memoryConversationActiveIcon from '@/assets/images/menuNew/memoryConversation_active.svg';
|
||||
import apiKeyIcon from '@/assets/images/menuNew/apiKey.svg';
|
||||
import apiKeyActiveIcon from '@/assets/images/menuNew/apiKey_active.svg';
|
||||
import applicationIcon from '@/assets/images/menuNew/application.svg';
|
||||
import applicationActiveIcon from '@/assets/images/menuNew/application_active.svg';
|
||||
import dashboardIcon from '@/assets/images/menuNew/dashboard.svg';
|
||||
import dashboardActiveIcon from '@/assets/images/menuNew/dashboard_active.svg';
|
||||
import knowledgeIcon from '@/assets/images/menuNew/knowledge.svg';
|
||||
import knowledgeActiveIcon from '@/assets/images/menuNew/knowledge_active.svg';
|
||||
import memberIcon from '@/assets/images/menuNew/member.svg';
|
||||
import memberActiveIcon from '@/assets/images/menuNew/member_active.svg';
|
||||
import ontologyIcon from '@/assets/images/menuNew/ontology.svg'
|
||||
import ontologyActiveIcon from '@/assets/images/menuNew/ontology_active.svg'
|
||||
import spaceConfigIcon from '@/assets/images/menuNew/spaceConfig.svg'
|
||||
import spaceConfigActiveIcon from '@/assets/images/menuNew/spaceConfig_active.svg'
|
||||
import promptIcon from '@/assets/images/menuNew/prompt.svg'
|
||||
import promptActiveIcon from '@/assets/images/menuNew/prompt_active.svg'
|
||||
import memoryIcon from '@/assets/images/menuNew/memory.svg';
|
||||
import memoryActiveIcon from '@/assets/images/menuNew/memory_active.svg';
|
||||
import memoryConversationIcon from '@/assets/images/menuNew/memoryConversation.svg';
|
||||
import memoryConversationActiveIcon from '@/assets/images/menuNew/memoryConversation_active.svg';
|
||||
import ontologyIcon from '@/assets/images/menuNew/ontology.svg';
|
||||
import ontologyActiveIcon from '@/assets/images/menuNew/ontology_active.svg';
|
||||
import promptIcon from '@/assets/images/menuNew/prompt.svg';
|
||||
import promptActiveIcon from '@/assets/images/menuNew/prompt_active.svg';
|
||||
import spaceConfigIcon from '@/assets/images/menuNew/spaceConfig.svg';
|
||||
import spaceConfigActiveIcon from '@/assets/images/menuNew/spaceConfig_active.svg';
|
||||
import userMemoryIcon from '@/assets/images/menuNew/userMemory.svg';
|
||||
import userMemoryActiveIcon from '@/assets/images/menuNew/userMemory_active.svg';
|
||||
|
||||
// manage
|
||||
import modelIcon from '@/assets/images/menuNew/model.svg';
|
||||
import modelActiveIcon from '@/assets/images/menuNew/model_active.svg';
|
||||
import pricingIcon from '@/assets/images/menuNew/pricing.svg';
|
||||
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';
|
||||
import spaceIcon from '@/assets/images/menuNew/space.svg';
|
||||
import spaceActiveIcon from '@/assets/images/menuNew/space_active.svg';
|
||||
import userIcon from '@/assets/images/menuNew/user.svg';
|
||||
import userActiveIcon from '@/assets/images/menuNew/user_active.svg';
|
||||
import toolIcon from '@/assets/images/menuNew/tool.svg';
|
||||
import toolActiveIcon from '@/assets/images/menuNew/tool_active.svg';
|
||||
import pricingIcon from '@/assets/images/menuNew/pricing.svg'
|
||||
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'
|
||||
import userIcon from '@/assets/images/menuNew/user.svg';
|
||||
import userActiveIcon from '@/assets/images/menuNew/user_active.svg';
|
||||
|
||||
export interface PackagePlan {
|
||||
id: string
|
||||
@@ -115,7 +115,7 @@ export interface Subscription {
|
||||
started_at: number | null
|
||||
expired_at: number | null
|
||||
status: string
|
||||
quota: SubscriptionQuota
|
||||
quotas: SubscriptionQuota
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
@@ -431,7 +431,7 @@ const Menu: FC<{
|
||||
<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:text-[13px] rb:font-[MiSans-Semibold] rb:font-semibold">{subscription.quotas?.[key as keyof typeof subscription.quotas] ?? t('package.noLimit')}</div>
|
||||
<div className="rb:mt-1 rb:text-[#5B6167] rb:text-[10px] rb:leading-3.5">{t(`index.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -451,6 +451,9 @@ export const en = {
|
||||
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
|
||||
publicApiCannotRefreshToken: 'Public API cannot refresh token',
|
||||
refreshTokenNotExist: 'Refresh token does not exist',
|
||||
SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: 'This is a system preset scene and cannot be deleted',
|
||||
SYSTEM_DEFAULT_CLASS_CANNOT_DELETE: 'This scene is a system preset scene and cannot be deleted',
|
||||
SYSTEM_DEFAULT_SCENE_CANNOT_UPDATE: 'This scene is a system preset scene and cannot be modified',
|
||||
reset: 'Reset',
|
||||
refresh: 'Refresh',
|
||||
return: 'Return',
|
||||
@@ -2543,6 +2546,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
checkListErrors: {
|
||||
'llm.model_id': 'Model',
|
||||
'llm.messages': 'Messages',
|
||||
'llm.vision_input': 'Vision Variable',
|
||||
'end.output': 'Output',
|
||||
'knowledge-retrieval.knowledge_retrieval': 'Knowledge bases',
|
||||
'parameter-extractor.model_id': 'Model',
|
||||
@@ -2571,6 +2575,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'document-extractor.file_selector': 'File variable',
|
||||
'list-operator.input_list': 'Input list',
|
||||
'output.outputs': 'Output Variable',
|
||||
'tool.tool_id': 'Tool',
|
||||
},
|
||||
checkListHasErrors: 'Please resolve all issues in the checklist before publishing',
|
||||
variableSelect: {
|
||||
@@ -3104,6 +3109,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
editPackage: 'Edit Package',
|
||||
|
||||
viewDetail: 'View full package details',
|
||||
noLimit: 'Infinite',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,13 +66,13 @@ export const zh = {
|
||||
goConfig: '去配置',
|
||||
},
|
||||
indexTour:{
|
||||
startTitle:'欢迎来到 Memory Bear 👋',
|
||||
startDescription:'不知道从哪里开始?不妨先去 Model Management 看看,先把模型准备好,后面的操作会更顺畅。👉 点击左侧 Model Management 开始吧。',
|
||||
stepOne: '这里是 Model Management',
|
||||
stepOneDescription: '你可以在这里查看和配置可用的模型,为后续应用做好准备。模型准备好后,下一步去 Space Management 创建空间并开始使用吧。👉 点击左侧 Space Management 继续。',
|
||||
stepTwo: '这里是 Space Management',
|
||||
stepTwoDescription: '你可以在这里创建和管理不同的空间,把模型和数据组织到具体的使用场景中。空间创建完成后,可以去 User Management 邀请成员、分配权限,一起协作使用。👉 点击左侧 User Management 继续。',
|
||||
stepThree: '这里是用户管理页',
|
||||
startTitle:'欢迎来到 记忆熊 👋',
|
||||
startDescription:'不知道从哪里开始?不妨先去 模型管理 看看,先把模型准备好,后面的操作会更顺畅。👉 点击左侧 模型管理 开始吧。',
|
||||
stepOne: '这里是 模型管理',
|
||||
stepOneDescription: '你可以在这里查看和配置可用的模型,为后续应用做好准备。模型准备好后,下一步去 空间管理 创建空间并开始使用吧。👉 点击左侧 空间管理 继续。',
|
||||
stepTwo: '这里是 空间管理',
|
||||
stepTwoDescription: '你可以在这里创建和管理不同的空间,把模型和数据组织到具体的使用场景中。空间创建完成后,可以去 用户管理 邀请成员、分配权限,一起协作使用。👉 点击左侧 用户管理 继续。',
|
||||
stepThree: '这里是 用户管理',
|
||||
stepThreeDescription: '你可以在这里创建用户、分配角色,并管理团队成员的访问权限。完成用户设置后,基础配置就准备好了,可以开始实际使用平台的各项功能了 🎉',
|
||||
finishButtonText: '开始使用',
|
||||
},
|
||||
@@ -1130,6 +1130,9 @@ export const zh = {
|
||||
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
|
||||
publicApiCannotRefreshToken: '公共接口不能刷新token',
|
||||
refreshTokenNotExist: '刷新token不存在',
|
||||
SYSTEM_DEFAULT_SCENE_CANNOT_DELETE: '该场景为系统预设场景,不允许删除',
|
||||
SYSTEM_DEFAULT_CLASS_CANNOT_DELETE: '该场景为系统预设场景,不允许删除',
|
||||
SYSTEM_DEFAULT_SCENE_CANNOT_UPDATE: '该场景为系统预设场景,不允许修改',
|
||||
reset: '重置',
|
||||
refresh: '刷新',
|
||||
return: '返回',
|
||||
@@ -2507,6 +2510,7 @@ export const zh = {
|
||||
checkListErrors: {
|
||||
'llm.model_id': '模型',
|
||||
'llm.messages': '提示词',
|
||||
'llm.vision_input': '视觉变量',
|
||||
'end.output': '回复',
|
||||
'knowledge-retrieval.knowledge_retrieval': '知识库',
|
||||
'parameter-extractor.model_id': '模型',
|
||||
@@ -2535,6 +2539,7 @@ export const zh = {
|
||||
'document-extractor.file_selector': '文件变量',
|
||||
'list-operator.input_list': '输入变量',
|
||||
'output.outputs': '输出变量',
|
||||
'tool.tool_id': '工具',
|
||||
},
|
||||
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
|
||||
variableSelect: {
|
||||
@@ -3068,6 +3073,7 @@ export const zh = {
|
||||
editPackage: '编辑套餐',
|
||||
|
||||
viewDetail: '查看完整套餐详情',
|
||||
noLimit: '无限',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 16:35:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-21 14:20:39
|
||||
* @Last Modified time: 2026-04-22 10:16:43
|
||||
*/
|
||||
/**
|
||||
* Server-Sent Events (SSE) Stream Utility Module
|
||||
@@ -16,11 +16,11 @@
|
||||
* @module stream
|
||||
*/
|
||||
|
||||
import { refreshToken } from '@/api/user';
|
||||
import i18n from '@/i18n';
|
||||
import { message } from 'antd';
|
||||
import i18n from '@/i18n'
|
||||
import { cookieUtils } from './request'
|
||||
import { refreshToken } from '@/api/user'
|
||||
import { clearAuthData } from './auth'
|
||||
import { clearAuthData } from './auth';
|
||||
import { cookieUtils } from './request';
|
||||
const API_PREFIX = '/api'
|
||||
|
||||
// Token refresh state
|
||||
@@ -181,12 +181,12 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
case 500:
|
||||
case 502:
|
||||
const errorData = await response.json();
|
||||
const errorInfo = errorData.error || i18n.t('common.serviceUpgrading');
|
||||
const errorInfo = errorData.error || errorData.msg || i18n.t('common.serviceUpgrading');
|
||||
message.warning(errorInfo);
|
||||
throw new Error(errorData);
|
||||
case 400:
|
||||
const error = await response.json();
|
||||
const error400 = error.error || 'Bad Request';
|
||||
const error400 = error.error || error.msg || 'Bad Request';
|
||||
message.warning(error400);
|
||||
throw new Error(error);
|
||||
case 403:
|
||||
@@ -195,7 +195,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
throw new Error(errors);
|
||||
case 504:
|
||||
const errorJson = await response.json();
|
||||
const errorMsg = errorJson.error || i18n.t('common.serverError');
|
||||
const errorMsg = errorJson.error || errorJson.msg || i18n.t('common.serverError');
|
||||
message.warning(errorMsg);
|
||||
throw new Error(errorJson);
|
||||
case 401:
|
||||
@@ -209,6 +209,13 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!response.ok) {
|
||||
const defaultData = await response.json().catch(() => ({}));
|
||||
const defaultMsg = defaultData.error || defaultData.msg;
|
||||
if (defaultMsg) message.warning(defaultMsg);
|
||||
throw new Error(defaultMsg || `HTTP ${response.status}`);
|
||||
}
|
||||
}
|
||||
if (!response.body) throw new Error('No response body');
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigF
|
||||
const { id } = useParams();
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<Config | null>(null);
|
||||
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
@@ -94,7 +93,6 @@ const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigF
|
||||
* Fetch agent configuration data
|
||||
*/
|
||||
const getData = () => {
|
||||
setLoading(true)
|
||||
getApplicationConfig(id as string).then(res => {
|
||||
const response = res as Config
|
||||
const { skills, variables } = response
|
||||
@@ -127,8 +125,6 @@ const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigF
|
||||
tools: allTools
|
||||
})
|
||||
onFeaturesLoad?.(response.features)
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -421,7 +417,6 @@ const Agent = forwardRef<AgentRef, { onFeaturesLoad?: (features: FeaturesConfigF
|
||||
console.log('agent values', values)
|
||||
return (
|
||||
<>
|
||||
{loading && <Spin fullscreen></Spin>}
|
||||
<Row className="rb:h-full!" gutter={12}>
|
||||
<Col span={12} className="rb:h-full!">
|
||||
<Form form={form}>
|
||||
|
||||
@@ -68,7 +68,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
const compareLoadingRef = useRef(false)
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
const [message, setMessage] = useState<string | undefined>(undefined)
|
||||
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
||||
@@ -76,7 +76,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const abortRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
setLoading(false)
|
||||
return () => {
|
||||
abortRef.current?.()
|
||||
@@ -259,7 +259,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const handleSend = (msg?: string) => {
|
||||
if (loading || !id) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
compareLoadingRef.current = true
|
||||
const files = (fileList || []).filter(item => !['uploading', 'error'].includes(item.status))
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
@@ -285,7 +285,7 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
if (!isCanSend) {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,20 +310,20 @@ const Chat: FC<ChatProps> = ({
|
||||
|
||||
switch (item.event) {
|
||||
case 'model_reasoning':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
updateAssistantReasoningMessage(content, model_config_id, conversation_id)
|
||||
break;
|
||||
case 'model_message':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
updateAssistantMessage(content, model_config_id, conversation_id, audio_url)
|
||||
break;
|
||||
case 'model_end':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
const idToPoll = `${model_config_id}_${audio_url}`
|
||||
if (audio_url && !audioStatusMap[idToPoll]) {
|
||||
@@ -365,8 +365,8 @@ const Chat: FC<ChatProps> = ({
|
||||
updateErrorAssistantMessage(message_length, model_config_id)
|
||||
break;
|
||||
case 'compare_end':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
setLoading(false);
|
||||
break;
|
||||
@@ -401,18 +401,18 @@ const Chat: FC<ChatProps> = ({
|
||||
}, handleStreamMessage, (abort) => { abortRef.current = abort })
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
updateClusterErrorAssistantMessage(0)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const handleClusterSend = (msg?: string) => {
|
||||
if (loading || !id) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
compareLoadingRef.current = true
|
||||
const files = (fileList || []).filter(item => !['uploading', 'error'].includes(item.status))
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
@@ -500,8 +500,8 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
break
|
||||
case 'message':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
updateClusterAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
@@ -509,14 +509,14 @@ const Chat: FC<ChatProps> = ({
|
||||
}
|
||||
break;
|
||||
case 'model_end':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
updateClusterErrorAssistantMessage(message_length)
|
||||
break;
|
||||
case 'compare_end':
|
||||
if (compareLoading) {
|
||||
setCompareLoading(false)
|
||||
if (compareLoadingRef.current) {
|
||||
compareLoadingRef.current = false
|
||||
}
|
||||
setLoading(false);
|
||||
break;
|
||||
@@ -547,18 +547,18 @@ const Chat: FC<ChatProps> = ({
|
||||
)
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
updateClusterErrorAssistantMessage(0)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
})
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
compareLoadingRef.current = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -628,7 +628,7 @@ const Chat: FC<ChatProps> = ({
|
||||
/>}
|
||||
onSend={isCluster ? handleClusterSend : handleSend}
|
||||
data={chat.list || []}
|
||||
streamLoading={compareLoading}
|
||||
streamLoading={compareLoadingRef.current}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label || t(`application.ai`)}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:25:32
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-04 10:34:43
|
||||
* @Last Modified time: 2026-04-21 13:34:52
|
||||
*/
|
||||
/**
|
||||
* Knowledge Base Component
|
||||
@@ -54,7 +54,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
||||
if (basesWithoutName.length > 0) {
|
||||
// Call API to get complete knowledge base information
|
||||
getKnowledgeBaseList().then(res => {
|
||||
getKnowledgeBaseList(undefined, { kb_ids: basesWithoutName.map(vo => vo.kb_id).join(',') }).then(res => {
|
||||
const fullBases = knowledge_bases.map(base => {
|
||||
if (!base.name) {
|
||||
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-28 14:08:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-13 18:17:32
|
||||
* @Last Modified time: 2026-04-20 16:52:32
|
||||
*/
|
||||
/**
|
||||
* UploadModal Component
|
||||
@@ -16,6 +16,7 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
|
||||
import { Form, Steps, Flex, Alert, Button, Result, message } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import type { Application, UploadModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
@@ -51,6 +52,7 @@ const UploadModal = forwardRef<UploadModalRef, UploadModalProps>(({
|
||||
id
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// State management
|
||||
const [visible, setVisible] = useState(false); // Modal visibility
|
||||
@@ -146,6 +148,10 @@ const UploadModal = forwardRef<UploadModalRef, UploadModalProps>(({
|
||||
window.open(`/#/application/config/${appId}`, '_blank');
|
||||
}
|
||||
break;
|
||||
case 'list':
|
||||
if (id) {
|
||||
navigate('/application')
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 17:09:03
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-31 12:21:56
|
||||
* @Last Modified time: 2026-04-20 16:59:25
|
||||
*/
|
||||
/**
|
||||
* Memory Conversation Page
|
||||
@@ -78,8 +78,8 @@ interface DataItem {
|
||||
id: string;
|
||||
question: string;
|
||||
type: string;
|
||||
reason: string;
|
||||
}
|
||||
reason?: string;
|
||||
}
|
||||
/**
|
||||
* Log item for conversation analysis
|
||||
*/
|
||||
@@ -88,13 +88,15 @@ export interface LogItem {
|
||||
title: string;
|
||||
data?: DataItem[] | AnyObject;
|
||||
raw_results?: string | Record<string, AnyObject>;
|
||||
raw_result?: Array<AnyObject>;
|
||||
summary?: string;
|
||||
query?: string;
|
||||
reason?: string;
|
||||
result?: string;
|
||||
original_query: string;
|
||||
original_query?: string;
|
||||
index?: number;
|
||||
result_count?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +244,6 @@ const MemoryConversation: FC = () => {
|
||||
<ContentWrapper key={vo.id}>
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{vo.id}. {vo.question}</div>
|
||||
<div className="rb:mt-2 rb:text-[#5B6167]">{vo.reason}</div>
|
||||
</>
|
||||
</ContentWrapper>
|
||||
))}
|
||||
@@ -260,25 +261,9 @@ const MemoryConversation: FC = () => {
|
||||
</ContentWrapper>
|
||||
))}
|
||||
</Flex>
|
||||
: log.type === 'search_result' && log.raw_results && typeof log.raw_results !== 'string'
|
||||
: log.type === 'search_result' && log.result
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
{(log.raw_results.reranked_results as AnyObject)?.communities?.length > 0 && <>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{t('memoryConversation.communities')}</div>
|
||||
<ul className='rb:mt-2 rb:text-[#5B6167] rb:list-disc rb:pl-4'>
|
||||
{((log.raw_results.reranked_results as AnyObject)?.communities as { content: string }[]).map((item, index: number) => (
|
||||
<li key={index}>{item.content}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>}
|
||||
{(log.raw_results.reranked_results as AnyObject)?.summaries?.length > 0 && <>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{t('memoryConversation.summaries')}</div>
|
||||
<ul className='rb:mt-2 rb:text-[#5B6167] rb:list-disc rb:pl-4'>
|
||||
{((log.raw_results.reranked_results as AnyObject)?.summaries as { content: string }[]).map((item, index: number) => (
|
||||
<li key={index}>{item.content}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>}
|
||||
<Markdown content={log.result} />
|
||||
</ContentWrapper>
|
||||
: log.type === 'retrieval_summary' && log.summary
|
||||
? <ContentWrapper>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 16:49:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 18:03:53
|
||||
* @Last Modified time: 2026-04-21 15:02:53
|
||||
*/
|
||||
/**
|
||||
* Custom Model Modal
|
||||
@@ -230,21 +230,23 @@ const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={["api_keys", 0, "api_key"]}
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
{!isEdit && <>
|
||||
<Form.Item
|
||||
name={["api_keys", 0, "api_key"]}
|
||||
label={t('modelNew.api_key')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={["api_keys", 0, "api_base"]}
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["api_keys", 0, "api_base"]}
|
||||
label={t('modelNew.api_base')}
|
||||
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
|
||||
>
|
||||
<Input placeholder="https://api.example.com/v1" />
|
||||
</Form.Item>
|
||||
</>}
|
||||
|
||||
{['llm', 'chat'].includes(modelType as string) &&
|
||||
<Row gutter={16}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 11:43:57
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-14 11:44:40
|
||||
* @Last Modified time: 2026-04-21 15:44:13
|
||||
*/
|
||||
export const billingUnits = [
|
||||
{
|
||||
@@ -42,7 +42,7 @@ export const billingUnits = [
|
||||
},
|
||||
{
|
||||
key: 'model_quota',
|
||||
unit: 'ops', placeholder: 'numberPlaceholder',
|
||||
unit: 'pcs', placeholder: 'numberPlaceholder',
|
||||
icon: 'model',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-14 11:34:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-16 17:23:49
|
||||
* @Last Modified time: 2026-04-21 15:45:30
|
||||
*/
|
||||
/**
|
||||
* Package Component
|
||||
@@ -60,7 +60,7 @@ const btnClassNames = {
|
||||
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; }) => {
|
||||
export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#171719' }: { titleKey: string; value?: number | string | null; icon: string; unit?: string; theme_color?: string; }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderFeatureIcon = (iconKey: string, color: string) => {
|
||||
@@ -78,7 +78,7 @@ export const UnitWrapper = ({ titleKey, value, icon, unit, theme_color = '#17171
|
||||
>{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>
|
||||
{value ? <div>{value} {unit ? t(`package.${unit}`) : ''}</div> : <div>{t('package.noLimit')}</div>}
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
@@ -252,7 +252,6 @@ const Package: FC = () => {
|
||||
>
|
||||
{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}
|
||||
@@ -264,7 +263,7 @@ const Package: FC = () => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{pkg.tech_support && (
|
||||
{pkg.tech_support && pkg[getKeyWithLanguage('tech_support')] && (
|
||||
<UnitWrapper
|
||||
titleKey="tech_support"
|
||||
value={String(pkg[getKeyWithLanguage('tech_support')] ?? '')}
|
||||
@@ -272,7 +271,7 @@ const Package: FC = () => {
|
||||
theme_color={pkg.theme_color}
|
||||
/>
|
||||
)}
|
||||
{pkg.sla_compliance && (
|
||||
{pkg.sla_compliance && pkg[getKeyWithLanguage('sla_compliance')] && (
|
||||
<UnitWrapper
|
||||
titleKey="sla"
|
||||
value={String(pkg[getKeyWithLanguage('sla_compliance')] ?? '')}
|
||||
|
||||
@@ -31,6 +31,13 @@ interface EndUserProfileProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const formatValue = (value: string | string[] | null | undefined) => {
|
||||
if (!value) return '-'
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? value.join(' | ') : '-'
|
||||
}
|
||||
return value
|
||||
}
|
||||
const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ className, onDataLoaded }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
@@ -89,19 +96,19 @@ 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?.join(' | ') || '-'}</div>
|
||||
<div className="rb:mt-0.5">{formatValue(data?.profile?.role)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.domain')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.domain?.join(' | ') || '-'}</div>
|
||||
<div className="rb:mt-0.5">{formatValue(data?.profile?.domain)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.expertise')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.expertise?.join(' | ') || '-'}</div>
|
||||
<div className="rb:mt-0.5">{formatValue(data?.profile?.expertise)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rb:text-[#7B8085]">{t('userMemory.interests')}</div>
|
||||
<div className="rb:mt-0.5">{data?.profile?.interests?.join(' | ') || '-'}</div>
|
||||
<div className="rb:mt-0.5">{formatValue(data?.profile?.interests)}</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#7B8085] rb:text-[12px] rb:leading-4.5">
|
||||
|
||||
@@ -178,7 +178,7 @@ export interface EndUser {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
profile: {
|
||||
role: string[];
|
||||
role: string[] | string;
|
||||
domain: string[];
|
||||
expertise: string[];
|
||||
interests: string[];
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-04-09 18:58:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-20 10:39:17
|
||||
*/
|
||||
import { useState, useCallback, useEffect, useRef, type FC } from 'react'
|
||||
import { Popover, Flex } from 'antd'
|
||||
import { WarningFilled } from '@ant-design/icons'
|
||||
@@ -49,7 +55,7 @@ const specialValidators: Record<string, (val: any) => boolean> = {
|
||||
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 !!expr.left && (expr?.sub_variable_condition || !!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number')
|
||||
}
|
||||
return val.some(c => !c?.expressions?.length || c.expressions.some((expr: any) => !isExprSet(expr)))
|
||||
},
|
||||
@@ -100,6 +106,18 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
|
||||
if (isInvalid) errors.push({ key: specialKey, message: '' })
|
||||
})
|
||||
|
||||
// llm: vision_input required when vision is enabled
|
||||
if (type === 'llm') {
|
||||
const vision = get('vision')
|
||||
if (vision === true || vision === 'true') {
|
||||
const visionInput = get('vision_input')
|
||||
console.log('vision', vision, isEmpty(visionInput))
|
||||
if (isEmpty(visionInput)) {
|
||||
errors.push({ key: 'llm.vision_input', message: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http-request body.data (binary) — not a top-level required field, check separately
|
||||
if (type === 'http-request') {
|
||||
const body = get('body')
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useVariableList } from '../Properties/hooks/useVariableList'
|
||||
import { isSubExprSet } from '../../utils'
|
||||
import { fileSubFieldOperators } from '../Properties/CaseList'
|
||||
|
||||
const caculateIsSet = (item: any, type: string) => {
|
||||
const calculateIsSet = (item: any, type: string) => {
|
||||
switch (type) {
|
||||
case 'categories':
|
||||
return typeof item?.class_name === 'string' && item?.class_name !== ''
|
||||
@@ -90,7 +90,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
<div key={index} 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 justify="space-between">
|
||||
<span>{t('workflow.config.question-classifier.class_name')} {index + 1}</span>
|
||||
{caculateIsSet(item, 'categories') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
|
||||
{calculateIsSet(item, 'categories') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
@@ -100,17 +100,24 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
<Flex vertical gap={4} className="rb:mt-3!">
|
||||
{data.config?.cases?.defaultValue.map((item: any, index: number) => (
|
||||
<div key={index} className={item.expressions.length > 0 ? '' : 'rb:mb-1'}>
|
||||
<Flex justify={item.expressions.length > 0 ? "space-between" : 'end'} className="rb:mb-1">
|
||||
{item.expressions.length > 0 && <span className="rb:text-[#5B6167] rb:text-[10px]">CASE{index + 1}</span>}
|
||||
<Flex justify={item.expressions.length > 0 ? "space-between" : 'end'} className="rb:mb-1! rb:leading-4">
|
||||
{item.expressions.length > 0 && <span className="rb:text-[#5B6167] rb:text-[10px] rb:pl-1">CASE{index + 1}</span>}
|
||||
<span className="rb:text-[#212332] rb:font-medium rb:text-[12px]">{index === 0 ? 'IF' : `ELIF`}</span>
|
||||
</Flex>
|
||||
{item.expressions.length > 0 && <Flex vertical gap={2}>
|
||||
{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 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">
|
||||
{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 vertical gap={2}
|
||||
className={clsx("rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-4", {
|
||||
'rb:pt-1!': expression.sub_variable_condition?.conditions?.length > 0,
|
||||
'rb:py-1!': !expression.sub_variable_condition?.conditions || !expression.sub_variable_condition?.conditions?.length
|
||||
})}
|
||||
>
|
||||
<Flex align="center">
|
||||
{caculateIsSet(expression, 'cases')
|
||||
{calculateIsSet(expression, 'cases')
|
||||
? <>
|
||||
{labelRender(expression.left)}
|
||||
<span className="rb:mx-1">{getLocaleField(expression.operator, typeof expression.right)}</span>
|
||||
@@ -120,11 +127,16 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
}
|
||||
</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">
|
||||
? <div className="rb-border-l rb:ml-2 rb:mt-1">
|
||||
{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">
|
||||
<Flex align="center"
|
||||
className={clsx("rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5", {
|
||||
'rb:py-1!': sIndex !== 0,
|
||||
'rb:pb-1': sIndex === 0
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
@@ -140,7 +152,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
))}
|
||||
</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">
|
||||
? <Flex align="center" className="rb:pl-2! rb:rounded-md rb:pb-1! rb:px-1.5! rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-4">
|
||||
{t(`workflow.config.${data.type}.unset`)}
|
||||
</Flex>
|
||||
: null
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:24:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 20:47:49
|
||||
* @Last Modified time: 2026-04-20 10:46:05
|
||||
*/
|
||||
import { useEffect, useMemo, type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -39,7 +39,7 @@ interface Expression {
|
||||
sub_variable_condition?: SubVariableCondition;
|
||||
}
|
||||
|
||||
interface CaseItem {
|
||||
export interface CaseItem {
|
||||
logical_operator: 'and' | 'or';
|
||||
expressions: Expression[];
|
||||
}
|
||||
@@ -274,7 +274,9 @@ const ArrayFileSubConditions: FC<ArrayFileSubConditionsProps> = ({ conditionFiel
|
||||
className="rb:w-full!"
|
||||
suffix="Byte"
|
||||
size="small"
|
||||
onChange={(value) => { form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value); }}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex, 'value'], value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
@@ -483,13 +485,24 @@ const CaseList: FC<CaseListProps> = ({
|
||||
form.setFieldValue([name, index, 'logical_operator'], currentValue === 'and' ? 'or' : 'and');
|
||||
};
|
||||
|
||||
const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string) => {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: 'constant'
|
||||
});
|
||||
const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string, option?: Suggestion | undefined) => {
|
||||
if (option?.dataType === 'array[file]') {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
sub_variable_condition: {
|
||||
conditions: [],
|
||||
logical_operator: 'and'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: 'constant'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCase = (addCaseFunc: Function) => {
|
||||
@@ -590,7 +603,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val as string)}
|
||||
onChange={(val, option) => handleLeftFieldChange(caseIndex, conditionIndex, val as string, option as unknown as Suggestion)}
|
||||
variant="borderless"
|
||||
className="rb:w-36!"
|
||||
/>
|
||||
|
||||
@@ -29,12 +29,13 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
if (value && JSON.stringify(value) !== JSON.stringify(editConfig)) {
|
||||
setEditConfig({ ...(value || {}) })
|
||||
const knowledge_bases = [...(value.knowledge_bases || [])]
|
||||
setKnowledgeList(knowledge_bases)
|
||||
|
||||
// 检查是否有knowledge_bases缺少name字段
|
||||
const basesWithoutName = knowledge_bases.filter(base => !base.name)
|
||||
if (basesWithoutName.length > 0) {
|
||||
// 调用接口获取完整的知识库信息
|
||||
getKnowledgeBaseList().then(res => {
|
||||
getKnowledgeBaseList(undefined, { kb_ids: basesWithoutName.map(vo => vo.kb_id).join(',') }).then(res => {
|
||||
const fullBases = knowledge_bases.map(base => {
|
||||
if (!base.name) {
|
||||
const fullBase = res.items.find((item: any) => item.id === base.kb_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -45,15 +45,15 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
getToolDetail(values.tool_id)
|
||||
.then(res => {
|
||||
const detail = res as { tool_type: ToolType; }
|
||||
|
||||
|
||||
getTools({ tool_type: detail.tool_type })
|
||||
.then(toolsRes => {
|
||||
const tools = toolsRes as ToolItem[]
|
||||
|
||||
|
||||
getToolMethods(values.tool_id)
|
||||
.then(methodsRes => {
|
||||
const response = methodsRes as Array<{ method_id: string; name: string; parameters: Parameter[] }>
|
||||
|
||||
|
||||
setOptionList(prevList => {
|
||||
return prevList.map(item => {
|
||||
if (item.value === detail.tool_type) {
|
||||
@@ -76,7 +76,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
if (response.length > 1) {
|
||||
const filterTarget = response.find(vo => vo.name === values.tool_parameters?.operation)
|
||||
if (filterTarget) {
|
||||
@@ -98,7 +98,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
useEffect(() => {
|
||||
if (values.tools && values.tools.length === 3) {
|
||||
const [toolType, toolId, operation] = values.tools
|
||||
|
||||
|
||||
// 从 optionList 中查找对应的参数
|
||||
const typeOption = optionList.find(opt => opt.value === toolType)
|
||||
if (typeOption?.children) {
|
||||
@@ -147,21 +147,26 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
};
|
||||
|
||||
const handleChange: CascaderProps<Option>['onChange'] = (value, selectedOptions) => {
|
||||
if (!value) {
|
||||
setParameters([])
|
||||
form.resetFields()
|
||||
return
|
||||
}
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
const curParameters = [...(targetOption.parameters ?? [])]
|
||||
setParameters([...curParameters])
|
||||
const inititalValue: any = { tool_id: selectedOptions[1].value, tool_parameters: {} }
|
||||
const initialValue: any = { tool_id: selectedOptions[1].value, tool_parameters: { operation: undefined } }
|
||||
|
||||
if (value[0] === 'mcp' || (value[0] === 'builtin' && selectedOptions[1]?.children && selectedOptions[1].children.length > 1)) {
|
||||
inititalValue.tool_parameters.operation = value?.[2]
|
||||
initialValue.tool_parameters.operation = value?.[2]
|
||||
} else if (value[0] === 'custom') {
|
||||
inititalValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||
initialValue.tool_parameters.operation = selectedOptions?.[2].method_id
|
||||
}
|
||||
curParameters.forEach(vo => {
|
||||
inititalValue.tool_parameters[vo.name] = vo.default
|
||||
initialValue.tool_parameters[vo.name] = vo.default
|
||||
})
|
||||
|
||||
form.setFieldsValue(inititalValue)
|
||||
form.setFieldsValue(initialValue)
|
||||
}
|
||||
|
||||
// string -> string
|
||||
@@ -209,9 +214,9 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
name="tools"
|
||||
label={t('workflow.config.tool.tool_id')}
|
||||
>
|
||||
<Cascader
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={optionList}
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
changeOnSelect={false}
|
||||
@@ -239,8 +244,8 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch size="small" />
|
||||
: <Editor
|
||||
? <Switch size="small" />
|
||||
: <Editor
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
|
||||
@@ -393,18 +393,19 @@ export const useVariableList = (
|
||||
// Add chat variables
|
||||
chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' }));
|
||||
|
||||
// Process each relevant node: non-list-operator first, then list-operator
|
||||
const listOperatorIds: string[] = [];
|
||||
// Process each relevant node: deferred types last (they depend on prior variables)
|
||||
const deferredIds: string[] = [];
|
||||
relevantIds.forEach(id => {
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (!node) return;
|
||||
if (node.getData()?.type === 'list-operator') {
|
||||
listOperatorIds.push(id);
|
||||
const t = node.getData()?.type;
|
||||
if (['var-aggregator', 'list-operator', 'iteration'].includes(t)) {
|
||||
deferredIds.push(id);
|
||||
} else {
|
||||
processNodeVariables(node.getData(), node.getData().id, list, keys);
|
||||
}
|
||||
});
|
||||
listOperatorIds.forEach(id => {
|
||||
deferredIds.forEach(id => {
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (node) processNodeVariables(node.getData(), node.getData().id, list, keys);
|
||||
});
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-21 18:23:31
|
||||
*/
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import type { GroupMetadata, PortMetadata } from '@antv/x6/lib/model/port';
|
||||
import AddNode from './components/Nodes/AddNode';
|
||||
import ConditionNode from './components/Nodes/ConditionNode';
|
||||
import GroupStartNode from './components/Nodes/GroupStartNode';
|
||||
import AddNode from './components/Nodes/AddNode'
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
import NoteNode from './components/Nodes/NoteNode';
|
||||
import type { PortMetadata, GroupMetadata } from '@antv/x6/lib/model/port';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
import { memoryConfigListUrl } from '@/api/memory'
|
||||
import type { NodeLibrary } from './types'
|
||||
import { memoryConfigListUrl } from '@/api/memory';
|
||||
import type { NodeLibrary } from './types';
|
||||
|
||||
/**
|
||||
* Workflow node library configuration
|
||||
@@ -143,7 +143,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
vision_input: {
|
||||
type: 'variableList',
|
||||
onFilterVariableType: ['array[file]']
|
||||
onFilterVariableType: ['array[file]', 'file']
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -437,7 +437,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "tool", icon: 'rb:bg-[url("@/assets/images/workflow/tools.svg")]',
|
||||
config: {
|
||||
tool_id: {
|
||||
type: 'cascader'
|
||||
type: 'cascader',
|
||||
required: true
|
||||
},
|
||||
tool_parameters: {
|
||||
type: 'define'
|
||||
@@ -743,7 +744,7 @@ export const portTextAttrs = { fontSize: 12, fill: '#5B6167' }
|
||||
/**
|
||||
* Port position arguments
|
||||
*/
|
||||
export const portItemArgsY = 26.5;
|
||||
export const portItemArgsY = 27.5;
|
||||
export const portArgs = { x: nodeWidth, y: portItemArgsY }
|
||||
|
||||
const defaultPortGroup = {
|
||||
|
||||
@@ -465,11 +465,11 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||
}
|
||||
|
||||
graphRef.current.centerContent()
|
||||
// Initialize after completion, display nodes in visible area
|
||||
if (nodes.length > 0 || edges.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (graphRef.current) {
|
||||
graphRef.current.centerContent()
|
||||
graphRef.current.getNodes().forEach(node => {
|
||||
if (!node.getData()?.cycle) node.toFront();
|
||||
});
|
||||
|
||||
@@ -2,136 +2,70 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-03-24 15:07:49
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-17 20:40:47
|
||||
* @Last Modified time: 2026-04-20 14:20:34
|
||||
*/
|
||||
|
||||
import { portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from './constant'
|
||||
import { conditionNodePortItemArgsY, conditionNodeHeight } from './constant'
|
||||
|
||||
/**
|
||||
* Calculate the total height of a condition (if-else) node based on its cases.
|
||||
*
|
||||
* The height is composed of:
|
||||
* - `conditionNodeHeight`: the base height of the node (header + padding).
|
||||
* - `(cases.length - 1) * 26`: vertical spacing added for each additional case
|
||||
* beyond the first (each case separator row is 26px).
|
||||
* - `exprCount * 20`: each individual expression row occupies 20px.
|
||||
* - `hasMultiExprCount * 3`: a small extra padding (3px per expression) is added
|
||||
* for cases that contain more than one expression, to account for the logical
|
||||
* operator indicator (AND/OR) between expressions.
|
||||
*
|
||||
* @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;
|
||||
/**
|
||||
* Calculate the total height of a condition (if-else) node based on its cases.
|
||||
* Uses the same per-expression height logic as getConditionNodeCasePortY.
|
||||
*/
|
||||
export const calcConditionNodeTotalHeight = (cases: any[]) => {
|
||||
const casesHeight = cases.reduce((acc: number, c: any) => {
|
||||
const exprs = c?.expressions ?? [];
|
||||
const n = exprs.length;
|
||||
const exprsHeight = n === 0 ? 0 : exprs.reduce((s: number, e: any) => s + calcExpressionHeight(e), 0) + 2 * (n - 1);
|
||||
return acc + 20 + exprsHeight;
|
||||
}, 0);
|
||||
return conditionNodeHeight + casesHeight + (cases.length - 1) * 4 - 27.5;
|
||||
};
|
||||
|
||||
export const calcConditionNodeTotalHeight = (cases: any[]) => {
|
||||
// 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;
|
||||
/**
|
||||
* Height of a single expression block in ConditionNode (px).
|
||||
*
|
||||
* expression outer Flex padding:
|
||||
* - has sub conditions (length > 0): pt-1 (4px top only)
|
||||
* - no sub conditions: py-1 (4px top + 4px bottom)
|
||||
* expression main row: leading-4 = 16px
|
||||
* sub_variable_condition block (mt-1 = 4px gap):
|
||||
* - all isSet, m subs: sub[0] = leading-3.5(14) + pb-1(4) = 18px;
|
||||
* sub[k>0] = py-1(8) + leading-3.5(14) = 22px
|
||||
* total = 18 + 22*(m-1)
|
||||
* - exists but not all isSet: pb-1(4) + leading-4(16) = 20px
|
||||
*/
|
||||
const calcExpressionHeight = (expression: any): number => {
|
||||
const subs = expression?.sub_variable_condition?.conditions;
|
||||
if (!subs?.length) return 24; // py-1(8) + leading-4(16)
|
||||
const subBlockHeight = subs.every(isSubExprSet)
|
||||
? 18 + 22 * (subs.length - 1)
|
||||
: 20;
|
||||
return 4 + 16 + 4 + subBlockHeight - 2; // pt-1 + main row + mt-1 + sub block (-2 rendering correction)
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the Y-coordinate of the right-side output port for a specific case
|
||||
* in a condition (if-else) node.
|
||||
* in a condition (if-else) node, aligned with the IF/ELIF label in ConditionNode.
|
||||
*
|
||||
* The port position is determined by iterating through all preceding cases
|
||||
* (index 0 to caseIndex - 1) and accumulating their visual heights. Several
|
||||
* pixel-level corrections are applied to align ports with the rendered UI:
|
||||
*
|
||||
* 1. **Base offset**: starts at `conditionNodePortItemArgsY`, which is the Y
|
||||
* position of the first case port relative to the node top.
|
||||
*
|
||||
* 2. **Per-case accumulation**: for each preceding case with `n` expressions,
|
||||
* add `portItemArgsY * (n + 1)` — this accounts for `n` expression rows
|
||||
* plus one case header/separator row.
|
||||
*
|
||||
* 3. **Single-expression correction**: cases with exactly 1 expression render
|
||||
* slightly shorter than the generic formula predicts. Subtract
|
||||
* `singleExprCount * 7 + 2` to compensate for the reduced row height when
|
||||
* no logical operator row is shown.
|
||||
*
|
||||
* 4. **Multi-expression correction**: cases with 2+ expressions have a compact
|
||||
* logical operator row. Subtract `multiExprCount * 9` to offset the
|
||||
* over-estimated spacing.
|
||||
*
|
||||
* 5. **Extra expression correction**: for cases with more than 2 expressions,
|
||||
* each additional expression beyond the second introduces a minor spacing
|
||||
* discrepancy. Subtract `(extraExprs + 1) * 2` to fine-tune alignment.
|
||||
*
|
||||
* @param cases - Array of case objects, each containing an `expressions` array.
|
||||
* @param caseIndex - The zero-based index of the target case whose port Y is needed.
|
||||
* @returns The Y-coordinate (in pixels) for the output port of the given case.
|
||||
* Layout (from node top):
|
||||
* - 12px padding-top + 24px header + 12px mt-3 = 48px to cases area
|
||||
* - Each IF/ELIF label row: leading-4 (16px), center at +8px → first port Y = 56.5
|
||||
* - Each case: IF/ELIF row (leading-4=16) + mb-1(4) + expressions (gap={2}=2px between)
|
||||
* - Gap between cases (Flex gap={4}): 4px
|
||||
*/
|
||||
export const getConditionNodeCasePortY = (cases: any[], caseIndex: number) => {
|
||||
let y = conditionNodePortItemArgsY;
|
||||
let singleExprCount = 0;
|
||||
let multiExprCount = 0;
|
||||
let extraExprs = 0;
|
||||
let portItemArgsYNum = 0;
|
||||
|
||||
let y = conditionNodePortItemArgsY; // 56.5, center of first IF label
|
||||
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;
|
||||
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++;
|
||||
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
|
||||
}
|
||||
}
|
||||
const exprs = cases[i]?.expressions ?? [];
|
||||
const n = exprs.length;
|
||||
// IF/ELIF row (16) + mb-1 (4) = 20px base; expressions: sum of heights + 2px gap between
|
||||
const exprsHeight = n === 0 ? 0 : exprs.reduce((acc: number, e: any) => acc + calcExpressionHeight(e), 0) + 2 * (n - 1);
|
||||
y += 20 + exprsHeight + 4; // case height + Flex gap between cases
|
||||
}
|
||||
|
||||
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)
|
||||
y -= multiExprCount * 9;
|
||||
// Correction for cases with more than 2 expressions (minor spacing drift)
|
||||
if (extraExprs > 0) y -= (extraExprs + 1) * 2;
|
||||
|
||||
return y;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user