// Author: Shiv // Date: April 2026 "use client"; import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react"; import { createStompClient } from "@/lib/chatSocket"; import { useAuth } from "@/context/AuthContext"; //Chat widget context //Manages both the AI chat and the live support chat const ChatWidgetContext = createContext(null); const API_BASE = ""; //Provides chat state and actions for the floating chat widget export function ChatWidgetProvider({ children }) { const { user } = useAuth(); const [isOpen, setIsOpen] = useState(false); const [view, setView] = useState("ai"); // "ai" | "history" | "live" //AI chat messages, loaded from localStorage so they survive page refreshes const [aiMessages, setAiMessages] = useState(() => { try { const saved = localStorage.getItem("fc_aiMessages"); return saved ? JSON.parse(saved) : []; } catch { return []; } }); const [aiSending, setAiSending] = useState(false); const [aiError, setAiError] = useState(null); //Save AI messages to localStorage whenever they change useEffect(() => { localStorage.setItem("fc_aiMessages", JSON.stringify(aiMessages)); }, [aiMessages]); //Ref to the latest messages so the send function always sees current state const aiMessagesRef = useRef(aiMessages); useEffect(() => { aiMessagesRef.current = aiMessages; }, [aiMessages]); //Sends a message to the AI and appends the response to the chat const sendAiMessage = useCallback(async (text, token) => { if (!text.trim() || !token) return; const userMsg = { role: "user", content: text, id: Date.now() }; setAiMessages((prev) => [...prev, userMsg]); setAiSending(true); setAiError(null); try { const history = aiMessagesRef.current.map((m) => ({ role: m.role, content: m.content })); const res = await fetch(`${API_BASE}/api/v1/ai-chat/message`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ message: text, history }), }); const data = await res.json(); if (!res.ok || !data.success) { setAiError(data.error || "Failed to get a response."); return; } setAiMessages((prev) => [...prev, { role: "assistant", content: data.message, id: Date.now() + 1 }]); } catch { setAiError("Network error. Please try again."); } finally { setAiSending(false); } }, []); //Live chat state const [conversations, setConversations] = useState([]); const [convsLoading, setConvsLoading] = useState(false); const [activeConvId, setActiveConvId] = useState(null); const [activeConv, setActiveConv] = useState(null); const [liveMessages, setLiveMessages] = useState([]); const [liveSending, setLiveSending] = useState(false); const [switchingToHuman, setSwitchingToHuman] = useState(false); const stompRef = useRef(null); const activeConvIdRef = useRef(null); const tokenRef = useRef(null); //Disconnects the WebSocket if it is active const disconnectStomp = useCallback(() => { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } }, []); //Clears all chat state when the user logs out or switches accounts const prevUserIdRef = useRef(user?.id); useEffect(() => { const currentId = user?.id ?? null; const prevId = prevUserIdRef.current; prevUserIdRef.current = currentId; if (prevId !== null && prevId !== currentId) { setAiMessages([]); localStorage.removeItem("fc_aiMessages"); setConversations([]); setActiveConvId(null); setActiveConv(null); setLiveMessages([]); disconnectStomp(); setView("ai"); setIsOpen(false); } }, [user?.id, disconnectStomp]); //Subscribes to incoming messages and conversation updates for a given chat const subscribeToConversation = useCallback((client, convId) => { client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => { try { const msg = JSON.parse(frame.body); setLiveMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); } catch { /* silent */ } }); client.subscribe(`/user/queue/chat/conversations`, (frame) => { try { const conv = JSON.parse(frame.body); if (conv.id === convId) setActiveConv(conv); } catch { /* silent */ } }); }, []); const loadConversations = useCallback(async (token) => { if (!token) return; setConvsLoading(true); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations?mine=true`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return; const data = await res.json(); setConversations(Array.isArray(data) ? data : (data.content ?? [])); } catch { /* silent */ } finally { setConvsLoading(false); } }, []); const openLiveConversation = useCallback(async (convId, token) => { if (!convId || !token) return; disconnectStomp(); tokenRef.current = token; setActiveConvId(convId); activeConvIdRef.current = convId; setLiveMessages([]); setView("live"); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) setActiveConv(await res.json()); } catch { /* silent */ } try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { const data = await res.json(); if (Array.isArray(data)) setLiveMessages(data); } } catch { /* silent */ } const client = createStompClient(token); client.onConnect = () => subscribeToConversation(client, convId); stompRef.current = client; client.activate(); }, [disconnectStomp, subscribeToConversation]); const sendLiveMessage = useCallback(async (text, token, convId) => { if (!text.trim() || liveSending || !token || !convId) return; setLiveSending(true); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ content: text }), }); if (res.ok) { const msg = await res.json(); setLiveMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); } } catch { /* silent */ } finally { setLiveSending(false); } }, [liveSending]); //Creates a new live chat conversation and requests a human agent const startLiveChat = useCallback(async (token) => { if (!token || switchingToHuman) return; setSwitchingToHuman(true); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ message: "Hi, I'd like to speak with a support agent." }), }); if (!res.ok) return; const conv = await res.json(); await fetch(`${API_BASE}/api/v1/chat/conversations/${conv.id}/request-human`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); setConversations((prev) => [conv, ...prev.filter((c) => c.id !== conv.id)]); await openLiveConversation(conv.id, token); } catch { /* silent */ } finally { setSwitchingToHuman(false); } }, [switchingToHuman, openLiveConversation]); useEffect(() => { if (!isOpen || view !== "live") { disconnectStomp(); } else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current && !stompRef.current) { const convId = activeConvIdRef.current; const token = tokenRef.current; const client = createStompClient(token); client.onConnect = () => subscribeToConversation(client, convId); stompRef.current = client; client.activate(); } }, [isOpen, view, disconnectStomp, subscribeToConversation]); useEffect(() => { return () => disconnectStomp(); }, [disconnectStomp]); const toggleOpen = useCallback(() => setIsOpen((o) => !o), []); const openView = useCallback((v) => setView(v), []); return ( {children} ); } //Hook to access chat widget state - must be used inside a ChatWidgetProvider export function useChatWidget() { const ctx = useContext(ChatWidgetContext); if (!ctx) throw new Error("useChatWidget must be used within ChatWidgetProvider"); return ctx; }