w-aura/src/components/pages/AdvisorChat/index.tsx
2024-04-09 16:46:50 +00:00

391 lines
12 KiB
TypeScript

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 isPrivateChat = window.location.href.includes("/advisor-chat-private/");
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, birthtime, birthPlace } = useSelector(
selectors.selectQuestionnaire
);
const [assistant, setAssistant] = useState<IAssistant>();
const [messageText, setMessageText] = useState("");
const [textareaRows, setTextareaRows] = useState(1);
const [messages, setMessages] = useState<IMessage[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingSelfMessage, setIsLoadingSelfMessage] = useState(false);
const [isLoadingAdvisorMessage, setIsLoadingAdvisorMessage] = useState(false);
const [isLoadingLatestMessages, setIsLoadingLatestMessages] = useState(false);
const timeOutStatusRunRef = useRef<NodeJS.Timeout>();
const messagesEndRef = useRef<HTMLDivElement>(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.external_id === 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<Assistants.Response>(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<ResponseRunThread> => {
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 getContentMessage = (messageText: string) => {
const content = `#USER INFO: zodiac sign - ${zodiacSign}; gender - ${gender}; birthdate - ${birthdate}; name - ${
username || "unknown"
}; birthtime - ${birthtime || "unknown"}; birthPlace - ${
birthPlace || "unknown"
}# ${messageText}`;
return content;
};
const createMessage = async (messageText: string, threadId: string) => {
const content = getContentMessage(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 content = getContentMessage(messageText);
const thread = await createThread(content);
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<HTMLTextAreaElement>) => {
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 (
<section className={`${styles.page} page`}>
{isLoading && (
<Loader color={LoaderColor.Red} className={styles.loader} />
)}
{!isLoading && (
<ChatHeader
name={assistant?.name || ""}
avatar={assistant?.photo?.th2x || ""}
classNameContainer={styles["header-container"]}
clickBackButton={() => navigate(-1)}
hasBackButton={!isPrivateChat}
/>
)}
{!!messages.length && (
<div className={styles["messages-container"]}>
{isLoadingAdvisorMessage && (
<Message
avatar={assistant?.photo?.th2x || ""}
text={<LoaderDots />}
isSelf={false}
backgroundTextColor={"#c9c9c9"}
textColor={"#000"}
/>
)}
{messages.map((message) =>
message.content.map((content) => (
<Message
avatar={assistant?.photo?.th2x || ""}
text={deleteDataFromMessage(content.text.value)}
advisorName={assistant?.name || ""}
backgroundTextColor={
getIsSelfMessage(message.role) ? "#0080ff" : "#c9c9c9"
}
textColor={getIsSelfMessage(message.role) ? "#fff" : "#000"}
isSelf={getIsSelfMessage(message.role)}
key={message.id}
/>
))
)}
<div className={styles["loader-container"]}>
{isLoadingLatestMessages && <Loader color={LoaderColor.Red} />}
</div>
</div>
)}
<div ref={messagesEndRef} />
{!isLoading && (
<InputMessage
placeholder="Text message"
messageText={messageText}
textareaRows={textareaRows}
disabledTextArea={
isLoadingAdvisorMessage || isLoadingSelfMessage || isLoading
}
disabledButton={!messageText.length}
classNameContainer={styles["input-container"]}
handleChangeMessageText={handleChangeMessageText}
isLoading={isLoadingSelfMessage}
submitForm={sendMessage}
/>
)}
</section>
);
}
export default AdvisorChatPage;