Spaces:
Running
Running
File size: 8,099 Bytes
67c7241 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
import React, { useMemo } from "react";
import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle, CircleDashed } from "lucide-react";
import { ToolViewProps } from "./types";
import { extractBrowserUrl, extractBrowserOperation, formatTimestamp, getToolTitle } from "./utils";
import { ApiMessageType } from '@/components/thread/types';
import { safeJsonParse } from '@/components/thread/utils';
import { cn } from "@/lib/utils";
export function BrowserToolView({
name = "browser-operation",
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true,
isStreaming = false,
project,
agentStatus = 'idle',
messages = [],
currentIndex = 0,
totalCalls = 1
}: ToolViewProps) {
const url = extractBrowserUrl(assistantContent);
const operation = extractBrowserOperation(name);
const toolTitle = getToolTitle(name);
// --- message_id Extraction Logic ---
let browserStateMessageId: string | undefined;
try {
// 1. Parse the top-level JSON
const topLevelParsed = safeJsonParse<{ content?: string }>(toolContent, {});
const innerContentString = topLevelParsed?.content;
if (innerContentString && typeof innerContentString === 'string') {
// 2. Extract the output='...' string using regex
const outputMatch = innerContentString.match(/\boutput='(.*?)'(?=\s*\))/);
const outputString = outputMatch ? outputMatch[1] : null;
if (outputString) {
// 3. Unescape the JSON string (basic unescaping for \n and \")
const unescapedOutput = outputString.replace(/\\n/g, '\n').replace(/\\"/g, '"');
// 4. Parse the unescaped JSON to get message_id
const finalParsedOutput = safeJsonParse<{ message_id?: string }>(unescapedOutput, {});
browserStateMessageId = finalParsedOutput?.message_id;
}
}
} catch (error) {
console.error("[BrowserToolView] Error parsing tool content for message_id:", error);
}
// Find the browser_state message and extract the screenshot
let screenshotBase64: string | null = null;
if (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 }>(browserStateMessage.content, {});
screenshotBase64 = browserStateContent?.screenshot_base64 || null;
}
}
// Check if we have a VNC preview URL from the project
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);
// Memoize the VNC iframe to prevent reconnections on re-renders
const vncIframe = useMemo(() => {
if (!vncPreviewUrl) return null;
console.log("[BrowserToolView] Creating memoized VNC iframe with URL:", vncPreviewUrl);
return (
<iframe
src={vncPreviewUrl}
title="Browser preview"
className="w-full h-full border-0 flex-1"
/>
);
}, [vncPreviewUrl]); // Only recreate if the URL changes
return (
<div className="flex flex-col h-full">
<div className="flex-1 p-4 overflow-auto">
<div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col">
<div className="bg-zinc-100 dark:bg-zinc-900 p-2 flex items-center justify-between border-b border-zinc-200 dark:border-zinc-800">
<div className="flex items-center">
<MonitorPlay className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" />
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">Browser Window</span>
</div>
{url && (
<div className="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[340px]">
{url}
</div>
)}
</div>
{/* Preview Logic */}
<div className="flex-1 flex items-stretch bg-black">
{isLastToolCall ? (
// Only show live sandbox or fallback to sandbox for the last tool call
isRunning && vncIframe ? (
// Use the memoized iframe for live preview
vncIframe
) : screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
<img
src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot"
className="max-w-full max-h-full object-contain"
/>
</div>
) : vncIframe ? (
// Use the memoized iframe
vncIframe
) : (
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
<p className="text-sm font-medium">Browser preview not available</p>
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="mt-3 flex items-center text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 hover:underline"
>
Visit URL <ExternalLink className="h-3 w-3 ml-1" />
</a>
)}
</div>
)
) : (
// For non-last tool calls, only show screenshot if available, otherwise show "No Browser State image found"
screenshotBase64 ? (
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
<img
src={`data:image/jpeg;base64,${screenshotBase64}`}
alt="Browser Screenshot"
className="max-w-full max-h-full object-contain"
/>
</div>
) : (
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400">
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
<p className="text-sm font-medium">No Browser State image found</p>
</div>
)
)}
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800">
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400">
{!isRunning && (
<div className="flex items-center gap-2">
{isSuccess ? (
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" />
) : (
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
)}
<span>
{isSuccess ? `${operation} completed successfully` : `${operation} failed`}
</span>
</div>
)}
{isRunning && (
<div className="flex items-center gap-2">
<CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" />
<span>Executing browser action...</span>
</div>
)}
<div className="text-xs">
{toolTimestamp && !isRunning
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div>
</div>
</div>
</div>
);
} |