mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-01 13:50:19 +01:00
✨ feat: Quality-of-Life Chat/Edit-Message Enhancements (#5194)
* fix: rendering error for mermaid flowchart syntax * feat: add submit button ref and enable submit on Ctrl+Enter in EditMessage component * feat: add save button and keyboard shortcuts for saving and canceling in EditMessage component * feat: collapse chat on max height * refactor: implement scrollable detection for textarea on key down events and initial render * feat: add regenerate button for error handling in HoverButtons, closes #3658 * feat: add functionality to edit latest user message with the up arrow key when the input is empty
This commit is contained in:
parent
b01c744eb8
commit
8aa1e731ca
22 changed files with 242 additions and 66 deletions
|
|
@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
|
|||
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TEditProps } from '~/common';
|
||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { TextareaAutosize, TooltipAnchor } from '~/components/ui';
|
||||
import { cn, removeFocusRings } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Container from './Container';
|
||||
|
|
@ -21,6 +21,8 @@ const EditMessage = ({
|
|||
setSiblingIdx,
|
||||
}: TEditProps) => {
|
||||
const { addedIndex } = useAddedChatContext();
|
||||
const saveButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const submitButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const { getMessages, setMessages, conversation } = useChatContext();
|
||||
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||
store.latestMessageFamily(addedIndex),
|
||||
|
|
@ -127,6 +129,14 @@ const EditMessage = ({
|
|||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
submitButtonRef.current?.click();
|
||||
}
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
saveButtonRef.current?.click();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
enterEdit(true);
|
||||
|
|
@ -165,25 +175,42 @@ const EditMessage = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full justify-center text-center">
|
||||
<button
|
||||
className="btn btn-primary relative mr-2"
|
||||
disabled={
|
||||
isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser)
|
||||
<TooltipAnchor
|
||||
description="Ctrl + Enter / ⌘ + Enter"
|
||||
render={
|
||||
<button
|
||||
ref={submitButtonRef}
|
||||
className="btn btn-primary relative mr-2"
|
||||
disabled={
|
||||
isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser)
|
||||
}
|
||||
onClick={handleSubmit(resubmitMessage)}
|
||||
>
|
||||
{localize('com_ui_save_submit')}
|
||||
</button>
|
||||
}
|
||||
onClick={handleSubmit(resubmitMessage)}
|
||||
>
|
||||
{localize('com_ui_save_submit')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary relative mr-2"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit(updateMessage)}
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
<button className="btn btn-neutral relative" onClick={() => enterEdit(true)}>
|
||||
{localize('com_ui_cancel')}
|
||||
</button>
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description="Shift + Enter"
|
||||
render={
|
||||
<button
|
||||
ref={saveButtonRef}
|
||||
className="btn btn-secondary relative mr-2"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit(updateMessage)}
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description="Esc"
|
||||
render={
|
||||
<button className="btn btn-neutral relative" onClick={() => enterEdit(true)}>
|
||||
{localize('com_ui_cancel')}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,8 +60,34 @@ export default function HoverButtons({
|
|||
|
||||
const { isCreatedByUser, error } = message;
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
const renderRegenerate = () => {
|
||||
if (!regenerateEnabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={regenerate}
|
||||
type="button"
|
||||
title={localize('com_ui_regenerate')}
|
||||
>
|
||||
<RegenerateIcon
|
||||
className="hover:text-gray-500 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"
|
||||
size="19"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
if (error === true) {
|
||||
return (
|
||||
<div className="visible mt-0 flex justify-center gap-1 self-end text-gray-500 lg:justify-start">
|
||||
{renderRegenerate()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const onEdit = () => {
|
||||
|
|
@ -84,6 +110,7 @@ export default function HoverButtons({
|
|||
)}
|
||||
{isEditableEndpoint && (
|
||||
<button
|
||||
id={`edit-${message.messageId}`}
|
||||
className={cn(
|
||||
'hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
isCreatedByUser ? '' : 'active',
|
||||
|
|
@ -113,22 +140,7 @@ export default function HoverButtons({
|
|||
>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
|
||||
</button>
|
||||
{regenerateEnabled ? (
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={regenerate}
|
||||
type="button"
|
||||
title={localize('com_ui_regenerate')}
|
||||
>
|
||||
<RegenerateIcon
|
||||
className="hover:text-gray-500 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"
|
||||
size="19"
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
{renderRegenerate()}
|
||||
<Fork
|
||||
isLast={isLast}
|
||||
messageId={message.messageId}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default function MessagesView({
|
|||
<div className="flex-1 overflow-hidden overflow-y-auto">
|
||||
<div className="relative h-full">
|
||||
<div
|
||||
className="scrollbar-gutter-stable"
|
||||
onScroll={debouncedHandleScroll}
|
||||
ref={scrollableRef}
|
||||
style={{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue