import { useState, useEffect, useCallback, useRef } from "react"; import { Search, Plus, Check, Loader2, ExternalLink, Sparkles } from "lucide-react"; import { Modal } from "@/components/ui/Modal"; import { useTranslation } from "@/lib/i18n"; import { RECOMMENDED_PACKAGES } from "@/data/recommended-packages"; interface PackageSearchResult { name: string; description: string; version: string; downloads: number; link: string; } type PackageFilter = "all" | "installed" | "available"; type Tab = "recommended" | "search"; interface PackageBrowserProps { open: boolean; onClose: () => void; installed: Set; // ids like "npm:pkg-name" onInstall: (id: string) => void; } /** One package row with an install / installed control. */ function PackageRow({ name, description, link, installed, onInstall, }: { name: string; description: string; link?: string; installed: boolean; onInstall: () => void; }) { const { t } = useTranslation(); return (
{name}
{description && (

{description}

)}
{installed ? ( {t("settings.installed")} ) : ( )}
); } /** * Package browser modal with two tabs: * - Recommended: the curated list from src/data/recommended-packages.ts * - Search: fuzzy npm-registry search for any pi package * Installing writes "npm:" into settings.packages. */ export function PackageBrowser({ open, onClose, installed, onInstall }: PackageBrowserProps) { const { t } = useTranslation(); const [tab, setTab] = useState("recommended"); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [justAdded, setJustAdded] = useState>(new Set()); const [filter, setFilter] = useState("all"); const debounceRef = useRef(undefined); const runSearch = useCallback((q: string) => { setLoading(true); fetch(`/api/pi/packages/search?q=${encodeURIComponent(q)}`) .then((r) => r.json()) .then((d: { results?: PackageSearchResult[] }) => setResults(d.results ?? [])) .catch(() => setResults([])) .finally(() => setLoading(false)); }, []); // Load popular packages the first time the Search tab is opened; debounce typing. useEffect(() => { if (!open || tab !== "search") return; window.clearTimeout(debounceRef.current); debounceRef.current = window.setTimeout(() => runSearch(query), 350); return () => window.clearTimeout(debounceRef.current); }, [query, open, tab, runSearch]); const isInstalledId = (name: string) => { const id = `npm:${name}`; return installed.has(id) || justAdded.has(id); }; const handleInstall = (id: string) => { onInstall(id); setJustAdded((prev) => new Set(prev).add(id)); }; // Search-tab results after the installed/available filter. const filtered = results.filter((pkg) => { if (filter === "installed") return isInstalledId(pkg.name); if (filter === "available") return !isInstalledId(pkg.name); return true; }); const installedCount = results.filter((p) => isInstalledId(p.name)).length; return (
{/* Tabs */}
{(["recommended", "search"] as Tab[]).map((tKey) => ( ))}
{tab === "recommended" ? (
{RECOMMENDED_PACKAGES.map((pkg) => ( handleInstall(pkg.id)} /> ))}
) : ( <> {/* Search box */}
setQuery(e.target.value)} placeholder={t("settings.search_packages_placeholder")} className="w-full rounded-lg border py-2 pl-9 pr-3 text-sm outline-none focus:ring-1 focus:ring-blue-500" style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }} />
{/* Installed / available filter */}
{(["all", "available", "installed"] as PackageFilter[]).map((f) => { const count = f === "all" ? results.length : f === "installed" ? installedCount : results.length - installedCount; return ( ); })}
{/* Results */}
{loading && results.length === 0 ? (
) : filtered.length === 0 ? (

{t("settings.no_packages_found")}

) : ( filtered.map((pkg) => ( handleInstall(`npm:${pkg.name}`)} /> )) )}
)}
); }