Skip to content

Commit 03993d3

Browse files
committed
chore: add code and readme viewer to dojo demos
1 parent ef9286c commit 03993d3

File tree

14 files changed

+1581
-124
lines changed

14 files changed

+1581
-124
lines changed

typescript-sdk/apps/dojo/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build": "next build",
88
"start": "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: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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+
console.log("Loaded agents:", agentConfigs.length);
67+
68+
const featureFiles = ["page.tsx", "style.css", "README.mdx"]
69+
70+
function getFile(_filePath: string | undefined, _fileName?: string) {
71+
if (!_filePath) {
72+
console.warn(`File path is undefined, skipping.`);
73+
return {}
74+
}
75+
const fileName = _fileName ?? _filePath.split('/').pop() ?? ''
76+
const filePath = _fileName ? path.join(_filePath, fileName) : _filePath;
77+
if (!fs.existsSync(filePath)) {
78+
console.warn(`File not found: ${filePath}, skipping.`);
79+
return {}
80+
}
81+
82+
try {
83+
const content = fs.readFileSync(filePath, "utf8");
84+
const extension = fileName.split(".").pop();
85+
let language = extension;
86+
if (extension === "py") language = "python";
87+
else if (extension === "css") language = "css";
88+
else if (extension === "md" || extension === "mdx") language = "markdown";
89+
else if (extension === "tsx") language = "typescript";
90+
else if (extension === "js") language = "javascript";
91+
else if (extension === "json") language = "json";
92+
else if (extension === "yaml" || extension === "yml") language = "yaml";
93+
else if (extension === "toml") language = "toml";
94+
95+
return {
96+
name: fileName,
97+
content,
98+
// path: path.join(demoIdWithFramework, fileName), // Store relative path within agent/demo
99+
language,
100+
type: 'file'
101+
}
102+
} catch (error) {
103+
console.error(`Error reading file ${filePath}:`, error);
104+
}
105+
}
106+
107+
function getFeatureFrontendFiles(featureId: string) {
108+
const featurePath = path.join(__dirname, `../src/app/[integrationId]/feature/${featureId as string}`);
109+
const retrievedFiles = []
110+
111+
for (const fileName of featureFiles) {
112+
retrievedFiles.push(getFile(featurePath, fileName))
113+
}
114+
115+
return retrievedFiles;
116+
}
117+
118+
const integrationsFolderPath = '../../../integrations'
119+
const agentFilesMapper: Record<string, (agentKeys: string[]) => Record<string, string>> = {
120+
'middleware-starter': () => ({
121+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/middleware-starter/src/index.ts`)
122+
}),
123+
'pydantic-ai': (agentKeys: string[]) => {
124+
return agentKeys.reduce((acc, agentId) => ({
125+
...acc,
126+
[agentId]: `https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/ag_ui/api/${agentId}.py`
127+
}), {})
128+
},
129+
'server-starter': () => ({
130+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/__init__.py`)
131+
}),
132+
'server-starter-all-features': (agentKeys: string[]) => {
133+
return agentKeys.reduce((acc, agentId) => ({
134+
...acc,
135+
[agentId]: path.join(__dirname, integrationsFolderPath, `/server-starter/server/python/example_server/${agentId}.py`)
136+
}), {})
137+
},
138+
'mastra': () => ({
139+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
140+
}),
141+
'mastra-agent-lock': () => ({
142+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/mastra/example/src/mastra/agents/weather-agent.ts`)
143+
}),
144+
'vercel-ai-sdk': () => ({
145+
agentic_chat: path.join(__dirname, integrationsFolderPath, `/vercel-ai-sdk/src/index.ts`)
146+
}),
147+
'langgraph': (agentKeys: string[]) => {
148+
return agentKeys.reduce((acc, agentId) => ({
149+
...acc,
150+
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/examples/agents/${agentId}/agent.py`)
151+
}), {})
152+
},
153+
'langgraph-fastapi': (agentKeys: string[]) => {
154+
return agentKeys.reduce((acc, agentId) => ({
155+
...acc,
156+
[agentId]: path.join(__dirname, integrationsFolderPath, `/langgraph/python/ag_ui_langgraph/examples/agents/${agentId}.py`)
157+
}), {})
158+
},
159+
'agno': () => ({}),
160+
'llama-index': (agentKeys: string[]) => {
161+
return agentKeys.reduce((acc, agentId) => ({
162+
...acc,
163+
[agentId]: path.join(__dirname, integrationsFolderPath, `/llamaindex/server-py/server/routers/${agentId}.py`)
164+
}), {})
165+
},
166+
'crewai': (agentKeys: string[]) => {
167+
return agentKeys.reduce((acc, agentId) => ({
168+
...acc,
169+
[agentId]: path.join(__dirname, integrationsFolderPath, `/crewai/python/ag_ui_crewai/examples/${agentId}.py`)
170+
}), {})
171+
}
172+
}
173+
174+
function runGenerateContent() {
175+
const result = {}
176+
for (const agentConfig of agentConfigs) {
177+
// Use the parsed agent keys instead of executing the agents function
178+
const agentsPerFeatures = agentConfig.agentKeys
179+
180+
const agentFilePaths = agentFilesMapper[agentConfig.id](agentConfig.agentKeys)
181+
// Per feature, assign all the frontend files like page.tsx as well as all agent files
182+
agentsPerFeatures.forEach(featureId => {
183+
// @ts-expect-error -- redundant error about indexing of a new object.
184+
result[`${agentConfig.id}::${featureId}`] = [
185+
// Get all frontend files for the feature
186+
...getFeatureFrontendFiles(featureId),
187+
// Get the agent (python/TS) file
188+
getFile(agentFilePaths[featureId])
189+
]
190+
})
191+
}
192+
193+
return result
194+
}
195+
196+
// const result = {};
197+
// const agentDemoBaseDir = path.join(__dirname, "../agent/demo");
198+
//
199+
// for (const demoIdWithFramework in config) {
200+
// const demoFilesConfig = config[demoIdWithFramework];
201+
// const demoDirPath = path.join(agentDemoBaseDir, demoIdWithFramework);
202+
//
203+
// if (!fs.existsSync(demoDirPath)) {
204+
// console.warn(`Directory not found for demo: ${demoIdWithFramework}, skipping.`);
205+
// continue;
206+
// }
207+
//
208+
// result[demoIdWithFramework] = { files: [] };
209+
//
210+
// for (const fileName of demoFilesConfig) {
211+
// const filePath = path.join(demoDirPath, fileName);
212+
// if (!fs.existsSync(filePath)) {
213+
// console.warn(`File not found: ${filePath}, skipping.`);
214+
// continue;
215+
// }
216+
//
217+
// try {
218+
// const content = fs.readFileSync(filePath, "utf8");
219+
// const extension = fileName.split(".").pop();
220+
// let language = extension;
221+
// if (extension === "py") language = "python";
222+
// else if (extension === "css") language = "css";
223+
// else if (extension === "md" || extension === "mdx") language = "markdown";
224+
// else if (extension === "tsx") language = "typescript";
225+
// else if (extension === "js") language = "javascript";
226+
// else if (extension === "json") language = "json";
227+
// else if (extension === "yaml" || extension === "yml") language = "yaml";
228+
// else if (extension === "toml") language = "toml";
229+
//
230+
// result[demoIdWithFramework].files.push({
231+
// name: fileName,
232+
// content,
233+
// path: path.join(demoIdWithFramework, fileName), // Store relative path within agent/demo
234+
// language,
235+
// type: 'file'
236+
// });
237+
// } catch (error) {
238+
// console.error(`Error reading file ${filePath}:`, error);
239+
// }
240+
// }
241+
// }
242+
const result = runGenerateContent();
243+
fs.writeFileSync(
244+
path.join(__dirname, "../src/files.json"),
245+
JSON.stringify(result, null, 2)
246+
);
247+
248+
console.log("Successfully generated src/files.json");
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)