SIMCEEHALO's picture
Add 2 files
e3532f1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Image Generator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.model-card {
transition: all 0.3s ease;
}
.model-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.tab-button.active {
border-bottom: 2px solid #8b5cf6;
color: #8b5cf6;
}
.gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.gallery-image {
max-height: 300px;
object-fit: contain;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-content.open {
max-height: 1000px;
}
.progress-bar {
height: 6px;
background-color: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #ec4899, #8b5cf6);
transition: width 0.3s ease;
}
.performance-meter {
background: linear-gradient(90deg, #ec4899, #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: bold;
}
.model-tag {
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
margin-left: 0.5rem;
}
.cpu-tag {
background-color: #d1fae5;
color: #065f46;
}
.standard-tag {
background-color: #e0e7ff;
color: #3730a3;
}
.nsfw-tag {
background-color: #fee2e2;
color: #991b1b;
}
</style>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold bg-gradient-to-r from-pink-500 to-purple-600 bg-clip-text text-transparent">
AI Image Generator
</h1>
<p class="mt-2 text-gray-600">Generate images using various AI models</p>
<div class="mt-4 flex justify-center items-center space-x-4">
<div class="text-center">
<div class="text-sm text-gray-500">API Status</div>
<div id="api-status" class="performance-meter text-xl">Checking...</div>
</div>
<div class="text-center">
<div class="text-sm text-gray-500">Server Load</div>
<div id="server-load" class="performance-meter text-xl">-</div>
</div>
</div>
</header>
<!-- Main App Container -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<!-- Tabs Navigation -->
<div class="flex border-b border-gray-200">
<button class="tab-button px-4 py-3 font-medium active" data-tab="basic">Basic Settings</button>
<button class="tab-button px-4 py-3 font-medium" data-tab="advanced">Advanced Settings</button>
<button class="tab-button px-4 py-3 font-medium" data-tab="editor">Image Editor</button>
<button class="tab-button px-4 py-3 font-medium" data-tab="info">Information</button>
</div>
<!-- Tab Contents -->
<div class="p-6">
<!-- Basic Settings Tab -->
<div id="basic" class="tab-content active">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Prompt Section -->
<div class="md:col-span-2">
<div class="mb-4">
<label for="prompt" class="block text-sm font-medium text-gray-700 mb-1">Prompt</label>
<textarea id="prompt" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" placeholder="Enter a prompt here"></textarea>
</div>
<div class="mb-4">
<label for="style-preset" class="block text-sm font-medium text-gray-700 mb-1">Style Preset</label>
<select id="style-preset" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
<option value="none">None</option>
<option value="digital-art">Digital Art</option>
<option value="photographic">Photographic</option>
<option value="anime">Anime</option>
<option value="fantasy-art">Fantasy Art</option>
<option value="3d-model">3D Model</option>
<option value="pixel-art">Pixel Art</option>
</select>
</div>
<div class="mb-4">
<label for="batch-count" class="block text-sm font-medium text-gray-700 mb-1">Number of Images</label>
<select id="batch-count" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4" selected>4</option>
</select>
</div>
</div>
<!-- Model Selection -->
<div>
<div class="mb-4">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Select Model</label>
<button id="model-accordion" class="text-sm text-purple-600 hover:text-purple-800">
<i class="fas fa-chevron-down"></i> Show Models
</button>
</div>
<div class="mb-3">
<input type="text" id="model-search" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" placeholder="Search models...">
</div>
<div id="model-list-container" class="h-96 overflow-y-auto border border-gray-200 rounded-md p-2 hidden">
<div id="model-list" class="space-y-2">
<!-- Models will be populated here by JavaScript -->
</div>
</div>
<div id="selected-model-display" class="p-3 bg-gray-50 rounded-md border border-gray-200">
<p class="text-sm font-medium">Selected Model:</p>
<p id="selected-model" class="font-semibold text-purple-600">Loading models...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Settings Tab -->
<div id="advanced" class="tab-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="mb-4">
<label for="negative-prompt" class="block text-sm font-medium text-gray-700 mb-1">Negative Prompt</label>
<textarea id="negative-prompt" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" placeholder="What should not be in the image">(deformed, distorted, disfigured), poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, (mutated hands and fingers), disconnected limbs, mutation, mutated, ugly, disgusting, blurry, amputation, misspellings, typos</textarea>
</div>
<div class="mb-4">
<label for="sampling-method" class="block text-sm font-medium text-gray-700 mb-1">Sampling Method</label>
<select id="sampling-method" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
<option value="DPM++ 2M Karras" selected>DPM++ 2M Karras</option>
<option value="DPM++ SDE Karras">DPM++ SDE Karras</option>
<option value="Euler">Euler</option>
<option value="Euler a">Euler a</option>
</select>
</div>
</div>
<div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label for="width" class="block text-sm font-medium text-gray-700 mb-1">Width</label>
<input type="range" id="width" min="128" max="1024" step="64" value="512" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>128</span>
<span id="width-value">512</span>
<span>1024</span>
</div>
</div>
<div>
<label for="height" class="block text-sm font-medium text-gray-700 mb-1">Height</label>
<input type="range" id="height" min="128" max="1024" step="64" value="512" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>128</span>
<span id="height-value">512</span>
<span>1024</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="steps" class="block text-sm font-medium text-gray-700 mb-1">Sampling Steps</label>
<input type="range" id="steps" min="10" max="50" value="20" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>10</span>
<span id="steps-value">20</span>
<span>50</span>
</div>
</div>
<div>
<label for="cfg" class="block text-sm font-medium text-gray-700 mb-1">CFG Scale</label>
<input type="range" id="cfg" min="5" max="15" value="7" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>5</span>
<span id="cfg-value">7</span>
<span>15</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-4">
<div>
<label for="seed" class="block text-sm font-medium text-gray-700 mb-1">Seed</label>
<input type="number" id="seed" min="-1" max="1000000000" value="-1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label for="quality" class="block text-sm font-medium text-gray-700 mb-1">Quality</label>
<select id="quality" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
<option value="low">Low (faster)</option>
<option value="medium" selected>Medium</option>
<option value="high">High (slower)</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Image Editor Tab -->
<div id="editor" class="tab-content">
<div class="border-2 border-dashed border-gray-300 rounded-lg p-4 min-h-64 flex items-center justify-center">
<p class="text-gray-500">Image editor will be displayed here after generation</p>
</div>
<div class="mt-4 flex space-x-2">
<button class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Crop</button>
<button class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Adjust</button>
<button class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Filters</button>
</div>
</div>
<!-- Information Tab -->
<div id="info" class="tab-content">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1">Sample prompt</label>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200">
<code>{prompt} | ultra detail, ultra elaboration, ultra quality, perfect.</code>
</div>
</div>
<div class="mb-6">
<button id="featured-models-accordion" class="flex items-center justify-between w-full py-2 text-left font-medium text-purple-600 hover:text-purple-800">
<span>API Documentation</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="featured-models-content" class="accordion-content">
<div class="mt-2 p-4 bg-gray-50 rounded-md">
<h3 class="text-lg font-semibold mb-2">Available API Endpoints:</h3>
<ul class="list-disc pl-5 space-y-2 text-sm">
<li><strong>POST /api/generate</strong> - Generate images</li>
<li><strong>GET /api/models</strong> - List available models</li>
<li><strong>GET /api/status</strong> - Check server status</li>
<li><strong>GET /api/history</strong> - Get generation history</li>
</ul>
<h3 class="text-lg font-semibold mt-4 mb-2">Example Request:</h3>
<pre class="bg-gray-100 p-3 rounded text-xs overflow-x-auto">
{
"prompt": "a beautiful sunset over mountains",
"model": "stable-diffusion-xl",
"negative_prompt": "blurry, low quality",
"width": 512,
"height": 512,
"steps": 20,
"batch_size": 1,
"seed": -1,
"cfg_scale": 7
}</pre>
</div>
</div>
</div>
<div>
<button id="advanced-settings-accordion" class="flex items-center justify-between w-full py-2 text-left font-medium text-purple-600 hover:text-purple-800">
<span>System Information</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="advanced-settings-content" class="accordion-content">
<div class="mt-2 p-4 bg-gray-50 rounded-md">
<h3 class="text-lg font-semibold mb-2">Backend Information</h3>
<p class="mb-4 text-sm">This application uses a Python Flask backend with the following features:</p>
<ul class="list-disc pl-5 space-y-1 text-sm">
<li>RESTful API endpoints</li>
<li>Model management system</li>
<li>Job queue for image generation</li>
<li>Rate limiting</li>
<li>Authentication (optional)</li>
</ul>
<h3 class="text-lg font-semibold mt-4 mb-2">Frontend Information</h3>
<p class="mb-4 text-sm">The frontend is built with:</p>
<ul class="list-disc pl-5 space-y-1 text-sm">
<li>HTML5, CSS3, JavaScript</li>
<li>TailwindCSS for styling</li>
<li>Fetch API for backend communication</li>
<li>Responsive design</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Generate Button and Output -->
<div class="px-6 pb-6">
<div class="mb-4 hidden" id="generation-progress">
<div class="flex justify-between text-sm mb-1">
<span id="progress-status">Initializing model...</span>
<span id="progress-percent">0%</span>
</div>
<div class="progress-bar">
<div id="progress-bar-fill" class="progress-bar-fill" style="width: 0%"></div>
</div>
<div class="text-xs text-gray-500 mt-1" id="time-remaining">Estimated time: calculating...</div>
</div>
<button id="generate-btn" class="w-full py-3 px-4 bg-gradient-to-r from-pink-500 to-purple-600 text-white font-medium rounded-md hover:from-pink-600 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 flex items-center justify-center">
<span id="generate-text">Generate Image</span>
<span id="loading-spinner" class="ml-2 hidden">
<i class="fas fa-circle-notch loading-spinner"></i>
</span>
</button>
<div id="error-message" class="mt-4 hidden p-3 bg-red-100 border border-red-400 text-red-700 rounded"></div>
<div class="mt-6">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-medium text-gray-900">Output</h3>
<button id="download-all" class="hidden px-3 py-1 bg-gray-200 text-gray-800 rounded-md text-sm hover:bg-gray-300">
<i class="fas fa-download mr-1"></i> Download All
</button>
</div>
<div id="gallery-container" class="border border-gray-200 rounded-md p-4 min-h-64">
<div id="placeholder-text" class="flex items-center justify-center h-full text-gray-500">
<p>Your generated images will appear here</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Python Backend API Simulation -->
<script>
// This would normally be in a separate Flask/Python file
// For demonstration, we're simulating the API calls with JavaScript
// API Configuration
const API_BASE_URL = 'http://localhost:5000/api'; // Change this to your actual API URL
// Check API status on page load
async function checkApiStatus() {
try {
// In a real app, this would be a fetch request:
// const response = await fetch(`${API_BASE_URL}/status`);
// const data = await response.json();
// Simulated response
const data = {
status: 'online',
load: Math.floor(Math.random() * 30) + 10,
version: '1.0.0'
};
document.getElementById('api-status').textContent = data.status;
document.getElementById('server-load').textContent = `${data.load}%`;
// Load models after checking API status
loadModels();
} catch (error) {
document.getElementById('api-status').textContent = 'offline';
document.getElementById('server-load').textContent = '-';
showError('Could not connect to the API server');
}
}
// Load available models from API
async function loadModels() {
try {
// In a real app, this would be a fetch request:
// const response = await fetch(`${API_BASE_URL}/models`);
// const models = await response.json();
// Simulated response with all the models
const models = [
{ name: "3D Sketchfab", tags: ["standard"] },
{ name: "80s Cyberpunk", tags: ["standard"] },
{ name: "90s Anime Art", tags: ["standard"] },
{ name: "AbsoluteReality 1.8.1", tags: ["cpu"] },
{ name: "Analog", tags: ["standard"] },
{ name: "Analog Madness Realistic v7", tags: ["standard"] },
{ name: "Analog Redmond", tags: ["standard"] },
{ name: "Animagine 4.0", tags: ["cpu"] },
{ name: "Archfey Anime", tags: ["standard"] },
{ name: "Ascii Art", tags: ["cpu"] },
{ name: "Brain Melt Acid Art", tags: ["standard"] },
{ name: "Boreal", tags: ["standard"] },
{ name: "Caricature", tags: ["standard"] },
{ name: "Collage Flux", tags: ["standard"] },
{ name: "Coloring Book Flux", tags: ["cpu"] },
{ name: "Character Design", tags: ["standard"] },
{ name: "Chill Guy", tags: ["standard"] },
{ name: "Claude Art", tags: ["standard"] },
{ name: "Coloring Book Generator", tags: ["cpu"] },
{ name: "Cyborg Style XL", tags: ["standard"] },
{ name: "Disney", tags: ["standard"] },
{ name: "DreamPhotoGASM", tags: ["standard"] },
{ name: "Duchaiten Real3D NSFW XL", tags: ["nsfw"] },
{ name: "EBook Creative Cover", tags: ["standard"] },
{ name: "EpiCPhotoGasm", tags: ["standard"] },
{ name: "Fashion Hut Modeling LoRA", tags: ["standard"] },
{ name: "Filmgrain", tags: ["standard"] },
{ name: "FLUX.1 [Dev]", tags: ["cpu"] },
{ name: "FLUX.1 [Schnell]", tags: ["cpu"] },
{ name: "Flux Condensation", tags: ["cpu"] },
{ name: "Flux Handwriting", tags: ["cpu"] },
{ name: "Flux Realism LoRA", tags: ["cpu"] },
{ name: "Flux Super Realism LoRA", tags: ["cpu"] },
{ name: "Flux Uncensored", tags: ["nsfw"] },
{ name: "Flux Uncensored V2", tags: ["nsfw"] },
{ name: "Flux Game Assets V2", tags: ["cpu"] },
{ name: "Flux Icon Kit", tags: ["cpu"] },
{ name: "Flux Ghibsky Illustration", tags: ["cpu"] },
{ name: "Flux Animex V2", tags: ["cpu"] },
{ name: "Flux Animeo V1", tags: ["cpu"] },
{ name: "Flux AestheticAnime", tags: ["cpu"] },
{ name: "Flux SyntheticAnime", tags: ["cpu"] },
{ name: "Flux Stickers", tags: ["cpu"] },
{ name: "Flux Koda", tags: ["cpu"] },
{ name: "Flux Tarot v1", tags: ["cpu"] },
{ name: "Flux Tarot Cards", tags: ["cpu"] },
{ name: "Flux UltraRealism 2.0", tags: ["cpu"] },
{ name: "Flux Midjourney Anime", tags: ["cpu"] },
{ name: "Flux Miniature LoRA", tags: ["cpu"] },
{ name: "Flux Logo Design", tags: ["cpu"] },
{ name: "Flux Logo Design 2", tags: ["cpu"] },
{ name: "Flux Product Ad Backdrop", tags: ["cpu"] },
{ name: "Flux Outfit Generator", tags: ["cpu"] },
{ name: "Frosting Lane Flux", tags: ["cpu"] },
{ name: "Half Illustration", tags: ["standard"] },
{ name: "HiDream-I1-Full", tags: ["cpu"] },
{ name: "HiDream-I1-Dev", tags: ["cpu"] },
{ name: "HiDream-I1-Fast", tags: ["cpu"] },
{ name: "How2Draw", tags: ["standard"] },
{ name: "Huggieverse", tags: ["standard"] },
{ name: "Isometric 3D", tags: ["standard"] },
{ name: "Leonardo AI Style Illustration", tags: ["standard"] },
{ name: "Little Tinies", tags: ["standard"] },
{ name: "Lofi Cuties", tags: ["standard"] },
{ name: "Lustly Flux Uncensored v1", tags: ["nsfw"] },
{ name: "Maple Syrup", tags: ["standard"] },
{ name: "Meme XD", tags: ["standard"] },
{ name: "Midjourney", tags: ["standard"] },
{ name: "Midjourney Mix", tags: ["standard"] },
{ name: "Midjourney Mix 2", tags: ["standard"] },
{ name: "Movie Board", tags: ["standard"] },
{ name: "NSFWmodel", tags: ["nsfw"] },
{ name: "NSFW Master Flux", tags: ["nsfw"] },
{ name: "NSFW XL", tags: ["nsfw"] },
{ name: "OpenDalle v1.1", tags: ["cpu"] },
{ name: "Open Genmoji", tags: ["cpu"] },
{ name: "Pepe", tags: ["standard"] },
{ name: "Perfect Lewd Fantasy", tags: ["nsfw"] },
{ name: "Pixel Art Redmond", tags: ["standard"] },
{ name: "Pixel Art XL", tags: ["cpu"] },
{ name: "Pixel Art Sprites", tags: ["cpu"] },
{ name: "Pixel Background", tags: ["cpu"] },
{ name: "Product Design", tags: ["standard"] },
{ name: "Propaganda Poster", tags: ["standard"] },
{ name: "Purple Dreamy", tags: ["standard"] },
{ name: "Phantasma Anime", tags: ["standard"] },
{ name: "PS1 Style Flux", tags: ["cpu"] },
{ name: "Redmond SDXL", tags: ["standard"] },
{ name: "Retro Comic Flux", tags: ["cpu"] },
{ name: "SDXL HS Card Style", tags: ["standard"] },
{ name: "Sketch Smudge", tags: ["standard"] },
{ name: "Shou Xin", tags: ["standard"] },
{ name: "Softserve Anime", tags: ["standard"] },
{ name: "SoftPasty Flux", tags: ["cpu"] },
{ name: "Soviet Diffusion XL", tags: ["standard"] },
{ name: "Sketched Out Manga", tags: ["standard"] },
{ name: "Sketch Paint", tags: ["standard"] },
{ name: "SLDR FLUX NSFW v2 Studio", tags: ["nsfw"] },
{ name: "Selfie Photography", tags: ["standard"] },
{ name: "Stable Diffusion 2-1", tags: ["cpu"] },
{ name: "Stable Diffusion XL", tags: ["standard"] },
{ name: "Stable Diffusion 3 Medium", tags: ["cpu"] },
{ name: "Stable Diffusion 3.5 Large", tags: ["cpu"] },
{ name: "Stable Diffusion 3.5 Large Turbo", tags: ["cpu"] },
{ name: "YiffyMix", tags: ["nsfw"] }
];
populateModelList(models);
document.getElementById('selected-model').textContent = models[0].name;
} catch (error) {
showError('Failed to load models');
console.error('Error loading models:', error);
}
}
// Generate images using the API
async function generateImages(prompt, model, batchCount) {
try {
// Get all parameters from the form
const width = parseInt(document.getElementById('width').value);
const height = parseInt(document.getElementById('height').value);
const steps = parseInt(document.getElementById('steps').value);
const seed = parseInt(document.getElementById('seed').value);
const cfgScale = parseInt(document.getElementById('cfg').value);
const negativePrompt = document.getElementById('negative-prompt').value;
const samplingMethod = document.getElementById('sampling-method').value;
const stylePreset = document.getElementById('style-preset').value;
// In a real app, this would be a fetch request:
/*
const response = await fetch(`${API_BASE_URL}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
model,
negative_prompt: negativePrompt,
width,
height,
steps,
batch_size: batchCount,
seed,
cfg_scale: cfgScale,
sampler: samplingMethod,
style_preset: stylePreset
})
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
*/
// Simulated response with job ID
const data = {
job_id: `job_${Math.random().toString(36).substr(2, 9)}`,
estimated_time: batchCount * 10 // 10 seconds per image
};
// Poll for job completion
await pollJobStatus(data.job_id, batchCount);
} catch (error) {
throw error;
}
}
// Poll job status until completion
async function pollJobStatus(jobId, batchCount) {
let attempts = 0;
const maxAttempts = 30; // 30 attempts with 2 second delay = 60 seconds max
while (attempts < maxAttempts) {
attempts++;
try {
// In a real app, this would be a fetch request:
/*
const response = await fetch(`${API_BASE_URL}/job/${jobId}`);
if (!response.ok) {
throw new Error(`Job status request failed with status ${response.status}`);
}
const data = await response.json();
*/
// Simulated response
const progress = Math.min(100, Math.floor((attempts / maxAttempts) * 100));
const data = {
status: progress < 100 ? 'processing' : 'completed',
progress: progress,
images: progress === 100 ?
Array(batchCount).fill().map((_, i) => ({
url: `https://placehold.co/${document.getElementById('width').value}x${document.getElementById('height').value}/8b5cf6/FFFFFF?text=Generated+Image+${i+1}`,
prompt: document.getElementById('prompt').value
})) : null
};
// Update progress
updateProgress(data.progress, `Generating images (${data.status})`);
if (data.status === 'completed') {
// Display the generated images
displayGeneratedImages(data.images);
return;
}
// Wait before polling again
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
throw error;
}
}
throw new Error('Job did not complete in the expected time');
}
// Display generated images in the gallery
function displayGeneratedImages(images) {
const galleryContainer = document.getElementById('gallery-container');
if (!images || images.length === 0) {
galleryContainer.innerHTML = '<div id="placeholder-text" class="flex items-center justify-center h-full text-gray-500"><p>No images were generated</p></div>';
return;
}
galleryContainer.innerHTML = '<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" id="gallery-grid"></div>';
const galleryGrid = document.getElementById('gallery-grid');
images.forEach((image, index) => {
const imageContainer = document.createElement('div');
imageContainer.className = 'relative';
imageContainer.innerHTML = `
<img src="${image.url}" alt="Generated image: ${image.prompt}" class="gallery-image w-full h-64 object-cover rounded-md">
<div class="absolute bottom-2 right-2">
<button class="download-btn px-2 py-1 bg-white bg-opacity-80 rounded text-xs hover:bg-opacity-100" data-url="${image.url}">
<i class="fas fa-download"></i>
</button>
</div>
`;
// Add download event listener
imageContainer.querySelector('.download-btn').addEventListener('click', (e) => {
e.stopPropagation();
downloadImage(e.target.closest('button').getAttribute('data-url'), `generated-image-${index + 1}.jpg`);
});
galleryGrid.appendChild(imageContainer);
});
// Show download all button if generating more than 1 image
if (images.length > 1) {
document.getElementById('download-all').classList.remove('hidden');
document.getElementById('download-all').addEventListener('click', () => {
images.forEach((image, index) => {
downloadImage(image.url, `generated-image-${index + 1}.jpg`);
});
});
}
}
// Download single image
function downloadImage(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Update progress bar
function updateProgress(percent, status) {
const progressBar = document.getElementById('progress-bar-fill');
const progressPercent = document.getElementById('progress-percent');
const progressStatus = document.getElementById('progress-status');
progressBar.style.width = `${percent}%`;
progressPercent.textContent = `${percent}%`;
progressStatus.textContent = status;
// Estimate time remaining (simplified for demo)
if (percent < 100) {
const estimatedTime = Math.round((100 - percent) * 0.3); // 0.3 seconds per percent
document.getElementById('time-remaining').textContent = `Estimated time: ${estimatedTime} seconds`;
} else {
document.getElementById('time-remaining').textContent = 'Completed';
}
}
// Show error message
function showError(message) {
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.classList.remove('hidden');
}
// Clear gallery
function clearGallery() {
const galleryContainer = document.getElementById('gallery-container');
galleryContainer.innerHTML = '<div id="placeholder-text" class="flex items-center justify-center h-full text-gray-500"><p>Your generated images will appear here</p></div>';
document.getElementById('download-all').classList.add('hidden');
}
// Populate model list
function populateModelList(models) {
const modelListContainer = document.getElementById('model-list');
modelListContainer.innerHTML = '';
models.forEach(model => {
const modelCard = document.createElement('div');
// Determine background color based on tags
let bgColor = 'bg-gray-50';
let borderColor = 'border-gray-200';
if (model.tags.includes('cpu')) {
bgColor = 'bg-green-50';
borderColor = 'border-green-200';
} else if (model.tags.includes('nsfw')) {
bgColor = 'bg-red-50';
borderColor = 'border-red-200';
}
modelCard.className = `model-card p-3 rounded-md cursor-pointer ${bgColor} border ${borderColor}`;
// Create tags HTML
let tagsHtml = '';
if (model.tags.includes('cpu')) {
tagsHtml += '<span class="model-tag cpu-tag">CPU Optimized</span>';
}
if (model.tags.includes('nsfw')) {
tagsHtml += '<span class="model-tag nsfw-tag">NSFW</span>';
}
if (!model.tags.includes('cpu') && !model.tags.includes('nsfw')) {
tagsHtml += '<span class="model-tag standard-tag">Standard</span>';
}
modelCard.innerHTML = `
<div class="flex items-center justify-between">
<span class="font-medium">${model.name}</span>
<div class="flex">
${tagsHtml}
</div>
</div>
`;
modelCard.addEventListener('click', () => {
document.getElementById('selected-model').textContent = model.name;
document.getElementById('model-list-container').classList.add('hidden');
});
modelListContainer.appendChild(modelCard);
});
}
// Tab functionality
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
// Remove active class from all tabs and buttons
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Add active class to clicked button and corresponding content
button.classList.add('active');
const tabId = button.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
// Accordion functionality
function setupAccordion(buttonId, contentId) {
const button = document.getElementById(buttonId);
const content = document.getElementById(contentId);
button.addEventListener('click', () => {
content.classList.toggle('open');
const icon = button.querySelector('i');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up');
});
}
// Initialize accordions
setupAccordion('model-accordion', 'model-list-container');
setupAccordion('featured-models-accordion', 'featured-models-content');
setupAccordion('advanced-settings-accordion', 'advanced-settings-content');
// Update slider value displays
function updateSliderValue(sliderId, displayId) {
const slider = document.getElementById(sliderId);
const display = document.getElementById(displayId);
display.textContent = slider.value;
slider.addEventListener('input', () => {
display.textContent = slider.value;
});
}
// Initialize sliders
updateSliderValue('width', 'width-value');
updateSliderValue('height', 'height-value');
updateSliderValue('steps', 'steps-value');
updateSliderValue('cfg', 'cfg-value');
// Generate button functionality
document.getElementById('generate-btn').addEventListener('click', async () => {
const prompt = document.getElementById('prompt').value.trim();
const selectedModel = document.getElementById('selected-model').textContent;
const batchCount = parseInt(document.getElementById('batch-count').value);
if (!prompt) {
showError('Please enter a prompt');
return;
}
// Show loading state
const generateBtn = document.getElementById('generate-btn');
const generateText = document.getElementById('generate-text');
const loadingSpinner = document.getElementById('loading-spinner');
const progressContainer = document.getElementById('generation-progress');
generateText.textContent = 'Generating...';
loadingSpinner.classList.remove('hidden');
generateBtn.disabled = true;
progressContainer.classList.remove('hidden');
// Hide any previous error
document.getElementById('error-message').classList.add('hidden');
try {
// Clear previous results
clearGallery();
// Show progress bar
updateProgress(0, 'Initializing model...');
// Generate images
await generateImages(prompt, selectedModel, batchCount);
} catch (error) {
showError(error.message);
} finally {
// Reset button state
generateText.textContent = 'Generate Image';
loadingSpinner.classList.add('hidden');
generateBtn.disabled = false;
}
});
// Model search functionality
document.getElementById('model-search').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
const filteredModels = allModels.filter(model =>
model.name.toLowerCase().includes(searchTerm)
);
populateModelList(filteredModels);
});
// Check API status when page loads
document.addEventListener('DOMContentLoaded', checkApiStatus);
</script>
<!-- Here's what the Python Flask backend would look like (for reference) -->
<!--
# app.py (Python Flask Backend)
from flask import Flask, request, jsonify
import time
import random
from threading import Thread
app = Flask(__name__)
# In-memory job store (in a real app, use a database)
jobs = {}
models = [
{"name": "Stable Diffusion XL", "tags": ["standard"]},
{"name": "AbsoluteReality 1.8.1", "tags": ["cpu"]},
# ... all other models ...
]
@app.route('/api/status', methods=['GET'])
def api_status():
return jsonify({
"status": "online",
"load": random.randint(10, 40),
"version": "1.0.0"
})
@app.route('/api/models', methods=['GET'])
def get_models():
return jsonify(models)
@app.route('/api/generate', methods=['POST'])
def generate_image():
data = request.json
job_id = f"job_{random.randint(100000, 999999)}"
# Store the job
jobs[job_id] = {
"status": "queued",
"progress": 0,
"created_at": time.time(),
"params": data
}
# Start a background thread to process the job
thread = Thread(target=process_job, args=(job_id,))
thread.start()
return jsonify({
"job_id": job_id,
"estimated_time": data.get("batch_size", 1) * 10 # 10 seconds per image
})
@app.route('/api/job/<job_id>', methods=['GET'])
def get_job_status(job_id):
job = jobs.get(job_id)
if not job:
return jsonify({"error": "Job not found"}), 404
return jsonify(job)
def process_job(job_id):
job = jobs[job_id]
# Simulate processing
for i in range(1, 101):
time.sleep(0.2) # Simulate work
job["progress"] = i
job["status"] = "processing"
if i == 100:
job["status"] = "completed"
job["images"] = [
{"url": f"https://example.com/generated/{job_id}_{n}.jpg"}
for n in range(job["params"].get("batch_size", 1))
]
if __name__ == '__main__':
app.run(debug=True)
-->
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=SIMCEEHALO/less-steep-learning-curve" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>