"use client"; // FrameworkSelector — persistent "agentic backend" dropdown that anchors // the docs experience. Opens a panel listing every registry integration // grouped by category. Selecting an entry navigates to `/`: // changing backends is a pivot into that framework's overview, not an // attempt to preserve the current page's feature slug. import React, { useEffect, useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { usePostHog } from "posthog-js/react"; import { useFramework } from "./framework-provider"; import { FrameworkLogo } from "./icons/framework-icons"; import { compareByDisplayOrder } from "@/lib/framework-order"; export interface FrameworkOption { slug: string; name: string; category: string; logo?: string | null; deployed: boolean; } export interface FrameworkSelectorProps { options: FrameworkOption[]; /** * Ordered category ids (from the registry) used to group entries in the * dropdown panel. Unknown categories fall through to "Other". */ categoryOrder: { id: string; name: string }[]; /** Extra wrapper class (positioning). */ className?: string; /** * Presentation flavor. * - `topbar` (default, legacy): compact pill sized for a horizontal bar. * - `sidebar`: full-width pill with integration logo left, name center, * chevron right — styled to match the docs.copilotkit.ai sidebar header. */ variant?: "topbar" | "sidebar"; } export function FrameworkSelector({ options, categoryOrder, className, variant = "topbar", }: FrameworkSelectorProps) { const router = useRouter(); const pathname = usePathname() ?? ""; const posthog = usePostHog(); const { effectiveFramework, setStoredFramework } = useFramework(); const [open, setOpen] = useState(false); const panelRef = useRef(null); const buttonRef = useRef(null); // Close on outside-click / Escape useEffect(() => { if (!open) return; const handleClick = (e: MouseEvent) => { // `e.target` is typed as `EventTarget | null`; `Node.contains` // requires an actual `Node`. Guard instead of casting so we don't // silently invoke `contains` with non-DOM targets (e.g. events // dispatched against `window`). const target = e.target instanceof Node ? e.target : null; if (!target) return; if ( panelRef.current?.contains(target) || buttonRef.current?.contains(target) ) { return; } setOpen(false); }; const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", handleClick); document.addEventListener("keydown", handleKey); return () => { document.removeEventListener("mousedown", handleClick); document.removeEventListener("keydown", handleKey); }; }, [open]); // Display whatever the page is currently rendering as: URL framework // when present, then stored choice, then the soft-default // (Built-in Agent). The selector should never read "Pick a backend" // when the docs are actually rendering BIA code — that's misleading. const current = options.find((o) => o.slug === effectiveFramework); // BIA is the soft-default and the framing on the sidebar is "you're // reading CopilotKit's docs" rather than "you've picked the Built-in // Agent backend." Show "CopilotKit" in the sidebar selector chrome // (closed pill + dropdown row) but keep the registry name elsewhere // so DocsLandingNext, IntegrationGrid, etc. still call it Built-in // Agent where the framing is about choosing a backend. const isSidebar = variant === "sidebar"; const displayNameFor = (opt: FrameworkOption) => isSidebar && opt.slug === "built-in-agent" ? "CopilotKit" : opt.name; const label = current ? displayNameFor(current) : "Pick an agentic backend"; function selectFramework(slug: string) { setStoredFramework(slug); // Fire a PostHog event so analytics dashboards can see which // backend readers pick. Wrapped in try/catch — PostHog can be // blocked by ad blockers or fail to initialize, and a broken // analytics call must never break navigation. try { const opt = options.find((o) => o.slug === slug); posthog?.capture("docs.framework_selected", { framework: slug, framework_name: opt?.name ?? slug, category: opt?.category, from_path: pathname, }); } catch { // Swallow — analytics is fire-and-forget. } // replace vs push: picking a backend is a pivot on the same logical // page, not a forward navigation. Using `push` clutters the back // stack with every framework the user clicked through, which makes // the browser Back button useless. `replace` keeps history sane. // Framework changes intentionally drop the current feature slug. The // selector is a backend pivot, so landing on the framework root gives // readers the right overview before they drill into framework-specific // docs. router.replace(`/${slug}`); setOpen(false); } // Single flat list, ordered by the canonical display order. The // category buckets ("Most Popular / Agent Frameworks / Enterprise / // Emerging") used to live here but partners read them as a tier // list — we now show every backend in one neutral list. const flatOptions = options .filter((opt) => !(isSidebar && opt.slug === "built-in-agent")) .slice() .sort((a, b) => compareByDisplayOrder(a.slug, b.slug)); // BIA pinned at the top of the sidebar dropdown — only the sidebar // variant (the topbar selector renders the flat list inline). const pinnedBIA = isSidebar ? (options.find((o) => o.slug === "built-in-agent") ?? null) : null; // Sidebar variant: full-width select with integration logo box on the // left, framework name center, chevron right. It uses the global // shadcn radius and a subtle accent wash so it reads as a selected // docs context control without hardcoding a lavender value. const sidebarBtnClasses = [ "shell-docs-radius-control w-full flex items-center gap-2 p-1.5 border h-12", "shadow-[var(--shadow-control)] transition-colors cursor-pointer", "text-[13px] font-medium text-[var(--text)]", current ? "bg-[var(--accent-dim)] border-[var(--nav-control-border)] hover:bg-[var(--accent-light)] hover:border-[var(--nav-control-border-hover)]" : "bg-[var(--bg-surface)]/60 border-[var(--border)] hover:border-[var(--accent)]", ].join(" "); const topbarBtnClasses = "shell-docs-radius-control flex items-center gap-1.5 px-2.5 py-1.5 border border-[var(--border)] bg-[var(--bg-surface)] text-[12px] font-medium text-[var(--text)] hover:border-[var(--accent)] transition-colors cursor-pointer max-w-[220px]"; return (
{open && (
{pinnedBIA && ( )}
{flatOptions.map((opt) => { const isActive = opt.slug === effectiveFramework; return ( ); })}
)}
); }