瀏覽代碼

fix frontend : highlight active assistant text and maintain chat seq | for #476 (#598)

* highlight active assistant text and maintain seq

* adjust speed

* alter autoscrolll
808vita 1 年之前
父節點
當前提交
398749fcd8

+ 125 - 26
frontend/src/components/ChatInterface.tsx

@@ -5,16 +5,21 @@ import assistantAvatar from "../assets/assistant-avatar.png";
 import CogTooth from "../assets/cog-tooth";
 import userAvatar from "../assets/user-avatar.png";
 import { useTypingEffect } from "../hooks/useTypingEffect";
-import { sendChatMessage } from "../services/chatService";
-import { Message } from "../state/chatSlice";
+import {
+  sendChatMessage,
+  setCurrentQueueMarkerState,
+  setCurrentTypingMsgState,
+  setTypingAcitve,
+  addAssistanctMessageToChat,
+} from "../services/chatService";
 import { RootState } from "../store";
+import { Message } from "../state/chatSlice";
 
-interface ITypingChatProps {
+interface IChatBubbleProps {
   msg: Message;
 }
 
 /**
- * @param msg
  * @returns jsx
  *
  * component used for typing effect when assistant replies
@@ -22,14 +27,25 @@ interface ITypingChatProps {
  * makes uses of useTypingEffect hook
  *
  */
-function TypingChat({ msg }: ITypingChatProps) {
+function TypingChat() {
+  const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
+    useSelector((state: RootState) => state.chat);
+
   return (
     // eslint-disable-next-line react/jsx-no-useless-fragment
     <>
-      {msg?.content && (
-        <Card>
+      {currentQueueMarker !== null && (
+        <Card className="bg-success-100">
           <CardBody>
-            {useTypingEffect([msg?.content], { loop: false })}
+            {useTypingEffect([currentTypingMessage], {
+              loop: false,
+              setTypingAcitve,
+              setCurrentQueueMarkerState,
+              currentQueueMarker,
+              playbackRate: 0.1,
+              addAssistanctMessageToChat,
+              assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
+            })}
           </CardBody>
         </Card>
       )}
@@ -37,36 +53,119 @@ function TypingChat({ msg }: ITypingChatProps) {
   );
 }
 
+function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {
+  return (
+    <div className="flex mb-2.5 pr-5 pl-5">
+      <div
+        className={`flex mt-2.5 mb-0 min-w-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={`${msg?.sender === "user" ? "bg-primary-100" : ""}`}>
+          <CardBody>{msg?.content}</CardBody>
+        </Card>
+      </div>
+    </div>
+  );
+}
+
 function MessageList(): JSX.Element {
   const messagesEndRef = useRef<HTMLDivElement>(null);
-  const { messages } = useSelector((state: RootState) => state.chat);
+  const {
+    messages,
+    queuedTyping,
+    typingActive,
+    currentQueueMarker,
+    currentTypingMessage,
+    newChatSequence,
+  } = useSelector((state: RootState) => state.chat);
+
+  const messageScroll = () => {
+    messagesEndRef.current?.scrollIntoView({
+      behavior: "smooth",
+      block: "end",
+    });
+  };
 
   useEffect(() => {
-    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
-  }, [messages]);
+    messageScroll();
+    if (!typingActive) return;
+
+    const interval = setInterval(() => {
+      messageScroll();
+    }, 1000);
+
+    // eslint-disable-next-line consistent-return
+    return () => clearInterval(interval);
+  }, [newChatSequence, typingActive]);
+
+  useEffect(() => {
+    const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
+
+    if (
+      currentQueueMarker !== null &&
+      currentQueueMarker !== 0 &&
+      currentTypingMessage !== newMessage
+    ) {
+      setCurrentTypingMsgState(
+        messages?.[queuedTyping?.[currentQueueMarker]]?.content,
+      );
+    }
+  }, [queuedTyping]);
+
+  useEffect(() => {
+    if (currentTypingMessage === "") return;
+
+    if (!typingActive) setTypingAcitve(true);
+  }, [currentTypingMessage]);
+
+  useEffect(() => {
+    const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
+    if (
+      newMessage &&
+      typingActive === false &&
+      currentTypingMessage !== newMessage
+    ) {
+      if (currentQueueMarker !== 0) {
+        setCurrentTypingMsgState(
+          messages?.[queuedTyping?.[currentQueueMarker]]?.content,
+        );
+      }
+    }
+  }, [typingActive]);
+
+  useEffect(() => {
+    if (currentQueueMarker === 0) {
+      setCurrentTypingMsgState(messages?.[queuedTyping?.[0]]?.content);
+    }
+  }, [currentQueueMarker]);
 
   return (
     <div className="flex-1 overflow-y-auto">
-      {messages.map((msg, index) => (
-        <div key={index} className="flex mb-2.5 pr-5 pl-5">
-          <div
-            className={`flex mt-2.5 mb-0 min-w-0 ${msg.sender === "user" && "flex-row-reverse ml-auto"}`}
-          >
+      {newChatSequence.map((msg, index) =>
+        // eslint-disable-next-line no-nested-ternary
+        msg.sender === "user" || msg.sender === "assistant" ? (
+          <ChatBubble key={index} msg={msg} />
+        ) : (
+          <div key={index} />
+        ),
+      )}
+
+      {typingActive && (
+        <div className="flex mb-2.5 pr-5 pl-5 bg-s">
+          <div className="flex mt-2.5 mb-0 min-w-0 ">
             <img
-              src={msg.sender === "user" ? userAvatar : assistantAvatar}
-              alt={`${msg.sender} avatar`}
+              src={assistantAvatar}
+              alt="assistant avatar"
               className="w-[40px] h-[40px] mx-2.5"
             />
-            {msg.sender !== "user" ? (
-              <TypingChat msg={msg} />
-            ) : (
-              <Card className="bg-primary">
-                <CardBody>{msg.content}</CardBody>
-              </Card>
-            )}
+            <TypingChat />
           </div>
         </div>
-      ))}
+      )}
       <div ref={messagesEndRef} />
     </div>
   );

+ 22 - 1
frontend/src/hooks/useTypingEffect.ts

@@ -1,4 +1,5 @@
 import { useEffect, useState } from "react";
+import { Message } from "../state/chatSlice";
 /**
  * hook to be used for typing chat effect
  */
@@ -7,9 +8,26 @@ export const useTypingEffect = (
   {
     loop = false,
     playbackRate = 0.1,
-  }: { loop?: boolean; playbackRate?: number } = {
+    setTypingAcitve = () => {},
+    setCurrentQueueMarkerState = () => {},
+    currentQueueMarker = 0,
+    addAssistanctMessageToChat = () => {},
+    assistantMessageObj = { content: "", sender: "assistant" },
+  }: {
+    loop?: boolean;
+    playbackRate?: number;
+    setTypingAcitve?: (bool: boolean) => void;
+    setCurrentQueueMarkerState?: (marker: number) => void;
+    currentQueueMarker?: number;
+    addAssistanctMessageToChat?: (msg: Message) => void;
+    assistantMessageObj?: Message;
+  } = {
     loop: false,
     playbackRate: 0.1,
+    setTypingAcitve: () => {},
+    currentQueueMarker: 0,
+    addAssistanctMessageToChat: () => {},
+    assistantMessageObj: { content: "", sender: "assistant" },
   },
 ) => {
   // eslint-disable-next-line prefer-const
@@ -31,6 +49,9 @@ export const useTypingEffect = (
       stringIndex++;
       if (stringIndex === strings.length) {
         if (!loop) {
+          setTypingAcitve(false);
+          setCurrentQueueMarkerState(currentQueueMarker + 1);
+          addAssistanctMessageToChat(assistantMessageObj);
           return;
         }
         stringIndex = 0;

+ 27 - 1
frontend/src/services/chatService.ts

@@ -1,4 +1,12 @@
-import { appendUserMessage } from "../state/chatSlice";
+import {
+  Message,
+  appeendToNewChatSequence,
+  appendUserMessage,
+  emptyOutQueuedTyping,
+  setCurrentQueueMarker,
+  setCurrentTypingMessage,
+  toggleTypingActive,
+} from "../state/chatSlice";
 import socket from "../socket/socket";
 import store from "../store";
 
@@ -8,3 +16,21 @@ export function sendChatMessage(message: string): void {
   const eventString = JSON.stringify(event);
   socket.send(eventString);
 }
+
+export function setTypingAcitve(bool: boolean): void {
+  store.dispatch(toggleTypingActive(bool));
+}
+
+export function resetQueuedTyping(): void {
+  store.dispatch(emptyOutQueuedTyping());
+}
+
+export function setCurrentTypingMsgState(msg: string): void {
+  store.dispatch(setCurrentTypingMessage(msg));
+}
+export function setCurrentQueueMarkerState(index: number): void {
+  store.dispatch(setCurrentQueueMarker(index));
+}
+export function addAssistanctMessageToChat(msg: Message): void {
+  store.dispatch(appeendToNewChatSequence(msg));
+}

+ 43 - 2
frontend/src/state/chatSlice.ts

@@ -6,22 +6,63 @@ export type Message = {
 };
 
 const initialMessages: Message[] = [];
-
+const queuedMessages: number[] = [];
+const currentQueueMarker: number = 0;
 export const chatSlice = createSlice({
   name: "chat",
   initialState: {
     messages: initialMessages,
+    queuedTyping: queuedMessages,
+    typingActive: false,
+    currentTypingMessage: "",
+    currentQueueMarker,
+    userMessages: initialMessages,
+    assistantMessages: initialMessages,
+    newChatSequence: initialMessages,
   },
   reducers: {
     appendUserMessage: (state, action) => {
       state.messages.push({ content: action.payload, sender: "user" });
+      state.userMessages.push({ content: action.payload, sender: "user" });
+      state.newChatSequence.push({ content: action.payload, sender: "user" });
     },
     appendAssistantMessage: (state, action) => {
       state.messages.push({ content: action.payload, sender: "assistant" });
+      state.assistantMessages.push({
+        content: action.payload,
+        sender: "assistant",
+      });
+      // state.queuedTyping.push(action.payload);
+      const assistantMessageIndex = state.messages.length - 1;
+      state.queuedTyping.push(assistantMessageIndex);
+    },
+    setCurrentQueueMarker: (state, action) => {
+      state.currentQueueMarker = action.payload;
+    },
+    toggleTypingActive: (state, action) => {
+      state.typingActive = action.payload;
+    },
+    emptyOutQueuedTyping: (state) => {
+      state.queuedTyping = [];
+    },
+    setCurrentTypingMessage: (state, action) => {
+      state.currentTypingMessage = action.payload;
+      // state.currentQueueMarker += 1;
+    },
+    appeendToNewChatSequence: (state, action) => {
+      state.newChatSequence.push(action.payload);
     },
   },
 });
 
-export const { appendUserMessage, appendAssistantMessage } = chatSlice.actions;
+export const {
+  appendUserMessage,
+  appendAssistantMessage,
+  toggleTypingActive,
+  emptyOutQueuedTyping,
+  setCurrentTypingMessage,
+  setCurrentQueueMarker,
+  appeendToNewChatSequence,
+} = chatSlice.actions;
 
 export default chatSlice.reducer;