Преглед изворни кода

Feat: Added history management and paste handling features #3221 (#9266)

### What problem does this PR solve?

feat(agent): Added history management and paste handling features #3221

- Added a PasteHandlerPlugin to handle paste operations, optimizing the
multi-line text pasting experience
- Implemented the AgentHistoryManager class to manage history,
supporting undo and redo functionality
- Integrates history management functionality into the Agent component

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
tags/v0.20.1
chanx пре 2 месеци
родитељ
комит
7a27d5e463
No account linked to committer's email address

+ 2
- 0
web/src/pages/agent/form/components/prompt-editor/index.tsx Прегледај датотеку

@@ -26,6 +26,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { Variable } from 'lucide-react';
import { ReactNode, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PasteHandlerPlugin } from './paste-handler-plugin';
import theme from './theme';
import { VariableNode } from './variable-node';
import { VariableOnChangePlugin } from './variable-on-change-plugin';
@@ -172,6 +173,7 @@ export function PromptEditor({
ErrorBoundary={LexicalErrorBoundary}
/>
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
<PasteHandlerPlugin />
<VariableOnChangePlugin
onChange={onValueChange}
></VariableOnChangePlugin>

+ 83
- 0
web/src/pages/agent/form/components/prompt-editor/paste-handler-plugin.tsx Прегледај датотеку

@@ -0,0 +1,83 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$createParagraphNode,
$createTextNode,
$getSelection,
$isRangeSelection,
PASTE_COMMAND,
} from 'lexical';
import { useEffect } from 'react';

function PasteHandlerPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
const removeListener = editor.registerCommand(
PASTE_COMMAND,
(clipboardEvent: ClipboardEvent) => {
const clipboardData = clipboardEvent.clipboardData;
if (!clipboardData) {
return false;
}

const text = clipboardData.getData('text/plain');
if (!text) {
return false;
}

// Check if text contains line breaks
if (text.includes('\n')) {
editor.update(() => {
const selection = $getSelection();
if (selection && $isRangeSelection(selection)) {
// Normalize line breaks, merge multiple consecutive line breaks into a single line break
const normalizedText = text.replace(/\n{2,}/g, '\n');

// Clear current selection
selection.removeText();

// Create a paragraph node to contain all content
const paragraph = $createParagraphNode();

// Split text by line breaks
const lines = normalizedText.split('\n');

// Process each line
lines.forEach((lineText, index) => {
// Add line text (if any)
if (lineText) {
const textNode = $createTextNode(lineText);
paragraph.append(textNode);
}

// If not the last line, add a line break
if (index < lines.length - 1) {
const lineBreak = $createTextNode('\n');
paragraph.append(lineBreak);
}
});

// Insert paragraph
selection.insertNodes([paragraph]);
}
});

// Prevent default paste behavior
clipboardEvent.preventDefault();
return true;
}

// If no line breaks, use default behavior
return false;
},
4,
);

return () => {
removeListener();
};
}, [editor]);

return null;
}

export { PasteHandlerPlugin };

+ 2
- 2
web/src/pages/agent/index.tsx Прегледај датотеку

@@ -44,6 +44,7 @@ import {
} from './hooks/use-save-graph';
import { useShowEmbedModal } from './hooks/use-show-dialog';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useAgentHistoryManager } from './use-agent-history-manager';
import { VersionDialog } from './version-dialog';

function AgentDropdownMenuItem({
@@ -66,8 +67,7 @@ export default function Agent() {
showModal: showChatDrawer,
} = useSetModalState();
const { t } = useTranslation();

// const openDocument = useOpenDocument();
useAgentHistoryManager();
const {
handleExportJson,
handleImportJson,

+ 163
- 0
web/src/pages/agent/use-agent-history-manager.ts Прегледај датотеку

@@ -0,0 +1,163 @@
import { useEffect, useRef } from 'react';
import useGraphStore from './store';

// History management class
export class HistoryManager {
private history: { nodes: any[]; edges: any[] }[] = [];
private currentIndex: number = -1;
private readonly maxSize: number = 50; // Limit maximum number of history records
private setNodes: (nodes: any[]) => void;
private setEdges: (edges: any[]) => void;
private lastSavedState: string = ''; // Used to compare if state has changed

constructor(
setNodes: (nodes: any[]) => void,
setEdges: (edges: any[]) => void,
) {
this.setNodes = setNodes;
this.setEdges = setEdges;
}

// Compare if two states are equal
private statesEqual(
state1: { nodes: any[]; edges: any[] },
state2: { nodes: any[]; edges: any[] },
): boolean {
return JSON.stringify(state1) === JSON.stringify(state2);
}

push(nodes: any[], edges: any[]) {
const currentState = {
nodes: JSON.parse(JSON.stringify(nodes)),
edges: JSON.parse(JSON.stringify(edges)),
};

// If state hasn't changed, don't save
if (
this.history.length > 0 &&
this.statesEqual(currentState, this.history[this.currentIndex])
) {
return;
}

// If current index is not at the end of history, remove subsequent states
if (this.currentIndex < this.history.length - 1) {
this.history.splice(this.currentIndex + 1);
}

// Add current state
this.history.push(currentState);

// Limit history record size
if (this.history.length > this.maxSize) {
this.history.shift();
this.currentIndex = this.history.length - 1;
} else {
this.currentIndex = this.history.length - 1;
}

// Update last saved state
this.lastSavedState = JSON.stringify(currentState);
}

undo() {
if (this.canUndo()) {
this.currentIndex--;
const prevState = this.history[this.currentIndex];
this.setNodes(JSON.parse(JSON.stringify(prevState.nodes)));
this.setEdges(JSON.parse(JSON.stringify(prevState.edges)));
return true;
}
return false;
}

redo() {
console.log('redo');
if (this.canRedo()) {
this.currentIndex++;
const nextState = this.history[this.currentIndex];
this.setNodes(JSON.parse(JSON.stringify(nextState.nodes)));
this.setEdges(JSON.parse(JSON.stringify(nextState.edges)));
return true;
}
return false;
}

canUndo() {
return this.currentIndex > 0;
}

canRedo() {
return this.currentIndex < this.history.length - 1;
}

// Reset history records
reset() {
this.history = [];
this.currentIndex = -1;
this.lastSavedState = '';
}
}

export const useAgentHistoryManager = () => {
// Get current state and history state
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const setNodes = useGraphStore((state) => state.setNodes);
const setEdges = useGraphStore((state) => state.setEdges);

// Use useRef to keep HistoryManager instance unchanged
const historyManagerRef = useRef<HistoryManager | null>(null);

// Initialize HistoryManager
if (!historyManagerRef.current) {
historyManagerRef.current = new HistoryManager(setNodes, setEdges);
}

const historyManager = historyManagerRef.current;

// Save state history - use useEffect instead of useMemo to avoid re-rendering
useEffect(() => {
historyManager.push(nodes, edges);
}, [nodes, edges, historyManager]);

// Keyboard event handling
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Check if focused on an input element
const activeElement = document.activeElement;
const isInputFocused =
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement?.hasAttribute('contenteditable');

// Skip keyboard shortcuts if typing in an input field
if (isInputFocused) {
return;
}
// Ctrl+Z or Cmd+Z undo
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'z' || e.key === 'Z') &&
!e.shiftKey
) {
e.preventDefault();
historyManager.undo();
}
// Ctrl+Shift+Z or Cmd+Shift+Z redo
else if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'z' || e.key === 'Z') &&
e.shiftKey
) {
e.preventDefault();
historyManager.redo();
}
};

document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [historyManager]);
};

Loading…
Откажи
Сачувај