Nuzwa commited on
Commit
6651ecb
·
verified ·
1 Parent(s): aa25ce6

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +237 -146
index.html CHANGED
@@ -5,42 +5,70 @@
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
  <title>imagegen</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
8
- <style>
9
- *, *::before, *::after { box-sizing:border-box; }
10
- html, body { height:100%; margin:0; padding:0; font-family:'Poppins',sans-serif; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
11
- body { background: radial-gradient(circle at 20% 20%, rgba(140,60,200,0.08), transparent 40%), radial-gradient(circle at 80% 80%, rgba(200,60,255,0.05), transparent 40%), #000; color:#fff; overflow:hidden; }
12
 
13
- .topbar { position:fixed; top:0; left:0; right:0; height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 16px; gap:12px; z-index:1000; background:transparent; }
14
- .btn-small { background:#141414; border-radius:10px; padding:8px 12px; font-size:13px; color:#fff; display:inline-flex; align-items:center; gap:10px; border:1px solid rgba(255,255,255,0.03); cursor:pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- .container { width:70%; max-width:1100px; margin:5em auto 16px; padding:0 12px; display:flex; flex-direction:column; gap:12px; position:relative; }
17
- @media (max-width: 768px) { .container { width:100%; padding:0 12px; margin-top:5em; } }
18
 
19
  .input-row { display:flex; gap:10px; align-items:flex-start; }
20
- textarea#prompt { flex:1; min-width:0; max-height:250px; height:56px; resize:none; border-radius:12px; padding:12px; background:#0e0e0e; border:1px solid rgba(255,255,255,0.03); color:#fff; font-size:14px; line-height:1.35; overflow:auto; outline:none; }
21
- .generate-btn { background: linear-gradient(90deg,#ff93e9,#9b7cff); border:none; border-radius:12px; padding:12px 18px; font-size:14px; cursor:pointer; color:#fff; min-height:56px; display:inline-flex; align-items:center; justify-content:center; transition:opacity .15s; }
22
- .generate-btn[disabled]{ opacity:0.6; cursor:default; }
23
-
24
- /* New: always-visible controls panel */
25
- .controls { display:flex; flex-wrap:wrap; gap:10px; padding:10px; border:1px solid rgba(255,255,255,0.06); background:#0f0f0f; border-radius:12px; }
26
- .controls label { font-size:13px; color:#eaeaea; display:flex; align-items:center; gap:6px; }
27
- .controls input[type="number"], .controls select { background:#0c0c0c; border:1px solid rgba(255,255,255,0.04); color:#fff; padding:6px 8px; border-radius:8px; font-size:13px; min-width:90px; }
28
-
29
- /* Styles grid visible inline */
30
- .styles-wrap { margin-top:6px; }
31
- .styles-grid { display:grid; grid-template-columns:repeat(2, minmax(140px,1fr)); gap:8px; }
32
- @media (min-width:760px){ .styles-grid { grid-template-columns:repeat(5, minmax(120px,1fr)); } }
33
- .style-card { background:#101010; border-radius:10px; overflow:hidden; cursor:pointer; text-align:center; border:2px solid transparent; padding-bottom:8px; font-size:12px; }
 
 
 
 
 
34
  .style-card img { width:100%; height:72px; object-fit:cover; display:block; }
35
- .style-card.selected { border-color:#ff93e9; box-shadow:0 8px 30px rgba(155,124,255,0.06); }
 
 
 
 
 
 
 
36
 
37
- #images { width:100%; margin-top:8px; display:flex; gap:12px; flex-wrap:wrap; justify-content:center; align-items:flex-start; overflow:auto; padding:10px; border-radius:10px; background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); border:1px solid rgba(255,255,255,0.02); max-height: calc(100vh - 5em - 260px); }
38
- .image-card { background:#0b0b0b; border-radius:12px; padding:8px; min-height:120px; width:min(420px, 96vw); box-shadow:0 10px 30px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.02); display:flex; flex-direction:column; gap:6px; }
 
39
  .image-card img { width:100%; height:auto; display:block; border-radius:8px; }
40
- .img-actions { display:flex; gap:8px; justify-content:flex-end; }
 
41
  .img-actions button { background:#141414; border:1px solid rgba(255,255,255,0.08); color:#fff; border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; }
42
- .loader { border:3px solid #222; border-top:3px solid #ff93e9; border-radius:50%; width:22px; height:22px; animation:spin 1s linear infinite; margin-right:6px; }
 
43
  @keyframes spin { 0%{ transform:rotate(0) } 100%{ transform:rotate(360deg) } }
 
44
  .muted { color:#9b9bb9; font-size:13px; }
45
  .note { font-size:13px; color:#cfcfcf; margin-top:6px; }
46
  </style>
@@ -48,7 +76,11 @@
48
  <body>
49
 
50
  <div class="topbar" role="banner">
 
51
  <div class="btn-small" onclick="window.open('https://manus.im/invitation/XHHHQTMBM0ZCTX')">Manus</div>
 
 
 
52
  <a class="btn-small" aria-disabled="true">
53
  <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="coffee">
54
  <span>Buy me a coffee</span>
@@ -61,48 +93,25 @@
61
  <button id="generateBtn" class="generate-btn" aria-label="Generate">Generate</button>
62
  </div>
63
 
64
- <!-- Always-visible controls -->
65
- <div class="controls" id="controls">
66
- <label>Model
67
- <select id="model">
68
- <option value="flux">flux</option>
69
- <option value="turbo">turbo (uncensored)</option>
70
- </select>
71
- </label>
72
-
73
- <label>Aspect
74
- <select id="aspect">
75
- <option value="1:1">1:1</option>
76
- <option value="16:9">16:9</option>
77
- <option value="19:6" selected>19:6</option>
78
- <option value="3:2">3:2</option>
79
- <option value="2:3">2:3</option>
80
- <option value="4:5">4:5</option>
81
- <option value="9:16">9:16</option>
82
- </select>
83
- </label>
84
-
85
- <label>Framing
86
- <select id="framing">
87
- <option value="auto" selected>Auto</option>
88
- <option value="close">Close‑up</option>
89
- <option value="medium">Medium</option>
90
- <option value="full">Full‑body</option>
91
- </select>
92
- </label>
93
-
94
- <label>Width <input id="width" type="number" min="64" max="2048" value="2048" /></label>
95
- <label>Height <input id="height" type="number" min="64" max="2048" value="646" /></label>
96
-
97
- <label>Enhance <input id="enhance" type="checkbox" /></label>
98
-
99
- <label>Seed <input id="seed" type="number" value="42" /></label>
100
- <label><input id="randomSeed" type="checkbox" checked /> Random</label>
101
  </div>
102
 
103
- <!-- Styles inline -->
104
- <div class="styles-wrap">
105
- <div class="note">Choose a style (optional):</div>
 
 
 
 
 
 
 
106
  <div class="styles-grid" id="stylesGrid">
107
  <div class="style-card selected" data-name="none"><img src="none.jpg" alt="No style"><div>No style</div></div>
108
  <div class="style-card" data-name="cinema"><img src="https://xyplon.web.app/assets/Cinematic.jpeg" alt="Cinema"><div>Cinema</div></div>
@@ -110,10 +119,76 @@
110
  <div class="style-card" data-name="photography"><img src="https://xyplon.web.app/assets/Photography.jpeg" alt="Photography"><div>Photography</div></div>
111
  <div class="style-card" data-name="fantasy"><img src="https://xyplon.web.app/assets/Digital.jpeg" alt="Fantasy"><div>Fantasy</div></div>
112
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
 
114
 
115
- <div class="note">Generated images (scroll this pane). New images appear at the top.</div>
116
- <div id="images" aria-live="polite"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
 
119
  <script>
@@ -121,30 +196,49 @@
121
  let selectedStyle = null;
122
  let isGenerating = false;
123
 
124
- // Tweaked presets (removed letterbox; photography uses wider lens)
125
  const styleTemplates = {
126
- cinema: " cinematic lighting, rich color grading",
127
  realistic: " realistic stock photo",
128
- photography: "35mm lens, wide shot, subject fully in frame, natural film grain",
129
  fantasy: " epic fantasy, vibrant colors, surreal composition"
130
  };
131
 
132
- // Framing presets
133
  const framingTemplates = {
134
  auto: "",
135
- close: "tight framing, close-up portrait, 85mm lens, shallow depth of field, face filling the frame",
136
- medium: "medium shot, from waist up, 50mm lens, natural perspective",
137
- full: "full body, head-to-toe, wide shot, 35mm lens, subject fully in frame, feet visible"
138
  };
139
 
140
- const SYSTEM_PROMPT = `You are ai machine which enhances users prompt for generating high quality the most amazing images from flux model...`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- // ---------- Settings persistence ----------
143
  function saveSettingsToStorage() {
144
  const s = {
145
  model: document.getElementById('model').value,
146
  aspect: document.getElementById('aspect').value,
147
- framing: document.getElementById('framing').value,
148
  width: Number(document.getElementById('width').value || 1024),
149
  height: Number(document.getElementById('height').value || 1024),
150
  seed: Number(document.getElementById('seed').value || 42),
@@ -158,104 +252,99 @@
158
  const s = JSON.parse(localStorage.getItem('ai_img_settings') || '{}');
159
  if (s.model) document.getElementById('model').value = s.model;
160
  if (s.aspect) document.getElementById('aspect').value = s.aspect;
161
- if (s.framing) document.getElementById('framing').value = s.framing;
162
  if (s.width) document.getElementById('width').value = s.width;
163
  if (s.height) document.getElementById('height').value = s.height;
164
  if (s.seed || s.seed === 0) document.getElementById('seed').value = s.seed;
165
- document.getElementById('randomSeed').checked = s.randomSeed !== undefined ? s.randomSeed : true;
166
- document.getElementById('enhance').checked = (s.enhance !== undefined) ? s.enhance : true;
167
  } catch(e){}
168
  }
 
169
 
170
- // Helpers
171
- function parseAspect(aspectStr){ const [w,h]=aspectStr.split(':').map(Number); return (!w||!h)?1:(w/h); }
172
- function sizeForAspect(aspectStr, maxDim=2048){
173
  const r = parseAspect(aspectStr);
174
- if (r >= 1){ const width=maxDim; const height=Math.max(64, Math.round(width/r)); return {width,height}; }
175
- else { const height=maxDim; const width=Math.max(64, Math.round(height*r)); return {width,height}; }
176
  }
177
  function applyAspectToFields(){
178
- const {width, height} = sizeForAspect(document.getElementById('aspect').value, 2048);
 
 
179
  document.getElementById('width').value = width;
180
  document.getElementById('height').value = height;
181
  }
182
 
183
- loadSettingsFromStorage();
184
- // Default sync with selected aspect if first time
185
- (function(){
186
- if (!localStorage.getItem('ai_img_settings')) applyAspectToFields();
187
- })();
188
-
189
- // Style selection
190
- document.querySelectorAll('.style-card').forEach(card=>{
191
- card.addEventListener('click',()=>{
192
- document.querySelectorAll('.style-card').forEach(c=>c.classList.remove('selected'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  card.classList.add('selected');
194
  selectedStyle = card.dataset.name === 'none' ? null : card.dataset.name;
195
  });
196
  });
197
 
198
- // Aspect change auto size
199
  document.getElementById('aspect').addEventListener('change', applyAspectToFields);
200
 
201
- // Random seed toggle disables seed input
 
 
 
 
 
202
  const randomSeedEl = document.getElementById('randomSeed');
203
  const seedEl = document.getElementById('seed');
204
- randomSeedEl.addEventListener('change', ()=>{ seedEl.disabled = randomSeedEl.checked; });
205
  seedEl.disabled = randomSeedEl.checked;
206
 
207
- // UI niceties
208
- const promptEl = document.getElementById('prompt');
209
- function autoResizeTextarea(){ promptEl.style.height='auto'; promptEl.style.height=Math.min(250, promptEl.scrollHeight) + 'px'; }
210
- promptEl.addEventListener('input', autoResizeTextarea); setTimeout(autoResizeTextarea,0);
211
-
212
  const imagesContainer = document.getElementById('images');
213
  const generateBtn = document.getElementById('generateBtn');
214
-
215
- function randomInt(min,max){ return Math.floor(Math.random()*(max-min+1))+min; }
216
-
217
- async function enhancePrompt(userPrompt){
218
- try{
219
- const chatHistory = [{role:'system',content:SYSTEM_PROMPT},{role:'user',content:`"${userPrompt}"`}];
220
- const response = await fetch('https://text.pollinations.ai/openai',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:'openai',messages:chatHistory})});
221
- if(!response.ok) throw new Error('HTTP '+response.status);
222
- const data = await response.json();
223
- return (data.choices?.[0]?.message?.content || userPrompt).trim();
224
- }catch(e){ console.warn('Enhance fail:',e); return userPrompt; }
225
- }
226
 
227
  function makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height){
228
  const bar = document.createElement('div');
229
  bar.className = 'img-actions';
230
 
231
- // Download
232
  const dl = document.createElement('button');
233
  dl.textContent = 'Download';
234
- dl.addEventListener('click', ()=>{
235
- const a = document.createElement('a');
236
- a.href = imgUrl;
237
- a.download = filename;
238
- document.body.appendChild(a);
239
- a.click();
240
- a.remove();
241
- });
242
 
243
- // Open
244
  const openBtn = document.createElement('button');
245
  openBtn.textContent = 'Open';
246
- openBtn.addEventListener('click', ()=> window.open(imgUrl,'_blank'));
247
 
248
- // Copy prompt
249
  const cp = document.createElement('button');
250
  cp.textContent = 'Copy prompt';
251
- cp.addEventListener('click', async ()=>{
252
- try{ await navigator.clipboard.writeText(finalPrompt); cp.textContent='Copied!'; setTimeout(()=>cp.textContent='Copy prompt',900);}catch(e){}
253
- });
254
 
255
- // Re-run same seed
256
  const rerun = document.createElement('button');
257
  rerun.textContent = 'Re‑generate';
258
- rerun.addEventListener('click', ()=>{
259
  document.getElementById('randomSeed').checked = false;
260
  document.getElementById('seed').disabled = false;
261
  document.getElementById('seed').value = seed;
@@ -275,27 +364,30 @@
275
  if (isGenerating) return;
276
  const rawPrompt = promptEl.value.trim();
277
  if (!rawPrompt){
278
- const temp=document.createElement('div'); temp.className='image-card';
279
- temp.innerHTML='<div class="muted">Please enter a prompt</div>'; imagesContainer.prepend(temp);
280
- setTimeout(()=>temp.remove(),1200); return;
281
  }
282
 
283
  isGenerating = true; generateBtn.disabled=true;
284
 
285
  const card = document.createElement('div');
286
  card.className='image-card';
287
- card.innerHTML = `<div style="display:flex;align-items:center;"><div class="loader"></div><div class="muted">Generating...</div></div>`;
288
  imagesContainer.prepend(card);
289
 
290
- // Build prompt (style + framing first)
291
  const enhanceOn = document.getElementById('enhance').checked;
292
  const framingChoice = document.getElementById('framing').value;
293
 
294
- let promptWithStyle = rawPrompt;
295
- if (selectedStyle && styleTemplates[selectedStyle]) promptWithStyle += ' ' + styleTemplates[selectedStyle];
296
- if (framingTemplates[framingChoice]) promptWithStyle += ' ' + framingTemplates[framingChoice];
 
297
 
298
- let finalPrompt = enhanceOn ? await enhancePrompt(promptWithStyle) : promptWithStyle;
 
 
 
 
 
299
 
300
  // Params
301
  const model = encodeURIComponent(document.getElementById('model').value || 'flux');
@@ -318,8 +410,7 @@
318
  console.error(err);
319
  card.innerHTML = `<div class="muted" style="color:#ff6b6b;">Error generating image</div>`;
320
  }finally{
321
- isGenerating=false; generateBtn.disabled=false;
322
- saveSettingsToStorage();
323
  }
324
  }
325
 
 
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
  <title>imagegen</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
 
 
 
 
8
 
9
+ <!-- Tracking removed per request (Smartlook + Google Tag). TODO: add GTM later if needed. -->
10
+
11
+ <style>
12
+ *, *::before, *::after { box-sizing: border-box; }
13
+ html, body { height: 100%; margin: 0; padding: 0; font-family: 'Poppins', sans-serif; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
14
+ body { background: radial-gradient(circle at 20% 20%, rgba(140,60,200,0.08), transparent 40%), radial-gradient(circle at 80% 80%, rgba(200,60,255,0.05), transparent 40%), #000; color:#fff; overflow: hidden; }
15
+
16
+ .topbar {
17
+ position: fixed; top: 0; left: 0; right: 0; height: 56px;
18
+ display: flex; align-items: center; justify-content: space-between;
19
+ padding: 0 16px; gap: 12px; z-index: 1000; background: transparent;
20
+ }
21
+ .btn-small {
22
+ background: #141414; border-radius: 10px; padding: 8px 12px;
23
+ font-size: 13px; color: #fff; display:inline-flex; align-items:center; gap:10px;
24
+ border:1px solid rgba(255,255,255,0.03); cursor: pointer;
25
+ }
26
+ .btn-small img { width:14px; height:14px; display:block; }
27
 
28
+ .container { width: 70%; max-width: 1100px; margin: 5em auto 16px; padding: 0 12px; display: flex; flex-direction: column; gap: 12px; position: relative; }
29
+ @media (max-width: 768px) { .container { width: 100%; padding: 0 12px; margin-top:5em; } }
30
 
31
  .input-row { display:flex; gap:10px; align-items:flex-start; }
32
+ textarea#prompt {
33
+ flex:1; min-width:0; max-height:250px; height:56px; resize:none; border-radius:12px; padding:12px;
34
+ background:#0e0e0e; border:1px solid rgba(255,255,255,0.03); color:#fff; font-size:14px; line-height:1.35; overflow:auto; outline:none;
35
+ }
36
+ .generate-btn {
37
+ background: linear-gradient(90deg,#ff93e9,#9b7cff); border:none; border-radius:12px; padding:12px 18px; font-size:14px; cursor:pointer; color:#fff;
38
+ min-height:56px; display:inline-flex; align-items:center; justify-content:center; transition:opacity .15s;
39
+ }
40
+ .generate-btn[disabled] { opacity:0.6; cursor:default; }
41
+
42
+ .options-row { display:flex; justify-content:space-between; align-items:center; }
43
+ .left-group, .right-group { display:flex; gap:8px; align-items:center; }
44
+
45
+ .modal-backdrop { display:none; position: fixed; inset: 0; background: rgba(0,0,0,0.72); align-items: center; justify-content: center; padding: 20px; z-index: 2000; }
46
+ .modal { width: 100%; max-width: 720px; background: #0f0f0f; border-radius: 12px; padding: 18px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); color: #fff; }
47
+ @media (max-width:420px) { .modal { padding: 14px; max-width: calc(100% - 12px); border-radius: 10px; } }
48
+
49
+ .styles-grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; margin-top:12px; }
50
+ .style-card { background:#101010; border-radius:10px; overflow:hidden; cursor:pointer; text-align:center; border:2px solid transparent; padding-bottom:8px; font-size:13px; }
51
  .style-card img { width:100%; height:72px; object-fit:cover; display:block; }
52
+ .style-card.selected { border-color:#ff93e9; box-shadow: 0 8px 30px rgba(155,124,255,0.06); }
53
+
54
+ .settings-row { display:flex; gap:12px; align-items:center; margin-top:10px; flex-wrap:wrap; }
55
+ .settings-row label { font-size:14px; display:flex; gap:8px; align-items:center; color:#eaeaea; }
56
+ .settings-row input[type="number"], .settings-row select { background:#0c0c0c; border:1px solid rgba(255,255,255,0.04); color:#fff; padding:6px 8px; border-radius:8px; font-size:14px; min-width:90px; }
57
+
58
+ .modal-actions { margin-top:14px; display:flex; justify-content:flex-end; gap:10px; }
59
+ .modal-actions button { background:#1a1a1a; border-radius:10px; padding:9px 12px; font-size:14px; color:#fff; border:1px solid rgba(255,255,255,0.03); cursor:pointer; }
60
 
61
+ #images { width: 100%; margin-top:8px; display:flex; gap:12px; flex-wrap:wrap; justify-content:center; align-items:flex-start; overflow:auto; padding:10px; border-radius:10px; background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent); border:1px solid rgba(255,255,255,0.02); max-height: calc(100vh - 5em - 220px); }
62
+
63
+ .image-card { background:#0b0b0b; border-radius:12px; padding:8px; min-height:120px; width: min(350px, 90vw); box-shadow: 0 10px 30px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.02); display:flex; flex-direction:column; gap:8px; align-items:center; justify-content:center; }
64
  .image-card img { width:100%; height:auto; display:block; border-radius:8px; }
65
+
66
+ .img-actions { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; width:100%; }
67
  .img-actions button { background:#141414; border:1px solid rgba(255,255,255,0.08); color:#fff; border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; }
68
+
69
+ .loader { border:3px solid #222; border-top:3px solid #ff93e9; border-radius:50%; width:22px; height:22px; animation:spin 1s linear infinite; }
70
  @keyframes spin { 0%{ transform:rotate(0) } 100%{ transform:rotate(360deg) } }
71
+
72
  .muted { color:#9b9bb9; font-size:13px; }
73
  .note { font-size:13px; color:#cfcfcf; margin-top:6px; }
74
  </style>
 
76
  <body>
77
 
78
  <div class="topbar" role="banner">
79
+ <!-- Replaced Discord invite with Manus invite -->
80
  <div class="btn-small" onclick="window.open('https://manus.im/invitation/XHHHQTMBM0ZCTX')">Manus</div>
81
+
82
+ <!-- Keep Buy Me a Coffee visible but remove link for now -->
83
+ <!-- TODO: add Buy Me a Coffee link later -->
84
  <a class="btn-small" aria-disabled="true">
85
  <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="coffee">
86
  <span>Buy me a coffee</span>
 
93
  <button id="generateBtn" class="generate-btn" aria-label="Generate">Generate</button>
94
  </div>
95
 
96
+ <div class="options-row">
97
+ <div class="left-group">
98
+ <div id="styleBtn" class="btn-small" aria-haspopup="dialog">🎨 Style</div>
99
+ </div>
100
+ <div class="right-group">
101
+ <div id="settingsBtn" class="btn-small" aria-haspopup="dialog">⚙️ Settings</div>
102
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
 
105
+ <div class="note">Generated images (scroll this pane). New images appear at the top.</div>
106
+ <div id="images" aria-live="polite"></div>
107
+ </div>
108
+
109
+ <!-- Style & Framing Modal -->
110
+ <div id="styleModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true">
111
+ <div class="modal" role="document">
112
+ <h2 style="margin:0 0 6px 0;">Choose a style</h2>
113
+ <div class="note">Click a style — it will be applied silently to your request.</div>
114
+
115
  <div class="styles-grid" id="stylesGrid">
116
  <div class="style-card selected" data-name="none"><img src="none.jpg" alt="No style"><div>No style</div></div>
117
  <div class="style-card" data-name="cinema"><img src="https://xyplon.web.app/assets/Cinematic.jpeg" alt="Cinema"><div>Cinema</div></div>
 
119
  <div class="style-card" data-name="photography"><img src="https://xyplon.web.app/assets/Photography.jpeg" alt="Photography"><div>Photography</div></div>
120
  <div class="style-card" data-name="fantasy"><img src="https://xyplon.web.app/assets/Digital.jpeg" alt="Fantasy"><div>Fantasy</div></div>
121
  </div>
122
+
123
+ <!-- Framing presets (one-line each) -->
124
+ <div class="settings-row" style="margin-top:14px;">
125
+ <label>Framing:
126
+ <select id="framing">
127
+ <option value="auto" selected>Auto</option>
128
+ <option value="close">Close‑up</option>
129
+ <option value="medium">Medium</option>
130
+ <option value="full">Full‑body</option>
131
+ </select>
132
+ </label>
133
+ </div>
134
+
135
+ <div class="modal-actions">
136
+ <button id="closeStyle">Close</button>
137
+ </div>
138
  </div>
139
+ </div>
140
 
141
+ <!-- Settings Modal -->
142
+ <div id="settingsModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true">
143
+ <div class="modal" role="document">
144
+ <h2 style="margin:0 0 6px 0;">API Settings</h2>
145
+
146
+ <div class="settings-row">
147
+ <label>Model:
148
+ <select id="model">
149
+ <option value="flux">flux</option>
150
+ <option value="turbo">turbo (uncensored)</option>
151
+ </select>
152
+ </label>
153
+
154
+ <label>Aspect:
155
+ <select id="aspect">
156
+ <option value="custom" selected>Custom</option>
157
+ <option value="1:1">1:1</option>
158
+ <option value="16:9">16:9</option>
159
+ <option value="9:16">9:16</option>
160
+ <option value="3:2">3:2</option>
161
+ <option value="2:3">2:3</option>
162
+ <option value="4:5">4:5</option>
163
+ </select>
164
+ </label>
165
+
166
+ <label>Width:
167
+ <input id="width" type="number" min="64" max="2048" value="1024" />
168
+ </label>
169
+
170
+ <label>Height:
171
+ <input id="height" type="number" min="64" max="2048" value="1024" />
172
+ </label>
173
+
174
+ <label>Enhance:
175
+ <input id="enhance" type="checkbox" />
176
+ </label>
177
+
178
+ <label>Seed:
179
+ <input id="seed" type="number" value="42" />
180
+ </label>
181
+
182
+ <label style="display:flex;align-items:center;gap:6px;">
183
+ <input id="randomSeed" type="checkbox" checked /> Random
184
+ </label>
185
+ </div>
186
+
187
+ <div class="modal-actions">
188
+ <button id="closeSettings">Close</button>
189
+ <button id="saveSettings">Save</button>
190
+ </div>
191
+ </div>
192
  </div>
193
 
194
  <script>
 
196
  let selectedStyle = null;
197
  let isGenerating = false;
198
 
199
+ // --- Style templates (concise) ---
200
  const styleTemplates = {
201
+ cinema: " cinematic lighting, letterbox aspect, rich color grading",
202
  realistic: " realistic stock photo",
203
+ photography: "85mm lens, shallow depth of field, natural film grain",
204
  fantasy: " epic fantasy, vibrant colors, surreal composition"
205
  };
206
 
207
+ // --- Framing presets: one-line each ---
208
  const framingTemplates = {
209
  auto: "",
210
+ close: " tight framing, head-and-shoulders, 85mm lens, shallow depth of field",
211
+ medium: " waist-up, 50mm lens, natural perspective",
212
+ full: " head-to-toe, 35mm lens, subject fully in frame"
213
  };
214
 
215
+ // --- Safer SYSTEM_PROMPT ---
216
+ const SYSTEM_PROMPT = `You are an assistant that enhances user prompts for high-quality, creative image generation with the Flux model.\n\nSafety: Do not produce or encourage illegal, harmful, violent, hateful, or sexual content (including minors), or content that violates platform policies. Keep outputs appropriate and respectful.\n\nGuidelines:\n- Prefer technical clarity (camera type, lens, exposure, lighting, environment) over vague adjectives.\n- Keep color natural; avoid oversaturation unless the user asks.\n- Choose a single coherent style; avoid mixing incompatible styles (e.g., do not mix "realistic" with "photography" keywords redundantly).\n- If the user already specified lens/shot details, do not duplicate them.\n- Keep the final prompt concise and descriptive; return only the enhanced prompt text with no preamble.`;
217
+
218
+ // --- Enhance prompt via text endpoint ---
219
+ async function enhancePrompt(userPrompt) {
220
+ try {
221
+ const messages = [
222
+ { role: 'system', content: SYSTEM_PROMPT },
223
+ { role: 'user', content: `"${userPrompt}"` }
224
+ ];
225
+ const res = await fetch('https://text.pollinations.ai/openai', {
226
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ model: 'openai', messages })
228
+ });
229
+ if (!res.ok) throw new Error('HTTP ' + res.status);
230
+ const data = await res.json();
231
+ return (data.choices?.[0]?.message?.content || userPrompt).trim();
232
+ } catch (e) {
233
+ console.error('Enhance fail:', e); return userPrompt;
234
+ }
235
+ }
236
 
237
+ // --- Persist settings ---
238
  function saveSettingsToStorage() {
239
  const s = {
240
  model: document.getElementById('model').value,
241
  aspect: document.getElementById('aspect').value,
 
242
  width: Number(document.getElementById('width').value || 1024),
243
  height: Number(document.getElementById('height').value || 1024),
244
  seed: Number(document.getElementById('seed').value || 42),
 
252
  const s = JSON.parse(localStorage.getItem('ai_img_settings') || '{}');
253
  if (s.model) document.getElementById('model').value = s.model;
254
  if (s.aspect) document.getElementById('aspect').value = s.aspect;
 
255
  if (s.width) document.getElementById('width').value = s.width;
256
  if (s.height) document.getElementById('height').value = s.height;
257
  if (s.seed || s.seed === 0) document.getElementById('seed').value = s.seed;
258
+ document.getElementById('randomSeed').checked = (s.randomSeed !== undefined) ? s.randomSeed : true;
259
+ document.getElementById('enhance').checked = (s.enhance !== undefined) ? s.enhance : true; // bug fixed
260
  } catch(e){}
261
  }
262
+ loadSettingsFromStorage();
263
 
264
+ // --- Aspect helpers ---
265
+ function parseAspect(aspectStr){ const [w,h] = aspectStr.split(':').map(Number); return (!w||!h)?1:(w/h); }
266
+ function sizeForAspect(aspectStr, base=1024){
267
  const r = parseAspect(aspectStr);
268
+ if (r >= 1){ const width=base; const height=Math.max(64, Math.round(width/r)); return {width,height}; }
269
+ else { const height=base; const width=Math.max(64, Math.round(height*r)); return {width,height}; }
270
  }
271
  function applyAspectToFields(){
272
+ const aspect = document.getElementById('aspect').value;
273
+ if (aspect === 'custom') return; // respect manual values
274
+ const {width, height} = sizeForAspect(aspect, 1024);
275
  document.getElementById('width').value = width;
276
  document.getElementById('height').value = height;
277
  }
278
 
279
+ // --- Modal plumbing ---
280
+ const styleModal = document.getElementById('styleModal');
281
+ const settingsModal = document.getElementById('settingsModal');
282
+ function openModal(modal) { modal.style.display = 'flex'; modal.setAttribute('aria-hidden','false'); document.body.style.overflow = 'hidden'; }
283
+ function closeModal(modal) { modal.style.display = 'none'; modal.setAttribute('aria-hidden','true'); document.body.style.overflow = ''; }
284
+ document.getElementById('styleBtn').addEventListener('click', () => openModal(styleModal));
285
+ document.getElementById('closeStyle').addEventListener('click', () => closeModal(styleModal));
286
+ document.getElementById('settingsBtn').addEventListener('click', () => openModal(settingsModal));
287
+ document.getElementById('closeSettings').addEventListener('click', () => closeModal(settingsModal));
288
+ document.getElementById('saveSettings').addEventListener('click', () => {
289
+ // clamp + optional aspect auto-size
290
+ const widthEl = document.getElementById('width');
291
+ const heightEl = document.getElementById('height');
292
+ widthEl.value = Math.max(64, Math.min(2048, Number(widthEl.value) || 1024));
293
+ heightEl.value = Math.max(64, Math.min(2048, Number(heightEl.value) || 1024));
294
+ applyAspectToFields();
295
+ saveSettingsToStorage();
296
+ closeModal(settingsModal);
297
+ });
298
+ document.querySelectorAll('.modal-backdrop').forEach(back => back.addEventListener('click', (e) => { if (e.target === back) closeModal(back); }));
299
+ window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (styleModal.style.display === 'flex') closeModal(styleModal); if (settingsModal.style.display === 'flex') closeModal(settingsModal); } });
300
+
301
+ // Style select
302
+ document.querySelectorAll('.style-card').forEach(card => {
303
+ card.addEventListener('click', () => {
304
+ document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected'));
305
  card.classList.add('selected');
306
  selectedStyle = card.dataset.name === 'none' ? null : card.dataset.name;
307
  });
308
  });
309
 
310
+ // Aspect change auto-size
311
  document.getElementById('aspect').addEventListener('change', applyAspectToFields);
312
 
313
+ // Prompt textarea auto-size
314
+ const promptEl = document.getElementById('prompt');
315
+ function autoResizeTextarea(){ promptEl.style.height='auto'; promptEl.style.height = Math.min(250, promptEl.scrollHeight) + 'px'; }
316
+ promptEl.addEventListener('input', autoResizeTextarea); setTimeout(autoResizeTextarea, 0);
317
+
318
+ // Seed UI
319
  const randomSeedEl = document.getElementById('randomSeed');
320
  const seedEl = document.getElementById('seed');
321
+ randomSeedEl.addEventListener('change', () => { seedEl.disabled = randomSeedEl.checked; });
322
  seedEl.disabled = randomSeedEl.checked;
323
 
324
+ // Images + Generate
 
 
 
 
325
  const imagesContainer = document.getElementById('images');
326
  const generateBtn = document.getElementById('generateBtn');
327
+ function randomInt(min, max){ return Math.floor(Math.random()*(max-min+1))+min; }
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  function makeActionsBar(imgUrl, filename, finalPrompt, seed, width, height){
330
  const bar = document.createElement('div');
331
  bar.className = 'img-actions';
332
 
 
333
  const dl = document.createElement('button');
334
  dl.textContent = 'Download';
335
+ dl.addEventListener('click', () => { const a = document.createElement('a'); a.href = imgUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); });
 
 
 
 
 
 
 
336
 
 
337
  const openBtn = document.createElement('button');
338
  openBtn.textContent = 'Open';
339
+ openBtn.addEventListener('click', () => window.open(imgUrl,'_blank'));
340
 
 
341
  const cp = document.createElement('button');
342
  cp.textContent = 'Copy prompt';
343
+ cp.addEventListener('click', async () => { try { await navigator.clipboard.writeText(finalPrompt); cp.textContent='Copied!'; setTimeout(()=>cp.textContent='Copy prompt',900);} catch(e){} });
 
 
344
 
 
345
  const rerun = document.createElement('button');
346
  rerun.textContent = 'Re‑generate';
347
+ rerun.addEventListener('click', () => {
348
  document.getElementById('randomSeed').checked = false;
349
  document.getElementById('seed').disabled = false;
350
  document.getElementById('seed').value = seed;
 
364
  if (isGenerating) return;
365
  const rawPrompt = promptEl.value.trim();
366
  if (!rawPrompt){
367
+ const temp=document.createElement('div'); temp.className='image-card'; temp.innerHTML='<div class="muted">Please enter a prompt</div>'; imagesContainer.prepend(temp); setTimeout(()=>temp.remove(),1200); return;
 
 
368
  }
369
 
370
  isGenerating = true; generateBtn.disabled=true;
371
 
372
  const card = document.createElement('div');
373
  card.className='image-card';
374
+ card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`;
375
  imagesContainer.prepend(card);
376
 
 
377
  const enhanceOn = document.getElementById('enhance').checked;
378
  const framingChoice = document.getElementById('framing').value;
379
 
380
+ // Build prompt (style + framing in concise one-liners)
381
+ let promptWithMods = rawPrompt;
382
+ if (selectedStyle && styleTemplates[selectedStyle]) promptWithMods += styleTemplates[selectedStyle];
383
+ if (framingTemplates[framingChoice]) promptWithMods += framingTemplates[framingChoice];
384
 
385
+ let finalPrompt = promptWithMods;
386
+ if (enhanceOn){
387
+ card.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Enhancing prompt...</div></div>`;
388
+ finalPrompt = await enhancePrompt(promptWithMods);
389
+ card.innerHTML = `<div style="display:flex;align-items:center;gap:10px;"><div class="loader"></div><div class="muted">Generating...</div></div>`;
390
+ }
391
 
392
  // Params
393
  const model = encodeURIComponent(document.getElementById('model').value || 'flux');
 
410
  console.error(err);
411
  card.innerHTML = `<div class="muted" style="color:#ff6b6b;">Error generating image</div>`;
412
  }finally{
413
+ isGenerating=false; generateBtn.disabled=false; saveSettingsToStorage();
 
414
  }
415
  }
416