mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### 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)
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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 };
|
||||
@ -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
web/src/pages/agent/use-agent-history-manager.ts
Normal file
163
web/src/pages/agent/use-agent-history-manager.ts
Normal file
@ -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]);
|
||||
};
|
||||
Reference in New Issue
Block a user