Send chat transcripts with Chatkit and Sendgrid

In this tutorial, we’ll take a look at how chat transcripts can be retrieved and sent to an email address using Sendgrid’s API. Here’s a demo of what we’ll be building:

A common feature on most customer support chat systems is the ability to send a transcript of a chat session after it ends. This provides a reference for customers in case they need to remember something that was discussed during a particular chat session.

Prerequisites

To follow along with this tutorial, you need to have Node.js (version 8 or later), and npm installed on your machine. Prior experience with React and Chatkit is also required for you to gain a full understanding of how the code that will be written in the tutorial works.

Sign up for Chatkit

Open this link to create a new Chatkit account or login to your existing account. Once you’re logged in, create a new Chatkit instance for this tutorial, then locate the Credentials tab on your instance’s dashboard and take note of the Instance Locator and Secret Key.

Also make sure the Test Token Provider for the instance is active as shown below. Once enabled, the URL from which the test token will be generated will be displayed on the page.

Head over to the Console tab, and create a new user for your application. This will be the user account for the support staff who will interact with customers. The user identifier for this user should be support as shown below:

Sign up for SendGrid

Create a free account at Sendgrid.com. Once you’re logged in, select Settings > API Keys on the sidebar and then create a new API key. Give your API key a name, and select Full Access under API Key Permissions. Once your key is created, keep it in view until after we’ve added it to an .env file in the next section.

Set up the server

Launch a new terminal instance and create a new directory for your project as shown below:

    mkdir chat-transcript-tutorial

Next, cd into the new directory and initialize your project with a package.json file using the npm init -y command. Following that, run the command below to install all the dependencies that we’ll need to build our Node server.

    npm install express dotenv body-parser @pusher/chatkit-server cors date-fns @sendgrid/mail --save

Next, create a .env file in the root of your project directory and paste in the credentials you retrieved from your Chatkit and Sendgrid dashboards respectively:

    // .env

    PORT=5200
    CHATKIT_INSTANCE_LOCATOR=<your chatkit instance locator>
    CHATKIT_SECRET_KEY=<your chatkit secret key>
    SENDGRID_API_KEY=<your sendgrid api key>

Create another file, server.js in the root of your project directory, and add the following code into it to set up the Node server:

    require('dotenv').config({ path: '.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Chatkit = require('@pusher/chatkit-server');
    const dateFns = require('date-fns');

    const sgMail = require('@sendgrid/mail')

    sgMail.setApiKey(process.env.SENDGRID_API_KEY)

    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: process.env.CHATKIT_INSTANCE_LOCATOR,
      key: process.env.CHATKIT_SECRET_KEY,
    });

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/users', (req, res) => {
      const { username } = req.body;

      chatkit
        .createUser({
          id: username,
          name: username,
        })
        .then(() => {
          res.sendStatus(201);
        })
        .catch(err => {
          if (err.error === 'services/chatkit/user_already_exists') {
            console.log(`User already exists: ${username}`);
            res.sendStatus(200);
          } else {
            res.status(err.status).json(err);
          }
        });
    });

    app.post('/transcript', (req, res) => {
      const { roomId, email, name } = req.body;
      chatkit.fetchMultipartMessages({
        roomId,
        limit: 100,
      })
        .then(messages => {
          const t = constructTranscript(messages);

          const msg = {
            to: email,
            from: 'noreply@fictionalservice.com',
            subject: 'Chat transcript',
            html: `
              <p>Dear ${name},</p>

              <p>Thank you for taking the time to chat with us today. Below is a copy of your chat transcript for future reference.</p>

              <p><strong>Chat ${dateFns.format(new Date(), 'DD/MM/YYYY HH:mm')}</strong></p>
              <ul>
                ${t.join('')}
              </ul>

              <p>Thank you,</p>
              <p>Customer Care</p>
            `,
          };

          return sgMail.send(msg)
        })
        .then(() => {
          res.send("Success!");
        })
        .catch((err) => {
          console.error(err);
          res.status(500).send("An error occured");
        });
    });

    function constructTranscript(messages) {
        return messages.reverse().map(message => {
          return `
            <li className="message">
              <div>
                <span>[${dateFns.format(message.created_at, 'DD/MM/YYYY HH:mm')}]</span>
                <strong className="user-id">${message.user_id}:</strong>
                <span className="message-text">${message.parts[0].content}</span>
              </div>
            </li>
          `
        });
    }

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running → PORT ${server.address().port}`);
    });

If you’ve worked with Chatkit before, this should all be familiar to you. Otherwise, here’s a brief explanation:

  • First instantiate our chatkit instance using the Instance Locator and Secret Key noted in the previous step.
  • In the /users route, we take a username and create a Chatkit user through our chatkit instance.
  • The /transcript route is where messages from a room are fetched, processed into an HTML template, and sent to an email address via Sendgrid. We’ll take a closer look at what this code does in a later section.

You can go ahead and start the server on port 5200 by running node server.js in the terminal.

Set up the application frontend

Install create-react-app globally, then use it to bootstrap a new React application:

    npm install -g create-react-app
    create-react-app frontend

Once the app has been created, cd into the new directory and install the following additional dependencies that we’ll be needing in the course of building the chat app, then launch the development server on http://localhost:3000:

    npm install @pusher/chatkit-client react-toastify axios random-words --save
    npm start

Add the styles for the application

Open up frontend/src/App.css and change its contents to look like this:

    // frontend/src/App.css

    html {
      box-sizing: border-box;
    }

    *, *::before, *::after {
      box-sizing: inherit;
      margin: 0;
      padding: 0;
    }

    .login-form {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      border: 1px solid #ddd;
      padding: 20px;
      width: 100%;
      max-width: 500px;
    }

    .login-form h2 {
      margin-bottom: 20px;
    }

    button[type="submit"] {
      width: 100%;
      padding: 10px;
      font-size: 18px;
      color: #fff;
      background-color: #000;
      cursor: pointer;
      border: 1px solid transparent;
    }

    button[type="submit"]:hover {
      color: #000;
      background-color: #fff;
      border: 1px solid #dcc;
    }

    input {
      width: 100%;
      margin-bottom: 20px;
      padding: 10px;
      border: 1px solid #ccc;
    }

    label {
      margin-bottom: 10px;
      display: inline-block;
    }

    /* Chat Widget
       ========================================================================== */

    .chat-widget {
      position: absolute;
      bottom: 20px;
      right: 60px;
      border: 1px solid #ccc;
      height: 500px;
      width: 350px;
      display: flex;
      flex-direction: column;
    }

    .chat-header {
      border-bottom: 1px solid #ccc;
      padding: 10px 15px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .chat-header h2 {
      font-size: 20px;
    }

    .end-chat {
      background: transparent;
      border: none;
    }

    .chat-messages {
      padding: 15px;
      flex-grow: 1;
      list-style: none;
    }

    .chat-messages li {
      margin-bottom: 10px;
    }

    .message-input {
      margin-bottom: 0;
      border: none;
      border-top: 1px solid #ccc;
    }

    .notice {
      display: none;
    }

    .dialog-container {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background-color: rgba(0, 0, 0, 0.8);
      display: flex;
      justify-content:center;
      align-items: center;
    }

    .dialog {
      width: 500px;
      background-color: white;
      display: flex;
      align-items:  center;
    }

    .dialog-form {
      width: 100%;
      margin-bottom: 0;
      padding: 20px;
    }

    .dialog-form > * {
      display: block;
    }

    .submit-btn {
      color: #5C8436;
      background-color: #181919;
      width: 100%;
    }

    .submit-btn:hover {
      color: #5C8436;
      background-color: #222;
    }

Create a basic chat application

This next step involves creating a simple interface with which a potential customer will interact with a support agent. Open up frontend/src/App.js and change it to look like this:

    // frontend/src/App.js

    import React from 'react';
    import { handleInput, joinChat, sendMessage, endChat, sendTranscript } from './methods.js';
    import { ToastContainer } from 'react-toastify';

    import 'react-toastify/dist/ReactToastify.min.css';
    import './App.css';

    class App extends React.Component {
      constructor() {
        super();
        this.state = {
          showTranscriptDialog: false,
          transcriptEmail: "",
          username: "",
          currentRoom: null,
          currentUser: null,
          messages: [],
          newMessage: "",
        }

        this.handleInput = handleInput.bind(this);
        this.joinChat = joinChat.bind(this);
        this.sendMessage = sendMessage.bind(this);
        this.endChat = endChat.bind(this);
        this.sendTranscript = sendTranscript.bind(this);
      }

      render() {
        const { currentUser, messages, newMessage, showTranscriptDialog } = this.state;

        const messageList = messages.map(message => {
          const arr = message.parts.map(p => {
            return (
              <span className="message-text">{p.payload.content}</span>
            );
          });

          return (
            <li className="message" key={message.id}>
              <div>
                <span className="user-id"><strong>{message.sender.name}: </strong></span>
                {arr}
              </div>
            </li>
          )
        });

        return (
          <div className="App">
            {!currentUser ? (
              <div className="login-form">
                <h2>Chat with a Support Agent</h2>
                <form onSubmit={this.joinChat}>
                    <label htmlFor="username">Enter your username</label>
                    <input onChange={this.handleInput} type="text" id="username" name="username"
                      placeholder="Username" />
                      <button type="submit">Start chatting!</button>
                    </form>
                  </div>
            ) : (
              <div className="chat-widget">
                <header className="chat-header">
                  <h2>Support</h2>
                  <button onClick={this.endChat} className="end-chat">
                    End Chat
                  </button>
                </header>
                <ul className="chat-messages">
                  {messageList}
                </ul>

                <form onSubmit={this.sendMessage} className="message-form">
                  <input
                    className="message-input"
                    autoFocus
                    name="newMessage"
                    placeholder="Compose your message and hit ENTER to send"
                    onChange={this.handleInput}
                    value={newMessage}
                  />
                </form>
              </div>
              )}

              <ToastContainer />

              {showTranscriptDialog ? (
                <div className="dialog-container">
                  <div className="dialog">
                    <form className="dialog-form" onSubmit={this.sendTranscript}>
                      <label htmlFor="email">Send a transcript of the chat to the following email address:</label>
                      <input onChange={this.handleInput} type="email" id="email" name="transcriptEmail"
                        placeholder="name@example.com" />
                      <button type="submit" className="submit-btn">
                        Send transcript
                      </button>
                      </form>
                    </div>
                  </div>
              ) : null}
          </div>
        );
      }
    }

    export default App;

Create a new methods.js file within the src directory and add the following code to it:

    import axios from 'axios';
    import { ChatManager, TokenProvider } from '@pusher/chatkit-client'
    import { toast } from 'react-toastify';
    import randomWords from 'random-words';

    function handleInput(event) {
      const { value, name } = event.target;

      this.setState({
        [name]: value
      });
    }

    function addSupportStaffToRoom() {
      const { currentRoom, currentUser } = this.state;

      return currentUser.addUserToRoom({
        userId: "support",
        roomId: currentRoom.id
      });
    };

    function createRoom() {
      const { currentUser } = this.state;

      currentUser
        .createRoom({
          name: randomWords({ exactly: 2, join: ' ' }),
          private: true
        })
        .then(room => connectToRoom.call(this, room.id))
        .then(() => addSupportStaffToRoom.call(this))
        .catch(console.error);
    };

    function connectToRoom(id) {
      const { currentUser } = this.state;

      return currentUser
        .subscribeToRoomMultipart({
          roomId: `${id}`,
          messageLimit: 100,
          hooks: {
            onMessage: message => {
              this.setState({
                messages: [...this.state.messages, message]
              });
            },
          }
        })
        .then(currentRoom => {
          this.setState({
            currentRoom
          });
        });
    }

    function joinChat(event) {
      event.preventDefault();

      const { username } = this.state;

      if (username.trim() === "") {
        alert("A valid username is required");
      } else {
        axios
          .post("http://localhost:5200/users", { username })
          .then(() => {
            const tokenProvider = new TokenProvider({
              url: "<your token provider endpoint>"
            });

            const chatManager = new ChatManager({
              instanceLocator: "<your chatkit instance locator>",
              userId: username,
              tokenProvider
            });

            return chatManager.connect().then(currentUser => {
              console.log(currentUser);
              this.setState(
                {
                  currentUser,
                },
                () => createRoom.call(this)
              );
            });
          })
          .catch(console.error);
      }
    }

    function sendMessage(event) {
      event.preventDefault();
      const { newMessage, currentUser, currentRoom } = this.state;

      if (newMessage.trim() === "") return;

      currentUser.sendSimpleMessage({
        roomId: `${currentRoom.id}`,
        text: newMessage
      });

      this.setState({
        newMessage: "",
      });
    }

    function endChat(event) {
      event.preventDefault();

      this.setState({
        showTranscriptDialog: true
      });
    }

    function sendTranscript(event) {
      event.preventDefault();

      const { currentRoom, currentUser, transcriptEmail } = this.state;

      axios.post("http://localhost:5200/transcript", {
        roomId: currentRoom.id,
        email: transcriptEmail,
        name: currentUser.name,
      })
        .then(res => {
          toast.success("Transcript sent successfully!");

          this.setState({
            showTranscriptDialog: false,
            transcriptEmail: "",
          })
        })
        .catch(err => {
          console.error(err);
          toast.error("An problem occured");
        })
        .finally(() => {
          currentUser.disconnect();
          this.setState({
            currentUser: null,
            currentRoom: null,
            messages: [],
            newMessage: "",
          })
        });
    }

    export {
      handleInput,
      joinChat,
      sendMessage,
      endChat,
      sendTranscript,
    }

Don’t forget to replace the <your token provider endpoint> and <your chatkit instance locator> placeholders above with the appropriate values from your Chatkit dashboard.

Here’s a quick run down on what the code above does:

  • Once a user connects to the Chatkit instance, a new room is created for the interaction through the createRoom() method.
  • Once the user is connected to the room, the support staff agent is also added to the room instantaneously. In a real world application, you will probably have several support agents, so there will be a (hopefully short) waiting time for the user before they are connected to an available agent.
  • Messages can now be sent back and forth between the current user and the support agent. You can navigate to the Console tab on the instance dashboard to add a message to the room as the Support agent.
  • Once the chat is ended, a new dialog will appear prompting the user to enter an email address where the transcript will be sent.
  • The sendTranscript() function makes a request to the /transcript endpoint on the server, and passes along the email address along with the room’s ID.
  • If you look at the /transcript endpoint on the server, you will see that it simply fetches the messages in the room, and constructs some HTML which is then sent to the provided email address via SendGrid’s API.

Here’s how the above process looks like in practice:

Wrap up

In this tutorial, we covered how to send chat transcripts with Chatkit and Sendgrid. We covered a simple use case but you can take it further by customizing the transcript email template or creating an archive of all your chats which can come in handy if you need to keep a record of past chats on file for all of your agents.

You can checkout other things Chatkit can do by viewing its extensive documentation. Don’t forget to grab the full source code used in this tutorial here.

#reactjs #Sendgrid #javascript #Chatkit

Send chat transcripts with Chatkit and Sendgrid
1 Likes3.10 GEEK