diff --git a/client/src/components/Artifacts/Mermaid.tsx b/client/src/components/Artifacts/Mermaid.tsx index af2bf83fe6..551a5a5a78 100644 --- a/client/src/components/Artifacts/Mermaid.tsx +++ b/client/src/components/Artifacts/Mermaid.tsx @@ -47,7 +47,6 @@ const MermaidDiagram: React.FC = ({ content }) => { diagramPadding: 8, htmlLabels: true, useMaxWidth: true, - defaultRenderer: 'dagre-d3', padding: 15, wrappingWidth: 200, }, diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 36eb05f45d..c0b61f4fec 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -1,4 +1,4 @@ -import { memo, useRef, useMemo, useEffect } from 'react'; +import { memo, useRef, useMemo, useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { supportsFiles, @@ -20,14 +20,15 @@ import { useQueryParams, useSubmitMessage, } from '~/hooks'; +import { cn, removeFocusRings, checkIfScrollable } from '~/utils'; import FileFormWrapper from './Files/FileFormWrapper'; import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; -import { cn, removeFocusRings } from '~/utils'; import TextareaHeader from './TextareaHeader'; import PromptsCommand from './PromptsCommand'; import AudioRecorder from './AudioRecorder'; import { mainTextareaId } from '~/common'; +import CollapseChat from './CollapseChat'; import StreamAudio from './StreamAudio'; import StopButton from './StopButton'; import SendButton from './SendButton'; @@ -39,6 +40,9 @@ const ChatForm = ({ index = 0 }) => { const textAreaRef = useRef(null); useQueryParams({ textAreaRef }); + const [isCollapsed, setIsCollapsed] = useState(false); + const [isScrollable, setIsScrollable] = useState(false); + const SpeechToText = useRecoilValue(store.speechToText); const TextToSpeech = useRecoilValue(store.textToSpeech); const automaticPlayback = useRecoilValue(store.automaticPlayback); @@ -64,6 +68,7 @@ const ChatForm = ({ index = 0 }) => { const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({ textAreaRef, submitButtonRef, + setIsScrollable, disabled: !!(requiresKey ?? false), }); @@ -129,11 +134,19 @@ const ChatForm = ({ index = 0 }) => { } }, [isSearching, disableInputs]); + useEffect(() => { + if (textAreaRef.current) { + checkIfScrollable(textAreaRef.current); + } + }, []); + const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? endpoint ?? ''] ?? false; const isUploadDisabled: boolean = endpointFileConfig?.disabled ?? false; - const baseClasses = - 'md:py-3.5 m-0 w-full resize-none bg-surface-tertiary py-[13px] placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] max-h-[65vh] md:max-h-[75vh]'; + const baseClasses = cn( + 'md:py-3.5 m-0 w-full resize-none bg-surface-tertiary py-[13px] placeholder-black/50 dark:placeholder-white/50 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]', + isCollapsed ? 'max-h-[52px]' : 'max-h-[65vh] md:max-h-[75vh]', + ); const uploadActive = endpointSupportsFiles && !isUploadDisabled; const speechClass = isRTL @@ -172,25 +185,45 @@ const ChatForm = ({ index = 0 }) => { {endpoint && ( - { - ref(e); - textAreaRef.current = e; - }} - disabled={disableInputs} - onPaste={handlePaste} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - onCompositionStart={handleCompositionStart} - onCompositionEnd={handleCompositionEnd} - id={mainTextareaId} - tabIndex={0} - data-testid="text-input" - style={{ height: 44, overflowY: 'auto' }} - rows={1} - className={cn(baseClasses, speechClass, removeFocusRings)} - /> + <> + + { + ref(e); + textAreaRef.current = e; + }} + disabled={disableInputs} + onPaste={handlePaste} + onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onHeightChange={() => { + if (textAreaRef.current) { + const scrollable = checkIfScrollable(textAreaRef.current); + setIsScrollable(scrollable); + } + }} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + id={mainTextareaId} + tabIndex={0} + data-testid="text-input" + rows={1} + onFocus={() => isCollapsed && setIsCollapsed(false)} + onClick={() => isCollapsed && setIsCollapsed(false)} + style={{ height: 44, overflowY: 'auto' }} + className={cn( + baseClasses, + speechClass, + removeFocusRings, + 'transition-[max-height] duration-200', + )} + /> + )} {SpeechToText && ( diff --git a/client/src/components/Chat/Input/CollapseChat.tsx b/client/src/components/Chat/Input/CollapseChat.tsx new file mode 100644 index 0000000000..ce6fdbf807 --- /dev/null +++ b/client/src/components/Chat/Input/CollapseChat.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Minimize2 } from 'lucide-react'; +import { TooltipAnchor } from '~/components/ui'; +import { useLocalize } from '~/hooks'; +import { cn } from '~/utils'; + +const CollapseChat = ({ + isScrollable, + isCollapsed, + setIsCollapsed, +}: { + isScrollable: boolean; + isCollapsed: boolean; + setIsCollapsed: React.Dispatch>; +}) => { + const localize = useLocalize(); + if (!isScrollable) { + return null; + } + + if (isCollapsed) { + return null; + } + + return ( + setIsCollapsed(true)} + className={cn( + 'absolute right-2 top-2 z-10 size-[35px] rounded-full p-2 transition-colors', + 'hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50', + )} + > + + + ); +}; + +export default CollapseChat; diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index ae9018d70e..a098854f4a 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -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(null); + const submitButtonRef = useRef(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) => { + 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 = ({ />
- } - onClick={handleSubmit(resubmitMessage)} - > - {localize('com_ui_save_submit')} - - - + /> + + {localize('com_ui_save')} + + } + /> + enterEdit(true)}> + {localize('com_ui_cancel')} + + } + />
); diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index 950c4a7f9c..fa7bd59ce0 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -60,8 +60,34 @@ export default function HoverButtons({ const { isCreatedByUser, error } = message; - if (error) { - return null; + const renderRegenerate = () => { + if (!regenerateEnabled) { + return null; + } + return ( + + ); + }; + + if (error === true) { + return ( +
+ {renderRegenerate()} +
+ ); } const onEdit = () => { @@ -84,6 +110,7 @@ export default function HoverButtons({ )} {isEditableEndpoint && ( - {regenerateEnabled ? ( - - ) : null} + {renderRegenerate()}
) => { + if (!latestMessage) { + return; + } + + const element = document.getElementById(`edit-${latestMessage.parentMessageId}`); + if (!element) { + return; + } + event.preventDefault(); + element.click(); + }, + [latestMessage], + ); + /** * Main key up handler. */ const handleKeyUp = useCallback( (event: React.KeyboardEvent) => { const text = textAreaRef.current?.value; + if (event.key === 'ArrowUp' && text?.length === 0) { + handleUpArrow(event); + return; + } if (typeof text !== 'string' || text.length === 0) { return; } @@ -115,7 +136,7 @@ const useHandleKeyUp = ({ handler(); } }, - [textAreaRef, commandHandlers], + [textAreaRef, commandHandlers, handleUpArrow], ); return handleKeyUp; diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index ca7bf79e0c..3a74a5ac5e 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -4,7 +4,13 @@ import { useRecoilValue, useRecoilState } from 'recoil'; import { Constants } from 'librechat-data-provider'; import type { TEndpointOption } from 'librechat-data-provider'; import type { KeyboardEvent } from 'react'; -import { forceResize, insertTextAtCursor, getEntityName, getEntity } from '~/utils'; +import { + forceResize, + insertTextAtCursor, + getEntityName, + getEntity, + checkIfScrollable, +} from '~/utils'; import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext'; import { useAgentsMapContext } from '~/Providers/AgentsMapContext'; import useGetSender from '~/hooks/Conversations/useGetSender'; @@ -20,10 +26,12 @@ type KeyEvent = KeyboardEvent; export default function useTextarea({ textAreaRef, submitButtonRef, + setIsScrollable, disabled = false, }: { textAreaRef: React.RefObject; submitButtonRef: React.RefObject; + setIsScrollable: React.Dispatch>; disabled?: boolean; }) { const localize = useLocalize(); @@ -170,6 +178,10 @@ export default function useTextarea({ const handleKeyDown = useCallback( (e: KeyEvent) => { + if (textAreaRef.current && checkIfScrollable(textAreaRef.current)) { + const scrollable = checkIfScrollable(textAreaRef.current); + scrollable && setIsScrollable(scrollable); + } if (e.key === 'Enter' && isSubmitting) { return; } @@ -209,7 +221,15 @@ export default function useTextarea({ submitButtonRef.current?.click(); } }, - [isSubmitting, checkHealth, filesLoading, enterToSend, textAreaRef, submitButtonRef], + [ + isSubmitting, + checkHealth, + filesLoading, + enterToSend, + setIsScrollable, + textAreaRef, + submitButtonRef, + ], ); const handleCompositionStart = () => { diff --git a/client/src/localization/languages/Ar.ts b/client/src/localization/languages/Ar.ts index 6d61bc2e6a..e888d76f10 100644 --- a/client/src/localization/languages/Ar.ts +++ b/client/src/localization/languages/Ar.ts @@ -913,4 +913,5 @@ export default { com_endpoint_ai: 'الذكاء الاصطناعي', com_endpoint_message_new: 'الرسالة {0} أو اكتب "@" للتبديل إلى الذكاء الاصطناعي', com_nav_maximize_chat_space: 'تكبير مساحة الدردشة', + com_ui_collapse_chat: 'طي الدردشة', }; diff --git a/client/src/localization/languages/De.ts b/client/src/localization/languages/De.ts index f310e3c1b4..6c79fbe658 100644 --- a/client/src/localization/languages/De.ts +++ b/client/src/localization/languages/De.ts @@ -945,4 +945,5 @@ export default { com_ui_bookmarks_add: 'Lesezeichen hinzufügen', com_endpoint_message_new: 'Nachricht {0} oder "@" eingeben, um KI zu wechseln', com_nav_maximize_chat_space: 'Chat-Bereich maximieren', + com_ui_collapse_chat: 'Chat einklappen', }; diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index 46397cee40..64aa137140 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -3,6 +3,7 @@ // file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file export default { + com_ui_collapse_chat: 'Collapse Chat', com_ui_enter_api_key: 'Enter API Key', com_ui_librechat_code_api_title: 'Run AI Code', com_ui_librechat_code_api_subtitle: 'Secure. Multi-language. Input/Output Files.', diff --git a/client/src/localization/languages/Es.ts b/client/src/localization/languages/Es.ts index d2701df405..a1627a5b5d 100644 --- a/client/src/localization/languages/Es.ts +++ b/client/src/localization/languages/Es.ts @@ -1201,4 +1201,5 @@ export default { com_endpoint_message_new: 'Mensaje {0} o escriba "@" para cambiar de IA', com_nav_maximize_chat_space: 'Maximizar espacio del chat', com_endpoint_ai: 'IA', + com_ui_collapse_chat: 'Contraer Chat', }; diff --git a/client/src/localization/languages/Fr.ts b/client/src/localization/languages/Fr.ts index 692ff19d6b..9ba87cc3e2 100644 --- a/client/src/localization/languages/Fr.ts +++ b/client/src/localization/languages/Fr.ts @@ -963,4 +963,5 @@ export default { com_nav_maximize_chat_space: 'Maximiser l\'espace de discussion', com_endpoint_message_new: 'Message {0} ou tapez "@" pour changer d\'IA', com_ui_page: 'Page', + com_ui_collapse_chat: 'Réduire la discussion', }; diff --git a/client/src/localization/languages/It.ts b/client/src/localization/languages/It.ts index a262c4345f..992b0cf376 100644 --- a/client/src/localization/languages/It.ts +++ b/client/src/localization/languages/It.ts @@ -957,4 +957,5 @@ export default { com_endpoint_ai: 'IA', com_nav_maximize_chat_space: 'Massimizza spazio chat', com_ui_page: 'Pagina', + com_ui_collapse_chat: 'Comprimi Chat', }; diff --git a/client/src/localization/languages/Jp.ts b/client/src/localization/languages/Jp.ts index 911078eaa6..c2fd0883f7 100644 --- a/client/src/localization/languages/Jp.ts +++ b/client/src/localization/languages/Jp.ts @@ -911,4 +911,5 @@ export default { com_endpoint_ai: 'AI', com_endpoint_message_new: 'メッセージ {0} または「@」を入力してAIを切り替え', com_nav_maximize_chat_space: 'チャット画面を最大化', + com_ui_collapse_chat: 'チャットを折りたたむ', }; diff --git a/client/src/localization/languages/Ko.ts b/client/src/localization/languages/Ko.ts index 6620385d5b..07f63a22e1 100644 --- a/client/src/localization/languages/Ko.ts +++ b/client/src/localization/languages/Ko.ts @@ -1149,4 +1149,5 @@ export default { com_endpoint_ai: '인공지능', com_nav_maximize_chat_space: '채팅창 최대화', com_endpoint_message_new: '메시지 {0} 또는 "@"를 입력하여 AI 전환', + com_ui_collapse_chat: '채팅 접기', }; diff --git a/client/src/localization/languages/Ru.ts b/client/src/localization/languages/Ru.ts index f4eddc124f..d74bb9aaf8 100644 --- a/client/src/localization/languages/Ru.ts +++ b/client/src/localization/languages/Ru.ts @@ -1175,4 +1175,5 @@ export default { com_endpoint_message_new: 'Сообщение {0} или введите "@" для смены ИИ', com_nav_maximize_chat_space: 'Развернуть чат', com_ui_bookmarks_add: 'Добавить закладку', + com_ui_collapse_chat: 'Свернуть чат', }; diff --git a/client/src/localization/languages/Zh.ts b/client/src/localization/languages/Zh.ts index 4b1c0e01d2..928550d79d 100644 --- a/client/src/localization/languages/Zh.ts +++ b/client/src/localization/languages/Zh.ts @@ -903,4 +903,5 @@ export default { com_ui_page: '页面', com_nav_maximize_chat_space: '最大化聊天窗口', com_endpoint_message_new: '发送消息 {0} 或输入"@"切换AI', + com_ui_collapse_chat: '收起聊天', }; diff --git a/client/src/localization/languages/ZhTraditional.ts b/client/src/localization/languages/ZhTraditional.ts index e18b3ca4d2..070d92ff98 100644 --- a/client/src/localization/languages/ZhTraditional.ts +++ b/client/src/localization/languages/ZhTraditional.ts @@ -880,4 +880,5 @@ export default { com_nav_maximize_chat_space: '最大化聊天視窗', com_endpoint_ai: 'AI', com_endpoint_message_new: '輸入訊息 {0} 或輸入 "@" 以切換 AI', + com_ui_collapse_chat: '收合對話', }; diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 60d37f83fd..3bc67b5996 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -131,7 +131,7 @@ const standardDependencies = { const mermaidDependencies = Object.assign( { - mermaid: '^11.0.2', + mermaid: '^11.4.1', 'react-zoom-pan-pinch': '^3.6.1', }, standardDependencies, diff --git a/client/src/utils/mermaid.ts b/client/src/utils/mermaid.ts index 9ca2a3bcc6..bc95eb1f1d 100644 --- a/client/src/utils/mermaid.ts +++ b/client/src/utils/mermaid.ts @@ -50,7 +50,6 @@ const MermaidDiagram: React.FC = ({ content }) => { diagramPadding: 8, htmlLabels: true, useMaxWidth: true, - defaultRenderer: "dagre-d3", padding: 15, wrappingWidth: 200, }, diff --git a/client/src/utils/textarea.ts b/client/src/utils/textarea.ts index 8081bf5e1e..ef9aaba405 100644 --- a/client/src/utils/textarea.ts +++ b/client/src/utils/textarea.ts @@ -73,3 +73,15 @@ export function removeCharIfLast(textarea: HTMLTextAreaElement, charToRemove: st textarea.focus(); } + +/** + * Check if the textarea is scrollable. + * @param element + * @returns + */ +export const checkIfScrollable = (element: HTMLTextAreaElement | null) => { + if (!element) { + return false; + } + return element.scrollHeight > element.clientHeight; +};