mirror of https://github.com/kortix-ai/suna.git
memoized iframe
This commit is contained in:
parent
b7105e71d3
commit
6f90c67bda
|
@ -1,11 +1,11 @@
|
||||||
'use client';
|
'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 Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause
|
ArrowDown, CircleDashed, Info, File, ChevronRight, Play, Pause, MonitorPlay, ExternalLink
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { getMessages, getProject, getThread, Project, Message as BaseApiMessageType } from '@/lib/api';
|
import { getMessages, getProject, getThread, Project, Message as BaseApiMessageType } from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
@ -1090,6 +1090,7 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
||||||
}
|
}
|
||||||
}, [projectName]);
|
}, [projectName]);
|
||||||
|
|
||||||
|
// In the useEffect where streamingTextContent is handled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
|
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
|
||||||
// Find the last assistant message to update with streaming content
|
// 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]);
|
}, [streamingTextContent, streamHookStatus, messages, isStreamingText, isPlaying, currentMessageIndex, streamText]);
|
||||||
|
|
||||||
// Create a message-to-tool-index map for faster lookups
|
// Create a memoized VNC iframe component to prevent reconnections
|
||||||
const [messageToToolIndex, setMessageToToolIndex] = useState<Record<string, number>>({});
|
const memoizedVncIframe = useMemo(() => {
|
||||||
|
if (!vncPreviewUrl) return null;
|
||||||
// Build the message-to-tool-index map when tool calls change
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={vncPreviewUrl}
|
||||||
|
title="Browser preview"
|
||||||
|
className="w-full h-full border-0 flex-1"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [vncPreviewUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!toolCalls.length) return;
|
if (streamingTextContent && streamHookStatus === 'streaming' && messages.length > 0) {
|
||||||
|
// Find the last assistant message to update with streaming content
|
||||||
const mapBuilder: Record<string, number> = {};
|
const lastAssistantIndex = messages.findIndex(m =>
|
||||||
|
m.type === 'assistant' && m.message_id === messages[currentMessageIndex]?.message_id);
|
||||||
toolCalls.forEach((tool, index) => {
|
|
||||||
const content = tool.assistantCall?.content || '';
|
if (lastAssistantIndex >= 0) {
|
||||||
const match = content.match(/<!-- messageId:([\w-]+) -->/);
|
const assistantMessage = {...messages[lastAssistantIndex]};
|
||||||
if (match && match[1]) {
|
assistantMessage.content = streamingTextContent;
|
||||||
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 (assistantId && messageToToolIndex[assistantId] !== undefined) {
|
// Update the message in the messages array
|
||||||
const toolIndex = messageToToolIndex[assistantId];
|
const updatedMessages = [...messages];
|
||||||
console.log(`Direct mapping: Setting tool index to ${toolIndex} for message ${assistantId}`);
|
updatedMessages[lastAssistantIndex] = assistantMessage;
|
||||||
setCurrentToolIndex(toolIndex);
|
|
||||||
}
|
// Only show the streaming message if we're not already streaming and we're in play mode
|
||||||
} catch (e) {
|
if (!isStreamingText && isPlaying) {
|
||||||
console.error('Error in direct tool mapping:', e);
|
const cleanup = streamText(streamingTextContent, () => {
|
||||||
}
|
// When streaming completes, update the visible messages
|
||||||
}
|
setVisibleMessages(prev => {
|
||||||
}, [currentMessageIndex, isPlaying, messages, messageToToolIndex]);
|
const messageExists = prev.some(m => m.message_id === assistantMessage.message_id);
|
||||||
|
if (messageExists) {
|
||||||
// Add a helper function to extract tool calls from message content
|
// Replace the existing message
|
||||||
const extractToolCallsFromMessage = (content: string) => {
|
return prev.map(m => m.message_id === assistantMessage.message_id ? assistantMessage : m);
|
||||||
const toolCallRegex = /<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
|
} else {
|
||||||
const results = [];
|
// Add as a new message
|
||||||
let match;
|
return [...prev, assistantMessage];
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
} 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
|
// ... rest of the existing code ...
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle, CircleDashed } from "lucide-react";
|
import { Globe, MonitorPlay, ExternalLink, CheckCircle, AlertTriangle, CircleDashed } from "lucide-react";
|
||||||
import { ToolViewProps } from "./types";
|
import { ToolViewProps } from "./types";
|
||||||
import { extractBrowserUrl, extractBrowserOperation, formatTimestamp, getToolTitle } from "./utils";
|
import { extractBrowserUrl, extractBrowserOperation, formatTimestamp, getToolTitle } from "./utils";
|
||||||
|
@ -72,6 +72,21 @@ export function BrowserToolView({
|
||||||
const isRunning = isStreaming || agentStatus === 'running';
|
const isRunning = isStreaming || agentStatus === 'running';
|
||||||
const isLastToolCall = currentIndex === (totalCalls - 1);
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 p-4 overflow-auto">
|
<div className="flex-1 p-4 overflow-auto">
|
||||||
|
@ -92,12 +107,9 @@ export function BrowserToolView({
|
||||||
<div className="flex-1 flex items-stretch bg-black">
|
<div className="flex-1 flex items-stretch bg-black">
|
||||||
{isLastToolCall ? (
|
{isLastToolCall ? (
|
||||||
// Only show live sandbox or fallback to sandbox for the last tool call
|
// Only show live sandbox or fallback to sandbox for the last tool call
|
||||||
isRunning && vncPreviewUrl ? (
|
isRunning && vncIframe ? (
|
||||||
<iframe
|
// Use the memoized iframe for live preview
|
||||||
src={vncPreviewUrl}
|
vncIframe
|
||||||
title="Browser preview (Live)"
|
|
||||||
className="w-full h-full border-0 flex-1"
|
|
||||||
/>
|
|
||||||
) : screenshotBase64 ? (
|
) : screenshotBase64 ? (
|
||||||
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
|
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
|
||||||
<img
|
<img
|
||||||
|
@ -106,12 +118,9 @@ export function BrowserToolView({
|
||||||
className="max-w-full max-h-full object-contain"
|
className="max-w-full max-h-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : vncPreviewUrl ? (
|
) : vncIframe ? (
|
||||||
<iframe
|
// Use the memoized iframe
|
||||||
src={vncPreviewUrl}
|
vncIframe
|
||||||
title="Browser preview"
|
|
||||||
className="w-full h-full border-0 flex-1"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<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" />
|
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
|
||||||
|
|
Loading…
Reference in New Issue