// 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;
});