Files
copilotkit__copilotkit/docs/components/ui/reference-sidebar/version-selector.tsx
2026-02-20 00:38:31 -05:00

224 lines
7.7 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { usePathname, useRouter } from "next/navigation";
import ChevronDownIcon from "../icons/chevron";
import CheckIcon from "../icons/check";
export type ReferenceVersion = "v1" | "v2";
const VERSION_OPTIONS: { value: ReferenceVersion; label: string }[] = [
{ value: "v2", label: "v2 (Latest)" },
{ value: "v1", label: "v1" },
];
/**
* Maps v1 page suffixes to their v2 equivalents and vice versa.
* Suffix is the path after `/reference/vN/`, e.g. "hooks/useCopilotAction".
* When switching versions, if the direct path doesn't exist in the target,
* we look up the closest equivalent here, or fall back to the version root.
*/
const V1_TO_V2: Record<string, string> = {
"hooks/useCopilotAction": "hooks/useFrontendTool",
"hooks/useCopilotReadable": "hooks/useAgentContext",
"hooks/useCopilotAdditionalInstructions": "hooks/useAgentContext",
"hooks/useCopilotChat": "hooks/useAgent",
"hooks/useCopilotChatHeadless_c": "hooks/useAgent",
"hooks/useCopilotChatSuggestions": "hooks/useConfigureSuggestions",
"hooks/useCoAgent": "hooks/useAgent",
"hooks/useCoAgentStateRender": "hooks/useRenderToolCall",
"hooks/useDefaultTool": "hooks/useDefaultRenderTool",
"hooks/useLangGraphInterrupt": "hooks/useHumanInTheLoop",
"components/chat/CopilotChat": "components/CopilotChat",
"components/chat/CopilotPopup": "components/CopilotPopup",
"components/chat/CopilotSidebar": "components/CopilotSidebar",
"components/chat": "components/CopilotChat",
};
// Build the reverse mapping (v2 → v1) from the forward mapping.
// For many-to-one mappings, the first entry wins.
const V2_TO_V1: Record<string, string> = {};
for (const [v1Path, v2Path] of Object.entries(V1_TO_V2)) {
if (!(v2Path in V2_TO_V1)) {
V2_TO_V1[v2Path] = v1Path;
}
}
function resolveVersionPath(
suffix: string,
fromVersion: ReferenceVersion,
toVersion: ReferenceVersion,
): string {
const map = fromVersion === "v1" ? V1_TO_V2 : V2_TO_V1;
// Exact match in the mapping → use the equivalent page
if (suffix in map) {
return `/reference/${toVersion}/${map[suffix]}`;
}
// Direct path exists in target (same page name in both versions) → keep it.
// Pages that exist in both: hooks/useAgent, hooks/useFrontendTool, hooks/useHumanInTheLoop,
// hooks/useRenderToolCall, components/CopilotKit, etc.
// We can't check the filesystem from the client, so we maintain a set of
// known pages per version and verify against that.
// For simplicity, if it's not in the mapping, try the direct path — the
// fallback below will catch pages that only exist in one version.
const directPath = `/reference/${toVersion}/${suffix}`;
// Pages that only exist in v1 (no v2 equivalent at all)
const v1Only = new Set([
"classes/CopilotRuntime",
"classes/CopilotTask",
"classes/llm-adapters/OpenAIAdapter",
"classes/llm-adapters/OpenAIAssistantAdapter",
"classes/llm-adapters/AnthropicAdapter",
"classes/llm-adapters/LangChainAdapter",
"classes/llm-adapters/GoogleGenerativeAIAdapter",
"classes/llm-adapters/GroqAdapter",
"sdk/python/LangGraph",
"sdk/python/LangGraphAgent",
"sdk/python/CrewAI",
"sdk/python/CrewAIAgent",
"sdk/python/RemoteEndpoints",
"sdk/js/LangGraph",
"components/CopilotTextarea",
]);
// Pages that only exist in v2 (no v1 equivalent)
const v2Only = new Set([
"hooks/useAgentContext",
"hooks/useSuggestions",
"hooks/useConfigureSuggestions",
"hooks/useCopilotKit",
"hooks/useCopilotChatConfiguration",
"components/CopilotChatView",
"components/CopilotChatMessageView",
"components/CopilotChatAssistantMessage",
"components/CopilotChatUserMessage",
"components/CopilotChatInput",
]);
const blockedSet = toVersion === "v2" ? v1Only : v2Only;
if (blockedSet.has(suffix)) {
return `/reference/${toVersion}`;
}
return directPath;
}
interface VersionSelectorProps {
onNavigate?: () => void;
}
export function getVersionFromPathname(pathname: string): ReferenceVersion {
if (pathname.startsWith("/reference/v1")) return "v1";
return "v2";
}
const VersionSelector = ({ onNavigate }: VersionSelectorProps) => {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const router = useRouter();
const dropdownRef = useRef<HTMLDivElement>(null);
const currentVersion = getVersionFromPathname(pathname);
// Close dropdown on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleVersionClick = (version: ReferenceVersion) => {
setIsOpen(false);
if (version === currentVersion) return;
// Extract the page suffix after `/reference/vN/`
const prefix = `/reference/${currentVersion}/`;
const suffix = pathname.startsWith(prefix)
? pathname.slice(prefix.length)
: "";
const newPath = suffix
? resolveVersionPath(suffix, currentVersion, version)
: `/reference/${version}`;
onNavigate?.();
router.push(newPath);
};
const currentOption = VERSION_OPTIONS.find(
(opt) => opt.value === currentVersion,
)!;
return (
<div className="relative w-full" ref={dropdownRef}>
<div
className="flex justify-between items-center p-2 mt-3 mb-3 w-full h-12 rounded-lg border cursor-pointer bg-[#BEC2FF33] dark:bg-[#7076D533] border-[#7076D5] dark:border-[#BEC2FF] [box-shadow:0px_17px_12px_-10px_rgba(112,118,213,0.3)]"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setIsOpen(!isOpen);
}
}}
tabIndex={0}
role="button"
aria-label="Select API version"
aria-expanded={isOpen}
>
<div className="flex gap-2 items-center">
<div className="flex justify-center items-center w-8 h-8 shrink-0 rounded-md bg-[#BEC2FF] dark:bg-[#7076D5]">
<span className="text-xs font-bold text-[#0C1112] dark:text-white">
API
</span>
</div>
<span className="text-sm font-medium text-foreground">
{currentOption.label}
</span>
</div>
<div className="flex items-center gap-1">
<ChevronDownIcon className="mr-1 w-4 h-4" />
</div>
</div>
{isOpen && (
<div className="absolute top-full left-0 w-full bg-[#F7F7FA] shadow-2xl dark:bg-[#0C1112] border border-border rounded-lg p-1 z-30">
{VERSION_OPTIONS.map(({ value, label }) => (
<div
key={value}
className={`flex gap-2 justify-between items-center p-2 rounded-lg cursor-pointer group pr-3 ${
currentVersion === value
? "bg-[#BEC2FF33] dark:bg-[#7076D533]"
: "hover:bg-[#0C1112]/5 dark:hover:bg-white/5"
}`}
onClick={() => handleVersionClick(value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleVersionClick(value);
}
}}
tabIndex={0}
role="option"
aria-selected={currentVersion === value}
>
<span className="text-sm font-medium">{label}</span>
{currentVersion === value && (
<CheckIcon className="text-[#5C64DA] dark:text-[#7076D5]" />
)}
</div>
))}
</div>
)}
</div>
);
};
export default VersionSelector;