fix(web): workflow statement support variable

This commit is contained in:
zhaoying
2026-04-07 17:06:53 +08:00
parent 9a4a614fc8
commit c3ee3c4af9
12 changed files with 198 additions and 98 deletions

View File

@@ -91,10 +91,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
},
{
key: '3',
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/userInfo.svg')]"></div>,
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/userInfo.svg')]"></div>,
label: <Flex justify="space-between" align="center">
{t('header.userInfo')}
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/arrow_t_r.svg')]"></div>
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/arrow_t_r.svg')]"></div>
</Flex>,
className: 'rb:text-[#212332]!',
onClick: () => {
@@ -103,10 +103,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
},
{
key: '4',
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/settings.svg')]"></div>,
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/settings.svg')]"></div>,
label: <Flex justify="space-between" align="center">
{t('header.settings')}
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/arrow_t_r.svg')]"></div>
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/arrow_t_r.svg')]"></div>
</Flex>,
className: 'rb:text-[#212332]!',
onClick: () => {
@@ -120,7 +120,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
},
{
key: '6',
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/logout_red.svg')]"></div>,
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/logout_red.svg')]"></div>,
label: t('header.logout'),
danger: true,
className: 'rb:hover:rb:bg-transparent rb:hover:text-[#FF5D34]!',

View File

@@ -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<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh,
workflowRef,
appRef,
features,
onFeaturesChange,
}) => {
const { t } = useTranslation();
@@ -175,10 +173,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
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<ConfigHeaderProps> = ({
</Flex>}
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
<FeaturesConfig
source={application?.type}
value={features as FeaturesConfigForm}
refresh={handleSaveFeaturesConfig}
chatVariables={(workflowRef.current?.chatVariables || []).map(v => ({ ...v, display_name: v.name }))}
/>
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
onClick={handleFeaturesConfig}
></div>
</Popover>
<Popover content={t('workflow.clear')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
<div
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/clear.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 16:27:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-02 17:49:51
* @Last Modified time: 2026-04-07 16:13:44
*/
/**
* Copy Application Modal
@@ -205,6 +205,7 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
/>
<OpenStatementSettingModal
ref={openStatementSettingModalRef}
source={source}
chatVariables={chatVariables}
onSave={handleSaveStatement}
/>

View File

@@ -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<OpenStatementSettingModalRef, OpenStatementSettingModalProps>(({
onSave,
chatVariables = []
chatVariables = [],
source
}, ref) => {
const { t } = useTranslation();
const { modal } = App.useApp()
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<FeaturesConfigForm['opening_statement']>();
console.log('chatVariables', chatVariables)
const handleClose = () => {
setVisible(false);
form.resetFields();
@@ -49,6 +55,7 @@ const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenS
if (values?.enabled && values?.statement && values?.statement?.trim() !== '') {
const usedVars = [...new Set([...values.statement?.matchAll(/\{\{(\w+)\}\}/g)].map(m => 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<OpenStatementSettingModalRef, OpenS
label={t('application.opening_statement')}
name="statement"
>
<Input.TextArea
placeholder={t('common.pleaseEnter')}
/>
{source === 'workflow'
? <Editor options={chatVariables as any} variant="outlined" />
: <Input.TextArea
placeholder={t('common.pleaseEnter')}
/>
}
</Form.Item>
<Form.List name="suggested_questions">

View File

@@ -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;
}

View File

@@ -48,8 +48,8 @@ const ConversationMemory: FC = () => {
gap={12}
>
<div className={clsx("rb:size-8 rb:bg-cover", {
'rb:bg-[url(src/assets/images/conversation/user.png)]': item.role === 'user',
'rb:bg-[url(src/assets/images/conversation/ai.png)]': item.role === 'assistant',
'rb:bg-[url(@/assets/images/conversation/user.png)]': item.role === 'user',
'rb:bg-[url(@/assets/images/conversation/ai.png)]': item.role === 'assistant',
})}></div>
<div
className="rb:flex-1"

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-02 18:01:09
* @Last Modified time: 2026-04-07 17:06:02
*/
/**
* Workflow Chat Component
@@ -40,6 +40,7 @@ import ChatToolbar from '@/components/Chat/ChatToolbar'
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
import Runtime from './Runtime';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
import { replaceVariables } from '@/views/ApplicationConfig/Agent';
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({
appId, graphRef, features
@@ -419,6 +420,22 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
handleClose
}));
useEffect(() => {
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 (
<RbDrawer
title={<Flex align="center" gap={10}>

View File

@@ -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<LexicalEditorProps> =({
language = 'string',
height,
className,
waitForInit = false,
}) => {
console.log('Editor value', value)
const [_count, setCount] = useState(0);
@@ -149,10 +148,10 @@ const Editor: FC<LexicalEditorProps> =({
/>
<HistoryPlugin />
<CommandPlugin />
<AutocompletePlugin options={options} enableJinja2={false} />
<AutocompletePlugin options={options} />
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
<BlurPlugin enableJinja2={false} />
<InitialValuePlugin value={value} options={options} />
<BlurPlugin />
</div>
</LexicalComposer>
);

View File

@@ -33,6 +33,18 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
setSelected(!isSelected);
};
if (!data.nodeData?.name) {
return (
<span
onClick={handleClick}
className="rb:inline rb:cursor-pointer rb:text-[#171719]"
contentEditable={false}
>
{data.value}
</span>
);
}
return (
<span
onClick={handleClick}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-23 16:22:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-02 17:12:41
* @Last Modified time: 2026-04-07 16:51:04
*/
import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
@@ -168,7 +168,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
// Group suggestions by node ID
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, 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 }) => {
}}
>
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
<Flex vertical gap={12}>
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
const nodeIcon = nodeOptions[0]?.nodeData?.icon;
return (
<div key={nodeId}>
<Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
{nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />}
{nodeName}
</Flex>
{nodeOptions.map((option) => {
const globalIndex = flatOptions.indexOf(option);
const isExpanded = expandedParent?.key === option.key;
const hasChildren = !!option.children?.length;
return (
<Flex
key={option.key}
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
data-selected={selectedIndex === globalIndex}
className="rb:pl-6! rb:pr-3! rb:py-2!"
align="center"
justify="space-between"
style={{
cursor: option.disabled ? 'not-allowed' : 'pointer',
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
opacity: option.disabled ? 0.5 : 1,
}}
onClick={() => {
if (option.disabled) return;
insertMention(option);
}}
onMouseEnter={() => {
setSelectedIndex(globalIndex);
if (hasChildren) {
const el = itemRefs.current.get(option.key);
if (el && popupRef.current) {
const elRect = el.getBoundingClientRect();
const popupRect = popupRef.current.getBoundingClientRect();
setChildPanelTop(calcChildPanelTop(elRect, popupRect));
<Flex vertical gap={12}>
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
const nodeIcon = nodeOptions[0]?.nodeData?.icon;
return (
<div key={nodeId}>
{nodeName !== 'undefined' && <Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
{nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />}
{nodeName}
</Flex>}
{nodeOptions.map((option) => {
const globalIndex = flatOptions.indexOf(option);
const isExpanded = expandedParent?.key === option.key;
const hasChildren = !!option.children?.length;
return (
<Flex
key={option.key}
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
data-selected={selectedIndex === globalIndex}
className="rb:pl-6! rb:pr-3! rb:py-2!"
align="center"
justify="space-between"
style={{
cursor: option.disabled ? 'not-allowed' : 'pointer',
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
opacity: option.disabled ? 0.5 : 1,
}}
onClick={() => {
if (option.disabled) return;
insertMention(option);
}}
onMouseEnter={() => {
setSelectedIndex(globalIndex);
if (hasChildren) {
const el = itemRefs.current.get(option.key);
if (el && popupRef.current) {
const elRect = el.getBoundingClientRect();
const popupRect = popupRef.current.getBoundingClientRect();
setChildPanelTop(calcChildPanelTop(elRect, popupRect));
}
setExpandedParent(option);
} else {
setExpandedParent(null);
}
setExpandedParent(option);
} else {
setExpandedParent(null);
}
}}
>
<Space size={4}>
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : `{x}`}</span>
<span>{option.label}</span>
</Space>
<Space size={4}>
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1"></span>}
</Space>
</Flex>
);
})}
</div>
);
})}
</Flex>
}}
>
{option.label && <Space size={4}>
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : `{x}`}</span>
<span>{option.label}</span>
</Space>}
<Space size={4}>
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1"></span>}
</Space>
</Flex>
);
})}
</div>
);
})}
</Flex>
</div>
{/* Child variables panel - floats to the left */}
{expandedParent?.children?.length && (

View File

@@ -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,
};
};

View File

@@ -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<WorkflowRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -35,7 +36,8 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
setChatVariables,
handleAddNotes,
handleSaveFeaturesConfig,
features
features,
getStartNodeVariables,
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
const onDragOver = (event: React.DragEvent) => {
@@ -51,6 +53,15 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
addChatVariableRef.current?.handleOpen()
}
// Ref used to imperatively open the config modal
const funConfigModalRef = useRef<FeaturesConfigModalRef>(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<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
chatVariables,
config,
features: features,
handleFeaturesConfig,
handleSaveFeaturesConfig
}))
return (
@@ -112,6 +124,13 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
variables={chatVariables}
onChange={setChatVariables}
/>
{/* Modal for editing feature settings; calls refresh on save */}
<FeaturesConfigModal
ref={funConfigModalRef}
refresh={handleSaveFeaturesConfig}
source="workflow"
chatVariables={getStartNodeVariables().map(v => ({ name: v.name, key: `start_${v.name}`, label: v.name, type: 'variable', dataType: v.type, value:`{{${v.name}}}` })) as any}
/>
</div>
);
});