Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion typescript-sdk/apps/dojo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"mastra:dev": "mastra dev"
"mastra:dev": "mastra dev",
"generate-content-json": "tsx scripts/generate-content-json.ts"
},
"dependencies": {
"@ag-ui/agno": "workspace:*",
Expand Down Expand Up @@ -85,6 +86,7 @@
"eslint": "^9",
"eslint-config-next": "15.2.1",
"tailwindcss": "^4",
"tsx": "^4.7.0",
"typescript": "^5"
}
}
274 changes: 274 additions & 0 deletions typescript-sdk/apps/dojo/scripts/generate-content-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import fs from "fs";
import path from "path";

// Function to parse agents.ts file and extract agent keys without executing
function parseAgentsFile(): Array<{id: string, agentKeys: string[]}> {
const agentsFilePath = path.join(__dirname, '../src/agents.ts');
const agentsContent = fs.readFileSync(agentsFilePath, 'utf8');

const agentConfigs: Array<{id: string, agentKeys: string[]}> = [];

// Split the content to process each agent configuration individually
const agentBlocks = agentsContent.split(/(?=\s*{\s*id:\s*["'])/);

for (const block of agentBlocks) {
// Extract the ID
const idMatch = block.match(/id:\s*["']([^"']+)["']/);
if (!idMatch) continue;

const id = idMatch[1];

// Find the return object by looking for the pattern and then manually parsing balanced braces
const returnMatch = block.match(/agents:\s*async\s*\(\)\s*=>\s*{\s*return\s*{/);
if (!returnMatch) continue;

const startIndex = returnMatch.index! + returnMatch[0].length;
const returnObjectContent = extractBalancedBraces(block, startIndex);


// Extract keys from the return object - only capture keys that are followed by a colon and then 'new'
// This ensures we only get the top-level keys like "agentic_chat: new ..." not nested keys like "url: ..."
const keyRegex = /^\s*(\w+):\s*new\s+\w+/gm;
const keys: string[] = [];
let keyMatch;
while ((keyMatch = keyRegex.exec(returnObjectContent)) !== null) {
keys.push(keyMatch[1]);
}

agentConfigs.push({ id, agentKeys: keys });
}

return agentConfigs;
}

// Helper function to extract content between balanced braces
function extractBalancedBraces(text: string, startIndex: number): string {
let braceCount = 0;
let i = startIndex;

while (i < text.length) {
if (text[i] === '{') {
braceCount++;
} else if (text[i] === '}') {
if (braceCount === 0) {
// Found the closing brace for the return object
return text.substring(startIndex, i);
}
braceCount--;
}
i++;
}

return '';
}

const agentConfigs = parseAgentsFile();

const featureFiles = ["page.tsx", "style.css", "README.mdx"]

async function getFile(_filePath: string | undefined, _fileName?: string) {
if (!_filePath) {
console.warn(`File path is undefined, skipping.`);
return {}
}

const fileName = _fileName ?? _filePath.split('/').pop() ?? ''
const filePath = _fileName ? path.join(_filePath, fileName) : _filePath;

// Check if it's a remote URL
const isRemoteUrl = _filePath.startsWith('http://') || _filePath.startsWith('https://');

let content: string;

try {
if (isRemoteUrl) {
// Convert GitHub URLs to raw URLs for direct file access
let fetchUrl = _filePath;
if (_filePath.includes('github.com') && _filePath.includes('/blob/')) {
fetchUrl = _filePath.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/');
}

// Fetch remote file content
console.log(`Fetching remote file: ${fetchUrl}`);
const response = await fetch(fetchUrl);
if (!response.ok) {
console.warn(`Failed to fetch remote file: ${fetchUrl}, status: ${response.status}`);
return {}
}
content = await response.text();
} else {
// Handle local file
if (!fs.existsSync(filePath)) {
console.warn(`File not found: ${filePath}, skipping.`);
return {}
}
content = fs.readFileSync(filePath, "utf8");
}

const extension = fileName.split(".").pop();
let language = extension;
if (extension === "py") language = "python";
else if (extension === "css") language = "css";
else if (extension === "md" || extension === "mdx") language = "markdown";
else if (extension === "tsx") language = "typescript";
else if (extension === "js") language = "javascript";
else if (extension === "json") language = "json";
else if (extension === "yaml" || extension === "yml") language = "yaml";
else if (extension === "toml") language = "toml";

return {
name: fileName,
content,
language,
type: 'file'
}
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return {}
}
}

async function getFeatureFrontendFiles(featureId: string) {
const featurePath = path.join(__dirname, `../src/app/[integrationId]/feature/${featureId as string}`);
const retrievedFiles = []

for (const fileName of featureFiles) {
retrievedFiles.push(await getFile(featurePath, fileName))
}

return retrievedFiles;
}

const integrationsFolderPath = '../../../integrations'
const agentFilesMapper: Record<string, (agentKeys: string[]) => Record<string, string>> = {
'middleware-starter': () => ({
agentic_chat: path.join(__dirname, integrationsFolderPath, `/middleware-starter/src/index.ts`)
}),
'pydantic-ai': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: `https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/ag_ui/api/${agentId}.py`
}), {})
},
'server-starter': () => ({
agentic_chat: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/__init__.py`)
}),
'server-starter-all-features': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/${agentId}.py`)
}), {})
},
'mastra': () => ({
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
}),
'mastra-agent-lock': () => ({
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
}),
'vercel-ai-sdk': () => ({
agentic_chat: path.join(__dirname, integrationsFolderPath, `/vercel-ai-sdk/src/index.ts`)
}),
'langgraph': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/examples/agents/${agentId}/agent.py`)
}), {})
},
'langgraph-fastapi': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/python/ag_ui_langgraph/examples/agents/${agentId}.py`)
}), {})
},
'agno': () => ({}),
'llama-index': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: path.join(__dirname, integrationsFolderPath, `/llamaindex/server-py/server/routers/${agentId}.py`)
}), {})
},
'crewai': (agentKeys: string[]) => {
return agentKeys.reduce((acc, agentId) => ({
...acc,
[agentId]: path.join(__dirname, integrationsFolderPath, `/crewai/python/ag_ui_crewai/examples/${agentId}.py`)
}), {})
}
}

async function runGenerateContent() {
const result = {}
for (const agentConfig of agentConfigs) {
// Use the parsed agent keys instead of executing the agents function
const agentsPerFeatures = agentConfig.agentKeys

const agentFilePaths = agentFilesMapper[agentConfig.id](agentConfig.agentKeys)
// Per feature, assign all the frontend files like page.tsx as well as all agent files
for (const featureId of agentsPerFeatures) {
// @ts-expect-error -- redundant error about indexing of a new object.
result[`${agentConfig.id}::${featureId}`] = [
// Get all frontend files for the feature
...(await getFeatureFrontendFiles(featureId)),
// Get the agent (python/TS) file
await getFile(agentFilePaths[featureId])
]
}
}

return result
}

// const result = {};
// const agentDemoBaseDir = path.join(__dirname, "../agent/demo");
//
// for (const demoIdWithFramework in config) {
// const demoFilesConfig = config[demoIdWithFramework];
// const demoDirPath = path.join(agentDemoBaseDir, demoIdWithFramework);
//
// if (!fs.existsSync(demoDirPath)) {
// console.warn(`Directory not found for demo: ${demoIdWithFramework}, skipping.`);
// continue;
// }
//
// result[demoIdWithFramework] = { files: [] };
//
// for (const fileName of demoFilesConfig) {
// const filePath = path.join(demoDirPath, fileName);
// if (!fs.existsSync(filePath)) {
// console.warn(`File not found: ${filePath}, skipping.`);
// continue;
// }
//
// try {
// const content = fs.readFileSync(filePath, "utf8");
// const extension = fileName.split(".").pop();
// let language = extension;
// if (extension === "py") language = "python";
// else if (extension === "css") language = "css";
// else if (extension === "md" || extension === "mdx") language = "markdown";
// else if (extension === "tsx") language = "typescript";
// else if (extension === "js") language = "javascript";
// else if (extension === "json") language = "json";
// else if (extension === "yaml" || extension === "yml") language = "yaml";
// else if (extension === "toml") language = "toml";
//
// result[demoIdWithFramework].files.push({
// name: fileName,
// content,
// path: path.join(demoIdWithFramework, fileName), // Store relative path within agent/demo
// language,
// type: 'file'
// });
// } catch (error) {
// console.error(`Error reading file ${filePath}:`, error);
// }
// }
// }
(async () => {
const result = await runGenerateContent();
fs.writeFileSync(
path.join(__dirname, "../src/files.json"),
JSON.stringify(result, null, 2)
);

console.log("Successfully generated src/files.json");
})();
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
export default function FeatureLayout({ children }: { children: React.ReactNode }) {
return <div className="bg-(--copilot-kit-background-color) w-full h-full">{children}</div>;
'use client';

import React, { useMemo } from "react";
import { usePathname } from "next/navigation";
import filesJSON from '../../../files.json'
import Readme from "@/components/readme/readme";
import CodeViewer from "@/components/code-viewer/code-viewer";
import { useURLParams } from "@/contexts/url-params-context";

type FileItem = {
name: string;
content: string;
language: string;
type: string;
};

type FilesJsonType = Record<string, FileItem[]>;

interface Props {
params: Promise<{
integrationId: string;
}>;
children: React.ReactNode
}

export default function FeatureLayout({ children, params }: Props) {
const { integrationId } = React.use(params);
const pathname = usePathname();
const { view } = useURLParams();

// Extract featureId from pathname: /[integrationId]/feature/[featureId]
const pathParts = pathname.split('/');
const featureId = pathParts[pathParts.length - 1]; // Last segment is the featureId

const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`];

const readme = files.find(file => file.name.includes('.mdx'));
const codeFiles = files.filter(file => !file.name.includes('.mdx'));


const content = useMemo(() => {
switch (view) {
case "code":
return (
<CodeViewer codeFiles={codeFiles} />
)
case "readme":
return (
<Readme content={readme?.content ?? ''} />
)
default:
return (
<div className="h-full">{children}</div>
)
}
}, [children, codeFiles, readme, view])

return <div className="bg-(--copilot-kit-background-color) w-full h-full">{content}</div>;
}
8 changes: 7 additions & 1 deletion typescript-sdk/apps/dojo/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";
import { ThemeProvider } from "@/components/theme-provider";
import { MainLayout } from "@/components/layout/main-layout";
import { URLParamsProvider } from "@/contexts/url-params-context";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -34,7 +36,11 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<MainLayout>{children}</MainLayout>
<Suspense>
<URLParamsProvider>
<MainLayout>{children}</MainLayout>
</URLParamsProvider>
</Suspense>
</ThemeProvider>
</body>
</html>
Expand Down
Loading