Forráskód Böngészése

feat(call-log-detail): add realtime updates for analytics status

- Subscribe to call_logs table changes for the specific call log ID
- Auto-update UI when analytics status changes (pending → running → completed)
- Remove polling logic in favor of realtime updates
- Add live/offline indicator in detail page header
- Reuse existing realtime translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 hónapja
szülő
commit
17be27ef35

+ 73 - 26
shopcall.ai-main/src/components/CallLogDetailContent.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef, useCallback } from "react";
 import { useParams, useNavigate } from "react-router-dom";
 import { Button } from "@/components/ui/button";
 import { Card } from "@/components/ui/card";
@@ -8,6 +8,7 @@ import { supabase } from "@/lib/supabase";
 import { API_URL, ENABLE_MANUAL_ANALYTICS } from "@/lib/config";
 import ReactMarkdown from 'react-markdown';
 import { AnalyticsStatus, ANALYTICS_STATUS, RelatedCustomer, RelatedOrder, RelatedProduct } from "@/types/database.types";
+import { RealtimeChannel } from "@supabase/supabase-js";
 
 interface CallLog {
   id: string;
@@ -134,7 +135,9 @@ export function CallLogDetailContent() {
   const [isPlaying, setIsPlaying] = useState(false);
   const [currentTime, setCurrentTime] = useState(0);
   const [isAnalyticsTriggering, setIsAnalyticsTriggering] = useState(false);
+  const [isRealtimeConnected, setIsRealtimeConnected] = useState(false);
   const audioRef = useRef<HTMLAudioElement>(null);
+  const channelRef = useRef<RealtimeChannel | null>(null);
 
   const handleTriggerAnalytics = async () => {
     if (!id || !callLog) return;
@@ -160,35 +163,13 @@ export function CallLogDetailContent() {
       }
 
       // Update local state to show running status
+      // Realtime subscription will handle further updates automatically
       setCallLog({
         ...callLog,
         analytics_status: ANALYTICS_STATUS.RUNNING,
         analytics_error: null,
       });
 
-      // Start polling for updates
-      const pollInterval = setInterval(async () => {
-        const pollResponse = await fetch(`${API_URL}/api/call-logs/${id}`, {
-          headers: {
-            'Authorization': `Bearer ${session.access_token}`,
-            'Content-Type': 'application/json',
-          },
-        });
-
-        if (pollResponse.ok) {
-          const data = await pollResponse.json();
-          setCallLog(data.call_log);
-
-          // Stop polling if completed or failed
-          if (data.call_log.analytics_status !== ANALYTICS_STATUS.RUNNING) {
-            clearInterval(pollInterval);
-          }
-        }
-      }, 3000); // Poll every 3 seconds
-
-      // Stop polling after 2 minutes max
-      setTimeout(() => clearInterval(pollInterval), 120000);
-
     } catch (err) {
       console.error('Failed to trigger analytics:', err);
       setCallLog({
@@ -201,6 +182,46 @@ export function CallLogDetailContent() {
     }
   };
 
+  // Handle realtime updates for this call log
+  const handleRealtimeUpdate = useCallback((payload: { eventType: string; new: CallLog }) => {
+    console.log('[Realtime] Call log updated:', payload.eventType);
+    if (payload.eventType === 'UPDATE' && payload.new) {
+      setCallLog(payload.new);
+    }
+  }, []);
+
+  // Setup realtime subscription for this specific call log
+  const setupRealtimeSubscription = useCallback((callLogId: string) => {
+    // Clean up existing subscription
+    if (channelRef.current) {
+      supabase.removeChannel(channelRef.current);
+      channelRef.current = null;
+    }
+
+    console.log('[Realtime] Setting up subscription for call log:', callLogId);
+
+    const channel = supabase
+      .channel(`call_log_detail_${callLogId}`)
+      .on(
+        'postgres_changes',
+        {
+          event: 'UPDATE',
+          schema: 'public',
+          table: 'call_logs',
+          filter: `id=eq.${callLogId}`
+        },
+        (payload) => {
+          handleRealtimeUpdate(payload as { eventType: string; new: CallLog });
+        }
+      )
+      .subscribe((status) => {
+        console.log('[Realtime] Detail subscription status:', status);
+        setIsRealtimeConnected(status === 'SUBSCRIBED');
+      });
+
+    channelRef.current = channel;
+  }, [handleRealtimeUpdate]);
+
   useEffect(() => {
     const fetchCallLog = async () => {
       if (!id) return;
@@ -228,6 +249,9 @@ export function CallLogDetailContent() {
 
         const data = await response.json();
         setCallLog(data.call_log);
+
+        // Setup realtime subscription after initial fetch
+        setupRealtimeSubscription(id);
       } catch (err) {
         setError(err instanceof Error ? err.message : 'An error occurred');
       } finally {
@@ -236,7 +260,16 @@ export function CallLogDetailContent() {
     };
 
     fetchCallLog();
-  }, [id]);
+
+    // Cleanup on unmount
+    return () => {
+      if (channelRef.current) {
+        console.log('[Realtime] Cleaning up detail subscription');
+        supabase.removeChannel(channelRef.current);
+        channelRef.current = null;
+      }
+    };
+  }, [id, setupRealtimeSubscription]);
 
   const handleBack = () => {
     navigate('/call-logs');
@@ -307,7 +340,21 @@ export function CallLogDetailContent() {
       {/* Modal Header */}
       <div className="border-b border-slate-800 bg-slate-900 p-6">
         <div className="flex items-center justify-between">
-          <h1 className="text-xl font-semibold text-white">{t('callLogDetail.title')}</h1>
+          <div className="flex items-center gap-3">
+            <h1 className="text-xl font-semibold text-white">{t('callLogDetail.title')}</h1>
+            {/* Realtime indicator */}
+            <div className="flex items-center gap-1.5 px-2 py-1 rounded-full bg-slate-700/50" title={isRealtimeConnected ? t('callLogs.realtime.liveDescription') : t('callLogs.realtime.disconnectedDescription')}>
+              <span className="relative flex h-2 w-2">
+                {isRealtimeConnected && (
+                  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
+                )}
+                <span className={`relative inline-flex rounded-full h-2 w-2 ${isRealtimeConnected ? 'bg-green-500' : 'bg-slate-500'}`}></span>
+              </span>
+              <span className="text-xs text-slate-400">
+                {isRealtimeConnected ? t('callLogs.realtime.live') : t('callLogs.realtime.disconnected')}
+              </span>
+            </div>
+          </div>
           <Button
             variant="ghost"
             size="sm"

+ 3 - 2
supabase/functions/_shared/vapi-client.ts

@@ -208,9 +208,10 @@ function buildAssistantConfig(config: VapiAssistantConfig): Record<string, unkno
     },
     serverMessages: [
       'end-of-call-report',
-      'transcript[transcriptType="final"]'
+      'transcript[transcriptType="final"]',
+      'session.created'
     ],
-    backgroundSound: 'office',
+    backgroundSound: 'off',
     firstMessageMode: 'assistant-speaks-first',
     analysisPlan: {
       minMessagesThreshold: 2