/** * FileExplorer Component * * 文件树组件,支持展开/折叠和文件选择 */ import { useState, useEffect, useCallback } from 'react'; import { ChevronRight, ChevronDown, File, Folder, FolderOpen, RefreshCw } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../utils/cn.js'; import { getFileTree, type FileTreeNode } from '../api/client.js'; interface FileExplorerProps { onFileSelect?: (path: string, name: string) => void; className?: string; /** 工作目录路径,显示在标题栏 */ workingDirectory?: string; } // 文件图标颜色映射 const fileIconColors: Record = { ts: 'text-blue-400', tsx: 'text-blue-400', js: 'text-yellow-300', jsx: 'text-yellow-300', json: 'text-yellow-500', md: 'text-fg-muted', css: 'text-pink-400', scss: 'text-pink-400', html: 'text-orange-400', py: 'text-green-400', go: 'text-cyan-400', rs: 'text-orange-500', java: 'text-red-400', c: 'text-blue-500', cpp: 'text-blue-500', h: 'text-purple-400', }; function getFileIconColor(name: string): string { const ext = name.split('.').pop()?.toLowerCase() || ''; return fileIconColors[ext] || 'text-fg-muted'; } interface TreeNodeProps { node: FileTreeNode; depth: number; onFileSelect?: (path: string, name: string) => void; expandedPaths: Set; onToggleExpand: (path: string) => void; } function TreeNode({ node, depth, onFileSelect, expandedPaths, onToggleExpand }: TreeNodeProps) { const isExpanded = expandedPaths.has(node.path); const isDirectory = node.type === 'directory'; const hasChildren = isDirectory && node.children && node.children.length > 0; const handleClick = () => { if (isDirectory) { onToggleExpand(node.path); } else { onFileSelect?.(node.path, node.name); } }; return (
{/* 展开/折叠图标 */} {isDirectory ? ( {hasChildren ? ( isExpanded ? ( ) : ( ) ) : null} ) : ( )} {/* 文件/文件夹图标 */} {isDirectory ? ( isExpanded ? ( ) : ( ) ) : ( )} {/* 文件名 */} {node.name} {/* 子节点 */} {isDirectory && isExpanded && node.children && ( {node.children.map((child) => ( ))} )}
); } const STORAGE_KEY_EXPANDED = 'ai-assistant-file-explorer-expanded'; export function FileExplorer({ onFileSelect, className, workingDirectory }: FileExplorerProps) { const [tree, setTree] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // 从 localStorage 恢复展开状态 const [expandedPaths, setExpandedPaths] = useState>(() => { try { const saved = localStorage.getItem(STORAGE_KEY_EXPANDED); return saved ? new Set(JSON.parse(saved)) : new Set(); } catch { return new Set(); } }); // 加载文件树 const loadTree = useCallback(async () => { setLoading(true); setError(null); try { const result = await getFileTree('.', 4); if (result.success) { setTree(result.data.tree); // 保持已保存的展开状态(不重置) } else { setError('Failed to load file tree'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load file tree'); } finally { setLoading(false); } }, []); useEffect(() => { loadTree(); }, [loadTree]); const handleToggleExpand = useCallback((path: string) => { setExpandedPaths((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } // 保存到 localStorage localStorage.setItem(STORAGE_KEY_EXPANDED, JSON.stringify([...next])); return next; }); }, []); if (loading) { return (
); } if (error) { return (

{error}

); } return (
{/* Header */}
{workingDirectory || 'Files'} {/* Tooltip - 悬浮时显示完整路径 */} {workingDirectory && (

{workingDirectory}

)}
{/* Tree */}
{tree.length === 0 ? (

No files found

) : ( tree.map((node) => ( )) )}
); }