mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
🔀 feat: Save & Submit Message Content Parts (#8171)
* 🐛 fix: Enhance provider validation and error handling in getProviderConfig function
* WIP: edit text part
* refactor: Allow updating of both TEXT and THINK content types in message updates
* WIP: first pass, save & submit
* chore: remove legacy generation user message field
* feat: merge edited content
* fix: update placeholder and description for bedrock setting
* fix: remove unsupported warning message for AI resubmission
This commit is contained in:
parent
a648ad3d13
commit
434289fe92
14 changed files with 240 additions and 84 deletions
|
|
@ -13,7 +13,6 @@ const {
|
||||||
const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models');
|
const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models');
|
||||||
const { checkBalance } = require('~/models/balanceMethods');
|
const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { truncateToolCallOutputs } = require('./prompts');
|
const { truncateToolCallOutputs } = require('./prompts');
|
||||||
const { addSpaceIfNeeded } = require('~/server/utils');
|
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
const TextStream = require('./TextStream');
|
const TextStream = require('./TextStream');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
@ -572,7 +571,7 @@ class BaseClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { generation = '' } = opts;
|
const { editedContent } = opts;
|
||||||
|
|
||||||
// It's not necessary to push to currentMessages
|
// It's not necessary to push to currentMessages
|
||||||
// depending on subclass implementation of handling messages
|
// depending on subclass implementation of handling messages
|
||||||
|
|
@ -587,11 +586,21 @@ class BaseClient {
|
||||||
isCreatedByUser: false,
|
isCreatedByUser: false,
|
||||||
model: this.modelOptions?.model ?? this.model,
|
model: this.modelOptions?.model ?? this.model,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
text: generation,
|
|
||||||
};
|
};
|
||||||
this.currentMessages.push(userMessage, latestMessage);
|
this.currentMessages.push(userMessage, latestMessage);
|
||||||
} else {
|
} else if (editedContent != null) {
|
||||||
latestMessage.text = generation;
|
// Handle editedContent for content parts
|
||||||
|
if (editedContent && latestMessage.content && Array.isArray(latestMessage.content)) {
|
||||||
|
const { index, text, type } = editedContent;
|
||||||
|
if (index >= 0 && index < latestMessage.content.length) {
|
||||||
|
const contentPart = latestMessage.content[index];
|
||||||
|
if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) {
|
||||||
|
contentPart[ContentTypes.THINK] = text;
|
||||||
|
} else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) {
|
||||||
|
contentPart[ContentTypes.TEXT] = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.continued = true;
|
this.continued = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -672,16 +681,32 @@ class BaseClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof completion === 'string') {
|
if (typeof completion === 'string') {
|
||||||
responseMessage.text = addSpaceIfNeeded(generation) + completion;
|
responseMessage.text = completion;
|
||||||
} else if (
|
} else if (
|
||||||
Array.isArray(completion) &&
|
Array.isArray(completion) &&
|
||||||
(this.clientName === EModelEndpoint.agents ||
|
(this.clientName === EModelEndpoint.agents ||
|
||||||
isParamEndpoint(this.options.endpoint, this.options.endpointType))
|
isParamEndpoint(this.options.endpoint, this.options.endpointType))
|
||||||
) {
|
) {
|
||||||
responseMessage.text = '';
|
responseMessage.text = '';
|
||||||
|
|
||||||
|
if (!opts.editedContent || this.currentMessages.length === 0) {
|
||||||
responseMessage.content = completion;
|
responseMessage.content = completion;
|
||||||
|
} else {
|
||||||
|
const latestMessage = this.currentMessages[this.currentMessages.length - 1];
|
||||||
|
if (!latestMessage?.content) {
|
||||||
|
responseMessage.content = completion;
|
||||||
|
} else {
|
||||||
|
const existingContent = [...latestMessage.content];
|
||||||
|
const { type: editedType } = opts.editedContent;
|
||||||
|
responseMessage.content = this.mergeEditedContent(
|
||||||
|
existingContent,
|
||||||
|
completion,
|
||||||
|
editedType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (Array.isArray(completion)) {
|
} else if (Array.isArray(completion)) {
|
||||||
responseMessage.text = addSpaceIfNeeded(generation) + completion.join('');
|
responseMessage.text = completion.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -1095,6 +1120,50 @@ class BaseClient {
|
||||||
return numTokens;
|
return numTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges completion content with existing content when editing TEXT or THINK types
|
||||||
|
* @param {Array} existingContent - The existing content array
|
||||||
|
* @param {Array} newCompletion - The new completion content
|
||||||
|
* @param {string} editedType - The type of content being edited
|
||||||
|
* @returns {Array} The merged content array
|
||||||
|
*/
|
||||||
|
mergeEditedContent(existingContent, newCompletion, editedType) {
|
||||||
|
if (!newCompletion.length) {
|
||||||
|
return existingContent.concat(newCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editedType !== ContentTypes.TEXT && editedType !== ContentTypes.THINK) {
|
||||||
|
return existingContent.concat(newCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = existingContent.length - 1;
|
||||||
|
const lastExisting = existingContent[lastIndex];
|
||||||
|
const firstNew = newCompletion[0];
|
||||||
|
|
||||||
|
if (lastExisting?.type !== firstNew?.type || firstNew?.type !== editedType) {
|
||||||
|
return existingContent.concat(newCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedContent = [...existingContent];
|
||||||
|
if (editedType === ContentTypes.TEXT) {
|
||||||
|
mergedContent[lastIndex] = {
|
||||||
|
...mergedContent[lastIndex],
|
||||||
|
[ContentTypes.TEXT]:
|
||||||
|
(mergedContent[lastIndex][ContentTypes.TEXT] || '') + (firstNew[ContentTypes.TEXT] || ''),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
mergedContent[lastIndex] = {
|
||||||
|
...mergedContent[lastIndex],
|
||||||
|
[ContentTypes.THINK]:
|
||||||
|
(mergedContent[lastIndex][ContentTypes.THINK] || '') +
|
||||||
|
(firstNew[ContentTypes.THINK] || ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining completion items
|
||||||
|
return mergedContent.concat(newCompletion.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
async sendPayload(payload, opts = {}) {
|
async sendPayload(payload, opts = {}) {
|
||||||
if (opts && typeof opts === 'object') {
|
if (opts && typeof opts === 'object') {
|
||||||
this.setOptions(opts);
|
this.setOptions(opts);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
text,
|
text,
|
||||||
endpointOption,
|
endpointOption,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
isContinued = false,
|
||||||
|
editedContent = null,
|
||||||
parentMessageId = null,
|
parentMessageId = null,
|
||||||
overrideParentMessageId = null,
|
overrideParentMessageId = null,
|
||||||
|
responseMessageId: editedResponseMessageId = null,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
let sender;
|
let sender;
|
||||||
|
|
@ -67,7 +70,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
handler();
|
handler();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore cleanup errors
|
logger.error('[AgentController] Error in cleanup handler', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +158,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
try {
|
try {
|
||||||
res.removeListener('close', closeHandler);
|
res.removeListener('close', closeHandler);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
logger.error('[AgentController] Error removing close listener', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -163,10 +166,14 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
user: userId,
|
user: userId,
|
||||||
onStart,
|
onStart,
|
||||||
getReqData,
|
getReqData,
|
||||||
|
isContinued,
|
||||||
|
editedContent,
|
||||||
conversationId,
|
conversationId,
|
||||||
parentMessageId,
|
parentMessageId,
|
||||||
abortController,
|
abortController,
|
||||||
overrideParentMessageId,
|
overrideParentMessageId,
|
||||||
|
isEdited: !!editedContent,
|
||||||
|
responseMessageId: editedResponseMessageId,
|
||||||
progressOptions: {
|
progressOptions: {
|
||||||
res,
|
res,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -235,12 +235,13 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) =
|
||||||
return res.status(400).json({ error: 'Content part not found' });
|
return res.status(400).json({ error: 'Content part not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedContent[index].type !== ContentTypes.TEXT) {
|
const currentPartType = updatedContent[index].type;
|
||||||
|
if (currentPartType !== ContentTypes.TEXT && currentPartType !== ContentTypes.THINK) {
|
||||||
return res.status(400).json({ error: 'Cannot update non-text content' });
|
return res.status(400).json({ error: 'Cannot update non-text content' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldText = updatedContent[index].text;
|
const oldText = updatedContent[index][currentPartType];
|
||||||
updatedContent[index] = { type: ContentTypes.TEXT, text };
|
updatedContent[index] = { type: currentPartType, [currentPartType]: text };
|
||||||
|
|
||||||
let tokenCount = message.tokenCount;
|
let tokenCount = message.tokenCount;
|
||||||
if (tokenCount !== undefined) {
|
if (tokenCount !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,16 @@ const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
||||||
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
|
/** Check if the provider is a known custom provider
|
||||||
|
* @param {string | undefined} [provider] - The provider string
|
||||||
|
* @returns {boolean} - True if the provider is a known custom provider, false otherwise
|
||||||
|
*/
|
||||||
|
function isKnownCustomProvider(provider) {
|
||||||
|
return [Providers.XAI, Providers.OLLAMA, Providers.DEEPSEEK, Providers.OPENROUTER].includes(
|
||||||
|
provider || '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const providerConfigMap = {
|
const providerConfigMap = {
|
||||||
[Providers.XAI]: initCustom,
|
[Providers.XAI]: initCustom,
|
||||||
[Providers.OLLAMA]: initCustom,
|
[Providers.OLLAMA]: initCustom,
|
||||||
|
|
@ -46,6 +56,13 @@ async function getProviderConfig(provider) {
|
||||||
overrideProvider = Providers.OPENAI;
|
overrideProvider = Providers.OPENAI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isKnownCustomProvider(overrideProvider)) {
|
||||||
|
customEndpointConfig = await getCustomEndpointConfig(provider);
|
||||||
|
if (!customEndpointConfig) {
|
||||||
|
throw new Error(`Provider ${provider} not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getOptions,
|
getOptions,
|
||||||
overrideProvider,
|
overrideProvider,
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,11 @@ export type TAskProps = {
|
||||||
export type TOptions = {
|
export type TOptions = {
|
||||||
editedMessageId?: string | null;
|
editedMessageId?: string | null;
|
||||||
editedText?: string | null;
|
editedText?: string | null;
|
||||||
|
editedContent?: {
|
||||||
|
index: number;
|
||||||
|
text: string;
|
||||||
|
type: 'text' | 'think';
|
||||||
|
};
|
||||||
isRegenerate?: boolean;
|
isRegenerate?: boolean;
|
||||||
isContinued?: boolean;
|
isContinued?: boolean;
|
||||||
isEdited?: boolean;
|
isEdited?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -81,14 +81,23 @@ const ContentParts = memo(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{content.map((part, idx) => {
|
{content.map((part, idx) => {
|
||||||
if (part?.type !== ContentTypes.TEXT || typeof part.text !== 'string') {
|
if (!part) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isTextPart =
|
||||||
|
part?.type === ContentTypes.TEXT ||
|
||||||
|
typeof (part as unknown as Agents.MessageContentText)?.text !== 'string';
|
||||||
|
const isThinkPart =
|
||||||
|
part?.type === ContentTypes.THINK ||
|
||||||
|
typeof (part as unknown as Agents.ReasoningDeltaUpdate)?.think !== 'string';
|
||||||
|
if (!isTextPart && !isThinkPart) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditTextPart
|
<EditTextPart
|
||||||
index={idx}
|
index={idx}
|
||||||
text={part.text}
|
part={part as Agents.MessageContentText | Agents.ReasoningDeltaUpdate}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
enterEdit={enterEdit}
|
enterEdit={enterEdit}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { ContentTypes } from 'librechat-data-provider';
|
import { ContentTypes } from 'librechat-data-provider';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useRef, useEffect, useCallback, useMemo } from 'react';
|
|
||||||
import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query';
|
import { useUpdateMessageContentMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import type { Agents } from 'librechat-data-provider';
|
||||||
import type { TEditProps } from '~/common';
|
import type { TEditProps } from '~/common';
|
||||||
import Container from '~/components/Chat/Messages/Content/Container';
|
import Container from '~/components/Chat/Messages/Content/Container';
|
||||||
import { useChatContext, useAddedChatContext } from '~/Providers';
|
import { useChatContext, useAddedChatContext } from '~/Providers';
|
||||||
|
|
@ -12,18 +13,19 @@ import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const EditTextPart = ({
|
const EditTextPart = ({
|
||||||
text,
|
part,
|
||||||
index,
|
index,
|
||||||
messageId,
|
messageId,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
enterEdit,
|
enterEdit,
|
||||||
}: Omit<TEditProps, 'message' | 'ask'> & {
|
}: Omit<TEditProps, 'message' | 'ask' | 'text'> & {
|
||||||
index: number;
|
index: number;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
part: Agents.MessageContentText | Agents.ReasoningDeltaUpdate;
|
||||||
}) => {
|
}) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { addedIndex } = useAddedChatContext();
|
const { addedIndex } = useAddedChatContext();
|
||||||
const { getMessages, setMessages, conversation } = useChatContext();
|
const { ask, getMessages, setMessages, conversation } = useChatContext();
|
||||||
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
const [latestMultiMessage, setLatestMultiMessage] = useRecoilState(
|
||||||
store.latestMessageFamily(addedIndex),
|
store.latestMessageFamily(addedIndex),
|
||||||
);
|
);
|
||||||
|
|
@ -34,15 +36,16 @@ const EditTextPart = ({
|
||||||
[getMessages, messageId],
|
[getMessages, messageId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chatDirection = useRecoilValue(store.chatDirection);
|
||||||
|
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const updateMessageContentMutation = useUpdateMessageContentMutation(conversationId ?? '');
|
const updateMessageContentMutation = useUpdateMessageContentMutation(conversationId ?? '');
|
||||||
|
|
||||||
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
|
const isRTL = chatDirection?.toLowerCase() === 'rtl';
|
||||||
const isRTL = chatDirection === 'rtl';
|
|
||||||
|
|
||||||
const { register, handleSubmit, setValue } = useForm({
|
const { register, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
text: text ?? '',
|
text: (ContentTypes.THINK in part ? part.think : part.text) || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,15 +58,7 @@ const EditTextPart = ({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/*
|
const resubmitMessage = (data: { text: string }) => {
|
||||||
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 messages = getMessages();
|
||||||
const parentMessage = messages?.find((msg) => msg.messageId === message?.parentMessageId);
|
const parentMessage = messages?.find((msg) => msg.messageId === message?.parentMessageId);
|
||||||
|
|
||||||
|
|
@ -73,17 +68,19 @@ const EditTextPart = ({
|
||||||
ask(
|
ask(
|
||||||
{ ...parentMessage },
|
{ ...parentMessage },
|
||||||
{
|
{
|
||||||
editedText: data.text,
|
editedContent: {
|
||||||
|
index,
|
||||||
|
text: data.text,
|
||||||
|
type: part.type,
|
||||||
|
},
|
||||||
editedMessageId: messageId,
|
editedMessageId: messageId,
|
||||||
isRegenerate: true,
|
isRegenerate: true,
|
||||||
isEdited: true,
|
isEdited: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
setSiblingIdx((siblingIdx ?? 0) - 1);
|
|
||||||
enterEdit(true);
|
enterEdit(true);
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
const updateMessage = (data: { text: string }) => {
|
const updateMessage = (data: { text: string }) => {
|
||||||
const messages = getMessages();
|
const messages = getMessages();
|
||||||
|
|
@ -167,13 +164,13 @@ const EditTextPart = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex w-full justify-center text-center">
|
<div className="mt-2 flex w-full justify-center text-center">
|
||||||
{/* <button
|
<button
|
||||||
className="btn btn-primary relative mr-2"
|
className="btn btn-primary relative mr-2"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={handleSubmit(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}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
ContentTypes,
|
ContentTypes,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
isAgentsEndpoint,
|
||||||
parseCompactConvo,
|
parseCompactConvo,
|
||||||
replaceSpecialVars,
|
replaceSpecialVars,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
|
|
@ -36,15 +37,6 @@ const logChatRequest = (request: Record<string, unknown>) => {
|
||||||
logger.log('=====================================');
|
logger.log('=====================================');
|
||||||
};
|
};
|
||||||
|
|
||||||
const usesContentStream = (endpoint: EModelEndpoint | undefined, endpointType?: string) => {
|
|
||||||
if (endpointType === EModelEndpoint.custom) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useChatFunctions({
|
export default function useChatFunctions({
|
||||||
index = 0,
|
index = 0,
|
||||||
files,
|
files,
|
||||||
|
|
@ -93,7 +85,7 @@ export default function useChatFunctions({
|
||||||
messageId = null,
|
messageId = null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
editedText = null,
|
editedContent = null,
|
||||||
editedMessageId = null,
|
editedMessageId = null,
|
||||||
isResubmission = false,
|
isResubmission = false,
|
||||||
isRegenerate = false,
|
isRegenerate = false,
|
||||||
|
|
@ -245,14 +237,11 @@ export default function useChatFunctions({
|
||||||
setFilesToDelete({});
|
setFilesToDelete({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const generation = editedText ?? latestMessage?.text ?? '';
|
|
||||||
const responseText = isEditOrContinue ? generation : '';
|
|
||||||
|
|
||||||
const responseMessageId =
|
const responseMessageId =
|
||||||
editedMessageId ?? (latestMessage?.messageId ? latestMessage?.messageId + '_' : null) ?? null;
|
editedMessageId ?? (latestMessage?.messageId ? latestMessage?.messageId + '_' : null) ?? null;
|
||||||
const initialResponse: TMessage = {
|
const initialResponse: TMessage = {
|
||||||
sender: responseSender,
|
sender: responseSender,
|
||||||
text: responseText,
|
text: '',
|
||||||
endpoint: endpoint ?? '',
|
endpoint: endpoint ?? '',
|
||||||
parentMessageId: isRegenerate ? messageId : intermediateId,
|
parentMessageId: isRegenerate ? messageId : intermediateId,
|
||||||
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
||||||
|
|
@ -272,34 +261,37 @@ export default function useChatFunctions({
|
||||||
{
|
{
|
||||||
type: ContentTypes.TEXT,
|
type: ContentTypes.TEXT,
|
||||||
[ContentTypes.TEXT]: {
|
[ContentTypes.TEXT]: {
|
||||||
value: responseText,
|
value: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else if (endpoint === EModelEndpoint.agents) {
|
} else if (endpoint != null) {
|
||||||
initialResponse.model = conversation?.agent_id ?? '';
|
initialResponse.model = isAgentsEndpoint(endpoint)
|
||||||
|
? (conversation?.agent_id ?? '')
|
||||||
|
: (conversation?.model ?? '');
|
||||||
initialResponse.text = '';
|
initialResponse.text = '';
|
||||||
initialResponse.content = [
|
|
||||||
{
|
if (editedContent && latestMessage?.content) {
|
||||||
type: ContentTypes.TEXT,
|
initialResponse.content = cloneDeep(latestMessage.content);
|
||||||
[ContentTypes.TEXT]: {
|
const { index, text, type } = editedContent;
|
||||||
value: responseText,
|
if (initialResponse.content && index >= 0 && index < initialResponse.content.length) {
|
||||||
},
|
const contentPart = initialResponse.content[index];
|
||||||
},
|
if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) {
|
||||||
];
|
contentPart[ContentTypes.THINK] = text;
|
||||||
setShowStopButton(true);
|
} else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) {
|
||||||
} else if (usesContentStream(endpoint, endpointType)) {
|
contentPart[ContentTypes.TEXT] = text;
|
||||||
initialResponse.text = '';
|
}
|
||||||
initialResponse.content = [
|
}
|
||||||
{
|
|
||||||
type: ContentTypes.TEXT,
|
|
||||||
[ContentTypes.TEXT]: {
|
|
||||||
value: responseText,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
setShowStopButton(true);
|
|
||||||
} else {
|
} else {
|
||||||
|
initialResponse.content = [
|
||||||
|
{
|
||||||
|
type: ContentTypes.TEXT,
|
||||||
|
[ContentTypes.TEXT]: {
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
setShowStopButton(true);
|
setShowStopButton(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +308,6 @@ export default function useChatFunctions({
|
||||||
endpointOption,
|
endpointOption,
|
||||||
userMessage: {
|
userMessage: {
|
||||||
...currentMsg,
|
...currentMsg,
|
||||||
generation,
|
|
||||||
responseMessageId,
|
responseMessageId,
|
||||||
overrideParentMessageId: isRegenerate ? messageId : null,
|
overrideParentMessageId: isRegenerate ? messageId : null,
|
||||||
},
|
},
|
||||||
|
|
@ -328,6 +319,7 @@ export default function useChatFunctions({
|
||||||
initialResponse,
|
initialResponse,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
ephemeralAgent,
|
ephemeralAgent,
|
||||||
|
editedContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRegenerate) {
|
if (isRegenerate) {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,26 @@ export default function useStepHandler({
|
||||||
const messageMap = useRef(new Map<string, TMessage>());
|
const messageMap = useRef(new Map<string, TMessage>());
|
||||||
const stepMap = useRef(new Map<string, Agents.RunStep>());
|
const stepMap = useRef(new Map<string, Agents.RunStep>());
|
||||||
|
|
||||||
|
const calculateContentIndex = (
|
||||||
|
baseIndex: number,
|
||||||
|
initialContent: TMessageContentParts[],
|
||||||
|
incomingContentType: string,
|
||||||
|
existingContent?: TMessageContentParts[],
|
||||||
|
): number => {
|
||||||
|
/** Only apply -1 adjustment for TEXT or THINK types when they match existing content */
|
||||||
|
if (
|
||||||
|
initialContent.length > 0 &&
|
||||||
|
(incomingContentType === ContentTypes.TEXT || incomingContentType === ContentTypes.THINK)
|
||||||
|
) {
|
||||||
|
const targetIndex = baseIndex + initialContent.length - 1;
|
||||||
|
const existingType = existingContent?.[targetIndex]?.type;
|
||||||
|
if (existingType === incomingContentType) {
|
||||||
|
return targetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseIndex + initialContent.length;
|
||||||
|
};
|
||||||
|
|
||||||
const updateContent = (
|
const updateContent = (
|
||||||
message: TMessage,
|
message: TMessage,
|
||||||
index: number,
|
index: number,
|
||||||
|
|
@ -170,6 +190,11 @@ export default function useStepHandler({
|
||||||
lastAnnouncementTimeRef.current = currentTime;
|
lastAnnouncementTimeRef.current = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initialContent: TMessageContentParts[] = [];
|
||||||
|
if (submission?.editedContent != null) {
|
||||||
|
initialContent = submission?.initialResponse?.content ?? initialContent;
|
||||||
|
}
|
||||||
|
|
||||||
if (event === 'on_run_step') {
|
if (event === 'on_run_step') {
|
||||||
const runStep = data as Agents.RunStep;
|
const runStep = data as Agents.RunStep;
|
||||||
const responseMessageId = runStep.runId ?? '';
|
const responseMessageId = runStep.runId ?? '';
|
||||||
|
|
@ -189,7 +214,7 @@ export default function useStepHandler({
|
||||||
parentMessageId: userMessage.messageId,
|
parentMessageId: userMessage.messageId,
|
||||||
conversationId: userMessage.conversationId,
|
conversationId: userMessage.conversationId,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
content: [],
|
content: initialContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, response);
|
messageMap.current.set(responseMessageId, response);
|
||||||
|
|
@ -214,7 +239,9 @@ export default function useStepHandler({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
|
/** Tool calls don't need index adjustment */
|
||||||
|
const currentIndex = runStep.index + initialContent.length;
|
||||||
|
updatedResponse = updateContent(updatedResponse, currentIndex, contentPart);
|
||||||
});
|
});
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
|
|
@ -234,7 +261,9 @@ export default function useStepHandler({
|
||||||
|
|
||||||
const response = messageMap.current.get(responseMessageId);
|
const response = messageMap.current.get(responseMessageId);
|
||||||
if (response) {
|
if (response) {
|
||||||
const updatedResponse = updateContent(response, agent_update.index, data);
|
// Agent updates don't need index adjustment
|
||||||
|
const currentIndex = agent_update.index + initialContent.length;
|
||||||
|
const updatedResponse = updateContent(response, currentIndex, data);
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
const currentMessages = getMessages() || [];
|
const currentMessages = getMessages() || [];
|
||||||
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
|
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
|
||||||
|
|
@ -255,7 +284,13 @@ export default function useStepHandler({
|
||||||
? messageDelta.delta.content[0]
|
? messageDelta.delta.content[0]
|
||||||
: messageDelta.delta.content;
|
: messageDelta.delta.content;
|
||||||
|
|
||||||
const updatedResponse = updateContent(response, runStep.index, contentPart);
|
const currentIndex = calculateContentIndex(
|
||||||
|
runStep.index,
|
||||||
|
initialContent,
|
||||||
|
contentPart.type || '',
|
||||||
|
response.content,
|
||||||
|
);
|
||||||
|
const updatedResponse = updateContent(response, currentIndex, contentPart);
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
const currentMessages = getMessages() || [];
|
const currentMessages = getMessages() || [];
|
||||||
|
|
@ -277,7 +312,13 @@ export default function useStepHandler({
|
||||||
? reasoningDelta.delta.content[0]
|
? reasoningDelta.delta.content[0]
|
||||||
: reasoningDelta.delta.content;
|
: reasoningDelta.delta.content;
|
||||||
|
|
||||||
const updatedResponse = updateContent(response, runStep.index, contentPart);
|
const currentIndex = calculateContentIndex(
|
||||||
|
runStep.index,
|
||||||
|
initialContent,
|
||||||
|
contentPart.type || '',
|
||||||
|
response.content,
|
||||||
|
);
|
||||||
|
const updatedResponse = updateContent(response, currentIndex, contentPart);
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
const currentMessages = getMessages() || [];
|
const currentMessages = getMessages() || [];
|
||||||
|
|
@ -318,7 +359,9 @@ export default function useStepHandler({
|
||||||
contentPart.tool_call.expires_at = runStepDelta.delta.expires_at;
|
contentPart.tool_call.expires_at = runStepDelta.delta.expires_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
|
/** Tool calls don't need index adjustment */
|
||||||
|
const currentIndex = runStep.index + initialContent.length;
|
||||||
|
updatedResponse = updateContent(updatedResponse, currentIndex, contentPart);
|
||||||
});
|
});
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
|
|
@ -350,7 +393,9 @@ export default function useStepHandler({
|
||||||
tool_call: result.tool_call,
|
tool_call: result.tool_call,
|
||||||
};
|
};
|
||||||
|
|
||||||
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart, true);
|
/** Tool calls don't need index adjustment */
|
||||||
|
const currentIndex = runStep.index + initialContent.length;
|
||||||
|
updatedResponse = updateContent(updatedResponse, currentIndex, contentPart, true);
|
||||||
|
|
||||||
messageMap.current.set(responseMessageId, updatedResponse);
|
messageMap.current.set(responseMessageId, updatedResponse);
|
||||||
const updatedMessages = messages.map((msg) =>
|
const updatedMessages = messages.map((msg) =>
|
||||||
|
|
|
||||||
|
|
@ -1067,6 +1067,5 @@
|
||||||
"com_ui_x_selected": "{{0}} selected",
|
"com_ui_x_selected": "{{0}} selected",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You",
|
"com_user_message": "You"
|
||||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export default function createPayload(submission: t.TSubmission) {
|
||||||
isContinued,
|
isContinued,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
ephemeralAgent,
|
ephemeralAgent,
|
||||||
|
editedContent,
|
||||||
} = submission;
|
} = submission;
|
||||||
const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
|
const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
|
||||||
const { endpoint: _e, endpointType } = endpointOption as {
|
const { endpoint: _e, endpointType } = endpointOption as {
|
||||||
|
|
@ -34,6 +35,7 @@ export default function createPayload(submission: t.TSubmission) {
|
||||||
isContinued: !!(isEdited && isContinued),
|
isContinued: !!(isEdited && isContinued),
|
||||||
conversationId,
|
conversationId,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
|
editedContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { server, payload };
|
return { server, payload };
|
||||||
|
|
|
||||||
|
|
@ -401,7 +401,9 @@ const bedrock: Record<string, SettingDefinition> = {
|
||||||
labelCode: true,
|
labelCode: true,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'input',
|
component: 'input',
|
||||||
placeholder: 'com_endpoint_anthropic_maxoutputtokens',
|
description: 'com_endpoint_anthropic_maxoutputtokens',
|
||||||
|
descriptionCode: true,
|
||||||
|
placeholder: 'com_nav_theme_system',
|
||||||
placeholderCode: true,
|
placeholderCode: true,
|
||||||
optionType: 'model',
|
optionType: 'model',
|
||||||
columnSpan: 2,
|
columnSpan: 2,
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,7 @@ export const tMessageSchema = z.object({
|
||||||
title: z.string().nullable().or(z.literal('New Chat')).default('New Chat'),
|
title: z.string().nullable().or(z.literal('New Chat')).default('New Chat'),
|
||||||
sender: z.string().optional(),
|
sender: z.string().optional(),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
|
/** @deprecated */
|
||||||
generation: z.string().nullable().optional(),
|
generation: z.string().nullable().optional(),
|
||||||
isCreatedByUser: z.boolean(),
|
isCreatedByUser: z.boolean(),
|
||||||
error: z.boolean().optional(),
|
error: z.boolean().optional(),
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,11 @@ export type TPayload = Partial<TMessage> &
|
||||||
messages?: TMessages;
|
messages?: TMessages;
|
||||||
isTemporary: boolean;
|
isTemporary: boolean;
|
||||||
ephemeralAgent?: TEphemeralAgent | null;
|
ephemeralAgent?: TEphemeralAgent | null;
|
||||||
|
editedContent?: {
|
||||||
|
index: number;
|
||||||
|
text: string;
|
||||||
|
type: 'text' | 'think';
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSubmission = {
|
export type TSubmission = {
|
||||||
|
|
@ -127,6 +132,11 @@ export type TSubmission = {
|
||||||
endpointOption: TEndpointOption;
|
endpointOption: TEndpointOption;
|
||||||
clientTimestamp?: string;
|
clientTimestamp?: string;
|
||||||
ephemeralAgent?: TEphemeralAgent | null;
|
ephemeralAgent?: TEphemeralAgent | null;
|
||||||
|
editedContent?: {
|
||||||
|
index: number;
|
||||||
|
text: string;
|
||||||
|
type: 'text' | 'think';
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialResponse: TMessage };
|
export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialResponse: TMessage };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue