import { AnimatePresence, motion } from 'framer-motion' import { Edit, File, Plus, Trash } from 'lucide-react' import { useEffect, useState } from 'react' import AIEditor from 'components/ui/AIEditor' import { toast } from 'sonner' import { Button, ContextMenu_Shadcn_, ContextMenuContent_Shadcn_, ContextMenuItem_Shadcn_, ContextMenuSeparator_Shadcn_, ContextMenuTrigger_Shadcn_, flattenTree, TreeView, TreeViewItem, } from 'ui' import { getLanguageFromFileName, isBinaryFile } from './FileExplorerAndEditor.utils' interface FileData { id: number name: string content: string selected?: boolean } interface FileExplorerAndEditorProps { files: FileData[] onFilesChange: (files: FileData[]) => void aiEndpoint?: string aiMetadata?: { projectRef?: string connectionString?: string | null orgSlug?: string } } const denoJsonDefaultContent = JSON.stringify({ imports: {} }, null, '\t') export const FileExplorerAndEditor = ({ files, onFilesChange, aiEndpoint, aiMetadata, }: FileExplorerAndEditorProps) => { const selectedFile = files.find((f) => f.selected) ?? files[0] const [isDragOver, setIsDragOver] = useState(false) const [treeData, setTreeData] = useState({ name: '', children: files.map((file) => ({ id: file.id.toString(), name: file.name, metadata: { isEditing: false, originalId: file.id, }, })), }) const handleChange = (value: string) => { const updatedFiles = files.map((file) => file.id === selectedFile.id ? { ...file, content: value } : file ) onFilesChange(updatedFiles) } const addNewFile = () => { const newId = Math.max(0, ...files.map((f) => f.id)) + 1 const updatedFiles = files.map((f) => ({ ...f, selected: false })) onFilesChange([ ...updatedFiles, { id: newId, name: `file${newId}.ts`, content: '', selected: true, }, ]) } const addDroppedFiles = async (droppedFiles: FileList) => { const newFiles: FileData[] = [] const updatedFiles = files.map((f) => ({ ...f, selected: false })) for (let i = 0; i < droppedFiles.length; i++) { const file = droppedFiles[i] const newId = Math.max(0, ...files.map((f) => f.id), ...newFiles.map((f) => f.id)) + 1 try { let content: string if (isBinaryFile(file.name)) { // For binary files, read as ArrayBuffer and convert to base64 or keep as binary data const arrayBuffer = await file.arrayBuffer() const bytes = new Uint8Array(arrayBuffer) content = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') } else { content = await file.text() } newFiles.push({ id: newId, name: file.name, content, selected: i === droppedFiles.length - 1, // Select the last dropped file }) } catch (error) { console.error(`Failed to read file ${file.name}:`, error) } } if (newFiles.length > 0) { onFilesChange([...updatedFiles, ...newFiles]) } } const handleStartRename = (id: number) => { // Force re-render of the TreeView with the updated metadata setTreeData({ name: '', children: files.map((file) => ({ id: file.id.toString(), name: file.name, metadata: { isEditing: file.id === id, originalId: file.id, }, })), }) } const exitEditMode = () => { // Force re-render of the TreeView with the updated metadata setTreeData({ name: '', children: files.map((file) => ({ id: file.id.toString(), name: file.name, metadata: { isEditing: false, originalId: file.id, }, })), }) } const handleFileNameChange = (id: number, newName: string) => { // Don't allow empty names if (!newName.trim()) { toast.error('File name cannot be empty') return exitEditMode() } // Check if the new name already exists in other files const isDuplicate = files.some((file) => file.id !== id && file.name === newName) if (isDuplicate) { toast.error( `The name ${newName} already exists in the current directory. Please use a different name.` ) return exitEditMode() } const updatedFiles = files.map((file) => file.id === id ? { ...file, name: newName, content: newName === 'deno.json' && file.content === '' ? denoJsonDefaultContent : file.content, } : file ) onFilesChange(updatedFiles) } const handleFileDelete = (id: number) => { if (files.length <= 1) { // Don't allow deleting the last file return } const fileToDelete = files.find((f) => f.id === id) const isSelected = fileToDelete?.selected const updatedFiles = files.filter((file) => file.id !== id) // If the deleted file was selected, select another file if (isSelected && updatedFiles.length > 0) { updatedFiles[0].selected = true } onFilesChange(updatedFiles) } const handleFileSelect = (id: number) => { const updatedFiles = files.map((file) => ({ ...file, selected: file.id === id, })) onFilesChange(updatedFiles) } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) } const handleDrop = async (e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) const droppedFiles = e.dataTransfer.files if (droppedFiles.length > 0) { await addDroppedFiles(droppedFiles) } } // Update treeData when files change useEffect(() => { setTreeData({ name: '', children: files.map((file) => ({ id: file.id.toString(), name: file.name, metadata: { isEditing: false, originalId: file.id, }, })), }) }, [files]) return (