- COMP-139: Command palette for quick navigation - COMP-140: Accessibility improvements - COMP-141: Scroll animations with animate-on-scroll component - COMP-143: Demo workspace with seed data and demo banner - COMP-145: Keyboard navigation and shortcuts help Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { signOut } from "next-auth/react";
|
|
import { Command } from "cmdk";
|
|
import {
|
|
Activity,
|
|
GitBranch,
|
|
Key,
|
|
Settings,
|
|
LogOut,
|
|
Plus,
|
|
Search,
|
|
ArrowRight,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface RecentTrace {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
startedAt: string;
|
|
}
|
|
|
|
export function CommandPalette() {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
const [recentTraces, setRecentTraces] = useState<RecentTrace[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const fetchRecentTraces = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/traces?limit=5", { cache: "no-store" });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setRecentTraces(data.traces ?? []);
|
|
}
|
|
} catch {
|
|
// Silently fail -- palette still works for navigation
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
fetchRecentTraces();
|
|
}
|
|
}, [open, fetchRecentTraces]);
|
|
|
|
useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
e.preventDefault();
|
|
setOpen((prev) => !prev);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, []);
|
|
|
|
function runCommand(command: () => void) {
|
|
setOpen(false);
|
|
command();
|
|
}
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[100]">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={() => setOpen(false)}
|
|
/>
|
|
|
|
{/* Palette */}
|
|
<div className="absolute inset-0 flex items-start justify-center pt-[20vh] px-4">
|
|
<Command
|
|
className="w-full max-w-xl rounded-xl border border-neutral-800 bg-neutral-900/95 backdrop-blur-xl shadow-2xl shadow-black/40 overflow-hidden"
|
|
loop
|
|
>
|
|
{/* Search input */}
|
|
<div className="flex items-center gap-3 border-b border-neutral-800 px-4">
|
|
<Search className="w-4 h-4 text-neutral-500 shrink-0" />
|
|
<Command.Input
|
|
placeholder="Search traces, navigate, or run actions..."
|
|
className="w-full py-4 bg-transparent text-sm text-neutral-100 placeholder-neutral-500 outline-none"
|
|
autoFocus
|
|
/>
|
|
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-1 rounded bg-neutral-800 border border-neutral-700 text-[10px] font-mono text-neutral-500">
|
|
ESC
|
|
</kbd>
|
|
</div>
|
|
|
|
{/* Results */}
|
|
<Command.List className="max-h-80 overflow-y-auto p-2">
|
|
<Command.Empty className="py-8 text-center text-sm text-neutral-500">
|
|
No results found.
|
|
</Command.Empty>
|
|
|
|
{/* Recent Traces */}
|
|
{recentTraces.length > 0 && (
|
|
<Command.Group
|
|
heading="Recent Traces"
|
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500"
|
|
>
|
|
{loading ? (
|
|
<div className="px-2 py-3 text-xs text-neutral-500">
|
|
Loading traces...
|
|
</div>
|
|
) : (
|
|
recentTraces.map((trace) => (
|
|
<Command.Item
|
|
key={trace.id}
|
|
value={`trace ${trace.name} ${trace.id}`}
|
|
onSelect={() =>
|
|
runCommand(() =>
|
|
router.push(`/dashboard/traces/${trace.id}`)
|
|
)
|
|
}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer transition-colors",
|
|
"text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400"
|
|
)}
|
|
>
|
|
<Activity className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1 truncate">{trace.name}</span>
|
|
<span
|
|
className={cn(
|
|
"text-xs px-1.5 py-0.5 rounded",
|
|
trace.status === "COMPLETED" &&
|
|
"bg-emerald-500/10 text-emerald-400",
|
|
trace.status === "ERROR" &&
|
|
"bg-red-500/10 text-red-400",
|
|
trace.status === "RUNNING" &&
|
|
"bg-amber-500/10 text-amber-400"
|
|
)}
|
|
>
|
|
{trace.status.toLowerCase()}
|
|
</span>
|
|
<ArrowRight className="w-3.5 h-3.5 shrink-0 opacity-0 group-data-[selected=true]:opacity-100" />
|
|
</Command.Item>
|
|
))
|
|
)}
|
|
</Command.Group>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<Command.Group
|
|
heading="Navigation"
|
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500"
|
|
>
|
|
<Command.Item
|
|
value="Dashboard Traces"
|
|
onSelect={() =>
|
|
runCommand(() => router.push("/dashboard"))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400 transition-colors"
|
|
>
|
|
<Activity className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">Dashboard</span>
|
|
</Command.Item>
|
|
|
|
<Command.Item
|
|
value="Decisions"
|
|
onSelect={() =>
|
|
runCommand(() => router.push("/dashboard/decisions"))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400 transition-colors"
|
|
>
|
|
<GitBranch className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">Decisions</span>
|
|
</Command.Item>
|
|
|
|
<Command.Item
|
|
value="API Keys"
|
|
onSelect={() =>
|
|
runCommand(() => router.push("/dashboard/keys"))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400 transition-colors"
|
|
>
|
|
<Key className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">API Keys</span>
|
|
</Command.Item>
|
|
|
|
<Command.Item
|
|
value="Settings"
|
|
onSelect={() =>
|
|
runCommand(() => router.push("/dashboard/settings"))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400 transition-colors"
|
|
>
|
|
<Settings className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">Settings</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
{/* Actions */}
|
|
<Command.Group
|
|
heading="Actions"
|
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500"
|
|
>
|
|
<Command.Item
|
|
value="Create New API Key"
|
|
onSelect={() =>
|
|
runCommand(() => router.push("/dashboard/keys"))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-emerald-500/10 data-[selected=true]:text-emerald-400 transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">New API Key</span>
|
|
</Command.Item>
|
|
|
|
<Command.Item
|
|
value="Sign Out Logout"
|
|
onSelect={() =>
|
|
runCommand(() => signOut({ callbackUrl: "/" }))
|
|
}
|
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm cursor-pointer text-neutral-300 data-[selected=true]:bg-red-500/10 data-[selected=true]:text-red-400 transition-colors"
|
|
>
|
|
<LogOut className="w-4 h-4 shrink-0 text-neutral-500" />
|
|
<span className="flex-1">Logout</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
</Command.List>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between border-t border-neutral-800 px-4 py-2.5">
|
|
<div className="flex items-center gap-3 text-[11px] text-neutral-500">
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="px-1.5 py-0.5 rounded bg-neutral-800 border border-neutral-700 font-mono">
|
|
↑↓
|
|
</kbd>
|
|
Navigate
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="px-1.5 py-0.5 rounded bg-neutral-800 border border-neutral-700 font-mono">
|
|
↵
|
|
</kbd>
|
|
Select
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="px-1.5 py-0.5 rounded bg-neutral-800 border border-neutral-700 font-mono">
|
|
esc
|
|
</kbd>
|
|
Close
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Command>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|