@@ -172,6 +172,16 @@ let micStream: MediaStream | null = null;
172
172
let systemStream: MediaStream | null = null ;
173
173
let sessionsReady = false ;
174
174
175
+ // Conversation session tracking
176
+ const currentSessionId = ref <number | null >(null );
177
+ const isSavingData = ref (false );
178
+ const transcriptQueue: Array <{ speaker: string ; text: string ; timestamp: number ; groupId? : string ; systemCategory? : string }> = [];
179
+ const insightQueue: Array <{ type: string ; data: any ; timestamp: number }> = [];
180
+ let saveInterval: NodeJS .Timeout | null = null ;
181
+ const callStartTime = ref <Date | null >(null );
182
+ const callDurationSeconds = ref (0 );
183
+ let durationInterval: NodeJS .Timeout | null = null ;
184
+
175
185
// Tool definitions using SDK
176
186
const coachingTools = [
177
187
tool ({
@@ -266,9 +276,23 @@ const coachingTools = [
266
276
267
277
// Function call handler (same as original)
268
278
const handleFunctionCall = (name : string , args : any ) => {
279
+ const timestamp = Date .now ();
280
+
269
281
switch (name ) {
270
282
case ' track_discussion_topic' :
271
283
realtimeStore .trackDiscussionTopic (args .name , args .sentiment , args .context );
284
+ // Queue for saving
285
+ if (currentSessionId .value ) {
286
+ insightQueue .push ({
287
+ type: ' topic' ,
288
+ data: {
289
+ name: args .name ,
290
+ sentiment: args .sentiment ,
291
+ context: args .context ,
292
+ },
293
+ timestamp: timestamp ,
294
+ });
295
+ }
272
296
break ;
273
297
274
298
case ' analyze_customer_intent' :
@@ -282,14 +306,53 @@ const handleFunctionCall = (name: string, args: any) => {
282
306
283
307
case ' highlight_insight' :
284
308
realtimeStore .addKeyInsight (args .type , args .text , args .importance );
309
+ // Queue for saving
310
+ if (currentSessionId .value ) {
311
+ insightQueue .push ({
312
+ type: ' key_insight' ,
313
+ data: {
314
+ type: args .type ,
315
+ text: args .text ,
316
+ importance: args .importance ,
317
+ },
318
+ timestamp: timestamp ,
319
+ });
320
+ }
285
321
break ;
286
322
287
323
case ' detect_commitment' :
288
324
realtimeStore .captureCommitment (args .speaker , args .text , args .type , args .deadline );
325
+ // Queue for saving
326
+ if (currentSessionId .value ) {
327
+ insightQueue .push ({
328
+ type: ' commitment' ,
329
+ data: {
330
+ speaker: args .speaker ,
331
+ text: args .text ,
332
+ type: args .type ,
333
+ deadline: args .deadline ,
334
+ },
335
+ timestamp: timestamp ,
336
+ });
337
+ }
289
338
break ;
290
339
291
340
case ' create_action_item' :
292
341
realtimeStore .addActionItem (args .text , args .owner , args .type , args .deadline , args .relatedCommitment );
342
+ // Queue for saving
343
+ if (currentSessionId .value ) {
344
+ insightQueue .push ({
345
+ type: ' action_item' ,
346
+ data: {
347
+ text: args .text ,
348
+ owner: args .owner ,
349
+ type: args .type ,
350
+ deadline: args .deadline ,
351
+ relatedCommitment: args .relatedCommitment ,
352
+ },
353
+ timestamp: timestamp ,
354
+ });
355
+ }
293
356
break ;
294
357
295
358
case ' detect_information_need' :
@@ -299,6 +362,34 @@ const handleFunctionCall = (name: string, args: any) => {
299
362
}
300
363
};
301
364
365
+ // Helper to add system messages and queue them for saving
366
+ const addSystemMessage = (messages : string | string [], systemCategory ? : string ) => {
367
+ const timestamp = Date .now ();
368
+ const groupId = ` system-${timestamp } ` ;
369
+ const messageArray = Array .isArray (messages ) ? messages : [messages ];
370
+
371
+ realtimeStore .addTranscriptGroup ({
372
+ id: groupId ,
373
+ role: ' system' ,
374
+ messages: messageArray .map (text => ({ text , timestamp })),
375
+ startTime: timestamp ,
376
+ systemCategory ,
377
+ });
378
+
379
+ // Queue for database saving if we have a session
380
+ if (currentSessionId .value ) {
381
+ messageArray .forEach (text => {
382
+ transcriptQueue .push ({
383
+ speaker: ' system' ,
384
+ text ,
385
+ timestamp ,
386
+ groupId ,
387
+ systemCategory ,
388
+ });
389
+ });
390
+ }
391
+ };
392
+
302
393
// Check onboarding requirements
303
394
const checkOnboardingRequirements = async () => {
304
395
// Check if onboarding was already completed
@@ -428,14 +519,11 @@ const startCall = async () => {
428
519
// Auto-enable screen protection during calls
429
520
screenProtection .enableForCall ();
430
521
522
+ // Start conversation session in database
523
+ await startConversationSession ();
431
524
432
525
// Add initial system message
433
- realtimeStore .addTranscriptGroup ({
434
- id: ` system-${Date .now ()} ` ,
435
- role: ' system' ,
436
- messages: [{ text: ' 📞 Call started' , timestamp: Date .now () }],
437
- startTime: Date .now (),
438
- });
526
+ addSystemMessage (' 📞 Call started' );
439
527
440
528
} catch (error ) {
441
529
console .error (' Failed to start call:' , error );
@@ -457,6 +545,28 @@ const startCall = async () => {
457
545
458
546
const endCall = async () => {
459
547
try {
548
+ // Save any remaining data before stopping
549
+ if (currentSessionId .value ) {
550
+ await saveQueuedData (true ); // Force save
551
+ await endConversationSession ();
552
+ }
553
+
554
+ // Clear save interval
555
+ if (saveInterval ) {
556
+ clearInterval (saveInterval );
557
+ saveInterval = null ;
558
+ }
559
+
560
+ // Clear duration interval
561
+ if (durationInterval ) {
562
+ clearInterval (durationInterval );
563
+ durationInterval = null ;
564
+ }
565
+
566
+ // Reset call tracking
567
+ callStartTime .value = null ;
568
+ callDurationSeconds .value = 0 ;
569
+
460
570
// Stop microphone stream
461
571
if (micStream ) {
462
572
micStream .getTracks ().forEach (track => track .stop ());
@@ -517,12 +627,7 @@ const endCall = async () => {
517
627
realtimeStore .setConnectionStatus (' disconnected' );
518
628
519
629
// Add end message
520
- realtimeStore .addTranscriptGroup ({
521
- id: ` system-${Date .now ()} ` ,
522
- role: ' system' ,
523
- messages: [{ text: ' Call ended.' , timestamp: Date .now () }],
524
- startTime: Date .now (),
525
- });
630
+ addSystemMessage (' Call ended.' );
526
631
527
632
} catch (error ) {
528
633
console .error (' Failed to end call:' , error );
@@ -694,17 +799,19 @@ const setupSessionHandlers = () => {
694
799
if (salespersonSession .transport ) {
695
800
salespersonSession .transport .on (' conversation.item.input_audio_transcription.completed' , (event : any ) => {
696
801
if (event .transcript ) {
802
+ const timestamp = Date .now ();
803
+ const groupId = ` salesperson-${timestamp } ` ;
804
+
697
805
// Try to append to last group if same speaker
698
806
const appended = realtimeStore .appendToLastTranscriptGroup (' salesperson' , event .transcript );
699
807
700
808
if (! appended ) {
701
809
// Create new group if not appended
702
- const groupId = ` salesperson-${Date .now ()} ` ;
703
810
realtimeStore .addTranscriptGroup ({
704
811
id: groupId ,
705
812
role: ' salesperson' ,
706
- messages: [{ text: event .transcript , timestamp: Date . now () }],
707
- startTime: Date . now () ,
813
+ messages: [{ text: event .transcript , timestamp: timestamp }],
814
+ startTime: timestamp ,
708
815
});
709
816
}
710
817
@@ -729,17 +836,19 @@ const setupSessionHandlers = () => {
729
836
if (coachSession .transport ) {
730
837
coachSession .transport .on (' conversation.item.input_audio_transcription.completed' , (event : any ) => {
731
838
if (event .transcript ) {
839
+ const timestamp = Date .now ();
840
+ const groupId = ` customer-${timestamp } ` ;
841
+
732
842
// Try to append to last group if same speaker
733
843
const appended = realtimeStore .appendToLastTranscriptGroup (' customer' , event .transcript );
734
844
735
845
if (! appended ) {
736
846
// Create new group if not appended
737
- const groupId = ` customer-${Date .now ()} ` ;
738
847
realtimeStore .addTranscriptGroup ({
739
848
id: groupId ,
740
849
role: ' customer' ,
741
- messages: [{ text: event .transcript , timestamp: Date . now () }],
742
- startTime: Date . now () ,
850
+ messages: [{ text: event .transcript , timestamp: timestamp }],
851
+ startTime: timestamp ,
743
852
});
744
853
}
745
854
@@ -1223,6 +1332,101 @@ watch(selectedTemplate, (newTemplate) => {
1223
1332
}
1224
1333
});
1225
1334
1335
+ // Conversation session management functions
1336
+ const startConversationSession = async () => {
1337
+ try {
1338
+ const response = await axios .post (' /conversations' , {
1339
+ template_used: selectedTemplate .value ?.name || null ,
1340
+ customer_name: realtimeStore .customerInfo .name || null ,
1341
+ customer_company: realtimeStore .customerInfo .company || null ,
1342
+ });
1343
+
1344
+ currentSessionId .value = response .data .session_id ;
1345
+ callStartTime .value = new Date ();
1346
+
1347
+ // Start periodic saving
1348
+ saveInterval = setInterval (() => {
1349
+ saveQueuedData ();
1350
+ }, 5000 ); // Save every 5 seconds
1351
+
1352
+ // Start duration timer
1353
+ durationInterval = setInterval (() => {
1354
+ if (realtimeStore .isActive ) {
1355
+ callDurationSeconds .value ++ ;
1356
+ }
1357
+ }, 1000 );
1358
+
1359
+ } catch (error ) {
1360
+ console .error (' Failed to start conversation session:' , error );
1361
+ }
1362
+ };
1363
+
1364
+ const endConversationSession = async () => {
1365
+ if (! currentSessionId .value ) return ;
1366
+
1367
+ try {
1368
+ // Save final state
1369
+ await axios .post (` /conversations/${currentSessionId .value }/end ` , {
1370
+ duration_seconds: callDurationSeconds .value ,
1371
+ final_intent: realtimeStore .customerIntelligence .intent ,
1372
+ final_buying_stage: realtimeStore .customerIntelligence .buyingStage ,
1373
+ final_engagement_level: realtimeStore .customerIntelligence .engagementLevel ,
1374
+ final_sentiment: realtimeStore .customerIntelligence .sentiment ,
1375
+ ai_summary: null , // Could generate a summary here if needed
1376
+ });
1377
+
1378
+ currentSessionId .value = null ;
1379
+ } catch (error ) {
1380
+ console .error (' Failed to end conversation session:' , error );
1381
+ }
1382
+ };
1383
+
1384
+ const saveQueuedData = async (force : boolean = false ) => {
1385
+ if (! currentSessionId .value || isSavingData .value ) return ;
1386
+
1387
+ // Only save if we have data or forced
1388
+ if (! force && transcriptQueue .length === 0 && insightQueue .length === 0 ) return ;
1389
+
1390
+ isSavingData .value = true ;
1391
+
1392
+ try {
1393
+ // Save transcripts
1394
+ if (transcriptQueue .length > 0 ) {
1395
+ const transcriptsToSave = [... transcriptQueue ];
1396
+ transcriptQueue .length = 0 ; // Clear queue
1397
+
1398
+ await axios .post (` /conversations/${currentSessionId .value }/transcripts ` , {
1399
+ transcripts: transcriptsToSave .map ((t ) => ({
1400
+ speaker: t .speaker ,
1401
+ text: t .text ,
1402
+ spoken_at: t .timestamp ,
1403
+ group_id: t .groupId || null ,
1404
+ system_category: t .systemCategory || null ,
1405
+ })),
1406
+ });
1407
+ }
1408
+
1409
+ // Save insights
1410
+ if (insightQueue .length > 0 ) {
1411
+ const insightsToSave = [... insightQueue ];
1412
+ insightQueue .length = 0 ; // Clear queue
1413
+
1414
+ await axios .post (` /conversations/${currentSessionId .value }/insights ` , {
1415
+ insights: insightsToSave .map ((i ) => ({
1416
+ insight_type: i .type ,
1417
+ data: i .data ,
1418
+ captured_at: i .timestamp ,
1419
+ })),
1420
+ });
1421
+ }
1422
+ } catch (error ) {
1423
+ console .error (' Failed to save queued data:' , error );
1424
+ // Consider re-adding failed items back to queue
1425
+ } finally {
1426
+ isSavingData .value = false ;
1427
+ }
1428
+ };
1429
+
1226
1430
// Developer console methods
1227
1431
const enableMockMode = () => {
1228
1432
realtimeStore .enableMockMode ();
@@ -1242,6 +1446,56 @@ if (typeof window !== 'undefined') {
1242
1446
};
1243
1447
}
1244
1448
1449
+ // Watch for new transcript groups and queue them for saving
1450
+ watch (() => realtimeStore .transcriptGroups , (newGroups , oldGroups ) => {
1451
+ // Only process if we have a session and groups were added (not removed)
1452
+ if (! currentSessionId .value || ! oldGroups ) return ;
1453
+
1454
+ // If new groups were added
1455
+ if (newGroups .length > oldGroups .length ) {
1456
+ // Process only the new groups
1457
+ const newGroupsAdded = newGroups .slice (oldGroups .length );
1458
+
1459
+ newGroupsAdded .forEach (group => {
1460
+ // Queue all messages from this group
1461
+ group .messages .forEach (message => {
1462
+ transcriptQueue .push ({
1463
+ speaker: group .role ,
1464
+ text: message .text ,
1465
+ timestamp: message .timestamp ,
1466
+ groupId: group .id ,
1467
+ systemCategory: group .systemCategory ,
1468
+ });
1469
+ });
1470
+ });
1471
+ }
1472
+ }, { deep: true });
1473
+
1474
+ // Watch for changes to existing transcript groups (appended messages)
1475
+ watch (() => realtimeStore .transcriptGroups .map (g => g .messages .length ), (newLengths , oldLengths ) => {
1476
+ if (! currentSessionId .value || ! oldLengths ) return ;
1477
+
1478
+ // Check each group for new messages
1479
+ newLengths .forEach ((newLength , index ) => {
1480
+ const oldLength = oldLengths [index ] || 0 ;
1481
+ if (newLength > oldLength ) {
1482
+ const group = realtimeStore .transcriptGroups [index ];
1483
+ // Queue only the new messages
1484
+ const newMessages = group .messages .slice (oldLength );
1485
+
1486
+ newMessages .forEach (message => {
1487
+ transcriptQueue .push ({
1488
+ speaker: group .role ,
1489
+ text: message .text ,
1490
+ timestamp: message .timestamp ,
1491
+ groupId: group .id ,
1492
+ systemCategory: group .systemCategory ,
1493
+ });
1494
+ });
1495
+ }
1496
+ });
1497
+ });
1498
+
1245
1499
// Lifecycle
1246
1500
onMounted (() => {
1247
1501
initialize ();
0 commit comments