391 lines
12 KiB
TypeScript
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;
|