Skip to content

Commit ac5aab2

Browse files
authored
Merge pull request #223 from ag-ui-protocol/chore/display-code-in-dojo
chore: display code in dojo
2 parents 45f3527 + 31653a4 commit ac5aab2

File tree

15 files changed

+1807
-425
lines changed

15 files changed

+1807
-425
lines changed

typescript-sdk/apps/dojo/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
6+
"dev": "npm run generate-content-json && next dev",
77
"build": "next build",
8-
"start": "next start",
8+
"start": "npm run generate-content-json && next start",
99
"lint": "next lint",
10-
"mastra:dev": "mastra dev"
10+
"mastra:dev": "mastra dev",
11+
"generate-content-json": "tsx scripts/generate-content-json.ts"
1112
},
1213
"dependencies": {
1314
"@ag-ui/agno": "workspace:*",
@@ -85,6 +86,7 @@
8586
"eslint": "^9",
8687
"eslint-config-next": "15.2.1",
8788
"tailwindcss": "^4",
89+
"tsx": "^4.7.0",
8890
"typescript": "^5"
8991
}
9092
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import fs from "fs";
2+
import path from "path";
3+
4+
// Function to parse agents.ts file and extract agent keys without executing
5+
function parseAgentsFile(): Array<{id: string, agentKeys: string[]}> {
6+
const agentsFilePath = path.join(__dirname, '../src/agents.ts');
7+
const agentsContent = fs.readFileSync(agentsFilePath, 'utf8');
8+
9+
const agentConfigs: Array<{id: string, agentKeys: string[]}> = [];
10+
11+
// Split the content to process each agent configuration individually
12+
const agentBlocks = agentsContent.split(/(?=\s*{\s*id:\s*["'])/);
13+
14+
for (const block of agentBlocks) {
15+
// Extract the ID
16+
const idMatch = block.match(/id:\s*["']([^"']+)["']/);
17+
if (!idMatch) continue;
18+
19+
const id = idMatch[1];
20+
21+
// Find the return object by looking for the pattern and then manually parsing balanced braces
22+
const returnMatch = block.match(/agents:\s*async\s*\(\)\s*=>\s*{\s*return\s*{/);
23+
if (!returnMatch) continue;
24+
25+
const startIndex = returnMatch.index! + returnMatch[0].length;
26+
const returnObjectContent = extractBalancedBraces(block, startIndex);
27+
28+
29+
// Extract keys from the return object - only capture keys that are followed by a colon and then 'new'
30+
// This ensures we only get the top-level keys like "agentic_chat: new ..." not nested keys like "url: ..."
31+
const keyRegex = /^\s*(\w+):\s*new\s+\w+/gm;
32+
const keys: string[] = [];
33+
let keyMatch;
34+
while ((keyMatch = keyRegex.exec(returnObjectContent)) !== null) {
35+
keys.push(keyMatch[1]);
36+
}
37+
38+
agentConfigs.push({ id, agentKeys: keys });
39+
}
40+
41+
return agentConfigs;
42+
}
43+
44+
// Helper function to extract content between balanced braces
45+
function extractBalancedBraces(text: string, startIndex: number): string {
46+
let braceCount = 0;
47+
let i = startIndex;
48+
49+
while (i < text.length) {
50+
if (text[i] === '{') {
51+
braceCount++;
52+
} else if (text[i] === '}') {
53+
if (braceCount === 0) {
54+
// Found the closing brace for the return object
55+
return text.substring(startIndex, i);
56+
}
57+
braceCount--;
58+
}
59+
i++;
60+
}
61+
62+
return '';
63+
}
64+
65+
const agentConfigs = parseAgentsFile();
66+
67+
const featureFiles = ["page.tsx", "style.css", "README.mdx"]
68+
69+
async function getFile(_filePath: string | undefined, _fileName?: string) {
70+
if (!_filePath) {
71+
console.warn(`File path is undefined, skipping.`);
72+
return {}
73+
}
74+
75+
const fileName = _fileName ?? _filePath.split('/').pop() ?? ''
76+
const filePath = _fileName ? path.join(_filePath, fileName) : _filePath;
77+
78+
// Check if it's a remote URL
79+
const isRemoteUrl = _filePath.startsWith('http://') || _filePath.startsWith('https://');
80+
81+
let content: string;
82+
83+
try {
84+
if (isRemoteUrl) {
85+
// Convert GitHub URLs to raw URLs for direct file access
86+
let fetchUrl = _filePath;
87+
if (_filePath.includes('github.com') && _filePath.includes('/blob/')) {
88+
fetchUrl = _filePath.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/');
89+
}
90+
91+
// Fetch remote file content
92+
console.log(`Fetching remote file: ${fetchUrl}`);
93+
const response = await fetch(fetchUrl);
94+
if (!response.ok) {
95+
console.warn(`Failed to fetch remote file: ${fetchUrl}, status: ${response.status}`);
96+
return {}
97+
}
98+
content = await response.text();
99+
} else {
100+
// Handle local file
101+
if (!fs.existsSync(filePath)) {
102+
console.warn(`File not found: ${filePath}, skipping.`);
103+
return {}
104+
}
105+
content = fs.readFileSync(filePath, "utf8");
106+
}
107+
108+
const extension = fileName.split(".").pop();
109+
let language = extension;
110+
if (extension === "py") language = "python";
111+
else if (extension === "css") language = "css";
112+
else if (extension === "md" || extension === "mdx") language = "markdown";
113+
else if (extension === "tsx") language = "typescript";
114+
else if (extension === "js") language = "javascript";
115+
else if (extension === "json") language = "json";
116+
else if (extension === "yaml" || extension === "yml") language = "yaml";
117+
else if (extension === "toml") language = "toml";
118+
119+
return {
120+
name: fileName,
121+
content,
122+
language,
123+
type: 'file'
124+
}
125+
} catch (error) {
126+
console.error(`Error reading file ${filePath}:`, error);
127+
return {}
128+
}
129+
}
130+
131+
async function getFeatureFrontendFiles(featureId: string) {
132+
const featurePath = path.join(__dirname, `../src/app/[integrationId]/feature/${featureId as string}`);
133+
const retrievedFiles = []
134+
135+
for (const fileName of featureFiles) {
136+
retrievedFiles.push(await getFile(featurePath, fileName))
137+
}
138+
139+
return retrievedFiles;
140+
}
141+
142+
const integrationsFolderPath = '../../../integrations'
143+
const agentFilesMapper: Record<string, (agentKeys: string[]) => Record<string, string>> = {
144+
'middleware-starter': () => ({
145+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/middleware-starter/src/index.ts`)
146+
}),
147+
'pydantic-ai': (agentKeys: string[]) => {
148+
return agentKeys.reduce((acc, agentId) => ({
149+
...acc,
150+
[agentId]: `https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/ag_ui/api/${agentId}.py`
151+
}), {})
152+
},
153+
'server-starter': () => ({
154+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/__init__.py`)
155+
}),
156+
'server-starter-all-features': (agentKeys: string[]) => {
157+
return agentKeys.reduce((acc, agentId) => ({
158+
...acc,
159+
[agentId]: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/${agentId}.py`)
160+
}), {})
161+
},
162+
'mastra': () => ({
163+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
164+
}),
165+
'mastra-agent-lock': () => ({
166+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
167+
}),
168+
'vercel-ai-sdk': () => ({
169+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/vercel-ai-sdk/src/index.ts`)
170+
}),
171+
'langgraph': (agentKeys: string[]) => {
172+
return agentKeys.reduce((acc, agentId) => ({
173+
...acc,
174+
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/examples/agents/${agentId}/agent.py`)
175+
}), {})
176+
},
177+
'langgraph-fastapi': (agentKeys: string[]) => {
178+
return agentKeys.reduce((acc, agentId) => ({
179+
...acc,
180+
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/python/ag_ui_langgraph/examples/agents/${agentId}.py`)
181+
}), {})
182+
},
183+
'agno': () => ({}),
184+
'llama-index': (agentKeys: string[]) => {
185+
return agentKeys.reduce((acc, agentId) => ({
186+
...acc,
187+
[agentId]: path.join(__dirname, integrationsFolderPath, `/llamaindex/server-py/server/routers/${agentId}.py`)
188+
}), {})
189+
},
190+
'crewai': (agentKeys: string[]) => {
191+
return agentKeys.reduce((acc, agentId) => ({
192+
...acc,
193+
[agentId]: path.join(__dirname, integrationsFolderPath, `/crewai/python/ag_ui_crewai/examples/${agentId}.py`)
194+
}), {})
195+
}
196+
}
197+
198+
async function runGenerateContent() {
199+
const result = {}
200+
for (const agentConfig of agentConfigs) {
201+
// Use the parsed agent keys instead of executing the agents function
202+
const agentsPerFeatures = agentConfig.agentKeys
203+
204+
const agentFilePaths = agentFilesMapper[agentConfig.id](agentConfig.agentKeys)
205+
// Per feature, assign all the frontend files like page.tsx as well as all agent files
206+
for (const featureId of agentsPerFeatures) {
207+
// @ts-expect-error -- redundant error about indexing of a new object.
208+
result[`${agentConfig.id}::${featureId}`] = [
209+
// Get all frontend files for the feature
210+
...(await getFeatureFrontendFiles(featureId)),
211+
// Get the agent (python/TS) file
212+
await getFile(agentFilePaths[featureId])
213+
]
214+
}
215+
}
216+
217+
return result
218+
}
219+
220+
(async () => {
221+
const result = await runGenerateContent();
222+
fs.writeFileSync(
223+
path.join(__dirname, "../src/files.json"),
224+
JSON.stringify(result, null, 2)
225+
);
226+
227+
console.log("Successfully generated src/files.json");
228+
})();
Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,59 @@
1-
export default function FeatureLayout({ children }: { children: React.ReactNode }) {
2-
return <div className="bg-(--copilot-kit-background-color) w-full h-full">{children}</div>;
1+
'use client';
2+
3+
import React, { useMemo } from "react";
4+
import { usePathname } from "next/navigation";
5+
import filesJSON from '../../../files.json'
6+
import Readme from "@/components/readme/readme";
7+
import CodeViewer from "@/components/code-viewer/code-viewer";
8+
import { useURLParams } from "@/contexts/url-params-context";
9+
10+
type FileItem = {
11+
name: string;
12+
content: string;
13+
language: string;
14+
type: string;
15+
};
16+
17+
type FilesJsonType = Record<string, FileItem[]>;
18+
19+
interface Props {
20+
params: Promise<{
21+
integrationId: string;
22+
}>;
23+
children: React.ReactNode
24+
}
25+
26+
export default function FeatureLayout({ children, params }: Props) {
27+
const { integrationId } = React.use(params);
28+
const pathname = usePathname();
29+
const { view } = useURLParams();
30+
31+
// Extract featureId from pathname: /[integrationId]/feature/[featureId]
32+
const pathParts = pathname.split('/');
33+
const featureId = pathParts[pathParts.length - 1]; // Last segment is the featureId
34+
35+
const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`];
36+
37+
const readme = files.find(file => file.name.includes('.mdx'));
38+
const codeFiles = files.filter(file => !file.name.includes('.mdx'));
39+
40+
41+
const content = useMemo(() => {
42+
switch (view) {
43+
case "code":
44+
return (
45+
<CodeViewer codeFiles={codeFiles} />
46+
)
47+
case "readme":
48+
return (
49+
<Readme content={readme?.content ?? ''} />
50+
)
51+
default:
52+
return (
53+
<div className="h-full">{children}</div>
54+
)
55+
}
56+
}, [children, codeFiles, readme, view])
57+
58+
return <div className="bg-(--copilot-kit-background-color) w-full h-full">{content}</div>;
359
}

typescript-sdk/apps/dojo/src/app/layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { Suspense } from "react";
12
import type { Metadata } from "next";
23
import { Geist, Geist_Mono } from "next/font/google";
34
import "./globals.css";
45
import "@copilotkit/react-ui/styles.css";
56
import { ThemeProvider } from "@/components/theme-provider";
67
import { MainLayout } from "@/components/layout/main-layout";
8+
import { URLParamsProvider } from "@/contexts/url-params-context";
79

810
const geistSans = Geist({
911
variable: "--font-geist-sans",
@@ -34,7 +36,11 @@ export default function RootLayout({
3436
enableSystem
3537
disableTransitionOnChange
3638
>
37-
<MainLayout>{children}</MainLayout>
39+
<Suspense>
40+
<URLParamsProvider>
41+
<MainLayout>{children}</MainLayout>
42+
</URLParamsProvider>
43+
</Suspense>
3844
</ThemeProvider>
3945
</body>
4046
</html>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
import Editor from "@monaco-editor/react";
3+
import { useTheme } from "next-themes";
4+
import { FeatureFile } from "@/types/feature";
5+
interface CodeEditorProps {
6+
file?: FeatureFile;
7+
onFileChange?: (fileName: string, content: string) => void;
8+
}
9+
10+
export function CodeEditor({ file, onFileChange }: CodeEditorProps) {
11+
const handleEditorChange = (value: string | undefined) => {
12+
if (value && onFileChange) {
13+
onFileChange(file!.name, value);
14+
}
15+
};
16+
17+
const theme = useTheme();
18+
19+
return file ? (
20+
<div className="h-full flex flex-col">
21+
<Editor
22+
height="100%"
23+
language={file.language}
24+
value={file.content}
25+
onChange={handleEditorChange}
26+
options={{
27+
minimap: { enabled: false },
28+
fontSize: 16,
29+
lineNumbers: "on",
30+
readOnly: true,
31+
wordWrap: "on",
32+
stickyScroll: {
33+
enabled: false,
34+
},
35+
}}
36+
theme="vs-dark"
37+
/>
38+
</div>
39+
) : (
40+
<div className="p-6 text-center text-muted-foreground">
41+
Select a file from the file tree to view its code
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)