LibreChat/client/src/utils/textarea.ts
Danny Avila 156c52e293
🌿 feat: Multi-response Streaming (#3191)
* chore: comment back handlePlusCommand

* chore: ignore .git dir

* refactor: pass newConversation to `useSelectMention`

refactor: pass newConversation to Mention component

refactor: useChatFunctions for modular use of `ask` and `regenerate`

refactor: set latest message only for the first index in useChatFunctions

refactor: pass setLatestMessage to useChatFunctions

refactor: Pass setSubmission to useChatFunctions for submission handling

refactor: consolidate event handlers to separate hook from useSSE

WIP: additional response handlers

feat: responsive added convo, clears on new chat/navigating to chat, assistants excluded

feat: Add conversationByKeySelector to select any conversation by index

WIP: handle second submission with messages paired to root

* style: surface-primary-contrast

* refactor: remove unnecessary console.log statement in useChatFunctions

* refactor: Consolidate imports in ChatForm and Input hooks

* refactor: compositional usage of useSSE for multiple streams

* WIP: set latest 'multi' message

* WIP: first pass, added response streaming

* pass: performant multi-message stream

* fix: styling and message render

* second pass: modular, performant multi-stream

* fix: align parentMessageId of multiMessage

* refactor: move resetting latestMultiMessage

* chore: update footer text in Chat component

* fix: stop button styling

* fix: handle abortMessage request for multi-response

* clear messages but bug with latest message reset present

* fix: add delay for additional message generation

* fix: access LAST_CONVO_SETUP by index

* style: add div to prevent layout shift before hover buttons render

* chore: Update Message component styling for card messages

* chore: move hook use order

* fix: abort middleware using unsent field from req.body

* feat: support multi-response stream from initial message

* refactor: buildTree function to improve readability and remove unused code

* feat: add logger for frontend dev

* refactor: use depth to track if message is really last in its branch

* fix(buildTree): default export

* fix: share parent message Id and avoid duplication error for multi-response streams

* fix: prevent addedConvo reset to response convo

* feat: allow setting multi message as latest message to control which to respond to

* chore: wrap setSiblingIdxRev with useCallback

* chore: styling and allow editing messages

* style: styling fixes

* feat: Add "AddMultiConvo" component to Chat Header

* feat: prevent clearing added convos on endpoint, preset, mention, or modelSpec switch

* fix: message styling fixes, mainly related to code blocks

* fix: stop button visibility logic

* fix: Handle edge case in abortMiddleware for non-existant `abortControllers`

* refactor: optimize/memoize icons

* chore(GoogleClient): change info to debug logs

* style: active message styling

* style: prevent layout shift due to placeholder row

* chore: remove unused code

* fix: Update BaseClient to handle optional request body properties

* fix(ci): `onStart` now accepts 2 args, the 2nd being responseMessageId

* chore: bump data-provider
2024-06-25 03:02:38 -04:00

75 lines
3 KiB
TypeScript

/**
* Insert text at the cursor position in a textarea.
*/
export function insertTextAtCursor(element: HTMLTextAreaElement, textToInsert: string) {
element.focus();
// Use the browser's built-in undoable actions if possible
if (window.getSelection() && document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, textToInsert);
} else {
console.warn('insertTextAtCursor: document.execCommand is not supported');
const startPos = element.selectionStart;
const endPos = element.selectionEnd;
const beforeText = element.value.substring(0, startPos);
const afterText = element.value.substring(endPos);
element.value = beforeText + textToInsert + afterText;
element.selectionStart = element.selectionEnd = startPos + textToInsert.length;
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
}
/**
* Necessary resize helper for edge cases where paste doesn't update the container height.
*
1) Resetting the height to 'auto' forces the component to recalculate height based on its current content
2) Forcing a reflow. Accessing offsetHeight will cause a reflow of the page,
ensuring that the reset height takes effect before resetting back to the scrollHeight.
This step is necessary because changes to the DOM do not instantly cause reflows.
3) Reseting back to scrollHeight reads and applies the ideal height for the current content dynamically
*/
export const forceResize = (element: HTMLTextAreaElement | null) => {
if (!element) {
return;
}
element.style.height = 'auto';
element.style.height = `${element.scrollHeight}px`;
};
/**
* Necessary undo event helper for edge cases where undoing pasted content leaves newlines filling the previous container height.
*/
export const trimUndoneRange = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
if (!textAreaRef.current) {
return;
}
const { value, selectionStart, selectionEnd } = textAreaRef.current;
const afterCursor = value.substring(selectionEnd).trim();
if (afterCursor.length) {
return;
}
const beforeCursor = value.substring(0, selectionStart);
const newValue = beforeCursor + afterCursor;
textAreaRef.current.value = newValue;
textAreaRef.current.setSelectionRange(selectionStart, selectionStart);
};
/**
* Remove the specified character from the end of the textarea's text if it's present.
* This function ensures that the specified character is only removed if it's the last character.
*
* @param {HTMLTextAreaElement} textarea - The textarea element where text manipulation will occur.
* @param {string} charToRemove - The character to remove if it's the last character in the textarea's value.
*/
export function removeCharIfLast(textarea: HTMLTextAreaElement, charToRemove: string) {
if (textarea.value.endsWith(charToRemove)) {
textarea.value = textarea.value.slice(0, -1);
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
textarea.focus();
}