Merge pull request #309 from RecentRunner/websitefinal
merge websitefinal
This commit is contained in:
@@ -30,7 +30,7 @@ body {
|
|||||||
padding: 0.5rem 2rem;
|
padding: 0.5rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
justify-content: space-between;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
/* border-radius: 0px 0px 10px 10px; */
|
/* border-radius: 0px 0px 10px 10px; */
|
||||||
}
|
}
|
||||||
@@ -65,17 +65,18 @@ body {
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
color: #2f2f2f;
|
color: #2f2f2f;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.25s ease, color 0.25s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alternative Hover Effect - Background */
|
/* Alternative Hover Effect - Background */
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.171);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,21 @@ export default function FloatingChat() {
|
|||||||
|
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
const prevAiLengthRef = useRef(0);
|
||||||
|
const prevLiveLengthRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
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]);
|
}, [aiMessages, liveMessages, isOpen]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view === "history" && token && isOpen) loadConversations(token);
|
if (view === "history" && token && isOpen) loadConversations(token);
|
||||||
}, [view, token, isOpen, loadConversations]);
|
}, [view, token, isOpen, loadConversations]);
|
||||||
@@ -130,23 +140,67 @@ export default function FloatingChat() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{conversations.map((conv) => (
|
|
||||||
|
{/*open & active conversations*/}
|
||||||
|
{conversations.filter(c => c.status === "OPEN" && c.staffId).length > 0 && (
|
||||||
|
<div style={s.sectionLabel}> Active
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{conversations.filter(c => c.status === "OPEN" && c.staffId).map((conv) => (
|
||||||
<button key={conv.id} style={s.convItem} onClick={() => openLiveConversation(conv.id, token)}>
|
<button key={conv.id} style={s.convItem} onClick={() => openLiveConversation(conv.id, token)}>
|
||||||
<div style={s.convTop}>
|
<div style={s.convTop}>
|
||||||
<span style={s.convSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
<span style={s.convSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
<span style={{ ...s.statusBadge, ...(conv.status === "OPEN" ? s.statusOpen : s.statusClosed) }}>
|
<span style={{ ...s.statusBadge, ...s.statusOpen }}>Active</span>
|
||||||
{conv.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={s.convBottom}>
|
<div style={s.convBottom}>
|
||||||
<span style={s.convMode}>{conv.mode === "HUMAN" ? "👤 Live Support" : "🤖 AI Support"}</span>
|
<span style={s.convMode}>Live Support</span>
|
||||||
<span style={s.convDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
|
<span style={s.convDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Unclaimed - waiting for staff */}
|
||||||
|
{conversations.filter(c => c.status === "OPEN" && !c.staffId).length > 0 && (
|
||||||
|
<div style={s.sectionLabel}> Waiting </div>
|
||||||
|
)}
|
||||||
|
{conversations.filter(c => c.status === "OPEN" && !c.staffId).map((conv) => (
|
||||||
|
<button key={conv.id} style={s.convItem} onClick={() => openLiveConversation(conv.id, token)}>
|
||||||
|
<div style={s.convTop}>
|
||||||
|
<span style={s.convSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
|
<span style={{ ...s.statusBadge, ...s.statusUnclaimed }}>Waiting</span>
|
||||||
|
</div>
|
||||||
|
<div style={s.convBottom}>
|
||||||
|
<span style={s.convMode}>⏳ Waiting for agent</span>
|
||||||
|
<span style={s.convDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Closed convesations*/}
|
||||||
|
{conversations.filter(c => c.status === "CLOSED").length > 0 && (
|
||||||
|
<div style={s.sectionLabel}> Closed</div>
|
||||||
|
)}
|
||||||
|
{conversations.filter(c => c.status === "CLOSED").map((conv) => (
|
||||||
|
<button key={conv.id} style={{ ...s.convItem, ...s.convItemClosed }} onClick={() => openLiveConversation(conv.id, token)}>
|
||||||
|
<div style={s.convTop}>
|
||||||
|
<span style={{ ...s.convSubject, color: "#aaa" }}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
|
<span style={{ ...s.statusBadge, ...s.statusClosed }}>Closed</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Live chat view */}
|
{/* Live chat view */}
|
||||||
{user && view === "live" && (
|
{user && view === "live" && (
|
||||||
<>
|
<>
|
||||||
@@ -457,6 +511,14 @@ const s = {
|
|||||||
padding: "0.45rem 0.85rem", borderBottom: "1px solid #f0f0f0",
|
padding: "0.45rem 0.85rem", borderBottom: "1px solid #f0f0f0",
|
||||||
background: "#fafafa", flexShrink: 0,
|
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: {
|
closedBanner: {
|
||||||
background: "#f5f5f5", borderTop: "1px solid #e0e0e0", color: "#888",
|
background: "#f5f5f5", borderTop: "1px solid #e0e0e0", color: "#888",
|
||||||
padding: "0.65rem 0.85rem", fontSize: "0.84rem", textAlign: "center", flexShrink: 0,
|
padding: "0.65rem 0.85rem", fontSize: "0.84rem", textAlign: "center", flexShrink: 0,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function DisplayNav() {
|
|||||||
<Link href="/appointments" className="nav-link">Appointments</Link>
|
<Link href="/appointments" className="nav-link">Appointments</Link>
|
||||||
<Link href="/ai-chat" className="nav-link">Help</Link>
|
<Link href="/ai-chat" className="nav-link">Help</Link>
|
||||||
<Link href="/contact" className="nav-link">Contact</Link>
|
<Link href="/contact" className="nav-link">Contact</Link>
|
||||||
<Link href="/about" className="nav-link">About</Link>
|
{/*} <Link href="/about" className="nav-link">About</Link> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nav-auth">
|
<div className="nav-auth">
|
||||||
@@ -128,7 +128,7 @@ export default function DisplayNav() {
|
|||||||
<Link href="/appointments" className="nav-drawer-link" onClick={closeMenu}>Appointments</Link>
|
<Link href="/appointments" className="nav-drawer-link" onClick={closeMenu}>Appointments</Link>
|
||||||
<Link href="/ai-chat" className="nav-drawer-link" onClick={closeMenu}>Help</Link>
|
<Link href="/ai-chat" className="nav-drawer-link" onClick={closeMenu}>Help</Link>
|
||||||
<Link href="/contact" className="nav-drawer-link" onClick={closeMenu}>Contact</Link>
|
<Link href="/contact" className="nav-drawer-link" onClick={closeMenu}>Contact</Link>
|
||||||
<Link href="/about" className="nav-drawer-link" onClick={closeMenu}>About</Link>
|
{/* <Link href="/about" className="nav-drawer-link" onClick={closeMenu}>About</Link> */}
|
||||||
|
|
||||||
<div className="nav-drawer-divider" />
|
<div className="nav-drawer-divider" />
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,20 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
const [view, setView] = useState("ai"); // "ai" | "history" | "live"
|
const [view, setView] = useState("ai"); // "ai" | "history" | "live"
|
||||||
|
|
||||||
// AI chat
|
// AI chat
|
||||||
const [aiMessages, setAiMessages] = useState([]);
|
const [aiMessages, setAiMessages] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem("fc_aiMessages");
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch { return []; }
|
||||||
|
});
|
||||||
const [aiSending, setAiSending] = useState(false);
|
const [aiSending, setAiSending] = useState(false);
|
||||||
const [aiError, setAiError] = useState(null);
|
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)
|
// Keep a ref so sendAiMessage stays stable (no stale-closure over messages)
|
||||||
const aiMessagesRef = useRef(aiMessages);
|
const aiMessagesRef = useRef(aiMessages);
|
||||||
useEffect(() => { aiMessagesRef.current = aiMessages; }, [aiMessages]);
|
useEffect(() => { aiMessagesRef.current = aiMessages; }, [aiMessages]);
|
||||||
@@ -52,6 +62,7 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
|
|
||||||
const pollRef = useRef(null);
|
const pollRef = useRef(null);
|
||||||
const activeConvIdRef = useRef(null);
|
const activeConvIdRef = useRef(null);
|
||||||
|
const tokenRef = useRef(null); // FIX: store token so polling can restart
|
||||||
|
|
||||||
const stopPolling = useCallback(() => {
|
const stopPolling = useCallback(() => {
|
||||||
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
||||||
@@ -87,6 +98,7 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
const openLiveConversation = useCallback(async (convId, token) => {
|
const openLiveConversation = useCallback(async (convId, token) => {
|
||||||
if (!convId || !token) return;
|
if (!convId || !token) return;
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
tokenRef.current = token; // FIX: save token for polling restart
|
||||||
setActiveConvId(convId);
|
setActiveConvId(convId);
|
||||||
activeConvIdRef.current = convId;
|
activeConvIdRef.current = convId;
|
||||||
setLiveMessages([]);
|
setLiveMessages([]);
|
||||||
@@ -137,9 +149,19 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
}
|
}
|
||||||
}, [switchingToHuman, openLiveConversation]);
|
}, [switchingToHuman, openLiveConversation]);
|
||||||
|
|
||||||
// Stop polling when navigating away from live view or closing widget
|
// FIX: Single effect that handles both stopping AND restarting polling
|
||||||
useEffect(() => { if (view !== "live") stopPolling(); }, [view, stopPolling]);
|
useEffect(() => {
|
||||||
useEffect(() => { if (!isOpen) stopPolling(); }, [isOpen, stopPolling]);
|
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 toggleOpen = useCallback(() => setIsOpen((o) => !o), []);
|
||||||
const openView = useCallback((v) => setView(v), []);
|
const openView = useCallback((v) => setView(v), []);
|
||||||
|
|||||||
Reference in New Issue
Block a user