import React, { useState, useMemo, useCallback } from 'react'; import { FolderOpen, Folder, File, ChevronRight, ChevronDown, FileJson, FileText, FolderTree, Code2, Package, Loader2, ExternalLink, FileCode, Maximize2, Copy, Check, Globe, Play } from 'lucide-react'; import { ToolViewProps } from '../types'; import { getToolTitle, normalizeContentToString, extractToolData } from '../utils'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from "@/components/ui/scroll-area"; import { LoadingState } from '../shared/LoadingState'; import { Button } from '@/components/ui/button'; import { CheckCircle, AlertTriangle } from 'lucide-react'; import { CodeBlockCode } from '@/components/ui/code-block'; import { getLanguageFromFileName } from '../file-operation/_utils'; import { constructHtmlPreviewUrl } from '@/lib/utils/url'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { toast } from 'sonner'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; interface FileNode { name: string; path: string; type: 'file' | 'directory'; children?: FileNode[]; expanded?: boolean; } interface ProjectData { projectName: string; structure: FileNode; packageInfo?: string; rawStructure?: string; isNextJs?: boolean; isReact?: boolean; isVite?: boolean; } function parseFileStructure(flatList: string[]): FileNode { const root: FileNode = { name: '.', path: '.', type: 'directory', children: [], expanded: true }; const paths = flatList .map(line => line.trim()) .filter(line => line && line !== '.') .map(line => line.startsWith('./') ? line.substring(2) : line); const pathSet = new Set(paths); const directories = new Set(); paths.forEach(path => { const parts = path.split('/'); for (let i = 1; i < parts.length; i++) { const parentPath = parts.slice(0, i).join('/'); directories.add(parentPath); } }); const nodeMap = new Map(); nodeMap.set('.', root); paths.sort(); paths.forEach(path => { const parts = path.split('/'); let currentPath = ''; let parent = root; parts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; if (!nodeMap.has(currentPath)) { const isDirectory = directories.has(currentPath) || index < parts.length - 1; const node: FileNode = { name: part, path: currentPath, type: isDirectory ? 'directory' : 'file', children: isDirectory ? [] : undefined, expanded: false }; if (!parent.children) parent.children = []; parent.children.push(node); nodeMap.set(currentPath, node); } if (nodeMap.get(currentPath)?.type === 'directory') { parent = nodeMap.get(currentPath)!; } }); }); const sortChildren = (node: FileNode) => { if (node.children) { node.children.sort((a, b) => { if (a.type !== b.type) { return a.type === 'directory' ? -1 : 1; } return a.name.localeCompare(b.name); }); node.children.forEach(sortChildren); } }; sortChildren(root); return root; } function getFileIcon(fileName: string): React.ReactNode { const ext = fileName.split('.').pop()?.toLowerCase(); if (fileName === 'package.json') { return ; } switch (ext) { case 'ts': case 'tsx': return ; case 'js': case 'jsx': return ; case 'json': return ; case 'css': case 'scss': return ; case 'md': return ; case 'html': return ; default: return ; } } const FileTreeNode: React.FC<{ node: FileNode; level: number; onFileClick: (path: string) => void; selectedFile?: string; onToggle: (path: string) => void; }> = ({ node, level, onFileClick, selectedFile, onToggle }) => { const isSelected = selectedFile === node.path; const handleClick = () => { if (node.type === 'file') { onFileClick(node.path); } else { onToggle(node.path); } }; if (node.name === '.') { return ( <> {node.children?.map((child) => ( ))} ); } return ( <>
{node.type === 'directory' && ( node.expanded ? : )} {node.type === 'directory' ? ( node.expanded ? : ) : ( getFileIcon(node.name) )} {node.name}
{node.type === 'directory' && node.expanded && node.children && ( <> {node.children.map((child) => ( ))} )} ); }; const FileExplorer: React.FC<{ fileTree: FileNode | null; projectData: ProjectData | null; selectedFile?: string; expandedNodes: Set; onToggle: (path: string) => void; onFileClick: (path: string) => void; fileContent: string; loadingFile: boolean; previewUrl?: string; language: string; }> = ({ fileTree, projectData, selectedFile, expandedNodes, onToggle, onFileClick, fileContent, loadingFile, previewUrl, language }) => { const [isCopying, setIsCopying] = useState(false); const handleCopyCode = async () => { if (!fileContent) return; try { await navigator.clipboard.writeText(fileContent); setIsCopying(true); toast.success('Code copied to clipboard'); setTimeout(() => setIsCopying(false), 2000); } catch (err) { toast.error('Failed to copy code'); } }; return (
Explorer {projectData && ( {projectData.projectName} )}
{fileTree && ( )}
{selectedFile ? ( <>
{selectedFile}
{fileContent && ( )} {previewUrl && ( )}
{loadingFile ? (
) : fileContent ? (
) : (

Unable to load file content

)}
) : (

Select a file to view its contents

)}
); }; function extractProjectData(assistantContent: any, toolContent: any): ProjectData | null { const toolData = extractToolData(toolContent); let outputStr: string | null = null; let projectName: string | null = null; if (toolData.toolResult) { outputStr = toolData.toolResult.toolOutput; projectName = toolData.arguments?.project_name || null; } if (!outputStr) { outputStr = normalizeContentToString(toolContent) || normalizeContentToString(assistantContent); } if (!outputStr) return null; if (!projectName) { const projectNameMatch = outputStr.match(/Project structure for '([^']+)':/); projectName = projectNameMatch ? projectNameMatch[1] : 'Unknown Project'; } else { projectName = projectName as string; } const lines = outputStr.split('\n'); const structureLines: string[] = []; let packageInfo = ''; let inPackageInfo = false; for (const line of lines) { if (line.includes('Project structure for')) { continue; } if (line.includes('Package.json info:') || line.includes('📋 Package.json info:')) { inPackageInfo = true; continue; } if (line.includes('To run this project:')) { break; } if (!inPackageInfo && line.trim() && !line.includes('📁')) { structureLines.push(line); } if (inPackageInfo) { packageInfo += line + '\n'; } } const structure = parseFileStructure(structureLines); // Detect project type from package.json info const isNextJs = packageInfo.includes('"next"'); const isReact = packageInfo.includes('"react"') || packageInfo.includes('react-scripts'); const isVite = packageInfo.includes('"vite"'); return { projectName, structure, packageInfo: packageInfo.trim(), rawStructure: structureLines.join('\n'), isNextJs, isReact, isVite }; } // Helper function to construct preview URL function getProjectPreviewUrl(project: any, projectName: string): string | null { // Check if there's an exposed port for this project // This would typically come from the project's sandbox configuration // For now, we'll construct a URL based on common patterns if (project?.sandbox?.exposed_ports) { // Look for common dev server ports (3000, 5173, 8080, etc.) const commonPorts = [3000, 3001, 5173, 5174, 8080, 8000, 4200]; const exposedPort = project.sandbox.exposed_ports.find((p: any) => commonPorts.includes(p.port) ); if (exposedPort) { return exposedPort.url; } } // If sandbox has a base URL, construct preview URL if (project?.sandbox?.sandbox_url) { // Try common dev server ports return `${project.sandbox.sandbox_url}:3000`; } return null; } export function GetProjectStructureView({ name = 'get_project_structure', assistantContent, toolContent, assistantTimestamp, toolTimestamp, isSuccess = true, isStreaming = false, project, }: ToolViewProps) { const [selectedFile, setSelectedFile] = useState(); const [fileContent, setFileContent] = useState(''); const [loadingFile, setLoadingFile] = useState(false); const [expandedNodes, setExpandedNodes] = useState>(new Set(['.', 'src'])); const [isDialogOpen, setIsDialogOpen] = useState(false); const projectData = useMemo(() => extractProjectData(assistantContent, toolContent), [assistantContent, toolContent] ); const toolTitle = getToolTitle(name); const updateNodeExpanded = useCallback((node: FileNode): FileNode => { return { ...node, expanded: expandedNodes.has(node.path), children: node.children?.map(updateNodeExpanded) }; }, [expandedNodes]); const fileTree = useMemo(() => { if (!projectData) return null; return updateNodeExpanded(projectData.structure); }, [projectData, updateNodeExpanded]); const handleToggle = useCallback((path: string) => { setExpandedNodes(prev => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }, []); const handleFileClick = useCallback(async (filePath: string) => { if (!project?.sandbox?.sandbox_url || !projectData) return; setSelectedFile(filePath); setLoadingFile(true); setFileContent(''); try { const fileUrl = constructHtmlPreviewUrl( project.sandbox.sandbox_url, `${projectData.projectName}/${filePath}` ); if (fileUrl) { const response = await fetch(fileUrl); if (response.ok) { const content = await response.text(); setFileContent(content); } else { setFileContent('// Failed to load file content'); } } else { setFileContent('// Unable to construct file URL'); } } catch (error) { console.error('Error loading file:', error); setFileContent('// Error loading file content'); } finally { setLoadingFile(false); } }, [project, projectData]); const isHtmlFile = selectedFile?.endsWith('.html'); const previewUrl = isHtmlFile && project?.sandbox?.sandbox_url && projectData ? constructHtmlPreviewUrl( project.sandbox.sandbox_url, `${projectData.projectName}/${selectedFile}` ) : undefined; const language = selectedFile ? getLanguageFromFileName(selectedFile) : 'plaintext'; // Get the project preview URL if available const projectPreviewUrl = projectData ? getProjectPreviewUrl(project, projectData.projectName) : null; const handlePreview = () => { if (projectPreviewUrl) { window.open(projectPreviewUrl, '_blank'); } else { toast.info(

Application not running

For best performance, build and run in production mode:

{projectData?.isNextJs && ( <> npm run build npm run start )} {projectData?.isVite && ( <> npm run build npm run preview )} {projectData?.isReact && !projectData?.isVite && ( <> npm run build npx serve -s build -l 3000 )} {!projectData?.isNextJs && !projectData?.isReact && !projectData?.isVite && ( npm run dev )}

Then use the expose_port tool

, { duration: 8000 } ); } }; return ( <>
{toolTitle}

{projectPreviewUrl ? "Open running application" : "Build and start production server first" }

Expand full view

{!isStreaming && ( {isSuccess ? ( ) : ( )} {isSuccess ? 'Loaded' : 'Failed'} )}
{isStreaming ? ( ) : fileTree ? ( ) : (

No project structure available

)}
{projectData ? `Project Explorer - ${projectData.projectName}` : 'Project Explorer'} {projectData && ( )}
{fileTree && ( )}
); }