import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { createPortal } from "react-dom"; import type { ChatSummary, Doc } from "../../api/types"; import { DocPicker } from "../DocPicker"; import { ChevronIcon } from "../icons/Icons"; /** Pick posts from a dropdown: a button that says how many are chosen, and a panel under it with the search box and the * ticks. Replaces the list that would otherwise sit under the source dropdown. `single` picks one post from the whole * archive (the picker loads the list itself). */ /** A button that opens a panel under it (or above it when the window is short below). Used by the posts and chats menus. */ export function MenuPanel({ label, disabled = false, ariaLabel, children }: { label: string; disabled?: boolean; ariaLabel: string; children: ReactNode; }) { const [open, setOpen] = useState(false); const [place, setPlace] = useState({}); const button = useRef(null); const panel = useRef(null); // Under the button, as wide as it (at least 440px), or above it when the window is short below. useLayoutEffect(() => { if (!open || !button.current) return; const r = button.current.getBoundingClientRect(); const width = Math.min(Math.max(r.width, 440), window.innerWidth - 24); const left = Math.min(Math.max(12, r.left), window.innerWidth - width - 12); const below = window.innerHeight - r.bottom; setPlace(below >= 360 || below >= r.top ? { top: r.bottom + 6, left, width, maxHeight: Math.max(240, below - 18) } : { bottom: window.innerHeight - r.top + 6, left, width, maxHeight: Math.max(240, r.top - 18) }); }, [open]); useEffect(() => { if (!open) return; const away = (e: MouseEvent) => { const t = e.target as Node; if (!panel.current?.contains(t) && !button.current?.contains(t)) setOpen(false); }; const close = () => setOpen(false); document.addEventListener("mousedown", away); window.addEventListener("resize", close); return () => { document.removeEventListener("mousedown", away); window.removeEventListener("resize", close); }; }, [open]); return ( <> {open && createPortal(
{children}
, document.body, )} ); } /** Pick posts from a dropdown: a button that says how many are chosen, and a panel under it with the search box and the * ticks. `single` picks one post from the whole archive (the picker loads the list itself). */ export function PostsMenu({ docs, selected, onChange, max, single = false, loading = false }: { docs?: Doc[] | null; selected: string[]; onChange: (ids: string[]) => void; max?: number; single?: boolean; loading?: boolean; }) { const total = docs?.length ?? 0; const label = loading ? "Loading posts" : single ? (selected.length ? "1 post selected" : "Pick a post") : selected.length === 0 ? "Posts: none" : `Posts: ${selected.length} of ${total}`; return ( {single ? : } {!single && max && Number.isFinite(max) &&

Up to {max} posts. They are combined into one.

}
); } /** Pick notebook chats from a dropdown, the same way: a button with the count, and the chats to tick in a panel. */ export function ChatsMenu({ chats, selected, onChange, max }: { chats: ChatSummary[]; selected: string[]; onChange: (ids: string[]) => void; max: number; }) { const toggle = (id: string) => onChange(selected.includes(id) ? selected.filter((x) => x !== id) : selected.length < max ? [...selected, id] : selected); const label = chats.length === 0 ? "Chats: none yet" : selected.length === 0 ? "Chats: none" : `Chats: ${selected.length} of ${chats.length}`; return (
    {chats.map((c) => (
  • ))}

Up to {max} chats. Questions come from what was said in them.

); }