@ctchealth/plato-sdk 0.0.20 → 0.0.22
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 +101 -10
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +4 -0
- package/src/index.js.map +1 -1
- package/src/lib/plato-intefaces.d.ts +2 -16
- package/src/lib/plato-intefaces.js +1 -14
- package/src/lib/plato-intefaces.js.map +1 -1
- package/src/lib/plato-sdk.d.ts +9 -75
- package/src/lib/plato-sdk.js +423 -275
- package/src/lib/plato-sdk.js.map +1 -1
- package/src/lib/utils.d.ts +8 -0
- package/src/lib/utils.js +33 -0
- package/src/lib/utils.js.map +1 -1
package/src/lib/plato-sdk.js
CHANGED
|
@@ -21,13 +21,14 @@ const utils_1 = require("./utils");
|
|
|
21
21
|
const constants_1 = require("./constants");
|
|
22
22
|
class PlatoApiClient {
|
|
23
23
|
config;
|
|
24
|
-
static ACTIVE_CALL_STORAGE_KEY = 'plato_active_call';
|
|
24
|
+
// private static readonly ACTIVE_CALL_STORAGE_KEY = 'plato_active_call';
|
|
25
25
|
http;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
27
27
|
eventListeners = {};
|
|
28
28
|
callControllerInstance;
|
|
29
29
|
eventsAttached = false;
|
|
30
|
-
currentCallId;
|
|
30
|
+
// private currentCallId?: string;
|
|
31
|
+
// private receivedAnyTranscript = false;
|
|
31
32
|
// Vapi-native events (events that Vapi SDK supports)
|
|
32
33
|
// Exclude our custom SDK events from this list
|
|
33
34
|
vapiEventNames = [
|
|
@@ -49,6 +50,7 @@ class PlatoApiClient {
|
|
|
49
50
|
'message',
|
|
50
51
|
'volume-level',
|
|
51
52
|
'call-details-ready',
|
|
53
|
+
'call-ended-reason',
|
|
52
54
|
];
|
|
53
55
|
constructor(config) {
|
|
54
56
|
this.config = config;
|
|
@@ -130,132 +132,125 @@ class PlatoApiClient {
|
|
|
130
132
|
listeners.forEach(listener => listener(payload));
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
storeCallState(state) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
console.error('Error during call recovery:', error);
|
|
255
|
-
this.clearCallState();
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
135
|
+
// /**
|
|
136
|
+
// * Store active call state in localStorage for recovery purposes.
|
|
137
|
+
// * @private
|
|
138
|
+
// */
|
|
139
|
+
// private storeCallState(state: ActiveCallState): void {
|
|
140
|
+
// try {
|
|
141
|
+
// localStorage.setItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY, JSON.stringify(state));
|
|
142
|
+
// } catch (error) {
|
|
143
|
+
// console.warn('Failed to store call state:', error);
|
|
144
|
+
// }
|
|
145
|
+
// }
|
|
146
|
+
// /**
|
|
147
|
+
// * Retrieve active call state from localStorage.
|
|
148
|
+
// * Validates the stored data and clears it if invalid.
|
|
149
|
+
// * @private
|
|
150
|
+
// * @returns The stored call state or null if not found or invalid
|
|
151
|
+
// */
|
|
152
|
+
// private getStoredCallState(): ActiveCallState | null {
|
|
153
|
+
// try {
|
|
154
|
+
// const stored = localStorage.getItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY);
|
|
155
|
+
// if (!stored) {
|
|
156
|
+
// return null;
|
|
157
|
+
// }
|
|
158
|
+
// const parsed = JSON.parse(stored) as ActiveCallState;
|
|
159
|
+
// // Validate required fields
|
|
160
|
+
// if (!parsed.callId || !parsed.externalCallId || !parsed.simulationId || !parsed.startedAt) {
|
|
161
|
+
// this.clearCallState();
|
|
162
|
+
// return null;
|
|
163
|
+
// }
|
|
164
|
+
// return parsed;
|
|
165
|
+
// } catch {
|
|
166
|
+
// return null;
|
|
167
|
+
// }
|
|
168
|
+
// }
|
|
169
|
+
// /**
|
|
170
|
+
// * Clear active call state from localStorage.
|
|
171
|
+
// * @private
|
|
172
|
+
// */
|
|
173
|
+
// private clearCallState(): void {
|
|
174
|
+
// try {
|
|
175
|
+
// localStorage.removeItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY);
|
|
176
|
+
// } catch {
|
|
177
|
+
// // ignore
|
|
178
|
+
// }
|
|
179
|
+
// }
|
|
180
|
+
// /**
|
|
181
|
+
// * Check if a stored call is considered abandoned based on age.
|
|
182
|
+
// * Calls older than 5 minutes are considered abandoned.
|
|
183
|
+
// * @private
|
|
184
|
+
// * @param state The call state to check
|
|
185
|
+
// * @returns true if the call is abandoned, false otherwise
|
|
186
|
+
// */
|
|
187
|
+
// private isCallAbandoned(state: ActiveCallState): boolean {
|
|
188
|
+
// const ABANDONMENT_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
189
|
+
// const startedAt = new Date(state.startedAt).getTime();
|
|
190
|
+
// const now = Date.now();
|
|
191
|
+
// const age = now - startedAt;
|
|
192
|
+
// return age > ABANDONMENT_THRESHOLD_MS;
|
|
193
|
+
// }
|
|
194
|
+
// /**
|
|
195
|
+
// * Recover and clean up any abandoned calls from previous sessions.
|
|
196
|
+
// *
|
|
197
|
+
// * This method should be called during application initialization,
|
|
198
|
+
// * typically in ngOnInit() or useEffect(). It detects calls that were
|
|
199
|
+
// * active when the page was last refreshed and notifies the backend
|
|
200
|
+
// * to process them if they're older than 5 minutes.
|
|
201
|
+
// *
|
|
202
|
+
// * The backend endpoint is idempotent, so calling this method multiple
|
|
203
|
+
// * times for the same call is safe.
|
|
204
|
+
// *
|
|
205
|
+
// * @returns Promise<boolean> - true if an abandoned call was recovered and processed
|
|
206
|
+
// *
|
|
207
|
+
// * @example
|
|
208
|
+
// * // In Angular component
|
|
209
|
+
// * async ngOnInit(): Promise<void> {
|
|
210
|
+
// * const recovered = await this.platoClient.recoverAbandonedCall();
|
|
211
|
+
// * if (recovered) {
|
|
212
|
+
// * console.log('Recovered abandoned call from previous session');
|
|
213
|
+
// * }
|
|
214
|
+
// * }
|
|
215
|
+
// *
|
|
216
|
+
// * @example
|
|
217
|
+
// * // In React component
|
|
218
|
+
// * useEffect(() => {
|
|
219
|
+
// * platoClient.recoverAbandonedCall()
|
|
220
|
+
// * .then(recovered => {
|
|
221
|
+
// * if (recovered) {
|
|
222
|
+
// * console.log('Recovered abandoned call');
|
|
223
|
+
// * }
|
|
224
|
+
// * })
|
|
225
|
+
// * .catch(console.error);
|
|
226
|
+
// * }, []);
|
|
227
|
+
// */
|
|
228
|
+
// async recoverAbandonedCall(): Promise<boolean> {
|
|
229
|
+
// try {
|
|
230
|
+
// const storedState = this.getStoredCallState();
|
|
231
|
+
// if (!storedState) {
|
|
232
|
+
// return false;
|
|
233
|
+
// }
|
|
234
|
+
// if (!this.isCallAbandoned(storedState)) {
|
|
235
|
+
// return false;
|
|
236
|
+
// }
|
|
237
|
+
// console.log('Detected abandoned call, notifying backend:', storedState.callId);
|
|
238
|
+
// try {
|
|
239
|
+
// const response = await this.http.post('/api/v1/postcall/call-ended', {
|
|
240
|
+
// callId: storedState.callId,
|
|
241
|
+
// });
|
|
242
|
+
// console.log('Backend notified of abandoned call:', response.data);
|
|
243
|
+
// } catch (error) {
|
|
244
|
+
// console.error('Failed to notify backend of abandoned call:', error);
|
|
245
|
+
// }
|
|
246
|
+
// this.clearCallState();
|
|
247
|
+
// return true;
|
|
248
|
+
// } catch (error) {
|
|
249
|
+
// console.error('Error during call recovery:', error);
|
|
250
|
+
// this.clearCallState();
|
|
251
|
+
// return false;
|
|
252
|
+
// }
|
|
253
|
+
// }
|
|
259
254
|
async createSimulation(createSimulationParams) {
|
|
260
255
|
try {
|
|
261
256
|
const res = await this.http.post('/api/v1/simulation', {
|
|
@@ -267,9 +262,9 @@ class PlatoApiClient {
|
|
|
267
262
|
};
|
|
268
263
|
}
|
|
269
264
|
catch (e) {
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
}
|
|
265
|
+
// if (axios.isAxiosError(e)) {
|
|
266
|
+
// console.error('Error creating simulation:', e.response?.data.message);
|
|
267
|
+
// }
|
|
273
268
|
throw e;
|
|
274
269
|
}
|
|
275
270
|
}
|
|
@@ -283,9 +278,9 @@ class PlatoApiClient {
|
|
|
283
278
|
return res.data;
|
|
284
279
|
}
|
|
285
280
|
catch (e) {
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
}
|
|
281
|
+
// if (axios.isAxiosError(e)) {
|
|
282
|
+
// console.error('Error getting user simulations:', e.response?.data.message);
|
|
283
|
+
// }
|
|
289
284
|
throw e;
|
|
290
285
|
}
|
|
291
286
|
}
|
|
@@ -295,9 +290,9 @@ class PlatoApiClient {
|
|
|
295
290
|
return res.data;
|
|
296
291
|
}
|
|
297
292
|
catch (e) {
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
}
|
|
293
|
+
// if (axios.isAxiosError(e)) {
|
|
294
|
+
// console.error('Error getting simulation details:', e.response?.data.message);
|
|
295
|
+
// }
|
|
301
296
|
throw e;
|
|
302
297
|
}
|
|
303
298
|
}
|
|
@@ -311,9 +306,9 @@ class PlatoApiClient {
|
|
|
311
306
|
await this.http.delete(`/api/v1/simulation/${simulationId}`);
|
|
312
307
|
}
|
|
313
308
|
catch (e) {
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
}
|
|
309
|
+
// if (axios.isAxiosError(e)) {
|
|
310
|
+
// console.error('Error deleting simulation:', e.response?.data.message);
|
|
311
|
+
// }
|
|
317
312
|
throw e;
|
|
318
313
|
}
|
|
319
314
|
}
|
|
@@ -323,9 +318,9 @@ class PlatoApiClient {
|
|
|
323
318
|
return res.data;
|
|
324
319
|
}
|
|
325
320
|
catch (e) {
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
}
|
|
321
|
+
// if (axios.isAxiosError(e)) {
|
|
322
|
+
// console.error('Error getting call details:', e.response?.data.message);
|
|
323
|
+
// }
|
|
329
324
|
throw e;
|
|
330
325
|
}
|
|
331
326
|
}
|
|
@@ -337,9 +332,9 @@ class PlatoApiClient {
|
|
|
337
332
|
return res.data;
|
|
338
333
|
}
|
|
339
334
|
catch (e) {
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
}
|
|
335
|
+
// if (axios.isAxiosError(e)) {
|
|
336
|
+
// console.error('Error getting call recordings:', e.response?.data.message);
|
|
337
|
+
// }
|
|
343
338
|
throw e;
|
|
344
339
|
}
|
|
345
340
|
}
|
|
@@ -349,9 +344,9 @@ class PlatoApiClient {
|
|
|
349
344
|
return res.data;
|
|
350
345
|
}
|
|
351
346
|
catch (e) {
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
}
|
|
347
|
+
// if (axios.isAxiosError(e)) {
|
|
348
|
+
// console.error('Error getting call recording:', e.response?.data.message);
|
|
349
|
+
// }
|
|
355
350
|
throw e;
|
|
356
351
|
}
|
|
357
352
|
}
|
|
@@ -371,39 +366,218 @@ class PlatoApiClient {
|
|
|
371
366
|
});
|
|
372
367
|
this.eventsAttached = false;
|
|
373
368
|
}
|
|
369
|
+
// async startCall(simulationId: string) {
|
|
370
|
+
// // Check for any previous call state before starting new call
|
|
371
|
+
// // If found, notify backend to ensure it gets processed
|
|
372
|
+
// const storedState = this.getStoredCallState();
|
|
373
|
+
// if (storedState) {
|
|
374
|
+
// try {
|
|
375
|
+
// await this.http.post('/api/v1/postcall/call-ended', {
|
|
376
|
+
// callId: storedState.callId,
|
|
377
|
+
// });
|
|
378
|
+
// } catch {
|
|
379
|
+
// // ignore
|
|
380
|
+
// }
|
|
381
|
+
// }
|
|
382
|
+
//
|
|
383
|
+
// // Now clear the state before starting new call
|
|
384
|
+
// this.clearCallState();
|
|
385
|
+
//
|
|
386
|
+
// this.callControllerInstance = new Vapi(
|
|
387
|
+
// this.config.publicKey,
|
|
388
|
+
// 'https://db41aykk1gw9e.cloudfront.net' // base url
|
|
389
|
+
// );
|
|
390
|
+
// if (!this.eventsAttached) {
|
|
391
|
+
// this.attachEvents();
|
|
392
|
+
// }
|
|
393
|
+
//
|
|
394
|
+
// this.receivedAnyTranscript = false;
|
|
395
|
+
// this.callControllerInstance.on(
|
|
396
|
+
// 'message',
|
|
397
|
+
// (msg: { transcript?: string; type?: string; status?: string; endedReason?: string }) => {
|
|
398
|
+
// if (msg.transcript && msg.transcript.trim().length > 0) {
|
|
399
|
+
// this.receivedAnyTranscript = true;
|
|
400
|
+
// }
|
|
401
|
+
// if (msg.type === 'status-update' && msg.status === 'ended' && msg.endedReason) {
|
|
402
|
+
// const reason = msg.endedReason;
|
|
403
|
+
// const isInactivity = /silence|inactivity/i.test(reason);
|
|
404
|
+
// this.emit('call-ended-reason', { reason, isInactivity });
|
|
405
|
+
// }
|
|
406
|
+
// }
|
|
407
|
+
// );
|
|
408
|
+
//
|
|
409
|
+
// // Internal call-end listener
|
|
410
|
+
// this.callControllerInstance.on('call-end', () => {
|
|
411
|
+
// this.onCallEnded().catch(() => {
|
|
412
|
+
// // ignore
|
|
413
|
+
// });
|
|
414
|
+
// });
|
|
415
|
+
//
|
|
416
|
+
// const { data } = await this.http.get(`/api/v1/simulation/${simulationId}`);
|
|
417
|
+
// const assistantId = data as string;
|
|
418
|
+
//
|
|
419
|
+
// let capturedVapiError: unknown;
|
|
420
|
+
// const vapiErrorCapture = (error: unknown) => {
|
|
421
|
+
// capturedVapiError = error;
|
|
422
|
+
// };
|
|
423
|
+
// this.callControllerInstance.on('error', vapiErrorCapture);
|
|
424
|
+
//
|
|
425
|
+
// let call: Call | null;
|
|
426
|
+
// try {
|
|
427
|
+
// call = await this.callControllerInstance.start(assistantId);
|
|
428
|
+
// } catch (e) {
|
|
429
|
+
// this.callControllerInstance.off('error', vapiErrorCapture);
|
|
430
|
+
// this.callControllerInstance?.stop();
|
|
431
|
+
// this.removeAllEventListeners();
|
|
432
|
+
// this.clearCallState();
|
|
433
|
+
// throw e;
|
|
434
|
+
// }
|
|
435
|
+
//
|
|
436
|
+
// this.callControllerInstance.off('error', vapiErrorCapture);
|
|
437
|
+
//
|
|
438
|
+
// if (!call || !call.assistantId) {
|
|
439
|
+
// this.callControllerInstance?.stop();
|
|
440
|
+
// this.removeAllEventListeners();
|
|
441
|
+
// this.clearCallState();
|
|
442
|
+
// if (isCallConcurrencyError(capturedVapiError)) {
|
|
443
|
+
// throw new CallConcurrencyLimitError();
|
|
444
|
+
// }
|
|
445
|
+
// throw new Error('Cannot start a call, please try again later');
|
|
446
|
+
// }
|
|
447
|
+
// try {
|
|
448
|
+
// const apiCall = await this.createCall({
|
|
449
|
+
// callId: call.id,
|
|
450
|
+
// assistantId: call.assistantId,
|
|
451
|
+
// });
|
|
452
|
+
//
|
|
453
|
+
// this.currentCallId = apiCall._id;
|
|
454
|
+
//
|
|
455
|
+
// this.storeCallState({
|
|
456
|
+
// callId: apiCall._id,
|
|
457
|
+
// externalCallId: call.id,
|
|
458
|
+
// simulationId: simulationId,
|
|
459
|
+
// startedAt: new Date().toISOString(),
|
|
460
|
+
// version: 1,
|
|
461
|
+
// });
|
|
462
|
+
//
|
|
463
|
+
// return {
|
|
464
|
+
// stopCall: () => {
|
|
465
|
+
// this.callControllerInstance?.stop();
|
|
466
|
+
// },
|
|
467
|
+
// callId: apiCall._id,
|
|
468
|
+
// on: <K extends CallEventNames>(event: K, listener: CallEventListener<K>) =>
|
|
469
|
+
// this.on(event, listener),
|
|
470
|
+
// off: <K extends CallEventNames>(event: K, listener: CallEventListener<K>) =>
|
|
471
|
+
// this.off(event, listener),
|
|
472
|
+
// };
|
|
473
|
+
// } catch (e) {
|
|
474
|
+
// this.callControllerInstance?.stop();
|
|
475
|
+
// this.removeAllEventListeners();
|
|
476
|
+
// this.clearCallState();
|
|
477
|
+
// throw e;
|
|
478
|
+
// }
|
|
479
|
+
// }
|
|
480
|
+
/**
|
|
481
|
+
* Starts a call for the given simulation.
|
|
482
|
+
* Polls the backend after call-end until the call is finalized, then emits 'call-details-ready'.
|
|
483
|
+
*/
|
|
374
484
|
async startCall(simulationId) {
|
|
375
|
-
|
|
376
|
-
// If found, notify backend to ensure it gets processed
|
|
377
|
-
const storedState = this.getStoredCallState();
|
|
378
|
-
if (storedState) {
|
|
379
|
-
console.log('Found previous call state, notifying backend before starting new call:', storedState.callId);
|
|
380
|
-
try {
|
|
381
|
-
await this.http.post('/api/v1/postcall/call-ended', {
|
|
382
|
-
callId: storedState.callId,
|
|
383
|
-
});
|
|
384
|
-
console.log('Backend notified of previous call');
|
|
385
|
-
}
|
|
386
|
-
catch (error) {
|
|
387
|
-
console.error('Failed to notify backend of previous call:', error);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
// Now clear the state before starting new call
|
|
391
|
-
this.clearCallState();
|
|
392
|
-
this.callControllerInstance = new web_1.default(this.config.publicKey, 'https://db41aykk1gw9e.cloudfront.net' // base url
|
|
393
|
-
);
|
|
485
|
+
this.callControllerInstance = new web_1.default(this.config.publicKey, 'https://db41aykk1gw9e.cloudfront.net');
|
|
394
486
|
if (!this.eventsAttached) {
|
|
395
487
|
this.attachEvents();
|
|
396
488
|
}
|
|
397
|
-
|
|
489
|
+
this.callControllerInstance.on('message', (msg) => {
|
|
490
|
+
if (msg.type === 'status-update' && msg.status === 'ended' && msg.endedReason) {
|
|
491
|
+
const reason = msg.endedReason;
|
|
492
|
+
const isInactivity = /silence|inactivity/i.test(reason);
|
|
493
|
+
this.emit('call-ended-reason', { reason, isInactivity });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
let callId;
|
|
497
|
+
let stopPolling;
|
|
398
498
|
this.callControllerInstance.on('call-end', () => {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
499
|
+
if (!callId)
|
|
500
|
+
return;
|
|
501
|
+
const callAge = Date.now() - callStartedAt;
|
|
502
|
+
// console.log(`[PlatoSDK] Call ended. Age: ${callAge}ms (${(callAge / 1000).toFixed(2)}s)`);
|
|
503
|
+
if (callAge < 20000) {
|
|
504
|
+
// Call ended within 20s — return whatever is in the DB without requiring a transcript
|
|
505
|
+
const fetchAndEmit = async () => {
|
|
506
|
+
try {
|
|
507
|
+
// console.log('[PlatoSDK] Early end (<20s) — fetching call details immediately (no transcript check)');
|
|
508
|
+
const { data } = await this.http.get(`/api/v1/simulation/call/${callId}`);
|
|
509
|
+
this.emit('call-details-ready', data);
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
this.emit('error', e instanceof Error ? e : new Error('Failed to fetch call details'));
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
this.removeAllEventListeners();
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
void fetchAndEmit();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let stopped = false;
|
|
522
|
+
let nextPollTimer;
|
|
523
|
+
const poll = async () => {
|
|
524
|
+
if (stopped)
|
|
525
|
+
return;
|
|
526
|
+
try {
|
|
527
|
+
// console.log('[PlatoSDK] Polling for call details (waiting for transcript)...');
|
|
528
|
+
const { data } = await this.http.get(`/api/v1/simulation/call/${callId}`);
|
|
529
|
+
if (data.transcript) {
|
|
530
|
+
stopped = true;
|
|
531
|
+
clearTimeout(globalTimer);
|
|
532
|
+
// console.log('[PlatoSDK] Transcript ready — emitting call-details-ready');
|
|
533
|
+
this.emit('call-details-ready', data);
|
|
534
|
+
this.removeAllEventListeners();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// ignore transient errors, keep polling
|
|
540
|
+
}
|
|
541
|
+
nextPollTimer = setTimeout(() => void poll(), 3000);
|
|
542
|
+
};
|
|
543
|
+
nextPollTimer = setTimeout(() => void poll(), 3000);
|
|
544
|
+
const globalTimer = setTimeout(() => {
|
|
545
|
+
stopped = true;
|
|
546
|
+
clearTimeout(nextPollTimer);
|
|
547
|
+
this.removeAllEventListeners();
|
|
548
|
+
}, 60000);
|
|
549
|
+
stopPolling = () => {
|
|
550
|
+
stopped = true;
|
|
551
|
+
clearTimeout(nextPollTimer);
|
|
552
|
+
clearTimeout(globalTimer);
|
|
553
|
+
};
|
|
402
554
|
});
|
|
403
555
|
const { data } = await this.http.get(`/api/v1/simulation/${simulationId}`);
|
|
404
556
|
const assistantId = data;
|
|
405
|
-
|
|
557
|
+
let capturedVapiError;
|
|
558
|
+
const vapiErrorCapture = (error) => {
|
|
559
|
+
capturedVapiError = error;
|
|
560
|
+
};
|
|
561
|
+
this.callControllerInstance.on('error', vapiErrorCapture);
|
|
562
|
+
const callStartedAt = Date.now();
|
|
563
|
+
// console.log('[PlatoSDK] Call start initiated at:', new Date(callStartedAt).toISOString());
|
|
564
|
+
let call;
|
|
565
|
+
try {
|
|
566
|
+
call = await this.callControllerInstance.start(assistantId);
|
|
567
|
+
}
|
|
568
|
+
catch (e) {
|
|
569
|
+
this.callControllerInstance.off('error', vapiErrorCapture);
|
|
570
|
+
this.callControllerInstance.stop();
|
|
571
|
+
this.removeAllEventListeners();
|
|
572
|
+
throw e;
|
|
573
|
+
}
|
|
574
|
+
this.callControllerInstance.off('error', vapiErrorCapture);
|
|
406
575
|
if (!call || !call.assistantId) {
|
|
576
|
+
this.callControllerInstance.stop();
|
|
577
|
+
this.removeAllEventListeners();
|
|
578
|
+
if ((0, utils_1.isCallConcurrencyError)(capturedVapiError)) {
|
|
579
|
+
throw new utils_1.CallConcurrencyLimitError();
|
|
580
|
+
}
|
|
407
581
|
throw new Error('Cannot start a call, please try again later');
|
|
408
582
|
}
|
|
409
583
|
try {
|
|
@@ -411,82 +585,62 @@ class PlatoApiClient {
|
|
|
411
585
|
callId: call.id,
|
|
412
586
|
assistantId: call.assistantId,
|
|
413
587
|
});
|
|
414
|
-
|
|
415
|
-
this.currentCallId = apiCall._id;
|
|
416
|
-
// Store call state in localStorage for recovery
|
|
417
|
-
this.storeCallState({
|
|
418
|
-
callId: apiCall._id,
|
|
419
|
-
externalCallId: call.id,
|
|
420
|
-
simulationId: simulationId,
|
|
421
|
-
startedAt: new Date().toISOString(),
|
|
422
|
-
version: 1,
|
|
423
|
-
});
|
|
424
|
-
// Return stopCall, callId, and event subscription methods with strict typing
|
|
588
|
+
callId = apiCall._id;
|
|
425
589
|
return {
|
|
426
590
|
stopCall: () => {
|
|
427
591
|
this.callControllerInstance?.stop();
|
|
428
|
-
|
|
429
|
-
this.clearCallState();
|
|
592
|
+
stopPolling?.();
|
|
430
593
|
},
|
|
431
594
|
callId: apiCall._id,
|
|
432
|
-
/**
|
|
433
|
-
* Subscribe to call events for this call with strict typing.
|
|
434
|
-
* @param event Event name
|
|
435
|
-
* @param listener Listener function
|
|
436
|
-
*/
|
|
437
595
|
on: (event, listener) => this.on(event, listener),
|
|
438
|
-
/**
|
|
439
|
-
* Unsubscribe from call events for this call with strict typing.
|
|
440
|
-
* @param event Event name
|
|
441
|
-
* @param listener Listener function
|
|
442
|
-
*/
|
|
443
596
|
off: (event, listener) => this.off(event, listener),
|
|
444
597
|
};
|
|
445
598
|
}
|
|
446
599
|
catch (e) {
|
|
447
600
|
this.callControllerInstance?.stop();
|
|
448
601
|
this.removeAllEventListeners();
|
|
449
|
-
this.clearCallState();
|
|
450
602
|
throw e;
|
|
451
603
|
}
|
|
452
604
|
}
|
|
453
|
-
async onCallEnded() {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
605
|
+
// private async onCallEnded(): Promise<void> {
|
|
606
|
+
// let callIdForRetry: string | undefined;
|
|
607
|
+
// try {
|
|
608
|
+
// if (!this.currentCallId) {
|
|
609
|
+
// return;
|
|
610
|
+
// }
|
|
611
|
+
//
|
|
612
|
+
// if (!this.receivedAnyTranscript) {
|
|
613
|
+
// return;
|
|
614
|
+
// }
|
|
615
|
+
//
|
|
616
|
+
// callIdForRetry = this.currentCallId;
|
|
617
|
+
//
|
|
618
|
+
// // First attempt
|
|
619
|
+
// let response = await this.http.post('/api/v1/postcall/call-ended', {
|
|
620
|
+
// callId: callIdForRetry,
|
|
621
|
+
// });
|
|
622
|
+
//
|
|
623
|
+
// // If status is "processing", retry once immediately
|
|
624
|
+
// if (response.data?.status === 'processing') {
|
|
625
|
+
// response = await this.http.post('/api/v1/postcall/call-ended', {
|
|
626
|
+
// callId: callIdForRetry,
|
|
627
|
+
// });
|
|
628
|
+
// }
|
|
629
|
+
//
|
|
630
|
+
// try {
|
|
631
|
+
// const callDetails = await this.getCallDetails(callIdForRetry);
|
|
632
|
+
// this.emit('call-details-ready', callDetails);
|
|
633
|
+
// } catch {
|
|
634
|
+
// // ignore
|
|
635
|
+
// }
|
|
636
|
+
// } catch {
|
|
637
|
+
// // ignore
|
|
638
|
+
// } finally {
|
|
639
|
+
// this.currentCallId = undefined;
|
|
640
|
+
// this.clearCallState();
|
|
641
|
+
// this.removeAllEventListeners();
|
|
642
|
+
// }
|
|
643
|
+
// }
|
|
490
644
|
async createCall(payload) {
|
|
491
645
|
const response = await this.http.post('/api/v1/simulation/call', {
|
|
492
646
|
...payload,
|
|
@@ -512,10 +666,16 @@ class PlatoApiClient {
|
|
|
512
666
|
if (/[^a-zA-Z0-9._ -]/.test(filename)) {
|
|
513
667
|
throw new Error('Filename contains invalid characters. Only English letters, numbers, dots, hyphens, and underscores are allowed.');
|
|
514
668
|
}
|
|
515
|
-
const { presignedPost, pdfId } = (await this.http.post('/api/v1/pdfSlides/request-upload', {
|
|
669
|
+
const { presignedPost, pdfId, alreadyExists } = (await this.http.post('/api/v1/pdfSlides/request-upload', {
|
|
516
670
|
contentHash,
|
|
517
671
|
filename,
|
|
518
672
|
})).data;
|
|
673
|
+
if (alreadyExists) {
|
|
674
|
+
throw new utils_1.PdfAlreadyExistsError(pdfId);
|
|
675
|
+
}
|
|
676
|
+
if (!presignedPost) {
|
|
677
|
+
throw new Error('[uploadPdfSlides] No presigned post URL returned from server.');
|
|
678
|
+
}
|
|
519
679
|
const formData = new FormData();
|
|
520
680
|
Object.entries(presignedPost.fields).forEach(([key, value]) => {
|
|
521
681
|
formData.append(key, value);
|
|
@@ -533,8 +693,8 @@ class PlatoApiClient {
|
|
|
533
693
|
try {
|
|
534
694
|
await this.deleteSlideAnalysis(pdfId);
|
|
535
695
|
}
|
|
536
|
-
catch
|
|
537
|
-
console.error('Failed to clean up PDF record after upload failure:', deleteError);
|
|
696
|
+
catch {
|
|
697
|
+
// console.error('Failed to clean up PDF record after upload failure:', deleteError);
|
|
538
698
|
}
|
|
539
699
|
throw uploadError;
|
|
540
700
|
}
|
|
@@ -548,12 +708,14 @@ class PlatoApiClient {
|
|
|
548
708
|
return pdfId;
|
|
549
709
|
}
|
|
550
710
|
catch (e) {
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
711
|
+
// if (axios.isAxiosError(e)) {
|
|
712
|
+
// console.error('Error uploading PDF slides:', e.response?.data?.message || e.message);
|
|
713
|
+
// } else {
|
|
714
|
+
// console.error(
|
|
715
|
+
// 'Error uploading PDF slides:',
|
|
716
|
+
// e instanceof Error ? e.message : 'Unknown error'
|
|
717
|
+
// );
|
|
718
|
+
// }
|
|
557
719
|
throw e;
|
|
558
720
|
}
|
|
559
721
|
}
|
|
@@ -563,23 +725,9 @@ class PlatoApiClient {
|
|
|
563
725
|
return res.data;
|
|
564
726
|
}
|
|
565
727
|
catch (e) {
|
|
566
|
-
if (
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
throw e;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
async getSlidesAnalysis(queryParams) {
|
|
573
|
-
try {
|
|
574
|
-
const res = await this.http.get('/api/v1/pdfSlides', {
|
|
575
|
-
params: queryParams,
|
|
576
|
-
});
|
|
577
|
-
return res.data;
|
|
578
|
-
}
|
|
579
|
-
catch (e) {
|
|
580
|
-
if (axios_1.default.isAxiosError(e)) {
|
|
581
|
-
console.error('Error getting PDF slides analysis:', e.response?.data.message);
|
|
582
|
-
}
|
|
728
|
+
// if (axios.isAxiosError(e)) {
|
|
729
|
+
// console.error('Error getting recommendations:', e.response?.data.message);
|
|
730
|
+
// }
|
|
583
731
|
throw e;
|
|
584
732
|
}
|
|
585
733
|
}
|
|
@@ -589,9 +737,9 @@ class PlatoApiClient {
|
|
|
589
737
|
return res.data;
|
|
590
738
|
}
|
|
591
739
|
catch (e) {
|
|
592
|
-
if (
|
|
593
|
-
|
|
594
|
-
}
|
|
740
|
+
// if (axios.isAxiosError(e)) {
|
|
741
|
+
// console.error('Error getting PDF slide analysis by ID:', e.response?.data.message);
|
|
742
|
+
// }
|
|
595
743
|
throw e;
|
|
596
744
|
}
|
|
597
745
|
}
|
|
@@ -600,9 +748,9 @@ class PlatoApiClient {
|
|
|
600
748
|
await this.http.delete(`/api/v1/pdfSlides/${id}`);
|
|
601
749
|
}
|
|
602
750
|
catch (e) {
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
}
|
|
751
|
+
// if (axios.isAxiosError(e)) {
|
|
752
|
+
// console.error('Error deleting PDF slide analysis:', e.response?.data.message);
|
|
753
|
+
// }
|
|
606
754
|
throw e;
|
|
607
755
|
}
|
|
608
756
|
}
|
|
@@ -612,9 +760,9 @@ class PlatoApiClient {
|
|
|
612
760
|
return res.data;
|
|
613
761
|
}
|
|
614
762
|
catch (e) {
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
}
|
|
763
|
+
// if (axios.isAxiosError(e)) {
|
|
764
|
+
// console.error('Error checking PDF status:', e.response?.data.message);
|
|
765
|
+
// }
|
|
618
766
|
throw e;
|
|
619
767
|
}
|
|
620
768
|
}
|
|
@@ -624,9 +772,9 @@ class PlatoApiClient {
|
|
|
624
772
|
return res.data;
|
|
625
773
|
}
|
|
626
774
|
catch (e) {
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
}
|
|
775
|
+
// if (axios.isAxiosError(e)) {
|
|
776
|
+
// console.error('Error getting assistant images:', e.response?.data.message);
|
|
777
|
+
// }
|
|
630
778
|
throw e;
|
|
631
779
|
}
|
|
632
780
|
}
|
|
@@ -642,9 +790,9 @@ class PlatoApiClient {
|
|
|
642
790
|
await this.http.delete(`/api/v1/simulation/call/${callId}`);
|
|
643
791
|
}
|
|
644
792
|
catch (e) {
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
}
|
|
793
|
+
// if (axios.isAxiosError(e)) {
|
|
794
|
+
// console.error('Error deleting call:', e.response?.data.message);
|
|
795
|
+
// }
|
|
648
796
|
throw e;
|
|
649
797
|
}
|
|
650
798
|
}
|