import React, { useMemo, useState } from 'react'; import { Box, Typography, Chip, LinearProgress, Collapse, Button, } from '@mui/material'; import { CheckCircle as CheckCircleIcon, WarningAmber as WarningAmberIcon, Error as ErrorIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, ArrowForward as ArrowForwardIcon, InfoOutlined as InfoIcon, } from '@mui/icons-material'; import { AgentEventItem } from '../../hooks/useAgentHuddleFeed'; interface CommitteeProposal { agent: string; title: string; pillar_id: string; priority: string; valid: boolean; accepted: boolean; reasoning?: string; rejected_reason?: string | null; estimated_time?: number; action_type?: string; } interface CommitteePayload { agents_polled: number; total_proposals: number; accepted_count: number; rejected_count: number; proposals: CommitteeProposal[]; } const PILLAR_ORDER = ['plan', 'generate', 'publish', 'analyze', 'engage', 'remarket']; const PILLAR_INFO: Record = { plan: { label: 'Plan', short: 'Plan', desc: 'Strategy & planning' }, generate: { label: 'Generate', short: 'Create', desc: 'Content creation' }, publish: { label: 'Publish', short: 'Pub.', desc: 'Publishing & scheduling' }, analyze: { label: 'Analyze', short: 'Audit', desc: 'Performance review' }, engage: { label: 'Engage', short: 'Share', desc: 'Social engagement' }, remarket: { label: 'Remarket', short: 'ReMkt', desc: 'Repurpose & promote' }, }; // ─── Status Banner ────────────────────────────────── const statusMeta = (accepted: number, total: number) => { const pct = total > 0 ? accepted / total : 0; if (pct >= 0.8) return { color: '#4caf50', bg: 'rgba(76,175,80,0.12)', icon: , text: 'All systems good' }; if (pct >= 0.5) return { color: '#ff9800', bg: 'rgba(255,152,0,0.12)', icon: , text: 'Needs review' }; return { color: '#f44336', bg: 'rgba(244,67,54,0.12)', icon: , text: 'Attention needed' }; }; const StatusBanner: React.FC<{ accepted: number; total: number; agents: number }> = ({ accepted, total, agents }) => { const meta = statusMeta(accepted, total); const pct = total > 0 ? Math.round(accepted / total * 100) : 0; return ( {meta.icon} {meta.text} — {accepted} of {total} proposals adopted from {agents} areas {pct}% ); }; // ─── Adoption Bar ─────────────────────────────────── const AdoptionBar: React.FC<{ accepted: number; total: number }> = ({ accepted, total }) => { const pct = total > 0 ? accepted / total * 100 : 0; const color = pct >= 80 ? '#4caf50' : pct >= 50 ? '#ff9800' : '#f44336'; return ( Adoption Adopted {accepted} of {total} proposals ); }; // ─── Coverage Flow ────────────────────────────────── const coverageHealth = (count: number): { color: string; label: string; dot: string } => { if (count >= 3) return { color: '#4caf50', label: 'covered', dot: '●' }; if (count >= 1) return { color: '#ff9800', label: 'light', dot: '◕' }; return { color: '#f44336', label: 'missing', dot: '○' }; }; const CoverageFlow: React.FC<{ proposals: CommitteeProposal[] }> = ({ proposals }) => { const counts = PILLAR_ORDER.map((p) => ({ ...PILLAR_INFO[p], key: p, count: proposals.filter((pr) => pr.pillar_id === p).length, })); return ( Today's Coverage {counts.map((p, i) => { const health = coverageHealth(p.count); return ( {p.short} {p.count} {health.label} {i < counts.length - 1 && ( )} ); })} ); }; // ─── Rejected List (redesigned) ───────────────────── const plainReason = (p: CommitteeProposal): string => { if (p.rejected_reason) return p.rejected_reason; if (!p.valid) return `"${p.pillar_id}" isn't a valid workflow phase — this is a system configuration issue.`; return 'This suggestion was similar to an existing task or had lower priority.'; }; const actionForProposal = (p: CommitteeProposal): { label: string; icon?: React.ReactNode } | null => { const title = p.title.toLowerCase(); if (title.includes('twitter') || title.includes('tweet')) { return { label: 'Connect Twitter' }; } if (title.includes('linkedin')) { return { label: 'Connect LinkedIn' }; } if (title.includes('facebook') || title.includes('instagram')) { return { label: 'Connect Social' }; } return null; }; const RejectedList: React.FC<{ proposals: CommitteeProposal[] }> = ({ proposals }) => { const rejected = proposals.filter((p) => !p.accepted); const [open, setOpen] = useState(false); if (rejected.length === 0) return null; return ( setOpen(!open)} sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', py: 0.5, borderRadius: 1, '&:hover': { bgcolor: 'rgba(255,255,255,0.04)' }, }} > suggestion{rejected.length > 1 ? 's' : ''} not included {open ? 'hide' : 'view'} {open ? : } {rejected.map((p, i) => { const action = actionForProposal(p); return ( “{p.title}” {plainReason(p)} {action && ( )} ); })} ); }; // ─── Agent Row (details section) ──────────────────── type AgentStatus = 'all_accepted' | 'partial' | 'all_rejected'; interface AgentSummary { name: string; total: number; accepted: number; status: AgentStatus; proposals: CommitteeProposal[]; } const agentStatusIcon = (s: AgentStatus) => { if (s === 'all_accepted') return ; if (s === 'partial') return ; return ; }; const agentStatusColor = (s: AgentStatus): 'success' | 'warning' | 'error' => { if (s === 'all_accepted') return 'success'; if (s === 'partial') return 'warning'; return 'error'; }; const AgentRow: React.FC<{ agent: AgentSummary; expanded: boolean; onToggle: () => void }> = ({ agent, expanded, onToggle }) => { const pct = agent.total > 0 ? agent.accepted / agent.total : 0; return ( {agentStatusIcon(agent.status)} {agent.name} {agent.accepted}/{agent.total} {expanded ? : } {agent.proposals.map((p, i) => ( {p.title} ))} ); }; // ─── Main Component ───────────────────────────────── const CommitteeSummary: React.FC<{ events: AgentEventItem[] }> = ({ events }) => { const [showDetails, setShowDetails] = useState(false); const [expandedAgent, setExpandedAgent] = useState(null); const meeting = useMemo(() => { const last = events.find((e) => e.event_type === 'committee_meeting'); if (!last?.payload) return null; return (typeof last.payload === 'string' ? JSON.parse(last.payload) : last.payload) as CommitteePayload; }, [events]); const agents = useMemo(() => { if (!meeting) return []; const map = new Map(); for (const p of meeting.proposals) { if (!map.has(p.agent)) map.set(p.agent, []); map.get(p.agent)!.push(p); } return Array.from(map.entries()).map(([name, proposals]) => { const accepted = proposals.filter((p) => p.accepted).length; const total = proposals.length; let status: AgentStatus = 'all_accepted'; if (accepted === 0) status = 'all_rejected'; else if (accepted < total) status = 'partial'; return { name, total, accepted, status, proposals }; }); }, [meeting]); if (!meeting) return null; const summaryLine = `ALwrity reviewed ${meeting.total_proposals} suggestions across ${meeting.agents_polled} areas of your content workflow and built today's plan from ${meeting.accepted_count} of them.`; return ( {/* Header + summary line */} Daily Committee Brief {summaryLine} {/* Status banner */} {/* Adoption bar */} {/* Coverage flow */} {/* Rejected proposals */} {/* Details section: agent-level breakdown */} Agent Breakdown {agents.map((agent) => ( setExpandedAgent(expandedAgent === agent.name ? null : agent.name)} /> ))} ); }; export default CommitteeSummary;