Send edited user images and full message content in chat regenerate while leaving reload on server-resolved turns.

This commit is contained in:
CREDO23 2026-04-27 19:25:26 +02:00
parent 056870464a
commit a07c44f496

View file

@ -77,7 +77,10 @@ import {
type ThreadListResponse, type ThreadListResponse,
type ThreadRecord, type ThreadRecord,
} from "@/lib/chat/thread-persistence"; } from "@/lib/chat/thread-persistence";
import { extractUserTurnForNewChatApi } from "@/lib/chat/user-turn-api-parts"; import {
extractUserTurnForNewChatApi,
type NewChatUserImagePayload,
} from "@/lib/chat/user-turn-api-parts";
import { NotFoundError } from "@/lib/error"; import { NotFoundError } from "@/lib/error";
import { import {
trackChatCreated, trackChatCreated,
@ -1337,15 +1340,24 @@ export default function NewChatPage() {
* Handle regeneration (edit or reload) by calling the regenerate endpoint * Handle regeneration (edit or reload) by calling the regenerate endpoint
* and streaming the response. This rewinds the LangGraph checkpointer state. * and streaming the response. This rewinds the LangGraph checkpointer state.
* *
* @param newUserQuery - The new user query (for edit). Pass null/undefined for reload. * @param newUserQuery - `null` = reload with same turn from the server. A string = edit
* (including an empty string when the edited turn is images-only); pass `editExtras` for images/content.
*/ */
const handleRegenerate = useCallback( const handleRegenerate = useCallback(
async (newUserQuery?: string | null) => { async (
newUserQuery: string | null,
editExtras?: {
userMessageContent: ThreadMessageLike["content"];
userImages: NewChatUserImagePayload[];
}
) => {
if (!threadId) { if (!threadId) {
toast.error("Cannot regenerate: no active chat thread"); toast.error("Cannot regenerate: no active chat thread");
return; return;
} }
const isEdit = newUserQuery !== null;
// Abort any previous streaming request // Abort any previous streaming request
if (abortControllerRef.current) { if (abortControllerRef.current) {
abortControllerRef.current.abort(); abortControllerRef.current.abort();
@ -1359,11 +1371,11 @@ export default function NewChatPage() {
} }
// Extract the original user query BEFORE removing messages (for reload mode) // Extract the original user query BEFORE removing messages (for reload mode)
let userQueryToDisplay = newUserQuery; let userQueryToDisplay: string | undefined;
let originalUserMessageContent: ThreadMessageLike["content"] | null = null; let originalUserMessageContent: ThreadMessageLike["content"] | null = null;
let originalUserMessageMetadata: ThreadMessageLike["metadata"] | undefined; let originalUserMessageMetadata: ThreadMessageLike["metadata"] | undefined;
if (!newUserQuery) { if (!isEdit) {
// Reload mode - find and preserve the last user message content // Reload mode - find and preserve the last user message content
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user"); const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
if (lastUserMessage) { if (lastUserMessage) {
@ -1377,6 +1389,8 @@ export default function NewChatPage() {
} }
} }
} }
} else {
userQueryToDisplay = newUserQuery;
} }
// Remove the last two messages (user + assistant) from the UI immediately // Remove the last two messages (user + assistant) from the UI immediately
@ -1412,11 +1426,13 @@ export default function NewChatPage() {
const userMessage: ThreadMessageLike = { const userMessage: ThreadMessageLike = {
id: userMsgId, id: userMsgId,
role: "user", role: "user",
content: newUserQuery content: isEdit
? [{ type: "text", text: newUserQuery }] ? (editExtras?.userMessageContent ?? [
{ type: "text", text: newUserQuery ?? "" },
])
: originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }], : originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }],
createdAt: new Date(), createdAt: new Date(),
metadata: newUserQuery ? undefined : originalUserMessageMetadata, metadata: isEdit ? undefined : originalUserMessageMetadata,
}; };
setMessages((prev) => [...prev, userMessage]); setMessages((prev) => [...prev, userMessage]);
@ -1433,20 +1449,24 @@ export default function NewChatPage() {
try { try {
const selection = await getAgentFilesystemSelection(); const selection = await getAgentFilesystemSelection();
const requestBody: Record<string, unknown> = {
search_space_id: searchSpaceId,
user_query: newUserQuery,
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform,
local_filesystem_mounts: selection.local_filesystem_mounts,
};
if (isEdit) {
requestBody.user_images = editExtras?.userImages ?? [];
}
const response = await fetch(getRegenerateUrl(threadId), { const response = await fetch(getRegenerateUrl(threadId), {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody),
search_space_id: searchSpaceId,
user_query: newUserQuery || null,
disabled_tools: disabledTools.length > 0 ? disabledTools : undefined,
filesystem_mode: selection.filesystem_mode,
client_platform: selection.client_platform,
local_filesystem_mounts: selection.local_filesystem_mounts,
}),
signal: controller.signal, signal: controller.signal,
}); });
@ -1536,8 +1556,10 @@ export default function NewChatPage() {
if (contentParts.length > 0) { if (contentParts.length > 0) {
try { try {
// Persist user message (for both edit and reload modes, since backend deleted it) // Persist user message (for both edit and reload modes, since backend deleted it)
const userContentToPersist = newUserQuery const userContentToPersist = isEdit
? [{ type: "text", text: newUserQuery }] ? (editExtras?.userMessageContent ?? [
{ type: "text", text: newUserQuery ?? "" },
])
: originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }]; : originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }];
const savedUserMessage = await appendMessage(threadId, { const savedUserMessage = await appendMessage(threadId, {
@ -1602,21 +1624,15 @@ export default function NewChatPage() {
// Handle editing a message - truncates history and regenerates with new query // Handle editing a message - truncates history and regenerates with new query
const onEdit = useCallback( const onEdit = useCallback(
async (message: AppendMessage) => { async (message: AppendMessage) => {
// Extract the new user query from the message content const { userQuery, userImages } = extractUserTurnForNewChatApi(message, []);
let newUserQuery = ""; const queryForApi = userQuery.trim();
for (const part of message.content) { if (!queryForApi && userImages.length === 0) {
if (part.type === "text") {
newUserQuery += part.text;
}
}
if (!newUserQuery.trim()) {
toast.error("Cannot edit with empty message"); toast.error("Cannot edit with empty message");
return; return;
} }
// Call regenerate with the new query const userMessageContent = message.content as unknown as ThreadMessageLike["content"];
await handleRegenerate(newUserQuery.trim()); await handleRegenerate(queryForApi, { userMessageContent, userImages });
}, },
[handleRegenerate] [handleRegenerate]
); );