// Import at the top // import { formatDistanceToNow } from 'date-fns'; import { FileText, FileCode, FileImage, FileJson, File, FolderOpen, FileType, FileVideo, FileAudio, FileArchive, Table, } from 'lucide-react'; import { parseXmlToolCalls, isNewXmlFormat } from './xml-parser'; import { parseToolResult, ParsedToolResult } from './tool-result-parser'; // Helper function to format timestamp export function formatTimestamp(isoString?: string): string { if (!isoString) return ''; try { const date = new Date(isoString); return isNaN(date.getTime()) ? 'Invalid date' : date.toLocaleString(); } catch (e) { return 'Invalid date'; } } // Get standardized tool title export function getToolTitle(toolName: string): string { // Normalize tool name const normalizedName = toolName.toLowerCase(); // Map of tool names to their display titles const toolTitles: Record = { 'execute-command': 'Execute Command', 'check-command-output': 'Check Command Output', 'str-replace': 'String Replace', 'create-file': 'Create File', 'full-file-rewrite': 'Rewrite File', 'delete-file': 'Delete File', 'web-search': 'Web Search', 'crawl-webpage': 'Web Crawl', 'scrape-webpage': 'Web Scrape', 'browser-navigate': 'Browser Navigate', 'browser-click': 'Browser Click', 'browser-extract': 'Browser Extract', 'browser-fill': 'Browser Fill', 'browser-wait': 'Browser Wait', 'see-image': 'View Image', 'ask': 'Ask', 'complete': 'Task Complete', 'execute-data-provider-call': 'Data Provider Call', 'get-data-provider-endpoints': 'Data Endpoints', 'deploy': 'Deploy', 'generic-tool': 'Tool', 'default': 'Tool', }; // Return the mapped title or a formatted version of the name if (toolTitles[normalizedName]) { return toolTitles[normalizedName]; } // For browser tools not explicitly mapped if (normalizedName.startsWith('browser-')) { const operation = normalizedName.replace('browser-', '').replace(/-/g, ' '); return 'Browser ' + operation.charAt(0).toUpperCase() + operation.slice(1); } // Format any other tool name return toolName .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } // Helper to extract command from execute-command content export function extractCommand(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; // First try to extract from XML tags (with or without attributes) const commandMatch = contentStr.match( /]*>([\s\S]*?)<\/execute-command>/, ); if (commandMatch) { return commandMatch[1].trim(); } // Try to find command in JSON structure (for native tool calls) try { const parsed = JSON.parse(contentStr); if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) { const execCommand = parsed.tool_calls.find((tc: any) => tc.function?.name === 'execute-command' || tc.function?.name === 'execute_command' ); if (execCommand && execCommand.function?.arguments) { try { const args = typeof execCommand.function.arguments === 'string' ? JSON.parse(execCommand.function.arguments) : execCommand.function.arguments; if (args.command) return args.command; } catch (e) { // If arguments parsing fails, continue } } } } catch (e) { // Not JSON, continue with other checks } // If no XML tags found, check if the content itself is the command // This handles cases where the command is passed directly if (!contentStr.includes('')) { // Check if it looks like a command (not JSON, not XML) if (!contentStr.startsWith('{') && !contentStr.startsWith('<')) { // Don't return content that looks like a tool result or error message if (!contentStr.includes('ToolResult') && !contentStr.includes('No command')) { return contentStr.trim(); } } } console.log('extractCommand: Could not extract command from content:', contentStr.substring(0, 200)); return null; } // Helper to extract session name from check-command-output content export function extractSessionName(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; // First try to extract from XML tags (with or without attributes) const sessionMatch = contentStr.match( /]*session_name=["']([^"']+)["']/, ); if (sessionMatch) { return sessionMatch[1].trim(); } // Try to find session_name in JSON structure (for native tool calls) try { const parsed = JSON.parse(contentStr); if (parsed.tool_calls && Array.isArray(parsed.tool_calls)) { const checkCommand = parsed.tool_calls.find((tc: any) => tc.function?.name === 'check-command-output' || tc.function?.name === 'check_command_output' ); if (checkCommand && checkCommand.function?.arguments) { try { const args = typeof checkCommand.function.arguments === 'string' ? JSON.parse(checkCommand.function.arguments) : checkCommand.function.arguments; if (args.session_name) return args.session_name; } catch (e) { // If arguments parsing fails, continue } } } } catch (e) { // Not JSON, continue with other checks } // Look for session_name attribute in the content const sessionNameMatch = contentStr.match(/session_name=["']([^"']+)["']/); if (sessionNameMatch) { return sessionNameMatch[1].trim(); } return null; } // Helper to extract command output from tool result content export function extractCommandOutput( content: string | object | undefined | null, ): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { // First try to parse the JSON content const parsedContent = JSON.parse(contentStr); // Handle check-command-output specific format if (parsedContent.output && typeof parsedContent.output === 'string') { return parsedContent.output; } if (parsedContent.content && typeof parsedContent.content === 'string') { // Look for a tool_result tag const toolResultMatch = parsedContent.content.match( /\s*<(?:execute-command|check-command-output)>([\s\S]*?)<\/(?:execute-command|check-command-output)>\s*<\/tool_result>/, ); if (toolResultMatch) { return toolResultMatch[1].trim(); } // Look for output field in a ToolResult pattern const outputMatch = parsedContent.content.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (outputMatch) { return outputMatch[1]; } // Return the content itself as a fallback return parsedContent.content; } // If parsedContent is the actual output (new format) if (typeof parsedContent === 'string') { return parsedContent; } } catch (e) { // If JSON parsing fails, try regex directly const toolResultMatch = contentStr.match( /\s*<(?:execute-command|check-command-output)>([\s\S]*?)<\/(?:execute-command|check-command-output)>\s*<\/tool_result>/, ); if (toolResultMatch) { return toolResultMatch[1].trim(); } const outputMatch = contentStr.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (outputMatch) { return outputMatch[1]; } // If no special format is found, return the content as-is // This handles cases where the output is stored directly if (!contentStr.startsWith('<') && !contentStr.includes('ToolResult')) { return contentStr; } } return contentStr; } // Helper to extract the exit code from tool result export function extractExitCode(content: string | object | undefined | null): number | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { const exitCodeMatch = contentStr.match(/exit_code=(\d+)/); if (exitCodeMatch && exitCodeMatch[1]) { return parseInt(exitCodeMatch[1], 10); } return 0; // Assume success if no exit code found but command completed } catch (e) { return null; } } // Helper to extract file path from commands export function extractFilePath(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { // Try to parse content as JSON first const parsedContent = JSON.parse(contentStr); if (parsedContent.content) { // Check if it's the new XML format if (isNewXmlFormat(parsedContent.content)) { const toolCalls = parseXmlToolCalls(parsedContent.content); if (toolCalls.length > 0 && toolCalls[0].parameters.file_path) { return cleanFilePath(toolCalls[0].parameters.file_path); } } // Fall back to old format const oldFormatMatch = parsedContent.content.match( /file_path=["']([^"']+)["']/, ); if (oldFormatMatch) { return cleanFilePath(oldFormatMatch[1]); } } } catch (e) { // Fall back to direct regex search if JSON parsing fails } // Check if it's the new XML format in raw content if (isNewXmlFormat(contentStr)) { const toolCalls = parseXmlToolCalls(contentStr); if (toolCalls.length > 0 && toolCalls[0].parameters.file_path) { return cleanFilePath(toolCalls[0].parameters.file_path); } } // Direct regex search in the content string (old format) const directMatch = contentStr.match(/file_path=["']([^"']+)["']/); if (directMatch) { return cleanFilePath(directMatch[1]); } // Handle double-escaped JSON (old format) if (typeof content === 'string' && content.startsWith('"{') && content.endsWith('}"')) { try { // First parse to get the inner JSON string const innerString = JSON.parse(content); // Then parse the inner string to get the actual object const parsed = JSON.parse(innerString); if (parsed && typeof parsed === 'object') { if (parsed.file_path) { return cleanFilePath(parsed.file_path); } if (parsed.arguments && parsed.arguments.file_path) { return cleanFilePath(parsed.arguments.file_path); } } } catch (e) { // Continue with normal extraction } } // First, check if content is already a parsed object (new format after double-escape fix) if (typeof content === 'object' && content !== null) { try { // Check if it's a direct object with content field if ('content' in content && typeof content.content === 'string') { // Look for XML tags in the content string const xmlFilePathMatch = content.content.match(/<(?:create-file|delete-file|full-file-rewrite|str-replace)[^>]*\s+file_path=["']([\s\S]*?)["']/i) || content.content.match(/]*\s+file_path=["']([\s\S]*?)["']/i) || content.content.match(/]*>([^<]+)<\/delete-file>/i) || content.content.match(/<(?:create-file|delete-file|full-file-rewrite)\s+file_path=["']([^"']+)/i); if (xmlFilePathMatch) { return cleanFilePath(xmlFilePathMatch[1]); } } // Check for direct file_path property if ('file_path' in content) { return cleanFilePath(content.file_path as string); } // Check for arguments.file_path if ('arguments' in content && content.arguments && typeof content.arguments === 'object') { const args = content.arguments as any; if (args.file_path) { return cleanFilePath(args.file_path); } } } catch (e) { // Continue with string parsing if object parsing fails } } // Try parsing as JSON string (old format) try { const parsedContent = JSON.parse(contentStr); if (parsedContent.file_path) { return cleanFilePath(parsedContent.file_path); } if (parsedContent.arguments && parsedContent.arguments.file_path) { return cleanFilePath(parsedContent.arguments.file_path); } } catch (e) { // Continue with original content if parsing fails } // Look for file_path in different formats const filePathMatch = contentStr.match(/file_path=["']([\s\S]*?)["']/i) || contentStr.match(/target_file=["']([\s\S]*?)["']/i) || contentStr.match(/path=["']([\s\S]*?)["']/i); if (filePathMatch) { const path = filePathMatch[1].trim(); // Handle newlines and return first line if multiple lines return cleanFilePath(path); } // Look for file_path in XML-like tags (including incomplete ones for streaming) const xmlFilePathMatch = contentStr.match(/<(?:create-file|delete-file|full-file-rewrite|str-replace)[^>]*\s+file_path=["']([\s\S]*?)["']/i) || contentStr.match(/]*\s+file_path=["']([\s\S]*?)["']/i) || contentStr.match(/]*>([^<]+)<\/delete-file>/i) || // Handle incomplete tags during streaming contentStr.match(/<(?:create-file|delete-file|full-file-rewrite)\s+file_path=["']([^"']+)/i); if (xmlFilePathMatch) { return cleanFilePath(xmlFilePathMatch[1]); } // Look for file paths in delete operations in particular if ( contentStr.toLowerCase().includes('delete') || contentStr.includes('delete-file') ) { // Look for patterns like "Deleting file: path/to/file.txt" const deletePathMatch = contentStr.match( /(?:delete|remove|deleting)\s+(?:file|the file)?:?\s+["']?([\w\-./\\]+\.\w+)["']?/i, ); if (deletePathMatch) return cleanFilePath(deletePathMatch[1]); // Look for isolated file paths with extensions const fileMatch = contentStr.match(/["']?([\w\-./\\]+\.\w+)["']?/); if (fileMatch) return cleanFilePath(fileMatch[1]); } return null; } // Helper to clean and process a file path string, handling escaped chars function cleanFilePath(path: string): string { if (!path) return path; // Handle escaped newlines and other escaped characters return path .replace(/\\n/g, '\n') // Replace \n with actual newlines .replace(/\\t/g, '\t') // Replace \t with actual tabs .replace(/\\r/g, '') // Remove \r .replace(/\\\\/g, '\\') // Replace \\ with \ .replace(/\\"/g, '"') // Replace \" with " .replace(/\\'/g, "'") // Replace \' with ' .split('\n')[0] // Take only the first line if multiline .trim(); // Trim whitespace } // Helper to extract str-replace old and new strings export function extractStrReplaceContent(content: string | object | undefined | null): { oldStr: string | null; newStr: string | null; } { const contentStr = normalizeContentToString(content); if (!contentStr) return { oldStr: null, newStr: null }; // First try to extract from a str-replace tag with attributes const strReplaceMatch = contentStr.match(/]*>([\s\S]*?)<\/str-replace>/); if (strReplaceMatch) { const innerContent = strReplaceMatch[1]; const oldMatch = innerContent.match(/([\s\S]*?)<\/old_str>/); const newMatch = innerContent.match(/([\s\S]*?)<\/new_str>/); return { oldStr: oldMatch ? oldMatch[1] : null, newStr: newMatch ? newMatch[1] : null, }; } // Fall back to direct search for old_str and new_str tags const oldMatch = contentStr.match(/([\s\S]*?)<\/old_str>/); const newMatch = contentStr.match(/([\s\S]*?)<\/new_str>/); return { oldStr: oldMatch ? oldMatch[1] : null, newStr: newMatch ? newMatch[1] : null, }; } // Helper to extract file content from create-file or file-rewrite export function extractFileContent( content: string | object | undefined | null, toolType: 'create-file' | 'full-file-rewrite', ): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { // Try to parse content as JSON first const parsedContent = JSON.parse(contentStr); if (parsedContent.content) { // Check if it's the new XML format if (isNewXmlFormat(parsedContent.content)) { const toolCalls = parseXmlToolCalls(parsedContent.content); if (toolCalls.length > 0 && toolCalls[0].parameters.file_contents) { return processFileContent(toolCalls[0].parameters.file_contents); } } // Fall back to old format const tagName = toolType === 'create-file' ? 'create-file' : 'full-file-rewrite'; const fileContentMatch = parsedContent.content.match( new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i'), ); if (fileContentMatch) { return processFileContent(fileContentMatch[1]); } } } catch (e) { // Fall back to direct regex search if JSON parsing fails } // Check if it's the new XML format in raw content if (isNewXmlFormat(contentStr)) { const toolCalls = parseXmlToolCalls(contentStr); if (toolCalls.length > 0 && toolCalls[0].parameters.file_contents) { return processFileContent(toolCalls[0].parameters.file_contents); } } // Direct regex search in the content string (old format) const tagName = toolType === 'create-file' ? 'create-file' : 'full-file-rewrite'; const fileContentMatch = contentStr.match( new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i'), ); if (fileContentMatch) { return processFileContent(fileContentMatch[1]); } return null; } function processFileContent(content: string | object): string { if (!content) return ''; if (typeof content === 'object') { return JSON.stringify(content, null, 2); } const trimmedContent = content.trim(); const isLikelyJson = (trimmedContent.startsWith('{') && trimmedContent.endsWith('}')) || (trimmedContent.startsWith('[') && trimmedContent.endsWith(']')); if (isLikelyJson) { try { const parsed = JSON.parse(content); return JSON.stringify(parsed, null, 2); } catch (e) { } } return content .replace(/\\n/g, '\n') .replace(/\\t/g, '\t') .replace(/\\r/g, '') .replace(/\\\\/g, '\\') .replace(/\\"/g, '"') .replace(/\\'/g, "'"); } // Helper to determine file type (for syntax highlighting) export function getFileType(filePath: string): string { const extension = filePath.split('.').pop()?.toLowerCase() || ''; switch (extension) { case 'js': return 'JavaScript'; case 'ts': return 'TypeScript'; case 'jsx': case 'tsx': return 'React'; case 'py': return 'Python'; case 'html': return 'HTML'; case 'css': return 'CSS'; case 'json': return 'JSON'; case 'md': return 'Markdown'; default: return extension.toUpperCase() || 'Text'; } } // Helper to extract URL from browser navigate operations export function extractBrowserUrl(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; const urlMatch = contentStr.match(/url=["'](https?:\/\/[^"']+)["']/); return urlMatch ? urlMatch[1] : null; } // Helper to extract browser operation type export function extractBrowserOperation(toolName: string | undefined): string { if (!toolName) return 'Browser Operation'; const operation = toolName.replace('browser-', '').replace(/-/g, ' '); return operation.charAt(0).toUpperCase() + operation.slice(1); } // Helper to extract search query export function extractSearchQuery(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; // First, look for ToolResult pattern in the content string const toolResultMatch = contentStr.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (toolResultMatch) { try { // Parse the output JSON from ToolResult const outputJson = JSON.parse(toolResultMatch[1]); // Check if this is the new Tavily response format with query field if (outputJson.query && typeof outputJson.query === 'string') { return outputJson.query; } } catch (e) { // Continue with other extraction methods } } let contentToSearch = contentStr; // Start with the normalized content // Try parsing as JSON first try { const parsedContent = JSON.parse(contentStr); // Check if it's the new Tavily response format if (parsedContent.query && typeof parsedContent.query === 'string') { return parsedContent.query; } // Continue with existing logic for backward compatibility if (typeof parsedContent.content === 'string') { // If the outer content is JSON and has a 'content' string field, // use that inner content for searching the query. contentToSearch = parsedContent.content; // Also check common JSON structures within the outer parsed object itself if (typeof parsedContent.query === 'string') { return parsedContent.query; } if ( typeof parsedContent.arguments === 'object' && parsedContent.arguments !== null && typeof parsedContent.arguments.query === 'string' ) { return parsedContent.arguments.query; } if ( Array.isArray(parsedContent.tool_calls) && parsedContent.tool_calls.length > 0 ) { const toolCall = parsedContent.tool_calls[0]; if ( typeof toolCall.arguments === 'object' && toolCall.arguments !== null && typeof toolCall.arguments.query === 'string' ) { return toolCall.arguments.query; } if (typeof toolCall.arguments === 'string') { try { const argsParsed = JSON.parse(toolCall.arguments); if (typeof argsParsed.query === 'string') { return argsParsed.query; } } catch {} } } } } catch (e) { // If parsing fails, continue with the original content string } // Now search within contentToSearch (either original or nested content) // 1. Try regex for attribute within tag (with or without attributes) const xmlQueryMatch = contentToSearch.match( /]*\s+query=[\"']([^\"']*)["'][^>]*>/i, ); if (xmlQueryMatch && xmlQueryMatch[1]) { return xmlQueryMatch[1].trim(); } // 2. Try simple attribute regex (fallback, less specific) const simpleAttrMatch = contentToSearch.match(/query=[\"']([\s\S]*?)["']/i); if (simpleAttrMatch && simpleAttrMatch[1]) { return simpleAttrMatch[1].split(/[\"']/)[0].trim(); } // 4. If nothing found after checking original/nested content and JSON structure, return null return null; } // Helper to extract URLs and titles with regex export function extractUrlsAndTitles( content: string, ): Array<{ title: string; url: string; snippet?: string }> { const results: Array<{ title: string; url: string; snippet?: string }> = []; // Try to parse as JSON first to extract proper results try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { return parsed.map(result => ({ title: result.title || '', url: result.url || '', snippet: result.content || result.snippet || '', })); } if (parsed.results && Array.isArray(parsed.results)) { return parsed.results.map((result: any) => ({ title: result.title || '', url: result.url || '', snippet: result.content || '', })); } } catch (e) { // Not valid JSON, continue with regex extraction } // Look for properly formatted JSON objects with title and url const jsonObjectPattern = /\{\s*"title"\s*:\s*"([^"]+)"\s*,\s*"url"\s*:\s*"(https?:\/\/[^"]+)"\s*(?:,\s*"content"\s*:\s*"([^"]*)")?\s*\}/g; let objectMatch; while ((objectMatch = jsonObjectPattern.exec(content)) !== null) { const title = objectMatch[1]; const url = objectMatch[2]; const snippet = objectMatch[3] || ''; if (url && title && !results.some((r) => r.url === url)) { results.push({ title, url, snippet }); } } // If we didn't find any results with the JSON fragment approach, fall back to standard URL extraction if (results.length === 0) { // Regex to find URLs, attempting to exclude common trailing unwanted characters/tags const urlRegex = /https?:\/\/[^\s"<]+/g; let match; while ((match = urlRegex.exec(content)) !== null) { let url = match[0]; // --- Start: New Truncation Logic --- // Find the first occurrence of potential garbage separators like /n or \n after the protocol. const protocolEndIndex = url.indexOf('://'); const searchStartIndex = protocolEndIndex !== -1 ? protocolEndIndex + 3 : 0; const newlineIndexN = url.indexOf('/n', searchStartIndex); const newlineIndexSlashN = url.indexOf('\\n', searchStartIndex); let firstNewlineIndex = -1; if (newlineIndexN !== -1 && newlineIndexSlashN !== -1) { firstNewlineIndex = Math.min(newlineIndexN, newlineIndexSlashN); } else if (newlineIndexN !== -1) { firstNewlineIndex = newlineIndexN; } else if (newlineIndexSlashN !== -1) { firstNewlineIndex = newlineIndexSlashN; } // If a newline indicator is found, truncate the URL there. if (firstNewlineIndex !== -1) { url = url.substring(0, firstNewlineIndex); } // --- End: New Truncation Logic --- // Basic cleaning: remove common tags or artifacts if they are directly appended url = url .replace(/<\/?url>$/, '') .replace(/<\/?content>$/, '') .replace(/%3C$/, ''); // Remove trailing %3C (less than sign) // Aggressive trailing character removal (common issues) // Apply this *after* potential truncation while (/[);.,\/]$/.test(url)) { url = url.slice(0, -1); } // Decode URI components to handle % sequences, but catch errors try { // Decode multiple times? Sometimes needed for double encoding url = decodeURIComponent(decodeURIComponent(url)); } catch (e) { try { // Try decoding once if double decoding failed url = decodeURIComponent(url); } catch (e2) { console.warn('Failed to decode URL component:', url, e2); } } // Final cleaning for specific problematic sequences like ellipsis or remaining tags url = url.replace(/\u2026$/, ''); // Remove trailing ellipsis (…) url = url.replace(/<\/?url>$/, '').replace(/<\/?content>$/, ''); // Re-apply tag removal after decode // Try to find a title near this URL const urlIndex = match.index; const surroundingText = content.substring( Math.max(0, urlIndex - 100), urlIndex + url.length + 200, ); // Look for title patterns more robustly const titleMatch = surroundingText.match(/title"?\s*:\s*"([^"]+)"/i) || surroundingText.match(/Title[:\s]+([^\n<]+)/i) || surroundingText.match(/\"(.*?)\"[\s\n]*?https?:\/\//); let title = cleanUrl(url); // Default to cleaned URL hostname/path if (titleMatch && titleMatch[1].trim()) { title = titleMatch[1].trim(); } // Avoid adding duplicates if the cleaning resulted in the same URL if (url && !results.some((r) => r.url === url)) { results.push({ title: title, url: url, }); } } } return results; } // Helper to clean URL for display export function cleanUrl(url: string): string { try { const urlObj = new URL(url); return ( urlObj.hostname.replace('www.', '') + (urlObj.pathname !== '/' ? urlObj.pathname : '') ); } catch (e) { return url; } } // Helper to extract URL for webpage crawling/scraping export function extractCrawlUrl(content: string | object | undefined | null): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { // Try to parse content as JSON first (for the new format) const parsedContent = JSON.parse(contentStr); if (parsedContent.content) { // Look for URL in the content string (with or without attributes) const urlMatch = parsedContent.content.match( /<(?:crawl|scrape)-webpage[^>]*\s+url=["'](https?:\/\/[^"']+)["']/i, ); if (urlMatch) return urlMatch[1]; } } catch (e) { // Fall back to direct regex search if JSON parsing fails } // Direct regex search in the content string (updated to handle attributes) const urlMatch = contentStr.match( /<(?:crawl|scrape)-webpage[^>]*\s+url=["'](https?:\/\/[^"']+)["']/i, ) || contentStr.match(/url=["'](https?:\/\/[^"']+)["']/i); return urlMatch ? urlMatch[1] : null; } // Helper to extract webpage content from crawl/scrape result export function extractWebpageContent( content: string | object | undefined | null, ): { title: string; text: string } | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; try { // Try to parse the JSON content const parsedContent = JSON.parse(contentStr); // Handle case where content is in parsedContent.content field if (parsedContent.content && typeof parsedContent.content === 'string') { // Look for tool_result tag (with attributes) const toolResultMatch = parsedContent.content.match( /]*>\s*<(?:crawl|scrape)-webpage[^>]*>([\s\S]*?)<\/(?:crawl|scrape)-webpage>\s*<\/tool_result>/, ); if (toolResultMatch) { try { // Try to parse the content inside the tags const rawData = toolResultMatch[1]; // Look for ToolResult pattern in the raw data const toolResultOutputMatch = rawData.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (toolResultOutputMatch) { try { // If ToolResult pattern found, try to parse its output which may be a stringified JSON const outputJson = JSON.parse( toolResultOutputMatch[1] .replace(/\\\\n/g, '\\n') .replace(/\\\\u/g, '\\u'), ); // Handle array format (first item) if (Array.isArray(outputJson) && outputJson.length > 0) { const item = outputJson[0]; return { title: item.Title || item.title || '', text: item.Text || item.text || item.content || '', }; } // Handle direct object format return { title: outputJson.Title || outputJson.title || '', text: outputJson.Text || outputJson.text || outputJson.content || '', }; } catch (e) { // If parsing fails, use the raw output return { title: 'Webpage Content', text: toolResultOutputMatch[1], }; } } // Try to parse as direct JSON if no ToolResult pattern const crawlData = JSON.parse(rawData); // Handle array format if (Array.isArray(crawlData) && crawlData.length > 0) { const item = crawlData[0]; return { title: item.Title || item.title || '', text: item.Text || item.text || item.content || '', }; } // Handle direct object format return { title: crawlData.Title || crawlData.title || '', text: crawlData.Text || crawlData.text || crawlData.content || '', }; } catch (e) { // Fallback to basic text extraction return { title: 'Webpage Content', text: toolResultMatch[1], }; } } // Handle ToolResult pattern in the content directly const toolResultOutputMatch = parsedContent.content.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (toolResultOutputMatch) { try { // Parse the output which might be a stringified JSON const outputJson = JSON.parse( toolResultOutputMatch[1] .replace(/\\\\n/g, '\\n') .replace(/\\\\u/g, '\\u'), ); // Handle array format if (Array.isArray(outputJson) && outputJson.length > 0) { const item = outputJson[0]; return { title: item.Title || item.title || '', text: item.Text || item.text || item.content || '', }; } // Handle direct object format return { title: outputJson.Title || outputJson.title || '', text: outputJson.Text || outputJson.text || outputJson.content || '', }; } catch (e) { // If parsing fails, use the raw output return { title: 'Webpage Content', text: toolResultOutputMatch[1], }; } } } // Direct handling of or format outside of content field (with attributes) const webpageMatch = contentStr.match( /<(?:crawl|scrape)-webpage[^>]*>([\s\S]*?)<\/(?:crawl|scrape)-webpage>/, ); if (webpageMatch) { const rawData = webpageMatch[1]; // Look for ToolResult pattern const toolResultOutputMatch = rawData.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (toolResultOutputMatch) { try { // Parse the output which might be a stringified JSON const outputString = toolResultOutputMatch[1] .replace(/\\\\n/g, '\\n') .replace(/\\\\u/g, '\\u'); const outputJson = JSON.parse(outputString); // Handle array format if (Array.isArray(outputJson) && outputJson.length > 0) { const item = outputJson[0]; return { title: item.Title || item.title || (item.URL ? new URL(item.URL).hostname : ''), text: item.Text || item.text || item.content || '', }; } // Handle direct object format return { title: outputJson.Title || outputJson.title || '', text: outputJson.Text || outputJson.text || outputJson.content || '', }; } catch (e) { // If parsing fails, use the raw output return { title: 'Webpage Content', text: toolResultOutputMatch[1], }; } } } // Direct content extraction from parsed JSON if it's an array if (Array.isArray(parsedContent) && parsedContent.length > 0) { const item = parsedContent[0]; return { title: item.Title || item.title || '', text: item.Text || item.text || item.content || '', }; } // Direct content extraction from parsed JSON as object if (typeof parsedContent === 'object' && parsedContent !== null) { // Check if it's already the webpage data (new format after double-escape fix) if ('Title' in parsedContent || 'title' in parsedContent || 'Text' in parsedContent || 'text' in parsedContent) { return { title: parsedContent.Title || parsedContent.title || 'Webpage Content', text: parsedContent.Text || parsedContent.text || parsedContent.content || '', }; } // Otherwise, try to stringify it return { title: 'Webpage Content', text: JSON.stringify(parsedContent), }; } } catch (e) { // Last resort, try to match the ToolResult pattern directly in the raw content const toolResultMatch = contentStr.match( /ToolResult\(.*?output='([\s\S]*?)'.*?\)/, ); if (toolResultMatch) { try { // Try to parse the output which might be a stringified JSON const outputJson = JSON.parse( toolResultMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\u/g, '\\u'), ); // Handle array format if (Array.isArray(outputJson) && outputJson.length > 0) { const item = outputJson[0]; return { title: item.Title || item.title || '', text: item.Text || item.text || item.content || '', }; } // Handle direct object format return { title: outputJson.Title || outputJson.title || '', text: outputJson.Text || outputJson.text || outputJson.content || '', }; } catch (e) { // If parsing fails, use the raw output return { title: 'Webpage Content', text: toolResultMatch[1], }; } } // If all else fails, return the content as-is if (contentStr) { return { title: 'Webpage Content', text: contentStr, }; } } return null; } // Helper to extract search results from tool response export function extractSearchResults( content: string | object | undefined | null, ): Array<{ title: string; url: string; snippet?: string }> { const contentStr = normalizeContentToString(content); if (!contentStr) return []; try { // Instead of trying to parse the complex ToolResult JSON, // let's look for the results array pattern directly in the content // Look for the results array pattern within the content const resultsPattern = /"results":\s*\[([^\]]*(?:\[[^\]]*\][^\]]*)*)\]/; const resultsMatch = contentStr.match(resultsPattern); if (resultsMatch) { try { // Extract just the results array and parse it const resultsArrayStr = '[' + resultsMatch[1] + ']'; const results = JSON.parse(resultsArrayStr); if (Array.isArray(results)) { return results.map(result => ({ title: result.title || '', url: result.url || '', snippet: result.content || '', })); } } catch (e) { console.warn('Failed to parse results array:', e); } } // Fallback: Look for individual result objects const resultObjectPattern = /\{\s*"url":\s*"([^"]+)"\s*,\s*"title":\s*"([^"]+)"\s*,\s*"content":\s*"([^"]*)"[^}]*\}/g; const results = []; let match; while ((match = resultObjectPattern.exec(contentStr)) !== null) { results.push({ url: match[1], title: match[2], snippet: match[3], }); } if (results.length > 0) { return results; } // Try parsing the entire content as JSON (for direct Tavily responses) const parsedContent = JSON.parse(contentStr); // Check if this is the new Tavily response format if (parsedContent.results && Array.isArray(parsedContent.results)) { return parsedContent.results.map((result: any) => ({ title: result.title || '', url: result.url || '', snippet: result.content || '', })); } // Continue with existing logic for backward compatibility if (parsedContent.content && typeof parsedContent.content === 'string') { // Look for a tool_result tag (with attributes) const toolResultTagMatch = parsedContent.content.match( /]*>\s*]*>([\s\S]*?)<\/web-search>\s*<\/tool_result>/, ); if (toolResultTagMatch) { // Try to parse the results array try { return JSON.parse(toolResultTagMatch[1]); } catch (e) { // Fallback to regex extraction of URLs and titles return extractUrlsAndTitles(toolResultTagMatch[1]); } } // Try to find JSON array in the content const jsonArrayMatch = parsedContent.content.match(/\[\s*{[\s\S]*}\s*\]/); if (jsonArrayMatch) { try { return JSON.parse(jsonArrayMatch[0]); } catch (e) { return extractUrlsAndTitles(parsedContent.content); } } // If none of the above worked, try the whole content return extractUrlsAndTitles(parsedContent.content); } } catch (e) { // If JSON parsing fails, extract directly from the content return extractUrlsAndTitles(contentStr); } // Last resort fallback return extractUrlsAndTitles(contentStr); } // Function to determine which tool component to render based on the tool name export function getToolComponent(toolName: string): string { if (!toolName) return 'GenericToolView'; const normalizedName = toolName.toLowerCase(); // Map specific tool names to their respective components switch (normalizedName) { // Browser tools case 'browser-navigate': case 'browser-click': case 'browser-extract': case 'browser-fill': case 'browser-wait': case 'browser-screenshot': return 'BrowserToolView'; // Command execution case 'execute-command': return 'CommandToolView'; // File operations case 'create-file': case 'delete-file': case 'full-file-rewrite': case 'read-file': return 'FileOperationToolView'; // String operations case 'str-replace': return 'StrReplaceToolView'; // Web operations case 'web-search': return 'WebSearchToolView'; case 'crawl-webpage': return 'WebCrawlToolView'; case 'scrape-webpage': return 'WebScrapeToolView'; // Data provider operations case 'execute-data-provider-call': case 'get-data-provider-endpoints': return 'DataProviderToolView'; //Deploy case 'deploy': return 'DeployToolView'; // Default default: return 'GenericToolView'; } } // Helper function to normalize content to string export function normalizeContentToString(content: string | object | undefined | null): string | null { if (!content) return null; if (typeof content === 'string') { // Check if it's a double-escaped JSON string (old format) if (content.startsWith('"{') && content.endsWith('}"')) { try { // First parse to get the inner JSON string const innerString = JSON.parse(content); // Then parse the inner string to get the actual object const parsed = JSON.parse(innerString); // Return the content field if it exists if (parsed && typeof parsed === 'object' && 'content' in parsed) { return parsed.content; } // Otherwise return the stringified object return JSON.stringify(parsed); } catch (e) { // If parsing fails, return as is } } return content; } if (typeof content === 'object' && content !== null) { try { // Handle case where content is a parsed object with content field (new format) if ('content' in content && typeof content.content === 'string') { return content.content; } // Handle case where content is a parsed object with content field that's also an object else if ('content' in content && typeof content.content === 'object' && content.content !== null) { // Check if the nested content has a content field if ('content' in content.content && typeof content.content.content === 'string') { return content.content.content; } // Try to stringify nested content object return JSON.stringify(content.content); } // Handle message format {role: 'tool', content: '...'} else if ('role' in content && 'content' in content && typeof content.content === 'string') { return content.content; } // Handle nested message format {role: 'assistant', content: {role: 'assistant', content: '...'}} else if ('role' in content && 'content' in content && typeof content.content === 'object' && content.content !== null) { if ('content' in content.content && typeof content.content.content === 'string') { return content.content.content; } // Try to stringify nested content object return JSON.stringify(content.content); } // Handle direct object that might be the content itself (new format) else { // If it looks like it might contain XML or structured content, stringify it const stringified = JSON.stringify(content); // Check if the stringified version contains XML tags or other structured content if (stringified.includes('<') || stringified.includes('file_path') || stringified.includes('command')) { return stringified; } // Otherwise, try to extract meaningful content return stringified; } } catch (e) { console.error('Error in normalizeContentToString:', e, 'Content:', content); return null; } } return null; } // Helper function to extract file content for streaming (handles incomplete XML) export function extractStreamingFileContent( content: string | object | undefined | null, toolType: 'create-file' | 'full-file-rewrite', ): string | null { const contentStr = normalizeContentToString(content); if (!contentStr) return null; const tagName = toolType === 'create-file' ? 'create-file' : 'full-file-rewrite'; // First check if content is already a parsed object (new format) if (typeof content === 'object' && content !== null) { try { if ('content' in content && typeof content.content === 'string') { // Look for the opening tag const openTagMatch = content.content.match(new RegExp(`<${tagName}[^>]*>`, 'i')); if (openTagMatch) { // Find where the tag ends const tagEndIndex = content.content.indexOf(openTagMatch[0]) + openTagMatch[0].length; // Extract everything after the opening tag const afterTag = content.content.substring(tagEndIndex); // Check if there's a closing tag const closeTagMatch = afterTag.match(new RegExp(`<\\/${tagName}>`, 'i')); if (closeTagMatch) { // Return content between tags return processFileContent(afterTag.substring(0, closeTagMatch.index)); } else { // No closing tag yet (streaming), return what we have return processFileContent(afterTag); } } } } catch (e) { // Continue with string parsing } } // Fallback to string-based extraction // Look for the opening tag const openTagMatch = contentStr.match(new RegExp(`<${tagName}[^>]*>`, 'i')); if (openTagMatch) { // Find where the tag ends const tagEndIndex = contentStr.indexOf(openTagMatch[0]) + openTagMatch[0].length; // Extract everything after the opening tag const afterTag = contentStr.substring(tagEndIndex); // Check if there's a closing tag const closeTagMatch = afterTag.match(new RegExp(`<\\/${tagName}>`, 'i')); if (closeTagMatch) { // Return content between tags return processFileContent(afterTag.substring(0, closeTagMatch.index)); } else { // No closing tag yet (streaming), return what we have return processFileContent(afterTag); } } return null; } export const getFileIconAndColor = (filename: string) => { const ext = filename.split('.').pop()?.toLowerCase(); switch (ext) { case 'js': case 'jsx': case 'ts': case 'tsx': return { icon: FileCode, color: 'text-yellow-500 dark:text-yellow-400', bgColor: 'bg-gradient-to-br from-yellow-500/20 to-yellow-600/10 border border-yellow-500/20' }; case 'py': return { icon: FileCode, color: 'text-blue-500 dark:text-blue-400', bgColor: 'bg-gradient-to-br from-blue-500/20 to-blue-600/10 border border-blue-500/20' }; case 'html': case 'css': case 'scss': return { icon: FileCode, color: 'text-orange-500 dark:text-orange-400', bgColor: 'bg-gradient-to-br from-orange-500/20 to-orange-600/10 border border-orange-500/20' }; // Data files case 'json': return { icon: FileJson, color: 'text-green-500 dark:text-green-400', bgColor: 'bg-gradient-to-br from-green-500/20 to-green-600/10 border border-green-500/20' }; case 'csv': return { icon: Table, color: 'text-emerald-500 dark:text-emerald-400', bgColor: 'bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 border border-emerald-500/20' }; case 'xml': case 'yaml': case 'yml': return { icon: FileCode, color: 'text-purple-500 dark:text-purple-400', bgColor: 'bg-gradient-to-br from-purple-500/20 to-purple-600/10 border border-purple-500/20' }; // Image files case 'jpg': case 'jpeg': case 'png': case 'gif': case 'svg': case 'webp': return { icon: FileImage, color: 'text-pink-500 dark:text-pink-400', bgColor: 'bg-gradient-to-br from-pink-500/20 to-pink-600/10 border border-pink-500/20' }; // Document files case 'md': case 'mdx': return { icon: FileText, color: 'text-slate-500 dark:text-slate-400', bgColor: 'bg-gradient-to-br from-slate-500/20 to-slate-600/10 border border-slate-500/20' }; case 'txt': return { icon: FileText, color: 'text-zinc-500 dark:text-zinc-400', bgColor: 'bg-gradient-to-br from-zinc-500/20 to-zinc-600/10 border border-zinc-500/20' }; case 'pdf': return { icon: FileType, color: 'text-red-500 dark:text-red-400', bgColor: 'bg-gradient-to-br from-red-500/20 to-red-600/10 border border-red-500/20' }; // Media files case 'mp4': case 'avi': case 'mov': return { icon: FileVideo, color: 'text-indigo-500 dark:text-indigo-400', bgColor: 'bg-gradient-to-br from-indigo-500/20 to-indigo-600/10 border border-indigo-500/20' }; case 'mp3': case 'wav': case 'ogg': return { icon: FileAudio, color: 'text-teal-500 dark:text-teal-400', bgColor: 'bg-gradient-to-br from-teal-500/20 to-teal-600/10 border border-teal-500/20' }; // Archive files case 'zip': case 'tar': case 'gz': case 'rar': return { icon: FileArchive, color: 'text-amber-500 dark:text-amber-400', bgColor: 'bg-gradient-to-br from-amber-500/20 to-amber-600/10 border border-amber-500/20' }; // Default default: if (!ext || filename.includes('/')) { return { icon: FolderOpen, color: 'text-blue-500 dark:text-blue-400', bgColor: 'bg-gradient-to-br from-blue-500/20 to-blue-600/10 border border-blue-500/20' }; } return { icon: File, color: 'text-zinc-500 dark:text-zinc-400', bgColor: 'bg-gradient-to-br from-zinc-500/20 to-zinc-600/10 border border-zinc-500/20' }; } }; /** * Extract tool data from content using the new parser with backwards compatibility */ export function extractToolData(content: any): { toolResult: ParsedToolResult | null; arguments: Record; filePath: string | null; fileContent: string | null; command: string | null; url: string | null; query: string | null; } { const toolResult = parseToolResult(content); if (toolResult) { const args = toolResult.arguments || {}; return { toolResult, arguments: args, filePath: args.file_path || args.path || null, fileContent: args.file_contents || args.content || null, command: args.command || null, url: args.url || null, query: args.query || null, }; } // Fallback to legacy parsing if new format not detected return { toolResult: null, arguments: {}, filePath: null, fileContent: null, command: null, url: null, query: null, }; }