"use client"; import { useEffect, useRef } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref"; import { CheckCircleIcon, FolderIcon, LayersIcon } from "@/components/icons"; import { useI18n } from "@/lib/i18n"; import { springSheet } from "@/lib/motion"; import { useIsPhone } from "@/lib/use-phone"; import { deckHasNotes } from "@/lib/chat-context"; import type { ChatScope } from "@/lib/chat"; import type { Deck, Folder } from "@/lib/schemas"; import { cn } from "@/lib/utils"; /** * Scope picker for the context chip: All notes · selected folders/decks · * None (general chat). Only what is selected here can ever travel as context. */ export function ContextPicker({ open, onClose, scope, onChange, decks, folders, }: { open: boolean; onClose: () => void; scope: ChatScope; onChange: (scope: ChatScope) => void; decks: Deck[]; folders: Folder[]; }) { const { t, plural } = useI18n(); const isPhone = useIsPhone(); const reduceMotion = useReducedMotion(); const panel = useRef(null); useEffect(() => { if (!open) return; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); /* Desktop dropdown: a press anywhere outside closes it, except on the trigger chips, which toggle their own popover (see Composer). */ useEffect(() => { if (!open || isPhone) return; const onDown = (event: PointerEvent) => { const target = event.target; if (!(target instanceof Element)) return; if (panel.current?.contains(target)) return; if (target.closest("[data-picker-trigger]")) return; onClose(); }; document.addEventListener("pointerdown", onDown); return () => document.removeEventListener("pointerdown", onDown); }, [open, isPhone, onClose]); const selected = scope.kind === "decks" ? new Set(scope.deckIds) : new Set(); const choose = (next: ChatScope) => { onChange(next); if (next.kind !== "decks") onClose(); }; const toggleDeck = (deckId: string) => { const next = new Set(selected); if (next.has(deckId)) next.delete(deckId); else next.add(deckId); onChange({ kind: "decks", deckIds: [...next] }); }; const toggleFolder = (folderId: string) => { const inFolder = decks.filter((deck) => deck.folderId === folderId).map((deck) => deck.id); const allSelected = inFolder.length > 0 && inFolder.every((id) => selected.has(id)); const next = new Set(selected); for (const id of inFolder) { if (allSelected) next.delete(id); else next.add(id); } onChange({ kind: "decks", deckIds: [...next] }); }; const modeRow = ( value: ChatScope["kind"], title: string, note: string, icon: React.ReactNode, ) => ( ); const body = (
{modeRow("all", t("chat.scope.all"), t("chat.scope.allNote"), )} {modeRow("decks", t("chat.scope.pick"), t("chat.scope.pickNote"), )} {scope.kind === "decks" ? (
{folders.map((folder) => { const inFolder = decks.filter((deck) => deck.folderId === folder.id); if (inFolder.length === 0) return null; const allSelected = inFolder.every((deck) => selected.has(deck.id)); return ( ); })} {decks.map((deck) => { const active = selected.has(deck.id); return ( ); })}
) : null} {modeRow("none", t("chat.scope.none"), t("chat.scope.noneNote"), 💬)}
); if (isPhone) { return ( {open ? (

{t("chat.scope.title")}

{body}

{t("chat.scope.note")}

) : null} ); } return ( {open ? (

{t("chat.scope.title")}

{body}

{t("chat.scope.note")}

) : null}
); }