Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions typescript-sdk/apps/dojo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "npm run generate-content-json && next dev",
"build": "next build",
"start": "next start",
"start": "npm run generate-content-json && 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"
}
}
228 changes: 228 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,228 @@
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
}

(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import Editor from "@monaco-editor/react";
import { useTheme } from "next-themes";
import { FeatureFile } from "@/types/feature";
interface CodeEditorProps {
file?: FeatureFile;
onFileChange?: (fileName: string, content: string) => void;
}

export function CodeEditor({ file, onFileChange }: CodeEditorProps) {
const handleEditorChange = (value: string | undefined) => {
if (value && onFileChange) {
onFileChange(file!.name, value);
}
};

const theme = useTheme();

return file ? (
<div className="h-full flex flex-col">
<Editor
height="100%"
language={file.language}
value={file.content}
onChange={handleEditorChange}
options={{
minimap: { enabled: false },
fontSize: 16,
lineNumbers: "on",
readOnly: true,
wordWrap: "on",
stickyScroll: {
enabled: false,
},
}}
theme="vs-dark"
/>
</div>
) : (
<div className="p-6 text-center text-muted-foreground">
Select a file from the file tree to view its code
</div>
);
}
Loading