diff --git a/web/app/globals.css b/web/app/globals.css index bd339347..ec01a40b 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -30,7 +30,7 @@ body { padding: 0.5rem 2rem; display: flex; align-items: center; - flex-wrap: wrap; + justify-content: space-between; min-height: 70px; /* border-radius: 0px 0px 10px 10px; */ } @@ -65,17 +65,18 @@ body { .nav-link { color: #2f2f2f; text-decoration: none; - font-size: 1.1rem; - font-weight: 700; + font-size: 1.05rem; + font-weight: 600; padding: 0.5rem 1rem; - border-radius: 4px; - transition: all 0.3s ease; + border-radius: 6px; + transition: background-color 0.25s ease, color 0.25s ease; position: relative; } /* Alternative Hover Effect - Background */ .nav-link:hover { - background-color: rgba(255, 255, 255, 0.171); + background-color: rgba(255, 255, 255, 0.25); + } diff --git a/web/components/FloatingChat.js b/web/components/FloatingChat.js index 4b17232d..4c6b45ab 100644 --- a/web/components/FloatingChat.js +++ b/web/components/FloatingChat.js @@ -21,10 +21,20 @@ export default function FloatingChat() { const [input, setInput] = useState(""); const messagesEndRef = useRef(null); + const prevAiLengthRef = useRef(0); +const prevLiveLengthRef = useRef(0); useEffect(() => { - if (isOpen) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [aiMessages, liveMessages, isOpen]); + if (!isOpen) return; + const aiGrew = aiMessages.length > prevAiLengthRef.current; + const liveGrew = liveMessages.length > prevLiveLengthRef.current; + prevAiLengthRef.current = aiMessages.length; + prevLiveLengthRef.current = liveMessages.length; + if (aiGrew || liveGrew) { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } +}, [aiMessages, liveMessages, isOpen]); + useEffect(() => { if (view === "history" && token && isOpen) loadConversations(token); @@ -130,22 +140,66 @@ export default function FloatingChat() { )} - {conversations.map((conv) => ( + + {/*open & active conversations*/} + {conversations.filter(c => c.status === "OPEN" && c.staffId).length > 0 && ( +
Active +
+ )} + {conversations.filter(c => c.status === "OPEN" && c.staffId).map((conv) => ( - ))} - + + + + ))} + + {/* Unclaimed - waiting for staff */} + {conversations.filter(c => c.status === "OPEN" && !c.staffId).length > 0 && ( +
Waiting
)} + {conversations.filter(c => c.status === "OPEN" && !c.staffId).map((conv) => ( + + + ))} + + {/* Closed convesations*/} + {conversations.filter(c => c.status === "CLOSED").length > 0 && ( +
Closed
+ )} + {conversations.filter(c => c.status === "CLOSED").map((conv) => ( + + ))} + + + )} + + {/* Live chat view */} {user && view === "live" && ( @@ -457,6 +511,14 @@ const s = { padding: "0.45rem 0.85rem", borderBottom: "1px solid #f0f0f0", background: "#fafafa", flexShrink: 0, }, + statusUnclaimed: { background: "#fff8e1", color: "#f57f17" }, + sectionLabel: { + fontSize: "0.7rem", fontWeight: 700, color: "#aaa", + padding: "0.4rem 0.85rem 0.2rem", textTransform: "uppercase", + letterSpacing: "0.05em", background: "#fafafa", + }, + +convItemClosed: { background: "#fafafa", opacity: 0.75 }, closedBanner: { background: "#f5f5f5", borderTop: "1px solid #e0e0e0", color: "#888", padding: "0.65rem 0.85rem", fontSize: "0.84rem", textAlign: "center", flexShrink: 0, diff --git a/web/components/Navigation.js b/web/components/Navigation.js index 39baa63a..d84f865b 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -52,7 +52,7 @@ export default function DisplayNav() { Appointments Help Contact - About + {/*} About */}
@@ -128,7 +128,7 @@ export default function DisplayNav() { Appointments Help Contact - About + {/* About */}
diff --git a/web/context/ChatWidgetContext.js b/web/context/ChatWidgetContext.js index 0b506dbf..82943654 100644 --- a/web/context/ChatWidgetContext.js +++ b/web/context/ChatWidgetContext.js @@ -6,13 +6,23 @@ const ChatWidgetContext = createContext(null); const API_BASE = ""; export function ChatWidgetProvider({ children }) { - const [isOpen, setIsOpen] = useState(false); - const [view, setView] = useState("ai"); // "ai" | "history" | "live" + const [isOpen, setIsOpen] = useState(false); + const [view, setView] = useState("ai"); // "ai" | "history" | "live" // AI chat - const [aiMessages, setAiMessages] = useState([]); - const [aiSending, setAiSending] = useState(false); - const [aiError, setAiError] = useState(null); + 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); @@ -41,17 +51,18 @@ export function ChatWidgetProvider({ children }) { } }, []); - //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); + // 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 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; } @@ -87,6 +98,7 @@ export function ChatWidgetProvider({ children }) { 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([]); @@ -137,12 +149,22 @@ export function ChatWidgetProvider({ children }) { } }, [switchingToHuman, openLiveConversation]); - // Stop polling when navigating away from live view or closing widget - useEffect(() => { if (view !== "live") stopPolling(); }, [view, stopPolling]); - useEffect(() => { if (!isOpen) stopPolling(); }, [isOpen, stopPolling]); + // 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), []); + const openView = useCallback((v) => setView(v), []); return (