mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
fix: preserve message parts order in chat display (#151)
- Fix bug where text after tool calls was merged with initial text - Group consecutive text/file parts into bubbles while keeping tools in order - Parts now display as: plan -> tool_result -> additional text - Remove debug logs from fixToolCallInputs function Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
This commit is contained in:
@@ -67,41 +67,23 @@ function isMinimalDiagram(xml: string): boolean {
|
|||||||
// Helper function to fix tool call inputs for Bedrock API
|
// Helper function to fix tool call inputs for Bedrock API
|
||||||
// Bedrock requires toolUse.input to be a JSON object, not a string
|
// Bedrock requires toolUse.input to be a JSON object, not a string
|
||||||
function fixToolCallInputs(messages: any[]): any[] {
|
function fixToolCallInputs(messages: any[]): any[] {
|
||||||
return messages.map((msg, msgIndex) => {
|
return messages.map((msg) => {
|
||||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
const fixedContent = msg.content.map((part: any, partIndex: number) => {
|
const fixedContent = msg.content.map((part: any) => {
|
||||||
if (part.type === "tool-call") {
|
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") {
|
if (typeof part.input === "string") {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(part.input)
|
const parsed = JSON.parse(part.input)
|
||||||
console.log(
|
|
||||||
`[fixToolCallInputs] Parsed string input to JSON:`,
|
|
||||||
parsed,
|
|
||||||
)
|
|
||||||
return { ...part, input: parsed }
|
return { ...part, input: parsed }
|
||||||
} catch {
|
} catch {
|
||||||
// If parsing fails, wrap the string in an object
|
// If parsing fails, wrap the string in an object
|
||||||
console.log(
|
|
||||||
`[fixToolCallInputs] Failed to parse, wrapping in object`,
|
|
||||||
)
|
|
||||||
return { ...part, input: { rawInput: part.input } }
|
return { ...part, input: { rawInput: part.input } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Input is already an object, but verify it's not null/undefined
|
// Input is already an object, but verify it's not null/undefined
|
||||||
if (part.input === null || part.input === undefined) {
|
if (part.input === null || part.input === undefined) {
|
||||||
console.log(
|
|
||||||
`[fixToolCallInputs] Input is null/undefined, using empty object`,
|
|
||||||
)
|
|
||||||
return { ...part, input: {} }
|
return { ...part, input: {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,139 +509,209 @@ export function ChatMessageDisplay({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Text content in bubble */
|
/* Render parts in order, grouping consecutive text/file parts into bubbles */
|
||||||
message.parts?.some(
|
(() => {
|
||||||
(part) =>
|
const parts = message.parts || []
|
||||||
part.type === "text" ||
|
const groups: {
|
||||||
part.type === "file",
|
type: "content" | "tool"
|
||||||
) && (
|
parts: typeof parts
|
||||||
<div
|
startIndex: number
|
||||||
className={`px-4 py-3 text-sm leading-relaxed ${
|
}[] = []
|
||||||
message.role === "user"
|
|
||||||
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
parts.forEach((part, index) => {
|
||||||
: message.role ===
|
const isToolPart =
|
||||||
"system"
|
part.type?.startsWith(
|
||||||
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
"tool-",
|
||||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
)
|
||||||
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
const isContentPart =
|
||||||
role={
|
part.type === "text" ||
|
||||||
message.role === "user" &&
|
part.type === "file"
|
||||||
isLastUserMessage &&
|
|
||||||
onEditMessage
|
if (isToolPart) {
|
||||||
? "button"
|
groups.push({
|
||||||
: undefined
|
type: "tool",
|
||||||
}
|
parts: [part],
|
||||||
tabIndex={
|
startIndex: index,
|
||||||
message.role === "user" &&
|
})
|
||||||
isLastUserMessage &&
|
} else if (isContentPart) {
|
||||||
onEditMessage
|
const lastGroup =
|
||||||
? 0
|
groups[
|
||||||
: undefined
|
groups.length - 1
|
||||||
}
|
]
|
||||||
onClick={() => {
|
|
||||||
if (
|
if (
|
||||||
message.role ===
|
lastGroup?.type ===
|
||||||
"user" &&
|
"content"
|
||||||
isLastUserMessage &&
|
|
||||||
onEditMessage
|
|
||||||
) {
|
) {
|
||||||
setEditingMessageId(
|
lastGroup.parts.push(
|
||||||
message.id,
|
part,
|
||||||
)
|
)
|
||||||
setEditText(
|
} else {
|
||||||
userMessageText,
|
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) => {
|
// Content bubble
|
||||||
if (
|
return (
|
||||||
(e.key === "Enter" ||
|
<div
|
||||||
e.key === " ") &&
|
key={`${message.id}-content-${group.startIndex}`}
|
||||||
message.role ===
|
className={`px-4 py-3 text-sm leading-relaxed ${
|
||||||
"user" &&
|
message.role ===
|
||||||
isLastUserMessage &&
|
"user"
|
||||||
onEditMessage
|
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
||||||
) {
|
: message.role ===
|
||||||
e.preventDefault()
|
"system"
|
||||||
setEditingMessageId(
|
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
||||||
message.id,
|
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||||
)
|
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""} ${groupIndex > 0 ? "mt-3" : ""}`}
|
||||||
setEditText(
|
role={
|
||||||
userMessageText,
|
message.role ===
|
||||||
)
|
"user" &&
|
||||||
}
|
isLastUserMessage &&
|
||||||
}}
|
onEditMessage
|
||||||
title={
|
? "button"
|
||||||
message.role === "user" &&
|
: undefined
|
||||||
isLastUserMessage &&
|
}
|
||||||
onEditMessage
|
tabIndex={
|
||||||
? "Click to edit"
|
message.role ===
|
||||||
: undefined
|
"user" &&
|
||||||
}
|
isLastUserMessage &&
|
||||||
>
|
onEditMessage
|
||||||
{message.parts?.map(
|
? 0
|
||||||
(part, index) => {
|
: undefined
|
||||||
switch (part.type) {
|
}
|
||||||
case "text":
|
onClick={() => {
|
||||||
return (
|
if (
|
||||||
<div
|
message.role ===
|
||||||
key={`${message.id}-text-${index}`}
|
"user" &&
|
||||||
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
isLastUserMessage &&
|
||||||
message.role ===
|
onEditMessage
|
||||||
"user"
|
) {
|
||||||
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
setEditingMessageId(
|
||||||
: "dark:prose-invert"
|
message.id,
|
||||||
}`}
|
)
|
||||||
>
|
setEditText(
|
||||||
<ReactMarkdown>
|
userMessageText,
|
||||||
{
|
)
|
||||||
part.text
|
}
|
||||||
}
|
}}
|
||||||
</ReactMarkdown>
|
onKeyDown={(e) => {
|
||||||
</div>
|
if (
|
||||||
)
|
(e.key ===
|
||||||
case "file":
|
"Enter" ||
|
||||||
return (
|
e.key ===
|
||||||
<div
|
" ") &&
|
||||||
key={`${message.id}-file-${part.url}`}
|
message.role ===
|
||||||
className="mt-2"
|
"user" &&
|
||||||
>
|
isLastUserMessage &&
|
||||||
<Image
|
onEditMessage
|
||||||
src={
|
) {
|
||||||
part.url
|
e.preventDefault()
|
||||||
}
|
setEditingMessageId(
|
||||||
width={
|
message.id,
|
||||||
200
|
)
|
||||||
}
|
setEditText(
|
||||||
height={
|
userMessageText,
|
||||||
200
|
)
|
||||||
}
|
}
|
||||||
alt={`Uploaded diagram or image for AI analysis`}
|
}}
|
||||||
className="rounded-lg border border-white/20"
|
title={
|
||||||
style={{
|
message.role ===
|
||||||
objectFit:
|
"user" &&
|
||||||
"contain",
|
isLastUserMessage &&
|
||||||
}}
|
onEditMessage
|
||||||
/>
|
? "Click to edit"
|
||||||
</div>
|
: undefined
|
||||||
)
|
}
|
||||||
default:
|
>
|
||||||
return null
|
{group.parts.map(
|
||||||
}
|
(
|
||||||
},
|
part,
|
||||||
)}
|
partIndex,
|
||||||
</div>
|
) => {
|
||||||
)
|
if (
|
||||||
)}
|
part.type ===
|
||||||
{/* Tool calls outside bubble */}
|
"text"
|
||||||
{message.parts?.map((part) => {
|
) {
|
||||||
if (part.type?.startsWith("tool-")) {
|
return (
|
||||||
return renderToolPart(
|
<div
|
||||||
part as ToolPartLike,
|
key={`${message.id}-text-${group.startIndex}-${partIndex}`}
|
||||||
|
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
||||||
|
message.role ===
|
||||||
|
"user"
|
||||||
|
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
|
||||||
|
: "dark:prose-invert"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ReactMarkdown>
|
||||||
|
{
|
||||||
|
(
|
||||||
|
part as {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.text
|
||||||
|
}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
part.type ===
|
||||||
|
"file"
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${message.id}-file-${group.startIndex}-${partIndex}`}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={
|
||||||
|
(
|
||||||
|
part as {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.url
|
||||||
|
}
|
||||||
|
width={
|
||||||
|
200
|
||||||
|
}
|
||||||
|
height={
|
||||||
|
200
|
||||||
|
}
|
||||||
|
alt={`Uploaded diagram or image for AI analysis`}
|
||||||
|
className="rounded-lg border border-white/20"
|
||||||
|
style={{
|
||||||
|
objectFit:
|
||||||
|
"contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
})()
|
||||||
return null
|
)}
|
||||||
})}
|
|
||||||
{/* Action buttons for assistant messages */}
|
{/* Action buttons for assistant messages */}
|
||||||
{message.role === "assistant" && (
|
{message.role === "assistant" && (
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user