"use client"; import { useState, useEffect, useRef } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { File, Folder, FolderOpen, Upload, Download, ChevronRight, Home, ArrowLeft, Save } from "lucide-react"; import { listSandboxFiles, getSandboxFileContent, type FileInfo } from "@/lib/api"; import { toast } from "sonner"; import { createClient } from "@/lib/supabase/client"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { FileRenderer } from "@/components/file-renderers"; // Define API_URL const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; interface FileViewerModalProps { open: boolean; onOpenChange: (open: boolean) => void; sandboxId: string; } export function FileViewerModal({ open, onOpenChange, sandboxId }: FileViewerModalProps) { const [workspaceFiles, setWorkspaceFiles] = useState([]); const [isLoadingFiles, setIsLoadingFiles] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileContent, setFileContent] = useState(null); const [binaryFileUrl, setBinaryFileUrl] = useState(null); const [fileType, setFileType] = useState<'text' | 'image' | 'pdf' | 'binary'>('text'); const [isLoadingContent, setIsLoadingContent] = useState(false); const fileInputRef = useRef(null); // Navigation state const [currentPath, setCurrentPath] = useState("/workspace"); const [pathHistory, setPathHistory] = useState(["/workspace"]); const [historyIndex, setHistoryIndex] = useState(0); // Load files when the modal opens or sandbox ID changes useEffect(() => { if (open && sandboxId) { loadFilesAtPath(currentPath); } }, [open, sandboxId, currentPath]); // Function to load files from a specific path const loadFilesAtPath = async (path: string) => { if (!sandboxId) return; setIsLoadingFiles(true); try { const files = await listSandboxFiles(sandboxId, path); setWorkspaceFiles(files); } catch (error) { console.error(`Failed to load files at ${path}:`, error); toast.error("Failed to load files"); } finally { setIsLoadingFiles(false); } }; // Navigate to a folder const navigateToFolder = (folderPath: string) => { // Update current path setCurrentPath(folderPath); // Add to navigation history, discarding any forward history if we're not at the end if (historyIndex < pathHistory.length - 1) { setPathHistory(prevHistory => [...prevHistory.slice(0, historyIndex + 1), folderPath]); setHistoryIndex(historyIndex + 1); } else { setPathHistory(prevHistory => [...prevHistory, folderPath]); setHistoryIndex(pathHistory.length); } // Reset file selection and content setSelectedFile(null); setFileContent(null); setBinaryFileUrl(null); }; // Go back in history const goBack = () => { if (historyIndex > 0) { setHistoryIndex(historyIndex - 1); setCurrentPath(pathHistory[historyIndex - 1]); } }; // Go to home directory const goHome = () => { setCurrentPath("/workspace"); // Reset file selection and content setSelectedFile(null); setFileContent(null); setBinaryFileUrl(null); }; // Determine file type based on extension const getFileType = (filename: string): 'text' | 'image' | 'pdf' | 'binary' => { const extension = filename.split('.').pop()?.toLowerCase() || ''; const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp']; if (imageExtensions.includes(extension)) { return 'image'; } if (extension === 'pdf') { return 'pdf'; } const textExtensions = [ 'txt', 'md', 'js', 'jsx', 'ts', 'tsx', 'html', 'css', 'json', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'php', 'rb', 'go', 'rs', 'sh', 'yml', 'yaml', 'toml', 'xml', 'csv', 'sql' ]; if (textExtensions.includes(extension)) { return 'text'; } return 'binary'; }; // Handle file or folder click const handleFileClick = async (file: FileInfo) => { if (file.is_dir) { // If it's a directory, navigate to it navigateToFolder(file.path); return; } // Otherwise handle as regular file setSelectedFile(file.path); setIsLoadingContent(true); setFileContent(null); setBinaryFileUrl(null); try { // Determine file type based on extension const fileType = getFileType(file.path); setFileType(fileType); const content = await getSandboxFileContent(sandboxId, file.path); // Force certain file types to be treated as text if (fileType === 'text') { // For text files (including markdown), always try to render as text if (typeof content === 'string') { setFileContent(content); } else if (content instanceof Blob) { // If we got a Blob for a text file, convert it to text const text = await content.text(); setFileContent(text); } } else if (fileType === 'image' || fileType === 'pdf') { // For images and PDFs, create a blob URL to render them if (content instanceof Blob) { const url = URL.createObjectURL(content); setBinaryFileUrl(url); } else if (typeof content === 'string') { try { // For base64 content or binary text, create a blob const blob = new Blob([content]); const url = URL.createObjectURL(blob); setBinaryFileUrl(url); } catch (e) { console.error("Failed to create blob URL:", e); setFileType('text'); setFileContent(content); } } } else { // For other binary files if (content instanceof Blob) { const url = URL.createObjectURL(content); setBinaryFileUrl(url); } else if (typeof content === 'string') { setFileContent("[Binary file]"); try { const blob = new Blob([content]); const url = URL.createObjectURL(blob); setBinaryFileUrl(url); } catch (e) { console.error("Failed to create blob URL:", e); setFileContent(content); } } } } catch (error) { console.error("Failed to load file content:", error); toast.error("Failed to load file content"); setFileContent(null); setBinaryFileUrl(null); } finally { setIsLoadingContent(false); } }; // Clean up blob URLs on unmount or when they're no longer needed useEffect(() => { return () => { if (binaryFileUrl) { URL.revokeObjectURL(binaryFileUrl); } }; }, [binaryFileUrl]); // Handle file upload const handleFileUpload = () => { if (fileInputRef.current) { fileInputRef.current.click(); } }; // Process the file upload - upload to current directory const processFileUpload = async (event: React.ChangeEvent) => { if (!sandboxId || !event.target.files || event.target.files.length === 0) return; try { setIsLoadingFiles(true); const file = event.target.files[0]; if (file.size > 50 * 1024 * 1024) { // 50MB limit toast.error("File size exceeds 50MB limit"); return; } // Create a FormData object const formData = new FormData(); formData.append('file', file); formData.append('path', `${currentPath}/${file.name}`); const supabase = createClient(); const { data: { session } } = await supabase.auth.getSession(); if (!session?.access_token) { throw new Error('No access token available'); } // Upload using FormData - no need for any encoding/decoding const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { method: 'POST', headers: { 'Authorization': `Bearer ${session.access_token}`, // Important: Do NOT set Content-Type header here, let the browser set it with the boundary }, body: formData }); if (!response.ok) { throw new Error(`Upload failed: ${response.statusText}`); } toast.success(`File uploaded: ${file.name}`); // Refresh file list for current path loadFilesAtPath(currentPath); } catch (error) { console.error("File upload failed:", error); toast.error(typeof error === 'string' ? error : (error instanceof Error ? error.message : "Failed to upload file")); } finally { setIsLoadingFiles(false); // Reset the input event.target.value = ''; } }; // Render breadcrumb navigation const renderBreadcrumbs = () => { const parts = currentPath.split('/').filter(Boolean); const isInWorkspace = parts[0] === 'workspace'; const pathParts = isInWorkspace ? parts.slice(1) : parts; return (
{pathParts.map((part, index) => { const pathUpToHere = isInWorkspace ? `/workspace/${pathParts.slice(0, index + 1).join('/')}` : `/${pathParts.slice(0, index + 1).join('/')}`; return (
); })} {/* Show selected file name in breadcrumb */} {selectedFile && (
{selectedFile.split('/').pop()}
)}
); }; // Function to download file content const handleDownload = async () => { if (!selectedFile) return; try { let content: string | Blob; let filename = selectedFile.split('/').pop() || 'download'; if (fileType === 'text' && fileContent) { // For text files, use the text content content = new Blob([fileContent], { type: 'text/plain' }); } else if (binaryFileUrl) { // For binary files, fetch the content from the URL const response = await fetch(binaryFileUrl); content = await response.blob(); } else { throw new Error('No content available for download'); } // Create download link const url = URL.createObjectURL(content); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success('File downloaded successfully'); } catch (error) { console.error('Download failed:', error); toast.error('Failed to download file'); } }; return ( Workspace Files
{/* File browser sidebar */}
{/* Breadcrumb navigation */}
{renderBreadcrumbs()}
{/* File tree */}
{isLoadingFiles ? (
{[1, 2, 3, 4, 5].map((i) => ( ))}
) : workspaceFiles.length === 0 ? (

This folder is empty

Upload files or create new ones to get started

) : ( workspaceFiles.map((file, index) => (
{/* Show download button on hover for files */} {!file.is_dir && ( )}
)) )}
{/* Navigation controls */}
{/* File content pane */}
{/* File content */}
{isLoadingContent ? (
{[1, 2, 3, 4, 5].map((i) => ( ))}
) : !selectedFile ? (

Select a file to view its contents

Choose a file from the sidebar to preview or edit

) : ( )}
); }