diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 83e750a..10ae529 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -67,41 +67,23 @@ function isMinimalDiagram(xml: string): boolean { // Helper function to fix tool call inputs for Bedrock API // Bedrock requires toolUse.input to be a JSON object, not a string function fixToolCallInputs(messages: any[]): any[] { - return messages.map((msg, msgIndex) => { + return messages.map((msg) => { if (msg.role !== "assistant" || !Array.isArray(msg.content)) { return msg } - const fixedContent = msg.content.map((part: any, partIndex: number) => { + const fixedContent = msg.content.map((part: any) => { if (part.type === "tool-call") { - console.log( - `[fixToolCallInputs] msg[${msgIndex}].content[${partIndex}] tool-call:`, - { - toolName: part.toolName, - inputType: typeof part.input, - input: part.input, - }, - ) if (typeof part.input === "string") { try { const parsed = JSON.parse(part.input) - console.log( - `[fixToolCallInputs] Parsed string input to JSON:`, - parsed, - ) return { ...part, input: parsed } } catch { // If parsing fails, wrap the string in an object - console.log( - `[fixToolCallInputs] Failed to parse, wrapping in object`, - ) return { ...part, input: { rawInput: part.input } } } } // Input is already an object, but verify it's not null/undefined if (part.input === null || part.input === undefined) { - console.log( - `[fixToolCallInputs] Input is null/undefined, using empty object`, - ) return { ...part, input: {} } } } diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index 27bf204..3c618ef 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -509,139 +509,209 @@ export function ChatMessageDisplay({ ) : ( - /* Text content in bubble */ - message.parts?.some( - (part) => - part.type === "text" || - part.type === "file", - ) && ( -
{ + /* Render parts in order, grouping consecutive text/file parts into bubbles */ + (() => { + const parts = message.parts || [] + const groups: { + type: "content" | "tool" + parts: typeof parts + startIndex: number + }[] = [] + + parts.forEach((part, index) => { + const isToolPart = + part.type?.startsWith( + "tool-", + ) + const isContentPart = + part.type === "text" || + part.type === "file" + + if (isToolPart) { + groups.push({ + type: "tool", + parts: [part], + startIndex: index, + }) + } else if (isContentPart) { + const lastGroup = + groups[ + groups.length - 1 + ] if ( - message.role === - "user" && - isLastUserMessage && - onEditMessage + lastGroup?.type === + "content" ) { - setEditingMessageId( - message.id, + lastGroup.parts.push( + part, ) - setEditText( - userMessageText, + } else { + groups.push({ + type: "content", + parts: [part], + startIndex: index, + }) + } + } + }) + + return groups.map( + (group, groupIndex) => { + if (group.type === "tool") { + return renderToolPart( + group + .parts[0] as ToolPartLike, ) } - }} - onKeyDown={(e) => { - if ( - (e.key === "Enter" || - e.key === " ") && - message.role === - "user" && - isLastUserMessage && - onEditMessage - ) { - e.preventDefault() - setEditingMessageId( - message.id, - ) - setEditText( - userMessageText, - ) - } - }} - title={ - message.role === "user" && - isLastUserMessage && - onEditMessage - ? "Click to edit" - : undefined - } - > - {message.parts?.map( - (part, index) => { - switch (part.type) { - case "text": - return ( -
*:first-child]:mt-0 [&>*:last-child]:mb-0 ${ - message.role === - "user" - ? "[&_*]:!text-primary-foreground prose-code:bg-white/20" - : "dark:prose-invert" - }`} - > - - { - part.text - } - -
- ) - case "file": - return ( -
- {`Uploaded -
- ) - default: - return null - } - }, - )} -
- ) - )} - {/* Tool calls outside bubble */} - {message.parts?.map((part) => { - if (part.type?.startsWith("tool-")) { - return renderToolPart( - part as ToolPartLike, + + // Content bubble + return ( +
0 ? "mt-3" : ""}`} + role={ + message.role === + "user" && + isLastUserMessage && + onEditMessage + ? "button" + : undefined + } + tabIndex={ + message.role === + "user" && + isLastUserMessage && + onEditMessage + ? 0 + : undefined + } + onClick={() => { + if ( + message.role === + "user" && + isLastUserMessage && + onEditMessage + ) { + setEditingMessageId( + message.id, + ) + setEditText( + userMessageText, + ) + } + }} + onKeyDown={(e) => { + if ( + (e.key === + "Enter" || + e.key === + " ") && + message.role === + "user" && + isLastUserMessage && + onEditMessage + ) { + e.preventDefault() + setEditingMessageId( + message.id, + ) + setEditText( + userMessageText, + ) + } + }} + title={ + message.role === + "user" && + isLastUserMessage && + onEditMessage + ? "Click to edit" + : undefined + } + > + {group.parts.map( + ( + part, + partIndex, + ) => { + if ( + part.type === + "text" + ) { + return ( +
*:first-child]:mt-0 [&>*:last-child]:mb-0 ${ + message.role === + "user" + ? "[&_*]:!text-primary-foreground prose-code:bg-white/20" + : "dark:prose-invert" + }`} + > + + { + ( + part as { + text: string + } + ) + .text + } + +
+ ) + } + if ( + part.type === + "file" + ) { + return ( +
+ {`Uploaded +
+ ) + } + return null + }, + )} +
+ ) + }, ) - } - return null - })} + })() + )} {/* Action buttons for assistant messages */} {message.role === "assistant" && (