Merge pull request #302 from RecentRunner/web-features
fix web chat features
This commit is contained in:
@@ -123,6 +123,22 @@ function AiChatPage() {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const msg of messages) {
|
||||||
|
const prefix = msg.role === "assistant" ? "[AI Assistant] " : "[Customer] ";
|
||||||
|
await fetch(`${API_BASE}/api/v1/chat/conversations/${conv.id}/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content: prefix + msg.content }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Navigate anyway — agent has partial context
|
||||||
|
}
|
||||||
|
|
||||||
setHumanRequested(true);
|
setHumanRequested(true);
|
||||||
router.push(`/chat?id=${conv.id}`);
|
router.push(`/chat?id=${conv.id}`);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -20,12 +20,17 @@ function ChatPage() {
|
|||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [loadingConv, setLoadingConv] = useState(true);
|
const [loadingConv, setLoadingConv] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [conversations, setConversations] = useState([]);
|
||||||
|
const [convsLoading, setConvsLoading] = useState(false);
|
||||||
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const messagesAreaRef = useRef(null);
|
const messagesAreaRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const pollRef = useRef(null);
|
const pollRef = useRef(null);
|
||||||
const lastMessageIdRef = useRef(null);
|
const lastMessageIdRef = useRef(null);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading && !user) {
|
if (!authLoading && !user) {
|
||||||
@@ -88,6 +93,23 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
const fetchConversations = useCallback(async () => {
|
||||||
|
if (!token) return;
|
||||||
|
setConvsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
const startPolling = useCallback((convId) => {
|
const startPolling = useCallback((convId) => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (pollRef.current) clearInterval(pollRef.current);
|
||||||
pollRef.current = setInterval(async () => {
|
pollRef.current = setInterval(async () => {
|
||||||
@@ -168,11 +190,22 @@ function ChatPage() {
|
|||||||
};
|
};
|
||||||
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling]);
|
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSidebar && token) fetchConversations();
|
||||||
|
}, [showSidebar, token, fetchConversations]);
|
||||||
|
|
||||||
async function handleSend(e) {
|
async function handleSend(e) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
if (!text || sending || !conversation) return;
|
if ((!text && !selectedFile) || sending || !conversation) return;
|
||||||
|
if (selectedFile) {
|
||||||
|
await handleSendAttachment(text);
|
||||||
|
} else {
|
||||||
|
await handleSendText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSendText(text) {
|
||||||
setInput("");
|
setInput("");
|
||||||
setSending(true);
|
setSending(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -214,6 +247,59 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSendAttachment(optionalText) {
|
||||||
|
setSending(true);
|
||||||
|
setError(null);
|
||||||
|
const file = selectedFile;
|
||||||
|
setSelectedFile(null);
|
||||||
|
setInput("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
if (optionalText) formData.append("content", optionalText);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE}/api/v1/chat/conversations/${conversation.id}/attachments`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
router.push("/login?next=" + encodeURIComponent("/chat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
setError(data?.message || "Failed to send attachment.");
|
||||||
|
setSelectedFile(file);
|
||||||
|
setInput(optionalText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = await res.json();
|
||||||
|
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||||
|
lastMessageIdRef.current = msg.id;
|
||||||
|
} catch {
|
||||||
|
setError("Network error. Please try again.");
|
||||||
|
setSelectedFile(file);
|
||||||
|
setInput(optionalText);
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(e) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) setSelectedFile(file);
|
||||||
|
e.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(e) {
|
function handleKeyDown(e) {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -258,6 +344,19 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function switchConversation(convId) {
|
||||||
|
if (pollRef.current) clearInterval(pollRef.current);
|
||||||
|
setMessages([]);
|
||||||
|
setError(null);
|
||||||
|
setLoadingConv(true);
|
||||||
|
await fetchConversation(convId);
|
||||||
|
await fetchMessages(convId);
|
||||||
|
setLoadingConv(false);
|
||||||
|
startPolling(convId);
|
||||||
|
router.replace(`/chat?id=${convId}`, { scroll: false });
|
||||||
|
setShowSidebar(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (authLoading || loadingConv) {
|
if (authLoading || loadingConv) {
|
||||||
return (
|
return (
|
||||||
<main style={s.page}>
|
<main style={s.page}>
|
||||||
@@ -292,6 +391,43 @@ function ChatPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style={s.chatSection}>
|
<section style={s.chatSection}>
|
||||||
|
<div style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}>
|
||||||
|
{showSidebar && (
|
||||||
|
<div style={s.sidebar}>
|
||||||
|
<div style={s.sidebarHeader}>
|
||||||
|
<span style={s.sidebarTitle}>All Conversations</span>
|
||||||
|
<button style={s.sidebarClose} onClick={() => setShowSidebar(false)}>✕</button>
|
||||||
|
</div>
|
||||||
|
{convsLoading && <p style={s.sidebarEmpty}>Loading...</p>}
|
||||||
|
{!convsLoading && conversations.length === 0 && (
|
||||||
|
<p style={s.sidebarEmpty}>No conversations yet.</p>
|
||||||
|
)}
|
||||||
|
<div style={{ overflowY: "auto", flex: 1 }}>
|
||||||
|
{conversations.map((conv) => (
|
||||||
|
<button
|
||||||
|
key={conv.id}
|
||||||
|
style={{ ...s.convItem, ...(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, ...(conv.status === "OPEN" ? s.convStatusOpen : s.convStatusClosed) }}>
|
||||||
|
{conv.status}
|
||||||
|
</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>
|
||||||
|
<button style={s.newConvSidebarBtn} onClick={() => { setShowSidebar(false); handleNewConversation(); }}>
|
||||||
|
+ New Conversation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
{!conversation ? (
|
{!conversation ? (
|
||||||
<div style={s.noConvCard}>
|
<div style={s.noConvCard}>
|
||||||
<div style={s.noConvIcon}>💬</div>
|
<div style={s.noConvIcon}>💬</div>
|
||||||
@@ -301,6 +437,9 @@ function ChatPage() {
|
|||||||
<button style={s.startBtn} onClick={handleNewConversation}>
|
<button style={s.startBtn} onClick={handleNewConversation}>
|
||||||
Start a Conversation
|
Start a Conversation
|
||||||
</button>
|
</button>
|
||||||
|
<button style={s.backBtn} onClick={() => setShowSidebar(true)}>
|
||||||
|
View Past Conversations
|
||||||
|
</button>
|
||||||
<button style={s.backBtn} onClick={() => router.push("/ai-chat")}>
|
<button style={s.backBtn} onClick={() => router.push("/ai-chat")}>
|
||||||
Back to AI Assistant
|
Back to AI Assistant
|
||||||
</button>
|
</button>
|
||||||
@@ -320,13 +459,21 @@ function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
style={s.aiBtn}
|
<button
|
||||||
onClick={() => router.push("/ai-chat")}
|
style={{ ...s.historyBtn, ...(showSidebar ? s.historyBtnActive : {}) }}
|
||||||
title="Back to AI Assistant"
|
onClick={() => setShowSidebar((v) => !v)}
|
||||||
>
|
>
|
||||||
AI Assistant
|
History
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
style={s.aiBtn}
|
||||||
|
onClick={() => router.push("/ai-chat")}
|
||||||
|
title="Back to AI Assistant"
|
||||||
|
>
|
||||||
|
AI Assistant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!hasStaff && !hasStaffMessage && !isClosed && (
|
{!hasStaff && !hasStaffMessage && !isClosed && (
|
||||||
@@ -413,31 +560,62 @@ function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form style={s.inputArea} onSubmit={handleSend}>
|
<form style={s.inputArea} onSubmit={handleSend}>
|
||||||
<textarea
|
<input
|
||||||
ref={inputRef}
|
type="file"
|
||||||
style={s.textarea}
|
ref={fileInputRef}
|
||||||
value={input}
|
style={{ display: "none" }}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={handleFileChange}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Type a message..."
|
|
||||||
rows={1}
|
|
||||||
disabled={sending}
|
|
||||||
maxLength={2000}
|
|
||||||
/>
|
/>
|
||||||
<button
|
{selectedFile && (
|
||||||
type="submit"
|
<div style={s.filePreview}>
|
||||||
style={{
|
<span style={s.filePreviewName}>📎 {selectedFile.name}</span>
|
||||||
...s.sendBtn,
|
<button
|
||||||
...((!input.trim() || sending) ? s.sendBtnDisabled : {}),
|
type="button"
|
||||||
}}
|
style={s.filePreviewRemove}
|
||||||
disabled={!input.trim() || sending}
|
onClick={() => setSelectedFile(null)}
|
||||||
>
|
>
|
||||||
{sending ? "..." : "Send"}
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={s.inputRow}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={s.attachBtn}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={sending}
|
||||||
|
title="Attach a file"
|
||||||
|
>
|
||||||
|
📎
|
||||||
|
</button>
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
style={s.textarea}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={selectedFile ? "Add a caption (optional)..." : "Type a message..."}
|
||||||
|
rows={1}
|
||||||
|
disabled={sending}
|
||||||
|
maxLength={2000}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{
|
||||||
|
...s.sendBtn,
|
||||||
|
...((!input.trim() && !selectedFile) || sending ? s.sendBtnDisabled : {}),
|
||||||
|
}}
|
||||||
|
disabled={(!input.trim() && !selectedFile) || sending}
|
||||||
|
>
|
||||||
|
{sending ? "..." : "Send"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -480,7 +658,7 @@ const s = {
|
|||||||
margin: "1rem auto 0",
|
margin: "1rem auto 0",
|
||||||
},
|
},
|
||||||
chatSection: {
|
chatSection: {
|
||||||
maxWidth: 800,
|
maxWidth: 1060,
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
padding: "1.5rem 1rem 2rem",
|
padding: "1.5rem 1rem 2rem",
|
||||||
},
|
},
|
||||||
@@ -737,11 +915,16 @@ const s = {
|
|||||||
},
|
},
|
||||||
inputArea: {
|
inputArea: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "0.6rem",
|
flexDirection: "column",
|
||||||
|
gap: "0.4rem",
|
||||||
padding: "0.85rem 1.25rem",
|
padding: "0.85rem 1.25rem",
|
||||||
borderTop: "1px solid #f0f0f0",
|
borderTop: "1px solid #f0f0f0",
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
inputRow: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.6rem",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-end",
|
||||||
},
|
},
|
||||||
textarea: {
|
textarea: {
|
||||||
@@ -772,6 +955,155 @@ const s = {
|
|||||||
background: "#aaa",
|
background: "#aaa",
|
||||||
cursor: "not-allowed",
|
cursor: "not-allowed",
|
||||||
},
|
},
|
||||||
|
attachBtn: {
|
||||||
|
background: "none",
|
||||||
|
border: "1.5px solid #e0e0e0",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.5rem 0.6rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
flexShrink: 0,
|
||||||
|
color: "#666",
|
||||||
|
lineHeight: 1,
|
||||||
|
},
|
||||||
|
filePreview: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
background: "#f8f8f8",
|
||||||
|
border: "1px solid #e8e8e8",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.35rem 0.75rem",
|
||||||
|
},
|
||||||
|
filePreviewName: {
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
color: "#555",
|
||||||
|
flex: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
},
|
||||||
|
filePreviewRemove: {
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#999",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
padding: "0 0.15rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
historyBtn: {
|
||||||
|
background: "white",
|
||||||
|
border: "2px solid #555",
|
||||||
|
color: "#555",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.45rem 0.9rem",
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: "pointer",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
},
|
||||||
|
historyBtnActive: {
|
||||||
|
background: "#555",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
width: 230,
|
||||||
|
flexShrink: 0,
|
||||||
|
background: "white",
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: "0 4px 24px rgba(0,0,0,0.08)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
maxHeight: "calc(100vh - 220px)",
|
||||||
|
minHeight: 300,
|
||||||
|
},
|
||||||
|
sidebarHeader: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "0.85rem 1rem",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
sidebarTitle: { fontWeight: 700, fontSize: "0.88rem", color: "#333" },
|
||||||
|
sidebarClose: {
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
color: "#999",
|
||||||
|
padding: "0.1rem 0.25rem",
|
||||||
|
},
|
||||||
|
sidebarEmpty: {
|
||||||
|
color: "#aaa",
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
padding: "1rem",
|
||||||
|
textAlign: "center",
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
convItem: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.2rem",
|
||||||
|
padding: "0.65rem 1rem",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
background: "white",
|
||||||
|
border: "none",
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomStyle: "solid",
|
||||||
|
borderBottomColor: "#f0f0f0",
|
||||||
|
textAlign: "left",
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
convItemActive: { background: "#f8f8f8" },
|
||||||
|
convItemTop: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "0.4rem",
|
||||||
|
},
|
||||||
|
convItemBottom: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
convItemSubject: {
|
||||||
|
fontSize: "0.82rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#222",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
convItemMode: { fontSize: "0.7rem", color: "#999" },
|
||||||
|
convItemDate: { fontSize: "0.68rem", color: "#bbb" },
|
||||||
|
convStatusBadge: {
|
||||||
|
fontSize: "0.62rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: "0.1rem 0.45rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
},
|
||||||
|
convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" },
|
||||||
|
convStatusClosed: { background: "#f0f0f0", color: "#888" },
|
||||||
|
newConvSidebarBtn: {
|
||||||
|
margin: "0.65rem 1rem",
|
||||||
|
background: "#333",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "0.5rem 0.75rem",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: "pointer",
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default dynamic(() => Promise.resolve(ChatPage), { ssr: false });
|
export default dynamic(() => Promise.resolve(ChatPage), { ssr: false });
|
||||||
|
|||||||
Reference in New Issue
Block a user