Просмотр исходного кода

feat(frontend): typing chat feature | for #187 (#353)

* feat(frontend): typing chat

* fix linting

* typo corrections - ChatInterface.tsx

typo corrections in comments

* refactor and fix typing
808vita 1 год назад
Родитель
Сommit
a00f604995

+ 34 - 7
frontend/src/components/ChatInterface.tsx

@@ -6,6 +6,31 @@ import userAvatar from "../assets/user-avatar.png";
 import { sendChatMessage } from "../services/chatService";
 import { RootState } from "../store";
 import CogTooth from "../assets/cog-tooth";
+import { useTypingEffect } from "../hooks/useTypingEffect";
+import { Message } from "../state/chatSlice";
+
+interface ITypingChatProps {
+  msg: Message;
+}
+
+/**
+ * @param msg
+ * @returns jsx
+ *
+ * component used for typing effect when assistant replies
+ *
+ * makes uses of useTypingEffect hook
+ *
+ */
+function TypingChat({ msg }: ITypingChatProps) {
+  return (
+    msg?.content && (
+      <Card>
+        <CardBody>{useTypingEffect([msg?.content], { loop: false })}</CardBody>
+      </Card>
+    )
+  );
+}
 
 function MessageList(): JSX.Element {
   const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -18,20 +43,22 @@ function MessageList(): JSX.Element {
   return (
     <div className="flex-1 overflow-y-auto">
       {messages.map((msg, index) => (
-        <div key={index} className="flex mb-2.5">
+        <div key={index} className="flex mb-2.5 pr-5 pl-5">
           <div
-            className={`${msg.sender === "user" ? "flex flex-row-reverse mt-2.5 mr-2.5 mb-0 ml-auto" : "flex"}`}
+            className={`flex mt-2.5 mb-0 ${msg.sender === "user" && "flex-row-reverse ml-auto"}`}
           >
             <img
               src={msg.sender === "user" ? userAvatar : assistantAvatar}
               alt={`${msg.sender} avatar`}
               className="w-[40px] h-[40px] mx-2.5"
             />
-            <Card
-              className={`w-4/5 ${msg.sender === "user" ? "bg-primary" : ""}`}
-            >
-              <CardBody>{msg.content}</CardBody>
-            </Card>
+            {msg.sender !== "user" ? (
+              <TypingChat msg={msg} />
+            ) : (
+              <Card className="bg-primary">
+                <CardBody>{msg.content}</CardBody>
+              </Card>
+            )}
           </div>
         </div>
       ))}

+ 59 - 0
frontend/src/hooks/useTypingEffect.ts

@@ -0,0 +1,59 @@
+import { useEffect, useState } from "react";
+/**
+ * hook to be used for typing chat effect
+ */
+export const useTypingEffect = (
+  strings: string[] = [""],
+  {
+    loop = false,
+    playbackRate = 0.1,
+  }: { loop?: boolean; playbackRate?: number } = {
+    loop: false,
+    playbackRate: 0.1,
+  },
+) => {
+  // eslint-disable-next-line prefer-const
+  let [{ stringIndex, characterIndex }, setState] = useState<{
+    stringIndex: number;
+    characterIndex: number;
+  }>({
+    stringIndex: 0,
+    characterIndex: 0,
+  });
+
+  let timeoutId: number;
+  const emulateKeyStroke = () => {
+    // eslint-disable-next-line no-plusplus
+    characterIndex++;
+    if (characterIndex === strings[stringIndex].length) {
+      characterIndex = 0;
+      // eslint-disable-next-line no-plusplus
+      stringIndex++;
+      if (stringIndex === strings.length) {
+        if (!loop) {
+          return;
+        }
+        stringIndex = 0;
+      }
+      timeoutId = window.setTimeout(emulateKeyStroke, 100 * playbackRate);
+    } else if (characterIndex === strings[stringIndex].length - 1) {
+      timeoutId = window.setTimeout(emulateKeyStroke, 2000 * playbackRate);
+    } else {
+      timeoutId = window.setTimeout(emulateKeyStroke, 100 * playbackRate);
+    }
+    setState({
+      characterIndex,
+      stringIndex,
+    });
+  };
+
+  useEffect(() => {
+    emulateKeyStroke();
+    return () => {
+      window.clearTimeout(timeoutId);
+    };
+  }, []);
+
+  const nonBreakingSpace = "\u00A0";
+  return strings[stringIndex].slice(0, characterIndex + 1) || nonBreakingSpace;
+};

+ 1 - 1
frontend/src/state/chatSlice.ts

@@ -1,6 +1,6 @@
 import { createSlice } from "@reduxjs/toolkit";
 
-type Message = {
+export type Message = {
   content: string;
   sender: "user" | "assistant";
 };