├── .cursorrules ├── .gitignore ├── README.md ├── components.json ├── env.example ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── copilotkit-logo-dark.png ├── copilotkit-logo-light.png └── map-overlay.png ├── src ├── app │ ├── (canvas-pages) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── globals.css │ ├── icon.png │ └── layout.tsx ├── components │ ├── Loader.tsx │ ├── MCPToolCall.tsx │ ├── McpServerManager.tsx │ ├── Nodes.tsx │ ├── Storage.tsx │ ├── Todo.tsx │ ├── ToolRenderer.tsx │ ├── VisualRepresentation.tsx │ ├── app-sidebar.tsx │ ├── canvas.tsx │ ├── chat-window.tsx │ ├── coagents-provider.tsx │ ├── mcp-config-modal.tsx │ ├── skeletons │ │ └── index.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── contexts │ └── TodoContext.tsx ├── hooks │ ├── use-local-storage.tsx │ └── use-mobile.tsx ├── lib │ ├── available-agents.ts │ ├── mcp-config-types.ts │ └── utils.ts └── providers │ └── Providers.tsx ├── tailwind.config.ts └── tsconfig.json /.cursorrules: -------------------------------------------------------------------------------- 1 | 2 | # Instructions 3 | Use the following commands to get AI assistance: 4 | 5 | cursor-tools web "your question" - Get answers from the web using Perplexity AI 6 | cursor-tools repo "your question" - Get context-aware answers about this repository using Google Gemini 7 | cursor-tools doc [options] - Generate comprehensive documentation for this repository 8 | cursor-tools github pr [number] - Get the last 10 PRs, or a specific PR by number 9 | cursor-tools github issue [number] - Get the last 10 issues, or a specific issue by number 10 | 11 | cursor-tools web is good for getting up-to-date information from the web that are not repository specific. For example, you can ask it to get the names and details of the latest OpenAI models or details about an external API. 12 | cursor-tools repo has the entire repository context available to it so it is good for repository search and tasks that require holistic understanding such as planning, debugging and answering questions about the architecture. 13 | cursor-tools doc can generate comprehensive documentation for your repository, with options like --output to save to a file and --fromGithub to document a remote GitHub repository. 14 | 15 | Running the commands: 16 | 1. Using the installed version: 17 | If cursor-tools is in your path run it as `cursor-tools `. If it is not found in your PATH, you can run it with `npm exec cursor-tools "your question"` or `yarn cursor-tools "your question"` or `pnpm cursor-tools "your question"` depending on your package manager - if cursor-tools is installed as a local dependency. If cursor-tools is not installed as a dependency you should fall back to using `npx -y cursor-tools@latest "your question"` or `bunx -y cursor-tools@latest "your question"` if you have bun installed. 18 | 19 | ## Additional command options 20 | All commands support these general options: 21 | --model=: Specify an alternative AI model to use 22 | --max-tokens=: Control response length 23 | --save-to=: Save command output to a file (in *addition* to displaying it, like tee) 24 | --help: View all available options (help has not been implemented for all commands yet) 25 | 26 | Documentation command specific options: 27 | --from-github=/[@]: Generate documentation for a remote GitHub repository 28 | 29 | GitHub command specific options: 30 | --from-github=/[@]: Access PRs/issues from a specific GitHub repository 31 | 32 | ## Notes 33 | - more information about cursor-tools can be found in node_modules/cursor-tools/README.md if installed locally. 34 | - configuration is in cursor-tools.config.json (falling back to ~/.cursor-tools/config.json) 35 | - api keys are loaded from .cursor-tools.env (falling back to ~/.cursor-tools/.env) 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # misc 4 | .DS_Store 5 | *.pem 6 | __snapshots__ 7 | @generated 8 | node_modules 9 | .idea/ 10 | cdk_outputs.json 11 | .turbo 12 | test-run-comment.md 13 | 14 | *.egg-info/ 15 | .installed.cfg 16 | *.egg 17 | **/.langgraph_api 18 | 19 | *.sqlite 20 | *.sqlite-shm 21 | *.sqlite-wal 22 | 23 | __pycache__/ 24 | *.env 25 | *.next 26 | 27 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 28 | 29 | # dependencies 30 | /node_modules 31 | /.pnp 32 | .pnp.* 33 | .yarn/* 34 | !.yarn/patches 35 | !.yarn/plugins 36 | !.yarn/releases 37 | !.yarn/versions 38 | 39 | # testing 40 | /coverage 41 | 42 | # next.js 43 | /.next/ 44 | /out/ 45 | 46 | # production 47 | /build 48 | 49 | # misc 50 | .DS_Store 51 | *.pem 52 | 53 | # debug 54 | npm-debug.log* 55 | yarn-debug.log* 56 | yarn-error.log* 57 | .pnpm-debug.log* 58 | 59 | # env files (can opt-in for committing if needed) 60 | .env 61 | 62 | # vercel 63 | .vercel 64 | 65 | # typescript 66 | *.tsbuildinfo 67 | next-env.d.ts 68 | .venv* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Working Memory 4 | 5 | ![CopilotKit-Banner](https://github.com/user-attachments/assets/8167c845-0381-45d9-ad1c-83f995d48290) 6 |
7 | 8 | Working Memory is an example for the implementation of the MCP server-client integrations to handle and manage your projects and tasks from your project management applications like Linear. 9 | 10 | ## Key Features 11 | 12 | - **CopilotKit AI Chat Interface:** 13 | Chat with the CopilotKit AI which acts as useful assitant who can able to provide answers to user queries and perform executable actions inside the application. 14 | 15 | - **Real-Time Interactivity:** 16 | Enjoy a live chat powered by `@copilotkit/react-ui` that orchestrates dynamic state changes and agent responses. 17 | 18 | - **State Management & Agent Coordination:** 19 | Leverages `@copilotkit/react-core` for robust agent state management and smooth integration of travel and research functionalities. 20 | 21 | - **Responsive & Modern UI:** 22 | Designed with Tailwind CSS to ensure your experience is smooth and adaptive across all devices. 23 | 24 | ## Technology Stack 25 | 26 | - **Framework:** [Next.js](https://nextjs.org) 27 | - **UI Library:** React, [CopilotKit UI](https://www.npmjs.com/package/@copilotkit/react-ui) 28 | - **State Management:** [CopilotKit React Core](https://www.npmjs.com/package/@copilotkit/react-core) 29 | 30 | - **Styling:** Tailwind CSS 31 | - **Additional Libraries:** 32 | - React Query for data fetching 33 | - Framer Motion for animations 34 | - Radix UI for accessible components 35 | - React Flow for flow diagrams 36 | 37 | ## Setup Instructions 38 | 39 | 1. **Prerequisites:** 40 | - [Node.js](https://nodejs.org) (LTS version recommended) 41 | - npm, yarn, or pnpm 42 | 43 | 2. **Installation:** 44 | ```bash 45 | # Clone the repository 46 | git clone 47 | 48 | # Install dependencies 49 | npm install 50 | # or 51 | yarn install 52 | # or 53 | pnpm install 54 | ``` 55 | 56 | 3. **Environment Setup:** 57 | Create a `.env` file in the root directory with the necessary environment variables. 58 | ```bash 59 | OPENAI_API_KEY = YOUR_API_KEY 60 | ``` 61 | 62 | 4. **Running the Development Server:** 63 | ```bash 64 | npm run dev 65 | # or 66 | yarn dev 67 | # or 68 | pnpm dev 69 | ``` 70 | Then, open [http://localhost:3000](http://localhost:3000) in your browser. 71 | 72 | ## Project Structure 73 | 74 | - **/src/app:** 75 | Contains Next.js page components, layouts, and global styles. 76 | 77 | - **/src/components:** 78 | Houses reusable components including agent interfaces (Travel, Research, Chat, Map, Sidebar) and UI elements. 79 | 80 | - **/src/providers:** 81 | Wraps the global state providers responsible for managing agent states. 82 | 83 | - **/src/lib:** 84 | Contains utility functions and configuration files. 85 | 86 | - **/src/hooks:** 87 | Custom React hooks for shared functionality. 88 | 89 | - **/src/contexts:** 90 | React context providers for global state management. 91 | 92 | ## Development 93 | 94 | - **Linting:** 95 | ```bash 96 | npm run lint 97 | # or 98 | yarn lint 99 | # or 100 | pnpm lint 101 | ``` 102 | 103 | - **Building for Production:** 104 | ```bash 105 | npm run build 106 | # or 107 | yarn build 108 | # or 109 | pnpm build 110 | ``` 111 | 112 | ## Deployment 113 | 114 | The easiest way to deploy this project is with [Vercel](https://vercel.com). Build and start your application with: 115 | ```bash 116 | npm run build 117 | npm run start 118 | ``` 119 | Follow Vercel's deployment guide for more details if needed. 120 | 121 | ## Contributing 122 | 123 | Contributions are welcome! Fork the repository and submit a pull request with any improvements, bug fixes, or new features. 124 | 125 | ## License 126 | 127 | Distributed under the MIT License. See `LICENSE` for more information. 128 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | //You only need to choose one of the following: 2 | 3 | OPENAI_API_KEY= 4 | NEXT_PUBLIC_COPILOT_CLOUD_API_KEY= 5 | 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | 13 | const eslintConfig = [ 14 | ...compat.config({ 15 | extends: ['next'], 16 | rules: { 17 | 'react/no-unescaped-entities': 'off', 18 | 'react/no-unknown-property': 'off', 19 | 'react/no-unused-vars': 'off', 20 | 21 | '@next/next/no-page-custom-font': 'off', 22 | }, 23 | }), 24 | ] 25 | 26 | export default eslintConfig; 27 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | eslint:{ 6 | ignoreDuringBuilds: true, 7 | } 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@copilotkit/react-core": "1.8.9", 13 | "@copilotkit/react-ui": "1.8.9", 14 | "@copilotkit/runtime": "^1.8.9", 15 | "@emotion/react": "^11.14.0", 16 | "@emotion/styled": "^11.14.0", 17 | "@modelcontextprotocol/sdk": "^1.10.2", 18 | "@mui/icons-material": "^7.0.2", 19 | "@radix-ui/react-checkbox": "^1.1.4", 20 | "@radix-ui/react-dialog": "^1.1.6", 21 | "@radix-ui/react-separator": "^1.1.2", 22 | "@radix-ui/react-slot": "^1.1.2", 23 | "@radix-ui/react-tooltip": "^1.1.8", 24 | "@tanstack/react-query": "^5.66.0", 25 | "@tanstack/react-query-devtools": "^5.66.0", 26 | "@types/dagre": "^0.7.52", 27 | "@types/leaflet": "^1.9.16", 28 | "class-variance-authority": "^0.7.1", 29 | "clsx": "^2.1.1", 30 | "dagre": "^0.8.5", 31 | "framer-motion": "^12.7.4", 32 | "leaflet": "^1.9.4", 33 | "leaflet-defaulticon-compatibility": "^0.1.2", 34 | "lucide-react": "^0.474.0", 35 | "next": "15.1.6", 36 | "react": "^19.0.0", 37 | "react-dom": "^19.0.0", 38 | "react-leaflet": "^5.0.0", 39 | "react-markdown": "^9.0.3", 40 | "reactflow": "^11.11.4", 41 | "tailwind-merge": "^3.0.1", 42 | "tailwindcss-animate": "^1.0.7" 43 | }, 44 | "devDependencies": { 45 | "@eslint/eslintrc": "^3", 46 | "@types/node": "^20", 47 | "@types/react": "^19", 48 | "@types/react-dom": "^19", 49 | "cursor-tools": "latest", 50 | "eslint": "^9", 51 | "eslint-config-next": "15.1.6", 52 | "postcss": "^8", 53 | "tailwindcss": "^3.4.1", 54 | "typescript": "^5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/copilotkit-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/copilotkit-mcp-demo/f965d88d27bcdfe9ade8593de0acb82d506ef244/public/copilotkit-logo-dark.png -------------------------------------------------------------------------------- /public/copilotkit-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/copilotkit-mcp-demo/f965d88d27bcdfe9ade8593de0acb82d506ef244/public/copilotkit-logo-light.png -------------------------------------------------------------------------------- /public/map-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/copilotkit-mcp-demo/f965d88d27bcdfe9ade8593de0acb82d506ef244/public/map-overlay.png -------------------------------------------------------------------------------- /src/app/(canvas-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 |
{children}
5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(canvas-pages)/page.tsx: -------------------------------------------------------------------------------- 1 | import Canvas from "@/components/canvas"; 2 | // import WorkingMemory from "@/components/working-memory"; 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 20 14.3% 4.1%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 20 14.3% 4.1%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 20 14.3% 4.1%; 17 | --primary: 24 9.8% 10%; 18 | --primary-foreground: 60 9.1% 97.8%; 19 | --secondary: 60 4.8% 95.9%; 20 | --secondary-foreground: 24 9.8% 10%; 21 | --muted: 60 4.8% 95.9%; 22 | --muted-foreground: 25 5.3% 44.7%; 23 | --accent: 60 4.8% 95.9%; 24 | --accent-foreground: 24 9.8% 10%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 60 9.1% 97.8%; 27 | --border: 20 5.9% 90%; 28 | --input: 20 5.9% 90%; 29 | --ring: 20 14.3% 4.1%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | --sidebar-background: 0 0% 98%; 37 | --sidebar-foreground: 240 5.3% 26.1%; 38 | --sidebar-primary: 240 5.9% 10%; 39 | --sidebar-primary-foreground: 0 0% 98%; 40 | --sidebar-accent: 240 4.8% 95.9%; 41 | --sidebar-accent-foreground: 240 5.9% 10%; 42 | --sidebar-border: 220 13% 91%; 43 | --sidebar-ring: 217.2 91.2% 59.8%; 44 | } 45 | .dark { 46 | --background: 20 14.3% 4.1%; 47 | --foreground: 60 9.1% 97.8%; 48 | --card: 20 14.3% 4.1%; 49 | --card-foreground: 60 9.1% 97.8%; 50 | --popover: 20 14.3% 4.1%; 51 | --popover-foreground: 60 9.1% 97.8%; 52 | --primary: 60 9.1% 97.8%; 53 | --primary-foreground: 24 9.8% 10%; 54 | --secondary: 12 6.5% 15.1%; 55 | --secondary-foreground: 60 9.1% 97.8%; 56 | --muted: 12 6.5% 15.1%; 57 | --muted-foreground: 24 5.4% 63.9%; 58 | --accent: 12 6.5% 15.1%; 59 | --accent-foreground: 60 9.1% 97.8%; 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 60 9.1% 97.8%; 62 | --border: 12 6.5% 15.1%; 63 | --input: 12 6.5% 15.1%; 64 | --ring: 24 5.7% 82.9%; 65 | --chart-1: 220 70% 50%; 66 | --chart-2: 160 60% 45%; 67 | --chart-3: 30 80% 55%; 68 | --chart-4: 280 65% 60%; 69 | --chart-5: 340 75% 55%; 70 | --sidebar-background: 240 5.9% 10%; 71 | --sidebar-foreground: 240 4.8% 95.9%; 72 | --sidebar-primary: 224.3 76.3% 48%; 73 | --sidebar-primary-foreground: 0 0% 100%; 74 | --sidebar-accent: 240 3.7% 15.9%; 75 | --sidebar-accent-foreground: 240 4.8% 95.9%; 76 | --sidebar-border: 240 3.7% 15.9%; 77 | --sidebar-ring: 217.2 91.2% 59.8%; 78 | } 79 | } 80 | 81 | @layer base { 82 | * { 83 | @apply border-border; 84 | } 85 | body { 86 | @apply bg-background text-foreground; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/copilotkit-mcp-demo/f965d88d27bcdfe9ade8593de0acb82d506ef244/src/app/icon.png -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter, JetBrains_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import Providers from "@/providers/Providers"; 5 | 6 | const inter = Inter({ 7 | variable: "--font-inter", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const jetbrainsMono = JetBrains_Mono({ 12 | variable: "--font-jetbrains", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "MCP Demo app", 18 | description: "MCP Demo app by CopilotKit", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, memo, useMemo } from "react"; 2 | import { Loader2 } from "lucide-react"; 3 | 4 | interface LoaderProps { texts: string[] } 5 | 6 | export const Loader: React.FC = memo(({ texts }) => { 7 | return ( 8 |
9 | 10 | {texts[0]} 11 |
12 | ); 13 | }); 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/MCPToolCall.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader2 } from "lucide-react"; 4 | import * as React from "react"; 5 | 6 | interface ToolCallProps { 7 | status: "complete" | "inProgress" | "executing"; 8 | name?: string; 9 | args?: any; 10 | result?: any; 11 | } 12 | 13 | export default function MCPToolCall({ 14 | status, 15 | name = "", 16 | args, 17 | result, 18 | }: ToolCallProps) { 19 | const [isOpen, setIsOpen] = React.useState(false); 20 | 21 | // Format content for display 22 | const format = (content: any): string => { 23 | if (!content) return ""; 24 | const text = 25 | typeof content === "object" 26 | ? JSON.stringify(content, null, 2) 27 | : String(content); 28 | return text 29 | .replace(/\\n/g, "\n") 30 | .replace(/\\t/g, "\t") 31 | .replace(/\\"/g, '"') 32 | .replace(/\\\\/g, "\\"); 33 | }; 34 | 35 | const getStatusIcon = () => { 36 | 37 | if (status === "complete") { 38 | console.log(result, "MCPToolCall Result"); 39 | if (result && result.error) { 40 | const errorMessage = JSON.stringify(result.error); 41 | // window.alert(`Tool Call Error (${name || 'Unknown Tool'}):\n${errorMessage}`); 42 | console.log(errorMessage, "MCPToolCall Error"); 43 | return ( 44 | 45 | 46 | 47 | ); 48 | } 49 | else { 50 | return (( result=="" ? false : JSON.parse(result?.content[0].text)?.error)) ? ( 51 | 52 | 53 | 54 | ) : ( 55 | 56 | 57 | 58 | ); 59 | } 60 | } 61 | return ( 62 | 63 | ); 64 | }; 65 | 66 | return ( 67 |
71 |
setIsOpen(!isOpen)} 74 | > 75 |
76 |
77 | {getStatusIcon()} 78 |
79 | 80 | {name || "MCP Tool Call"} 81 | 82 |
83 |
84 | 85 | {/* {isOpen && ( 86 |
87 | {args && ( 88 |
89 |
Parameters:
90 |
 91 |                 {format(args)}
 92 |               
93 |
94 | )} 95 | 96 | {status === "complete" && result && ( 97 |
98 |
Result:
99 |
100 |                 {format(result)}
101 |               
102 |
103 | )} 104 |
105 | )} */} 106 |
107 | ); 108 | } -------------------------------------------------------------------------------- /src/components/McpServerManager.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCopilotChat } from "@copilotkit/react-core"; 4 | import { useEffect } from "react"; 5 | import { Config } from "@/providers/Providers"; 6 | 7 | function McpServerManager({configs}:{configs:Config[]}) { 8 | const { setMcpServers } = useCopilotChat(); 9 | 10 | useEffect(() => { 11 | setMcpServers(configs); 12 | }, [setMcpServers]); 13 | 14 | return null; 15 | } 16 | 17 | export default McpServerManager; -------------------------------------------------------------------------------- /src/components/Nodes.tsx: -------------------------------------------------------------------------------- 1 | import { SubTask, Todo } from "@/contexts/TodoContext"; 2 | import "../app/globals.css"; 3 | import { Handle, Position } from "reactflow"; 4 | import DoneRoundedIcon from '@mui/icons-material/DoneRounded'; 5 | const ParentNode = ({ data }: { data: Todo }) => { 6 | // console.log(data, "data from parent"); 7 | return ( 8 |
20 |
{data.text}
21 | {data.completed && ( 22 |
35 | 36 |
37 | )} 38 | 39 | 40 |
41 | ); 42 | }; 43 | 44 | const ChildNode = ({ data }: { data: SubTask }) => { 45 | // console.log(data, "data from child"); 46 | return ( 47 |
59 |
{data.text}
60 | {data.completed && ( 61 |
74 | 75 |
76 | )} 77 | 78 |
79 | ); 80 | }; 81 | 82 | export { ParentNode, ChildNode }; -------------------------------------------------------------------------------- /src/components/Storage.tsx: -------------------------------------------------------------------------------- 1 | import { useTodo } from "@/contexts/TodoContext"; 2 | import { useEffect, useState, useCallback } from "react"; 3 | import ReactFlow, { 4 | Node, 5 | Edge, 6 | Controls, 7 | Background, 8 | useNodesState, 9 | useEdgesState, 10 | Position, 11 | } from "reactflow"; 12 | import "reactflow/dist/style.css"; 13 | import styled from "@emotion/styled"; 14 | import { motion } from "framer-motion"; 15 | 16 | const TodoNode = styled.div` 17 | padding: 10px 20px; 18 | border-radius: 8px; 19 | background: white; 20 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 21 | border: 2px solid ${(props: { completed: boolean }) => 22 | props.completed ? "#10B981" : "#E5E7EB"}; 23 | min-width: 150px; 24 | text-align: center; 25 | transition: all 0.3s ease; 26 | 27 | &:hover { 28 | transform: translateY(-2px); 29 | box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15); 30 | } 31 | `; 32 | 33 | const NodeContent = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | gap: 8px; 37 | `; 38 | 39 | const NodeTitle = styled.h3` 40 | margin: 0; 41 | font-size: 14px; 42 | color: ${(props: { completed: boolean }) => 43 | props.completed ? "#6B7280" : "#1F2937"}; 44 | text-decoration: ${(props: { completed: boolean }) => 45 | props.completed ? "line-through" : "none"}; 46 | `; 47 | 48 | const SubTaskList = styled.div` 49 | display: flex; 50 | flex-direction: column; 51 | gap: 4px; 52 | margin-top: 8px; 53 | `; 54 | 55 | const SubTaskItem = styled.div` 56 | font-size: 12px; 57 | color: ${(props: { completed: boolean }) => 58 | props.completed ? "#6B7280" : "#4B5563"}; 59 | text-decoration: ${(props: { completed: boolean }) => 60 | props.completed ? "line-through" : "none"}; 61 | padding: 4px 8px; 62 | background: ${(props: { completed: boolean }) => 63 | props.completed ? "#F3F4F6" : "#F9FAFB"}; 64 | border-radius: 4px; 65 | `; 66 | 67 | const CustomNode = ({ data }: { data: any }) => { 68 | return ( 69 | 70 | 71 | {data.label} 72 | {data.subtasks && data.subtasks.length > 0 && ( 73 | 74 | {data.subtasks.map((subtask: any) => ( 75 | 76 | {subtask.text} 77 | 78 | ))} 79 | 80 | )} 81 | 82 | 83 | ); 84 | }; 85 | 86 | const nodeTypes = { 87 | custom: CustomNode, 88 | }; 89 | 90 | function VisualRepresentation() { 91 | const { todos } = useTodo(); 92 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 93 | const [edges, setEdges, onEdgesChange] = useEdgesState([]); 94 | const [selectedTodo, setSelectedTodo] = useState(null); 95 | 96 | const generateGraph = useCallback(() => { 97 | const newNodes: Node[] = []; 98 | const newEdges: Edge[] = []; 99 | 100 | if (selectedTodo === null) { 101 | // Show all parent tasks 102 | todos.forEach((todo, index) => { 103 | newNodes.push({ 104 | id: `todo-${todo.id}`, 105 | type: "custom", 106 | position: { x: index * 250, y: 100 }, 107 | data: { 108 | label: todo.text, 109 | completed: todo.completed, 110 | subtasks: todo.subtasks, 111 | }, 112 | sourcePosition: Position.Bottom, 113 | targetPosition: Position.Top, 114 | }); 115 | }); 116 | } else { 117 | // Show selected todo and its subtasks 118 | const selectedTodoData = todos.find(t => t.id === selectedTodo); 119 | if (selectedTodoData) { 120 | // Parent node 121 | newNodes.push({ 122 | id: `todo-${selectedTodoData.id}`, 123 | type: "custom", 124 | position: { x: 0, y: 0 }, 125 | data: { 126 | label: selectedTodoData.text, 127 | completed: selectedTodoData.completed, 128 | subtasks: [], 129 | }, 130 | sourcePosition: Position.Bottom, 131 | targetPosition: Position.Top, 132 | }); 133 | 134 | // Subtask nodes 135 | selectedTodoData.subtasks.forEach((subtask, index) => { 136 | newNodes.push({ 137 | id: `subtask-${subtask.id}`, 138 | type: "custom", 139 | position: { x: (index + 1) * 200, y: 150 }, 140 | data: { 141 | label: subtask.text, 142 | completed: subtask.completed, 143 | }, 144 | sourcePosition: Position.Bottom, 145 | targetPosition: Position.Top, 146 | }); 147 | 148 | newEdges.push({ 149 | id: `edge-${selectedTodoData.id}-${subtask.id}`, 150 | source: `todo-${selectedTodoData.id}`, 151 | target: `subtask-${subtask.id}`, 152 | animated: true, 153 | style: { stroke: subtask.completed ? "#10B981" : "#E5E7EB" }, 154 | }); 155 | }); 156 | } 157 | } 158 | 159 | setNodes(newNodes); 160 | setEdges(newEdges); 161 | }, [todos, selectedTodo]); 162 | 163 | useEffect(() => { 164 | generateGraph(); 165 | }, [generateGraph]); 166 | 167 | const onNodeClick = (event: any, node: Node) => { 168 | const todoId = parseInt(node.id.split("-")[1]); 169 | setSelectedTodo(selectedTodo === todoId ? null : todoId); 170 | }; 171 | 172 | return ( 173 | 179 | 188 | 189 | 190 | 191 | 192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import { useTodo } from "@/contexts/TodoContext"; 2 | import { useState, ChangeEvent } from "react"; 3 | import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core"; 4 | import { Check, ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react"; 5 | 6 | 7 | 8 | export const TodoApp = () => { 9 | const { todos, addTodo, toggleTodo, deleteTodo, toggleAccordion, addSubtask, toggleSubtask, deleteSubtask, addTaskAndSubtask } = useTodo(); 10 | const [newTodo, setNewTodo] = useState(""); 11 | const [newSubtask, setNewSubtask] = useState<{ parentId: number | null; text: string }>({ 12 | parentId: null, 13 | text: "", 14 | }); 15 | // const subtaskInputRef = useRef(null); 16 | 17 | const handleAddTodo = () => { 18 | if (newTodo.trim() === "") return; 19 | addTodo(newTodo); 20 | setNewTodo(""); 21 | }; 22 | 23 | const handleAddSubtask = (parentIndex: number) => { 24 | if (newSubtask.text.trim() === "") return; 25 | addSubtask(parentIndex, newSubtask.text); 26 | setNewSubtask({ parentId: null, text: "" }); 27 | 28 | }; 29 | 30 | const handleSubtaskChange = (e: ChangeEvent, todoId: number) => { 31 | setNewSubtask({ parentId: todoId, text: e.target.value }); 32 | }; 33 | 34 | useCopilotAction({ 35 | name: "ADD_TASK", 36 | description: "Adds a task to the todo list", 37 | parameters: [ 38 | { 39 | name: "title", 40 | type: "string", 41 | description: "The title of the task", 42 | required: true, 43 | }, 44 | ], 45 | handler: ({ title }) => { 46 | addTodo(title); 47 | } 48 | }); 49 | 50 | useCopilotAction({ 51 | name: "ADD_SUBTASK", 52 | description: "Adds a subtask to the todo list", 53 | parameters: [ 54 | { 55 | name: "id", 56 | type: "number", 57 | description: "The id of the parent task in the todo list", 58 | required: true, 59 | }, 60 | { 61 | name: "subtask", 62 | type: "string", 63 | description: "The subtask to add", 64 | required: true, 65 | }, 66 | ], 67 | handler: ({ id, subtask }) => { 68 | debugger 69 | addSubtask(id, subtask); 70 | } 71 | }); 72 | 73 | 74 | useCopilotReadable({ 75 | description: "The current state of the todo list", 76 | value: JSON.stringify(todos), 77 | }) 78 | 79 | useCopilotAction({ 80 | name: "ADD_TASK_AND_SUBTASK", 81 | description: "Adds a task and its subtask to the todo list", 82 | parameters: [ 83 | { 84 | name: "title", 85 | type: "string", 86 | description: "The title of the task", 87 | required: true, 88 | }, 89 | { 90 | name: "subtask", 91 | type: "string[]", 92 | description: "The subtask to add", 93 | required: true, 94 | }, 95 | ], 96 | handler: ({ title, subtask }) => { 97 | addTaskAndSubtask(title, subtask); 98 | } 99 | }); 100 | 101 | useCopilotAction({ 102 | name: "DELETE_TASK", 103 | description: "Deletes a parent todo item from the todo list", 104 | parameters: [ 105 | { 106 | name: "id", 107 | type: "number", 108 | description: "The id of the todo item to be deleted", 109 | required: true, 110 | }, 111 | ], 112 | handler: ({ id }) => { 113 | deleteTodo(id); 114 | }, 115 | }); 116 | 117 | useCopilotAction({ 118 | name: "DELETE_SUBTASK", 119 | description: "Deletes a subtask from the todo list", 120 | parameters: [ 121 | { 122 | name: "parentId", 123 | type: "number", 124 | description: "The id of the parent todo item to be deleted", 125 | required: true, 126 | }, 127 | { 128 | name: "subtaskId", 129 | type: "number", 130 | description: "The id of the subtask to be deleted", 131 | required: true, 132 | }, 133 | ], 134 | handler: ({ parentId, subtaskId }) => { 135 | deleteSubtask(parentId, subtaskId); 136 | }, 137 | }); 138 | 139 | useCopilotAction({ 140 | name: "COMPLETE_TASK", 141 | description: "Completes a parent todo item from the todo list", 142 | parameters: [ 143 | { 144 | name: "id", 145 | type: "number", 146 | description: "The id of the todo item to be completed", 147 | required: true, 148 | }, 149 | ], 150 | handler: ({ id }) => { 151 | toggleTodo(id); 152 | }, 153 | }); 154 | 155 | useCopilotAction({ 156 | name: "COMPLETE_SUBTASK", 157 | description: "Completes a subtask from the todo list", 158 | parameters: [ 159 | { 160 | name: "parentId", 161 | type: "number", 162 | description: "The id of the parent todo item", 163 | required: true, 164 | }, 165 | { 166 | name: "subtaskId", 167 | type: "number", 168 | description: "The id of the subtask to be completed", 169 | required: true, 170 | }, 171 | ], 172 | handler: ({ parentId, subtaskId }) => { 173 | toggleSubtask(parentId, subtaskId); 174 | }, 175 | }); 176 | 177 | return ( 178 |
179 |
180 |

Focused Actions

181 |

Immediate items to complete

182 |
183 | 184 |
185 | setNewTodo(e.target.value)} 189 | onKeyDown={(e) => e.key === "Enter" && handleAddTodo()} 190 | placeholder="Add a new task..." 191 | className="flex-grow p-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500" 192 | /> 193 | 199 |
200 | 201 |
202 | {todos.length === 0 ? ( 203 |

No todos yet. Add one above!

204 | ) : ( 205 | todos.map((todo,index) => ( 206 |
210 |
toggleAccordion(todo.id)} 213 | > 214 |
215 | 228 | 240 | 245 | {todo.text} 246 | 247 |
248 |
249 | {todo.subtasks.length > 0 && ( 250 | 251 | {todo.subtasks.length} 252 | 253 | )} 254 | 263 |
264 |
265 | 266 | {todo.expanded && ( 267 |
268 |
269 | handleSubtaskChange(e, todo.id)} 273 | onKeyDown={(e) => e.key === "Enter" && handleAddSubtask(todo.id)} 274 | placeholder="Add a subtask..." 275 | className="flex-grow p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" 276 | /> 277 | 283 |
284 | 285 | {todo.subtasks.length > 0 ? ( 286 |
287 | {todo.subtasks.map((subtask) => ( 288 |
292 |
293 | 305 | 309 | {subtask.text} 310 | 311 |
312 | 321 |
322 | ))} 323 |
324 | ) : ( 325 |

No subtasks yet

326 | )} 327 |
328 | )} 329 |
330 | )) 331 | )} 332 |
333 |
334 | ); 335 | }; -------------------------------------------------------------------------------- /src/components/ToolRenderer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | useCopilotAction, 5 | CatchAllActionRenderProps, 6 | } from "@copilotkit/react-core"; 7 | import MCPToolCall from "./MCPToolCall"; 8 | 9 | export function ToolRenderer() { 10 | useCopilotAction({ 11 | /** 12 | * The asterisk (*) matches all tool calls 13 | */ 14 | name: "*", 15 | render: ({ name, status, args, result }: CatchAllActionRenderProps<[]>) => { 16 | return 17 | }, 18 | }); 19 | return null; 20 | } -------------------------------------------------------------------------------- /src/components/VisualRepresentation.tsx: -------------------------------------------------------------------------------- 1 | import { useTodo } from '@/contexts/TodoContext'; 2 | import React, { useEffect, useMemo } from 'react'; 3 | import "../app/globals.css"; 4 | 5 | import { 6 | Background, 7 | ReactFlow, 8 | useNodesState, 9 | useEdgesState, 10 | useReactFlow, 11 | ReactFlowProvider, 12 | Edge, 13 | Node, 14 | } from 'reactflow'; 15 | import "reactflow/dist/style.css"; 16 | import { ChildNode, ParentNode } from './Nodes'; 17 | 18 | 19 | 20 | const VisualRepresentation = () => { 21 | const { todos,toggleTodo,toggleSubtask } = useTodo(); 22 | // const reactFlowWrapper = useRef(null); 23 | 24 | const [nodes, setNodes, onNodesChange] = useNodesState([]); 25 | const [edges, setEdges] = useEdgesState([]); 26 | const { fitView } = useReactFlow(); 27 | 28 | // This effect calculates nodes/edges based on todos 29 | useEffect(() => { 30 | console.log("Calculating nodes/edges based on todos:", todos); 31 | const units: Node[] = todos.flatMap((item) => { 32 | if (!item.expanded && todos.length != 1) return []; 33 | const arr: Node[] = [{ 34 | id: item.id.toString(), 35 | data: item, 36 | position: { x: nodes.find((node) => node.id.toString() === item.id.toString())?.position.x || 0 * 100, y: nodes.find((node) => node.id.toString() === item.id.toString())?.position.y || 0 * 100 }, 37 | type: "ParentNode", 38 | }] 39 | if (item.subtasks.length > 0) { 40 | for (let i = 0; i < item.subtasks.length; i++) { 41 | arr.push({ 42 | id: item.subtasks[i].id.toString(), 43 | data: {...item.subtasks[i],parentId:item.id.toString()}, 44 | position: { 45 | x: nodes.find((node) => node.id.toString() === item.subtasks[i].id.toString())?.position.x || (i % 2 == 0 ? -100 : 100), 46 | y: nodes.find((node) => node.id.toString() === item.subtasks[i].id.toString())?.position.y || (i % 2 == 0 ? (i * 100) + 100 : ((i - 1) * 100) + 100) 47 | }, 48 | type: "ChildNode", 49 | }) 50 | } 51 | } 52 | return [...arr]; 53 | }); 54 | setNodes(units); 55 | const edges: Edge[] = todos.flatMap((item) => { 56 | if (!item.expanded && todos.length != 1) return []; 57 | if (item.subtasks.length > 0) { 58 | let arr = []; 59 | for (let i = 0; i < item.subtasks.length; i++) { 60 | arr.push({ 61 | id: `${item.id}-${item.subtasks[i].id}`, 62 | source: item.id.toString(), 63 | target: item.subtasks[i].id.toString(), 64 | animated: true, 65 | }) 66 | } 67 | console.log(arr, "arr"); 68 | return [...arr]; 69 | } 70 | return []; 71 | }) 72 | setEdges(edges); 73 | }, [todos]); 74 | 75 | // This separate effect calls fitView when nodes change 76 | useEffect(() => { 77 | // Only fit view if there are nodes 78 | if (nodes.length > 0) { 79 | console.log("Nodes changed, calling fitView"); 80 | // Call fitView after a short delay 81 | const timer = setTimeout(() => { 82 | fitView({ padding: 0.2, duration: 200 }); 83 | }, 50); // Slightly increased delay 84 | return () => clearTimeout(timer); // Cleanup timeout 85 | } 86 | }, [nodes, fitView]); 87 | 88 | // --- Calculate progress (Placeholder logic) --- 89 | // This assumes we are showing progress for the *first* expanded todo 90 | // A more robust solution would need context on which task is "active" 91 | const activeTodo = useMemo(() => todos.find(todo => todo.expanded) || (todos.length > 0 ? todos[0] : null), [todos]); 92 | const totalSubtasks = useMemo(() => activeTodo?.subtasks.length || 0, [activeTodo]); 93 | const completedSubtasks = useMemo(() => activeTodo?.subtasks.filter(sub => sub.completed).length || 0, [activeTodo]); 94 | const progressValue = useMemo(() => (totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0), [completedSubtasks, totalSubtasks]); 95 | const taskTitle = useMemo(() => activeTodo?.text || "Task Overview", [activeTodo]); 96 | 97 | 98 | return ( 99 | // Wrap existing content in a flex column container 100 |
101 | {/* Header Section */} 102 |
103 |

{taskTitle}

104 |
105 | {completedSubtasks} of {totalSubtasks} subtasks completed 106 |
107 |
111 |
112 |
113 |
114 | 115 | {/* Visualization Label */} 116 |

Task Visualization Tree

117 | 118 | {/* React Flow Area - make it flexible */} 119 |
{/* Added border and rounded */} 120 | { 126 | if(node.type == "ParentNode"){ 127 | // Only toggle if there's more than one todo, otherwise it collapses the only view 128 | if (todos.length > 1) { 129 | toggleTodo(node.data.id); 130 | } 131 | } 132 | else{ 133 | toggleSubtask(parseInt(node.data.parentId), node.data.id); 134 | } 135 | }} 136 | // fitView // Consider re-enabling fitView if needed 137 | defaultViewport={{ x: 200, y: 100, zoom: 1 }} 138 | nodeTypes={nodeTypes} 139 | proOptions={{ hideAttribution: true }} // Hide React Flow attribution 140 | > 141 | 142 | 143 |
144 |
145 | ); 146 | }; 147 | 148 | 149 | const nodeTypes = { 150 | ParentNode: ParentNode, 151 | ChildNode: ChildNode, 152 | } 153 | export default function Test() { 154 | return ( 155 | 156 | 157 | 158 | ); 159 | } -------------------------------------------------------------------------------- /src/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { Input } from "@/components/ui/input"; 5 | import { 6 | Sidebar, 7 | SidebarContent, 8 | SidebarFooter, 9 | SidebarGroup, 10 | SidebarHeader, 11 | } from "@/components/ui/sidebar"; 12 | import * as Dialog from "@radix-ui/react-dialog"; 13 | import { Mail, User, ClipboardList } from "lucide-react"; 14 | import Image from "next/image"; 15 | import Link from "next/link"; 16 | 17 | export function AppSidebar() { 18 | return ( 19 | 20 | 21 |
22 | OMAC 29 |

OMAC

30 |
31 |
32 | 33 | 34 |
35 | 36 | 37 | Task Manager 38 | 39 |
40 |
41 |
42 | 43 | 44 |
45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | Update Profile 57 | 58 |
59 |
60 | 63 | 68 |
69 |
70 | 73 | 78 |
79 |
80 | 83 | 88 |
89 |
90 | 91 | 94 | 95 | 96 |
97 |
98 |
99 |
100 |
101 | 106 |
107 |
108 | Version 0.0.1 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/canvas.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Skeletons from "@/components/skeletons"; 4 | import { Settings } from "lucide-react"; 5 | import React, { Suspense, useState } from "react"; 6 | import { ChatWindow } from "./chat-window"; 7 | import { MCPConfigModal } from "./mcp-config-modal"; 8 | import { TodoProvider } from "@/contexts/TodoContext"; 9 | import { TodoApp } from "./Todo"; 10 | import VisualRepresentation from "./VisualRepresentation"; 11 | import { useCopilotChatSuggestions } from "@copilotkit/react-ui"; 12 | 13 | export default function Canvas() { 14 | const [showMCPConfigModal, setShowMCPConfigModal] = useState(false); 15 | useCopilotChatSuggestions( 16 | { 17 | instructions: 18 | "Check the Asana workspace. Make sure it's the parent workspace. If Asana is connected, first get the workspace projects and ID details, then read them back to me. Then, suggest creating a ticket in Asana with each task as a bullet point. If Typefully is connected, suggest a draft tweet with the Asana tasks an individual Tweet in Typefully.", 19 | minSuggestions: 1, 20 | maxSuggestions: 2, 21 | }, 22 | [] 23 | ); 24 | return ( 25 | 26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |

Working Memory

34 | 41 |
42 | 43 |
44 |
45 | }> 46 | 47 | 48 |
49 | 50 |
51 | }> 52 | 53 | 54 |
55 |
56 |
57 | 58 | setShowMCPConfigModal(false)} 61 | /> 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/chat-window.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTodo } from "@/contexts/TodoContext"; 3 | import { CopilotChat } from "@copilotkit/react-ui"; 4 | import "@copilotkit/react-ui/styles.css"; 5 | import { Loader2, RotateCw, SendIcon, Square } from "lucide-react"; 6 | import { FC } from "react"; 7 | import { Loader } from "./Loader"; 8 | 9 | export const ChatWindow: FC = () => { 10 | const { todos } = useTodo(); 11 | return ( 12 | 28 | ), 29 | activityIcon: ( 30 | 37 | ), 38 | spinnerIcon: , 39 | stopIcon: ( 40 | 41 | ), 42 | regenerateIcon: ( 43 | 44 | ), 45 | }} 46 | /> 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/coagents-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // import { AvailableAgents } from "@/lib/available-agents"; 3 | 4 | /** 5 | * Base Agent State 6 | */ 7 | 8 | 9 | /** 10 | * Travel Agent Types 11 | */ 12 | export type Place = { 13 | id: string; 14 | name: string; 15 | address: string; 16 | latitude: number; 17 | longitude: number; 18 | rating: number; 19 | description?: string; 20 | }; 21 | 22 | export type Trip = { 23 | id: string; 24 | name: string; 25 | center_latitude: number; 26 | center_longitude: number; 27 | zoom_level?: number | 13; 28 | places: Place[]; 29 | }; 30 | 31 | export type SearchProgress = { 32 | query: string; 33 | done: boolean; 34 | }; 35 | 36 | 37 | /** 38 | * Research Agent Types 39 | */ 40 | export interface Section { 41 | title: string; 42 | content: string; 43 | idx: number; 44 | footer?: string; 45 | id: string; 46 | } 47 | 48 | export interface Source { 49 | content: string; 50 | published_date: string; 51 | score: number; 52 | title: string; 53 | url: string; 54 | } 55 | export type Sources = Record; 56 | 57 | export interface Log { 58 | message: string; 59 | done: boolean; 60 | } 61 | 62 | /** 63 | * This provider wraps state from all agents 64 | */ 65 | export const CoAgentsProvider = ({ 66 | children, 67 | }: { 68 | children: React.ReactNode; 69 | }) => { 70 | 71 | 72 | 73 | return ( 74 | <> 75 | {children} 76 | 77 | ); 78 | }; 79 | 80 | -------------------------------------------------------------------------------- /src/components/mcp-config-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef, useContext } from "react"; 4 | import { useCoAgent, useCopilotChat } from "@copilotkit/react-core"; 5 | import { useLocalStorage } from "@/hooks/use-local-storage"; 6 | import { ConnectionType, ServerConfig, MCP_STORAGE_KEY, MCPConfig } from "@/lib/mcp-config-types"; 7 | import { X, Plus, Server, Globe, Trash2 } from "lucide-react"; 8 | import { ServerConfigsContext } from "@/providers/Providers"; 9 | // External link icon component 10 | const ExternalLink = () => ( 11 | 18 | 24 | 25 | ); 26 | 27 | interface MCPConfigModalProps { 28 | isOpen: boolean; 29 | onClose: () => void; 30 | } 31 | 32 | interface Config { 33 | endpoint: string; 34 | serverName: string; 35 | } 36 | export function MCPConfigModal({ isOpen, onClose }: MCPConfigModalProps) { 37 | // Use ref to avoid re-rendering issues 38 | const configsRef = useRef>({}); 39 | 40 | // Use localStorage hook for persistent storage 41 | const [savedConfigs, setSavedConfigs] = useLocalStorage< 42 | Record 43 | >(MCP_STORAGE_KEY, {}); 44 | // console.log(savedConfigs, "savedConfigs"); 45 | // Set the ref value once we have the saved configs 46 | useEffect(() => { 47 | if (Object.keys(savedConfigs).length > 0) { 48 | configsRef.current = savedConfigs; 49 | } 50 | }, [savedConfigs]); 51 | 52 | const con = useContext(ServerConfigsContext); 53 | const [configs, setConfigs] = useState(con?.config || []); 54 | const [mcpConfig, setMcpConfig] = useLocalStorage("mcpConfig", []); 55 | const [serverName, setServerName] = useState(""); 56 | const [connectionType, setConnectionType] = useState("sse"); 57 | const [command, setCommand] = useState(""); 58 | const [args, setArgs] = useState(""); 59 | const [url, setUrl] = useState(""); 60 | const [isLoading, setIsLoading] = useState(true); 61 | const [showAddServerForm, setShowAddServerForm] = useState(false); 62 | 63 | // Calculate server statistics 64 | const totalServers = configs.length; 65 | const stdioServers = 0 66 | const sseServers = configs.length 67 | 68 | const { setMcpServers } = useCopilotChat(); 69 | 70 | 71 | 72 | // Set loading to false when state is loaded 73 | useEffect(() => { 74 | setIsLoading(false); 75 | return () => { 76 | setMcpConfig(configs); 77 | } 78 | }, []); 79 | 80 | const addConfig = () => { 81 | if (!serverName) return; 82 | 83 | 84 | setConfigs([...configs, { 85 | endpoint: url, 86 | serverName: serverName, 87 | }]); 88 | con?.setConfig([...configs, { 89 | endpoint: url, 90 | serverName: serverName, 91 | }]); 92 | setMcpConfig([...configs, { 93 | endpoint: url, 94 | serverName: serverName, 95 | }]); 96 | setMcpServers([...configs, { 97 | endpoint: url, 98 | serverName: serverName, 99 | }]); 100 | 101 | // Reset form 102 | setServerName(""); 103 | setCommand(""); 104 | setArgs(""); 105 | setUrl(""); 106 | setShowAddServerForm(false); 107 | }; 108 | 109 | const removeConfig = (index: number) => { 110 | setConfigs((prev) => { return prev.filter((_item, i) => i != index) }); 111 | con?.setConfig(con?.config.filter((_item, i: number) => i != index)); 112 | setMcpConfig(mcpConfig.filter((_item: Config[], i: number) => i != index)); 113 | }; 114 | 115 | if (!isOpen) return null; 116 | 117 | if (isLoading) { 118 | return ( 119 |
120 |
121 |
Loading configuration...
122 |
123 |
124 | ); 125 | } 126 | 127 | return ( 128 |
129 |
130 | {/* Header */} 131 |
132 |
133 |
134 | 135 |

MCP Server Configuration

136 |
137 | 143 |
144 | 145 |
146 |

147 | Manage and configure your MCP servers 148 |

149 | 156 |
157 |
158 | 159 | {/* Server Statistics */} 160 |
161 |
162 |
Total Servers
163 |
{totalServers}
164 |
165 |
166 |
Stdio Servers
167 |
{stdioServers}
168 |
169 |
170 |
SSE Servers
171 |
{sseServers}
172 |
173 |
174 | 175 | {/* Server List */} 176 |
177 |

Server List

178 | 179 | {totalServers === 0 ? ( 180 |
181 | No servers configured. Click "Add Server" to get started. 182 |
183 | ) : ( 184 |
185 | {configs.map((config, index) => ( 186 |
190 |
191 |
192 |
193 |

{config?.serverName}

194 |
195 | {/* {config.transport === "stdio" ? ( 196 | 197 | ) : ( 198 | 199 | )} */} 200 | 201 | 202 | {/* {config.transport} */} 203 | SSE 204 |
205 |
206 | 212 |
213 |
214 | {/* {config.transport === "stdio" ? ( 215 | <> 216 |

Command: {config.command}

217 |

218 | Args: {config.args.join(" ")} 219 |

220 | 221 | ) : ( */} 222 |

URL: {config.endpoint}

223 | {/* )} */} 224 |
225 |
226 |
227 | ))} 228 |
229 | )} 230 | 231 | {/* Reference */} 232 |
233 | More MCP servers available on the web, e.g.{" "} 234 | 240 | mcp.composio.dev 241 | 242 | 243 | and{" "} 244 | 250 | mcp.run 251 | 252 | 253 |
254 |
255 | 256 | {/* Add Server Modal */} 257 | {showAddServerForm && ( 258 |
259 |
260 |
261 |

262 | 263 | Add New Server 264 |

265 | 271 |
272 | 273 |
274 |
275 | 278 | setServerName(e.target.value)} 282 | className="w-full px-3 py-2 border rounded-md text-sm" 283 | placeholder="e.g., api-service, data-processor" 284 | /> 285 |
286 | 287 | 288 | {connectionType === "stdio" ? ( 289 | <> 290 |
291 | 294 | setCommand(e.target.value)} 298 | className="w-full px-3 py-2 border rounded-md text-sm" 299 | placeholder="e.g., python, node" 300 | /> 301 |
302 |
303 | 306 | setArgs(e.target.value)} 310 | className="w-full px-3 py-2 border rounded-md text-sm" 311 | placeholder="e.g., path/to/script.py" 312 | /> 313 |
314 | 315 | ) : ( 316 |
317 | 318 | setUrl(e.target.value)} 322 | className="w-full px-3 py-2 border rounded-md text-sm" 323 | placeholder="e.g., http://localhost:8000/events" 324 | /> 325 |
326 | )} 327 | 328 |
329 | 336 | 343 |
344 |
345 |
346 |
347 | )} 348 |
349 |
350 | ); 351 | } 352 | -------------------------------------------------------------------------------- /src/components/skeletons/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Skeleton } from "../ui/skeleton"; 3 | 4 | export const EmailSkeleton: FC = () => ( 5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | ); 14 | 15 | export const EmailListSkeleton: FC = () => ( 16 |
17 | {Array.from({ length: 5 }).map((_, idx) => ( 18 |
22 | 23 |
24 |
25 |
26 | {/* Sender name */} 27 | {/* Time */} 28 |
29 | {" "} 30 | {/* Unread indicator */} 31 |
32 | {/* Subject */} 33 | {/* Preview text */} 34 |
35 |
36 | {/* Star/flag */} 37 | {/* Attachment */} 38 |
39 |
40 | ))} 41 |
42 | ); 43 | 44 | export const ResearchPaperSkeleton: FC = () => ( 45 |
46 | {/* Title */} 47 |
48 | 49 |
50 | 51 | {/* Sections */} 52 |
53 | {Array.from({ length: 3 }).map((_, i) => ( 54 |
55 | {/* Section title */} 56 |
57 | {Array.from({ length: 4 }).map((_, idx) => ( 58 | 59 | ))} 60 |
61 |
62 | ))} 63 |
64 | 65 | {/* Sources */} 66 |
67 | {/* Sources title */} 68 |
69 | {Array.from({ length: 3 }).map((_, idx) => ( 70 |
71 | {/* Source title */} 72 | {/* Source content */} 73 |
74 | ))} 75 |
76 |
77 |
78 | ); 79 | 80 | export const XKCDSkeleton: FC = () => ( 81 |
82 |
83 | 84 |
85 | {/* For the speed/function display */} 86 |
87 |
88 |
89 | {/* Prev button */} 90 | {/* Next button */} 91 |
92 |
93 | ); 94 | 95 | export const ChatSkeleton: FC = () => ( 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 | ); 105 | 106 | export const GenericSkeleton: FC = () => ( 107 |
108 | {/* Loading blocks */} 109 |
110 | 111 |
112 |
113 | ); 114 | 115 | export const MapSkeleton: FC = () => ( 116 |
117 |
118 |
119 |
120 |
121 | ); 122 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | 68 | 69 | Close 70 | 71 | {children} 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /src/components/ui/sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Slot } from "@radix-ui/react-slot"; 5 | import { VariantProps, cva } from "class-variance-authority"; 6 | import { PanelLeft } from "lucide-react"; 7 | 8 | import { useIsMobile } from "@/hooks/use-mobile"; 9 | import { cn } from "@/lib/utils"; 10 | import { Button } from "@/components/ui/button"; 11 | import { Input } from "@/components/ui/input"; 12 | import { Separator } from "@/components/ui/separator"; 13 | import { Sheet, SheetContent } from "@/components/ui/sheet"; 14 | import { Skeleton } from "@/components/ui/skeleton"; 15 | import { 16 | Tooltip, 17 | TooltipContent, 18 | TooltipProvider, 19 | TooltipTrigger, 20 | } from "@/components/ui/tooltip"; 21 | 22 | const SIDEBAR_COOKIE_NAME = "sidebar:state"; 23 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; 24 | const SIDEBAR_WIDTH = "16rem"; 25 | const SIDEBAR_WIDTH_MOBILE = "18rem"; 26 | const SIDEBAR_WIDTH_ICON = "3rem"; 27 | const SIDEBAR_KEYBOARD_SHORTCUT = "b"; 28 | 29 | type SidebarContext = { 30 | state: "expanded" | "collapsed"; 31 | open: boolean; 32 | setOpen: (open: boolean) => void; 33 | openMobile: boolean; 34 | setOpenMobile: (open: boolean) => void; 35 | isMobile: boolean; 36 | toggleSidebar: () => void; 37 | }; 38 | 39 | const SidebarContext = React.createContext(null); 40 | 41 | function useSidebar() { 42 | const context = React.useContext(SidebarContext); 43 | if (!context) { 44 | throw new Error("useSidebar must be used within a SidebarProvider."); 45 | } 46 | 47 | return context; 48 | } 49 | 50 | export interface SidebarProviderProps { 51 | children: React.ReactNode; 52 | style?: React.CSSProperties; 53 | } 54 | 55 | const SidebarProvider = React.forwardRef< 56 | HTMLDivElement, 57 | React.ComponentProps<"div"> & { 58 | defaultOpen?: boolean; 59 | open?: boolean; 60 | onOpenChange?: (open: boolean) => void; 61 | } 62 | >( 63 | ( 64 | { 65 | defaultOpen = true, 66 | open: openProp, 67 | onOpenChange: setOpenProp, 68 | className, 69 | style, 70 | children, 71 | ...props 72 | }, 73 | ref 74 | ) => { 75 | const isMobile = useIsMobile(); 76 | const [openMobile, setOpenMobile] = React.useState(false); 77 | 78 | // This is the internal state of the sidebar. 79 | // We use openProp and setOpenProp for control from outside the component. 80 | const [_open, _setOpen] = React.useState(defaultOpen); 81 | const open = openProp ?? _open; 82 | const setOpen = React.useCallback( 83 | (value: boolean | ((value: boolean) => boolean)) => { 84 | const openState = typeof value === "function" ? value(open) : value; 85 | if (setOpenProp) { 86 | setOpenProp(openState); 87 | } else { 88 | _setOpen(openState); 89 | } 90 | 91 | // This sets the cookie to keep the sidebar state. 92 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; 93 | }, 94 | [setOpenProp, open] 95 | ); 96 | 97 | // Helper to toggle the sidebar. 98 | const toggleSidebar = React.useCallback(() => { 99 | return isMobile 100 | ? setOpenMobile((open) => !open) 101 | : setOpen((open) => !open); 102 | }, [isMobile, setOpen, setOpenMobile]); 103 | 104 | // Adds a keyboard shortcut to toggle the sidebar. 105 | React.useEffect(() => { 106 | const handleKeyDown = (event: KeyboardEvent) => { 107 | if ( 108 | event.key === SIDEBAR_KEYBOARD_SHORTCUT && 109 | (event.metaKey || event.ctrlKey) 110 | ) { 111 | event.preventDefault(); 112 | toggleSidebar(); 113 | } 114 | }; 115 | 116 | window.addEventListener("keydown", handleKeyDown); 117 | return () => window.removeEventListener("keydown", handleKeyDown); 118 | }, [toggleSidebar]); 119 | 120 | // We add a state so that we can do data-state="expanded" or "collapsed". 121 | // This makes it easier to style the sidebar with Tailwind classes. 122 | const state = open ? "expanded" : "collapsed"; 123 | 124 | const contextValue = React.useMemo( 125 | () => ({ 126 | state, 127 | open, 128 | setOpen, 129 | isMobile, 130 | openMobile, 131 | setOpenMobile, 132 | toggleSidebar, 133 | }), 134 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] 135 | ); 136 | 137 | return ( 138 | 139 | 140 |
155 | {children} 156 |
157 |
158 |
159 | ); 160 | } 161 | ); 162 | SidebarProvider.displayName = "SidebarProvider"; 163 | 164 | const Sidebar = React.forwardRef< 165 | HTMLDivElement, 166 | React.ComponentProps<"div"> & { 167 | side?: "left" | "right"; 168 | variant?: "sidebar" | "floating" | "inset"; 169 | collapsible?: "offcanvas" | "icon" | "none"; 170 | } 171 | >( 172 | ( 173 | { 174 | side = "left", 175 | variant = "sidebar", 176 | collapsible = "offcanvas", 177 | className, 178 | children, 179 | ...props 180 | }, 181 | ref 182 | ) => { 183 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); 184 | 185 | if (collapsible === "none") { 186 | return ( 187 |
195 | {children} 196 |
197 | ); 198 | } 199 | 200 | if (isMobile) { 201 | return ( 202 | 203 | 214 |
{children}
215 |
216 |
217 | ); 218 | } 219 | 220 | return ( 221 |
229 | {/* This is what handles the sidebar gap on desktop */} 230 |
240 | 261 |
262 | ); 263 | } 264 | ); 265 | Sidebar.displayName = "Sidebar"; 266 | 267 | const SidebarTrigger = React.forwardRef< 268 | React.ElementRef, 269 | React.ComponentProps 270 | >(({ className, onClick, ...props }, ref) => { 271 | const { toggleSidebar } = useSidebar(); 272 | 273 | return ( 274 | 289 | ); 290 | }); 291 | SidebarTrigger.displayName = "SidebarTrigger"; 292 | 293 | const SidebarRail = React.forwardRef< 294 | HTMLButtonElement, 295 | React.ComponentProps<"button"> 296 | >(({ className, ...props }, ref) => { 297 | const { toggleSidebar } = useSidebar(); 298 | 299 | return ( 300 |