chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,53 @@
# Copilot Anthropic Pinecone Demo Example
## 🛠️ Getting Started
### Prerequisites
- Node.js 18+
- yarn
- Anthropic API key
- Pinecone account and API key
### Installation Steps
1. Clone the repository:
```bash
git clone https://github.com/CopilotKit/CopilotKit.git
```
- /examples/copilot-anthropic-pinecone
2. Install dependencies:
```bash
pnpm i
```
3. Set up Pinecone and Anthropic Acc:
- Create a Pinecone and Anthropic accounts
- Note your API keys
4. Configure environment variables:
Create a `.env` file with:
```bash
ANTHROPIC_API_KEY="your_anthropic_key"
PINECONE_API_KEY="your_pinecone_key"
NEXT_PUBLIC_API_BASE_URL="app_base_URK"
```
5. Start the development server:
```bash
pnpm dev
```
6. Open [http://localhost:3000](http://localhost:3000) in your browser
## 📚 Additional Resources
For a complete guide on building this project, check out our detailed tutorial:
[Build a RAG Copilot on Your Own Knowledge Base with CopilotKit, Pinecone & Anthropic](https://dev.to/copilotkit/build-a-rag-copilot-on-your-own-knowledge-base-with-copilotkit-pinecone-anthropic-21m9)
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
@@ -0,0 +1,31 @@
{
"name": "product-knowledge-base",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@copilotkit/react-core": "^1.8.11",
"@copilotkit/react-ui": "^1.8.11",
"@copilotkit/runtime": "^1.8.11",
"@mantine/core": "^7.14.3",
"@mantine/hooks": "^7.14.3",
"@pinecone-database/pinecone": "^4.0.0",
"axios": "^1.12.0",
"lucide-react": "^0.466.0",
"next": "14.2.35",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
import {
CopilotRuntime,
AnthropicAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { Pinecone } from "@pinecone-database/pinecone";
import { posts } from "@/app/lib/data/data";
import { NextRequest } from "next/server";
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const PINECONE_API_KEY = process.env.PINECONE_API_KEY;
if (!ANTHROPIC_API_KEY || !PINECONE_API_KEY) {
console.error("Missing required API keys.");
process.exit(1);
}
const serviceAdapter = new AnthropicAdapter({
model: "claude-3-5-sonnet-20240620",
});
const pinecone = new Pinecone({ apiKey: PINECONE_API_KEY });
const model = "multilingual-e5-large";
const indexName = "knowledge-base-data";
// Function to create the Pinecone index
const initializePinecone = async () => {
const maxRetries = 3;
const retryDelay = 2000;
for (let i = 0; i < maxRetries; i++) {
try {
const indexList = await pinecone.listIndexes();
if (!indexList.indexes?.some((index) => index.name === indexName)) {
await pinecone.createIndex({
name: indexName,
dimension: 1024,
metric: "cosine",
spec: {
serverless: {
cloud: "aws",
region: "us-east-1",
},
},
});
await new Promise((resolve) => setTimeout(resolve, 5000));
}
return pinecone.index(indexName);
} catch (error) {
if (i === maxRetries - 1) throw error;
console.warn(
`Retrying Pinecone initialization... (${i + 1}/${maxRetries})`,
);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
return null;
};
// Initialize Pinecone and prepare the index
(async () => {
try {
const index = await initializePinecone();
if (index) {
const embeddings = await pinecone.inference.embed(
model,
posts.map((d) => d.content),
{ inputType: "passage", truncate: "END" },
);
const records = posts.map((d, i) => ({
id: d.id.toString(),
values: embeddings[i]?.values ?? [],
metadata: { text: d.content },
}));
await index.namespace("knowledge-base-data-namespace").upsert(
records.map((record) => ({
...record,
values: record.values || [],
})),
);
}
} catch (error) {
console.error("Error initializing Pinecone:", error);
process.exit(1);
}
})();
const runtime = new CopilotRuntime({
actions: () => [
{
name: "FetchKnowledgebaseArticles",
description:
"Fetch relevant knowledge base articles based on a user query",
parameters: [
{
name: "query",
type: "string",
description:
"The User query for the knowledge base index search to perform",
required: true,
},
],
handler: async ({ query }: { query: string }) => {
console.log(
`[Pinecone] Executing FetchKnowledgebaseArticles with query: "${query}"`,
);
try {
const queryEmbedding = await pinecone.inference.embed(
model,
[query],
{ inputType: "query" },
);
console.log(`[Pinecone] Successfully generated embedding for query`);
const queryResponse = await pinecone
.index(indexName)
.namespace("knowledge-base-data-namespace")
.query({
topK: 5,
vector: queryEmbedding[0]?.values || [],
includeValues: false,
includeMetadata: true,
});
console.log(
`[Pinecone] Query response: Found ${
queryResponse?.matches?.length || 0
} matches`,
);
// Format the results in a more structured way for the AI
const formattedResults =
queryResponse?.matches?.map((match, index) => {
return {
id: match.id,
relevance_score: match.score,
content: match.metadata?.text || "No content available",
};
}) || [];
if (queryResponse?.matches?.length > 0) {
console.log(
`[Pinecone] First match score: ${queryResponse.matches[0].score}`,
);
console.log(
`[Pinecone] First match metadata:`,
queryResponse.matches[0].metadata,
);
}
// Provide a more comprehensive response structure
return {
articles: formattedResults,
query: query,
total_results: formattedResults.length,
content_summary:
formattedResults.length > 0
? "Here are the key CopilotKit features found in our knowledge base:\n" +
formattedResults
.map(
(result, i) =>
`${i + 1}. ${String(result.content).trim()}`,
)
.join("\n\n")
: "No relevant articles found about CopilotKit features. The knowledge base may not contain comprehensive documentation.",
};
} catch (error) {
console.error("Error fetching knowledge base articles:", error);
throw new Error("Failed to fetch knowledge base articles.", { cause: error });
}
},
},
],
});
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import { posts } from "@/app/lib/data/data";
export async function GET() {
return NextResponse.json(posts);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,42 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
@@ -0,0 +1,24 @@
import "./globals.css";
import { MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import "@copilotkit/react-ui/styles.css";
import { CopilotKit } from "@copilotkit/react-core";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<CopilotKit runtimeUrl="/api/copilotkit">
<MantineProvider>{children}</MantineProvider>
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,153 @@
export interface Post {
id: number;
title: string;
summary: string;
content: string;
category: string;
createdAt: string;
}
export const posts: Post[] = [
{
id: 1,
title: "Getting Started with CopilotKit",
summary:
"Learn the basics of CopilotKit and how to set up your first project.",
content: `
CopilotKit is an open-source framework for building AI copilots and assistants for your applications. Here's how to get started:
1. Installation: Install CopilotKit using npm or yarn with 'npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime'.
2. Basic Setup: Import and set up the CopilotKit provider in your application's root component.
3. Configuration: Configure your OpenAI or Anthropic API keys in your environment variables to connect your AI service.
4. Add Components: Use CopilotKit's UI components like CopilotSidebar or CopilotChat to add AI assistance to your app.
5. Testing: Test your implementation locally to ensure proper functionality before deploying.
For more detailed information, visit the official documentation at https://docs.copilotkit.ai
`,
category: "Basics",
createdAt: "2024-11-21",
},
{
id: 2,
title: "Key Features of CopilotKit",
summary: "Comprehensive overview of all CopilotKit's powerful features.",
content: `
CopilotKit offers a comprehensive set of features for building AI-powered assistants:
1. In-App AI Chatbot: Easily add a sophisticated AI chatbot to your app with the CopilotChat and CopilotSidebar components.
2. Copilot Readable State: Allow your AI assistant to read and understand your application's state for context-aware interactions using the useCopilotContext hook.
3. Copilot Actions: Enable your AI to perform actions within your application via the useCopilotAction hook, giving it ability to make API calls, update state, and interact with your app.
4. Generative UI: Create dynamic UI elements through the AI interface, letting your assistant generate and display custom components.
5. Copilot Textarea: Implement AI-powered autocompletion in any textarea with the CopilotTextarea component.
6. File Attachments: Allow users to upload and reference files in their conversations with the AI.
7. Multi-modal Support: Process and generate both text and images in your AI interactions.
8. Knowledge Base Integration: Connect your AI to your application's knowledge base for more accurate responses.
9. Memory & Context Management: Maintain conversation history and context across user sessions.
10. Custom Styling: Fully customize the appearance of all CopilotKit components to match your application's design.
`,
category: "Features",
createdAt: "2024-11-21",
},
{
id: 3,
title: "Implementing CopilotKit Components",
summary:
"Learn how to use the different components provided by CopilotKit.",
content: `
CopilotKit provides several key components for your applications:
1. CopilotSidebar: A collapsible sidebar component that provides an AI chat interface. Implement it with:
<CopilotSidebar instructions="Your instructions here" />
2. CopilotChat: A customizable chat interface for AI interactions. Use it with:
<CopilotChat instructions="Your instructions here" />
3. CopilotTextarea: An enhanced textarea with AI-powered autocompletion:
<CopilotTextarea placeholder="Type here..." />
4. CopilotContext: Wrap your application with this provider to enable global access to CopilotKit features:
<CopilotContext>
<YourApp />
</CopilotContext>
5. CopilotPopover: A floating AI assistant that can be triggered from any part of your application.
Each component can be extensively customized through props to match your application's needs and design language.
`,
category: "Implementation",
createdAt: "2024-11-22",
},
{
id: 4,
title: "Troubleshooting Common Issues",
summary:
"Follow these steps to troubleshoot common issues when using CopilotKit.",
content: `
When troubleshooting CopilotKit implementations, follow these steps:
1. Check API Configuration: Ensure your OpenAI or Anthropic API keys are correctly set in your environment variables.
2. Verify SDK Integration: Confirm that your React app properly imports and uses CopilotKit components and hooks.
3. Inspect Console Logs: Check your browser or server console for error messages that might indicate configuration issues.
4. Component Setup: Verify that CopilotKit components have proper instructions and configuration props.
5. Actions and Context: Ensure that any CopilotActions or context providers are correctly implemented with proper typings.
6. Version Compatibility: Make sure you're using compatible versions of all CopilotKit packages.
7. Server-Side Setup: If using server components, verify your API routes are correctly configured.
8. Streaming Responses: For issues with streaming, check that your API endpoints support streaming responses.
For persistent issues, check the GitHub repository issues or join the Discord community for support.
`,
category: "Support",
createdAt: "2024-11-23",
},
{
id: 5,
title: "Advanced CopilotKit Usage",
summary:
"Take your CopilotKit implementation to the next level with advanced techniques.",
content: `
Advanced techniques for leveraging CopilotKit in your applications:
1. Custom LLM Integration: Connect CopilotKit to any LLM provider beyond OpenAI and Anthropic using custom adapters.
2. Multi-modal Interactions: Process and generate both text and images in your AI interactions for richer experiences.
3. Vector Database Integration: Connect to vector databases like Pinecone or MongoDB Atlas to give your AI access to your knowledge base.
4. Function Calling: Define complex actions that your AI can perform using the useCopilotAction hook with structured parameters.
5. State Management: Use useCopilotContext to provide your AI with access to application state for more context-aware interactions.
6. Streaming Responses: Implement streaming API responses for more responsive AI interactions.
7. Backend Integration: Create custom backend handlers for your AI actions to interact with your databases and services.
8. Error Handling: Implement robust error handling for AI actions to create resilient user experiences.
9. Custom UI Elements: Build custom UI components that can be rendered by your AI assistant using the generativeUI feature.
10. Authentication: Implement secure authentication for your AI services using JWT or OAuth.
`,
category: "Advanced",
createdAt: "2024-11-24",
},
];
@@ -0,0 +1,8 @@
export interface Post {
id: number;
title: string;
summary: string;
content: string;
category: string;
createdAt: string;
}
@@ -0,0 +1,9 @@
import KnowledgeBase from "@/app/ui/components/KnowledgeBase";
export default function Home() {
return (
<div>
<KnowledgeBase />
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import { useState, useEffect } from "react";
import {
Container,
Title,
Grid,
Card,
Text,
Badge,
Group,
Stack,
Box,
Modal,
List,
} from "@mantine/core";
import { BookOpen } from "lucide-react";
import { Post } from "@/app/lib/types/post";
import { fetchPosts } from "@/app/ui/service";
import { CopilotSidebar } from "@copilotkit/react-ui";
import { useCopilotAction } from "@copilotkit/react-core";
export default function KnowledgeBase() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
useEffect(() => {
const loadPosts = async () => {
try {
const data = await fetchPosts();
setPosts(data);
} catch (error) {
console.error("Error loading posts:", error);
} finally {
setLoading(false);
}
};
loadPosts();
}, []);
useCopilotAction({
name: "FetchKnowledgebaseArticles",
description: "Fetch relevant knowledge base articles based on a user query",
parameters: [
{
name: "query",
type: "string",
description: "User query for the knowledge base",
required: true,
},
],
render: "Getting relevant answers to your query...",
});
const handlePostClick = (post: Post) => {
setSelectedPost(post);
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<Container size="md" py="xl" ml="xl">
<Stack gap="xl">
<Group justify="center" align="center">
<BookOpen size={32} />
<Title order={1}>CopilotKit Product Knowledge Base</Title>
</Group>
<Grid>
{posts.map((post) => (
<Grid.Col key={post.id} span={{ base: 12, sm: 6, md: 4 }}>
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
onClick={() => handlePostClick(post)}
style={{ cursor: "pointer" }}
>
<Stack gap="md">
<Title order={3}>{post.title}</Title>
<Badge color="blue" variant="light">
{post.category}
</Badge>
<Text size="sm" c="dimmed">
{post.summary}
</Text>
<Text size="xs" c="dimmed">
Posted on: {new Date(post.createdAt).toLocaleDateString()}
</Text>
</Stack>
</Card>
</Grid.Col>
))}
</Grid>
{selectedPost && (
<Modal
opened={!!selectedPost}
onClose={() => setSelectedPost(null)}
title={selectedPost.title}
centered
size="xl"
>
<Stack gap="md">
<List>
{selectedPost.content
.split("\n")
.filter((item) => item.trim() !== "")
.map((item, index) => (
<List.Item key={index}>{item}</List.Item>
))}
</List>
</Stack>
</Modal>
)}
<Group justify="center" style={{ width: "100%" }}>
<Box style={{ flex: 1, maxWidth: "350px" }}>
<CopilotSidebar
instructions={`You are a helpful assistant for CopilotKit. When users ask about CopilotKit features or usage:
1. ALWAYS use the FetchKnowledgebaseArticles action immediately to retrieve information
2. Read the content_summary in the response - this contains formatted information about CopilotKit
3. Base your answers directly on the retrieved information
4. Present the information clearly with specific examples from the knowledge base
5. Always mention specific CopilotKit features and how to use them based on the retrieved data
Never respond with "I couldn't retrieve specific details" as the knowledge base contains comprehensive information about CopilotKit features.`}
labels={{
initial:
"Welcome! I'm your CopilotKit assistant. Ask me anything about CopilotKit features or how to use it!",
}}
defaultOpen={true}
clickOutsideToClose={false}
/>
</Box>
</Group>
</Stack>
</Container>
);
}
@@ -0,0 +1,9 @@
import axios from "axios";
import { Post } from "@/app/lib/types/post";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
export const fetchPosts = async (): Promise<Post[]> => {
const response = await axios.get(`${API_BASE_URL}/api/posts`);
return response.data;
};
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.mjs"
],
"exclude": ["node_modules"]
}
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,56 @@
# Copilot Fully Custom
![leafy-green](https://github.com/user-attachments/assets/63f347ef-fefe-49c5-9162-6c88161fd9e0)
CopilotKit fully customized using components from MongoDB's Leafy Green Design System.
https://github.com/user-attachments/assets/92356944-090a-440c-bf8f-749bec5475e2
## Tech Stack
- [CopilotKit](https://copilotkit.ai)
- Next.js
- TypeScript
- [MongoDB Leafy Green Design System](https://www.mongodb.design/)
- TailwindCSS
## Getting Started
1. Install dependencies:
```bash
npm install
```
2. Setup your runtime:
CopilotKit requires `runtime`, a production-ready proxy for your LLM requests. You can either use Copilot Cloud or self-host it.
First, make a `.env` file in the root of the project.
```bash
touch .env
```
Now, you can either provide your [Copilot Cloud public API key](https://dashboard.operations.copilotkit.ai) or [OpenAI API key](https://platform.openai.com/api-keys).
> **Note:** Copilot Cloud will provide you some free OpenAI API credits to get you started!
```bash
OPENAI_API_KEY=sk... #if you want to use OpenAI
COPILOT_CLOUD_PUBLIC_API_KEY=ck... #if you want to use Copilot Cloud
```
2. Run the development server:
```bash
npm run dev
```
3. Open [http://localhost:3000](http://localhost:3000) to see the result.
## Customization
This project demonstrates how to fully customize CopilotKit using components from MongoDB's Leafy Green Design System.
To see this in action, take a look at the [components](./components) folder. In particular, the [Chat.tsx](./components/Chat.tsx) file demonstrates how to customize the chat interface using Leafy Green components.
@@ -0,0 +1,20 @@
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { NextRequest } from "next/server";
const serviceAdapter = new OpenAIAdapter();
const runtime = new CopilotRuntime();
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
import { CopilotKit } from "@copilotkit/react-core";
import "./globals.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<CopilotKit
publicApiKey={process.env.COPILOT_CLOUD_PUBLIC_API_KEY}
runtimeUrl={
process.env.COPILOT_CLOUD_PUBLIC_API_KEY
? undefined // Copilot Cloud will provide the runtime URL
: "/api/copilotkit" // Local runtime
}
showDevConsole={false}
>
{children}
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,12 @@
"use client";
import { Chat } from "@/components/Chat";
import "@copilotkit/react-ui/styles.css";
export default function Home() {
return (
<main className="flex justify-center items-center h-screen bg-green-50">
<Chat className="h-[900px] w-[700px] rounded-xl" />
</main>
);
}
@@ -0,0 +1,34 @@
import { CopilotChat } from "@copilotkit/react-ui";
import { Header } from "@/components/chat/Header";
import { CustomUserMessage } from "@/components/chat/UserMessage";
import { CustomAssistantMessage } from "@/components/chat/AssistantMessage";
import { CustomResponseButton } from "./chat/ResponseButton";
import ContactInfo from "./generative-ui/ContactInfo";
import { useCopilotAction } from "@copilotkit/react-core";
export function Chat({ className }: { className?: string }) {
useCopilotAction({
name: "contactInfo",
description: "Collect contact information from the user",
renderAndWaitForResponse: ({ respond, status }) => {
if (status === "complete") return <></>;
return <ContactInfo onSubmit={(form) => respond?.(form)} />;
},
});
return (
<div>
<Header />
<CopilotChat
className={`rounded-xl border border-t-0 rounded-t-none shadow-xl ${className}`}
UserMessage={CustomUserMessage}
AssistantMessage={CustomAssistantMessage}
ResponseButton={CustomResponseButton}
labels={{
initial:
"Hi! I'm a fully customized CopilotKit assistant. How can I help you today? \n\nTry asking me to collect your contact information.",
}}
/>
</div>
);
}
@@ -0,0 +1,58 @@
import Card from "@leafygreen-ui/card";
import { Avatar, Format, AvatarSize } from "@leafygreen-ui/avatar";
import { Spinner } from "@leafygreen-ui/loading-indicator";
import Button from "@leafygreen-ui/button";
import Icon from "@leafygreen-ui/icon";
import "@copilotkit/react-ui/styles.css";
import { AssistantMessageProps, Markdown } from "@copilotkit/react-ui";
import { useCopilotChat } from "@copilotkit/react-core";
export const CustomAssistantMessage = (props: AssistantMessageProps) => {
const { message, isLoading, isGenerating, subComponent, rawData } = props;
const id = rawData?.id;
return (
<div className="py-2">
<div className="flex items-end gap-2">
{!subComponent && (
<Avatar format={Format.MongoDB} size={AvatarSize.XLarge} />
)}
{subComponent ? (
subComponent
) : (
<Card className="flex w-full justify-start flex-col">
{message && <Markdown content={message.content || ""} />}
{isLoading && (
<div className="flex justify-start">
<Spinner />
</div>
)}
{!isGenerating && !isLoading && <ResponseButtons id={id} />}
</Card>
)}
</div>
</div>
);
};
const ResponseButtons = ({ id }: { id: string }) => {
const { reloadMessages } = useCopilotChat();
return (
<div className="flex gap-2 items-center mt-6">
<p className="text-gray-500">How was this response?</p>
<Button size={"xsmall"} onClick={() => alert("Thumbs up sent")}>
<Icon glyph="ThumbsUp" />
</Button>
<Button size={"xsmall"} onClick={() => alert("Thumbs down sent")}>
<Icon glyph="ThumbsDown" />
</Button>
<div className="flex gap-2 items-center">
|
<Button size={"xsmall"} onClick={() => reloadMessages(id)}>
<Icon glyph="Refresh" />
</Button>
</div>
</div>
);
};
@@ -0,0 +1,12 @@
import { Avatar, Format, AvatarSize } from "@leafygreen-ui/avatar";
import Badge from "@leafygreen-ui/badge";
export function Header() {
return (
<div className="flex items-center justify-center gap-2 border py-4 rounded-t-xl bg-white">
<Avatar format={Format.MongoDB} size={AvatarSize.Default} />
<span className="text-lg font-bold">AI Assistant</span>
<Badge variant="blue">Beta</Badge>
</div>
);
}
@@ -0,0 +1,6 @@
import { ResponseButtonProps } from "@copilotkit/react-ui";
// empty response button since we don't need it
export function CustomResponseButton(_: ResponseButtonProps) {
return <></>;
}
@@ -0,0 +1,23 @@
import { Markdown, UserMessageProps } from "@copilotkit/react-ui";
import { Avatar, Format, AvatarSize } from "@leafygreen-ui/avatar";
import Card from "@leafygreen-ui/card";
export const CustomUserMessage = (props: UserMessageProps) => {
const wrapperStyles = "flex items-end gap-2 justify-end mt-4 w-full";
const messageStyles = "bg-emerald-500 flex items-end justify-end text-white";
const avatarStyles = "text-sm bg-emerald-500";
return (
<div className={wrapperStyles}>
<Card className={messageStyles}>
<Markdown content={props.message?.content || ""} />
</Card>
<Avatar
format={Format.Icon}
glyph="Person"
size={AvatarSize.XLarge}
className={avatarStyles}
/>
</div>
);
};
@@ -0,0 +1,79 @@
import { useState } from "react";
import TextInput from "@leafygreen-ui/text-input";
import Button from "@leafygreen-ui/button";
import TextArea from "@leafygreen-ui/text-area";
import Card from "@leafygreen-ui/card";
import Icon from "@leafygreen-ui/icon";
interface ContactInfoProps {
onSubmit: (form: any) => void;
}
export default function ContactInfo({ onSubmit }: ContactInfoProps) {
// Add state for form fields
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
funFact: "",
});
const handleChange =
(field: string) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
...formData,
[field]: e.target.value,
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit} className="w-full">
<Card className="flex flex-col gap-2 border-emerald-500 shadow-lg">
<TextInput
label="First Name"
onChange={handleChange("firstName")}
value={formData.firstName}
placeholder="John"
/>
<TextInput
label="Last Name"
onChange={handleChange("lastName")}
value={formData.lastName}
placeholder="Doe"
/>
<TextInput
label="Email"
onChange={handleChange("email")}
value={formData.email}
placeholder="john.doe@example.com"
/>
<TextInput
label="Phone"
onChange={handleChange("phone")}
value={formData.phone}
placeholder="(123) 456-7890"
/>
<TextArea
label="Fun Fact"
onChange={handleChange("funFact")}
value={formData.funFact}
placeholder="I love to code!"
/>
<Button
type="submit"
className="mt-4"
leftGlyph={<Icon glyph="Checkmark" />}
>
Submit
</Button>
</Card>
</form>
);
}
@@ -0,0 +1,37 @@
{
"name": "leafy-green",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@copilotkit/react-core": "1.9.3",
"@copilotkit/react-ui": "1.9.3",
"@copilotkit/runtime": "1.10.0",
"@copilotkit/shared": "1.9.3",
"@leafygreen-ui/avatar": "^2.0.2",
"@leafygreen-ui/badge": "^9.0.2",
"@leafygreen-ui/button": "^23.0.0",
"@leafygreen-ui/card": "^12.0.2",
"@leafygreen-ui/checkbox": "^14.1.2",
"@leafygreen-ui/icon": "^13.1.2",
"@leafygreen-ui/loading-indicator": "^3.0.4",
"@leafygreen-ui/text-area": "^10.0.4",
"@leafygreen-ui/text-input": "^14.0.4",
"next": "^14.2.35",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
@@ -0,0 +1,18 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
} satisfies Config;
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Wachira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,56 @@
# CopilotKit OpenAI MongoDB Atlas Vector Search Demo Example
## 🛠️ Getting Started
### Prerequisites
- Node.js 18+
- Yarn package manager
- OpenAI API key
- MongoDB Atlas account
### Installation Steps
1. Clone the repository:
```bash
git clone https://github.com/CopilotKit/CopilotKit.git
```
- /examples/copilot-openai-mongodb-atlas-vector-search
2. Install dependencies:
```bash
pnpm install
```
3. Set up MongoDB Atlas:
- Create a MongoDB Atlas account at https://www.mongodb.com/cloud/atlas
- Set up a new cluster
- Enable Atlas Vector Search
- Get your connection URI
4. Configure environment variables:
Create a `.env` file with:
```bash
OPENAI_API_KEY="your_openai_key"
MONGODB_ATLAS_CONNECTION_URI="your_mongodb_connection_uri"
NEXT_PUBLIC_API_BASE_URL="http://localhost:3000"
```
5. Start the development server:
```bash
pnpm run dev
```
6. Open [http://localhost:3000](http://localhost:3000) in your browser
## 📚 Additional Resources
For a complete guide on building this project, check out our detailed tutorial:
[ OpenAI & MongoDB Atlas Vector Search Article URL....](url)
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
{
"name": "product-knowledge-base",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.36.3",
"@copilotkit/react-core": "^1.5.20",
"@copilotkit/react-ui": "^1.5.20",
"@copilotkit/runtime": "^1.5.20",
"@mantine/core": "^7.17.0",
"@mantine/hooks": "^7.17.0",
"@pinecone-database/pinecone": "^5.0.0",
"axios": "^1.12.0",
"lucide-react": "^0.475.0",
"mongodb": "^6.13.0",
"next": "15.5.15",
"product-knowledge-base": "file:",
"react": "^19",
"react-dom": "^19"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,171 @@
import OpenAI from "openai";
import { MongoClient } from "mongodb";
import { posts } from "@/app/lib/data/data";
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { NextRequest } from "next/server";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const MONGODB_URI = process.env.MONGODB_CONNECTION_URI;
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
const serviceAdapter = new OpenAIAdapter({ openai });
if (!OPENAI_API_KEY || !MONGODB_URI) {
console.error("Missing required API keys or MongoDB URI.");
process.exit(1);
}
const client = new MongoClient(MONGODB_URI);
const database = client.db("knowledge_base");
const collection = database.collection("articles");
// Function to create vector index
const createVectorIndex = async () => {
try {
const index = {
name: "vector_index",
type: "vectorSearch",
definition: {
fields: [
{
type: "vector",
numDimensions: 1536,
path: "embedding",
similarity: "cosine",
},
],
},
};
const result = await collection.createSearchIndex(index);
console.log(`Vector index created: ${result}`);
interface SearchIndex {
name: string;
queryable: boolean;
}
let isQueryable = false;
while (!isQueryable) {
const cursor = collection.listSearchIndexes();
for await (const index of cursor as unknown as SearchIndex[]) {
if (index.name === result) {
if (index.queryable) {
console.log(`${result} is ready for querying`);
isQueryable = true;
} else {
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
}
} catch (error) {
console.error("Error creating vector index:", error);
throw error;
}
};
// Function to create and store embeddings for the data
const initializeData = async () => {
try {
await client.connect();
const embeddings = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: posts.map((d) => d.content),
});
// Store documents with embeddings
for (let i = 0; i < posts.length; i++) {
await collection.updateOne(
{ id: posts[i].id.toString() },
{
$set: {
content: posts[i].content,
embedding: embeddings.data[i].embedding,
},
},
{ upsert: true },
);
}
await createVectorIndex();
console.log("success....");
} catch (error) {
console.error("error...", error);
throw error;
}
};
initializeData().catch(console.error);
//copilotkit runtime with the search functionality -- each query is converted to an embedding and then the search is performed on the vector index
const runtime = new CopilotRuntime({
actions: () => [
{
name: "FetchKnowledgebaseArticles",
description:
"Fetch relevant knowledge base articles based on a user query",
parameters: [
{
name: "query",
type: "string",
description:
"The User query for the knowledge base index search to perform",
required: true,
},
],
handler: async ({ query }: { query: string }) => {
try {
const queryEmbedding = await openai.embeddings.create({
model: "text-embedding-ada-002",
input: query,
});
const database = client.db("knowledge_base");
const collection = database.collection("articles");
const articles = await collection
.aggregate([
{
$vectorSearch: {
index: "vector_index",
queryVector: queryEmbedding.data[0].embedding,
path: "embedding",
numCandidates: 100,
limit: 3,
},
},
{
$project: {
_id: 0,
content: 1,
score: { $meta: "vectorSearchScore" },
},
},
])
.toArray();
return { articles };
} catch (error) {
console.error("Error fetching knowledge base articles:", error);
throw new Error("Failed to fetch knowledge base articles.", { cause: error });
}
},
},
],
});
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
@@ -0,0 +1,6 @@
import { NextResponse } from "next/server";
import { posts } from "@/app/lib/data/data";
export async function GET() {
return NextResponse.json(posts);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,42 @@
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
@@ -0,0 +1,24 @@
import "./globals.css";
import { MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import "@copilotkit/react-ui/styles.css";
import { CopilotKit } from "@copilotkit/react-core";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<CopilotKit runtimeUrl="/api/copilotkit">
<MantineProvider>{children}</MantineProvider>
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,54 @@
export interface Post {
id: number;
title: string;
summary: string;
content: string;
category: string;
createdAt: string;
}
export const posts: Post[] = [
{
id: 1,
title: "Getting Started with CopilotKit",
summary:
"Learn the basics of CopilotKit and how to set up your first project.",
content: `
Explore the documentation to understand the SDK and UI components.
Integrate your React app with CopilotKit using the provided SDK.
Test your setup by running the app in your local environment.
Review CopilotKits documentation for additional setup and troubleshooting.
`,
category: "Basics",
createdAt: "2024-11-21",
},
{
id: 2,
title: "Advanced Features of CopilotKit",
summary: " CopilotKit advanced features.",
content: `
In-App AI Chatbot: Easily add an AI chatbot to your app with plug-and-play components.
Copilot Readable State: Enable your Copilot to read and understand the application's state for intelligent interactions.
Copilot Actions: Let your Copilot perform actions in the app based on the state and user input.
Generative UI: Render any component dynamically through the AI chat interface.
Copilot Textarea: Add AI-powered autocompletion to any textarea, enhancing user experience.
AI Autosuggestions: Provide smart autosuggestions in the AI chat interface for faster interactions.
Copilot Tasks: Allow your Copilot to take proactive actions based on the application state.
`,
category: "Advanced",
createdAt: "2024-11-21",
},
{
id: 3,
title: "Troubleshooting common issues",
summary:
"Follow these steps to troubleshoot common issues when using CopilotKit.",
content: `
Step 1: Check SDK Integration: Ensure that your React app is properly integrated with the CopilotKit SDK.
Step 2: Inspect Console Logs: Look for error messages or warnings in the browser console or server logs for any issues.
Step 3: Test Components Independently: Isolate components and test their functionality separately and debug accordingly.
`,
category: "Support",
createdAt: "2024-11-21",
},
];
@@ -0,0 +1,8 @@
export interface Post {
id: number;
title: string;
summary: string;
content: string;
category: string;
createdAt: string;
}
@@ -0,0 +1,9 @@
import KnowledgeBase from "@/app/ui/components/KnowledgeBase";
export default function Home() {
return (
<div>
<KnowledgeBase />
</div>
);
}
@@ -0,0 +1,139 @@
"use client";
import { useState, useEffect } from "react";
import {
Container,
Title,
Grid,
Card,
Text,
Badge,
Group,
Stack,
Box,
Modal,
List,
} from "@mantine/core";
import { BookOpen } from "lucide-react";
import { Post } from "@/app/lib/types/post";
import { fetchPosts } from "@/app/ui/service";
import { CopilotSidebar } from "@copilotkit/react-ui";
import { useCopilotAction } from "@copilotkit/react-core";
export default function KnowledgeBase() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
useEffect(() => {
const loadPosts = async () => {
try {
const data = await fetchPosts();
setPosts(data);
} catch (error) {
console.error("Error loading posts:", error);
} finally {
setLoading(false);
}
};
loadPosts();
}, []);
useCopilotAction({
name: "FetchKnowledgebaseArticles",
description: "Fetch relevant knowledge base articles based on a user query",
parameters: [
{
name: "query",
type: "string",
description: "User query for the knowledge base",
required: true,
},
],
handler: async ({ query }: { query: string }) => {},
render: "Getting relevant answers to your query...",
});
const handlePostClick = (post: Post) => {
setSelectedPost(post);
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<Container size="md" py="xl" ml="xl">
<Stack gap="xl">
<Group justify="center" align="center">
<BookOpen size={32} />
<Title order={1}>CopilotKit Product Knowledge Base</Title>
</Group>
<Grid>
{posts.map((post) => (
<Grid.Col key={post.id} span={{ base: 12, sm: 6, md: 4 }}>
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
onClick={() => handlePostClick(post)}
style={{ cursor: "pointer" }}
>
<Stack gap="md">
<Title order={3}>{post.title}</Title>
<Badge color="blue" variant="light">
{post.category}
</Badge>
<Text size="sm" c="dimmed">
{post.summary}
</Text>
<Text size="xs" c="dimmed">
Posted on: {new Date(post.createdAt).toLocaleDateString()}
</Text>
</Stack>
</Card>
</Grid.Col>
))}
</Grid>
{selectedPost && (
<Modal
opened={!!selectedPost}
onClose={() => setSelectedPost(null)}
title={selectedPost.title}
centered
size="xl"
>
<Stack gap="md">
<List>
{selectedPost.content
.split("\n")
.filter((item) => item.trim() !== "")
.map((item, index) => (
<List.Item key={index}>{item}</List.Item>
))}
</List>
</Stack>
</Modal>
)}
<Group justify="center" style={{ width: "100%" }}>
<Box style={{ flex: 1, maxWidth: "350px" }}>
<CopilotSidebar
instructions="Help the user get the information they need."
labels={{
initial:
"Welcome! Describe the query you need assistance with.",
}}
defaultOpen={true}
clickOutsideToClose={false}
/>
</Box>
</Group>
</Stack>
</Container>
);
}
@@ -0,0 +1,9 @@
import axios from "axios";
import { Post } from "@/app/lib/types/post";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
export const fetchPosts = async (): Promise<Post[]> => {
const response = await axios.get(`${API_BASE_URL}/api/posts`);
return response.data;
};
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.mjs"
],
"exclude": ["node_modules"]
}
@@ -0,0 +1,176 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
/node_modules
/frontend/.next
/frontend/node_modules
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
@@ -0,0 +1,64 @@
# Dynamic SaaS Dashboard
## Overview
Deployed on Vercel: https://dynamic-saas-dashboard.vercel.app
This project is a modern dashboard and analytics solution built with Next.js (in the `frontend` directory). It features:
- Clean, responsive UI for PR and repository analytics
- Interactive charts and tables
- Integration with OpenAI via CopilotKit
## Quick Start (Frontend)
1. **Install dependencies**
```bash
cd frontend
pnpm install
```
2. **Set up environment variables**
- Create a `.env` file in the `frontend` directory:
```bash
echo "OPENAI_API_KEY=your_openai_api_key_here" > .env
```
- Replace `your_openai_api_key_here` with your actual OpenAI API key.
3. **Run the development server**
```bash
pnpm run dev
```
The app will be available at [http://localhost:3000](http://localhost:3000).
---
## Quick Start (Backend Agent)
1. **Install dependencies**
```bash
cd agent
poetry install
```
2. **Set up environment variables**
- Create a `.env` file in the `agent` directory:
```bash
echo "OPENAI_API_KEY=your_openai_api_key_here" > .env
```
- Replace `your_openai_api_key_here` with your actual OpenAI API key.
3. **Run the Langgraph server**
```bash
python agent.py
```
---
To refer to the recording to the demo, Refer here :
```bash
https://www.loom.com/share/43be7bcbf1954672934e62ff8b3ee86e
```
@@ -0,0 +1,36 @@
# Dynamic SaaS Dashboard
This project is a dynamic SaaS dashboard built with Next.js (in the `frontend` directory). It features:
- Clean, responsive UI for PR and repository analytics
- Interactive charts and tables
- Integration with OpenAI via CopilotKit
## Backend Agent
1. **Install dependencies**
```bash
cd agent
poetry install
```
2. **Set up environment variables**
- Create a `.env` file in the `agent` directory:
```bash
echo "OPENAI_API_KEY=your_openai_api_key_here" > .env
```
- Replace `your_openai_api_key_here` with your actual OpenAI API key.
3. **Run the Langgraph server**
```bash
python agent.py
```
---
To refer to the recording to the demo, Refer here :
```bash
https://www.loom.com/share/43be7bcbf1954672934e62ff8b3ee86e
```
@@ -0,0 +1,459 @@
"""
A LangGraph implementation for the testing agent.
"""
from fastapi import FastAPI
import uvicorn
from copilotkit.integrations.fastapi import add_fastapi_endpoint
from copilotkit import CopilotKitSDK, LangGraphAGUIAgent
import os
import uuid
import json
from typing import Dict, List, Any
from dotenv import load_dotenv
load_dotenv()
# LangGraph imports
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END, START
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import MemorySaver
# CopilotKit imports
from copilotkit import CopilotKitState
from copilotkit.langgraph import (
copilotkit_customize_config,
copilotkit_emit_state,
copilotkit_interrupt,
)
# LLM imports
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from copilotkit.langgraph import copilotkit_exit
DEFINE_TEST_SCRIPT_TOOL = {
"type": "function",
"function": {
"name": "generate_test_scripts",
"description": "Make up 3 test scripts for a given task based on the context provided. The test scripts should be in the form of a list of steps.",
"parameters": {
"type": "object",
"properties": {
"testSuites": {
"type": "array",
"items": {
"type": "object",
"properties": {
"testId": {"type": "string"},
"prId": {"type": "string"},
"title": {"type": "string"},
"status": {
"type": "string",
"enum": ["passed", "failed", "idle"],
},
"shortDescription": {"type": "string"},
"testCases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"status": {
"type": "string",
"enum": [
"passed",
"failed",
"idle",
"pending",
],
},
"executionTime": {"type": "string"},
"createdAt": {
"type": "string",
"format": "date-time",
},
"updatedAt": {
"type": "string",
"format": "date-time",
},
"environment": {"type": "string"},
"browser": {"type": "string"},
"device": {"type": "string"},
"testSteps": {
"type": "array",
"items": {"type": "string"},
},
"failureReason": {"type": "string"},
},
"required": [
"id",
"name",
"status",
"executionTime",
"createdAt",
"updatedAt",
"environment",
"testSteps",
],
},
},
"totalTestCases": {"type": "number"},
"passedTestCases": {"type": "number"},
"failedTestCases": {"type": "number"},
"skippedTestCases": {"type": "number"},
"coverage": {"type": "number"},
"createdAt": {"type": "string", "format": "date-time"},
"updatedAt": {"type": "string", "format": "date-time"},
"executedBy": {"type": "string"},
},
"required": [
"testId",
"prId",
"title",
"status",
"shortDescription",
"testCases",
"totalTestCases",
"passedTestCases",
"failedTestCases",
"skippedTestCases",
"coverage",
"createdAt",
"updatedAt",
"executedBy",
],
},
}
},
"required": ["testSuites"],
},
},
}
class AgentState(CopilotKitState):
"""
The state of the agent.
It inherits from CopilotKitState which provides the basic fields needed by CopilotKit.
"""
testScripts: List[Dict[str, str]] = []
async def start_flow(state: Dict[str, Any], config: RunnableConfig):
"""
This is the entry point for the flow.
"""
# Initialize steps list if not exists
if "testScripts" not in state:
state["testScripts"] = []
return Command(
goto="chat_node",
update={
"messages": state["messages"],
"testScripts": state["testScripts"],
},
)
async def chat_node(state: Dict[str, Any], config: RunnableConfig):
"""
Standard chat node where the agent processes messages and generates responses.
If task steps are defined, the user can enable/disable them using interrupts.
"""
system_prompt = """
You are a helpful assistant that can perform any task related to software testing and PR validation.
You MUST call the `generate_test_scripts` function when the user asks you to perform a task.
Once generated with the test scripts, provide a summary of it in maximum 5 sentences. Dont list the entire thing in detail. Also prompt user that you can add the script to your testing list.
For every agent request, YOU MUST ALWAYS GENERATE 4 DIFFERENT TEST SUITES, each as a separate object in the array. Each test suite should be relevant to the context which is the CopilotKitReadables or PR provided by the user, and should have unique test cases and details. All the data which involves the user emails should be referred from the CopilotKitReadables.
The test suite object you work with has the following structure (all fields are required unless marked optional):
- testId: string
- prId: string
- title: string
- status: 'passed' | 'failed' | 'idle'
- shortDescription: string (a concise summary of what this test suite covers)
- testCases: array of objects, each with:
- id: string
- name: string
- status: 'passed' | 'failed' | 'idle' | 'pending'
- executionTime: string
- createdAt: string (date-time)
- updatedAt: string (date-time)
- environment: string
- browser?: string
- device?: string
- testSteps: array of strings
- failureReason?: string
- totalTestCases: number
- passedTestCases: number
- failedTestCases: number
- skippedTestCases: number
- coverage: number
- createdAt: string (date-time)
- updatedAt: string (date-time)
- executedBy: string
When generating or reasoning about test scripts, always use this schema and ensure your output is relevant to the PR and test context provided by the user.
"""
# Define the model
try:
model = ChatOpenAI(model="gpt-4o-mini")
except Exception as e:
print(e)
model = ChatOpenAI(model="gpt-4o")
# Define config for the model
if config is None:
config = RunnableConfig(recursion_limit=25)
# Use CopilotKit's custom config functions to properly set up streaming for the steps state
config = copilotkit_customize_config(
config,
emit_intermediate_state=[
{"state_key": "testScripts", "tool": "generate_test_scripts"}
],
)
# Bind the tools to the model
model_with_tools = model.bind_tools(
[*state["copilotkit"]["actions"], DEFINE_TEST_SCRIPT_TOOL],
# Disable parallel tool calls to avoid race conditions
parallel_tool_calls=False,
)
# Run the model and generate a response
response = await model_with_tools.ainvoke(
[
SystemMessage(content=system_prompt),
*state["messages"],
],
config,
)
# Update messages with the response
messages = state["messages"] + [response]
# Handle tool calls
if (
hasattr(response, "tool_calls")
and response.tool_calls
and len(response.tool_calls) > 0
):
tool_call = response.tool_calls[0]
# Extract tool call information
tool_call_id = ""
if hasattr(tool_call, "id"):
tool_call_id = tool_call.id
tool_call_name = tool_call.name
tool_call_args = (
tool_call.args
if not isinstance(tool_call.args, str)
else json.loads(tool_call.args)
)
else:
tool_call_id = tool_call.get("id", "")
tool_call_name = tool_call.get("name", "")
args = tool_call.get("args", {})
tool_call_args = args if not isinstance(args, str) else json.loads(args)
if tool_call_name == "generate_test_scripts":
# Get the steps from the tool call
state["testScripts"] = tool_call_args
print(tool_call_args, "tool_call_args")
tool_response = {
"role": "tool",
"content": "Test scripts generated. Allow user to select the test suites they want to run.",
"tool_call_id": tool_call_id,
}
# render_grid_tool_call = {
# "role": "assistant",
# "content": "",
# "tool_calls": [{
# "id": tool_call_id,
# "type": "function",
# "function": {
# "name": "renderGridWithTestCases",
# "arguments": json.dumps(tool_call_args)
# }
# }]
# }
messages = messages + [tool_response]
await copilotkit_exit(config)
return Command(
goto=END,
update={
"messages": messages,
"testScripts": state["testScripts"],
},
)
testScripts_raw = tool_call_args.get("testSuites", [])
print(testScripts_raw)
# Set initial status to "enabled" for all steps
testScripts_data = []
# Handle different potential formats of steps data
if isinstance(testScripts_raw, list):
for testScript in testScripts_raw:
if isinstance(testScript, dict) and "testId" in testScript:
testScripts_data.append(
{"testId": testScript["testId"], "status": "enabled"}
)
elif isinstance(testScript, str):
testScripts_data.append(
{"testId": testScript, "status": "enabled"}
)
state["testScripts"] = tool_call_args
# Generate a UUID for the tool call
tool_call_uuid = str(uuid.uuid4())
# Insert the assistant message with the tool call
# Now insert the tool response referencing the same tool_call_id
tool_response = {
"role": "tool",
"content": "Task steps generated.",
"tool_call_id": tool_call_uuid,
}
messages = messages + [tool_response]
# Move to the process_steps_node which will handle the interrupt and final response
return Command(
goto="process_steps_node",
update={
"messages": messages,
"testScripts": state["testScripts"],
},
)
# If no tool calls or not generate_task_steps, return to END with the updated messages
await copilotkit_exit(config)
return Command(
goto=END,
update={
"messages": messages,
"testScripts": state["testScripts"],
},
)
# async def process_steps_node(state: Dict[str, Any], config: RunnableConfig):
# """
# This node handles the user interrupt for step customization and generates the final response.
# """
# # Check if we already have a user_response in the state
# # This happens when the node restarts after an interrupt
# if "user_response" in state and state["user_response"]:
# user_response = state["user_response"]
# else:
# # Use LangGraph interrupt to get user input on steps
# # This will pause execution and wait for user input in the frontend
# user_response = interrupt({"steps": state["steps"]})
# # Store the user response in state for when the node restarts
# state["user_response"] = user_response
# # Generate the creative completion response
# final_prompt = """
# Provide a textual description of how you are performing the task.
# If the user has disabled a step, you are not allowed to perform that step.
# However, you should find a creative workaround to perform the task, and if an essential step is disabled, you can even use
# some humor in the description of how you are performing the task.
# Don't just repeat a list of steps, come up with a creative but short description (3 sentences max) of how you are performing the task.
# """
# final_response = await ChatOpenAI(model="gpt-4o").ainvoke([
# SystemMessage(content=final_prompt),
# {"role": "user", "content": user_response}
# ], config)
# # Add the final response to messages
# messages = state["messages"] + [final_response]
# # Clear the user_response from state to prepare for future interactions
# if "user_response" in state:
# state.pop("user_response")
# # Return to END with the updated messages
# await copilotkit_exit(config)
# return Command(
# goto=END,
# update={
# "messages": messages,
# "steps": state["steps"],
# }
# )
# Define the graph
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("start_flow", start_flow)
workflow.add_node("chat_node", chat_node)
# workflow.add_node("process_steps_node", process_steps_node)
# Add edges
workflow.set_entry_point("start_flow")
workflow.add_edge(START, "start_flow")
workflow.add_edge("start_flow", "chat_node")
# workflow.add_edge("chat_node", "process_steps_node") # Removed unconditional edge
# workflow.add_edge("process_steps_node", END)
workflow.add_edge("chat_node", END) # Removed unconditional edge
# Add conditional edges from chat_node
# def should_continue(command: Command):
# if command.goto == "process_steps_node":
# return "process_steps_node"
# else:
# return END
# workflow.add_conditional_edges(
# "chat_node",
# should_continue,
# {
# "process_steps_node": "process_steps_node",
# END: END,
# },
# )
# Compile the graph
testing_graph = workflow.compile(checkpointer=MemorySaver())
app = FastAPI()
sdk = CopilotKitSDK(
agents=[
LangGraphAGUIAgent(
name="testing_agent",
description="An example for a testing agent.",
graph=testing_graph,
)
]
)
add_fastapi_endpoint(app, sdk, "/copilotkit")
def main():
"""Run the uvicorn server."""
port = int(os.getenv("PORT", "8000"))
uvicorn.run(
"agent:app",
host="0.0.0.0",
port=port,
reload=True,
)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
[tool.poetry]
name = "agent"
version = "0.1.0"
description = ""
authors = ["Orca CopilotKit <testapp@copilotkit.ai>"]
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.12,<3.13"
copilotkit = "0.1.48"
langchain = ">=0.1.0"
langchain-core = ">=0.1.5"
langchain-community = ">=0.0.1"
langchain-experimental = ">=0.0.11"
langchain-openai = ">=0.0.1"
langgraph = "^0.3.25"
dotenv = "^0.9.9"
uvicorn = "^0.34.0"
fastapi = "0.115.12"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
server = "agent:main"
@@ -0,0 +1,5 @@
import { ChatInterface } from "@/components/chat-interface";
export default function DefaultChat() {
return <ChatInterface />;
}
@@ -0,0 +1,5 @@
import { ChatInterface } from "@/components/chat-interface";
export default function ChatPage() {
return <ChatInterface />;
}
@@ -0,0 +1,10 @@
import axios, { AxiosRequestConfig } from "axios";
export const client = async (config: AxiosRequestConfig) => {
try {
const res = await axios(config);
return res.data;
} catch (error) {
throw error;
}
};
@@ -0,0 +1,56 @@
export interface PRData {
id: string;
title: string;
status: string;
assignedReviewer: string;
assignedTester: string;
daysSinceStatusChange: number;
createdAt: string;
updatedAt: string;
userId: number;
author: string;
repository: string;
branch: string;
}
export interface chartData {
name: string;
value: number;
}
export interface WeeklyCount {
week: string;
count: number;
}
export interface TestsData {
testId: string;
prId: string;
title: string;
status: "idle" | "passed" | "failed" | "in_progress";
testCases: TestCase[];
totalTestCases: number;
passedTestCases: number;
failedTestCases: number;
skippedTestCases: number;
coverage: number;
createdAt: string;
updatedAt: string;
executedBy: string;
shortDescription?: string;
codeSnippet?: string;
}
interface TestCase {
id: string;
name: string;
status: "passed" | "failed" | "in_progress" | "pending";
executionTime: string;
createdAt: string;
updatedAt: string;
environment: string;
browser?: string;
device?: string;
testSteps: string[];
failureReason?: string;
}
@@ -0,0 +1,27 @@
import { client } from "../Client/client";
export async function getPRDataService() {
try {
let config = {
method: "POST",
url: "/api/getPRdata",
};
const res = await client(config);
return res;
} catch (error) {
throw error;
}
}
export async function getTestsService() {
try {
let config = {
method: "POST",
url: "/api/getTests",
};
const res = await client(config);
return res;
} catch (error) {
throw error;
}
}
@@ -0,0 +1,263 @@
import { prData } from "@/lib/data";
import testData from "@/lib/testData";
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { NextRequest } from "next/server";
const serviceAdapter = new OpenAIAdapter();
export const POST = async (req: NextRequest) => {
// console.log("req", req);
const runtime = new CopilotRuntime({
actions: ({ properties, url }) => {
return [
{
name: "fetchData_allPRData",
description: `Data fetching action that fetches all the PR data from the database.
The PR data structure includes:
- id: Unique PR identifier (e.g., 'PR01')
- title: PR title
- status: Current status ('approved', 'needs_revision', 'merged', 'in_review')
- assignedReviewer: Email of the assigned reviewer
- assignedTester: Email of the assigned tester
- daysSinceStatusChange: Number of days since last status change
- createdAt: ISO timestamp of creation
- updatedAt: ISO timestamp of last update
- userId: Numeric user ID
- author: Email of the PR author
- repository: Repository name
- branch: Branch name
Example PR data:
{
"id": "PR01324",
"title": "Implement user authentication flow",
"status": "approved",
"assignedReviewer": "johnknfjsg.doe@got.com",
"assignedTester": "janjnglkrfe.smith@got.com",
"daysSinceStatusChange": 2,
"createdAt": "2025-04-28T14:06:36.848Z",
"updatedAt": "2025-04-29T00:46:50.492Z",
"userId": 1,
"author": "Jon.snnrfmlkgow@got.com",
"repository": "frontend",
"branch": "feature/auth-flow"
}`,
parameters: [],
handler: async () => {
return prData;
},
},
{
name: "fetchData_PRDataByUserId",
description: `Data fetching action that filters PR data based on userId parameter.
The PR data structure includes:
- id: Unique PR identifier (e.g., 'PR01')
- title: PR title
- status: Current status ('approved', 'needs_revision', 'merged', 'in_review')
- assignedReviewer: Email of the assigned reviewer
- assignedTester: Email of the assigned tester
- daysSinceStatusChange: Number of days since last status change
- createdAt: ISO timestamp of creation
- updatedAt: ISO timestamp of last update
- userId: Numeric user ID
- author: Email of the PR author
- repository: Repository name
- branch: Branch name
Example PR data:
{
"id": "PR01hkfdiugo",
"title": "Implement user authentication flow",
"status": "approved",
"assignedReviewer": "johhgtuyhion.doe@got.com",
"assignedTester": "janeafsdg.smith@got.com",
"daysSinceStatusChange": 2,
"createdAt": "2025-04-28T14:06:36.848Z",
"updatedAt": "2025-04-29T00:46:50.492Z",
"userId": 1,
"author": "Jon.sfdgsnow@got.com",
"repository": "frontend",
"branch": "feature/auth-flow"
}`,
parameters: [
{
name: "userId",
type: "number",
description: "The user ID to filter PRs by",
},
],
handler: async ({ userId }: { userId: number }) => {
return prData.filter((pr) => pr.userId === userId);
},
},
{
name: "fetchData_AuthorNames",
description: `Data fetching action that gets all the unique author names from the PR data.
The PR data structure includes:
- id: Unique PR identifier (e.g., 'PR01')
- title: PR title
- status: Current status ('approved', 'needs_revision', 'merged', 'in_review')
- assignedReviewer: Email of the assigned reviewer
- assignedTester: Email of the assigned tester
- daysSinceStatusChange: Number of days since last status change
- createdAt: ISO timestamp of creation
- updatedAt: ISO timestamp of last update
- userId: Numeric user ID
- author: Email of the PR author
- repository: Repository name
- branch: Branch name
Example PR data:
{
"id": "PR0asdas1",
"title": "Implement user authentication flow",
"status": "approved",
"assignedReviewer": "johafsdkhoin.doe@got.com",
"assignedTester": "jaakdsjfogne.smith@got.com",
"daysSinceStatusChange": 2,
"createdAt": "2025-04-28T14:06:36.848Z",
"updatedAt": "2025-04-29T00:46:50.492Z",
"userId": 1,
"author": "Jon.snadsljfkoow@got.com",
"repository": "frontend",
"branch": "feature/auth-flow"
}`,
parameters: [],
handler: async () => {
let authorNames = prData.map((pr) => pr.author);
let uniqueAuthorNames = [...new Set(authorNames)];
console.log(uniqueAuthorNames, "uniqueAuthorNames");
return uniqueAuthorNames;
},
},
{
name: "fetchData_ReviewerNames",
description: `Data fetching action that gets all the unique reviewer names from the PR data.
The PR data structure includes:
- id: Unique PR identifier (e.g., 'PR01')
- title: PR title
- status: Current status ('approved', 'needs_revision', 'merged', 'in_review')
- assignedReviewer: Email of the assigned reviewer
- assignedTester: Email of the assigned tester
- daysSinceStatusChange: Number of days since last status change
- createdAt: ISO timestamp of creation
- updatedAt: ISO timestamp of last update
- userId: Numeric user ID
- author: Email of the PR author
- repository: Repository name
- branch: Branch name
Example PR data:
{
"id": "PR99",
"title": "Implement user authentication flow",
"status": "approved",
"assignedReviewer": "johnasd.doe@got.com",
"assignedTester": "jane.smitasdasdh@got.com",
"daysSinceStatusChange": 2,
"createdAt": "2025-04-28T14:06:36.848Z",
"updatedAt": "2025-04-29T00:46:50.492Z",
"userId": 1,
"author": "Jon.asdfsadfsnow@got.com",
"repository": "frontend",
"branch": "feature/auth-flow"
}`,
parameters: [],
handler: async () => {
let reviewerNames = prData.map((pr) => pr.assignedReviewer);
let uniqueReviewerNames = [...new Set(reviewerNames)];
console.log(uniqueReviewerNames, "uniqueReviewerNames");
return uniqueReviewerNames;
},
},
{
name: "fetchData_TesterNames",
description: `Data fetching action that gets all the unique tester names from the Tests data.
The Tests data structure includes:
- testId: Unique test identifier (e.g., 'TEST001')
- prId: Associated PR identifier (e.g., 'PR01')
- title: Test suite title
- status: Current status ('passed', 'failed', 'idle', 'running')
- testCases: Array of test cases, each containing:
- id: Test case identifier (e.g., 'TC001-1')
- name: Test case name
- status: Test case status
- executionTime: Time taken to execute
- createdAt: ISO timestamp of creation
- updatedAt: ISO timestamp of last update
- environment: Test environment
- browser: Browser used
- testSteps: Array of test steps
- totalTestCases: Total number of test cases
- passedTestCases: Number of passed test cases
- failedTestCases: Number of failed test cases
- skippedTestCases: Number of skipped test cases
- coverage: Test coverage percentage
- createdAt: ISO timestamp of test suite creation
- updatedAt: ISO timestamp of last update
- executedBy: Email of the tester who executed the tests
Example Test data:
{
"testId": "TEST69",
"prId": "PR81",
"title": "User Authentication Flow Test Suite",
"status": "passed",
"testCases": [
{
"id": "TC001-1",
"name": "Login with valid credentials",
"status": "passed",
"executionTime": "1.2s",
"createdAt": "2025-04-28T15:00:00.000Z",
"updatedAt": "2025-04-28T15:05:00.000Z",
"environment": "staging",
"browser": "Chrome",
"testSteps": [
"Enter valid email",
"Enter valid password",
"Click login button",
"Verify successful login"
]
}
],
"totalTestCases": 3,
"passedTestCases": 3,
"failedTestCases": 0,
"skippedTestCases": 0,
"coverage": 85,
"createdAt": "2025-04-28T14:30:00.000Z",
"updatedAt": "2025-04-28T15:30:00.000Z",
"executedBy": "jane.smith@got.com"
}`,
parameters: [],
handler: async () => {
let testerNames = testData.map((test) => test.executedBy);
let uniqueTesterNames = [...new Set(testerNames)];
console.log(uniqueTesterNames, "uniqueTesterNames");
return uniqueTesterNames;
},
},
] as any;
},
remoteEndpoints: [
{
url:
process.env.REMOTE_ACTION_URL || "http://localhost:8000/copilotkit",
},
],
});
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
@@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
import { prData } from "@/lib/data";
export const POST = async () => {
return NextResponse.json(prData);
};
@@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
import { testData } from "@/lib/testData";
export const POST = async () => {
return NextResponse.json(testData);
};
@@ -0,0 +1,10 @@
import { DashboardShell } from "@/components/dashboard-shell";
import { DeveloperDashboard } from "@/components/developer-dashboard";
export default function DeveloperPage() {
return (
<DashboardShell>
<DeveloperDashboard />
</DashboardShell>
);
}
@@ -0,0 +1,10 @@
import { DashboardShell } from "@/components/dashboard-shell";
import { PlaceholderDashboard } from "@/components/placeholder-dashboard";
export default function ExecutivePage() {
return (
<DashboardShell>
<PlaceholderDashboard title="Executive Dashboard" />
</DashboardShell>
);
}
@@ -0,0 +1,100 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
/* Sidebar variables */
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 221.2 83.2% 53.3%;
--sidebar-primary-foreground: 210 40% 98%;
--sidebar-accent: 210 40% 96.1%;
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
--sidebar-border: 214.3 31.8% 91.4%;
--sidebar-ring: 221.2 83.2% 53.3%;
}
.h-a {
height: 704px;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
/* Sidebar variables */
--sidebar-background: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 217.2 91.2% 59.8%;
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@@ -0,0 +1,10 @@
import { DashboardShell } from "@/components/dashboard-shell";
import { PlaceholderDashboard } from "@/components/placeholder-dashboard";
export default function LabAdminPage() {
return (
<DashboardShell>
<PlaceholderDashboard title="Lab Admin Dashboard" />
</DashboardShell>
);
}
@@ -0,0 +1,50 @@
import type React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { SidebarProvider } from "@/components/ui/sidebar";
import { ThemeProvider } from "@/components/theme-provider";
import { SharedProvider } from "@/lib/shared-context";
import { CopilotKit } from "@copilotkit/react-core";
import { SharedTestsProvider } from "@/lib/shared-tests-context";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Dynamic SaaS Dashboard",
description: "Dynamic SaaS Dashboard with persistent chat",
};
export default function RootLayout({
children,
chat,
}: {
children: React.ReactNode;
chat: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<CopilotKit runtimeUrl="/api/copilotkit">
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
<SharedProvider>
<SharedTestsProvider>
<SidebarProvider>
<div className="flex h-screen w-full overflow-hidden bg-background">
{children}
{chat}
</div>
</SidebarProvider>
</SharedTestsProvider>
</SharedProvider>
</ThemeProvider>
</CopilotKit>
</body>
</html>
);
}
@@ -0,0 +1,11 @@
import { DashboardShell } from "@/components/dashboard-shell";
import { DeveloperDashboard } from "@/components/developer-dashboard";
import { CopilotKit } from "@copilotkit/react-core";
export default function Home() {
return (
<DashboardShell>
<DeveloperDashboard />
</DashboardShell>
);
}
@@ -0,0 +1,10 @@
import { DashboardShell } from "@/components/dashboard-shell";
import { TesterDashboard } from "@/components/tester-dashboard";
export default function TesterPage() {
return (
<DashboardShell>
<TesterDashboard />
</DashboardShell>
);
}
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
@@ -0,0 +1,20 @@
"use client";
import { CopilotChat } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import { useSharedContext } from "@/lib/shared-context";
import { instructions } from "@/lib/prompts";
export function ChatInterface() {
const { prData } = useSharedContext();
return (
<div className="flex h-full w-80 flex-col border-l bg-background">
<div className="flex items-center justify-between border-b px-4 py-4">
<h2 className="font-semibold">EnterpriseX Assistant</h2>
</div>
<CopilotChat
className="flex-1 min-h-0 py-4"
instructions={instructions.replace("{prData}", JSON.stringify(prData))}
/>
</div>
);
}
@@ -0,0 +1,142 @@
"use client";
import type React from "react";
import { usePathname } from "next/navigation";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { ModeToggle } from "@/components/mode-toggle";
import {
Code2,
FlaskConical,
LayoutDashboard,
LogOut,
Settings,
TestTube2,
TrendingUp,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Link from "next/link";
import { useCopilotChat, useCopilotReadable } from "@copilotkit/react-core";
import {
devSuggestions,
generalSuggestions,
testerPersonaSuggestions,
} from "@/lib/prompts";
import { useSharedContext } from "@/lib/shared-context";
import { useCopilotChatSuggestions } from "@copilotkit/react-ui";
import { useSharedTestsContext } from "@/lib/shared-tests-context";
import { useEffect } from "react";
export function DashboardShell({ children }: { children: React.ReactNode }) {
const { prData } = useSharedContext();
const { testsData } = useSharedTestsContext();
const pathname = usePathname();
const { setMessages } = useCopilotChat();
const routes = [
{
title: "Developer",
href: "/developer",
icon: Code2,
isActive: pathname === "/developer" || pathname === "/",
},
{
title: "Tester",
href: "/tester",
icon: TestTube2,
isActive: pathname === "/tester",
},
{
title: "Lab Admin",
href: "/lab-admin",
icon: FlaskConical,
isActive: pathname === "/lab-admin",
},
{
title: "Executive",
href: "/executive",
icon: TrendingUp,
isActive: pathname === "/executive",
},
];
useCopilotChatSuggestions(
{
instructions: devSuggestions,
maxSuggestions: 3,
},
[pathname],
);
useCopilotReadable({
description: "Current pathname",
value: pathname,
});
return (
<div className="flex h-full flex-1">
<Sidebar side="left" className="border-r">
<SidebarHeader className="flex justify-between px-4 py-2">
<div className="flex items-center gap-2">
<LayoutDashboard className="h-6 w-6" />
<h1 className="text-xl font-semibold tracking-tight">
EnterpriseX
</h1>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{routes.map((route) => (
<SidebarMenuItem key={route.href} className="p-3 px-6">
<SidebarMenuButton asChild isActive={route.isActive}>
<Link onClick={() => setMessages([])} href={route.href}>
<route.icon className="mr-2 h-5 w-5" />
<span>{route.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="#">
<Settings className="mr-2 h-5 w-5" />
<span>Settings</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="#">
<LogOut className="mr-2 h-5 w-5" />
<span>Logout</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<div className="flex flex-1 flex-col overflow-hidden">
<header className="flex h-14 items-center gap-4 border-b bg-background px-6">
<div className="ml-auto flex items-center gap-4">
{/* <ModeToggle /> */}
<Avatar>
<AvatarImage src="/abstract-geometric-shapes.png" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
</div>
</header>
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
</div>
);
}
@@ -0,0 +1,107 @@
"use client";
// import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "@/components/ui/chart"
import { Pie, PieChart, Cell, Tooltip } from "recharts";
import { useSharedContext } from "@/lib/shared-context";
import { useEffect, useState } from "react";
import { PRData, chartData } from "@/app/Interfaces/interface";
import { CustomPieTooltip } from "./pr-pie-all-data";
interface DataChartProps {
data: PRData[];
}
export function DataChart({ data }: DataChartProps) {
// Extract keys excluding 'name'
// const dataKeys = Object.keys(data[0]).filter((key) => key !== "name")
const [chartData, setChartData] = useState<chartData[]>([]);
// const {prData} = useSharedContext()
const status = [
{
name: "approved",
color: "bg-green-300",
value: "rgb(134 239 172)",
},
{
name: "needs_revision",
color: "bg-yellow-300",
value: "rgb(253 224 71)",
},
{
name: "merged",
color: "bg-purple-300",
value: "rgb(216 180 254)",
},
{
name: "in_review",
color: "bg-blue-300",
value: "rgb(147 197 253)",
},
];
useEffect(() => {
let buffer = status.map((status) => {
return {
name: status.name,
value: data.filter((pr: PRData) => pr.status === status.name).length,
};
});
setChartData(buffer);
}, [data]);
// Generate colors for each data key
const colors = [
"hsl(var(--chart-1))",
"hsl(var(--chart-2))",
"hsl(var(--chart-3))",
"hsl(var(--chart-4))",
"hsl(var(--chart-5))",
];
return (
<>
{/* <div className="flex-1 p-4 rounded-2xl shadow-lg flex flex-col items-center min-w-[250px] max-w-[350px]"> */}
{/* <h2 className="text-2xl font-semibold mb-2 text-gray-700 text-center">PR Status Distribution</h2> */}
<div className="h-[250px] flex flex-col items-center justify-center align-center">
<PieChart width={260} height={200}>
<Pie
data={chartData}
cx={130}
cy={90}
innerRadius={50}
outerRadius={90}
fill="#94a3b8"
paddingAngle={0}
dataKey="value"
label
labelLine={false}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
status.find((status) => status.name === entry.name)?.value
}
/>
))}
</Pie>
{/* bg-white p-2 rounded shadow text-black */}
<Tooltip content={<CustomPieTooltip />} />
</PieChart>
{/* Custom Legend */}
<div className="flex flex-row justify-center gap-6 mt-2">
{chartData.map((entry, idx) => (
<div key={entry.name} className="flex items-center gap-1">
<span
className={`inline-block w-4 h-4 rounded-full ${status.find((status) => status.name === entry.name)?.color}`}
/>
<span className="text-sm text-black">
{entry.name.split("_").join(" ")}
</span>
</div>
))}
</div>
</div>
{/* </div> */}
</>
);
}
@@ -0,0 +1,208 @@
import { useEffect, useState } from "react";
import { TestsData } from "@/app/Interfaces/interface";
import {
TableHead,
TableHeader,
TableRow,
TableCell,
TableBody,
Table,
} from "./ui/table";
import { Checkbox } from "./ui/checkbox";
import { codeSnippets } from "@/public/snippets";
import { Button } from "@/components/ui/button";
import React from "react";
import { Badge } from "./ui/badge";
import { getStatusColor } from "./data-table-results";
export function ChatGrid({
status,
state,
testSuite,
setTestSuite,
testCaseStatus,
setTestCaseStatus,
}: {
status: string;
state: any;
testSuite: TestsData[];
setTestSuite: (testSuite: TestsData[]) => void;
testCaseStatus: any;
setTestCaseStatus: (testCaseStatus: any) => void;
}) {
const [newScriptsData, setNewScriptsData] = useState<TestsData[]>([]);
const [expandedRow, setExpandedRow] = useState<number | null>(null);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [disabled, setDisabled] = useState<boolean>(false);
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedRows(newScriptsData?.map((_, index) => index) || []);
} else {
setSelectedRows([]);
}
};
const handleRowSelect = (index: number, checked: boolean) => {
if (checked) {
setSelectedRows([...selectedRows, index]);
} else {
setSelectedRows(selectedRows.filter((i) => i !== index));
}
};
const handleSelectedAction = () => {
// Handle the action for selected rows
// if (respond) {
console.log("Selected rows:", selectedRows);
// onToggle([...testSuite, ...newScriptsData.filter((_, index) => selectedRows.includes(index))])
setTestSuite([
...testSuite,
...newScriptsData.filter((_, index) => selectedRows.includes(index)),
]);
setSelectedRows([]);
setDisabled(true);
// respond("The selected test suites have been added successfully")
// }
// Add your custom logic here
};
useEffect(() => {
// console.log(nodeName,status, "nodeNamenodeName")
// if (status === "executing") {
setNewScriptsData(state?.testScripts?.testSuites);
// }
}, [state, status]);
const handleRowClick = (rowIndex: number) => {
setExpandedRow(expandedRow === rowIndex ? null : rowIndex);
};
return (
<>
{newScriptsData && (
<div className="rounded-md border w-full min-w-[200px]">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">
<Checkbox
disabled={disabled}
className="rounded-md border-gray-300 dark:border-gray-600"
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Test Suite</TableHead>
<TableHead>Test Cases</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{newScriptsData?.map((script, index) => (
<React.Fragment key={index}>
<TableRow
className="hover:bg-gray-50 transition"
onClick={() => handleRowClick(index)}
>
<TableCell className="w-[50px]">
<Checkbox
disabled={disabled}
className="rounded-md border-gray-300 dark:border-gray-600"
checked={selectedRows.includes(index)}
onCheckedChange={(checked) => {
handleRowSelect(index, checked as boolean);
event?.stopPropagation();
}}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell>{script?.title}</TableCell>
<TableCell>{script?.testCases?.length}</TableCell>
</TableRow>
{expandedRow === index &&
!disabled &&
status === "complete" && (
<TableRow>
<TableCell
colSpan={3}
className="bg-gray-50 dark:bg-[#181f2a] p-0 border-t-0"
>
<div className="p-4">
<div className="font-semibold mb-2">
Test Suite Description:
</div>
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300">
{script.shortDescription ||
"No description available."}
</div>
<div className="font-semibold mb-2">
Code Snippet:
</div>
<pre className="bg-gray-100 dark:bg-[#181f2a] rounded p-2 mb-4 overflow-x-auto text-xs">
{
codeSnippets[
Math.floor(
Math.random() * codeSnippets.length,
)
]
}
</pre>
<div className="font-semibold mb-2">
Test Cases Details:
</div>
<ul className="space-y-4">
{script.testCases.map((tc, idx) => (
<li
key={tc.id}
className="border rounded p-3 bg-white dark:bg-[#232b3b]"
>
<div className="mb-1 flex items-center gap-2">
<StatusBadge
status={
testCaseStatus[index]?.[idx] ||
tc.status
}
/>
<span className="font-semibold">
{tc.name}
</span>
</div>
</li>
))}
</ul>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
</Table>
{selectedRows.length > 0 && (
<div className="p-4 border-t flex justify-between items-center bg-gray-50 dark:bg-[#181f2a]">
<div className="text-sm text-gray-600 dark:text-gray-300">
{selectedRows.length}{" "}
{selectedRows.length === 1 ? "row" : "rows"} selected
</div>
<Button
onClick={handleSelectedAction}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
Add Selected Tests
</Button>
</div>
)}
</div>
)}
</>
);
}
function StatusBadge({ status }: { status: string }) {
return (
<Badge
variant="outline"
className={`px-2 py-1 rounded-full text-xs font-medium text-center ${getStatusColor(status)}`}
>
{status.split("_").join(" ")}
</Badge>
);
}
@@ -0,0 +1,311 @@
export function DataCode() {
const pythonCode = `"""
A LangGraph implementation for the testing agent.
"""
from fastapi import FastAPI
import uvicorn
from copilotkit.integrations.fastapi import add_fastapi_endpoint
from copilotkit import CopilotKitSDK, LangGraphAgent
import os
import uuid
import json
from typing import Dict, List, Any
from dotenv import load_dotenv
load_dotenv()
# LangGraph imports
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END, START
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import MemorySaver
# CopilotKit imports
from copilotkit import CopilotKitState
from copilotkit.langgraph import copilotkit_customize_config, copilotkit_emit_state, copilotkit_interrupt
# LLM imports
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage
from copilotkit.langgraph import (copilotkit_exit)
DEFINE_TEST_SCRIPT_TOOL = {
"type": "function",
"function": {
"name": "generate_test_scripts",
"description": "Make up 3 test scripts for a given task based on the context provided. The test scripts should be in the form of a list of steps.",
"parameters": {
"type": "object",
"properties": {
"testSuites": {
"type": "array",
"items": {
"type": "object",
"properties": {
"testId": { "type": "string" },
"prId": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string", "enum": ["passed", "failed", "yet_to_start"] },
"shortDescription": { "type": "string" },
"testCases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"status": { "type": "string", "enum": ["passed", "failed", "yet_to_start", "pending"] },
"executionTime": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" },
"environment": { "type": "string" },
"browser": { "type": "string" },
"device": { "type": "string" },
"testSteps": {
"type": "array",
"items": { "type": "string" }
},
"failureReason": { "type": "string" }
},
"required": [
"id",
"name",
"status",
"executionTime",
"createdAt",
"updatedAt",
"environment",
"testSteps",
]
}
},
"totalTestCases": { "type": "number" },
"passedTestCases": { "type": "number" },
"failedTestCases": { "type": "number" },
"skippedTestCases": { "type": "number" },
"coverage": { "type": "number" },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" },
"executedBy": { "type": "string" }
},
"required": [
"testId",
"prId",
"title",
"status",
"shortDescription",
"testCases",
"totalTestCases",
"passedTestCases",
"failedTestCases",
"skippedTestCases",
"coverage",
"createdAt",
"updatedAt",
"executedBy"
]
}
}
},
"required": ["testSuites"]
}
}
}
class AgentState(CopilotKitState):
"""
The state of the agent.
It inherits from CopilotKitState which provides the basic fields needed by CopilotKit.
"""
testScripts: List[Dict[str, str]] = []
async def start_flow(state: Dict[str, Any], config: RunnableConfig):
"""
This is the entry point for the flow.
"""
# Initialize steps list if not exists
if "testScripts" not in state:
state["testScripts"] = []
return Command(
goto="chat_node",
update={
"messages": state["messages"],
"testScripts": state["testScripts"],
}
)
async def chat_node(state: Dict[str, Any], config: RunnableConfig):
"""
Standard chat node where the agent processes messages and generates responses.
If task steps are defined, the user can enable/disable them using interrupts.
"""
system_prompt = """
You are a helpful assistant that can perform any task related to software testing and PR validation.
You MUST call the \`generate_test_scripts\` function when the user asks you to perform a task.
Once generated with the test scripts, provide a summary of it in maximum 5 sentences. Dont list the entire thing in detail. Also prompt user that you can add the script to your testing list.
For every agent request, YOU MUST ALWAYS GENERATE 4 DIFFERENT TEST SUITES, each as a separate object in the array. Each test suite should be relevant to the context which is the CopilotKitReadables or PR provided by the user, and should have unique test cases and details. All the data which involves the user emails should be referred from the CopilotKitReadables.
The test suite object you work with has the following structure (all fields are required unless marked optional):
- testId: string
- prId: string
- title: string
- status: 'passed' | 'failed' | 'yet_to_start'
- shortDescription: string (a concise summary of what this test suite covers)
- testCases: array of objects, each with:
- id: string
- name: string
- status: 'passed' | 'failed' | 'yet_to_start' | 'pending'
- executionTime: string
- createdAt: string (date-time)
- updatedAt: string (date-time)
- environment: string
- browser?: string
- device?: string
- testSteps: array of strings
- failureReason?: string
- totalTestCases: number
- passedTestCases: number
- failedTestCases: number
- skippedTestCases: number
- coverage: number
- createdAt: string (date-time)
- updatedAt: string (date-time)
- executedBy: string
When generating or reasoning about test scripts, always use this schema and ensure your output is relevant to the PR and test context provided by the user.
"""
# Define the model
model = ChatOpenAI(model="gpt-4o-mini")
# Define config for the model
if config is None:
config = RunnableConfig(recursion_limit=25)
# Use CopilotKit's custom config functions to properly set up streaming for the steps state
config = copilotkit_customize_config(
config,
emit_intermediate_state=[{
"state_key": "testScripts",
"tool": "generate_test_scripts"
}],
)
# Bind the tools to the model
model_with_tools = model.bind_tools(
[
*state["copilotkit"]["actions"],
DEFINE_TEST_SCRIPT_TOOL
],
# Disable parallel tool calls to avoid race conditions
parallel_tool_calls=False,
)
# Run the model and generate a response
response = await model_with_tools.ainvoke([
SystemMessage(content=system_prompt),
*state["messages"],
], config)
# Update messages with the response
messages = state["messages"] + [response]
# Handle tool calls
if hasattr(response, "tool_calls") and response.tool_calls and len(response.tool_calls) > 0:
tool_call = response.tool_calls[0]
# Extract tool call information
tool_call_id = ""
if hasattr(tool_call, "id"):
tool_call_id = tool_call.id
tool_call_name = tool_call.name
tool_call_args = tool_call.args if not isinstance(tool_call.args, str) else json.loads(tool_call.args)
else:
tool_call_id = tool_call.get("id", "")
tool_call_name = tool_call.get("name", "")
args = tool_call.get("args", {})
tool_call_args = args if not isinstance(args, str) else json.loads(args)
if tool_call_name == "generate_test_scripts":
# Get the steps from the tool call
state["testScripts"] = tool_call_args
print(tool_call_args, "tool_call_args")
tool_response = {
"role": "tool",
"content": "Test scripts generated. Allow user to select the test suites they want to run.",
"tool_call_id": tool_call_id
}
messages = messages + [tool_response]
await copilotkit_exit(config)
return Command(
goto=END,
update={
"messages": messages,
"testScripts": state["testScripts"],
}
)
# If no tool calls or not generate_task_steps, return to END with the updated messages
await copilotkit_exit(config)
return Command(
goto=END,
update={
"messages": messages,
"testScripts": state["testScripts"],
}
)
# Define the graph
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("start_flow", start_flow)
workflow.add_node("chat_node", chat_node)
# Add edges
workflow.set_entry_point("start_flow")
workflow.add_edge(START, "start_flow")
workflow.add_edge("start_flow", "chat_node")
workflow.add_edge("chat_node", END) # Removed unconditional edge
# Compile the graph
testing_graph = workflow.compile(checkpointer=MemorySaver())
app = FastAPI()
sdk = CopilotKitSDK(
agents=[
LangGraphAgent(
name="testing_agent",
description="An example for a testing agent.",
graph=testing_graph,
)
]
)
add_fastapi_endpoint(app, sdk, "/copilotkit")
def main():
"""Run the uvicorn server."""
port = int(os.getenv("PORT", "8000"))
uvicorn.run(
"agent:app",
host="0.0.0.0",
port=port,
reload=True,
)
if __name__ == "__main__":
main()`;
return (
<div className="p-4 bg-white rounded-lg border border-gray-200 shadow-sm w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-800">Agent Code</h2>
<span className="text-sm text-gray-500">Python</span>
</div>
<pre className="overflow-x-auto">
<code className="text-sm text-gray-700 font-mono whitespace-pre-wrap break-words">
{pythonCode}
</code>
</pre>
</div>
);
}
@@ -0,0 +1,186 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { PRData, TestsData } from "@/app/Interfaces/interface";
import React, { useEffect, useState } from "react";
import { useCoAgent, useCoAgentStateRender } from "@copilotkit/react-core";
import { Checkbox } from "@/components/ui/checkbox";
interface DataTableProps {
columns: {
accessorKey: string;
header: string;
}[];
data: TestsData[];
}
export function DataTable({ columns, data }: DataTableProps) {
const [expandedRow, setExpandedRow] = useState<number | null>(null);
// Get all possible keys from data (assuming all rows have same keys)
const allKeys = data.length > 0 ? Object.keys(data[0]) : [];
const mainKeys = columns.map((col) => col.accessorKey);
const extraKeys = allKeys.filter((key) => !mainKeys.includes(key));
const handleRowClick = (rowIndex: number) => {
setExpandedRow(expandedRow === rowIndex ? null : rowIndex);
};
return (
<div className="rounded-md border w-full min-w-[700px]">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">
<Checkbox
className="rounded-md border-gray-300 dark:border-gray-600"
onCheckedChange={(checked) => {
// Handle select all logic here
}}
/>
</TableHead>
<TableHead>Test Id</TableHead>
<TableHead>Test Cases</TableHead>
<TableHead>PR Ref</TableHead>
<TableHead>Executed By</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-gray-400 py-8"
>
No test data available.
</TableCell>
</TableRow>
) : (
data.map((row, rowIndex) => (
<React.Fragment key={rowIndex}>
<TableRow
className="cursor-pointer hover:bg-gray-50 transition"
onClick={() => handleRowClick(rowIndex)}
>
<TableCell className="w-[50px]">
<Checkbox
className="rounded-md border-gray-300 dark:border-gray-600"
onCheckedChange={(checked) => {
// Handle individual checkbox logic here
event?.stopPropagation(); // Prevent row click when clicking checkbox
}}
onClick={(e) => e.stopPropagation()} // Prevent row click when clicking checkbox
/>
</TableCell>
{columns.map((column) => (
<TableCell key={column.accessorKey} className="w-2">
{column.accessorKey === "status" ? (
<StatusBadge
status={
row[column.accessorKey as keyof TestsData] as string
}
/>
) : column.accessorKey === "testCases" ? (
`${row.testCases.length} cases`
) : (
String(row[column.accessorKey as keyof TestsData])
)}
</TableCell>
))}
</TableRow>
{expandedRow === rowIndex && extraKeys.length > 0 && (
<TableRow>
<TableCell
colSpan={columns.length}
className="bg-gray-50 dark:bg-[#181f2a] p-0 border-t-0"
>
<div className="p-4 grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Coverage:
</span>
<span className="font-semibold">{row.coverage}%</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Created:
</span>
<span className="font-semibold">
{new Date(row.createdAt).toLocaleString()}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Updated:
</span>
<span className="font-semibold">
{new Date(row.updatedAt).toLocaleString()}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Test Cases:
</span>
<span className="font-semibold">
{row.passedTestCases} Passed, {row.failedTestCases}{" "}
Failed, {row.skippedTestCases} Skipped
</span>
</div>
</div>
<div className="p-4">
<div className="font-semibold mb-2">Test Cases:</div>
<ul className="space-y-1">
{row.testCases.map((tc) => (
<li key={tc.id} className="flex items-center gap-2">
<StatusBadge status={tc.status} />
<span>{tc.name}</span>
</li>
))}
</ul>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
))
)}
</TableBody>
</Table>
</div>
);
}
export const getStatusColor = (status: string) => {
switch (status) {
case "in_progress":
return "bg-blue-100 text-blue-700";
case "passed":
return "bg-green-100 text-green-700";
case "failed":
return "bg-red-100 text-red-700";
case "skipped":
return "bg-gray-100 text-gray-700";
default:
return "bg-gray-100 text-gray-700";
}
};
function StatusBadge({ status }: { status: string }) {
return (
<Badge
variant="outline"
className={`px-2 py-1 rounded-full text-xs font-medium text-center ${getStatusColor(status)}`}
>
{status.split("_").join(" ")}
</Badge>
);
}
@@ -0,0 +1,302 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { PRData, TestsData } from "@/app/Interfaces/interface";
import React, { useEffect, useState } from "react";
import { Button } from "./ui/button";
import { codeSnippets } from "@/public/snippets";
import { Checkbox } from "./ui/checkbox";
import {
useCopilotAction,
ActionRenderPropsWait,
} from "@copilotkit/react-core";
import { PlayCircle, Loader2, CheckCircle2, XCircle } from "lucide-react";
import { ChatGrid } from "./data-chat-grid";
interface DataTableProps {
columns: {
accessorKey: string;
header: string;
}[];
data: TestsData[];
onToggle: (testSuite: TestsData[]) => void;
setTestsData: (testsData: TestsData[]) => void;
testsData: TestsData[];
}
export function DataTable({
columns,
data,
onToggle,
setTestsData,
testsData,
}: DataTableProps) {
const [expandedRow, setExpandedRow] = useState<number | null>(null);
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [testSuite, setTestSuite] = useState<TestsData[]>(data || []);
const [testSuiteToMove, setTestSuiteToMove] = useState<TestsData[]>([]);
const [testStatus, setTestStatus] = useState<{
[key: number]: "idle" | "running" | "passed" | "failed";
}>({});
const [testCaseStatus, setTestCaseStatus] = useState<{
[rowIndex: number]: string[];
}>({});
const [snippetsHandler, setSnippetsHandler] = useState<TestsData[]>([]);
// Get all possible keys from data (assuming all rows have same keys)
const allKeys = data.length > 0 ? Object.keys(data[0]) : [];
const mainKeys = columns.map((col) => col.accessorKey);
const extraKeys = allKeys.filter((key) => !mainKeys.includes(key));
useEffect(() => {
setTestSuite(data);
setTestStatus((prev) => ({
...prev,
...Object.fromEntries(data.map((item, index) => [index, item.status])),
}));
}, [data]);
const handleRowClick = (rowIndex: number) => {
setExpandedRow(expandedRow === rowIndex ? null : rowIndex);
};
// Handler for play icon
const runTest = (rowIndex: number, row: TestsData) => {
if (testStatus[rowIndex] === "running") return;
setTestStatus((prev) => ({ ...prev, [rowIndex]: "running" }));
setTestCaseStatus((prev) => ({
...prev,
[rowIndex]: testSuite[rowIndex]?.testCases.map(() => "running"),
}));
setTimeout(() => {
const isPassed = Math.random() > 0.5;
setTestStatus((prev) => ({
...prev,
[rowIndex]: isPassed ? "passed" : "failed",
}));
setTestCaseStatus((prev) => ({
...prev,
[rowIndex]: testSuite[rowIndex]?.testCases.map(() =>
isPassed ? "passed" : "failed",
),
}));
const suiteToMove = testSuite[rowIndex];
console.log(suiteToMove, "suiteToMove", rowIndex);
if (suiteToMove) {
// onToggle([...testSuite.filter((_, idx) => idx !== rowIndex)])
console.log(suiteToMove, "suiteToMove");
suiteToMove.testCases.forEach((element) => {
element.status = isPassed ? "passed" : "failed";
});
suiteToMove.status = isPassed ? "passed" : "failed";
console.log(testSuiteToMove, "testSuiteToMove");
setTestSuiteToMove([
...testSuiteToMove,
{
title: suiteToMove.title,
status: suiteToMove.status,
shortDescription: suiteToMove.shortDescription,
testCases: suiteToMove.testCases,
testId: suiteToMove.testId,
prId: suiteToMove.prId,
failedTestCases: suiteToMove.failedTestCases,
passedTestCases: suiteToMove.passedTestCases,
skippedTestCases: suiteToMove.skippedTestCases,
totalTestCases: suiteToMove.totalTestCases,
codeSnippet: suiteToMove.codeSnippet,
executedBy: suiteToMove.executedBy,
coverage: suiteToMove.coverage,
createdAt: suiteToMove.createdAt,
updatedAt: suiteToMove.updatedAt,
},
]);
}
}, 2000);
};
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead className="font-bold" key={column.accessorKey}>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{testSuite.length == 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-gray-400 py-8"
>
No test data available.
</TableCell>
</TableRow>
) : (
testSuite.map((row, rowIndex) => (
<React.Fragment key={rowIndex}>
<TableRow
className="hover:bg-gray-50 transition"
onClick={() => handleRowClick(rowIndex)}
style={{ cursor: "default" }}
>
{columns.map((column) => (
<TableCell
key={column.accessorKey}
className={
column.accessorKey === "status"
? "w-[40px] text-center"
: column.accessorKey === "testName"
? "min-w-[300px]"
: column.accessorKey === "assignedTo"
? "w-[180px]"
: column.accessorKey === "prRef"
? "w-[100px]"
: column.accessorKey === "testId"
? "w-[90px]"
: ""
}
>
{column.accessorKey === "status" ? (
<div className="flex items-center justify-center">
{testStatus[rowIndex] === "running" ? (
<Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
) : testStatus[rowIndex] === "passed" ? (
<CheckCircle2 className="w-5 h-5 text-green-600" />
) : testStatus[rowIndex] === "failed" ? (
<XCircle className="w-5 h-5 text-red-600" />
) : (
<PlayCircle
className="w-5 h-5 text-blue-600 hover:text-blue-700 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
if (
!Object.values(testStatus).some(
(status) => status === "running",
)
) {
runTest(rowIndex, row);
}
}}
/>
)}
</div>
) : (
String(row[column.accessorKey as keyof TestsData])
)}
</TableCell>
))}
</TableRow>
{expandedRow === rowIndex && (
<TableRow>
<TableCell
colSpan={columns.length}
className="!p-0 border-t-0"
style={{ background: "none" }}
>
<div className="relative bg-gray-50 dark:bg-[#f5f7fa]/10 rounded-b-lg shadow-sm mx-2 my-2 p-6 border-2 border-dotted border-gray-300 dark:border-gray-600">
<div className="font-semibold mb-2 text-gray-800 dark:text-gray-100">
Description:
</div>
<div className="mb-4 text-sm text-gray-600 dark:text-gray-300">
{row.shortDescription || "No description available."}
</div>
<div className="font-semibold mb-2 text-gray-800 dark:text-gray-100">
Code Snippet:
</div>
<pre className="bg-gray-100 dark:bg-[#181f2a] rounded p-3 mb-4 overflow-x-auto text-xs border border-gray-200 dark:border-gray-700">
{
codeSnippets[
Math.floor(Math.random() * codeSnippets.length)
]
}
</pre>
<div className="font-semibold mb-2 text-gray-800 dark:text-gray-100">
Test Cases:
</div>
<ul className="space-y-1">
{row.testCases.map((tc, idx) => (
<li key={tc.id} className="flex items-center gap-2">
<StatusBadge
status={
testCaseStatus[rowIndex]?.[idx] || tc.status
}
/>
<span>{tc.name}</span>
</li>
))}
</ul>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
))
)}
</TableBody>
</Table>
<div className="flex justify-center my-4">
<Button
variant="outline"
className="mt-4"
disabled={
Object.values(testStatus).length == 0 ||
Object.values(testStatus).some(
(status) => status === "idle" || status === "running",
)
}
onClick={() => {
onToggle([]);
setTestSuite([]);
setTestsData([...testsData, ...testSuiteToMove]);
setTestSuiteToMove([]);
setTestStatus({});
}}
>
Move Completed Tests to Results
</Button>
</div>
</div>
);
}
const getStatusColor = (status: string) => {
switch (status) {
case "in_progress":
return "bg-blue-100 text-blue-700";
case "passed":
return "bg-green-100 text-green-700";
case "failed":
return "bg-red-100 text-red-700";
case "skipped":
return "bg-gray-100 text-gray-700";
default:
return "bg-gray-100 text-gray-700";
}
};
function StatusBadge({ status }: { status: string }) {
return (
<Badge
variant="outline"
className={`px-2 py-1 rounded-full text-xs font-medium text-center ${getStatusColor(status)}`}
>
{status.split("_").join(" ")}
</Badge>
);
}
@@ -0,0 +1,168 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { PRData } from "@/app/Interfaces/interface";
import React, { useState } from "react";
interface DataTableProps {
columns: {
accessorKey: string;
header: string;
}[];
data: PRData[];
}
export function DataTable({ columns, data }: DataTableProps) {
const [expandedRow, setExpandedRow] = useState<number | null>(null);
// Get all possible keys from data (assuming all rows have same keys)
// const allKeys = data.length > 0 ? Object.keys(data[0]) : [];
// const mainKeys = columns.map(col => col.accessorKey);
// const extraKeys = allKeys.filter(key => !mainKeys.includes(key));
const handleRowClick = (rowIndex: number) => {
setExpandedRow(expandedRow === rowIndex ? null : rowIndex);
};
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead className="font-bold" key={column.accessorKey}>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data?.map((row, rowIndex) => (
<React.Fragment key={rowIndex}>
<TableRow
className="cursor-pointer hover:bg-gray-50 transition"
onClick={() => handleRowClick(rowIndex)}
>
{columns.map((column) => (
<TableCell key={column.accessorKey} className="w-2">
{column.accessorKey === "status" ? (
<StatusBadge status={row[column.accessorKey]} />
) : (
row[column.accessorKey as keyof PRData]
)}
</TableCell>
))}
</TableRow>
{expandedRow === rowIndex && (
<TableRow>
<TableCell
colSpan={columns.length}
className="bg-gray-50 dark:bg-[#181f2a] p-0 border-t-0"
>
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${expandedRow === rowIndex ? "max-h-96 opacity-100 scale-y-100" : "max-h-0 opacity-0 scale-y-95"}`}
>
<div className="p-4 grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Branch:
</span>
<span className="font-semibold">{row.branch}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Days in Status:
</span>
<span className="font-semibold">
{row.daysSinceStatusChange} days
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Created:
</span>
<span className="font-semibold">
{row.createdAt
? new Date(row.createdAt).toLocaleString(
undefined,
{ dateStyle: "medium", timeStyle: "short" },
)
: ""}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Updated:
</span>
<span className="font-semibold">
{row.updatedAt
? new Date(row.updatedAt).toLocaleString(
undefined,
{ dateStyle: "medium", timeStyle: "short" },
)
: ""}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Reviewer:
</span>
<span className="font-semibold">
{row.assignedReviewer}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 mr-1">
Tester:
</span>
<span className="font-semibold">
{row.assignedTester}
</span>
</div>
</div>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
</Table>
</div>
);
}
const getStatusColor = (status: string) => {
switch (status) {
case "in_review":
return "bg-blue-100 text-blue-700";
case "approved":
return "bg-green-100 text-green-700";
case "needs_revision":
return "bg-yellow-100 text-yellow-700";
case "merged":
return "bg-purple-100 text-purple-700";
default:
return "bg-gray-100 text-gray-700";
}
};
function StatusBadge({ status }: { status: string }) {
return (
<Badge
variant="outline"
className={`px-2 py-1 rounded-full text-xs font-medium text-center ${getStatusColor(status)}`}
>
{status?.split("_").join(" ")}
</Badge>
);
}
@@ -0,0 +1,468 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DataTable } from "@/components/data-table";
import { DataChart } from "@/components/data-chart";
import { Button } from "@/components/ui/button";
import { BarChart3, Table2, Filter } from "lucide-react";
import { getPRDataService } from "@/app/Services/service";
import { PRData } from "@/app/Interfaces/interface";
import { useSharedContext } from "@/lib/shared-context";
import { useCopilotAction, useCopilotReadable } from "@copilotkit/react-core";
import { PieChart, Pie, Cell, Tooltip } from "recharts";
import { PRPieData } from "./pr-pie-all-data";
import { PRReviewBarData } from "./pr-review-bar-data";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { PRPieFilterData } from "./pr-pie-filter-data";
import { PRLineChartData } from "./pr-line-chart-data";
import { Loader } from "./ui/loader";
import { useCopilotChatSuggestions } from "@copilotkit/react-ui";
import { devSuggestions } from "@/lib/prompts";
// Sample data for the developer dashboard
const tableColumns = [
{
accessorKey: "id",
header: "ID",
},
{
accessorKey: "title",
header: "TITLE",
},
{
accessorKey: "author",
header: "AUTHOR",
},
{
accessorKey: "repository",
header: "REPOSITORY",
},
{
accessorKey: "status",
header: "STATUS",
},
];
export function DeveloperDashboard() {
const { prData, setPrData } = useSharedContext();
const [filteredData, setFilteredData] = useState<PRData[]>([]);
const [filterParams, setFilterParams] = useState<{
status: string;
author: string;
}>({ status: "a", author: "b" });
const [viewMode, setViewMode] = useState<"table" | "chart">("table");
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
getPRData();
}, []);
useCopilotReadable({
description: "A list of all the PR Data",
value: JSON.stringify(prData),
});
useCopilotReadable({
description: "The currently logged in username and userId",
value: JSON.stringify({ username: "Jon.Snow@got.com", userId: 1 }),
});
useCopilotAction({
name: "renderData_PieChart",
description: `Render a Pie-chart for labelled numeric data. Example input format: [{"name": "approved", "value": 25, "shortName": "Approved", "color": "rgb(134 239 172)"}, {"name": "in_review", "value": 15, "shortName": "In Review", "color": "rgb(216 180 254)"}, {"name": "needs_revision", "value": 10, "shortName": "Needs Revision", "color": "rgb(253 224 71)"}, {"name": "merged", "value": 5, "shortName": "Merged", "color": "rgb(147 197 253)"}] When assigning color, use the same colors if data is related to status otherwise generate random colors. Provide short name for the item in the input if the name is long. Keep it the same as the name if the name is short. For example, If the name is Jon.snow@got.com, then the short name is Jon`,
parameters: [
{
name: "items",
type: "object[]",
description: "Array of items to be displayed in the pie chart",
required: true,
items: {
type: "object",
attributes: [
{
name: "name",
type: "string",
description: "Name of the item",
required: true,
},
{
name: "shortName",
type: "string",
description: "Short Name of the item",
required: true,
},
{
name: "value",
type: "number",
description: "Value of the item",
required: true,
},
{
name: "color",
type: "string",
description: "Color of the item",
required: true,
},
],
},
},
],
render: ({ args }: any) => {
useEffect(() => {
console.log(args, "args");
}, [args]);
return <PRPieData args={args} />;
},
});
useCopilotAction({
name: "renderData_BarChart",
description: `Render a Bar-chart for labelled numeric data. Example input format: [{"name": "approved", "value": 25, "color": "rgb(134 239 172)"}, {"name": "in_review", "value": 15, "color": "rgb(216 180 254)"}, {"name": "needs_revision", "value": 10, "color": "rgb(253 224 71)"}, {"name": "merged", "value": 5, "color": "rgb(147 197 253)"}] When assigning color, use the same colors if data is related to status otherwise generate random colors. Provide short name for the item in the input if the name is long. Keep it the same as the name if the name is short. For example, If the name is Jon.snow@got.com, then the short name is Jon`,
parameters: [
{
name: "items",
type: "object[]",
description: "Array of items to be displayed in the bar chart",
required: true,
items: {
type: "object",
attributes: [
{
name: "name",
type: "string",
description: "Name of the item",
required: true,
},
{
name: "value",
type: "number",
description: "Value of the item",
required: true,
},
],
},
},
],
render: ({ args }: any) => {
return <PRReviewBarData args={args} />;
},
});
useCopilotAction({
name: "renderData_LineChart",
description: `Render a Line-chart based on the PR data which shows the trend of PR creation over time. Example input format: [[{"name": "12/25", "value": 10,accessorKey : "Jon"}, {"name": "7/22", "value": 20,accessorKey : "Jon"}, {"name": "12/18", "value": 30,accessorKey : "Jon"}],[{"name": "12/25", "value": 10,accessorKey : "Ned"}, {"name": "7/22", "value": 20,accessorKey : "Ned"}, {"name": "12/18", "value": 30,accessorKey : "Ned"}]]. If dates are present convert them to the format "MM/DD". Also if name length is long, provide short name for the item in the input. For example, If the name is Jon.snow@got.com, then the short name is Jon`,
parameters: [
{
name: "items",
type: "object[]",
description: "The data to be displayed in the line chart",
required: true,
items: {
type: "object",
attributes: [
{
name: "name",
type: "string",
description: "The name of the item",
required: true,
},
{
name: "value",
type: "number",
description: "The value of the item",
required: true,
},
{
name: "accessorKey",
type: "string",
description:
"The accessor key of the item. It can be the author name or the repository name or the branch name or the status name or the date just value.",
required: true,
},
],
},
},
],
render: ({ args }: any) => {
console.log(args, "args");
// return <div>Hello</div>
return <PRLineChartData args={args} />;
},
});
useCopilotAction({
name: "renderData_Table",
description: `Render a table based on the PR data. Example input format: {id: 'PR22',title: 'Add Longclaw sword animation effects',status: 'needs_revision',assignedReviewer: 'lisa.martin@got.com',assignedTester: 'sarah.wilson@got.com',daysSinceStatusChange: 2,createdAt: '2025-04-22T18:41:32.868Z',updatedAt: '2025-05-18T04:36:14.176Z',userId: 1,author: 'Jon.snow@got.com',repository: 'frontend',branch: 'feature/longclaw-animations'}`,
parameters: [
{
name: "items",
type: "object[]",
description:
"The data to be displayed in the table. It should be an array of objects",
required: true,
attributes: [
{
name: "id",
type: "string",
description: "The id of the PR",
required: true,
},
{
name: "title",
type: "string",
description: "The title of the PR",
required: true,
},
{
name: "status",
type: "string",
description: "The status of the PR",
required: true,
},
{
name: "assignedReviewer",
type: "string",
description: "The assigned reviewer of the PR",
required: true,
},
{
name: "assignedTester",
type: "string",
description: "The assigned tester of the PR",
required: true,
},
{
name: "daysSinceStatusChange",
type: "number",
description:
"The number of days since the status of the PR was changed",
required: true,
},
{
name: "createdAt",
type: "string",
description: "The date and time when the PR was created",
required: true,
},
{
name: "updatedAt",
type: "string",
description: "The date and time when the PR was last updated",
required: true,
},
{
name: "userId",
type: "number",
description: "The id of the user who created the PR",
required: true,
},
{
name: "author",
type: "string",
description: "The author of the PR",
required: true,
},
{
name: "repository",
type: "string",
description: "The repository of the PR",
required: true,
},
{
name: "branch",
type: "string",
description: "The branch of the PR",
required: true,
},
],
},
],
render: ({ args, status }) => {
useEffect(() => {
if (args?.items) {
setFilteredData(args.items);
}
}, [args.items]);
if (status === "inProgress") {
return "...";
}
return <></>;
},
handler: (items: any) => {
setFilteredData(items?.items);
},
});
async function getPRData() {
try {
const res = await getPRDataService();
setPrData(res);
setFilteredData(res);
setIsLoading(false);
} catch (error) {
console.log(error);
}
}
return (
<div className="space-y-6">
{isLoading && <Loader />}
<div className="flex items-center justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
Developer Dashboard
</h1>
<div className="flex items-center gap-2">
<Button
variant={viewMode === "table" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("table")}
>
<Table2 className="mr-2 h-4 w-4" />
Table
</Button>
<Button
variant={viewMode === "chart" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("chart")}
>
<BarChart3 className="mr-2 h-4 w-4" />
Chart
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Repository Performance</CardTitle>
<CardDescription>
Monitor build times and test coverage across repositories
</CardDescription>
<div className="flex flex-wrap gap-4 mt-4 items-center">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Filters:</span>
</div>
<Select
value={filterParams.status}
onValueChange={(e) => {
setFilterParams({ ...filterParams, status: e });
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a">All Statuses</SelectItem>
<SelectItem value="approved">approved</SelectItem>
<SelectItem value="needs revision">needs revision</SelectItem>
<SelectItem value="merged">merged</SelectItem>
<SelectItem value="in review">in review</SelectItem>
</SelectContent>
</Select>
<Select
value={filterParams.author}
onValueChange={(e) => {
setFilterParams({ ...filterParams, author: e });
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Author" />
</SelectTrigger>
<SelectContent>
<SelectItem value="b">All Authors</SelectItem>
<SelectItem value="Jon.snow@got.com">
Jon.snow@got.com
</SelectItem>
<SelectItem value="robert.baratheon@got.com">
robert.baratheon@got.com
</SelectItem>
<SelectItem value="ned.stark@got.com">
ned.stark@got.com
</SelectItem>
<SelectItem value="cersei.lannister@got.com">
cersei.lannister@got.com
</SelectItem>
</SelectContent>
</Select>
<Button
onClick={() => {
if (
filterParams.status === "a" &&
filterParams.author === "b"
) {
setFilteredData(
prData.filter((pr: PRData) => pr.status !== "running"),
);
} else if (filterParams.status === "a") {
setFilteredData(
prData.filter(
(pr: PRData) =>
pr.author.toLowerCase() ===
filterParams.author?.toLowerCase() &&
pr.status !== "running",
),
);
} else if (filterParams.author === "b") {
setFilteredData(
prData.filter(
(pr: PRData) =>
pr.status.split("_").join(" ").toLowerCase() ===
filterParams.status?.toLowerCase() &&
pr.status !== "running",
),
);
} else {
setFilteredData(
prData.filter(
(pr: PRData) =>
pr.status.split("_").join(" ").toLowerCase() ===
filterParams.status?.toLowerCase() &&
pr.author.toLowerCase() ===
filterParams.author?.toLowerCase() &&
pr.status !== "running",
),
);
}
}}
variant="ghost"
size="sm"
>
Apply Filters
</Button>
<Button
onClick={() => {
setFilteredData(
prData.filter((pr: PRData) => pr.status !== "running"),
);
setFilterParams({ status: "a", author: "b" });
}}
variant="ghost"
size="sm"
>
Clear Filters
</Button>
</div>
</CardHeader>
<CardContent>
{viewMode === "table" ? (
<DataTable columns={tableColumns} data={filteredData} />
) : (
<DataChart data={filteredData} />
)}
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,38 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,72 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function PlaceholderDashboard({ title }: { title: string }) {
return (
<div className="space-y-6">
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
<div className="grid gap-6 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle>Metric One</CardTitle>
<CardDescription>Sample description</CardDescription>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">95.2%</div>
<p className="text-xs text-muted-foreground">
+2.1% from last period
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle>Metric Two</CardTitle>
<CardDescription>Sample description</CardDescription>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">$12.4K</div>
<p className="text-xs text-muted-foreground">
+5.2% from last period
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle>Metric Three</CardTitle>
<CardDescription>Sample description</CardDescription>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">842</div>
<p className="text-xs text-muted-foreground">
-1.8% from last period
</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Placeholder Content</CardTitle>
<CardDescription>
This is a placeholder dashboard. Real content will be implemented
later.
</CardDescription>
</CardHeader>
<CardContent className="h-80 flex items-center justify-center">
<div className="text-center text-muted-foreground">
<p className="text-lg">Content coming soon</p>
<p className="text-sm">
This section will contain relevant data visualizations and tables
</p>
</div>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,134 @@
import { useEffect, useState } from "react";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useSharedContext } from "@/lib/shared-context";
import { PRData, WeeklyCount } from "@/app/Interfaces/interface";
export function PRLineChartData({ args }: any) {
const { prData } = useSharedContext();
const [lineData, setLineData] = useState<WeeklyCount[] | any>([]);
const [xarr, setXarr] = useState<string[]>([]);
useEffect(() => {
console.log(args);
if (args?.items) {
const allKeys = new Set();
args?.items
?.flat()
?.forEach(({ accessorKey }: any) => allKeys.add(accessorKey));
const merged: any = {};
args?.items?.flat()?.forEach(({ name, value, accessorKey }: any) => {
if (!merged[name]) {
merged[name] = { name };
allKeys.forEach((key: any) => {
merged[name][key] = 0; // initialize all keys with 0
});
}
merged[name][accessorKey] = value;
});
const result = Object.values(merged);
console.log(result, "result");
console.log(
args?.items?.map((item: any) => item[0]?.accessorKey),
"xarr",
);
setLineData(result);
setXarr(args?.items?.map((item: any) => item[0]?.accessorKey));
}
}, [args?.items]);
function groupPRsByWeek(prs: PRData[]): WeeklyCount[] {
const weekMap: Record<string, number> = {};
prs.forEach((pr) => {
const date = new Date(pr.createdAt);
const day = date.getUTCDay(); // 0 (Sun) to 6 (Sat)
const diffToMonday = (day + 6) % 7; // get difference to previous Monday
const monday = new Date(date);
monday.setUTCDate(date.getUTCDate() - diffToMonday);
monday.setUTCHours(0, 0, 0, 0); // normalize to midnight
const mondayStr = monday.toISOString().split("T")[0];
weekMap[mondayStr] = (weekMap[mondayStr] || 0) + 1;
});
return Object.entries(weekMap)
.map(([week, count]) => ({ week, count }))
.sort((a, b) => a.week.localeCompare(b.week));
}
const chartColors = [
"hsl(12, 76%, 61%)",
"hsl(173, 58%, 39%)",
"hsl(43, 74%, 66%)",
"hsl(27, 87%, 67%)",
"hsl(12, 76%, 61%)",
"hsl(173, 58%, 39%)",
"hsl(43, 74%, 66%)",
"hsl(27, 87%, 67%)",
"hsl(12, 76%, 61%)",
"hsl(173, 58%, 39%)",
"hsl(43, 74%, 66%)",
"hsl(27, 87%, 67%)",
"hsl(12, 76%, 61%)",
"hsl(173, 58%, 39%)",
"hsl(43, 74%, 66%)",
"hsl(27, 87%, 67%)",
"hsl(197, 37%, 24%)",
];
return (
<div className="p-4 rounded-2xl shadow-lg flex flex-col items-center w-full min-w-[250px] max-w-full">
<h2 className="text-xl font-semibold mb-2 text-gray-700 text-center">
Trend Distribution
</h2>
<div className="h-[200px] w-full flex items-center justify-center">
<LineChart width={520} height={220} data={lineData}>
<CartesianGrid strokeDasharray="4 4" stroke="#B6C7DB" />
<XAxis dataKey="name" stroke="#4F5A66" />
<YAxis stroke="#4F5A66" />
{/* <Tooltip content={<CustomPieTooltip />} /> */}
<Legend
verticalAlign="bottom"
// height={36}
width={225}
align="center"
wrapperStyle={{ color: "black", fontSize: "12px", paddingLeft: 10 }}
/>
{xarr.map((item: any, index: number) => {
return (
<Line
type="monotone"
dataKey={item}
stroke={chartColors[index]}
strokeWidth={3}
dot={{ r: 5 }}
/>
);
})}
{/* <Line type="monotone" dataKey="merged" stroke="#475569" strokeWidth={3} dot={{ r: 5 }} /> */}
{/* <Line type="monotone" dataKey="closed" stroke="#B6C7DB" strokeWidth={3} dot={{ r: 5 }} /> */}
</LineChart>
</div>
</div>
);
}
const CustomPieTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const { week, count } = payload[0].payload;
return (
<div className="bg-white p-2 rounded shadow text-black">
{`${week} - ${count}`}
</div>
);
}
return null;
};
@@ -0,0 +1,100 @@
import { Cell } from "recharts";
import { useEffect, useState } from "react";
import { Pie, PieChart, Tooltip } from "recharts";
interface PieDataItem {
name: string;
value: number;
color: string;
shortName: string;
}
interface PieDataProps {
args: {
items: PieDataItem[];
title?: string;
};
}
export function PRPieData({ args }: PieDataProps) {
const [chartData, setChartData] = useState<PieDataItem[]>([]);
useEffect(() => {
console.log(JSON.stringify(args), "argsarhs");
if (args?.items) {
setChartData(args?.items);
}
}, [args?.items]);
return (
<div className="flex-1 p-4 rounded-2xl shadow-lg flex flex-col items-center min-w-[250px] max-w-[350px]">
<h2 className="text-xl font-semibold mb-2 text-gray-700 text-center">
{args.title || "Data Distribution"}
</h2>
<div className="h-[180px] flex flex-col items-center justify-center">
<PieChart width={260} height={180}>
<Pie
data={chartData}
cx={130}
cy={90}
innerRadius={30}
outerRadius={70}
paddingAngle={0}
dataKey="value"
labelLine={false}
label={({ value }) => value}
>
{chartData.map((entry, index: number) => (
<Cell key={`cell-${index}`} fill={chartData[index].color} />
))}
</Pie>
<Tooltip content={<CustomPieTooltip />} />
</PieChart>
</div>
<div className="flex flex-col items-center mt-4">
{chunkArray(chartData, 2).map((row, rowIdx) => (
<div
key={rowIdx}
className="flex flex-row justify-center items-center gap-x-6 gap-y-2 w-full"
>
{row.map((entry: PieDataItem) => (
<div
key={entry.name}
className="flex items-center gap-1 min-w-[110px]"
>
<span
style={{ backgroundColor: entry.color }}
className={`inline-block w-4 h-4 rounded-full`}
/>
<span style={{ width: "94px" }} className="text-sm text-black">
{entry?.shortName}
</span>
</div>
))}
</div>
))}
</div>
</div>
);
}
export const CustomPieTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const { name, value } = payload[0].payload;
return (
<div className="bg-white p-2 rounded shadow text-black">
<div>{name.split("_").join(" ")}</div>
<div>Value: {value}</div>
</div>
);
}
return null;
};
export function chunkArray<T>(array: T[], size: number): T[][] {
const result = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
@@ -0,0 +1,117 @@
import { Cell } from "recharts";
import { PRData } from "@/app/Interfaces/interface";
import { useSharedContext } from "@/lib/shared-context";
import { useEffect, useState } from "react";
import { Pie, PieChart, Tooltip } from "recharts";
import { CustomPieTooltip } from "./pr-pie-all-data";
import { chunkArray } from "./pr-pie-all-data";
export function PRPieFilterData({ args }: any) {
const [userPRData, setUserPRData] = useState<any[]>([]);
const { prData } = useSharedContext();
const status = [
{
name: "approved",
color: "bg-green-300",
value: "rgb(134 239 172)",
},
{
name: "needs_revision",
color: "bg-yellow-300",
value: "rgb(253 224 71)",
},
{
name: "merged",
color: "bg-purple-300",
value: "rgb(216 180 254)",
},
{
name: "in_review",
color: "bg-blue-300",
value: "rgb(147 197 253)",
},
];
useEffect(() => {
const now = new Date();
const pieData = Object.entries(
getStatusCounts(
prData.filter((pr: PRData) => {
if (args?.userId) {
if (pr.userId !== args?.userId) return false;
}
if (!pr.createdAt) return false;
const createdDate = new Date(pr.createdAt);
const diffDays =
(now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24);
return diffDays <= args.dayCount;
}),
),
).map(([status, count]) => ({
name: status,
value: count,
}));
console.log(pieData);
setUserPRData(pieData);
}, [args]);
const getStatusCounts = (data: PRData[]) => {
return data.reduce((acc: any, pr: PRData) => {
acc[pr.status] = (acc[pr.status] || 0) + 1;
return acc;
}, {});
};
return (
<div className="flex-1 p-4 rounded-2xl shadow-lg flex flex-col items-center min-w-[250px] max-w-[350px]">
<h2 className="text-xl font-semibold mb-2 text-gray-700 text-center">
PR Status Distribution
</h2>
{/* <h2 className="text-xl font-semibold mb-2 text-gray-700 text-center">DANDTIME</h2> */}
<div className="h-[180px] flex flex-col items-center justify-center">
<PieChart width={260} height={180}>
<Pie
data={userPRData}
cx={130}
cy={90}
innerRadius={30}
outerRadius={70}
paddingAngle={0}
dataKey="value"
labelLine={false}
label={({ value }) => value}
>
{userPRData.map((entry, index: number) => (
<Cell
key={`cell-${index}`}
fill={status.find((s: any) => s.name === entry.name)?.value}
/>
))}
</Pie>
<Tooltip content={<CustomPieTooltip />} />
{/* <Tooltip contentStyle={{ background: '', border: 'none', color: 'white' }} /> */}
</PieChart>
</div>
<div className="flex flex-col items-center mt-4">
{chunkArray(status, 2).map((row, rowIdx) => (
<div
key={rowIdx}
className="flex flex-row justify-center items-center gap-x-6 gap-y-2 w-full"
>
{row.map((entry: any) => (
<div
key={entry.name}
className="flex items-center gap-1 min-w-[110px]"
>
<span
className={`inline-block w-4 h-4 rounded-full ${entry.color}`}
// style={{ backgroundColor: entry.color }}
/>
<span className="text-sm text-black">
{entry.name.split("_").join(" ")}
</span>
</div>
))}
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,74 @@
import { PRData } from "@/app/Interfaces/interface";
import { useSharedContext } from "@/lib/shared-context";
import { useEffect, useState } from "react";
import { Legend } from "recharts";
import { CartesianGrid, Tooltip, XAxis, YAxis } from "recharts";
import { Bar } from "recharts";
import { BarChart } from "recharts";
interface BarChartData {
name: string;
value: number;
}
export function PRReviewBarData({ args }: any) {
const [data, setData] = useState<BarChartData[]>([]);
const chartColors = [
"hsl(12, 76%, 61%)",
"hsl(173, 58%, 39%)",
"hsl(197, 37%, 24%)",
"hsl(43, 74%, 66%)",
"hsl(27, 87%, 67%)",
];
useEffect(() => {
console.log(args);
if (args?.items) {
setData(args?.items);
}
}, [args?.items]);
function getUniqueReviewers(prArray: PRData[]): string[] {
const reviewerSet = new Set<string>();
for (const pr of prArray) {
if (pr.assignedReviewer) {
reviewerSet.add(pr.assignedReviewer.toLowerCase()); // normalize casing if needed
}
}
return Array.from(reviewerSet);
}
return (
<>
{/* Bar Chart Section */}
<div className="flex-1 p-4 rounded-2xl shadow-lg flex flex-col items-center min-w-[250px] max-w-[350px]">
<h2 className="text-xl font-semibold mb-2 text-gray-700 text-center">
Data Distribution
</h2>
<div className="h-[180px] flex items-center justify-center">
<BarChart width={260} height={180} data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#94a3b855" />
<XAxis
dataKey="name"
stroke="#cbd5e1"
className="text-black"
tickFormatter={(value: string) => value[0]?.toUpperCase()}
/>
<YAxis stroke="#cbd5e1" />
<Tooltip
contentStyle={{
background: "#1f2937",
border: "none",
color: "white",
}}
/>
<Legend wrapperStyle={{ color: "white" }} />
<Bar dataKey="value" fill={chartColors[3]} />
{/* <Bar dataKey="merged" fill="#475569" /> */}
{/* <Bar dataKey="closed" fill="#cbd5e1" /> */}
</BarChart>
</div>
</div>
</>
);
}
@@ -0,0 +1,235 @@
"use client";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DataTable } from "@/components/data-table-results";
import { DataTable as DataTableTests } from "@/components/data-table-tests";
import { DataChart } from "@/components/data-chart";
import { Button } from "@/components/ui/button";
import { BarChart3, Table2, Code2 } from "lucide-react";
import { getTestsService } from "@/app/Services/service";
import Loader from "./ui/loader";
import { testData } from "@/lib/testData";
import { DataCode } from "./data-code";
import { useCoAgentStateRender } from "@copilotkit/react-core";
import { ChatGrid } from "./data-chat-grid";
import { useCopilotChatSuggestions } from "@copilotkit/react-ui";
import { testerPersonaSuggestions } from "@/lib/prompts";
import { useSharedTestsContext } from "@/lib/shared-tests-context";
import { TestsData } from "@/app/Interfaces/interface";
// Sample data for the tester dashboard
const tableColumns = [
{
accessorKey: "testId",
header: "Test ID",
},
{
accessorKey: "title",
header: "Test Name",
},
{
accessorKey: "prId",
header: "PR Ref",
},
{
accessorKey: "executedBy",
header: "Run By",
},
{
accessorKey: "status",
header: "Status",
},
];
const tableColumnsTests = [
{
accessorKey: "testId",
header: "Test ID",
},
{
accessorKey: "title",
header: "Test Name",
},
{
accessorKey: "prId",
header: "PR Ref",
},
{
accessorKey: "executedBy",
header: "Assigned To",
},
{
accessorKey: "status",
header: "Action",
},
];
const chartData = [
{
name: "Mon",
Passed: 42,
Failed: 8,
Skipped: 5,
},
{
name: "Tue",
Passed: 45,
Failed: 5,
Skipped: 3,
},
{
name: "Wed",
Passed: 48,
Failed: 7,
Skipped: 4,
},
{
name: "Thu",
Passed: 51,
Failed: 4,
Skipped: 2,
},
{
name: "Fri",
Passed: 47,
Failed: 6,
Skipped: 3,
},
{
name: "Sat",
Passed: 44,
Failed: 3,
Skipped: 2,
},
{
name: "Sun",
Passed: 50,
Failed: 2,
Skipped: 1,
},
];
export function TesterDashboard() {
const [viewMode, setViewMode] = useState<"results" | "tests" | "code">(
"results",
);
const { testsData, setTestsData } = useSharedTestsContext();
const [testSuites, setTestSuites] = useState<any>([]);
const [loading, setLoading] = useState(true);
const [testCaseStatus, setTestCaseStatus] = useState<{
[rowIndex: number]: string[];
}>({});
useCopilotChatSuggestions({
available: "enabled",
instructions: testerPersonaSuggestions,
minSuggestions: 2,
maxSuggestions: 4,
});
useCoAgentStateRender({
name: "testing_agent",
render: (props) => {
return (
<ChatGrid
status={props.status}
state={props.state}
testSuite={testSuites}
setTestSuite={setTestSuites}
testCaseStatus={testCaseStatus}
setTestCaseStatus={setTestCaseStatus}
/>
);
},
});
useEffect(() => {
getTests();
}, []);
async function getTests() {
// const tests = await getTestsService()
const tests = testData;
// console.log(tests)
setTestsData(tests as TestsData[]);
setLoading(false);
}
return (
<div className="space-y-6">
{loading && <Loader />}
<div className="flex items-center justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
Tester Dashboard
</h1>
<div className="flex items-center gap-2">
<Button
variant={viewMode === "code" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("code")}
>
<Code2 className=" h-4 w-4" />
Code
</Button>
<Button
variant={viewMode === "results" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("results")}
>
<Table2 className="mr-2 h-4 w-4" />
Results
</Button>
<Button
variant={viewMode === "tests" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("tests")}
>
<Table2 className="mr-2 h-4 w-4" />
Tests
</Button>
{/* <Button variant={viewMode === "chart" ? "default" : "outline"} size="sm" onClick={() => setViewMode("chart")}>
<BarChart3 className="mr-2 h-4 w-4" />
Chart
</Button> */}
</div>
</div>
<Card>
<CardHeader>
{viewMode != "code" && (
<>
<CardTitle>
{viewMode === "results" ? "Test Results" : "Testing Grounds"}
</CardTitle>
<CardDescription>
{viewMode === "results"
? "Monitor test results and performance metrics"
: "Perform testing on the latest PRs"}
</CardDescription>
</>
)}
</CardHeader>
<CardContent>
{viewMode === "results" ? (
<DataTable columns={tableColumns} data={testsData} />
) : viewMode === "tests" ? (
<DataTableTests
columns={tableColumnsTests}
data={testSuites}
onToggle={setTestSuites}
setTestsData={setTestsData}
testsData={testsData}
/>
) : (
<DataCode />
)}
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
@@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,141 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className,
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
@@ -0,0 +1,59 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
@@ -0,0 +1,7 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };
@@ -0,0 +1,50 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

Some files were not shown because too many files have changed in this diff Show More