chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* CopilotKit API route with MCP Apps middleware.
|
||||
* Connects to the travel booking MCP server and enables UI-enabled tools.
|
||||
*
|
||||
* Reference: v2.x/apps/react/demo/src/app/api/copilotkit-mcp/[[...slug]]/route.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
CopilotRuntime,
|
||||
createCopilotEndpoint,
|
||||
InMemoryAgentRunner,
|
||||
} from "@copilotkit/runtime";
|
||||
import { handle } from "hono/vercel";
|
||||
import { BuiltInAgent } from "@copilotkit/runtime/v2";
|
||||
import { MCPAppsMiddleware } from "@ag-ui/mcp-apps-middleware";
|
||||
|
||||
// Determine which LLM model to use based on available API keys
|
||||
const determineModel = () => {
|
||||
if (process.env.OPENAI_API_KEY?.trim()) {
|
||||
return "openai/gpt-5.2";
|
||||
}
|
||||
if (process.env.ANTHROPIC_API_KEY?.trim()) {
|
||||
return "anthropic/claude-sonnet-4.5";
|
||||
}
|
||||
if (process.env.GOOGLE_API_KEY?.trim()) {
|
||||
return "google/gemini-2.5-pro";
|
||||
}
|
||||
return "openai/gpt-5.2";
|
||||
};
|
||||
|
||||
// Create the agent with multi-app assistant persona and MCP Apps middleware
|
||||
const agent = new BuiltInAgent({
|
||||
model: determineModel(),
|
||||
prompt: `You are an AI assistant with access to 4 interactive apps that render in the chat. Each app provides a rich UI for specific tasks.
|
||||
|
||||
## Available Apps
|
||||
|
||||
### 1. Airline Booking (search-flights)
|
||||
Search for flights, select seats, and complete bookings with a full wizard experience.
|
||||
- Parameters: origin (airport code like JFK, LAX, LHR), destination (airport code), departureDate (YYYY-MM-DD), passengers (1-9), cabinClass (economy/business/first)
|
||||
- Example: "Book a flight from New York to Los Angeles on January 20th for 2 passengers"
|
||||
- Helper tools: select-flight, select-seats, book-flight
|
||||
|
||||
### 2. Hotel Booking (search-hotels)
|
||||
Browse hotels, compare rooms, and book accommodations in cities worldwide.
|
||||
- Parameters: city (Paris, Tokyo, New York, etc.), checkIn (YYYY-MM-DD), checkOut (YYYY-MM-DD), guests (1-6), rooms (1-4)
|
||||
- Example: "Find a hotel in Paris from January 15 to 18 for 2 guests"
|
||||
- Helper tools: select-hotel, select-room, book-hotel
|
||||
|
||||
### 3. Investment Simulator (create-portfolio)
|
||||
Create mock investment portfolios with holdings, charts, and trading.
|
||||
- Parameters: initialBalance (1000-1000000), riskTolerance (conservative/moderate/aggressive), focus (tech/healthcare/diversified/growth/dividend)
|
||||
- Example: "Create a $10,000 aggressive tech-focused portfolio"
|
||||
- Helper tools: execute-trade, refresh-prices
|
||||
|
||||
### 4. Kanban Board (create-board)
|
||||
Create task boards with drag-drop cards and columns.
|
||||
- Parameters: projectName (string), template (blank/software/marketing/personal)
|
||||
- Example: "Create a kanban board for my software project"
|
||||
- Helper tools: add-card, update-card, delete-card, move-card
|
||||
|
||||
## Guidelines
|
||||
- When a user's request matches an app, use the appropriate tool to render the interactive UI
|
||||
- Ask clarifying questions if key parameters are missing
|
||||
- Each app has helper tools for additional interactions within the UI
|
||||
- Be helpful and guide users through the interactive features`,
|
||||
}).use(
|
||||
new MCPAppsMiddleware({
|
||||
mcpServers: [
|
||||
{
|
||||
type: "http",
|
||||
url: process.env.MCP_SERVER_URL || "http://localhost:3001/mcp",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Create CopilotKit runtime
|
||||
const runtime = new CopilotRuntime({
|
||||
agents: {
|
||||
default: agent,
|
||||
},
|
||||
runner: new InMemoryAgentRunner(),
|
||||
});
|
||||
|
||||
// Create Hono endpoint
|
||||
const app = createCopilotEndpoint({
|
||||
runtime,
|
||||
basePath: "/api/copilotkit",
|
||||
});
|
||||
|
||||
export const GET = handle(app);
|
||||
export const POST = handle(app);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,447 @@
|
||||
/* Plus Jakarta Sans - CopilotKit's brand font */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap");
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
/* =============================================================================
|
||||
DESIGN SYSTEM - CopilotKit Palette
|
||||
============================================================================= */
|
||||
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--color-lilac: #bec2ff;
|
||||
--color-lilac-light: #d4d7ff;
|
||||
--color-lilac-dark: #9599cc;
|
||||
--color-mint: #85e0ce;
|
||||
--color-mint-light: #a8e9dc;
|
||||
--color-mint-dark: #1b936f;
|
||||
|
||||
/* Surfaces */
|
||||
--color-surface: #dedee9;
|
||||
--color-surface-light: #f7f7f9;
|
||||
--color-container: #ffffff;
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #010507;
|
||||
--color-text-secondary: #57575b;
|
||||
--color-text-tertiary: #8e8e93;
|
||||
|
||||
/* Borders */
|
||||
--color-border: #dbdbe5;
|
||||
--color-border-light: #ebebf0;
|
||||
--color-border-glass: rgba(255, 255, 255, 0.3);
|
||||
|
||||
/* Glassmorphism */
|
||||
--color-glass: rgba(255, 255, 255, 0.7);
|
||||
--color-glass-subtle: rgba(255, 255, 255, 0.5);
|
||||
--color-glass-dark: rgba(255, 255, 255, 0.85);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg:
|
||||
0 10px 25px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
|
||||
--shadow-glass: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
|
||||
/* Radii */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 24px;
|
||||
|
||||
/* Typography */
|
||||
--font-family: "Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 32px;
|
||||
--font-size-4xl: 40px;
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--color-surface-light);
|
||||
--color-foreground: var(--color-text-primary);
|
||||
--font-sans: var(--font-family);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
BASE STYLES
|
||||
============================================================================= */
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-surface-light);
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
ABSTRACT SHAPES BACKGROUND
|
||||
============================================================================= */
|
||||
|
||||
.abstract-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-surface-light) 0%,
|
||||
var(--color-surface) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.abstract-bg::before,
|
||||
.abstract-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.abstract-bg::before {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: var(--color-lilac);
|
||||
top: -200px;
|
||||
right: -100px;
|
||||
animation: blob1 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.abstract-bg::after {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: var(--color-mint);
|
||||
bottom: -150px;
|
||||
left: -100px;
|
||||
animation: blob2 30s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-lilac-light),
|
||||
var(--color-mint-light)
|
||||
);
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.4;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: blob3 35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes blob1 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translate(-30px, 50px) scale(1.1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(20px, -30px) scale(0.95);
|
||||
}
|
||||
75% {
|
||||
transform: translate(40px, 20px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blob2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(50px, -40px) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blob3 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translate(-45%, -55%) scale(1.15);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-55%, -45%) scale(0.9);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-48%, -52%) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive blobs - reduce size and blur for performance */
|
||||
@media (max-width: 767px) {
|
||||
.abstract-bg::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
filter: blur(60px);
|
||||
}
|
||||
.abstract-bg::after {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
filter: blur(50px);
|
||||
}
|
||||
.blob-3 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
filter: blur(40px);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
GLASSMORPHISM COMPONENTS
|
||||
============================================================================= */
|
||||
|
||||
.glass {
|
||||
background: var(--color-glass);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-glass);
|
||||
}
|
||||
|
||||
.glass-subtle {
|
||||
background: var(--color-glass-subtle);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: var(--color-glass);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-glass);
|
||||
padding: var(--space-6);
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
APP CARDS
|
||||
============================================================================= */
|
||||
|
||||
.app-card {
|
||||
background: var(--color-glass);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-5);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--color-lilac), var(--color-mint));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.app-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--space-4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.app-card-icon.flights {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.app-card-icon.hotels {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.app-card-icon.trading {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
}
|
||||
|
||||
.app-card-icon.kanban {
|
||||
background: linear-gradient(135deg, #f093fb, #f5576c);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
PROMPT PILLS
|
||||
============================================================================= */
|
||||
|
||||
.prompt-pill {
|
||||
background: var(--color-glass-subtle);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prompt-pill:hover {
|
||||
background: var(--color-glass);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-lilac);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
DOCS BUTTONS
|
||||
============================================================================= */
|
||||
|
||||
.docs-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
border-radius: var(--radius-xl);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.docs-btn-primary {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-lilac-dark),
|
||||
var(--color-mint-dark)
|
||||
);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.docs-btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(27, 147, 111, 0.35);
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.docs-btn-secondary {
|
||||
background: var(--color-glass);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border-glass);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.docs-btn-secondary:hover {
|
||||
background: var(--color-glass-dark);
|
||||
border-color: var(--color-lilac);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
COPILOTKIT SIDEBAR OVERRIDES
|
||||
============================================================================= */
|
||||
|
||||
/* Sidebar container */
|
||||
[data-copilotkit-sidebar] {
|
||||
--copilot-kit-background-color: rgba(247, 247, 249, 0.95) !important;
|
||||
--copilot-kit-secondary-color: rgba(255, 255, 255, 0.7) !important;
|
||||
--copilot-kit-primary-color: var(--color-lilac) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-left: 1px solid var(--color-border-glass) !important;
|
||||
}
|
||||
|
||||
/* Message bubbles */
|
||||
[data-copilotkit-sidebar] .copilotkit-message {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Input area */
|
||||
[data-copilotkit-sidebar] textarea,
|
||||
[data-copilotkit-sidebar] input {
|
||||
font-family: var(--font-family);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
UTILITY CLASSES
|
||||
============================================================================= */
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-lilac-dark),
|
||||
var(--color-mint-dark)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.border-gradient {
|
||||
border: 2px solid transparent;
|
||||
background:
|
||||
linear-gradient(var(--color-container), var(--color-container)) padding-box,
|
||||
linear-gradient(135deg, var(--color-lilac), var(--color-mint)) border-box;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "@copilotkit/react-core/v2/styles.css";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI Fitness Coach - MCP Apps Demo",
|
||||
description: "Interactive workout demo built with CopilotKit and MCP Apps",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CopilotKitProvider,
|
||||
CopilotSidebar,
|
||||
CopilotPopup,
|
||||
useAgent,
|
||||
useCopilotKit,
|
||||
useCopilotChatConfiguration,
|
||||
} from "@copilotkit/react-core/v2";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { randomUUID, DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Lucide-style SVG icons (inline for reliability)
|
||||
const Icons = {
|
||||
plane: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z" />
|
||||
</svg>
|
||||
),
|
||||
building: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width="16" height="20" x="4" y="2" rx="2" ry="2" />
|
||||
<path d="M9 22v-4h6v4" />
|
||||
<path d="M8 6h.01" />
|
||||
<path d="M16 6h.01" />
|
||||
<path d="M12 6h.01" />
|
||||
<path d="M12 10h.01" />
|
||||
<path d="M12 14h.01" />
|
||||
<path d="M16 10h.01" />
|
||||
<path d="M16 14h.01" />
|
||||
<path d="M8 10h.01" />
|
||||
<path d="M8 14h.01" />
|
||||
</svg>
|
||||
),
|
||||
trendingUp: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
|
||||
<polyline points="16 7 22 7 22 13" />
|
||||
</svg>
|
||||
),
|
||||
layoutGrid: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
),
|
||||
sparkles: (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
|
||||
<path d="M20 3v4" />
|
||||
<path d="M22 5h-4" />
|
||||
<path d="M4 17v2" />
|
||||
<path d="M5 18H3" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
const apps = [
|
||||
{
|
||||
id: "flights",
|
||||
name: "Airline Booking",
|
||||
description:
|
||||
"Search flights, select seats, and complete bookings with a full wizard experience",
|
||||
icon: Icons.plane,
|
||||
iconClass: "flights",
|
||||
prompts: [
|
||||
"Book a flight from New York to Los Angeles on January 20th for 2 passengers",
|
||||
"Find flights from London to Paris next week",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "hotels",
|
||||
name: "Hotel Booking",
|
||||
description:
|
||||
"Browse hotels, compare rooms, and book accommodations in cities worldwide",
|
||||
icon: Icons.building,
|
||||
iconClass: "hotels",
|
||||
prompts: [
|
||||
"Find a hotel in Paris from January 15 to 18 for 2 guests",
|
||||
"Search for hotels in Tokyo for 3 nights",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "trading",
|
||||
name: "Investment Simulator",
|
||||
description:
|
||||
"Build portfolios, execute trades, and track performance with live charts",
|
||||
icon: Icons.trendingUp,
|
||||
iconClass: "trading",
|
||||
prompts: [
|
||||
"Create a $10,000 tech-focused portfolio",
|
||||
"Build a conservative dividend portfolio",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "kanban",
|
||||
name: "Kanban Board",
|
||||
description:
|
||||
"Manage projects with drag-drop cards, columns, and task tracking",
|
||||
icon: Icons.layoutGrid,
|
||||
iconClass: "kanban",
|
||||
prompts: [
|
||||
"Create a kanban board for my software project",
|
||||
"Set up a marketing campaign board",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function MCPAppsDemo() {
|
||||
return (
|
||||
<CopilotKitProvider runtimeUrl="/api/copilotkit" showDevConsole="auto">
|
||||
<AppLayout />
|
||||
</CopilotKitProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppLayout() {
|
||||
const { agent } = useAgent({ agentId: DEFAULT_AGENT_ID });
|
||||
const { copilotkit } = useCopilotKit();
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
const config = useCopilotChatConfiguration();
|
||||
|
||||
// Send a message to the chat and run the agent
|
||||
const sendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
config?.setModalOpen(true);
|
||||
agent.addMessage({
|
||||
id: randomUUID(),
|
||||
role: "user",
|
||||
content: message,
|
||||
});
|
||||
try {
|
||||
await copilotkit.runAgent({ agent });
|
||||
} catch (error) {
|
||||
console.error("Failed to run agent:", error);
|
||||
}
|
||||
},
|
||||
[agent, copilotkit, config],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
{/* Animated Abstract Background */}
|
||||
<div className="abstract-bg">
|
||||
<div className="blob-3" />
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative z-10 mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-6 md:px-6 md:py-12">
|
||||
{/* Hero Section */}
|
||||
<section className="text-center space-y-6">
|
||||
<div className="inline-flex items-center gap-2 glass-subtle px-4 py-2 rounded-full text-sm text-[var(--color-text-secondary)]">
|
||||
{Icons.sparkles}
|
||||
<span>MCP Apps Demo</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||||
Interactive AI Apps in Chat
|
||||
</h1>
|
||||
<p className="max-w-2xl mx-auto text-lg text-[var(--color-text-secondary)]">
|
||||
Rich UI components that render directly in the chat sidebar. Powered
|
||||
by the MCP Apps Extension (SEP-1865) with bidirectional
|
||||
communication.
|
||||
</p>
|
||||
|
||||
{/* Docs buttons */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
|
||||
<a
|
||||
href="https://go.copilotkit.ai/mcp-apps"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="docs-btn docs-btn-primary"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
||||
</svg>
|
||||
Read more
|
||||
</a>
|
||||
<a
|
||||
href="https://go.copilotkit.ai/mcp-apps-docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="docs-btn docs-btn-secondary"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5z" />
|
||||
<path d="M2 17l10 5 10-5" />
|
||||
</svg>
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* App Cards Grid */}
|
||||
<section className="grid gap-6 md:grid-cols-2">
|
||||
{apps.map((app) => (
|
||||
<div key={app.id} className="app-card">
|
||||
<div className={`app-card-icon ${app.iconClass}`}>{app.icon}</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
|
||||
{app.name}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||
{app.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{app.prompts.map((prompt, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="prompt-pill text-xs cursor-pointer hover:scale-105 transition-transform"
|
||||
onClick={() => sendMessage(prompt)}
|
||||
>
|
||||
“{prompt}”
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Info Banner */}
|
||||
<section className="glass-card">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-gradient-to-br from-[var(--color-lilac)] to-[var(--color-mint)] flex items-center justify-center text-white">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--color-text-primary)] mb-1">
|
||||
How It Works
|
||||
</h4>
|
||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||
Each app renders in a sandboxed iframe within the chat. The UI
|
||||
communicates with the MCP server via JSON-RPC over postMessage,
|
||||
enabling real-time interactions like selecting flight seats,
|
||||
booking hotel rooms, or executing trades.
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-[var(--color-text-tertiary)]">
|
||||
MCP Server:{" "}
|
||||
<code className="glass-subtle px-2 py-0.5 rounded text-[var(--color-text-secondary)]">
|
||||
cd mcp-server && npm run dev
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* CopilotKit Chat UI - Sidebar on desktop, Popup on mobile */}
|
||||
{isDesktop ? (
|
||||
<CopilotSidebar defaultOpen={true} width="50%" />
|
||||
) : (
|
||||
<CopilotPopup
|
||||
defaultOpen={false}
|
||||
labels={{
|
||||
modalHeaderTitle: "MCP Apps Assistant",
|
||||
chatInputPlaceholder: "What would you like to try today?",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof window.matchMedia === "undefined"
|
||||
)
|
||||
return;
|
||||
const mediaQueryList = window.matchMedia(query);
|
||||
const updateMatch = () => setMatches(mediaQueryList.matches);
|
||||
updateMatch();
|
||||
mediaQueryList.addEventListener("change", updateMatch);
|
||||
return () => mediaQueryList.removeEventListener("change", updateMatch);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export default useMediaQuery;
|
||||
Reference in New Issue
Block a user