mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 17:16:52 +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)
164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
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]);
|
|
};
|