diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index b2ed5707..14d8d817 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -752,6 +752,14 @@ export const en = { quicklyForget: 'Quickly forget', slowForgetting: 'Slow forgetting', currentConfig: 'Current Config', + + decay_constant: 'Decay Constant', + max_history_length: 'Max Access History Length', + forgetting_threshold: 'Forgetting Threshold', + min_days_since_access: 'Minimum Days Since Access', + enable_llm_summary: 'Enable LLM Summary Generation', + max_merge_batch_size: 'Max Merge Batch Size', + forgetting_interval_hours: 'Forgetting Interval Hours' }, application: { searchPlaceholder: 'Search for applications or clusters', @@ -905,8 +913,6 @@ export const en = { toolCalling: 'Tool Calling', toolCallingDesc: 'The main control agent calls sub agents as tools', toolCallingFeature: 'Centralized control, suitable for structured workflow', - handoffs: 'Handoffs', - handoffsDesc: 'Agents between dynamic transfer of control rights', handoffsFeature: 'Decentralized control, suitable for complex conversation scenarios', recommend: 'Recommend', advanced: 'Advanced', @@ -1027,6 +1033,21 @@ export const en = { promptChatPlaceholder: 'Describe the prompt you need, e.g.: I need a customer service assistant', promptChatEmpty: 'No conversation content available', promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.', + + master: 'Supervisor Mode', + master_agent: 'Supervisor Mode', + master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', + handoffs: 'Collaboration Mode', + handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', + masterConfig: 'Supervisor Configuration', + orchestrationMode: 'Task Assignment Strategy', + conditional: 'Intelligent Assignment', + sequential: 'Sequential Assignment', + parallel: 'Parallel Assignment', + aggregationStrategy: 'Result Aggregation Method', + merge: 'Complete Aggregation', + vote: 'Key Information Extraction', + priority: 'Structured Integration', }, userMemory: { userMemory: 'User Memory', @@ -1382,15 +1403,15 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re last_health_check: 'Last Connection', responseTime: 'Response Time', status: { - available: '可用', - unconfigured: '未配置', - configured_disabled: '已配置未启用', - error: '链接异常' + available: 'Available', + unconfigured: 'Unconfigured', + configured_disabled: 'Configured but Disabled', + error: 'Connection Error' }, - available_desc: 'API 已配置并启用', - unconfigured_desc: '需要配置 API Key', - configured_disabled_desc: 'API 已配置但未启用', - error_desc: 'API 已配置但链接异常', + available_desc: 'API is configured and enabled', + unconfigured_desc: 'Need to configure API Key', + configured_disabled_desc: 'API is configured but not enabled', + error_desc: 'API is configured but connection error', serviceEndpoint: 'Service Endpoint URL', serviceEndpointPlaceholder: 'URL of the service endpoint', @@ -1564,10 +1585,10 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re parallel: 'Parallel Execution', 'var-aggregator': 'Variable Aggregator', externalInteraction: 'External Interaction', - http_request: 'HTTP Request', + "http-request": 'HTTP Request', tools: 'Tools', code_execution: 'Code Execution', - template_rendering: 'Template Rendering', + "jinja-render": 'Template Rendering', cognitiveUpgrading: 'Cognitive Upgrading (Innovation)', task_planning: 'Task Planning', reasoning_control: 'Reasoning Control', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 71523af3..a3bfd1d9 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -535,7 +535,7 @@ export const zh = { master: '主管模式', master_agent: '主管模式', - masterDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', + master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', handoffs: '协作模式', handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', masterConfig: '主管配置', @@ -1117,6 +1117,14 @@ export const zh = { quicklyForget: '快速遗忘', slowForgetting: '缓慢遗忘', currentConfig: '当前配置', + + decay_constant: '衰减常数', + max_history_length: '访问历史最大长度', + forgetting_threshold: '遗忘阈值', + min_days_since_access: '最小未访问天数', + enable_llm_summary: '是否使用 LLM 生成摘要', + max_merge_batch_size: '单次最大融合节点对数', + forgetting_interval_hours: '遗忘周期间隔' }, userMemory: { userMemory: '用户记忆', @@ -1677,10 +1685,10 @@ export const zh = { parallel: '并行执行', 'var-aggregator': '变量聚合器', externalInteraction: '外部交互', - http_request: 'HTTP请求', + "http-request": 'HTTP请求', tools: '工具 (Tools)', code_execution: '代码执行', - template_rendering: '模板渲染', + "jinja-render": '模板渲染', cognitiveUpgrading: '认知升级(创新)', task_planning: '任务规划', reasoning_control: '推理控制', @@ -1782,7 +1790,37 @@ export const zh = { "gt": '>', "ge": '>=', else_desc: '用于定义当 if 条件不满足时应执行的逻辑。' - } + }, + 'http-request': { + auth: '鉴权', + authType: '鉴权类型', + apiKey: 'API Key', + basic: '基础', + bearer: 'Bearer', + custom: '自定义', + header: 'Header', + api_key: 'API Key', + timeouts: '超时设置', + "connect_timeout": '连接超时(秒)', + "read_timeout": '读取超时(秒)', + "write_timeout": '写入超时(秒)', + retry: '失败时重试', + error_handle: '异常处理', + verify_ssl: '验证 SSL 证书', + none: '无', + default: '默认值', + branch: '异常分支', + status_code: '状态码', + max_attempts: '最大重试次数', + retry_interval: '重试间隔', + }, + 'jinja-render': { + template: '代码', + mapping: '输入变量' + }, + name: '键', + type: '类型', + value: '值', }, clear: '清空', diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index 436cd74f..c6aa63e8 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -17,7 +17,8 @@ import type { KnowledgeConfig, Variable, MemoryConfig, - AiPromptModalRef + AiPromptModalRef, + Source } from './types' import type { Model } from '@/views/ModelManagement/types' import { getModelList } from '@/api/models'; @@ -200,7 +201,7 @@ const Agent = forwardRef((_props, ref) => { }) } - const refresh = (vo: ModelConfig, type: 'model' | 'chat') => { + const refresh = (vo: ModelConfig, type: Source) => { if (type === 'model') { const { default_model_config_id, ...rest } = vo form.setFieldsValue({ @@ -445,7 +446,6 @@ const Agent = forwardRef((_props, ref) => { diff --git a/web/src/views/ApplicationConfig/Api.tsx b/web/src/views/ApplicationConfig/Api.tsx index e9fdea68..02c066e4 100644 --- a/web/src/views/ApplicationConfig/Api.tsx +++ b/web/src/views/ApplicationConfig/Api.tsx @@ -77,6 +77,7 @@ const Api: FC<{ application: Application | null }> = ({ application }) => { title: t('common.confirmDeleteDesc', { name: vo.name }), content: t('application.apiKeyDeleteContent'), okText: t('common.delete'), + cancelText: t('common.cancel'), okType: 'danger', onOk: () => { deleteApiKey(vo.id) diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 4330cd60..058c3442 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -1,29 +1,31 @@ -import { type FC, useEffect, useState, useRef, forwardRef, useImperativeHandle, type Key } from 'react' +import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom'; import Card from './components/Card' -import { Form, Space, Row, Col, Button, Flex, App } from 'antd' -import type { DefaultOptionType } from 'antd/es/select' +import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd' import Tag, { type TagProps } from './components/Tag' import CustomSelect from '@/components/CustomSelect'; -import { getApplicationListUrl, getMultiAgentConfig, saveMultiAgentConfig } from '@/api/application'; +import { getMultiAgentConfig, saveMultiAgentConfig } from '@/api/application'; import type { Config, SubAgentModalRef, ChatData, SubAgentItem, - ClusterRef + ClusterRef, + ModelConfigModalRef } from './types' import Chat from './components/Chat' import RbCard from '@/components/RbCard/Card' import SubAgentModal from './components/SubAgentModal' import Empty from '@/components/Empty' -import type { Application } from '@/views/ApplicationManagement/types' +import RadioGroupCard from '@/components/RadioGroupCard' +import { getModelListUrl } from '@/api/models' +import ModelConfigModal from './components/ModelConfigModal' const tagColors = ['processing', 'warning', 'default'] const MAX_LENGTH = 5; -const Cluster = forwardRef(({application}, ref) => { +const Cluster = forwardRef((_props, ref) => { const { t } = useTranslation() const { message } = App.useApp() const [form] = Form.useForm() @@ -39,7 +41,15 @@ const Cluster = forwardRef(({applicati ]) const handleSave = (flag = true) => { + if (!data) return Promise.resolve() + if (!values.default_model_config_id) { + message.warning(t('common.selectPlaceholder', { title: t('application.model') })) + return Promise.resolve() + } + const params = { + id: data.id, + app_id: data.app_id, ...values, sub_agents: (subAgents || []).map(item => ({ ...item, @@ -47,6 +57,8 @@ const Cluster = forwardRef(({applicati })) } + console.log('params', params) + return new Promise((resolve, reject) => { form.validateFields().then(() => { saveMultiAgentConfig(id as string, params) @@ -60,21 +72,14 @@ const Cluster = forwardRef(({applicati reject(error) }) }) - .catch(error => { - reject(error) - }) + .catch(error => { + reject(error) + }) }) } useEffect(() => { getData() }, [id]) - useEffect(() => { - if (application) { - form.setFieldsValue({ - name: application.name, - }) - } - }, [application]) const getData = () => { if (!id) { @@ -93,7 +98,6 @@ const Cluster = forwardRef(({applicati subAgentModalRef.current?.handleOpen(agent) } const refreshSubAgents = (agent: SubAgentItem) => { - // setSubAgents(subAgents) const index = subAgents.findIndex(item => item.agent_id === agent.agent_id) const newSubAgents = [...subAgents] if (index === -1) { @@ -110,90 +114,131 @@ const Cluster = forwardRef(({applicati const handleDeleteSubAgent = (agent: SubAgentItem) => { setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id)) } - const handleChange = (value: Key, option?: DefaultOptionType | DefaultOptionType[] | undefined) => { - if (option && !Array.isArray(option)) { - form.setFieldsValue({ master_agent_name: option.children }) - } - } useImperativeHandle(ref, () => ({ handleSave })) + const modelConfigModalRef = useRef(null) + const handleEditModelConfig = () => { + modelConfigModalRef.current?.handleOpen('multi_agent', values.model_parameters) + } + const handleSaveModelConfig = (values: Config['model_parameters']) => { + form.setFieldsValue({ + model_parameters: values + }) + } + return ( -
+
- - - - - {t('application.agentName')} -
- } - className="rb:mb-[20px]!" - rules={[{ required: true, message: t('common.pleaseSelect') }]} - > - - -
+ + + ({ + value: type, + label: t(`application.${type}`), + labelDesc: t(`application.${type}Desc`), + }))} + allowClear={false} + /> + -
{t('application.added')}: {subAgents.length}/{MAX_LENGTH}
+
{t('application.added')}: {subAgents.length}/{MAX_LENGTH}
{subAgents.length === 0 ? : subAgents.map((agent, index) => ( - -
+
{agent.name?.[0]}
{agent.name} - {agent.role &&
{agent.role || '-'}
} - {agent.capabilities && {agent.capabilities.map((tag, tagIndex) => {tag})}} + {agent.role &&
{agent.role || '-'}
} + {agent.capabilities && {agent.capabilities.map((tag, tagIndex) => {tag})}}
-
handleSubAgentModal(agent)} >
-
handleDeleteSubAgent(agent)} >
))} + + + + + + + + + + + + + + + + + + ({ + value: type, + label: t(`application.${type}`), + }))} + /> + + @@ -213,6 +258,11 @@ const Cluster = forwardRef(({applicati ref={subAgentModalRef} refresh={refreshSubAgents} /> + ) }) diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index ea35fa5a..bd826ba1 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -16,7 +16,7 @@ interface ChatProps { chatList: ChatData[]; data: Config; updateChatList: React.Dispatch>; - handleSave: (flag?: boolean) => Promise; + handleSave: (flag?: boolean) => Promise; source?: 'multi_agent' | 'agent'; } const Chat: FC = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => { @@ -74,7 +74,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc const curModelChat = modelChatList[targetIndex] const curChatMsgList = curModelChat.list || [] const lastMsg = curChatMsgList[curChatMsgList.length - 1] - if (lastMsg.role === 'assistant') { + if (lastMsg && lastMsg.role === 'assistant') { modelChatList[targetIndex] = { ...modelChatList[targetIndex], conversation_id: conversation_id, @@ -128,7 +128,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc .then(() => { const message = form.getFieldValue('message') if (!message?.trim()) return - + addUserMessage(message) form.setFieldsValue({ message: undefined }) addAssistantMessage() @@ -139,7 +139,7 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc data.map(item => { const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number }; - switch(item.event) { + switch (item.event) { case 'model_message': updateAssistantMessage(content, model_config_id, conversation_id) break; diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 894071e6..abe7a008 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -93,18 +93,15 @@ export interface Config extends MultiAgentConfig { export interface MultiAgentConfig { id: string; app_id: string; - // system_prompt: string; - // default_model_config_id?: string; - // model_parameters: ModelConfig; - // knowledge_retrieval: KnowledgeConfig | null; - // memory?: MemoryConfig; - // variables: Variable[]; - // tools: Record; - // is_active: boolean; - // created_at: number; - // updated_at: number; - master_agent_id?: string; + default_model_config_id?: string; + model_parameters: ModelConfig; + orchestration_mode: 'conditional' | 'sequential' | 'parallel'; sub_agents?: SubAgentItem[]; + routing_rules: null; + execution_config: { + routing_mode: 'master' | 'handoffs' + }; + aggregation_strategy: 'merge' | 'vote' | 'priority' } // 创建表单数据类型 @@ -116,21 +113,22 @@ export interface ApplicationModalData { // 定义组件暴露的方法接口 export interface AgentRef { - handleSave: (flag?: boolean) => Promise; + handleSave: (flag?: boolean) => Promise; } export interface ClusterRef { - handleSave: (flag?: boolean) => Promise; + handleSave: (flag?: boolean) => Promise; } export interface WorkflowRef { - handleSave: (flag?: boolean) => Promise; + handleSave: (flag?: boolean) => Promise; handleRun: () => void; graphRef: GraphRef } export interface ApplicationModalRef { handleOpen: (application?: Config) => void; } +export type Source = 'chat' | 'model' | 'multi_agent' export interface ModelConfigModalRef { - handleOpen: (source: 'chat' | 'model') => void; + handleOpen: (source: Source, model?: any) => void; } export interface ModelConfigModalData { model: string; diff --git a/web/src/views/OrderHistory/components/OrderDetail.tsx b/web/src/views/OrderHistory/components/OrderDetail.tsx index 562397be..e4c5bf51 100644 --- a/web/src/views/OrderHistory/components/OrderDetail.tsx +++ b/web/src/views/OrderHistory/components/OrderDetail.tsx @@ -9,7 +9,7 @@ import { getOrderDetail } from '@/api/order' import { STATUS } from '../index'; -const OrderDetail = forwardRef((_props, ref) => { +const OrderDetail = forwardRef void; }>(({ getProductType }, ref) => { const { t } = useTranslation(); const [visible, setVisible] = useState(false); const [data, setData] = useState({}) @@ -38,7 +38,7 @@ const OrderDetail = forwardRef((_props, ref) => { : key === 'status' && value ? t(`pricing.${STATUS[value as keyof typeof STATUS].key}`) : key === 'product_type' && value - ? t(`pricing.${value.toLowerCase()}.type`) + ? t(`pricing.${getProductType(value)}.type`) : value } }) diff --git a/web/src/views/OrderHistory/index.tsx b/web/src/views/OrderHistory/index.tsx index 1dd31376..1446434b 100644 --- a/web/src/views/OrderHistory/index.tsx +++ b/web/src/views/OrderHistory/index.tsx @@ -51,10 +51,10 @@ const OrderHistory: React.FC = () => { ] const productTypeOptions = [ { label: t('pricing.allType'), value: null }, - ...PRICE_LIST.map(vo => ({ - label: t(`pricing.${vo.type}.type`), - value: vo.type - })) + { label: t('pricing.personal.type'), value: 'FREE' }, + { label: t('pricing.team.type'), value: 'TEAM' }, + { label: t('pricing.biz.type'), value: 'ENTERPRISE' }, + { label: t('pricing.commerce.type'), value: 'OEM' }, ] const handleView = (order: Order) => { @@ -128,6 +128,16 @@ const OrderHistory: React.FC = () => { end_time })) } + + const getProductType = (type: string) => { + const typeMap: Record = { + 'FREE': 'personal', + 'TEAM': 'team', + 'ENTERPRISE': 'biz', + 'OEM': 'commerce' + }; + return typeMap[type] || 'ENTERPRISE'; + }; // 表格列配置 const columns: ColumnsType = [ { @@ -140,7 +150,7 @@ const OrderHistory: React.FC = () => { title: t('pricing.product_type'), dataIndex: 'product_type', key: 'product_type', - render: (type) => t(`pricing.${type.toLowerCase()}.type`) + render: (type) => t(`pricing.${getProductType(type)}.type`) }, { title: t('pricing.payable_amount'), @@ -219,7 +229,7 @@ const OrderHistory: React.FC = () => { isScroll={true} /> - +
); }; diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 152083f4..d3570b0f 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type FC } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $getRoot, $getSelection } from 'lexical'; +import { $getSelection, $isRangeSelection } from 'lexical'; import { INSERT_VARIABLE_COMMAND } from '../commands'; import type { NodeProperties } from '../../../types' @@ -23,43 +23,55 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { useEffect(() => { return editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { - const root = $getRoot(); - const text = root.getTextContent(); - const shouldShow = text.includes('/'); + const selection = $getSelection(); + + if (!selection || !$isRangeSelection(selection)) { + setShowSuggestions(false); + return; + } + + const anchorNode = selection.anchor.getNode(); + const anchorOffset = selection.anchor.offset; + + // Get the text content of the current node + const nodeText = anchorNode.getTextContent(); + + // Check if we have a '/' at the current position or after line break + const textBeforeCursor = nodeText.substring(0, anchorOffset); + const shouldShow = textBeforeCursor.endsWith('/') || + (textBeforeCursor === '/' && anchorOffset === 1); + setShowSuggestions(shouldShow); if (shouldShow) { - const selection = $getSelection(); - if (selection) { - const domSelection = window.getSelection(); - if (domSelection && domSelection.rangeCount > 0) { - const range = domSelection.getRangeAt(0); - const rect = range.getBoundingClientRect(); + const domSelection = window.getSelection(); + if (domSelection && domSelection.rangeCount > 0) { + const range = domSelection.getRangeAt(0); + const rect = range.getBoundingClientRect(); - const popupWidth = 280; - const popupHeight = 200; - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; + const popupWidth = 280; + const popupHeight = 200; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; - let left = rect.left; - let top = rect.top - 10; + let left = rect.left; + let top = rect.top - 10; - if (left + popupWidth > viewportWidth) { - left = viewportWidth - popupWidth - 10; - } - if (left < 10) { - left = 10; - } - - if (top - popupHeight < 10) { - top = rect.bottom + 10; - if (top + popupHeight > viewportHeight) { - top = viewportHeight - popupHeight - 10; - } - } - - setPopupPosition({ top, left }); + if (left + popupWidth > viewportWidth) { + left = viewportWidth - popupWidth - 10; } + if (left < 10) { + left = 10; + } + + if (top - popupHeight < 10) { + top = rect.bottom + 10; + if (top + popupHeight > viewportHeight) { + top = viewportHeight - popupHeight - 10; + } + } + + setPopupPosition({ top, left }); } } }); @@ -112,7 +124,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
{nodeName}
- {nodeOptions.map((option, index) => { + {nodeOptions.map((option) => { const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option); return (
{ INSERT_VARIABLE_COMMAND, (payload: InsertVariableCommandPayload) => { editor.update(() => { - const root = $getRoot(); - const text = root.getTextContent(); - const lastSlashIndex = text.lastIndexOf('/'); + const selection = $getSelection(); + if (!selection || !$isRangeSelection(selection)) return; - // Find the paragraph and the position to insert - const paragraph = root.getFirstChild(); - if (!paragraph || !$isParagraphNode(paragraph)) return; + const anchorNode = selection.anchor.getNode(); + const anchorOffset = selection.anchor.offset; - const children = paragraph.getChildren(); - let insertPosition = 0; - let currentTextLength = 0; - - // Find where to insert the new tag - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const childText = child.getTextContent(); + if ($isTextNode(anchorNode)) { + const nodeText = anchorNode.getTextContent(); + const textBeforeCursor = nodeText.substring(0, anchorOffset); + const textAfterCursor = nodeText.substring(anchorOffset); - if (currentTextLength + childText.length > lastSlashIndex) { - // Split this text node if needed - if ($isTextNode(child)) { - const beforeSlash = childText.substring(0, lastSlashIndex - currentTextLength); - const afterSlash = childText.substring(lastSlashIndex - currentTextLength + 1); - - if (beforeSlash) { - child.setTextContent(beforeSlash); - insertPosition = i + 1; - } else { - insertPosition = i; - child.remove(); - } - - // Insert tag and space - const tagNode = $createVariableNode(payload.data); - const spaceNode = $createTextNode(' '); - - if (insertPosition < paragraph.getChildrenSize()) { - paragraph.getChildAtIndex(insertPosition)?.insertBefore(tagNode); - tagNode.insertAfter(spaceNode); - } else { - paragraph.append(tagNode); - paragraph.append(spaceNode); - } - - if (afterSlash) { - spaceNode.insertAfter($createTextNode(afterSlash)); - } - - // Set cursor after space - const selection = $createRangeSelection(); - selection.anchor.set(spaceNode.getKey(), 1, 'text'); - selection.focus.set(spaceNode.getKey(), 1, 'text'); - $setSelection(selection); + // Find the last '/' position + const lastSlashIndex = textBeforeCursor.lastIndexOf('/'); + + if (lastSlashIndex !== -1) { + // Split the text: before slash, insert variable, after cursor + const beforeSlash = textBeforeCursor.substring(0, lastSlashIndex); + + // Update the current text node with text before slash + anchorNode.setTextContent(beforeSlash); + + // Create and insert the variable node + const tagNode = $createVariableNode(payload.data); + const spaceNode = $createTextNode(' '); + + anchorNode.insertAfter(tagNode); + tagNode.insertAfter(spaceNode); + + // Add remaining text if any + if (textAfterCursor) { + spaceNode.insertAfter($createTextNode(textAfterCursor)); } - break; + + // Set cursor after space + const newSelection = $createRangeSelection(); + newSelection.anchor.set(spaceNode.getKey(), 1, 'text'); + newSelection.focus.set(spaceNode.getKey(), 1, 'text'); + $setSelection(newSelection); } - - currentTextLength += childText.length; - insertPosition = i + 1; } }); return true; diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx index 54086c90..57ac251b 100644 --- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -1,7 +1,7 @@ import { type FC } from 'react'; import { useTranslation } from 'react-i18next' import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { MinusCircleOutlined } from '@ant-design/icons'; import Editor from '../Editor' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' @@ -48,7 +48,7 @@ const MessageEditor: FC = ({ {(fields, { add, remove }) => ( {fields.map(({ key, name, ...restField }) => { - const currentRole = values[parentName]?.[key].role || 'USER' + const currentRole = (values[parentName]?.[key].role || 'USER').toUpperCase() return ( @@ -86,7 +86,7 @@ const MessageEditor: FC = ({ ) })} - diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index be4405db..ec2116cb 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -237,6 +237,20 @@ const Properties: FC = ({ }); } break + case 'knowledge-retrieval': + const knowledgeKey = `${nodeId}_message`; + if (!addedKeys.has(knowledgeKey)) { + addedKeys.add(knowledgeKey); + variableList.push({ + key: knowledgeKey, + label: 'message', + type: 'variable', + dataType: 'String', + value: `${nodeId}.message`, + nodeData: nodeData, + }); + } + break } });