@gravity-rail/sdk 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agui.cjs +78 -0
- package/dist/agui.cjs.map +1 -0
- package/dist/agui.d.cts +13 -0
- package/dist/agui.d.ts +13 -0
- package/dist/agui.js +48 -0
- package/dist/agui.js.map +1 -0
- package/dist/audio.cjs +223 -0
- package/dist/audio.cjs.map +1 -0
- package/dist/audio.d.cts +25 -0
- package/dist/audio.d.ts +25 -0
- package/dist/audio.js +147 -0
- package/dist/audio.js.map +1 -0
- package/dist/chunk-BSOOMHW6.js +62 -0
- package/dist/chunk-BSOOMHW6.js.map +1 -0
- package/dist/{chunk-XRKRVY46.js → chunk-PEWFZXNK.js} +194 -31
- package/dist/chunk-PEWFZXNK.js.map +1 -0
- package/dist/{index-BhUZfOAG.d.cts → client-CLxHFOeB.d.cts} +310 -1632
- package/dist/{index-fPswHAIF.d.ts → client-Dft3Kmkn.d.ts} +310 -1632
- package/dist/helpers.cjs +123 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +28 -0
- package/dist/helpers.d.ts +28 -0
- package/dist/helpers.js +92 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index-CeVDRf6l.d.cts +1607 -0
- package/dist/index-D1G8EKz2.d.ts +1607 -0
- package/dist/index.cjs +186 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/{inquiries-DiTDrTG2.d.cts → inquiries-BIlL6Obb.d.cts} +28 -3
- package/dist/{inquiries-DiTDrTG2.d.ts → inquiries-BIlL6Obb.d.ts} +28 -3
- package/dist/internal.cjs +195 -30
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +4 -3
- package/dist/internal.d.ts +4 -3
- package/dist/internal.js +5 -1
- package/dist/oauth-client.cjs +78 -0
- package/dist/oauth-client.cjs.map +1 -0
- package/dist/oauth-client.d.cts +15 -0
- package/dist/oauth-client.d.ts +15 -0
- package/dist/oauth-client.js +48 -0
- package/dist/oauth-client.js.map +1 -0
- package/dist/realtime.cjs +637 -0
- package/dist/realtime.cjs.map +1 -0
- package/dist/realtime.d.cts +94 -0
- package/dist/realtime.d.ts +94 -0
- package/dist/realtime.js +580 -0
- package/dist/realtime.js.map +1 -0
- package/dist/schemas.cjs +7 -3
- package/dist/schemas.cjs.map +1 -1
- package/dist/schemas.d.cts +28 -6
- package/dist/schemas.d.ts +28 -6
- package/dist/schemas.js +6 -3
- package/dist/schemas.js.map +1 -1
- package/dist/types-DkYyrvoC.d.cts +19 -0
- package/dist/types-DkYyrvoC.d.ts +19 -0
- package/package.json +26 -1
- package/dist/chunk-XRKRVY46.js.map +0 -1
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SERVER_SAMPLE_RATE,
|
|
3
|
+
TARGET_SAMPLE_RATE,
|
|
4
|
+
base64ToFloat32Array,
|
|
5
|
+
float32ArrayToBase64
|
|
6
|
+
} from "./chunk-BSOOMHW6.js";
|
|
7
|
+
|
|
8
|
+
// src/realtime/types.ts
|
|
9
|
+
function isActiveCallPhase(phase) {
|
|
10
|
+
return phase === "dialing" || phase === "live" || phase === "onHold";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/realtime/workspace-client.ts
|
|
14
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
15
|
+
var RECONNECT_DELAY_MS = 3e3;
|
|
16
|
+
var MAX_RECONNECT_ATTEMPTS = 5;
|
|
17
|
+
var CLOSE_DELAY_MS = 1e3;
|
|
18
|
+
function createSocketState() {
|
|
19
|
+
return {
|
|
20
|
+
ws: null,
|
|
21
|
+
connected: false,
|
|
22
|
+
connecting: false,
|
|
23
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
24
|
+
subscribedChannels: /* @__PURE__ */ new Set(),
|
|
25
|
+
reconnectAttempts: 0,
|
|
26
|
+
reconnectTimer: null,
|
|
27
|
+
heartbeatTimer: null,
|
|
28
|
+
closeTimer: null,
|
|
29
|
+
intentionalClose: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function buildWorkspaceWsUrl(session) {
|
|
33
|
+
const isSsl = session.apiUrl.startsWith("https");
|
|
34
|
+
const host = session.apiUrl.replace(/^https?:\/\//, "");
|
|
35
|
+
return `${isSsl ? "wss" : "ws"}://${host}/api/v2/w/${session.workspaceUuid}/ws/realtime`;
|
|
36
|
+
}
|
|
37
|
+
var WorkspaceRealtimeClient = class {
|
|
38
|
+
session = null;
|
|
39
|
+
getSessionFn;
|
|
40
|
+
state = createSocketState();
|
|
41
|
+
connectionListeners = /* @__PURE__ */ new Set();
|
|
42
|
+
constructor(config) {
|
|
43
|
+
if (typeof config === "function") {
|
|
44
|
+
this.getSessionFn = config;
|
|
45
|
+
} else {
|
|
46
|
+
this.getSessionFn = null;
|
|
47
|
+
if (config) {
|
|
48
|
+
this.session = config;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolveSession() {
|
|
53
|
+
return this.session ?? this.getSessionFn?.() ?? null;
|
|
54
|
+
}
|
|
55
|
+
notifyConnectionListeners() {
|
|
56
|
+
for (const listener of this.connectionListeners) {
|
|
57
|
+
listener(this.state.connected);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
clearCloseTimer() {
|
|
61
|
+
if (this.state.closeTimer) {
|
|
62
|
+
clearTimeout(this.state.closeTimer);
|
|
63
|
+
this.state.closeTimer = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
setSession(session) {
|
|
67
|
+
const changed = this.session?.accessToken !== session.accessToken || this.session?.workspaceUuid !== session.workspaceUuid || this.session?.apiUrl !== session.apiUrl;
|
|
68
|
+
this.session = session;
|
|
69
|
+
if (changed && this.state.ws) {
|
|
70
|
+
this.closeConnection();
|
|
71
|
+
}
|
|
72
|
+
if (this.state.listeners.size > 0) {
|
|
73
|
+
this.ensureConnection();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
subscribe(channel, listener) {
|
|
77
|
+
this.clearCloseTimer();
|
|
78
|
+
let listeners = this.state.listeners.get(channel);
|
|
79
|
+
if (!listeners) {
|
|
80
|
+
listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
this.state.listeners.set(channel, listeners);
|
|
82
|
+
}
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
this.ensureConnection();
|
|
85
|
+
if (this.state.connected && this.state.ws?.readyState === WebSocket.OPEN && !this.state.subscribedChannels.has(channel)) {
|
|
86
|
+
this.state.ws.send(JSON.stringify({ type: "subscribe", channels: [channel] }));
|
|
87
|
+
}
|
|
88
|
+
return () => {
|
|
89
|
+
const channelListeners = this.state.listeners.get(channel);
|
|
90
|
+
channelListeners?.delete(listener);
|
|
91
|
+
if (channelListeners && channelListeners.size === 0) {
|
|
92
|
+
this.state.listeners.delete(channel);
|
|
93
|
+
this.state.subscribedChannels.delete(channel);
|
|
94
|
+
if (this.state.connected && this.state.ws?.readyState === WebSocket.OPEN) {
|
|
95
|
+
this.state.ws.send(JSON.stringify({ type: "unsubscribe", channels: [channel] }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (this.state.listeners.size === 0) {
|
|
99
|
+
this.state.closeTimer = setTimeout(() => {
|
|
100
|
+
if (this.state.listeners.size === 0) {
|
|
101
|
+
this.closeConnection();
|
|
102
|
+
}
|
|
103
|
+
}, CLOSE_DELAY_MS);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
send(channel, data) {
|
|
108
|
+
if (!this.state.connected || !this.state.ws) return;
|
|
109
|
+
this.state.ws.send(JSON.stringify({ type: "message", channel, data }));
|
|
110
|
+
}
|
|
111
|
+
onConnection(listener) {
|
|
112
|
+
this.connectionListeners.add(listener);
|
|
113
|
+
listener(this.state.connected);
|
|
114
|
+
return () => {
|
|
115
|
+
this.connectionListeners.delete(listener);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
isConnected() {
|
|
119
|
+
return this.state.connected;
|
|
120
|
+
}
|
|
121
|
+
ensureConnection() {
|
|
122
|
+
const session = this.resolveSession();
|
|
123
|
+
if (!session) return;
|
|
124
|
+
if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
|
|
125
|
+
if (this.state.ws || this.state.connecting) return;
|
|
126
|
+
this.clearCloseTimer();
|
|
127
|
+
this.state.connecting = true;
|
|
128
|
+
this.state.intentionalClose = false;
|
|
129
|
+
const ws = new WebSocket(buildWorkspaceWsUrl(session));
|
|
130
|
+
ws.onopen = () => {
|
|
131
|
+
ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
|
|
132
|
+
};
|
|
133
|
+
ws.onmessage = (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const msg = JSON.parse(String(event.data));
|
|
136
|
+
if (msg.type === "protocol.init" && !this.state.connected) {
|
|
137
|
+
this.state.connected = true;
|
|
138
|
+
this.state.connecting = false;
|
|
139
|
+
this.state.reconnectAttempts = 0;
|
|
140
|
+
if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
|
|
141
|
+
this.state.heartbeatTimer = setInterval(() => {
|
|
142
|
+
if (this.state.ws?.readyState === WebSocket.OPEN) {
|
|
143
|
+
this.state.ws.send(JSON.stringify({ type: "heartbeat" }));
|
|
144
|
+
}
|
|
145
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
146
|
+
if (this.state.listeners.size > 0) {
|
|
147
|
+
ws.send(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
type: "subscribe",
|
|
150
|
+
channels: Array.from(this.state.listeners.keys())
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
this.notifyConnectionListeners();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (msg.type === "subscribed" && Array.isArray(msg.channels)) {
|
|
158
|
+
for (const ch of msg.channels) {
|
|
159
|
+
this.state.subscribedChannels.add(ch);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (msg.type === "unsubscribed" && Array.isArray(msg.channels)) {
|
|
164
|
+
for (const ch of msg.channels) {
|
|
165
|
+
this.state.subscribedChannels.delete(ch);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (msg.type === "event" && msg.channel && msg.event) {
|
|
170
|
+
const channelListeners = this.state.listeners.get(msg.channel);
|
|
171
|
+
if (channelListeners) {
|
|
172
|
+
for (const channelListener of channelListeners) {
|
|
173
|
+
try {
|
|
174
|
+
channelListener(msg.event);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error("Channel listener error:", err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
ws.onclose = () => {
|
|
185
|
+
this.state.connected = false;
|
|
186
|
+
this.state.connecting = false;
|
|
187
|
+
this.state.ws = null;
|
|
188
|
+
this.state.subscribedChannels.clear();
|
|
189
|
+
if (this.state.heartbeatTimer) {
|
|
190
|
+
clearInterval(this.state.heartbeatTimer);
|
|
191
|
+
this.state.heartbeatTimer = null;
|
|
192
|
+
}
|
|
193
|
+
this.notifyConnectionListeners();
|
|
194
|
+
if (!this.state.intentionalClose && this.state.listeners.size > 0 && this.state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
195
|
+
this.state.reconnectAttempts += 1;
|
|
196
|
+
this.state.reconnectTimer = setTimeout(() => {
|
|
197
|
+
this.ensureConnection();
|
|
198
|
+
}, RECONNECT_DELAY_MS * this.state.reconnectAttempts);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
ws.onerror = () => {
|
|
202
|
+
};
|
|
203
|
+
this.state.ws = ws;
|
|
204
|
+
}
|
|
205
|
+
closeConnection() {
|
|
206
|
+
this.state.intentionalClose = true;
|
|
207
|
+
this.clearCloseTimer();
|
|
208
|
+
if (this.state.reconnectTimer) {
|
|
209
|
+
clearTimeout(this.state.reconnectTimer);
|
|
210
|
+
this.state.reconnectTimer = null;
|
|
211
|
+
}
|
|
212
|
+
if (this.state.heartbeatTimer) {
|
|
213
|
+
clearInterval(this.state.heartbeatTimer);
|
|
214
|
+
this.state.heartbeatTimer = null;
|
|
215
|
+
}
|
|
216
|
+
this.state.ws?.close();
|
|
217
|
+
this.state.ws = null;
|
|
218
|
+
this.state.connected = false;
|
|
219
|
+
this.state.connecting = false;
|
|
220
|
+
this.state.subscribedChannels.clear();
|
|
221
|
+
this.notifyConnectionListeners();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var defaultClient = new WorkspaceRealtimeClient();
|
|
225
|
+
function initWorkspaceSocket(session) {
|
|
226
|
+
defaultClient.setSession(session);
|
|
227
|
+
}
|
|
228
|
+
function subscribeWorkspaceChannel(channel, listener) {
|
|
229
|
+
return defaultClient.subscribe(channel, listener);
|
|
230
|
+
}
|
|
231
|
+
function onWorkspaceSocketConnection(listener) {
|
|
232
|
+
return defaultClient.onConnection(listener);
|
|
233
|
+
}
|
|
234
|
+
function isWorkspaceSocketConnected() {
|
|
235
|
+
return defaultClient.isConnected();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/realtime/operator-session-client.ts
|
|
239
|
+
var OperatorSessionClient = class {
|
|
240
|
+
constructor(handlers = {}) {
|
|
241
|
+
this.handlers = handlers;
|
|
242
|
+
}
|
|
243
|
+
ws = null;
|
|
244
|
+
audioContext = null;
|
|
245
|
+
mediaStream = null;
|
|
246
|
+
processor = null;
|
|
247
|
+
sessionState = "idle";
|
|
248
|
+
callPhase = "idle";
|
|
249
|
+
nextPlaybackTime = 0;
|
|
250
|
+
playbackSources = [];
|
|
251
|
+
micActive = false;
|
|
252
|
+
onHold = false;
|
|
253
|
+
receivedRemoteAudio = false;
|
|
254
|
+
heartbeatTimer = null;
|
|
255
|
+
sessionCloseNotified = false;
|
|
256
|
+
intentionalDisconnect = false;
|
|
257
|
+
getSessionState() {
|
|
258
|
+
return this.sessionState;
|
|
259
|
+
}
|
|
260
|
+
getCallPhase() {
|
|
261
|
+
return this.callPhase;
|
|
262
|
+
}
|
|
263
|
+
setSessionState(next) {
|
|
264
|
+
this.sessionState = next;
|
|
265
|
+
this.handlers.onSessionStateChange?.(next);
|
|
266
|
+
}
|
|
267
|
+
setCallPhase(next) {
|
|
268
|
+
this.callPhase = next;
|
|
269
|
+
this.handlers.onCallPhaseChange?.(next);
|
|
270
|
+
}
|
|
271
|
+
notifySessionClosed(reason) {
|
|
272
|
+
if (this.sessionCloseNotified) return;
|
|
273
|
+
this.sessionCloseNotified = true;
|
|
274
|
+
this.handlers.onSessionClosed?.(reason);
|
|
275
|
+
}
|
|
276
|
+
async connect(session, chatId) {
|
|
277
|
+
if (typeof window === "undefined" || typeof WebSocket === "undefined") {
|
|
278
|
+
throw new Error("Operator session WebSocket is not available in this environment");
|
|
279
|
+
}
|
|
280
|
+
await this.disconnect();
|
|
281
|
+
this.sessionCloseNotified = false;
|
|
282
|
+
this.setSessionState("connecting");
|
|
283
|
+
this.setCallPhase("connecting");
|
|
284
|
+
const isSsl = session.apiUrl.startsWith("https");
|
|
285
|
+
const host = session.apiUrl.replace(/^https?:\/\//, "");
|
|
286
|
+
const wsUrl = `${isSsl ? "wss" : "ws"}://${host}/api/v2/w/${session.workspaceUuid}/operator/chats/${chatId}/session-ws`;
|
|
287
|
+
await new Promise((resolve, reject) => {
|
|
288
|
+
const ws = new WebSocket(wsUrl);
|
|
289
|
+
this.ws = ws;
|
|
290
|
+
ws.onopen = () => {
|
|
291
|
+
ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
|
|
292
|
+
};
|
|
293
|
+
ws.onmessage = (event) => {
|
|
294
|
+
try {
|
|
295
|
+
const msg = JSON.parse(String(event.data));
|
|
296
|
+
this.handleMessage(msg, resolve, reject);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
ws.onerror = () => {
|
|
301
|
+
const message = "Operator session WebSocket error";
|
|
302
|
+
const wasConnecting = this.sessionState === "connecting";
|
|
303
|
+
this.setSessionState("error");
|
|
304
|
+
if (wasConnecting) {
|
|
305
|
+
this.setCallPhase("idle");
|
|
306
|
+
reject(new Error(message));
|
|
307
|
+
} else {
|
|
308
|
+
this.setCallPhase("failed");
|
|
309
|
+
this.handlers.onError?.(message);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
ws.onclose = () => {
|
|
313
|
+
this.clearHeartbeat();
|
|
314
|
+
this.stopMicrophone();
|
|
315
|
+
this.stopPlayback();
|
|
316
|
+
const wasConnected = this.sessionState === "connected";
|
|
317
|
+
this.setSessionState("idle");
|
|
318
|
+
this.setCallPhase("idle");
|
|
319
|
+
if (wasConnected && !this.intentionalDisconnect) {
|
|
320
|
+
this.notifySessionClosed("disconnect");
|
|
321
|
+
}
|
|
322
|
+
this.intentionalDisconnect = false;
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
handleMessage(msg, resolveLogin, rejectLogin) {
|
|
327
|
+
if (msg.type === "RUN_ERROR" || msg.type === "error") {
|
|
328
|
+
const message = msg.message ?? "Operator session error";
|
|
329
|
+
if (this.sessionState === "connecting") {
|
|
330
|
+
this.setSessionState("error");
|
|
331
|
+
rejectLogin(new Error(message));
|
|
332
|
+
} else {
|
|
333
|
+
this.setCallPhase("failed");
|
|
334
|
+
this.handlers.onError?.(message);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (msg.type === "protocol.init" && this.sessionState === "connecting") {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (msg.type === "CUSTOM" && msg.name === "operator.session_ready") {
|
|
342
|
+
this.setSessionState("connected");
|
|
343
|
+
this.setCallPhase("ready");
|
|
344
|
+
this.startHeartbeat();
|
|
345
|
+
resolveLogin();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (msg.type === "event" && msg.event) {
|
|
349
|
+
if (this.handleRemoteVoiceAudio(msg.event)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.handlers.onAguiEvent?.(msg.event);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (msg.type === "CUSTOM" && msg.name === "operator.call.started") {
|
|
356
|
+
this.receivedRemoteAudio = false;
|
|
357
|
+
this.setCallPhase("dialing");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (msg.type === "CUSTOM" && msg.name === "operator.call.ended") {
|
|
361
|
+
this.onHold = false;
|
|
362
|
+
this.receivedRemoteAudio = false;
|
|
363
|
+
this.stopMicrophone();
|
|
364
|
+
this.stopPlayback();
|
|
365
|
+
const endSession = msg.value?.end_session === true;
|
|
366
|
+
const reason = typeof msg.value?.reason === "string" ? msg.value.reason : void 0;
|
|
367
|
+
if (endSession) {
|
|
368
|
+
this.setCallPhase("idle");
|
|
369
|
+
this.notifySessionClosed(reason ?? "peer_hangup");
|
|
370
|
+
} else {
|
|
371
|
+
this.setCallPhase("ready");
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Play callee PCM from wrapped chat-stream events; returns true if handled. */
|
|
377
|
+
handleRemoteVoiceAudio(event) {
|
|
378
|
+
if (event.type !== "CUSTOM" || event.name !== "voice.audio") {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
if (this.onHold) return true;
|
|
382
|
+
const value = event.value;
|
|
383
|
+
if (!value || typeof value !== "object") return true;
|
|
384
|
+
const data = value.data;
|
|
385
|
+
if (typeof data !== "string" || !data) return true;
|
|
386
|
+
if (!this.receivedRemoteAudio && (this.callPhase === "dialing" || this.callPhase === "live")) {
|
|
387
|
+
this.receivedRemoteAudio = true;
|
|
388
|
+
this.setCallPhase("live");
|
|
389
|
+
}
|
|
390
|
+
void this.playPcm16(base64ToFloat32Array(data));
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
startHeartbeat() {
|
|
394
|
+
this.clearHeartbeat();
|
|
395
|
+
this.heartbeatTimer = setInterval(() => {
|
|
396
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
397
|
+
this.ws.send(JSON.stringify({ type: "heartbeat" }));
|
|
398
|
+
}
|
|
399
|
+
}, 3e4);
|
|
400
|
+
}
|
|
401
|
+
clearHeartbeat() {
|
|
402
|
+
if (this.heartbeatTimer) {
|
|
403
|
+
clearInterval(this.heartbeatTimer);
|
|
404
|
+
this.heartbeatTimer = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
sendCustom(name, value = {}) {
|
|
408
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
409
|
+
throw new Error("Operator session WebSocket is not connected");
|
|
410
|
+
}
|
|
411
|
+
this.ws.send(JSON.stringify({ type: "CUSTOM", name, value }));
|
|
412
|
+
}
|
|
413
|
+
async placeCall(memberId) {
|
|
414
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
415
|
+
throw new Error("Operator session WebSocket is not connected");
|
|
416
|
+
}
|
|
417
|
+
if (this.sessionState !== "connected") {
|
|
418
|
+
throw new Error("Operator session is not ready");
|
|
419
|
+
}
|
|
420
|
+
await this.startMicrophone();
|
|
421
|
+
this.sendCustom("operator.call.place", { member_id: memberId });
|
|
422
|
+
}
|
|
423
|
+
hangUp() {
|
|
424
|
+
try {
|
|
425
|
+
this.sendCustom("operator.call.hangup");
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
this.onHold = false;
|
|
429
|
+
this.receivedRemoteAudio = false;
|
|
430
|
+
}
|
|
431
|
+
forwardCall(phoneNumber) {
|
|
432
|
+
this.receivedRemoteAudio = false;
|
|
433
|
+
this.setCallPhase("dialing");
|
|
434
|
+
this.sendCustom("operator.call.forward", { phone_number: phoneNumber });
|
|
435
|
+
}
|
|
436
|
+
setHold(onHold) {
|
|
437
|
+
this.onHold = onHold;
|
|
438
|
+
this.sendCustom("operator.call.hold", { on_hold: onHold });
|
|
439
|
+
if (this.callPhase === "live" || this.callPhase === "onHold") {
|
|
440
|
+
this.setCallPhase(onHold ? "onHold" : "live");
|
|
441
|
+
}
|
|
442
|
+
if (onHold) {
|
|
443
|
+
this.stopPlayback();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async startMicrophone() {
|
|
447
|
+
if (typeof window === "undefined") return;
|
|
448
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
449
|
+
throw new Error("Microphone not available in this browser");
|
|
450
|
+
}
|
|
451
|
+
if (this.micActive) return;
|
|
452
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
453
|
+
audio: {
|
|
454
|
+
echoCancellation: true,
|
|
455
|
+
noiseSuppression: true,
|
|
456
|
+
autoGainControl: true
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
460
|
+
if (!Ctx) throw new Error("Web Audio not supported");
|
|
461
|
+
this.audioContext = new Ctx();
|
|
462
|
+
if (this.audioContext.state === "suspended") {
|
|
463
|
+
await this.audioContext.resume();
|
|
464
|
+
}
|
|
465
|
+
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
466
|
+
const processor = this.audioContext.createScriptProcessor(4096, 1, 1);
|
|
467
|
+
this.processor = processor;
|
|
468
|
+
processor.onaudioprocess = (event) => {
|
|
469
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
470
|
+
if (this.onHold) return;
|
|
471
|
+
if (this.callPhase !== "dialing" && this.callPhase !== "live" && this.callPhase !== "onHold") {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const input = event.inputBuffer.getChannelData(0);
|
|
475
|
+
const copy = new Float32Array(input.length);
|
|
476
|
+
copy.set(input);
|
|
477
|
+
const sampleRate = this.audioContext?.sampleRate ?? TARGET_SAMPLE_RATE;
|
|
478
|
+
let pcm = copy;
|
|
479
|
+
if (sampleRate !== TARGET_SAMPLE_RATE) {
|
|
480
|
+
const ratio = sampleRate / TARGET_SAMPLE_RATE;
|
|
481
|
+
const newLen = Math.floor(copy.length / ratio);
|
|
482
|
+
const down = new Float32Array(newLen);
|
|
483
|
+
for (let i = 0; i < newLen; i += 1) {
|
|
484
|
+
down[i] = copy[Math.floor(i * ratio)];
|
|
485
|
+
}
|
|
486
|
+
pcm = down;
|
|
487
|
+
}
|
|
488
|
+
this.ws.send(
|
|
489
|
+
JSON.stringify({
|
|
490
|
+
type: "CUSTOM",
|
|
491
|
+
name: "voice.audio",
|
|
492
|
+
value: {
|
|
493
|
+
data: float32ArrayToBase64(pcm),
|
|
494
|
+
format: "pcm16",
|
|
495
|
+
sample_rate: TARGET_SAMPLE_RATE
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
};
|
|
500
|
+
source.connect(processor);
|
|
501
|
+
processor.connect(this.audioContext.destination);
|
|
502
|
+
this.micActive = true;
|
|
503
|
+
}
|
|
504
|
+
stopMicrophone() {
|
|
505
|
+
this.processor?.disconnect();
|
|
506
|
+
this.processor = null;
|
|
507
|
+
for (const track of this.mediaStream?.getTracks() ?? []) {
|
|
508
|
+
track.stop();
|
|
509
|
+
}
|
|
510
|
+
this.mediaStream = null;
|
|
511
|
+
this.micActive = false;
|
|
512
|
+
}
|
|
513
|
+
async playPcm16(samples) {
|
|
514
|
+
if (typeof window === "undefined") return;
|
|
515
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
516
|
+
if (!Ctx) return;
|
|
517
|
+
if (!this.audioContext || this.audioContext.state === "closed") {
|
|
518
|
+
this.audioContext = new Ctx();
|
|
519
|
+
}
|
|
520
|
+
if (this.audioContext.state === "suspended") {
|
|
521
|
+
await this.audioContext.resume();
|
|
522
|
+
}
|
|
523
|
+
const buffer = this.audioContext.createBuffer(1, samples.length, SERVER_SAMPLE_RATE);
|
|
524
|
+
buffer.copyToChannel(new Float32Array(samples), 0);
|
|
525
|
+
const source = this.audioContext.createBufferSource();
|
|
526
|
+
source.buffer = buffer;
|
|
527
|
+
source.connect(this.audioContext.destination);
|
|
528
|
+
const now = this.audioContext.currentTime;
|
|
529
|
+
const startAt = this.nextPlaybackTime < now + 0.02 ? now + 0.02 : this.nextPlaybackTime;
|
|
530
|
+
source.start(startAt);
|
|
531
|
+
this.nextPlaybackTime = startAt + buffer.duration;
|
|
532
|
+
this.playbackSources.push(source);
|
|
533
|
+
source.onended = () => {
|
|
534
|
+
const idx = this.playbackSources.indexOf(source);
|
|
535
|
+
if (idx >= 0) this.playbackSources.splice(idx, 1);
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
stopPlayback() {
|
|
539
|
+
for (const src of this.playbackSources) {
|
|
540
|
+
try {
|
|
541
|
+
src.stop();
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
this.playbackSources = [];
|
|
546
|
+
this.nextPlaybackTime = 0;
|
|
547
|
+
}
|
|
548
|
+
async disconnect() {
|
|
549
|
+
this.intentionalDisconnect = true;
|
|
550
|
+
if (this.callPhase === "dialing" || this.callPhase === "live" || this.callPhase === "onHold") {
|
|
551
|
+
this.hangUp();
|
|
552
|
+
}
|
|
553
|
+
this.clearHeartbeat();
|
|
554
|
+
this.stopMicrophone();
|
|
555
|
+
this.stopPlayback();
|
|
556
|
+
if (this.ws) {
|
|
557
|
+
this.ws.close();
|
|
558
|
+
this.ws = null;
|
|
559
|
+
}
|
|
560
|
+
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
561
|
+
await this.audioContext.close().catch(() => void 0);
|
|
562
|
+
}
|
|
563
|
+
this.audioContext = null;
|
|
564
|
+
this.onHold = false;
|
|
565
|
+
this.receivedRemoteAudio = false;
|
|
566
|
+
this.setSessionState("idle");
|
|
567
|
+
this.setCallPhase("idle");
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
export {
|
|
571
|
+
OperatorSessionClient,
|
|
572
|
+
WorkspaceRealtimeClient,
|
|
573
|
+
buildWorkspaceWsUrl,
|
|
574
|
+
initWorkspaceSocket,
|
|
575
|
+
isActiveCallPhase,
|
|
576
|
+
isWorkspaceSocketConnected,
|
|
577
|
+
onWorkspaceSocketConnection,
|
|
578
|
+
subscribeWorkspaceChannel
|
|
579
|
+
};
|
|
580
|
+
//# sourceMappingURL=realtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/realtime/types.ts","../src/realtime/workspace-client.ts","../src/realtime/operator-session-client.ts"],"sourcesContent":["/** Event received from a subscribed workspace realtime channel. */\nexport interface ChannelEvent {\n type: string;\n [key: string]: unknown;\n}\n\nexport type ChannelListener = (event: ChannelEvent) => void;\nexport type Unsubscribe = () => void;\n\n/** Auth context for workspace realtime WebSocket connections. */\nexport interface RealtimeAuthSession {\n apiUrl: string;\n workspaceUuid: string;\n accessToken: string;\n}\n\nexport type OperatorSessionState = 'idle' | 'connecting' | 'connected' | 'error';\n\nexport type OperatorCallPhase =\n | 'idle'\n | 'connecting'\n | 'ready'\n | 'dialing'\n | 'live'\n | 'onHold'\n | 'ended'\n | 'failed';\n\n/** Phases where Hold / Forward / Hang up apply to an active phone leg. */\nexport function isActiveCallPhase(phase: OperatorCallPhase): boolean {\n return phase === 'dialing' || phase === 'live' || phase === 'onHold';\n}\n","/**\n * Unified workspace WebSocket client for real-time events.\n *\n * Mirrors `/api/v2/w/{wid}/ws/realtime`: login with OAuth access token,\n * subscribe to channels, receive AGUI events.\n *\n * Protocol:\n * Client → Server: subscribe, unsubscribe, heartbeat, message\n * Server → Client: protocol.init, subscribed, unsubscribed, event, error\n */\n\nimport type { ChannelEvent, ChannelListener, RealtimeAuthSession, Unsubscribe } from './types';\n\nconst HEARTBEAT_INTERVAL_MS = 30_000;\nconst RECONNECT_DELAY_MS = 3_000;\nconst MAX_RECONNECT_ATTEMPTS = 5;\nconst CLOSE_DELAY_MS = 1_000;\n\ninterface SocketState {\n ws: WebSocket | null;\n connected: boolean;\n connecting: boolean;\n listeners: Map<string, Set<ChannelListener>>;\n subscribedChannels: Set<string>;\n reconnectAttempts: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n heartbeatTimer: ReturnType<typeof setInterval> | null;\n closeTimer: ReturnType<typeof setTimeout> | null;\n intentionalClose: boolean;\n}\n\nfunction createSocketState(): SocketState {\n return {\n ws: null,\n connected: false,\n connecting: false,\n listeners: new Map(),\n subscribedChannels: new Set(),\n reconnectAttempts: 0,\n reconnectTimer: null,\n heartbeatTimer: null,\n closeTimer: null,\n intentionalClose: false,\n };\n}\n\n/** Build the workspace realtime WebSocket URL from session config. */\nexport function buildWorkspaceWsUrl(session: RealtimeAuthSession): string {\n const isSsl = session.apiUrl.startsWith('https');\n const host = session.apiUrl.replace(/^https?:\\/\\//, '');\n return `${isSsl ? 'wss' : 'ws'}://${host}/api/v2/w/${session.workspaceUuid}/ws/realtime`;\n}\n\nexport class WorkspaceRealtimeClient {\n private session: RealtimeAuthSession | null = null;\n private readonly getSessionFn: (() => RealtimeAuthSession | null) | null;\n private readonly state: SocketState = createSocketState();\n private readonly connectionListeners = new Set<(connected: boolean) => void>();\n\n constructor(config?: RealtimeAuthSession | (() => RealtimeAuthSession | null)) {\n if (typeof config === 'function') {\n this.getSessionFn = config;\n } else {\n this.getSessionFn = null;\n if (config) {\n this.session = config;\n }\n }\n }\n\n private resolveSession(): RealtimeAuthSession | null {\n return this.session ?? this.getSessionFn?.() ?? null;\n }\n\n private notifyConnectionListeners(): void {\n for (const listener of this.connectionListeners) {\n listener(this.state.connected);\n }\n }\n\n private clearCloseTimer(): void {\n if (this.state.closeTimer) {\n clearTimeout(this.state.closeTimer);\n this.state.closeTimer = null;\n }\n }\n\n setSession(session: RealtimeAuthSession): void {\n const changed =\n this.session?.accessToken !== session.accessToken ||\n this.session?.workspaceUuid !== session.workspaceUuid ||\n this.session?.apiUrl !== session.apiUrl;\n\n this.session = session;\n if (changed && this.state.ws) {\n this.closeConnection();\n }\n if (this.state.listeners.size > 0) {\n this.ensureConnection();\n }\n }\n\n subscribe(channel: string, listener: ChannelListener): Unsubscribe {\n this.clearCloseTimer();\n\n let listeners = this.state.listeners.get(channel);\n if (!listeners) {\n listeners = new Set();\n this.state.listeners.set(channel, listeners);\n }\n listeners.add(listener);\n\n this.ensureConnection();\n\n if (\n this.state.connected &&\n this.state.ws?.readyState === WebSocket.OPEN &&\n !this.state.subscribedChannels.has(channel)\n ) {\n this.state.ws.send(JSON.stringify({ type: 'subscribe', channels: [channel] }));\n }\n\n return () => {\n const channelListeners = this.state.listeners.get(channel);\n channelListeners?.delete(listener);\n if (channelListeners && channelListeners.size === 0) {\n this.state.listeners.delete(channel);\n this.state.subscribedChannels.delete(channel);\n if (this.state.connected && this.state.ws?.readyState === WebSocket.OPEN) {\n this.state.ws.send(JSON.stringify({ type: 'unsubscribe', channels: [channel] }));\n }\n }\n\n if (this.state.listeners.size === 0) {\n this.state.closeTimer = setTimeout(() => {\n if (this.state.listeners.size === 0) {\n this.closeConnection();\n }\n }, CLOSE_DELAY_MS);\n }\n };\n }\n\n send(channel: string, data: unknown): void {\n if (!this.state.connected || !this.state.ws) return;\n this.state.ws.send(JSON.stringify({ type: 'message', channel, data }));\n }\n\n onConnection(listener: (connected: boolean) => void): () => void {\n this.connectionListeners.add(listener);\n listener(this.state.connected);\n return () => {\n this.connectionListeners.delete(listener);\n };\n }\n\n isConnected(): boolean {\n return this.state.connected;\n }\n\n private ensureConnection(): void {\n const session = this.resolveSession();\n if (!session) return;\n if (typeof window === 'undefined' || typeof WebSocket === 'undefined') return;\n if (this.state.ws || this.state.connecting) return;\n\n this.clearCloseTimer();\n this.state.connecting = true;\n this.state.intentionalClose = false;\n\n const ws = new WebSocket(buildWorkspaceWsUrl(session));\n\n ws.onopen = () => {\n ws.send(JSON.stringify({ type: 'login', token: session.accessToken }));\n };\n\n ws.onmessage = (event: MessageEvent) => {\n try {\n const msg = JSON.parse(String(event.data)) as {\n type?: string;\n channels?: string[];\n channel?: string;\n event?: ChannelEvent;\n };\n\n if (msg.type === 'protocol.init' && !this.state.connected) {\n this.state.connected = true;\n this.state.connecting = false;\n this.state.reconnectAttempts = 0;\n\n if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);\n this.state.heartbeatTimer = setInterval(() => {\n if (this.state.ws?.readyState === WebSocket.OPEN) {\n this.state.ws.send(JSON.stringify({ type: 'heartbeat' }));\n }\n }, HEARTBEAT_INTERVAL_MS);\n\n if (this.state.listeners.size > 0) {\n ws.send(\n JSON.stringify({\n type: 'subscribe',\n channels: Array.from(this.state.listeners.keys()),\n })\n );\n }\n\n this.notifyConnectionListeners();\n return;\n }\n\n if (msg.type === 'subscribed' && Array.isArray(msg.channels)) {\n for (const ch of msg.channels) {\n this.state.subscribedChannels.add(ch);\n }\n return;\n }\n\n if (msg.type === 'unsubscribed' && Array.isArray(msg.channels)) {\n for (const ch of msg.channels) {\n this.state.subscribedChannels.delete(ch);\n }\n return;\n }\n\n if (msg.type === 'event' && msg.channel && msg.event) {\n const channelListeners = this.state.listeners.get(msg.channel);\n if (channelListeners) {\n for (const channelListener of channelListeners) {\n try {\n channelListener(msg.event);\n } catch (err) {\n console.error('Channel listener error:', err);\n }\n }\n }\n }\n } catch {\n // Ignore malformed frames.\n }\n };\n\n ws.onclose = () => {\n this.state.connected = false;\n this.state.connecting = false;\n this.state.ws = null;\n this.state.subscribedChannels.clear();\n\n if (this.state.heartbeatTimer) {\n clearInterval(this.state.heartbeatTimer);\n this.state.heartbeatTimer = null;\n }\n\n this.notifyConnectionListeners();\n\n if (\n !this.state.intentionalClose &&\n this.state.listeners.size > 0 &&\n this.state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS\n ) {\n this.state.reconnectAttempts += 1;\n this.state.reconnectTimer = setTimeout(() => {\n this.ensureConnection();\n }, RECONNECT_DELAY_MS * this.state.reconnectAttempts);\n }\n };\n\n ws.onerror = () => {\n // onclose will fire after this\n };\n\n this.state.ws = ws;\n }\n\n private closeConnection(): void {\n this.state.intentionalClose = true;\n this.clearCloseTimer();\n if (this.state.reconnectTimer) {\n clearTimeout(this.state.reconnectTimer);\n this.state.reconnectTimer = null;\n }\n if (this.state.heartbeatTimer) {\n clearInterval(this.state.heartbeatTimer);\n this.state.heartbeatTimer = null;\n }\n this.state.ws?.close();\n this.state.ws = null;\n this.state.connected = false;\n this.state.connecting = false;\n this.state.subscribedChannels.clear();\n this.notifyConnectionListeners();\n }\n}\n\nconst defaultClient = new WorkspaceRealtimeClient();\n\nexport function initWorkspaceSocket(session: RealtimeAuthSession): void {\n defaultClient.setSession(session);\n}\n\nexport function subscribeWorkspaceChannel(channel: string, listener: ChannelListener): Unsubscribe {\n return defaultClient.subscribe(channel, listener);\n}\n\nexport function onWorkspaceSocketConnection(listener: (connected: boolean) => void): () => void {\n return defaultClient.onConnection(listener);\n}\n\nexport function isWorkspaceSocketConnected(): boolean {\n return defaultClient.isConnected();\n}\n","/**\n * Human operator session WebSocket client.\n *\n * Connects to `/api/v2/w/{wid}/operator/chats/{chatId}/session-ws` — AGUI\n * events, bidirectional PCM, and call control. No AI inference.\n */\n\nimport {\n base64ToFloat32Array,\n float32ArrayToBase64,\n SERVER_SAMPLE_RATE,\n TARGET_SAMPLE_RATE,\n} from '../audio/pcm';\nimport type {\n ChannelEvent,\n OperatorCallPhase,\n OperatorSessionState,\n RealtimeAuthSession,\n} from './types';\n\nexport interface OperatorSessionHandlers {\n onSessionStateChange?: (state: OperatorSessionState) => void;\n onCallPhaseChange?: (phase: OperatorCallPhase) => void;\n onAguiEvent?: (event: ChannelEvent) => void;\n onError?: (message: string) => void;\n /** Fired when the server closes the session (e.g. callee hung up). */\n onSessionClosed?: (reason?: string) => void;\n}\n\nexport class OperatorSessionClient {\n private ws: WebSocket | null = null;\n private audioContext: AudioContext | null = null;\n private mediaStream: MediaStream | null = null;\n private processor: ScriptProcessorNode | null = null;\n private sessionState: OperatorSessionState = 'idle';\n private callPhase: OperatorCallPhase = 'idle';\n private nextPlaybackTime = 0;\n private playbackSources: AudioBufferSourceNode[] = [];\n private micActive = false;\n private onHold = false;\n private receivedRemoteAudio = false;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private sessionCloseNotified = false;\n private intentionalDisconnect = false;\n\n constructor(private handlers: OperatorSessionHandlers = {}) {}\n\n getSessionState(): OperatorSessionState {\n return this.sessionState;\n }\n\n getCallPhase(): OperatorCallPhase {\n return this.callPhase;\n }\n\n private setSessionState(next: OperatorSessionState): void {\n this.sessionState = next;\n this.handlers.onSessionStateChange?.(next);\n }\n\n private setCallPhase(next: OperatorCallPhase): void {\n this.callPhase = next;\n this.handlers.onCallPhaseChange?.(next);\n }\n\n private notifySessionClosed(reason?: string): void {\n if (this.sessionCloseNotified) return;\n this.sessionCloseNotified = true;\n this.handlers.onSessionClosed?.(reason);\n }\n\n async connect(session: RealtimeAuthSession, chatId: number): Promise<void> {\n if (typeof window === 'undefined' || typeof WebSocket === 'undefined') {\n throw new Error('Operator session WebSocket is not available in this environment');\n }\n\n await this.disconnect();\n this.sessionCloseNotified = false;\n this.setSessionState('connecting');\n this.setCallPhase('connecting');\n\n const isSsl = session.apiUrl.startsWith('https');\n const host = session.apiUrl.replace(/^https?:\\/\\//, '');\n const wsUrl = `${isSsl ? 'wss' : 'ws'}://${host}/api/v2/w/${session.workspaceUuid}/operator/chats/${chatId}/session-ws`;\n\n await new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(wsUrl);\n this.ws = ws;\n\n ws.onopen = () => {\n ws.send(JSON.stringify({ type: 'login', token: session.accessToken }));\n };\n\n ws.onmessage = (event) => {\n try {\n const msg = JSON.parse(String(event.data)) as {\n type?: string;\n name?: string;\n value?: Record<string, unknown>;\n message?: string;\n code?: string;\n event?: ChannelEvent;\n };\n this.handleMessage(msg, resolve, reject);\n } catch {\n /* ignore malformed frames */\n }\n };\n\n ws.onerror = () => {\n const message = 'Operator session WebSocket error';\n const wasConnecting = this.sessionState === 'connecting';\n this.setSessionState('error');\n if (wasConnecting) {\n this.setCallPhase('idle');\n reject(new Error(message));\n } else {\n this.setCallPhase('failed');\n this.handlers.onError?.(message);\n }\n };\n\n ws.onclose = () => {\n this.clearHeartbeat();\n this.stopMicrophone();\n this.stopPlayback();\n const wasConnected = this.sessionState === 'connected';\n this.setSessionState('idle');\n this.setCallPhase('idle');\n if (wasConnected && !this.intentionalDisconnect) {\n this.notifySessionClosed('disconnect');\n }\n this.intentionalDisconnect = false;\n };\n });\n }\n\n private handleMessage(\n msg: {\n type?: string;\n name?: string;\n value?: Record<string, unknown>;\n message?: string;\n code?: string;\n event?: ChannelEvent;\n },\n resolveLogin: () => void,\n rejectLogin: (err: Error) => void\n ): void {\n if (msg.type === 'RUN_ERROR' || msg.type === 'error') {\n const message = msg.message ?? 'Operator session error';\n if (this.sessionState === 'connecting') {\n this.setSessionState('error');\n rejectLogin(new Error(message));\n } else {\n this.setCallPhase('failed');\n this.handlers.onError?.(message);\n }\n return;\n }\n\n if (msg.type === 'protocol.init' && this.sessionState === 'connecting') {\n return;\n }\n\n if (msg.type === 'CUSTOM' && msg.name === 'operator.session_ready') {\n this.setSessionState('connected');\n this.setCallPhase('ready');\n this.startHeartbeat();\n resolveLogin();\n return;\n }\n\n if (msg.type === 'event' && msg.event) {\n if (this.handleRemoteVoiceAudio(msg.event)) {\n return;\n }\n this.handlers.onAguiEvent?.(msg.event);\n return;\n }\n\n if (msg.type === 'CUSTOM' && msg.name === 'operator.call.started') {\n this.receivedRemoteAudio = false;\n this.setCallPhase('dialing');\n return;\n }\n\n if (msg.type === 'CUSTOM' && msg.name === 'operator.call.ended') {\n this.onHold = false;\n this.receivedRemoteAudio = false;\n this.stopMicrophone();\n this.stopPlayback();\n const endSession = msg.value?.end_session === true;\n const reason = typeof msg.value?.reason === 'string' ? msg.value.reason : undefined;\n if (endSession) {\n this.setCallPhase('idle');\n this.notifySessionClosed(reason ?? 'peer_hangup');\n } else {\n this.setCallPhase('ready');\n }\n return;\n }\n }\n\n /** Play callee PCM from wrapped chat-stream events; returns true if handled. */\n private handleRemoteVoiceAudio(event: ChannelEvent): boolean {\n if (event.type !== 'CUSTOM' || event.name !== 'voice.audio') {\n return false;\n }\n if (this.onHold) return true;\n\n const value = event.value;\n if (!value || typeof value !== 'object') return true;\n\n const data = (value as Record<string, unknown>).data;\n if (typeof data !== 'string' || !data) return true;\n\n if (\n !this.receivedRemoteAudio &&\n (this.callPhase === 'dialing' || this.callPhase === 'live')\n ) {\n this.receivedRemoteAudio = true;\n this.setCallPhase('live');\n }\n void this.playPcm16(base64ToFloat32Array(data));\n return true;\n }\n\n private startHeartbeat(): void {\n this.clearHeartbeat();\n this.heartbeatTimer = setInterval(() => {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({ type: 'heartbeat' }));\n }\n }, 30_000);\n }\n\n private clearHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n private sendCustom(name: string, value: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n throw new Error('Operator session WebSocket is not connected');\n }\n this.ws.send(JSON.stringify({ type: 'CUSTOM', name, value }));\n }\n\n async placeCall(memberId: number): Promise<void> {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n throw new Error('Operator session WebSocket is not connected');\n }\n if (this.sessionState !== 'connected') {\n throw new Error('Operator session is not ready');\n }\n await this.startMicrophone();\n this.sendCustom('operator.call.place', { member_id: memberId });\n }\n\n hangUp(): void {\n try {\n this.sendCustom('operator.call.hangup');\n } catch {\n /* socket already closed */\n }\n this.onHold = false;\n this.receivedRemoteAudio = false;\n }\n\n forwardCall(phoneNumber: string): void {\n this.receivedRemoteAudio = false;\n this.setCallPhase('dialing');\n this.sendCustom('operator.call.forward', { phone_number: phoneNumber });\n }\n\n setHold(onHold: boolean): void {\n this.onHold = onHold;\n this.sendCustom('operator.call.hold', { on_hold: onHold });\n if (this.callPhase === 'live' || this.callPhase === 'onHold') {\n this.setCallPhase(onHold ? 'onHold' : 'live');\n }\n if (onHold) {\n this.stopPlayback();\n }\n }\n\n async startMicrophone(): Promise<void> {\n if (typeof window === 'undefined') return;\n if (!navigator.mediaDevices?.getUserMedia) {\n throw new Error('Microphone not available in this browser');\n }\n if (this.micActive) return;\n\n this.mediaStream = await navigator.mediaDevices.getUserMedia({\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n\n const Ctx =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) throw new Error('Web Audio not supported');\n\n this.audioContext = new Ctx();\n if (this.audioContext.state === 'suspended') {\n await this.audioContext.resume();\n }\n\n const source = this.audioContext.createMediaStreamSource(this.mediaStream);\n const processor = this.audioContext.createScriptProcessor(4096, 1, 1);\n this.processor = processor;\n\n processor.onaudioprocess = (event) => {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n if (this.onHold) return;\n if (\n this.callPhase !== 'dialing' &&\n this.callPhase !== 'live' &&\n this.callPhase !== 'onHold'\n ) {\n return;\n }\n\n const input = event.inputBuffer.getChannelData(0);\n const copy = new Float32Array(input.length);\n copy.set(input);\n\n const sampleRate = this.audioContext?.sampleRate ?? TARGET_SAMPLE_RATE;\n let pcm = copy;\n if (sampleRate !== TARGET_SAMPLE_RATE) {\n const ratio = sampleRate / TARGET_SAMPLE_RATE;\n const newLen = Math.floor(copy.length / ratio);\n const down = new Float32Array(newLen);\n for (let i = 0; i < newLen; i += 1) {\n down[i] = copy[Math.floor(i * ratio)];\n }\n pcm = down;\n }\n\n this.ws.send(\n JSON.stringify({\n type: 'CUSTOM',\n name: 'voice.audio',\n value: {\n data: float32ArrayToBase64(pcm),\n format: 'pcm16',\n sample_rate: TARGET_SAMPLE_RATE,\n },\n })\n );\n };\n\n source.connect(processor);\n processor.connect(this.audioContext.destination);\n this.micActive = true;\n }\n\n stopMicrophone(): void {\n this.processor?.disconnect();\n this.processor = null;\n for (const track of this.mediaStream?.getTracks() ?? []) {\n track.stop();\n }\n this.mediaStream = null;\n this.micActive = false;\n }\n\n private async playPcm16(samples: Float32Array): Promise<void> {\n if (typeof window === 'undefined') return;\n\n const Ctx =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) return;\n\n if (!this.audioContext || this.audioContext.state === 'closed') {\n this.audioContext = new Ctx();\n }\n if (this.audioContext.state === 'suspended') {\n await this.audioContext.resume();\n }\n\n const buffer = this.audioContext.createBuffer(1, samples.length, SERVER_SAMPLE_RATE);\n buffer.copyToChannel(new Float32Array(samples), 0);\n\n const source = this.audioContext.createBufferSource();\n source.buffer = buffer;\n source.connect(this.audioContext.destination);\n\n const now = this.audioContext.currentTime;\n const startAt = this.nextPlaybackTime < now + 0.02 ? now + 0.02 : this.nextPlaybackTime;\n source.start(startAt);\n this.nextPlaybackTime = startAt + buffer.duration;\n this.playbackSources.push(source);\n source.onended = () => {\n const idx = this.playbackSources.indexOf(source);\n if (idx >= 0) this.playbackSources.splice(idx, 1);\n };\n }\n\n private stopPlayback(): void {\n for (const src of this.playbackSources) {\n try {\n src.stop();\n } catch {\n /* already stopped */\n }\n }\n this.playbackSources = [];\n this.nextPlaybackTime = 0;\n }\n\n async disconnect(): Promise<void> {\n this.intentionalDisconnect = true;\n if (\n this.callPhase === 'dialing' ||\n this.callPhase === 'live' ||\n this.callPhase === 'onHold'\n ) {\n this.hangUp();\n }\n this.clearHeartbeat();\n this.stopMicrophone();\n this.stopPlayback();\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n if (this.audioContext && this.audioContext.state !== 'closed') {\n await this.audioContext.close().catch(() => undefined);\n }\n this.audioContext = null;\n this.onHold = false;\n this.receivedRemoteAudio = false;\n this.setSessionState('idle');\n this.setCallPhase('idle');\n }\n}\n"],"mappings":"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","names":[]}
|