Skip to content

Instantly share code, notes, and snippets.

@paulweezydesign
Last active December 9, 2025 12:14
Show Gist options
  • Select an option

  • Save paulweezydesign/36733b8c1968e3c0c4fcbc46776fb1f6 to your computer and use it in GitHub Desktop.

Select an option

Save paulweezydesign/36733b8c1968e3c0c4fcbc46776fb1f6 to your computer and use it in GitHub Desktop.
'use client'
import { LucideIcon } from 'lucide-react'
interface AgentCardProps {
agent: {
id: string
name: string
description: string
icon: LucideIcon
color: string
}
isSelected: boolean
onClick: () => void
}
export default function AgentCard({ agent, isSelected, onClick }: AgentCardProps) {
const Icon = agent.icon
return (
<div
onClick={onClick}
className={`
agent-card-hover
cursor-pointer
bg-white dark:bg-gray-800
rounded-xl
shadow-md
p-6
border-2
transition-all
${
isSelected
? 'border-purple-500 ring-4 ring-purple-200 dark:ring-purple-900'
: 'border-transparent hover:border-gray-300 dark:hover:border-gray-600'
}
`}
>
<div className="flex items-start gap-4">
<div className={`${agent.color} p-3 rounded-lg text-white`}>
<Icon className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
{agent.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{agent.description}
</p>
</div>
</div>
{isSelected && (
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 text-sm text-purple-600 dark:text-purple-400">
<div className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></div>
<span className="font-medium">Active Agent</span>
</div>
</div>
)}
</div>
)
}
import { NextRequest, NextResponse } from 'next/server'
import { getAgentById } from '@/lib/mastra'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { agentId, message, conversationHistory = [] } = body
if (!agentId || !message) {
return NextResponse.json(
{ error: 'Missing required fields: agentId and message' },
{ status: 400 }
)
}
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json(
{ error: `Agent not found: ${agentId}` },
{ status: 404 }
)
}
// Build messages array with conversation history
const messages = [
...conversationHistory.map((msg: any) => ({
role: msg.role,
content: msg.content,
})),
{
role: 'user',
content: message,
},
]
// Generate response using the agent
const response = await agent.generate(messages)
return NextResponse.json({
success: true,
response: response.text,
agentId,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Agent API Error:', error)
return NextResponse.json(
{
error: 'Failed to process request',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}
}
export async function GET() {
return NextResponse.json({
message: 'Mastra Multi-Agent System API',
version: '1.0.0',
agents: [
'project-manager',
'research-agent',
'design-agent',
'frontend-agent',
'backend-agent',
'qa-agent',
],
})
}
'use client'
import { useState, useRef, useEffect } from 'react'
import { Send, X, Loader2, User, Bot } from 'lucide-react'
interface Message {
role: 'user' | 'assistant'
content: string
timestamp: string
}
interface ChatInterfaceProps {
agentId: string
agentName: string
onClose: () => void
}
export default function ChatInterface({ agentId, agentName, onClose }: ChatInterfaceProps) {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
useEffect(() => {
// Add welcome message when agent is selected
setMessages([{
role: 'assistant',
content: `Hello! I'm the ${agentName}. How can I assist you today?`,
timestamp: new Date().toISOString(),
}])
}, [agentId, agentName])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!input.trim() || isLoading) return
const userMessage: Message = {
role: 'user',
content: input.trim(),
timestamp: new Date().toISOString(),
}
setMessages(prev => [...prev, userMessage])
setInput('')
setIsLoading(true)
try {
const response = await fetch('/api/agents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agentId,
message: userMessage.content,
conversationHistory: messages,
}),
})
const data = await response.json()
if (data.success) {
const assistantMessage: Message = {
role: 'assistant',
content: data.response,
timestamp: data.timestamp,
}
setMessages(prev => [...prev, assistantMessage])
} else {
throw new Error(data.error || 'Failed to get response')
}
} catch (error) {
console.error('Chat error:', error)
const errorMessage: Message = {
role: 'assistant',
content: `Sorry, I encountered an error. Please make sure you have set your OPENAI_API_KEY environment variable. Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: new Date().toISOString(),
}
setMessages(prev => [...prev, errorMessage])
} finally {
setIsLoading(false)
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-4xl h-[80vh] flex flex-col">
{/* Header */}
<div className="gradient-bg text-white p-6 rounded-t-2xl flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">{agentName}</h2>
<p className="text-gray-100 text-sm mt-1">AI Assistant</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-white hover:bg-opacity-20 rounded-lg transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={`chat-message flex gap-3 ${
message.role === 'user' ? 'flex-row-reverse' : 'flex-row'
}`}
>
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
message.role === 'user'
? 'bg-purple-500 text-white'
: 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-200'
}`}
>
{message.role === 'user' ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
</div>
<div
className={`flex-1 max-w-[80%] ${
message.role === 'user' ? 'text-right' : 'text-left'
}`}
>
<div
className={`inline-block p-4 rounded-2xl ${
message.role === 'user'
? 'bg-purple-500 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
}`}
>
<p className="whitespace-pre-wrap break-words">{message.content}</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 px-2">
{new Date(message.timestamp).toLocaleTimeString()}
</p>
</div>
</div>
))}
{isLoading && (
<div className="flex gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center">
<Bot className="w-4 h-4 text-gray-700 dark:text-gray-200" />
</div>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-2xl">
<Loader2 className="w-5 h-5 animate-spin text-gray-600 dark:text-gray-300" />
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-6 border-t border-gray-200 dark:border-gray-700">
<div className="flex gap-3">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
disabled={isLoading}
className="flex-1 px-4 py-3 bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-6 py-3 bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
<Send className="w-5 h-5" />
<span>Send</span>
</>
)}
</button>
</div>
</form>
</div>
</div>
)
}
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.agent-card-hover {
transition: all 0.3s ease;
}
.agent-card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}
.chat-message {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.workflow-node {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Mastra Multi-Agent System',
description: 'A comprehensive multi-agent system for software development using Mastra.ai',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
'use client'
import { useState } from 'react'
import AgentCard from '@/components/agent-card'
import ChatInterface from '@/components/chat-interface'
import WorkflowVisualizer from '@/components/workflow-visualizer'
import {
Briefcase,
Search,
Palette,
Code,
Server,
Bug,
Workflow
} from 'lucide-react'
const agents = [
{
id: 'project-manager',
name: 'Project Manager',
description: 'Coordinates tasks, manages timeline, and orchestrates other agents',
icon: Briefcase,
color: 'bg-blue-500',
},
{
id: 'research-agent',
name: 'Research Agent',
description: 'Conducts deep research, gathers requirements, and analyzes market trends',
icon: Search,
color: 'bg-purple-500',
},
{
id: 'design-agent',
name: 'Design Agent',
description: 'Creates UI/UX designs, mockups, and design systems',
icon: Palette,
color: 'bg-pink-500',
},
{
id: 'frontend-agent',
name: 'Frontend Agent',
description: 'Builds responsive user interfaces and implements designs',
icon: Code,
color: 'bg-green-500',
},
{
id: 'backend-agent',
name: 'Backend Agent',
description: 'Develops APIs, databases, and server-side logic',
icon: Server,
color: 'bg-orange-500',
},
{
id: 'qa-agent',
name: 'QA Agent',
description: 'Tests functionality, ensures quality, and reports bugs',
icon: Bug,
color: 'bg-red-500',
},
]
export default function Home() {
const [selectedAgent, setSelectedAgent] = useState<string | null>(null)
const [showWorkflow, setShowWorkflow] = useState(false)
return (
<main className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
{/* Header */}
<header className="gradient-bg text-white py-8 px-6 shadow-lg">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold mb-2">Mastra Multi-Agent System</h1>
<p className="text-gray-100 text-lg">
AI-powered agents working together to build software projects
</p>
</div>
</header>
<div className="max-w-7xl mx-auto p-6">
{/* Workflow Toggle */}
<div className="mb-6 flex justify-end">
<button
onClick={() => setShowWorkflow(!showWorkflow)}
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-all"
>
<Workflow className="w-5 h-5" />
{showWorkflow ? 'Hide Workflow' : 'Show Workflow'}
</button>
</div>
{/* Workflow Visualizer */}
{showWorkflow && (
<div className="mb-8">
<WorkflowVisualizer agents={agents} />
</div>
)}
{/* Agents Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{agents.map((agent) => (
<AgentCard
key={agent.id}
agent={agent}
isSelected={selectedAgent === agent.id}
onClick={() => setSelectedAgent(agent.id)}
/>
))}
</div>
{/* Chat Interface */}
{selectedAgent && (
<div className="mt-8">
<ChatInterface
agentId={selectedAgent}
agentName={agents.find(a => a.id === selectedAgent)?.name || ''}
onClose={() => setSelectedAgent(null)}
/>
</div>
)}
{/* Info Section */}
{!selectedAgent && (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8 mt-8">
<h2 className="text-2xl font-bold mb-4">How It Works</h2>
<div className="space-y-4 text-gray-700 dark:text-gray-300">
<p>
<strong className="text-gray-900 dark:text-white">Project Manager:</strong> Orchestrates the entire development process, delegates tasks to specialized agents, and ensures project alignment.
</p>
<p>
<strong className="text-gray-900 dark:text-white">Research Agent:</strong> Performs comprehensive research on technologies, best practices, and user requirements.
</p>
<p>
<strong className="text-gray-900 dark:text-white">Design Agent:</strong> Creates user-centered designs, wireframes, and maintains design consistency.
</p>
<p>
<strong className="text-gray-900 dark:text-white">Frontend Agent:</strong> Implements responsive interfaces using modern frameworks and best practices.
</p>
<p>
<strong className="text-gray-900 dark:text-white">Backend Agent:</strong> Builds scalable APIs, databases, and handles server-side business logic.
</p>
<p>
<strong className="text-gray-900 dark:text-white">QA Agent:</strong> Ensures code quality through testing, identifies bugs, and validates requirements.
</p>
</div>
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
💡 <strong>Tip:</strong> Select an agent card above to start interacting with it!
</p>
</div>
</div>
)}
</div>
</main>
)
}
'use client'
import { LucideIcon } from 'lucide-react'
import { ArrowRight } from 'lucide-react'
interface Agent {
id: string
name: string
description: string
icon: LucideIcon
color: string
}
interface WorkflowVisualizerProps {
agents: Agent[]
}
export default function WorkflowVisualizer({ agents }: WorkflowVisualizerProps) {
// Define workflow connections
const workflow = [
{
from: 'project-manager',
to: 'research-agent',
label: 'Requirements',
},
{
from: 'research-agent',
to: 'design-agent',
label: 'Findings',
},
{
from: 'design-agent',
to: 'frontend-agent',
label: 'Designs',
},
{
from: 'design-agent',
to: 'backend-agent',
label: 'API Specs',
},
{
from: 'frontend-agent',
to: 'qa-agent',
label: 'UI Code',
},
{
from: 'backend-agent',
to: 'qa-agent',
label: 'API Code',
},
{
from: 'qa-agent',
to: 'project-manager',
label: 'Test Results',
},
]
return (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold mb-6 text-gray-900 dark:text-white">
Agent Workflow
</h2>
{/* Desktop View - Flowchart */}
<div className="hidden md:block">
<div className="grid grid-cols-3 gap-8">
{/* Row 1: Project Manager */}
<div className="col-span-3 flex justify-center">
<WorkflowNode agent={agents[0]} />
</div>
{/* Arrow Down */}
<div className="col-span-3 flex justify-center">
<ArrowRight className="rotate-90 text-purple-400" size={32} />
</div>
{/* Row 2: Research Agent */}
<div className="col-span-3 flex justify-center">
<WorkflowNode agent={agents[1]} />
</div>
{/* Arrow Down */}
<div className="col-span-3 flex justify-center">
<ArrowRight className="rotate-90 text-purple-400" size={32} />
</div>
{/* Row 3: Design Agent */}
<div className="col-span-3 flex justify-center">
<WorkflowNode agent={agents[2]} />
</div>
{/* Arrows to Frontend and Backend */}
<div className="col-span-3 flex justify-center gap-32 my-4">
<ArrowRight className="rotate-45 text-purple-400" size={32} />
<ArrowRight className="-rotate-45 text-purple-400" size={32} />
</div>
{/* Row 4: Frontend and Backend */}
<div className="col-span-3 flex justify-center gap-16">
<WorkflowNode agent={agents[3]} />
<WorkflowNode agent={agents[4]} />
</div>
{/* Arrows to QA */}
<div className="col-span-3 flex justify-center gap-32 my-4">
<ArrowRight className="-rotate-45 text-purple-400" size={32} />
<ArrowRight className="rotate-45 text-purple-400" size={32} />
</div>
{/* Row 5: QA Agent */}
<div className="col-span-3 flex justify-center">
<WorkflowNode agent={agents[5]} />
</div>
</div>
</div>
{/* Mobile View - List */}
<div className="md:hidden space-y-4">
{agents.map((agent, index) => (
<div key={agent.id}>
<WorkflowNode agent={agent} compact />
{index < agents.length - 1 && (
<div className="flex justify-center my-2">
<ArrowRight className="rotate-90 text-purple-400" size={24} />
</div>
)}
</div>
))}
</div>
{/* Legend */}
<div className="mt-8 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="font-semibold text-gray-900 dark:text-white mb-2">
Workflow Process:
</h3>
<ol className="text-sm text-gray-700 dark:text-gray-300 space-y-1 list-decimal list-inside">
<li>Project Manager defines requirements and coordinates the team</li>
<li>Research Agent conducts deep research and analysis</li>
<li>Design Agent creates UI/UX designs and specifications</li>
<li>Frontend & Backend Agents implement the solution in parallel</li>
<li>QA Agent tests the implementation and reports back</li>
<li>Project Manager reviews and iterates if needed</li>
</ol>
</div>
</div>
)
}
function WorkflowNode({ agent, compact = false }: { agent: Agent; compact?: boolean }) {
const Icon = agent.icon
return (
<div
className={`workflow-node bg-white dark:bg-gray-700 rounded-lg shadow-md border-2 border-gray-200 dark:border-gray-600 ${
compact ? 'p-3' : 'p-4'
}`}
>
<div className="flex items-center gap-3">
<div className={`${agent.color} p-2 rounded-lg text-white flex-shrink-0`}>
<Icon className={compact ? 'w-4 h-4' : 'w-5 h-5'} />
</div>
<div>
<h4
className={`font-semibold text-gray-900 dark:text-white ${
compact ? 'text-sm' : 'text-base'
}`}
>
{agent.name}
</h4>
{!compact && (
<p className="text-xs text-gray-600 dark:text-gray-300 mt-1">
{agent.description.substring(0, 50)}...
</p>
)}
</div>
</div>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment