AI Email Assistant for GMAIL

Assist in generating email replies using AI.

// ==UserScript==
// @name         AI Email Assistant for GMAIL
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Assist in generating email replies using AI.
// @author       Morgan Schaefer
// @match        https://mail.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const LOCAL_STORAGE_KEY = 'openai_api_key';

    function promptForApiKey() {
        let apiKey = window.prompt("Enter your OpenAI API Key:");
        if (apiKey) {
            localStorage.setItem(LOCAL_STORAGE_KEY, apiKey);
            return apiKey;
        }
        alert("API Key is required to use this script.");
        return null;
    }

    function getApiKey() {
        let apiKey = localStorage.getItem(LOCAL_STORAGE_KEY);
        if (!apiKey) {
            apiKey = promptForApiKey();
        }
        return apiKey;
    }

    const API_KEY = getApiKey();
    if (!API_KEY) return;

    function createButton(label, onClick) {
        const button = document.createElement('button');
        button.textContent = label;
        styleButton(button);
        button.addEventListener('click', onClick);
        return button;
    }

    function styleButton(button) {
        Object.assign(button.style, {
            margin: '5px',
            padding: '5px',
            backgroundColor: '#1a73e8',
            color: '#fff',
            border: 'none',
            borderRadius: '3px',
            cursor: 'pointer'
        });
    }

    function appendButtonsToComposeWindow() {
        const composeWindow = document.querySelector('td.I5 form.bAs');
        if (composeWindow) {
            const targetTable = composeWindow.querySelector('table.IG');
            if (targetTable && !document.getElementById('ai-assistant-button')) {
                const newTr = document.createElement('tr');
                newTr.id = 'ai-assistant-row';

                const newTd = document.createElement('td');
                newTd.colSpan = 2;

                // Create the AI Assistant button
                const assistantButton = createButton('Assistant AI', onButtonClick);
                assistantButton.id = 'ai-assistant-button';

                // Create the input field
                const inputField = document.createElement('input');
                inputField.type = 'text';
                inputField.id = 'ai-input-field';
                inputField.placeholder = 'Instructions supplémentaires...';
                styleInputField(inputField);

                // Append the button and input field to the table cell
                newTd.appendChild(assistantButton);
                newTd.appendChild(inputField);
                newTr.appendChild(newTd);
                targetTable.querySelector('tbody').appendChild(newTr);
            }
        }
    }

    function styleInputField(input) {
        Object.assign(input.style, {
            marginLeft: '10px',
            padding: '5px',
            border: '1px solid #ccc',
            borderRadius: '3px',
            width: '200px'
        });
    }

    function getEmailAddresses() {
        const fromElement = document.querySelector('span#\\:vf');
        const toElement = document.querySelector('div.afZ.af1 div.akl');

        const from = fromElement ? fromElement.textContent.trim() : 'Unknown Sender';
        const to = toElement ? toElement.textContent.trim() : 'Unknown Recipient';

        return { from, to };
    }

    function getConversationContent() {
        const messages = document.querySelectorAll('.ii.gt .a3s.aiL');
        return Array.from(messages).map(msg => {
            const parentContainer = msg.closest('.adn.ads');
            const senderNameElement = parentContainer.querySelector('.gD span');
            const senderName = senderNameElement ? senderNameElement.textContent : 'Unknown Sender';

            const dateTimeElement = parentContainer.querySelector('.g3');
            const dateTime = dateTimeElement ? dateTimeElement.getAttribute('title') : 'Unknown Date/Time';

            const messageContent = msg.innerText.trim();
            console.log(`Sender: ${senderName}, Date/Time: ${dateTime}`);
            return `Sender: ${senderName}, Date/Time: ${dateTime}\n${messageContent}`;
        }).join('\n\n').trim();
    }

    async function fetchOpenAIResponse(endpoint, payload) {
        try {
            const response = await fetch(`https://api.openai.com/v1/${endpoint}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${API_KEY}`
                },
                body: JSON.stringify(payload)
            });

            const data = await response.json();
            return response.ok ? data.choices.map(choice => choice.message.content.trim()) : ['Failed to generate response.'];
        } catch (error) {
            console.error('Error fetching AI responses:', error);
            return ['Failed to generate response.'];
        }
    }

    async function generateAIResponses(prompt) {
        return fetchOpenAIResponse('chat/completions', {
            model: "gpt-4o-2024-08-06",
            messages: [
                { role: "system", content: `You are an assistant that responds in the same language as the input.` },
                { role: "user", content: prompt }
            ],
            max_tokens: 1500,
            temperature: 0.9
        });
    }

    async function identifyKeyPointsAndVariables(conversation) {
        return fetchOpenAIResponse('chat/completions', {
            model: "gpt-4o-2024-08-06",
            messages: [
                { role: "system", content: `Tu es un assistant qui analyse un mail reçu et extrait les éléments de réponse que l'interlocuteur attend.` },
                { role: "user", content: `${conversation}` }
            ],
            max_tokens: 150
        });
    }

    async function generateThreeDistinctResponses(keyPoints, additionalInstructions) {
        const initialResponse = await fetchOpenAIResponse('chat/completions', {
            model: "gpt-4o-2024-08-06",
            messages: [
                { role: "system", content: `You are an assistant that provides concise and distinct responses. Generate three distinct short responses to the following key points. The response should not be longer than 6 words per key point.` },
                { role: "user", content: `Provide three distinct responses for these key points or questions:\n\n${keyPoints}\n\n the responses must be variation of the response : ${additionalInstructions}` }
            ],
            max_tokens: 150,
            n: 1,
            temperature: 0.8
        });

        if (initialResponse && initialResponse.length > 0) {
            return initialResponse[0].split('\n').map(resp => resp.trim()).filter(Boolean).slice(0, 3);
        }
        return ['Failed to generate responses.'];
    }

    async function insertResponseInEmailBody(emailBody, response) {
        const fragment = document.createDocumentFragment();
        response.split('\n').forEach((line) => {
            const textNode = document.createTextNode(line);
            fragment.appendChild(textNode);
            fragment.appendChild(document.createElement('br'));
        });
        emailBody.appendChild(fragment);
    }

    async function onButtonClick() {
        const emailBody = document.querySelector('div[contenteditable="true"][role="textbox"]');
        if (emailBody) {
            emailBody.focus();
            const conversationContent = getConversationContent();
            const { from, to } = getEmailAddresses();

            // Get the additional instructions from the input field
            const additionalInstructions = document.getElementById('ai-input-field').value || '';

            try {
                const keyPointsAndVariables = await identifyKeyPointsAndVariables(conversationContent);
                const shortResponses = await generateThreeDistinctResponses(keyPointsAndVariables, additionalInstructions); // Pass additional instructions here

                let buttonContainer = document.querySelector('#response-options-container');
                if (!buttonContainer) {
                    buttonContainer = document.createElement('div');
                    buttonContainer.id = 'response-options-container';

                    const composeWindow = document.querySelector('td.I5 form.bAs');
                    if (composeWindow) {
                        composeWindow.appendChild(buttonContainer);
                    }
                }

                displayResponseOptions(shortResponses, emailBody, conversationContent, buttonContainer, from, to, additionalInstructions);
            } catch (error) {
                console.error('Error inserting AI response:', error);
            }
        }
    }

    function displayResponseOptions(responses, emailBody, conversationContent, buttonContainer, from, to, additionalInstructions) {
        responses.forEach((response) => {
            const responseButton = createButton(response, async () => {
                const aiPrompt = `You are an email assistant tasked with generating a detailed response. The response is as follows:\n\nFrom: ${from}\nTo: ${to}\n\n${conversationContent}\n\n The reponse must be a elaboration of: ${additionalInstructions}\n\nBased on the above conversation, generate a detailed response using the selected short response option:\n\n${response}. You should generate only the body of the response`;
                const aiResponses = await generateAIResponses(aiPrompt);
                await insertResponseInEmailBody(emailBody, aiResponses[0]);
            });

            buttonContainer.appendChild(responseButton);
        });
    }

    function observeDOMChanges() {
        const observer = new MutationObserver(() => appendButtonsToComposeWindow());
        observer.observe(document.body, { childList: true, subtree: true });
    }

    observeDOMChanges();

})();