// 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 😅</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();