mirror of https://github.com/kortix-ai/suna.git
mid
This commit is contained in:
parent
918f82fff9
commit
542c18cd2f
|
@ -185,6 +185,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
|
const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
|
||||||
const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]);
|
const [toolCalls, setToolCalls] = useState<ToolCallInput[]>([]);
|
||||||
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
|
const [currentToolIndex, setCurrentToolIndex] = useState<number>(0);
|
||||||
|
const [autoOpenedPanel, setAutoOpenedPanel] = useState(false);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -263,6 +264,11 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
return [...prev, message];
|
return [...prev, message];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we received a tool message, refresh the tool panel
|
||||||
|
if (message.type === 'tool') {
|
||||||
|
setAutoOpenedPanel(false);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStreamStatusChange = useCallback((hookStatus: string) => {
|
const handleStreamStatusChange = useCallback((hookStatus: string) => {
|
||||||
|
@ -274,6 +280,8 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
case 'agent_not_running':
|
case 'agent_not_running':
|
||||||
setAgentStatus('idle');
|
setAgentStatus('idle');
|
||||||
setAgentRunId(null);
|
setAgentRunId(null);
|
||||||
|
// Reset auto-opened state when agent completes to trigger tool detection
|
||||||
|
setAutoOpenedPanel(false);
|
||||||
break;
|
break;
|
||||||
case 'connecting':
|
case 'connecting':
|
||||||
setAgentStatus('connecting');
|
setAgentStatus('connecting');
|
||||||
|
@ -539,6 +547,10 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
|
|
||||||
console.log('[PAGE] Refetched messages after stop:', unifiedMessages.length);
|
console.log('[PAGE] Refetched messages after stop:', unifiedMessages.length);
|
||||||
setMessages(unifiedMessages);
|
setMessages(unifiedMessages);
|
||||||
|
|
||||||
|
// Clear auto-opened state to trigger tool detection with fresh messages
|
||||||
|
setAutoOpenedPanel(false);
|
||||||
|
|
||||||
scrollToBottom('smooth');
|
scrollToBottom('smooth');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -594,6 +606,82 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
setFileViewerOpen(true);
|
setFileViewerOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Automatically detect and populate tool calls from messages
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if we've already auto-opened the panel or if it's already open
|
||||||
|
if (autoOpenedPanel || isSidePanelOpen) return;
|
||||||
|
|
||||||
|
const historicalToolPairs: ToolCallInput[] = [];
|
||||||
|
const assistantMessages = messages.filter(m => m.type === 'assistant' && m.message_id);
|
||||||
|
|
||||||
|
if (assistantMessages.length === 0) return;
|
||||||
|
|
||||||
|
assistantMessages.forEach(assistantMsg => {
|
||||||
|
const resultMessage = messages.find(toolMsg => {
|
||||||
|
if (toolMsg.type !== 'tool' || !toolMsg.metadata || !assistantMsg.message_id) return false;
|
||||||
|
try {
|
||||||
|
const metadata = JSON.parse(toolMsg.metadata);
|
||||||
|
return metadata.assistant_message_id === assistantMsg.message_id;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultMessage) {
|
||||||
|
// Determine tool name from assistant message content
|
||||||
|
let toolName = 'unknown';
|
||||||
|
try {
|
||||||
|
// Try to extract tool name from content
|
||||||
|
const xmlMatch = assistantMsg.content.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/);
|
||||||
|
if (xmlMatch) {
|
||||||
|
toolName = xmlMatch[1] || xmlMatch[2] || 'unknown';
|
||||||
|
} else {
|
||||||
|
// Fallback to checking for tool_calls JSON structure
|
||||||
|
const assistantContentParsed = safeJsonParse<{ tool_calls?: { name: string }[] }>(assistantMsg.content, {});
|
||||||
|
if (assistantContentParsed.tool_calls && assistantContentParsed.tool_calls.length > 0) {
|
||||||
|
toolName = assistantContentParsed.tool_calls[0].name || 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
let isSuccess = true;
|
||||||
|
try {
|
||||||
|
const toolContent = resultMessage.content?.toLowerCase() || '';
|
||||||
|
isSuccess = !(toolContent.includes('failed') ||
|
||||||
|
toolContent.includes('error') ||
|
||||||
|
toolContent.includes('failure'));
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
historicalToolPairs.push({
|
||||||
|
assistantCall: {
|
||||||
|
name: toolName,
|
||||||
|
content: assistantMsg.content,
|
||||||
|
timestamp: assistantMsg.created_at
|
||||||
|
},
|
||||||
|
toolResult: {
|
||||||
|
content: resultMessage.content,
|
||||||
|
isSuccess: isSuccess,
|
||||||
|
timestamp: resultMessage.created_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (historicalToolPairs.length > 0) {
|
||||||
|
setToolCalls(historicalToolPairs);
|
||||||
|
setCurrentToolIndex(historicalToolPairs.length - 1); // Set to the most recent tool call
|
||||||
|
setIsSidePanelOpen(true);
|
||||||
|
setAutoOpenedPanel(true);
|
||||||
|
}
|
||||||
|
}, [messages, autoOpenedPanel, isSidePanelOpen]);
|
||||||
|
|
||||||
|
// Reset auto-opened state when panel is closed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSidePanelOpen) {
|
||||||
|
setAutoOpenedPanel(false);
|
||||||
|
}
|
||||||
|
}, [isSidePanelOpen]);
|
||||||
|
|
||||||
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
const handleToolClick = useCallback((clickedAssistantMessageId: string | null, clickedToolName: string) => {
|
||||||
if (!clickedAssistantMessageId) {
|
if (!clickedAssistantMessageId) {
|
||||||
console.warn("Clicked assistant message ID is null. Cannot open side panel.");
|
console.warn("Clicked assistant message ID is null. Cannot open side panel.");
|
||||||
|
@ -626,12 +714,17 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
// otherwise fallback to a generic name or the one passed from the click.
|
// otherwise fallback to a generic name or the one passed from the click.
|
||||||
let toolNameForResult = clickedToolName; // Fallback
|
let toolNameForResult = clickedToolName; // Fallback
|
||||||
try {
|
try {
|
||||||
const assistantContentParsed = safeJsonParse<{ tool_calls?: { name: string }[] }>(assistantMsg.content, {});
|
// Try to extract tool name from content
|
||||||
// A simple heuristic: if the assistant message content has tool_calls structure
|
const xmlMatch = assistantMsg.content.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/);
|
||||||
if (assistantContentParsed.tool_calls && assistantContentParsed.tool_calls.length > 0) {
|
if (xmlMatch) {
|
||||||
toolNameForResult = assistantContentParsed.tool_calls[0].name || clickedToolName;
|
toolNameForResult = xmlMatch[1] || xmlMatch[2] || clickedToolName;
|
||||||
|
} else {
|
||||||
|
// Fallback to checking for tool_calls JSON structure
|
||||||
|
const assistantContentParsed = safeJsonParse<{ tool_calls?: { name: string }[] }>(assistantMsg.content, {});
|
||||||
|
if (assistantContentParsed.tool_calls && assistantContentParsed.tool_calls.length > 0) {
|
||||||
|
toolNameForResult = assistantContentParsed.tool_calls[0].name || clickedToolName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// More advanced: parse the XML in assistant message content to find the tool name associated with this result
|
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
let isSuccess = true;
|
let isSuccess = true;
|
||||||
|
@ -681,6 +774,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSidePanelOpen(true);
|
setIsSidePanelOpen(true);
|
||||||
|
setAutoOpenedPanel(true);
|
||||||
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
@ -689,17 +783,54 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
if (!toolCall) return;
|
if (!toolCall) return;
|
||||||
console.log("[STREAM] Received tool call:", toolCall.name || toolCall.xml_tag_name);
|
console.log("[STREAM] Received tool call:", toolCall.name || toolCall.xml_tag_name);
|
||||||
|
|
||||||
// Now with Markdown support, we can enable the side panel for streaming calls
|
// Create a properly formatted tool call input for the streaming tool
|
||||||
|
// that matches the format of historical tool calls
|
||||||
|
const toolName = toolCall.name || toolCall.xml_tag_name || 'Unknown Tool';
|
||||||
|
const toolArguments = toolCall.arguments || '';
|
||||||
|
|
||||||
|
// Format the arguments in a way that matches the expected XML format for each tool
|
||||||
|
// This ensures the specialized tool views render correctly
|
||||||
|
let formattedContent = toolArguments;
|
||||||
|
if (toolName.toLowerCase().includes('command') && !toolArguments.includes('<execute-command>')) {
|
||||||
|
formattedContent = `<execute-command>${toolArguments}</execute-command>`;
|
||||||
|
} else if (toolName.toLowerCase().includes('file') && !toolArguments.includes('<create-file>')) {
|
||||||
|
// For file operations, wrap with appropriate tag if not already wrapped
|
||||||
|
const fileOpTags = ['create-file', 'delete-file', 'full-file-rewrite'];
|
||||||
|
const matchingTag = fileOpTags.find(tag => toolName.toLowerCase().includes(tag));
|
||||||
|
if (matchingTag && !toolArguments.includes(`<${matchingTag}>`)) {
|
||||||
|
formattedContent = `<${matchingTag}>${toolArguments}</${matchingTag}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newToolCall: ToolCallInput = {
|
const newToolCall: ToolCallInput = {
|
||||||
assistantCall: {
|
assistantCall: {
|
||||||
name: toolCall.name || toolCall.xml_tag_name || 'Unknown Tool',
|
name: toolName,
|
||||||
content: toolCall.arguments || '',
|
content: formattedContent,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
},
|
||||||
|
// For streaming tool calls, provide empty content that indicates streaming
|
||||||
|
toolResult: {
|
||||||
|
content: "STREAMING",
|
||||||
|
isSuccess: true,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
}
|
}
|
||||||
// No toolResult available yet for streaming calls
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setToolCalls([newToolCall]);
|
// Update the tool calls state to reflect the streaming tool
|
||||||
|
setToolCalls(prev => {
|
||||||
|
// If the same tool is already being streamed, update it instead of adding a new one
|
||||||
|
if (prev.length > 0 && prev[0].assistantCall.name === toolName) {
|
||||||
|
return [{
|
||||||
|
...prev[0],
|
||||||
|
assistantCall: {
|
||||||
|
...prev[0].assistantCall,
|
||||||
|
content: formattedContent
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [newToolCall];
|
||||||
|
});
|
||||||
|
|
||||||
setCurrentToolIndex(0);
|
setCurrentToolIndex(0);
|
||||||
setIsSidePanelOpen(true);
|
setIsSidePanelOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -149,6 +149,8 @@ interface ToolCallSidePanelProps {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
onNavigate: (newIndex: number) => void;
|
onNavigate: (newIndex: number) => void;
|
||||||
project?: Project;
|
project?: Project;
|
||||||
|
renderAssistantMessage?: (assistantContent?: string, toolContent?: string) => React.ReactNode;
|
||||||
|
renderToolResult?: (toolContent?: string, isSuccess?: boolean) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallSidePanel({
|
export function ToolCallSidePanel({
|
||||||
|
@ -157,7 +159,9 @@ export function ToolCallSidePanel({
|
||||||
toolCalls,
|
toolCalls,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
project
|
project,
|
||||||
|
renderAssistantMessage,
|
||||||
|
renderToolResult
|
||||||
}: ToolCallSidePanelProps) {
|
}: ToolCallSidePanelProps) {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
@ -166,6 +170,26 @@ export function ToolCallSidePanel({
|
||||||
const currentToolName = currentToolCall?.assistantCall?.name || 'Tool Call';
|
const currentToolName = currentToolCall?.assistantCall?.name || 'Tool Call';
|
||||||
const CurrentToolIcon = getToolIcon(currentToolName === 'Tool Call' ? 'unknown' : currentToolName);
|
const CurrentToolIcon = getToolIcon(currentToolName === 'Tool Call' ? 'unknown' : currentToolName);
|
||||||
|
|
||||||
|
// Determine if this is a streaming tool call
|
||||||
|
const isStreaming = currentToolCall?.toolResult?.content === "STREAMING";
|
||||||
|
|
||||||
|
// Set up a pulse animation for streaming
|
||||||
|
const [dots, setDots] = React.useState('');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isStreaming) return;
|
||||||
|
|
||||||
|
// Create a loading animation with dots
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDots(prev => {
|
||||||
|
if (prev === '...') return '';
|
||||||
|
return prev + '.';
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isStreaming]);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!currentToolCall) {
|
if (!currentToolCall) {
|
||||||
return (
|
return (
|
||||||
|
@ -182,7 +206,7 @@ export function ToolCallSidePanel({
|
||||||
currentToolCall.toolResult?.content,
|
currentToolCall.toolResult?.content,
|
||||||
currentToolCall.assistantCall.timestamp,
|
currentToolCall.assistantCall.timestamp,
|
||||||
currentToolCall.toolResult?.timestamp,
|
currentToolCall.toolResult?.timestamp,
|
||||||
currentToolCall.toolResult?.isSuccess ?? true,
|
isStreaming ? true : (currentToolCall.toolResult?.isSuccess ?? true),
|
||||||
project
|
project
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -190,7 +214,11 @@ export function ToolCallSidePanel({
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l flex flex-col z-10">
|
<div className="fixed inset-y-0 right-0 w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[600px] bg-background border-l flex flex-col z-10">
|
||||||
<div className="p-4 flex items-center justify-between">
|
<div className="p-4 flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold">Suna's Computer</h3>
|
<h3 className="text-sm font-semibold">
|
||||||
|
{isStreaming
|
||||||
|
? `Suna's Computer (Running${dots})`
|
||||||
|
: "Suna's Computer"}
|
||||||
|
</h3>
|
||||||
<Button variant="ghost" size="icon" onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
<Button variant="ghost" size="icon" onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -204,7 +232,7 @@ export function ToolCallSidePanel({
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<CurrentToolIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
<CurrentToolIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
<span className="text-xs font-medium text-foreground truncate" title={currentToolName}>
|
<span className="text-xs font-medium text-foreground truncate" title={currentToolName}>
|
||||||
{currentToolName}
|
{currentToolName} {isStreaming && `(Running${dots})`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground flex-shrink-0">
|
<span className="text-xs text-muted-foreground flex-shrink-0">
|
||||||
|
|
|
@ -2,6 +2,8 @@ import React from "react";
|
||||||
import { ToolViewProps } from "./types";
|
import { ToolViewProps } from "./types";
|
||||||
import { formatTimestamp } from "./utils";
|
import { formatTimestamp } from "./utils";
|
||||||
import { getToolIcon } from "../utils";
|
import { getToolIcon } from "../utils";
|
||||||
|
import { CircleDashed } from "lucide-react";
|
||||||
|
import { Markdown } from "@/components/home/ui/markdown";
|
||||||
|
|
||||||
export function GenericToolView({
|
export function GenericToolView({
|
||||||
name,
|
name,
|
||||||
|
@ -12,6 +14,23 @@ export function GenericToolView({
|
||||||
toolTimestamp
|
toolTimestamp
|
||||||
}: ToolViewProps & { name?: string }) {
|
}: ToolViewProps & { name?: string }) {
|
||||||
const toolName = name || 'Unknown Tool';
|
const toolName = name || 'Unknown Tool';
|
||||||
|
const isStreaming = toolContent === "STREAMING";
|
||||||
|
|
||||||
|
// Parse the assistant content to extract tool parameters
|
||||||
|
const parsedContent = React.useMemo(() => {
|
||||||
|
if (!assistantContent) return null;
|
||||||
|
|
||||||
|
// Try to extract content from XML tags
|
||||||
|
const xmlMatch = assistantContent.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>([^<]*)<\/\1>/);
|
||||||
|
if (xmlMatch) {
|
||||||
|
return {
|
||||||
|
tag: xmlMatch[1],
|
||||||
|
content: xmlMatch[2].trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [assistantContent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
|
@ -25,39 +44,86 @@ export function GenericToolView({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{toolContent && (
|
{toolContent && !isStreaming && (
|
||||||
<div className={`px-2 py-1 rounded-full text-xs ${
|
<div className={`px-2 py-1 rounded-full text-xs ${
|
||||||
isSuccess ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
|
isSuccess ? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300'
|
||||||
|
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
|
||||||
}`}>
|
}`}>
|
||||||
{isSuccess ? 'Success' : 'Failed'}
|
{isSuccess ? 'Success' : 'Failed'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isStreaming && (
|
||||||
|
<div className="px-2 py-1 rounded-full text-xs bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-300 flex items-center gap-1">
|
||||||
|
<CircleDashed className="h-3 w-3 animate-spin" />
|
||||||
|
<span>Running</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assistant Message */}
|
{/* Tool Parameters */}
|
||||||
<div className="space-y-1">
|
{parsedContent && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="space-y-1">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
|
<div className="flex justify-between items-center">
|
||||||
{assistantTimestamp && (
|
<div className="text-xs font-medium text-muted-foreground">Tool Parameters</div>
|
||||||
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div>
|
{assistantTimestamp && (
|
||||||
)}
|
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/50 p-3">
|
||||||
|
{parsedContent.content.startsWith('{') ? (
|
||||||
|
// If content looks like JSON, render it prettified
|
||||||
|
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">
|
||||||
|
{JSON.stringify(JSON.parse(parsedContent.content), null, 2)}
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
// Otherwise render as Markdown
|
||||||
|
<Markdown className="text-xs prose prose-xs dark:prose-invert max-w-none">
|
||||||
|
{parsedContent.content}
|
||||||
|
</Markdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border bg-muted/50 p-3">
|
)}
|
||||||
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{assistantContent}</pre>
|
|
||||||
|
{/* Show original assistant content if couldn't parse properly or for debugging */}
|
||||||
|
{assistantContent && !parsedContent && !isStreaming && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">Assistant Message</div>
|
||||||
|
{assistantTimestamp && (
|
||||||
|
<div className="text-xs text-muted-foreground">{formatTimestamp(assistantTimestamp)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/50 p-3">
|
||||||
|
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{assistantContent}</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Tool Result */}
|
{/* Tool Result */}
|
||||||
{toolContent && (
|
{toolContent && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Tool Result</div>
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
{toolTimestamp && (
|
{isStreaming ? "Tool Execution" : "Tool Result"}
|
||||||
|
</div>
|
||||||
|
{toolTimestamp && !isStreaming && (
|
||||||
<div className="text-xs text-muted-foreground">{formatTimestamp(toolTimestamp)}</div>
|
<div className="text-xs text-muted-foreground">{formatTimestamp(toolTimestamp)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`rounded-md border p-3 ${isSuccess ? 'bg-muted/50' : 'bg-red-50'}`}>
|
<div className={`rounded-md border p-3 ${
|
||||||
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{toolContent}</pre>
|
isStreaming ? 'bg-blue-50/30 dark:bg-blue-900/20' :
|
||||||
|
(isSuccess ? 'bg-muted/50' : 'bg-red-50/30 dark:bg-red-900/20')
|
||||||
|
}`}>
|
||||||
|
{isStreaming ? (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-blue-700 dark:text-blue-300">
|
||||||
|
<CircleDashed className="h-3 w-3 animate-spin" />
|
||||||
|
<span>Executing {toolName.toLowerCase()}...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="text-xs overflow-auto whitespace-pre-wrap break-words">{toolContent}</pre>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue