Merge branch 'release/v0.3.0' into develop
* release/v0.3.0: (44 commits) Revert "fix(web): prompt editor" fix(web): prompt editor fix(prompt-optimizer): handle escaped quotes in JSON parsing fix(custom-tools): remove parameter coercion in custom tool base class fix(core): conditionally apply thinking parameters based on model support refactor(custom-tools): coerce query and request body parameters to schema types fix(prompt-optimizer): support list content type in prompt optimizer refactor(memory): unify user placeholder names and harden alias sync logic fix(rag): replace semicolon separators with newlines in Excel parser output fix(web): Compatible with Windows whitespace fix(memory): make PgSQL the single source of truth for user entity aliases refactor(rag): simplify Excel parsing logic and remove redundant chunk_token_num assignment fix(web): Hide error message when workflow node error message equals empty string ci(wechat-notify): add Sourcery summary extraction with Qwen fallback fix(http-request,embedding,naive): tighten form-data validation, reduce truncation length to 8000, and disable chunking for Excel fix(web): adjust the value of End User Name fix(http-request): support array and file variables in form-data files upload fix(web): change http body key name fix(web): header user name fix(web): calculate using the filtered breadcrumbs length ... # Conflicts: # web/src/views/UserMemoryDetail/Neo4j.tsx # web/src/views/UserMemoryDetail/components/EndUserProfile.tsx # web/src/views/UserMemoryDetail/types.ts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -77,7 +77,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
{
|
||||
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]}
|
||||
{/[\u4e00-\u9fa5]/.test(user.username) ? user.username.slice(-2) : user.username[0]}
|
||||
</Flex>,
|
||||
label: (<>
|
||||
<div className="rb:text-[#212332] rb:leading-5">{user.username}</div>
|
||||
@@ -135,28 +135,30 @@ 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;
|
||||
});
|
||||
}
|
||||
@@ -180,7 +182,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
>
|
||||
<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]}
|
||||
{/[\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')]", {
|
||||
|
||||
@@ -116,7 +116,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',
|
||||
@@ -2559,6 +2559,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',
|
||||
|
||||
@@ -116,7 +116,7 @@ export const zh = {
|
||||
prompt: '提示词工程',
|
||||
skills: '技能库',
|
||||
workbench: '工作台',
|
||||
memoryRelated: '记忆相关',
|
||||
memoryRelated: '记忆中枢',
|
||||
advancedSettings: '高级设置',
|
||||
promptHistory: '我的历史',
|
||||
platformManagement: '平台管理',
|
||||
@@ -2523,6 +2523,9 @@ export const zh = {
|
||||
'list-operator.input_list': '输入变量',
|
||||
},
|
||||
checkListHasErrors: '发布前确认检查清单中所有问题均已解决',
|
||||
variableSelect: {
|
||||
empty: '暂无变量',
|
||||
},
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
|
||||
@@ -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 18:33:30
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-10 18:40:52
|
||||
* @Last Modified time: 2026-04-14 16:03:41
|
||||
*/
|
||||
/**
|
||||
* End User Profile Component
|
||||
@@ -28,11 +28,11 @@ 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 +52,7 @@ const EndUserProfile = forwardRef<EndUserProfileRef, EndUserProfileProps>(({ cla
|
||||
const userData = res as EndUser
|
||||
setData(userData)
|
||||
setLoading(false)
|
||||
onDataLoaded?.(userData as EndUser)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
|
||||
@@ -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-14 16:03:16
|
||||
*/
|
||||
/**
|
||||
* User Memory Detail Types
|
||||
@@ -172,6 +172,7 @@ 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;
|
||||
|
||||
@@ -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-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)}
|
||||
|
||||
@@ -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,62 +112,6 @@ 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)
|
||||
@@ -222,7 +164,8 @@ 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 }> }>
|
||||
const operation = toolParameters?.operation
|
||||
@@ -251,21 +194,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])
|
||||
|
||||
@@ -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,13 @@
|
||||
* @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 { 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'
|
||||
@@ -284,23 +285,24 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
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)]"
|
||||
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:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
|
||||
<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;
|
||||
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>}
|
||||
<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;
|
||||
@@ -310,14 +312,13 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
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!"
|
||||
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]': selectedIndex === globalIndex || isExpanded,
|
||||
'rb:cursor-not-allowed rb:opacity-65': option.disabled,
|
||||
'rb:cursor-pointer': !option.disabled,
|
||||
})}
|
||||
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);
|
||||
@@ -337,17 +338,19 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{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>}
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
@@ -356,7 +359,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
{/* Child variables panel - floats to the left */}
|
||||
{expandedParent?.children?.length && (
|
||||
<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)]"
|
||||
className="rb:absolute rb:min-w-70 rb:max-h-57.5 rb:overflow-y-auto rb:text-[12px] 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: childPanelTop,
|
||||
right: 'calc(100% + 8px)',
|
||||
@@ -364,9 +367,8 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
}}
|
||||
onMouseEnter={() => setExpandedParent(expandedParent)}
|
||||
>
|
||||
{/* 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>
|
||||
@@ -377,19 +379,20 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
<Flex
|
||||
key={child.key}
|
||||
data-selected={selectedIndex === childIndex}
|
||||
className="rb:px-3! rb:py-2!"
|
||||
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]': selectedIndex === childIndex,
|
||||
'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)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -31,6 +31,8 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
};
|
||||
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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -329,7 +329,9 @@ 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 operatorList = leftFieldType && operatorsObj[leftFieldType]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-13 11:25:40
|
||||
*/
|
||||
import { useState, useRef, useEffect, useLayoutEffect, type FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@@ -190,20 +190,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-[#4096ff] rb:shadow-[0_0_0_2px_rgba(5,145,255,0.1)]': 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 +224,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,17 +238,19 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</Flex>
|
||||
) : (
|
||||
<span className="rb:text-[#bfbfbf] rb:flex-1 rb:text-[12px]">{placeholder}</span>
|
||||
<span className="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>
|
||||
@@ -266,18 +278,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 +301,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,
|
||||
'rb:cursor-not-allowed rb:opacity-65': s.disabled,
|
||||
'rb:cursor-pointer': !s.disabled,
|
||||
})}
|
||||
align="center"
|
||||
@@ -314,16 +325,15 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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 +344,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,18 +356,13 @@ 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)}
|
||||
>
|
||||
<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>
|
||||
@@ -365,32 +370,27 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
const isSelected = multiple
|
||||
? selectedValues.includes(`{{${child.value}}}`)
|
||||
: `{{${child.value}}}` === value;
|
||||
const hasGrandChildren = !!child.children?.length;
|
||||
return (
|
||||
<Flex
|
||||
key={child.key}
|
||||
className={clsx("rb:px-3! rb:py-2! rb:hover:bg-[#f0f8ff]!", {
|
||||
'rb:bg-[#f0f8ff]': isSelected,
|
||||
'rb:white': !isSelected
|
||||
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,
|
||||
'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(),
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:48
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-04-07 23:17:50
|
||||
* @Last Modified time: 2026-04-14 17:43:14
|
||||
*/
|
||||
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6';
|
||||
import { register } from '@antv/x6-react-shape';
|
||||
@@ -111,6 +111,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 })
|
||||
}
|
||||
})
|
||||
@@ -203,7 +204,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))
|
||||
@@ -1022,24 +1023,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();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1184,9 +1200,6 @@ export const useWorkflowGraph = ({
|
||||
}) || [];
|
||||
const edges = graphRef.current?.getEdges() || []
|
||||
|
||||
|
||||
console.log('config', config)
|
||||
|
||||
const params = {
|
||||
...config,
|
||||
features: featuresRef.current,
|
||||
@@ -1243,9 +1256,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]) {
|
||||
|
||||
Reference in New Issue
Block a user