Hamdast1/src/app/components/PublicChatPage.tsx
reza7321 213c0a70f0 امروز ۳۱ اردیبهشت
چت بات عمومی خرابه
2026-05-21 14:50:03 +03:30

290 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
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: crypto.randomUUID(),
type: "user",
content: msg.question,
timestamp: toPersianDigits(msg.datetime1),
});
}
if (msg.answer) {
converted.push({
id: crypto.randomUUID(),
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: crypto.randomUUID(),
type: "user",
content: trimmedText,
timestamp: new Date().toLocaleTimeString("fa-IR", {
hour: "2-digit",
minute: "2-digit",
}),
};
const loadingMessageId = crypto.randomUUID();
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: crypto.randomUUID(),
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>
);
}