feat(web): workflow support lexical editor
This commit is contained in:
@@ -1569,6 +1569,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
export: 'Export',
|
||||
variableConfig: 'Variable Configuration',
|
||||
variableRequired: 'required',
|
||||
addMessage: 'Add Message',
|
||||
answerDesc: 'Reply'
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
|
||||
@@ -1657,6 +1657,8 @@ export const zh = {
|
||||
export: '导出',
|
||||
variableConfig: '变量配置',
|
||||
variableRequired: '必填',
|
||||
addMessage: '添加消息',
|
||||
answerDesc: '回复'
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
|
||||
86
web/src/views/Workflow/components/Editor/index.tsx
Normal file
86
web/src/views/Workflow/components/Editor/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||
import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin'
|
||||
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||
|
||||
interface LexicalEditorProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
suggestions: Suggestion[];
|
||||
}
|
||||
|
||||
const theme = {
|
||||
paragraph: 'editor-paragraph',
|
||||
text: {
|
||||
bold: 'editor-text-bold',
|
||||
italic: 'editor-text-italic',
|
||||
},
|
||||
};
|
||||
|
||||
const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
onChange,
|
||||
suggestions,
|
||||
}) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const initialConfig = {
|
||||
namespace: 'AutocompleteEditor',
|
||||
theme,
|
||||
onError: (error: Error) => {
|
||||
console.error(error);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: '60px',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
color: '#5B6167',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<AutoFocusPlugin />
|
||||
<AutocompletePlugin suggestions={suggestions} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} />
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
113
web/src/views/Workflow/components/Editor/nodes/TagNode.tsx
Normal file
113
web/src/views/Workflow/components/Editor/nodes/TagNode.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
DecoratorNode,
|
||||
} from 'lexical';
|
||||
import type { NodeKey, SerializedLexicalNode, Spread } from 'lexical';
|
||||
import React from 'react';
|
||||
|
||||
export type SerializedTagNode = Spread<
|
||||
{
|
||||
label: string;
|
||||
tagType: string;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class TagNode extends DecoratorNode<JSX.Element> {
|
||||
__label: string;
|
||||
__type: string;
|
||||
|
||||
static getType(): string {
|
||||
return 'tagNode';
|
||||
}
|
||||
|
||||
static clone(node: TagNode): TagNode {
|
||||
return new TagNode(node.__label, node.__type, node.__key);
|
||||
}
|
||||
|
||||
constructor(label: string, type: string, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__label = label;
|
||||
this.__type = type;
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
return document.createElement('span');
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTagNode): TagNode {
|
||||
const { label, tagType } = serializedNode;
|
||||
return $createTagNode(label, tagType);
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTagNode {
|
||||
return {
|
||||
label: this.__label,
|
||||
tagType: this.__type,
|
||||
type: 'tagNode',
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return this.__label;
|
||||
}
|
||||
|
||||
decorate(): JSX.Element {
|
||||
const getIconAndColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'context':
|
||||
return { icon: '📄', bgColor: '#722ed1' };
|
||||
case 'system':
|
||||
return { icon: 'x', bgColor: '#1890ff' };
|
||||
default:
|
||||
return { icon: 'x', bgColor: '#52c41a' };
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, bgColor } = getIconAndColor(this.__type);
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
background: '#f0f8ff',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
padding: '2px 6px',
|
||||
fontSize: '14px',
|
||||
margin: '0 2px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
background: bgColor,
|
||||
color: 'white',
|
||||
padding: '1px 4px',
|
||||
borderRadius: '2px',
|
||||
fontSize: '10px',
|
||||
minWidth: '12px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{this.__label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createTagNode(label: string, type: string): TagNode {
|
||||
return new TagNode(label, type);
|
||||
}
|
||||
|
||||
export function $isTagNode(node: any): node is TagNode {
|
||||
return node instanceof TagNode;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getRoot, $createTextNode, $createParagraphNode, $setSelection, $createRangeSelection, $getSelection } from 'lexical';
|
||||
import type { NodeProperties } from '../../../types'
|
||||
export interface Suggestion {
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
dataType: string;
|
||||
value: string;
|
||||
nodeData: NodeProperties
|
||||
}
|
||||
const AutocompletePlugin: FC<{ suggestions: Suggestion[] }> = ({ suggestions }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const text = root.getTextContent();
|
||||
const shouldShow = text.includes('/');
|
||||
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();
|
||||
|
||||
// Calculate popup dimensions
|
||||
const popupWidth = 280;
|
||||
const popupHeight = 200;
|
||||
|
||||
// Get viewport dimensions
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Calculate position with viewport constraints
|
||||
let left = rect.left;
|
||||
let top = rect.top - 10;
|
||||
|
||||
// Adjust horizontal position if popup would overflow
|
||||
if (left + popupWidth > viewportWidth) {
|
||||
left = viewportWidth - popupWidth - 10;
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10;
|
||||
}
|
||||
|
||||
// Adjust vertical position if popup would overflow
|
||||
if (top - popupHeight < 10) {
|
||||
// Show below cursor if not enough space above
|
||||
top = rect.bottom + 10;
|
||||
if (top + popupHeight > viewportHeight) {
|
||||
top = viewportHeight - popupHeight - 10;
|
||||
}
|
||||
}
|
||||
|
||||
setPopupPosition({ top, left });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
const insertMention = (suggestion: any) => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const text = root.getTextContent();
|
||||
const lastSlashIndex = text.lastIndexOf('/');
|
||||
const beforeSlash = text.slice(0, lastSlashIndex);
|
||||
const afterSlash = text.slice(lastSlashIndex + 1);
|
||||
const insertedText = `{{${suggestion.value}}} `;
|
||||
const newText = beforeSlash + insertedText + afterSlash;
|
||||
const cursorPosition = beforeSlash.length + insertedText.length;
|
||||
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
paragraph.append($createTextNode(newText));
|
||||
root.append(paragraph);
|
||||
|
||||
// Set cursor after the inserted text
|
||||
const textNode = paragraph.getFirstChild();
|
||||
if (textNode) {
|
||||
const selection = $createRangeSelection();
|
||||
selection.anchor.set(textNode.getKey(), cursorPosition, 'text');
|
||||
selection.focus.set(textNode.getKey(), cursorPosition, 'text');
|
||||
$setSelection(selection);
|
||||
}
|
||||
});
|
||||
setShowSuggestions(false);
|
||||
};
|
||||
|
||||
if (!showSuggestions) return null;
|
||||
|
||||
// Group suggestions by node name
|
||||
const groupedSuggestions = suggestions.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const { nodeData } = suggestion
|
||||
const nodeName = (nodeData.name || nodeData.id) as string;
|
||||
if (!groups[nodeName]) {
|
||||
groups[nodeName] = [];
|
||||
}
|
||||
groups[nodeName].push(suggestion);
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: popupPosition.top,
|
||||
left: popupPosition.left,
|
||||
zIndex: 1000,
|
||||
background: 'white',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
minWidth: '280px',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
transform: 'translateY(-100%)',
|
||||
}}
|
||||
>
|
||||
{Object.entries(groupedSuggestions).map(([nodeName, nodeOptions], groupIndex) => (
|
||||
<div key={nodeName}>
|
||||
{groupIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||
{nodeName}
|
||||
</div>
|
||||
{nodeOptions.map((option, index) => {
|
||||
const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option);
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
background: selectedIndex === globalIndex ? '#f0f8ff' : 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onClick={() => insertMention(option)}
|
||||
onMouseEnter={() => setSelectedIndex(globalIndex)}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span
|
||||
style={{
|
||||
background: option.type === 'context' ? '#722ed1' :
|
||||
option.type === 'system' ? '#1890ff' : '#52c41a',
|
||||
color: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
minWidth: '16px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{option.type === 'context' ? '📄' :
|
||||
option.type === 'system' ? 'x' : 'x'}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px' }}>{option.label}</span>
|
||||
</div>
|
||||
{option.dataType && (
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>
|
||||
{option.dataType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default AutocompletePlugin
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { $getRoot } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const textContent = root.getTextContent();
|
||||
setCount(textContent.length);
|
||||
onChange?.(textContent);
|
||||
});
|
||||
});
|
||||
}, [editor, setCount, onChange]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default CharacterCountPlugin
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
||||
|
||||
interface InitialValuePluginProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
if (root.getTextContent() === '') {
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
paragraph.append($createTextNode(value));
|
||||
root.append(paragraph);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default InitialValuePlugin;
|
||||
@@ -1,13 +1,20 @@
|
||||
import { type FC } from 'react';
|
||||
import { type FC, useMemo } 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 { Graph, Node } from '@antv/x6';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface TextareaProps {
|
||||
isArray?: boolean;
|
||||
parentName?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value?: string) => void;
|
||||
selectedNode?: Node | null;
|
||||
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
@@ -15,11 +22,92 @@ const roleOptions = [
|
||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||
]
|
||||
const MessageEditor: FC<TextareaProps> = ({
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
selectedNode,
|
||||
graphRef,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
const values = form.getFieldsValue()
|
||||
|
||||
const suggestions = useMemo(() => {
|
||||
if (!selectedNode || !graphRef?.current) return [];
|
||||
|
||||
const suggestions: Suggestion[] = [];
|
||||
const graph = graphRef.current;
|
||||
const edges = graph.getEdges();
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find all connected previous nodes (recursive)
|
||||
const getAllPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
if (visited.has(nodeId)) return [];
|
||||
visited.add(nodeId);
|
||||
|
||||
const directPrevious = edges
|
||||
.filter(edge => edge.getTargetCellId() === nodeId)
|
||||
.map(edge => edge.getSourceCellId());
|
||||
|
||||
const allPrevious = [...directPrevious];
|
||||
directPrevious.forEach(prevNodeId => {
|
||||
allPrevious.push(...getAllPreviousNodes(prevNodeId, visited));
|
||||
});
|
||||
|
||||
return allPrevious;
|
||||
};
|
||||
|
||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||
console.log('allPreviousNodeIds', allPreviousNodeIds)
|
||||
|
||||
allPreviousNodeIds.forEach(nodeId => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const nodeData = node.getData();
|
||||
|
||||
switch(nodeData.type) {
|
||||
case 'start':
|
||||
const list = [
|
||||
...(nodeData.config?.variables?.defaultValue ?? []),
|
||||
...(nodeData.config?.variables?.value ?? [])
|
||||
]
|
||||
list.forEach((variable: any) => {
|
||||
suggestions.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `${nodeId}.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
});
|
||||
nodeData.config?.variables?.sys.forEach((variable: any) => {
|
||||
suggestions.push({
|
||||
key: `${nodeId}_${variable.name}`,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `sys.${variable.name}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
});
|
||||
break
|
||||
case 'llm':
|
||||
suggestions.push({
|
||||
key: `${nodeId}_output`,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
value: `${nodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
break
|
||||
}
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
}, [selectedNode, graphRef]);
|
||||
|
||||
const handleAdd = (add: FormListOperation['add']) => {
|
||||
const list = values[parentName];
|
||||
@@ -30,58 +118,74 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
content: undefined
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const currentRole = values[parentName]?.[key].role || 'USER'
|
||||
|
||||
return (
|
||||
<Space key={key} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'role']}
|
||||
noStyle
|
||||
>
|
||||
{currentRole === 'SYSTEM'
|
||||
? <Input disabled />
|
||||
:
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && <Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</div>
|
||||
</Col>}
|
||||
</Row>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'content']}
|
||||
noStyle
|
||||
>
|
||||
<Input.TextArea placeholder={placeholder} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)
|
||||
})}
|
||||
<Form.Item className="rb:mt-3!">
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block icon={<PlusOutlined />}>
|
||||
Add field
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
{isArray
|
||||
? <Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const currentRole = values[parentName]?.[key].role || 'USER'
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'role']}
|
||||
noStyle
|
||||
>
|
||||
{currentRole === 'SYSTEM'
|
||||
? <Input disabled />
|
||||
:
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && <Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</div>
|
||||
</Col>}
|
||||
</Row>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'content']}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} suggestions={suggestions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block icon={<PlusOutlined />}>
|
||||
+{t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space >
|
||||
)}
|
||||
</Form.List>
|
||||
:
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{t('workflow.answerDesc')}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
name={parentName}
|
||||
noStyle
|
||||
>
|
||||
<Editor placeholder={placeholder} suggestions={suggestions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ interface PropertiesProps {
|
||||
}
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
@@ -60,9 +61,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('values', values)
|
||||
if (values && selectedNode) {
|
||||
const { id, ...rest } = values
|
||||
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
if (selectedNode.data?.config[key]) {
|
||||
selectedNode.data.config[key].defaultValue = values[key]
|
||||
@@ -181,7 +184,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
if (selectedNode.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor />
|
||||
<MessageEditor selectedNode={selectedNode} graphRef={graphRef} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor isArray={false} parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: "end", icon: endIcon,
|
||||
config: {
|
||||
output: {
|
||||
type: 'textarea'
|
||||
type: 'define'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -121,7 +121,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: 'define',
|
||||
defaultValue: [
|
||||
{
|
||||
role: 'SYSTEM',
|
||||
role: 'system',
|
||||
content: undefined,
|
||||
readonly: true
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface NodeConfig {
|
||||
export interface NodeProperties {
|
||||
type: string;
|
||||
icon: string;
|
||||
name?: string;
|
||||
id?: string;
|
||||
config?: Record<string, NodeConfig>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user