AI Email Assistant for GMAIL

Assist in generating email replies using AI.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();

})();