← back to Dw Boardroom Governance
frontend/src/components/Boardroom.tsx
166 lines
import { useState, useEffect } from 'react';
import { api } from '../api';
import AgendaBuilder from './AgendaBuilder';
import type { Meeting, MeetingMessage, WSEvent } from '../types';
const PHASES = ['Caucus', 'Call to Order', 'Old Business', 'Current Business', 'Breakout', 'New Business', 'Minutes', 'Adjournment'];
const PHASE_KEYS = ['caucus', 'call_to_order', 'old_business', 'current_business', 'breakout', 'new_business', 'minutes', 'adjournment'];
export default function Boardroom({ events }: { events: WSEvent[] }) {
const [meeting, setMeeting] = useState<Meeting | null>(null);
const [messages, setMessages] = useState<MeetingMessage[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadActive();
}, []);
// React to WS events
useEffect(() => {
const last = events[events.length - 1];
if (!last) return;
if (last.type === 'meeting_started' || last.type === 'meeting_phase' || last.type === 'meeting_halted' || last.type === 'meeting_completed') {
loadActive();
}
if (last.type === 'meeting_message' && meeting) {
setMessages(prev => [...prev, last.payload as MeetingMessage]);
}
}, [events]);
async function loadActive() {
try {
const data = await api.getActiveMeeting();
if (data.active) {
setMeeting(data.meeting);
setMessages(data.messages || []);
} else {
setMeeting(null);
setMessages([]);
}
} catch {}
}
async function startMeeting(type: string, agendaItems?: any[]) {
setLoading(true);
try {
await api.startMeeting(type, agendaItems);
await loadActive();
} catch (err: any) {
alert(err.message);
}
setLoading(false);
}
async function halt() {
if (!meeting) return;
try {
await api.haltMeeting(meeting.id);
await loadActive();
} catch (err: any) {
alert(err.message);
}
}
async function advance() {
if (!meeting) return;
try {
await api.advanceMeeting(meeting.id);
await loadActive();
} catch (err: any) {
alert(err.message);
}
}
const currentPhaseIdx = meeting ? PHASE_KEYS.indexOf(meeting.phase) : -1;
return (
<div>
{/* Phase Bar */}
<div style={styles.phaseBar}>
{PHASES.map((phase, i) => (
<div key={phase} style={{
...styles.phaseItem,
background: i === currentPhaseIdx ? '#8b5cf6' : i < currentPhaseIdx ? '#22c55e33' : '#1a1a2e',
color: i === currentPhaseIdx ? '#fff' : i < currentPhaseIdx ? '#22c55e' : '#7a7f96',
borderColor: i === currentPhaseIdx ? '#8b5cf6' : '#252540',
}}>
{phase}
</div>
))}
</div>
{/* Controls */}
<div style={styles.controls}>
{!meeting ? (
<>
<button style={{ ...styles.btn, background: '#8b5cf6' }} onClick={() => startMeeting('strategic')} disabled={loading}>
🏛️ Strategic
</button>
<button style={{ ...styles.btn, background: '#3b82f6' }} onClick={() => startMeeting('ops')} disabled={loading}>
⚙️ Ops
</button>
<button style={{ ...styles.btn, background: '#f59e0b', color: '#000' }} onClick={() => startMeeting('initiative')} disabled={loading}>
🚀 Initiative
</button>
<button style={{ ...styles.btn, background: '#ef4444' }} onClick={() => startMeeting('emergency')} disabled={loading}>
🚨 Emergency
</button>
</>
) : (
<>
<button style={{ ...styles.btn, background: '#6366f1' }} onClick={advance}>
⏭️ Next Phase
</button>
<button style={{ ...styles.btn, background: '#ef4444' }} onClick={halt}>
🛑 Halt
</button>
<span style={styles.meetingLabel}>
{meeting.meeting_type.toUpperCase()} — Phase: {PHASES[currentPhaseIdx] || meeting.phase}
</span>
</>
)}
</div>
<div style={styles.split}>
{/* Live Feed */}
<div style={styles.card}>
<h3 style={styles.cardTitle}>Live Feed</h3>
<div style={styles.feed}>
{messages.length === 0 && <div style={styles.empty}>No messages yet. Start a meeting.</div>}
{messages.map((msg, i) => (
<div key={i} style={styles.msg}>
<span style={styles.msgAgent}>{msg.agent_name}</span>
<span style={styles.msgText}>{msg.message}</span>
<span style={styles.msgTime}>{new Date(msg.created_at).toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit' })}</span>
</div>
))}
</div>
</div>
{/* Agenda Builder */}
<div style={styles.card}>
<h3 style={styles.cardTitle}>Agenda Builder</h3>
<AgendaBuilder onRun={(items: any[]) => startMeeting('strategic', items)} disabled={!!meeting} />
</div>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
phaseBar: { display: 'flex', gap: 4, marginBottom: 16, flexWrap: 'wrap' },
phaseItem: { padding: '8px 14px', borderRadius: 6, fontSize: 11, fontWeight: 600, border: '1px solid', transition: 'all 0.3s' },
controls: { display: 'flex', gap: 8, marginBottom: 20, alignItems: 'center', flexWrap: 'wrap' },
btn: { padding: '8px 16px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 13, cursor: 'pointer', fontFamily: 'inherit' },
meetingLabel: { fontSize: 13, fontWeight: 700, color: '#8b5cf6', marginLeft: 12 },
split: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 },
card: { background: '#1a1a2e', borderRadius: 12, padding: 20, border: '1px solid #252540' },
cardTitle: { margin: '0 0 12px', fontSize: 15, fontWeight: 700, color: '#e8eaf0' },
feed: { maxHeight: 400, overflowY: 'auto' as const },
empty: { color: '#7a7f96', fontSize: 13, padding: 20, textAlign: 'center' as const },
msg: { display: 'flex', gap: 8, padding: '6px 0', borderBottom: '1px solid #252540', fontSize: 12, alignItems: 'baseline' },
msgAgent: { fontWeight: 700, color: '#8b5cf6', minWidth: 80, flexShrink: 0 },
msgText: { flex: 1, color: '#e8eaf0' },
msgTime: { color: '#7a7f96', fontSize: 10, flexShrink: 0 },
};