mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
5728611dfd
Stack upgrade - fumadocs-core/ui 15.8.5 → 16.8.12, next 15 → 16 (Turbopack), react 19 → 19.2 - Swap "next lint" → "oxlint ." to match the rest of the repo - New deps for the page-actions component: @radix-ui/react-popover, class-variance-authority, clsx, tailwind-merge Layout & brand polish - Sidebar floats as a rounded-2xl card with column-aligned padding; framework picker pill, accent-purple section icons (16px), accent active state, and a single divider line at the footer - New custom <ThemeSwitch> — single 50×28 neutral switch replaces the fumadocs sun/moon split (drops the vertical divider and purple tint) - Sidebar folder collapse state persists across navigations via SidebarFolderStatePreserver - BrandNav: wider top bar, lowercase "Talk to an engineer", BookIcon for Docs, GitHub/Discord icons rendered inline in our footer row - Mobile: nav clipping + content padding fixes, content grid-span-full - TOC-less pages: lift article max-width so content stretches into the empty TOC column on wide viewports New routes - /llms.txt — page index per fumadocs LLMs integration - /llms-full.txt — concatenated full text of every docs page - /<path>.md and /<path>.mdx — per-page raw markdown with <Snippet> regions inlined as fenced code blocks (resolver in lib/llm-text.ts reuses the same demo-content.json the <Snippet> runtime reads) - Page-actions bar: Copy Markdown + Open in Claude / Claude Code / Windsurf / Codex (Codex links to https://chatgpt.com/codex for universal coverage) Content fixes - Reasoning page (generative-ui/reasoning.mdx): rewrite to point at the real reasoning-default / reasoning-custom cells instead of the stale agentic-chat-reasoning / reasoning-default-render names - Strip <FeatureIntegrations /> chip list ("SUPPORTED BY ...") from 16 docs MDX files (component definition kept in mdx-registry) - Drop hideTOC: true from 11 pages so they pick up the lifted-cap rule - Default home (/) to the built-in-agent authored sidebar; fix active state matching on the home url - Restore default fumadocs Callout (drop the bespoke docs-callout) - OpsPlatformCTA redesign — light bordered card with accent stripe - FrameworkOverview redesign — drop atmospheric chrome, smaller hero - Homepage / docs-landing redesign Integrations (LGP / LGT / ADK) - Tag @region[default-reasoning-zero-config] in reasoning-default and @region[reasoning-block-render] in reasoning-custom for all three frameworks so the docs <Snippet> calls resolve - Tag @region[use-agent-simple] + @region[message-list-simple] in headless-simple and @region[use-rendered-messages-hook] + @region[manual-tool-call-rendering] + @region[manual-activity-message-rendering] + @region[custom-bubbles] across headless-complete Other - docs/components/layout/mobile-sidebar.tsx: lowercase "engineer" to match shell-docs - .claude/launch.json + .claude/preview/ — dev launch configs for the worktree so /preview brings up shell-docs on :3003
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "fumadocs-core/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useMemo, useState, useEffect, useCallback } from "react";
|
|
import { usePostHog } from "posthog-js/react";
|
|
// Components
|
|
import Separator from "@/components/ui/sidebar/separator";
|
|
import Page from "@/components/ui/sidebar/page";
|
|
import Folder from "@/components/ui/sidebar/folder";
|
|
import Dropdown from "@/components/ui/mobile-sidebar/dropdown";
|
|
import IntegrationSelector from "@/components/ui/integrations-sidebar/integration-selector";
|
|
import IntegrationSelectorSkeleton from "@/components/ui/integrations-sidebar/skeleton";
|
|
import { OpenedFoldersProvider } from "@/lib/hooks/use-opened-folders";
|
|
// Icons
|
|
import DiscordIcon from "@/components/ui/icons/discord";
|
|
import GithubIcon from "@/components/ui/icons/github";
|
|
import CrossIcon from "@/components/ui/icons/cross";
|
|
// Types
|
|
import { NavbarLink } from "./navbar";
|
|
import { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
|
|
import { Integration } from "../ui/integrations-sidebar/integration-selector";
|
|
import { INTEGRATION_ORDER, INTEGRATION_METADATA } from "@/lib/integrations";
|
|
import { normalizeUrl } from "@/lib/analytics-utils";
|
|
|
|
interface MobileSidebarProps {
|
|
pageTree: DocsLayoutProps["tree"];
|
|
setIsOpen: (isOpen: boolean) => void;
|
|
handleToggleTheme: () => void;
|
|
}
|
|
|
|
type Node = DocsLayoutProps["tree"]["children"][number] & {
|
|
url: string;
|
|
name?: string;
|
|
index?: { url: string };
|
|
children?: Node[];
|
|
};
|
|
|
|
const LEFT_LINKS: NavbarLink[] = [
|
|
{
|
|
icon: <GithubIcon />,
|
|
href: "https://github.com/copilotkit/copilotkit",
|
|
target: "_blank",
|
|
},
|
|
{
|
|
icon: <DiscordIcon />,
|
|
href: "https://discord.gg/6dffbvGU3D",
|
|
target: "_blank",
|
|
},
|
|
];
|
|
|
|
const NODE_COMPONENTS: Record<
|
|
Node["type"],
|
|
React.ComponentType<{ node: Node; onNavigate?: () => void }>
|
|
> = {
|
|
separator: Separator,
|
|
page: Page,
|
|
folder: Folder,
|
|
};
|
|
|
|
const ANIMATION_DURATION = 300; // ms
|
|
|
|
const MobileSidebar = ({
|
|
pageTree,
|
|
setIsOpen,
|
|
handleToggleTheme,
|
|
}: MobileSidebarProps) => {
|
|
const pathname = usePathname();
|
|
const posthog = usePostHog();
|
|
const [selectedIntegration, setSelectedIntegration] =
|
|
useState<Integration | null>(null);
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
|
|
const handleTalkToEngineerClick = () => {
|
|
posthog?.capture("talk_to_us_clicked", {
|
|
location: "docs_navbar_mobile",
|
|
});
|
|
window.location.href = "https://copilotkit.ai/talk-to-an-engineer";
|
|
};
|
|
|
|
// Trigger slide-in animation on mount
|
|
useEffect(() => {
|
|
// Small delay to ensure the initial state is rendered before animating
|
|
const timer = setTimeout(() => setIsVisible(true), 10);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
// Handle closing with animation
|
|
const handleClose = useCallback(() => {
|
|
setIsVisible(false);
|
|
setTimeout(() => {
|
|
setIsOpen(false);
|
|
}, ANIMATION_DURATION);
|
|
}, [setIsOpen]);
|
|
|
|
// Determine route type from pathname
|
|
const normalizedPathname = normalizeUrl(pathname);
|
|
const firstSegment = normalizedPathname.replace(/^\//, "").split("/")[0];
|
|
const isIntegrationRoute = INTEGRATION_ORDER.includes(
|
|
firstSegment as (typeof INTEGRATION_ORDER)[number],
|
|
);
|
|
const isReferenceRoute = firstSegment === "reference";
|
|
|
|
// Get integration-specific pages when an integration is selected
|
|
const integrationPages = useMemo(() => {
|
|
if (!selectedIntegration) return [];
|
|
|
|
const integrationMeta = INTEGRATION_METADATA[selectedIntegration];
|
|
const integrationLabel = integrationMeta?.label;
|
|
|
|
const possiblePaths = [
|
|
`/${selectedIntegration}`,
|
|
`/integrations/${selectedIntegration}`,
|
|
];
|
|
|
|
const FOLDER_NAME_MAPPINGS: Record<string, string> = {
|
|
AutoGen2: "ag2",
|
|
autogen2: "ag2",
|
|
};
|
|
|
|
const matchesIntegration = (folderNode: Node): boolean => {
|
|
if (folderNode.type !== "folder") return false;
|
|
|
|
const url = folderNode.index?.url || folderNode.url;
|
|
if (url && possiblePaths.includes(url)) {
|
|
return true;
|
|
}
|
|
|
|
if (folderNode.name) {
|
|
const folderNameLower = folderNode.name.toLowerCase();
|
|
const labelLower = integrationLabel?.toLowerCase() || "";
|
|
const idLower = selectedIntegration.toLowerCase();
|
|
|
|
const mappedId =
|
|
FOLDER_NAME_MAPPINGS[folderNode.name] ||
|
|
FOLDER_NAME_MAPPINGS[folderNameLower];
|
|
if (mappedId && mappedId === selectedIntegration.toLowerCase()) {
|
|
return true;
|
|
}
|
|
|
|
if (folderNameLower === labelLower || folderNameLower === idLower) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
let integrationFolder = pageTree.children.find((node) =>
|
|
matchesIntegration(node as Node),
|
|
) as Node | undefined;
|
|
|
|
if (!integrationFolder) {
|
|
const integrationsParent = pageTree.children.find((node) => {
|
|
const folderNode = node as Node;
|
|
return (
|
|
folderNode.type === "folder" &&
|
|
(folderNode.index?.url === "/integrations" ||
|
|
folderNode.name?.toLowerCase() === "integrations")
|
|
);
|
|
}) as Node | undefined;
|
|
|
|
if (integrationsParent?.children) {
|
|
integrationFolder = integrationsParent.children.find((node) =>
|
|
matchesIntegration(node as Node),
|
|
) as Node | undefined;
|
|
}
|
|
}
|
|
|
|
return integrationFolder?.children ?? [];
|
|
}, [selectedIntegration, pageTree.children]);
|
|
|
|
// Get reference-specific pages
|
|
const referencePages = useMemo(() => {
|
|
if (!isReferenceRoute) return [];
|
|
|
|
const referenceFolder = pageTree.children.find((node) => {
|
|
if (node.type !== "folder") return false;
|
|
const folderNode = node as Node;
|
|
const url = folderNode.index?.url || folderNode.url;
|
|
const name =
|
|
typeof folderNode.name === "string" ? folderNode.name : undefined;
|
|
return url === "/reference" || name?.toLowerCase() === "reference";
|
|
}) as Node | undefined;
|
|
|
|
if (referenceFolder && "children" in referenceFolder) {
|
|
return (referenceFolder as Node).children || [];
|
|
}
|
|
|
|
return [];
|
|
}, [isReferenceRoute, pageTree.children]);
|
|
|
|
// Determine which pages to show
|
|
const pagesToShow = useMemo(() => {
|
|
if (isIntegrationRoute && selectedIntegration) {
|
|
return integrationPages;
|
|
}
|
|
if (isReferenceRoute) {
|
|
return referencePages;
|
|
}
|
|
return pageTree.children;
|
|
}, [
|
|
isIntegrationRoute,
|
|
selectedIntegration,
|
|
integrationPages,
|
|
isReferenceRoute,
|
|
referencePages,
|
|
pageTree.children,
|
|
]);
|
|
|
|
return (
|
|
<div
|
|
className={`flex fixed top-0 left-0 z-50 justify-end p-1 w-full h-full transition-colors duration-300 ${
|
|
isVisible ? "bg-black/30" : "bg-black/0"
|
|
}`}
|
|
onClick={(e) => {
|
|
// Close when clicking the backdrop (outside the sidebar)
|
|
if (e.target === e.currentTarget) handleClose();
|
|
}}
|
|
>
|
|
<OpenedFoldersProvider>
|
|
<aside
|
|
className={`flex flex-col w-full max-w-[280px] h-[calc(100vh-8px)] border border-r-0 border-border bg-sidebar rounded-2xl pl-3 pr-1 transition-transform duration-300 ease-out ${
|
|
isVisible ? "translate-x-0" : "translate-x-full"
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-center my-2 w-full">
|
|
<div className="flex gap-1 items-center">
|
|
{LEFT_LINKS.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
target={link.target}
|
|
className="flex justify-center items-center w-11 h-11 shrink-0"
|
|
>
|
|
<span className="flex items-center h-full">{link.icon}</span>
|
|
</Link>
|
|
))}
|
|
<button
|
|
className="flex justify-center items-center w-11 h-11 cursor-pointer"
|
|
onClick={handleToggleTheme}
|
|
>
|
|
<Image
|
|
src="/images/navbar/theme-moon.svg"
|
|
alt="Theme icon"
|
|
width={20}
|
|
height={20}
|
|
className="hidden dark:inline-block"
|
|
/>
|
|
<Image
|
|
src="/images/navbar/theme-sun.svg"
|
|
alt="Theme icon"
|
|
width={20}
|
|
height={20}
|
|
className="dark:hidden"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<button
|
|
className="flex justify-center items-center w-11 h-full cursor-pointer"
|
|
onClick={handleClose}
|
|
>
|
|
<CrossIcon />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Talk to an Engineer — pinned at the top of the mobile
|
|
* drawer as a primary CTA. */}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
handleClose();
|
|
handleTalkToEngineerClick();
|
|
}}
|
|
className="text-left rounded-md px-3 py-2.5 mb-2 text-[14px] font-medium bg-indigo-500/10 text-indigo-500 hover:bg-indigo-500/20 transition-all cursor-pointer"
|
|
aria-label="Talk to an engineer"
|
|
>
|
|
Talk to an engineer
|
|
</button>
|
|
|
|
<Dropdown onSelect={handleClose} />
|
|
|
|
{!isReferenceRoute && (
|
|
<IntegrationSelector
|
|
selectedIntegration={selectedIntegration}
|
|
setSelectedIntegration={setSelectedIntegration}
|
|
onNavigate={handleClose}
|
|
/>
|
|
)}
|
|
|
|
{isIntegrationRoute && selectedIntegration ? (
|
|
<ul className="flex overflow-y-auto flex-col mt-6 max-h-full custom-scrollbar [&>*:first-child]:mt-0">
|
|
{integrationPages.map((page, index) => {
|
|
const Component = NODE_COMPONENTS[page.type];
|
|
const pageUrl =
|
|
(page as Node).index?.url ||
|
|
(page as Node).url ||
|
|
`page-${index}`;
|
|
const key = `${page.type}-${pageUrl}`;
|
|
return (
|
|
<Component
|
|
key={key}
|
|
node={page as Node}
|
|
onNavigate={handleClose}
|
|
/>
|
|
);
|
|
})}
|
|
</ul>
|
|
) : isIntegrationRoute && !selectedIntegration ? (
|
|
<IntegrationSelectorSkeleton />
|
|
) : (
|
|
<ul className="flex overflow-y-auto flex-col mt-6 max-h-full custom-scrollbar [&>*:first-child]:mt-0">
|
|
{pagesToShow.map((page, index) => {
|
|
const Component = NODE_COMPONENTS[page.type];
|
|
const pageUrl =
|
|
(page as Node).index?.url ||
|
|
(page as Node).url ||
|
|
`page-${index}`;
|
|
const key = `${page.type}-${pageUrl}`;
|
|
return (
|
|
<Component
|
|
key={key}
|
|
node={page as Node}
|
|
onNavigate={handleClose}
|
|
/>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</aside>
|
|
</OpenedFoldersProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MobileSidebar;
|