From 4cc8bd173ed023602b289f4e9c77d78709dfb792 Mon Sep 17 00:00:00 2001 From: Vukasin Date: Tue, 27 May 2025 19:26:49 +0200 Subject: [PATCH 1/3] fix: add vnc preload --- .../(dashboard)/agents/[threadId]/page.tsx | 7 +- frontend/src/app/share/[threadId]/page.tsx | 16 +- frontend/src/hooks/useVncPreloader.ts | 140 ++++++++++++++++++ 3 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 frontend/src/hooks/useVncPreloader.ts diff --git a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx index 5bae5938..2b740be4 100644 --- a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx @@ -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 diff --git a/frontend/src/app/share/[threadId]/page.tsx b/frontend/src/app/share/[threadId]/page.tsx index 7ecf6caf..e6a476de 100644 --- a/frontend/src/app/share/[threadId]/page.tsx +++ b/frontend/src/app/share/[threadId]/page.tsx @@ -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; }); diff --git a/frontend/src/hooks/useVncPreloader.ts b/frontend/src/hooks/useVncPreloader.ts new file mode 100644 index 00000000..4f21e8a8 --- /dev/null +++ b/frontend/src/hooks/useVncPreloader.ts @@ -0,0 +1,140 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { Project } from '@/lib/api'; + +export function useVncPreloader(project: Project | null) { + const preloadedIframeRef = useRef(null); + const isPreloadedRef = useRef(false); + const retryTimeoutRef = useRef(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 + }; +} \ No newline at end of file From 27ef3a59563700364f9f26916f3a33492a7561a8 Mon Sep 17 00:00:00 2001 From: Vukasin Date: Tue, 27 May 2025 22:12:19 +0200 Subject: [PATCH 2/3] wip: file scroll and fixes --- .../[projectId]/thread/[threadId]/page.tsx | 6 +- .../thread/_components/ThreadLayout.tsx | 14 +- frontend/src/app/share/[threadId]/page.tsx | 2 +- .../components/thread/attachment-group.tsx | 8 +- .../thread/content/ThreadContent.tsx | 28 ++-- .../src/components/thread/file-attachment.tsx | 2 +- .../components/thread/file-viewer-modal.tsx | 107 +++++++++++++- .../thread/tool-views/AskToolView.tsx | 50 +++---- .../thread/tool-views/CompleteToolView.tsx | 20 +-- .../tool-views/FileOperationToolView.tsx | 82 +++++------ .../thread/tool-views/WebSearchToolView.tsx | 59 +++++--- .../src/components/thread/tool-views/utils.ts | 136 ++++++++++++------ frontend/src/components/ui/code-block.tsx | 17 ++- 13 files changed, 362 insertions(+), 169 deletions(-) diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx index f8aaa90e..422bf3ea 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx @@ -45,6 +45,7 @@ export default function ThreadPage({ const [isSending, setIsSending] = useState(false); const [fileViewerOpen, setFileViewerOpen] = useState(false); const [fileToView, setFileToView] = useState(null); + const [filePathList, setFilePathList] = useState(undefined); const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const [debugMode, setDebugMode] = useState(false); const [initialPanelOpenAttempted, setInitialPanelOpenAttempted] = useState(false); @@ -333,12 +334,13 @@ export default function ThreadPage({ } }, [stopStreaming, agentRunId, stopAgentMutation, agentRunsQuery, setAgentStatus]); - const handleOpenFileViewer = useCallback((filePath?: string) => { + const handleOpenFileViewer = useCallback((filePath?: string, filePathList?: string[]) => { if (filePath) { setFileToView(filePath); } else { setFileToView(null); } + setFilePathList(filePathList); setFileViewerOpen(true); }, []); @@ -527,6 +529,7 @@ export default function ThreadPage({ fileViewerOpen={fileViewerOpen} setFileViewerOpen={setFileViewerOpen} fileToView={fileToView} + filePathList={filePathList} toolCalls={toolCalls} messages={messages as ApiMessageType[]} externalNavIndex={externalNavIndex} @@ -568,6 +571,7 @@ export default function ThreadPage({ fileViewerOpen={fileViewerOpen} setFileViewerOpen={setFileViewerOpen} fileToView={fileToView} + filePathList={filePathList} toolCalls={toolCalls} messages={messages as ApiMessageType[]} externalNavIndex={externalNavIndex} diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/ThreadLayout.tsx b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/ThreadLayout.tsx index ae61e78d..b891a1a0 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/ThreadLayout.tsx +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/_components/ThreadLayout.tsx @@ -17,10 +17,11 @@ interface ThreadLayoutProps { isSidePanelOpen: boolean; onToggleSidePanel: () => void; onProjectRenamed?: (newName: string) => void; - onViewFiles: (filePath?: string) => void; + onViewFiles: (filePath?: string, filePathList?: string[]) => void; fileViewerOpen: boolean; setFileViewerOpen: (open: boolean) => void; fileToView: string | null; + filePathList?: string[]; toolCalls: ToolCallInput[]; messages: ApiMessageType[]; externalNavIndex?: number; @@ -53,6 +54,7 @@ export function ThreadLayout({ fileViewerOpen, setFileViewerOpen, fileToView, + filePathList, toolCalls, messages, externalNavIndex, @@ -79,11 +81,10 @@ export function ThreadLayout({ )}
)} diff --git a/frontend/src/app/share/[threadId]/page.tsx b/frontend/src/app/share/[threadId]/page.tsx index e6a476de..6e816c23 100644 --- a/frontend/src/app/share/[threadId]/page.tsx +++ b/frontend/src/app/share/[threadId]/page.tsx @@ -616,7 +616,7 @@ export default function ThreadPage({ [messages, toolCalls], ); - const handleOpenFileViewer = useCallback((filePath?: string) => { + const handleOpenFileViewer = useCallback((filePath?: string, filePathList?: string[]) => { if (filePath) { setFileToView(filePath); } else { diff --git a/frontend/src/components/thread/attachment-group.tsx b/frontend/src/components/thread/attachment-group.tsx index f28fb716..9ceb17c6 100644 --- a/frontend/src/components/thread/attachment-group.tsx +++ b/frontend/src/components/thread/attachment-group.tsx @@ -29,7 +29,7 @@ interface AttachmentGroupProps { onRemove?: (index: number) => void; layout?: LayoutStyle; className?: string; - onFileClick?: (path: string) => void; + onFileClick?: (path: string, filePathList?: string[]) => void; showPreviews?: boolean; maxHeight?: string; gridImageHeight?: number; // New prop for grid image height @@ -101,8 +101,10 @@ export function AttachmentGroup({ // Ensure path has proper format when clicking const handleFileClick = (path: string) => { if (onFileClick) { - // Just pass the path to the parent handler which will call getFileUrl - onFileClick(path); + // Create the file path list from all files in the group + const filePathList = uniqueFiles.map(file => getFilePath(file)); + // Pass both the clicked path and the complete list + onFileClick(path, filePathList); } }; diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index d3fea3a3..594cfad5 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -52,7 +52,7 @@ const HIDE_STREAMING_XML_TAGS = new Set([ ]); // Helper function to render attachments (keeping original implementation for now) -export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string) => void, sandboxId?: string, project?: Project) { +export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void, sandboxId?: string, project?: Project) { if (!attachments || attachments.length === 0) return null; // Note: Preloading is now handled by React Query in the main ThreadContent component @@ -72,7 +72,7 @@ export function renderMarkdownContent( content: string, handleToolClick: (assistantMessageId: string | null, toolName: string) => void, messageId: string | null, - fileViewerHandler?: (filePath?: string) => void, + fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void, sandboxId?: string, project?: Project, debugMode?: boolean @@ -165,7 +165,7 @@ export interface ThreadContentProps { streamingToolCall?: any; agentStatus: 'idle' | 'running' | 'connecting' | 'error'; handleToolClick: (assistantMessageId: string | null, toolName: string) => void; - handleOpenFileViewer: (filePath?: string) => void; + handleOpenFileViewer: (filePath?: string, filePathList?: string[]) => void; readOnly?: boolean; visibleMessages?: UnifiedMessage[]; // For playback mode streamingText?: string; // For playback mode @@ -282,7 +282,7 @@ export const ThreadContent: React.FC = ({ const groupedMessages: MessageGroup[] = []; let currentGroup: MessageGroup | null = null; let assistantGroupCounter = 0; // Counter for assistant groups - + displayMessages.forEach((message, index) => { const messageType = message.type; const key = message.message_id || `msg-${index}`; @@ -306,10 +306,10 @@ export const ThreadContent: React.FC = ({ } // Create a new assistant group with a group-level key assistantGroupCounter++; - currentGroup = { - type: 'assistant_group', - messages: [message], - key: `assistant-group-${assistantGroupCounter}` + currentGroup = { + type: 'assistant_group', + messages: [message], + key: `assistant-group-${assistantGroupCounter}` }; } } else if (messageType !== 'status') { @@ -320,20 +320,20 @@ export const ThreadContent: React.FC = ({ } } }); - + // Finalize any remaining group if (currentGroup) { groupedMessages.push(currentGroup); } - + // Handle streaming content - if(streamingTextContent) { + if (streamingTextContent) { const lastGroup = groupedMessages.at(-1); - if(!lastGroup || lastGroup.type === 'user'){ + if (!lastGroup || lastGroup.type === 'user') { // Create new assistant group for streaming content assistantGroupCounter++; groupedMessages.push({ - type: 'assistant_group', + type: 'assistant_group', messages: [{ content: streamingTextContent, type: 'assistant', @@ -344,7 +344,7 @@ export const ThreadContent: React.FC = ({ is_llm_message: true, thread_id: 'streamingTextContent', sequence: Infinity, - }], + }], key: `assistant-group-${assistantGroupCounter}-streaming` }); } else if (lastGroup.type === 'assistant_group') { diff --git a/frontend/src/components/thread/file-attachment.tsx b/frontend/src/components/thread/file-attachment.tsx index 4465a17a..a98f137f 100644 --- a/frontend/src/components/thread/file-attachment.tsx +++ b/frontend/src/components/thread/file-attachment.tsx @@ -521,7 +521,7 @@ export function FileAttachment({ interface FileAttachmentGridProps { attachments: string[]; - onFileClick?: (path: string) => void; + onFileClick?: (path: string, filePathList?: string[]) => void; className?: string; sandboxId?: string; showPreviews?: boolean; diff --git a/frontend/src/components/thread/file-viewer-modal.tsx b/frontend/src/components/thread/file-viewer-modal.tsx index b9c921a5..8a400b98 100644 --- a/frontend/src/components/thread/file-viewer-modal.tsx +++ b/frontend/src/components/thread/file-viewer-modal.tsx @@ -59,6 +59,7 @@ interface FileViewerModalProps { sandboxId: string; initialFilePath?: string | null; project?: Project; + filePathList?: string[]; } export function FileViewerModal({ @@ -67,6 +68,7 @@ export function FileViewerModal({ sandboxId, initialFilePath, project, + filePathList, }: FileViewerModalProps) { // Safely handle initialFilePath to ensure it's a string or null const safeInitialFilePath = typeof initialFilePath === 'string' ? initialFilePath : null; @@ -78,6 +80,10 @@ export function FileViewerModal({ const [currentPath, setCurrentPath] = useState('/workspace'); const [isInitialLoad, setIsInitialLoad] = useState(true); + // Add navigation state for file list mode + const [currentFileIndex, setCurrentFileIndex] = useState(-1); + const isFileListMode = Boolean(filePathList && filePathList.length > 0); + // Use React Query for directory listing const { data: files = [], @@ -376,6 +382,42 @@ export function FileViewerModal({ [sandboxId], ); + // Navigation functions for file list mode + const navigateToFileByIndex = useCallback((index: number) => { + if (!isFileListMode || !filePathList || index < 0 || index >= filePathList.length) { + return; + } + + const filePath = filePathList[index]; + setCurrentFileIndex(index); + + // Create a temporary FileInfo object for the file + const fileName = filePath.split('/').pop() || ''; + const normalizedPath = normalizePath(filePath); + + const fileInfo: FileInfo = { + name: fileName, + path: normalizedPath, + is_dir: false, + size: 0, + mod_time: new Date().toISOString(), + }; + + openFile(fileInfo); + }, [isFileListMode, filePathList, normalizePath, openFile]); + + const navigatePrevious = useCallback(() => { + if (currentFileIndex > 0) { + navigateToFileByIndex(currentFileIndex - 1); + } + }, [currentFileIndex, navigateToFileByIndex]); + + const navigateNext = useCallback(() => { + if (isFileListMode && filePathList && currentFileIndex < filePathList.length - 1) { + navigateToFileByIndex(currentFileIndex + 1); + } + }, [currentFileIndex, isFileListMode, filePathList, navigateToFileByIndex]); + // Handle initial file path - Runs ONLY ONCE on open if initialFilePath is provided useEffect(() => { // Only run if modal is open, initial path is provided, AND it hasn't been processed yet @@ -384,6 +426,19 @@ export function FileViewerModal({ `[FILE VIEWER] useEffect[initialFilePath]: Processing initial path: ${safeInitialFilePath}`, ); + // If we're in file list mode, find the index and navigate to it + if (isFileListMode && filePathList) { + const normalizedInitialPath = normalizePath(safeInitialFilePath); + const index = filePathList.findIndex(path => normalizePath(path) === normalizedInitialPath); + + if (index !== -1) { + console.log(`[FILE VIEWER] File list mode: navigating to index ${index} for ${normalizedInitialPath}`); + navigateToFileByIndex(index); + setInitialPathProcessed(true); + return; + } + } + // Normalize the initial path const fullPath = normalizePath(safeInitialFilePath); const lastSlashIndex = fullPath.lastIndexOf('/'); @@ -434,7 +489,7 @@ export function FileViewerModal({ ); setInitialPathProcessed(false); } - }, [open, safeInitialFilePath, initialPathProcessed, normalizePath, currentPath, openFile]); + }, [open, safeInitialFilePath, initialPathProcessed, normalizePath, currentPath, openFile, isFileListMode, filePathList, navigateToFileByIndex]); // Effect to handle cached file content updates useEffect(() => { @@ -505,6 +560,24 @@ export function FileViewerModal({ }; }, [blobUrlForRenderer, isDownloading]); + // Keyboard navigation + useEffect(() => { + if (!open || !isFileListMode) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') { + e.preventDefault(); + navigatePrevious(); + } else if (e.key === 'ArrowRight') { + e.preventDefault(); + navigateNext(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [open, isFileListMode, navigatePrevious, navigateNext]); + // Handle modal close const handleOpenChange = useCallback( (open: boolean) => { @@ -522,6 +595,7 @@ export function FileViewerModal({ // React Query will handle clearing the files data setInitialPathProcessed(false); setIsInitialLoad(true); + setCurrentFileIndex(-1); // Reset file index } onOpenChange(open); }, @@ -860,10 +934,39 @@ export function FileViewerModal({ return ( - + Workspace Files +
+ {/* Navigation arrows for file list mode */} + {isFileListMode && selectedFilePath && filePathList && ( + <> + +
+ {currentFileIndex + 1} / {filePathList.length} +
+ + + )}
{/* Navigation Bar */} diff --git a/frontend/src/components/thread/tool-views/AskToolView.tsx b/frontend/src/components/thread/tool-views/AskToolView.tsx index 7c2e1a0c..90d260fe 100644 --- a/frontend/src/components/thread/tool-views/AskToolView.tsx +++ b/frontend/src/components/thread/tool-views/AskToolView.tsx @@ -57,7 +57,7 @@ export function AskToolView({ if (assistantContent) { try { const contentStr = normalizeContentToString(assistantContent); - + // Extract attachments if present const attachmentsMatch = contentStr.match(/attachments=["']([^"']*)["']/i); if (attachmentsMatch) { @@ -92,13 +92,13 @@ export function AskToolView({
- + {!isStreaming && ( - @@ -129,12 +129,12 @@ export function AskToolView({ Files ({askData.attachments.length}) - +
4 ? "grid-cols-1 sm:grid-cols-2 md:grid-cols-3" : - "grid-cols-1 sm:grid-cols-2" + askData.attachments.length > 4 ? "grid-cols-1 sm:grid-cols-2 md:grid-cols-3" : + "grid-cols-1 sm:grid-cols-2" )}> {askData.attachments .sort((a, b) => { @@ -142,7 +142,7 @@ export function AskToolView({ const bIsImage = isImageFile(b); const aIsPreviewable = isPreviewableFile(a); const bIsPreviewable = isPreviewableFile(b); - + if (aIsImage && !bIsImage) return -1; if (!aIsImage && bIsImage) return 1; if (aIsPreviewable && !bIsPreviewable) return -1; @@ -152,10 +152,10 @@ export function AskToolView({ .map((attachment, index) => { const isImage = isImageFile(attachment); const isPreviewable = isPreviewableFile(attachment); - const shouldSpanFull = (askData.attachments!.length % 2 === 1 && - askData.attachments!.length > 1 && - index === askData.attachments!.length - 1); - + const shouldSpanFull = (askData.attachments!.length % 2 === 1 && + askData.attachments!.length > 1 && + index === askData.attachments!.length - 1); + return (
- + {assistantTimestamp && (
@@ -230,7 +230,7 @@ export function AskToolView({ User Interaction
- +
{assistantTimestamp ? formatTimestamp(assistantTimestamp) : ''}
diff --git a/frontend/src/components/thread/tool-views/CompleteToolView.tsx b/frontend/src/components/thread/tool-views/CompleteToolView.tsx index faff5278..26a4d5f0 100644 --- a/frontend/src/components/thread/tool-views/CompleteToolView.tsx +++ b/frontend/src/components/thread/tool-views/CompleteToolView.tsx @@ -54,7 +54,7 @@ export function CompleteToolView({ if (assistantContent) { try { const contentStr = normalizeContentToString(assistantContent); - + // Try to extract content from tag const completeMatch = contentStr.match(/]*>([^<]*)<\/complete>/); if (completeMatch) { @@ -88,7 +88,7 @@ export function CompleteToolView({ if (toolContent && !isStreaming) { try { const contentStr = normalizeContentToString(toolContent); - + // Try to extract from ToolResult pattern const toolResultMatch = contentStr.match(/ToolResult\([^)]*output=['"]([^'"]+)['"]/); if (toolResultMatch) { @@ -143,13 +143,13 @@ export function CompleteToolView({
- + {!isStreaming && ( - @@ -211,7 +211,7 @@ export function CompleteToolView({ const { icon: FileIcon, color, bgColor } = getFileIconAndColor(attachment); const fileName = attachment.split('/').pop() || attachment; const filePath = attachment.includes('/') ? attachment.substring(0, attachment.lastIndexOf('/')) : ''; - + return (