memoized iframe

This commit is contained in:
marko-kraemer 2025-04-21 13:52:06 +01:00
parent b7105e71d3
commit 6f90c67bda
2 changed files with 68 additions and 258 deletions

View File

@ -1,11 +1,11 @@
'use client';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import {
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause, MonitorPlay, ExternalLink
} from 'lucide-react';
import { getMessages, getProject, getThread, Project, Message as BaseApiMessageType } from '@/lib/api';
import { toast } from 'sonner';
@ -1090,6 +1090,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
}
}, [projectName]);
// In the useEffect where streamingTextContent is handled
useEffect(() => {
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
// Find the last assistant message to update with streaming content
@ -1126,256 +1127,56 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
}
}, [streamingTextContent, streamHookStatus, messages, isStreamingText, isPlaying, currentMessageIndex, streamText]);
// Create a message-to-tool-index map for faster lookups
const [messageToToolIndex, setMessageToToolIndex] = useState<Record<string, number>>({});
// Build the message-to-tool-index map when tool calls change
// Create a memoized VNC iframe component to prevent reconnections
const memoizedVncIframe = useMemo(() => {
if (!vncPreviewUrl) return null;
return (
<iframe
src={vncPreviewUrl}
title="Browser preview"
className="w-full h-full border-0 flex-1"
/>
);
}, [vncPreviewUrl]);
useEffect(() => {
if (!toolCalls.length) return;
const mapBuilder: Record<string, number> = {};
toolCalls.forEach((tool, index) => {
const content = tool.assistantCall?.content || '';
const match = content.match(/<!-- messageId:([\w-]+) -->/);
if (match && match[1]) {
mapBuilder[match[1]] = index;
console.log(`Mapped message ID ${match[1]} to tool index ${index}`);
}
});
setMessageToToolIndex(mapBuilder);
}, [toolCalls]);
// Very direct approach to update the tool index during message playback
useEffect(() => {
if (!isPlaying || currentMessageIndex <= 0 || !messages.length) return;
// Check if current message is a tool message
const currentMsg = messages[currentMessageIndex - 1]; // Look at previous message that just played
if (currentMsg?.type === 'tool' && currentMsg.metadata) {
try {
const metadata = safeJsonParse<ParsedMetadata>(currentMsg.metadata, {});
const assistantId = metadata.assistant_message_id;
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
// Find the last assistant message to update with streaming content
const lastAssistantIndex = messages.findIndex(m =>
m.type === 'assistant' && m.message_id === messages[currentMessageIndex]?.message_id);
if (lastAssistantIndex >= 0) {
const assistantMessage = {...messages[lastAssistantIndex]};
assistantMessage.content = streamingTextContent;
if (assistantId && messageToToolIndex[assistantId] !== undefined) {
const toolIndex = messageToToolIndex[assistantId];
console.log(`Direct mapping: Setting tool index to ${toolIndex} for message ${assistantId}`);
setCurrentToolIndex(toolIndex);
}
} catch (e) {
console.error('Error in direct tool mapping:', e);
}
}
}, [currentMessageIndex, isPlaying, messages, messageToToolIndex]);
// Add a helper function to extract tool calls from message content
const extractToolCallsFromMessage = (content: string) => {
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
const results = [];
let match;
while ((match = toolCallRegex.exec(content)) !== null) {
const toolName = match[1] || match[2];
results.push({
name: toolName,
fullMatch: match[0]
});
}
return results;
};
// Force an explicit update to the tool panel based on the current message index
useEffect(() => {
// Skip if not playing or no messages
if (!isPlaying || messages.length === 0 || currentMessageIndex <= 0) return;
// Get all messages up to the current index
const currentMessages = messages.slice(0, currentMessageIndex);
// Find the most recent tool message to determine which panel to show
for (let i = currentMessages.length - 1; i >= 0; i--) {
const msg = currentMessages[i];
if (msg.type === 'tool' && msg.metadata) {
try {
const metadata = safeJsonParse<ParsedMetadata>(msg.metadata, {});
const assistantId = metadata.assistant_message_id;
if (assistantId) {
console.log(`Looking for tool panel for assistant message ${assistantId}`);
// Scan for matching tool call
for (let j = 0; j < toolCalls.length; j++) {
const content = toolCalls[j].assistantCall?.content || '';
if (content.includes(assistantId)) {
console.log(`Found matching tool call at index ${j}, updating panel`);
setCurrentToolIndex(j);
return;
// Update the message in the messages array
const updatedMessages = [...messages];
updatedMessages[lastAssistantIndex] = assistantMessage;
// Only show the streaming message if we're not already streaming and we're in play mode
if (!isStreamingText && isPlaying) {
const cleanup = streamText(streamingTextContent, () => {
// When streaming completes, update the visible messages
setVisibleMessages(prev => {
const messageExists = prev.some(m => m.message_id === assistantMessage.message_id);
if (messageExists) {
// Replace the existing message
return prev.map(m => m.message_id === assistantMessage.message_id ? assistantMessage : m);
} else {
// Add as a new message
return [...prev, assistantMessage];
}
}
}
} catch (e) {
console.error('Error parsing tool message metadata:', e);
});
});
return cleanup;
}
}
}
}, [currentMessageIndex, isPlaying, messages, toolCalls]);
}, [streamingTextContent, streamHookStatus, messages, isStreamingText, isPlaying, currentMessageIndex, streamText]);
// Add a special button to each tool call to show its debug info
// This replaces the existing ToolCallSidePanel component with a wrapper that adds debug info
const ToolCallPanelWithDebugInfo = React.useMemo(() => {
const WrappedPanel = (props: any) => {
const { isOpen, onClose, toolCalls, currentIndex, onNavigate, ...rest } = props;
// Add a function to show debug info for the current tool call
const showDebugInfo = useCallback(() => {
if (toolCalls && toolCalls.length > 0 && currentIndex >= 0 && currentIndex < toolCalls.length) {
const tool = toolCalls[currentIndex];
console.log('Current tool call debug info:', {
name: tool.assistantCall?.name,
content: tool.assistantCall?.content,
messageIdMatches: tool.assistantCall?.content?.match(/<!-- messageId:([\w-]+) -->/),
toolResult: tool.toolResult
});
}
}, [toolCalls, currentIndex]);
return (
<div>
<ToolCallSidePanel
isOpen={isOpen}
onClose={onClose}
toolCalls={toolCalls}
currentIndex={currentIndex}
onNavigate={onNavigate}
{...rest}
/>
{/* Add debug button */}
{isOpen && toolCalls && toolCalls.length > 0 && (
<div className="fixed bottom-4 right-4 z-50">
<Button
variant="outline"
size="sm"
className="h-8 w-8 rounded-full p-0 bg-background/50 backdrop-blur"
onClick={showDebugInfo}
>
<span className="sr-only">Debug</span>
<Info className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
};
return WrappedPanel;
}, []);
if (isLoading && !initialLoadCompleted.current) {
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
{/* Skeleton Header */}
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center gap-4 px-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full" />
<Skeleton className="h-5 w-40" />
</div>
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</div>
{/* Skeleton Chat Messages */}
<div className="flex-1 overflow-y-auto px-6 py-4 pb-[5.5rem]">
<div className="mx-auto max-w-3xl space-y-6">
{/* User message */}
<div className="flex justify-end">
<div className="max-w-[85%] rounded-lg bg-primary/10 px-4 py-3">
<div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div>
</div>
{/* Assistant response with tool usage */}
<div>
<div className="flex items-start gap-3">
<Skeleton className="flex-shrink-0 w-5 h-5 mt-2 rounded-full" />
<div className="flex-1 space-y-2">
<div className="max-w-[90%] w-full rounded-lg bg-muted px-4 py-3">
<div className="space-y-3">
<div>
<Skeleton className="h-4 w-full max-w-[360px] mb-2" />
<Skeleton className="h-4 w-full max-w-[320px] mb-2" />
<Skeleton className="h-4 w-full max-w-[290px]" />
</div>
{/* Tool call button skeleton */}
<div className="py-1">
<Skeleton className="h-6 w-32 rounded-md" />
</div>
<div>
<Skeleton className="h-4 w-full max-w-[340px] mb-2" />
<Skeleton className="h-4 w-full max-w-[280px]" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Skeleton Side Panel (closed state) */}
<div className={`hidden sm:block ${isSidePanelOpen ? 'block' : ''}`}>
<div className="h-screen w-[450px] border-l">
<div className="p-4">
<Skeleton className="h-8 w-32 mb-4" />
<Skeleton className="h-20 w-full rounded-md mb-4" />
<Skeleton className="h-40 w-full rounded-md" />
</div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-screen">
<div className={`flex flex-col flex-1 overflow-hidden transition-all duration-200 ease-in-out ${isSidePanelOpen ? 'mr-[90%] sm:mr-[450px] md:mr-[500px] lg:mr-[550px] xl:mr-[650px]' : ''}`}>
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center gap-4 px-4">
<div className="flex-1">
<span className="text-foreground font-medium">Shared Conversation</span>
</div>
</div>
</div>
<div className="flex flex-1 items-center justify-center p-4">
<div className="flex w-full max-w-md flex-col items-center gap-4 rounded-lg border bg-card p-6 text-center">
<h2 className="text-lg font-semibold text-destructive">Error</h2>
<p className="text-sm text-muted-foreground">{error}</p>
<Button variant="outline" onClick={() => router.push(`/`)}>
Back to Home
</Button>
</div>
</div>
</div>
</div>
);
}
// ... rest of the existing code ...
return (
<div className="flex h-screen">

View File

@ -1,4 +1,4 @@
import React from "react";
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";
@ -72,6 +72,21 @@ export function BrowserToolView({
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">
@ -92,12 +107,9 @@ export function BrowserToolView({
<div className="flex-1 flex items-stretch bg-black">
{isLastToolCall ? (
// Only show live sandbox or fallback to sandbox for the last tool call
isRunning && vncPreviewUrl ? (
<iframe
src={vncPreviewUrl}
title="Browser preview (Live)"
className="w-full h-full border-0 flex-1"
/>
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
@ -106,12 +118,9 @@ export function BrowserToolView({
className="max-w-full max-h-full object-contain"
/>
</div>
) : vncPreviewUrl ? (
<iframe
src={vncPreviewUrl}
title="Browser preview"
className="w-full h-full border-0 flex-1"
/>
) : 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" />