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 { Variable } from 'lucide-react';
|
||||||
import { ReactNode, useCallback, useState } from 'react';
|
import { ReactNode, useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PasteHandlerPlugin } from './paste-handler-plugin';
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
import { VariableNode } from './variable-node';
|
import { VariableNode } from './variable-node';
|
||||||
import { VariableOnChangePlugin } from './variable-on-change-plugin';
|
import { VariableOnChangePlugin } from './variable-on-change-plugin';
|
||||||
@ -172,6 +173,7 @@ export function PromptEditor({
|
|||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
|
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
|
||||||
|
<PasteHandlerPlugin />
|
||||||
<VariableOnChangePlugin
|
<VariableOnChangePlugin
|
||||||
onChange={onValueChange}
|
onChange={onValueChange}
|
||||||
></VariableOnChangePlugin>
|
></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';
|
} from './hooks/use-save-graph';
|
||||||
import { useShowEmbedModal } from './hooks/use-show-dialog';
|
import { useShowEmbedModal } from './hooks/use-show-dialog';
|
||||||
import { UploadAgentDialog } from './upload-agent-dialog';
|
import { UploadAgentDialog } from './upload-agent-dialog';
|
||||||
|
import { useAgentHistoryManager } from './use-agent-history-manager';
|
||||||
import { VersionDialog } from './version-dialog';
|
import { VersionDialog } from './version-dialog';
|
||||||
|
|
||||||
function AgentDropdownMenuItem({
|
function AgentDropdownMenuItem({
|
||||||
@ -66,8 +67,7 @@ export default function Agent() {
|
|||||||
showModal: showChatDrawer,
|
showModal: showChatDrawer,
|
||||||
} = useSetModalState();
|
} = useSetModalState();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
useAgentHistoryManager();
|
||||||
// const openDocument = useOpenDocument();
|
|
||||||
const {
|
const {
|
||||||
handleExportJson,
|
handleExportJson,
|
||||||
handleImportJson,
|
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