chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
+10616
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 |
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
);
|
||||
}
|
||||
+143
@@ -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
|
||||
|
||||

|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
+14069
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;
|
||||
+19293
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"
|
||||
}
|
||||
}
|
||||
+11631
File diff suppressed because it is too large
Load Diff
+171
@@ -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);
|
||||
};
|
||||
+6
@@ -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 |
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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>
|
||||
);
|
||||
}
|
||||
+54
@@ -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 CopilotKit’s 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",
|
||||
},
|
||||
];
|
||||
+8
@@ -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>
|
||||
);
|
||||
}
|
||||
+139
@@ -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>
|
||||
);
|
||||
}
|
||||
+9
@@ -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>
|
||||
);
|
||||
}
|
||||
+186
@@ -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>
|
||||
);
|
||||
}
|
||||
+468
@@ -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>
|
||||
);
|
||||
}
|
||||
+72
@@ -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>
|
||||
);
|
||||
}
|
||||
+134
@@ -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;
|
||||
}
|
||||
+117
@@ -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>
|
||||
);
|
||||
}
|
||||
+74
@@ -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
Reference in New Issue
Block a user