@pinecall/web 0.1.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.
@@ -0,0 +1,554 @@
1
+ 'use strict';
2
+
3
+ // src/core/VoiceSession.ts
4
+ var INITIAL_STATE = {
5
+ status: "idle",
6
+ error: null,
7
+ isMuted: false,
8
+ phase: "idle",
9
+ userSpeaking: false,
10
+ agentSpeaking: false,
11
+ duration: 0,
12
+ messages: [],
13
+ toolCalls: [],
14
+ idleWarning: null
15
+ };
16
+ var VoiceSession = class extends EventTarget {
17
+ constructor(opts) {
18
+ super();
19
+ this.opts = opts;
20
+ }
21
+ opts;
22
+ state = { ...INITIAL_STATE };
23
+ listeners = /* @__PURE__ */ new Set();
24
+ pc = null;
25
+ stream = null;
26
+ audio = null;
27
+ dc = null;
28
+ timer = null;
29
+ ping = null;
30
+ startedAt = 0;
31
+ botWords = {};
32
+ /** Read-only snapshot of current state (stable reference until next mutation). */
33
+ getState() {
34
+ return this.state;
35
+ }
36
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
37
+ subscribe(listener) {
38
+ this.listeners.add(listener);
39
+ return () => {
40
+ this.listeners.delete(listener);
41
+ };
42
+ }
43
+ setState(patch) {
44
+ const prev = this.state;
45
+ this.state = { ...prev, ...patch };
46
+ for (const l of this.listeners) l();
47
+ if (patch.status !== void 0 && patch.status !== prev.status) {
48
+ this.dispatchEvent(
49
+ new CustomEvent("status", { detail: { status: this.state.status } })
50
+ );
51
+ }
52
+ if (patch.phase !== void 0 && patch.phase !== prev.phase) {
53
+ this.dispatchEvent(
54
+ new CustomEvent("phase", { detail: { phase: this.state.phase } })
55
+ );
56
+ }
57
+ if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
58
+ this.dispatchEvent(
59
+ new CustomEvent("error", { detail: { error: this.state.error } })
60
+ );
61
+ }
62
+ this.dispatchEvent(
63
+ new CustomEvent("change", { detail: { state: this.state } })
64
+ );
65
+ }
66
+ setMessages(updater) {
67
+ const next = updater(this.state.messages);
68
+ this.setState({ messages: next });
69
+ const last = next[next.length - 1];
70
+ if (last) {
71
+ this.dispatchEvent(
72
+ new CustomEvent("message", { detail: { message: last } })
73
+ );
74
+ }
75
+ }
76
+ cleanup() {
77
+ if (this.ping) {
78
+ clearInterval(this.ping);
79
+ this.ping = null;
80
+ }
81
+ if (this.timer) {
82
+ clearInterval(this.timer);
83
+ this.timer = null;
84
+ }
85
+ if (this.pc) {
86
+ this.pc.close();
87
+ this.pc = null;
88
+ }
89
+ this.dc = null;
90
+ if (this.stream) {
91
+ this.stream.getTracks().forEach((t) => t.stop());
92
+ this.stream = null;
93
+ }
94
+ if (this.audio) {
95
+ this.audio.pause();
96
+ this.audio.srcObject = null;
97
+ this.audio = null;
98
+ }
99
+ this.botWords = {};
100
+ this.setState({
101
+ isMuted: false,
102
+ phase: "idle",
103
+ userSpeaking: false,
104
+ agentSpeaking: false,
105
+ idleWarning: null
106
+ });
107
+ }
108
+ async connect() {
109
+ if (this.pc) return;
110
+ try {
111
+ this.setState({
112
+ status: "connecting",
113
+ error: null,
114
+ duration: 0,
115
+ messages: []
116
+ });
117
+ this.botWords = {};
118
+ const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
119
+ /\/$/,
120
+ ""
121
+ );
122
+ let token;
123
+ let voiceServer;
124
+ if (this.opts.tokenProvider) {
125
+ const t = await this.opts.tokenProvider();
126
+ token = t.token;
127
+ voiceServer = t.server;
128
+ } else {
129
+ const tRes = await fetch(
130
+ `${base}/webrtc/token?agent_id=${encodeURIComponent(this.opts.agent)}`
131
+ );
132
+ if (!tRes.ok) throw new Error(`Token: ${tRes.status}`);
133
+ const t = await tRes.json();
134
+ token = t.token;
135
+ voiceServer = t.server;
136
+ }
137
+ if (!voiceServer) throw new Error("Token response missing server URL");
138
+ let ice = [{ urls: "stun:stun.l.google.com:19302" }];
139
+ try {
140
+ const r = await fetch(`${voiceServer}/webrtc/ice-servers`);
141
+ if (r.ok) {
142
+ const d = await r.json();
143
+ ice = d.iceServers || d.ice_servers || ice;
144
+ }
145
+ } catch {
146
+ }
147
+ this.stream = await navigator.mediaDevices.getUserMedia({
148
+ audio: {
149
+ echoCancellation: true,
150
+ noiseSuppression: true,
151
+ autoGainControl: true
152
+ },
153
+ video: false
154
+ });
155
+ const pc = new RTCPeerConnection({ iceServers: ice });
156
+ this.pc = pc;
157
+ this.stream.getTracks().forEach((t) => pc.addTrack(t, this.stream));
158
+ pc.ontrack = (e) => {
159
+ if (!this.audio) {
160
+ this.audio = new Audio();
161
+ this.audio.autoplay = true;
162
+ }
163
+ this.audio.srcObject = e.streams[0];
164
+ };
165
+ const dc = pc.createDataChannel("events", { ordered: true });
166
+ this.dc = dc;
167
+ dc.onopen = () => {
168
+ this.ping = setInterval(() => {
169
+ if (dc.readyState === "open") dc.send("ping");
170
+ }, 1e3);
171
+ };
172
+ dc.onmessage = (msg) => this.handleDataChannelMessage(msg);
173
+ pc.onconnectionstatechange = () => {
174
+ if (pc.connectionState === "connected") {
175
+ this.setState({ status: "connected", phase: "listening" });
176
+ this.startedAt = Date.now();
177
+ this.timer = setInterval(() => {
178
+ this.setState({
179
+ duration: Math.floor((Date.now() - this.startedAt) / 1e3)
180
+ });
181
+ }, 1e3);
182
+ } else if (pc.connectionState === "disconnected" || pc.connectionState === "failed") {
183
+ this.cleanup();
184
+ this.setState({ status: "idle" });
185
+ }
186
+ };
187
+ const offer = await pc.createOffer({
188
+ offerToReceiveAudio: true,
189
+ offerToReceiveVideo: false
190
+ });
191
+ await pc.setLocalDescription(offer);
192
+ await new Promise((resolve) => {
193
+ if (pc.iceGatheringState === "complete") return resolve();
194
+ const t = setTimeout(resolve, 2e3);
195
+ pc.onicegatheringstatechange = () => {
196
+ if (pc.iceGatheringState === "complete") {
197
+ clearTimeout(t);
198
+ resolve();
199
+ }
200
+ };
201
+ });
202
+ const offerBody = {
203
+ sdp: pc.localDescription.sdp,
204
+ type: pc.localDescription.type,
205
+ token
206
+ };
207
+ if (this.opts.config && Object.keys(this.opts.config).length > 0) {
208
+ offerBody.config = this.opts.config;
209
+ }
210
+ if (this.opts.metadata && Object.keys(this.opts.metadata).length > 0) {
211
+ offerBody.metadata = this.opts.metadata;
212
+ }
213
+ const res = await fetch(`${voiceServer}/webrtc/offer`, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify(offerBody)
217
+ });
218
+ if (!res.ok) throw new Error(`Offer: ${res.status}`);
219
+ const answer = await res.json();
220
+ await pc.setRemoteDescription({ type: answer.type, sdp: answer.sdp });
221
+ } catch (err) {
222
+ this.setState({
223
+ error: err instanceof Error ? err.message : String(err),
224
+ status: "error"
225
+ });
226
+ this.cleanup();
227
+ }
228
+ }
229
+ handleDataChannelMessage(msg) {
230
+ let d;
231
+ try {
232
+ d = JSON.parse(msg.data);
233
+ } catch {
234
+ return;
235
+ }
236
+ switch (d.event) {
237
+ // ── User speech (STT) ──
238
+ case "speech.started":
239
+ this.setState({ userSpeaking: true, idleWarning: null });
240
+ break;
241
+ case "speech.ended":
242
+ this.setState({ userSpeaking: false });
243
+ break;
244
+ case "user.speaking":
245
+ if (d.text) {
246
+ this.setMessages((prev) => {
247
+ const idx = prev.findLastIndex(
248
+ (m) => m.role === "user" && m.isInterim
249
+ );
250
+ if (idx >= 0) {
251
+ return prev.map(
252
+ (m, i) => i === idx ? { ...m, text: d.text } : m
253
+ );
254
+ }
255
+ return [
256
+ ...prev,
257
+ {
258
+ id: Date.now(),
259
+ role: "user",
260
+ text: d.text,
261
+ isInterim: true
262
+ }
263
+ ];
264
+ });
265
+ }
266
+ this.setState({ phase: "listening", userSpeaking: true });
267
+ break;
268
+ case "user.message":
269
+ if (d.text) {
270
+ this.setMessages((prev) => {
271
+ const idx = prev.findLastIndex(
272
+ (m) => m.role === "user" && m.isInterim
273
+ );
274
+ if (idx >= 0) {
275
+ return prev.map(
276
+ (m, i) => i === idx ? { ...m, text: d.text, isInterim: false } : m
277
+ );
278
+ }
279
+ return [
280
+ ...prev,
281
+ {
282
+ id: Date.now(),
283
+ role: "user",
284
+ text: d.text,
285
+ isInterim: false
286
+ }
287
+ ];
288
+ });
289
+ }
290
+ this.setState({ userSpeaking: false, phase: "thinking" });
291
+ break;
292
+ // ── Turn detection ──
293
+ case "turn.pause":
294
+ this.setState({ phase: "pause" });
295
+ break;
296
+ case "turn.end":
297
+ this.setState({ phase: "thinking", userSpeaking: false });
298
+ break;
299
+ case "turn.resumed":
300
+ this.setState({ phase: "listening" });
301
+ break;
302
+ // ── Bot speech (TTS word-by-word) ──
303
+ case "bot.speaking":
304
+ if (d.message_id) {
305
+ this.botWords[d.message_id] = [];
306
+ }
307
+ break;
308
+ case "bot.word":
309
+ if (d.message_id && d.word) {
310
+ const ref = this.botWords;
311
+ if (!ref[d.message_id]) ref[d.message_id] = [];
312
+ const idx = d.word_index ?? ref[d.message_id].length;
313
+ ref[d.message_id][idx] = d.word;
314
+ const newText = ref[d.message_id].filter(Boolean).join(" ");
315
+ this.setMessages((prev) => {
316
+ const mi = prev.findIndex((m) => m.messageId === d.message_id);
317
+ if (mi >= 0)
318
+ return prev.map(
319
+ (m, i) => i === mi ? { ...m, text: newText } : m
320
+ );
321
+ return [
322
+ ...prev,
323
+ {
324
+ id: Date.now(),
325
+ role: "bot",
326
+ text: newText,
327
+ messageId: d.message_id,
328
+ speaking: true
329
+ }
330
+ ];
331
+ });
332
+ this.setState({ agentSpeaking: true, phase: "speaking" });
333
+ }
334
+ break;
335
+ case "bot.finished":
336
+ if (d.message_id) {
337
+ this.setMessages((prev) => {
338
+ const msg2 = prev.find((m) => m.messageId === d.message_id);
339
+ if (msg2 && !msg2.text && !d.text) {
340
+ return prev.filter((m) => m.messageId !== d.message_id);
341
+ }
342
+ return prev.map(
343
+ (m) => m.messageId === d.message_id ? { ...m, speaking: false, ...d.text ? { text: d.text } : {} } : m
344
+ );
345
+ });
346
+ }
347
+ this.setState({ agentSpeaking: false, phase: "listening" });
348
+ break;
349
+ case "bot.interrupted":
350
+ if (d.message_id) {
351
+ this.setMessages(
352
+ (prev) => prev.map(
353
+ (m) => m.messageId === d.message_id ? { ...m, speaking: false, interrupted: true } : m
354
+ )
355
+ );
356
+ }
357
+ this.setState({ agentSpeaking: false, phase: "listening" });
358
+ break;
359
+ // ── Audio metrics ──
360
+ case "audio.metrics":
361
+ if (d.source === "user" && d.is_speech !== void 0) {
362
+ this.setState({ userSpeaking: d.is_speech });
363
+ }
364
+ break;
365
+ // ── Session limits ──
366
+ case "session.idle_warning":
367
+ this.setState({ idleWarning: d.remaining_seconds ?? 0 });
368
+ break;
369
+ case "session.timeout":
370
+ this.disconnect();
371
+ break;
372
+ // ── Tool events (server-side LLM) ──
373
+ case "llm.tool_call": {
374
+ if (d.tool_calls?.length) {
375
+ this.setMessages((prev) => [
376
+ ...prev,
377
+ ...d.tool_calls.map((tc) => ({
378
+ id: Date.now() + Math.random(),
379
+ role: "system",
380
+ text: `\u{1F527} Using ${tc.name}\u2026`,
381
+ toolCallId: tc.id
382
+ }))
383
+ ]);
384
+ const tracked = this.opts.trackedTools;
385
+ const newEntries = d.tool_calls.filter((tc) => !tracked || tracked.includes(tc.name)).map((tc) => {
386
+ let args = {};
387
+ try {
388
+ args = typeof tc.arguments === "string" ? JSON.parse(tc.arguments) : tc.arguments ?? {};
389
+ } catch {
390
+ }
391
+ return {
392
+ toolCallId: tc.id,
393
+ name: tc.name,
394
+ arguments: args,
395
+ timestamp: Date.now()
396
+ };
397
+ });
398
+ if (newEntries.length) {
399
+ this.setState({
400
+ toolCalls: [...this.state.toolCalls, ...newEntries]
401
+ });
402
+ }
403
+ }
404
+ break;
405
+ }
406
+ case "llm.tool_result": {
407
+ if (d.tool_call_id) {
408
+ const sysMsg = this.state.messages.find(
409
+ (m) => m.toolCallId === d.tool_call_id
410
+ );
411
+ const toolName = (d.name || sysMsg?.text?.match(/Using (\S+)/)?.[1] || "Tool").replace(/…$/, "");
412
+ this.setMessages(
413
+ (prev2) => prev2.map(
414
+ (m) => m.toolCallId === d.tool_call_id ? { ...m, text: `\u2713 ${toolName}` } : m
415
+ )
416
+ );
417
+ const prev = this.state.toolCalls;
418
+ const idx = prev.findIndex(
419
+ (t) => t.toolCallId === d.tool_call_id
420
+ );
421
+ if (idx >= 0) {
422
+ let parsed = d.result;
423
+ if (typeof parsed === "string") {
424
+ try {
425
+ parsed = JSON.parse(parsed);
426
+ } catch {
427
+ }
428
+ }
429
+ const updated = prev.map(
430
+ (t, i) => i === idx ? { ...t, result: parsed } : t
431
+ );
432
+ this.setState({ toolCalls: updated });
433
+ }
434
+ }
435
+ break;
436
+ }
437
+ }
438
+ this.dispatchEvent(new CustomEvent("event", { detail: d }));
439
+ }
440
+ disconnect() {
441
+ this.cleanup();
442
+ this.setState({ status: "idle" });
443
+ }
444
+ toggleMute() {
445
+ this.setMuted(!this.state.isMuted);
446
+ }
447
+ setMuted(muted) {
448
+ const stream = this.stream;
449
+ if (!stream) return;
450
+ stream.getAudioTracks().forEach((t) => {
451
+ t.enabled = !muted;
452
+ });
453
+ const dc = this.dc;
454
+ if (dc && dc.readyState === "open") {
455
+ dc.send(JSON.stringify({ action: muted ? "mute" : "unmute" }));
456
+ }
457
+ this.setState({ isMuted: muted });
458
+ }
459
+ /**
460
+ * Send a configuration update via DataChannel during an active call.
461
+ * Use this for mid-call language/voice/STT switching.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * session.configure({ voice: "coral", stt: "deepgram", language: "es" });
466
+ * ```
467
+ */
468
+ configure(config) {
469
+ const dc = this.dc;
470
+ if (dc && dc.readyState === "open") {
471
+ dc.send(JSON.stringify({ action: "configure", ...config }));
472
+ }
473
+ }
474
+ /**
475
+ * Inject text into the conversation as if the user spoke it.
476
+ *
477
+ * Use this for click-based interactions in tool UIs (e.g., selecting a
478
+ * calendar slot). The server routes the text to the LLM, producing the
479
+ * same effect as the user speaking.
480
+ *
481
+ * @example
482
+ * ```ts
483
+ * session.sendText("I'd like the 10:00 AM slot");
484
+ * ```
485
+ */
486
+ sendText(text) {
487
+ const dc = this.dc;
488
+ if (dc && dc.readyState === "open") {
489
+ dc.send(JSON.stringify({ action: "inject_text", text }));
490
+ }
491
+ }
492
+ /**
493
+ * Remove a tool UI entry from state.
494
+ *
495
+ * Call this after the user interacts with a tool UI (e.g., selects a slot)
496
+ * to dismiss the rendered component from the transcript.
497
+ */
498
+ dismissTool(toolCallId) {
499
+ this.setState({
500
+ toolCalls: this.state.toolCalls.filter(
501
+ (t) => t.toolCallId !== toolCallId
502
+ )
503
+ });
504
+ }
505
+ /**
506
+ * Set or clear a keyed context block in the LLM system prompt.
507
+ *
508
+ * Use this to inject dynamic UI state (form data, selections, etc.)
509
+ * into the agent's prompt so it can see what the user is doing on screen.
510
+ * Each key is a named section — setting the same key replaces its value.
511
+ * Pass `null` to remove a context key.
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * // Inject form state so the agent sees what's filled
516
+ * session.setContext("contact_form", JSON.stringify({
517
+ * name: "John",
518
+ * email: "john@example.com",
519
+ * phone: "",
520
+ * }));
521
+ *
522
+ * // Clear when form is submitted
523
+ * session.setContext("contact_form", null);
524
+ * ```
525
+ */
526
+ setContext(key, value) {
527
+ const dc = this.dc;
528
+ if (dc && dc.readyState === "open") {
529
+ dc.send(JSON.stringify({ action: "set_context", key, value }));
530
+ }
531
+ }
532
+ /**
533
+ * Update session options before the next `connect()` call.
534
+ * Has no effect on an already-connected session — use `configure()` for that.
535
+ */
536
+ updateOptions(patch) {
537
+ if (patch.config !== void 0) {
538
+ this.opts = { ...this.opts, config: patch.config };
539
+ }
540
+ if (patch.metadata !== void 0) {
541
+ this.opts = { ...this.opts, metadata: patch.metadata };
542
+ }
543
+ }
544
+ /** Tear down the session and clear subscribers. After this, do not reuse. */
545
+ destroy() {
546
+ this.cleanup();
547
+ this.setState({ status: "idle" });
548
+ this.listeners.clear();
549
+ }
550
+ };
551
+
552
+ exports.VoiceSession = VoiceSession;
553
+ //# sourceMappingURL=index.cjs.map
554
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/VoiceSession.ts"],"names":["msg","prev"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * VoiceSession — Framework-agnostic WebRTC voice client for Pinecall agents.\n *\n * Extends EventTarget. Two ways to consume:\n * 1. session.subscribe(cb) + session.getState() — for React useSyncExternalStore\n * 2. session.addEventListener('status' | 'phase' | 'message' | 'error' | 'event' | 'change', cb)\n *\n * WebRTC flow: token → ICE → mic → PeerConnection → DataChannel → SDP offer/answer\n */\nimport type {\n VoiceSessionState,\n VoiceSessionOptions,\n TranscriptMessage,\n ToolUI,\n} from \"./types\";\n\nconst INITIAL_STATE: VoiceSessionState = {\n status: \"idle\",\n error: null,\n isMuted: false,\n phase: \"idle\",\n userSpeaking: false,\n agentSpeaking: false,\n duration: 0,\n messages: [],\n toolCalls: [],\n idleWarning: null,\n};\n\nexport class VoiceSession extends EventTarget {\n private state: VoiceSessionState = { ...INITIAL_STATE };\n private listeners = new Set<() => void>();\n\n private pc: RTCPeerConnection | null = null;\n private stream: MediaStream | null = null;\n private audio: HTMLAudioElement | null = null;\n private dc: RTCDataChannel | null = null;\n private timer: ReturnType<typeof setInterval> | null = null;\n private ping: ReturnType<typeof setInterval> | null = null;\n private startedAt = 0;\n private botWords: Record<string, string[]> = {};\n\n constructor(private opts: VoiceSessionOptions) {\n super();\n }\n\n /** Read-only snapshot of current state (stable reference until next mutation). */\n getState(): Readonly<VoiceSessionState> {\n return this.state;\n }\n\n /** Subscribe to ANY state change (for React useSyncExternalStore). */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private setState(patch: Partial<VoiceSessionState>): void {\n const prev = this.state;\n this.state = { ...prev, ...patch };\n for (const l of this.listeners) l();\n\n if (patch.status !== undefined && patch.status !== prev.status) {\n this.dispatchEvent(\n new CustomEvent(\"status\", { detail: { status: this.state.status } }),\n );\n }\n if (patch.phase !== undefined && patch.phase !== prev.phase) {\n this.dispatchEvent(\n new CustomEvent(\"phase\", { detail: { phase: this.state.phase } }),\n );\n }\n if (\n patch.error !== undefined &&\n patch.error !== null &&\n patch.error !== prev.error\n ) {\n this.dispatchEvent(\n new CustomEvent(\"error\", { detail: { error: this.state.error } }),\n );\n }\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { state: this.state } }),\n );\n }\n\n private setMessages(\n updater: (prev: TranscriptMessage[]) => TranscriptMessage[],\n ): void {\n const next = updater(this.state.messages);\n this.setState({ messages: next });\n // Emit message-level event for the last touched message (best effort).\n const last = next[next.length - 1];\n if (last) {\n this.dispatchEvent(\n new CustomEvent(\"message\", { detail: { message: last } }),\n );\n }\n }\n\n private cleanup(): void {\n if (this.ping) {\n clearInterval(this.ping);\n this.ping = null;\n }\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n if (this.pc) {\n this.pc.close();\n this.pc = null;\n }\n this.dc = null;\n if (this.stream) {\n this.stream.getTracks().forEach((t) => t.stop());\n this.stream = null;\n }\n if (this.audio) {\n this.audio.pause();\n this.audio.srcObject = null;\n this.audio = null;\n }\n this.botWords = {};\n this.setState({\n isMuted: false,\n phase: \"idle\",\n userSpeaking: false,\n agentSpeaking: false,\n idleWarning: null,\n });\n }\n\n async connect(): Promise<void> {\n if (this.pc) return;\n try {\n this.setState({\n status: \"connecting\",\n error: null,\n duration: 0,\n messages: [],\n });\n this.botWords = {};\n const base = (this.opts.server ?? \"https://voice.pinecall.io\").replace(\n /\\/$/,\n \"\",\n );\n\n // Fetch token — use tokenProvider (backend proxy) or direct fetch (allowedOrigins)\n let token: string;\n let voiceServer: string;\n if (this.opts.tokenProvider) {\n const t = await this.opts.tokenProvider();\n token = t.token;\n voiceServer = t.server;\n } else {\n const tRes = await fetch(\n `${base}/webrtc/token?agent_id=${encodeURIComponent(this.opts.agent)}`,\n );\n if (!tRes.ok) throw new Error(`Token: ${tRes.status}`);\n const t = await tRes.json();\n token = t.token;\n voiceServer = t.server;\n }\n if (!voiceServer) throw new Error(\"Token response missing server URL\");\n\n let ice: RTCIceServer[] = [{ urls: \"stun:stun.l.google.com:19302\" }];\n try {\n const r = await fetch(`${voiceServer}/webrtc/ice-servers`);\n if (r.ok) {\n const d = await r.json();\n ice = d.iceServers || d.ice_servers || ice;\n }\n } catch {\n /* stun fallback */\n }\n\n this.stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n video: false,\n });\n\n const pc = new RTCPeerConnection({ iceServers: ice });\n this.pc = pc;\n this.stream.getTracks().forEach((t) => pc.addTrack(t, this.stream!));\n\n pc.ontrack = (e) => {\n if (!this.audio) {\n this.audio = new Audio();\n this.audio.autoplay = true;\n }\n this.audio.srcObject = e.streams[0];\n };\n\n const dc = pc.createDataChannel(\"events\", { ordered: true });\n this.dc = dc;\n dc.onopen = () => {\n this.ping = setInterval(() => {\n if (dc.readyState === \"open\") dc.send(\"ping\");\n }, 1000);\n };\n dc.onmessage = (msg) => this.handleDataChannelMessage(msg);\n\n pc.onconnectionstatechange = () => {\n if (pc.connectionState === \"connected\") {\n this.setState({ status: \"connected\", phase: \"listening\" });\n this.startedAt = Date.now();\n this.timer = setInterval(() => {\n this.setState({\n duration: Math.floor((Date.now() - this.startedAt) / 1000),\n });\n }, 1000);\n } else if (\n pc.connectionState === \"disconnected\" ||\n pc.connectionState === \"failed\"\n ) {\n this.cleanup();\n this.setState({ status: \"idle\" });\n }\n };\n\n const offer = await pc.createOffer({\n offerToReceiveAudio: true,\n offerToReceiveVideo: false,\n });\n await pc.setLocalDescription(offer);\n await new Promise<void>((resolve) => {\n if (pc.iceGatheringState === \"complete\") return resolve();\n const t = setTimeout(resolve, 2000);\n pc.onicegatheringstatechange = () => {\n if (pc.iceGatheringState === \"complete\") {\n clearTimeout(t);\n resolve();\n }\n };\n });\n\n const offerBody: Record<string, unknown> = {\n sdp: pc.localDescription!.sdp,\n type: pc.localDescription!.type,\n token,\n };\n if (this.opts.config && Object.keys(this.opts.config).length > 0) {\n offerBody.config = this.opts.config;\n }\n if (this.opts.metadata && Object.keys(this.opts.metadata).length > 0) {\n offerBody.metadata = this.opts.metadata;\n }\n\n const res = await fetch(`${voiceServer}/webrtc/offer`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(offerBody),\n });\n if (!res.ok) throw new Error(`Offer: ${res.status}`);\n const answer = await res.json();\n await pc.setRemoteDescription({ type: answer.type, sdp: answer.sdp });\n } catch (err) {\n this.setState({\n error: err instanceof Error ? err.message : String(err),\n status: \"error\",\n });\n this.cleanup();\n }\n }\n\n private handleDataChannelMessage(msg: MessageEvent): void {\n let d: any;\n try {\n d = JSON.parse(msg.data);\n } catch {\n return; // ignore non-JSON\n }\n\n switch (d.event) {\n // ── User speech (STT) ──\n case \"speech.started\":\n this.setState({ userSpeaking: true, idleWarning: null });\n break;\n case \"speech.ended\":\n this.setState({ userSpeaking: false });\n break;\n\n case \"user.speaking\":\n if (d.text) {\n this.setMessages((prev) => {\n const idx = prev.findLastIndex(\n (m) => m.role === \"user\" && m.isInterim,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx ? { ...m, text: d.text } : m,\n );\n }\n return [\n ...prev,\n {\n id: Date.now(),\n role: \"user\",\n text: d.text,\n isInterim: true,\n },\n ];\n });\n }\n this.setState({ phase: \"listening\", userSpeaking: true });\n break;\n\n case \"user.message\":\n if (d.text) {\n this.setMessages((prev) => {\n const idx = prev.findLastIndex(\n (m) => m.role === \"user\" && m.isInterim,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx ? { ...m, text: d.text, isInterim: false } : m,\n );\n }\n return [\n ...prev,\n {\n id: Date.now(),\n role: \"user\",\n text: d.text,\n isInterim: false,\n },\n ];\n });\n }\n this.setState({ userSpeaking: false, phase: \"thinking\" });\n break;\n\n // ── Turn detection ──\n case \"turn.pause\":\n this.setState({ phase: \"pause\" });\n break;\n case \"turn.end\":\n this.setState({ phase: \"thinking\", userSpeaking: false });\n break;\n case \"turn.resumed\":\n this.setState({ phase: \"listening\" });\n break;\n\n // ── Bot speech (TTS word-by-word) ──\n case \"bot.speaking\":\n if (d.message_id) {\n this.botWords[d.message_id] = [];\n // Don't create empty bot message here — bot.word creates it on first word.\n // This prevents phantom dots when LLM goes straight to tool calls.\n }\n break;\n\n case \"bot.word\":\n if (d.message_id && d.word) {\n const ref = this.botWords;\n if (!ref[d.message_id]) ref[d.message_id] = [];\n const idx = d.word_index ?? ref[d.message_id].length;\n ref[d.message_id][idx] = d.word;\n const newText = ref[d.message_id].filter(Boolean).join(\" \");\n this.setMessages((prev) => {\n const mi = prev.findIndex((m) => m.messageId === d.message_id);\n if (mi >= 0)\n return prev.map((m, i) =>\n i === mi ? { ...m, text: newText } : m,\n );\n return [\n ...prev,\n {\n id: Date.now(),\n role: \"bot\",\n text: newText,\n messageId: d.message_id,\n speaking: true,\n },\n ];\n });\n this.setState({ agentSpeaking: true, phase: \"speaking\" });\n }\n break;\n\n case \"bot.finished\":\n if (d.message_id) {\n this.setMessages((prev) => {\n const msg = prev.find((m) => m.messageId === d.message_id);\n // Remove empty bot messages entirely (LLM went straight to tool call)\n if (msg && !msg.text && !d.text) {\n return prev.filter((m) => m.messageId !== d.message_id);\n }\n return prev.map((m) =>\n m.messageId === d.message_id\n ? { ...m, speaking: false, ...(d.text ? { text: d.text } : {}) }\n : m,\n );\n });\n }\n this.setState({ agentSpeaking: false, phase: \"listening\" });\n break;\n\n case \"bot.interrupted\":\n if (d.message_id) {\n this.setMessages((prev) =>\n prev.map((m) =>\n m.messageId === d.message_id\n ? { ...m, speaking: false, interrupted: true }\n : m,\n ),\n );\n }\n this.setState({ agentSpeaking: false, phase: \"listening\" });\n break;\n\n // ── Audio metrics ──\n case \"audio.metrics\":\n if (d.source === \"user\" && d.is_speech !== undefined) {\n this.setState({ userSpeaking: d.is_speech });\n }\n break;\n\n // ── Session limits ──\n case \"session.idle_warning\":\n this.setState({ idleWarning: d.remaining_seconds ?? 0 });\n break;\n case \"session.timeout\":\n // Server will hang up — disconnect immediately\n this.disconnect();\n break;\n\n // ── Tool events (server-side LLM) ──\n case \"llm.tool_call\": {\n if (d.tool_calls?.length) {\n // Inline system messages in transcript\n this.setMessages((prev) => [\n ...prev,\n ...d.tool_calls.map((tc: any) => ({\n id: Date.now() + Math.random(),\n role: \"system\" as const,\n text: `🔧 Using ${tc.name}…`,\n toolCallId: tc.id,\n })),\n ]);\n\n // Always track in toolCalls state (for ThinkingIndicator + trackedTools UI)\n const tracked = this.opts.trackedTools;\n const newEntries: ToolUI[] = d.tool_calls\n .filter((tc: any) => !tracked || tracked.includes(tc.name))\n .map((tc: any) => {\n let args: Record<string, unknown> = {};\n try {\n args =\n typeof tc.arguments === \"string\"\n ? JSON.parse(tc.arguments)\n : tc.arguments ?? {};\n } catch {\n /* leave empty */\n }\n return {\n toolCallId: tc.id,\n name: tc.name,\n arguments: args,\n timestamp: Date.now(),\n };\n });\n if (newEntries.length) {\n this.setState({\n toolCalls: [...this.state.toolCalls, ...newEntries],\n });\n }\n }\n break;\n }\n\n case \"llm.tool_result\": {\n if (d.tool_call_id) {\n // Recover tool name from the system message we added on llm.tool_call\n const sysMsg = this.state.messages.find(\n (m) => m.toolCallId === d.tool_call_id,\n );\n const toolName = (d.name || sysMsg?.text?.match(/Using (\\S+)/)?.[1] || \"Tool\").replace(/…$/, \"\");\n this.setMessages((prev) =>\n prev.map((m) =>\n m.toolCallId === d.tool_call_id\n ? { ...m, text: `✓ ${toolName}` }\n : m,\n ),\n );\n\n // Update tracked tool state\n const prev = this.state.toolCalls;\n const idx = prev.findIndex(\n (t) => t.toolCallId === d.tool_call_id,\n );\n if (idx >= 0) {\n let parsed: unknown = d.result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n /* keep as string */\n }\n }\n const updated = prev.map((t, i) =>\n i === idx ? { ...t, result: parsed } : t,\n );\n this.setState({ toolCalls: updated });\n }\n }\n break;\n }\n }\n\n // Emit raw event for power users (does not affect state mutations above).\n this.dispatchEvent(new CustomEvent(\"event\", { detail: d }));\n }\n\n disconnect(): void {\n this.cleanup();\n this.setState({ status: \"idle\" });\n }\n\n toggleMute(): void {\n this.setMuted(!this.state.isMuted);\n }\n\n setMuted(muted: boolean): void {\n const stream = this.stream;\n if (!stream) return;\n stream.getAudioTracks().forEach((t) => {\n t.enabled = !muted;\n });\n const dc = this.dc;\n if (dc && dc.readyState === \"open\") {\n dc.send(JSON.stringify({ action: muted ? \"mute\" : \"unmute\" }));\n }\n this.setState({ isMuted: muted });\n }\n\n /**\n * Send a configuration update via DataChannel during an active call.\n * Use this for mid-call language/voice/STT switching.\n *\n * @example\n * ```ts\n * session.configure({ voice: \"coral\", stt: \"deepgram\", language: \"es\" });\n * ```\n */\n configure(config: Record<string, unknown>): void {\n const dc = this.dc;\n if (dc && dc.readyState === \"open\") {\n dc.send(JSON.stringify({ action: \"configure\", ...config }));\n }\n }\n\n /**\n * Inject text into the conversation as if the user spoke it.\n *\n * Use this for click-based interactions in tool UIs (e.g., selecting a\n * calendar slot). The server routes the text to the LLM, producing the\n * same effect as the user speaking.\n *\n * @example\n * ```ts\n * session.sendText(\"I'd like the 10:00 AM slot\");\n * ```\n */\n sendText(text: string): void {\n const dc = this.dc;\n if (dc && dc.readyState === \"open\") {\n dc.send(JSON.stringify({ action: \"inject_text\", text }));\n }\n }\n\n /**\n * Remove a tool UI entry from state.\n *\n * Call this after the user interacts with a tool UI (e.g., selects a slot)\n * to dismiss the rendered component from the transcript.\n */\n dismissTool(toolCallId: string): void {\n this.setState({\n toolCalls: this.state.toolCalls.filter(\n (t) => t.toolCallId !== toolCallId,\n ),\n });\n }\n\n /**\n * Set or clear a keyed context block in the LLM system prompt.\n *\n * Use this to inject dynamic UI state (form data, selections, etc.)\n * into the agent's prompt so it can see what the user is doing on screen.\n * Each key is a named section — setting the same key replaces its value.\n * Pass `null` to remove a context key.\n *\n * @example\n * ```ts\n * // Inject form state so the agent sees what's filled\n * session.setContext(\"contact_form\", JSON.stringify({\n * name: \"John\",\n * email: \"john@example.com\",\n * phone: \"\",\n * }));\n *\n * // Clear when form is submitted\n * session.setContext(\"contact_form\", null);\n * ```\n */\n setContext(key: string, value: string | null): void {\n const dc = this.dc;\n if (dc && dc.readyState === \"open\") {\n dc.send(JSON.stringify({ action: \"set_context\", key, value }));\n }\n }\n\n /**\n * Update session options before the next `connect()` call.\n * Has no effect on an already-connected session — use `configure()` for that.\n */\n updateOptions(\n patch: Partial<Pick<VoiceSessionOptions, \"config\" | \"metadata\">>,\n ): void {\n if (patch.config !== undefined) {\n this.opts = { ...this.opts, config: patch.config };\n }\n if (patch.metadata !== undefined) {\n this.opts = { ...this.opts, metadata: patch.metadata };\n }\n }\n\n /** Tear down the session and clear subscribers. After this, do not reuse. */\n destroy(): void {\n this.cleanup();\n this.setState({ status: \"idle\" });\n this.listeners.clear();\n }\n}\n"]}