trajectory_rag / webapp /components /tree-visualizer /CompactTreeNavigator.tsx
gaaaaaaaaaaa's picture
Publish trajectory_rag
a414867 verified
Raw History Blame Contribute Delete
6.72 kB
"use client";
import { ArrowDown, ArrowUp, BookOpen, ChevronLeft, ChevronRight, Crosshair, Pause, Rows3 } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { getTreeNavigationState } from "@/lib/tree/navigation";
import { visibleNodeIdsForStep } from "@/lib/tree/playback";
import type { TrajectoryTree } from "@/lib/tree/types";
import { cn } from "@/lib/utils";
export type CompactNavigatorFocusMode = "node" | "floor";
interface CompactTreeNavigatorProps {
tree: TrajectoryTree | null;
revealCount: number;
selectedNodeId: string | null;
autoReadEnabled: boolean;
onNavigate: (nodeId: string, mode: CompactNavigatorFocusMode) => void;
onToggleAutoRead: () => void;
}
function shortLabel(value: string) {
const trimmed = value.trim();
if (trimmed.length <= 46) return trimmed;
return `${trimmed.slice(0, 43)}...`;
}
export function CompactTreeNavigator({
tree,
revealCount,
selectedNodeId,
autoReadEnabled,
onNavigate,
onToggleAutoRead
}: CompactTreeNavigatorProps) {
if (!tree || tree.nodes.length === 0) return null;
const visibleIds = visibleNodeIdsForStep(tree.nodes, revealCount);
const navigation = getTreeNavigationState(tree.nodes, visibleIds, selectedNodeId);
const current = navigation.currentNode;
if (!current) return null;
const currentFloorPosition = navigation.floors.indexOf(navigation.currentDepth) + 1;
const totalFloors = navigation.floors.length;
return (
<div className="pointer-events-none absolute bottom-5 left-1/2 z-20 w-[min(calc(100%-2rem),900px)] -translate-x-1/2">
<div className="pointer-events-auto rounded-xl border border-paper-300 bg-white/86 p-2.5 shadow-panel backdrop-blur-xl">
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-paper-50 p-1">
<Button
size="icon"
variant="ghost"
disabled={!navigation.previousNode}
title="View previous node"
onClick={() => navigation.previousNode && onNavigate(navigation.previousNode.id, "node")}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
disabled={!navigation.nextNode}
title="View next node"
onClick={() => navigation.nextNode && onNavigate(navigation.nextNode.id, "node")}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="rounded-md bg-claude-500 px-2 py-1 font-mono text-[11px] font-semibold text-white">
{current.id}
</span>
<span className="truncate text-xs font-semibold text-ink-700">{current.kind}</span>
<span className="hidden text-xs text-ink-400 sm:inline">
{navigation.currentIndex + 1}/{navigation.orderedNodes.length}
</span>
</div>
<div className="mt-1 truncate text-xs text-ink-500">{shortLabel(current.goal || "Untitled node")}</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
<div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-paper-50 p-1">
<Button
size="icon"
variant="ghost"
disabled={!navigation.previousFloorNode}
title="View previous floor"
onClick={() => navigation.previousFloorNode && onNavigate(navigation.previousFloorNode.id, "floor")}
>
<ArrowUp className="h-4 w-4" />
</Button>
<div className="flex h-9 items-center gap-1 px-2 text-xs font-medium text-ink-600">
<Rows3 className="h-4 w-4 text-claude-500" />
{currentFloorPosition}/{totalFloors}
</div>
<Button
size="icon"
variant="ghost"
disabled={!navigation.nextFloorNode}
title="View next floor"
onClick={() => navigation.nextFloorNode && onNavigate(navigation.nextFloorNode.id, "floor")}
>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-1 rounded-lg border border-paper-300 bg-white/82 p-1 shadow-sm">
<Button
size="sm"
variant="ghost"
title="Recenter current node"
onClick={() => onNavigate(current.id, "node")}
>
<Crosshair className="h-4 w-4" />
Focus
</Button>
<Button
className="min-w-[104px]"
size="sm"
variant={autoReadEnabled ? "primary" : "secondary"}
title={autoReadEnabled ? "Pause auto read" : "Auto read notes"}
onClick={onToggleAutoRead}
>
{autoReadEnabled ? <Pause className="h-4 w-4" /> : <BookOpen className="h-4 w-4" />}
{autoReadEnabled ? "Pause" : "Auto read"}
</Button>
</div>
</div>
</div>
{navigation.sameFloorNodes.length > 1 && (
<div className="mt-2 flex items-center gap-2 border-t border-paper-200 pt-2">
<span className="shrink-0 text-[11px] font-semibold uppercase text-ink-400">Neighbors</span>
<div className="flex min-w-0 flex-1 gap-1 overflow-x-auto scrollbar-thin">
{navigation.sameFloorNodes.map((node) => (
<button
key={node.id}
type="button"
className={cn(
"shrink-0 rounded-full border px-2.5 py-1 text-xs font-semibold transition",
node.id === current.id
? "border-claude-300 bg-claude-50 text-claude-700"
: "border-paper-300 bg-white/70 text-ink-500 hover:border-claude-300 hover:text-claude-700"
)}
title={node.goal || node.id}
onClick={() => onNavigate(node.id, "floor")}
>
{node.id}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}