Build a Tone Email Editor with SendGrid, React, and OpenAI

March 07, 2024
Written by
Eman Hassan
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

 

Build a Tone Email Editor with SendGrid, React, and OpenAI

Emails are crucial for marketing and business communication. On busy, stressful days, poorly written emails can convey unintended tones or impressions. That is why It's important to be thoughtful when writing emails, making sure your message comes across clearly, especially in a fast-paced and high-pressure work environment. Your emails reflect your professionalism and how others see you in the business world. This tutorial guides you in creating an email editor to improve tone, ensuring your emails are received as intended using Twilio SendGrid, and OpenAI.

Prerequisites

Building the app

Environment setup

Create a new folder where you want your project and call it EmailEditor then open Visual Studio Code then go to File > Open Folder and open the folder EmailEditor then go to Terminal > New Terminal from the header menu.

Setup node server

Run the following commands to initialize the node server that you will use to call the APIs to send email and analyze the email body tone. Then install the express package to use in the backend.

npm init -y
npm i express

You will then see a file called package.json created within the EmailEditor directory. Open this file in Visual Studio Code and add the proxy and scripts attributes as shown below which let’s you start the server on localhost:3001:

{
  "name": "emaileditor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "proxy": "http://localhost:3001",
  "scripts": {
    "start": "node server/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2"
  }
}

You will also need to enable cross origin resource sharing to be able to call the node server from the react UI app. Enter the following command on your terminal to install CORS.

npm install cors

Now within the EmailEditor directory, create a folder called Server. Within the Server folder, create a file called index.js. Place the following code within the index.js file:

const express = require("express");
const cors = require("cors");
const PORT = process.env.PORT || 3001;
const app = express();
// Enable CORS
app.use(cors());
app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});

If you try to open the terminal and run npm start on your terminal, it should successfully run the node server.

SendGrid setup

Install the sendgrid npm package with the following command on your terminal:

npm install --save @sendgrid/mail

Then create a new file called .env which we will use to store our API keys which will authenticate the API calls to SendGrid and OpenAI.

Before you can start sending emails, there are a few more mandatory steps you’ll need to complete.

Once you’ve completed the above steps, you’ll need to create a new API Key . Keep this key somewhere safe as we’ll need it in the subsequent steps.

You can also create the API key when you click on Email API side menu tab then click on Integration Guide it will walk you through step by step on integrating your test email with whatever language you choose.

Navigate to the .env file and add the following while replacing the placeholders (XXXXX) with your actual key:

SENDGRID_API_KEY=XXXXX

OpenAI setup

Navigate back to your terminal and install the openai npm package:

Npm install openai

To use the OpenAI API, you'll need to generate an OpenAI API key and add it to the .env file.

To get your API Key, login to OpenAI and navigate to API Keys page here then click on Create New Secret Key.

Add a name to your secret key then click Create Secret Key. It will then show your generated API key so it is important to copy it and save it in a secure storage because you will not be able to show it again for security standards.

Then you paste this key in the .env file as a value for OPENAI_API_KEY.

OPENAI_API_KEY=XXXXX

Setup .env in NodeJs

In order to be able to read the constants from .env, you need to install the dotenv package:

npm install dotenv --save

Import the dotenv library in top of Server/index.js file

require('dotenv').config();

Setup request body parser

To parse the API requests’ body correctly in NodeJs, we need to install this package:

npm install body-parser

Then, import it in Server/Index.js at top of the file

const bodyParser = require('body-parser');

Then, add the following code under app initialization line as follows:

app.use(bodyParser.json());

React setup

To set up a react application, run the command below which will create a new folder containing the react UI application:

npx create-react-app email-editor-app

Start the app to make sure that it was set up correctly:

cd email-editor-app
npm start

You should see the following UI showing up in your browser at localhost:3000.

To exit the run mode from the terminal press ctrl+c then write y and press enter to terminate.

Install npm packages for UI components

Make sure you have the VSCode terminal inside the UI folder email-editor-app and install react-quill rich text editor

npm install react-quill

Install react-bootstrap library to style the editor’s form

npm install react-bootstrap bootstrap@4.6.0

Create the backend endpoints

Open another terminal on the root folder where the Node server is and within the Server folder, create a new folder called Services which will hold the logic of calling the APIs and processing the requests. Within this folder, create a file called SendGridService.js which will contain a class with an async send email method as listed below:

const sgMail = require('@sendgrid/mail');
class SendGridService {
    constructor(apiKey) {
        sgMail.setApiKey(apiKey);
    }
    async sendEmail(fromEmail, toEmail, subjectEmail, emailBody) {
        const msg = {
            to: toEmail,
            from: fromEmail,
            subject: subjectEmail,
            html: emailBody,
        };
        sgMail.send(msg).then((response) => {
            console.log('Email sent')
            return response;
          })
          .catch((error) => {
            throw new Error(`Failed to send email: ${error.message}`);
          })
    }
}
module.exports = SendGridService;

The sendEmail method will then call SendGrid through sgMail.send() with the email parameters wrapped in the msg json object .

Now, the SendGridService will be ready to be added as an endpoint in Server/index.js so that you can call this API from the front end.

const sendgridapiKey = process.env.SENDGRID_API_KEY;
  app.post('/send-email', async (req, res) => {
    const { fromEmail, toEmail, emailSubject, emailBody } = req.body;
    const sendGridService = new SendGridService(sendgridapiKey);
    try {
      const response = await sendGridService.sendEmail(fromEmail, toEmail, emailSubject, emailBody);
      res.status(200).json({ message: 'Email sent successfully', response });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
});

And make sure to include the reference to the SendGridService at the top of Server/index.js file as follows:

const SendGridService = require("./Services/SendGridService");

Under the Server/Services folder, create a file called OpenAIService.js which will contain a class with an async getChatCompletion method to generate recommended email body with selected tone as listed below:

const OpenAI = require('openai');
class OpenAIService {
    constructor(apikey) {
        this.apiKey = apikey;
        this.openAI = new OpenAI({ apiKey: this.apiKey });
    }
    async getChatCompletion(tone, body) {
        try {
            const systemMessage = 'You are an email tone analyzer. You will be provided with a required tone and email body and you should analyze the body against the required tone and rephrase the email body to better match the required tone while keeping the same context. Provide output in JSON format as follows: {"email_body":"the generated email body in html format"}';
            const prompt = `Analyze this email body '''${body}''' and suggest email body that is better aligned with ${tone} tone.`;
            const response = await this.openAI.chat.completions.create({
                model: 'gpt-3.5-turbo',
                messages: [{ role: 'system', content: systemMessage }, { role: 'user', content: prompt }],
                temperature: 0.7,
            });
            return response.choices[0].message.content.trim();
        } catch (error) {
            console.error('Error calling OpenAI Chat Completions API:', error);
            throw error;
        }
    }
}
module.exports = OpenAIService;

In order to invoke openAI.chat.completions.create, you’ll need to pass a message parameter to the OpenAI API. Begin by configuring a message with a system role, providing context for the intended actions. Subsequently, include another message with a user role, incorporating the prompt containing the variables—in this instance, the email body and desired tone.

You’ll also need to configure the model, which is set here to gpt-3.5-turbo, and the temperature, controlling the randomness and creativity of ChatGPT's output. The temperature value ranges from 0 to 1. In this application, the temperature is set to 0.7 to introduce a degree of creativity in generating emails. However, feel free to experiment and adjust it to compare different output variations.

Now that the OpenAIService is ready,you’ll need to add the endpoint below in Server/Index.js to be able to call it from the react app.

const openaiapiKey = process.env.OPENAI_API_KEY;
app.post('/check-tone', async (req, res) => {
  const {tone, value} = req.body;
  const openaiService = new OpenAIService(openaiapiKey);
  try {
        const response = await openaiService.getChatCompletion(tone, value);
        res.status(200).json({ message: 'Text generated successfully', response });
      } catch (error) {
          res.status(500).json({ error: error.message });
      }
 });

And make sure to include the reference to the SendGridService at the top of Server/index.js file as follows:

const OpenAIService = require("./Services/OpenAIService");

Now the node server is ready with two endpoints: /send-email and /check-tone.

Create the UI with react

Right click email-editor-app/src then create a new folder called Assets and create a new folder inside it called Styles then create a new file within Styles called EmailEditor.css. You can find and copy the css content from here .

Under email-editor-app/src create a new folder called Components then create a new folder in it called EmailEditor and create a new file called EmailEditor.js.

Add the following code which creates a form containing ToEmail input field, EmailSubject input field, EmailBody rich editor component, Tone dropdown and two buttons Check & Recommend to recommend the email body with the selected tone and Send Email to finally send the email.

The results will then show up in the box beside the form and a button that displays the text, Copy Recommendation, will replace the email body in the rich text editor with the recommended text.

import React, { useState } from 'react';
import { Form, Container, Row, Col, Button } from 'react-bootstrap';
import '../../Assets/Styles/EmailEditor.css';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import fetch from 'node-fetch';

const EmailEditor = () => {
    const [toEmail, setToEmail] = useState('');
    const [emailSubject, setemailSubject] = useState('');
    const [value, setValue] = useState('');
    const [tone, setTone] = useState('Professional');
    const [recommendation, setRecommendation] = useState('');
    const [emailSent, setEmailSent] = useState(false);
      
 const handleCheckTone = async () => {
// TODO           
};
 const fromEmail= "<your_sendgrid_from_email>";
    const handleSendEmail = async () => {
       // TODO
    };
    const handleCopyRecommendation = () => {
        console.log(recommendation);
        setValue(recommendation);
    };
    return (
        <Container className="email-editor-container">
            <Row>
                <Col>
                    <Form>
                        <Form.Group controlId="toEmail">
                            <Form.Label>To Email</Form.Label>
                            <Form.Control
                                type="email"
                                required
                                placeholder="Enter email"
                                value={toEmail}
                                onChange={(e) => setToEmail(e.target.value)}
                            />
                        </Form.Group>
                        <Form.Group controlId="subject">
                                    <Form.Label>Email Subject</Form.Label>
                                    <Form.Control
                                        type="text"
                                        required
                                        placeholder="Enter subject"
                                        value={emailSubject}
                                        onChange={(e) => setemailSubject(e.target.value)}
                                    />
                                </Form.Group>
                        <Form.Group controlId="richEditor" className='richEditor'>
                            <Form.Label>Email Body</Form.Label>
                            <ReactQuill theme="snow" value={value} onChange={setValue} required />
                        </Form.Group>
                        <Form.Group controlId="tone">
                            <Form.Label>Tone</Form.Label>
                            <Form.Control
                                as="select"
                                className='toneSelector'
                                required
                                value={tone}
                                onChange={(e) => setTone(e.target.value)}
                            >
                                <option value="Professional">Professional</option>
                                <option value="Friendly">Friendly</option>
                                <option value="Salesy">Salesy</option>
                            </Form.Control>
                        </Form.Group>
                        <Button variant="primary" onClick={handleCheckTone}>
                            Check & Recommend Tone
                        </Button>{' '}
                        <Button variant="success" onClick={handleSendEmail}>
                            Send Email
                        </Button>
                        {emailSent && <span style={{ color: 'green', marginLeft: '10px' }}>Email sent successfully</span>}
                    </Form>
                </Col>
                <Col>
                    <div className="result-container">
                        <div className="result-label">
                            <span className="positive">Recommendation</span>
                        </div>
                        <div
                            className="result-text"
                            // this could be enhanced with an react html parser
                        >
                            <ReactQuill  theme="snow" value={recommendation} toolbar={false} readOnly />
                        </div>
                        <Button variant="secondary" onClick={handleCopyRecommendation}>
                            Copy Recommendation
                        </Button>
                    </div>
                </Col>
            </Row>
        </Container>
    );
};
export default EmailEditor;

On line 19, don’t forget to replace the placeholder with the email you added in SendGrid as your verified identity.

This method will trigger when you click the Send Email button. It will call the backend to SendGridService API send-email with the form values to send the email. Replace the `handleSendEmail` function in EmailEditor.js file with the following code:

const handleSendEmail = async () => {
        try {
            const response = await fetch('http://localhost:3001/send-email', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    fromEmail,
                    toEmail,
                    emailSubject,
                    emailBody: value
                })
            });
           
            if (response.ok) {
                console.log('Email sent successfully');
                setEmailSent(true);
            } else {
                console.error('Failed to send email');
            }
        } catch (error) {
            console.error('An error occurred while sending email:', error);
        }
    };

This method will trigger when you click the Check & Recommend Tone button. It will call the OpenAIService API /check-tone route to generate an email body recommendation that matches the tone you selected. Replace the handleCheckTone function in EmailEditor.js file with the following code:

const handleCheckTone = async () => {
        try {
            //const randomRecommendation = Math.random() < 0.5 ? 'Positive' : 'Negative';
            const response = await fetch('http://localhost:3001/check-tone', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    tone,
                    value,
                })
            });

            if (response.ok) {
                const result = await response.json();
                const values = JSON.parse(result.response);
                setRecommendation(values.email_body);
            } else {
                console.error('Failed to check tone');
            }
        } catch (error) {
            console.error('An error occurred while checking tone:', error);
        }
    };

Update App.js file

After creating the email editor UI you need to update the App.js file under the src folder to show the email editor component. Replace the existing code with the following:

import './App.css';
import EmailEditor from './Components/EmailEditor/EmailEditor';
import 'bootstrap/dist/css/bootstrap.min.css';
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <EmailEditor />
      </header>
    </div>
  );
}
export default App;

The complete app

Start UI and backend servers

Now that you have implemented both the backend and frontend, open two terminals: one for the backend under the root folder EmailEditor and the other for the email-editor-app folder. Run both the UI and backend simultaneously using the command `npm start` in each terminal.

Try out the email editor

Open localhost:3000 in your browser and the app should look like the following:

Demo of the completed app

Validation

Extend the application further by incorporating form validation and email body validation to ensure the exclusion of improper content. Leverage OpenAI’s Moderation API to validate the email body text before initiating the API call to OpenAI, thereby confirming content compliance.

Conclusion

At the conclusion of this tutorial, you will have acquired the skills to: set up and utilize Twilio SendGrid APIs, configure and employ OpenAI APIs, establish a Node.js server with API endpoints, and develop a comprehensive React application. The entire code is accessible on Github.

Eman Hassan, a dedicated software engineer, prioritizes innovation, cutting-edge technologies, leadership, and mentorship. She finds joy in both learning and sharing expertise with the community. Explore more on her Medium profile: https://emhassan.medium.com/.