File size: 5,011 Bytes
dec56a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2e28ca
 
 
 
 
 
 
 
dec56a6
 
 
f2e28ca
 
dec56a6
 
f2e28ca
dec56a6
 
 
 
f2e28ca
 
dec56a6
f2e28ca
 
dec56a6
f2e28ca
dec56a6
f2e28ca
dec56a6
 
 
 
 
 
 
f2e28ca
 
 
 
 
 
 
 
 
 
 
dec56a6
 
f2e28ca
dec56a6
 
f2e28ca
dec56a6
 
 
 
 
 
 
 
f2e28ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import clsx from 'clsx';
import { fmt } from '../utils/format.js';

function EventCard({ event }) {
  const sevClass = `s${event.severity}`;
  const cls = event.is_positive ? 'positive' : event.severity >= 4 ? 'critical' : '';
  return (
    <div className={clsx('event', cls)}>
      <div className="ev-head">
        <span className="ev-name">{event.name}</span>
        <span className={clsx('ev-sev', sevClass)}>SEV {event.severity}</span>
      </div>
      <div className="ev-narrative">{event.narrative}</div>
      <div className="ev-mults">
        {Object.entries(event.affected_sectors).map(([k, v]) => (
          <span key={k}>{k} ×{v.toFixed(2)}</span>
        ))}
      </div>
      {event.treasury_injection > 0 && (
        <div className="ev-injection">+{fmt(event.treasury_injection)} treasury injection</div>
      )}
    </div>
  );
}

function ProposalCard({ proposal, voteResult, votes, ownerDept }) {
  const status = voteResult?.status || 'PENDING';
  const cls = status === 'APPROVED' ? 'approved' : status.startsWith('REJ') || status.startsWith('AUTO') ? 'rejected' : '';
  const voteList = votes.filter(v => v.proposal_id === proposal.proposal_id);

  return (
    <div className={clsx('proposal', cls)}>
      <div className="p-head">
        <span className="p-name">{proposal.department}</span>
        <span className="p-amount">{fmt(proposal.amount)}</span>
      </div>
      <div className="p-just">{proposal.justification}</div>
      <div className="p-votes">
        {voteList.map(v => (
          <span
            key={`${v.proposal_id}-${v.agent_id}`}
            className={clsx('vote-chip', v.vote, { own: v.department === ownerDept })}
            title={v.department === ownerDept ? 'Self-vote (must abstain)' : `${v.department}: ${v.vote}`}
          >
            {v.department.slice(0, 3)}: {v.vote === 'ABSTAIN' ? '—' : v.vote === 'YES' ? '✓' : '✗'}
          </span>
        ))}
      </div>
      {voteResult && (
        <div className="p-tally">
          {status} · {voteResult.yes} yes / {voteResult.no} no / {voteResult.abstain} abstain
        </div>
      )}
    </div>
  );
}

export default function PhasePanel({ round }) {
  if (!round) return null;

  const events = round.events || [];
  const debate = round.debate || [];
  const proposals = round.proposals || [];
  const votes = round.votes || [];
  const voteResults = round.vote_results || [];
  const proposalOrder = round.proposal_order || [];
  const phase = round._phase; // only present on live partial rounds

  return (
    <div className="phase-panel">
      <div className="phase-block">
        <h4>Phase 1 — Events{phase === 'events' && <LiveDot />}</h4>
        {events.length === 0 ? (
          <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>No events this round (40% baseline).</div>
        ) : (
          events.map((e, i) => <EventCard key={`${round.round_num}-${i}`} event={e} />)
        )}
      </div>

      <div className="phase-block">
        <h4>Phase 2 — Debate{phase === 'debate' && <LiveDot />}</h4>
        {debate.length === 0 && phase !== 'debate' ? (
          <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>All ministers stayed silent.</div>
        ) : debate.length === 0 && phase === 'debate' ? (
          <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>Waiting for first minister to speak…</div>
        ) : (
          debate.map((m, i) => (
            <div key={i} className="debate-msg">
              <div className="who">{m.department || m.agent_id}</div>
              <div className="text">{m.message}</div>
            </div>
          ))
        )}
      </div>

      <div className="phase-block">
        <h4>Phase 3–4 — Proposals & Votes{(phase === 'proposals' || phase === 'votes') && <LiveDot />}</h4>
        {proposalOrder.length > 0 && (
          <div style={{ color: 'var(--text-mute)', fontSize: 11, marginBottom: 6 }}>
            rotating order: {proposalOrder.join(' → ')}
          </div>
        )}
        {proposals.length === 0 && (phase === 'proposals' || phase === 'votes') && (
          <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>Waiting for proposals…</div>
        )}
        {proposals.map(p => {
          const result = voteResults.find(v => v.proposal_id === p.proposal_id);
          return (
            <ProposalCard
              key={p.proposal_id || `p-${proposals.indexOf(p)}`}
              proposal={p}
              voteResult={result}
              votes={votes}
              ownerDept={p.department}
            />
          );
        })}
      </div>
    </div>
  );
}

function LiveDot() {
  return (
    <span
      style={{
        display: 'inline-block',
        width: 6,
        height: 6,
        borderRadius: '50%',
        background: 'var(--accent-bad)',
        marginLeft: 6,
        verticalAlign: 'middle',
        animation: 'liveBlink 1.4s ease-in-out infinite',
      }}
      title="Streaming live"
    />
  );
}