/** * FileStructurePanel - Shows study file structure in the canvas sidebar * * Features: * - Tree view of study directory * - Highlights model files (.prt, .fem, .sim) * - Shows file dependencies * - One-click to set as model source * - Refresh button to reload */ import { useState, useEffect, useCallback } from 'react'; import { Folder, FolderOpen, FileBox, ChevronRight, RefreshCw, Box, Cpu, FileCode, AlertCircle, CheckCircle, Plus, } from 'lucide-react'; interface FileNode { name: string; path: string; type: 'file' | 'directory'; extension?: string; size?: number; children?: FileNode[]; isModelFile?: boolean; isSelected?: boolean; } interface FileStructurePanelProps { studyId: string | null; onModelSelect?: (filePath: string, fileType: string) => void; selectedModelPath?: string; className?: string; } // File type to icon mapping const FILE_ICONS: Record = { '.prt': { icon: Box, color: 'text-blue-400' }, '.sim': { icon: Cpu, color: 'text-violet-400' }, '.fem': { icon: FileCode, color: 'text-emerald-400' }, '.afem': { icon: FileCode, color: 'text-emerald-400' }, '.dat': { icon: FileBox, color: 'text-amber-400' }, '.bdf': { icon: FileBox, color: 'text-amber-400' }, '.op2': { icon: FileBox, color: 'text-rose-400' }, '.f06': { icon: FileBox, color: 'text-dark-400' }, }; const MODEL_EXTENSIONS = ['.prt', '.sim', '.fem', '.afem']; export function FileStructurePanel({ studyId, onModelSelect, selectedModelPath, className = '', }: FileStructurePanelProps) { const [files, setFiles] = useState([]); const [expandedPaths, setExpandedPaths] = useState>(new Set()); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Load study file structure const loadFileStructure = useCallback(async () => { if (!studyId) { setFiles([]); return; } setIsLoading(true); setError(null); try { const response = await fetch(`/api/files/structure/${encodeURIComponent(studyId)}`); if (!response.ok) { if (response.status === 404) { setError('Study not found'); } else { throw new Error(`Failed to load: ${response.status}`); } setFiles([]); return; } const data = await response.json(); // Process the file tree to mark model files const processNode = (node: FileNode): FileNode => { if (node.type === 'directory' && node.children) { return { ...node, children: node.children.map(processNode), }; } const ext = '.' + node.name.split('.').pop()?.toLowerCase(); return { ...node, extension: ext, isModelFile: MODEL_EXTENSIONS.includes(ext), isSelected: node.path === selectedModelPath, }; }; const processedFiles = (data.files || []).map(processNode); setFiles(processedFiles); // Auto-expand 1_setup and root directories const toExpand = new Set(); processedFiles.forEach((node: FileNode) => { if (node.type === 'directory') { toExpand.add(node.path); if (node.name === '1_setup' && node.children) { node.children.forEach((child: FileNode) => { if (child.type === 'directory') { toExpand.add(child.path); } }); } } }); setExpandedPaths(toExpand); } catch (err) { console.error('Failed to load file structure:', err); setError('Failed to load files'); } finally { setIsLoading(false); } }, [studyId, selectedModelPath]); useEffect(() => { loadFileStructure(); }, [loadFileStructure]); // Toggle directory expansion const toggleExpand = (path: string) => { setExpandedPaths((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }; // Handle file selection const handleFileClick = (node: FileNode) => { if (node.type === 'directory') { toggleExpand(node.path); } else if (node.isModelFile && onModelSelect) { onModelSelect(node.path, node.extension || ''); } }; // Render a file/folder node const renderNode = (node: FileNode, depth: number = 0) => { const isExpanded = expandedPaths.has(node.path); const isDirectory = node.type === 'directory'; const fileInfo = node.extension ? FILE_ICONS[node.extension] : null; const Icon = isDirectory ? isExpanded ? FolderOpen : Folder : fileInfo?.icon || FileBox; const iconColor = isDirectory ? 'text-amber-400' : fileInfo?.color || 'text-dark-400'; const isSelected = node.path === selectedModelPath; return (
{/* Children */} {isDirectory && isExpanded && node.children && (
{node.children.map((child) => renderNode(child, depth + 1))}
)}
); }; // No study selected state if (!studyId) { return (

No study selected

Load a study to see its files

); } return (
{/* Header */}
Files
{/* Content */}
{isLoading && files.length === 0 ? (
Loading...
) : error ? (
{error}
) : files.length === 0 ? (

No files found

Add model files to 1_setup/

) : (
{files.map((node) => renderNode(node))}
)}
{/* Footer hint */}
Click a model file to select it
); } export default FileStructurePanel;