From 675d0fc5ef4f488ddaf9b58bbd5afa2f4185b785 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 14:08:26 +0800 Subject: [PATCH 1/5] feat(web): workflow run time ui --- web/src/components/Markdown/CodeBlock.tsx | 10 +- .../Workflow/components/Chat/Runtime.tsx | 160 +++++++++++------- .../Workflow/components/Chat/chat.module.css | 7 +- 3 files changed, 112 insertions(+), 65 deletions(-) diff --git a/web/src/components/Markdown/CodeBlock.tsx b/web/src/components/Markdown/CodeBlock.tsx index a830827d..f863cc48 100644 --- a/web/src/components/Markdown/CodeBlock.tsx +++ b/web/src/components/Markdown/CodeBlock.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-02 15:15:11 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-02 15:15:11 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-07 14:04:33 */ /** * CodeBlock Component @@ -27,6 +27,7 @@ type ICodeBlockProps = { needCopy?: boolean; size?: 'small' | 'default'; showLineNumbers?: boolean; + background?: string; } /** Code block component for displaying formatted code with optional copy functionality */ @@ -34,7 +35,8 @@ const CodeBlock: FC = ({ value, needCopy = true, size = 'default', - showLineNumbers = false + showLineNumbers = false, + background = '#F0F3F8' }) => { return ( @@ -43,7 +45,7 @@ const CodeBlock: FC = ({ style={atelierHeathLight} customStyle={{ padding: '8px 12px 8px 12px', - backgroundColor: '#F0F3F8', + backgroundColor: background, borderRadius: 8, fontSize: size === 'small' ? 12 : 14, wordBreak: 'break-all' diff --git a/web/src/views/Workflow/components/Chat/Runtime.tsx b/web/src/views/Workflow/components/Chat/Runtime.tsx index 7da550e0..68bdc452 100644 --- a/web/src/views/Workflow/components/Chat/Runtime.tsx +++ b/web/src/views/Workflow/components/Chat/Runtime.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-24 17:57:08 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-12 13:39:24 + * @Last Modified time: 2026-04-07 14:05:50 */ /* * Runtime Component @@ -18,13 +18,15 @@ import { type FC, useState } from 'react' import { useTranslation } from 'react-i18next' import clsx from 'clsx' -import { Space, Button, Collapse, Flex } from 'antd' +import { App, Button, Collapse, Flex } from 'antd' import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons' +import copy from 'copy-to-clipboard' import styles from './chat.module.css' import type { ChatItem } from '@/components/Chat/types' import Markdown from '@/components/Markdown' import CodeBlock from '@/components/Markdown/CodeBlock' +import RbAlert from '@/components/RbAlert' /** * Runtime component props @@ -36,10 +38,12 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ index }) => { const { t } = useTranslation() + const { message } = App.useApp() // Stores the currently selected detail view (for nested loop/iteration exploration) const [detail, setDetail] = useState(null) // Tracks whether the current detail view is for a loop (true) or iteration (false) const [loop, setLoop] = useState(null) + const [expanded, setExpanded] = useState(false) /** * Handles navigation into nested loop/iteration details @@ -57,7 +61,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ * @returns Tailwind CSS class for appropriate color */ const getStatus = (status?: string) => { - return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]' + return status === 'completed' ? 'rb:text-[#369F21]!' : status === 'failed' ? 'rb:text-[#FF5D34]!' : 'rb:text-[#5B6167]!' } /** @@ -76,7 +80,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ return ( - + {Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => { return ( = ({ /> ) })} - + ) } @@ -103,7 +107,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ */ const renderChild = (list: any) => { if (Array.isArray(list)) { - return + return {list?.map(vo => { const isLoop = vo.node_type === 'loop'; // Render cycle variables for loop nodes without node_name @@ -114,6 +118,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
@@ -133,35 +138,44 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ return ( -
- {vo.icon &&
} -
{vo.node_name}
-
- + label:
+ + {vo.icon &&
} +
{vo.node_name}
+ + {typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms} - {vo.status === 'completed' ? : vo.status === 'failed' ? : } - + {vo.status === 'completed' + ? + : vo.status === 'failed' + ? + : + } +
, className: styles.collapseItem, children: ( - + {/* Display error message for failed nodes */} - {vo.status === 'failed' && -
-
- {t(`workflow.error`)} - -
-
+ + {item.error && + + + + {t(`workflow.error`)} + + -
-
+
+ } {/* Display navigation to nested cycles if subContent exists */} {vo.subContent?.length > 0 && ( @@ -172,12 +186,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ )} {/* Display input and output data as JSON code blocks */} {['input', 'output'].map(key => ( -
+
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}`)}
@@ -186,55 +201,80 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'} needCopy={false} showLineNumbers={true} + background="#EBEBEB" />
))} - + ) }]} /> ) })} - + } return
} + /** Copy value to clipboard and show success message */ + const handleCopy = (value: string) => { + copy(value) + message.success(t('common.copySuccess')) + } + return ( -
- - {item.status === 'completed' ? : item.status === 'failed' ? : } - {t('application.workflow')} -
, - className: styles.collapseItem, - children: ( - detail - ? ( -
- - {renderDetailChild(detail.subContent)} -
- ) - : <> - {item.error && -
- -
- } - {renderChild(item.subContent)} - +
+ + + {item.status === 'completed' + ? + : item.status === 'failed' + ? + : + } + {t('application.workflow')} + + { setExpanded(v => !v); setDetail(null) }} + > +
+ + + {expanded && ( + detail + ? ( +
+ + {renderDetailChild(detail.subContent)} +
) - }]} - /> + :
+ {item.error && + + } + {renderChild(item.subContent)} +
+ )}
) } diff --git a/web/src/views/Workflow/components/Chat/chat.module.css b/web/src/views/Workflow/components/Chat/chat.module.css index 46360d47..1754aa71 100644 --- a/web/src/views/Workflow/components/Chat/chat.module.css +++ b/web/src/views/Workflow/components/Chat/chat.module.css @@ -21,7 +21,9 @@ line-height: 16px; } .collapse-item:global(.ant-collapse-item>.ant-collapse-header) { - padding: 8px 12px; + padding: 8px 16px 8px 10px; + display: flex; + align-items: center; } .collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) { height: 16px; @@ -45,4 +47,7 @@ } .collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) { padding: 0 4px 4px 4px; +} +:global(.ant-collapse-borderless)>.collapse-item:global(.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box) { + padding: 0 12px 12px 12px; } \ No newline at end of file From 0b5a030e46f5c4d68d8b10cad91d2eb4361859c6 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 14:23:12 +0800 Subject: [PATCH 2/5] fix(web): open statement --- .../components/FeaturesConfig/OpenStatementSettingModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx index 2d829790..67501255 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-05 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-27 14:38:28 + * @Last Modified time: 2026-04-07 14:22:40 */ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Button, Form, Input, Flex, App } from 'antd'; @@ -45,7 +45,7 @@ const OpenStatementSettingModal = forwardRef { form.validateFields().then(values => { const { suggested_questions, ...rest } = values - const filterSuggestedQuestions = suggested_questions.filter(vo => vo && vo.trim() !== '' && vo !== null) + const filterSuggestedQuestions = suggested_questions?.filter(vo => vo && vo.trim() !== '' && vo !== null) if (values?.enabled && values?.statement && values?.statement?.trim() !== '') { const usedVars = [...new Set([...values.statement?.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]))] From 9a4a614fc8c01f6ed62d69e18796bddb791b20bd Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 15:24:53 +0800 Subject: [PATCH 3/5] fix(web): jinja2 editor --- web/src/components/Markdown/index.tsx | 6 +- .../components/Editor/Jinja2Editor.tsx | 12 ++-- .../plugin/Jinja2AutocompletePlugin.tsx | 6 +- .../plugin/Jinja2InitialValuePlugin.tsx | 57 +++++++++---------- .../plugin/Jinjia2CharacterCountPlugin.tsx | 23 ++++++++ 5 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 web/src/views/Workflow/components/Editor/plugin/Jinjia2CharacterCountPlugin.tsx diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx index 6b9dc12a..10cf177b 100644 --- a/web/src/components/Markdown/index.tsx +++ b/web/src/components/Markdown/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-02 15:17:31 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-02 16:06:03 + * @Last Modified time: 2026-04-07 15:14:02 */ /** * RbMarkdown Component @@ -97,10 +97,6 @@ const buildComponents = (onFormSubmit?: (values: Record) => void) = const ctx = useContext(FormContext) return ctx?.onSubmit?.(ctx.values)}>{[children]} }, - table: ({ children, ...props }: any) =>
{children}
, - tr: ({ children, ...props }: any) => {children}, - th: ({ children, ...props }: any) => {children}, - td: ({ children, ...props }: any) => {children}, input: ({ children, ...props }: any) => { const ctx = useContext(FormContext) const handleChange = useCallback((val: any) => { diff --git a/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx index 0ee795ba..9a21a3b2 100644 --- a/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx +++ b/web/src/views/Workflow/components/Editor/Jinja2Editor.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-04-02 15:15:36 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-02 15:15:36 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-07 14:48:00 */ import { type FC, useEffect, useMemo } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; @@ -12,7 +12,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { type Suggestion } from './plugin/AutocompletePlugin'; -import CharacterCountPlugin from './plugin/CharacterCountPlugin'; +import Jinjia2CharacterCountPlugin from './plugin/Jinjia2CharacterCountPlugin'; import Jinja2InitialValuePlugin from './plugin/Jinja2InitialValuePlugin'; import Jinja2AutocompletePlugin from './plugin/Jinja2AutocompletePlugin'; import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; @@ -89,7 +89,7 @@ export interface Jinja2EditorProps { const Jinja2Editor: FC = ({ placeholder = '请输入内容...', - value = '', + value, onChange, options = [], variant = 'borderless', @@ -174,8 +174,8 @@ const Jinja2Editor: FC = ({ - {}} onChange={onChange} waitForInit /> - + {}} /> +
diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx index 00b4c3d1..86e8fa45 100644 --- a/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2AutocompletePlugin.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-04-02 17:10:59 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-02 17:10:59 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-07 14:50:14 */ import { useEffect, useState, useRef, type FC } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; @@ -161,7 +161,7 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => (
- {nodeOptions[0]?.nodeData?.icon && } + {nodeOptions[0]?.nodeData?.icon &&
} {nodeOptions[0]?.nodeData?.name || nodeId} {nodeOptions.map((option) => { diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx index 6b5f7363..b146ecee 100644 --- a/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2InitialValuePlugin.tsx @@ -6,53 +6,50 @@ */ import { useEffect, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; +import { $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode } from 'lexical'; interface Jinja2InitialValuePluginProps { - value: string; + value?: string; + onChange?: (value: string) => void; } -const Jinja2InitialValuePlugin: React.FC = ({ value }) => { +const Jinja2InitialValuePlugin: React.FC = ({ value, onChange }) => { const [editor] = useLexicalComposerContext(); - const prevValueRef = useRef(''); - const isUserInputRef = useRef(false); + const internalValueRef = useRef(undefined); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; useEffect(() => { return editor.registerUpdateListener(({ editorState, tags }) => { if (tags.has('programmatic')) return; + if (internalValueRef.current === undefined) return; editorState.read(() => { - const textContent = $getRoot().getTextContent(); - if (textContent !== prevValueRef.current) { - isUserInputRef.current = true; - prevValueRef.current = textContent; + const paragraphs = $getRoot().getChildren() + .filter($isParagraphNode) + .map(p => p.getChildren().map(n => n.getTextContent()).join('')); + const text = paragraphs.join('\n'); + if (text !== internalValueRef.current) { + internalValueRef.current = text; + onChangeRef.current?.(text); } }); }); }, [editor]); useEffect(() => { - if (value === prevValueRef.current) return; + if (value === undefined) return; + if (value === internalValueRef.current) return; - if (isUserInputRef.current) { - prevValueRef.current = value; - isUserInputRef.current = false; - return; - } - - prevValueRef.current = value; - isUserInputRef.current = false; - - queueMicrotask(() => { - editor.update(() => { - const root = $getRoot(); - root.clear(); - value.split('\n').forEach((line) => { - const paragraph = $createParagraphNode(); - paragraph.append($createTextNode(line)); - root.append(paragraph); - }); - }, { tag: 'programmatic' }); - }); + internalValueRef.current = value; + editor.update(() => { + const root = $getRoot(); + root.clear(); + value.split('\n').forEach((line) => { + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode(line)); + root.append(paragraph); + }); + }, { tag: 'programmatic' }); }, [value, editor]); return null; diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinjia2CharacterCountPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinjia2CharacterCountPlugin.tsx new file mode 100644 index 00000000..75189f92 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Jinjia2CharacterCountPlugin.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { $getRoot, $isParagraphNode } from 'lexical'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; + +const Jinjia2CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const root = $getRoot(); + const paragraphs = root.getChildren() + .filter($isParagraphNode) + .map(p => p.getChildren().map(n => n.getTextContent()).join('')); + setCount(paragraphs.join('\n').length); + }); + }); + }, [editor, setCount]); + + return null; +} + +export default Jinjia2CharacterCountPlugin \ No newline at end of file From c3ee3c4af923f3830ef33f38f8183786ddec9b35 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 17:06:53 +0800 Subject: [PATCH 4/5] fix(web): workflow statement support variable --- web/src/components/Header/index.tsx | 10 +- .../components/ConfigHeader.tsx | 23 ++-- .../FeaturesConfig/FeaturesConfigModal.tsx | 3 +- .../OpenStatementSettingModal.tsx | 20 ++- web/src/views/ApplicationConfig/types.ts | 3 +- .../components/ConversationMemory.tsx | 4 +- .../views/Workflow/components/Chat/Chat.tsx | 19 ++- .../Workflow/components/Editor/index.tsx | 9 +- .../components/Editor/nodes/VariableNode.tsx | 12 ++ .../Editor/plugin/AutocompletePlugin.tsx | 124 +++++++++--------- .../views/Workflow/hooks/useWorkflowGraph.ts | 46 ++++++- web/src/views/Workflow/index.tsx | 23 +++- 12 files changed, 198 insertions(+), 98 deletions(-) diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index 1de2e038..49988223 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -91,10 +91,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { }, { key: '3', - icon:
, + icon:
, label: {t('header.userInfo')} -
+
, className: 'rb:text-[#212332]!', onClick: () => { @@ -103,10 +103,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { }, { key: '4', - icon:
, + icon:
, label: {t('header.settings')} -
+
, className: 'rb:text-[#212332]!', onClick: () => { @@ -120,7 +120,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { }, { key: '6', - icon:
, + icon:
, label: t('header.logout'), danger: true, className: 'rb:hover:rb:bg-transparent rb:hover:text-[#FF5D34]!', diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index ece533c1..d77ae27c 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:27:52 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-27 19:07:24 + * @Last Modified time: 2026-04-07 16:28:33 */ import { type FC, useRef, useMemo, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -18,7 +18,6 @@ import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FeaturesConfigFor import { deleteApplication, appExport } from '@/api/application' import CopyModal from './CopyModal' import PageHeader from '@/components/Layout/PageHeader' -import FeaturesConfig from './FeaturesConfig' /** * Tab keys for application configuration @@ -70,7 +69,6 @@ const ConfigHeader: FC = ({ application, activeTab, handleChangeTab, refresh, workflowRef, appRef, - features, onFeaturesChange, }) => { const { t } = useTranslation(); @@ -175,10 +173,9 @@ const ConfigHeader: FC = ({ return items }, [t, handleClick, application]) - const handleSaveFeaturesConfig = useCallback((value: FeaturesConfigForm) => { - appRef?.current?.handleSaveFeaturesConfig?.(value) - onFeaturesChange?.(value) - }, [appRef, onFeaturesChange]) + const handleFeaturesConfig = () => { + workflowRef.current?.handleFeaturesConfig?.() + } return ( <> @@ -209,12 +206,12 @@ const ConfigHeader: FC = ({ } extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement' ? - ({ ...v, display_name: v.name }))} - /> + +
+
diff --git a/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx index 67501255..91d0d19f 100644 --- a/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx +++ b/web/src/views/ApplicationConfig/components/FeaturesConfig/OpenStatementSettingModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-03-05 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-07 14:22:40 + * @Last Modified time: 2026-04-07 16:58:10 */ import { forwardRef, useImperativeHandle, useState } from 'react'; import { Button, Form, Input, Flex, App } from 'antd'; @@ -12,6 +12,8 @@ import RbModal from '@/components/RbModal'; import type { FeaturesConfigForm } from '../../types' import type { Variable } from '../VariableList/types' import Tag from '@/components/Tag' +import type { Application } from '@/views/ApplicationManagement/types'; +import Editor from '@/views/Workflow/components/Editor'; export interface OpenStatementSettingModalRef { handleOpen: (values?: FeaturesConfigForm['opening_statement']) => void; @@ -21,17 +23,21 @@ export interface OpenStatementSettingModalRef { interface OpenStatementSettingModalProps { onSave: (values: FeaturesConfigForm['opening_statement']) => void; chatVariables?: Variable[]; + source?: Application['type']; } const OpenStatementSettingModal = forwardRef(({ onSave, - chatVariables = [] + chatVariables = [], + source }, ref) => { const { t } = useTranslation(); const { modal } = App.useApp() const [visible, setVisible] = useState(false); const [form] = Form.useForm(); + console.log('chatVariables', chatVariables) + const handleClose = () => { setVisible(false); form.resetFields(); @@ -49,6 +55,7 @@ const OpenStatementSettingModal = forwardRef m[1]))] + console.log('usedVars', usedVars, chatVariables) const validNames = new Set(chatVariables.map(v => v.name)) const invalid = usedVars.filter(v => !validNames.has(v)) if (invalid.length > 0) { @@ -100,9 +107,12 @@ const OpenStatementSettingModal = forwardRef - + {source === 'workflow' + ? + : + } diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index 1d6e9e57..61d8d9be 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:49 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-31 15:45:17 + * @Last Modified time: 2026-04-07 15:46:19 */ import type { KnowledgeConfig } from './components/Knowledge/types' import type { Variable } from './components/VariableList/types' @@ -168,6 +168,7 @@ export interface WorkflowRef { chatVariables: ChatVariable[]; config: WorkflowConfig | null; features: WorkflowConfig['features']; + handleFeaturesConfig?: () => void; handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void; } diff --git a/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx b/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx index c209274b..cd080cc6 100644 --- a/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx +++ b/web/src/views/UserMemoryDetail/components/ConversationMemory.tsx @@ -48,8 +48,8 @@ const ConversationMemory: FC = () => { gap={12} >
(({ appId, graphRef, features @@ -419,6 +420,22 @@ const Chat = forwardRef { + const opening_statement = features?.opening_statement + + if (opening_statement?.enabled && opening_statement?.statement && opening_statement?.statement.trim() !== '') { + const assistantMsg: ChatItem = { + role: 'assistant', + content: replaceVariables(opening_statement.statement, variables as any), + meta_data: { + suggested_questions: opening_statement?.suggested_questions + } + } + console.log('variables', assistantMsg) + setChatList(prev => [assistantMsg, ...prev.slice(1)]) + } + }, [chatList.length, features?.opening_statement, variables]) + return ( diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index 5f6844df..a631c9a3 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-23 16:22:51 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-03 20:44:16 + * @Last Modified time: 2026-04-07 16:29:36 */ import { type FC, useState, useMemo } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; @@ -57,7 +57,6 @@ const Editor: FC =({ language = 'string', height, className, - waitForInit = false, }) => { console.log('Editor value', value) const [_count, setCount] = useState(0); @@ -149,10 +148,10 @@ const Editor: FC =({ /> - + - - + +
); diff --git a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx index 24560395..5688342c 100644 --- a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx +++ b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx @@ -33,6 +33,18 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({ setSelected(!isSelected); }; + if (!data.nodeData?.name) { + return ( + + {data.value} + + ); + } + return ( = ({ options }) => { // Group suggestions by node ID const groupedSuggestions = options.reduce((groups: Record, suggestion) => { const { nodeData } = suggestion - const nodeId = nodeData.id as string; + const nodeId = nodeData?.id as string; if (!groups[nodeId]) { groups[nodeId] = []; } @@ -291,67 +291,67 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { }} >
- - {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => { - const nodeName = nodeOptions[0]?.nodeData?.name || nodeId; - const nodeIcon = nodeOptions[0]?.nodeData?.icon; - return ( -
- - {nodeIcon &&
} - {nodeName} - - {nodeOptions.map((option) => { - const globalIndex = flatOptions.indexOf(option); - const isExpanded = expandedParent?.key === option.key; - const hasChildren = !!option.children?.length; - return ( - { if (el) itemRefs.current.set(option.key, el); }} - data-selected={selectedIndex === globalIndex} - className="rb:pl-6! rb:pr-3! rb:py-2!" - align="center" - justify="space-between" - style={{ - cursor: option.disabled ? 'not-allowed' : 'pointer', - background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white', - opacity: option.disabled ? 0.5 : 1, - }} - onClick={() => { - if (option.disabled) return; - insertMention(option); - }} - onMouseEnter={() => { - setSelectedIndex(globalIndex); - if (hasChildren) { - const el = itemRefs.current.get(option.key); - if (el && popupRef.current) { - const elRect = el.getBoundingClientRect(); - const popupRect = popupRef.current.getBoundingClientRect(); - setChildPanelTop(calcChildPanelTop(elRect, popupRect)); + + {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => { + const nodeName = nodeOptions[0]?.nodeData?.name || nodeId; + const nodeIcon = nodeOptions[0]?.nodeData?.icon; + return ( +
+ {nodeName !== 'undefined' && + {nodeIcon &&
} + {nodeName} + } + {nodeOptions.map((option) => { + const globalIndex = flatOptions.indexOf(option); + const isExpanded = expandedParent?.key === option.key; + const hasChildren = !!option.children?.length; + return ( + { if (el) itemRefs.current.set(option.key, el); }} + data-selected={selectedIndex === globalIndex} + className="rb:pl-6! rb:pr-3! rb:py-2!" + align="center" + justify="space-between" + style={{ + cursor: option.disabled ? 'not-allowed' : 'pointer', + background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white', + opacity: option.disabled ? 0.5 : 1, + }} + onClick={() => { + if (option.disabled) return; + insertMention(option); + }} + onMouseEnter={() => { + setSelectedIndex(globalIndex); + if (hasChildren) { + const el = itemRefs.current.get(option.key); + if (el && popupRef.current) { + const elRect = el.getBoundingClientRect(); + const popupRect = popupRef.current.getBoundingClientRect(); + setChildPanelTop(calcChildPanelTop(elRect, popupRect)); + } + setExpandedParent(option); + } else { + setExpandedParent(null); } - setExpandedParent(option); - } else { - setExpandedParent(null); - } - }} - > - - {option.isContext ? '📄' : `{x}`} - {option.label} - - - {option.dataType && {option.dataType}} - {hasChildren && } - - - ); - })} -
- ); - })} -
+ }} + > + {option.label && + {option.isContext ? '📄' : `{x}`} + {option.label} + } + + {option.dataType && {option.dataType}} + {hasChildren && } + + + ); + })} +
+ ); + })} +
{/* Child variables panel - floats to the left */} {expandedParent?.children?.length && ( diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index e42fae01..86d6007f 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:17:48 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-31 11:13:23 + * @Last Modified time: 2026-04-07 16:47:09 */ import { useRef, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -18,6 +18,7 @@ import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application' import { useUser } from '@/store/user'; import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types' import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils' +import type { Suggestion } from '../components/Editor/plugin/AutocompletePlugin'; /** * Props for useWorkflowGraph hook @@ -73,6 +74,8 @@ export interface UseWorkflowGraphReturn { handleAddNotes: () => void; handleSaveFeaturesConfig: (value: FeaturesConfigForm) => void; features?: FeaturesConfigForm; + /** Get start node output variable list (user-defined + system variables) */ + getStartNodeVariables: () => Array<{ name: string; type: string; readonly?: boolean }>; } /** @@ -1363,9 +1366,49 @@ export const useWorkflowGraph = ({ data: { ...cleanNodeData }, }); } + const getStartNodeVariables = (): Array<{ name: string; type: string; readonly?: boolean }> => { + const startNode = graphRef.current?.getNodes().find(n => n.getData()?.type === 'start') + if (!startNode) return [] + const data = startNode.getData() + const userVars: Array<{ name: string; type: string; readonly?: boolean }> = + (data?.config?.variables?.defaultValue ?? []).map((v: any) => ({ name: v.name, type: v.type })) + return userVars + } + const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => { + const { statement = '' } = value?.opening_statement || {} featuresRef.current = value onFeaturesLoad?.(value) + + const usedVars = [...new Set([...(statement?.matchAll(/\{\{(\w+)\}\}/g) ?? [])].map(m => m[1]))] + const startVars = getStartNodeVariables() + const validNames = new Set(startVars.map(v => v.name)) + const invalid = usedVars.filter(v => !validNames.has(v)) + if (invalid.length > 0) { + const newVars = invalid.map(name => ({ + name, + description: name, + type: 'string', + required: true, + defaultValue: '', + })) + + const startNode = graphRef.current?.getNodes().find(n => n.getData()?.type === 'start') + if (startNode) { + const data = startNode.getData() + console.log('startNode', [...startVars, ...newVars]) + startNode.setData({ + ...data, + config: { + ...data.config, + variables: { + ...data.config.variables, + defaultValue: [...startVars, ...newVars], + }, + }, + }) + } + } } return { @@ -1389,5 +1432,6 @@ export const useWorkflowGraph = ({ handleAddNotes, handleSaveFeaturesConfig, features: featuresRef.current, + getStartNodeVariables, }; }; diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx index 34ef91a7..db3fe352 100644 --- a/web/src/views/Workflow/index.tsx +++ b/web/src/views/Workflow/index.tsx @@ -6,10 +6,11 @@ import Properties from './components/Properties'; import CanvasToolbar from './components/CanvasToolbar'; import PortClickHandler from './components/PortClickHandler'; import { useWorkflowGraph } from './hooks/useWorkflowGraph'; -import type { WorkflowRef, FeaturesConfigForm } from '@/views/ApplicationConfig/types' +import type { WorkflowRef, FeaturesConfigForm, FeaturesConfigModalRef } from '@/views/ApplicationConfig/types' import Chat from './components/Chat/Chat'; import type { ChatRef, AddChatVariableRef } from './types' import AddChatVariable from './components/AddChatVariable'; +import FeaturesConfigModal from '@/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal' const Workflow = forwardRef void }>(({ onFeaturesLoad }, ref) => { const containerRef = useRef(null); @@ -35,7 +36,8 @@ const Workflow = forwardRef { @@ -51,6 +53,15 @@ const Workflow = forwardRef(null) + + /** Open the feature config modal pre-populated with the current values */ + const handleFeaturesConfig = () => { + blankClick() + funConfigModalRef.current?.handleOpen(features as FeaturesConfigForm) + } + useImperativeHandle(ref, () => ({ handleSave, handleRun, @@ -59,6 +70,7 @@ const Workflow = forwardRef + {/* Modal for editing feature settings; calls refresh on save */} + ({ name: v.name, key: `start_${v.name}`, label: v.name, type: 'variable', dataType: v.type, value:`{{${v.name}}}` })) as any} + />
); }); From af86cb3556394b561f9e116c84ccffddc2ed319b Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 17:12:21 +0800 Subject: [PATCH 5/5] fix(web): tool node number support variable --- .../Properties/ToolConfig/index.tsx | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx index 8ae3cd4d..7857eb23 100644 --- a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useState } from "react"; +import { type FC, useEffect, useState, useMemo } from "react"; import { useTranslation } from 'react-i18next' import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps, Tooltip } from 'antd' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' @@ -163,6 +163,24 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ form.setFieldsValue(inititalValue) } + const getNumberOptions = useMemo(() => { + const list: Suggestion[] = [] + + options.forEach(vo => { + if (vo.children && vo?.children?.length > 0) { + const filterChild = vo.children.filter(child => child.dataType === 'number') + + if (filterChild.length > 0) { + list.push({ ...vo, children: filterChild }) + } else { + list.push({ ...vo, children: [] }) + } + } else { + list.push({ ...vo }) + } + }) + return list + }, [options]) return ( <> @@ -202,15 +220,14 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ : parameter.type === 'boolean' ? : parameter.type === 'integer' || parameter.type === 'number' - ? form.setFieldValue(['tool_parameters', parameter.name], value)} - /> + ? :