<html>
  <head>
    <title>Webapp Factory 🏭</title>
    <link href="https://cdn.jsdelivr.net/npm/daisyui@3.1.6/dist/full.css" rel="stylesheet" type="text/css" />
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.2/iframeResizer.contentWindow.min.js"></script>
  </head>
  <body>
    <div class="flex flex-col md:flex-row" x-data="app()" x-init="init()">
      <div
        class="hero md:h-screen bg-stone-100 transition-[width] delay-150 ease-in-out"
        :class="open ? 'w-full md:w-2/6' : 'w-full md:w-6/6'"
      >
        <div class="hero-content text-center">
          <div class="flex flex-col w-full md:max-w-xl space-y-3 md:space-y-6">
            <h1
              class="font-bold text-stone-600 mb-1 md:mb-3 transition-all delay-150 ease-in-out"
              :class="open
              ? 'text-2xl md:text-3xl lg:text-4xl'
              : 'text-2xl md:text-3xl lg:text-6xl'"
            >
              Webapp Factory 🏭
            </h1>
            <div
              class="py-1 md:py-2 space-y-2 md:space-y-3 text-stone-600 transition-all delay-150 ease-in-out"
              :class="open
              ? 'text-lg lg:text-xl'
              : 'text-lg lg:text-xl'"
            >
              <p>A space to generate tiny web apps.</p>
              <p>In case of hallucination try generating again 🎲</p>
            </div>
            <textarea
              name="promptDraft"
              x-model="promptDraft"
              rows="10"
              placeholder="Describe your web app"
              class="input w-full rounded text-stone-800 bg-stone-50 border-2 border-stone-400 font-mono text-md md:text-lg h-24 md:h-48"
            ></textarea>
            <p class="py-1 md:py-2 text-stone-700 text-italic">
              Examples:

              <a href="/?prompt=a simple page to compute the BMI using metric units" class="text-bold underline">compute my BMI</a>,
              <a href="/?prompt=app listing various types of savanna animals, with their photos" class="text-bold underline">photos of savanna animals</a>
            </p>
            <button
              class="btn disabled:text-stone-400"
              @click="open = true, prompt = promptDraft, state = state === 'stopped' ? 'loading' : 'stopped', state === 'streaming' ? stopGeneration() : true"
              :class="promptDraft.length < minPromptSize ? 'btn-neutral' : state === 'stopped' ? 'btn-accent' : 'btn-warning'"
              :disabled="promptDraft.length < minPromptSize"
            >
              <span x-show="promptDraft.length < minPromptSize">Prompt too short to generate</span>
              <span x-show="promptDraft.length >= minPromptSize && state !== 'stopped'">Stop now</span>
              <span x-show="promptDraft.length >= minPromptSize && state === 'stopped'">Generate!</span>
            </button>
            <div class="flex flex-col text-stone-700 space-y-1 md:space-y-2">
              <p class="text-stone-700">
                Model used:
                <a href="https://huggingface.co/WizardLM/WizardCoder-15B-V1.0" class="underline" target="_blank">
                  WizardCoder-15B-1.0
                </a>
              </p>
              <p>Powered by 🤗 <a href="https://huggingface.co/inference-endpoints" class="underline" target="_blank">Inference Endpoints</a></p>
              <p class="text-stone-700" x-show="state === 'loading'">
                Waiting for the stream to begin (might take a few minutes)..
              </p>
              <p class="text-stone-700" x-show="state === 'streaming'">
                Content size: <span x-text="humanFileSize(size, true, 2)"></span>. This version generates up
                to 1686 tokens.
              </p>
            </div>
          </div>
        </div>
      </div>
      <div
        class="flex flex-col transition-[width] delay-150 ease-in-out md:h-screen"
        :class="open ? 'w-full md:w-4/6' : 'w-full md:w-0'"
      >
        <iframe
          id="iframe"
          class="border-none w-full md:min-h-screen"
          :src="!open
            ? '/placeholder.html'
            : `/app?prompt=${encodeURIComponent(prompt)}`
          "
        ></iframe>

        <div
          x-show="state !== 'stopped'"
          class="flex w-full -mt-20 items-end justify-center pointer-events-none">
          <div class="flex flex-row py-3 px-8 text-center bg-stone-200 text-stone-600 rounded-md shadow-md">
            <div class="animate-bounce duration-150 mr-1">🤖</div>
            <div>Generating your app..</div>
          </div>
        </div>
      </div>
    </div>
    <script>
      /**
       * Format bytes as human-readable text.
       *
       * @param bytes Number of bytes.
       * @param si True to use metric (SI) units, aka powers of 1000. False to use
       *           binary (IEC), aka powers of 1024.
       * @param dp Number of decimal places to display.
       *
       * @return Formatted string.
       */
      function humanFileSize(bytes, si = false, dp = 1) {
        const thresh = si ? 1000 : 1024;

        if (Math.abs(bytes) < thresh) {
          return bytes + " B";
        }

        const units = si
          ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
          : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
        let u = -1;
        const r = 10 ** dp;

        do {
          bytes /= thresh;
          ++u;
        } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);

        return bytes.toFixed(dp) + " " + units[u];
      }

      function stopGeneration() {
        console.log("stopping generation..");
        document?.getElementById("iframe")?.contentWindow?.stop?.();
      }

      function app() {
        return {
          open: false,
          promptDraft:
            new URLSearchParams(window.location.search).get("prompt") || '',
          prompt: "",
          size: 0,
          minPromptSize: 16, // if you change this, you will need to also change in src/index.mts
          timeoutInSec: 15, // time before we determine something went wrong
          state: "stopped",
          lastTokenAt: +new Date(),
          init() {
            setInterval(() => {
              if (this.state === "stopped") {
                this.lastTokenAt = +new Date();
                return;
              }
              const html = document?.getElementById("iframe")?.contentWindow?.document?.documentElement?.outerHTML;
              const size = Number(html?.length); // count how many characters we have generated

              if (isNaN(size) || !isFinite(size)) {
                this.size = 0;
                this.state = "loading";
                return;
              }

              this.state = "streaming";

              const now = +new Date();
              const newSize = new Blob([html]).size;
              const hasChanged = newSize !== this.size;

              if (hasChanged) {
                this.lastTokenAt = now;
              }

              this.size = newSize;
     
              const timeSinceLastUpdate = (now - this.lastTokenAt) / 1000;

              if (timeSinceLastUpdate > this.timeoutInSec) {
                console.log(`no changes detected for the past ${this.timeoutInSec} seconds -> considering we're done`);
                this.state = "stopped";
              }
            }, 100);
          },
        };
      }
    </script>
  </body>
</html>