From 0f5fed85aa12143701223c76bd39b0250d576058 Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Wed, 16 Apr 2025 22:34:28 +0100 Subject: [PATCH] wip broken xml parser, parses only 1 tool, doesnt properly catch tag or attributes --- .../app/dashboard/agents/[threadId]/page.tsx | 410 +++++++++++------- 1 file changed, 256 insertions(+), 154 deletions(-) diff --git a/frontend/src/app/dashboard/agents/[threadId]/page.tsx b/frontend/src/app/dashboard/agents/[threadId]/page.tsx index 1e9c2c4f..62bdab90 100644 --- a/frontend/src/app/dashboard/agents/[threadId]/page.tsx +++ b/frontend/src/app/dashboard/agents/[threadId]/page.tsx @@ -4,7 +4,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; -import { ArrowDown, File, Terminal, ExternalLink, User, CheckCircle, CircleDashed } from 'lucide-react'; +import { + ArrowDown, FileText, Terminal, ExternalLink, User, CheckCircle, CircleDashed, + FileEdit, Search, Globe, Code, MessageSquare, Folder, FileX, CloudUpload, Wrench, Cog +} from 'lucide-react'; +import type { ElementType } from 'react'; import { addUserMessage, getMessages, startAgent, stopAgent, getAgentStatus, streamAgent, getAgentRuns, getProject, getThread, updateProject } from '@/lib/api'; import { toast } from 'sonner'; import { Skeleton } from "@/components/ui/skeleton"; @@ -50,11 +54,88 @@ function isToolSequence(item: RenderItem): item is ToolSequence { return (item as ToolSequence).type === 'tool_sequence'; } +// Helper function to get an icon based on tool name +const getToolIcon = (toolName: string): ElementType => { + // Ensure we handle null/undefined toolName gracefully + if (!toolName) return Cog; + + // Convert to lowercase for case-insensitive matching + const normalizedName = toolName.toLowerCase(); + + switch (normalizedName) { + case 'create-file': + case 'str-replace': + case 'write-file': + return FileEdit; + case 'run_terminal_cmd': + case 'run_command': + return Terminal; + case 'web_search': + return Search; + case 'browse_url': + return Globe; + case 'call_api': + return Code; + case 'send_message': + return MessageSquare; + case 'list_dir': + return Folder; + case 'read_file': + return FileText; + case 'delete_file': + return FileX; + case 'deploy': + return CloudUpload; + default: + // Add logging for debugging unhandled tool types + console.log(`[PAGE] Using default icon for unknown tool type: ${toolName}`); + return Cog; // Default icon + } +}; + +// Helper function to extract a primary parameter from XML/arguments +const extractPrimaryParam = (toolName: string, content: string | undefined): string | null => { + if (!content) return null; + + try { + // Simple regex for common parameters - adjust as needed + let match: RegExpMatchArray | null = null; + switch (toolName?.toLowerCase()) { + case 'edit_file': + case 'read_file': + case 'delete_file': + case 'write_file': + match = content.match(/target_file=(?:"|')([^"|']+)(?:"|')/); + // Return just the filename part + return match ? match[1].split('/').pop() || match[1] : null; + case 'run_terminal_cmd': + case 'run_command': + match = content.match(/command=(?:"|')([^"|']+)(?:"|')/); + // Truncate long commands + return match ? (match[1].length > 30 ? match[1].substring(0, 27) + '...' : match[1]) : null; + case 'web_search': + match = content.match(/query=(?:"|')([^"|']+)(?:"|')/); + return match ? (match[1].length > 30 ? match[1].substring(0, 27) + '...' : match[1]) : null; + case 'browse_url': + match = content.match(/url=(?:"|')([^"|']+)(?:"|')/); + return match ? match[1] : null; + // Add more cases as needed for other tools + default: + return null; + } + } catch (e) { + console.warn("Error parsing tool parameters:", e); + return null; + } +}; + +// Flag to control whether tool result messages are rendered +const SHOULD_RENDER_TOOL_RESULTS = false; + // Function to group consecutive assistant tool call / user tool result pairs function groupMessages(messages: ApiMessage[]): RenderItem[] { const grouped: RenderItem[] = []; let i = 0; - const excludedTags = ['ask', 'inform']; // Tags to exclude from grouping while (i < messages.length) { const currentMsg = messages[i]; @@ -68,13 +149,6 @@ function groupMessages(messages: ApiMessage[]): RenderItem[] { const toolTagMatch = currentMsg.content?.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>/); if (toolTagMatch && nextMsg && nextMsg.role === 'user') { const expectedTag = toolTagMatch[1]; - // *** Check if the tag is excluded *** - if (excludedTags.includes(expectedTag)) { - // If excluded, treat as a normal message and break potential sequence start - grouped.push(currentMsg); - i++; - continue; - } // Regex to check for ... // Using 's' flag for dotall to handle multiline content within tags -> Replaced with [\s\S] to avoid ES target issues @@ -95,11 +169,6 @@ function groupMessages(messages: ApiMessage[]): RenderItem[] { const nextToolTagMatch = potentialAssistant.content?.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>/); if (nextToolTagMatch && potentialUser && potentialUser.role === 'user') { const nextExpectedTag = nextToolTagMatch[1]; - // *** Check if the continuation tag is excluded *** - if (excludedTags.includes(nextExpectedTag)) { - // If excluded, break the sequence - break; - } // Replaced dotall 's' flag with [\s\S] const nextToolResultRegex = new RegExp(`^\\s*<(${nextExpectedTag})(?:\\s+[^>]*)?>[\\s\\S]*?\\s*`); @@ -1040,28 +1109,18 @@ export default function ThreadPage({ params }: { params: Promise }
- {/* Left border for the sequence */} - - - {/* Kortix Suna Label (Hover) */} - - Kortix Suna - - - {/* Render Avatar & Name ONCE for the sequence */} -
{/* Position avatar centered on the line */} -
- Suna Logo + {/* Simplified header with logo and name */} +
+
+ Suna
-
-
{/* Adjust margin to align name */} - Suna + Suna
{/* Container for the pairs within the sequence */} -
+
{pairs.map((pair, pairIndex) => { // Parse assistant message content const assistantContent = pair.assistantCall.content || ''; @@ -1072,54 +1131,55 @@ export default function ThreadPage({ params }: { params: Promise } const postContent = xmlMatch ? assistantContent.substring(xmlMatch.index + xmlMatch[0].length).trim() : ''; const userResultName = pair.userResult.content?.match(/\s*<([a-zA-Z\-_]+)/)?.[1] || 'Result'; + // Get icon and parameter for the tag + const IconComponent = getToolIcon(toolName); + const paramDisplay = extractPrimaryParam(toolName, assistantContent); + return (
- {/* Assistant Content (No Avatar/Name here) */} -
- {/* Pre-XML Content */} + {/* Tool execution content */} +
+ {/* First show any text content before the tool call */} {preContent && ( -
-
- {preContent} -
-
+

+ {preContent} +

)} - - {/* Tool Call Button */} + + {/* Clickable Tool Tag */} {xmlMatch && ( - + {paramDisplay && ( + + {paramDisplay} + + )} + )} {/* Post-XML Content (Less Common) */} {postContent && ( -
-
- {postContent} -
-
+

+ {postContent} +

)}
- {/* User Tool Result Part */} -
-
- - {userResultName} Result Received - {/* Optional: Add a button to show result details here too? */} - {/* */} + {/* Simple tool result indicator */} + {SHOULD_RENDER_TOOL_RESULTS && userResultName && ( +
+ + {userResultName} completed
-
+ )} +
); })} @@ -1137,65 +1197,86 @@ export default function ThreadPage({ params }: { params: Promise }
0 ? 'border-t border-gray-100' : ''}`} // Add top border between messages > {/* Avatar (User = Right, Assistant/Tool = Left) */} {message.role === 'user' ? ( // User bubble comes first in flex-end - <> -
- {/* User message bubble */} -
-
- {message.content} -
-
-
- +
+ {message.content} +
) : ( // Assistant / Tool bubble on the left - <> - {/* Assistant Avatar */} -
- Suna Logo -
- {/* Content Bubble */} -
- Suna -
-
- {/* Use existing logic for structured tool calls/results and normal messages */} - {message.type === 'tool_call' && message.tool_call ? ( - // Existing rendering for structured tool_call type -
-
- - Tool Call: {message.tool_call.function.name} -
-
- {message.tool_call.function.arguments} -
-
- ) : message.role === 'tool' ? ( - // Existing rendering for standard 'tool' role messages -
-
- - Tool Result: {message.name || 'Unknown Tool'} -
-
- {/* Render content safely, handle potential objects */} - {typeof message.content === 'string' ? message.content : JSON.stringify(message.content)} -
-
- ) : ( - // Default rendering for plain assistant messages - message.content - )} -
+
+ {/* Simplified header with logo and name */} +
+
+ Suna
+ Suna
- + + {/* Message content */} + {message.type === 'tool_call' && message.tool_call ? ( + // Clickable Tool Tag (Live) +
+ {(() => { // IIFE for scope + const toolName = message.tool_call.function.name; + const IconComponent = getToolIcon(toolName); + const paramDisplay = extractPrimaryParam(toolName, message.tool_call.function.arguments); + return ( + + ); + })()} +
+                                      {message.tool_call.function.arguments}
+                                    
+
+ ) : (message.role === 'tool' && SHOULD_RENDER_TOOL_RESULTS) ? ( + // Clean tool result UI +
+
+
+ + + {message.name || 'Unknown Tool'} + +
+
+
+                                      {typeof message.content === 'string' ? message.content : JSON.stringify(message.content, null, 2)}
+                                    
+
+ ) : ( + // Plain text message +
+ {message.content} +
+ )} +
)}
); @@ -1205,58 +1286,79 @@ export default function ThreadPage({ params }: { params: Promise } {streamContent && (
- {/* Assistant Avatar */} -
- Suna Logo + {/* Simplified header with logo and name */} +
+
+ Suna +
+ Suna
- {/* Content Bubble */} -
- Suna -
-
- {toolCallData ? ( - // Streaming Tool Call -
-
- - Tool Call: {toolCallData.name} -
-
- {toolCallData.arguments || ''} -
-
- ) : ( - // Streaming Text Content - streamContent - )} - {/* Blinking Cursor */} + +
+ {toolCallData ? ( + // Clickable Tool Tag (Streaming) +
+ {(() => { // IIFE for scope + const toolName = toolCallData.name; + const IconComponent = getToolIcon(toolName); + const paramDisplay = extractPrimaryParam(toolName, toolCallData.arguments); + return ( + + ); + })()} +
+                                {toolCallData.arguments || ''}
+                              
+
+ ) : ( + // Simple text streaming +
+ {streamContent} {isStreaming && ( - + )}
-
+ )}
)} {/* Loading indicator (three dots) */} {agentStatus === 'running' && !streamContent && !toolCallData && ( -
{/* Assistant style */} -
- Suna Logo -
-
- Suna -
-
-
-
-
-
+
+ {/* Simplified header with logo and name */} +
+
+ Suna
+ Suna +
+ +
+
+
+
)}