import React, { useEffect, useState, useCallback } from "react"; import Editor, { loader } from "@monaco-editor/react"; import { http } from "../lib/api"; import { Folder, File as FileIcon, RefreshCw, Save, ArrowUp, AlertCircle, Eye, Code2 } from "lucide-react"; import { toast } from "sonner"; import FilePreviewPane from "./FilePreviewPane"; import { basename, isHtmlPath, parentDir } from "../lib/filePreview"; const joinPath = (base, name) => { if (!base || base === "~") return name; const sep = base.includes("\\") ? "\\" : "/"; if (base.endsWith(sep)) return `${base}${name}`; return `${base}${sep}${name}`; }; const detectLang = (path) => { const ext = path.split(".").pop().toLowerCase(); const map = { py: "python", js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript", json: "json", html: "html", css: "css", scss: "scss", md: "markdown", cpp: "cpp", c: "c", h: "cpp", hpp: "cpp", cs: "csharp", java: "java", rs: "rust", go: "go", lua: "lua", sh: "shell", yml: "yaml", yaml: "yaml", toml: "toml", xml: "xml", svg: "xml", gd: "gdscript", }; return map[ext] || "plaintext"; }; export default function FileExplorer({ agentOnline, externalPreview = null }) { const [cwd, setCwd] = useState("~"); const [listing, setListing] = useState({ files: [], dirs: [], path: "~" }); const [loading, setLoading] = useState(false); const [currentFile, setCurrentFile] = useState(null); const [content, setContent] = useState(""); const [originalContent, setOriginalContent] = useState(""); const [editorLoading, setEditorLoading] = useState(false); const [fsError, setFsError] = useState(""); const [editorFailed, setEditorFailed] = useState(false); const [editorTheme, setEditorTheme] = useState("vs-dark"); const [viewMode, setViewMode] = useState("code"); useEffect(() => { const syncTheme = () => { setEditorTheme(document.documentElement.classList.contains("theme-light") ? "vs" : "vs-dark"); }; syncTheme(); const obs = new MutationObserver(syncTheme); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); return () => obs.disconnect(); }, []); const refresh = useCallback(async (path) => { if (!agentOnline) return; setLoading(true); setFsError(""); try { const r = await http.get("/agent/fs/list", { params: { path } }); setListing({ path: r.data?.path || path, files: Array.isArray(r.data?.files) ? r.data.files : [], dirs: Array.isArray(r.data?.dirs) ? r.data.dirs : [], }); setCwd(r.data?.path || path); } catch (e) { const msg = e?.response?.data?.detail || e.message || "Erreur"; setFsError(String(msg)); toast.error("Fichiers : " + msg); } finally { setLoading(false); } }, [agentOnline]); useEffect(() => { loader.init().catch(() => setEditorFailed(true)); }, []); useEffect(() => { if (agentOnline) refresh("~"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [agentOnline]); useEffect(() => { if (!externalPreview?.path) return; const path = externalPreview.path; setCurrentFile(path); if (externalPreview.preview != null) { setContent(externalPreview.preview); setOriginalContent(externalPreview.preview); } setViewMode("code"); const parent = parentDir(path); if (parent && parent !== cwd) refresh(parent); // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalPreview?.path, externalPreview?.preview]); const openFile = async (name) => { const path = joinPath(cwd, name); setEditorLoading(true); setEditorFailed(false); try { const r = await http.get("/agent/fs/read", { params: { path } }); setCurrentFile(r.data.path); setContent(r.data.content || ""); setOriginalContent(r.data.content || ""); setViewMode("code"); } catch (e) { toast.error("Lecture : " + (e?.response?.data?.detail || e.message)); } finally { setEditorLoading(false); } }; const saveFile = async () => { if (!currentFile) return; try { await http.post("/agent/fs/write", { path: currentFile, content }); setOriginalContent(content); toast.success("Sauvegardé"); } catch (e) { toast.error("Écriture : " + (e?.response?.data?.detail || e.message)); } }; const cdUp = () => { refresh(parentDir(cwd)); }; const dirs = listing.dirs || []; const files = listing.files || []; const dirty = content !== originalContent; const fileName = currentFile ? basename(currentFile) : null; const isHtml = currentFile ? isHtmlPath(currentFile) : false; const showPreviewTab = currentFile && !isHtml; const effectiveViewMode = isHtml ? "code" : viewMode; if (!agentOnline) { return (
Agent local hors ligne.
Agent local requis.
{externalPreview?.path && (
{(externalPreview.preview || "").slice(0, 4000)}
) : (
{cwd}
vide
)}