Files
ragflow/web/src/pages/agent/use-agent-history-manager.ts
chanx 7a27d5e463 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)
2025-08-06 10:29:44 +08:00

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