merge main into branch
This commit is contained in:
@@ -4,9 +4,69 @@ import dynamic from "next/dynamic";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { createStompClient } from "@/lib/chatSocket";
|
||||
|
||||
const API_BASE = "";
|
||||
const POLL_INTERVAL = 2500;
|
||||
|
||||
function isImageFilename(name) {
|
||||
return /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(name || "");
|
||||
}
|
||||
|
||||
function AttachmentPreview({ url, name, token }) {
|
||||
const [blobUrl, setBlobUrl] = useState(null);
|
||||
const isImage = isImageFilename(name);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url || !token) return;
|
||||
let objectUrl;
|
||||
fetch(url, { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then((r) => (r.ok ? r.blob() : null))
|
||||
.then((blob) => {
|
||||
if (blob) {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setBlobUrl(objectUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => { if (objectUrl) URL.revokeObjectURL(objectUrl); };
|
||||
}, [url, token]);
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
{blobUrl ? (
|
||||
<img
|
||||
src={blobUrl}
|
||||
alt={name || "attachment"}
|
||||
style={{ maxWidth: "220px", maxHeight: "220px", borderRadius: "8px", cursor: "pointer", display: "block" }}
|
||||
onClick={() => window.open(blobUrl, "_blank")}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: "0.8rem", opacity: 0.7 }}>📎 Loading image…</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "0.4rem" }}>
|
||||
<a
|
||||
href="#"
|
||||
style={{ color: "inherit", fontSize: "0.85rem", opacity: 0.85 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!blobUrl) return;
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrl;
|
||||
a.download = name || "attachment";
|
||||
a.click();
|
||||
}}
|
||||
>
|
||||
📎 {name || "Attachment"}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AiChatPage() {
|
||||
const { user, token, loading: authLoading } = useAuth();
|
||||
@@ -22,15 +82,20 @@ function AiChatPage() {
|
||||
const [error, setError] = useState(null);
|
||||
const [conversations, setConversations] = useState([]);
|
||||
const [convsLoading, setConvsLoading] = useState(false);
|
||||
const [closedExpanded, setClosedExpanded] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [botTyping, setBotTyping] = useState(false);
|
||||
const [switchingConv, setSwitchingConv] = useState(false);
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
const messagesAreaRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const pollRef = useRef(null);
|
||||
const stompRef = useRef(null);
|
||||
const lastMessageIdRef = useRef(null);
|
||||
const fileInputRef = useRef(null);
|
||||
const lastScrolledIdRef = useRef(null);
|
||||
const initialLoadDoneRef = useRef(false);
|
||||
const botTypingTimeoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
@@ -42,6 +107,7 @@ function AiChatPage() {
|
||||
if (messages.length === 0) return;
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
if (lastMsg.id === lastScrolledIdRef.current) return;
|
||||
if (!initialLoadDoneRef.current) return;
|
||||
lastScrolledIdRef.current = lastMsg.id;
|
||||
const area = messagesAreaRef.current;
|
||||
if (!area) return;
|
||||
@@ -51,8 +117,17 @@ function AiChatPage() {
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!botTyping) return;
|
||||
const area = messagesAreaRef.current;
|
||||
if (!area) return;
|
||||
const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80;
|
||||
if (nearBottom) area.scrollTop = area.scrollHeight;
|
||||
}, [botTyping]);
|
||||
|
||||
const fetchMessages = useCallback(async (convId) => {
|
||||
if (!token || !convId) return;
|
||||
initialLoadDoneRef.current = false;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -61,10 +136,17 @@ function AiChatPage() {
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
setMessages(data);
|
||||
if (data.length > 0) lastMessageIdRef.current = data[data.length - 1].id;
|
||||
if (data.length > 0) {
|
||||
lastMessageIdRef.current = data[data.length - 1].id;
|
||||
lastScrolledIdRef.current = data[data.length - 1].id;
|
||||
}
|
||||
setTimeout(() => {
|
||||
const area = messagesAreaRef.current;
|
||||
if (area) area.scrollTop = area.scrollHeight;
|
||||
initialLoadDoneRef.current = true;
|
||||
}, 50);
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
@@ -100,38 +182,36 @@ function AiChatPage() {
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const startPolling = useCallback((convId) => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollRef.current = setInterval(async () => {
|
||||
if (!token || !convId) return;
|
||||
try {
|
||||
const [msgsRes, convRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
]);
|
||||
if (msgsRes.ok) {
|
||||
const data = await msgsRes.json();
|
||||
if (Array.isArray(data)) {
|
||||
const lastId = data.length > 0 ? data[data.length - 1].id : null;
|
||||
if (lastId !== lastMessageIdRef.current) {
|
||||
lastMessageIdRef.current = lastId;
|
||||
setMessages(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (convRes.ok) {
|
||||
const convData = await convRes.json();
|
||||
setConversation(convData);
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
}, [token]);
|
||||
const connectStomp = useCallback((convId) => {
|
||||
if (stompRef.current) {
|
||||
stompRef.current.deactivate();
|
||||
stompRef.current = null;
|
||||
}
|
||||
const client = createStompClient(token);
|
||||
client.onConnect = () => {
|
||||
client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => {
|
||||
try {
|
||||
const msg = JSON.parse(frame.body);
|
||||
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||
lastMessageIdRef.current = msg.id;
|
||||
if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current);
|
||||
setBotTyping(false);
|
||||
} catch { /* silent */ }
|
||||
});
|
||||
const convTopic = user?.role === "CUSTOMER"
|
||||
? `/user/queue/chat/conversations`
|
||||
: `/topic/chat/conversations`;
|
||||
client.subscribe(convTopic, (frame) => {
|
||||
try {
|
||||
const conv = JSON.parse(frame.body);
|
||||
if (conv.id === convId) setConversation(conv);
|
||||
setConversations((prev) => prev.map((c) => c.id === conv.id ? conv : c));
|
||||
} catch { /* silent */ }
|
||||
});
|
||||
};
|
||||
stompRef.current = client;
|
||||
client.activate();
|
||||
}, [token, user?.role]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || authLoading) return;
|
||||
@@ -171,7 +251,7 @@ function AiChatPage() {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ message: "Hello! I'd like to chat with the AI assistant." }),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (stale) return;
|
||||
if (res.ok) {
|
||||
@@ -197,7 +277,7 @@ function AiChatPage() {
|
||||
]);
|
||||
if (stale) return;
|
||||
setLoadingConv(false);
|
||||
startPolling(convId);
|
||||
connectStomp(convId);
|
||||
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
||||
}
|
||||
|
||||
@@ -205,9 +285,9 @@ function AiChatPage() {
|
||||
|
||||
return () => {
|
||||
stale = true;
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||
};
|
||||
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations, router]);
|
||||
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations, router]);
|
||||
|
||||
async function handleSend(e) {
|
||||
e?.preventDefault();
|
||||
@@ -250,6 +330,11 @@ function AiChatPage() {
|
||||
const msg = await res.json();
|
||||
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||
lastMessageIdRef.current = msg.id;
|
||||
if (!isEscalated) {
|
||||
setBotTyping(true);
|
||||
if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current);
|
||||
botTypingTimeoutRef.current = setTimeout(() => setBotTyping(false), 30000);
|
||||
}
|
||||
} catch {
|
||||
setError("Network error. Please try again.");
|
||||
setInput(text);
|
||||
@@ -320,7 +405,7 @@ function AiChatPage() {
|
||||
}
|
||||
|
||||
async function handleNewConversation() {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||
setError(null);
|
||||
setLoadingConv(true);
|
||||
try {
|
||||
@@ -330,7 +415,7 @@ function AiChatPage() {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ message: "Hello! I'd like to chat with the AI assistant." }),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
@@ -342,7 +427,7 @@ function AiChatPage() {
|
||||
setConversation(conv);
|
||||
await Promise.all([fetchMessages(conv.id), fetchConversations()]);
|
||||
setLoadingConv(false);
|
||||
startPolling(conv.id);
|
||||
connectStomp(conv.id);
|
||||
router.replace(`/ai-chat?id=${conv.id}`, { scroll: false });
|
||||
} catch {
|
||||
setError("Network error. Please try again.");
|
||||
@@ -351,9 +436,10 @@ function AiChatPage() {
|
||||
}
|
||||
|
||||
async function switchConversation(convId) {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
setBotTyping(false);
|
||||
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
||||
}
|
||||
|
||||
@@ -364,12 +450,28 @@ function AiChatPage() {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
router.push(`/chat?id=${conversation.id}`);
|
||||
setConversation((prev) => prev ? { ...prev, mode: "HUMAN" } : prev);
|
||||
} catch {
|
||||
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) {
|
||||
return (
|
||||
<main style={s.page}>
|
||||
@@ -382,6 +484,8 @@ function AiChatPage() {
|
||||
|
||||
const isEscalated = conversation?.mode === "HUMAN";
|
||||
const isClosed = conversation?.status === "CLOSED";
|
||||
const hasStaff = !!conversation?.staffId;
|
||||
const hasStaffMessage = messages.some((m) => m.senderId !== user?.id);
|
||||
|
||||
return (
|
||||
<main style={s.page}>
|
||||
@@ -402,7 +506,7 @@ function AiChatPage() {
|
||||
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
||||
)}
|
||||
<div style={{ overflowY: "auto", flex: 1 }}>
|
||||
{conversations.map((conv) => (
|
||||
{conversations.filter(c => c.status !== "CLOSED").map((conv) => (
|
||||
<button
|
||||
key={conv.id}
|
||||
style={{ ...s.convItem, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
|
||||
@@ -410,9 +514,7 @@ function AiChatPage() {
|
||||
>
|
||||
<div style={s.convItemTop}>
|
||||
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
|
||||
<span style={{ ...s.convStatusBadge, ...(conv.status === "OPEN" ? s.convStatusOpen : s.convStatusClosed) }}>
|
||||
{conv.status}
|
||||
</span>
|
||||
<span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span>
|
||||
</div>
|
||||
<div style={s.convItemBottom}>
|
||||
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
|
||||
@@ -421,6 +523,30 @@ function AiChatPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{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>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<button style={s.newConvSidebarBtn} onClick={handleNewConversation}>
|
||||
+ New Conversation
|
||||
</button>
|
||||
@@ -444,41 +570,44 @@ function AiChatPage() {
|
||||
<div style={s.chatCard}>
|
||||
<div style={s.chatHeader}>
|
||||
<div style={s.chatHeaderLeft}>
|
||||
<div style={s.aiAvatar}>🐾</div>
|
||||
<div style={isEscalated ? s.agentAvatar : s.aiAvatar}>{isEscalated ? "👤" : "🐾"}</div>
|
||||
<div>
|
||||
<div style={s.chatHeaderTitle}>Leon's Pet Assistant</div>
|
||||
<div style={s.chatHeaderStatus}>
|
||||
<span style={s.statusDot} /> Online
|
||||
<div style={s.chatHeaderTitle}>
|
||||
{isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"}
|
||||
</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 style={{ display: "flex", gap: "0.5rem" }}>
|
||||
{!isEscalated && !isClosed && (
|
||||
<button
|
||||
style={s.humanBtn}
|
||||
onClick={handleSwitchToHuman}
|
||||
title="Connect with a human support agent"
|
||||
>
|
||||
<button style={s.humanBtn} onClick={handleSwitchToHuman} title="Connect with a human support agent">
|
||||
Chat with a Real Person
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
style={s.liveBtn}
|
||||
onClick={() => router.push("/chat")}
|
||||
title="Go to Live Support"
|
||||
>
|
||||
Live Support
|
||||
</button>
|
||||
{!isClosed && (
|
||||
<button style={s.closeConvBtn} onClick={handleCloseConversation} title="Close this conversation">
|
||||
Close Chat
|
||||
</button>
|
||||
)}
|
||||
</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}>
|
||||
{messages.length === 0 && (
|
||||
<div style={s.emptyState}>
|
||||
<div style={s.emptyIcon}>🐾</div>
|
||||
<div style={s.emptyIcon}>{isEscalated ? "💬" : "🐾"}</div>
|
||||
<p style={s.emptyText}>
|
||||
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!
|
||||
{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!`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -493,7 +622,7 @@ function AiChatPage() {
|
||||
...(isOwn ? s.messageRowUser : s.messageRowAgent),
|
||||
}}
|
||||
>
|
||||
{!isOwn && <div style={s.aiAvatarSmall}>🐾</div>}
|
||||
{!isOwn && <div style={isEscalated ? s.agentAvatarSmall : s.aiAvatarSmall}>{isEscalated ? "👤" : "🐾"}</div>}
|
||||
<div
|
||||
style={{
|
||||
...s.messageBubble,
|
||||
@@ -507,16 +636,7 @@ function AiChatPage() {
|
||||
</span>
|
||||
))}
|
||||
{msg.attachmentUrl && (
|
||||
<div style={s.attachment}>
|
||||
<a
|
||||
href={msg.attachmentUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={s.attachmentLink}
|
||||
>
|
||||
📎 {msg.attachmentName || "Attachment"}
|
||||
</a>
|
||||
</div>
|
||||
<AttachmentPreview url={msg.attachmentUrl} name={msg.attachmentName} token={token} />
|
||||
)}
|
||||
<div style={{ ...s.timestamp, ...(isOwn ? s.timestampUser : {}) }}>
|
||||
{msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}
|
||||
@@ -530,6 +650,24 @@ function AiChatPage() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{botTyping && !isEscalated && (
|
||||
<div style={{ ...s.messageRow, ...s.messageRowAgent }}>
|
||||
<div style={s.aiAvatarSmall}>🐾</div>
|
||||
<div style={{ ...s.messageBubble, ...s.bubbleAgent, display: "flex", alignItems: "center", gap: "4px", padding: "0.6rem 0.9rem" }}>
|
||||
<span className="fc-dot" />
|
||||
<span className="fc-dot" style={{ animationDelay: "0.2s" }} />
|
||||
<span className="fc-dot" style={{ animationDelay: "0.4s" }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{switchingConv && (
|
||||
<div style={{ textAlign: "center", padding: "1rem", color: "#aaa", fontSize: "0.85rem" }}>
|
||||
Loading messages…
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
@@ -726,6 +864,21 @@ const s = {
|
||||
},
|
||||
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
||||
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: {
|
||||
margin: "0.65rem 1rem",
|
||||
background: "#333",
|
||||
@@ -815,6 +968,60 @@ const s = {
|
||||
cursor: "pointer",
|
||||
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: {
|
||||
background: "white",
|
||||
borderRadius: 16,
|
||||
|
||||
Reference in New Issue
Block a user