🪨 fix: Minor AWS Bedrock/Misc. Improvements (#3974)

* 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
This commit is contained in:
Danny Avila 2024-09-10 12:56:19 -04:00 committed by GitHub
parent 341e086d70
commit 1a1e6850a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 441 additions and 203 deletions

View file

@ -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 # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
# Notes on specific models: # Notes on specific models:
# 'ai21.j2-mid-v1', # Not supported, as it doesn't support streaming # The following models are not support due to not supporting streaming:
# 'ai21.j2-ultra-v1', # Not supported, as it doesn't support conversation history # 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 # # Google #

View file

@ -1,4 +1,5 @@
const express = require('express'); const express = require('express');
const { ContentTypes } = require('librechat-data-provider');
const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models'); const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models');
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware'); const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
const { countTokens } = require('~/server/utils'); 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) => { router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
try { try {
const { messageId, model } = req.params; const { conversationId, messageId } = req.params;
const { text } = req.body; const { text, index, model } = req.body;
const tokenCount = await countTokens(text, model);
const result = await updateMessage(req, { messageId, text, tokenCount }); if (index === undefined) {
res.status(200).json(result); 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) { } catch (error) {
logger.error('Error updating message:', error); logger.error('Error updating message:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });

View file

@ -32,6 +32,10 @@ const initializeClient = async ({ req, res, endpointOption }) => {
model_parameters: endpointOption.model_parameters, 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 }; let modelOptions = { model: agent.model };
// TODO: pass-in override settings that are specific to current run // TODO: pass-in override settings that are specific to current run

View file

@ -339,8 +339,12 @@ export type TAdditionalProps = {
export type TMessageContentProps = TInitialProps & TAdditionalProps; export type TMessageContentProps = TInitialProps & TAdditionalProps;
export type TText = Pick<TInitialProps, 'text'> & { className?: string }; export type TText = Pick<TInitialProps, 'text'> & { className?: string };
export type TEditProps = Pick<TInitialProps, 'text' | 'isSubmitting'> & export type TEditProps = Pick<TInitialProps, 'isSubmitting'> &
Omit<TAdditionalProps, 'isCreatedByUser'>; Omit<TAdditionalProps, 'isCreatedByUser' | 'siblingIdx'> & {
text?: string;
index?: number;
siblingIdx: number | null;
};
export type TDisplayProps = TText & export type TDisplayProps = TText &
Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & { Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & {
showCursor?: boolean; showCursor?: boolean;

View file

@ -1,5 +1,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { ContentTypes } from 'librechat-data-provider';
import type { TMessageContentParts } from 'librechat-data-provider'; import type { TMessageContentParts } from 'librechat-data-provider';
import EditTextPart from './Parts/EditTextPart';
import Part from './Part'; import Part from './Part';
type ContentPartsProps = { type ContentPartsProps = {
@ -8,13 +10,54 @@ type ContentPartsProps = {
isCreatedByUser: boolean; isCreatedByUser: boolean;
isLast: boolean; isLast: boolean;
isSubmitting: boolean; isSubmitting: boolean;
edit?: boolean;
enterEdit?: (cancel?: boolean) => void | null | undefined;
siblingIdx?: number;
setSiblingIdx?:
| ((value: number) => void | React.Dispatch<React.SetStateAction<number>>)
| null
| undefined;
}; };
const ContentParts = memo( const ContentParts = memo(
({ content, messageId, isCreatedByUser, isLast, isSubmitting }: ContentPartsProps) => { ({
content,
messageId,
isCreatedByUser,
isLast,
isSubmitting,
edit,
enterEdit,
siblingIdx,
setSiblingIdx,
}: ContentPartsProps) => {
if (!content) { if (!content) {
return null; return null;
} }
if (edit === true && enterEdit && setSiblingIdx) {
return (
<>
{content.map((part, idx) => {
if (part?.type !== ContentTypes.TEXT || typeof part.text !== 'string') {
return null;
}
return (
<EditTextPart
index={idx}
text={part.text}
messageId={messageId}
isSubmitting={isSubmitting}
enterEdit={enterEdit}
siblingIdx={siblingIdx ?? null}
setSiblingIdx={setSiblingIdx}
key={`edit-${messageId}-${idx}`}
/>
);
})}
</>
);
}
return ( return (
<> <>
{content {content

View file

@ -1,10 +1,11 @@
import { useRecoilState } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import TextareaAutosize from 'react-textarea-autosize';
import { EModelEndpoint } from 'librechat-data-provider'; 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 { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
import type { TEditProps } from '~/common'; import type { TEditProps } from '~/common';
import { useChatContext, useAddedChatContext } from '~/Providers'; import { useChatContext, useAddedChatContext } from '~/Providers';
import { TextareaAutosize } from '~/components/ui';
import { cn, removeFocusRings } from '~/utils'; import { cn, removeFocusRings } from '~/utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import Container from './Container'; import Container from './Container';
@ -25,7 +26,6 @@ const EditMessage = ({
store.latestMessageFamily(addedIndex), store.latestMessageFamily(addedIndex),
); );
const [editedText, setEditedText] = useState<string>(text ?? '');
const textAreaRef = useRef<HTMLTextAreaElement | null>(null); const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
const { conversationId, parentMessageId, messageId } = message; const { conversationId, parentMessageId, messageId } = message;
@ -34,6 +34,15 @@ const EditMessage = ({
const updateMessageMutation = useUpdateMessageMutation(conversationId ?? ''); const updateMessageMutation = useUpdateMessageMutation(conversationId ?? '');
const localize = useLocalize(); const localize = useLocalize();
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
const isRTL = chatDirection === 'rtl';
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
text: text ?? '',
},
});
useEffect(() => { useEffect(() => {
const textArea = textAreaRef.current; const textArea = textAreaRef.current;
if (textArea) { if (textArea) {
@ -43,11 +52,11 @@ const EditMessage = ({
} }
}, []); }, []);
const resubmitMessage = () => { const resubmitMessage = (data: { text: string }) => {
if (message.isCreatedByUser) { if (message.isCreatedByUser) {
ask( ask(
{ {
text: editedText, text: data.text,
parentMessageId, parentMessageId,
conversationId, conversationId,
}, },
@ -67,7 +76,7 @@ const EditMessage = ({
ask( ask(
{ ...parentMessage }, { ...parentMessage },
{ {
editedText, editedText: data.text,
editedMessageId: messageId, editedMessageId: messageId,
isRegenerate: true, isRegenerate: true,
isEdited: true, isEdited: true,
@ -80,7 +89,7 @@ const EditMessage = ({
enterEdit(true); enterEdit(true);
}; };
const updateMessage = () => { const updateMessage = (data: { text: string }) => {
const messages = getMessages(); const messages = getMessages();
if (!messages) { if (!messages) {
return; return;
@ -88,24 +97,24 @@ const EditMessage = ({
updateMessageMutation.mutate({ updateMessageMutation.mutate({
conversationId: conversationId ?? '', conversationId: conversationId ?? '',
model: conversation?.model ?? 'gpt-3.5-turbo', model: conversation?.model ?? 'gpt-3.5-turbo',
text: editedText, text: data.text,
messageId, messageId,
}); });
if (message.messageId === latestMultiMessage?.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) { if (!isInMessages) {
message.text = editedText; message.text = data.text;
} else { } else {
setMessages( setMessages(
messages.map((msg) => messages.map((msg) =>
msg.messageId === messageId msg.messageId === messageId
? { ? {
...msg, ...msg,
text: editedText, text: data.text,
isEdited: true, isEdited: true,
} }
: msg, : msg,
@ -126,43 +135,33 @@ const EditMessage = ({
[enterEdit], [enterEdit],
); );
const { ref, ...registerProps } = register('text', {
required: true,
onChange: (e) => {
setValue('text', e.target.value, { shouldValidate: true });
},
});
return ( return (
<Container message={message}> <Container message={message}>
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border dark:border-gray-600 dark:text-white [&:has(textarea:focus)]:border-gray-300 [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)] dark:[&:has(textarea:focus)]:border-gray-500"> <div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border border-border-medium text-text-primary [&:has(textarea:focus)]:border-border-heavy [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]">
<TextareaAutosize <TextareaAutosize
ref={textAreaRef} {...registerProps}
onChange={(e) => { ref={(e) => {
setEditedText(e.target.value); ref(e);
textAreaRef.current = e;
}} }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
data-testid="message-text-editor" data-testid="message-text-editor"
className={cn( className={cn(
'markdown prose dark:prose-invert light whitespace-pre-wrap break-words', 'markdown prose dark:prose-invert light whitespace-pre-wrap break-words pl-3 md:pl-4',
'pl-3 md:pl-4',
'm-0 w-full resize-none border-0 bg-transparent py-[10px]', '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 ', 'placeholder-text-secondary focus:ring-0 focus-visible:ring-0 md:py-3.5',
'pr-3 md:pr-4', isRTL ? 'text-right' : 'text-left',
'max-h-[65vh] md:max-h-[75vh]', 'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4',
removeFocusRings, removeFocusRings,
)} )}
onPaste={(e) => { dir={isRTL ? 'rtl' : 'ltr'}
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"
/> />
</div> </div>
<div className="mt-2 flex w-full justify-center text-center"> <div className="mt-2 flex w-full justify-center text-center">
@ -171,14 +170,14 @@ const EditMessage = ({
disabled={ disabled={
isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser) isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser)
} }
onClick={resubmitMessage} onClick={handleSubmit(resubmitMessage)}
> >
{localize('com_ui_save_submit')} {localize('com_ui_save_submit')}
</button> </button>
<button <button
className="btn btn-secondary relative mr-2" className="btn btn-secondary relative mr-2"
disabled={isSubmitting} disabled={isSubmitting}
onClick={updateMessage} onClick={handleSubmit(updateMessage)}
> >
{localize('com_ui_save')} {localize('com_ui_save')}
</button> </button>

View file

@ -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<TEditProps, 'message' | 'ask'> & {
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<HTMLTextAreaElement | null>(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<HTMLTextAreaElement>) => {
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 (
<Container message={message}>
<div className="bg-token-main-surface-primary relative flex w-full flex-grow flex-col overflow-hidden rounded-2xl border border-border-medium text-text-primary [&:has(textarea:focus)]:border-border-heavy [&:has(textarea:focus)]:shadow-[0_2px_6px_rgba(0,0,0,.05)]">
<TextareaAutosize
{...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',
'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'}
/>
</div>
<div className="mt-2 flex w-full justify-center text-center">
{/* <button
className="btn btn-primary relative mr-2"
disabled={isSubmitting}
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>
</div>
</Container>
);
};
export default EditTextPart;

View file

@ -83,13 +83,13 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
height: size, height: size,
}} }}
className={cn( 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 ?? '', props.className ?? '',
)} )}
> >
{icon} {icon}
{error === true && ( {error === true && (
<span className="absolute right-0 top-[20px] -mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-white bg-red-500 text-[10px] text-black dark:text-white"> <span className="absolute right-0 top-[20px] -mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-white bg-red-500 text-[10px] text-text-secondary">
! !
</span> </span>
)} )}

View file

@ -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<HTMLDivElement | null>(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 (
<Container>
<div
data-testid="message-text-editor"
className="markdown prose dark:prose-invert light w-full whitespace-pre-wrap break-words border-none focus:outline-none"
contentEditable={true}
ref={textEditor}
suppressContentEditableWarning={true}
dir="auto"
>
{text}
</div>
<div className="mt-2 flex w-full justify-center text-center">
<button
className="btn btn-primary relative mr-2"
disabled={isSubmitting}
onClick={resubmitMessage}
>
{localize('com_ui_save_submit')}
</button>
<button
className="btn btn-secondary relative mr-2"
disabled={isSubmitting}
onClick={updateMessage}
>
{localize('com_ui_save')}
</button>
<button className="btn btn-neutral relative" onClick={() => enterEdit(true)}>
{localize('com_ui_cancel')}
</button>
</div>
</Container>
);
};
export default EditMessage;

View file

@ -134,6 +134,10 @@ const ContentRender = memo(
isCreatedByUser={msg.isCreatedByUser} isCreatedByUser={msg.isCreatedByUser}
isLast={isLast} isLast={isLast}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
edit={edit}
enterEdit={enterEdit}
siblingIdx={siblingIdx}
setSiblingIdx={setSiblingIdx}
/> />
</div> </div>
</div> </div>

View file

@ -33,7 +33,7 @@ export default function ExportModal({
]; ];
useEffect(() => { useEffect(() => {
setFileName(filenamify(String(conversation?.title || 'file'))); setFileName(filenamify(String(conversation?.title ?? 'file')));
setType('screenshot'); setType('screenshot');
setIncludeOptions(true); setIncludeOptions(true);
setExportBranches(false); setExportBranches(false);

View file

@ -509,13 +509,33 @@ export const useGetAgentByIdQuery = (
/** STT/TTS */ /** STT/TTS */
/* Text to speech voices */ /* Text to speech voices */
export const useVoicesQuery = (): UseQueryResult<t.VoiceResponse> => { export const useVoicesQuery = (
return useQuery([QueryKeys.voices], () => dataService.getVoices()); config?: UseQueryOptions<t.VoiceResponse>,
): QueryObserverResult<t.VoiceResponse> => {
return useQuery<t.VoiceResponse>([QueryKeys.voices], () => dataService.getVoices(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
});
}; };
/* Custom config speech */ /* Custom config speech */
export const useCustomConfigSpeechQuery = () => { export const useCustomConfigSpeechQuery = (
return useQuery([QueryKeys.customConfigSpeech], () => dataService.getCustomConfigSpeech()); config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
return useQuery<t.TCustomConfigSpeechResponse>(
[QueryKeys.customConfigSpeech],
() => dataService.getCustomConfigSpeech(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
}; };
/** Prompt */ /** Prompt */

View file

@ -20,6 +20,12 @@ import { useScreenshot } from '~/hooks/ScreenshotContext';
import { cleanupPreset, buildTree } from '~/utils'; import { cleanupPreset, buildTree } from '~/utils';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
type ExportValues = {
fieldName: string;
fieldValues: string[];
};
type ExportEntries = ExportValues[];
export default function useExportConversation({ export default function useExportConversation({
conversation, conversation,
filename, filename,
@ -48,7 +54,7 @@ export default function useExportConversation({
return dataTree?.length === 0 ? null : dataTree ?? null; return dataTree?.length === 0 ? null : dataTree ?? null;
}, [paramId, conversation?.conversationId, queryClient]); }, [paramId, conversation?.conversationId, queryClient]);
const getMessageText = (message: TMessage, format = 'text') => { const getMessageText = (message: TMessage | undefined, format = 'text') => {
if (!message) { if (!message) {
return ''; return '';
} }
@ -77,7 +83,7 @@ export default function useExportConversation({
* Currently, content whose type is `TOOL_CALL` basically returns JSON as is. * Currently, content whose type is `TOOL_CALL` basically returns JSON as is.
* In the future, different formatted text may be returned for each type. * 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) { if (!content) {
return []; return [];
} }
@ -89,7 +95,9 @@ export default function useExportConversation({
if (content.type === ContentTypes.TEXT) { if (content.type === ContentTypes.TEXT) {
// 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) { if (content.type === ContentTypes.TOOL_CALL) {
@ -154,7 +162,7 @@ export default function useExportConversation({
messageId: conversation?.conversationId, messageId: conversation?.conversationId,
message: null, message: null,
messages: getMessageTree(), messages: getMessageTree(),
branches: !!exportBranches, branches: Boolean(exportBranches),
recursive: false, recursive: false,
}); });
@ -171,7 +179,7 @@ export default function useExportConversation({
fileName: filename, fileName: filename,
extension: 'csv', extension: 'csv',
exportType: exportFromJSON.types.csv, exportType: exportFromJSON.types.csv,
beforeTableEncode: (entries) => [ beforeTableEncode: (entries: ExportEntries | undefined) => [
{ {
fieldName: 'sender', fieldName: 'sender',
fieldValues: entries?.find((e) => e.fieldName == 'sender')?.fieldValues ?? [], fieldValues: entries?.find((e) => e.fieldName == 'sender')?.fieldValues ?? [],
@ -216,7 +224,7 @@ export default function useExportConversation({
`- title: ${conversation?.title}\n` + `- title: ${conversation?.title}\n` +
`- exportAt: ${new Date().toTimeString()}\n`; `- exportAt: ${new Date().toTimeString()}\n`;
if (includeOptions) { if (includeOptions === true) {
data += '\n## Options\n'; data += '\n## Options\n';
const options = cleanupPreset({ preset: conversation as TPreset }); const options = cleanupPreset({ preset: conversation as TPreset });
@ -240,7 +248,7 @@ export default function useExportConversation({
if (message.error) { if (message.error) {
data += '*(This is an error message)*\n'; data += '*(This is an error message)*\n';
} }
if (message.unfinished) { if (message.unfinished === true) {
data += '*(This is an unfinished message)*\n'; data += '*(This is an unfinished message)*\n';
} }
data += '\n\n'; data += '\n\n';
@ -250,7 +258,7 @@ export default function useExportConversation({
if (messages.error) { if (messages.error) {
data += '*(This is an error message)*\n'; data += '*(This is an error message)*\n';
} }
if (messages.unfinished) { if (messages.unfinished === true) {
data += '*(This is an unfinished message)*\n'; data += '*(This is an unfinished message)*\n';
} }
} }
@ -272,7 +280,7 @@ export default function useExportConversation({
`title: ${conversation?.title}\n` + `title: ${conversation?.title}\n` +
`exportAt: ${new Date().toTimeString()}\n`; `exportAt: ${new Date().toTimeString()}\n`;
if (includeOptions) { if (includeOptions === true) {
data += '\nOptions\n########################\n'; data += '\nOptions\n########################\n';
const options = cleanupPreset({ preset: conversation as TPreset }); const options = cleanupPreset({ preset: conversation as TPreset });
@ -296,7 +304,7 @@ export default function useExportConversation({
if (message.error) { if (message.error) {
data += '(This is an error message)\n'; data += '(This is an error message)\n';
} }
if (message.unfinished) { if (message.unfinished === true) {
data += '(This is an unfinished message)\n'; data += '(This is an unfinished message)\n';
} }
data += '\n\n'; data += '\n\n';
@ -306,7 +314,7 @@ export default function useExportConversation({
if (messages.error) { if (messages.error) {
data += '(This is an error message)\n'; data += '(This is an error message)\n';
} }
if (messages.unfinished) { if (messages.unfinished === true) {
data += '(This is an unfinished message)\n'; data += '(This is an unfinished message)\n';
} }
} }
@ -329,7 +337,7 @@ export default function useExportConversation({
recursive: recursive, recursive: recursive,
}; };
if (includeOptions) { if (includeOptions === true) {
data['options'] = cleanupPreset({ preset: conversation as TPreset }); data['options'] = cleanupPreset({ preset: conversation as TPreset });
} }
@ -337,11 +345,11 @@ export default function useExportConversation({
messageId: conversation?.conversationId, messageId: conversation?.conversationId,
message: null, message: null,
messages: getMessageTree(), messages: getMessageTree(),
branches: !!exportBranches, branches: Boolean(exportBranches),
recursive: !!recursive, recursive: Boolean(recursive),
}); });
if (recursive && !Array.isArray(messages)) { if (recursive === true && !Array.isArray(messages)) {
data['messagesTree'] = messages.children; data['messagesTree'] = messages.children;
} else { } else {
data['messages'] = messages; data['messages'] = messages;

View file

@ -14,12 +14,13 @@ export default function useCopyToClipboard({
if (content) { if (content) {
messageText = content.reduce((acc, curr, i) => { messageText = content.reduce((acc, curr, i) => {
if (curr.type === ContentTypes.TEXT) { 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; return acc;
}, ''); }, '');
} }
copy(messageText ?? '', { format: 'text/plain' }); copy(messageText, { format: 'text/plain' });
setTimeout(() => { setTimeout(() => {
setIsCopied(false); setIsCopied(false);

View file

@ -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.', '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_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_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: 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.', '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.', com_error_invalid_user_key: 'Invalid key provided. Please provide a valid key and try again.',

View file

@ -11,7 +11,7 @@ export const getLengthAndLastTenChars = (str?: string): string => {
return `${length}${lastTenChars}`; return `${length}${lastTenChars}`;
}; };
export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) => { export const getLatestText = (message?: TMessage | null, includeIndex?: boolean): string => {
if (!message) { if (!message) {
return ''; return '';
} }
@ -21,11 +21,12 @@ export const getLatestText = (message?: TMessage | null, includeIndex?: boolean)
if (message.content && message.content.length > 0) { if (message.content && message.content.length > 0) {
for (let i = message.content.length - 1; i >= 0; i--) { for (let i = message.content.length - 1; i >= 0; i--) {
const part = message.content[i]; const part = message.content[i];
if ( if (part.type !== ContentTypes.TEXT) {
part.type === ContentTypes.TEXT && continue;
((part[ContentTypes.TEXT].value as string | undefined)?.length ?? 0) > 0 }
) {
const text = part[ContentTypes.TEXT].value; const text = (typeof part.text === 'string' ? part.text : part.text.value) || '';
if (text.length > 0) {
if (includeIndex === true) { if (includeIndex === true) {
return `${text}-${i}`; return `${text}-${i}`;
} else { } else {

2
package-lock.json generated
View file

@ -36399,7 +36399,7 @@
}, },
"packages/data-provider": { "packages/data-provider": {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.421", "version": "0.7.422",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View file

@ -1,6 +1,6 @@
{ {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.421", "version": "0.7.422",
"description": "data services for librechat apps", "description": "data services for librechat apps",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -12,6 +12,7 @@ export const bedrockInputSchema = s.tConversationSchema
spec: true, spec: true,
maxOutputTokens: true, maxOutputTokens: true,
maxContextTokens: true, maxContextTokens: true,
artifacts: true,
/* Bedrock params; optionType: 'model' */ /* Bedrock params; optionType: 'model' */
region: true, region: true,
system: true, system: true,
@ -38,6 +39,7 @@ export const bedrockInputParser = s.tConversationSchema
iconURL: true, iconURL: true,
greeting: true, greeting: true,
spec: true, spec: true,
artifacts: true,
maxOutputTokens: true, maxOutputTokens: true,
maxContextTokens: true, maxContextTokens: true,
/* Bedrock params; optionType: 'model' */ /* Bedrock params; optionType: 'model' */
@ -61,6 +63,7 @@ export const bedrockInputParser = s.tConversationSchema
'greeting', 'greeting',
'spec', 'spec',
'maxOutputTokens', 'maxOutputTokens',
'artifacts',
'additionalModelRequestFields', 'additionalModelRequestFields',
'region', 'region',
'model', 'model',

View file

@ -617,8 +617,8 @@ export const bedrockModels = [
'anthropic.claude-v2', 'anthropic.claude-v2',
'anthropic.claude-v2:1', 'anthropic.claude-v2:1',
'anthropic.claude-instant-v1', 'anthropic.claude-instant-v1',
'cohere.command-text-v14', // 'cohere.command-text-v14', // no conversation history
'cohere.command-light-text-v14', // 'cohere.command-light-text-v14', // no conversation history
'cohere.command-r-v1:0', 'cohere.command-r-v1:0',
'cohere.command-r-plus-v1:0', 'cohere.command-r-plus-v1:0',
'meta.llama2-13b-chat-v1', 'meta.llama2-13b-chat-v1',

View file

@ -74,6 +74,15 @@ export function updateMessage(payload: t.TUpdateMessageRequest): Promise<unknown
return request.put(endpoints.messages(conversationId, messageId), { text }); return request.put(endpoints.messages(conversationId, messageId), { text });
} }
export function updateMessageContent(payload: t.TUpdateMessageContent): Promise<unknown> {
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) { export function updateUserKey(payload: t.TUpdateUserKeyRequest) {
const { value } = payload; const { value } = payload;
if (!value) { if (!value) {

View file

@ -124,6 +124,20 @@ export const useUpdateMessageMutation = (
}); });
}; };
export const useUpdateMessageContentMutation = (
conversationId: string,
): UseMutationResult<unknown, unknown, t.TUpdateMessageContent, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TUpdateMessageContent) => dataService.updateMessageContent(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.messages, conversationId]);
},
},
);
};
export const useUpdateUserKeysMutation = (): UseMutationResult< export const useUpdateUserKeysMutation = (): UseMutationResult<
t.TUser, t.TUser,
unknown, unknown,

View file

@ -128,6 +128,13 @@ export type TUpdateMessageRequest = {
text: string; text: string;
}; };
export type TUpdateMessageContent = {
conversationId: string;
messageId: string;
index: number;
text: string;
};
export type TUpdateUserKeyRequest = { export type TUpdateUserKeyRequest = {
name: string; name: string;
value: string; value: string;