import React, { useState, useEffect, useCallback } from 'react';
import { nodeFsList, nodeFsCat, nodeFsTouch, nodeFsRm, nodeFsUpload, nodeFsDownloadBlob } from '../services/apiService';
/**
* A modular File Navigator component similar to VS Code's side panel.
* Displays a tree structure from an Agent Node's filesystem.
*/
const FileSystemNavigator = ({
nodeId,
initialPath = ".",
sessionId = "__fs_explorer__",
showSyncStatus = false
}) => {
const [tree, setTree] = useState([]);
const [loading, setLoading] = useState(false);
const [expanded, setExpanded] = useState({}); // { [path]: boolean }
const [error, setError] = useState(null);
const [selectedFile, setSelectedFile] = useState(null); // { path, content }
const [isEditing, setIsEditing] = useState(false);
const [newItemModal, setNewItemModal] = useState(null); // { parentPath, isDir }
const [deleteModal, setDeleteModal] = useState(null); // path
const [operationLoading, setOperationLoading] = useState(false);
const [folderLoading, setFolderLoading] = useState({}); // { [path]: boolean }
const [previewImage, setPreviewImage] = useState(null); // { path, url }
const uploadInputRef = React.useRef(null);
const [uploadTargetPath, setUploadTargetPath] = useState(null);
const fetchLevel = useCallback(async (path) => {
const data = await nodeFsList(nodeId, path, sessionId);
return data.files || [];
}, [nodeId, sessionId]);
const loadRoot = useCallback(async () => {
setLoading(true);
setError(null);
try {
const files = await fetchLevel(initialPath);
setTree(files);
} catch (err) {
setError(err.message || "Failed to connect to node filesystem.");
} finally {
setLoading(false);
}
}, [initialPath, fetchLevel]);
useEffect(() => {
if (nodeId) {
loadRoot();
// Poll for updates if we are in a specific session (Coding Assistant)
// or if explicitly requested via showSyncStatus
const shouldPoll = showSyncStatus || (sessionId && sessionId !== "__fs_explorer__");
if (shouldPoll) {
const interval = setInterval(() => {
loadRoot();
}, 5000);
return () => clearInterval(interval);
}
}
}, [nodeId, loadRoot, showSyncStatus, sessionId]);
const toggleFolder = async (path) => {
const isExpanded = expanded[path];
if (!isExpanded) {
setExpanded(prev => ({ ...prev, [path]: true }));
// Normalize path for prefix check: e.g. "etc" -> "etc/"
const target = (path === "/" || path === ".") ? "" : (path.startsWith("/") ? path.slice(1) : path);
const prefix = target === "" ? "" : (target.endsWith("/") ? target : target + "/");
const hasChildren = tree.some(node => {
const nodeRel = node.path.startsWith("/") ? node.path.slice(1) : node.path;
return nodeRel.startsWith(prefix) && nodeRel !== target;
});
if (!hasChildren) {
setFolderLoading(prev => ({ ...prev, [path]: true }));
try {
// Node expects relative path for subdirs, or "." for root
const fetchPath = (path === "/" || path === ".") ? "." : (path.startsWith("/") ? path.slice(1) : path);
const children = await fetchLevel(fetchPath);
setTree(prev => {
const existingPaths = new Set(prev.map(f => f.path));
const newOnes = children.filter(c => !existingPaths.has(c.path));
return [...prev, ...newOnes];
});
} catch (err) {
console.error("Folder expansion failed:", err);
setError(`Failed to open folder: ${err.message}`);
} finally {
setFolderLoading(prev => ({ ...prev, [path]: false }));
}
}
} else {
setExpanded(prev => ({ ...prev, [path]: false }));
}
};
const handleCreateFinal = async (name) => {
if (!name || !newItemModal) return;
const { parentPath, isDir } = newItemModal;
let fullPath;
if (!parentPath || parentPath === "." || parentPath === "" || parentPath === "/") {
fullPath = name;
} else {
fullPath = `${parentPath}/${name}`;
}
setOperationLoading(true);
setError(null);
try {
await nodeFsTouch(nodeId, fullPath, "", isDir, sessionId);
setNewItemModal(null);
setTimeout(loadRoot, 500);
} catch (err) {
setError(`Failed to create: ${err.message}`);
} finally {
setOperationLoading(false);
}
};
const handleDeleteFinal = async () => {
if (!deleteModal) return;
const path = deleteModal;
setOperationLoading(true);
setError(null);
try {
await nodeFsRm(nodeId, path, sessionId);
setDeleteModal(null);
setTimeout(loadRoot, 500);
} catch (err) {
setError(`Failed to delete: ${err.message}`);
} finally {
setOperationLoading(false);
}
};
const isBinaryFile = (path) => {
const ext = path.split('.').pop().toLowerCase();
const binaryExts = ['pdf', 'zip', 'gz', 'tar', 'exe', 'dll', 'so', 'bin', 'pyc', 'node', 'db', 'sqlite', 'mp3', 'mp4', 'wav', 'mov'];
return binaryExts.includes(ext);
};
const isImageFile = (path) => {
const ext = path.split('.').pop().toLowerCase();
const imgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp'];
return imgExts.includes(ext);
};
const handleView = async (path) => {
if (isImageFile(path)) {
setOperationLoading(true);
try {
const blob = await nodeFsDownloadBlob(nodeId, path, sessionId);
const url = URL.createObjectURL(blob);
setPreviewImage({ path, url });
} catch (err) {
setError(`Failed to load image: ${err.message}`);
} finally {
setOperationLoading(false);
}
return;
}
if (isBinaryFile(path)) {
setError(`Cannot view binary file: ${path}. Please download it instead.`);
return;
}
setOperationLoading(true);
setError(null);
try {
const res = await nodeFsCat(nodeId, path, sessionId);
setSelectedFile({ path, content: res.content });
setIsEditing(false);
} catch (err) {
setError(`Failed to read file: ${err.message}`);
} finally {
setOperationLoading(false);
}
};
const handleDownload = async (path) => {
setOperationLoading(true);
setError(null);
try {
const blob = await nodeFsDownloadBlob(nodeId, path, sessionId);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', path.split('/').pop());
document.body.appendChild(link);
link.click();
link.remove();
} catch (err) {
setError(`Download failed: ${err.message}`);
} finally {
setOperationLoading(false);
}
};
const handleUploadClick = (targetPath) => {
setUploadTargetPath(targetPath);
if (uploadInputRef.current) {
uploadInputRef.current.click();
}
};
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file || !uploadTargetPath) return;
setOperationLoading(true);
setError(null);
try {
// If uploadTargetPath is ".", upload to root. If a dir path, use as-is.
await nodeFsUpload(nodeId, uploadTargetPath, file, sessionId);
setTimeout(loadRoot, 600);
} catch (err) {
setError(`Upload failed: ${err.message}`);
} finally {
setOperationLoading(false);
setUploadTargetPath(null);
event.target.value = ''; // Reset input
}
};
const handleSave = async () => {
setOperationLoading(true);
setError(null);
try {
await nodeFsTouch(nodeId, selectedFile.path, selectedFile.content, false, sessionId);
setSelectedFile(null);
} catch (err) {
setError(`Failed to save: ${err.message}`);
} finally {
setOperationLoading(false);
}
};
// Helper to render tree recursively
const renderSubTree = (currentPath, depth = 0) => {
// Normalize currentPath for filtering
const normCurrent = (currentPath === "/" || currentPath === "." || currentPath === "")
? ""
: (currentPath.startsWith("/") ? currentPath.slice(1) : currentPath);
const children = tree.filter(node => {
const nodePath = node.path.startsWith("/") ? node.path.slice(1) : node.path;
if (normCurrent === "") {
// Root level: paths without slashes
return !nodePath.includes("/");
}
// Nested level: starts with "parent/" and has no further slashes
const prefix = normCurrent.endsWith("/") ? normCurrent : normCurrent + "/";
if (!nodePath.startsWith(prefix)) return false;
if (nodePath === normCurrent) return false;
const sub = nodePath.slice(prefix.length);
return !sub.includes("/");
});
// Sort: Folders first, then Alphabetical
const sorted = [...children].sort((a, b) => {
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.localeCompare(b.name);
});
return sorted.map(node => (
<div key={node.path} className="group">
<div style={{ paddingLeft: `${depth * 12}px` }} className="flex items-center py-1 px-2 hover:bg-gray-100 dark:hover:bg-gray-700/50 rounded cursor-pointer transition-colors text-xs">
<span
className="mr-2 text-gray-400"
onClick={() => node.is_dir ? toggleFolder(node.path) : handleView(node.path)}
>
{folderLoading[node.path] ? (
<div className="w-3.5 h-3.5 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin" />
) : node.is_dir ? (
<svg className={`w-3.5 h-3.5 transform transition-transform ${expanded[node.path] ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
) : (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
)}
</span>
<span
className={`flex-1 truncate flex items-center ${node.is_dir ? 'font-bold text-gray-700 dark:text-gray-300' : 'text-gray-600 dark:text-gray-400'}`}
onClick={() => node.is_dir ? toggleFolder(node.path) : handleView(node.path)}
>
{showSyncStatus && (
<span
className={`w-2 h-2 rounded-full mr-2 shrink-0 transition-all duration-500 shadow-sm ${node.is_synced ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]' : 'bg-amber-400 animate-pulse shadow-[0_0_8px_rgba(251,191,36,0.4)]'}`}
title={node.is_synced ? "Synced to cloud" : "Syncing to cloud..."}
/>
)}
{node.name}
</span>
<div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity ml-2">
{node.is_dir ? (
<>
<button onClick={(e) => { e.stopPropagation(); handleUploadClick(node.path); }} className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded text-gray-400 hover:text-indigo-500" title="Upload to this folder">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
</button>
<button onClick={(e) => { e.stopPropagation(); setNewItemModal({ parentPath: node.path, isDir: false }); }} className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded text-gray-400 hover:text-indigo-500" title="New File">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
</button>
<button onClick={(e) => { e.stopPropagation(); setNewItemModal({ parentPath: node.path, isDir: true }); }} className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded text-gray-400 hover:text-indigo-500" title="New Folder">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" /></svg>
</button>
</>
) : (
<button onClick={(e) => { e.stopPropagation(); handleDownload(node.path); }} className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded text-gray-400 hover:text-green-500" title="Download">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</button>
)}
<button onClick={(e) => { e.stopPropagation(); setDeleteModal(node.path); }} className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded text-gray-400 hover:text-red-500" title="Delete">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
</div>
{node.is_dir && expanded[node.path] && (
<div className="animate-in slide-in-from-left-2 duration-200">
{renderSubTree(node.path, depth + 1)}
</div>
)}
</div>
));
};
return (
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-xl border dark:border-gray-700 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/20">
<h3 className="text-[10px] font-black uppercase tracking-widest text-gray-500 dark:text-gray-400 flex items-center">
<svg className="w-3 h-3 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
File Explorer
</h3>
<div className="flex space-x-2">
<button onClick={() => handleUploadClick(".")} className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors" title="Upload to Root">
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
</button>
<button onClick={() => setNewItemModal({ parentPath: ".", isDir: false })} className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors" title="New File">
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</button>
<button onClick={() => setNewItemModal({ parentPath: ".", isDir: true })} className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors" title="New Folder">
<svg className="w-3.5 h-3.5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" /></svg>
</button>
<button onClick={loadRoot} className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors" title="Refresh">
<svg className={`w-3.5 h-3.5 text-gray-500 ${loading ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
</div>
</div>
{/* List */}
<div className="flex-1 overflow-y-auto p-2 scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600">
{error && <div className="p-4 text-xs text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg">{error}</div>}
{!loading && tree.length === 0 && !error && (
<div className="flex flex-col items-center justify-center h-48 text-gray-400">
<svg className="w-8 h-8 mb-2 opacity-20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
<p className="text-[10px]">No files found or node offline</p>
</div>
)}
{renderSubTree(initialPath)}
</div>
{/* Create Item Modal */}
{newItemModal && (
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-900 rounded-2xl w-full max-w-md shadow-2xl border dark:border-gray-700 overflow-hidden">
<div className="p-4 border-b dark:border-gray-800 flex justify-between items-center bg-gray-50 dark:bg-gray-800/50">
<h4 className="text-sm font-bold text-gray-900 dark:text-white">
{newItemModal.isDir ? 'Create New Folder' : 'Create New File'}
</h4>
<button onClick={() => setNewItemModal(null)} className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors">
<svg className="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<form onSubmit={(e) => {
e.preventDefault();
handleCreateFinal(e.target.elements.name.value);
}} className="p-4">
<div className="mb-4">
<label className="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Item Name</label>
<input
name="name"
type="text"
autoFocus
autoComplete="off"
className="w-full p-2.5 bg-gray-50 dark:bg-gray-800 border dark:border-gray-700 rounded-xl text-sm focus:ring-2 focus:ring-indigo-500 focus:outline-none dark:text-white"
placeholder={newItemModal.isDir ? "folder_name" : "file_name.txt"}
required
/>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setNewItemModal(null)}
className="px-4 py-2 text-xs font-bold text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={operationLoading}
className="px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-xs font-black shadow-lg shadow-indigo-500/20 transition-all disabled:opacity-50"
>
{operationLoading ? 'Creating...' : 'Create Item'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Editor Modal */}
{selectedFile && (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-900 rounded-2xl w-full max-w-5xl h-[85vh] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden border dark:border-gray-700">
<div className="p-4 border-b dark:border-gray-800 flex justify-between items-center bg-gray-50 dark:bg-gray-800/50">
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white truncate max-w-md">{selectedFile.path}</h4>
<p className="text-[10px] text-gray-500 uppercase font-mono mt-0.5">{isEditing ? 'Editing Mode' : 'Read-Only View'}</p>
</div>
<div className="flex space-x-2">
{!isEditing ? (
<button onClick={() => setIsEditing(true)} className="px-3 py-1.5 bg-indigo-600 text-white rounded-lg text-xs font-bold hover:bg-indigo-700 transition-colors">
Edit Content
</button>
) : (
<button onClick={handleSave} className="px-3 py-1.5 bg-green-600 text-white rounded-lg text-xs font-bold hover:bg-green-700 transition-colors">
Save Changes
</button>
)}
<button onClick={() => setSelectedFile(null)} className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors">
<svg className="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
</div>
<div className="flex-1 p-0 overflow-hidden">
<textarea
className="w-full h-full p-6 text-sm font-mono bg-white dark:bg-gray-950 text-gray-800 dark:text-gray-200 resize-none focus:outline-none"
value={selectedFile.content}
readOnly={!isEditing}
onChange={(e) => setSelectedFile({ ...selectedFile, content: e.target.value })}
placeholder="Empty file..."
/>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{deleteModal && (
<div className="fixed inset-0 z-[80] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-900 rounded-2xl w-full max-w-sm shadow-2xl border dark:border-gray-700 overflow-hidden">
<div className="p-6 text-center">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4 text-red-600">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</div>
<h4 className="text-lg font-bold text-gray-900 dark:text-white mb-2">Confirm Delete</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 break-all mb-6">
Are you sure you want to delete <span className="font-mono text-red-500">{deleteModal}</span>? This action cannot be undone.
</p>
<div className="flex space-x-3">
<button
onClick={() => setDeleteModal(null)}
className="flex-1 px-4 py-2 text-xs font-bold text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-400 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
Cancel
</button>
<button
onClick={handleDeleteFinal}
disabled={operationLoading}
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-xs font-black shadow-lg shadow-red-500/20 transition-all disabled:opacity-50"
>
{operationLoading ? 'Deleting...' : 'Delete Forever'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Hidden File Input for Uploads */}
<input
type="file"
ref={uploadInputRef}
style={{ display: 'none' }}
onChange={handleFileUpload}
/>
{/* Image Preview Modal */}
{previewImage && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-8 bg-black/95 backdrop-blur-md animate-in fade-in duration-300 text-white">
<div className="absolute top-6 right-8 flex space-x-4">
<button
onClick={() => handleDownload(previewImage.path)}
className="p-3 bg-white/10 hover:bg-white/20 text-white rounded-full transition-all border border-white/10"
title="Download Image"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
</button>
<button
onClick={() => {
URL.revokeObjectURL(previewImage.url);
setPreviewImage(null);
}}
className="p-3 bg-white/10 hover:bg-red-500/80 text-white rounded-full transition-all border border-white/10"
title="Close"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="max-w-7xl max-h-[85vh] relative flex flex-col items-center">
<img
src={previewImage.url}
alt={previewImage.path}
className="max-w-full max-h-full object-contain rounded-lg shadow-2xl border border-white/5 bg-gray-900/40 shadow-indigo-500/10"
/>
<p className="mt-4 text-gray-400 font-mono text-xs bg-black/40 px-3 py-1.5 rounded-full border border-white/10">
{previewImage.path}
</p>
</div>
</div>
)}
</div>
);
};
export default FileSystemNavigator;