mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +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
|
|
@ -47,7 +47,6 @@ const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
|
|||
diagramPadding: 8,
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
defaultRenderer: 'dagre-d3',
|
||||
padding: 15,
|
||||
wrappingWidth: 200,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<HTMLTextAreaElement | null>(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 }) => {
|
|||
<TextareaHeader addedConvo={addedConvo} setAddedConvo={setAddedConvo} />
|
||||
<FileFormWrapper disableInputs={disableInputs}>
|
||||
{endpoint && (
|
||||
<TextareaAutosize
|
||||
{...registerProps}
|
||||
ref={(e) => {
|
||||
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)}
|
||||
/>
|
||||
<>
|
||||
<CollapseChat
|
||||
isCollapsed={isCollapsed}
|
||||
isScrollable={isScrollable}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
{...registerProps}
|
||||
ref={(e) => {
|
||||
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',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FileFormWrapper>
|
||||
{SpeechToText && (
|
||||
|
|
|
|||
41
client/src/components/Chat/Input/CollapseChat.tsx
Normal file
41
client/src/components/Chat/Input/CollapseChat.tsx
Normal file
|
|
@ -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<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
if (!isScrollable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipAnchor
|
||||
role="button"
|
||||
description={localize('com_ui_collapse_chat')}
|
||||
aria-label={localize('com_ui_collapse_chat')}
|
||||
onClick={() => 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',
|
||||
)}
|
||||
>
|
||||
<Minimize2 className="h-full w-full" />
|
||||
</TooltipAnchor>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapseChat;
|
||||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const useHandleKeyUp = ({
|
|||
permissionType: PermissionTypes.MULTI_CONVO,
|
||||
permission: Permissions.USE,
|
||||
});
|
||||
const latestMessage = useRecoilValue(store.latestMessageFamily(index));
|
||||
const setShowPromptsPopover = useSetRecoilState(store.showPromptsPopoverFamily(index));
|
||||
|
||||
// Get the current state of command toggles
|
||||
|
|
@ -94,12 +95,32 @@ const useHandleKeyUp = ({
|
|||
[handleAtCommand, handlePlusCommand, handlePromptsCommand],
|
||||
);
|
||||
|
||||
const handleUpArrow = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
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<HTMLTextAreaElement>) => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<HTMLTextAreaElement>;
|
|||
export default function useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
setIsScrollable,
|
||||
disabled = false,
|
||||
}: {
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
submitButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
setIsScrollable: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
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 = () => {
|
||||
|
|
|
|||
|
|
@ -913,4 +913,5 @@ export default {
|
|||
com_endpoint_ai: 'الذكاء الاصطناعي',
|
||||
com_endpoint_message_new: 'الرسالة {0} أو اكتب "@" للتبديل إلى الذكاء الاصطناعي',
|
||||
com_nav_maximize_chat_space: 'تكبير مساحة الدردشة',
|
||||
com_ui_collapse_chat: 'طي الدردشة',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: 'チャットを折りたたむ',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1149,4 +1149,5 @@ export default {
|
|||
com_endpoint_ai: '인공지능',
|
||||
com_nav_maximize_chat_space: '채팅창 최대화',
|
||||
com_endpoint_message_new: '메시지 {0} 또는 "@"를 입력하여 AI 전환',
|
||||
com_ui_collapse_chat: '채팅 접기',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1175,4 +1175,5 @@ export default {
|
|||
com_endpoint_message_new: 'Сообщение {0} или введите "@" для смены ИИ',
|
||||
com_nav_maximize_chat_space: 'Развернуть чат',
|
||||
com_ui_bookmarks_add: 'Добавить закладку',
|
||||
com_ui_collapse_chat: 'Свернуть чат',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -903,4 +903,5 @@ export default {
|
|||
com_ui_page: '页面',
|
||||
com_nav_maximize_chat_space: '最大化聊天窗口',
|
||||
com_endpoint_message_new: '发送消息 {0} 或输入"@"切换AI',
|
||||
com_ui_collapse_chat: '收起聊天',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: '收合對話',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ const MermaidDiagram: React.FC<MermaidDiagramProps> = ({ content }) => {
|
|||
diagramPadding: 8,
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
defaultRenderer: "dagre-d3",
|
||||
padding: 15,
|
||||
wrappingWidth: 200,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue