fix: add vnc preload

This commit is contained in:
Vukasin 2025-05-27 19:26:49 +02:00
parent f203484bb8
commit 4cc8bd173e
3 changed files with 155 additions and 8 deletions

View File

@ -63,6 +63,7 @@ import { useBillingStatusQuery } from '@/hooks/react-query/threads/use-billing-s
import { useSubscription, isPlan } from '@/hooks/react-query/subscriptions/use-subscriptions';
import { SubscriptionStatus } from '@/components/thread/chat-input/_use-model-selection';
import { ParsedContent } from '@/components/thread/types';
import { useVncPreloader } from '@/hooks/useVncPreloader';
// Extend the base Message type with the expected database fields
interface ApiMessageType extends BaseApiMessageType {
@ -161,6 +162,8 @@ export default function ThreadPage({
? 'active'
: 'no_subscription';
// Preload VNC iframe as soon as project data is available
useVncPreloader(project);
const handleProjectRenamed = useCallback((newName: string) => {
setProjectName(newName);
@ -720,7 +723,7 @@ export default function ThreadPage({
return assistantMsg.content;
}
})();
// Try to extract tool name from content
const xmlMatch = assistantContent.match(
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
@ -762,7 +765,7 @@ export default function ThreadPage({
return resultMessage.content;
}
})();
// Check for ToolResult pattern first
if (toolResultContent && typeof toolResultContent === 'string') {
// Look for ToolResult(success=True/False) pattern

View File

@ -30,6 +30,7 @@ import { safeJsonParse } from '@/components/thread/utils';
import { useAgentStream } from '@/hooks/useAgentStream';
import { threadErrorCodeMessages } from '@/lib/constants/errorCodeMessages';
import { ThreadSkeleton } from '@/components/thread/content/ThreadSkeleton';
import { useVncPreloader } from '@/hooks/useVncPreloader';
// Extend the base Message type with the expected database fields
interface ApiMessageType extends BaseApiMessageType {
@ -101,6 +102,9 @@ export default function ThreadPage({
const userClosedPanelRef = useRef(false);
// Preload VNC iframe as soon as project data is available
useVncPreloader(project);
useEffect(() => {
userClosedPanelRef.current = true;
setIsSidePanelOpen(false);
@ -361,9 +365,9 @@ export default function ThreadPage({
const projectData = threadData?.project_id
? await getProject(threadData.project_id).catch((err) => {
console.warn('[SHARE] Could not load project data:', err);
return null;
})
console.warn('[SHARE] Could not load project data:', err);
return null;
})
: null;
if (isMounted) {
@ -423,7 +427,7 @@ export default function ThreadPage({
return assistantMsg.content;
}
})();
// Try to extract tool name from content
const xmlMatch = assistantContent.match(
/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>|<([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/,
@ -460,7 +464,7 @@ export default function ThreadPage({
return resultMessage.content;
}
})();
// Check for ToolResult pattern first
if (toolResultContent && typeof toolResultContent === 'string') {
// Look for ToolResult(success=True/False) pattern
@ -721,7 +725,7 @@ export default function ThreadPage({
(m) => m.message_id === assistantId && m.type === 'assistant'
);
if (!assistantMessage) return false;
// Check if this tool call matches
return tc.assistantCall?.content === assistantMessage.content;
});

View File

@ -0,0 +1,140 @@
import { useEffect, useRef, useCallback } from 'react';
import { Project } from '@/lib/api';
export function useVncPreloader(project: Project | null) {
const preloadedIframeRef = useRef<HTMLIFrameElement | null>(null);
const isPreloadedRef = useRef(false);
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const maxRetriesRef = useRef(0);
const isRetryingRef = useRef(false);
const startPreloading = useCallback((vncUrl: string) => {
// Prevent multiple simultaneous preload attempts
if (isRetryingRef.current || isPreloadedRef.current) {
return;
}
isRetryingRef.current = true;
console.log(`[VNC PRELOADER] Attempt ${maxRetriesRef.current + 1}/10 - Starting VNC preload:`, vncUrl);
// Create hidden iframe for preloading
const iframe = document.createElement('iframe');
iframe.src = vncUrl;
iframe.style.position = 'absolute';
iframe.style.left = '-9999px';
iframe.style.top = '-9999px';
iframe.style.width = '1024px';
iframe.style.height = '768px';
iframe.style.border = '0';
iframe.title = 'VNC Preloader';
// Set a timeout to detect if iframe fails to load (for 502 errors)
const loadTimeout = setTimeout(() => {
console.log('[VNC PRELOADER] Load timeout - VNC service likely not ready');
// Clean up current iframe
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
// Retry if we haven't exceeded max retries
if (maxRetriesRef.current < 10) {
maxRetriesRef.current++;
isRetryingRef.current = false;
// Exponential backoff: 2s, 3s, 4.5s, 6.75s, etc. (max 15s)
const delay = Math.min(2000 * Math.pow(1.5, maxRetriesRef.current - 1), 15000);
console.log(`[VNC PRELOADER] Retrying in ${delay}ms (attempt ${maxRetriesRef.current + 1}/10)`);
retryTimeoutRef.current = setTimeout(() => {
startPreloading(vncUrl);
}, delay);
} else {
console.log('[VNC PRELOADER] Max retries reached, giving up on preloading');
isRetryingRef.current = false;
}
}, 5000); // 5 second timeout
// Handle successful iframe load
iframe.onload = () => {
clearTimeout(loadTimeout);
console.log('[VNC PRELOADER] ✅ VNC iframe preloaded successfully!');
isPreloadedRef.current = true;
isRetryingRef.current = false;
preloadedIframeRef.current = iframe;
};
// Handle iframe load errors
iframe.onerror = () => {
clearTimeout(loadTimeout);
console.log('[VNC PRELOADER] VNC iframe failed to load (onerror)');
// Clean up current iframe
if (iframe.parentNode) {
iframe.parentNode.removeChild(iframe);
}
// Retry if we haven't exceeded max retries
if (maxRetriesRef.current < 10) {
maxRetriesRef.current++;
isRetryingRef.current = false;
const delay = Math.min(2000 * Math.pow(1.5, maxRetriesRef.current - 1), 15000);
console.log(`[VNC PRELOADER] Retrying in ${delay}ms (attempt ${maxRetriesRef.current + 1}/10)`);
retryTimeoutRef.current = setTimeout(() => {
startPreloading(vncUrl);
}, delay);
} else {
console.log('[VNC PRELOADER] Max retries reached, giving up on preloading');
isRetryingRef.current = false;
}
};
// Add to DOM to start loading
document.body.appendChild(iframe);
console.log('[VNC PRELOADER] VNC iframe added to DOM, waiting for load...');
}, []);
useEffect(() => {
// Only preload if we have project data with VNC info and haven't started preloading yet
if (!project?.sandbox?.vnc_preview || !project?.sandbox?.pass || isPreloadedRef.current || isRetryingRef.current) {
return;
}
const vncUrl = `${project.sandbox.vnc_preview}/vnc_lite.html?password=${project.sandbox.pass}&autoconnect=true&scale=local&width=1024&height=768`;
// Reset retry counter for new project
maxRetriesRef.current = 0;
isRetryingRef.current = false;
// Start the preloading process with a small delay to let the sandbox initialize
const initialDelay = setTimeout(() => {
startPreloading(vncUrl);
}, 1000); // 1 second initial delay
// Cleanup function
return () => {
clearTimeout(initialDelay);
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
if (preloadedIframeRef.current && preloadedIframeRef.current.parentNode) {
preloadedIframeRef.current.parentNode.removeChild(preloadedIframeRef.current);
preloadedIframeRef.current = null;
}
isPreloadedRef.current = false;
isRetryingRef.current = false;
maxRetriesRef.current = 0;
};
}, [project?.sandbox?.vnc_preview, project?.sandbox?.pass, startPreloading]);
return {
isPreloaded: isPreloadedRef.current,
preloadedIframe: preloadedIframeRef.current
};
}