|
@@ -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 { useParams, useNavigate } from "react-router-dom";
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Card } from "@/components/ui/card";
|
|
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 { API_URL, ENABLE_MANUAL_ANALYTICS } from "@/lib/config";
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
import { AnalyticsStatus, ANALYTICS_STATUS, RelatedCustomer, RelatedOrder, RelatedProduct } from "@/types/database.types";
|
|
import { AnalyticsStatus, ANALYTICS_STATUS, RelatedCustomer, RelatedOrder, RelatedProduct } from "@/types/database.types";
|
|
|
|
|
+import { RealtimeChannel } from "@supabase/supabase-js";
|
|
|
|
|
|
|
|
interface CallLog {
|
|
interface CallLog {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -134,7 +135,9 @@ export function CallLogDetailContent() {
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
|
const [isAnalyticsTriggering, setIsAnalyticsTriggering] = useState(false);
|
|
const [isAnalyticsTriggering, setIsAnalyticsTriggering] = useState(false);
|
|
|
|
|
+ const [isRealtimeConnected, setIsRealtimeConnected] = useState(false);
|
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
|
+ const channelRef = useRef<RealtimeChannel | null>(null);
|
|
|
|
|
|
|
|
const handleTriggerAnalytics = async () => {
|
|
const handleTriggerAnalytics = async () => {
|
|
|
if (!id || !callLog) return;
|
|
if (!id || !callLog) return;
|
|
@@ -160,35 +163,13 @@ export function CallLogDetailContent() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Update local state to show running status
|
|
// Update local state to show running status
|
|
|
|
|
+ // Realtime subscription will handle further updates automatically
|
|
|
setCallLog({
|
|
setCallLog({
|
|
|
...callLog,
|
|
...callLog,
|
|
|
analytics_status: ANALYTICS_STATUS.RUNNING,
|
|
analytics_status: ANALYTICS_STATUS.RUNNING,
|
|
|
analytics_error: null,
|
|
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) {
|
|
} catch (err) {
|
|
|
console.error('Failed to trigger analytics:', err);
|
|
console.error('Failed to trigger analytics:', err);
|
|
|
setCallLog({
|
|
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(() => {
|
|
useEffect(() => {
|
|
|
const fetchCallLog = async () => {
|
|
const fetchCallLog = async () => {
|
|
|
if (!id) return;
|
|
if (!id) return;
|
|
@@ -228,6 +249,9 @@ export function CallLogDetailContent() {
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
const data = await response.json();
|
|
|
setCallLog(data.call_log);
|
|
setCallLog(data.call_log);
|
|
|
|
|
+
|
|
|
|
|
+ // Setup realtime subscription after initial fetch
|
|
|
|
|
+ setupRealtimeSubscription(id);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
|
} finally {
|
|
} finally {
|
|
@@ -236,7 +260,16 @@ export function CallLogDetailContent() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
fetchCallLog();
|
|
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 = () => {
|
|
const handleBack = () => {
|
|
|
navigate('/call-logs');
|
|
navigate('/call-logs');
|
|
@@ -307,7 +340,21 @@ export function CallLogDetailContent() {
|
|
|
{/* Modal Header */}
|
|
{/* Modal Header */}
|
|
|
<div className="border-b border-slate-800 bg-slate-900 p-6">
|
|
<div className="border-b border-slate-800 bg-slate-900 p-6">
|
|
|
<div className="flex items-center justify-between">
|
|
<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
|
|
<Button
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
size="sm"
|
|
size="sm"
|