mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🪨 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:
parent
341e086d70
commit
1a1e6850a3
23 changed files with 441 additions and 203 deletions
|
@ -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 #
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -339,8 +339,12 @@ export type TAdditionalProps = {
|
|||
export type TMessageContentProps = TInitialProps & TAdditionalProps;
|
||||
|
||||
export type TText = Pick<TInitialProps, 'text'> & { className?: string };
|
||||
export type TEditProps = Pick<TInitialProps, 'text' | 'isSubmitting'> &
|
||||
Omit<TAdditionalProps, 'isCreatedByUser'>;
|
||||
export type TEditProps = Pick<TInitialProps, 'isSubmitting'> &
|
||||
Omit<TAdditionalProps, 'isCreatedByUser' | 'siblingIdx'> & {
|
||||
text?: string;
|
||||
index?: number;
|
||||
siblingIdx: number | null;
|
||||
};
|
||||
export type TDisplayProps = TText &
|
||||
Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & {
|
||||
showCursor?: boolean;
|
||||
|
|
|
@ -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<React.SetStateAction<number>>)
|
||||
| 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 (
|
||||
<EditTextPart
|
||||
index={idx}
|
||||
text={part.text}
|
||||
messageId={messageId}
|
||||
isSubmitting={isSubmitting}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx ?? null}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
key={`edit-${messageId}-${idx}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{content
|
||||
|
|
|
@ -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<string>(text ?? '');
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(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 (
|
||||
<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
|
||||
ref={textAreaRef}
|
||||
onChange={(e) => {
|
||||
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'}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full justify-center text-center">
|
||||
|
@ -171,14 +170,14 @@ const EditMessage = ({
|
|||
disabled={
|
||||
isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser)
|
||||
}
|
||||
onClick={resubmitMessage}
|
||||
onClick={handleSubmit(resubmitMessage)}
|
||||
>
|
||||
{localize('com_ui_save_submit')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary relative mr-2"
|
||||
disabled={isSubmitting}
|
||||
onClick={updateMessage}
|
||||
onClick={handleSubmit(updateMessage)}
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</button>
|
||||
|
|
|
@ -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;
|
|
@ -83,13 +83,13 @@ const MinimalIcon: React.FC<IconProps> = (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 && (
|
||||
<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>
|
||||
)}
|
||||
|
|
|
@ -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;
|
|
@ -134,6 +134,10 @@ const ContentRender = memo(
|
|||
isCreatedByUser={msg.isCreatedByUser}
|
||||
isLast={isLast}
|
||||
isSubmitting={isSubmitting}
|
||||
edit={edit}
|
||||
enterEdit={enterEdit}
|
||||
siblingIdx={siblingIdx}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -509,13 +509,33 @@ export const useGetAgentByIdQuery = (
|
|||
/** STT/TTS */
|
||||
|
||||
/* Text to speech voices */
|
||||
export const useVoicesQuery = (): UseQueryResult<t.VoiceResponse> => {
|
||||
return useQuery([QueryKeys.voices], () => dataService.getVoices());
|
||||
export const useVoicesQuery = (
|
||||
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 */
|
||||
export const useCustomConfigSpeechQuery = () => {
|
||||
return useQuery([QueryKeys.customConfigSpeech], () => dataService.getCustomConfigSpeech());
|
||||
export const useCustomConfigSpeechQuery = (
|
||||
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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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 {
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -74,6 +74,15 @@ export function updateMessage(payload: t.TUpdateMessageRequest): Promise<unknown
|
|||
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) {
|
||||
const { value } = payload;
|
||||
if (!value) {
|
||||
|
|
|
@ -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<
|
||||
t.TUser,
|
||||
unknown,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue