import React, { useEffect, useContext, useId, useLayoutEffect, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { ReaderThemeContext } from "./theme.js"; import { CaretDown, Check, MagnifyingGlass, Sparkle, } from "@phosphor-icons/react"; export function ModelPicker({ choices, selected, onChange, onSettings, inline = false, }) { const theme = useContext(ReaderThemeContext); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const [position, setPosition] = useState(null); const trigger = useRef(null); const popup = useRef(null); const search = useRef(null); const id = useId(); const filtered = choices.filter((choice) => `${choice.name} ${choice.providerName}` .toLowerCase() .includes(query.toLowerCase()), ); const close = (restore = false) => { setOpen(false); if (restore) trigger.current?.focus(); }; const choose = (choice) => { if (!choice) return; onChange({ providerId: choice.providerId, model: choice.name }); close(true); }; useLayoutEffect(() => { if (!open || inline) return; const align = () => { const rect = trigger.current.getBoundingClientRect(); const width = Math.min(Math.max(rect.width, 300), window.innerWidth - 24); const height = Math.min(330, Math.max(180, rect.top - 24)); setPosition({ width, maxHeight: height, left: Math.max(12, Math.min(rect.left, window.innerWidth - width - 12)), bottom: window.innerHeight - rect.top + 8, }); }; align(); window.addEventListener("resize", align); window.addEventListener("scroll", align, true); return () => { window.removeEventListener("resize", align); window.removeEventListener("scroll", align, true); }; }, [open, inline]); useEffect(() => { if (!open) return; setQuery(""); setActive( Math.max( 0, choices.findIndex((choice) => choice.value === selected?.value), ), ); search.current?.focus(); const outside = (event) => { if ( !popup.current?.contains(event.target) && !trigger.current?.contains(event.target) ) close(); }; const escape = (event) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(true); } }; document.addEventListener("pointerdown", outside); document.addEventListener("keydown", escape, true); return () => { document.removeEventListener("pointerdown", outside); document.removeEventListener("keydown", escape, true); }; }, [open]); useEffect(() => { popup.current ?.querySelector('[aria-selected="true"]') ?.scrollIntoView({ block: "nearest" }); }, [active, query]); const popupContent = (
{filtered.map((choice, index) => ( ))} {!filtered.length && (

No matching models

)}
); return ( <> {open && (inline ? popupContent : createPortal(
{popupContent}
, document.body))} ); }