Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react-chatbotify with Signal R #300

Open
div3791 opened this issue Mar 2, 2025 · 24 comments
Open

react-chatbotify with Signal R #300

div3791 opened this issue Mar 2, 2025 · 24 comments
Assignees
Labels
help wanted Extra attention is needed

Comments

@div3791
Copy link

div3791 commented Mar 2, 2025

Help Description:
I want to integrate Signal R for real time messaging with react-chatbotify. I also want to show typing indicator immedietly after user sends a message and typing indicator should be displayed untill any message doesnt come through Signal R event.

Library version:
2.0.0-beta.28.

Additional context:
I am using Vite + React + Signal R (Through Background Service Worker). When user opens chat window for first time, it should show welcome message. and after user sends first message, REST API is being called to send message to server and my chatbot is waiting for response from server through Signal R. I am using background service worker to manage signal connections and events

@div3791 div3791 added the help wanted Extra attention is needed label Mar 2, 2025
@div3791 div3791 changed the title [Help] react-chatbotify with Signal R Mar 2, 2025
@tjtanjin
Copy link
Owner

tjtanjin commented Mar 2, 2025

Hey @div3791! To preface, real-time messaging requires significant custom code currently. This is because there are inherent assumptions made within the core library (such as turn-based conversations) that must be addressed for live chat to work.

There are plans to make a live chat plugin, but the plugins for LLM are taking priority at the moment so until those are completed, live chat plugin is still a while away.

That said, you can go ahead and implement live chat by adopting the following approach. Note that I have yet to properly validate this approach but it is what comes to mind (my intention was to validate it when I start work on the live chat plugin but here goes):

  • Listen to change path event and detect when a path enters the block responsible for live chat
  • Within this block, all change path events have to be caught and prevented to avoid leaving the block (which includes blocking looping the block itself)
  • Listen to user message event to send the message to the backend for a response
  • When backend sends a message, params.injectMessage or params.streamMessage it into the frontend
  • Typing indicator will have to be done through a custom component that is injected and removed when required (core library cannot do it for you because it has no idea when your backend is done sending messages)

Overall a significant amount of code additions but what I've shared is the tentative approach I intend to use for the live chat plugin as well. Things may and will likely change when I get down to actual implementation but this is currently it. If this is urgent for you and you'd like to hack away at it, feel free to reach out on discord. I don't mind exploring solutions with you since this is a task I'll get to eventually as well :) Though note that even if we bounce ideas currently, you'll have to do most of the coding and testing because my hands are tied on other areas of work such as the LLM plugins 🥹

@div3791
Copy link
Author

div3791 commented Mar 2, 2025

Thanks a lot @tjtanjin for prompt reply!!!!!
Could you please give small example for the suggested approach? It would be a big favor from you.

When I tried to add custom component in injectMessage( it added to the chat window. but when I received signal r message, and tried to delete last message which is typing indicator component by message id it doesnt remove the typing indicator. I tried to check local storage where all the chats are being stored, Some times I can see typing indicator is not geeting deleted. and some times I got different message Id when I saved result of awat injectMessage(<TypingIndicatorCustomComponent/>) than saved message id. so may be that is causing not removal of typing indicator

let id = await injectMessage(<TypingIndicatorComponent />);
        console.log('Submitted Saved Indicator Id: ', id);
        setTypingIndicatorMessageId(id!);

Removal logic of typing indicator written in different file

const {messages, removeMessage, injectMessage} = useMessages();
let indicatorMessageItem = messages.findIndex(item=>item.content.toString().includes('<div'));
if (indicatorMessageItem !== -1) { 
          let removedMessageId = await removeMessage(messages[indicatorMessageItem].id);
          console.log(removedMessageId);
}

As a thanks and gratitude gesture, I will contribute to make this library more robust which can support signal r and other sockets once my current project is completed!!!

Thanks & Best Regards,
Divyesh Shani

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 2, 2025

Hey @div3791, unfortunately I'm overseas at the moment and don't have my PC/laptop with me 🥹 I'll only be back in the middle of march so until then, I don't have a conducive setup for development-related work.

With that said, I can provide you with a couple of links I believe will come in useful:

  • Plugin example - plugins follow the same pattern of listening for events and responding to it with chatbot interactions via hooks
  • Change Path Event - when a new path is entered, you probably want to check if this is the path you want to handle live chat within
  • Pre Inject Message Event - within the live chat path, you want to listen on all pre inject message events (from users) and have them sent to your backend server or do whatever additional processing you need
  • Params - you're already using this, so I trust you know what's useful here

From the brief implementation you shared above, I suggest you store the id of the typing indicator and pass it directly into removeMessage instead of pulling the item out (is there a reason you're doing this?)

Appreciate your interest to help, happy to have you around as work for the live chat plugin kicks in 😊

@div3791
Copy link
Author

div3791 commented Mar 2, 2025

Thanks @tjtanjin I will go through the links given by you and will back to you!!

@div3791
Copy link
Author

div3791 commented Mar 2, 2025

const { injectMessage, removeMessage } = useMessages();
let removedMessageId = await removeMessage(typingId);

here typingId is string message id.

This is not removing message from chat window as well as local storage @tjtanjin

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 3, 2025

const { injectMessage, removeMessage } = useMessages();
let removedMessageId = await removeMessage(typingId);

here typingId is string message id.

This is not removing message from chat window as well as local storage @tjtanjin

Have you checked if your id passed in is actually correct?

Here's what works on the playground (formatting might be off, was trying it from my mobile):

const MyChatBotWrapper = () => {
    const { toggleAudio } = useAudio();
    const { restartFlow } = useFlow();
    const { showToast } = useToasts();
    const { playNotificationSound } = useNotifications();
    const { injectMessage, removeMessage } = useMessages();
    const idRef = React.useRef(null);

    const settings = {
        general: {embedded: true},
        chatHistory: {storageKey: "example_custom_hooks"},
        audio: {disabled: false}
    }

    const flow={
        start: {
            message: "Welcome to the playground 🥳! Edit and experiment as you wish!",
            path: "end_loop"
        },
        end_loop: {
            message: (params) => `Received: ${params.userInput}`,
            path: "end_loop"
        }
    }

    return (
        <>
          <ExampleButton onClick={toggleAudio} text="Click me to toggle audio!"/>
          <ExampleButton onClick={restartFlow} text="Click me to reset the flow!"/>
          <ExampleButton onClick={() => showToast("Hello there!")} text="Click me to show a toast!"/>
          <ExampleButton onClick={playNotificationSound} text="Click me to play a notification sound!"/>
          <ExampleButton onClick={async () => idRef.current = await injectMessage("Hello I'm an injected message!")} text="Click me to inject a message!"/>
          <ExampleButton onClick={() => removeMessage(idRef.current)} text="Click me to remove a message!"/>
          <ChatBot settings={settings} flow={flow}/>
        </>
    );
};

const MyChatBotProvider = () => {
    return (
        <ChatBotProvider>
            <MyChatBotWrapper/>
        </ChatBotProvider>
    );
};

// button to test above feature
const exampleButtonStyle = {
    backgroundColor: '#491D8D',
    color: 'white',
    border: 'none',
    padding: '10px 20px',
    textAlign: 'center',
    textDecoration: 'none',
    display: 'inline-block',
    fontSize: '16px',
    borderRadius: '5px',
    cursor: 'pointer',
    transition: 'background-color 0.2s',
    margin: 10,
};
const ExampleButton = (props) => {
    return (
        <button onClick={props.onClick} style={exampleButtonStyle}>{props.text}</button>
    );
};

render(<MyChatBotProvider/>)

@div3791
Copy link
Author

div3791 commented Mar 4, 2025

Yes @tjtanjin I re-verified messageId its exactly same which is there in local storage where react-chatbotify stores messages.
the thing is awat removeMessage(messageId) or removeMessage(messageId) is not removing the message from chat window as well as from local storage.

I tried with setTimeout also but didnt worked Sir.

I am in trouble now. If possible, please help me @tjtanjin

Thanks & Best Regards,
Divyesh Shani

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 4, 2025

Hey @div3791, are you able to recreate the issue on the playground and paste it here for me to reproduce and debug? 🥹

@div3791
Copy link
Author

div3791 commented Mar 4, 2025

@tjtanjin Observed one thing. removeMessage function is not working in react hooks like useEffect. is it true?

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 4, 2025

@tjtanjin Observed one thing. removeMessage function is not working in react hooks like useEffect. is it true?

It should work 🥹 Where are you placing this specific line? const { injectMessage, removeMessage } = useMessages();

If possible, can you share the full code for ease of reference?

@div3791
Copy link
Author

div3791 commented Mar 6, 2025

I am placing const { injectMessage, removeMessage } = useMessages(); inside my custom hook. and that hook is called from Component which is already children of <ChatbotProvider>
injectMessage is working properly but removeMessage is not removing anything from anywhere.

let me explain scenario. When user send any message, I am manually injecting message into chat window using await injectMessage(messageText) and then again injecting my typing indicator component with let savedMessageId = await injectMessage(<CustomTypingIndicator/>) and then I am storing saveMessageId into zustand store.

When signal r receives any message, before calling injectMessage(messageText), I am calling await removeMessage(savedMessageId) to remove typing indicator but its not removing anything from anywhere.

I cross verified messageIds into local storage and saved message id in zustand store.

Please suggest me something @tjtanjin . My boss is now anticipating complete the project within 2 days

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 6, 2025

So what I'd do is to add a console.log(messageId) right before calling removeMessage to make sure that the correct id is passed in. If the correct id is passed in but the message is still not removed, then I'll need you to either reproduce the issue on the playground and share it here, or to share the code that you have currently (sufficient for me to reproduce the error). I'd love to help you but I am currently unable to reproduce the behavior you're describing 🥹

@div3791
Copy link
Author

div3791 commented Mar 6, 2025

Okay @tjtanjin could you please tell me where should I prepare sample code?? suggest playground link from where i can share you a implementation becasue my react app project is very large. it will be too difficult to extract perticular code.

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 6, 2025

Okay @tjtanjin could you please tell me where should I prepare sample code?? suggest playground link from where i can share you a implementation becasue my react app project is very large. it will be too difficult to extract perticular code.

You can try to reproduce the issue here on the playground then paste the code below once you're done 😀

To make things easier, can I suggest you work off this example shared here to edit (paste the snippet in the playground): #300 (comment)

@div3791
Copy link
Author

div3791 commented Mar 6, 2025

Or else you can explain below things in your prefered way.

how getChatHistory() and setChatHistory() works from useChatHistory() hook in react-chatbotify 2.0.0-beta.28?? and how messages property from useMessages() hook??

and what is the difference between messages from useMessages() hook and getChatHistory() from useChatHistory() hook??

I want to inject typing indicator after user submits or sends any message everytime. and remove typing indicator when i am getting any message from server

actually sometimes I am getting getChatHistory() and messages from useMessages() as blank array. sometimes it has some messages. cant understand behaviour of these hooks and methods. which messages will it return from getChatHistory() and messages object? and will setChatHistory() trigger the UI update or not? how it behaves in case of page hard reload or tab reopen etc.

I am using vite+react+typescript

I tried to tweak the flow by using getChatHistory() and setChatHistory() and messages object. I tried to remove last message which is of course typing indicator message. but its not working.

Below is my chatbot code.

import ChatBot, { useChatWindow, useMessages } from "react-chatbotify";
import { useChatBotSettings } from "@/hooks/chatbot/useChatbotSettings";
import { useChatbotStyle } from "@/hooks/chatbot/useChatbotStyle";
import useUserSubmitEvent from "@/hooks/chatbot_events/useUserSubmitEvent";
import { useEffect } from "react";
import { useDataStore } from "@/store/useDataStore";
import useSignalR from "@/hooks/signalr/useSignalR";
import Logger from "@/utils/logger";

const printLog = new Logger("Chatbot_Component");

const AppChatbot = () => {
  const { isConnectedToSignalRServer } = useSignalR();
  useUserSubmitEvent();
  const { isChatWindowOpen } = useChatWindow();
  const { injectMessage } = useMessages();
  const settings = useChatBotSettings();
  const styles = useChatbotStyle();
  const { conversationId } = useDataStore();

  useEffect(() => {
    if (!conversationId && isChatWindowOpen) {
      let oldMessages = localStorage.getItem("chatbot_chat_history");
      if (!oldMessages) {
        injectMessage(
          `Welcome! Im here to support you with all your eye health and vision needs, ensuring you receive the best care.`
        );
        
        setTimeout(async () => {}, 2000);
      }
    }
  }, [isChatWindowOpen]);

  useEffect(() => {
    printLog.info("SignalR Connection State: ", isConnectedToSignalRServer);
  }, [isConnectedToSignalRServer]);

  return (
    <div
      data-testid="app-chatbot"
      className="fixed bottom-0 right-0 p-4 bg-red rounded-lg z-150 flex flex-col"
    >
      <ChatBot
        flow={{
          start: {
            message: undefined,
            options: [],
          },
        }}
        styles={styles}
        settings={settings}
      />
    </div>
  );
};

export default AppChatbot;

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 6, 2025

For your use case, you should stick with messages from useMessages. The chat history utility functions only deal with chat history messages that belong to a previous session so you shouldn't be touching the chat history hook at all.

I see your code snippet, but it doesn't contain the part where it's using removeMessage 🥹. The easiest way for me to help is if you can reproduce the issue on the playground - that way I can paste it in and debug directly with your provided snippet.

Edit: On a side note, beta.31 is the latest version so it might help to bump the package while you're at this.

@div3791
Copy link
Author

div3791 commented Mar 6, 2025

Okay I will prepare code on playground

Till than could you please provide any resource from where I can understand how messages object works and when it triggers UI update and local storage messages??

Sometimes I noticed even there are chats in local storage, messages object shows blank array

How can I manipulate messages object?? Which actually remove message from local storage as well as chat window?

Thanks and Best Regards

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 6, 2025

You are strongly discouraged from modifying or referring to the local storage as it's handled internally by the library. The local storage basically stores chat history and is refreshed/updated whenever there are message changes. The messages array in the chatbot on the other hand represents what is being shown in the chat window itself. As such, your messages array in the chatbot may or may not match what is in local storage depending on whether the chat history has been loaded by the user.

If the above sounds confusing to you, that is because these are internal behaviors handled within the chatbot. Users such as yourself shouldn't have to dig into such behaviors and should only interact with messages via the provided utility functions. The resources for these functions are on the documentation website which you've probably been referring to. There are currently no resources for understanding the chat history implementation (there's a developer guide in the repository, but regrettably the chat history implementation hasn't been updated). Regardless, this is not a behavior you should have to dig into.

@div3791
Copy link
Author

div3791 commented Mar 6, 2025

Okay so messages object contains those messages which is present in chat window right?

So how can I update messages object? Basically i want to remove last message from chat window

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 6, 2025

Okay so messages object contains those messages which is present in chat window right?

So how can I update messages object? Basically i want to remove last message from chat window

Circles back to removeMessage 🥹 Based on what I've tried on my end, it works. That's why I'm hoping you can provide a snippet for me to paste on the playground to reproduce when it does not work. That way if it's a bug I can fix it.

@div3791
Copy link
Author

div3791 commented Mar 8, 2025

Hi @tjtanjin , I reproduced the issue. I tried using playground and there also removeMessage(messageId) not working. I am sharing playground code along with console logs screenshot. send first message and then after 3 seconds you will see Typing... from bot side. and after 5 seconds it supposed to remove that typing text.

Here is playground code:

const useSubmitHandler = () => {
  const { injectMessage, removeMessage } = useMessages();
  const { setTextAreaValue } = useTextArea();
  let typingindicator = React.useRef<string | null>(null);

  function callAPI() {
    console.log("callAPI");
    setTimeout(async () => {
      console.log("callAPI->settimeout->removing typing->", typingindicator.current);
      let removed = await removeMessage(typingindicator.current);
      console.log("removed id: ", removed);
    }, 5000);
  }

  const handleUserSubmit = async (event: any) => {
    event.preventDefault();
    let text = (event as RcbUserSubmitTextEvent).data.inputText.trim();
    console.log(text);
    if (text) {
      try {
        await injectMessage(text, "USER");
        setTextAreaValue("");
        setTimeout(async () => {
          typingindicator.current = await injectMessage(
            <h4>Typing...</h4>,
            "BOT"
          );
          console.log("added typing: ", typingindicator.current);
          callAPI();
        }, 3000);
      } catch (err) {
        console.log("error on injecting message: ", err);
      }
    }
  };

  React.useEffect(() => {
    window.addEventListener("rcb-user-submit-text", handleUserSubmit);
    return () => {
      window.removeEventListener("rcb-user-submit-text", handleUserSubmit);
    };
  }, []);
};

const MyChatBotApp = () => {
  useSubmitHandler();

  return (
    <ChatBot
      flow={{
        start: {
          message: undefined,
          options: [],
        },
      }}
      settings={{
        general: {
          embedded: true,
        },
        event: {
          rcbUserSubmitText: true,
        },
      }}
    />
  );
};

function App() {
  return (
    <ChatBotProvider>
      <MyChatBotApp />
    </ChatBotProvider>
  );
}

render(<App/>)

Here is console logs screenshot:

Image

@tjtanjin
Copy link
Owner

tjtanjin commented Mar 8, 2025

Hey @div3791, noticed you're using setTimeout. It's likely the callback inside is capturing the state (and function) values at the time that it was created.

Instead of using removeMessage directly, use a ref:

const removeMessageRef = React.useRef(removeMessage);

Then add an effect to update it:

React.useEffect(() => {
    removeMessageRef.current = removeMessage;
  }, [removeMessage]);

Inside your callback, use this ref instead. This should resolve your issue, let me know!

@div3791
Copy link
Author

div3791 commented Mar 10, 2025

Thank you so much @tjtanjin It worked now. Thank you for saving me ❤️

I want to add signal r support to this library. How can I start my contribution?

@tjtanjin
Copy link
Owner

Thank you so much @tjtanjin It worked now. Thank you for saving me ❤️

I want to add signal r support to this library. How can I start my contribution?

Happy to hear it works! For signal r support, it's probably best to introduce it in the form of a plugin. You can make references to existing plugins as well as a provided template here: https://github.com/React-ChatBotify-Plugins

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants