fix(web): workflow statement support variable
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user