chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,44 @@
import {
CopilotRuntime,
CopilotKitIntelligence,
createCopilotEndpoint,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { HttpAgent } from "@ag-ui/client";
import { handle } from "hono/vercel";
const runtime = new CopilotRuntime({
agents: {
default: new HttpAgent({
url:
(process.env.AGENT_URL || "http://localhost:8000").replace(/\/$/, "") +
"/agui",
}),
},
// --- copilotkit:intelligence (remove this block to opt out) ---
...(process.env.COPILOTKIT_LICENSE_TOKEN
? {
intelligence: new CopilotKitIntelligence({
apiKey: process.env.INTELLIGENCE_API_KEY ?? "",
apiUrl: process.env.INTELLIGENCE_API_URL ?? "http://localhost:4201",
wsUrl:
process.env.INTELLIGENCE_GATEWAY_WS_URL ?? "ws://localhost:4401",
}),
// Demo stub — replace with your real auth-derived user identity before any
// multi-user deployment, or all users share one thread history.
identifyUser: () => ({ id: "demo-user", name: "Demo User" }),
licenseToken: process.env.COPILOTKIT_LICENSE_TOKEN,
}
: { runner: new InMemoryAgentRunner() }),
// --- /copilotkit:intelligence ---
});
const app = createCopilotEndpoint({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,76 @@
@import "tailwindcss";
/*
* Threads panel theme — match to agno's CopilotKit chat (CopilotSidebar).
*
* The CopilotKit v2 chat renders LIGHT regardless of OS dark mode (neutral
* zinc/gray surfaces on white, ~1rem bubble radius, system-ui sans). To read as
* native to that chat, the SDK <CopilotThreadsDrawer> is held light by token
* inheritance: we drop agno's old `prefers-color-scheme: dark` override of
* --background/--foreground so the panel never goes dark while the chat beside
* it stays light. agno's own demo content sets all of its colors locally
* (text-white, bg-white/20, the indigo themeColor backdrop), so it is
* unaffected by this.
*
* Tokens are defined at :root (not a wrapper) so the drawer's delete-confirm
* dialog — which renders through a React portal to <body>, outside the panel —
* still resolves the same surfaces. The SDK drawer is driven entirely by these
* tokens; the indigo accent (#6366f1) is agno's own app accent, tying the panel
* to the surrounding product; the neutral zinc surfaces/borders/muted text
* mirror the chat's light chrome.
*/
:root {
/* Neutral surfaces — match the chat's white/zinc light palette */
--background: #ffffff;
--foreground: #18181b; /* zinc-900 */
--card: #ffffff;
--card-foreground: #18181b;
/* Agno's indigo accent for the primary new-thread button + active thread */
--primary: #6366f1;
--primary-foreground: #ffffff;
/* Tints / hovers — zinc scale, matching the chat's neutral chrome */
--secondary: #f4f4f5; /* zinc-100 */
--secondary-foreground: #18181b;
--muted: #f4f4f5;
--muted-foreground: #71717a; /* zinc-500 */
--accent: #eef2ff; /* indigo-50 — selected thread row tint */
--accent-foreground: #18181b;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e4e4e7; /* zinc-200 */
--input: #e4e4e7;
--ring: #a5b4fc; /* indigo-300 — focus ring keyed to the accent */
/* Match the chat's rounded-2xl (1rem) corner language */
--radius: 1rem;
/* Match the chat's system-ui sans stack */
--font-body:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
/* The CopilotKit v2 chat (CopilotSidebar) stays LIGHT regardless of the OS color
scheme, so the threads layout is held light too — otherwise the SDK drawer
would clash with the sidebar it sits beside. The confirm dialog renders in a
portal on <body>, so re-pin there as well. Values mirror the V2 light theme
(oklch(0.145 0 0) / oklch(1 0 0)). */
.threadsLayout,
body > [role="presentation"] {
--foreground: oklch(0.145 0 0);
--background: oklch(1 0 0);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
body,
html {
height: 100%;
}
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { CopilotKit } from "@copilotkit/react-core/v2";
import "./globals.css";
import "@copilotkit/react-core/v2/styles.css";
export const metadata: Metadata = {
title: "Agno + CopilotKit Starter",
description: "A starter demo connecting an Agno agent to CopilotKit.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
{/*
suppressHydrationWarning: browser extensions (e.g. Grammarly) inject
attributes like data-gr-ext-installed onto <body> before React hydrates,
which would otherwise surface as a hydration mismatch on first load.
This only relaxes the check for <body>'s own attributes (one level deep);
everything rendered inside <body> is still fully hydration-checked.
*/}
<body className={"antialiased"} suppressHydrationWarning>
{/* Force REST transport so runtime-info + threads both hit the multi-route endpoint (auto-detect races the lazily-compiled API route in next dev). */}
<CopilotKit runtimeUrl="/api/copilotkit" useSingleEndpoint={false}>
{children}
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,50 @@
.layout {
display: grid;
/*
Reserve the desktop drawer's width (its default 320px) as a fixed first
column so the layout doesn't shift when the client-only <CopilotThreadsDrawer>
mounts after hydration. On mobile the drawer is an off-canvas overlay (out
of flow), so the column collapses and the content fills the width.
*/
grid-template-columns: var(--cpk-drawer-reserved-width, 320px) minmax(0, 1fr);
/* Drawer sets --cpk-drawer-reserved-width to 0 on desktop-collapse, so the
reserved column collapses and the chat reclaims the space. */
transition: grid-template-columns 0.2s ease;
/*
Bound the single grid row to the viewport (minmax(0,1fr) lets it shrink
below content) so a long thread list scrolls INTERNALLY in the drawer with
the header pinned, instead of the drawer growing past the viewport and the
page scrolling the header away.
*/
grid-template-rows: minmax(0, 1fr);
height: 100dvh;
width: 100%;
overflow: hidden;
}
.mainPanel {
/*
Pin the content to the SECOND track explicitly. The client-only drawer
renders nothing during SSR, so without this the content would flow into the
reserved first column at first paint and then jump once the drawer mounts.
*/
grid-column: 2;
min-width: 0;
height: 100dvh;
overflow: hidden;
}
/*
Mobile (≤768px): the drawer is an off-canvas overlay — collapse to a single
track. MUST come after the base rules (media queries add no specificity, so a
later same-specificity base rule would otherwise win and leak the two-column
desktop layout onto mobile).
*/
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.mainPanel {
grid-column: auto;
}
}
+218
View File
@@ -0,0 +1,218 @@
"use client";
import {
useAgent,
useConfigureSuggestions,
useDefaultRenderTool,
useFrontendTool,
useRenderTool,
CopilotSidebar,
CopilotChatConfigurationProvider,
CopilotThreadsDrawer,
} from "@copilotkit/react-core/v2";
import React, { useEffect, useState } from "react";
import { z } from "zod";
import { DefaultToolComponent } from "@/components/default-tool-ui";
import { WeatherCard } from "@/components/weather";
import styles from "./page.module.css";
// agno registers a single agent under the key "default" (see
// src/app/api/copilotkit/[[...slug]]/route.ts), so the threads drawer + chat
// config provider must address that same agent id.
const AGENT_ID = "default";
export default function CopilotKitPage() {
const [themeColor, setThemeColor] = useState("#6366f1");
// 🪁 Frontend Actions: https://docs.copilotkit.ai/guides/frontend-actions
useFrontendTool({
name: "set_theme_color",
parameters: z.object({
theme_color: z
.string()
.describe("The theme color to set. Make sure to pick nice colors."),
}),
handler: async ({ theme_color }) => {
setThemeColor(theme_color);
return `Changing theme color to ${theme_color}`;
},
});
// 🪁 Suggestions: https://docs.copilotkit.ai/guides/suggestions
useConfigureSuggestions({
available: "always",
suggestions: [
{
title: "Generative UI",
message: "What's the weather in San Francisco?",
},
{
title: "Frontend Tools",
message: "Set the theme to green.",
},
{
title: "Default Tool Rendering",
message: "What's the latest price of Apple stock?",
},
{
title: "Writing Agent State",
message: "Add a proverb about AI.",
},
],
});
return (
/*
One UNCONTROLLED CopilotChatConfigurationProvider (no `threadId` prop) owns
the active thread for the whole surface. The SDK <CopilotThreadsDrawer> drives it
directly — selecting a row sets the active thread, "+ New" resets to a
fresh thread — with no host thread-state. The proverbs/weather content
and the CopilotSidebar read the same active thread from the provider (the
content's `useAgent()` falls back to it). A *controlled* provider would
block "+ New" from resetting, so uncontrolled-inside-provider is required.
`.threadsLayout` (globals.css) pins the light theme vars the drawer +
sidebar inherit; the SDK drawer follows them by token inheritance.
*/
<CopilotChatConfigurationProvider agentId={AGENT_ID}>
<div className={`${styles.layout} threadsLayout`}>
{/* SDK threads drawer (replaces the hand-rolled fork). License-gated: the locked view's Upgrade CTA opens the Intelligence docs by default. */}
<CopilotThreadsDrawer agentId={AGENT_ID} />
<div className={styles.mainPanel}>
<main
style={
{
"--copilot-kit-primary-color": themeColor,
} as React.CSSProperties
}
>
<YourMainContent themeColor={themeColor} />
<CopilotSidebar
defaultOpen={true}
// Adds an initial message to the chat
labels={{
modalHeaderTitle: "Popup Assistant",
welcomeMessageText:
"👋 Hi, there! You're chatting with an Agno agent.",
}}
/>
{/* CopilotSidebar self-docks; main content renders as a sibling. */}
</main>
</div>
</div>
</CopilotChatConfigurationProvider>
);
}
// State of the agent, make sure this aligns with your agent's state.
type AgentState = {
proverbs: string[];
};
function YourMainContent({ themeColor }: { themeColor: string }) {
// 🪁 Shared State: https://docs.copilotkit.ai/coagents/shared-state
// V2: useAgent returns the agent; read agent.state and write via agent.setState.
const { agent } = useAgent({ agentId: "default" });
const state = (agent.state as AgentState | undefined) ?? { proverbs: [] };
const setState = (next: AgentState) => agent.setState(next);
// Seed an initial proverb once (the V2 agent starts with empty state).
useEffect(() => {
if ((agent.state as AgentState | undefined)?.proverbs === undefined) {
agent.setState({
proverbs: [
"CopilotKit may be new, but it's the best thing since sliced bread.",
],
});
}
}, [agent]);
// 🪁 Frontend Actions: https://docs.copilotkit.ai/agno/frontend-tools
useFrontendTool({
name: "add_proverb",
parameters: z.object({
proverb: z
.string()
.describe("The proverb to add. Make it witty, short and concise."),
}),
handler: async ({ proverb }) => {
// Read agent.state at call time so rapid successive adds don't drop
// earlier proverbs via a stale closure over `state`.
agent.setState({
proverbs: [
...((agent.state as AgentState | undefined)?.proverbs ?? []),
proverb,
],
});
return `Added proverb: ${proverb}`;
},
});
//🪁 Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
useRenderTool(
{
name: "get_weather",
parameters: z.object({
location: z.string(),
}),
render: ({ parameters }) => (
<WeatherCard themeColor={themeColor} location={parameters.location} />
),
},
[themeColor],
);
//🪁 Default Generative UI: https://docs.copilotkit.ai/agno/generative-ui/backend-tools
useDefaultRenderTool(
{
render: (props) => (
<DefaultToolComponent themeColor={themeColor} {...props} />
),
},
[themeColor],
);
return (
<div
style={{ backgroundColor: themeColor }}
className="h-screen flex justify-center items-center flex-col transition-colors duration-300"
>
<div className="bg-white/20 backdrop-blur-md p-8 rounded-2xl shadow-xl max-w-2xl w-full">
<h1 className="text-4xl font-bold text-white mb-2 text-center">
Proverbs
</h1>
<p className="text-gray-200 text-center italic mb-6">
This is a demonstrative page, but it could be anything you want! 🪁
</p>
<hr className="border-white/20 my-6" />
<div className="flex flex-col gap-3">
{state.proverbs?.map((proverb, index) => (
<div
key={index}
className="bg-white/15 p-4 rounded-xl text-white relative group hover:bg-white/20 transition-all"
>
<p className="pr-8">{proverb}</p>
<button
onClick={() =>
setState({
...state,
proverbs: state.proverbs?.filter((_, i) => i !== index),
})
}
className="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity
bg-red-500 hover:bg-red-600 text-white rounded-full h-6 w-6 flex items-center justify-center"
>
</button>
</div>
))}
</div>
{state.proverbs?.length === 0 && (
<p className="text-center text-white/80 italic my-8">
No proverbs yet. Ask the assistant to add some!
</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,166 @@
import { useState } from "react";
export type BackendToolsProps = {
name: string;
parameters: unknown;
status: "inProgress" | "executing" | "complete";
result: string | undefined;
themeColor: string;
};
export function DefaultToolComponent({
name,
parameters,
status,
result,
themeColor,
}: BackendToolsProps) {
const [showArgs, setShowArgs] = useState(false);
const [showResult, setShowResult] = useState(false);
const getStatusColor = () => {
switch (status) {
case "executing":
case "inProgress":
return "bg-blue-500/20 text-blue-300 border-blue-400/30";
case "complete":
return "bg-green-500/20 text-green-300 border-green-400/30";
default:
return "bg-white/20 text-white/70 border-white/30";
}
};
const getStatusIcon = () => {
switch (status) {
case "executing":
case "inProgress":
return (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
case "complete":
return (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
);
default:
return null;
}
};
return (
<div
style={{ backgroundColor: themeColor }}
className="backdrop-blur-sm rounded-lg p-4 mt-4 mb-4 max-w-md w-full border border-white/20"
>
{/* Header with tool name and status */}
<div className="flex items-center justify-between mb-3">
<h3 className="text-white font-medium text-sm">🔧 {name}</h3>
<span
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${getStatusColor()}`}
>
{getStatusIcon()}
{status}
</span>
</div>
{/* Arguments */}
{!!parameters &&
typeof parameters === "object" &&
Object.keys(parameters as Record<string, unknown>).length > 0 && (
<div className="mb-3">
<button
onClick={() => setShowArgs(!showArgs)}
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
>
<svg
className={`w-3 h-3 transition-transform ${showArgs ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
Arguments
</button>
{showArgs && (
<div className="bg-black/20 rounded p-2 space-y-1">
{Object.entries(parameters as Record<string, unknown>).map(
([key, value]) => (
<div key={key} className="flex gap-2">
<span className="text-white/50 text-xs">{key}:</span>
<span className="text-white/80 text-xs font-mono">
{JSON.stringify(value)}
</span>
</div>
),
)}
</div>
)}
</div>
)}
{/* Result */}
{result && (
<div>
<button
onClick={() => setShowResult(!showResult)}
className="flex items-center gap-2 text-white/80 text-xs font-medium mb-1.5 hover:text-white transition-colors"
>
<svg
className={`w-3 h-3 transition-transform ${showResult ? "rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
Result
</button>
{showResult && (
<div className="bg-black/20 rounded p-2">
<pre className="text-white/80 text-xs font-mono whitespace-pre-wrap break-words">
{typeof result === "string"
? result
: JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,52 @@
import * as React from "react";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius)] text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
{
variants: {
variant: {
default:
"bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90",
secondary:
"bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:opacity-80",
outline:
"border border-[var(--border)] bg-[var(--background)] hover:bg-[var(--secondary)]",
ghost:
"hover:bg-[var(--secondary)] hover:text-[var(--secondary-foreground)]",
destructive:
"bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:opacity-90",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-6",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
),
);
Button.displayName = "Button";
export { Button, buttonVariants };
@@ -0,0 +1,85 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-[var(--radius)] border border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)] shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-[var(--muted-foreground)]", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
};
@@ -0,0 +1,64 @@
interface WeatherCardProps {
location?: string;
themeColor: string;
}
function SunIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
className="w-14 h-14 text-yellow-200"
>
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
strokeWidth="2"
stroke="currentColor"
/>
</svg>
);
}
export function WeatherCard({ location, themeColor }: WeatherCardProps) {
return (
<div
style={{ backgroundColor: themeColor }}
className="rounded-xl shadow-xl mt-6 mb-4 max-w-md w-full"
>
<div className="bg-white/20 p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-bold text-white capitalize">
{location}
</h3>
<p className="text-white">Current Weather</p>
</div>
<SunIcon />
</div>
<div className="mt-4 flex items-end justify-between">
<div className="text-3xl font-bold text-white">70°</div>
<div className="text-sm text-white">Clear skies</div>
</div>
<div className="mt-4 pt-4 border-t border-white">
<div className="grid grid-cols-3 gap-2 text-center">
<div>
<p className="text-white text-xs">Humidity</p>
<p className="text-white font-medium">45%</p>
</div>
<div>
<p className="text-white text-xs">Wind</p>
<p className="text-white font-medium">5 mph</p>
</div>
<div>
<p className="text-white text-xs">Feels Like</p>
<p className="text-white font-medium">72°</p>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,7 @@
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}