← 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 },
};