gaaaaaaaaaaa's picture
Publish trajectory_rag
a414867 verified
Raw History Blame Contribute Delete
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>
);
}