mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 04:30:10 +01:00
* initial work - undo works except typed keys * working but clunky code * single letter and paste with no selection working * add comments and keep the previous selection * optimizations + add redo feature * linting --------- Co-authored-by: Mr Simon C <iamontheinternet@yahoo.com> Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
This commit is contained in:
parent
2d59e30290
commit
2f56327962
@ -18,6 +18,8 @@ import AttachItem from "./AttachItem";
|
|||||||
import { PASTE_ATTACHMENT_EVENT } from "../DnDWrapper";
|
import { PASTE_ATTACHMENT_EVENT } from "../DnDWrapper";
|
||||||
|
|
||||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||||
|
const MAX_EDIT_STACK_SIZE = 100;
|
||||||
|
|
||||||
export default function PromptInput({
|
export default function PromptInput({
|
||||||
submit,
|
submit,
|
||||||
onChange,
|
onChange,
|
||||||
@ -32,14 +34,24 @@ export default function PromptInput({
|
|||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
const [_, setFocused] = useState(false);
|
const [_, setFocused] = useState(false);
|
||||||
|
const undoStack = useRef([]);
|
||||||
|
const redoStack = useRef([]);
|
||||||
|
|
||||||
// To prevent too many re-renders we remotely listen for updates from the parent
|
/**
|
||||||
// via an event cycle. Otherwise, using message as a prop leads to a re-render every
|
* To prevent too many re-renders we remotely listen for updates from the parent
|
||||||
// change on the input.
|
* via an event cycle. Otherwise, using message as a prop leads to a re-render every
|
||||||
|
* change on the input.
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
function handlePromptUpdate(e) {
|
function handlePromptUpdate(e) {
|
||||||
setPromptInput(e?.detail ?? "");
|
setPromptInput(e?.detail ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetTextAreaHeight() {
|
||||||
|
if (!textareaRef.current) return;
|
||||||
|
textareaRef.current.style.height = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!window)
|
if (!!window)
|
||||||
window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
|
window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
|
||||||
@ -48,51 +60,120 @@ export default function PromptInput({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputDisabled && textareaRef.current) {
|
if (!inputDisabled && textareaRef.current) textareaRef.current.focus();
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
resetTextAreaHeight();
|
resetTextAreaHeight();
|
||||||
}, [inputDisabled]);
|
}, [inputDisabled]);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
/**
|
||||||
|
* Save the current state before changes
|
||||||
|
* @param {number} adjustment
|
||||||
|
*/
|
||||||
|
function saveCurrentState(adjustment = 0) {
|
||||||
|
if (undoStack.current.length >= MAX_EDIT_STACK_SIZE)
|
||||||
|
undoStack.current.shift();
|
||||||
|
undoStack.current.push({
|
||||||
|
value: promptInput,
|
||||||
|
cursorPositionStart: textareaRef.current.selectionStart + adjustment,
|
||||||
|
cursorPositionEnd: textareaRef.current.selectionEnd + adjustment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const debouncedSaveState = debounce(saveCurrentState, 250);
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
submit(e);
|
submit(e);
|
||||||
};
|
}
|
||||||
|
|
||||||
const resetTextAreaHeight = () => {
|
function resetTextAreaHeight() {
|
||||||
if (textareaRef.current) {
|
if (!textareaRef.current) return;
|
||||||
textareaRef.current.style.height = "auto";
|
textareaRef.current.style.height = "auto";
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const checkForSlash = (e) => {
|
function checkForSlash(e) {
|
||||||
const input = e.target.value;
|
const input = e.target.value;
|
||||||
if (input === "/") setShowSlashCommand(true);
|
if (input === "/") setShowSlashCommand(true);
|
||||||
if (showSlashCommand) setShowSlashCommand(false);
|
if (showSlashCommand) setShowSlashCommand(false);
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
const watchForSlash = debounce(checkForSlash, 300);
|
||||||
|
|
||||||
const checkForAt = (e) => {
|
function checkForAt(e) {
|
||||||
const input = e.target.value;
|
const input = e.target.value;
|
||||||
if (input === "@") return setShowAgents(true);
|
if (input === "@") return setShowAgents(true);
|
||||||
if (showAgents) return setShowAgents(false);
|
if (showAgents) return setShowAgents(false);
|
||||||
};
|
}
|
||||||
|
const watchForAt = debounce(checkForAt, 300);
|
||||||
|
|
||||||
const captureEnter = (event) => {
|
/**
|
||||||
if (event.keyCode == 13) {
|
* Capture enter key press to handle submission, redo, or undo
|
||||||
if (!event.shiftKey) {
|
* via keyboard shortcuts
|
||||||
submit(event);
|
* @param {KeyboardEvent} event
|
||||||
}
|
*/
|
||||||
|
function captureEnterOrUndo(event) {
|
||||||
|
// Is simple enter key press w/o shift key
|
||||||
|
if (event.keyCode === 13 && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
return submit(event);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const adjustTextArea = (event) => {
|
// Is undo with Ctrl+Z or Cmd+Z + Shift key = Redo
|
||||||
|
if (
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.key === "z" &&
|
||||||
|
event.shiftKey
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (redoStack.current.length === 0) return;
|
||||||
|
|
||||||
|
const nextState = redoStack.current.pop();
|
||||||
|
if (!nextState) return;
|
||||||
|
|
||||||
|
undoStack.current.push({
|
||||||
|
value: promptInput,
|
||||||
|
cursorPositionStart: textareaRef.current.selectionStart,
|
||||||
|
cursorPositionEnd: textareaRef.current.selectionEnd,
|
||||||
|
});
|
||||||
|
setPromptInput(nextState.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
textareaRef.current.setSelectionRange(
|
||||||
|
nextState.cursorPositionStart,
|
||||||
|
nextState.cursorPositionEnd
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo with Ctrl+Z or Cmd+Z
|
||||||
|
if (
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.key === "z" &&
|
||||||
|
!event.shiftKey
|
||||||
|
) {
|
||||||
|
if (undoStack.current.length === 0) return;
|
||||||
|
const lastState = undoStack.current.pop();
|
||||||
|
if (!lastState) return;
|
||||||
|
|
||||||
|
redoStack.current.push({
|
||||||
|
value: promptInput,
|
||||||
|
cursorPositionStart: textareaRef.current.selectionStart,
|
||||||
|
cursorPositionEnd: textareaRef.current.selectionEnd,
|
||||||
|
});
|
||||||
|
setPromptInput(lastState.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
textareaRef.current.setSelectionRange(
|
||||||
|
lastState.cursorPositionStart,
|
||||||
|
lastState.cursorPositionEnd
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustTextArea(event) {
|
||||||
const element = event.target;
|
const element = event.target;
|
||||||
element.style.height = "auto";
|
element.style.height = "auto";
|
||||||
element.style.height = `${element.scrollHeight}px`;
|
element.style.height = `${element.scrollHeight}px`;
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePasteEvent = (e) => {
|
function handlePasteEvent(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.clipboardData.items.length === 0) return false;
|
if (e.clipboardData.items.length === 0) return false;
|
||||||
|
|
||||||
@ -140,10 +221,16 @@ export default function PromptInput({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
const watchForSlash = debounce(checkForSlash, 300);
|
function handleChange(e) {
|
||||||
const watchForAt = debounce(checkForAt, 300);
|
debouncedSaveState(-1);
|
||||||
|
onChange(e);
|
||||||
|
watchForSlash(e);
|
||||||
|
watchForAt(e);
|
||||||
|
adjustTextArea(e);
|
||||||
|
setPromptInput(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
|
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
|
||||||
@ -168,15 +255,12 @@ export default function PromptInput({
|
|||||||
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
<div className="flex items-center w-full border-b-2 border-gray-500/50">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
onChange={(e) => {
|
onChange={handleChange}
|
||||||
onChange(e);
|
onKeyDown={captureEnterOrUndo}
|
||||||
watchForSlash(e);
|
onPaste={(e) => {
|
||||||
watchForAt(e);
|
saveCurrentState();
|
||||||
adjustTextArea(e);
|
handlePasteEvent(e);
|
||||||
setPromptInput(e.target.value);
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={captureEnter}
|
|
||||||
onPaste={handlePasteEvent}
|
|
||||||
required={true}
|
required={true}
|
||||||
disabled={inputDisabled}
|
disabled={inputDisabled}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
|
Loading…
Reference in New Issue
Block a user