Files
Tyler Slaton 5d4ed8f312 fix: run formatter to resolve lint issues
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
2026-03-18 15:36:46 -04:00

197 lines
6.4 KiB
TypeScript

"use client";
import { Banner } from "fumadocs-ui/components/banner";
import Link from "next/link";
import { PaintbrushIcon, Rocket } from "lucide-react";
import { SiCrewai } from "@icons-pack/react-simple-icons";
import { useState, useEffect } from "react";
// Time in milliseconds before a dismissed banner reappears
const BANNER_REAPPEAR_DELAY = 3 * 24 * 60 * 60 * 1000; // 3 days
const BANNER_DISMISSED_KEY = "nd-banner-rotating-banner";
const BANNER_DISMISSED_TIME_KEY = "nd-banner-rotating-banner-dismissed-at";
export function Banners() {
const [currentBanner, setCurrentBanner] = useState(0);
const [key, setKey] = useState(0); // Force re-render to show banner again
const bannerContent = [
{
icon: <Rocket className="w-5 h-5 hidden md:block flex-shrink-0" />,
mobileText: "CopilotKit fully supports MCP Apps!",
desktopText: "Bring MCP Apps interaction to your users with CopilotKit!",
buttonText: "See What's New",
href: "/generative-ui/mcp-apps",
},
];
// Rotate banners
useEffect(() => {
const interval = setInterval(() => {
setCurrentBanner((prev) => (prev + 1) % bannerContent.length);
}, 8000);
return () => clearInterval(interval);
}, []);
// Check for dismissed banner and handle reappearance based on timestamp
useEffect(() => {
const checkBannerExpiry = () => {
const isDismissed = localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
if (isDismissed) {
const dismissedAt = localStorage.getItem(BANNER_DISMISSED_TIME_KEY);
if (dismissedAt) {
const elapsed = Date.now() - parseInt(dismissedAt, 10);
if (elapsed >= BANNER_REAPPEAR_DELAY) {
// Time has passed - show banner again
localStorage.removeItem(BANNER_DISMISSED_KEY);
localStorage.removeItem(BANNER_DISMISSED_TIME_KEY);
setKey((prev) => prev + 1);
} else {
// Schedule check for when delay expires
const remaining = BANNER_REAPPEAR_DELAY - elapsed;
const timeout = setTimeout(() => {
localStorage.removeItem(BANNER_DISMISSED_KEY);
localStorage.removeItem(BANNER_DISMISSED_TIME_KEY);
setKey((prev) => prev + 1);
}, remaining);
return () => clearTimeout(timeout);
}
} else {
// Banner was just dismissed - record the time
localStorage.setItem(
BANNER_DISMISSED_TIME_KEY,
Date.now().toString(),
);
}
}
};
checkBannerExpiry();
// Also listen for storage changes (in case dismissed in this or another tab)
const handleStorage = () => {
const isDismissed = localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
const hasTimestamp = localStorage.getItem(BANNER_DISMISSED_TIME_KEY);
if (isDismissed && !hasTimestamp) {
localStorage.setItem(BANNER_DISMISSED_TIME_KEY, Date.now().toString());
}
};
window.addEventListener("storage", handleStorage);
// Also check periodically in case the banner was dismissed while on this page
const interval = setInterval(checkBannerExpiry, 1000);
return () => {
window.removeEventListener("storage", handleStorage);
clearInterval(interval);
};
}, [key]);
const content = bannerContent[currentBanner];
return (
<div key={key} className="w-full px-1 mt-1 xl:px-2 xl:mt-2">
<Banner
className="w-full text-foreground bg-secondary/80 backdrop-blur-sm border border-border rounded-2xl py-1.5 md:py-2"
id="rotating-banner"
>
<div className="flex flex-row items-center justify-center gap-1.5 md:gap-3 w-full px-1 md:px-4">
<div
key={currentBanner}
className="flex items-center gap-1.5 md:gap-2 flex-shrink min-w-0"
>
{content.icon}
<p
className="text-xs md:text-base font-normal md:hidden font-sans"
style={{ fontWeight: 400 }}
>
{content.mobileText}
</p>
<p
className="text-sm sm:text-base font-normal hidden md:block font-sans"
style={{ fontWeight: 400 }}
>
{content.desktopText}
</p>
</div>
<Link
href={content.href}
className="text-indigo-800 dark:text-indigo-300 ring-1 ring-indigo-200 dark:ring-indigo-900 text-xs md:text-sm items-center bg-gradient-to-r from-indigo-200/50 to-purple-200/80 dark:from-indigo-900/40 dark:to-purple-900/50 flex px-2 py-0.5 md:px-4 md:py-1 no-underline whitespace-nowrap transition-all duration-100 hover:ring-2 hover:ring-indigo-400 hover:dark:text-indigo-200 rounded-lg flex-shrink-0"
>
{content.buttonText}
</Link>
</div>
</Banner>
</div>
);
}
export function NewLookAndFeelBanner() {
return (
<Banner
className="w-full text-white gap-2 bg-indigo-500 dark:bg-indigo-900 h-2!"
variant="rainbow"
id="new-look-and-feel-banner"
>
<PaintbrushIcon className="w-5 h-5" />
<p>
We are launching a new default look and feel! Checkout the{" "}
<span className="underline">
<Link href="/troubleshooting/migrate-to-1.8.2">
{" "}
migration guide{" "}
</Link>
</span>{" "}
to learn more.
</p>
</Banner>
);
}
export function CoagentsCrewAnnouncementBanner() {
return (
<Banner
className="w-full text-white gap-2 bg-indigo-500 dark:bg-indigo-900"
variant="rainbow"
id="coagents-crew-announcement-banner"
>
<SiCrewai className="w-5 h-5 inline mb-1" /> CrewAI support is here!
Checkout the{" "}
<Link href="/crewai-flows" className="underline">
CrewAI Flows
</Link>{" "}
documentation.
</Banner>
);
}
export function AGUIBanner() {
return (
<Banner
className="w-full text-white bg-indigo-500 dark:bg-indigo-900 h-24 sm:h-14 !important"
variant="rainbow"
id="agui-banner"
>
<p className="w-3/4">
CopilotKit and our framework partners have launched the AG-UI protocol
for agent-user interaction!{" "}
<Link
href="/ag-ui-protocol"
target="_blank"
className="underline"
rel="noopener noreferrer"
>
Learn more
</Link>
.
</p>
</Banner>
);
}