fix chat escalation and sidebar
This commit is contained in:
@@ -82,6 +82,7 @@ function AiChatPage() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [conversations, setConversations] = useState([]);
|
const [conversations, setConversations] = useState([]);
|
||||||
const [convsLoading, setConvsLoading] = useState(false);
|
const [convsLoading, setConvsLoading] = useState(false);
|
||||||
|
const [closedExpanded, setClosedExpanded] = useState(false);
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
@@ -418,12 +419,28 @@ function AiChatPage() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
router.push(`/chat?id=${conversation.id}`);
|
setConversation((prev) => prev ? { ...prev, mode: "HUMAN" } : prev);
|
||||||
} catch {
|
} catch {
|
||||||
setError("Could not connect to live support. Please try again.");
|
setError("Could not connect to live support. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCloseConversation() {
|
||||||
|
if (!conversation || conversation.status === "CLOSED") return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${conversation.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({ status: "CLOSED" }),
|
||||||
|
});
|
||||||
|
if (!res.ok) return;
|
||||||
|
const updated = await res.json();
|
||||||
|
setConversation(updated);
|
||||||
|
await fetchConversations();
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (authLoading || loadingConv) {
|
if (authLoading || loadingConv) {
|
||||||
return (
|
return (
|
||||||
<main style={s.page}>
|
<main style={s.page}>
|
||||||
@@ -436,6 +453,8 @@ function AiChatPage() {
|
|||||||
|
|
||||||
const isEscalated = conversation?.mode === "HUMAN";
|
const isEscalated = conversation?.mode === "HUMAN";
|
||||||
const isClosed = conversation?.status === "CLOSED";
|
const isClosed = conversation?.status === "CLOSED";
|
||||||
|
const hasStaff = !!conversation?.staffId;
|
||||||
|
const hasStaffMessage = messages.some((m) => m.senderId !== user?.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={s.page}>
|
<main style={s.page}>
|
||||||
@@ -456,7 +475,7 @@ function AiChatPage() {
|
|||||||
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
||||||
)}
|
)}
|
||||||
<div style={{ overflowY: "auto", flex: 1 }}>
|
<div style={{ overflowY: "auto", flex: 1 }}>
|
||||||
{conversations.map((conv) => (
|
{conversations.filter(c => c.status !== "CLOSED").map((conv) => (
|
||||||
<button
|
<button
|
||||||
key={conv.id}
|
key={conv.id}
|
||||||
style={{ ...s.convItem, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
style={{ ...s.convItem, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
||||||
@@ -464,9 +483,7 @@ function AiChatPage() {
|
|||||||
>
|
>
|
||||||
<div style={s.convItemTop}>
|
<div style={s.convItemTop}>
|
||||||
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
<span style={{ ...s.convStatusBadge, ...(conv.status === "OPEN" ? s.convStatusOpen : s.convStatusClosed) }}>
|
<span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span>
|
||||||
{conv.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={s.convItemBottom}>
|
<div style={s.convItemBottom}>
|
||||||
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
||||||
@@ -474,6 +491,33 @@ function AiChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{conversations.some(c => c.status === "CLOSED") && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
style={s.closedSectionToggle}
|
||||||
|
onClick={() => setClosedExpanded(p => !p)}
|
||||||
|
>
|
||||||
|
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
|
||||||
|
<span>{closedExpanded ? "▲" : "▼"}</span>
|
||||||
|
</button>
|
||||||
|
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
|
||||||
|
<button
|
||||||
|
key={conv.id}
|
||||||
|
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
||||||
|
onClick={() => switchConversation(conv.id)}
|
||||||
|
>
|
||||||
|
<div style={s.convItemTop}>
|
||||||
|
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
|
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
|
||||||
|
</div>
|
||||||
|
<div style={s.convItemBottom}>
|
||||||
|
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
||||||
|
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button style={s.newConvSidebarBtn} onClick={handleNewConversation}>
|
<button style={s.newConvSidebarBtn} onClick={handleNewConversation}>
|
||||||
+ New Conversation
|
+ New Conversation
|
||||||
@@ -498,41 +542,44 @@ function AiChatPage() {
|
|||||||
<div style={s.chatCard}>
|
<div style={s.chatCard}>
|
||||||
<div style={s.chatHeader}>
|
<div style={s.chatHeader}>
|
||||||
<div style={s.chatHeaderLeft}>
|
<div style={s.chatHeaderLeft}>
|
||||||
<div style={s.aiAvatar}>🐾</div>
|
<div style={isEscalated ? s.agentAvatar : s.aiAvatar}>{isEscalated ? "👤" : "🐾"}</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={s.chatHeaderTitle}>Leon's Pet Assistant</div>
|
<div style={s.chatHeaderTitle}>
|
||||||
<div style={s.chatHeaderStatus}>
|
{isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"}
|
||||||
<span style={s.statusDot} /> Online
|
</div>
|
||||||
|
<div style={{ ...s.chatHeaderStatus, color: isClosed ? "#999" : isEscalated && hasStaff ? "#4CAF50" : isEscalated ? "#ff8c00" : undefined }}>
|
||||||
|
<span style={{ ...s.statusDot, background: isClosed ? "#999" : isEscalated && hasStaff ? "#4CAF50" : isEscalated ? "#ff8c00" : undefined }} />
|
||||||
|
{isClosed ? "Conversation closed" : isEscalated && hasStaff ? "Support agent connected" : isEscalated ? "Waiting for a support agent..." : "Online"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
{!isEscalated && !isClosed && (
|
{!isEscalated && !isClosed && (
|
||||||
<button
|
<button style={s.humanBtn} onClick={handleSwitchToHuman} title="Connect with a human support agent">
|
||||||
style={s.humanBtn}
|
|
||||||
onClick={handleSwitchToHuman}
|
|
||||||
title="Connect with a human support agent"
|
|
||||||
>
|
|
||||||
Chat with a Real Person
|
Chat with a Real Person
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
{isEscalated && !isClosed && (
|
||||||
style={s.liveBtn}
|
<button style={s.closeConvBtn} onClick={handleCloseConversation} title="Close this conversation">
|
||||||
onClick={() => router.push("/chat")}
|
Close Chat
|
||||||
title="Go to Live Support"
|
|
||||||
>
|
|
||||||
Live Support
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isEscalated && !hasStaff && !hasStaffMessage && !isClosed && (
|
||||||
|
<div style={s.waitingBanner}>
|
||||||
|
<span style={s.waitingSpinner} />
|
||||||
|
A support agent will be with you shortly. You can send messages while you wait.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={s.messagesArea} ref={messagesAreaRef}>
|
<div style={s.messagesArea} ref={messagesAreaRef}>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div style={s.emptyState}>
|
<div style={s.emptyState}>
|
||||||
<div style={s.emptyIcon}>🐾</div>
|
<div style={s.emptyIcon}>{isEscalated ? "💬" : "🐾"}</div>
|
||||||
<p style={s.emptyText}>
|
<p style={s.emptyText}>
|
||||||
Hello{user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant.
|
{isEscalated ? "Your conversation has started. A support agent will join soon." : `Hello${user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant. Ask me about pet recommendations, care tips, supplies, or anything pet-related!`}
|
||||||
Ask me about pet recommendations, care tips, supplies, or anything pet-related!
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -547,7 +594,7 @@ function AiChatPage() {
|
|||||||
...(isOwn ? s.messageRowUser : s.messageRowAgent),
|
...(isOwn ? s.messageRowUser : s.messageRowAgent),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isOwn && <div style={s.aiAvatarSmall}>🐾</div>}
|
{!isOwn && <div style={isEscalated ? s.agentAvatarSmall : s.aiAvatarSmall}>{isEscalated ? "👤" : "🐾"}</div>}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...s.messageBubble,
|
...s.messageBubble,
|
||||||
@@ -771,6 +818,21 @@ const s = {
|
|||||||
},
|
},
|
||||||
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
||||||
convStatusClosed: { background: "#f0f0f0", color: "#888" },
|
convStatusClosed: { background: "#f0f0f0", color: "#888" },
|
||||||
|
convItemClosed: { opacity: 0.7 },
|
||||||
|
closedSectionToggle: {
|
||||||
|
width: "100%",
|
||||||
|
background: "#f5f5f5",
|
||||||
|
border: "none",
|
||||||
|
borderTop: "1px solid #e8e8e8",
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#666",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
newConvSidebarBtn: {
|
newConvSidebarBtn: {
|
||||||
margin: "0.65rem 1rem",
|
margin: "0.65rem 1rem",
|
||||||
background: "#333",
|
background: "#333",
|
||||||
@@ -860,6 +922,60 @@ const s = {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
},
|
},
|
||||||
|
agentAvatar: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "linear-gradient(135deg, #444, #666)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "1.3rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
agentAvatarSmall: {
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#e0e0e0",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
closeConvBtn: {
|
||||||
|
background: "white",
|
||||||
|
border: "2px solid #c0392b",
|
||||||
|
color: "#c0392b",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.45rem 0.9rem",
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: "pointer",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
},
|
||||||
|
waitingBanner: {
|
||||||
|
background: "#fff8f0",
|
||||||
|
borderBottom: "1px solid #ffe0b2",
|
||||||
|
padding: "0.6rem 1.25rem",
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
color: "#7c4a00",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
waitingSpinner: {
|
||||||
|
display: "inline-block",
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "2px solid #ff8c00",
|
||||||
|
borderTopColor: "transparent",
|
||||||
|
animation: "spin 0.8s linear infinite",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
noConvCard: {
|
noConvCard: {
|
||||||
background: "white",
|
background: "white",
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ function ChatPage() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [conversations, setConversations] = useState([]);
|
const [conversations, setConversations] = useState([]);
|
||||||
const [convsLoading, setConvsLoading] = useState(false);
|
const [convsLoading, setConvsLoading] = useState(false);
|
||||||
|
const [closedExpanded, setClosedExpanded] = useState(false);
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
@@ -478,7 +479,7 @@ function ChatPage() {
|
|||||||
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
||||||
)}
|
)}
|
||||||
<div style={{ overflowY: "auto", flex: 1 }}>
|
<div style={{ overflowY: "auto", flex: 1 }}>
|
||||||
{conversations.map((conv) => (
|
{conversations.filter(c => c.status !== "CLOSED").map((conv) => (
|
||||||
<button
|
<button
|
||||||
key={conv.id}
|
key={conv.id}
|
||||||
style={{ ...s.convItem, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
style={{ ...s.convItem, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
||||||
@@ -486,9 +487,7 @@ function ChatPage() {
|
|||||||
>
|
>
|
||||||
<div style={s.convItemTop}>
|
<div style={s.convItemTop}>
|
||||||
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
<span style={{ ...s.convStatusBadge, ...(conv.status === "OPEN" ? s.convStatusOpen : s.convStatusClosed) }}>
|
<span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span>
|
||||||
{conv.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={s.convItemBottom}>
|
<div style={s.convItemBottom}>
|
||||||
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
||||||
@@ -496,6 +495,33 @@ function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{conversations.some(c => c.status === "CLOSED") && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
style={s.closedSectionToggle}
|
||||||
|
onClick={() => setClosedExpanded(p => !p)}
|
||||||
|
>
|
||||||
|
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
|
||||||
|
<span>{closedExpanded ? "▲" : "▼"}</span>
|
||||||
|
</button>
|
||||||
|
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
|
||||||
|
<button
|
||||||
|
key={conv.id}
|
||||||
|
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
||||||
|
onClick={() => switchConversation(conv.id)}
|
||||||
|
>
|
||||||
|
<div style={s.convItemTop}>
|
||||||
|
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||||
|
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
|
||||||
|
</div>
|
||||||
|
<div style={s.convItemBottom}>
|
||||||
|
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
||||||
|
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button style={s.newConvSidebarBtn} onClick={() => handleNewConversation()}>
|
<button style={s.newConvSidebarBtn} onClick={() => handleNewConversation()}>
|
||||||
+ New Conversation
|
+ New Conversation
|
||||||
@@ -540,6 +566,7 @@ function ChatPage() {
|
|||||||
Close Chat
|
Close Chat
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{!isHuman && (
|
||||||
<button
|
<button
|
||||||
style={s.aiBtn}
|
style={s.aiBtn}
|
||||||
onClick={() => router.push("/ai-chat")}
|
onClick={() => router.push("/ai-chat")}
|
||||||
@@ -547,6 +574,7 @@ function ChatPage() {
|
|||||||
>
|
>
|
||||||
AI Assistant
|
AI Assistant
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1145,6 +1173,21 @@ const s = {
|
|||||||
},
|
},
|
||||||
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
||||||
convStatusClosed: { background: "#f0f0f0", color: "#888" },
|
convStatusClosed: { background: "#f0f0f0", color: "#888" },
|
||||||
|
convItemClosed: { opacity: 0.7 },
|
||||||
|
closedSectionToggle: {
|
||||||
|
width: "100%",
|
||||||
|
background: "#f5f5f5",
|
||||||
|
border: "none",
|
||||||
|
borderTop: "1px solid #e8e8e8",
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#666",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
newConvSidebarBtn: {
|
newConvSidebarBtn: {
|
||||||
margin: "0.65rem 1rem",
|
margin: "0.65rem 1rem",
|
||||||
background: "#333",
|
background: "#333",
|
||||||
|
|||||||
Reference in New Issue
Block a user