@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.
@@ -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
- * Store active call state in localStorage for recovery purposes.
135
- * @private
136
- */
137
- storeCallState(state) {
138
- try {
139
- localStorage.setItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY, JSON.stringify(state));
140
- }
141
- catch (error) {
142
- console.warn('Failed to store call state:', error);
143
- }
144
- }
145
- /**
146
- * Retrieve active call state from localStorage.
147
- * Validates the stored data and clears it if invalid.
148
- * @private
149
- * @returns The stored call state or null if not found or invalid
150
- */
151
- getStoredCallState() {
152
- try {
153
- const stored = localStorage.getItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY);
154
- if (!stored) {
155
- return null;
156
- }
157
- const parsed = JSON.parse(stored);
158
- // Validate required fields
159
- if (!parsed.callId || !parsed.externalCallId || !parsed.simulationId || !parsed.startedAt) {
160
- console.warn('Invalid stored call state, clearing');
161
- this.clearCallState();
162
- return null;
163
- }
164
- return parsed;
165
- }
166
- catch (error) {
167
- console.warn('Failed to retrieve call state:', error);
168
- return null;
169
- }
170
- }
171
- /**
172
- * Clear active call state from localStorage.
173
- * @private
174
- */
175
- clearCallState() {
176
- try {
177
- localStorage.removeItem(PlatoApiClient.ACTIVE_CALL_STORAGE_KEY);
178
- }
179
- catch (error) {
180
- console.warn('Failed to clear call state:', error);
181
- }
182
- }
183
- /**
184
- * Check if a stored call is considered abandoned based on age.
185
- * Calls older than 5 minutes are considered abandoned.
186
- * @private
187
- * @param state The call state to check
188
- * @returns true if the call is abandoned, false otherwise
189
- */
190
- isCallAbandoned(state) {
191
- const ABANDONMENT_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
192
- const startedAt = new Date(state.startedAt).getTime();
193
- const now = Date.now();
194
- const age = now - startedAt;
195
- return age > ABANDONMENT_THRESHOLD_MS;
196
- }
197
- /**
198
- * Recover and clean up any abandoned calls from previous sessions.
199
- *
200
- * This method should be called during application initialization,
201
- * typically in ngOnInit() or useEffect(). It detects calls that were
202
- * active when the page was last refreshed and notifies the backend
203
- * to process them if they're older than 5 minutes.
204
- *
205
- * The backend endpoint is idempotent, so calling this method multiple
206
- * times for the same call is safe.
207
- *
208
- * @returns Promise<boolean> - true if an abandoned call was recovered and processed
209
- *
210
- * @example
211
- * // In Angular component
212
- * async ngOnInit(): Promise<void> {
213
- * const recovered = await this.platoClient.recoverAbandonedCall();
214
- * if (recovered) {
215
- * console.log('Recovered abandoned call from previous session');
216
- * }
217
- * }
218
- *
219
- * @example
220
- * // In React component
221
- * useEffect(() => {
222
- * platoClient.recoverAbandonedCall()
223
- * .then(recovered => {
224
- * if (recovered) {
225
- * console.log('Recovered abandoned call');
226
- * }
227
- * })
228
- * .catch(console.error);
229
- * }, []);
230
- */
231
- async recoverAbandonedCall() {
232
- try {
233
- const storedState = this.getStoredCallState();
234
- if (!storedState) {
235
- return false;
236
- }
237
- if (!this.isCallAbandoned(storedState)) {
238
- return false;
239
- }
240
- console.log('Detected abandoned call, notifying backend:', storedState.callId);
241
- try {
242
- const response = await this.http.post('/api/v1/postcall/call-ended', {
243
- callId: storedState.callId,
244
- });
245
- console.log('Backend notified of abandoned call:', response.data);
246
- }
247
- catch (error) {
248
- console.error('Failed to notify backend of abandoned call:', error);
249
- }
250
- this.clearCallState();
251
- return true;
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 (axios_1.default.isAxiosError(e)) {
271
- console.error('Error creating simulation:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
287
- console.error('Error getting user simulations:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
299
- console.error('Error getting simulation details:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
315
- console.error('Error deleting simulation:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
327
- console.error('Error getting call details:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
341
- console.error('Error getting call recordings:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
353
- console.error('Error getting call recording:', e.response?.data.message);
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
- // Check for any previous call state before starting new call
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
- // Internal call-end listener
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
- this.onCallEnded().catch(error => {
400
- console.error('Error in onCallEnded: ', error);
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
- const call = await this.callControllerInstance.start(assistantId);
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
- // Store the callId for use in onCallEnded
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
- this.removeAllEventListeners();
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
- let callIdForRetry;
455
- try {
456
- if (!this.currentCallId) {
457
- return;
458
- }
459
- callIdForRetry = this.currentCallId;
460
- // First attempt
461
- let response = await this.http.post('/api/v1/postcall/call-ended', {
462
- callId: callIdForRetry,
463
- });
464
- // If status is "processing", retry once immediately
465
- if (response.data?.status === 'processing') {
466
- response = await this.http.post('/api/v1/postcall/call-ended', {
467
- callId: callIdForRetry,
468
- });
469
- }
470
- // After onCallEnded completes successfully, fetch and emit call details
471
- try {
472
- const callDetails = await this.getCallDetails(callIdForRetry);
473
- this.emit('call-details-ready', callDetails);
474
- }
475
- catch (error) {
476
- console.error('Error fetching call details after call ended:', error);
477
- // Don't throw - we don't want to break the onCallEnded flow
478
- }
479
- }
480
- catch {
481
- // Silently handle errors in post-call processing
482
- }
483
- finally {
484
- // Clean up the callId after processing (success or failure)
485
- this.currentCallId = undefined;
486
- // Clear stored call state after normal call end
487
- this.clearCallState();
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 (deleteError) {
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 (axios_1.default.isAxiosError(e)) {
552
- console.error('Error uploading PDF slides:', e.response?.data?.message || e.message);
553
- }
554
- else {
555
- console.error('Error uploading PDF slides:', e instanceof Error ? e.message : 'Unknown error');
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 (axios_1.default.isAxiosError(e)) {
567
- console.error('Error getting recommendations:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
593
- console.error('Error getting PDF slide analysis by ID:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
604
- console.error('Error deleting PDF slide analysis:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
616
- console.error('Error checking PDF status:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
628
- console.error('Error getting assistant images:', e.response?.data.message);
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 (axios_1.default.isAxiosError(e)) {
646
- console.error('Error deleting call:', e.response?.data.message);
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
  }