diff --git a/Frontend/src/components/shared/TicketChat.jsx b/Frontend/src/components/shared/TicketChat.jsx
index 836b6a6c5..1e30e080f 100644
--- a/Frontend/src/components/shared/TicketChat.jsx
+++ b/Frontend/src/components/shared/TicketChat.jsx
@@ -1,7 +1,10 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
-import { Send, User, ShieldCheck, Bot, MessageSquare, Circle, Loader2 } from 'lucide-react';
+import { Send, User, ShieldCheck, Bot, MessageSquare, Circle, Loader2, Wifi, WifiOff } from 'lucide-react';
import { supabase } from "../../lib/supabaseClient";
import useAuthStore from "../../store/authStore";
+import { API_CONFIG } from "../../config";
+import useResilientWebSocket from "../../hooks/useResilientWebSocket";
+import { buildWebSocketUrl } from "../../utils/websocket";
const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
const [messages, setMessages] = useState([]);
@@ -21,6 +24,54 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
const inputRef = useRef(null);
const channelRef = useRef(null);
+ // ─── Resilient WebSocket Channel (live agent communication) ───────────
+ const isStaffUser = profile && ['admin', 'super_admin', 'agent', 'master_admin'].includes(profile?.role);
+ const wsUrl = user?.id && isStaffUser
+ ? buildWebSocketUrl(API_CONFIG.BACKEND_URL, `/ws/agents/${user.id}`)
+ : null;
+
+ const handleWsMessage = useCallback((data) => {
+ if (!data || data.type !== 'message') return;
+
+ // Ignore our own broadcasts echoing back through the hub.
+ if (data.from && data.from === user?.id) return;
+
+ // Live frames are merged when they target this agent directly or when
+ // the payload explicitly scopes a ticket id we are currently viewing.
+ if (data.to && data.to !== user?.id) return;
+ if (data.ticket_id && String(data.ticket_id) !== String(ticketId)) return;
+
+ const content = data.content;
+ const liveMessage = {
+ id: `live-${data.timestamp || Date.now()}-${data.from || 'agent'}`,
+ ticket_id: ticketId,
+ sender_id: data.from || 'live-agent',
+ sender_name: data.sender_name || (data.from ? 'Agent' : 'Support'),
+ sender_role: 'admin',
+ message: content,
+ is_internal: false,
+ is_live: true,
+ created_at: data.timestamp || new Date().toISOString()
+ };
+
+ setMessages((prev) => {
+ if (prev.find(m => m.message === content && m.sender_id === liveMessage.sender_id)) return prev;
+ return [...prev, liveMessage];
+ });
+ setTimeout(() => {
+ scrollContainerRef.current?.scrollTo({
+ top: scrollContainerRef.current.scrollHeight,
+ behavior: 'smooth'
+ });
+ }, 50);
+ }, [user?.id, ticketId]);
+
+ const { status: wsStatus, send: wsSend } = useResilientWebSocket({
+ url: wsUrl,
+ enabled: Boolean(wsUrl && ticketId),
+ onMessage: handleWsMessage
+ });
+
// ─── Fetch Messages ──────────────────────────────────────────────────
const fetchMessages = async () => {
if (!ticketId) return;
@@ -208,6 +259,16 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
}]);
if (error) throw error;
}
+
+ // Fan the message out to online agents over the resilient socket.
+ wsSend({
+ type: 'broadcast',
+ ticket_id: ticketId,
+ content,
+ sender_id: user.id,
+ sender_name: profile?.full_name || user.email,
+ timestamp: new Date().toISOString()
+ });
} catch (err) {
console.error("Error sending message:", err);
setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
@@ -255,6 +316,23 @@ const TicketChat = ({ ticketId, currentUserRole = 'user' }) => {
+ {isStaff && (
+
+ {wsStatus === 'connected' ? : wsStatus === 'reconnecting' ? : }
+ {wsStatus === 'connected' ? 'Live' : wsStatus === 'reconnecting' ? 'Reconnecting' : wsStatus === 'connecting' ? 'Connecting' : 'Offline'}
+
+ )}
{isStaff && (