mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🛠️ refactor: Improve Input Placeholder Handling and Error Management 🔄 (#1296)
* chore: identify new chat buttons with testid * fix: avoid parsing error in useSSE, which causes errorHandler to fail * fix: ensure last message isn't setting latestMessage when conversationId is `new` and text is the same due to possible re-renders * refactor: set placeholder through inputRef and useEffect * Update useSSE.ts * Update useSSE.ts
This commit is contained in:
parent
2e390596ea
commit
9b2359fc27
7 changed files with 64 additions and 23 deletions
|
|
@ -11,7 +11,6 @@ export default function Textarea({ value, disabled, onChange, setText, submitMes
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleCompositionStart,
|
handleCompositionStart,
|
||||||
handleCompositionEnd,
|
handleCompositionEnd,
|
||||||
placeholder,
|
|
||||||
} = useTextarea({ setText, submitMessage, disabled });
|
} = useTextarea({ setText, submitMessage, disabled });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,7 +30,6 @@ export default function Textarea({ value, disabled, onChange, setText, submitMes
|
||||||
data-testid="text-input"
|
data-testid="text-input"
|
||||||
style={{ height: 44, overflowY: 'auto' }}
|
style={{ height: 44, overflowY: 'auto' }}
|
||||||
rows={1}
|
rows={1}
|
||||||
placeholder={placeholder}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
supportsFiles[endpoint] ? ' pl-10 md:pl-[55px]' : 'pl-3 md:pl-4',
|
supportsFiles[endpoint] ? ' pl-10 md:pl-[55px]' : 'pl-3 md:pl-4',
|
||||||
'm-0 w-full resize-none border-0 bg-transparent py-[10px] pr-10 placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 md:pr-12 ',
|
'm-0 w-full resize-none border-0 bg-transparent py-[10px] pr-10 placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 md:pr-12 ',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import { useMediaQuery } from '~/hooks';
|
import { useMediaQuery } from '~/hooks';
|
||||||
|
|
||||||
export default function Header() {
|
export default function NewChat() {
|
||||||
const { newConversation } = useChatContext();
|
const { newConversation } = useChatContext();
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
|
|
@ -9,6 +9,7 @@ export default function Header() {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
data-testid="wide-header-new-chat-button"
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-neutral btn-small border-token-border-medium relative ml-2 flex hidden h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg rounded-lg border focus:ring-0 focus:ring-offset-0 md:flex"
|
className="btn btn-neutral btn-small border-token-border-medium relative ml-2 flex hidden h-9 w-9 items-center justify-center whitespace-nowrap rounded-lg rounded-lg border focus:ring-0 focus:ring-offset-0 md:flex"
|
||||||
onClick={() => newConversation()}
|
onClick={() => newConversation()}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export default function MobileNav({
|
||||||
<div className="text-token-primary border-token-border-medium bg-token-surface-primary dark:bg-token-surface-secondary sticky top-0 z-10 flex min-h-[40px] items-center border-b dark:text-white md:hidden">
|
<div className="text-token-primary border-token-border-medium bg-token-surface-primary dark:bg-token-surface-secondary sticky top-0 z-10 flex min-h-[40px] items-center border-b dark:text-white md:hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="mobile-header-new-chat-button"
|
||||||
className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white"
|
className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white"
|
||||||
onClick={() => setNavVisible((prev) => !prev)}
|
onClick={() => setNavVisible((prev) => !prev)}
|
||||||
>
|
>
|
||||||
|
|
@ -41,7 +42,11 @@ export default function MobileNav({
|
||||||
<h1 className="flex-1 text-center text-base font-normal">
|
<h1 className="flex-1 text-center text-base font-normal">
|
||||||
{title || localize('com_ui_new_chat')}
|
{title || localize('com_ui_new_chat')}
|
||||||
</h1>
|
</h1>
|
||||||
<button type="button" className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white" onClick={() => newConversation()}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white active:opacity-50 dark:hover:text-white"
|
||||||
|
onClick={() => newConversation()}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export default function NewChat({ toggleNav }: { toggleNav: () => void }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
data-testid="new-chat-button"
|
data-testid="nav-new-chat-button"
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
className="flex h-11 flex-shrink-0 flex-grow cursor-pointer items-center gap-3 rounded-md border border-white/20 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
className="flex h-11 flex-shrink-0 flex-grow cursor-pointer items-center gap-3 rounded-md border border-white/20 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import { TEndpointOption, getResponseSender } from 'librechat-data-provider';
|
import { TEndpointOption, getResponseSender } from 'librechat-data-provider';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
|
|
@ -15,8 +16,9 @@ export default function useTextarea({ setText, submitMessage, disabled = false }
|
||||||
const { handleFiles } = useFileHandling();
|
const { handleFiles } = useFileHandling();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error;
|
|
||||||
const { conversationId, jailbreak } = conversation || {};
|
const { conversationId, jailbreak } = conversation || {};
|
||||||
|
const isNotAppendable = (latestMessage?.unfinished && !isSubmitting) || latestMessage?.error;
|
||||||
|
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
|
||||||
|
|
||||||
// auto focus to input, when enter a conversation.
|
// auto focus to input, when enter a conversation.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -44,6 +46,44 @@ export default function useTextarea({ setText, submitMessage, disabled = false }
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [isSubmitting]);
|
}, [isSubmitting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current?.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlaceholderText = () => {
|
||||||
|
if (disabled) {
|
||||||
|
return localize('com_endpoint_config_placeholder');
|
||||||
|
}
|
||||||
|
if (isNotAppendable) {
|
||||||
|
return localize('com_endpoint_message_not_appendable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = getResponseSender(conversation as TEndpointOption);
|
||||||
|
|
||||||
|
return `${localize('com_endpoint_message')} ${sender ? sender : 'ChatGPT'}…`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeholder = getPlaceholderText();
|
||||||
|
|
||||||
|
if (inputRef.current?.getAttribute('placeholder') === placeholder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPlaceholder = () => {
|
||||||
|
const placeholder = getPlaceholderText();
|
||||||
|
|
||||||
|
if (inputRef.current?.getAttribute('placeholder') !== placeholder) {
|
||||||
|
inputRef.current?.setAttribute('placeholder', placeholder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedSetPlaceholder = debounce(setPlaceholder, 80);
|
||||||
|
debouncedSetPlaceholder();
|
||||||
|
|
||||||
|
return () => debouncedSetPlaceholder.cancel();
|
||||||
|
}, [conversation, disabled, latestMessage, isNotAppendable, localize]);
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyEvent) => {
|
const handleKeyDown = (e: KeyEvent) => {
|
||||||
if (e.key === 'Enter' && isSubmitting) {
|
if (e.key === 'Enter' && isSubmitting) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -82,19 +122,6 @@ export default function useTextarea({ setText, submitMessage, disabled = false }
|
||||||
isComposing.current = false;
|
isComposing.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlaceholderText = () => {
|
|
||||||
if (disabled) {
|
|
||||||
return localize('com_endpoint_config_placeholder');
|
|
||||||
}
|
|
||||||
if (isNotAppendable) {
|
|
||||||
return localize('com_endpoint_message_not_appendable');
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = getResponseSender(conversation as TEndpointOption);
|
|
||||||
|
|
||||||
return `${localize('com_endpoint_message')} ${sender ? sender : 'ChatGPT'}…`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.clipboardData && e.clipboardData.files.length > 0) {
|
if (e.clipboardData && e.clipboardData.files.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -110,6 +137,5 @@ export default function useTextarea({ setText, submitMessage, disabled = false }
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleCompositionStart,
|
handleCompositionStart,
|
||||||
handleCompositionEnd,
|
handleCompositionEnd,
|
||||||
placeholder: getPlaceholderText(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
|
|
@ -6,6 +6,7 @@ import Icon from '~/components/Endpoints/Icon';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
|
||||||
export default function useMessageHelpers(props: TMessageProps) {
|
export default function useMessageHelpers(props: TMessageProps) {
|
||||||
|
const latestText = useRef('');
|
||||||
const { message, currentEditId, setCurrentEditId } = props;
|
const { message, currentEditId, setCurrentEditId } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -26,10 +27,15 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
} else if (isLast) {
|
} else if (
|
||||||
|
isLast &&
|
||||||
|
conversation?.conversationId !== 'new' &&
|
||||||
|
latestText.current !== message.text
|
||||||
|
) {
|
||||||
setLatestMessage({ ...message });
|
setLatestMessage({ ...message });
|
||||||
|
latestText.current = message.text;
|
||||||
}
|
}
|
||||||
}, [isLast, message, setLatestMessage]);
|
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
|
||||||
|
|
||||||
const enterEdit = (cancel?: boolean) =>
|
const enterEdit = (cancel?: boolean) =>
|
||||||
setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId);
|
setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId);
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,11 @@ export default function useSSE(submission: TSubmission | null, index = 0) {
|
||||||
const errorHandler = (data: TResData, submission: TSubmission) => {
|
const errorHandler = (data: TResData, submission: TSubmission) => {
|
||||||
const { messages, message } = submission;
|
const { messages, message } = submission;
|
||||||
|
|
||||||
|
if (!data.conversationId) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Error:', data);
|
console.log('Error:', data);
|
||||||
const errorResponse = tMessageSchema.parse({
|
const errorResponse = tMessageSchema.parse({
|
||||||
...data,
|
...data,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue