"use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { Activity, ListChecks, PauseOctagon, Play, RefreshCw, RotateCcw, Copy as CopyIcon, type LucideIcon, } from "lucide-react"; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "yt-dlp-transcript-common/components/ui/command"; import { toast } from "yt-dlp-transcript-common/components/ui/sonner"; import { NAV_GROUPS } from "../lib/nav"; import { refreshAllChannelSnapshotsAction, syncAllChannelsAction, } from "../channels/actions"; import { runDuplicateDetectionAction } from "../review/actions"; import { retryAllFailedAction, drainAllAction } from "../jobs/actions"; import { pauseLaneAction, resumeLaneAction } from "../operations/actions"; // The editor's command-first spine. Rebuilt on cmdk (shadcn Command) over the // shared NAV_GROUPS (so it never drifts from the sidebar) plus a set of global // "run a thing" actions that fire a server action, toast the result, and refresh. // Long per-channel streaming jobs still live on their pages (Phase 4); the // palette runs the quick, no-argument pool/site actions you'd otherwise hunt for. type RunAction = { id: string; label: string; icon: LucideIcon; keywords?: string; confirm?: string; // Resolve to a success toast message; throw to surface an error toast. run: () => Promise; }; const GLOBAL_ACTIONS: RunAction[] = [ { id: "act-sync-all", label: "Sync all channels", icon: RefreshCw, keywords: "fetch new videos refresh metadata", run: async () => { const r = await syncAllChannelsAction(); const n = r.queued.length; const skip = r.skipped.length ? ` · ${r.skipped.length} skipped` : ""; return `Queued ${n} channel sync${n === 1 ? "" : "s"}${skip}`; }, }, { id: "act-refresh-reports", label: "Refresh all channel reports", icon: RefreshCw, keywords: "snapshot recompute stats", run: async () => { const r = await refreshAllChannelSnapshotsAction(); const n = r.queued.length; const skip = r.skipped.length ? ` · ${r.skipped.length} skipped` : ""; return `Refreshing ${n} report${n === 1 ? "" : "s"}${skip}`; }, }, { id: "act-detect-duplicates", label: "Detect duplicate shorts", icon: CopyIcon, keywords: "find reposts cross-platform", run: async () => { const r = await runDuplicateDetectionAction("shorts"); if (!r.ok) throw new Error(r.error); return `Found ${r.clusters} duplicate cluster${r.clusters === 1 ? "" : "s"} (${r.videosInClusters} videos)`; }, }, { id: "act-retry-failed", label: "Retry all failed jobs", icon: RotateCcw, keywords: "requeue errors", run: async () => { const r = await retryAllFailedAction(); return `Retried ${r.count} failed job${r.count === 1 ? "" : "s"}`; }, }, { id: "act-drain-all", label: "Drain all queued jobs", icon: ListChecks, keywords: "cancel clear queue", confirm: "Drain every queued job? Running jobs are left alone.", run: async () => { const r = await drainAllAction(); return `Drained ${r.count} job${r.count === 1 ? "" : "s"}`; }, }, { id: "act-pause-workers", label: "Pause all workers", icon: PauseOctagon, keywords: "stop transcription halt", run: async () => { const r = await pauseLaneAction("transcription"); if (!r.ok) throw new Error(r.error ?? "Failed to pause workers"); return "Paused all workers"; }, }, { id: "act-resume-workers", label: "Resume all workers", icon: Play, keywords: "start transcription continue", run: async () => { const r = await resumeLaneAction("transcription"); if (!r.ok) throw new Error(r.error ?? "Failed to resume workers"); return "Resumed all workers"; }, }, ]; function channelSlugFrom(pathname: string): string | null { const m = /^\/channels\/([^/?#]+)/.exec(pathname); return m ? decodeURIComponent(m[1]) : null; } const CHANNEL_FILTERS: { filter: string; label: string }[] = [ { filter: "needs_action", label: "Needs action" }, { filter: "failed", label: "Failed" }, { filter: "no_audio", label: "No audio" }, { filter: "downloaded_no_transcript", label: "No transcript" }, { filter: "untranscribable", label: "Untranscribable" }, { filter: "running", label: "Running" }, { filter: "all", label: "All (clear filter)" }, ]; export function CommandPalette() { const router = useRouter(); const pathname = usePathname() ?? "/"; const search = useSearchParams(); const [open, setOpen] = useState(false); const slug = channelSlugFrom(pathname); useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((prev) => !prev); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const go = useCallback( (href: string) => { setOpen(false); router.push(href); }, [router], ); const runAction = useCallback( (action: RunAction) => { setOpen(false); if (action.confirm && !window.confirm(action.confirm)) return; const toastId = toast.loading(`${action.label}…`); void (async () => { try { const message = await action.run(); toast.success(message, { id: toastId }); router.refresh(); } catch (err) { toast.error(`${action.label} failed`, { id: toastId, description: err instanceof Error ? err.message : String(err), }); } })(); }, [router], ); const setChannelFilter = useCallback( (filter: string) => { if (!slug) return; setOpen(false); const params = new URLSearchParams(search.toString()); if (filter === "all") params.delete("filter"); else params.set("filter", filter); params.delete("video"); const q = params.toString(); router.push(q ? `/channels/${slug}?${q}` : `/channels/${slug}`); }, [router, search, slug], ); return ( No matches. {slug && ( <> {CHANNEL_FILTERS.map((f) => ( setChannelFilter(f.filter)} > Filter: {f.label} ))} )} {GLOBAL_ACTIONS.map((a) => { const Icon = a.icon; return ( runAction(a)} > {a.label} ); })} {NAV_GROUPS.map((group) => ( {group.links.map((link) => { const Icon = link.icon; return ( go(link.href)} > {link.label} ); })} ))} ); }