File size: 3,687 Bytes
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
// api.js
// ----------------------------------------------------------------------------
// Thin browser client for the FastAPI server in `server/visualizer_server.py`.
//
//   - `getModes()`           — list available inference modes (LLM + baselines).
//   - `getConfig()`          — sectors/baselines/etc. for the empty-state shell.
//   - `startRun(opts)`       — POST /api/runs and return the new {run_id, ...}.
//   - `streamRun(runId, on)` — open an EventSource and dispatch typed events.
//   - `getSnapshot(runId)`   — fetch the full episode-so-far (used on reload).
//
// The visualizer assumes Vite's dev proxy maps /api/* to the backend (see
// `vite.config.js`). In production builds the same path works as long as the
// FastAPI server is reverse-proxied under the same origin as the static UI.
// ----------------------------------------------------------------------------

const BASE = '/api';

async function jsonOrThrow(res) {
  if (!res.ok) {
    let message = `${res.status} ${res.statusText}`;
    try {
      const body = await res.json();
      if (body?.detail) message = typeof body.detail === 'string' ? body.detail : JSON.stringify(body.detail);
    } catch {
      /* empty body — fall back to status */
    }
    throw new Error(message);
  }
  return res.json();
}

export async function getHealth() {
  return jsonOrThrow(await fetch(`${BASE}/health`));
}

export async function getConfig() {
  return jsonOrThrow(await fetch(`${BASE}/config`));
}

export async function getModes() {
  return jsonOrThrow(await fetch(`${BASE}/modes`));
}

export async function listRuns() {
  return jsonOrThrow(await fetch(`${BASE}/runs`));
}

export async function startRun({
  mode,
  modelId = null,
  seed = 42,
  maxRounds = 20,
  temperature = 0.2,
}) {
  const res = await fetch(`${BASE}/runs`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      mode,
      model_id: modelId,
      seed,
      max_rounds: maxRounds,
      temperature,
    }),
  });
  return jsonOrThrow(res);
}

export async function getSnapshot(runId) {
  return jsonOrThrow(await fetch(`${BASE}/runs/${runId}/snapshot`));
}

/**
 * Open an SSE connection to a live run.
 *
 * `handlers` may contain any subset of:
 *   { onStart, onRound, onActivity, onSummary, onError, onDone, onConnectionError }
 *
 * `onActivity(data)` fires for each within-round action:
 *   data.kind = 'round_start' | 'debate' | 'proposal' | 'vote'
 *
 * Returns an object with a `close()` method to tear the connection down.
 */
export function streamRun(runId, handlers = {}) {
  const url = `${BASE}/runs/${runId}/stream`;
  const source = new EventSource(url);

  const route = (type, ev) => {
    let data = {};
    try {
      data = ev.data ? JSON.parse(ev.data) : {};
    } catch (err) {
      handlers.onConnectionError?.(err);
      return;
    }
    switch (type) {
      case 'start':
        handlers.onStart?.(data);
        break;
      case 'activity':
        handlers.onActivity?.(data);
        break;
      case 'round':
        handlers.onRound?.(data);
        break;
      case 'summary':
        handlers.onSummary?.(data);
        break;
      case 'error':
        handlers.onError?.(data);
        break;
      case 'done':
        handlers.onDone?.(data);
        source.close();
        break;
      default:
        break;
    }
  };

  ['start', 'activity', 'round', 'summary', 'error', 'done'].forEach(type => {
    source.addEventListener(type, e => route(type, e));
  });

  source.onerror = err => {
    handlers.onConnectionError?.(err);
  };

  return {
    close: () => source.close(),
    raw: source,
  };
}