Files
group-2-threaded-project-pe…/web/context/ChatWidgetContext.js
Nikitha 8e1ab89e6c Chat Widget, changes in nav bar
Auto Scroll chat and changes in Nav bar
2026-04-15 08:12:16 -06:00

188 lines
7.0 KiB
JavaScript

"use client";
import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react";
const ChatWidgetContext = createContext(null);
const API_BASE = "";
export function ChatWidgetProvider({ children }) {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("ai"); // "ai" | "history" | "live"
// AI chat
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);
// Persist aiMessages to localStorage
useEffect(() => {
localStorage.setItem("fc_aiMessages", JSON.stringify(aiMessages));
}, [aiMessages]);
// Keep a ref so sendAiMessage stays stable (no stale-closure over messages)
const aiMessagesRef = useRef(aiMessages);
useEffect(() => { aiMessagesRef.current = aiMessages; }, [aiMessages]);
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
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 pollRef = useRef(null);
const activeConvIdRef = useRef(null);
const tokenRef = useRef(null); // FIX: store token so polling can restart
const stopPolling = useCallback(() => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}, []);
const fetchLiveMessages = useCallback(async (convId, token) => {
if (!convId || !token) return;
try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return;
const data = await res.json();
if (Array.isArray(data)) setLiveMessages(data);
} 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;
stopPolling();
tokenRef.current = token; // FIX: save token for polling restart
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 */ }
await fetchLiveMessages(convId, token);
pollRef.current = setInterval(() => fetchLiveMessages(convId, token), 2500);
}, [stopPolling, fetchLiveMessages]);
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) await fetchLiveMessages(convId, token);
} catch { /* silent */ } finally {
setLiveSending(false);
}
}, [liveSending, fetchLiveMessages]);
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]);
// FIX: Single effect that handles both stopping AND restarting polling
useEffect(() => {
if (!isOpen || view !== "live") {
stopPolling();
} else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current) {
stopPolling();
fetchLiveMessages(activeConvIdRef.current, tokenRef.current);
pollRef.current = setInterval(
() => fetchLiveMessages(activeConvIdRef.current, tokenRef.current),
2500
);
}
}, [isOpen, view, stopPolling, fetchLiveMessages]);
const toggleOpen = useCallback(() => setIsOpen((o) => !o), []);
const openView = useCallback((v) => setView(v), []);
return (
<ChatWidgetContext.Provider value={{
isOpen, toggleOpen,
view, openView,
aiMessages, aiSending, aiError, setAiError, sendAiMessage,
conversations, convsLoading, loadConversations,
activeConvId, activeConv, liveMessages, liveSending,
openLiveConversation, sendLiveMessage,
startLiveChat, switchingToHuman,
}}>
{children}
</ChatWidgetContext.Provider>
);
}
export function useChatWidget() {
const ctx = useContext(ChatWidgetContext);
if (!ctx) throw new Error("useChatWidget must be used within ChatWidgetProvider");
return ctx;
}