@@ -172,6 +172,16 @@ let micStream: MediaStream | null = null;
172172let systemStream: MediaStream | null = null ;
173173let sessionsReady = false ;
174174
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+
175185// Tool definitions using SDK
176186const coachingTools = [
177187 tool ({
@@ -266,9 +276,23 @@ const coachingTools = [
266276
267277// Function call handler (same as original)
268278const handleFunctionCall = (name : string , args : any ) => {
279+ const timestamp = Date .now ();
280+
269281 switch (name ) {
270282 case ' track_discussion_topic' :
271283 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+ }
272296 break ;
273297
274298 case ' analyze_customer_intent' :
@@ -282,14 +306,53 @@ const handleFunctionCall = (name: string, args: any) => {
282306
283307 case ' highlight_insight' :
284308 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+ }
285321 break ;
286322
287323 case ' detect_commitment' :
288324 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+ }
289338 break ;
290339
291340 case ' create_action_item' :
292341 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+ }
293356 break ;
294357
295358 case ' detect_information_need' :
@@ -299,6 +362,34 @@ const handleFunctionCall = (name: string, args: any) => {
299362 }
300363};
301364
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+
302393// Check onboarding requirements
303394const checkOnboardingRequirements = async () => {
304395 // Check if onboarding was already completed
@@ -428,14 +519,11 @@ const startCall = async () => {
428519 // Auto-enable screen protection during calls
429520 screenProtection .enableForCall ();
430521
522+ // Start conversation session in database
523+ await startConversationSession ();
431524
432525 // 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' );
439527
440528 } catch (error ) {
441529 console .error (' Failed to start call:' , error );
@@ -457,6 +545,28 @@ const startCall = async () => {
457545
458546const endCall = async () => {
459547 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+
460570 // Stop microphone stream
461571 if (micStream ) {
462572 micStream .getTracks ().forEach (track => track .stop ());
@@ -517,12 +627,7 @@ const endCall = async () => {
517627 realtimeStore .setConnectionStatus (' disconnected' );
518628
519629 // 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.' );
526631
527632 } catch (error ) {
528633 console .error (' Failed to end call:' , error );
@@ -694,17 +799,19 @@ const setupSessionHandlers = () => {
694799 if (salespersonSession .transport ) {
695800 salespersonSession .transport .on (' conversation.item.input_audio_transcription.completed' , (event : any ) => {
696801 if (event .transcript ) {
802+ const timestamp = Date .now ();
803+ const groupId = ` salesperson-${timestamp } ` ;
804+
697805 // Try to append to last group if same speaker
698806 const appended = realtimeStore .appendToLastTranscriptGroup (' salesperson' , event .transcript );
699807
700808 if (! appended ) {
701809 // Create new group if not appended
702- const groupId = ` salesperson-${Date .now ()} ` ;
703810 realtimeStore .addTranscriptGroup ({
704811 id: groupId ,
705812 role: ' salesperson' ,
706- messages: [{ text: event .transcript , timestamp: Date . now () }],
707- startTime: Date . now () ,
813+ messages: [{ text: event .transcript , timestamp: timestamp }],
814+ startTime: timestamp ,
708815 });
709816 }
710817
@@ -729,17 +836,19 @@ const setupSessionHandlers = () => {
729836 if (coachSession .transport ) {
730837 coachSession .transport .on (' conversation.item.input_audio_transcription.completed' , (event : any ) => {
731838 if (event .transcript ) {
839+ const timestamp = Date .now ();
840+ const groupId = ` customer-${timestamp } ` ;
841+
732842 // Try to append to last group if same speaker
733843 const appended = realtimeStore .appendToLastTranscriptGroup (' customer' , event .transcript );
734844
735845 if (! appended ) {
736846 // Create new group if not appended
737- const groupId = ` customer-${Date .now ()} ` ;
738847 realtimeStore .addTranscriptGroup ({
739848 id: groupId ,
740849 role: ' customer' ,
741- messages: [{ text: event .transcript , timestamp: Date . now () }],
742- startTime: Date . now () ,
850+ messages: [{ text: event .transcript , timestamp: timestamp }],
851+ startTime: timestamp ,
743852 });
744853 }
745854
@@ -1223,6 +1332,101 @@ watch(selectedTemplate, (newTemplate) => {
12231332 }
12241333});
12251334
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+
12261430// Developer console methods
12271431const enableMockMode = () => {
12281432 realtimeStore .enableMockMode ();
@@ -1242,6 +1446,56 @@ if (typeof window !== 'undefined') {
12421446 };
12431447}
12441448
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+
12451499// Lifecycle
12461500onMounted (() => {
12471501 initialize ();
0 commit comments