FileExplorer.tsxā¢4.11 kB
import React, { useState, useEffect, useCallback } from 'react';
import { FileIcon, FolderIcon, PythonIcon, NewFileIcon } from './icons.js';
import type { VFS, FileSystemEntry } from '../lib/vfs.js';
import Spinner from './Spinner.js';
interface FileExplorerProps {
onOpenFile: (path: string) => void;
addToTerminal: (output: string) => void;
vfs: VFS;
refreshKey?: number;
}
const FileTree: React.FC<{ entries: FileSystemEntry[]; onOpenFile: (path: string) => void; level?: number }> = ({ entries, onOpenFile, level = 0 }) => {
return (
<div>
{entries.sort((a,b) => Number(b.isDirectory) - Number(a.isDirectory) || a.name.localeCompare(b.name)).map(entry => (
<div key={entry.path}>
<div style={{ ...styles.fileItem, paddingLeft: `${10 + level * 15}px` }} onClick={() => !entry.isDirectory && onOpenFile(entry.path)}>
<span style={styles.icon}>{entry.isDirectory ? <FolderIcon /> : entry.name.endsWith('.py') ? <PythonIcon /> : <FileIcon />}</span>
{entry.name}
</div>
</div>
))}
</div>
);
};
const FileExplorer: React.FC<FileExplorerProps> = ({ onOpenFile, addToTerminal, vfs, refreshKey }) => {
const [fileTree, setFileTree] = useState<FileSystemEntry[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchFiles = useCallback(async () => {
setIsLoading(true);
try {
// Always list files from the root of the workspace
const tree = await vfs.getTree('/workspace');
setFileTree(tree);
} catch (e) {
addToTerminal(`Error fetching file tree: ${e instanceof Error ? e.message : String(e)}`);
// If workspace doesn't exist, create it
if (e instanceof Error && (e.message.includes('No such file or directory') || e.message.includes('cannot access'))) {
addToTerminal('Workspace not found. Creating /workspace directory...');
try {
// A safe way to trigger directory creation if needed. The backend handles creation.
await vfs.writeFile('/workspace/.pyforge_init', '');
fetchFiles(); // retry fetching
} catch (createErr) {
addToTerminal(`Failed to create workspace: ${createErr}`);
}
}
} finally {
setIsLoading(false);
}
}, [vfs, addToTerminal]);
useEffect(() => {
fetchFiles();
}, [fetchFiles, refreshKey]);
const createNewFile = async () => {
const fileName = prompt("Enter new file name (within /workspace):", "new_file.py");
if (fileName) {
const path = `/workspace/${fileName.replace(/^\//, '')}`; // Ensure it's inside workspace
try {
await vfs.writeFile(path, '# New file created in PyForge');
addToTerminal(`Created file: ${path}`);
fetchFiles();
onOpenFile(path);
} catch (e) {
addToTerminal(`Error creating file: ${e instanceof Error ? e.message : String(e)}`);
}
}
};
return (
<div style={styles.explorerContainer}>
<div style={styles.header}>
<span>EXPLORER</span>
<button style={styles.headerButton} onClick={createNewFile} title="New File">
<NewFileIcon/>
</button>
</div>
<div style={styles.fileList}>
{isLoading && <Spinner />}
{fileTree && <FileTree entries={fileTree} onOpenFile={onOpenFile} />}
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
explorerContainer: { display: 'flex', flexDirection: 'column', height: '100%' },
header: { padding: '10px', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid var(--border-color)', flexShrink: 0 },
headerButton: { background: 'none', border: 'none', color: 'var(--text-primary)', cursor: 'pointer' },
fileList: { overflowY: 'auto', flex: 1, padding: '5px 0' },
fileItem: { display: 'flex', alignItems: 'center', padding: '4px 10px', cursor: 'pointer', fontSize: 'var(--font-size-small)' },
icon: { marginRight: '5px', display: 'flex', alignItems: 'center' },
};
export default FileExplorer;