Spaces:
Paused
Paused
| import React, { useState, useEffect } from 'react'; | |
| import { FaFolder, FaFolderOpen, FaFile, FaChevronRight, FaChevronDown, FaSpinner } from 'react-icons/fa'; | |
| import axios from 'axios'; | |
| interface TreeNode { | |
| name: string; | |
| path: string; | |
| type: 'file' | 'folder'; | |
| children?: TreeNode[]; | |
| } | |
| const FileTree: React.FC<{ projectPath: string; onFileSelect: (path: string) => void }> = ({ projectPath, onFileSelect }) => { | |
| const [tree, setTree] = useState<TreeNode[]>([]); | |
| const [expanded, setExpanded] = useState<Set<string>>(new Set()); | |
| const [selected, setSelected] = useState<string | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| axios.get('/api/files', { params: { project_path: projectPath } }) | |
| .then(res => setTree(res.data)) | |
| .catch(() => setTree([])) | |
| .finally(() => setLoading(false)); | |
| }, [projectPath]); | |
| const toggle = (path: string) => { | |
| setExpanded(prev => { | |
| const next = new Set(prev); | |
| if (next.has(path)) next.delete(path); else next.add(path); | |
| return next; | |
| }); | |
| }; | |
| const handleClick = (node: TreeNode) => { | |
| if (node.type === 'folder') { | |
| toggle(node.path); | |
| } else { | |
| setSelected(node.path); | |
| onFileSelect(node.path); | |
| } | |
| }; | |
| const renderNode = (node: TreeNode, depth: number = 0) => { | |
| const isOpen = expanded.has(node.path); | |
| return ( | |
| <div key={node.path}> | |
| <div | |
| onClick={() => handleClick(node)} | |
| style={{ | |
| paddingLeft: depth * 16 + 8, | |
| padding: '4px 8px', | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 6, | |
| background: selected === node.path ? 'var(--accent)' : 'transparent', | |
| color: selected === node.path ? 'white' : 'var(--text-primary)', | |
| fontSize: 13, | |
| }} | |
| > | |
| {node.type === 'folder' ? ( | |
| <> | |
| {isOpen ? <FaChevronDown size={10} /> : <FaChevronRight size={10} />} | |
| {isOpen ? <FaFolderOpen size={14} /> : <FaFolder size={14} />} | |
| </> | |
| ) : ( | |
| <> | |
| <span style={{ width: 10 }} /> | |
| <FaFile size={14} /> | |
| </> | |
| )} | |
| <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</span> | |
| </div> | |
| {node.type === 'folder' && isOpen && node.children?.map(child => renderNode(child, depth + 1))} | |
| </div> | |
| ); | |
| }; | |
| if (loading) return <div style={{ padding: 12, color: 'var(--text-secondary)' }}><FaSpinner className="spin" /> Loading files...</div>; | |
| return <div style={{ overflowY: 'auto', flex: 1 }}>{tree.map(node => renderNode(node))}</div>; | |
| }; | |
| export default FileTree; |