ai-app-factory / public /index.html
jbilcke-hf's picture
jbilcke-hf HF Staff
fix
7510941
<html>
<head>
<title>AI App Factory 🏭</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.css" rel="stylesheet" type="text/css" />
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/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'"
>
AI App 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 Hugging Face Spaces using a LLM.</p>
<p>The Space will be created under your name, so please provide a valid Hugging Face token (with <span class="font-bold font-mono text-sm text-red-900">WRITE</span> access).</p>
</div>
<input
name="token"
type="password"
x-model="token"
placeholder="Your Hugging Face token"
class="input w-full rounded text-stone-800 bg-stone-50 border-2 border-stone-400 font-mono text-md md:text-lg"
/>
<textarea
name="promptDraft"
x-model="promptDraft"
rows="10"
placeholder="Describe your web app (eg. tictactoe web game)"
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 todo list app using nextjs" class="text-bold underline">todo list using nextjs</a>,
<a href="/?prompt=a simple html page for a cookie recipe" class="text-bold underline">cookie recipe</a>
</p>
<button
class="btn disabled:text-stone-400"
@click="open = true, saveToken(token), 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/deepseek-ai/DeepSeek-V3-0324" class="underline" target="_blank">
deepseek-ai/DeepSeek-V3-0324
</a>
</p>
<p>Est. 2023</p>
<p class="text-stone-700" x-show="state === 'loading'">
Waiting for the generation 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 7000 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="getIframeSource()"
></iframe>
<div
x-show="state !== 'stopped' && !spaceUrl"
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
x-show="state === 'stopped' && spaceUrl && !spaceUrlReady"
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-yellow-200 text-yellow-800 rounded-md shadow-md">
<div class="mr-1">⏳</div>
<div>Space created! Waiting for it to be ready...</div>
</div>
</div>
<div
x-show="state === 'stopped' && spaceUrl && spaceUrlReady"
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-green-200 text-green-800 rounded-md shadow-md">
<div class="mr-1">πŸš€</div>
<div>Space created successfully!</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() {
const storageKey = "SPACE_FACTORY_ACCESS_TOKEN"
return {
open: false,
promptDraft:
new URLSearchParams(window.location.search).get("prompt") || '',
prompt: "",
token: localStorage.getItem(storageKey) || "",
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(),
spaceUrl: null,
saveToken(token) {
localStorage.setItem(storageKey, token)
},
spaceUrlReady: false,
getIframeSource() {
if (!this.open) {
return '/placeholder.html';
}
if (this.spaceUrl && this.spaceUrlReady) {
return this.spaceUrl;
}
if (this.spaceUrl && !this.spaceUrlReady) {
return '/placeholder.html';
}
return `/app?prompt=${encodeURIComponent(this.prompt)}&token=${encodeURIComponent(this.token)}`;
},
checkForSpaceUrl(html) {
if (!html) return null;
// Match both regular and HTML-escaped space-url tags
const match = html.match(/(?:<space-url>|&lt;space-url&gt;)(?:\s*)?(https?:\/\/[^\s<&]+)(?:\s*)?(?:<\/space-url>|&lt;\/space-url&gt;)/i);
// console.log("Space URL regex check:", match);
return match ? match[1] : null;
},
init() {
setInterval(() => {
if (this.state === "stopped") {
this.lastTokenAt = +new Date();
return;
}
const iframeDocument = document?.getElementById("iframe")?.contentWindow?.document;
const html = iframeDocument?.documentElement?.outerHTML;
// Log a sample of the HTML content to debug space URL detection
if (html && html.includes('space-url')) {
console.log("Found space-url in HTML content:", html.substring(html.indexOf('space-url') - 50, html.indexOf('space-url') + 150));
}
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;
// Check if the space URL is in the response
const detectedSpaceUrl = this.checkForSpaceUrl(html);
// console.log("detectedSpaceUrl:", detectedSpaceUrl);
if (detectedSpaceUrl) {
console.log("Space URL detected:", detectedSpaceUrl);
this.spaceUrl = detectedSpaceUrl;
this.state = "stopped";
// Add 3-second delay before loading the space URL
console.log("Waiting 3 seconds before loading the space...");
setTimeout(() => {
console.log("Space URL is now ready to load");
this.spaceUrlReady = true;
}, 3000);
}
}
// Try to detect the space URL even if the size hasn't changed
// as it might be added at the end without changing overall size much
if (!this.spaceUrl && html) {
const retryDetectedSpaceUrl = this.checkForSpaceUrl(html);
if (retryDetectedSpaceUrl) {
console.log("Space URL detected on retry:", retryDetectedSpaceUrl);
this.spaceUrl = retryDetectedSpaceUrl;
this.state = "stopped";
// Add 3-second delay before loading the space URL
console.log("Waiting 3 seconds before loading the space...");
setTimeout(() => {
console.log("Space URL is now ready to load");
this.spaceUrlReady = true;
}, 3000);
}
}
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>