Flux GPT Prompt Generator

Automatically constructs easily-copyable prompts to answer Flux poll questions with the help of ChatGPT.

// ==UserScript==
// @name         Flux GPT Prompt Generator
// @namespace    https://skye.horse/
// @require      https://greasyfork.org/scripts/37236-monkeyconfig/code/MonkeyConfig.user.js
// @version      1.0.3
// @description  Automatically constructs easily-copyable prompts to answer Flux poll questions with the help of ChatGPT.
// @author       Brittank88
// @license      CC0
// @match        https://flux.qa/#/feeds/*?tab=polls
// @icon         https://www.google.com/s2/favicons?sz=32&domain=flux.qa
// @icon64       https://www.google.com/s2/favicons?sz=64&domain=flux.qa
// @grant        GM_openInTab
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_setClipboard
// ==/UserScript==

const LOG_PREFIX = '[Flux GPT Prompt Generator]';

// eslint-disable-next-line
const CONFIG = new MonkeyConfig({
    title: 'Flux GPT Prompt Generator Configuration',
    menuCommand: true,
    params: {
        "ChatGPT New Page": {
            type: 'checkbox',
            default: false
        }
    }
});

(function() {
    'use strict';

    if (CONFIG.get("ChatGPT New Page")) GM_openInTab('https://chat.openai.com/', true);

    // Check if cards already exist on document load.
    const initialCardElements = document.querySelectorAll('mat-card-content');
    initialCardElements.forEach(appendButton);
    console.log(`%c${LOG_PREFIX} Processed ${initialCardElements.length} questions present on document load!`, 'color:lime');

    // Recursive implementation to append button to each new card added, on-demand.
    const waitForElmCallback = cardElement => {
        appendButton(cardElement);
        console.log(`%c${LOG_PREFIX} Processed new question!`, 'color:lime');
        waitForElm('mat-card-content').then(waitForElmCallback);
    };

    // Trigger recursive mutation observer.
    waitForElm('mat-card-content').then(waitForElmCallback);
})();

/** Appends a button to the given card with the appropriate function as callback. **/
function appendButton(cardElement) {

    // Generate the elements.
    var spacerElement = document.createElement('div');
    spacerElement.className = 'spacer';

    var buttonElement = document.createElement('button');
    buttonElement.style = "display: flex; align-items: center;";
    buttonElement.onclick = () => {
        processCard(cardElement).then(prompt => {
            GM_setClipboard(prompt);
            console.log(`%c${LOG_PREFIX} Generated prompt:`, 'color:lime');
            console.log(`%c${prompt}`, 'color:lime; font-family:courier');
        }, msg => { throw new Error(`${LOG_PREFIX} ${msg}`); });
    }

    var buttonIcon = document.createElement('img');
    buttonIcon.src = "https://www.google.com/s2/favicons?sz=16&domain=chat.openai.com";

    var buttonText = document.createElement('span');
    buttonText.textContent = "Copy Prompt";

    var nbspTextNode = document.createTextNode('\u00A0');

    buttonElement.append(buttonIcon);
    buttonElement.append(nbspTextNode);
    buttonElement.append(buttonText);

    // Retrieve the element we will insert the button before.
    var subtitleElement = cardElement.previousElementSibling;
    var lastElementChild = subtitleElement.lastElementChild;
    subtitleElement.insertBefore(buttonElement, lastElementChild);
    subtitleElement.insertBefore(spacerElement, lastElementChild);
}

// Const map of question types and associated processing functions to call.
const responseTypeProcessingMap = {
    'FLUX-AUDIENCE-MULTIPLE-CHOICE': processMultiChoice,
    'FLUX-AUDIENCE-FREE-ANSWER': processFreeAnswer,
    'FLUX-AUDIENCE-WORD-CLOUD': processWordCloud
};

/** Determines the question type of the card element and generates the appropriate prompt as a result. **/
function processCard(cardElement) {
    return new Promise((resolve, reject) => {
        // First, extract the question text.
        var questionText = cardElement.querySelector('p.question').textContent;

        // Now, process differently depending on if the question is free-answer or multiple choice.
        var responseElement = [].slice.call(cardElement.children).find(c => c.tagName.startsWith('FLUX-AUDIENCE-'));

        if (responseElement === undefined) reject('Unable to extract response element!');

        var tagName = responseElement.tagName;
        let processingFunc;
        if ((processingFunc = responseTypeProcessingMap[tagName])) processingFunc(cardElement, questionText, responseElement).then(resolve, reject)
        else reject(`Unknown response element type '${tagName}'!`);
    });
}

/** Process a question featuring multiple-choice answers. **/
function processMultiChoice(cardElement, questionText, responseElement) {
    return new Promise((resolve, reject) => {

        // Extract the text from each possible response (also accounting for the submit button).
        var responseIdentifierList = [].slice.call(
            responseElement.querySelectorAll('form > button[type]:not([type="submit"]) div.button-content > div.label')
        );
        var responseList = [].slice.call(
            responseElement.querySelectorAll('form > button[type]:not([type="submit"]) div.button-content > span')
        );

        // Return the prompt string, or reject if we couldn't identify responses.
        responseList.length > 0 && responseList.length === responseIdentifierList.length
            ? resolve(`${questionText}\n\n${responseList.reduce((acc, b, i) => acc + `${responseIdentifierList[i].innerText}) ${b.innerText}\n`, '')}`)
            : reject('Could not extract possible responses!');
    });
}

/** Processes a question featuring an open multi-word response box. **/
function processFreeAnswer(cardElement, questionText, responseElement) {
    return new Promise((resolve, reject) => {

        // Extract the word limit from the response box.
        var wordLimit = responseElement.querySelector('input').maxLength;

        // Return the prompt string, or reject if we couldn't validate the word limit.
        wordLimit > 0 && typeof wordLimit === 'number'
            ? resolve(`Answer the following in ${wordLimit} words or less:\n\n${questionText}`)
            : reject('Failed to extract word limit!');
    });
}

/** Processes a question featuring an open single-word response box. **/
function processWordCloud(cardElement, questionText, responseElement) {
    return new Promise((resolve, reject) => {

        // Extract the character limit from the response box.
        var charLimit = responseElement.querySelector('input').maxLength;

        // Return the prompt string, or reject if we couldn't validate the character limit.
        charLimit > 0 && typeof charLimit === 'number'
            ? resolve(`Answer the following in ${charLimit} characters or less:\n\n${questionText}`)
            : reject('Failed to extract character limit!');
    });
}

/* MutationObserver implementation to resolve with the next element matching the selector that is created.
 * @author Brittank88
 */
function waitForElm(selector) {
    return new Promise(resolve => {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.addedNodes) {
                    mutation.addedNodes.forEach(node => {
                        if (node.matches && node.matches(selector)) {
                            observer.disconnect();
                            resolve(node);
                        } else if (node.querySelectorAll) {
                            const matchingNodes = node.querySelectorAll(selector);
                            matchingNodes.forEach(matchingNode => {
                                observer.disconnect();
                                resolve(matchingNode);
                            });
                        }
                    });
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}