import { useNavigate, useParams } from "react-router-dom"; import styles from "./styles.module.css"; import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; import { Assistants, useApi, useApiCall } from "@/api"; import { useSelector } from "react-redux"; import { selectors } from "@/store"; import { IAssistant } from "@/api/resources/Assistants"; import Loader, { LoaderColor } from "@/components/Loader"; import ChatHeader from "./components/ChatHeader"; import InputMessage from "./components/InputMessage"; import routes from "@/routes"; import { IMessage, ResponseRunThread } from "@/api/resources/OpenAI"; import Message from "./components/Message"; import LoaderDots from "./components/LoaderDots"; import useDetectScroll from "@smakss/react-scroll-direction"; import { getZodiacSignByDate } from "@/services/zodiac-sign"; function AdvisorChatPage() { const { id } = useParams(); const api = useApi(); const navigate = useNavigate(); const { scrollDir, scrollPosition } = useDetectScroll(); const token = useSelector(selectors.selectToken); const openAiToken = useSelector(selectors.selectOpenAiToken); const birthdate = useSelector(selectors.selectBirthdate); const zodiacSign = getZodiacSignByDate(birthdate); const { username } = useSelector(selectors.selectUser); const { gender } = useSelector(selectors.selectQuestionnaire); const [assistant, setAssistant] = useState(); const [messageText, setMessageText] = useState(""); const [textareaRows, setTextareaRows] = useState(1); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false); const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false); const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false); const timeOutStatusRunRef = useRef(); const messagesEndRef = useRef(null); const [hasMoreLatestMessages, setHasMoreLatestMessages] = useState(true); useEffect(() => { if ( scrollDir === "up" && scrollPosition.top < 50 && !isLoadingLatestMessages ) { loadLatestMessages(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrollDir, scrollPosition]); const loadLatestMessages = async () => { const lastIdMessage = messages[messages.length - 1]?.id; if (!hasMoreLatestMessages || !lastIdMessage) { return; } setIsLoadingLatestMessages(true); const listMessages = await api.getListMessages({ token: openAiToken, method: "GET", path: routes.openAi.getListMessages(assistant?.external_chat_id || ""), QueryParams: { after: lastIdMessage, limit: 20, }, }); setHasMoreLatestMessages(listMessages.has_more); setMessages((prev) => [...prev, ...listMessages.data]); setIsLoadingLatestMessages(false); }; const scrollToBottom = () => { setTimeout(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, 100); }; useEffect(() => { return () => { if (timeOutStatusRunRef.current) { clearTimeout(timeOutStatusRunRef.current); } }; }, []); const getCurrentAssistant = ( aiAssistants: Assistants.IAssistant[], idAssistant: string | number ) => { const currentAssistant = aiAssistants.find( (a) => a.id === Number(idAssistant) ); return currentAssistant; }; const updateMessages = async (threadId: string) => { const listMessages = await api.getListMessages({ token: openAiToken, method: "GET", path: routes.openAi.getListMessages(threadId), }); setMessages(listMessages.data); scrollToBottom(); }; const setExternalChatIdAssistant = async (threadId: string) => { await api.setExternalChatIdAssistant({ token, chatId: String(id), ai_assistant_chat: { external_id: threadId, }, }); }; const updateCurrentAssistant = async () => { const { ai_assistants } = await api.assistants({ token, }); const currentAssistant = getCurrentAssistant(ai_assistants, id || ""); setAssistant(currentAssistant); return { ai_assistants, currentAssistant, }; }; const loadData = useCallback(async () => { const { ai_assistants, currentAssistant } = await updateCurrentAssistant(); let listRuns: ResponseRunThread[] = []; if (currentAssistant?.external_chat_id?.length) { await updateMessages(currentAssistant.external_chat_id); const result = await api.getListRuns({ token: openAiToken, method: "GET", path: routes.openAi.getListRuns(currentAssistant.external_chat_id), }); listRuns = result.data; } setIsLoading(false); const runInProgress = listRuns.find((r) => r.status === "in_progress"); setTimeout(() => { scrollToBottom(); }); if (runInProgress) { setIsLoadingAdvisorMessage(true); await checkStatusAndGetLastMessage( runInProgress.thread_id, runInProgress.id ); setIsLoadingAdvisorMessage(false); } return { ai_assistants }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [api, id, openAiToken, token]); useApiCall(loadData); const createThread = async (messageText: string) => { const thread = await api.createThread({ token: openAiToken, method: "POST", path: routes.openAi.createThread(), messages: [ { role: "user", content: messageText, }, ], }); await setExternalChatIdAssistant(thread.id); await updateCurrentAssistant(); return thread; }; const getStatusThreadRun = async ( threadId: string, runId: string ): Promise => { await new Promise( (resolve) => (timeOutStatusRunRef.current = setTimeout(resolve, 1500)) ); const run = await api.getStatusRunThread({ token: openAiToken, method: "GET", path: routes.openAi.getStatusRunThread(threadId, runId), assistant_id: `${assistant?.external_id}`, }); if (run.status !== "completed") { return await getStatusThreadRun(threadId, runId); } return run; }; const createMessage = async (messageText: string, threadId: string) => { const content = `#USER INFO: zodiac sign - ${zodiacSign}; gender - ${gender}; birthdate - ${birthdate}; name - ${ username || "unknown" };# ${messageText}`; const message = await api.createMessage({ token: openAiToken, method: "POST", path: routes.openAi.createMessage(threadId), role: "user", content: content, }); return message; }; const runThread = async (threadId: string, assistantId: string) => { const run = await api.runThread({ token: openAiToken, method: "POST", path: routes.openAi.runThread(threadId), assistant_id: assistantId, }); return run; }; const checkStatusAndGetLastMessage = async ( threadId: string, runId: string ) => { const { status } = await getStatusThreadRun(threadId, runId); if (status === "completed") { await getLastMessage(threadId); } }; const getLastMessage = async (threadId: string) => { const lastMessage = await api.getListMessages({ token: openAiToken, method: "GET", path: routes.openAi.getListMessages(threadId), QueryParams: { limit: 1, }, }); setMessages((prev) => [lastMessage.data[0], ...prev]); }; const sendMessage = async (messageText: string) => { setMessageText(""); setIsLoadingSelfMessage(true); let threadId = ""; let assistantId = ""; if (assistant?.external_chat_id?.length) { const message = await createMessage( messageText, assistant.external_chat_id ); setMessages((prev) => [message, ...prev]); setIsLoadingSelfMessage(false); setIsLoadingAdvisorMessage(true); threadId = assistant.external_chat_id; assistantId = assistant.external_id || ""; } else { const thread = await createThread(messageText); threadId = thread.id; assistantId = assistant?.external_id || ""; await getLastMessage(threadId); setIsLoadingSelfMessage(false); setIsLoadingAdvisorMessage(true); } setTimeout(() => { scrollToBottom(); }); const run = await runThread(threadId, assistantId); await checkStatusAndGetLastMessage(threadId, run.id); setTimeout(() => { scrollToBottom(); }); setIsLoadingAdvisorMessage(false); }; const handleChangeMessageText = (e: ChangeEvent) => { const { scrollHeight, clientHeight, value } = e.target; if ( scrollHeight > clientHeight && textareaRows < 5 && value.length > messageText.length ) { setTextareaRows((prev) => prev + 1); } if ( scrollHeight === clientHeight && textareaRows > 1 && value.length < messageText.length ) { setTextareaRows((prev) => prev - 1); } setMessageText(e.target.value); }; const getIsSelfMessage = (role: string) => role === "user"; const deleteDataFromMessage = (messageText: string) => { const splittedText = messageText.split("#"); if (splittedText.length > 2) { return splittedText.slice(2).join("#"); } return messageText; }; return (
{isLoading && ( )} {!isLoading && ( navigate(-1)} /> )} {!!messages.length && (
{isLoadingAdvisorMessage && ( } isSelf={false} backgroundTextColor={"#c9c9c9"} textColor={"#000"} /> )} {messages.map((message) => message.content.map((content) => ( )) )}
{isLoadingLatestMessages && }
)}
{!isLoading && ( )}
); } export default AdvisorChatPage;