mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
593859fd9f
Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> Co-authored-by: Tyler Slaton <54378333+tylerslaton@users.noreply.github.com>
224 lines
7.7 KiB
TypeScript
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;
|