@jchaffin/voicekit 0.2.0 → 0.2.1

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.
@@ -73,12 +73,18 @@ var DeepgramSession = class extends EventEmitter {
73
73
  if (this.options.model) url.searchParams.set("model", this.options.model);
74
74
  if (this.options.language) url.searchParams.set("language", this.options.language);
75
75
  this.ws = new WebSocket(url.toString());
76
- await new Promise((resolve, reject) => {
77
- const ws = this.ws;
78
- ws.onopen = () => resolve();
79
- ws.onerror = (e) => reject(new Error("WebSocket connection failed"));
80
- ws.onclose = () => this.emit("status_change", "DISCONNECTED");
81
- });
76
+ try {
77
+ await new Promise((resolve, reject) => {
78
+ const ws = this.ws;
79
+ ws.onopen = () => resolve();
80
+ ws.onerror = () => reject(new Error("WebSocket connection failed"));
81
+ ws.onclose = () => this.emit("status_change", "DISCONNECTED");
82
+ });
83
+ } catch (err) {
84
+ this.ws?.close();
85
+ this.ws = null;
86
+ throw err;
87
+ }
82
88
  this.ws.onmessage = (event) => {
83
89
  try {
84
90
  const msg = JSON.parse(event.data);
@@ -19,12 +19,18 @@ var DeepgramSession = class extends EventEmitter {
19
19
  if (this.options.model) url.searchParams.set("model", this.options.model);
20
20
  if (this.options.language) url.searchParams.set("language", this.options.language);
21
21
  this.ws = new WebSocket(url.toString());
22
- await new Promise((resolve, reject) => {
23
- const ws = this.ws;
24
- ws.onopen = () => resolve();
25
- ws.onerror = (e) => reject(new Error("WebSocket connection failed"));
26
- ws.onclose = () => this.emit("status_change", "DISCONNECTED");
27
- });
22
+ try {
23
+ await new Promise((resolve, reject) => {
24
+ const ws = this.ws;
25
+ ws.onopen = () => resolve();
26
+ ws.onerror = () => reject(new Error("WebSocket connection failed"));
27
+ ws.onclose = () => this.emit("status_change", "DISCONNECTED");
28
+ });
29
+ } catch (err) {
30
+ this.ws?.close();
31
+ this.ws = null;
32
+ throw err;
33
+ }
28
34
  this.ws.onmessage = (event) => {
29
35
  try {
30
36
  const msg = JSON.parse(event.data);
@@ -73,11 +73,17 @@ var ElevenLabsSession = class extends EventEmitter {
73
73
  async connect(config) {
74
74
  const wsUrl = config.authToken?.startsWith("wss://") ? config.authToken : `${ELEVENLABS_WS_BASE}?agent_id=${this.agentId}`;
75
75
  this.ws = new WebSocket(wsUrl);
76
- await new Promise((resolve, reject) => {
77
- const ws = this.ws;
78
- ws.onopen = () => resolve();
79
- ws.onerror = () => reject(new Error("ElevenLabs WebSocket connection failed"));
80
- });
76
+ try {
77
+ await new Promise((resolve, reject) => {
78
+ const ws = this.ws;
79
+ ws.onopen = () => resolve();
80
+ ws.onerror = () => reject(new Error("ElevenLabs WebSocket connection failed"));
81
+ });
82
+ } catch (err) {
83
+ this.ws?.close();
84
+ this.ws = null;
85
+ throw err;
86
+ }
81
87
  this.ws.onmessage = (event) => {
82
88
  try {
83
89
  const msg = JSON.parse(event.data);
@@ -19,11 +19,17 @@ var ElevenLabsSession = class extends EventEmitter {
19
19
  async connect(config) {
20
20
  const wsUrl = config.authToken?.startsWith("wss://") ? config.authToken : `${ELEVENLABS_WS_BASE}?agent_id=${this.agentId}`;
21
21
  this.ws = new WebSocket(wsUrl);
22
- await new Promise((resolve, reject) => {
23
- const ws = this.ws;
24
- ws.onopen = () => resolve();
25
- ws.onerror = () => reject(new Error("ElevenLabs WebSocket connection failed"));
26
- });
22
+ try {
23
+ await new Promise((resolve, reject) => {
24
+ const ws = this.ws;
25
+ ws.onopen = () => resolve();
26
+ ws.onerror = () => reject(new Error("ElevenLabs WebSocket connection failed"));
27
+ });
28
+ } catch (err) {
29
+ this.ws?.close();
30
+ this.ws = null;
31
+ throw err;
32
+ }
27
33
  this.ws.onmessage = (event) => {
28
34
  try {
29
35
  const msg = JSON.parse(event.data);
@@ -106,9 +106,15 @@ var LiveKitSession = class extends EventEmitter {
106
106
  this.room.on(RoomEvent.Reconnected, () => {
107
107
  this.emit("status_change", "CONNECTED");
108
108
  });
109
- await this.room.connect(this.serverUrl, config.authToken);
110
- await this.room.localParticipant.setMicrophoneEnabled(true);
111
- this.emit("status_change", "CONNECTED");
109
+ try {
110
+ await this.room.connect(this.serverUrl, config.authToken);
111
+ await this.room.localParticipant.setMicrophoneEnabled(true);
112
+ this.emit("status_change", "CONNECTED");
113
+ } catch (err) {
114
+ await this.room.disconnect();
115
+ this.room = null;
116
+ throw err;
117
+ }
112
118
  }
113
119
  async disconnect() {
114
120
  if (this.room) {
@@ -42,9 +42,15 @@ var LiveKitSession = class extends EventEmitter {
42
42
  this.room.on(RoomEvent.Reconnected, () => {
43
43
  this.emit("status_change", "CONNECTED");
44
44
  });
45
- await this.room.connect(this.serverUrl, config.authToken);
46
- await this.room.localParticipant.setMicrophoneEnabled(true);
47
- this.emit("status_change", "CONNECTED");
45
+ try {
46
+ await this.room.connect(this.serverUrl, config.authToken);
47
+ await this.room.localParticipant.setMicrophoneEnabled(true);
48
+ this.emit("status_change", "CONNECTED");
49
+ } catch (err) {
50
+ await this.room.disconnect();
51
+ this.room = null;
52
+ throw err;
53
+ }
48
54
  }
49
55
  async disconnect() {
50
56
  if (this.room) {
@@ -152,12 +152,16 @@ var OpenAISession = class extends EventEmitter {
152
152
  await new Promise((resolve) => {
153
153
  const onDone = (event) => {
154
154
  if (event.type === "response.done" || event.type === "response.cancelled") {
155
+ clearTimeout(timeoutId);
155
156
  this.off("raw_event", onDone);
156
157
  resolve();
157
158
  }
158
159
  };
159
160
  this.on("raw_event", onDone);
160
- setTimeout(resolve, 1500);
161
+ const timeoutId = setTimeout(() => {
162
+ this.off("raw_event", onDone);
163
+ resolve();
164
+ }, 1500);
161
165
  });
162
166
  }
163
167
  this.session.sendMessage(text);
@@ -96,12 +96,16 @@ var OpenAISession = class extends EventEmitter {
96
96
  await new Promise((resolve) => {
97
97
  const onDone = (event) => {
98
98
  if (event.type === "response.done" || event.type === "response.cancelled") {
99
+ clearTimeout(timeoutId);
99
100
  this.off("raw_event", onDone);
100
101
  resolve();
101
102
  }
102
103
  };
103
104
  this.on("raw_event", onDone);
104
- setTimeout(resolve, 1500);
105
+ const timeoutId = setTimeout(() => {
106
+ this.off("raw_event", onDone);
107
+ resolve();
108
+ }, 1500);
105
109
  });
106
110
  }
107
111
  this.session.sendMessage(text);
package/dist/index.js CHANGED
@@ -217,6 +217,7 @@ function VoiceProvider({
217
217
  }, 500);
218
218
  } catch (error) {
219
219
  console.error("VoiceKit connection failed:", error);
220
+ sessionRef.current = null;
220
221
  onError?.(error instanceof Error ? error : new Error(String(error)));
221
222
  updateStatus("DISCONNECTED");
222
223
  }
@@ -823,6 +824,7 @@ function useAudioRecorder() {
823
824
  if (mediaRecorderRef.current?.state === "recording") {
824
825
  return;
825
826
  }
827
+ recordedChunksRef.current = [];
826
828
  try {
827
829
  const mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
828
830
  mediaRecorder.ondataavailable = (event) => {
@@ -1219,6 +1221,7 @@ function useSessionHistory() {
1219
1221
  const itemId = item.item_id;
1220
1222
  if (interruptedItemsRef.current.has(itemId)) return;
1221
1223
  if (itemId) {
1224
+ const displayedText = displayedTextRef.current.get(itemId) ?? accumulatedTextRef.current.get(itemId);
1222
1225
  const timer = deltaTimerRef.current.get(itemId);
1223
1226
  if (timer) clearTimeout(timer);
1224
1227
  deltaTimerRef.current.delete(itemId);
@@ -1227,14 +1230,13 @@ function useSessionHistory() {
1227
1230
  displayedTextRef.current.delete(itemId);
1228
1231
  accumulatedTextRef.current.delete(itemId);
1229
1232
  totalAudioDurationRef.current.delete(itemId);
1230
- const displayedText = displayedTextRef.current.get(itemId);
1231
1233
  const finalText = displayedText || item.transcript || "";
1232
1234
  const stripped = finalText.replace(/[\s.…]+/g, "");
1233
1235
  if (stripped.length > 0) {
1234
1236
  updateTranscriptMessage(itemId, finalText, false);
1235
1237
  }
1236
1238
  updateTranscriptItem(itemId, { status: "DONE" });
1237
- const transcriptItem = transcriptItems.find((i) => i.itemId === itemId);
1239
+ const transcriptItem = transcriptItemsRef.current.find((i) => i.itemId === itemId);
1238
1240
  if (transcriptItem?.guardrailResult?.status === "IN_PROGRESS") {
1239
1241
  updateTranscriptItem(itemId, {
1240
1242
  guardrailResult: {
@@ -1259,7 +1261,8 @@ function useSessionHistory() {
1259
1261
  const category = moderation.moderationCategory ?? "NONE";
1260
1262
  const rationale = moderation.moderationRationale ?? "";
1261
1263
  const offendingText = moderation.testText;
1262
- updateTranscriptItem(lastAssistant.itemId, {
1264
+ const assistantItemId = lastAssistant.itemId ?? lastAssistant.id;
1265
+ updateTranscriptItem(assistantItemId, {
1263
1266
  guardrailResult: {
1264
1267
  status: "DONE",
1265
1268
  category,
@@ -1326,13 +1329,15 @@ function useRealtimeSession(callbacks = {}) {
1326
1329
  const [status, setStatus] = (0, import_react8.useState)("DISCONNECTED");
1327
1330
  const { logClientEvent, logServerEvent } = useEvent();
1328
1331
  const codecParamRef = (0, import_react8.useRef)("opus");
1332
+ const callbacksRef = (0, import_react8.useRef)(callbacks);
1333
+ callbacksRef.current = callbacks;
1329
1334
  const updateStatus = (0, import_react8.useCallback)(
1330
1335
  (s) => {
1331
1336
  setStatus(s);
1332
- callbacks.onConnectionChange?.(s);
1337
+ callbacksRef.current.onConnectionChange?.(s);
1333
1338
  logClientEvent({}, s);
1334
1339
  },
1335
- [callbacks, logClientEvent]
1340
+ [logClientEvent]
1336
1341
  );
1337
1342
  const historyHandlers = useSessionHistory().current;
1338
1343
  const interruptedRef = (0, import_react8.useRef)(/* @__PURE__ */ new Set());
@@ -1392,7 +1397,7 @@ function useRealtimeSession(callbacks = {}) {
1392
1397
  );
1393
1398
  });
1394
1399
  session.on("agent_handoff", (_from, to) => {
1395
- callbacks.onAgentHandoff?.(to);
1400
+ callbacksRef.current.onAgentHandoff?.(to);
1396
1401
  });
1397
1402
  session.on("guardrail_tripped", (info) => {
1398
1403
  historyHandlers.handleGuardrailTripped(
@@ -1429,7 +1434,7 @@ function useRealtimeSession(callbacks = {}) {
1429
1434
  console.error("Session error:", msg);
1430
1435
  logServerEvent({ type: "error", message: msg });
1431
1436
  });
1432
- }, [callbacks, historyHandlers, logServerEvent]);
1437
+ }, [historyHandlers, logServerEvent]);
1433
1438
  const connect = (0, import_react8.useCallback)(
1434
1439
  async ({
1435
1440
  getEphemeralKey,
@@ -1445,6 +1450,9 @@ function useRealtimeSession(callbacks = {}) {
1445
1450
  "useRealtimeSession: `adapter` is required in ConnectOptions. Pass an adapter like openai() from @jchaffin/voicekit/openai."
1446
1451
  );
1447
1452
  }
1453
+ if (!initialAgents?.length) {
1454
+ throw new Error("useRealtimeSession: `initialAgents` must be a non-empty array.");
1455
+ }
1448
1456
  updateStatus("CONNECTING");
1449
1457
  const ek = await getEphemeralKey();
1450
1458
  const rootAgent = initialAgents[0];
@@ -1667,8 +1675,8 @@ function SuggestionProvider({
1667
1675
  (0, import_react9.useEffect)(() => {
1668
1676
  const handler = (e) => {
1669
1677
  const detail = e.detail;
1670
- if (detail?.group) {
1671
- setSuggestionsState(detail.group);
1678
+ if (detail) {
1679
+ setSuggestionsState(detail.group ?? null);
1672
1680
  }
1673
1681
  };
1674
1682
  window.addEventListener(SUGGESTION_EVENT, handler);
package/dist/index.mjs CHANGED
@@ -159,6 +159,7 @@ function VoiceProvider({
159
159
  }, 500);
160
160
  } catch (error) {
161
161
  console.error("VoiceKit connection failed:", error);
162
+ sessionRef.current = null;
162
163
  onError?.(error instanceof Error ? error : new Error(String(error)));
163
164
  updateStatus("DISCONNECTED");
164
165
  }
@@ -604,6 +605,7 @@ function useAudioRecorder() {
604
605
  if (mediaRecorderRef.current?.state === "recording") {
605
606
  return;
606
607
  }
608
+ recordedChunksRef.current = [];
607
609
  try {
608
610
  const mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
609
611
  mediaRecorder.ondataavailable = (event) => {
@@ -1005,6 +1007,7 @@ function useSessionHistory() {
1005
1007
  const itemId = item.item_id;
1006
1008
  if (interruptedItemsRef.current.has(itemId)) return;
1007
1009
  if (itemId) {
1010
+ const displayedText = displayedTextRef.current.get(itemId) ?? accumulatedTextRef.current.get(itemId);
1008
1011
  const timer = deltaTimerRef.current.get(itemId);
1009
1012
  if (timer) clearTimeout(timer);
1010
1013
  deltaTimerRef.current.delete(itemId);
@@ -1013,14 +1016,13 @@ function useSessionHistory() {
1013
1016
  displayedTextRef.current.delete(itemId);
1014
1017
  accumulatedTextRef.current.delete(itemId);
1015
1018
  totalAudioDurationRef.current.delete(itemId);
1016
- const displayedText = displayedTextRef.current.get(itemId);
1017
1019
  const finalText = displayedText || item.transcript || "";
1018
1020
  const stripped = finalText.replace(/[\s.…]+/g, "");
1019
1021
  if (stripped.length > 0) {
1020
1022
  updateTranscriptMessage(itemId, finalText, false);
1021
1023
  }
1022
1024
  updateTranscriptItem(itemId, { status: "DONE" });
1023
- const transcriptItem = transcriptItems.find((i) => i.itemId === itemId);
1025
+ const transcriptItem = transcriptItemsRef.current.find((i) => i.itemId === itemId);
1024
1026
  if (transcriptItem?.guardrailResult?.status === "IN_PROGRESS") {
1025
1027
  updateTranscriptItem(itemId, {
1026
1028
  guardrailResult: {
@@ -1045,7 +1047,8 @@ function useSessionHistory() {
1045
1047
  const category = moderation.moderationCategory ?? "NONE";
1046
1048
  const rationale = moderation.moderationRationale ?? "";
1047
1049
  const offendingText = moderation.testText;
1048
- updateTranscriptItem(lastAssistant.itemId, {
1050
+ const assistantItemId = lastAssistant.itemId ?? lastAssistant.id;
1051
+ updateTranscriptItem(assistantItemId, {
1049
1052
  guardrailResult: {
1050
1053
  status: "DONE",
1051
1054
  category,
@@ -1112,13 +1115,15 @@ function useRealtimeSession(callbacks = {}) {
1112
1115
  const [status, setStatus] = useState5("DISCONNECTED");
1113
1116
  const { logClientEvent, logServerEvent } = useEvent();
1114
1117
  const codecParamRef = useRef6("opus");
1118
+ const callbacksRef = useRef6(callbacks);
1119
+ callbacksRef.current = callbacks;
1115
1120
  const updateStatus = useCallback6(
1116
1121
  (s) => {
1117
1122
  setStatus(s);
1118
- callbacks.onConnectionChange?.(s);
1123
+ callbacksRef.current.onConnectionChange?.(s);
1119
1124
  logClientEvent({}, s);
1120
1125
  },
1121
- [callbacks, logClientEvent]
1126
+ [logClientEvent]
1122
1127
  );
1123
1128
  const historyHandlers = useSessionHistory().current;
1124
1129
  const interruptedRef = useRef6(/* @__PURE__ */ new Set());
@@ -1178,7 +1183,7 @@ function useRealtimeSession(callbacks = {}) {
1178
1183
  );
1179
1184
  });
1180
1185
  session.on("agent_handoff", (_from, to) => {
1181
- callbacks.onAgentHandoff?.(to);
1186
+ callbacksRef.current.onAgentHandoff?.(to);
1182
1187
  });
1183
1188
  session.on("guardrail_tripped", (info) => {
1184
1189
  historyHandlers.handleGuardrailTripped(
@@ -1215,7 +1220,7 @@ function useRealtimeSession(callbacks = {}) {
1215
1220
  console.error("Session error:", msg);
1216
1221
  logServerEvent({ type: "error", message: msg });
1217
1222
  });
1218
- }, [callbacks, historyHandlers, logServerEvent]);
1223
+ }, [historyHandlers, logServerEvent]);
1219
1224
  const connect = useCallback6(
1220
1225
  async ({
1221
1226
  getEphemeralKey,
@@ -1231,6 +1236,9 @@ function useRealtimeSession(callbacks = {}) {
1231
1236
  "useRealtimeSession: `adapter` is required in ConnectOptions. Pass an adapter like openai() from @jchaffin/voicekit/openai."
1232
1237
  );
1233
1238
  }
1239
+ if (!initialAgents?.length) {
1240
+ throw new Error("useRealtimeSession: `initialAgents` must be a non-empty array.");
1241
+ }
1234
1242
  updateStatus("CONNECTING");
1235
1243
  const ek = await getEphemeralKey();
1236
1244
  const rootAgent = initialAgents[0];
@@ -1453,8 +1461,8 @@ function SuggestionProvider({
1453
1461
  useEffect6(() => {
1454
1462
  const handler = (e) => {
1455
1463
  const detail = e.detail;
1456
- if (detail?.group) {
1457
- setSuggestionsState(detail.group);
1464
+ if (detail) {
1465
+ setSuggestionsState(detail.group ?? null);
1458
1466
  }
1459
1467
  };
1460
1468
  window.addEventListener(SUGGESTION_EVENT, handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jchaffin/voicekit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A provider-agnostic React library for building voice-enabled AI agents",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",