Merge #38 into develop_web from feature/20251219_zy

feat(web): update reflection engine result

* feature/20251219_zy: (4 commits)
  fix(web): workflow properties
  feat(web): workflow support lexical editor
  feat(web): workflow support lexical editor
  feat(web): update reflection engine result

Signed-off-by: zhaoying <zhaoying@redbearai.com>
Merged-by: zhaoying <zhaoying@redbearai.com>

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/38
This commit is contained in:
赵莹
2025-12-23 17:17:46 +08:00
13 changed files with 672 additions and 106 deletions

View File

@@ -1579,6 +1579,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',

View File

@@ -1666,6 +1666,8 @@ export const zh = {
export: '导出',
variableConfig: '变量配置',
variableRequired: '必填',
addMessage: '添加消息',
answerDesc: '回复'
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',

View File

@@ -272,69 +272,75 @@ const SelfReflectionEngine: React.FC = () => {
</div>
</div>
</RbCard>
<RbCard
title={t('reflectionEngine.conflictDetection')}
>
<Space size={12} direction="vertical" className="rb:w-full">
{result.reflexion_data.map((item, index) => (
{result.reflexion_data.length > 0 && (
<RbCard
title={t('reflectionEngine.conflictDetection')}
>
<Space size={12} direction="vertical" className="rb:w-full">
{result.reflexion_data.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['reason', 'solution'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof ReflexionData]}
</div>
</div>
))}
</div>
))}
</Space>
</RbCard>
)}
{result.quality_assessments.length > 0 && (
<RbCard
title={t('reflectionEngine.qualityAssessment')}
>
{result.quality_assessments.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['reason', 'solution'].map(key => (
{['score', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.${key}`)}</div>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.qualityAssessmentObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof ReflexionData]}
{item[key as keyof QualityAssessment]}
</div>
</div>
))}
</div>
))}
</Space>
</RbCard>
<RbCard
title={t('reflectionEngine.qualityAssessment')}
>
{result.quality_assessments.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['score', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.qualityAssessmentObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{item[key as keyof QualityAssessment]}
</RbCard>
)}
{result.memory_verifies.length > 0 && (
<RbCard
title={t('reflectionEngine.privacyAudit')}
>
{result.memory_verifies.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['has_privacy', 'privacy_types', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.privacyAuditObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{key === 'has_privacy'
? <Tag color={item[key as keyof MemoryVerify] ? 'success' : 'error'}>{t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}</Tag>
: key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
: item[key as keyof MemoryVerify]
}
</div>
</div>
</div>
))}
</div>
))}
</RbCard>
<RbCard
title={t('reflectionEngine.privacyAudit')}
>
{result.memory_verifies.map((item, index) => (
<div key={index} className="rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md rb:text-[12px]">
{['has_privacy', 'privacy_types', 'summary'].map(key => (
<div
key={key}
className="rb:flex rb:gap-4 rb:justify-start rb:text-[14px] rb:leading-5 rb:mb-3"
>
<div className="rb:whitespace-nowrap rb:w-45 rb:font-medium">{t(`reflectionEngine.privacyAuditObj.${key}`)}</div>
<div className='rb:flex-inline rb:text-left rb:py-px rb:rounded rb:text-[#5B6167] rb:flex-1'>
{key === 'has_privacy'
? <Tag color={item[key as keyof MemoryVerify] ? 'success' : 'error'}>{t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}</Tag>
: key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
: item[key as keyof MemoryVerify]
}
</div>
</div>
))}
</div>
))}
</RbCard>
))}
</div>
))}
</RbCard>
)}
</>}
</Space>
</Col>

View 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;

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

View File

@@ -0,0 +1,186 @@
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 id
const groupedSuggestions = suggestions.reduce((groups: Record<string, any[]>, suggestion) => {
const { nodeData } = suggestion
const nodeId = nodeData.id as string;
if (!groups[nodeId]) {
groups[nodeId] = [];
}
groups[nodeId].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(([nodeId, nodeOptions], groupIndex) => {
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
return (
<div key={nodeId}>
{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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ interface PropertiesProps {
}
const Properties: FC<PropertiesProps> = ({
selectedNode,
graphRef,
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
@@ -30,6 +31,12 @@ const Properties: FC<PropertiesProps> = ({
const variableModalRef = useRef<VariableEditModalRef>(null)
const [editIndex, setEditIndex] = useState<number | null>(null)
useEffect(() => {
if (selectedNode?.getData().id) {
form.resetFields()
}
}, [selectedNode?.getData().id])
useEffect(() => {
if (selectedNode && form) {
const { type = 'default', name = '', config } = selectedNode.getData() || {}
@@ -180,7 +187,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>
)
}

View File

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

View File

@@ -624,7 +624,7 @@ export const useWorkflowGraph = ({
let nodeLibraryConfig = [...nodeLibrary]
.flatMap(category => category.nodes)
.find(n => n.type === dragData.type);
nodeLibraryConfig = { config: {}, ...nodeLibraryConfig } as NodeProperties;
nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties
// 创建干净的节点数据,只保留必要的字段
const cleanNodeData = {

View File

@@ -26,6 +26,8 @@ export interface NodeConfig {
export interface NodeProperties {
type: string;
icon: string;
name?: string;
id?: string;
config?: Record<string, NodeConfig>;
}