Skip to content

Commit 1a1e685

Browse files
authored
🪨 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
1 parent 341e086 commit 1a1e685

File tree

23 files changed

+441
-203
lines changed

23 files changed

+441
-203
lines changed

.env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ BINGAI_TOKEN=user_provided
125125
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
126126

127127
# Notes on specific models:
128-
# 'ai21.j2-mid-v1', # Not supported, as it doesn't support streaming
129-
# 'ai21.j2-ultra-v1', # Not supported, as it doesn't support conversation history
128+
# The following models are not support due to not supporting streaming:
129+
# ai21.j2-mid-v1
130+
131+
# The following models are not support due to not supporting conversation history:
132+
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
130133

131134
#============#
132135
# Google #

api/server/routes/messages.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const express = require('express');
2+
const { ContentTypes } = require('librechat-data-provider');
23
const { saveConvo, saveMessage, getMessages, updateMessage, deleteMessages } = require('~/models');
34
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
45
const { countTokens } = require('~/server/utils');
@@ -54,11 +55,50 @@ router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) =
5455

5556
router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
5657
try {
57-
const { messageId, model } = req.params;
58-
const { text } = req.body;
59-
const tokenCount = await countTokens(text, model);
60-
const result = await updateMessage(req, { messageId, text, tokenCount });
61-
res.status(200).json(result);
58+
const { conversationId, messageId } = req.params;
59+
const { text, index, model } = req.body;
60+
61+
if (index === undefined) {
62+
const tokenCount = await countTokens(text, model);
63+
const result = await updateMessage(req, { messageId, text, tokenCount });
64+
return res.status(200).json(result);
65+
}
66+
67+
if (typeof index !== 'number' || index < 0) {
68+
return res.status(400).json({ error: 'Invalid index' });
69+
}
70+
71+
const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0];
72+
if (!message) {
73+
return res.status(404).json({ error: 'Message not found' });
74+
}
75+
76+
const existingContent = message.content;
77+
if (!Array.isArray(existingContent) || index >= existingContent.length) {
78+
return res.status(400).json({ error: 'Invalid index' });
79+
}
80+
81+
const updatedContent = [...existingContent];
82+
if (!updatedContent[index]) {
83+
return res.status(400).json({ error: 'Content part not found' });
84+
}
85+
86+
if (updatedContent[index].type !== ContentTypes.TEXT) {
87+
return res.status(400).json({ error: 'Cannot update non-text content' });
88+
}
89+
90+
const oldText = updatedContent[index].text;
91+
updatedContent[index] = { type: ContentTypes.TEXT, text };
92+
93+
let tokenCount = message.tokenCount;
94+
if (tokenCount !== undefined) {
95+
const oldTokenCount = await countTokens(oldText, model);
96+
const newTokenCount = await countTokens(text, model);
97+
tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount;
98+
}
99+
100+
const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount });
101+
return res.status(200).json(result);
62102
} catch (error) {
63103
logger.error('Error updating message:', error);
64104
res.status(500).json({ error: 'Internal server error' });

api/server/services/Endpoints/bedrock/initialize.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const initializeClient = async ({ req, res, endpointOption }) => {
3232
model_parameters: endpointOption.model_parameters,
3333
};
3434

35+
if (typeof endpointOption.artifactsPrompt === 'string' && endpointOption.artifactsPrompt) {
36+
agent.instructions = `${agent.instructions ?? ''}\n${endpointOption.artifactsPrompt}`.trim();
37+
}
38+
3539
let modelOptions = { model: agent.model };
3640

3741
// TODO: pass-in override settings that are specific to current run

client/src/common/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,12 @@ export type TAdditionalProps = {
339339
export type TMessageContentProps = TInitialProps & TAdditionalProps;
340340

341341
export type TText = Pick<TInitialProps, 'text'> & { className?: string };
342-
export type TEditProps = Pick<TInitialProps, 'text' | 'isSubmitting'> &
343-
Omit<TAdditionalProps, 'isCreatedByUser'>;
342+
export type TEditProps = Pick<TInitialProps, 'isSubmitting'> &
343+
Omit<TAdditionalProps, 'isCreatedByUser' | 'siblingIdx'> & {
344+
text?: string;
345+
index?: number;
346+
siblingIdx: number | null;
347+
};
344348
export type TDisplayProps = TText &
345349
Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & {
346350
showCursor?: boolean;

client/src/components/Chat/Messages/Content/ContentParts.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { memo } from 'react';
2+
import { ContentTypes } from 'librechat-data-provider';
23
import type { TMessageContentParts } from 'librechat-data-provider';
4+
import EditTextPart from './Parts/EditTextPart';
35
import Part from './Part';
46

57
type ContentPartsProps = {
@@ -8,13 +10,54 @@ type ContentPartsProps = {
810
isCreatedByUser: boolean;
911
isLast: boolean;
1012
isSubmitting: boolean;
13+
edit?: boolean;
14+
enterEdit?: (cancel?: boolean) => void | null | undefined;
15+
siblingIdx?: number;
16+
setSiblingIdx?:
17+
| ((value: number) => void | React.Dispatch<React.SetStateAction<number>>)
18+
| null
19+
| undefined;
1120
};
1221

1322
const ContentParts = memo(
14-
({ content, messageId, isCreatedByUser, isLast, isSubmitting }: ContentPartsProps) => {
23+
({
24+
content,
25+
messageId,
26+
isCreatedByUser,
27+
isLast,
28+
isSubmitting,
29+
edit,
30+
enterEdit,
31+
siblingIdx,
32+
setSiblingIdx,
33+
}: ContentPartsProps) => {
1534
if (!content) {
1635
return null;
1736
}
37+
if (edit === true && enterEdit && setSiblingIdx) {
38+
return (
39+
<>
40+
{content.map((part, idx) => {
41+
if (part?.type !== ContentTypes.TEXT || typeof part.text !== 'string') {
42+
return null;
43+
}
44+
45+
return (
46+
<EditTextPart
47+
index={idx}
48+
text={part.text}
49+
messageId={messageId}
50+
isSubmitting={isSubmitting}
51+
enterEdit={enterEdit}
52+
siblingIdx={siblingIdx ?? null}
53+
setSiblingIdx={setSiblingIdx}
54+
key={`edit-${messageId}-${idx}`}
55+
/>
56+
);
57+
})}
58+
</>
59+
);
60+
}
1861
return (
1962
<>
2063
{content

client/src/components/Chat/Messages/Content/EditMessage.tsx

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useRecoilState } from 'recoil';
2-
import TextareaAutosize from 'react-textarea-autosize';
1+
import { useRecoilState, useRecoilValue } from 'recoil';
32
import { EModelEndpoint } from 'librechat-data-provider';
4-
import { useState, useRef, useEffect, useCallback } from 'react';
3+
import { useRef, useEffect, useCallback } from 'react';
4+
import { useForm } from 'react-hook-form';
55
import { useUpdateMessageMutation } from 'librechat-data-provider/react-query';
66
import type { TEditProps } from '~/common';
77
import { useChatContext, useAddedChatContext } from '~/Providers';
8+
import { TextareaAutosize } from '~/components/ui';
89
import { cn, removeFocusRings } from '~/utils';
910
import { useLocalize } from '~/hooks';
1011
import Container from './Container';
@@ -25,7 +26,6 @@ const EditMessage = ({
2526
store.latestMessageFamily(addedIndex),
2627
);
2728

28-
const [editedText, setEditedText] = useState<string>(text ?? '');
2929
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
3030

3131
const { conversationId, parentMessageId, messageId } = message;
@@ -34,6 +34,15 @@ const EditMessage = ({
3434
const updateMessageMutation = useUpdateMessageMutation(conversationId ?? '');
3535
const localize = useLocalize();
3636

37+
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
38+
const isRTL = chatDirection === 'rtl';
39+
40+
const { register, handleSubmit, setValue } = useForm({
41+
defaultValues: {
42+
text: text ?? '',
43+
},
44+
});
45+
3746
useEffect(() => {
3847
const textArea = textAreaRef.current;
3948
if (textArea) {
@@ -43,11 +52,11 @@ const EditMessage = ({
4352
}
4453
}, []);
4554

46-
const resubmitMessage = () => {
55+
const resubmitMessage = (data: { text: string }) => {
4756
if (message.isCreatedByUser) {
4857
ask(
4958
{
50-
text: editedText,
59+
text: data.text,
5160
parentMessageId,
5261
conversationId,
5362
},
@@ -67,7 +76,7 @@ const EditMessage = ({
6776
ask(
6877
{ ...parentMessage },
6978
{
70-
editedText,
79+
editedText: data.text,
7180
editedMessageId: messageId,
7281
isRegenerate: true,
7382
isEdited: true,
@@ -80,32 +89,32 @@ const EditMessage = ({
8089
enterEdit(true);
8190
};
8291

83-
const updateMessage = () => {
92+
const updateMessage = (data: { text: string }) => {
8493
const messages = getMessages();
8594
if (!messages) {
8695
return;
8796
}
8897
updateMessageMutation.mutate({
8998
conversationId: conversationId ?? '',
9099
model: conversation?.model ?? 'gpt-3.5-turbo',
91-
text: editedText,
100+
text: data.text,
92101
messageId,
93102
});
94103

95104
if (message.messageId === latestMultiMessage?.messageId) {
96-
setLatestMultiMessage({ ...latestMultiMessage, text: editedText });
105+
setLatestMultiMessage({ ...latestMultiMessage, text: data.text });
97106
}
98107

99-
const isInMessages = messages?.some((message) => message?.messageId === messageId);
108+
const isInMessages = messages.some((message) => message.messageId === messageId);
100109
if (!isInMessages) {
101-
message.text = editedText;
110+
message.text = data.text;
102111
} else {
103112
setMessages(
104113
messages.map((msg) =>
105114
msg.messageId === messageId
106115
? {
107116
...msg,
108-
text: editedText,
117+
text: data.text,
109118
isEdited: true,
110119
}
111120
: msg,
@@ -126,43 +135,33 @@ const EditMessage = ({
126135
[enterEdit],
127136
);
128137

138+
const { ref, ...registerProps } = register('text', {
139+
required: true,
140+
onChange: (e) => {
141+
setValue('text', e.target.value, { shouldValidate: true });
142+
},
143+
});
144+
129145
return (
130146
<Container message={message}>
131-
<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">
147+
<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)]">
132148
<TextareaAutosize
133-
ref={textAreaRef}
134-
onChange={(e) => {
135-
setEditedText(e.target.value);
149+
{...registerProps}
150+
ref={(e) => {
151+
ref(e);
152+
textAreaRef.current = e;
136153
}}
137154
onKeyDown={handleKeyDown}
138155
data-testid="message-text-editor"
139156
className={cn(
140-
'markdown prose dark:prose-invert light whitespace-pre-wrap break-words',
141-
'pl-3 md:pl-4',
157+
'markdown prose dark:prose-invert light whitespace-pre-wrap break-words pl-3 md:pl-4',
142158
'm-0 w-full resize-none border-0 bg-transparent py-[10px]',
143-
'placeholder-black/50 focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder-white/50 md:py-3.5 ',
144-
'pr-3 md:pr-4',
145-
'max-h-[65vh] md:max-h-[75vh]',
159+
'placeholder-text-secondary focus:ring-0 focus-visible:ring-0 md:py-3.5',
160+
isRTL ? 'text-right' : 'text-left',
161+
'max-h-[65vh] pr-3 md:max-h-[75vh] md:pr-4',
146162
removeFocusRings,
147163
)}
148-
onPaste={(e) => {
149-
e.preventDefault();
150-
151-
const pastedData = e.clipboardData.getData('text/plain');
152-
const textArea = textAreaRef.current;
153-
if (!textArea) {
154-
return;
155-
}
156-
const start = textArea.selectionStart;
157-
const end = textArea.selectionEnd;
158-
const newValue =
159-
textArea.value.substring(0, start) + pastedData + textArea.value.substring(end);
160-
setEditedText(newValue);
161-
}}
162-
contentEditable={true}
163-
value={editedText}
164-
suppressContentEditableWarning={true}
165-
dir="auto"
164+
dir={isRTL ? 'rtl' : 'ltr'}
166165
/>
167166
</div>
168167
<div className="mt-2 flex w-full justify-center text-center">
@@ -171,14 +170,14 @@ const EditMessage = ({
171170
disabled={
172171
isSubmitting || (endpoint === EModelEndpoint.google && !message.isCreatedByUser)
173172
}
174-
onClick={resubmitMessage}
173+
onClick={handleSubmit(resubmitMessage)}
175174
>
176175
{localize('com_ui_save_submit')}
177176
</button>
178177
<button
179178
className="btn btn-secondary relative mr-2"
180179
disabled={isSubmitting}
181-
onClick={updateMessage}
180+
onClick={handleSubmit(updateMessage)}
182181
>
183182
{localize('com_ui_save')}
184183
</button>

0 commit comments

Comments
 (0)