// Use JS to write whatever HTML and data you want to the page // One way of using JS to write HTML and data is with a multi-line string// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals// Tweaker's Box// =============================================================================// https://platform.openai.com/docs/guides/images/introduction?context=node// Options for Models// "dall-e-2" - DALL-E 2 Model// "dall-e-3" - DALL-E 3 Modelconst MODEL ="dall-e-3";// Set the resolution of the image returned// Dalle 2 -> "256x256", "512x512", "1024x1024"// Dalle 3 -> "1024x1024", "1024x1792", "1792x1024"const RESOLUTION = MODEL ==="dall-e-3"?"1024x1024":"256x256";// Set image quality// Options// "standard"// "hd" (Will fucntion for both models but only provides meaning for DALLE-3)const QUALITY ="standard";//Options for mode// "prompt" - Uses the user supplied prompt to create both the image and text// "image" - Only supported by DALL-E 3. Use the enhanced prompt used to create the image to create the accompanying story// "story" - Uses the story created by the langauge model as the input to the image model// WARNING - Only "prompt" can make requests in sync. This leads to much longer load times for "image" and "story" const MODE ="story";// The word limit given to the llm i.e. "generate me a story about a duck who get lost in no more than <WORD_LIMIT> words"const WORD_LIMIT =180;// https://platform.openai.com/docs/guides/text-generation/completions-api// https://ancientbrain.com/world.php?world=2850716357// Select the LLM Modelconst LLM_MODEL ="gpt-3.5-turbo";// Control LLM Creativity 0-2// 0 is the least creative// 2 is the most creativeconst TEMPERATURE =1;const OPENAI_GENERATION_URL ="https://api.openai.com/v1/images/generations";const OPENAI_CHAT_URL ="https://api.openai.com/v1/chat/completions";// =============================================================================// Inital page load with cover/apikey form
document.write (`<div class="content-container"><div class="title-block"><h1>Generative AI -InfiniteStoryBook</h1><p>Welcome to the “InfiniteStoryBook”.To start your journey, enter a valid OpenAI API key and click the arrow (Or press Enter) to proceed.Enter your story idea in the input on the left page.Once your happy, click the "Generate" and watch as your story comes to life.Flip to the next page and repeat as long as your heart desires.</p><p>This page uses OpenAI’s ImageGeneration and TextGenerationAPIs.</p></div><div id="book-block"class="book-block"><div class="cover-background"><div class="cover-background-pages"></div><div class="cover-accent-line"></div><div class="cover"><div class="cover-content"><div class="cover-title">InfiniteStories</div><div class="apikey-form"><input id="apikey-input"class="apikey-input" placeholder="Enter Your API Key..."/><button id="apikey-submit"class="apikey-submit">></button></div></div></div></div></div><div class="details-block"><h4>Details:</h4><p>Model: ${MODEL}<br />Mode: ${MODE}</p></div></div>`);// HTML template for the open book layoutconst openBookTemplate =`<div class="book-background"><div id="text-page"class="text-page"><h2 id="page-title"></h2><div id="page-content"></div></div><div class="image-page"><div id="image-content"></div></div><button id="page-back"class="page-btn" disabled><</button><button id="page-forward"class="page-btn" disabled>></button><div id="left-page-number">1</div><div id="right-page-number">2</div><svg class="bookmark"><rect x="0" width="35" height="40" fill="#BFBB4E"/><polygon points="0 40, 35 40, 0 75" fill="#BFBB4E"/></svg></div>`;// Form template for user prompt inputconst promptForm =`<div><input id="prompt-field" name="prompt-field" type="text" placeholder="Enter Prompt"/><button id="prompt-submit" disabled>Generate</button></div>`;classPage{
constructor(title, text, imageURL){this.title = title;this.text = text;this.imageURL = imageURL;}}//Pagination logic varibalesconst pages =[];
let pageCount =0;
let backBtn;
let forwardBtn;
let apiKey ="";
let loading =false;//API Form inputsconst keySubmitBtn = document.getElementById("apikey-submit");const keyField = document.getElementById("apikey-input");// Create HTTP Header in the format required for both OpenAI API'sconst getOpenAIHeader =()=>{return{"Authorization":`Bearer ${apiKey}`,"Content-Type":"application/json"};};// Build and Fetch request to Image Generation API: https://api.openai.com/v1/images/generations
async function makeDALLERequest(prompt){const headers = getOpenAIHeader();const requestBody ={"model": MODEL,"prompt": prompt,"n":1,"size": RESOLUTION,"quality": QUALITY
};
console.log("DALLE Request Payload: ", requestBody);const response = await fetch(OPENAI_GENERATION_URL,{
method:"POST",
mode:"cors",
cache:"no-cache",
headers: headers,
body: JSON.stringify(requestBody)});
json_res = await response.json();
console.log("DALLE Reponse:", json_res);// API returns array of images (to handle n>1) In this app n=1 so just return the firstreturn json_res.data[0];}// Build and Fetch request to Text Generation API: https://api.openai.com/v1/chat/completions
async function makeLLMRequest(prompt){const headers = getOpenAIHeader();const requestBody ={"model": LLM_MODEL,"messages":[{"role":"user","content": prompt
}],"temperature": TEMPERATURE
};
console.log("LLM Request Payload: ", requestBody);const response = await fetch(OPENAI_CHAT_URL,{
method:"POST",
mode:"cors",
cache:"no-cache",
headers: headers,
body: JSON.stringify(requestBody)});
json_res = await response.json();
console.log("LLM Reponse:", json_res);return json_res.choices[0];}// Make the requests to the LLM and Image generation with irder and prompt depending on which mode is selected
async function makeOpenAIRequest(prompt){// image modeif(MODE ==="image"&& MODEL =="dall-e-3"){const image_response = await makeDALLERequest(`Create an image of ${prompt}`);const llm_response = await makeLLMRequest(`Write a short story to accompany an image created using the prompt "${image_response.revised_prompt}" in no more than ${WORD_LIMIT} words`);return{
url: image_response.url,
text: llm_response.message.content,
title: prompt
};}// story modeelseif(MODE ==="story"&& MODEL =="dall-e-3"){const llm_response = await makeLLMRequest(`Write a short story about ${prompt} in no more than ${WORD_LIMIT} words`);const image_response = await makeDALLERequest(`Create an image to accompany the story "${llm_response.message.content}"`);return{
url: image_response.url,
text: llm_response.message.content,
title: prompt
};}// prompt modeelse{const[image_response, llm_response]= await Promise.all([makeDALLERequest(`Create an image to accompany a story about ${prompt}`), makeLLMRequest(`Write a short story about ${prompt} in no more than ${WORD_LIMIT} words`)]);return{
url: image_response.url,
text: llm_response.message.content,
title: prompt
};}}// Build the template for image page with image src includedconst createImagePage =(url)=>{return`<img src=${url} height="375px", width="375px"/><div class="tape left-tape"></div><div class="tape right-tape"></div>`;};// Create a pageconst createNewPage =(prompt)=>{
console.log("Creating New Page with Prompt: ", prompt);
loading=true;
document.getElementById("page-content").innerHTML ="Generating Story. This may take a few minutes.";// Get title, text and image url from OpenAI APIs
makeOpenAIRequest(prompt).then((res)=>{
console.log("Creating New Page: ", res);// Add page to pagination list
pages.push(newPage(res.title, res.text, res.url));
loading=(false);// Redraw current page
changePage(0);}).catch((err)=>{
console.log("Request Error: ", err);// Refraw page and Notify user of error
loading=(false);
changePage(0)
document.getElementById("page-content").innerHTML +="<p>An error occured in one or more of the APIs. See console for details.</p>";});};// Render Page to screen based of pageCount varibleconst drawPage =()=>{// Get the data from the paginiation list at index pageCountconst pageData = pages[pageCount];// Get page numbers
document.getElementById("left-page-number").innerHTML = pageCount*2+1;
document.getElementById("right-page-number").innerHTML = pageCount*2+2;//If there is existing data about the page display itif(pageData){
document.getElementById("page-title").innerHTML = pageData.title;
document.getElementById("page-content").innerHTML = pageData.text;
document.getElementById("image-content").innerHTML = createImagePage(pageData.imageURL);}// If the page is currently loading display laoding message instead of input promptelseif(loading){
document.getElementById("page-title").innerHTML ="Tell Me A Story About...";
document.getElementById("page-content").innerHTML ="Generating Story. This may take a few minutes.";
document.getElementById("image-content").innerHTML ="";}// If no data exisits about the current page display prompt formelse{
document.getElementById("page-title").innerHTML ="Tell Me A Story About...";
document.getElementById("page-content").innerHTML = promptForm;
document.getElementById("image-content").innerHTML ="";const promptField = document.getElementById("prompt-field");const promptSubmitBtn = document.getElementById("prompt-submit");// Only enable "Generate" button after user has types
promptField.addEventListener("input",(e)=>{if(e.target.value) promptSubmitBtn.disabled =false;else promptSubmitBtn.disabled =true;});// https://stackoverflow.com/questions/155188/trigger-a-button-click-with-javascript-on-the-enter-key-in-a-text-box// Enable the use of Enter key to sumbit the prompt
promptField.addEventListener("keyup",(e)=>{
event.preventDefault();if(event.keyCode ===13&&!promptSubmitBtn.disabled){
promptSubmitBtn.click();}});
promptSubmitBtn.addEventListener("click",()=>{
createNewPage(promptField.value);});}};// Handles pagination increase/decreaseconst changePage =(n)=>{// If not on the page after the last story allow for increase in page countif(pageCount != pages.length || n <0){
pageCount += n;}// If on the page after the last story (prompt page) disable the forward page buttonif(pageCount == pages.length) forwardBtn.disabled =true;else forwardBtn.disabled =false;// If on the fist page siable the back page buttonif(pageCount <=0) backBtn.disabled =true;else backBtn.disabled =false;//Render the page
drawPage();};// Swap from cover templae to openBook templaeconst openBook =()=>{
console.log("Opening Book");
document.getElementById("book-block").innerHTML = openBookTemplate;//Add pagination button functionality
backBtn = document.getElementById("page-back");
forwardBtn = document.getElementById("page-forward");
backBtn.addEventListener("click",()=>{changePage(-1)});
forwardBtn.addEventListener("click",()=>{changePage(1)});//https://stackoverflow.com/questions/41303787/use-arrow-keys-as-event-listener//Allow for pagination using the left and right arrow keys
document.addEventListener('keyup',function(e){var code = e.which || e.keyCode;if(code =='37'&&!backBtn.disabled){
backBtn.click();}elseif(code =='39'&&!forwardBtn.disabled){
forwardBtn.click();}});// Render page
drawPage();};// Api Key submit button event listener
keySubmitBtn.addEventListener("click",()=>{if(keyField.value){
console.log("Setting API Key: ", keyField.value);
apiKey = keyField.value;
openBook();}});// https://stackoverflow.com/questions/155188/trigger-a-button-click-with-javascript-on-the-enter-key-in-a-text-box//Allow for the api key submit using Enter key
keyField.addEventListener("keyup",function(event){
event.preventDefault();if(event.keyCode ===13){
keySubmitBtn.click();}});// CSS// Add fonts
$('head').append(`<link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'>`);
$('head').append(`<link href='https://fonts.googleapis.com/css?family=Inknut Antiqua' rel='stylesheet'>`);// Style page
$('head').append(`<style>.content-container{
padding:0px20px;
width: calc(100vw-40px);
height: calc(100vh-40px);
display: flex;
flex-direction: column;
overflow:auto;}.title-block{
max-width:800px;
font-family:Inter;}.book-block{
flex-grow:1;
display: flex;
align-items: center;
justify-content: center;}.details-block{
font-family:Inter;}.cover-background{
height:610px;
width:460px;
background-color:#5B0808;
position: relative;
z-index:1;
border-radius:0px10px10px0px;}.cover-background-pages{
height: calc(100%-12px);
width: calc(100%-12px);
background-color:#F6F1C3;
position: absolute;
left:6;
top:6;
z-index:2;
border-radius:0px10px10px0px;}.cover-accent-line{
height:600px;
width:6px;
background-color:#BFBB4E;
position: absolute;
left:25;
z-index:4;}.cover{
height:600px;
width:460px;
background-color:#901414;
position: absolute;
z-index:3;
border-radius:0px10px10px0px;
display: flex;
font-family:InknutAntiqua;
color:#BFBB4E;
justify-content: center;}.cover-content{
height:300px;
display: flex;
flex-direction: column;
margin-top:100px;
justify-content: space-between;}.cover-title{
font-size:40px;}.apikey-form{
display: flex;
justify-content: center;}.apikey-input{
outline:0;
border-width:003px;
border-color:#BFBB4E;
background-color:#901414;
color:#BFBB4E;}.apikey-submit{
outline:0;
border: none;
background-color:#901414;
color:#BFBB4E;
font-size:20;
font-weight:900;
cursor: pointer;}.book-background{
height:600px;
width:920px;
background-color:#5B0808;
position: relative;
z-index:1;
border-radius:10px;}.text-page{
height:580px;
width:450px;
background-color:#F6F1C3;
position: absolute;
z-index:1;
border-radius:10px0px0px10px;
top:10;
left:10;
box-shadow:-30px0px20px0px rgba(0,0,0,0.25) inset;
padding:0px10px;}#page-title{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-transform: capitalize;}#page-content{
height:470px;
max-width:430px;
overflow-x:auto;}.image-page{
height:580px;
width:450px;
background-color:#F6F1C3;
position: absolute;
z-index:1;
border-radius:0px10px10px0px;
top:10;
left:460;
box-shadow:10px0px20px0px rgba(0,0,0,0.25) inset;
display: flex;
align-items: center;
justify-content: center;}#image-content{
height:450px;
width:450px;
position: relative;
z-index:5;
display: flex;
justify-content: center;
align-items: center;}.image-page{
height:580px;
width:450px;
background-color:#F6F1C3;
position: absolute;
z-index:1;
border-radius:0px10px10px0px;
top:10;
left:460;
box-shadow:10px0px20px0px rgba(0,0,0,0.25) inset;}.page-btn{
background-color: rgba(0,0,0,0);
outline:0;
border: none;
position: absolute;
z-index:2;
bottom:20;
font-size:20;
cursor: pointer;}#page-back{
left:20;}#page-forward{
right:20;}#left-page-number{
position: absolute;
bottom:20;
left:225;
z-index:10;}#right-page-number{
position: absolute;
bottom:20;
right:225;
z-index:10;}.bookmark{
position: absolute;
top:0;
right:150;
z-index:10;}.tape{
height:25px;
width:80px;
background-color: rgba(217,217,217,0.7);
z-index:10;
position: absolute;
transform: rotate(-45deg);}.left-tape{
top:45;
left:15;}.right-tape{
bottom:45;
right:15;}</style>`);