ChatInterface.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { Card, CardBody } from "@nextui-org/react";
  2. import React, { useEffect, useRef } from "react";
  3. import { useSelector } from "react-redux";
  4. import { useTranslation } from "react-i18next";
  5. import assistantAvatar from "../assets/assistant-avatar.png";
  6. import CogTooth from "../assets/cog-tooth";
  7. import userAvatar from "../assets/user-avatar.png";
  8. import { useTypingEffect } from "../hooks/useTypingEffect";
  9. import {
  10. setCurrentQueueMarkerState,
  11. setCurrentTypingMsgState,
  12. setTypingAcitve,
  13. addAssistantMessageToChat,
  14. } from "../services/chatService";
  15. import { RootState } from "../store";
  16. import { Message } from "../state/chatSlice";
  17. import Input from "./Input";
  18. import { I18nKey } from "../i18n/declaration";
  19. interface IChatBubbleProps {
  20. msg: Message;
  21. }
  22. /**
  23. * @returns jsx
  24. *
  25. * component used for typing effect when assistant replies
  26. *
  27. * makes uses of useTypingEffect hook
  28. *
  29. */
  30. function TypingChat() {
  31. const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
  32. useSelector((state: RootState) => state.chat);
  33. const messageContent = useTypingEffect([currentTypingMessage], {
  34. loop: false,
  35. setTypingAcitve,
  36. setCurrentQueueMarkerState,
  37. currentQueueMarker,
  38. playbackRate: 0.1,
  39. addAssistantMessageToChat,
  40. assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
  41. });
  42. return (
  43. currentQueueMarker !== null && (
  44. <Card className="bg-success-100">
  45. <CardBody>{messageContent}</CardBody>
  46. </Card>
  47. )
  48. );
  49. }
  50. function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
  51. return (
  52. <div className="flex mb-2.5 pr-5 pl-5">
  53. <div
  54. className={`flex mt-2.5 mb-0 min-w-0 ${msg?.sender === "user" && "flex-row-reverse ml-auto"}`}
  55. >
  56. <img
  57. src={msg?.sender === "user" ? userAvatar : assistantAvatar}
  58. alt={`${msg?.sender} avatar`}
  59. className="w-[40px] h-[40px] mx-2.5"
  60. />
  61. <Card className={`${msg?.sender === "user" ? "bg-primary-100" : ""}`}>
  62. <CardBody>{msg?.content}</CardBody>
  63. </Card>
  64. </div>
  65. </div>
  66. );
  67. }
  68. function MessageList(): JSX.Element {
  69. const messagesEndRef = useRef<HTMLDivElement>(null);
  70. const {
  71. messages,
  72. queuedTyping,
  73. typingActive,
  74. currentQueueMarker,
  75. currentTypingMessage,
  76. newChatSequence,
  77. } = useSelector((state: RootState) => state.chat);
  78. const messageScroll = () => {
  79. messagesEndRef.current?.scrollIntoView({
  80. behavior: "smooth",
  81. block: "end",
  82. });
  83. };
  84. useEffect(() => {
  85. messageScroll();
  86. if (!typingActive) return;
  87. const interval = setInterval(() => {
  88. messageScroll();
  89. }, 1000);
  90. // eslint-disable-next-line consistent-return
  91. return () => clearInterval(interval);
  92. }, [newChatSequence, typingActive]);
  93. useEffect(() => {
  94. const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
  95. if (
  96. currentQueueMarker !== null &&
  97. currentQueueMarker !== 0 &&
  98. currentTypingMessage !== newMessage
  99. ) {
  100. setCurrentTypingMsgState(
  101. messages?.[queuedTyping?.[currentQueueMarker]]?.content,
  102. );
  103. }
  104. }, [queuedTyping]);
  105. useEffect(() => {
  106. if (currentTypingMessage === "") return;
  107. if (!typingActive) setTypingAcitve(true);
  108. }, [currentTypingMessage]);
  109. useEffect(() => {
  110. const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
  111. if (
  112. newMessage &&
  113. typingActive === false &&
  114. currentTypingMessage !== newMessage
  115. ) {
  116. if (currentQueueMarker !== 0) {
  117. setCurrentTypingMsgState(
  118. messages?.[queuedTyping?.[currentQueueMarker]]?.content,
  119. );
  120. }
  121. }
  122. }, [typingActive]);
  123. useEffect(() => {
  124. if (currentQueueMarker === 0) {
  125. setCurrentTypingMsgState(messages?.[queuedTyping?.[0]]?.content);
  126. }
  127. }, [currentQueueMarker]);
  128. return (
  129. <div className="flex-1 overflow-y-auto">
  130. {newChatSequence.map((msg, index) =>
  131. // eslint-disable-next-line no-nested-ternary
  132. msg.sender === "user" || msg.sender === "assistant" ? (
  133. <ChatBubble key={index} msg={msg} />
  134. ) : (
  135. <div key={index} />
  136. ),
  137. )}
  138. {typingActive && (
  139. <div className="flex mb-2.5 pr-5 pl-5 bg-s">
  140. <div className="flex mt-2.5 mb-0 min-w-0 ">
  141. <img
  142. src={assistantAvatar}
  143. alt="assistant avatar"
  144. className="w-[40px] h-[40px] mx-2.5"
  145. />
  146. <TypingChat />
  147. </div>
  148. </div>
  149. )}
  150. <div ref={messagesEndRef} />
  151. </div>
  152. );
  153. }
  154. function InitializingStatus(): JSX.Element {
  155. const { t } = useTranslation();
  156. return (
  157. <div className="flex items-center m-auto h-full">
  158. <img
  159. src={assistantAvatar}
  160. alt="assistant avatar"
  161. className="w-[40px] h-[40px] mx-2.5"
  162. />
  163. <div>{t(I18nKey.CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE)}</div>
  164. </div>
  165. );
  166. }
  167. interface Props {
  168. setSettingOpen: (isOpen: boolean) => void;
  169. }
  170. function ChatInterface({ setSettingOpen }: Props): JSX.Element {
  171. const { initialized } = useSelector((state: RootState) => state.task);
  172. return (
  173. <div className="flex flex-col h-full p-0 bg-bg-light">
  174. <div className="w-full flex justify-between p-5">
  175. <div />
  176. <div
  177. className="cursor-pointer hover:opacity-80"
  178. onClick={() => setSettingOpen(true)}
  179. >
  180. <CogTooth />
  181. </div>
  182. </div>
  183. {initialized ? <MessageList /> : <InitializingStatus />}
  184. <Input />
  185. </div>
  186. );
  187. }
  188. export default ChatInterface;