How to Build an AI Email Reply Generator Chrome Extension with JavaScript
Wrtitng email reply form an gpt required too much too and fro, example, if i want to generate a email reply, then first i have to copy the all content of email conversion then go to gpt and then paste it, and then asked gpt to generate the email reply then its give response, copy those response and again come to our gmail page and paste it, and some time happen that format of reply can be disttrub, so that we have to also correct those format. So, instead of doing these to and fro, as a developer, why don't we create an extension that has an embedded button in the reply box, and by clicking it, within a second, it will generate the reply based on all previous conversations?
In this article, we will go through creating your Chrome browser extension that integrates directly into the Gmail interface to generate an email reply. This "Email AI" will have two powerful features:
- AI Reply Generator: A button that reads the context of an email conversation and uses an AI (like Google's Gemini) to generate a relevant, professional reply.
- Template Inserter: A dropdown menu to instantly insert pre-written templates for common emails like birthday wishes or leave applications, etc.
This project is a fantastic way to learn about developing a Chrome extension, DOM manipulation, and integrating AI for our personal use in our application. Let's get started!
Prerequisites
Before we dive in, make sure you have the following:
- A Code Editor IDE like VS Code.
- Basic knowledge of JavaScript, HTML, and CSS.
- An API key for an AI model (we will be using Google's Gemini, which has a generous free tier).
Project Structure: The Core Files
Our extension will be surprisingly simple, consisting of just two primary files:
1. manifest.json: The configuration file. It tells Chrome what our extension is, what permissions it needs, and which scripts to run.
2. content.js: Our main JavaScript file. This script will be injected into the Gmail page to add our buttons and handle all the logic. This basically contains the logic, or use can say it's a backend.
Let's create a new folder for our project and add these two empty files to it.
Step 1: Configuring the manifest.json File
The manifest.json file is the heart of any Chrome extension. It defines the extension's capabilities and permissions. Let's break down what each part does.
Copy the following code into your `manifest.json` file:
json
{
"name": "Email Writer Assistant",
"description": "AI-powered email reply generator and template tool.",
"version": "1.0",
"manifest_version": 3,
"permissions": [
"activeTab",
"storage"
],
"host_permissions": [
"*://mail.google.com/*",
"https://generativelanguage.googleapis.com/*"
],
"content_scripts": [
{
"js": [
"content.js"
],
"matches": [
"*://mail.google.com/*"
],
"run_at": "document_end"
}
],
"action": {
"default_title": "Email Writer Assistant"
}
}
Breaking Down the Manifest:
- name, description, version: Self-explanatory metadata for our Email reply generator extension.
- manifest_version: 3: Specifies that we are using the latest and most secure version of the Chrome extension platform. In simple words its specifes the extension version.
- permissions: We request "storage" to potentially save custom templates in the future and "activeTab" for general interaction.
- host_permissions: This is crucial for security. :*//mail.google.com/*: Grants our extension permission to run on Gmail.
- https://generativelanguage.googleapis.com/*: Grants permission to make API calls to Google's Gemini AI.
- content_scripts: This is where we tell Chrome to inject our content.js file into any page that matches the *://mail.google.com/* pattern. In simple words it tell the extension to run this file.
Step 2: Writing the Logic for our Extension - The content.js Script
console.log("Email Writer Extension - Content Script Loaded");
// --- Helper Functions to Create UI Elements ---
function createAIButton() {
const button = document.createElement('div');
button.className = 'T-I J-J5-Ji aoO v7 T-I-atl L3'; // Mimic Gmail's button style
button.style.marginRight = '8px';
button.innerHTML = 'AI Reply';
button.setAttribute('role', 'button');
button.setAttribute('data-tooltip', 'Generate AI Reply');
return button;
}
function createTemplateDropdown() {
const dropdown = document.createElement('select');
dropdown.className = 'T-I J-J5-Ji aoO v7 T-I-atl L3 template-dropdown';
dropdown.style.marginRight = '8px';
dropdown.setAttribute('role', 'button');
dropdown.setAttribute('data-tooltip', 'Insert Template');
// Default option
const defaultOption = document.createElement('option');
defaultOption.text = 'Templates';
defaultOption.disabled = true;
defaultOption.selected = true;
dropdown.appendChild(defaultOption);
// Hardcoded templates
const templates = [
{ name: "Birthday Wish", subject: "Happy Birthday ??", body: "Dear [Name],\n\nWishing you a very Happy Birthday! ????\n\nMay your day be filled with joy, love, and laughter.\n\nBest regards,\n[Your Name]" },
{ name: "Leave Application", subject: "Leave Application Request", body: "Dear [Manager's Name],\n\nI would like to request leave from [start date] to [end date] due to [reason].\n\nKindly approve my request.\n\nBest regards,\n[Your Name]" },
{ name: "Thank You Note", subject: "Thank You", body: "Dear [Recipient],\n\nI just wanted to say thank you for your support and guidance. I truly appreciate it.\n\nBest regards,\n[Your Name]" },
];
templates.forEach(template => {
const option = document.createElement('option');
option.value = template.name;
option.text = template.name;
option.dataset.subject = template.subject;
option.dataset.body = template.body;
dropdown.appendChild(option);
});
dropdown.addEventListener('change', (e) => {
const selected = e.target.selectedOptions[0];
const subject = selected.dataset.subject;
const body = selected.dataset.body;
const subjectBox = document.querySelector('input[name="subjectbox"]');
if (subjectBox) { subjectBox.value = subject; }
const composeBox = document.querySelector('[role="textbox"][g_editable="true"]');
if (composeBox) {
composeBox.innerHTML = ""; // clear old content
composeBox.focus();
document.execCommand('insertText', false, body);
}
e.target.selectedIndex = 0; // Reset dropdown
});
return dropdown;
}
// --- Core Logic Functions ---
function getEmailContent() {
// This function tries to find the content of the email being replied to.
const quote = document.querySelector('.gmail_quote');
if (quote) return quote.innerText.trim();
// Fallback for different Gmail layouts
const content = document.querySelector('.a3s.aiL');
if (content) return content.innerText.trim();
return '';
}
function findComposeToolbar() {
// Gmail's class names can change. This looks for the toolbar using several potential selectors.
return document.querySelector('.gU.Up') || document.querySelector('.aDh');
}
async function generateAIReply(emailContent) {
const API_KEY = 'AIzaSyCXpVrzhqsDB5uXowkjftJsE2OYCpKHr3o'; // IMPORTANT: Replace with your actual key
const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${API_KEY}`;
const prompt = `Generate a professional email reply for the following email content. Please don't generate a subject line. Email to reply to:\n\n---\n${emailContent}\n---`;
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
})
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
// Navigate through the Gemini response structure to get the text
return data.candidates[0].content.parts[0].text;
} catch (error) {
console.error("Error generating AI reply:", error);
return "Sorry, I was unable to generate a reply. Please try again.";
}
}
function injectButtons() {
// Avoid duplicate buttons
document.querySelector('.ai-reply-button')?.remove();
document.querySelector('.template-dropdown')?.remove();
const toolbar = findComposeToolbar();
if (!toolbar) {
console.log("Compose toolbar not found, will retry.");
return;
}
// Always add the Templates dropdown
const dropdown = createTemplateDropdown();
toolbar.insertBefore(dropdown, toolbar.firstChild);
// Only add the "AI Reply" button if we are replying to an email
const emailContent = getEmailContent();
if (emailContent && emailContent.length > 0) {
const button = createAIButton();
button.classList.add('ai-reply-button');
button.addEventListener('click', async () => {
button.innerHTML = 'Generating...';
button.disabled = true;
const generatedReply = await generateAIReply(emailContent);
const composeBox = document.querySelector('[role="textbox"][g_editable="true"]');
if (composeBox) {
composeBox.focus();
// We use execCommand for broad compatibility in content-editable divs
document.execCommand('insertText', false, '\n\n' + generatedReply);
}
button.innerHTML = 'AI Reply';
button.disabled = false;
});
toolbar.insertBefore(button, toolbar.firstChild);
}
}
// --- Observer to Detect When the Compose Window Opens ---
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
// Check if a new node was added that looks like a compose window
const composeWindowAdded = Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE && node.querySelector('.gU.Up, .aDh')
);
if (composeWindowAdded) {
console.log("Compose Window Detected, injecting buttons.");
// Wait a moment for Gmail's UI to fully render before injecting
setTimeout(injectButtons, 500);
return; // No need to check other mutations
}
}
});
// Start observing the entire document for changes
observer.observe(document.body, {
childList: true,
subtree: true
});