/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ "use client"; import React, { useState, useEffect, FormEvent } from "react"; import { ChevronRight, ChevronDown, File as FileIcon, FolderOpen, Folder, Save, Search, X, Plus, Trash2, FilePlus, FolderPlus } from "lucide-react"; import Editor from "@monaco-editor/react"; interface TreeNode { name: string; path: string; type: "directory" | "file"; children?: TreeNode[]; } interface SearchResult { file: string; line_number: number; snippet: string; } interface WebIDEProps { repoUrl: string; } const FileTreeNode = ({ node, activePath, onSelect, onCreate, onDelete, level = 0, }: { node: TreeNode; activePath: string | null; onSelect: (path: string) => void; onCreate: (parentPath: string, isDir: boolean) => void; onDelete: (path: string) => void; level?: number; }) => { const [isOpen, setIsOpen] = useState(false); const [isHovered, setIsHovered] = useState(false); const isDir = node.type === "directory"; const isActive = activePath === node.path; const handleToggle = (e: React.MouseEvent) => { e.stopPropagation(); if (isDir) { setIsOpen(!isOpen); } else { onSelect(node.path); } }; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} className={`flex items-center justify-between w-full text-left py-1 px-2 cursor-pointer select-none text-sm font-sans transition-colors group ${ isActive ? "bg-[#37373d] text-white" : "text-[#cccccc] hover:bg-[#2a2d2e] hover:text-white" }`} style={{ paddingLeft: `${level * 12 + 8}px` }} >
{isDir ? ( isOpen ? : ) : null} {isDir ? ( isOpen ? : ) : ( )} {node.name}
{/* Action Icons (Visible on Hover) */} {isHovered && (
{isDir && ( <> )}
)}
{isDir && isOpen && node.children && (
{node.children.map((child, idx) => ( ))}
)}
); }; function getBackendUrl(): string { if (process.env.NEXT_PUBLIC_BACKEND_URL) { return process.env.NEXT_PUBLIC_BACKEND_URL.replace(/\/$/, ""); } if (typeof window !== "undefined") { const port = window.location.port; const hostname = window.location.hostname; if (port === "3000") { return `${window.location.protocol}//${hostname}:8000`; } return `${window.location.protocol}//${window.location.host}`; } return "http://localhost:8000"; } export default function WebIDE({ repoUrl }: WebIDEProps) { // Tree State const [tree, setTree] = useState(null); const [loadingTree, setLoadingTree] = useState(true); const [error, setError] = useState(null); // Tab State const [openTabs, setOpenTabs] = useState([]); const [activeTab, setActiveTab] = useState(null); const [fileContents, setFileContents] = useState>({}); const [editedContents, setEditedContents] = useState>({}); const [loadingFiles, setLoadingFiles] = useState>({}); // Sidebar State const [activeSidebarMode, setActiveSidebarMode] = useState<"explorer" | "search">("explorer"); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [isSaving, setIsSaving] = useState(false); const fetchTree = async () => { try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/tree`); if (!res.ok) throw new Error("Repository not found or API error"); const data = await res.json(); setTree(data); } catch (err: any) { setError(err.message || "Failed to fetch repository tree"); } finally { setLoadingTree(false); } }; useEffect(() => { setLoadingTree(true); fetchTree(); }, [repoUrl]); const openFile = async (path: string) => { if (!openTabs.includes(path)) { setOpenTabs([...openTabs, path]); } setActiveTab(path); if (fileContents[path] === undefined) { setLoadingFiles(prev => ({...prev, [path]: true})); try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/file?file_path=${encodeURIComponent(path)}`); if (!res.ok) throw new Error("File not found"); const data = await res.json(); setFileContents(prev => ({...prev, [path]: data.content})); setEditedContents(prev => ({...prev, [path]: data.content})); } catch (err: any) { setFileContents(prev => ({...prev, [path]: `// Error: ${err.message}`})); setEditedContents(prev => ({...prev, [path]: `// Error: ${err.message}`})); } finally { setLoadingFiles(prev => ({...prev, [path]: false})); } } }; const closeTab = (e: React.MouseEvent, path: string) => { e.stopPropagation(); const newTabs = openTabs.filter(t => t !== path); setOpenTabs(newTabs); if (activeTab === path) { setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : null); } // Clean up memory setFileContents(prev => { const n = {...prev}; delete n[path]; return n; }); setEditedContents(prev => { const n = {...prev}; delete n[path]; return n; }); }; const handleSave = async () => { if (!activeTab || editedContents[activeTab] === undefined) return; const content = editedContents[activeTab]; if (content === fileContents[activeTab]) return; setIsSaving(true); try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/file`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_path: activeTab, content }) }); if (!res.ok) throw new Error("Failed to save"); setFileContents(prev => ({...prev, [activeTab]: content})); } catch (err: any) { alert("Failed to save file: " + err.message); } finally { setIsSaving(false); } }; const handleCreate = async (parentPath: string, isDir: boolean) => { const name = prompt(`Enter name for new ${isDir ? 'folder' : 'file'} in ${parentPath}:`); if (!name) return; const newPath = parentPath === "." || parentPath === "" ? name : `${parentPath}/${name}`; try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/file/create`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_path: newPath, is_dir: isDir, content: "" }) }); if (!res.ok) throw new Error("Failed to create"); await fetchTree(); if (!isDir) openFile(newPath); } catch (err: any) { alert("Failed to create: " + err.message); } }; const handleDelete = async (path: string) => { if (!confirm(`Are you sure you want to delete ${path}? This will also push a commit to GitHub.`)) return; try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/file?file_path=${encodeURIComponent(path)}`, { method: "DELETE" }); if (!res.ok) throw new Error("Failed to delete"); if (openTabs.includes(path)) closeTab({ stopPropagation: () => {} } as any, path); await fetchTree(); } catch (err: any) { alert("Failed to delete: " + err.message); } }; const handleSearch = async (e: FormEvent) => { e.preventDefault(); if (!searchQuery.trim()) return; setIsSearching(true); try { const res = await fetch(`${getBackendUrl()}/repo/${encodeURIComponent(repoUrl)}/search?q=${encodeURIComponent(searchQuery)}`); if (!res.ok) throw new Error("Search failed"); const data = await res.json(); setSearchResults(data.results); } catch (err: any) { alert(err.message); } finally { setIsSearching(false); } }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); handleSave(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [activeTab, editedContents]); const getLanguage = (filename: string) => { const ext = filename.split(".").pop()?.toLowerCase(); const map: Record = { js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript", py: "python", json: "json", md: "markdown", html: "html", css: "css", sh: "bash", }; return map[ext || ""] || "text"; }; const hasActiveTabUnsavedChanges = activeTab ? (editedContents[activeTab] !== undefined && editedContents[activeTab] !== fileContents[activeTab]) : false; return (
{/* Activity Bar */}
{/* Sidebar */}
{activeSidebarMode === "explorer" ? "Explorer" : "Search"} {activeSidebarMode === "explorer" && (
)}
{activeSidebarMode === "explorer" ? ( loadingTree ? (
Loading tree...
) : error ? (
{error}
) : tree && tree.children ? ( tree.children.map((child, idx) => ( )) ) : (
No files found.
) ) : (
setSearchQuery(e.target.value)} placeholder="Search..." className="w-full bg-[#3c3c3c] text-white border border-[#3c3c3c] focus:border-[#007acc] rounded px-2 py-1 text-sm outline-none" />
{isSearching ? (
Searching...
) : searchResults.length > 0 ? ( searchResults.map((res, i) => (
openFile(res.file)} >
{res.file}
{res.line_number}: {res.snippet}
)) ) : searchQuery ? (
No results found.
) : (
Enter a query to search.
)}
)}
{/* Editor Area */}
{openTabs.length > 0 ? ( <> {/* Tab Bar */}
{openTabs.map(tab => { const isTabActive = tab === activeTab; const isUnsaved = editedContents[tab] !== undefined && editedContents[tab] !== fileContents[tab]; return (
setActiveTab(tab)} className={`flex items-center gap-2 h-full px-3 text-sm cursor-pointer border-r border-[#1e1e1e] group ${ isTabActive ? "bg-[#1e1e1e] text-[#cccccc] border-t border-t-[#007acc]" : "bg-[#2d2d2d] text-[#888888] hover:bg-[#2b2b2b]" }`} style={{ minWidth: "120px", maxWidth: "200px" }} > {tab.split("/").pop()}
); })}
{/* Editor Breadcrumbs & Save */}
{repoUrl} > {activeTab?.split("/").join(" > ")}
{/* Code Content */}
{activeTab && loadingFiles[activeTab] ? (
Loading file content...
) : activeTab && editedContents[activeTab] !== undefined ? ( setEditedContents(prev => ({...prev, [activeTab]: value ?? ""}))} options={{ minimap: { enabled: true }, fontSize: 14, wordWrap: "on", padding: { top: 16 } }} /> ) : null}
) : (

AutoMaintainer Editor

Select a file from the explorer or search to begin.

Search Create File
)}
); }