import React, { useRef, useState, useCallback } from 'react';
import Image from 'next/image';
import { ArrowDown, CircleDashed } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Markdown } from '@/components/ui/markdown';
import { UnifiedMessage, ParsedContent, ParsedMetadata } from '@/components/thread/types';
import { safeJsonParse } from '@/components/thread/utils';
import { FileAttachmentGrid } from '@/components/thread/file-attachment';
import { FileCache } from '@/hooks/use-cached-file';
import { useAuth } from '@/components/AuthProvider';
import { Project } from '@/lib/api';
// Define the set of tags whose raw XML should be hidden during streaming
const HIDE_STREAMING_XML_TAGS = new Set([
'execute-command',
'create-file',
'delete-file',
'full-file-rewrite',
'str-replace',
'browser-click-element',
'browser-close-tab',
'browser-drag-drop',
'browser-get-dropdown-options',
'browser-go-back',
'browser-input-text',
'browser-navigate-to',
'browser-scroll-down',
'browser-scroll-to-text',
'browser-scroll-up',
'browser-select-dropdown-option',
'browser-send-keys',
'browser-switch-tab',
'browser-wait',
'deploy',
'ask',
'complete',
'crawl-webpage',
'web-search'
]);
// Helper function to render attachments
export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string) => void, sandboxId?: string, project?: Project) {
if (!attachments || attachments.length === 0) return null;
// Preload attachments into cache if we have a sandboxId
if (sandboxId) {
// Check if we can access localStorage and if there's a valid auth session before trying to preload
let hasValidSession = false;
let token = null;
try {
const sessionData = localStorage.getItem('auth');
if (sessionData) {
const session = JSON.parse(sessionData);
token = session?.access_token;
hasValidSession = !!token;
}
} catch (err) {
// Silent catch - localStorage might be unavailable in some contexts
}
// Only attempt to preload if we have a valid session
if (hasValidSession && token) {
// Use setTimeout to do this asynchronously without blocking rendering
setTimeout(() => {
FileCache.preload(sandboxId, attachments, token);
}, 0);
}
}
return ;
}
// Render Markdown content while preserving XML tags that should be displayed as tool calls
export function renderMarkdownContent(
content: string,
handleToolClick: (assistantMessageId: string | null, toolName: string) => void,
messageId: string | null,
fileViewerHandler?: (filePath?: string) => void,
sandboxId?: string,
project?: Project
) {
const xmlRegex = /<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
let lastIndex = 0;
const contentParts: React.ReactNode[] = [];
let match;
// If no XML tags found, just return the full content as markdown
if (!content.match(xmlRegex)) {
return {content};
}
while ((match = xmlRegex.exec(content)) !== null) {
// Add text before the tag as markdown
if (match.index > lastIndex) {
const textBeforeTag = content.substring(lastIndex, match.index);
contentParts.push(
{textBeforeTag}
);
}
const rawXml = match[0];
const toolName = match[1] || match[2];
const toolCallKey = `tool-${match.index}`;
if (toolName === 'ask') {
// Extract attachments from the XML attributes
const attachmentsMatch = rawXml.match(/attachments=["']([^"']*)["']/i);
const attachments = attachmentsMatch
? attachmentsMatch[1].split(',').map(a => a.trim())
: [];
// Extract content from the ask tag
const contentMatch = rawXml.match(/]*>([\s\S]*?)<\/ask>/i);
const askContent = contentMatch ? contentMatch[1] : '';
// Render tag content with attachment UI (using the helper)
contentParts.push(
{cleanContent && (
{cleanContent}
)}
{/* Use the helper function to render user attachments */}
{renderAttachments(attachments as string[], handleOpenFileViewer, sandboxId, project)}
);
} else if (group.type === 'assistant_group') {
return (