import React, { useMemo } from 'react'; import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle, CircleDashed, } from 'lucide-react'; import { ToolViewProps } from './types'; import { extractBrowserUrl, extractBrowserOperation, formatTimestamp, getToolTitle, extractToolData, } from './utils'; import { safeJsonParse } from '@/components/thread/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ImageLoader } from './shared/ImageLoader'; export function BrowserToolView({ name = 'browser-operation', assistantContent, toolContent, assistantTimestamp, toolTimestamp, isSuccess = true, isStreaming = false, project, agentStatus = 'idle', messages = [], currentIndex = 0, totalCalls = 1, }: ToolViewProps) { // Try to extract data using the new parser first const assistantToolData = extractToolData(assistantContent); const toolToolData = extractToolData(toolContent); let url: string | null = null; // Use data from the new format if available if (assistantToolData.toolResult) { url = assistantToolData.url; } else if (toolToolData.toolResult) { url = toolToolData.url; } // If not found in new format, fall back to legacy extraction if (!url) { url = extractBrowserUrl(assistantContent); } const operation = extractBrowserOperation(name); const toolTitle = getToolTitle(name); let browserStateMessageId: string | undefined; let screenshotUrl: string | null = null; let screenshotBase64: string | null = null; // Add loading states for images const [imageLoading, setImageLoading] = React.useState(true); const [imageError, setImageError] = React.useState(false); try { const topLevelParsed = safeJsonParse<{ content?: any }>(toolContent, {}); const innerContentString = topLevelParsed?.content || toolContent; if (innerContentString && typeof innerContentString === 'string') { const toolResultMatch = innerContentString.match(/ToolResult\([^)]*output='([\s\S]*?)'(?:\s*,|\s*\))/); if (toolResultMatch) { const outputString = toolResultMatch[1]; try { const cleanedOutput = outputString.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\u([0-9a-fA-F]{4})/g, (_match, grp) => String.fromCharCode(parseInt(grp, 16))); const outputJson = JSON.parse(cleanedOutput); if (outputJson.image_url) { screenshotUrl = outputJson.image_url; } if (outputJson.message_id) { browserStateMessageId = outputJson.message_id; } } catch (parseError) { } } if (!screenshotUrl) { const imageUrlMatch = innerContentString.match(/"image_url":\s*"([^"]+)"/); if (imageUrlMatch) { screenshotUrl = imageUrlMatch[1]; } } if (!browserStateMessageId) { const messageIdMatch = innerContentString.match(/"message_id":\s*"([^"]+)"/); if (messageIdMatch) { browserStateMessageId = messageIdMatch[1]; } } if (!browserStateMessageId && !screenshotUrl) { const outputMatch = innerContentString.match(/\boutput='(.*?)'(?=\s*\))/); const outputString = outputMatch ? outputMatch[1] : null; if (outputString) { const unescapedOutput = outputString .replace(/\\n/g, '\n') .replace(/\\"/g, '"'); const finalParsedOutput = safeJsonParse<{ message_id?: string; image_url?: string }>( unescapedOutput, {}, ); browserStateMessageId = finalParsedOutput?.message_id; screenshotUrl = finalParsedOutput?.image_url || null; } } } else if (innerContentString && typeof innerContentString === "object") { screenshotUrl = (() => { if (!innerContentString) return null; if (!("tool_execution" in innerContentString)) return null; if (!("result" in innerContentString.tool_execution)) return null; if (!("output" in innerContentString.tool_execution.result)) return null; if (!("image_url" in innerContentString.tool_execution.result.output)) return null; if (typeof innerContentString.tool_execution.result.output.image_url !== "string") return null; return innerContentString.tool_execution.result.output.image_url; })() } } catch (error) { } if (!screenshotUrl && !screenshotBase64 && browserStateMessageId && messages.length > 0) { const browserStateMessage = messages.find( (msg) => (msg.type as string) === 'browser_state' && msg.message_id === browserStateMessageId, ); if (browserStateMessage) { const browserStateContent = safeJsonParse<{ screenshot_base64?: string; image_url?: string; }>( browserStateMessage.content, {}, ); screenshotBase64 = browserStateContent?.screenshot_base64 || null; screenshotUrl = browserStateContent?.image_url || null; } } const vncPreviewUrl = project?.sandbox?.vnc_preview ? `${project.sandbox.vnc_preview}/vnc_lite.html?password=${project?.sandbox?.pass}&autoconnect=true&scale=local&width=1024&height=768` : undefined; const isRunning = isStreaming || agentStatus === 'running'; const isLastToolCall = currentIndex === totalCalls - 1; const vncIframe = useMemo(() => { if (!vncPreviewUrl) return null; return (