Code viewer for World: AI Generated Books

// 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

const HUGGING_FACE_API_KEY = "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";
const OLLAMA_API_KEY = "FsiDrvg68bOMUVMPuj77jHAWwryknSf";

class ImageGenerator {
  constructor() {
    this.apiKey = HUGGING_FACE_API_KEY;
  }

  /** @param {string} prompt */
  async inference(prompt) {
    if (this.apiKey === "") {
      throw {
        name: 'ApiKeyNotSet',
        message: 'Api key not set !'
      };
    }

// MH edit
console.log ( "Image prompt: " );
console.log ( prompt );


    const response = await fetch(
      "https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5",
      {
        headers: { Authorization: `Bearer ${this.apiKey}` },
        method: "POST",
        body: JSON.stringify(prompt),
      }
    );

    const result = await response.blob();
    return result;
  }

  /** @param {string} prompt */
  async generateImage(prompt, onDownload) {
    this.inference({"inputs": prompt}).then(blob => {
      const url = URL.createObjectURL(blob);
      const img = document.createElement("img");
      img.src = url;

      return img;
    }).then(img => onDownload(img));
  }
}

class BookWriter {
  constructor() {
    this.apiKey = OLLAMA_API_KEY;
  }

  /** @param {string} prompt */
  async inference(prompt) {
    if (this.apiKey === "") {
      throw {
        name: 'ApiKeyNotSet',
        message: 'Api key not set !'
      };
    }


// MH edit
console.log ( prompt );


    const response = await fetch(
      "https://ai.cathal.xyz/api/generate",
      {
        headers: {
          "Accept": "application/json",
          "X-API-Key": this.apiKey,
        },

        method: "POST",
        body: JSON.stringify(prompt),
      }
    );

    return response;
  }

  /**
   * @param {string} prompt
   * @param {function (string): void} callback - the api slowly responds with tokens of the response, this callback will be called on each token
   */
  generateBook(prompt, callback) {
    this.inference({"model": "author", "prompt": prompt})
      .then(res => this.forEachToken(res, callback))
      .catch(err => console.error(err));
  }

  /** @ref https://github.com/ollama-ui/ollama-ui/blob/main/api.js#L71 */
  forEachToken(res, callback) {
    let rdr = res.body.getReader();
    let decoder = new TextDecoder("utf-8");

    let part = "";

    rdr.read()
      .then(function pump({ done, value }) {
        if (done) {
          return;
        };

        const chunk = decoder.decode(value);
        const lines = (part + chunk).split("\n");
        part = lines.pop();

        for (const line of lines) {
          if (line.trim() === "") continue;
          const parsedRes = JSON.parse(line);
          callback(parsedRes.response);
        }

        if (part.trim() !== "") {
          const parsedRes = JSON.parse(part);
          callback(parsedRes.response);
        }

        return rdr.read().then(pump);
      })
      .catch(err => console.error(err));
  }
}

// This is how we emulate different pages, this cycles "states" which are just ids, at each
// _nextState_ all other elements are hidden and the _nextState_ element is shown.
class BookResult {
  constructor() {
    this.nextState = 1;
    this.states = ["book-prompting", "book-loading", "book-showing"];
  }

  /** @param {HTMLElement} el */
  hide(el) {
    el.style.display = "none";
  }

  /** @param {HTMLElement} el */
  show(el) {
    el.style.display = "block";
  }

  setNextState() {
    const currState = this.nextState;

    this.states.forEach((state, i) => {
      let stateEl = document.getElementById(state);

      if (i == currState) {
        this.show(stateEl);
      } else this.hide(stateEl);
    })

    this.nextState = (currState + 1) % this.states.length;
  }
}

function bookPromptHandler(ev) {
  ev.preventDefault();


  const prompt = document.getElementById("book-prompt");
  const promptValue = prompt.value;
  prompt.value = "";
  
  if (promptValue == "") {
      return; // do nothing if they enter nothing
  }
 
  let manager = new BookResult();
  manager.setNextState(); // this will move to the loading screen

  let title = document.getElementById("book-title");
  let blurb = document.getElementById("book-blurb");
  const author = new BookWriter();

  const cover = document.getElementById("book-cover");
  const artist = new ImageGenerator();

  let errorShown = false;
  let parsedTitleAndBlurb = false;
  let buffer = ""; // this "buffer" is needed to store the text until we have the tokens "Title:" and "Blurb:" to determine what 

  author.generateBook(promptValue, async (token) => {
    let components = buffer.split(/Title:|Blurb:/);

    if (!parsedTitleAndBlurb) {
      if (components.length < 3) {
        buffer += token;
        return;
      }

      parsedTitleAndBlurb = true;
      manager.setNextState(); // this will move to display of book page
      cover.innerHTML = "";

      let btn = document.getElementById("back-button");
      btn.onclick = () => {
        manager.setNextState(); // this will wrap back to the first state (form input)
      }

      title.innerHTML = components[1];
      blurb.innerHTML = components[2];

      // Generate the book cover once the title is parsed
      artist.generateImage(title.innerHTML, async (img) => {
        img.style["border-radius"] = "0.25rem";
        cover.replaceChildren(img);
      });
    }
    blurb.innerHTML += token;
  });
}

function html() {
  document.write ( `<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body>
		<style>
			.reset-font {
			  font-size: 1rem; /* 16px */
			  line-height: 1.5rem;
				font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
			}
		</style>

		<div class="reset-font h-full w-full flex flex-row justify-center bg-orange-100">
			<main class="flex flex-col w-8/12 justify-center items-center">
				<div id="book-prompting">
					<form onsubmit='bookPromptHandler(event);' class="flex flex-col items-center">
					    <!-- adjusted @ref https://tailwindcss.com/docs/animation#bounce -->
						<div class="animate-bounce p-2 w-10 h-10 flex items-center justify-center">
							<svg class="w-6 h-6 text-gray-500" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
								<path d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
							</svg>
						</div>
						
                        <!-- adjusted @ref https://tailwindui.com/components/application-ui/forms/form-layouts -->
						<div class="sm:col-span-3">
							<div class="mt-2">
								<input type="text" name="book-prompt" id="book-prompt" class="block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-md ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" />
							</div>
							<p class="mt-3 text-sm leading-6 text-gray-500">Enter short description of the book to be written</p>
						</div>
						<button id="book-prompt-button" type="submit" class="font-lg m-3 rounded bg-sky-200 hover:bg-sky-300 transition-color duration-200 w-full p-2 text-gray-600">Create book</button>
					</form>
				</div>


				<div id="book-loading" class="flex flex-col" style="display: none;">
				    <!-- modified @ref https://tailwindcss.com/docs/animation#pulse -->
					<div class="rounded shadow-md p-5 bg-white">
						<div class="animate-pulse flex flex-row space-x-4">
							<div class="rounded bg-slate-200 col-span-1" style="height: 256px; width: 256px;"></div>
							<div class="flex-1 space-y-6 py-1 col-span-2" style="width: 28vh">
								<div class="h-2 bg-slate-200 rounded"></div>
								<div class="space-y-3">
									<div class="grid grid-cols-3 gap-4">
										<div class="h-2 bg-slate-200 rounded col-span-2"></div>
										<div class="h-2 bg-slate-200 rounded col-span-1"></div>
									</div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
									<div class="h-2 bg-slate-200 rounded"></div>
								</div>
							</div>
						</div>
					</div>
					<p class="mt-3 text-sm leading-6 text-gray-500 text-center">This can take up to 30s; it is running on an old thinkcentre &#128517;</p>
				</div>

				<div id="book-showing" style="display: none;">
					<div class="rounded shadow-md p-5 bg-white">
						<div class="flex flex-row space-x-4 items-center">
							<div id="book-cover" style="width: 256px; height: 256px;"></div>
							<div class="space-y-3" style="width: 28vh;">
								<h1 id="book-title" class="font-bold text-base text-gray-700"></h1>
								<p id="book-blurb" class="text-sm text-gray-600"></p>
							</div>
						</div>
					</div>

					<button id="back-button" type="button" class="font-lg mt-3 rounded bg-sky-200 hover:bg-sky-300 transition-color duration-200 p-2 text-gray-600 flex flex-row gap-1">
					    <!-- @ref https://heroicons.com/ -->
						<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
							<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
						</svg>
						<p>Go back</p>
					</button>
				</div>
			</main>
		</div>
  </body>
</html>
` )
}

function main() {
  html();
}

main();