Code viewer for World: DALL-E image processing
// OpenAI Image generation API docs: https://platform.openai.com/docs/guides/images?context=node
// Open AI Rate Limits: https://platform.openai.com/account/limits

//Tweaker's Box
// ===============================================

// Cost Saving Mode will prevent the user from selecting "premium" options by mistake to prevent exhausting OpenAI credits
const COST_SAVING_MODE = true;

// ===============================================

//HTML Templates
// ===============================================

// Base page template
document.write ( `



<h1> OpenAI DALL·E API Demo</h1>

<p>
This is a demo application showing off OpenAI's DALL·E models (2&3).
</p>

<div id="modeSelectors">
</div>

<div id="inputSection">
    Please Eneter a valid OpenAI API Key with credits.
        <div>
            <div>
                <label for="apikeyfield"> API Key: </label>
                <input id="apikeyfield" name="apikey" type="text" />
            </div>
            <button id="apikeybtn" disabled=true>Set API Key</button>
        </div>
</div>

<div id="resultsSection">
</div>

` );

// Cost Saving Mode Warning message
if (COST_SAVING_MODE){
   document.write ( `
    <p id="warningMessage"> WARNING: You are running in "Cost Saving Mode" premium features are disabled</p>
` ); 
}

// Mode selection buttons
const modeSelectBtns = `
        <button id="generationModeBtn" disabled>Generation Mode</button>
        <button id="editModeBtn">Edit Mode</button>
        <button id="variationModeBtn">Variation Mode</button>
`;

// Form template for generation mode
// https://platform.openai.com/docs/api-reference/images/create
const generationModeForm = `
    <h2>Generation Mode</h2>
    <p>Please select the required parameters and then enter your prompt. Once you are satisfied click "generate".</p>
    <p>Image generation may take a few seconds.</p>
    <p><a href="https://platform.openai.com/docs/api-reference/images/create">Please refer to OpenAI documentation for more details</a></p>
    <ul>
        <li><b>Model: </b> Set which version of DALL-E to use for generation </li>
        <li><b>Resolution: </b> Set the resolution of the generated image. The diffrent models have diffrent supported sizes </li>
        <li><b>Quality: </b> Sets image quality. HD is more expensive but produces better results. Only works for DALL-E 3 </li>
        <li><b>Quantity: </b> How many images the model produces. Only n=1 supported for DALL-E 3. Additionally limitations are applied to prevent cost overspend ("Cost saving mode" max=2, else max=4)</li>
        <li><b>Prompt: </b> What image do you want the AI to create </li>
    </ul>
    
    <div>
        <div>
            <label for="modelSelector"> Model: </label>
            <select id="modelSelector" name="model">
                <option value="">Select Model</option>
                <option value="dall-e-2">DALL·E 2</option>
                <option value="dall-e-3">DALL·E 3</option>
            </select>
        </div>
        <div>
            <label for="sizeSelector"> Resolution: </label>
            <select id="sizeSelector" name="size" disabled>
                <option value="">Select Resolution</option>
            </select>
        </div>
        <div>
            <label for="qualitySelector"> Quality: </label>
            <select id="qualitySelector" name="quality" disabled>
                <option value="">Select Quality</option>
            </select>
        </div>
        <div>
            <label for="quantityField"> Quantity: </label>
            <input id="quantityField" name="n" type="number" min=1 max=4 disabled />
        </div>
        <div>
            <label for="promptField"> Prompt: </label>
            <input id="promptField" name="prompt" type="text"disabled placeholder="Enter a Prompt..."/>
        </div>
        <button id="generateBtn" disabled=true>Generate</button>
    </div>
`;

// Dropdown resolution options for dall-e-3
//Smallest size only when in Cost Saving Mode
const dalle3ResOptions = `
    <option value="">Select Resolution</option>
    <option value="1024x1024">1024x1024</option>
    ${!COST_SAVING_MODE ? '<option value="1024x1792">1024x1792</option>' : '<option value="1024x1792" disabled>1024x1792</option>'}
    ${!COST_SAVING_MODE ? '<option value="1792x1024">1792x1024</option>' : '<option value="1792x1024" disabled>1792x1024</option>'}
`;

// Dropdown resolution options for dall-e-2
//Smallest size only when in Cost Saving Mode
const dalle2ResOptions = `
    <option value="">Select Resolution</option>
    <option value="256x256">256x256</option>
    ${!COST_SAVING_MODE ? '<option value="512x512">512x512</option>' : '<option value="512x512" disabled>512x512</option>'}
    ${!COST_SAVING_MODE ? '<option value="1024x1024">1024x1024</option>' : '<option value="1024x1024" disabled>1024x1024</option>'}
`;

// Template for edit mode feature form
// https://platform.openai.com/docs/api-reference/images/createEdit
const editModeForm = `
    <h2>Edit Mode</h2>
    <p>Please select the required parameters and then enter your prompt. Once you are satisfied click "generate".</p>
    <p>This feature is only supported by DALL-E 2. Image generation may take a few seconds.</p>
    <p><a href="https://platform.openai.com/docs/api-reference/images/createEdit">Please refer to OpenAI documentation for more details</a></p>
    <ul>
        <li><b>Image: </b> The base image you want the AI to edit. Must be png and less than 4MB </li>
        <li><b>Mask: </b> The base image with the "area of intrest" set to be transparent. Must be png and less than 4MB </li>
        <li><b>Resolution: </b> Set the resolution of the generated image. </li>
        <li><b>Quantity: </b> How many images the model produces. True max value is 10. In this app limitations are applied to prevent cost overspend ("Cost saving mode" max=2, else max=4)</li>
        <li><b>Prompt: </b> What image do you want the AI to create in the specified area </li>
    </ul>
    
    <div>
        <div>
            <label for="imageSelector"> Image: </label>
            <input id="imageSelector" name="image" type="file"/>
        </div>
        <div>
            <label for="maskSelector"> Image: </label>
            <input id="maskSelector" name="mask" type="file" disabled/>
        </div>
        <div>
            <label for="sizeSelector"> Resolution: </label>
            <select id="sizeSelector" name="size" disabled>
                <option value="">Select Resolution</option>
            </select>
        </div>
        <div>
            <label for="quantityField"> Quantity: </label>
            <input id="quantityField" name="n" type="number" min=1 max=4 disabled />
        </div>
        <div>
            <label for="promptField"> Prompt: </label>
            <input id="promptField" name="prompt" type="text"disabled placeholder="Enter a Prompt..."/>
        </div>
        <button id="generateBtn" disabled=true>Generate</button>
    </div>
`;

// Template for variation mode feature form
// https://platform.openai.com/docs/api-reference/images/createVariation
const variationModeForm = `
    <h2>Variation Mode</h2>
    <p>Please select the image you want to create a variation of and the size of the output image. Once you are satisfied click "generate".</p>
    <p>This feature is only supported by DALL-E 2. Image generation may take a few seconds.</p>
    <p><a href="https://platform.openai.com/docs/api-reference/images/createVariation">Please refer to OpenAI documentation for more details</a></p>
    <ul>
        <li><b>Image: </b> The base image you want the AI to edit. Must be png and less than 4MB </li>
        <li><b>Resolution: </b> Set the resolution of the generated image </li>
    </ul>
    
    <div>
        <div>
            <label for="imageSelector"> Image: </label>
            <input id="imageSelector" name="image" type="file"/>
        </div>
        <div>
            <label for="sizeSelector"> Resolution: </label>
            <select id="sizeSelector" name="size" disabled>
                <option value="">Select Resolution</option>
            </select>
        </div>
        <button id="generateBtn" disabled=true>Generate</button>
    </div>
`;
// ===============================================

//CSS
// ===============================================
//TODO: Load Css through JQuery
  $('body').css( "margin", "20px" );
  $('body').css( "padding", "20px" );
  $('#warningMessage').css( "color", "red" );
// ===============================================
  
//JavaScript

let APIKey = "";
let selectedMode = "generation";

const apiKeyBtn = document.getElementById("apikeybtn");
const apiKeyField = document.getElementById("apikeyfield");
const inputSection = document.getElementById("inputSection");
const resultsSection = document.getElementById("resultsSection");

let generationBtn = null;
let editBtn = null;
let variationBtn = null;

let modelSelector = null;
let resolutionSelector = null;
let qualitySelector = null;
let quantityField = null;
let promptField = null;

//Make request to one of the three OpenAI endpoints
// Generation: https://api.openai.com/v1/images/generations
// Edit: https://api.openai.com/v1/images/edits
// Variation: https://api.openai.com/v1/images/variations

// Ancient Brain seems to give warning for async arrow functions but code runs fine
const makeOpenAIRequest = async (url, data, contentType) => {
    const headers = {
        "Authorization": `Bearer ${APIKey}`,
    };
    
    // Adding content type for multipart forms causes 400 error 
    if (contentType) headers["Content-Type"] = contentType;
    
    console.log("Payload", data)
    // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
    const response = await fetch(url, {
        method: "POST",
        mode: "cors",
        cache: "no-cache",
        headers: headers,
        body: data
    });
    
    console.log(response);
    
    return response.json();
}

// Create JSON request from form data
const createRequestBodyJson = () => {
    const inputs = Array.from(inputSection.getElementsByTagName('input')).concat(Array.from(inputSection.getElementsByTagName('select')));
    const requestBody = {};
    
    for (let input of inputs){
        if (input.type == "number") requestBody[input.name] = parseInt(input.value);
        else requestBody[input.name] = input.value;
    }
    return JSON.stringify(requestBody);
};

// Create Multipart form request from form data
const createRequestBodyForm = () => {
     const inputs = Array.from(inputSection.getElementsByTagName('input')).concat(Array.from(inputSection.getElementsByTagName('select')));
    const requestBody = new FormData();
        
    for (let input of inputs){
        if (input.type == "file") requestBody.append(input.name, input.files[0]);
        else if (input.type == "number") requestBody.append(input.name, parseInt(input.value));
        else requestBody.append(input.name, input.value);
    }
    return requestBody;
        
};

const submitForm = (url, type) => {
    let requestBody;
    // If the api endpoint requires the body to be mutlipart form create one else use json
    if (type === 'multipart') requestBody = createRequestBodyForm();
    else requestBody = createRequestBodyJson();
        
    console.log(requestBody);
    console.log(resultsSection);
    
    // Add laoding message
    resultsSection.innerHTML = "Loading. Please wait.";
    
    //Make API request    
    makeOpenAIRequest(url, requestBody, type !== 'multipart' ? "application/json" : null)
    .then((res) => {
        console.log("response", res);
        
        let displayContent = "";
        
        const data = res.data;
        // Iterate through array of returned images
        for (let imageData of data){
            // If using DALL-E 3 there will be an enhanced prompt returned as part of the response
            if(imageData.revised_prompt){
                displayContent += `<h4>Enhanced Prompt:</h4> <p>${imageData.revised_prompt}</p>`;
            }
            // Create the display image
            displayContent += `<img src="${imageData.url}"/><br/>`;
        }
        //Render images
        resultsSection.innerHTML = displayContent;
    })
    .catch((err) => {
        console.log("error", err);
        resultsSection.innerHTML = "An error occured while making the request. Check console for details.";
    });
};

// Clears value and disables input elements
const clearInputs = (inputs) => {
    for (let input of inputs) {
        input.value = "";
        input.disabled = true;
    }
};

// Add the mode selection buttons and their respective functionality
const createModeSelectButtons = () => {
    //Create the buttons
    document.getElementById("modeSelectors").innerHTML = modeSelectBtns;
    generationBtn = document.getElementById("generationModeBtn");
    editBtn = document.getElementById("editModeBtn");
    variationBtn = document.getElementById("variationModeBtn");
    
    generationBtn.addEventListener('click', () => {
        //Disable selected button and enable the non-selected ones to create 'tab-like' functionality
        generationBtn.disabled = true;
        editBtn.disabled = false;
        variationBtn.disabled = false;
        
        //Clear and currently disabled results
        resultsSection.innerHTML = null;
        createGenerationForm();
        console.log("generation");
    });
    
    editBtn.addEventListener('click', () => {
        //Disable selected button and enable the non-selected ones to create 'tab-like' functionality
        generationBtn.disabled = false;
        editBtn.disabled = true;
        variationBtn.disabled = false;
        
        //Clear and currently disabled results
        resultsSection.innerHTML = null;
        createEditForm();
        console.log("edit");
    });
    
    variationBtn.addEventListener('click', () => {
        //Disable selected button and enable the non-selected ones to create 'tab-like' functionality
        generationBtn.disabled = false;
        editBtn.disabled = false;
        variationBtn.disabled = true;
        
        //Clear and currently disabled results
        resultsSection.innerHTML = null;
        createVaritationForm();
        console.log("variation");
    });
};

// Render and add functionality to the generation mode form 
const createGenerationForm = () => {
    //Render the form
    inputSection.innerHTML = generationModeForm;
    
    modelSelector = document.getElementById("modelSelector");
    resolutionSelector = document.getElementById("sizeSelector");
    qualitySelector = document.getElementById("qualitySelector");
    quantityField = document.getElementById("quantityField");
    promptField = document.getElementById("promptField");
    generateBtn = document.getElementById("generateBtn");
    
    modelSelector.addEventListener('input', (e) => {
        let model = e.target.value;
        console.log("Model: ", model);
        // If a non null value is input enable the next form item (Resolution)
        if (model) {
            // Clear all proceeding inputs if the value is changed as the diffrent models have diffrent allowed values in other fields 
            clearInputs([resolutionSelector, qualitySelector, quantityField, promptField, generateBtn]);
            resolutionSelector.disabled = false;
        }
        // If the value input is null clear all proceeding fields as they cannot be set until the model is selected
        else clearInputs([resolutionSelector, qualitySelector, quantityField, promptField, generateBtn]);
        
        // Render the correct resolution dropdown menu items based on selected model
        if (model === "dall-e-3") resolutionSelector.innerHTML = dalle3ResOptions;
        else if (model === "dall-e-2") resolutionSelector.innerHTML = dalle2ResOptions;
    });
    
    resolutionSelector.addEventListener('input', (e) => {
        let resolution = e.target.value;
        console.log("Resolution: ", resolution);
        // If non-null value is selected enable the next form item (Quality)
        if (resolution) qualitySelector.disabled = false;
        //If the field is set to null clear and disable all proceeding fields
        else clearInputs([qualitySelector, quantityField, promptField, generateBtn]);
        
        //Set Quality dropdown inputs.
        let options = `
            <option value="">Select Quality</option>
            <option value="standard">Standard</option>
        `;
        
        // If model is dall-e 3 add quality selector (Unsupported by Dall-e 2)
        if (modelSelector.value === "dall-e-3"){
            //If in cost saving mode grey out 'hd' option
            if (!COST_SAVING_MODE) options += `<option value="hd">HD</option>`;
            else options += `<option value="hd" disabled>HD</option>`;
        }

        console.log("quality ", qualitySelector, options, modelSelector.value, modelSelector.value === "dalle-e-3")
        qualitySelector.innerHTML = options;
    });
    
    qualitySelector.addEventListener('input', (e) => {
        let quality = e.target.value;
        console.log("Quality: ", quality);
        // If non-null value is selected enable the next form item (Quantity)
        if (quality) quantityField.disabled = false;
        //If the field is set to null clear and disable all proceeding fields
        else clearInputs([quantityField, promptField, generateBtn]);
    });
    
    quantityField.addEventListener('input', (e) => {
        let quantity = e.target.value;
        // Limit the amount of images that can be generated at once
        // DALL-E-2 Cost saving mode: Max = 2
        // DALL-E-2 Normal: Max = 4. <-- The true API limit is greater but this is imposed to prevent overspending
        // DALL-E-3 Normal: Max = 1 <-- This is a true limitation of the model and this model can only output 1 image at a time
        if (quantity < 1 || modelSelector.value === "dall-e-3"){
            quantity = 1;
            quantityField.value = 1;
        }
        if (COST_SAVING_MODE && (quantity > 2)){
            quantity = 2;
            quantityField.value = 2;
        }
        else if(quantity > 4){
            quantity = 4;
            quantityField.value = 4;
        }
        console.log("Quantity: ", quantity);
        // If non-null value is selected enable the next form item (Prompt)
        if (quantity) promptField.disabled = false;
        //If the field is set to null clear and disable all proceeding fields
        else clearInputs([promptField, generateBtn]);
    });
    
    promptField.addEventListener('input', (e) => {
        let prompt = e.target.value;
        // If non-null value is selected enable the "Generate" button allowing the user to submit the request
        if (prompt) generateBtn.disabled = false;
        else clearInputs([generateBtn]);
    });
    
    generateBtn.addEventListener('click', (e) => {
        // Submit form inputs
        submitForm('https://api.openai.com/v1/images/generations', 'json');
    });
};

// Render and add functionality to edit mode form 
const createEditForm = () => {
    //Render the edit form
    inputSection.innerHTML = editModeForm;
    
    imageSelector = document.getElementById("imageSelector");
    maskSelector = document.getElementById("maskSelector");
    resolutionSelector = document.getElementById("sizeSelector");
    quantityField = document.getElementById("quantityField");
    promptField = document.getElementById("promptField");
    generateBtn = document.getElementById("generateBtn");
    
    imageSelector.addEventListener('input', (e) => {
        let image = e.target.files[0];
        console.log("Image: ", image);
        // If non-null value is selected enable the next form item (Mask)
        if (image) {
            clearInputs([resolutionSelector, maskSelector, quantityField, promptField, generateBtn]);
            maskSelector.disabled = false;
        }
        else clearInputs([resolutionSelector, maskSelector, quantityField, promptField, generateBtn]);
    });
    
    maskSelector.addEventListener('input', (e) => {
        let mask = e.target.files[0];
        console.log("Mask: ", mask);
        // If non-null value is selected enable the next form item (Resolution)
        if (mask) {
            clearInputs([resolutionSelector, quantityField, promptField, generateBtn]);
            resolutionSelector.disabled = false;
        }
        else clearInputs([resolutionSelector, quantityField, promptField, generateBtn]);
        
        resolutionSelector.innerHTML = dalle2ResOptions;
    });
    
    resolutionSelector.addEventListener('input', (e) => {
        let resolution = e.target.value;
        console.log("Resolution: ", resolution);
        // If non-null value is selected enable the next form item (Quantity)
        if (resolution) quantityField.disabled = false;
        else clearInputs([quantityField, promptField, generateBtn]);
    });
    
    quantityField.addEventListener('input', (e) => {
        // Limit the amount of images that can be generated at once
        // Cost saving mode: Max = 2
        // Normal: Max = 4. <-- The true API limit is greater but this is imposed to prevent overspending
        let quantity = e.target.value;
        if (quantity < 1){
            quantity = 1;
            quantityField.value = 1;
        }
        if (COST_SAVING_MODE && (quantity > 2)){
            quantity = 2;
            quantityField.value = 2;
        }
        else if(quantity > 4){
            quantity = 4;
            quantityField.value = 4;
        }
        console.log("Quantity: ", quantity);
        // If non-null value is selected enable the next form item (Prompt)
        if (quantity) promptField.disabled = false;
        else clearInputs([promptField, generateBtn]);
    });
    
    promptField.addEventListener('input', (e) => {
    let prompt = e.target.value;
    // If non-null value is selected enable the "Generate" button allowing the user to submit the request
    if (prompt) generateBtn.disabled = false;
    else clearInputs([generateBtn]);
    });
    
    generateBtn.addEventListener('click', (e) => {
        // Submit form inputs
        submitForm('https://api.openai.com/v1/images/edits', 'multipart');
    });
};

// Render and add functionality to edit mode form 
const createVaritationForm = () => {
    // Render the variations form
     inputSection.innerHTML = variationModeForm;
     
    imageSelector = document.getElementById("imageSelector");
    resolutionSelector = document.getElementById("sizeSelector");
    generateBtn = document.getElementById("generateBtn");
     
     imageSelector.addEventListener('input', (e) => {
        let image = e.target.files[0];
        console.log("Image: ", image);
        // If non-null value is selected enable the next form item (Resolution)
        if (image) {
            clearInputs([resolutionSelector, generateBtn]);
            resolutionSelector.disabled = false;
        }
        else clearInputs([resolutionSelector, generateBtn]);
        
        resolutionSelector.innerHTML = dalle2ResOptions;
    });
    
    resolutionSelector.addEventListener('input', (e) => {
        let resolution = e.target.value;
        console.log("Resolution: ", resolution);
        // If non-null value is selected enable the "Generate" button allowing the user to submit the request
        if (resolution) generateBtn.disabled = false;
        else clearInputs([generateBtn]);
    
    });
    
    generateBtn.addEventListener('click', (e) => {
        // Submit form inputs
        submitForm('https://api.openai.com/v1/images/variations', 'multipart');
    });
};

// Save the user submitted API Key for use in requests
const setAPIKey = (e) => {
    e.preventDefault();
    const apiKeyField = document.getElementById("apikeyfield");
    APIKey = apiKeyField.value;
    console.log("Set API Key: ", apiKeyField.value);
    
    createModeSelectButtons();
    createGenerationForm();
};

//"Set Api Key" button event listener
apiKeyBtn.addEventListener('click', setAPIKey);
//Onlt enable the "Set Api Key" button when the user has input something in the field
apiKeyField.addEventListener('input', (e) => {
    if (e.target.value) apiKeyBtn.disabled = false;
    else apiKeyBtn.disabled = true;
});