299 lines
9.6 KiB
TypeScript
299 lines
9.6 KiB
TypeScript
import { useState, useRef, useEffect, useCallback } from "react";
|
||
import { ChatMessages, ChatMessage } from "./public-chat/ChatMessages";
|
||
import { ChatInputBar } from "./chatbot/ChatInputBar";
|
||
import { ChatHistoryModal } from "./public-chat/ChatHistoryModal";
|
||
import {
|
||
loadChatList,
|
||
loadChat,
|
||
sendPublicChatMessage,
|
||
ChatListItem,
|
||
PublicChatMessage,
|
||
} from "../../services/publicChatService";
|
||
import { usePageTracking } from "../../hooks/usePageTracking";
|
||
import { toPersianDigits } from "../../utils/persianNumberUtils";
|
||
import chatbotAvatarIcon from "../../assets/chatbot-bot-avatar.png";
|
||
|
||
const createMessageId = () => {
|
||
if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
|
||
return globalThis.crypto.randomUUID();
|
||
}
|
||
|
||
const randomPart = Math.random().toString(36).slice(2, 10);
|
||
return `msg-${Date.now()}-${randomPart}`;
|
||
};
|
||
|
||
export function PublicChatPage() {
|
||
usePageTracking("چت عمومی");
|
||
|
||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||
const [showChatHistory, setShowChatHistory] = useState(false);
|
||
const [historyItems, setHistoryItems] = useState<ChatListItem[]>([]);
|
||
const [currentChatWorkflowID, setCurrentChatWorkflowID] = useState<string>("");
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [isSending, setIsSending] = useState(false);
|
||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||
|
||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||
|
||
const isNearBottom = () => {
|
||
const container = messagesContainerRef.current;
|
||
if (!container) return true;
|
||
|
||
return (
|
||
container.scrollHeight - container.scrollTop - container.clientHeight < 100
|
||
);
|
||
};
|
||
|
||
const scrollToBottom = (smooth = true) => {
|
||
messagesEndRef.current?.scrollIntoView({
|
||
behavior: smooth ? "smooth" : "auto",
|
||
block: "end",
|
||
});
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (shouldAutoScroll) {
|
||
scrollToBottom();
|
||
}
|
||
}, [messages, shouldAutoScroll]);
|
||
|
||
useEffect(() => {
|
||
const container = messagesContainerRef.current;
|
||
if (!container) return;
|
||
|
||
const handleScroll = () => {
|
||
setShouldAutoScroll(isNearBottom());
|
||
};
|
||
|
||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||
return () => container.removeEventListener("scroll", handleScroll);
|
||
}, []);
|
||
|
||
const convertPublicChatMessagesToChatMessages = (
|
||
publicMessages: PublicChatMessage[],
|
||
): ChatMessage[] => {
|
||
const converted: ChatMessage[] = [];
|
||
|
||
publicMessages.forEach((msg) => {
|
||
if (msg.question) {
|
||
converted.push({
|
||
id: createMessageId(),
|
||
type: "user",
|
||
content: msg.question,
|
||
timestamp: toPersianDigits(msg.datetime1),
|
||
});
|
||
}
|
||
|
||
if (msg.answer) {
|
||
converted.push({
|
||
id: createMessageId(),
|
||
type: "other",
|
||
content: msg.answer,
|
||
author: "ربات",
|
||
timestamp: toPersianDigits(msg.datetime1),
|
||
});
|
||
}
|
||
});
|
||
|
||
return converted;
|
||
};
|
||
|
||
const handleSelectChat = async (chatId: string) => {
|
||
setShowChatHistory(false);
|
||
setIsLoading(true);
|
||
setCurrentChatWorkflowID(chatId);
|
||
|
||
const result = await loadChat(chatId);
|
||
|
||
if (result.success) {
|
||
const convertedMessages = convertPublicChatMessagesToChatMessages(result.data);
|
||
setMessages(convertedMessages);
|
||
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
scrollToBottom(false);
|
||
});
|
||
});
|
||
} else {
|
||
console.error("Failed to load chat:", result.message);
|
||
alert("خطا در بارگذاری چت");
|
||
}
|
||
|
||
setIsLoading(false);
|
||
};
|
||
|
||
const handleSendMessage = async (message: string) => {
|
||
const trimmedText = message.trim();
|
||
if (!trimmedText || isSending) return;
|
||
|
||
const userMessage: ChatMessage = {
|
||
id: createMessageId(),
|
||
type: "user",
|
||
content: trimmedText,
|
||
timestamp: new Date().toLocaleTimeString("fa-IR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}),
|
||
};
|
||
|
||
const loadingMessageId = createMessageId();
|
||
const loadingMessage: ChatMessage = {
|
||
id: loadingMessageId,
|
||
type: "loading",
|
||
content: "",
|
||
author: "ربات",
|
||
timestamp: "",
|
||
};
|
||
|
||
setShouldAutoScroll(true);
|
||
setMessages((prev) => [...prev, userMessage, loadingMessage]);
|
||
setIsSending(true);
|
||
|
||
try {
|
||
const result = await sendPublicChatMessage(trimmedText, currentChatWorkflowID);
|
||
|
||
setMessages((prev) => prev.filter((msg) => msg.id !== loadingMessageId));
|
||
|
||
if (result.success && result.answer) {
|
||
if (result.newChatlistWorkflowID) {
|
||
setCurrentChatWorkflowID(result.newChatlistWorkflowID);
|
||
}
|
||
|
||
const botMessage: ChatMessage = {
|
||
id: createMessageId(),
|
||
type: "other",
|
||
content: result.answer,
|
||
author: "ربات",
|
||
timestamp: new Date().toLocaleTimeString("fa-IR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}),
|
||
isTyping: true,
|
||
};
|
||
|
||
setMessages((prev) => [...prev, botMessage]);
|
||
} else {
|
||
alert(result.message || "خطا در ارسال پیام");
|
||
}
|
||
} catch (error) {
|
||
setMessages((prev) => prev.filter((msg) => msg.id !== loadingMessageId));
|
||
alert("خطا در ارسال پیام");
|
||
} finally {
|
||
setIsSending(false);
|
||
}
|
||
};
|
||
|
||
const handleHistoryClick = useCallback(async () => {
|
||
setShowChatHistory(true);
|
||
|
||
const result = await loadChatList();
|
||
|
||
if (result.success) {
|
||
setHistoryItems(result.data);
|
||
} else {
|
||
console.error("Failed to load chat list:", result.message);
|
||
alert(result.message || "خطا در بارگذاری تاریخچه");
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const onHistoryRequest = () => {
|
||
void handleHistoryClick();
|
||
};
|
||
|
||
window.addEventListener("public-chat:history", onHistoryRequest);
|
||
return () => window.removeEventListener("public-chat:history", onHistoryRequest);
|
||
}, [handleHistoryClick]);
|
||
|
||
return (
|
||
<div className="relative h-full min-h-0 overflow-hidden">
|
||
<div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]">
|
||
<main className="relative min-h-0 overflow-hidden">
|
||
{isLoading ? (
|
||
<div className="flex h-full items-center justify-center px-5">
|
||
<p className="text-sm text-white">در حال بارگذاری...</p>
|
||
</div>
|
||
) : messages.length === 0 ? (
|
||
<div className="flex h-full items-center justify-center px-5">
|
||
<div
|
||
className="max-w-sm rounded-[22px] border-[0.5px] border-transparent px-5 py-4 text-center"
|
||
style={{
|
||
backgroundImage:
|
||
"linear-gradient(180deg, rgba(46, 27, 61, 0.92) 0%, rgba(35, 24, 62, 0.94) 100%), linear-gradient(120deg, #7c3aed 0%, #f97316 58%, #facc15 100%)",
|
||
backgroundOrigin: "border-box",
|
||
backgroundClip: "padding-box, border-box",
|
||
boxShadow:
|
||
"0 -7px 20px rgba(7, 0, 18, 0.46), 0 6px 14px rgba(5, 2, 12, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -2px 0 rgba(12, 7, 27, 0.66)",
|
||
backdropFilter: "blur(14px)",
|
||
WebkitBackdropFilter: "blur(14px)",
|
||
}}
|
||
>
|
||
<div className="text-center">
|
||
<div
|
||
className="mx-auto mb-2 flex h-14 w-14 items-center justify-center rounded-full border border-[#F0A6D8]/45"
|
||
style={{
|
||
background:
|
||
"radial-gradient(circle at 50% 36%, rgba(255, 187, 232, 0.38) 0%, rgba(134, 74, 164, 0.28) 48%, rgba(26, 18, 54, 0.7) 100%)",
|
||
boxShadow:
|
||
"0 0 14px rgba(255,104,205,0.48), 0 0 28px rgba(255,104,205,0.22), inset 0 1px 0 rgba(255,255,255,0.22)",
|
||
}}
|
||
>
|
||
<img
|
||
src={chatbotAvatarIcon}
|
||
alt="چتبات"
|
||
className="h-11 w-11 object-contain"
|
||
/>
|
||
</div>
|
||
<p className="mb-1.5 text-base font-bold text-[#FBE7F5]">
|
||
با ربات همدست چت کن!
|
||
</p>
|
||
<p className="text-xs text-[#EED3EC]/90">
|
||
سوالاتت رو بپرس و جواب بگیر
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="h-full min-h-0">
|
||
<ChatMessages
|
||
messages={messages}
|
||
containerRef={messagesContainerRef}
|
||
endRef={messagesEndRef}
|
||
onTyping={() => {
|
||
if (shouldAutoScroll) {
|
||
scrollToBottom(false);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</main>
|
||
|
||
<footer
|
||
className="shrink-0"
|
||
style={{
|
||
paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 12px)",
|
||
}}
|
||
>
|
||
<div className="px-3 pt-2">
|
||
<ChatInputBar onSendMessage={handleSendMessage} disabled={isSending} />
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<ChatHistoryModal
|
||
isOpen={showChatHistory}
|
||
onClose={() => setShowChatHistory(false)}
|
||
historyItems={historyItems.map((item) => ({
|
||
id: item.chatlist_workflowID,
|
||
title: item.title || "چت عمومی",
|
||
date: toPersianDigits(item.datetime1),
|
||
lastMessage: "",
|
||
}))}
|
||
onSelectChat={handleSelectChat}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|