diff --git a/frontend/src/App.js b/frontend/src/App.js index 5bbcb88..b4521aa 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,6 +1,6 @@ // App.js import React, { useState, useEffect } from "react"; -import Navbar from "./components/Navbar"; +import { Navbar } from "./shared/components"; import HomePage from "./pages/HomePage"; import { VoiceChatPage } from "./features/voice"; import SwarmControlPage from "./pages/SwarmControlPage"; diff --git a/frontend/src/components/FileSystemNavigator.js b/frontend/src/components/FileSystemNavigator.js deleted file mode 100644 index 7589614..0000000 --- a/frontend/src/components/FileSystemNavigator.js +++ /dev/null @@ -1,593 +0,0 @@ -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 mergeFiles = (prev, newFiles, parentPath) => { - const parentPrefix = (parentPath === "." || parentPath === "/" || parentPath === "") - ? "" - : (parentPath.endsWith("/") ? parentPath : parentPath + "/"); - - const newPaths = new Set(newFiles.map(f => f.path)); - const preserved = prev.filter(f => { - if (newPaths.has(f.path)) return false; - const rel = f.path.startsWith("/") ? f.path.slice(1) : f.path; - const pRel = parentPrefix.startsWith("/") ? parentPrefix.slice(1) : parentPrefix; - - if (pRel === "") { - if (!rel.includes("/")) return false; - } else { - if (rel.startsWith(pRel)) { - const sub = rel.slice(pRel.length); - if (!sub.includes("/") && sub.length > 0) return false; - } - } - return true; - }); - return [...newFiles, ...preserved]; - }; - - const loadRoot = useCallback(async () => { - setLoading(true); - setError(null); - try { - const files = await fetchLevel(initialPath); - setTree(prev => mergeFiles(prev, files, initialPath)); - } catch (err) { - setError(err.message || "Failed to connect to node filesystem."); - } finally { - setLoading(false); - } - }, [initialPath, fetchLevel]); - - useEffect(() => { - if (nodeId) { - loadRoot(); - - const shouldPoll = showSyncStatus || (sessionId && sessionId !== "__fs_explorer__"); - - if (shouldPoll) { - const interval = setInterval(() => { - loadRoot(); - }, 5000); - return () => clearInterval(interval); - } - } - }, [nodeId, loadRoot, showSyncStatus, sessionId]); - - // Keyboard Navigation for Media - useEffect(() => { - const handleKeyDown = (e) => { - if (!previewImage) return; - - if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { - e.preventDefault(); - // Get flat list of images in the current tree - const imageFiles = tree.filter(f => !f.is_dir && isImageFile(f.path)) - .sort((a, b) => a.path.localeCompare(b.path)); - - if (imageFiles.length <= 1) return; - - const currentIndex = imageFiles.findIndex(f => f.path === previewImage.path); - let nextIndex; - if (e.key === 'ArrowRight') { - nextIndex = (currentIndex + 1) % imageFiles.length; - } else { - nextIndex = (currentIndex - 1 + imageFiles.length) % imageFiles.length; - } - handleView(imageFiles[nextIndex].path); - } else if (e.key === 'Escape') { - if (previewImage.url) URL.revokeObjectURL(previewImage.url); - setPreviewImage(null); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [previewImage, tree]); - - 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); - // Optimistically remove from tree to force UI update - setTree(prev => prev.filter(f => !f.path.startsWith(path))); - 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(prev => { - if (prev && prev.url) URL.revokeObjectURL(prev.url); - return { 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 => ( -
-
- node.is_dir ? toggleFolder(node.path) : handleView(node.path)} - > - {folderLoading[node.path] ? ( -
- ) : node.is_dir ? ( - - ) : ( - - )} - - - node.is_dir ? toggleFolder(node.path) : handleView(node.path)} - > - {showSyncStatus && ( - - )} - {node.name} - - -
- {node.is_dir ? ( - <> - - - - - ) : ( - - )} - -
-
- {node.is_dir && expanded[node.path] && ( -
- {renderSubTree(node.path, depth + 1)} -
- )} -
- )); - }; - - return ( -
- {/* Header */} -
-

- - File Explorer -

-
- - - - -
-
- - {/* List */} -
- {error &&
{error}
} - {!loading && tree.length === 0 && !error && ( -
- -

No files found or node offline

-
- )} - {renderSubTree(initialPath)} -
- - {/* Create Item Modal */} - {newItemModal && ( -
-
-
-

- {newItemModal.isDir ? 'Create New Folder' : 'Create New File'} -

- -
-
{ - e.preventDefault(); - handleCreateFinal(e.target.elements.name.value); - }} className="p-4"> -
- - -
-
- - -
-
-
-
- )} - - {/* Editor Modal */} - {selectedFile && ( -
-
-
-
-

{selectedFile.path}

-

{isEditing ? 'Editing Mode' : 'Read-Only View'}

-
-
- {!isEditing ? ( - - ) : ( - - )} - -
-
-
-