Download webapp/components/tree-visualizer/TreeLibraryRail.tsx from gaaaaaaaaaaa/trajectory_rag: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/gaaaaaaaaaaa/trajectory_rag/resolve/main/webapp/components/tree-visualizer/TreeLibraryRail.tsx
- Command line
-
hf download hf://gaaaaaaaaaaa/trajectory_rag/webapp/components/tree-visualizer/TreeLibraryRail.tsx
-
curl -L -o TreeLibraryRail.tsx https://huggingface.co/gaaaaaaaaaaa/trajectory_rag/resolve/main/webapp/components/tree-visualizer/TreeLibraryRail.tsx
10.5 kB
| "use client"; | |
| import { useEffect, useMemo, useState } from "react"; | |
| import { CheckSquare, FileJson, GitBranch, Moon, Plus, Square, Sun, Trash2, X } from "lucide-react"; | |
| import { Button } from "@/components/ui/Button"; | |
| import { Badge } from "@/components/ui/Badge"; | |
| import type { TreeSummary } from "@/lib/tree/types"; | |
| import { cn } from "@/lib/utils"; | |
| interface TreeLibraryRailProps { | |
| trees: TreeSummary[]; | |
| selectedTreeId: string | null; | |
| isLoading: boolean; | |
| isDeleting: boolean; | |
| darkMode: boolean; | |
| width: number; | |
| onResizeStart: (event: React.PointerEvent<HTMLButtonElement>) => void; | |
| onToggleDarkMode: () => void; | |
| onCreate: () => void; | |
| onSelect: (treeId: string) => void; | |
| onDelete: (treeId: string) => void; | |
| onBulkDelete: (treeIds: string[]) => void; | |
| onDeleteAll: () => void; | |
| } | |
| export function TreeLibraryRail({ | |
| trees, | |
| selectedTreeId, | |
| isLoading, | |
| isDeleting, | |
| darkMode, | |
| width, | |
| onResizeStart, | |
| onToggleDarkMode, | |
| onCreate, | |
| onSelect, | |
| onDelete, | |
| onBulkDelete, | |
| onDeleteAll | |
| }: TreeLibraryRailProps) { | |
| const [bulkDeleteMode, setBulkDeleteMode] = useState(false); | |
| const [selectedDeleteIds, setSelectedDeleteIds] = useState<Set<string>>(() => new Set()); | |
| useEffect(() => { | |
| const availableIds = new Set(trees.map((tree) => tree.id)); | |
| setSelectedDeleteIds((current) => { | |
| const next = new Set([...current].filter((treeId) => availableIds.has(treeId))); | |
| return next.size === current.size ? current : next; | |
| }); | |
| if (trees.length === 0) setBulkDeleteMode(false); | |
| }, [trees]); | |
| const selectedCount = selectedDeleteIds.size; | |
| const allSelected = useMemo( | |
| () => trees.length > 0 && trees.every((tree) => selectedDeleteIds.has(tree.id)), | |
| [selectedDeleteIds, trees] | |
| ); | |
| const toggleBulkDeleteMode = () => { | |
| setBulkDeleteMode((current) => { | |
| if (current) setSelectedDeleteIds(new Set()); | |
| return !current; | |
| }); | |
| }; | |
| const toggleDeleteSelection = (treeId: string) => { | |
| setSelectedDeleteIds((current) => { | |
| const next = new Set(current); | |
| if (next.has(treeId)) next.delete(treeId); | |
| else next.add(treeId); | |
| return next; | |
| }); | |
| }; | |
| const toggleSelectAll = () => { | |
| setSelectedDeleteIds(allSelected ? new Set() : new Set(trees.map((tree) => tree.id))); | |
| }; | |
| const deleteSelectedTrees = () => { | |
| const treeIds = [...selectedDeleteIds]; | |
| if (treeIds.length === 0) return; | |
| if (!window.confirm(`Delete ${treeIds.length} selected saved tree${treeIds.length === 1 ? "" : "s"}?`)) return; | |
| onBulkDelete(treeIds); | |
| setSelectedDeleteIds(new Set()); | |
| }; | |
| const deleteEveryTree = () => { | |
| if (trees.length === 0) return; | |
| if (!window.confirm(`Delete all ${trees.length} saved trees? This cannot be undone.`)) return; | |
| onDeleteAll(); | |
| setSelectedDeleteIds(new Set()); | |
| setBulkDeleteMode(false); | |
| }; | |
| return ( | |
| <aside | |
| style={{ width, maxWidth: "min(460px, calc(100vw - 56px))" }} | |
| className="relative z-30 flex h-screen min-w-[240px] shrink-0 flex-col border-r border-paper-300 bg-white/82 shadow-panel backdrop-blur-xl" | |
| > | |
| <button | |
| type="button" | |
| aria-label="Resize tree library rail" | |
| onPointerDown={onResizeStart} | |
| className="absolute -right-1 top-0 z-20 h-full w-2 cursor-col-resize rounded-full transition hover:bg-claude-300/40 focus-visible:bg-claude-300/50 focus-visible:outline-none" | |
| /> | |
| <div className="flex h-16 items-center justify-between border-b border-paper-300 px-4"> | |
| <div className="flex min-w-0 items-center gap-3"> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| onClick={onToggleDarkMode} | |
| title={darkMode ? "Switch to light mode" : "Switch to dark mode"} | |
| className="shrink-0" | |
| > | |
| {darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />} | |
| </Button> | |
| <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-claude-500 text-white shadow-panel"> | |
| <GitBranch className="h-4 w-4" /> | |
| </div> | |
| <div className="min-w-0"> | |
| <p className="text-sm font-semibold text-ink-900">Tree Visualizer</p> | |
| <p className="text-xs text-ink-500">trajectory saves</p> | |
| </div> | |
| </div> | |
| <Button size="icon" variant="ghost" onClick={onCreate} title="Create tree" className="shrink-0"> | |
| <Plus className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| <div className="border-b border-paper-200 px-4 py-3"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <div className="flex min-w-0 items-center gap-2 text-xs font-semibold uppercase text-ink-500"> | |
| <FileJson className="h-3.5 w-3.5 shrink-0" /> | |
| <span className="truncate">/saves</span> | |
| </div> | |
| <Badge status={trees.length ? "valid" : "pending"}>{trees.length} trees</Badge> | |
| </div> | |
| <div className="mt-3 flex flex-wrap items-center gap-2"> | |
| <Button | |
| size="sm" | |
| variant={bulkDeleteMode ? "primary" : "secondary"} | |
| onClick={toggleBulkDeleteMode} | |
| disabled={trees.length === 0 || isDeleting} | |
| className="min-w-0 flex-1" | |
| > | |
| {bulkDeleteMode ? <X className="h-3.5 w-3.5" /> : <CheckSquare className="h-3.5 w-3.5" />} | |
| {bulkDeleteMode ? "Cancel" : "Bulk Delete"} | |
| </Button> | |
| <Button | |
| size="sm" | |
| variant="ghost" | |
| onClick={deleteEveryTree} | |
| disabled={trees.length === 0 || isDeleting} | |
| className="min-w-0 flex-1 text-red-600 hover:text-red-700" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| Delete All | |
| </Button> | |
| </div> | |
| {bulkDeleteMode && ( | |
| <div className="tree-delete-panel mt-3 rounded-lg p-2"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <button | |
| type="button" | |
| onClick={toggleSelectAll} | |
| className="tree-delete-inline-action inline-flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium transition" | |
| > | |
| {allSelected ? <CheckSquare className="h-3.5 w-3.5" /> : <Square className="h-3.5 w-3.5" />} | |
| {allSelected ? "Clear all" : "Select all"} | |
| </button> | |
| <Button | |
| size="sm" | |
| variant="danger" | |
| onClick={deleteSelectedTrees} | |
| disabled={selectedCount === 0 || isDeleting} | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| Delete {selectedCount} | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="scrollbar-thin flex-1 space-y-2 overflow-auto p-3"> | |
| {isLoading ? ( | |
| <div className="space-y-2"> | |
| {[1, 2, 3].map((item) => ( | |
| <div key={item} className="h-20 animate-pulse rounded-lg border border-paper-200 bg-paper-50" /> | |
| ))} | |
| </div> | |
| ) : trees.length === 0 ? ( | |
| <button | |
| onClick={onCreate} | |
| className="flex w-full flex-col items-start rounded-lg border border-dashed border-claude-300 bg-claude-50/70 p-4 text-left transition hover:bg-claude-50" | |
| > | |
| <span className="text-sm font-semibold text-ink-900">No saved trees</span> | |
| <span className="mt-1 text-xs leading-5 text-ink-500">Create or import a trajectory trace to populate this rail.</span> | |
| </button> | |
| ) : ( | |
| trees.map((tree) => { | |
| const isMarkedForDelete = selectedDeleteIds.has(tree.id); | |
| return ( | |
| <div | |
| key={tree.id} | |
| className={cn( | |
| "group rounded-lg border bg-white/76 p-3 transition", | |
| bulkDeleteMode && "tree-delete-card cursor-pointer", | |
| !bulkDeleteMode && selectedTreeId === tree.id | |
| ? "border-claude-300 shadow-panel" | |
| : "border-paper-200 hover:border-claude-200 hover:bg-claude-50/60", | |
| bulkDeleteMode && isMarkedForDelete && "tree-delete-card-selected shadow-sm" | |
| )} | |
| > | |
| <button | |
| type="button" | |
| className="w-full text-left" | |
| onClick={() => (bulkDeleteMode ? toggleDeleteSelection(tree.id) : onSelect(tree.id))} | |
| > | |
| <div className="flex items-start justify-between gap-2"> | |
| {bulkDeleteMode && ( | |
| <span className="tree-delete-check mt-0.5 shrink-0"> | |
| {isMarkedForDelete ? <CheckSquare className="h-4 w-4" /> : <Square className="h-4 w-4" />} | |
| </span> | |
| )} | |
| <div className="min-w-0 flex-1"> | |
| <p className="truncate text-sm font-semibold text-ink-900">{tree.title}</p> | |
| <p className="mt-1 break-all whitespace-normal font-mono text-[11px] leading-4 text-ink-400">{tree.id}</p> | |
| </div> | |
| <div className="shrink-0"> | |
| <Badge | |
| status={ | |
| tree.validationStatus === "error" | |
| ? "error" | |
| : tree.validationStatus === "warning" | |
| ? "warning" | |
| : "valid" | |
| } | |
| > | |
| {tree.validationStatus} | |
| </Badge> | |
| </div> | |
| </div> | |
| <div className="mt-3 flex items-center justify-between text-xs text-ink-500"> | |
| <span>{tree.nodeCount} nodes</span> | |
| <span>{new Date(tree.updatedAt).toLocaleDateString()}</span> | |
| </div> | |
| </button> | |
| <div className={cn("mt-2 flex justify-end opacity-0 transition group-hover:opacity-100", bulkDeleteMode && "hidden")}> | |
| <Button | |
| size="sm" | |
| variant="ghost" | |
| onClick={() => onDelete(tree.id)} | |
| title="Delete tree" | |
| className="text-red-600 hover:text-red-700" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| Delete | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </aside> | |
| ); | |
| } | |