What happens when you put 9 AI personas in a room and ask them to decide on a simple feature like "Should we add dark mode?"
Chaos. Absolutely delightful, painfully accurate chaos.
I built Boardroom.exe - a real-time simulation of corporate dysfunction where AI agents debate, interrupt, scope-creep, and occasionally have breakthroughs (or breakdowns). This post is a technical deep-dive into how it was built.
The Concept
Every tech worker has sat through meetings that should have been 15 minutes but somehow became 2-hour debates about nothing. Boardroom.exe captures this experience in a browser-based simulation.
The goal isn't productivity - it's entertainment and recognition. When users see the simulation, they think: "This is exactly how our meetings feel."
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js 15 (App Router) | React framework |
| TypeScript | Type safety |
| Tailwind CSS | Styling |
| Zustand | State management |
| React Flow (@xyflow/react) | Graph visualization |
| Framer Motion | Animations |
| Lucide React | Icons |
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Application Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User Input ──► Zustand Store ──► React Components │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Control Panel Agent State Meeting Room │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ Simulation ◄──── Timeline │
│ │ │ │
│ │ ▼ │
│ │ nextTurn() every 2.5s │
│ │ │ │
│ │ ▼ │
│ └──────► UI Updates + Metrics │
│ │
└─────────────────────────────────────────────────────────────────┘
Core Components
1. State Management (Zustand)
The entire simulation state lives in a Zustand store. Here's the core interface:
// lib/types.ts
export interface MeetingState {
isRunning: boolean;
phase: 'idle' | 'opening' | 'discussion' | 'debate' | 'deadlock' | 'resolution' | 'ended';
elapsedTime: number;
topic: string;
chaosLevel: number;
agents: Agent[];
currentSpeaker: string | null;
transcript: Message[];
internalThoughts: Message[];
buzzwords: Record<string, number>;
metrics: Metrics;
scopeCreep: number;
consensusLevel: number;
}
The store handles all state mutations:
// lib/store.ts
export const useMeetingStore = create<MeetingStore>((set, get) => ({
// ... initial state
nextTurn: () => {
const state = get();
if (!state.isRunning) return;
const { message, nextSpeaker, updatedAgents } = simulateTurn(
state.agents,
state.currentSpeaker,
state.topic,
{ chaosLevel: state.chaosLevel, scopeCreep: state.scopeCreep, consensusLevel: state.consensusLevel }
);
// Update all state
set({
agents: updatedAgents,
currentSpeaker: nextSpeaker,
transcript: [...state.transcript, message],
// ... more updates
});
},
}));
2. Agent System
Each agent is a configuration object with traits that influence behavior:
// lib/agents.ts
export const createAgent = (role: AgentRole, id: string): Agent => {
const baseConfig: Record<AgentRole, Partial<Agent>> = {
ceo: {
name: 'Marcus',
traits: ['visionary', 'pivot-prone', 'Elon references', 'AI-obsessed'],
speakingRate: 0.4,
interruptChance: 0.35,
buzzwordAffinity: 0.7,
},
// ... other agents
};
return {
id,
name: config.name!,
role,
roleLabel: AGENT_LABELS[role],
color: AGENT_COLORS[role],
influence: 50,
emotionalState: 'neutral',
traits: config.traits!,
speakingRate: config.speakingRate!,
interruptChance: config.interruptChance!,
buzzwordAffinity: config.buzzwordAffinity!,
isSpeaking: false,
lastSpoke: 0,
turnCount: 0,
};
};
3. Simulation Engine
The simulation engine generates responses based on agent personality:
// lib/simulation.ts
const AGENT_PERSONALITIES: Record<AgentRole, string[]> = {
ceo: [
"Let's think about this from first principles.",
"I just read something about this. We need to pivot to AI.",
"What would Elon do here?",
// ... more phrases
],
engineering: [
"That sounds great, but the architecture can't support it.",
"We already have 47 tech debt tickets.",
"The migration will take 6 months.",
// ... more phrases
],
// ... other agents
};
export const simulateTurn = (agents, currentSpeaker, topic, state) => {
// Select speaker based on speaking rates
let speaker = availableAgents[Math.floor(Math.random() * availableAgents.length)];
// Generate response based on personality + state
const content = generateResponse(speaker, topic, agents, state);
return {
message,
nextSpeaker: speaker.id,
updatedAgents: agents.map(/* update speaker state */)
};
};
4. React Flow Visualization
The meeting room is visualized using React Flow:
// components/meeting-room/MeetingRoom.tsx
const nodes: Node[] = useMemo(() => {
return agents.map((agent) => {
const isSpeaking = currentSpeaker === agent.id;
const influenceScale = 0.8 + (agent.influence / 100) * 0.4;
return {
id: agent.id,
position: nodePositions[agent.id],
data: {
label: agent.name,
role: agent.roleLabel,
color: agent.color,
isSpeaking,
influence: agent.influence,
},
type: 'agentNode',
};
});
}, [agents, currentSpeaker, nodePositions]);
// Custom agent node component
function AgentNode({ data }) {
return (
<div className="flex flex-col items-center">
{/* Speaking ring animation */}
{data.isSpeaking && (
<div className="absolute inset-0 rounded-full animate-ping"
style={{ border: `3px solid ${data.color}` }} />
)}
{/* Agent circle with initials */}
<div className="w-full h-full rounded-full border-2 flex items-center justify-center"
style={{ backgroundColor: '#12121a', borderColor: data.color }}>
<span className="text-xl font-bold" style={{ color: data.color }}>
{data.label[0]}
</span>
</div>
{/* Name and role badges */}
<div className="mt-2 text-center">
<span className="text-sm text-[#e4e4e7] font-medium block">{data.label}</span>
<span className="text-xs px-2 py-0.5 rounded mt-1 inline-block"
style={{ backgroundColor: `${data.color}20`, color: data.color }}>
{data.role}
</span>
</div>
</div>
);
}
5. Metrics System
Real-time KPIs track meeting health:
// lib/metrics.ts
export const calculateMetrics = (
currentMetrics: Metrics,
agents: Agent[],
buzzwordCount: number,
scopeCreep: number,
chaosLevel: number,
events: number
): Metrics => {
const agentInfluence = agents.reduce((sum, a) => sum + a.influence, 0) / agents.length;
const moraleImpact = agents.filter(a => a.emotionalState === 'frustrated' || a.emotionalState === 'angry').length * 5;
return {
productivity: Math.max(0, Math.min(100, currentMetrics.productivity + (-1 - events * 2 + agentInfluence / 20))),
burnRate: Math.max(0, Math.min(100, currentMetrics.burnRate + (chaosLevel / 50) + (events * 3))),
morale: Math.max(0, Math.min(100, currentMetrics.morale + (-moraleImpact - events * 5))),
technicalDebt: Math.max(0, Math.min(100, currentMetrics.technicalDebt + scopeCreep / 15)),
buzzwordDensity: Math.max(0, Math.min(100, currentMetrics.buzzwordDensity + buzzwordCount * 0.1)),
shippingProbability: Math.max(0, Math.min(100, 80 - scopeCreep / 10)),
pivotLikelihood: Math.max(0, Math.min(100, currentMetrics.pivotLikelihood + chaosLevel / 20 + events * 5)),
reorgRisk: Math.max(0, Math.min(100, currentMetrics.reorgRisk + chaosLevel / 15)),
investorSatisfaction: Math.max(0, Math.min(100, currentMetrics.investorSatisfaction - events * 5)),
};
};
Key Implementation Details
1. Deterministic Edge Generation (Fixing Hydration Errors)
The React Flow edges use a hash-based approach to ensure consistent rendering:
const edges: Edge[] = useMemo(() => {
return agents.map((source, i) => agents.map((target, j) => {
if (i < j) {
// Use deterministic hash instead of Math.random()
const hash = (source.id.charCodeAt(5) + target.id.charCodeAt(5)) % 10;
if (hash < 2) {
return {
id: `edge-${source.id}-${target.id}`,
source: source.id,
target: target.id,
// ... edge config
};
}
}
})).flat();
}, [agents]);
This prevents hydration mismatches between server and client rendering.
2. Simulation Timing
The simulation runs on an interval:
// app/page.tsx
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
nextTurn();
}, 2500); // Every 2.5 seconds
}
return () => clearInterval(intervalRef.current);
}, [isRunning, nextTurn]);
3. Phase Transitions
Meeting phases transition based on metrics:
let newPhase = state.phase;
if (newMetrics.reorgRisk > 80) newPhase = 'deadlock';
else if (newScopeCreep > 80) newPhase = 'debate';
else if (state.elapsedTime > 120000 && newConsensusLevel < 30) newPhase = 'deadlock';
else if (newConsensusLevel > 70) newPhase = 'resolution';
Adding Custom AI Integration
To replace rule-based responses with real AI:
// lib/simulation.ts
async function generateResponseWithAI(agent: Agent, topic: string, apiKey: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4',
messages: [
{
role: 'system',
content: `You are ${agent.name}, ${agent.roleLabel}. ${AGENT_PERSONALITIES[agent.role].join(' ')}`
},
{ role: 'user', content: `Respond to: ${topic}` }
],
temperature: 0.8,
})
});
return (await response.json()).choices[0].message.content;
}
Running the Project
# Install dependencies
npm install
# Development
npm run dev
# Production build
npm run build
npm start
Future Enhancements
- Real AI Integration - Connect to OpenAI/Anthropic for authentic responses
- Meeting Export - Generate PDF summaries, Slack threads, Jira tickets
- Custom Agent Builder - UI to create new agent personas
- Multi-Room Support - Run multiple simultaneous meetings
- Meeting Replay - Record and playback entire meeting sessions
Boardroom.exe demonstrates how emergent behavior can arise from simple rule-based agents. The simulation captures the essence of corporate dysfunction through carefully crafted personalities, real-time metrics, and dramatic visual feedback.
The project shows that you don't need complex AI to create engaging simulations - just well-designed rules and thoughtful UX.
Code & more: https://www.dailybuild.xyz/project/131-boardroomexe

Top comments (0)