generator-3000 / generator-3000.html
ArtelTaleb's picture
feat: default theme set to light
c4feacb verified
<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GENERATOR 3000</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
/* ── THEMES ── */
[data-theme="dark"] {
--bg:#0d0d0d; --bg2:#000; --fg:#fff; --fg2:#aaa;
--dim:#777; --muted:#555; --border:#fff; --border2:#444; --border3:#333;
--btn-primary-bg:#fff; --btn-primary-fg:#000;
--btn-secondary-bg:transparent; --btn-secondary-fg:#fff; --btn-secondary-border:#fff;
--input-bg:#000; --input-color:#ccc;
--scanlines:1; --card-border:#333;
}
[data-theme="light"] {
--bg:#fff; --bg2:#fff; --fg:#000; --fg2:#333;
--dim:#555; --muted:#777; --border:#000; --border2:#000; --border3:#ccc;
--btn-primary-bg:#000; --btn-primary-fg:#fff;
--btn-secondary-bg:transparent; --btn-secondary-fg:#000; --btn-secondary-border:#000;
--input-bg:#fff; --input-color:#000;
--scanlines:0; --card-border:#000;
}
body {
background:var(--bg);
color:var(--fg);
font-family:'Press Start 2P', monospace;
min-height:100vh;
overflow-x:hidden;
transition:background .2s, color .2s;
}
/* ── SCANLINES (dark only) ── */
body::before {
content:'';
position:fixed; inset:0;
background:repeating-linear-gradient(
to bottom,
transparent 0px, transparent 2px,
rgba(0,0,0,0.25) 2px, rgba(0,0,0,0.25) 3px
);
pointer-events:none; z-index:9998;
opacity:var(--scanlines);
transition:opacity .2s;
}
/* ── THEME TOGGLE ── */
.theme-toggle {
position:fixed; top:14px; right:16px; z-index:500;
background:var(--btn-secondary-bg);
border:1px solid var(--border2);
color:var(--dim);
font-family:'Press Start 2P', monospace;
font-size:7px; letter-spacing:1px;
padding:6px 10px; cursor:pointer; transition:all .15s;
}
.theme-toggle:hover { border-color:var(--border); color:var(--fg); }
/* ── HERO ── */
.hero {
text-align:center;
padding:52px 24px 28px;
}
.hero-title {
font-size:clamp(22px, 5.5vw, 60px);
color:var(--fg);
letter-spacing:4px;
line-height:1.2;
margin-bottom:18px;
}
[data-theme="dark"] .hero-title { text-shadow:3px 3px 0 #444; }
[data-theme="light"] .hero-title { text-shadow:3px 3px 0 #ccc; }
.hero-sub {
font-size:clamp(6px, 1vw, 10px);
letter-spacing:5px;
color:var(--dim);
}
.divider {
max-width:900px; margin:24px auto;
height:2px; background:var(--border); opacity:0.2;
}
/* ── MAIN AREA ── */
.main-area {
max-width:900px; margin:0 auto; padding:0 24px;
}
/* Token */
.token-row {
display:flex; align-items:center; gap:10px; margin-bottom:14px;
}
.token-label { font-size:7px; letter-spacing:2px; color:var(--muted); white-space:nowrap; }
#hf-token {
flex:1; background:var(--input-bg); border:1px solid var(--border2);
color:var(--input-color); font-family:'Press Start 2P', monospace;
font-size:7px; padding:8px 10px; outline:none; letter-spacing:2px;
transition:border-color .15s;
}
#hf-token:focus { border-color:var(--border); }
#hf-token::placeholder { color:var(--border3); }
/* ── PROMPT CONTAINER (unified box like light ref) ── */
.prompt-container {
border:2px solid var(--border);
background:var(--bg2);
margin-bottom:20px;
transition:border-color .2s, background .2s;
}
.prompt-top {
padding:18px 20px 14px;
position:relative;
min-height:100px;
}
#prompt {
width:100%; background:transparent; border:none;
color:var(--fg); font-family:'Press Start 2P', monospace;
font-size:clamp(9px, 1.4vw, 12px); line-height:2.2;
padding:0; outline:none; resize:none; caret-color:transparent;
}
#prompt::placeholder { color:var(--border3); }
.pixel-cursor {
display:inline-block;
width:10px; height:13px;
background:var(--fg);
margin-left:2px;
vertical-align:middle;
animation:blink .8s step-end infinite;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
.prompt-divider {
height:1px; background:var(--border); opacity:0.3; margin:0 0;
}
.prompt-bottom {
display:flex; align-items:center; justify-content:space-between;
gap:10px; padding:12px 16px;
}
.prompt-bottom-left { display:flex; gap:10px; }
/* ── PIXEL BUTTONS ── */
.pixel-btn {
font-family:'Press Start 2P', monospace;
font-size:clamp(7px, 1vw, 10px);
letter-spacing:2px; text-transform:uppercase;
padding:12px 16px; cursor:pointer;
display:flex; align-items:center; justify-content:center; gap:8px;
transition:all .1s; border:2px solid var(--border);
}
.pixel-btn:active { transform:translate(2px,2px); }
.pixel-btn svg { width:13px; height:13px; flex-shrink:0; }
.pixel-btn.primary {
background:var(--btn-primary-bg); color:var(--btn-primary-fg);
}
.pixel-btn.primary:hover { opacity:.85; }
.pixel-btn.primary:disabled { opacity:.3; cursor:not-allowed; }
.pixel-btn.secondary {
background:var(--btn-secondary-bg); color:var(--btn-secondary-fg);
}
.pixel-btn.secondary:hover {
background:var(--btn-primary-bg); color:var(--btn-primary-fg);
}
/* ── OUTPUT SECTION ── */
.output-header {
display:flex; align-items:center; gap:14px; margin-bottom:12px;
}
.output-title {
font-size:clamp(9px, 1.5vw, 13px); color:var(--fg); letter-spacing:3px; white-space:nowrap;
}
.output-line { flex:1; height:2px; background:var(--border); opacity:0.25; }
/* ── ACCORDIONS ── */
.accordion { border:1px solid var(--border3); margin-bottom:8px; }
.accordion-header {
display:flex; align-items:center; justify-content:space-between;
padding:9px 12px; cursor:pointer; user-select:none;
transition:background .1s;
}
.accordion-header:hover { background:rgba(128,128,128,0.05); }
.accordion-title { font-size:7px; letter-spacing:2px; color:var(--dim); text-transform:uppercase; }
.accordion-val { font-size:6px; letter-spacing:1px; color:var(--muted); }
.accordion-arrow { font-size:7px; color:var(--muted); transition:transform .2s; }
.accordion-arrow.open { transform:rotate(180deg); }
.accordion-body { overflow:hidden; max-height:0; transition:max-height .3s ease; border-top:1px solid transparent; }
.accordion-body.open { max-height:500px; border-top-color:var(--border3); }
.accordion-inner { padding:12px; }
/* Style pills */
.style-grid { display:flex; flex-wrap:wrap; gap:6px; }
.style-pill {
background:transparent; border:1px solid var(--border3);
color:var(--dim); padding:5px 9px; cursor:pointer;
font-family:'Press Start 2P', monospace; font-size:6px;
letter-spacing:1px; text-transform:uppercase; transition:all .1s;
}
.style-pill:hover { border-color:var(--border); color:var(--fg); }
.style-pill.active { border-color:var(--border); color:var(--fg); background:rgba(128,128,128,0.1); }
/* Params */
.params-grid { display:flex; gap:14px; flex-wrap:wrap; align-items:flex-start; }
.param-group { display:flex; flex-direction:column; gap:5px; }
.param-label { font-size:6px; letter-spacing:2px; color:var(--muted); text-transform:uppercase; }
.param-group select, .param-group input[type="number"] {
background:var(--input-bg); border:1px solid var(--border3); color:var(--input-color);
font-family:'Press Start 2P', monospace; font-size:7px; padding:5px 8px; outline:none;
}
.param-group select:focus, .param-group input:focus { border-color:var(--border); }
.param-group select option { background:var(--bg); }
.steps-row { display:flex; align-items:center; gap:8px; }
.steps-row input[type="range"] { width:80px; accent-color:var(--fg); }
.steps-val { font-size:7px; color:var(--dim); min-width:14px; }
.seed-row { display:flex; align-items:center; gap:5px; }
.rand-seed-btn {
background:none; border:1px solid var(--border3); color:var(--muted);
cursor:pointer; padding:3px 7px; font-size:11px; transition:.1s;
}
.rand-seed-btn:hover { border-color:var(--border); color:var(--fg); }
.neg-textarea {
background:var(--input-bg); border:1px solid var(--border3); color:var(--input-color);
font-family:'Press Start 2P', monospace; font-size:7px; padding:5px 8px;
outline:none; resize:none; height:46px; letter-spacing:1px; line-height:1.8;
}
.neg-textarea:focus { border-color:var(--border); }
.neg-textarea::placeholder { color:var(--border3); }
/* Error */
#error-box {
display:none; border:1px solid var(--dim); padding:8px 12px;
margin-bottom:12px; font-size:7px; color:var(--fg2); line-height:2;
background:rgba(128,128,128,0.05); align-items:center; gap:10px;
}
#retry-btn {
background:none; border:1px solid var(--border2); color:var(--fg2);
cursor:pointer; padding:4px 10px; font-family:'Press Start 2P', monospace;
font-size:6px; letter-spacing:2px; flex-shrink:0; white-space:nowrap;
}
#retry-btn:hover { border-color:var(--border); color:var(--fg); }
/* ── STATUS BAR ── */
.status-bar {
font-size:7px; letter-spacing:2px; color:var(--muted);
padding:12px 0 0;
line-height:2;
}
/* ── GALLERY ── */
.gallery-toolbar {
display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;
}
#dl-all {
background:none; border:1px solid var(--border3); color:var(--dim);
font-family:'Press Start 2P', monospace; font-size:7px; letter-spacing:1px;
cursor:pointer; padding:5px 10px; display:none; transition:.1s;
}
#dl-all:hover { border-color:var(--border); color:var(--fg); }
.empty-state {
border:2px solid var(--border); min-height:200px;
display:flex; align-items:center; justify-content:center;
font-size:8px; letter-spacing:3px; color:var(--border3); text-transform:uppercase;
line-height:2.5; text-align:center; padding:24px;
}
.gallery-grid {
display:grid;
grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
gap:10px;
}
.img-card {
background:var(--bg2); border:2px solid var(--card-border);
position:relative; overflow:hidden; transition:border-color .15s;
}
.img-card:hover { border-color:var(--border); }
.img-card img { width:100%; display:block; cursor:zoom-in; transition:opacity .15s; }
.img-card img:hover { opacity:0.85; }
.img-card-footer {
padding:6px 8px; border-top:1px solid var(--border3);
display:flex; align-items:center; justify-content:space-between; gap:6px;
}
.img-prompt {
font-size:6px; color:var(--muted); overflow:hidden;
text-overflow:ellipsis; white-space:nowrap; flex:1;
}
.img-actions { display:flex; gap:4px; flex-shrink:0; }
.img-btn {
font-size:7px; color:var(--muted); background:none; text-decoration:none;
padding:3px 7px; border:1px solid var(--border3); transition:.1s;
cursor:pointer; font-family:'Press Start 2P', monospace;
}
.img-btn:hover { color:var(--fg); border-color:var(--border); }
/* ── LOADING ── */
#loading {
position:fixed; inset:0; display:none;
align-items:center; justify-content:center; flex-direction:column; gap:20px;
background:rgba(0,0,0,0.96); z-index:200;
}
[data-theme="light"] #loading { background:rgba(255,255,255,0.96); }
.loading-inner { border:2px solid var(--border); padding:28px 48px; text-align:center; background:var(--bg); }
#loading-text { font-size:9px; letter-spacing:4px; color:var(--dim); margin-bottom:16px; }
.dot-row { display:flex; justify-content:center; gap:10px; }
.dot { width:10px; height:10px; background:var(--fg); animation:dotpulse 1.2s ease-in-out infinite; }
.dot:nth-child(2) { animation-delay:.2s; }
.dot:nth-child(3) { animation-delay:.4s; }
@keyframes dotpulse { 0%,80%,100%{opacity:0.2} 40%{opacity:1} }
/* ── LIGHTBOX ── */
#lightbox {
position:fixed; inset:0; display:none;
flex-direction:column; align-items:center; justify-content:center;
z-index:1000;
}
[data-theme="dark"] #lightbox { background:rgba(0,0,0,0.98); }
[data-theme="light"] #lightbox { background:rgba(255,255,255,0.98); }
#lightbox.open { display:flex; }
#lb-img { max-width:90vw; max-height:82vh; object-fit:contain; display:block; border:2px solid var(--border); }
#lb-nav-prev, #lb-nav-next {
position:fixed; top:50%; transform:translateY(-50%);
background:none; border:1px solid var(--border3); color:var(--dim);
font-family:'Press Start 2P', monospace; font-size:12px;
padding:12px 16px; cursor:pointer; transition:.1s; z-index:1001;
}
#lb-nav-prev { left:20px; }
#lb-nav-next { right:20px; }
#lb-nav-prev:hover, #lb-nav-next:hover { border-color:var(--border); color:var(--fg); }
#lb-nav-prev:disabled, #lb-nav-next:disabled { opacity:.15; cursor:default; }
#lb-bar {
position:fixed; bottom:0; left:0; right:0; padding:10px 24px;
background:var(--bg); border-top:1px solid var(--border3);
display:flex; align-items:center; justify-content:space-between; gap:16px;
}
#lb-prompt { font-size:7px; color:var(--muted); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#lb-counter { font-size:7px; color:var(--muted); letter-spacing:2px; flex-shrink:0; }
#lb-close {
background:none; border:1px solid var(--border3); color:var(--dim);
font-family:'Press Start 2P', monospace; font-size:7px; letter-spacing:2px;
padding:5px 12px; cursor:pointer; transition:.1s; flex-shrink:0;
}
#lb-close:hover { border-color:var(--border); color:var(--fg); }
#lb-dl {
background:none; border:1px solid var(--border3); color:var(--dim);
font-family:'Press Start 2P', monospace; font-size:7px; letter-spacing:2px;
padding:5px 12px; text-decoration:none; transition:.1s; flex-shrink:0;
}
#lb-dl:hover { border-color:var(--border); color:var(--fg); }
/* ── FOOTER ── */
.footer-divider { max-width:900px; margin:28px auto 0; padding:0 24px 48px; }
.footer-label {
text-align:center; font-size:7px; letter-spacing:5px;
color:var(--muted); padding:20px 0 28px;
}
</style>
</head>
<body>
<button class="theme-toggle" id="theme-toggle">◐ THEME</button>
<!-- HERO -->
<div class="hero">
<div class="hero-title">GENERATOR 3000</div>
<div class="hero-sub">8-BIT IMAGE GENERATOR</div>
</div>
<div class="divider"></div>
<!-- MAIN -->
<div class="main-area">
<!-- TOKEN -->
<div class="token-row">
<span class="token-label">HF TOKEN β€Ί</span>
<input type="password" id="hf-token" placeholder="hf_...">
</div>
<!-- PROMPT CONTAINER -->
<div class="prompt-container">
<div class="prompt-top">
<textarea id="prompt" placeholder="> ENTER PROMPT... " rows="3"></textarea>
</div>
<div class="prompt-divider"></div>
<div class="prompt-bottom">
<div class="prompt-bottom-left">
<button class="pixel-btn secondary" id="btn-auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z"/><path d="M19 15l.75 2.25L22 18l-2.25.75L19 21l-.75-2.25L16 18l2.25-.75z"/></svg>
AUTO
</button>
<button class="pixel-btn secondary" id="btn-random">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="1.5" fill="currentColor"/><circle cx="16" cy="8" r="1.5" fill="currentColor"/><circle cx="8" cy="16" r="1.5" fill="currentColor"/><circle cx="16" cy="16" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>
RANDOM
</button>
</div>
<button class="pixel-btn primary" id="gen-btn">
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21"/></svg>
GENERATE
</button>
</div>
</div>
<!-- ERROR -->
<div id="error-box">
<span id="error-text"></span>
<button id="retry-btn">RETRY</button>
</div>
<!-- STYLE ACCORDION -->
<div class="accordion">
<div class="accordion-header" id="style-toggle">
<span class="accordion-title">✦ Style Preset</span>
<span class="accordion-val" id="style-val">NONE</span>
<span class="accordion-arrow" id="style-arrow">β–Ό</span>
</div>
<div class="accordion-body" id="style-body">
<div class="accordion-inner">
<div class="style-grid">
<button class="style-pill active" data-suffix="" data-label="NONE">None</button>
<button class="style-pill" data-suffix=", photorealistic, 8k ultra HD, sharp focus, hyperdetailed, professional photography" data-label="REALISTIC">Realistic</button>
<button class="style-pill" data-suffix=", anime style, vibrant colors, studio ghibli, detailed illustration, soft lighting" data-label="ANIME">Anime</button>
<button class="style-pill" data-suffix=", oil painting, visible brushstrokes, rich colors, impressionist, museum quality" data-label="PAINTING">Painting</button>
<button class="style-pill" data-suffix=", cinematic lighting, film grain, movie still, anamorphic bokeh, color graded" data-label="CINEMA">Cinema</button>
<button class="style-pill" data-suffix=", pencil sketch, graphite drawing, crosshatching, black and white, fine linework" data-label="SKETCH">Sketch</button>
<button class="style-pill" data-suffix=", retro 80s synthwave, vaporwave neon glow, purple and cyan, grid, retrofuturism" data-label="SYNTHWAVE">Synthwave</button>
<button class="style-pill" data-suffix=", comic book art, halftone dots, bold ink outlines, flat colors, graphic novel" data-label="COMICS">Comics</button>
<button class="style-pill" data-suffix=", watercolor painting, soft washes, paper texture, delicate, translucent layers" data-label="WATERCOLOR">Watercolor</button>
<button class="style-pill" data-suffix=", isometric 3D render, clean geometry, soft shadows, pastel colors, game asset" data-label="ISOMETRIC">Isometric</button>
<button class="style-pill" data-suffix=", dark fantasy concept art, dramatic lighting, intricate details, epic scene" data-label="DARK FANTASY">Dark Fantasy</button>
<button class="style-pill" data-suffix=", minimal flat design, solid shapes, limited palette, bauhaus, geometric" data-label="MINIMAL">Minimal</button>
<button class="style-pill" data-suffix=", pixel art, 16-bit retro video game, sharp pixels, no anti-aliasing, limited color palette, NES SNES style" data-label="PIXEL ART">Pixel Art</button>
<button class="style-pill" data-suffix=", 3D render, octane render, physically based rendering, subsurface scattering, volumetric lighting, hyperrealistic CGI, 8k" data-label="3D RENDER">3D Render</button>
</div>
</div>
</div>
</div>
<!-- PARAMS ACCORDION -->
<div class="accordion">
<div class="accordion-header" id="params-toggle">
<span class="accordion-title">βš™ ParamΓ¨tres</span>
<span class="accordion-val" id="params-val">1024Γ—1024 Β· 4 STEPS</span>
<span class="accordion-arrow" id="params-arrow">β–Ό</span>
</div>
<div class="accordion-body" id="params-body">
<div class="accordion-inner">
<div class="params-grid">
<div class="param-group">
<span class="param-label">Taille</span>
<select id="dimensions">
<option value="512x512">512 Γ— 512</option>
<option value="768x768">768 Γ— 768</option>
<option value="1024x1024" selected>1024 Γ— 1024</option>
<option value="512x768">512 Γ— 768 Portrait</option>
<option value="768x512">768 Γ— 512 Paysage</option>
<option value="1024x576">1024 Γ— 576 Wide</option>
</select>
</div>
<div class="param-group">
<span class="param-label">Steps</span>
<div class="steps-row">
<input type="range" id="steps" min="1" max="50" value="4">
<span class="steps-val" id="steps-val">4</span>
</div>
</div>
<div class="param-group">
<span class="param-label">Seed</span>
<div class="seed-row">
<input type="number" id="seed" value="-1" min="-1" max="9999999" style="width:90px">
<button class="rand-seed-btn" id="rand-seed">⟳</button>
</div>
</div>
<div class="param-group" style="flex:1;min-width:160px;">
<span class="param-label">Prompt nΓ©gatif</span>
<textarea class="neg-textarea" id="neg-prompt" placeholder="blurry, low quality..."></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- MODEL SELECTOR -->
<div style="margin-bottom:8px;">
<div class="accordion">
<div class="accordion-header" id="model-toggle">
<span class="accordion-title">⊞ Modèle</span>
<span class="accordion-val" id="model-val">FLUX.1 [FAST]</span>
<span class="accordion-arrow" id="model-arrow">β–Ό</span>
</div>
<div class="accordion-body" id="model-body">
<div class="accordion-inner">
<div class="style-grid" id="model-grid">
<button class="style-pill active" data-model="flux-schnell">FLUX.1 SCHNELL</button>
<button class="style-pill" data-model="sdxl">SDXL BASE</button>
<button class="style-pill" data-model="sd21">DREAMSHAPER β˜… FREE</button>
<button class="style-pill" data-model="sd15">SD 1.5 β˜… FREE</button>
</div>
</div>
</div>
</div>
</div>
<!-- STATUS BAR -->
<div class="status-bar" id="status-bar">
SIZE: 1024Γ—1024 &nbsp;Β·&nbsp; STEPS: 4 &nbsp;Β·&nbsp; MODEL: FLUX.1 [FAST]
</div>
</div>
<div class="divider" style="margin-top:24px;"></div>
<!-- FOOTER / GALLERY -->
<div class="footer-divider">
<div class="footer-label">FREE &nbsp;+&nbsp; FAST &nbsp;+&nbsp; 8BIT</div>
<!-- OUTPUT -->
<div class="output-header">
<span class="output-title">✦ OUTPUT</span>
<div class="output-line"></div>
<div style="display:flex;align-items:center;gap:10px;">
<span id="img-count" style="font-size:7px;color:var(--muted);letter-spacing:2px;"></span>
<button id="dl-all">EXPORT ZIP</button>
</div>
</div>
<div id="empty-state" class="empty-state">
β€” &nbsp;NO OUTPUT YET&nbsp; β€”<br><br>PRESS GENERATE TO START
</div>
<div class="gallery-grid" id="gallery-grid" style="display:none"></div>
</div>
<!-- LOADING -->
<div id="loading">
<div class="loading-inner">
<div id="loading-text">GENERATING...</div>
<div class="dot-row">
<div class="dot"></div><div class="dot"></div><div class="dot"></div>
</div>
</div>
</div>
<!-- LIGHTBOX -->
<div id="lightbox">
<button id="lb-nav-prev">&#8592;</button>
<img id="lb-img" src="" alt="">
<button id="lb-nav-next">&#8594;</button>
<div id="lb-bar">
<span id="lb-prompt"></span>
<span id="lb-counter"></span>
<a id="lb-dl" download="">↓ DL</a>
<button id="lb-close">βœ• CLOSE</button>
</div>
</div>
<script>
const state = {
prompt:'', negPrompt:'', stylePreset:'',
dimensions:'1024x1024', steps:4, seed:-1, isLoading:false,
};
const MODELS = {
'flux-schnell': { endpoint:'https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell', label:'FLUX.1 [FAST]' },
'sdxl': { endpoint:'https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0', label:'SDXL [FREE]' },
'sd21': { endpoint:'https://router.huggingface.co/hf-inference/models/Lykon/dreamshaper-8', label:'DREAMSHAPER [FREE]' },
'sd15': { endpoint:'https://router.huggingface.co/hf-inference/models/stable-diffusion-v1-5/stable-diffusion-v1-5', label:'SD 1.5 [FREE]' },
};
let currentModel = 'flux-schnell';
const gallery = [];
let lbIndex = 0;
const RANDOM_PROMPTS = [
'a cyberpunk city at night, rain-soaked streets reflecting neon signs',
'an ancient library filled with floating glowing books',
'a lone astronaut standing on an alien purple planet, distant stars',
'a medieval castle on a cliff above the clouds at golden hour',
'a futuristic Tokyo street market, holographic advertisements',
'an underwater city with bioluminescent coral towers',
'a giant robot walking through a misty forest',
'a vintage space shuttle launch at dusk, dramatic smoke clouds',
'a cozy cabin in a snowy pine forest at night, warm light inside',
'a surreal floating island with a waterfall into the clouds',
];
const $ = id => {
const el = document.getElementById(id);
if (!el) throw new Error(`#${id} not found`);
return el;
};
// Theme toggle
$('theme-toggle').addEventListener('click', () => {
const html = document.documentElement;
html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark';
});
// Status bar
function updateStatus() {
$('status-bar').textContent =
`SIZE: ${state.dimensions.replace('x','Γ—')} Β· STEPS: ${state.steps} Β· MODEL: ${MODELS[currentModel].label}`;
}
// Wiring
$('prompt').addEventListener('input', e => { state.prompt = e.target.value; });
$('neg-prompt').addEventListener('input', e => { state.negPrompt = e.target.value; });
$('dimensions').addEventListener('change', e => { state.dimensions = e.target.value; updateStatus(); });
$('btn-random').addEventListener('click', () => {
const p = RANDOM_PROMPTS[Math.floor(Math.random() * RANDOM_PROMPTS.length)];
$('prompt').value = p; state.prompt = p;
});
$('btn-auto').addEventListener('click', () => {
const p = state.prompt.toLowerCase();
let dim = '1024x1024';
if (/portrait|person|face|woman|man|model|people/.test(p)) dim = '512x768';
else if (/landscape|panorama|wide|city|sky|ocean|mountain/.test(p)) dim = '768x512';
$('dimensions').value = dim; state.dimensions = dim; updateStatus();
});
document.querySelectorAll('.style-pill').forEach(pill => {
pill.addEventListener('click', () => {
document.querySelectorAll('.style-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
state.stylePreset = pill.dataset.suffix;
$('style-val').textContent = pill.dataset.label;
});
});
$('steps').addEventListener('input', e => {
state.steps = parseInt(e.target.value, 10);
$('steps-val').textContent = state.steps;
$('params-val').textContent = `${state.dimensions.replace('x','Γ—')} Β· ${state.steps} STEPS`;
updateStatus();
});
$('seed').addEventListener('change', e => {
const v = parseInt(e.target.value, 10);
state.seed = isNaN(v) ? -1 : v;
});
$('rand-seed').addEventListener('click', () => {
const s = Math.floor(Math.random() * 9999999);
state.seed = s; $('seed').value = s;
});
document.querySelectorAll('#model-grid .style-pill').forEach(pill => {
pill.addEventListener('click', () => {
document.querySelectorAll('#model-grid .style-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
currentModel = pill.dataset.model;
$('model-val').textContent = MODELS[currentModel].label;
updateStatus();
});
});
$('model-toggle').addEventListener('click', () => {
$('model-body').classList.toggle('open');
$('model-arrow').classList.toggle('open');
});
$('style-toggle').addEventListener('click', () => {
$('style-body').classList.toggle('open');
$('style-arrow').classList.toggle('open');
});
$('params-toggle').addEventListener('click', () => {
$('params-body').classList.toggle('open');
$('params-arrow').classList.toggle('open');
});
$('gen-btn').addEventListener('click', generateImage);
$('retry-btn').addEventListener('click', () => { hideError(); generateImage(); });
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') generateImage();
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') lbNav(-1);
if (e.key === 'ArrowRight') lbNav(1);
});
$('dl-all').addEventListener('click', downloadAll);
// Helpers
function showLoading() { $('loading').style.display = 'flex'; $('gen-btn').disabled = true; }
function hideLoading() { $('loading').style.display = 'none'; $('gen-btn').disabled = false; }
function showError(msg, showRetry = false) {
$('error-text').textContent = msg;
$('error-box').style.display = 'flex';
$('retry-btn').style.display = showRetry ? 'inline-block' : 'none';
}
function hideError() { $('error-box').style.display = 'none'; }
// Generate
async function generateImage() {
if (state.isLoading) return;
const token = $('hf-token').value.trim();
if (!token) { showError('ENTER HF TOKEN'); return; }
const promptText = state.prompt.trim();
if (!promptText) { showError('TYPE A PROMPT'); return; }
state.isLoading = true; showLoading(); hideError();
const [w, h] = state.dimensions.split('x').map(Number);
const seed = state.seed === -1 ? Math.floor(Math.random() * 9999999) : state.seed;
const fullPrompt = promptText + state.stylePreset;
try {
const body = { inputs: fullPrompt, parameters: {
negative_prompt: state.negPrompt || undefined,
width: w, height: h, num_inference_steps: state.steps, seed,
}};
Object.keys(body.parameters).forEach(k => { if (body.parameters[k] === undefined) delete body.parameters[k]; });
const res = await fetch(MODELS[currentModel].endpoint, {
method:'POST',
headers:{ 'Content-Type':'application/json', 'Authorization':`Bearer ${$('hf-token').value.trim()}` },
body:JSON.stringify(body),
});
if (res.status === 503) {
const json = await res.json().catch(() => ({}));
showError(`MODEL LOADING. RETRY IN ~${json.estimated_time ? Math.ceil(json.estimated_time) : 20}S`, true); return;
}
if (res.status === 429) { showError('RATE LIMIT. WAIT 30S', true); return; }
if (res.status === 402) { showError('402: MODEL REQUIRES PAYMENT. SWITCH TO SD 2.1 OR SD 1.5 (FREE) IN THE MODEL SELECTOR.'); return; }
if (!res.ok) throw new Error(`API ERROR ${res.status}`);
addImageToGallery({ url: URL.createObjectURL(await res.blob()), prompt: fullPrompt, seed });
} catch (err) {
showError(err.message);
} finally {
state.isLoading = false; hideLoading();
}
}
// Gallery
function addImageToGallery({ url, prompt, seed }) {
const id = Date.now();
gallery.unshift({ id, url, prompt, seed });
$('empty-state').style.display = 'none';
$('gallery-grid').style.display = 'grid';
$('dl-all').style.display = 'block';
$('img-count').textContent = `${gallery.length} IMG`;
const card = document.createElement('div'); card.className = 'img-card';
const img = document.createElement('img'); img.src = url; img.alt = prompt; img.loading = 'lazy';
const footer = document.createElement('div'); footer.className = 'img-card-footer';
const pText = document.createElement('div'); pText.className = 'img-prompt'; pText.textContent = prompt; pText.title = prompt;
const actions = document.createElement('div'); actions.className = 'img-actions';
const expand = document.createElement('button'); expand.className = 'img-btn'; expand.textContent = 'β€’';
const dl = document.createElement('a'); dl.className = 'img-btn'; dl.href = url; dl.download = `gen3000-${id}.jpg`; dl.textContent = '↓';
dl.addEventListener('click', e => e.stopPropagation());
actions.appendChild(expand); actions.appendChild(dl);
footer.appendChild(pText); footer.appendChild(actions);
card.appendChild(img); card.appendChild(footer);
$('gallery-grid').prepend(card);
updateCardIndices();
}
function updateCardIndices() {
$('gallery-grid').querySelectorAll('.img-card').forEach((card, i) => {
const btn = card.querySelector('.img-btn'); if (btn) btn.onclick = () => openLightbox(i);
const img = card.querySelector('img'); if (img) img.onclick = () => openLightbox(i);
});
}
// Lightbox
function openLightbox(i) { lbIndex = i; renderLightbox(); $('lightbox').classList.add('open'); document.body.style.overflow = 'hidden'; }
function closeLightbox() { $('lightbox').classList.remove('open'); document.body.style.overflow = ''; }
function lbNav(dir) { const n = lbIndex + dir; if (n < 0 || n >= gallery.length) return; lbIndex = n; renderLightbox(); }
function renderLightbox() {
const item = gallery[lbIndex]; if (!item) return;
$('lb-img').src = item.url; $('lb-img').alt = item.prompt;
$('lb-prompt').textContent = item.prompt;
$('lb-counter').textContent = `${lbIndex + 1} / ${gallery.length}`;
$('lb-dl').href = item.url; $('lb-dl').download = `gen3000-${item.id}.jpg`;
$('lb-nav-prev').disabled = lbIndex === 0;
$('lb-nav-next').disabled = lbIndex === gallery.length - 1;
}
$('lb-close').addEventListener('click', closeLightbox);
$('lb-nav-prev').addEventListener('click', () => lbNav(-1));
$('lb-nav-next').addEventListener('click', () => lbNav(1));
$('lightbox').addEventListener('click', e => { if (e.target === $('lightbox')) closeLightbox(); });
// ZIP
async function downloadAll() {
if (!gallery.length || typeof JSZip === 'undefined') return;
$('dl-all').textContent = 'ZIP...'; $('dl-all').disabled = true;
try {
const zip = new JSZip();
for (const item of gallery) { zip.file(`gen3000-${item.id}.jpg`, await (await fetch(item.url)).blob()); }
const zipUrl = URL.createObjectURL(await zip.generateAsync({ type:'blob' }));
const a = document.createElement('a'); a.href = zipUrl; a.download = 'generator-3000-gallery.zip';
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(zipUrl);
} finally { $('dl-all').textContent = 'EXPORT ZIP'; $('dl-all').disabled = false; }
}
</script>
</body>
</html>