anycoder-184b08af / index.html
Abhi1907's picture
Upload folder using huggingface_hub
e94ee03 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Z-Image Turbo PC | High-Speed AI Generator</title>
<!-- Import RemixIcon for modern UI icons -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap"
rel="stylesheet">
<style>
:root {
/* Color Palette - Cyberpunk / Dark Tech */
--bg-body: #09090b;
--bg-panel: #18181b;
--bg-input: #27272a;
--primary: #8b5cf6;
/* Violet */
--primary-hover: #7c3aed;
--primary-glow: rgba(139, 92, 246, 0.5);
--accent: #06b6d4;
/* Cyan */
--text-main: #f4f4f5;
--text-muted: #a1a1aa;
--border: #3f3f46;
--danger: #ef4444;
--success: #10b981;
/* Spacing & Radius */
--radius-lg: 16px;
--radius-md: 8px;
--radius-sm: 4px;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
/* Transitions */
--trans-fast: 0.2s ease;
--trans-smooth: 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* --- Animations --- */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulseGlow {
0% {
box-shadow: 0 0 5px var(--primary-glow);
}
50% {
box-shadow: 0 0 20px var(--primary-glow);
}
100% {
box-shadow: 0 0 5px var(--primary-glow);
}
}
/* --- Header --- */
header {
background: rgba(9, 9, 11, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: var(--spacing-md) var(--spacing-lg);
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.02em;
}
.brand i {
color: var(--primary);
font-size: 1.5rem;
}
.brand span {
background: linear-gradient(to right, #fff, var(--text-muted));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-links a {
color: var(--text-muted);
text-decoration: none;
font-size: 0.875rem;
transition: var(--trans-fast);
display: flex;
align-items: center;
gap: 4px;
}
.header-links a:hover {
color: var(--accent);
}
/* --- Main Layout --- */
main {
flex: 1;
display: grid;
grid-template-columns: 350px 1fr;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
@media (max-width: 1024px) {
main {
grid-template-columns: 1fr;
grid-template-rows: auto auto;
}
}
/* --- Controls Section --- */
.controls-panel {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
height: fit-content;
animation: fadeIn 0.6s ease-out;
}
.input-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
textarea {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-main);
border-radius: var(--radius-md);
padding: var(--spacing-md);
font-family: inherit;
resize: vertical;
min-height: 100px;
transition: var(--trans-fast);
font-size: 0.95rem;
}
textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
select,
input[type="text"],
input[type="number"] {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-main);
border-radius: var(--radius-md);
padding: 10px;
width: 100%;
font-family: inherit;
}
/* Custom Range Slider */
input[type="range"] {
-webkit-appearance: none;
width: 100%;
background: transparent;
margin: 10px 0;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
margin-top: -6px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: var(--border);
border-radius: 2px;
}
.btn-generate {
background: linear-gradient(135deg, var(--primary), var(--accent));
color: white;
border: none;
padding: 14px;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: var(--trans-fast);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
position: relative;
overflow: hidden;
margin-top: var(--spacing-sm);
}
.btn-generate:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.btn-generate:active {
transform: translateY(1px);
}
.btn-generate:disabled {
opacity: 0.7;
cursor: not-allowed;
filter: grayscale(0.5);
}
/* --- Preview Section --- */
.preview-panel {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
animation: fadeIn 0.8s ease-out;
}
.viewport {
background: #000;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
position: relative;
width: 100%;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.5);
}
.viewport img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
display: none;
opacity: 0;
transition: opacity 0.5s ease;
}
.viewport img.active {
display: block;
opacity: 1;
}
.placeholder-text {
color: var(--text-muted);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.placeholder-text i {
font-size: 3rem;
opacity: 0.2;
}
/* Loading Overlay */
.loading-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.loading-overlay.active {
opacity: 1;
pointer-events: all;
}
.loader-ring {
width: 50px;
height: 50px;
border: 3px solid var(--bg-input);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
margin-top: 15px;
font-family: 'JetBrains Mono', monospace;
color: var(--accent);
font-size: 0.9rem;
}
/* Actions Bar */
.actions-bar {
display: flex;
gap: var(--spacing-md);
justify-content: flex-end;
padding: var(--spacing-md);
background: var(--bg-panel);
border-radius: var(--radius-md);
border: 1px solid var(--border);
}
.action-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-main);
padding: 8px 16px;
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.9rem;
transition: var(--trans-fast);
}
.action-btn:hover {
background: var(--bg-input);
border-color: var(--text-muted);
}
.action-btn.primary {
background: var(--bg-input);
border-color: var(--primary);
color: var(--primary);
}
.action-btn.primary:hover {
background: var(--primary);
color: white;
}
/* History Strip */
.history-section {
margin-top: auto;
}
.history-header {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: var(--spacing-sm);
display: flex;
justify-content: space-between;
}
.history-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 10px;
}
.history-item {
aspect-ratio: 1;
border-radius: var(--radius-sm);
overflow: hidden;
border: 1px solid var(--border);
cursor: pointer;
position: relative;
transition: var(--trans-fast);
}
.history-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.history-item:hover {
border-color: var(--primary);
transform: scale(1.05);
z-index: 2;
}
/* --- Toast Notification --- */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.toast {
background: var(--bg-panel);
border: 1px solid var(--border);
padding: 12px 20px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease;
max-width: 300px;
}
.toast.success {
border-left: 4px solid var(--success);
}
.toast.error {
border-left: 4px solid var(--danger);
}
.toast.info {
border-left: 4px solid var(--accent);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* --- API Key Modal (Hidden by default) --- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
backdrop-filter: blur(5px);
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
width: 90%;
max-width: 500px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}
.modal h2 {
margin-bottom: var(--spacing-md);
}
.modal p {
color: var(--text-muted);
margin-bottom: var(--spacing-md);
line-height: 1.5;
font-size: 0.9rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
/* Footer */
footer {
margin-top: auto;
border-top: 1px solid var(--border);
padding: var(--spacing-lg);
text-align: center;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Utility classes */
.hidden {
display: none !important;
}
.tag-badge {
background: rgba(139, 92, 246, 0.1);
color: var(--primary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75rem;
font-family: 'JetBrains Mono', monospace;
border: 1px solid rgba(139, 92, 246, 0.2);
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="brand">
<i class="ri-cpu-line"></i>
<div>
Z-Image <span style="font-weight: 300;">Turbo PC</span>
</div>
</div>
<div class="header-links">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">
<i class="ri-code-s-slash-line"></i> Built with anycoder
</a>
</div>
</header>
<!-- Main Application -->
<main>
<!-- Controls Panel -->
<aside class="controls-panel">
<div class="input-group">
<label for="prompt">Prompt <span class="tag-badge">Required</span></label>
<textarea id="prompt" placeholder="Describe the image you want to generate... e.g., A futuristic cyberpunk city with neon lights, cinematic lighting, 8k"></textarea>
</div>
<div class="input-group">
<label for="negative-prompt">Negative Prompt</label>
<textarea id="negative-prompt" placeholder="Things to avoid... e.g., blurry, low quality, distorted" style="min-height: 60px;"></textarea>
</div>
<div class="settings-grid">
<div class="input-group">
<label>Aspect Ratio</label>
<select id="aspect-ratio">
<option value="1:1">1:1 (Square)</option>
<option value="16:9">16:9 (Landscape)</option>
<option value="9:16">9:16 (Portrait)</option>
<option value="4:3">4:3 (Classic)</option>
</select>
</div>
<div class="input-group">
<label>Style Preset</label>
<select id="style-preset">
<option value="none">None</option>
<option value="cinematic">Cinematic</option>
<option value="anime">Anime</option>
<option value="3d-model">3D Model</option>
<option value="digital-art">Digital Art</option>
<option value="photographic">Photographic</option>
</select>
</div>
</div>
<div class="input-group">
<label for="guidance-scale">Guidance Scale: <span id="guidance-value">7.5</span></label>
<input type="range" id="guidance-scale" min="1" max="20" step="0.5" value="7.5">
</div>
<div class="input-group">
<label for="inference-steps">Inference Steps: <span id="steps-value">4</span> <span class="tag-badge">Turbo</span></label>
<input type="range" id="inference-steps" min="1" max="10" step="1" value="4">
<small style="color: var(--text-muted); font-size: 0.75rem;">Lower steps are faster for Turbo models.</small>
</div>
<button id="generate-btn" class="btn-generate">
<i class="ri-magic-line"></i> Generate Image
</button>
<div style="text-align: center;">
<button id="settings-btn" class="action-btn" style="width: 100%; justify-content: center;">
<i class="ri-settings-3-line"></i> API Configuration
</button>
</div>
</aside>
<!-- Preview Panel -->
<section class="preview-panel">
<div class="viewport" id="viewport">
<div class="placeholder-text" id="placeholder">
<i class="ri-image-add-line"></i>
<p>Enter a prompt and hit Generate to start</p>
</div>
<img id="result-image" src="" alt="Generated Image">
<!-- Loading Overlay -->
<div class="loading-overlay" id="loader">
<div class="loader-ring"></div>
<div class="loading-text" id="loading-text">Initializing...</div>
</div>
</div>
<!-- Actions Bar -->
<div class="actions-bar">
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 4px;">
<span style="font-size: 0.8rem; color: var(--text-muted);">Generation Time</span>
<span id="gen-time" style="font-family: 'JetBrains Mono'; color: var(--accent);">-- ms</span>
</div>
<button class="action-btn" id="clear-btn">
<i class="ri-delete-bin-line"></i> Clear
</button>
<button class="action-btn primary" id="download-btn" disabled>
<i class="ri-download-line"></i> Download
</button>
</div>
<!-- History -->
<div class="history-section">
<div class="history-header">
<span>Session History</span>
<span id="history-count">0 items</span>
</div>
<div class="history-grid" id="history-grid">
<!-- History items injected here -->
</div>
</div>
</section>
</main>
<!-- API Configuration Modal -->
<div class="modal-overlay" id="api-modal">
<div class="modal">
<h2><i class="ri-key-2-line"></i> API Configuration</h2>
<p>To use the real <strong>Z-Image Turbo</strong> model (via Hugging Face Inference API), enter your token below.
If left blank, the app will run in <strong>Demo Mode</strong> (simulated results).</p>
<div class="input-group">
<label>HF API Token</label>
<input type="password" id="api-token" placeholder="hf_...">
</div>
<div class="input-group" style="margin-top: 10px;">
<label>Model Endpoint (Optional)</label>
<input type="text" id="model-endpoint" value="stabilityai/sd-turbo" placeholder="e.g. stabilityai/sd-turbo">
</div>
<div class="modal-actions">
<button class="action-btn" id="close-modal">Cancel</button>
<button class="action-btn primary" id="save-api">Save Configuration</button>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<footer>
&copy; 2023 Z-Image Turbo PC Interface. Designed for High Performance.
</footer>
<script>
/**
* Z-Image Turbo PC Logic
* Handles UI interactions, API calls (or simulation), and state management.
*/
// --- State Management ---
const state = {
isGenerating: false,
apiKey: '',
model: 'stabilityai/sd-turbo',
history: [],
currentImageBlob: null
};
// --- DOM Elements ---
const els = {
prompt: document.getElementById('prompt'),
negPrompt: document.getElementById('negative-prompt'),
aspectRatio: document.getElementById('aspect-ratio'),
stylePreset: document.getElementById('style-preset'),
guidance: document.getElementById('guidance-scale'),
guidanceVal: document.getElementById('guidance-value'),
steps: document.getElementById('inference-steps'),
stepsVal: document.getElementById('steps-value'),
generateBtn: document.getElementById('generate-btn'),
settingsBtn: document.getElementById('settings-btn'),
apiModal: document.getElementById('api-modal'),
closeModal: document.getElementById('close-modal'),
saveApi: document.getElementById('save-api'),
apiTokenInput: document.getElementById('api-token'),
modelEndpointInput: document.getElementById('model-endpoint'),
viewport: document.getElementById('viewport'),
resultImage: document.getElementById('result-image'),
placeholder: document.getElementById('placeholder'),
loader: document.getElementById('loader'),
loadingText: document.getElementById('loading-text'),
downloadBtn: document.getElementById('download-btn'),
clearBtn: document.getElementById('clear-btn'),
genTime: document.getElementById('gen-time'),
historyGrid: document.getElementById('history-grid'),
historyCount: document.getElementById('history-count'),
toastContainer: document.getElementById('toast-container')
};
// --- Event Listeners ---
// Sliders update text
els.guidance.addEventListener('input', (e) => els.guidanceVal.innerText = e.target.value);
els.steps.addEventListener('input', (e) => els.stepsVal.innerText = e.target.value);
// Modal Logic
els.settingsBtn.addEventListener('click', () => els.apiModal.classList.add('active'));
els.closeModal.addEventListener('click', () => els.apiModal.classList.remove('active'));
els.saveApi.addEventListener('click', () => {
state.apiKey = els.apiTokenInput.value.trim();
state.model = els.modelEndpointInput.value.trim() || 'stabilityai/sd-turbo';
showToast('Configuration saved', 'success');
els.apiModal.classList.remove('active');
});
// Generate Button
els.generateBtn.addEventListener('click', handleGenerate);
// Download Button
els.downloadBtn.addEventListener('click', () => {
if (!state.currentImageBlob) return;
const url = URL.createObjectURL(state.currentImageBlob);
const a = document.createElement('a');
a.href = url;
a.download = `z-turbo-${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Clear Button
els.clearBtn.addEventListener('click', () => {
els.resultImage.src = '';
els.resultImage.classList.remove('active');
els.placeholder.classList.remove('hidden');
els.downloadBtn.disabled = true;
state.currentImageBlob = null;
els.genTime.innerText = '-- ms';
});
// --- Helper Functions ---
function simulateDelay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getDimensions() {
const ratio = els.aspectRatio.value;
// Standard dimensions for SD-Turbo (must be multiples of 8 or 16 usually)
switch (ratio) {
case '16:9': return { width: 768, height: 432 };
case '9:16': return { width: 432, height: 768 };
case '4:3': return { width: 640, height: 480 };
case '1:1': default: return { width: 512, height: 512 };
}
}
function setLoading(isLoading) {
state.isGenerating = isLoading;
els.generateBtn.disabled = isLoading;
if (isLoading) {
els.loader.classList.add('active');
} else {
els.loader.classList.remove('active');
}
}
function updateLoadingText(text) {
els.loadingText.innerText = text;
}
function displayImage(url) {
els.placeholder.classList.add('hidden');
els.resultImage.onload = () => {
els.resultImage.classList.add('active');
};
els.resultImage.src = url;
}
function updateHistory(url, prompt) {
state.history.unshift({ url, prompt, timestamp: Date.now() });
// Limit history to 10 items
if (state.history.length > 10) state.history.pop();
els.historyCount.innerText = `${state.history.length} items`;
els.historyGrid.innerHTML = '';
state.history.forEach(item => {
const div = document.createElement('div');
div.className = 'history-item';
div.title = item.prompt;
const img = document.createElement('img');
img.src = item.url;
div.appendChild(img);
div.addEventListener('click', () => {
displayImage(item.url);
els.genTime.innerText = "From History";
});
els.historyGrid.appendChild(div);
});
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = '';
if (type === 'success') icon = '<i class="ri-checkbox-circle-line" style="color:var(--success)"></i>';
else if (type === 'error') icon = '<i class="ri-error-warning-line" style="color:var(--danger)"></i>';
else icon = '<i class="ri-information-line" style="color:var(--accent)"></i>';
toast.innerHTML = `${icon}<span>${message}</span>`;
els.toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// --- Core Functions ---
async function handleGenerate() {
const prompt = els.prompt.value.trim();
if (!prompt) {
showToast('Please enter a prompt', 'error');
els.prompt.focus();
return;
}
setLoading(true);
try {
const startTime = performance.now();
let imageUrl;
let blob;
// Check if we have an API Key
if (state.apiKey && state.apiKey.length > 5) {
// REAL API CALL
blob = await callHuggingFaceAPI(prompt);
imageUrl = URL.createObjectURL(blob);
} else {
// DEMO MODE (Simulation)
updateLoadingText("Running in Demo Mode (No API Key)...");
await simulateDelay(1500); // Fake network latency
// Use picsum with seed based on prompt to make it deterministic but random-looking
const seed = encodeURIComponent(prompt.substring(0, 20));
const width = getDimensions().width;
const height = getDimensions().height;
imageUrl = `https://picsum.photos/seed/${seed}/${width}/${height}`;
// For demo, we need to fetch the blob to enable download
const response = await fetch(imageUrl);
blob = await response.blob();
}
const endTime = performance.now();
const duration = Math.round(endTime - startTime);
// Update UI
displayImage(imageUrl);
updateHistory(imageUrl, prompt);
state.currentImageBlob = blob;
els.downloadBtn.disabled = false;
els.genTime.innerText = `${duration} ms`;
showToast('Image generated successfully!', 'success');
} catch (error) {
console.error(error);
showToast(error.message || 'Generation failed', 'error');
} finally {
setLoading(false);
}
}
// Call Hugging Face Inference API with Retry Logic
async function callHuggingFaceAPI(prompt) {
updateLoadingText("Connecting to GPU...");
const dimensions = getDimensions();
const negative = els.negPrompt.value.trim();
// Construct payload for SD-Turbo or similar
const payload = {
inputs: prompt,
parameters: {
negative_prompt: negative,
guidance_scale: parseFloat(els.guidance.value),
num_inference_steps: parseInt(els.steps.value),
// Some models require explicit width/height in payload
width: dimensions.width,
height: dimensions.height
}
};
const maxRetries = 3;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const response = await fetch(
`https://api-inference.huggingface.co/models/${state.model}`,
{
headers: {
Authorization: `Bearer ${state.apiKey}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(payload),
}
);
// 1. Handle Model Loading (503) - Common cause of failures
if (response.status === 503) {
updateLoadingText("Model is warming up...");
try {
const data = await response.json();
// estimated_time is in seconds, convert to ms
const waitTime = (data.estimated_time || 20) * 1000;
await simulateDelay(waitTime);
} catch (e) {
// Fallback wait if JSON parsing fails
await simulateDelay(20000);
}
retryCount++;
continue; // Retry the request
}
// 2. Handle other HTTP errors
if (!response.ok) {
let errorDetail = "Unknown Error";
try {
const errorJson = await response.json();
errorDetail = errorJson.error || `HTTP ${response.status}`;
} catch (e) {
errorDetail = response.statusText;
}
throw new Error(`API Error: ${errorDetail}`);
}
// 3. Success - Get Blob
updateLoadingText("Processing pixels...");
const blob = await response.blob();
return blob;
} catch (err) {
// If it's a network error (not 503), we can retry or fail
// For simplicity here, if max retries hit, throw error
if (retryCount >= maxRetries - 1) {
throw err;
}
retryCount++;
await simulateDelay(1000); // Wait a bit before retrying network error
}
}
throw new Error("Max retries reached. Please try again.");
}
</script>
</body>
</html>