Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate ToolResult display in ChatMessage component #71

Closed
wants to merge 13 commits into from
47 changes: 38 additions & 9 deletions hooks/chat/useChatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,43 @@ export function useChat({ propsId, onTitleUpdate }: { propsId?: Id<"threads">, o
const thread: Thread = {
id: threadId,
title: 'New Chat',
messages: fetchMessages.map((message: any) => ({
...message,
toolInvocations: message.tool_invocations ? message.tool_invocations.map((invocation: ToolInvocation) => ({
...invocation,
args: typeof invocation.args === 'string' ? JSON.parse(invocation.args) : invocation.args,
result: invocation.state === 'result' ? (typeof invocation.result === 'string' ? JSON.parse(invocation.result) : invocation.result) : undefined,
})) : undefined,
})) as Message[],
messages: fetchMessages.map((message: any) => {
const toolInvocations = message.tool_invocations
? message.tool_invocations.map((invocation: ToolInvocation) => {
let args = invocation.args;
let result = invocation.result;

// Parse args if it's a string
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
console.error('Error parsing args:', e);
}
}

// Parse result if it's a string and state is 'result'
if (invocation.state === 'result' && typeof result === 'string') {
try {
result = JSON.parse(result);
} catch (e) {
console.error('Error parsing result:', e);
}
}

return {
...invocation,
args,
result: invocation.state === 'result' ? result : undefined,
};
})
: undefined;

return {
...message,
toolInvocations,
};
}) as Message[],
createdAt: threadData.createdAt || new Date(),
userId: user?.id as Id<"users"> || '' as Id<"users">,
path: ''
Expand Down Expand Up @@ -178,4 +207,4 @@ export function useChat({ propsId, onTitleUpdate }: { propsId?: Id<"threads">, o
error,
stop: vercelChatProps.stop,
}
}
}
29 changes: 17 additions & 12 deletions panes/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import React, { useMemo } from "react"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import { CodeBlock } from "@/components/ui/codeblock"
Expand All @@ -14,6 +14,20 @@ export interface ChatMessageProps {
}

export function ChatMessage({ message, ...props }: ChatMessageProps) {
const renderedToolInvocations = useMemo(() => {
const renderedSet = new Set<string>();
const toolInvocations = [...(message.toolInvocations || []), ...(message.tool_invocations || [])];

return toolInvocations.filter(invocation => {
const key = `${invocation.toolName}-${JSON.stringify(invocation.args)}`;
if (!renderedSet.has(key)) {
renderedSet.add(key);
return true;
}
return false;
});
}, [message.toolInvocations, message.tool_invocations]);

return (
<div
className={cn('group relative mb-4 flex items-start')}
Expand Down Expand Up @@ -69,16 +83,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
{message.content}
</MemoizedReactMarkdown>
)}
{message.toolInvocations && message.toolInvocations.map(invocation => (
<ToolResult
key={invocation.toolCallId}
toolName={invocation.toolName}
args={invocation.args}
result={invocation.state === 'result' ? invocation.result : undefined}
state={invocation.state}
/>
))}
{message.tool_invocations && message.tool_invocations.map(invocation => (
{renderedToolInvocations.map(invocation => (
<ToolResult
key={invocation.toolCallId}
toolName={invocation.toolName}
Expand All @@ -91,4 +96,4 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
</div>
</div>
)
}
}
90 changes: 64 additions & 26 deletions panes/chat/ToolResult.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CheckCircle2, GitCompare, Loader2 } from "lucide-react"
import React, { useEffect, useState } from "react"
import React, { useEffect, useState, useRef } from "react"
import { cn } from "@/lib/utils"
import { FileViewer } from "./FileViewer"

Expand All @@ -16,25 +16,6 @@ const truncateLines = (str: string, maxLines: number) => {
return lines.slice(0, maxLines).join('\n') + '\n...';
};

const prettyPrintJson = (obj: any): string => {
if (typeof obj === 'string') {
try {
obj = JSON.parse(obj);
} catch (e) {
// If it's not valid JSON, just return the string
return obj;
}
}
const prettyJson = JSON.stringify(obj, (key, value) => {
if (key === 'token') return '[REDACTED]';
if (typeof value === 'string' && value.length > 50) {
return value.substring(0, 47) + '...';
}
return value;
}, 2);
return truncateLines(prettyJson, 5);
};

const getToolParams = (toolName: string, args: any): string => {
if (typeof args === 'string') {
try {
Expand All @@ -51,25 +32,80 @@ export const ToolResult: React.FC<ToolResultProps> = ({ toolName, args, result,
const [currentState, setCurrentState] = useState(state);
const [currentResult, setCurrentResult] = useState(result);
const [showOldContent, setShowOldContent] = useState(false);
const [processedResult, setProcessedResult] = useState<string | null>(null);
const resultRef = useRef<string | null>(null);

useEffect(() => {
setCurrentState(state);
setCurrentResult(result);
setProcessedResult(null);
resultRef.current = null;
console.log('ToolResult useEffect - state:', state, 'result:', result);
}, [state, result]);

const renderResult = () => {
console.log('renderResult - currentState:', currentState, 'currentResult:', currentResult);

if (currentState === 'result' && currentResult) {
if (typeof currentResult === 'object' && currentResult !== null) {
if ('summary' in currentResult) {
return currentResult.summary;
if (resultRef.current !== null) {
console.log('Returning cached result');
return resultRef.current;
}

let resultToRender = currentResult;

console.log('Initial resultToRender:', resultToRender);

// Handle rehydrated data structure (stringified JSON)
if (typeof resultToRender === 'string') {
try {
resultToRender = JSON.parse(resultToRender);
console.log('Parsed stringified result:', resultToRender);
} catch (e) {
console.log('Failed to parse stringified result, using as-is');
}
}

// Handle nested result structure
if (typeof resultToRender === 'object' && 'result' in resultToRender) {
resultToRender = resultToRender.result;
console.log('After handling nested result:', resultToRender);
}

let finalResult: string;

if (typeof resultToRender === 'string') {
console.log('Using string result:', resultToRender);
finalResult = resultToRender;
} else if (typeof resultToRender === 'object' && resultToRender !== null) {
console.log('Handling object result');
if ('summary' in resultToRender) {
console.log('Using summary:', resultToRender.summary);
finalResult = resultToRender.summary;
} else if ('content' in resultToRender) {
console.log('Using content:', resultToRender.content);
finalResult = resultToRender.content;
} else if ('details' in resultToRender) {
console.log('Using details:', resultToRender.details);
finalResult = resultToRender.details;
} else {
console.log('Stringifying object:', resultToRender);
finalResult = JSON.stringify(resultToRender, null, 2);
}
} else {
console.log('Fallback: stringifying result:', resultToRender);
finalResult = JSON.stringify(resultToRender, null, 2);
}
return prettyPrintJson(currentResult);

resultRef.current = finalResult;
return finalResult;
}
if (currentState === 'call') {
console.log('Returning call state message');
return `Calling ${toolName}...`;
}
return prettyPrintJson(args);
console.log('Returning empty string');
return '';
};

const renderStateIcon = () => {
Expand All @@ -87,6 +123,8 @@ export const ToolResult: React.FC<ToolResultProps> = ({ toolName, args, result,
setShowOldContent(!showOldContent);
};

console.log('ToolResult render - toolName:', toolName, 'args:', args, 'state:', currentState);

return (
<div className="my-2 px-4 text-sm text-foreground">
<div className={cn("bg-background text-foreground rounded border border-border overflow-hidden")}>
Expand Down Expand Up @@ -123,4 +161,4 @@ export const ToolResult: React.FC<ToolResultProps> = ({ toolName, args, result,
</div>
</div>
);
};
};
Loading