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