- {['score', 'summary'].map(key => (
-
-
{t(`reflectionEngine.qualityAssessmentObj.${key}`)}
-
- {item[key as keyof QualityAssessment]}
+
+ )}
+ {result.memory_verifies.length > 0 && (
+
+ {result.memory_verifies.map((item, index) => (
+
+ {['has_privacy', 'privacy_types', 'summary'].map(key => (
+
+
{t(`reflectionEngine.privacyAuditObj.${key}`)}
+
+ {key === 'has_privacy'
+ ? {t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}
+ : key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
+ : item[key as keyof MemoryVerify]
+ }
+
-
- ))}
-
- ))}
-
-
- {result.memory_verifies.map((item, index) => (
-
- {['has_privacy', 'privacy_types', 'summary'].map(key => (
-
-
{t(`reflectionEngine.privacyAuditObj.${key}`)}
-
- {key === 'has_privacy'
- ? {t(`reflectionEngine.privacyAuditObj.${item[key as keyof MemoryVerify]}`)}
- : key === 'privacy_types' ? (item[key as keyof MemoryVerify] as string[]).join('、')
- : item[key as keyof MemoryVerify]
- }
-
-
- ))}
-
- ))}
-
+ ))}
+
+ ))}
+
+ )}
>}
diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx
new file mode 100644
index 00000000..ac96dd73
--- /dev/null
+++ b/web/src/views/Workflow/components/Editor/index.tsx
@@ -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
=({
+ placeholder = "请输入内容...",
+ value = "",
+ onChange,
+ suggestions,
+}) => {
+ const [count, setCount] = useState(0);
+ const initialConfig = {
+ namespace: 'AutocompleteEditor',
+ theme,
+ onError: (error: Error) => {
+ console.error(error);
+ },
+ };
+
+ return (
+
+
+
+ }
+ placeholder={
+
+ {placeholder}
+
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+
+
+
+
{ setCount(count) }} onChange={onChange} />
+
+
+
+ );
+};
+
+export default Editor;
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Editor/nodes/TagNode.tsx b/web/src/views/Workflow/components/Editor/nodes/TagNode.tsx
new file mode 100644
index 00000000..f4264bfe
--- /dev/null
+++ b/web/src/views/Workflow/components/Editor/nodes/TagNode.tsx
@@ -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 {
+ __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 (
+
+
+ {icon}
+
+ {this.__label}
+
+ );
+ }
+}
+
+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;
+}
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx
new file mode 100644
index 00000000..79bd857b
--- /dev/null
+++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx
@@ -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, suggestion) => {
+ const { nodeData } = suggestion
+ const nodeId = nodeData.id as string;
+ if (!groups[nodeId]) {
+ groups[nodeId] = [];
+ }
+ groups[nodeId].push(suggestion);
+ return groups;
+ }, {});
+
+ return (
+
+ {Object.entries(groupedSuggestions).map(([nodeId, nodeOptions], groupIndex) => {
+ const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
+ return (
+
+ {groupIndex > 0 &&
}
+
+ {nodeName}
+
+ {nodeOptions.map((option, index) => {
+ const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option);
+ return (
+
insertMention(option)}
+ onMouseEnter={() => setSelectedIndex(globalIndex)}
+ >
+
+
+ {option.type === 'context' ? '📄' :
+ option.type === 'system' ? 'x' : 'x'}
+
+ {option.label}
+
+ {option.dataType && (
+
+ {option.dataType}
+
+ )}
+
+ );
+ })}
+
+ );
+ })}
+
+ );
+}
+export default AutocompletePlugin
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx
new file mode 100644
index 00000000..911eff3d
--- /dev/null
+++ b/web/src/views/Workflow/components/Editor/plugin/CharacterCountPlugin.tsx
@@ -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
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx
new file mode 100644
index 00000000..912801d8
--- /dev/null
+++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx
@@ -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 = ({ 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;
\ No newline at end of file
diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx
index 2714e45f..dfb3ccdb 100644
--- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx
+++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx
@@ -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;
}
const roleOptions = [
// { label: 'SYSTEM', value: 'SYSTEM' },
@@ -15,11 +22,92 @@ const roleOptions = [
{ label: 'ASSISTANT', value: 'ASSISTANT' },
]
const MessageEditor: FC = ({
+ 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[] => {
+ 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 = ({
content: undefined
})
}
-
+
return (
-
- {(fields, { add, remove }) => (
- <>
- {fields.map(({ key, name, ...restField }) => {
- const currentRole = values[parentName]?.[key].role || 'USER'
-
- return (
-
-
-
-
- {currentRole === 'SYSTEM'
- ?
- :
-
- }
-
-
- {currentRole !== 'SYSTEM' &&
-
- remove(name)} />
-
- }
-
-
-
-
-
- )
- })}
-
-
-
- >
- )}
-
+ {isArray
+ ?
+ {(fields, { add, remove }) => (
+
+ {fields.map(({ key, name, ...restField }) => {
+ const currentRole = values[parentName]?.[key].role || 'USER'
+
+ return (
+
+
+
+
+ {currentRole === 'SYSTEM'
+ ?
+ :
+
+ }
+
+
+ {currentRole !== 'SYSTEM' &&
+
+ remove(name)} />
+
+ }
+
+
+
+
+
+ )
+ })}
+
+
+
+
+ )}
+
+ :
+
+
+
+ {t('workflow.answerDesc')}
+
+
+
+
+
+
+ }
);
};
diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx
index 717df355..3a5be99c 100644
--- a/web/src/views/Workflow/components/Properties/index.tsx
+++ b/web/src/views/Workflow/components/Properties/index.tsx
@@ -21,6 +21,7 @@ interface PropertiesProps {
}
const Properties: FC = ({
selectedNode,
+ graphRef,
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
@@ -30,6 +31,12 @@ const Properties: FC = ({
const variableModalRef = useRef(null)
const [editIndex, setEditIndex] = useState(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 = ({
if (selectedNode.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
return (
-
+
+
+ )
+ }
+ if (selectedNode.data?.type === 'end' && key === 'output') {
+ return (
+
+
)
}
diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts
index 96fd89fc..dff525ef 100644
--- a/web/src/views/Workflow/constant.ts
+++ b/web/src/views/Workflow/constant.ts
@@ -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
},
diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
index 1e652a7b..d5cd9bb9 100644
--- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts
+++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
@@ -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 = {
diff --git a/web/src/views/Workflow/types.ts b/web/src/views/Workflow/types.ts
index fb7a05a4..9dabd7a6 100644
--- a/web/src/views/Workflow/types.ts
+++ b/web/src/views/Workflow/types.ts
@@ -26,6 +26,8 @@ export interface NodeConfig {
export interface NodeProperties {
type: string;
icon: string;
+ name?: string;
+ id?: string;
config?: Record;
}