feat: add React GitHub Pages atlas

This commit is contained in:
ashishpatel26
2026-06-06 11:20:03 +00:00
parent 187b9cffc4
commit 9fda658c73
9 changed files with 2864 additions and 17 deletions

View File

@@ -1,45 +1,57 @@
# Sample workflow for building and deploying a Jekyll site to GitHub Pages name: Deploy React Atlas to GitHub Pages
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
on: on:
# Runs on pushes targeting the default branch
push: push:
branches: ["main"] branches: ["main"]
pull_request:
# Allows you to run this workflow manually from the Actions tab branches: ["main"]
paths:
- "web/**"
- "README.md"
- "agents/**"
- "crewai_mcp_course/**"
- ".github/workflows/jekyll-gh-pages.yml"
workflow_dispatch: workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions: permissions:
contents: read contents: read
pages: write pages: write
id-token: write id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency: concurrency:
group: "pages" group: "pages"
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
# Build job
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
# Deployment job - name: Install website dependencies
run: npm install
working-directory: web
- name: Build React atlas
run: npm run build
working-directory: web
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: web/dist
deploy: deploy:
if: github.event_name != 'pull_request'
environment: environment:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}

16
web/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="A premium React atlas for 500+ AI agent projects, runnable implementations, use cases, and the CrewAI MCP course."
/>
<title>500 AI Agents Atlas</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

20
web/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "ai-agents-atlas",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.468.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.1"
}
}

1
web/public/.nojekyll Normal file
View File

@@ -0,0 +1 @@

1057
web/src/App.jsx Normal file

File diff suppressed because it is too large Load Diff

512
web/src/content.js Normal file
View File

@@ -0,0 +1,512 @@
import heroImage from "../../images/AIAgentUseCase.jpg";
import industryImage from "../../images/industry_usecase1.png";
import repoReadme from "../../README.md?raw";
import agentsReadme from "../../agents/README.md?raw";
import courseReadme from "../../crewai_mcp_course/README.md?raw";
const agentFiles = import.meta.glob(
"../../agents/*/{README.md,agent.py,metadata.yaml,requirements.txt}",
{
eager: true,
import: "default",
query: "?raw",
},
);
const courseFiles = import.meta.glob("../../crewai_mcp_course/**/*.{md,py,txt}", {
eager: true,
import: "default",
query: "?raw",
});
export function stripMarkdown(value = "") {
return String(value)
.replace(/<[^>]*>/g, " ")
.replace(/!\[[^\]]*]\([^)]+\)/g, " ")
.replace(/\[([^\]]+)]\([^)]+\)/g, "$1")
.replace(/[*_`>#]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export function slugify(value = "item") {
const cleaned = stripMarkdown(value)
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return cleaned || "item";
}
function titleCase(value = "") {
return stripMarkdown(value)
.replace(/^[^A-Za-z0-9]+/, "")
.replace(/[-_]+/g, " ")
.split(" ")
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
function formatFramework(value = "") {
const key = slugify(value);
const names = {
agno: "Agno",
autogen: "AutoGen",
crewai: "CrewAI",
langchain: "LangChain",
langgraph: "LangGraph",
llamaindex: "LlamaIndex",
"llama-index": "LlamaIndex",
};
return names[key] || titleCase(value || "Python");
}
function cleanCell(value = "") {
return stripMarkdown(value)
.replace(/^[^A-Za-z0-9]+/, "")
.replace(/\s+/g, " ")
.trim();
}
function extractUrl(value = "") {
const markdownUrls = Array.from(String(value).matchAll(/\]\((https?:\/\/[^)]+)\)/g))
.map((match) => match[1])
.filter((url) => !url.includes("img.shields.io"));
if (markdownUrls.length > 0) {
return markdownUrls[markdownUrls.length - 1];
}
const directUrls = String(value)
.match(/https?:\/\/[^\s)]+/g)
?.filter((url) => !url.includes("img.shields.io"));
return directUrls?.[directUrls.length - 1] || "";
}
function splitTableRow(line = "") {
return line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim());
}
function isDividerRow(line = "") {
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}
function getHeaderKey(header = "") {
return stripMarkdown(header).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
}
function inferFramework(source = "") {
if (/crewai/i.test(source)) return "CrewAI";
if (/autogen/i.test(source)) return "AutoGen";
if (/agno/i.test(source)) return "Agno";
if (/langgraph/i.test(source)) return "LangGraph";
if (/llamaindex|llama index/i.test(source)) return "LlamaIndex";
return "Cross-industry";
}
function isUseCaseTable(headers, sourceHeading) {
const headerText = headers.map(getHeaderKey).join(" ");
if (/navigation guide|framework comparison/i.test(sourceHeading)) return false;
return (
/description/.test(headerText) &&
/use case|project|agent|example/.test(headerText) &&
/code|github|notebook|link|project/.test(headerText)
);
}
function pickRowValue(row, candidates) {
const key = Object.keys(row).find((item) =>
candidates.some((candidate) => item.includes(candidate)),
);
return key ? row[key] : "";
}
export function parseReadmeUseCases(markdown = repoReadme) {
const lines = markdown.split(/\r?\n/);
const records = [];
const usedIds = new Set();
let inFence = false;
let h2 = "";
let h3 = "";
let subheading = "";
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (/^\s*```/.test(line)) {
inFence = !inFence;
continue;
}
if (inFence) continue;
const heading = line.match(/^(#{2,4})\s+(.+)$/);
if (heading) {
const level = heading[1].length;
const value = cleanCell(heading[2]);
if (level === 2) {
h2 = value;
h3 = "";
subheading = "";
} else if (level === 3) {
h3 = value;
subheading = "";
} else {
subheading = value;
}
continue;
}
const boldHeading = line.match(/^\*\*([^*]+)\*\*\s*$/);
if (boldHeading) {
subheading = cleanCell(boldHeading[1]);
continue;
}
if (!line.trim().startsWith("|") || !lines[index + 1] || !isDividerRow(lines[index + 1])) {
continue;
}
const headers = splitTableRow(line);
const sourceHeading = [h2, h3, subheading].filter(Boolean).join(" / ");
if (!isUseCaseTable(headers, sourceHeading)) {
continue;
}
index += 2;
while (index < lines.length && lines[index].trim().startsWith("|")) {
const cells = splitTableRow(lines[index]);
if (cells.length >= headers.length - 1) {
const row = {};
headers.forEach((header, cellIndex) => {
row[getHeaderKey(header)] = cells[cellIndex] || "";
});
const title =
cleanCell(pickRowValue(row, ["use case", "project", "agent", "example"])) ||
cleanCell(cells[0]);
const description = cleanCell(pickRowValue(row, ["description", "best for"]));
const industry = cleanCell(pickRowValue(row, ["industry", "domain", "category"]));
const url = extractUrl(cells.join(" "));
if (title && description) {
const baseId = slugify(`${sourceHeading}-${title}`);
let id = baseId;
let suffix = 2;
while (usedIds.has(id)) {
id = `${baseId}-${suffix}`;
suffix += 1;
}
usedIds.add(id);
records.push({
id,
title,
description,
industry: industry || "General",
framework: inferFramework(sourceHeading),
sourceHeading,
sourceGroup: h2,
sourceSubheading: [h3, subheading].filter(Boolean).join(" / "),
url,
resourceType: /notebook|ipynb/i.test(url) ? "Notebook" : "Code",
tags: [
inferFramework(sourceHeading),
industry || "General",
h2,
h3,
subheading,
].filter(Boolean),
});
}
}
index += 1;
}
index -= 1;
}
return records;
}
function parseYaml(raw = "") {
const data = {};
raw.split(/\r?\n/).forEach((line) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes(":")) return;
const [key, ...rest] = trimmed.split(":");
const value = rest.join(":").trim();
if (value.startsWith("[") && value.endsWith("]")) {
data[key.trim()] = value
.slice(1, -1)
.split(",")
.map((item) => item.trim().replace(/^["']|["']$/g, ""))
.filter(Boolean);
} else {
data[key.trim()] = value.replace(/^["']|["']$/g, "");
}
});
return data;
}
function firstHeading(markdown = "") {
return cleanCell(markdown.match(/^#\s+(.+)$/m)?.[1] || "");
}
function extractSection(markdown = "", headingPattern) {
const lines = markdown.split(/\r?\n/);
const start = lines.findIndex((line) => {
const heading = line.match(/^#{2,4}\s+(.+)$/);
return heading && headingPattern.test(heading[1]);
});
if (start === -1) return "";
const output = [];
for (let index = start + 1; index < lines.length; index += 1) {
if (/^#{2,4}\s+/.test(lines[index])) break;
output.push(lines[index]);
}
return output.join("\n").trim();
}
function extractList(section = "", limit = 6) {
return section
.split(/\r?\n/)
.map((line) => line.match(/^\s*(?:[-*]|\d+\.)\s+(.+)$/)?.[1])
.filter(Boolean)
.map(cleanCell)
.filter(Boolean)
.slice(0, limit);
}
function extractCodeFences(section = "") {
return Array.from(section.matchAll(/```(?:bash|sh|shell)?\s*\n([\s\S]*?)```/g))
.map((match) => match[1].trim())
.filter(Boolean);
}
function extractPythonConcepts(code = "") {
const functions = Array.from(code.matchAll(/^def\s+([A-Za-z_][A-Za-z0-9_]*)/gm)).map(
(match) => match[1],
);
const classes = Array.from(code.matchAll(/^class\s+([A-Za-z_][A-Za-z0-9_]*)/gm)).map(
(match) => match[1],
);
const signals = [
code.includes("StateGraph") && "state graph",
code.includes("Crew(") && "crew orchestration",
code.includes("Agent(") && "agent roles",
code.includes("Task(") && "task pipeline",
code.includes("Tool") && "tool calls",
code.includes("argparse") && "command line interface",
code.includes("sqlite3") && "local database",
code.includes("pandas") && "dataframe processing",
code.includes("Tavily") && "web search",
].filter(Boolean);
return {
functions: functions.slice(0, 10),
classes: classes.slice(0, 8),
signals,
};
}
function buildAgents() {
const folders = {};
Object.entries(agentFiles).forEach(([path, raw]) => {
const match = path.match(/agents\/([^/]+)\/([^/]+)$/);
if (!match) return;
const [, folder, fileName] = match;
folders[folder] = folders[folder] || {};
folders[folder][fileName] = raw;
});
return Object.entries(folders)
.map(([folder, files]) => {
const metadata = parseYaml(files["metadata.yaml"] || "");
const readme = files["README.md"] || "";
const code = files["agent.py"] || "";
const setupSection = extractSection(readme, /setup|installation/i);
const runSection = extractSection(readme, /run|usage/i);
const features =
extractList(extractSection(readme, /what it does/i), 7).length > 0
? extractList(extractSection(readme, /what it does/i), 7)
: extractList(extractSection(readme, /features|use cases/i), 7);
const title = titleCase(firstHeading(readme) || metadata.title || folder);
const number = folder.match(/^\d+/)?.[0] || "";
const concepts = extractPythonConcepts(code);
return {
id: folder,
slug: folder,
number,
title,
description: metadata.description || cleanCell(readme.split(/\r?\n/)[2] || ""),
framework: formatFramework(metadata.framework || "Python"),
llm: metadata.llm || "Configurable",
industry: titleCase(metadata.industry || "General"),
difficulty: titleCase(metadata.difficulty || "Intermediate"),
tags: Array.isArray(metadata.tags) ? metadata.tags : [],
language: titleCase(metadata.language || "Python"),
localPath: `agents/${folder}`,
entrypoint: metadata.entrypoint || "agent.py",
requirements: metadata.requirements || "requirements.txt",
readme,
code,
requirementsText: files["requirements.txt"] || "",
metadataText: files["metadata.yaml"] || "",
features,
setupCommands: extractCodeFences(setupSection),
runCommands: extractCodeFences(runSection),
concepts,
};
})
.sort((a, b) => a.slug.localeCompare(b.slug, undefined, { numeric: true }));
}
function getCourseFile(pathSuffix) {
const entry = Object.entries(courseFiles).find(([path]) => path.endsWith(pathSuffix));
return entry ? entry[1] : "";
}
const courseLessons = [
{
slug: "lesson-01",
number: "01",
title: "First CrewAI Researcher",
summary:
"Build a single researcher agent, give it a clear role, run one task, and inspect the response path.",
files: [
{
label: "agent.py",
path: "crewai_mcp_course/lesson_01/agent.py",
content: getCourseFile("lesson_01/agent.py"),
},
{
label: "requirements.txt",
path: "crewai_mcp_course/lesson_01/requirements.txt",
content: getCourseFile("lesson_01/requirements.txt"),
},
],
objectives: [
"Install the CrewAI lesson dependencies",
"Define a role-driven research agent",
"Attach the agent to one clear task",
"Run the crew and read the final output",
],
runCommands: ["cd crewai_mcp_course/lesson_01", "pip install -r requirements.txt", "python agent.py"],
},
{
slug: "lesson-02",
number: "02",
title: "Multi-Agent Research Crew",
summary:
"Split research, writing, and editing responsibilities into a sequential CrewAI workflow.",
files: [
{
label: "agent.py",
path: "crewai_mcp_course/lesson_02/agent.py",
content: getCourseFile("lesson_02/agent.py"),
},
{
label: "requirements.txt",
path: "crewai_mcp_course/lesson_02/requirements.txt",
content: getCourseFile("lesson_02/requirements.txt"),
},
],
objectives: [
"Create multiple specialist agents",
"Connect tasks into a sequential process",
"Pass context from researcher to writer to editor",
"Understand where tools fit into CrewAI",
],
runCommands: ["cd crewai_mcp_course/lesson_02", "pip install -r requirements.txt", "python agent.py"],
},
{
slug: "lesson-03",
number: "03",
title: "CrewAI With FastMCP-Style Tools",
summary:
"Use a FastMCP-style tool surface to let CrewAI agents query structured project data and produce a status report.",
files: [
{
label: "agent.py",
path: "crewai_mcp_course/lesson_03/agent.py",
content: getCourseFile("lesson_03/agent.py"),
},
{
label: "mcp_server.py",
path: "crewai_mcp_course/lesson_03/mcp_server.py",
content: getCourseFile("lesson_03/mcp_server.py"),
},
{
label: "requirements.txt",
path: "crewai_mcp_course/lesson_03/requirements.txt",
content: getCourseFile("lesson_03/requirements.txt"),
},
],
objectives: [
"Model a tool contract that resembles MCP server access",
"Let agents request project records through tools",
"Generate a management-ready project status report",
"See how MCP-style boundaries improve agent design",
],
runCommands: ["cd crewai_mcp_course/lesson_03", "pip install -r requirements.txt", "python agent.py"],
},
];
export const frameworks = [
{
slug: "langgraph",
name: "LangGraph",
accent: "cyan",
bestFor: "Stateful workflows, graph control, retries, RAG, and production routing.",
decision: "Choose it when the agent needs explicit state transitions and reliable recovery paths.",
},
{
slug: "crewai",
name: "CrewAI",
accent: "green",
bestFor: "Role-based teams, business automation, repeatable task handoffs, and fast prototypes.",
decision: "Choose it when the project maps naturally to specialists with goals and tasks.",
},
{
slug: "autogen",
name: "AutoGen",
accent: "amber",
bestFor: "Code generation, research collaboration, feedback loops, and executable experiments.",
decision: "Choose it when agents need to talk, critique, run code, and iterate.",
},
{
slug: "agno",
name: "Agno",
accent: "rose",
bestFor: "Lightweight tool agents, quick iteration, model flexibility, and compact apps.",
decision: "Choose it when you want a focused tool-calling agent with low ceremony.",
},
{
slug: "llamaindex",
name: "LlamaIndex",
accent: "violet",
bestFor: "Document Q&A, enterprise retrieval, data connectors, and knowledge workflows.",
decision: "Choose it when the data pipeline matters as much as the agent behavior.",
},
];
export const catalog = {
repoReadme,
agentsReadme,
courseReadme,
agents: buildAgents(),
useCases: parseReadmeUseCases(repoReadme),
courseLessons,
frameworks,
images: {
hero: heroImage,
industry: industryImage,
},
};

10
web/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

1207
web/src/styles.css Normal file

File diff suppressed because it is too large Load Diff

12
web/vite.config.js Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
base: "/500-AI-Agents-Projects/",
plugins: [react()],
server: {
fs: {
allow: [".."],
},
},
});