From 75bced523ccdc5134a3686badfd8c567696bf9c3 Mon Sep 17 00:00:00 2001 From: Jose Othon Date: Tue, 17 Sep 2024 22:44:34 -0300 Subject: [PATCH] feat: use our BE chatGPT/openAi integration and support local chat history --- src/pages/Chat/Chat.jsx | 497 +++++++++++-------- src/pages/Chat/FullPageChat/FullPageChat.jsx | 201 ++++---- src/pages/Chat/openai.js | 16 + src/style/Chat/FullPageChat/FullPageChat.css | 152 ++++-- 4 files changed, 515 insertions(+), 351 deletions(-) create mode 100644 src/pages/Chat/openai.js diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx index 402df2a..809a400 100644 --- a/src/pages/Chat/Chat.jsx +++ b/src/pages/Chat/Chat.jsx @@ -1,128 +1,245 @@ /* eslint-disable react/prop-types */ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FullPageChat } from "./FullPageChat/FullPageChat"; import { Widget } from "./Widget/Widget"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import useAuthenticationContext from "../../hook/Authentication/useAuthenticationContext"; import Chamados from "../../assets/base-chamados.json"; +import { handleOpenAiRequest } from "./openai"; + +const useRealApi = false; + +export function getRandomChatId() { + const firstSlice = Math.floor(Math.random() * 1000000) + .toString() + .slice(0, 4); + const secondSlice = Math.floor(Math.random() * 1000000) + .toString() + .slice(0, 4); + + return `${firstSlice}-${secondSlice}`; +} function Chat({ type }) { const { authedUser } = useAuthenticationContext(); + const navigate = useNavigate(); const location = useLocation(); - const [messages, setMessages] = useState([]); + const chatId = new URLSearchParams(location.search).get("chatId"); + const [isIaTyping, setIsIaTyping] = useState(false); const chatBodyRef = useRef(); const isWelcomeSent = useRef(false); - const processQuestion = (question) => { - const questionTokens = question.trim().split(" "); - - if (question.toLowerCase().includes("obrigado")) { - const gratitudeResponses = [ - "De nada, estou aqui para ajudar!", - "Disponha, precisando é só chamar!", - "Estou aqui para o que precisar!", - "Estou sempre à disposição!", - "Estou aqui para ajudar, não hesite em me chamar!", - ]; - - handleSendIaMessages([ - { - content: - gratitudeResponses[ - Math.floor( - Math.random() * gratitudeResponses.length - ) - ], - author: "ia", - timestamp: new Date(), - }, - ]); - setIsIaTyping(false); - return true; - } + const [chatHistory, setChatHistory] = useState({}); - if (questionTokens.length === 0 || questionTokens.length === 1) { - const giveMoreDetailsSamples = [ - "Você poderia elaborar melhor sua pergunta?", - "Estou com dificuldades de encontrar algo, poderia me dar mais detalhes?", - "Você poderia ser mais específico?", - "Poderia me explicar melhor?", - "Você pode me dar mais detalhes sobre seu problema?", - ]; - - handleSendIaMessages([ - { - content: - giveMoreDetailsSamples[ - Math.floor( - Math.random() * giveMoreDetailsSamples.length - ) - ], - author: "ia", - timestamp: new Date(), - }, - ]); - setIsIaTyping(false); - return true; + useEffect(() => { + if (!chatId) { + navigate(`/chat?chatId=${getRandomChatId()}`, { + replace: true, + }); } + }, [chatId, navigate]); - const chamadoPorNumero = Chamados.find((chamado) => - questionTokens.some( - (token) => - chamado["Número"].toLowerCase() === - token.replace("#", "").toLowerCase() - ) - ); - - const rankedChamados = chamadoPorNumero - ? [chamadoPorNumero] - : Chamados.reduce((acc, chamado) => { - const searchFields = [ - "Categoria Relatório", - "Subcategoria Relatório", - "Sintoma", - "Grupo de atribuição", - "Local", - "Descrição", - "Comentários Visiveis 1", - "Comentários Visiveis 2", - ]; - - let points = 0; - searchFields.forEach((field) => { - const hasMatches = questionTokens.some((token) => - chamado[field] - .toLowerCase() - .includes(token.replace("#", "").toLowerCase()) - ); - - if (hasMatches) { - points++; - } - }); + const messages = useMemo( + () => chatHistory[chatId] || [], + [chatHistory, chatId] + ); + + const setMessages = useCallback( + (prevFunc) => { + setChatHistory((prev) => { + return { + ...prev, + [chatId]: prevFunc(prev[chatId] || []), + }; + }); + }, + [chatId] + ); + + const handleSendIaMessages = useCallback( + async (messages) => { + setIsIaTyping(true); + + let index = 0; + for await (const msg of messages) { + const isLastMessage = index === messages.length - 1; + const words = msg.content.split(" "); + + let wordIndex = 0; + + setMessages((prev) => { + return [ + ...prev, + { + ...msg, + content: "...", + timestamp: new Date(), + author: "ia", + }, + ]; + }); + + for await (const word of words) { + const isLastWord = wordIndex === words.length - 1; + + await new Promise((resolve) => { + const randomWaitTime = Math.floor(Math.random() * 100); + + setTimeout( + () => { + setMessages((prev) => { + const lastMessage = prev[prev.length - 1]; + const lastMessageContent = + lastMessage?.content || ""; + const newContent = + lastMessageContent === "..." + ? word + : `${lastMessageContent} ${word}`; + + return [ + ...prev.slice(0, -1), + { + ...lastMessage, + content: newContent, + timestamp: new Date(), + }, + ]; + }); + + resolve(); + }, + index === 0 && wordIndex === 0 ? 0 : randomWaitTime + ); + }); + + if (isLastWord && isLastMessage) { + setTimeout(() => { + setIsIaTyping(false); + }, 500); + } + + wordIndex++; + } + index++; + } + }, + [setMessages] + ); - if (points > 0) { - acc.push({ - chamado, - points, + const handleFakeApi = useCallback( + (question) => { + const questionTokens = question.trim().split(" "); + + if (question.toLowerCase().includes("obrigado")) { + const gratitudeResponses = [ + "De nada, estou aqui para ajudar!", + "Disponha, precisando é só chamar!", + "Estou aqui para o que precisar!", + "Estou sempre à disposição!", + "Estou aqui para ajudar, não hesite em me chamar!", + ]; + + handleSendIaMessages([ + { + content: + gratitudeResponses[ + Math.floor( + Math.random() * gratitudeResponses.length + ) + ], + author: "ia", + timestamp: new Date(), + }, + ]); + setIsIaTyping(false); + return true; + } + + if (questionTokens.length === 0 || questionTokens.length === 1) { + const giveMoreDetailsSamples = [ + "Você poderia elaborar melhor sua pergunta?", + "Estou com dificuldades de encontrar algo, poderia me dar mais detalhes?", + "Você poderia ser mais específico?", + "Poderia me explicar melhor?", + "Você pode me dar mais detalhes sobre seu problema?", + ]; + + handleSendIaMessages([ + { + content: + giveMoreDetailsSamples[ + Math.floor( + Math.random() * + giveMoreDetailsSamples.length + ) + ], + author: "ia", + timestamp: new Date(), + }, + ]); + setIsIaTyping(false); + return true; + } + + const chamadoPorNumero = Chamados.find((chamado) => + questionTokens.some( + (token) => + chamado["Número"].toLowerCase() === + token.replace("#", "").toLowerCase() + ) + ); + + const rankedChamados = chamadoPorNumero + ? [chamadoPorNumero] + : Chamados.reduce((acc, chamado) => { + const searchFields = [ + "Categoria Relatório", + "Subcategoria Relatório", + "Sintoma", + "Grupo de atribuição", + "Local", + "Descrição", + "Comentários Visiveis 1", + "Comentários Visiveis 2", + ]; + + let points = 0; + searchFields.forEach((field) => { + const hasMatches = questionTokens.some((token) => + chamado[field] + .toLowerCase() + .includes( + token.replace("#", "").toLowerCase() + ) + ); + + if (hasMatches) { + points++; + } }); - } - return acc; - }, []); + if (points > 0) { + acc.push({ + chamado, + points, + }); + } + + return acc; + }, []); - const sortedChamados = rankedChamados.sort( - (chamadoA, chamadoB) => chamadoB.points - chamadoA.points - ); + const sortedChamados = rankedChamados.sort( + (chamadoA, chamadoB) => chamadoB.points - chamadoA.points + ); - if (chamadoPorNumero) { - const chamado = chamadoPorNumero; - handleSendIaMessages([ - { - content: `Aqui está o chamado que você procura: + if (chamadoPorNumero) { + const chamado = chamadoPorNumero; + handleSendIaMessages([ + { + content: `Aqui está o chamado que você procura: #${chamado["Número"]} Categoria Relatório: ${chamado["Categoria Relatório"]} @@ -133,48 +250,66 @@ function Chat({ type }) { Descrição: ${chamado["Descrição"]} Comentários Visiveis 1: ${chamado["Comentários Visiveis 1"]} Comentários Visiveis 2: ${chamado["Comentários Visiveis 2"]}`, - }, - { - content: `Este chamado foi resolvido da seguinte maneira: + }, + { + content: `Este chamado foi resolvido da seguinte maneira: "${chamado["Resolução"]}"`, - }, - { - content: "Posso te ajudar com mais alguma coisa?", - }, - ]); - return true; - } + }, + { + content: "Posso te ajudar com mais alguma coisa?", + }, + ]); + return true; + } - if (sortedChamados.length > 0) { - let listOfChamados = sortedChamados - .slice(0, 5) - .reduce( - (acc, { chamado }) => - `${acc}\nChamado #${chamado["Número"]} - ${chamado["Descrição"]}`.trim(), - "" - ); - - if (sortedChamados.length > 5) { - listOfChamados += `\nE mais ${sortedChamados.length - 5}...`; + if (sortedChamados.length > 0) { + let listOfChamados = sortedChamados + .slice(0, 5) + .reduce( + (acc, { chamado }) => + `${acc}\nChamado #${chamado["Número"]} - ${chamado["Descrição"]}`.trim(), + "" + ); + + if (sortedChamados.length > 5) { + listOfChamados += `\nE mais ${ + sortedChamados.length - 5 + }...`; + } + + handleSendIaMessages([ + { + content: `Encontrei ${sortedChamados.length} chamados que podem te ajudar:`, + }, + { + content: listOfChamados, + }, + { + content: `Escolha um dos chamados acima para mais detalhes ou diga "todos" para listar os ${sortedChamados.length}.`, + }, + ]); + return true; } - handleSendIaMessages([ - { - content: `Encontrei ${sortedChamados.length} chamados que podem te ajudar:`, - }, - { - content: listOfChamados, - }, - { - content: `Escolha um dos chamados acima para mais detalhes ou diga "todos" para listar os ${sortedChamados.length}.`, - }, - ]); + setIsIaTyping(false); return true; - } + }, + [handleSendIaMessages] + ); - setIsIaTyping(false); - return true; - }; + const processQuestion = useCallback( + async (prompt) => { + if (useRealApi) { + const response = await handleOpenAiRequest(prompt); + await handleSendIaMessages([{ content: response }]); + setIsIaTyping(false); + return true; + } else { + return handleFakeApi(prompt); + } + }, + [handleFakeApi, handleSendIaMessages] + ); const welcomeMessages = useMemo( () => [ @@ -184,11 +319,7 @@ function Chat({ type }) { authedUser?.name || authedUser?.username }, eu sou Dex! Agente de suporte técnico de IA generativa projetado para auxiliar os times da Sofftek. - Por ainda ser prototipo, nossa interação vai ser mais restrita.`.trim(), - }, - { - content: - `Você pode descobrir um pouco mais sobre mim no documento abaixo, ou até mesmo assistir a nosso vídeo pitch na home ou aqui mesmo.`.trim(), + Você pode descobrir um pouco mais sobre mim no documento abaixo, ou até mesmo assistir a nosso vídeo pitch na home ou aqui mesmo.`.trim(), }, { content: "/documents/mercury-presentation.pdf", @@ -201,80 +332,13 @@ function Chat({ type }) { [authedUser] ); - const handleSendIaMessages = async (messages) => { - setIsIaTyping(true); - - let index = 0; - for await (const msg of messages) { - const isLastMessage = index === messages.length - 1; - const words = msg.content.split(" "); - - let wordIndex = 0; - - setMessages((prev) => { - return [ - ...prev, - { - ...msg, - content: "...", - timestamp: new Date(), - author: "ia", - }, - ]; - }); - - for await (const word of words) { - const isLastWord = wordIndex === words.length - 1; - - await new Promise((resolve) => { - const randomWaitTime = Math.floor(Math.random() * 100); - - setTimeout( - () => { - setMessages((prev) => { - const lastMessage = prev[prev.length - 1]; - const lastMessageContent = - lastMessage?.content || ""; - const newContent = - lastMessageContent === "..." - ? word - : `${lastMessageContent} ${word}`; - - return [ - ...prev.slice(0, -1), - { - ...lastMessage, - content: newContent, - timestamp: new Date(), - }, - ]; - }); - - resolve(); - }, - index === 0 && wordIndex === 0 ? 0 : randomWaitTime - ); - }); - - if (isLastWord && isLastMessage) { - setTimeout(() => { - setIsIaTyping(false); - }, 500); - } - - wordIndex++; - } - index++; - } - }; - useEffect(() => { if (isWelcomeSent.current || !authedUser) { return; } isWelcomeSent.current = true; handleSendIaMessages(welcomeMessages); - }, [welcomeMessages, authedUser]); + }, [welcomeMessages, authedUser, handleSendIaMessages]); useEffect(() => { const iaPossibleMessages = [ @@ -314,7 +378,14 @@ function Chat({ type }) { behavior: "smooth", }); } - }, [isIaTyping, messages, isWelcomeSent, welcomeMessages]); + }, [ + isIaTyping, + messages, + isWelcomeSent, + welcomeMessages, + processQuestion, + handleSendIaMessages, + ]); if (type === "widget" && location.pathname === "/chat") { return null; @@ -325,12 +396,14 @@ function Chat({ type }) { messages={messages} onNewMessage={setMessages} isIaTyping={isIaTyping} + chatHistory={chatHistory} /> ) : ( ); } diff --git a/src/pages/Chat/FullPageChat/FullPageChat.jsx b/src/pages/Chat/FullPageChat/FullPageChat.jsx index 418c9d5..7d1e647 100644 --- a/src/pages/Chat/FullPageChat/FullPageChat.jsx +++ b/src/pages/Chat/FullPageChat/FullPageChat.jsx @@ -1,9 +1,14 @@ /* eslint-disable react/prop-types */ +import { useLocation, useNavigate } from "react-router-dom"; import "../../../style/Chat/FullPageChat/FullPageChat.css"; import { useEffect, useRef, useState } from "react"; +import { getRandomChatId } from "../Chat"; export function FullPageChat(props) { - const { messages, onNewMessage, isIaTyping } = props; + const { messages, onNewMessage, isIaTyping, chatHistory } = props; + + const navigate = useNavigate(); + const location = useLocation(); const chatBodyRef = useRef(); const [messageInput, setMessageInput] = useState(""); @@ -18,97 +23,123 @@ export function FullPageChat(props) { }, [messages]); return ( -
-
- {messages.map((message) => ( -
+
+
+

Histórico de suporte

+ +
    + {Object.entries(chatHistory).map(([chatId, history]) => ( +
  • { + navigate(`/chat?chatId=${chatId}`); + }} + > + {history[0]?.content || "-"} +
  • + ))} +
+
+
+
+ {messages.map((message) => (
- {message.type === "pdf" ? ( - -

- Casso o PDF não carregue, utilize o link - abaixo:
- - {location.origin + - "/documents/mercury-presentation.pdf"} - -

-
- ) : ( - {message.content} - )} +
+ {message.type === "pdf" ? ( + +

+ Casso o PDF não carregue, utilize o + link abaixo:
+ + {location.origin + + "/documents/mercury-presentation.pdf"} + +

+
+ ) : ( + {message.content} + )} +
+ + {message.timestamp.toLocaleDateString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + })} +
- - {message.timestamp.toLocaleDateString("pt-BR", { - hour: "2-digit", - minute: "2-digit", - })} - -
- ))} - {isIaTyping && ( -
-
- ... + ))} + {isIaTyping && ( +
+
+ ... +
+ )} +
+
{ + e.preventDefault(); + if (!e.target.message.value?.trim() || isIaTyping) { + return; + } + + onNewMessage((prev) => [ + ...prev, + { + author: "user", + content: messageInput, + timestamp: new Date(), + }, + ]); + setMessageInput(""); + }} + > +
+ setMessageInput(e.target.value)} + /> +
- )} +
-
{ - e.preventDefault(); - if (!e.target.message.value?.trim() || isIaTyping) { - return; - } - - onNewMessage((prev) => [ - ...prev, - { - author: "user", - content: messageInput, - timestamp: new Date(), - }, - ]); - setMessageInput(""); - }} - > -
- setMessageInput(e.target.value)} - /> - -
-
); } diff --git a/src/pages/Chat/openai.js b/src/pages/Chat/openai.js new file mode 100644 index 0000000..c58fea2 --- /dev/null +++ b/src/pages/Chat/openai.js @@ -0,0 +1,16 @@ +export const handleOpenAiRequest = async (prompt) => { + if (prompt == "" || prompt == null) return; + let mensagem = prompt; + prompt = ""; + + // Envia requisição com a mensagem para a API do ChatBot + const resposta = await fetch("http://127.0.0.1:3000/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ msg: mensagem }), + }); + const textoDaResposta = await resposta.text(); + return textoDaResposta; +}; diff --git a/src/style/Chat/FullPageChat/FullPageChat.css b/src/style/Chat/FullPageChat/FullPageChat.css index a5270d6..8a80044 100644 --- a/src/style/Chat/FullPageChat/FullPageChat.css +++ b/src/style/Chat/FullPageChat/FullPageChat.css @@ -1,85 +1,129 @@ -.chat-wrapper:not(.widget) { +.container { display: flex; - flex-direction: column; + flex-direction: row; width: 100%; height: calc(90vh - 2rem); margin: auto; - & .chat-body { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; + .chat-history { + background-color: #2c2c2c; + color: white; padding: 1rem; - margin-bottom: 1rem; + max-width: 20%; border-radius: 0.5rem; - overflow-y: auto; - background-color: #2c2c2c; - } - - & input.form-control { - background-color: #2c2c2c; - color: #fff; + text-align: center; - &::placeholder { - color: rgba(255, 255, 255, 0.75); + .btn { + margin: 1rem 0; } - &::-ms-input-placeholder { - color: rgba(255, 255, 255, 0.75); + ul { + list-style-type: none; + padding: 0; + margin: 0; + overflow-y: auto; + height: 100%; + text-align: left; + + li { + padding: 0.5rem; + margin-bottom: 0.5rem; + border-radius: 0.5rem; + background-color: #1d1d1d; + cursor: pointer; + max-height: 4rem; + overflow: hidden; + text-overflow: ellipsis; + text-wrap: wrap; + + &:hover { + background-color: #131313; + } + } } } - & .message-wrapper { + .chat-wrapper:not(.widget) { display: flex; flex-direction: column; - width: auto; - margin-bottom: 0.75rem; - align-self: flex-end; - max-width: 75%; - - &.pdf { - min-width: 100%; + width: 100%; - & .message-body { - min-height: 400px; - } + & .chat-body { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + flex: 1; + padding: 1rem; + margin-bottom: 1rem; + border-radius: 0.5rem; + overflow-y: auto; + background-color: #2c2c2c; } - & .message-body { - padding: 0.75rem; - margin-bottom: 0.25rem; - border-radius: 0.25rem; - - background-color: #1d1d1d; + & input.form-control { + background-color: #2c2c2c; color: #fff; - box-shadow: 0px 8px 5px 0.1rem rgba(0, 0, 0, 0.15); - & span { - white-space: pre-line; + &::placeholder { + color: rgba(255, 255, 255, 0.75); + } + + &::-ms-input-placeholder { + color: rgba(255, 255, 255, 0.75); } } - &.ia { - align-self: flex-start; + & .message-wrapper { + display: flex; + flex-direction: column; + width: auto; + margin-bottom: 0.75rem; + align-self: flex-end; + max-width: 75%; + + &.pdf { + min-width: 100%; + + & .message-body { + min-height: 400px; + } + } & .message-body { + padding: 0.75rem; + margin-bottom: 0.25rem; + border-radius: 0.5rem; + + background-color: #1d1d1d; color: #fff; - background-color: rgba(113, 59, 136, 1); - background: linear-gradient( - 90deg, - rgba(113, 59, 136, 1) 0%, - rgba(167, 46, 87, 1) 50%, - rgba(213, 25, 47, 1) 100% - ); + box-shadow: 0px 8px 5px 0.1rem rgba(0, 0, 0, 0.15); + + & span { + white-space: pre-line; + } } - } - & .secondary-data { - align-self: flex-end; - font-size: 70%; - color: #efefef; + &.ia { + align-self: flex-start; + + & .message-body { + color: #fff; + background-color: rgba(113, 59, 136, 1); + background: linear-gradient( + 90deg, + rgba(113, 59, 136, 1) 0%, + rgba(167, 46, 87, 1) 50%, + rgba(213, 25, 47, 1) 100% + ); + } + } + + & .secondary-data { + align-self: flex-end; + font-size: 70%; + color: #efefef; + } } } }