299 lines
8.9 KiB
TypeScript
299 lines
8.9 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { BottomNav } from "./BottomNav";
|
||
import { ChatHeader } from "./public-chat/ChatHeader";
|
||
import { ChatMessages, ChatMessage } from "./public-chat/ChatMessages";
|
||
import { ChatInput } from "./public-chat/ChatInput";
|
||
import { ChatHistoryModal } from "./public-chat/ChatHistoryModal";
|
||
import { AppBackground } from "./shared/AppBackground";
|
||
import {
|
||
loadChatList,
|
||
loadChat,
|
||
sendPublicChatMessage,
|
||
ChatListItem,
|
||
PublicChatMessage,
|
||
} from "../../services/publicChatService";
|
||
import { usePageTracking } from "../../hooks/usePageTracking";
|
||
import { backgroundImages } from "../../config/backgroundConfig";
|
||
|
||
export function PublicChatPage() {
|
||
const navigate = useNavigate();
|
||
usePageTracking("چت عمومی");
|
||
|
||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||
const [inputText, setInputText] = useState("");
|
||
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 inputRef = useRef<HTMLTextAreaElement>(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: msg.datetime1,
|
||
});
|
||
}
|
||
|
||
if (msg.answer) {
|
||
converted.push({
|
||
id: crypto.randomUUID(),
|
||
type: "other",
|
||
content: msg.answer,
|
||
author: "ربات",
|
||
timestamp: 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 () => {
|
||
const trimmedText = inputText.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]);
|
||
setInputText("");
|
||
setIsSending(true);
|
||
|
||
if (inputRef.current) {
|
||
inputRef.current.style.height = "auto";
|
||
}
|
||
|
||
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 || "خطا در ارسال پیام");
|
||
}
|
||
|
||
setIsSending(false);
|
||
inputRef.current?.focus();
|
||
};
|
||
|
||
const handleHistoryClick = 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 || "خطا در بارگذاری تاریخچه");
|
||
}
|
||
};
|
||
|
||
const handleNewChat = () => {
|
||
setMessages([]);
|
||
setCurrentChatWorkflowID("");
|
||
setShouldAutoScroll(true);
|
||
|
||
requestAnimationFrame(() => {
|
||
inputRef.current?.focus();
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="relative h-[100dvh] w-full overflow-hidden bg-black">
|
||
<AppBackground position="fixed" zIndex={0} imageUrl={backgroundImages.publicChat} />
|
||
|
||
<div className="relative z-10 mx-auto grid h-full w-full max-w-md grid-rows-[auto_minmax(0,1fr)_auto]">
|
||
<div className="shrink-0">
|
||
<ChatHeader onBack={() => navigate("/")} />
|
||
</div>
|
||
|
||
<main className="relative min-h-0 overflow-hidden">
|
||
<button
|
||
onClick={handleHistoryClick}
|
||
className="absolute left-4 top-3 z-20 px-3 py-1.5 rounded-full text-xs text-white font-bold"
|
||
style={{
|
||
background: "linear-gradient(135deg, rgba(138, 206, 224, 0.9) 0%, rgba(76, 127, 137, 0.9) 100%)",
|
||
border: "1px solid rgba(208, 240, 255, 0.6)",
|
||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
|
||
}}
|
||
>
|
||
تاریخچه
|
||
</button>
|
||
{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-2xl border-2 border-purple-400/50 bg-gradient-to-br from-purple-500/20 to-pink-500/20 p-5 shadow-2xl backdrop-blur-md">
|
||
<div className="text-center">
|
||
<div className="mb-2 text-3xl">🤖✨</div>
|
||
<p className="mb-1.5 text-base font-bold text-white">
|
||
با ربات همدست چت کن!
|
||
</p>
|
||
<p className="text-xs text-purple-200">
|
||
سوالاتت رو بپرس و جواب بگیر
|
||
</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 border-white/10 "
|
||
style={{
|
||
paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)",
|
||
}}
|
||
>
|
||
<div className="px-3 pt-2">
|
||
<ChatInput
|
||
inputText={inputText}
|
||
onInputChange={setInputText}
|
||
onSend={handleSendMessage}
|
||
onNewChat={handleNewChat}
|
||
inputRef={inputRef}
|
||
/>
|
||
</div>
|
||
|
||
<div className="px-2 pt-6">
|
||
<BottomNav />
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<ChatHistoryModal
|
||
isOpen={showChatHistory}
|
||
onClose={() => setShowChatHistory(false)}
|
||
historyItems={historyItems.map((item) => ({
|
||
id: item.chatlist_workflowID,
|
||
title: item.title || "چت عمومی",
|
||
date: item.datetime1,
|
||
lastMessage: "",
|
||
}))}
|
||
onSelectChat={handleSelectChat}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|