Spaces:
Sleeping
Sleeping
Download frontend/src/components/DirectoryTree.tsx from Wothmag07/Document-MCP: direct link, hf CLI and curl.
- Browser
- Download file 4.83 kB
-
https://huggingface.co/spaces/Wothmag07/Document-MCP/resolve/main/frontend/src/components/DirectoryTree.tsx
- Command line
-
hf download hf://spaces/Wothmag07/Document-MCP/frontend/src/components/DirectoryTree.tsx
-
curl -L -o DirectoryTree.tsx https://huggingface.co/spaces/Wothmag07/Document-MCP/resolve/main/frontend/src/components/DirectoryTree.tsx
4.83 kB
| /** | |
| * T077: Directory tree component with collapsible folders | |
| */ | |
| import { useState, useMemo } from 'react'; | |
| import { ChevronRight, ChevronDown, Folder, File } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import type { NoteSummary } from '@/types/note'; | |
| import { cn } from '@/lib/utils'; | |
| interface TreeNode { | |
| name: string; | |
| path: string; | |
| type: 'file' | 'folder'; | |
| children?: TreeNode[]; | |
| note?: NoteSummary; | |
| } | |
| interface DirectoryTreeProps { | |
| notes: NoteSummary[]; | |
| selectedPath?: string; | |
| onSelectNote: (path: string) => void; | |
| } | |
| /** | |
| * Build a tree structure from flat list of note paths | |
| */ | |
| function buildTree(notes: NoteSummary[]): TreeNode[] { | |
| const root: TreeNode = { name: '', path: '', type: 'folder', children: [] }; | |
| for (const note of notes) { | |
| const parts = note.note_path.split('/'); | |
| let current = root; | |
| // Navigate/create folders | |
| for (let i = 0; i < parts.length - 1; i++) { | |
| const folderName = parts[i]; | |
| const folderPath = parts.slice(0, i + 1).join('/'); | |
| let folder = current.children?.find( | |
| (child) => child.name === folderName && child.type === 'folder' | |
| ); | |
| if (!folder) { | |
| folder = { | |
| name: folderName, | |
| path: folderPath, | |
| type: 'folder', | |
| children: [], | |
| }; | |
| current.children = current.children || []; | |
| current.children.push(folder); | |
| } | |
| current = folder; | |
| } | |
| // Add file | |
| const fileName = parts[parts.length - 1]; | |
| current.children = current.children || []; | |
| current.children.push({ | |
| name: fileName, | |
| path: note.note_path, | |
| type: 'file', | |
| note, | |
| }); | |
| } | |
| // Sort children: folders first, then files, alphabetically | |
| const sortChildren = (node: TreeNode) => { | |
| if (node.children) { | |
| node.children.sort((a, b) => { | |
| if (a.type !== b.type) { | |
| return a.type === 'folder' ? -1 : 1; | |
| } | |
| return a.name.localeCompare(b.name); | |
| }); | |
| node.children.forEach(sortChildren); | |
| } | |
| }; | |
| sortChildren(root); | |
| return root.children || []; | |
| } | |
| interface TreeNodeItemProps { | |
| node: TreeNode; | |
| depth: number; | |
| selectedPath?: string; | |
| onSelectNote: (path: string) => void; | |
| } | |
| function TreeNodeItem({ node, depth, selectedPath, onSelectNote }: TreeNodeItemProps) { | |
| const [isOpen, setIsOpen] = useState(depth < 2); // Auto-expand first 2 levels | |
| if (node.type === 'folder') { | |
| return ( | |
| <div> | |
| <Button | |
| variant="ghost" | |
| className={cn( | |
| "w-full justify-start font-normal px-2 h-8", | |
| "hover:bg-accent" | |
| )} | |
| style={{ paddingLeft: `${depth * 12 + 8}px` }} | |
| onClick={() => setIsOpen(!isOpen)} | |
| > | |
| {isOpen ? ( | |
| <ChevronDown className="h-4 w-4 mr-1 shrink-0" /> | |
| ) : ( | |
| <ChevronRight className="h-4 w-4 mr-1 shrink-0" /> | |
| )} | |
| <Folder className="h-4 w-4 mr-2 shrink-0 text-muted-foreground" /> | |
| <span className="truncate">{node.name}</span> | |
| </Button> | |
| {isOpen && node.children && ( | |
| <div> | |
| {node.children.map((child) => ( | |
| <TreeNodeItem | |
| key={child.path} | |
| node={child} | |
| depth={depth + 1} | |
| selectedPath={selectedPath} | |
| onSelectNote={onSelectNote} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // File node | |
| const isSelected = node.path === selectedPath; | |
| // Remove .md extension for display | |
| const displayName = node.name.replace(/\.md$/, ''); | |
| return ( | |
| <Button | |
| variant="ghost" | |
| className={cn( | |
| "w-full justify-start font-normal px-2 h-8", | |
| "hover:bg-accent", | |
| isSelected && "bg-accent" | |
| )} | |
| style={{ paddingLeft: `${depth * 12 + 8}px` }} | |
| onClick={() => onSelectNote(node.path)} | |
| > | |
| <File className="h-4 w-4 mr-2 shrink-0 text-muted-foreground" /> | |
| <span className="truncate">{displayName}</span> | |
| </Button> | |
| ); | |
| } | |
| export function DirectoryTree({ notes, selectedPath, onSelectNote }: DirectoryTreeProps) { | |
| const tree = useMemo(() => buildTree(notes), [notes]); | |
| if (notes.length === 0) { | |
| return ( | |
| <div className="p-4 text-sm text-muted-foreground text-center"> | |
| No notes found. Create your first note to get started. | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <ScrollArea className="h-full"> | |
| <div className="py-2"> | |
| {tree.map((node) => ( | |
| <TreeNodeItem | |
| key={node.path} | |
| node={node} | |
| depth={0} | |
| selectedPath={selectedPath} | |
| onSelectNote={onSelectNote} | |
| /> | |
| ))} | |
| </div> | |
| </ScrollArea> | |
| ); | |
| } | |