From 1a1e6850a31c888d8b64920e7b100c1b345b2a78 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 10 Sep 2024 12:56:19 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=A8=20fix:=20Minor=20AWS=20Bedrock/Mis?= =?UTF-8?q?c.=20Improvements=20(#3974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(EditMessage): avoid manipulation of native paste handling, leverage react-hook-form for textarea changes * style: apply better theming for MinimalIcon * fix(useVoicesQuery/useCustomConfigSpeechQuery): make sure to only try request once per render * feat: edit message content parts * fix(useCopyToClipboard): handle both assistants and agents content blocks * refactor: remove save & submit and update text content correctly * chore(.env.example/config): exclude unsupported bedrock models * feat: artifacts for aws bedrock * fix: export options for bedrock conversations --- .env.example | 7 +- api/server/routes/messages.js | 50 ++++- .../services/Endpoints/bedrock/initialize.js | 4 + client/src/common/types.ts | 8 +- .../Chat/Messages/Content/ContentParts.tsx | 45 +++- .../Chat/Messages/Content/EditMessage.tsx | 83 ++++---- .../Messages/Content/Parts/EditTextPart.tsx | 193 ++++++++++++++++++ .../src/components/Endpoints/MinimalIcon.tsx | 4 +- .../Messages/Content/EditMessage.tsx | 117 ----------- .../src/components/Messages/ContentRender.tsx | 4 + .../Nav/ExportConversation/ExportModal.tsx | 2 +- client/src/data-provider/queries.ts | 28 ++- .../Conversations/useExportConversation.ts | 38 ++-- .../src/hooks/Messages/useCopyToClipboard.ts | 5 +- client/src/localization/languages/Eng.ts | 2 + client/src/utils/messages.ts | 13 +- package-lock.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/bedrock.ts | 3 + packages/data-provider/src/config.ts | 4 +- packages/data-provider/src/data-service.ts | 9 + .../src/react-query/react-query-service.ts | 14 ++ packages/data-provider/src/types.ts | 7 + 23 files changed, 441 insertions(+), 203 deletions(-) create mode 100644 client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx delete mode 100644 client/src/components/Messages/Content/EditMessage.tsx diff --git a/.env.example b/.env.example index 5f2d1574224..5571fe46cf1 100644 --- a/.env.example +++ b/.env.example @@ -125,8 +125,11 @@ BINGAI_TOKEN=user_provided # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns # Notes on specific models: -# 'ai21.j2-mid-v1', # Not supported, as it doesn't support streaming -# 'ai21.j2-ultra-v1', # Not supported, as it doesn't support conversation history +# The following models are not support due to not supporting streaming: +# ai21.j2-mid-v1 + +# The following models are not support due to not supporting conversation history: +# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14 #============# # Google # diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index f510f31f63e..0abca92001d 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -1,4 +1,5 @@ const express = require('express'); +const { ContentTypes } = require('librechat-data-provider'); const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models'); const { requireJwtAuth, validateMessageReq } = require('~/server/middleware'); const { countTokens } = require('~/server/utils'); @@ -54,11 +55,50 @@ router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) = router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { - const { messageId, model } = req.params; - const { text } = req.body; - const tokenCount = await countTokens(text, model); - const result = await updateMessage(req, { messageId, text, tokenCount }); - res.status(200).json(result); + const { conversationId, messageId } = req.params; + const { text, index, model } = req.body; + + if (index === undefined) { + const tokenCount = await countTokens(text, model); + const result = await updateMessage(req, { messageId, text, tokenCount }); + return res.status(200).json(result); + } + + if (typeof index !== 'number' || index < 0) { + return res.status(400).json({ error: 'Invalid index' }); + } + + const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0]; + if (!message) { + return res.status(404).json({ error: 'Message not found' }); + } + + const existingContent = message.content; + if (!Array.isArray(existingContent) || index >= existingContent.length) { + return res.status(400).json({ error: 'Invalid index' }); + } + + const updatedContent = [...existingContent]; + if (!updatedContent[index]) { + return res.status(400).json({ error: 'Content part not found' }); + } + + if (updatedContent[index].type !== ContentTypes.TEXT) { + return res.status(400).json({ error: 'Cannot update non-text content' }); + } + + const oldText = updatedContent[index].text; + updatedContent[index] = { type: ContentTypes.TEXT, text }; + + let tokenCount = message.tokenCount; + if (tokenCount !== undefined) { + const oldTokenCount = await countTokens(oldText, model); + const newTokenCount = await countTokens(text, model); + tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount; + } + + const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount }); + return res.status(200).json(result); } catch (error) { logger.error('Error updating message:', error); res.status(500).json({ error: 'Internal server error' }); diff --git a/api/server/services/Endpoints/bedrock/initialize.js b/api/server/services/Endpoints/bedrock/initialize.js index db90d5fc819..4a7e98a4ad4 100644 --- a/api/server/services/Endpoints/bedrock/initialize.js +++ b/api/server/services/Endpoints/bedrock/initialize.js @@ -32,6 +32,10 @@ const initializeClient = async ({ req, res, endpointOption }) => { model_parameters: endpointOption.model_parameters, }; + if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) { + agent.instructions = `${agent.instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim(); + } + let modelOptions = { model: agent.model }; // TODO: pass-in override settings that are specific to current run diff --git a/client/src/common/types.ts b/client/src/common/types.ts index bf003fbf68e..b969346e70a 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -339,8 +339,12 @@ export type TAdditionalProps = { export type TMessageContentProps = TInitialProps & TAdditionalProps; export type TText = Pick & { className?: string }; -export type TEditProps = Pick & - Omit; +export type TEditProps = Pick & + Omit & { + text?: string; + index?: number; + siblingIdx: number | null; + }; export type TDisplayProps = TText & Pick & { showCursor?: boolean; diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index 9d8e6e4cbf1..b64a289dd2b 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -1,5 +1,7 @@ import { memo } from 'react'; +import { ContentTypes } from 'librechat-data-provider'; import type { TMessageContentParts } from 'librechat-data-provider'; +import EditTextPart from './Parts/EditTextPart'; import Part from './Part'; type ContentPartsProps = { @@ -8,13 +10,54 @@ type ContentPartsProps = { isCreatedByUser: boolean; isLast: boolean; isSubmitting: boolean; + edit?: boolean; + enterEdit?: (cancel?: boolean) => void | null | undefined; + siblingIdx?: number; + setSiblingIdx?: + | ((value: number) => void | React.Dispatch>) + | null + | undefined; }; const ContentParts = memo( - ({ content, messageId, isCreatedByUser, isLast, isSubmitting }: ContentPartsProps) => { + ({ + content, + messageId, + isCreatedByUser, + isLast, + isSubmitting, + edit, + enterEdit, + siblingIdx, + setSiblingIdx, + }: ContentPartsProps) => { if (!content) { return null; } + if (edit === true && enterEdit && setSiblingIdx) { + return ( + <> + {content.map((part, idx) => { + if (part?.type !== ContentTypes.TEXT || typeof part.text !== 'string') { + return null; + } + + return ( + + ); + })} + + ); + } return ( <> {content diff --git a/client/src/components/Chat/Messages/Content/EditMessage.tsx b/client/src/components/Chat/Messages/Content/EditMessage.tsx index 8d7c9ffee18..ae9018d70eb 100644 --- a/client/src/components/Chat/Messages/Content/EditMessage.tsx +++ b/client/src/components/Chat/Messages/Content/EditMessage.tsx @@ -1,10 +1,11 @@ -import { useRecoilState } from 'recoil'; -import TextareaAutosize from 'react-textarea-autosize'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { EModelEndpoint } from 'librechat-data-provider'; -import { useState, useRef, useEffect, useCallback } from 'react'; +import { useRef, useEffect, useCallback } from 'react'; +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 { cn, removeFocusRings } from '~/utils'; import { useLocalize } from '~/hooks'; import Container from './Container'; @@ -25,7 +26,6 @@ const EditMessage = ({ store.latestMessageFamily(addedIndex), ); - const [editedText, setEditedText] = useState(text ?? ''); const textAreaRef = useRef(null); const { conversationId, parentMessageId, messageId } = message; @@ -34,6 +34,15 @@ const EditMessage = ({ const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); const localize = useLocalize(); + const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); + const isRTL = chatDirection === 'rtl'; + + const { register, handleSubmit, setValue } = useForm({ + defaultValues: { + text: text ?? '', + }, + }); + useEffect(() => { const textArea = textAreaRef.current; if (textArea) { @@ -43,11 +52,11 @@ const EditMessage = ({ } }, []); - const resubmitMessage = () => { + const resubmitMessage = (data: { text: string }) => { if (message.isCreatedByUser) { ask( { - text: editedText, + text: data.text, parentMessageId, conversationId, }, @@ -67,7 +76,7 @@ const EditMessage = ({ ask( { ...parentMessage }, { - editedText, + editedText: data.text, editedMessageId: messageId, isRegenerate: true, isEdited: true, @@ -80,7 +89,7 @@ const EditMessage = ({ enterEdit(true); }; - const updateMessage = () => { + const updateMessage = (data: { text: string }) => { const messages = getMessages(); if (!messages) { return; @@ -88,24 +97,24 @@ const EditMessage = ({ updateMessageMutation.mutate({ conversationId: conversationId ?? '', model: conversation?.model ?? 'gpt-3.5-turbo', - text: editedText, + text: data.text, messageId, }); if (message.messageId === latestMultiMessage?.messageId) { - setLatestMultiMessage({ ...latestMultiMessage, text: editedText }); + setLatestMultiMessage({ ...latestMultiMessage, text: data.text }); } - const isInMessages = messages?.some((message) => message?.messageId === messageId); + const isInMessages = messages.some((message) => message.messageId === messageId); if (!isInMessages) { - message.text = editedText; + message.text = data.text; } else { setMessages( messages.map((msg) => msg.messageId === messageId ? { ...msg, - text: editedText, + text: data.text, isEdited: true, } : msg, @@ -126,43 +135,33 @@ const EditMessage = ({ [enterEdit], ); + const { ref, ...registerProps } = register('text', { + required: true, + onChange: (e) => { + setValue('text', e.target.value, { shouldValidate: true }); + }, + }); + return ( -
+
{ - setEditedText(e.target.value); + {...registerProps} + ref={(e) => { + ref(e); + textAreaRef.current = e; }} onKeyDown={handleKeyDown} data-testid="message-text-editor" className={cn( - 'markdown prose dark:prose-invert light whitespace-pre-wrap break-words', - 'pl-3 md:pl-4', + 'markdown prose dark:prose-invert light whitespace-pre-wrap break-words pl-3 md:pl-4', 'm-0 w-full resize-none border-0 bg-transparent py-[10px]', - 'placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ', - 'pr-3 md:pr-4', - 'max-h-[65vh] md:max-h-[75vh]', + 'placeholder-text-secondary focus:ring-0 focus-visible:ring-0 md:py-3.5', + isRTL ? 'text-right' : 'text-left', + 'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4', removeFocusRings, )} - onPaste={(e) => { - e.preventDefault(); - - const pastedData = e.clipboardData.getData('text/plain'); - const textArea = textAreaRef.current; - if (!textArea) { - return; - } - const start = textArea.selectionStart; - const end = textArea.selectionEnd; - const newValue = - textArea.value.substring(0, start) + pastedData + textArea.value.substring(end); - setEditedText(newValue); - }} - contentEditable={true} - value={editedText} - suppressContentEditableWarning={true} - dir="auto" + dir={isRTL ? 'rtl' : 'ltr'} />
@@ -171,14 +170,14 @@ const EditMessage = ({ disabled={ isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser) } - onClick={resubmitMessage} + onClick={handleSubmit(resubmitMessage)} > {localize('com_ui_save_submit')} diff --git a/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx b/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx new file mode 100644 index 00000000000..d4ce5dff4d6 --- /dev/null +++ b/client/src/components/Chat/Messages/Content/Parts/EditTextPart.tsx @@ -0,0 +1,193 @@ +import { useForm } from 'react-hook-form'; +import { ContentTypes } from 'librechat-data-provider'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRef, useEffect, useCallback, useMemo } from 'react'; +import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query'; +import type { TEditProps } from '~/common'; +import Container from '~/components/Chat/Messages/Content/Container'; +import { useChatContext, useAddedChatContext } from '~/Providers'; +import { TextareaAutosize } from '~/components/ui'; +import { cn, removeFocusRings } from '~/utils'; +import { useLocalize } from '~/hooks'; +import store from '~/store'; + +const EditTextPart = ({ + text, + index, + messageId, + isSubmitting, + enterEdit, +}: Omit & { + index: number; + messageId: string; +}) => { + const localize = useLocalize(); + const { addedIndex } = useAddedChatContext(); + const { getMessages, setMessages, conversation } = useChatContext(); + const [latestMultiMessage, setLatestMultiMessage] = useRecoilState( + store.latestMessageFamily(addedIndex), + ); + + const { conversationId = '' } = conversation ?? {}; + const message = useMemo( + () => getMessages()?.find((msg) => msg.messageId === messageId), + [getMessages, messageId], + ); + + const textAreaRef = useRef(null); + const updateMessageContentMutation = useUpdateMessageContentMutation(conversationId ?? ''); + + const chatDirection = useRecoilValue(store.chatDirection).toLowerCase(); + const isRTL = chatDirection === 'rtl'; + + const { register, handleSubmit, setValue } = useForm({ + defaultValues: { + text: text ?? '', + }, + }); + + useEffect(() => { + const textArea = textAreaRef.current; + if (textArea) { + const length = textArea.value.length; + textArea.focus(); + textArea.setSelectionRange(length, length); + } + }, []); + + /* + const resubmitMessage = () => { + showToast({ + status: 'warning', + message: localize('com_warning_resubmit_unsupported'), + }); + + // const resubmitMessage = (data: { text: string }) => { + // Not supported by AWS Bedrock + const messages = getMessages(); + const parentMessage = messages?.find((msg) => msg.messageId === message?.parentMessageId); + + if (!parentMessage) { + return; + } + ask( + { ...parentMessage }, + { + editedText: data.text, + editedMessageId: messageId, + isRegenerate: true, + isEdited: true, + }, + ); + + setSiblingIdx((siblingIdx ?? 0) - 1); + enterEdit(true); + }; + */ + + const updateMessage = (data: { text: string }) => { + const messages = getMessages(); + if (!messages) { + return; + } + updateMessageContentMutation.mutate({ + index, + conversationId: conversationId ?? '', + text: data.text, + messageId, + }); + + if (messageId === latestMultiMessage?.messageId) { + setLatestMultiMessage({ ...latestMultiMessage, text: data.text }); + } + + const isInMessages = messages.some((msg) => msg.messageId === messageId); + if (!isInMessages) { + return enterEdit(true); + } + + const updatedContent = message?.content?.map((part, idx) => { + if (part.type === ContentTypes.TEXT && idx === index) { + return { ...part, text: data.text }; + } + return part; + }); + + setMessages( + messages.map((msg) => + msg.messageId === messageId + ? { + ...msg, + content: updatedContent, + isEdited: true, + } + : msg, + ), + ); + + enterEdit(true); + }; + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + enterEdit(true); + } + }, + [enterEdit], + ); + + const { ref, ...registerProps } = register('text', { + required: true, + onChange: (e) => { + setValue('text', e.target.value, { shouldValidate: true }); + }, + }); + + return ( + +
+ { + ref(e); + textAreaRef.current = e; + }} + onKeyDown={handleKeyDown} + data-testid="message-text-editor" + className={cn( + 'markdown prose dark:prose-invert light whitespace-pre-wrap break-words pl-3 md:pl-4', + 'm-0 w-full resize-none border-0 bg-transparent py-[10px]', + 'placeholder-text-secondary focus:ring-0 focus-visible:ring-0 md:py-3.5', + isRTL ? 'text-right' : 'text-left', + 'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4', + removeFocusRings, + )} + dir={isRTL ? 'rtl' : 'ltr'} + /> +
+
+ {/* */} + + +
+
+ ); +}; + +export default EditTextPart; diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx index 1f008e263cf..4e9a3b28d6c 100644 --- a/client/src/components/Endpoints/MinimalIcon.tsx +++ b/client/src/components/Endpoints/MinimalIcon.tsx @@ -83,13 +83,13 @@ const MinimalIcon: React.FC = (props) => { height: size, }} className={cn( - 'relative flex items-center justify-center rounded-sm text-black dark:text-white', + 'relative flex items-center justify-center rounded-sm text-text-secondary', props.className ?? '', )} > {icon} {error === true && ( - + ! )} diff --git a/client/src/components/Messages/Content/EditMessage.tsx b/client/src/components/Messages/Content/EditMessage.tsx deleted file mode 100644 index dabe9fe6482..00000000000 --- a/client/src/components/Messages/Content/EditMessage.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useRef } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { useUpdateMessageMutation } from 'librechat-data-provider/react-query'; -import type { TEditProps } from '~/common'; -import store from '~/store'; -import Container from './Container'; -import { useLocalize } from '~/hooks'; - -const EditMessage = ({ - text, - message, - isSubmitting, - ask, - enterEdit, - siblingIdx, - setSiblingIdx, -}: TEditProps) => { - const [messages, setMessages] = useRecoilState(store.messages); - const conversation = useRecoilValue(store.conversation); - const textEditor = useRef(null); - const { conversationId, parentMessageId, messageId } = message; - const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); - const localize = useLocalize(); - - const resubmitMessage = () => { - const text = textEditor?.current?.innerText ?? ''; - if (message.isCreatedByUser) { - ask({ - text, - parentMessageId, - conversationId, - }); - - setSiblingIdx((siblingIdx ?? 0) - 1); - } else { - const parentMessage = messages?.find((msg) => msg.messageId === parentMessageId); - - if (!parentMessage) { - return; - } - ask( - { ...parentMessage }, - { - editedText: text, - editedMessageId: messageId, - isRegenerate: true, - isEdited: true, - }, - ); - - setSiblingIdx((siblingIdx ?? 0) - 1); - } - - enterEdit(true); - }; - - const updateMessage = () => { - if (!messages) { - return; - } - const text = textEditor?.current?.innerText ?? ''; - updateMessageMutation.mutate({ - conversationId: conversationId ?? '', - model: conversation?.model ?? 'gpt-3.5-turbo', - messageId, - text, - }); - setMessages(() => - messages.map((msg) => - msg.messageId === messageId - ? { - ...msg, - text, - isEdited: true, - } - : msg, - ), - ); - enterEdit(true); - }; - - return ( - -
- {text} -
-
- - - -
-
- ); -}; - -export default EditMessage; diff --git a/client/src/components/Messages/ContentRender.tsx b/client/src/components/Messages/ContentRender.tsx index f341efbfd8d..4cdba914e8b 100644 --- a/client/src/components/Messages/ContentRender.tsx +++ b/client/src/components/Messages/ContentRender.tsx @@ -134,6 +134,10 @@ const ContentRender = memo( isCreatedByUser={msg.isCreatedByUser} isLast={isLast} isSubmitting={isSubmitting} + edit={edit} + enterEdit={enterEdit} + siblingIdx={siblingIdx} + setSiblingIdx={setSiblingIdx} />
diff --git a/client/src/components/Nav/ExportConversation/ExportModal.tsx b/client/src/components/Nav/ExportConversation/ExportModal.tsx index 27c4db2a6ef..4bdfbe09931 100644 --- a/client/src/components/Nav/ExportConversation/ExportModal.tsx +++ b/client/src/components/Nav/ExportConversation/ExportModal.tsx @@ -33,7 +33,7 @@ export default function ExportModal({ ]; useEffect(() => { - setFileName(filenamify(String(conversation?.title || 'file'))); + setFileName(filenamify(String(conversation?.title ?? 'file'))); setType('screenshot'); setIncludeOptions(true); setExportBranches(false); diff --git a/client/src/data-provider/queries.ts b/client/src/data-provider/queries.ts index af7c1dacb25..2157ed94624 100644 --- a/client/src/data-provider/queries.ts +++ b/client/src/data-provider/queries.ts @@ -509,13 +509,33 @@ export const useGetAgentByIdQuery = ( /** STT/TTS */ /* Text to speech voices */ -export const useVoicesQuery = (): UseQueryResult => { - return useQuery([QueryKeys.voices], () => dataService.getVoices()); +export const useVoicesQuery = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery([QueryKeys.voices], () => dataService.getVoices(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + retry: false, + ...config, + }); }; /* Custom config speech */ -export const useCustomConfigSpeechQuery = () => { - return useQuery([QueryKeys.customConfigSpeech], () => dataService.getCustomConfigSpeech()); +export const useCustomConfigSpeechQuery = ( + config?: UseQueryOptions, +): QueryObserverResult => { + return useQuery( + [QueryKeys.customConfigSpeech], + () => dataService.getCustomConfigSpeech(), + { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + retry: false, + ...config, + }, + ); }; /** Prompt */ diff --git a/client/src/hooks/Conversations/useExportConversation.ts b/client/src/hooks/Conversations/useExportConversation.ts index 8cd2133d066..a1291f940b8 100644 --- a/client/src/hooks/Conversations/useExportConversation.ts +++ b/client/src/hooks/Conversations/useExportConversation.ts @@ -20,6 +20,12 @@ import { useScreenshot } from '~/hooks/ScreenshotContext'; import { cleanupPreset, buildTree } from '~/utils'; import { useParams } from 'react-router-dom'; +type ExportValues = { + fieldName: string; + fieldValues: string[]; +}; +type ExportEntries = ExportValues[]; + export default function useExportConversation({ conversation, filename, @@ -48,7 +54,7 @@ export default function useExportConversation({ return dataTree?.length === 0 ? null : dataTree ?? null; }, [paramId, conversation?.conversationId, queryClient]); - const getMessageText = (message: TMessage, format = 'text') => { + const getMessageText = (message: TMessage | undefined, format = 'text') => { if (!message) { return ''; } @@ -77,7 +83,7 @@ export default function useExportConversation({ * Currently, content whose type is `TOOL_CALL` basically returns JSON as is. * In the future, different formatted text may be returned for each type. */ - const getMessageContent = (sender: string, content: TMessageContentParts): string[] => { + const getMessageContent = (sender: string, content?: TMessageContentParts): string[] => { if (!content) { return []; } @@ -89,7 +95,9 @@ export default function useExportConversation({ if (content.type === ContentTypes.TEXT) { // TEXT - return [sender, content[ContentTypes.TEXT].value]; + const textPart = content[ContentTypes.TEXT]; + const text = typeof textPart === 'string' ? textPart : textPart.value; + return [sender, text]; } if (content.type === ContentTypes.TOOL_CALL) { @@ -154,7 +162,7 @@ export default function useExportConversation({ messageId: conversation?.conversationId, message: null, messages: getMessageTree(), - branches: !!exportBranches, + branches: Boolean(exportBranches), recursive: false, }); @@ -171,7 +179,7 @@ export default function useExportConversation({ fileName: filename, extension: 'csv', exportType: exportFromJSON.types.csv, - beforeTableEncode: (entries) => [ + beforeTableEncode: (entries: ExportEntries | undefined) => [ { fieldName: 'sender', fieldValues: entries?.find((e) => e.fieldName == 'sender')?.fieldValues ?? [], @@ -216,7 +224,7 @@ export default function useExportConversation({ `- title: ${conversation?.title}\n` + `- exportAt: ${new Date().toTimeString()}\n`; - if (includeOptions) { + if (includeOptions === true) { data += '\n## Options\n'; const options = cleanupPreset({ preset: conversation as TPreset }); @@ -240,7 +248,7 @@ export default function useExportConversation({ if (message.error) { data += '*(This is an error message)*\n'; } - if (message.unfinished) { + if (message.unfinished === true) { data += '*(This is an unfinished message)*\n'; } data += '\n\n'; @@ -250,7 +258,7 @@ export default function useExportConversation({ if (messages.error) { data += '*(This is an error message)*\n'; } - if (messages.unfinished) { + if (messages.unfinished === true) { data += '*(This is an unfinished message)*\n'; } } @@ -272,7 +280,7 @@ export default function useExportConversation({ `title: ${conversation?.title}\n` + `exportAt: ${new Date().toTimeString()}\n`; - if (includeOptions) { + if (includeOptions === true) { data += '\nOptions\n########################\n'; const options = cleanupPreset({ preset: conversation as TPreset }); @@ -296,7 +304,7 @@ export default function useExportConversation({ if (message.error) { data += '(This is an error message)\n'; } - if (message.unfinished) { + if (message.unfinished === true) { data += '(This is an unfinished message)\n'; } data += '\n\n'; @@ -306,7 +314,7 @@ export default function useExportConversation({ if (messages.error) { data += '(This is an error message)\n'; } - if (messages.unfinished) { + if (messages.unfinished === true) { data += '(This is an unfinished message)\n'; } } @@ -329,7 +337,7 @@ export default function useExportConversation({ recursive: recursive, }; - if (includeOptions) { + if (includeOptions === true) { data['options'] = cleanupPreset({ preset: conversation as TPreset }); } @@ -337,11 +345,11 @@ export default function useExportConversation({ messageId: conversation?.conversationId, message: null, messages: getMessageTree(), - branches: !!exportBranches, - recursive: !!recursive, + branches: Boolean(exportBranches), + recursive: Boolean(recursive), }); - if (recursive && !Array.isArray(messages)) { + if (recursive === true && !Array.isArray(messages)) { data['messagesTree'] = messages.children; } else { data['messages'] = messages; diff --git a/client/src/hooks/Messages/useCopyToClipboard.ts b/client/src/hooks/Messages/useCopyToClipboard.ts index 50111f68aa1..06f5e06c5e3 100644 --- a/client/src/hooks/Messages/useCopyToClipboard.ts +++ b/client/src/hooks/Messages/useCopyToClipboard.ts @@ -14,12 +14,13 @@ export default function useCopyToClipboard({ if (content) { messageText = content.reduce((acc, curr, i) => { if (curr.type === ContentTypes.TEXT) { - return acc + curr.text.value + (i === content.length - 1 ? '' : '\n'); + const text = typeof curr.text === 'string' ? curr.text : curr.text.value; + return acc + text + (i === content.length - 1 ? '' : '\n'); } return acc; }, ''); } - copy(messageText ?? '', { format: 'text/plain' }); + copy(messageText, { format: 'text/plain' }); setTimeout(() => { setIsCopied(false); diff --git a/client/src/localization/languages/Eng.ts b/client/src/localization/languages/Eng.ts index ca6cb35a5e0..da456ce41d9 100644 --- a/client/src/localization/languages/Eng.ts +++ b/client/src/localization/languages/Eng.ts @@ -22,6 +22,8 @@ export default { 'It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We\'re unable to proceed with this specific topic. If you have any other questions or topics you\'d like to explore, please edit your message, or create a new conversation.', com_error_no_user_key: 'No key found. Please provide a key and try again.', com_error_no_base_url: 'No base URL found. Please provide one and try again.', + com_warning_resubmit_unsupported: + 'Resubmitting the AI message is not supported for this endpoint.', com_error_invalid_request: 'The AI service rejected the request due to an error. This could be caused by an invalid API key or an improperly formatted request.', com_error_invalid_user_key: 'Invalid key provided. Please provide a valid key and try again.', diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index d2d7ffe08eb..035c1d7e277 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -11,7 +11,7 @@ export const getLengthAndLastTenChars = (str?: string): string => { return `${length}${lastTenChars}`; }; -export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) => { +export const getLatestText = (message?: TMessage | null, includeIndex?: boolean): string => { if (!message) { return ''; } @@ -21,11 +21,12 @@ export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) if (message.content && message.content.length > 0) { for (let i = message.content.length - 1; i >= 0; i--) { const part = message.content[i]; - if ( - part.type === ContentTypes.TEXT && - ((part[ContentTypes.TEXT].value as string | undefined)?.length ?? 0) > 0 - ) { - const text = part[ContentTypes.TEXT].value; + if (part.type !== ContentTypes.TEXT) { + continue; + } + + const text = (typeof part.text === 'string' ? part.text : part.text.value) || ''; + if (text.length > 0) { if (includeIndex === true) { return `${text}-${i}`; } else { diff --git a/package-lock.json b/package-lock.json index f63604f10fe..44d59720268 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36399,7 +36399,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.421", + "version": "0.7.422", "license": "ISC", "dependencies": { "@types/js-yaml": "^4.0.9", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 1a040338601..36ffa583012 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.421", + "version": "0.7.422", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/bedrock.ts b/packages/data-provider/src/bedrock.ts index c4e231ec50c..6ee2be2fe0f 100644 --- a/packages/data-provider/src/bedrock.ts +++ b/packages/data-provider/src/bedrock.ts @@ -12,6 +12,7 @@ export const bedrockInputSchema = s.tConversationSchema spec: true, maxOutputTokens: true, maxContextTokens: true, + artifacts: true, /* Bedrock params; optionType: 'model' */ region: true, system: true, @@ -38,6 +39,7 @@ export const bedrockInputParser = s.tConversationSchema iconURL: true, greeting: true, spec: true, + artifacts: true, maxOutputTokens: true, maxContextTokens: true, /* Bedrock params; optionType: 'model' */ @@ -61,6 +63,7 @@ export const bedrockInputParser = s.tConversationSchema 'greeting', 'spec', 'maxOutputTokens', + 'artifacts', 'additionalModelRequestFields', 'region', 'model', diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 277146ae90a..597d264ccf1 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -617,8 +617,8 @@ export const bedrockModels = [ 'anthropic.claude-v2', 'anthropic.claude-v2:1', 'anthropic.claude-instant-v1', - 'cohere.command-text-v14', - 'cohere.command-light-text-v14', + // 'cohere.command-text-v14', // no conversation history + // 'cohere.command-light-text-v14', // no conversation history 'cohere.command-r-v1:0', 'cohere.command-r-plus-v1:0', 'meta.llama2-13b-chat-v1', diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 71c06654cc4..b5008b37ddd 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -74,6 +74,15 @@ export function updateMessage(payload: t.TUpdateMessageRequest): Promise { + const { conversationId, messageId, index, text } = payload; + if (!conversationId) { + throw new Error('conversationId is required'); + } + + return request.put(endpoints.messages(conversationId, messageId), { text, index }); +} + export function updateUserKey(payload: t.TUpdateUserKeyRequest) { const { value } = payload; if (!value) { diff --git a/packages/data-provider/src/react-query/react-query-service.ts b/packages/data-provider/src/react-query/react-query-service.ts index 76f2c11c9f4..8d11f040d74 100644 --- a/packages/data-provider/src/react-query/react-query-service.ts +++ b/packages/data-provider/src/react-query/react-query-service.ts @@ -124,6 +124,20 @@ export const useUpdateMessageMutation = ( }); }; +export const useUpdateMessageContentMutation = ( + conversationId: string, +): UseMutationResult => { + const queryClient = useQueryClient(); + return useMutation( + (payload: t.TUpdateMessageContent) => dataService.updateMessageContent(payload), + { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.messages, conversationId]); + }, + }, + ); +}; + export const useUpdateUserKeysMutation = (): UseMutationResult< t.TUser, unknown, diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 9f65ae8b209..4e4c2ded384 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -128,6 +128,13 @@ export type TUpdateMessageRequest = { text: string; }; +export type TUpdateMessageContent = { + conversationId: string; + messageId: string; + index: number; + text: string; +}; + export type TUpdateUserKeyRequest = { name: string; value: string;