"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import useSWR from "swr"; import { Search, MessageSquare, Plus, Columns2, Telescope, FlaskConical, Brain, BarChart3, Settings, CornerDownLeft, Sparkles } from "lucide-react"; import { fetcher } from "@/lib/client/fetcher"; import type { ConversationDTO } from "@/lib/client/types"; import { Dialog, cn, Kbd, Spinner } from "./ui"; import { resetDraft } from "@/lib/client/use-chat"; interface Result { id: string; conversation_id: string; role: string; model_id: string | null; created_at: string; title: string; snippet: string } export function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) { const router = useRouter(); const [q, setQ] = useState(""); const [idx, setIdx] = useState(0); const [debounced, setDebounced] = useState(""); const inputRef = useRef(null); useEffect(() => { const t = setTimeout(() => setDebounced(q.trim()), 220); return () => clearTimeout(t); }, [q]); useEffect(() => { if (open) { setQ(""); setIdx(0); setTimeout(() => inputRef.current?.focus(), 30); } }, [open]); const { data: convs } = useSWR<{ conversations: ConversationDTO[] }>(open ? "/api/conversations?limit=60" : null, fetcher); const { data: search, isLoading } = useSWR<{ results: Result[] }>(open && debounced.length >= 2 ? `/api/search?q=${encodeURIComponent(debounced)}` : null, fetcher, { keepPreviousData: true }); const actions = useMemo(() => [ { id: "new", label: "Nouvelle conversation", icon: Plus, run: () => { resetDraft(); router.push("/"); } }, { id: "compare", label: "Comparer des modèles", icon: Columns2, run: () => router.push("/compare") }, { id: "research", label: "Recherche approfondie", icon: Telescope, run: () => router.push("/research") }, { id: "models", label: "Laboratoire de modèles", icon: FlaskConical, run: () => router.push("/models") }, { id: "memory", label: "Mémoire", icon: Brain, run: () => router.push("/memory") }, { id: "usage", label: "Coûts & usage", icon: BarChart3, run: () => router.push("/usage") }, { id: "settings", label: "Réglages", icon: Settings, run: () => router.push("/settings") }, { id: "theme", label: "Basculer le thème", icon: Sparkles, run: () => { const cur = document.documentElement.dataset.theme; import("@/lib/client/prefs").then(m => m.setPrefs({ theme: cur === "light" ? "dark" : "light" })); } }, ], [router]); const items = useMemo(() => { const ql = q.trim().toLowerCase(); const acts = actions.filter(a => !ql || a.label.toLowerCase().includes(ql)).map(a => ({ kind: "action" as const, key: `a:${a.id}`, label: a.label, icon: a.icon, run: a.run })); const cs = (convs?.conversations ?? []).filter(c => !ql || c.title.toLowerCase().includes(ql)).slice(0, ql ? 8 : 6).map(c => ({ kind: "conv" as const, key: `c:${c.id}`, label: c.title, sub: c.project_name ?? undefined, icon: MessageSquare, run: () => router.push(`/c/${c.id}`) })); const ms = (search?.results ?? []).map(r => ({ kind: "msg" as const, key: `m:${r.id}`, label: r.title, sub: r.snippet.replace(/<\/?b>/g, ""), meta: r.model_id ?? r.role, icon: Search, run: () => router.push(`/c/${r.conversation_id}#${r.id}`) })); return [...(ql ? [] : acts.slice(0, 4)), ...cs, ...ms, ...(ql ? acts : [])]; }, [q, actions, convs, search, router]); useEffect(() => { setIdx(0); }, [items.length, q]); function onKey(e: React.KeyboardEvent) { if (e.key === "ArrowDown") { e.preventDefault(); setIdx(i => Math.min(items.length - 1, i + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setIdx(i => Math.max(0, i - 1)); } else if (e.key === "Enter") { e.preventDefault(); const it = items[idx]; if (it) { it.run(); onClose(); } } } return (
setQ(e.target.value)} onKeyDown={onKey} placeholder="Rechercher des conversations, des messages, des actions…" className="flex-1 bg-transparent text-[15px] outline-none placeholder:text-muted" /> {isLoading ? : esc}
{items.length === 0 &&
{debounced.length >= 2 ? "Aucun résultat" : "Tape pour rechercher (texte intégral + sémantique)"}
} {items.map((it, i) => ( ))}
); }