hadadrjt commited on
Commit
4f33245
·
1 Parent(s): 3044058

image: Production!

Browse files

Signed-off-by: Hadad <[email protected]>

assets/plugins/imageGenerator.js CHANGED
@@ -6,17 +6,383 @@
6
  (function () {
7
  'use strict';
8
 
9
- var isGenerating = false;
10
- var requestId = '';
11
-
 
 
 
12
  if (document && document.body) {
13
- isGenerating = document.body.dataset.isGenerating === 'true';
14
- requestId = document.body.dataset.requestId || '';
15
- }
16
-
17
- if (isGenerating) {
18
- setTimeout(function () {
19
- window.location.href = '/?rid=' + encodeURIComponent(requestId);
20
- }, 10000);
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  })();
 
6
  (function () {
7
  'use strict';
8
 
9
+ var ws = null;
10
+ var sessionId = '';
11
+ var reconnectTimer = null;
12
+ var isConnecting = false;
13
+ var currentImages = [];
14
+
15
  if (document && document.body) {
16
+ sessionId = document.body.dataset.sessionId || '';
 
 
 
 
 
 
 
17
  }
18
+
19
+ var connectWebSocket = function() {
20
+ if (isConnecting || (ws && ws.readyState === 1)) return;
21
+
22
+ isConnecting = true;
23
+ var protocol = window.location.protocol === 'https:'
24
+ ? 'wss:' : 'ws:';
25
+ var wsUrl = protocol + '//' +
26
+ window.location.host;
27
+
28
+ ws = new WebSocket(wsUrl);
29
+
30
+ ws.onopen = function() {
31
+ isConnecting = false;
32
+ if (reconnectTimer) {
33
+ clearTimeout(reconnectTimer);
34
+ reconnectTimer = null;
35
+ }
36
+
37
+ ws.send(JSON.stringify({
38
+ type: 'register',
39
+ sessionId: sessionId
40
+ }));
41
+ };
42
+
43
+ ws.onmessage = function(event) {
44
+ try {
45
+ var data = JSON.parse(event.data);
46
+
47
+ if (!data.sessionId ||
48
+ data.sessionId !== sessionId) {
49
+ return;
50
+ }
51
+
52
+ handleWebSocketMessage(data);
53
+ } catch (e) {}
54
+ };
55
+
56
+ ws.onclose = function() {
57
+ isConnecting = false;
58
+ reconnectWebSocket();
59
+ };
60
+
61
+ ws.onerror = function() {
62
+ isConnecting = false;
63
+ if (ws) ws.close();
64
+ };
65
+ };
66
+
67
+ var reconnectWebSocket = function() {
68
+ if (reconnectTimer) return;
69
+
70
+ reconnectTimer = setTimeout(function() {
71
+ reconnectTimer = null;
72
+ connectWebSocket();
73
+ }, 1000);
74
+ };
75
+
76
+ var handleWebSocketMessage = function(data) {
77
+ if (!data || !data.type) return;
78
+
79
+ switch(data.type) {
80
+ case 'progressUpdate':
81
+ updateProgressUI(data.progress);
82
+ break;
83
+
84
+ case 'generationStarted':
85
+ showGeneratingUI();
86
+ break;
87
+
88
+ case 'generationComplete':
89
+ handleGenerationComplete(data.images);
90
+ break;
91
+
92
+ case 'generationError':
93
+ handleGenerationError(data.error);
94
+ break;
95
+
96
+ case 'generationCancelled':
97
+ handleGenerationCancelled();
98
+ break;
99
+
100
+ case 'imageDeleted':
101
+ handleImageDeleted(data.images);
102
+ break;
103
+ }
104
+ };
105
+
106
+ var updateProgressUI = function(progress) {
107
+ var progressFill = document.querySelector('.progress-fill');
108
+ var progressText = document.querySelector('.progress-text');
109
+
110
+ if (progressFill) {
111
+ progressFill.style.width = progress + '%';
112
+ }
113
+
114
+ if (progressText) {
115
+ progressText.textContent =
116
+ Math.floor(progress) + '% Complete';
117
+ }
118
+ };
119
+
120
+ var showGeneratingUI = function() {
121
+ var outputSection = document.querySelector(
122
+ '.image-output-section'
123
+ );
124
+ var form = document.getElementById('generateForm');
125
+ var inputs = form ?
126
+ form.querySelectorAll('input, select, textarea') : [];
127
+
128
+ Array.prototype.forEach.call(inputs, function(input) {
129
+ input.disabled = true;
130
+ });
131
+
132
+ if (outputSection) {
133
+ outputSection.classList.remove('has-images');
134
+ outputSection.innerHTML =
135
+ '<div class="loading-container">' +
136
+ '<div class="loading-spinner" ' +
137
+ 'style="margin: 0 auto 20px;"></div>' +
138
+ '<p class="loading-text">Generating your image...</p>' +
139
+ '<div class="progress-bar">' +
140
+ '<div class="progress-fill" style="width: 0%;"></div>' +
141
+ '</div>' +
142
+ '<p class="progress-text">0% Complete</p>' +
143
+ '</div>';
144
+ }
145
+
146
+ updateButtonsForGeneration(true);
147
+ };
148
+
149
+ var hideGeneratingUI = function() {
150
+ var form = document.getElementById('generateForm');
151
+ var inputs = form ?
152
+ form.querySelectorAll('input, select, textarea') : [];
153
+
154
+ Array.prototype.forEach.call(inputs, function(input) {
155
+ input.disabled = false;
156
+ });
157
+
158
+ updateButtonsForGeneration(false);
159
+ window.validateInputs && window.validateInputs();
160
+ };
161
+
162
+ var resetToInitialState = function() {
163
+ hideGeneratingUI();
164
+
165
+ if (currentImages && currentImages.length > 0) {
166
+ displayImages(currentImages);
167
+ } else {
168
+ showPlaceholder();
169
+ }
170
+ };
171
+
172
+ var showPlaceholder = function() {
173
+ var outputSection = document.querySelector(
174
+ '.image-output-section'
175
+ );
176
+
177
+ if (!outputSection) return;
178
+
179
+ outputSection.classList.remove('has-images');
180
+ outputSection.innerHTML =
181
+ '<svg class="placeholder-icon" width="80" height="80" ' +
182
+ 'viewBox="0 0 24 24" fill="none">' +
183
+ '<path d="M21 3H3C2 3 1 4 1 5V19C1 20 2 21 3 21H21C22 ' +
184
+ '21 23 20 23 19V5C23 4 22 3 21 3ZM21 19H3V5H21V19Z" ' +
185
+ 'fill="currentColor"/>' +
186
+ '<path d="M4.5 16.5L9 12L11.5 14.5L16 10L19.5 13.5" ' +
187
+ 'stroke="currentColor" stroke-width="1.5" ' +
188
+ 'stroke-linecap="round"/>' +
189
+ '<circle cx="8" cy="8.5" r="1.5" fill="currentColor"/>' +
190
+ '</svg>' +
191
+ '<p class="placeholder-text">' +
192
+ 'No images generated yet. Start creating amazing visuals!' +
193
+ '</p>';
194
+ };
195
+
196
+ var updateButtonsForGeneration = function(isGenerating) {
197
+ var buttonsContainer = document.querySelector(
198
+ '.flex.justify-center.gap-4'
199
+ );
200
+
201
+ if (!buttonsContainer) return;
202
+
203
+ if (isGenerating) {
204
+ buttonsContainer.innerHTML =
205
+ '<button type="button" onclick="cancelGeneration()" ' +
206
+ 'class="btn btn-danger">' +
207
+ '<svg class="button-icon" viewBox="0 0 24 24" ' +
208
+ 'fill="none">' +
209
+ '<rect x="4" y="4" width="16" height="16" rx="3" ' +
210
+ 'fill="currentColor"/>' +
211
+ '</svg>' +
212
+ 'Stop Generation' +
213
+ '</button>';
214
+ } else {
215
+ buttonsContainer.innerHTML =
216
+ '<button type="submit" id="submitBtn" disabled ' +
217
+ 'class="btn btn-primary">' +
218
+ '<svg class="button-icon" viewBox="0 0 24 24" ' +
219
+ 'fill="none">' +
220
+ '<path d="M3 20V4L22 12L3 20ZM5 17L16.85 12L5 7V10.5' +
221
+ 'L11 12L5 13.5V17Z" fill="currentColor"/>' +
222
+ '</svg>' +
223
+ 'Generate Image' +
224
+ '</button>';
225
+ }
226
+ };
227
+
228
+ var handleGenerationComplete = function(images) {
229
+ currentImages = images || [];
230
+ hideGeneratingUI();
231
+ displayImages(currentImages);
232
+ };
233
+
234
+ var handleGenerationError = function(error) {
235
+ resetToInitialState();
236
+ showErrorModal(error);
237
+ };
238
+
239
+ var handleGenerationCancelled = function() {
240
+ resetToInitialState();
241
+ };
242
+
243
+ var handleImageDeleted = function(images) {
244
+ currentImages = images || [];
245
+ displayImages(currentImages);
246
+ };
247
+
248
+ var displayImages = function(images) {
249
+ var outputSection = document.querySelector(
250
+ '.image-output-section'
251
+ );
252
+
253
+ if (!outputSection) return;
254
+
255
+ if (!images || images.length === 0) {
256
+ showPlaceholder();
257
+ } else {
258
+ outputSection.classList.add('has-images');
259
+ var html = '<div class="image-grid">';
260
+
261
+ images.forEach(function(image, index) {
262
+ html +=
263
+ '<div class="image-card">' +
264
+ '<img src="data:image/png;base64,' + image.base64 +
265
+ '" alt="' + image.prompt + '">' +
266
+ '<div class="image-actions">' +
267
+ '<a href="data:image/png;base64,' + image.base64 +
268
+ '" download="generated-' + image.id + '.png" ' +
269
+ 'class="action-btn">' +
270
+ '<svg class="action-icon" viewBox="0 0 24 24" ' +
271
+ 'fill="none">' +
272
+ '<path d="M12 16L7 11L8.4 9.55L11 12.15V4H13V12.15' +
273
+ 'L15.6 9.55L17 11L12 16Z" fill="currentColor"/>' +
274
+ '<path d="M4 20C3.45 20 2.98 19.8 2.59 19.41C2.2 ' +
275
+ '19.02 2 18.55 2 18V15H4V18H20V15H22V18C22 18.55 ' +
276
+ '21.8 19.02 21.41 19.41C21.02 19.8 20.55 20 20 20H4Z" ' +
277
+ 'fill="currentColor"/>' +
278
+ '</svg>' +
279
+ '</a>' +
280
+ '<button type="button" onclick="deleteImage(' +
281
+ index + ')" class="action-btn">' +
282
+ '<svg class="action-icon" viewBox="0 0 24 24" ' +
283
+ 'fill="none">' +
284
+ '<path d="M18.3 5.71C17.91 5.32 17.28 5.32 16.89 ' +
285
+ '5.71L12 10.59L7.11 5.7C6.72 5.31 6.09 5.31 5.7 ' +
286
+ '5.7C5.31 6.09 5.31 6.72 5.7 7.11L10.59 12L5.7 ' +
287
+ '16.89C5.31 17.28 5.31 17.91 5.7 18.3C6.09 18.69 ' +
288
+ '6.72 18.69 7.11 18.3L12 13.41L16.89 18.3C17.28 ' +
289
+ '18.69 17.91 18.69 18.3 18.3C18.69 17.91 18.69 ' +
290
+ '17.28 18.3 16.89L13.41 12L18.3 7.11C18.68 6.73 ' +
291
+ '18.68 6.09 18.3 5.71Z" fill="currentColor"/>' +
292
+ '</svg>' +
293
+ '</button>' +
294
+ '</div>' +
295
+ '<div class="image-info">' +
296
+ '<p class="image-prompt">' + image.prompt + '</p>' +
297
+ '<p class="image-meta">' +
298
+ '<span class="image-model">' +
299
+ image.model.toUpperCase() + '</span> | ' +
300
+ image.size + '</p>' +
301
+ '</div>' +
302
+ '</div>';
303
+ });
304
+
305
+ html += '</div>';
306
+ outputSection.innerHTML = html;
307
+ }
308
+ };
309
+
310
+ var showErrorModal = function(error) {
311
+ var existingModal = document.getElementById('errorModal');
312
+ if (existingModal) existingModal.remove();
313
+
314
+ var modal = document.createElement('div');
315
+ modal.id = 'errorModal';
316
+ modal.className = 'modal-overlay';
317
+ modal.innerHTML =
318
+ '<div class="modal-content modal-error-content">' +
319
+ '<div class="modal-inner">' +
320
+ '<h3 class="modal-error-title">Error</h3>' +
321
+ '<p class="modal-error-text">' + error + '</p>' +
322
+ '<button onclick="closeErrorModal()" ' +
323
+ 'class="btn btn-primary w-full">OK</button>' +
324
+ '</div>' +
325
+ '</div>';
326
+
327
+ document.body.appendChild(modal);
328
+ };
329
+
330
+ window.deleteImage = function(index) {
331
+ var xhr = new XMLHttpRequest();
332
+ xhr.open('POST', '/', true);
333
+ xhr.setRequestHeader(
334
+ 'Content-Type',
335
+ 'application/json'
336
+ );
337
+
338
+ xhr.onload = function() {
339
+ try {
340
+ var response = JSON.parse(xhr.responseText);
341
+ if (response.success) {
342
+ currentImages = response.images || [];
343
+ displayImages(currentImages);
344
+ }
345
+ } catch (e) {}
346
+ };
347
+
348
+ xhr.send(JSON.stringify({
349
+ action: 'delete',
350
+ sessionId: sessionId,
351
+ imageIndex: index
352
+ }));
353
+ };
354
+
355
+ var initializeImages = function() {
356
+ var outputSection = document.querySelector(
357
+ '.image-output-section'
358
+ );
359
+
360
+ if (outputSection && outputSection.classList.contains('has-images')) {
361
+ var imageCards = outputSection.querySelectorAll('.image-card');
362
+ if (imageCards && imageCards.length > 0) {
363
+ currentImages = [];
364
+ imageCards.forEach(function(card) {
365
+ var img = card.querySelector('img');
366
+ var prompt = card.querySelector('.image-prompt');
367
+ var model = card.querySelector('.image-model');
368
+ var meta = card.querySelector('.image-meta');
369
+
370
+ if (img && img.src && img.src.includes('base64,')) {
371
+ var base64 = img.src.split('base64,')[1];
372
+ var size = meta ? meta.textContent.split('|')[1] : '';
373
+ currentImages.push({
374
+ id: 'existing-' + Math.random().toString(36).substring(2, 15),
375
+ base64: base64,
376
+ prompt: prompt ? prompt.textContent : '',
377
+ model: model ? model.textContent.toLowerCase() : '',
378
+ size: size ? size.trim() : ''
379
+ });
380
+ }
381
+ });
382
+ }
383
+ }
384
+ };
385
+
386
+ connectWebSocket();
387
+ initializeImages();
388
  })();
assets/plugins/loadParameter.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ const modelSelect = document.getElementById('modelSelect');
8
+ const sizeSelect = document.getElementById('sizeSelect');
9
+ const examplesGrid = document.getElementById('examplesGrid');
10
+
11
+ if (typeof models !== 'undefined' && modelSelect) {
12
+ models.forEach(model => {
13
+ const option = document.createElement('option');
14
+ option.value = model.value;
15
+ option.textContent = model.label;
16
+ modelSelect.appendChild(option);
17
+ });
18
+ }
19
+
20
+ if (typeof sizes !== 'undefined' && sizeSelect) {
21
+ Object.keys(sizes).forEach(category => {
22
+ const optgroup = document.createElement('optgroup');
23
+ optgroup.label = category.charAt(0).toUpperCase() +
24
+ category.slice(1);
25
+
26
+ sizes[category].forEach(size => {
27
+ const option = document.createElement('option');
28
+ option.value = size.value;
29
+ option.textContent = size.label;
30
+ optgroup.appendChild(option);
31
+ });
32
+
33
+ sizeSelect.appendChild(optgroup);
34
+ });
35
+ }
36
+
37
+ if (typeof examples !== 'undefined' && examplesGrid) {
38
+ examples.forEach(example => {
39
+ const div = document.createElement('div');
40
+ div.className = 'example-card';
41
+ div.onclick = function() {
42
+ triggerExample(
43
+ example.prompt,
44
+ example.model,
45
+ example.size
46
+ );
47
+ };
48
+
49
+ const promptP = document.createElement('p');
50
+ promptP.className = 'example-text';
51
+ promptP.textContent = example.prompt;
52
+
53
+ const metaP = document.createElement('p');
54
+ metaP.className = 'example-meta';
55
+
56
+ const modelSpan = document.createElement('span');
57
+ modelSpan.className = 'example-model';
58
+ modelSpan.textContent = example.modelLabel;
59
+
60
+ metaP.appendChild(modelSpan);
61
+ metaP.appendChild(
62
+ document.createTextNode(' | ' + example.sizeLabel)
63
+ );
64
+
65
+ if (example.note) {
66
+ metaP.appendChild(
67
+ document.createTextNode(' | ' + example.note)
68
+ );
69
+ }
70
+
71
+ div.appendChild(promptP);
72
+ div.appendChild(metaP);
73
+ examplesGrid.appendChild(div);
74
+ });
75
+ }
76
+ });
assets/plugins/webLoader.js CHANGED
@@ -23,14 +23,9 @@
23
  });
24
  }
25
 
26
- if (encode === false) {
27
- return clean;
28
- }
29
-
30
- if (typeof he !== 'undefined') {
31
  clean = he.encode(clean, {
32
- useNamedReferences: true,
33
- allowUnsafeSymbols: false
34
  });
35
  }
36
 
@@ -118,9 +113,14 @@
118
  !!promptValue &&
119
  promptValue.length > 0;
120
 
121
- if (typeof validator !== 'undefined') {
 
 
 
 
 
122
  isValid = isValid &&
123
- validator.isLength(promptValue, { min: 1 });
124
  }
125
 
126
  submitBtn.disabled = !isValid;
@@ -144,137 +144,89 @@
144
  setTimeout(function () {
145
  var fa = document.getElementById('formAction');
146
  if (fa) fa.value = 'generate';
147
- form.submit();
148
  }, 100);
149
  }
150
  };
151
 
152
- var cancelGeneration = function () {
153
  var form = document.getElementById('generateForm');
154
- var fa = document.getElementById('formAction');
155
- var cancelBtn = event && event.target
156
- ? event.target.closest('button')
157
- : null;
158
-
159
- if (cancelBtn) {
160
- cancelBtn.disabled = true;
161
- cancelBtn.style.opacity = '0.6';
162
- cancelBtn.style.cursor = 'not-allowed';
163
-
164
- var btnText = cancelBtn.querySelector('span') ||
165
- cancelBtn.childNodes[cancelBtn.childNodes.length - 1];
166
-
167
- if (btnText && btnText.nodeType === 3) {
168
- btnText.textContent = 'Cancelling...';
169
- } else if (!btnText) {
170
- var textNode = Array.from(cancelBtn.childNodes)
171
- .find(function(node) {
172
- return node.nodeType === 3 &&
173
- node.textContent.trim();
174
- });
175
- if (textNode) {
176
- textNode.textContent = 'Cancelling...';
177
- }
178
- }
179
- }
180
-
181
- if (fa) {
182
- fa.value = 'cancel';
183
-
184
- if (form) {
185
- var hiddenCancel = document.createElement('input');
186
- hiddenCancel.type = 'hidden';
187
- hiddenCancel.name = 'forceCancel';
188
- hiddenCancel.value = 'true';
189
- form.appendChild(hiddenCancel);
190
-
191
- var timestamp = document.createElement('input');
192
- timestamp.type = 'hidden';
193
- timestamp.name = 'cancelTime';
194
- timestamp.value = Date.now();
195
- form.appendChild(timestamp);
196
-
197
- try {
198
- form.submit();
199
- } catch (e) {
200
- form.requestSubmit && form.requestSubmit();
201
- }
202
-
203
- setTimeout(function() {
204
- if (form && fa) {
205
- fa.value = 'cancel';
206
- try {
207
- form.submit();
208
- } catch (e) {
209
- window.location.reload();
210
- }
211
- }
212
- }, 500);
213
-
214
- setTimeout(function() {
215
- window.location.reload();
216
- }, 3000);
217
- }
218
- } else if (form) {
219
- var newFA = document.createElement('input');
220
- newFA.type = 'hidden';
221
- newFA.id = 'formAction';
222
- newFA.name = 'action';
223
- newFA.value = 'cancel';
224
- form.appendChild(newFA);
225
-
226
- try {
227
- form.submit();
228
- } catch (e) {
229
- window.location.reload();
230
- }
231
- } else {
232
- var xhr = new XMLHttpRequest();
233
- xhr.open('POST', window.location.href, true);
234
- xhr.setRequestHeader(
235
- 'Content-Type',
236
- 'application/x-www-form-urlencoded'
237
- );
238
- xhr.onload = function() {
239
- window.location.reload();
240
- };
241
- xhr.onerror = function() {
242
- window.location.reload();
243
- };
244
- xhr.send('action=cancel&forceCancel=true');
245
- }
246
-
247
- window.cancelRequested = true;
248
-
249
- if (window.EventSource) {
250
- try {
251
- var sources = window.eventSources || [];
252
- sources.forEach(function(source) {
253
- if (source && source.close) {
254
- source.close();
255
- }
256
- });
257
- } catch (e) {}
258
  }
259
-
260
- if (window.AbortController && window.abortController) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  try {
262
- window.abortController.abort();
 
 
 
263
  } catch (e) {}
264
- }
 
 
 
265
 
266
- setTimeout(function() {
267
- if (window.cancelRequested) {
268
- window.stop && window.stop();
269
- document.execCommand &&
270
- document.execCommand('Stop');
271
- }
272
- }, 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  };
274
 
275
  var modelSelect = document.getElementById('modelSelect');
276
  var sizeSelect = document.getElementById('sizeSelect');
277
  var promptInput = document.getElementById('promptInput');
 
278
 
279
  if (modelSelect) {
280
  modelSelect.addEventListener('change', validateInputs);
@@ -302,17 +254,40 @@
302
  e.preventDefault();
303
  var submitBtn = document.getElementById('submitBtn');
304
  if (submitBtn && !submitBtn.disabled) {
305
- var form = document.getElementById('generateForm');
306
- if (form) form.submit();
307
  }
308
  }
309
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
 
312
  window.closeWelcomeModal = closeWelcomeModal;
313
  window.closeErrorModal = closeErrorModal;
314
  window.triggerExample = triggerExample;
315
  window.cancelGeneration = cancelGeneration;
 
316
 
317
  validateInputs();
318
 
@@ -329,11 +304,13 @@
329
 
330
  document.addEventListener('DOMContentLoaded', function() {
331
  InitWelcomeModal();
 
332
  });
333
 
334
  if (document.readyState === 'complete' ||
335
  document.readyState === 'interactive') {
336
  InitWelcomeModal();
 
337
  }
338
 
339
  setInterval(function () {
 
23
  });
24
  }
25
 
26
+ if (encode === true && typeof he !== 'undefined') {
 
 
 
 
27
  clean = he.encode(clean, {
28
+ encodeEverything: false
 
29
  });
30
  }
31
 
 
113
  !!promptValue &&
114
  promptValue.length > 0;
115
 
116
+ if (isValid) {
117
+ var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
118
+ isValid = isValid && hasNonWhitespace;
119
+ }
120
+
121
+ if (typeof validator !== 'undefined' && isValid) {
122
  isValid = isValid &&
123
+ validator.isLength(promptValue.trim(), { min: 1 });
124
  }
125
 
126
  submitBtn.disabled = !isValid;
 
144
  setTimeout(function () {
145
  var fa = document.getElementById('formAction');
146
  if (fa) fa.value = 'generate';
147
+ submitForm();
148
  }, 100);
149
  }
150
  };
151
 
152
+ var submitForm = function() {
153
  var form = document.getElementById('generateForm');
154
+ if (!form) return;
155
+
156
+ var formData = new FormData(form);
157
+ var promptValue = sanitizeInput(formData.get('prompt'));
158
+
159
+ var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
160
+
161
+ if (!promptValue || !hasNonWhitespace) {
162
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
164
+
165
+ var data = {
166
+ action: formData.get('action') || 'generate',
167
+ prompt: promptValue,
168
+ model: formData.get('model'),
169
+ size: formData.get('size'),
170
+ sessionId: document.body.dataset.sessionId || ''
171
+ };
172
+
173
+ var xhr = new XMLHttpRequest();
174
+ xhr.open('POST', '/', true);
175
+ xhr.setRequestHeader(
176
+ 'Content-Type',
177
+ 'application/json'
178
+ );
179
+
180
+ xhr.onload = function() {
181
  try {
182
+ var response = JSON.parse(xhr.responseText);
183
+ if (!response.success && response.error) {
184
+ showErrorModal(response.error);
185
+ }
186
  } catch (e) {}
187
+ };
188
+
189
+ xhr.send(JSON.stringify(data));
190
+ };
191
 
192
+ var cancelGeneration = function () {
193
+ var xhr = new XMLHttpRequest();
194
+ xhr.open('POST', '/', true);
195
+ xhr.setRequestHeader(
196
+ 'Content-Type',
197
+ 'application/json'
198
+ );
199
+
200
+ xhr.send(JSON.stringify({
201
+ action: 'cancel',
202
+ sessionId: document.body.dataset.sessionId || ''
203
+ }));
204
+ };
205
+
206
+ var showErrorModal = function(error) {
207
+ var existingModal = document.getElementById('errorModal');
208
+ if (existingModal) existingModal.remove();
209
+
210
+ var modal = document.createElement('div');
211
+ modal.id = 'errorModal';
212
+ modal.className = 'modal-overlay';
213
+ modal.innerHTML =
214
+ '<div class="modal-content modal-error-content">' +
215
+ '<div class="modal-inner">' +
216
+ '<h3 class="modal-error-title">Error</h3>' +
217
+ '<p class="modal-error-text">' + error + '</p>' +
218
+ '<button onclick="closeErrorModal()" ' +
219
+ 'class="btn btn-primary w-full">OK</button>' +
220
+ '</div>' +
221
+ '</div>';
222
+
223
+ document.body.appendChild(modal);
224
  };
225
 
226
  var modelSelect = document.getElementById('modelSelect');
227
  var sizeSelect = document.getElementById('sizeSelect');
228
  var promptInput = document.getElementById('promptInput');
229
+ var form = document.getElementById('generateForm');
230
 
231
  if (modelSelect) {
232
  modelSelect.addEventListener('change', validateInputs);
 
254
  e.preventDefault();
255
  var submitBtn = document.getElementById('submitBtn');
256
  if (submitBtn && !submitBtn.disabled) {
257
+ submitForm();
 
258
  }
259
  }
260
  });
261
+
262
+ promptInput.addEventListener('keyup', function () {
263
+ validateInputs();
264
+ });
265
+
266
+ promptInput.addEventListener('blur', function () {
267
+ validateInputs();
268
+ });
269
+ }
270
+
271
+ if (form) {
272
+ form.addEventListener('submit', function(e) {
273
+ e.preventDefault();
274
+
275
+ var promptValue = sanitizeInput(promptInput ? promptInput.value : '');
276
+ var hasNonWhitespace = /[^\s\t\n\r\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(promptValue);
277
+
278
+ if (!promptValue || !hasNonWhitespace) {
279
+ return;
280
+ }
281
+
282
+ submitForm();
283
+ });
284
  }
285
 
286
  window.closeWelcomeModal = closeWelcomeModal;
287
  window.closeErrorModal = closeErrorModal;
288
  window.triggerExample = triggerExample;
289
  window.cancelGeneration = cancelGeneration;
290
+ window.validateInputs = validateInputs;
291
 
292
  validateInputs();
293
 
 
304
 
305
  document.addEventListener('DOMContentLoaded', function() {
306
  InitWelcomeModal();
307
+ validateInputs();
308
  });
309
 
310
  if (document.readyState === 'complete' ||
311
  document.readyState === 'interactive') {
312
  InitWelcomeModal();
313
+ validateInputs();
314
  }
315
 
316
  setInterval(function () {
config.js CHANGED
@@ -23,11 +23,15 @@ export default {
23
  },
24
  generation: {
25
  progressInterval: 800,
26
- startDelay: 100,
27
  maxProgress: 99
28
  },
29
  paths: {
30
  views: 'public',
31
  mainView: 'webViewer'
 
 
 
 
32
  }
33
  };
 
23
  },
24
  generation: {
25
  progressInterval: 800,
26
+ startDelay: 50,
27
  maxProgress: 99
28
  },
29
  paths: {
30
  views: 'public',
31
  mainView: 'webViewer'
32
+ },
33
+ websocket: {
34
+ heartbeat: 30000,
35
+ reconnectDelay: 100
36
  }
37
  };
example.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ const examples = [
7
+ {
8
+ prompt: 'A majestic mountain landscape at golden hour with dramatic clouds and vibrant colors reflecting on a crystal clear alpine lake',
9
+ model: 'flux',
10
+ size: '1536x1024',
11
+ modelLabel: 'FLUX',
12
+ sizeLabel: '1536×1024',
13
+ note: 'Recommended model'
14
+ },
15
+ {
16
+ prompt: 'An enchanted forest with bioluminescent plants and magical floating particles, moonlight streaming through ancient mystical trees',
17
+ model: 'kontext',
18
+ size: '1024x1024',
19
+ modelLabel: 'KONTEXT',
20
+ sizeLabel: '1024×1024',
21
+ note: 'Sometimes the server is busy'
22
+ },
23
+ {
24
+ prompt: 'Underwater coral reef teeming with colorful tropical fish and sea life, sunbeams penetrating crystal clear turquoise water',
25
+ model: 'turbo',
26
+ size: '1024x768',
27
+ modelLabel: 'TURBO',
28
+ sizeLabel: '1024×768',
29
+ note: 'Sometimes the server is busy'
30
+ },
31
+ {
32
+ prompt: 'A woman riding a horse across an open field, cinematic and realistic style',
33
+ model: 'nanobanana',
34
+ size: '1024x1024',
35
+ modelLabel: 'NANO BANANA',
36
+ sizeLabel: '1024×1024',
37
+ note: 'Sometimes the resolution is forced to be square'
38
+ }
39
+ ];
40
+
41
+ if (typeof module !== 'undefined' && module.exports) {
42
+ module.exports = { examples };
43
+ }
model.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ const models = [
7
+ { value: 'flux', label: 'Flux' },
8
+ { value: 'kontext', label: 'Kontext' },
9
+ { value: 'turbo', label: 'Turbo' },
10
+ { value: 'nanobanana', label: 'Nano Banana' }
11
+ ];
12
+
13
+ if (typeof module !== 'undefined' && module.exports) {
14
+ module.exports = { models };
15
+ }
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "Image Generation Playground",
3
- "version": "0.0.2",
4
  "description": "Part of the UltimaX Intelligence ecosystem",
5
  "type": "module",
6
  "main": "server.js",
@@ -19,7 +19,8 @@
19
  "dependencies": {
20
  "axios": "latest",
21
  "ejs": "latest",
22
- "express": "latest"
 
23
  },
24
  "homepage": "https://umint-image.hf.space"
25
  }
 
1
  {
2
  "name": "Image Generation Playground",
3
+ "version": "1.0.0",
4
  "description": "Part of the UltimaX Intelligence ecosystem",
5
  "type": "module",
6
  "main": "server.js",
 
19
  "dependencies": {
20
  "axios": "latest",
21
  "ejs": "latest",
22
+ "express": "latest",
23
+ "ws": "latest"
24
  },
25
  "homepage": "https://umint-image.hf.space"
26
  }
public/webViewer.ejs CHANGED
@@ -68,12 +68,17 @@
68
  type="image/x-icon" />
69
 
70
  <title>Image Generation Playground</title>
 
71
  <script src="https://cdn.tailwindcss.com"></script>
72
 
73
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
 
74
 
75
  <link rel="stylesheet"
76
- href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
 
 
 
77
 
78
  <link rel="stylesheet" href="/__public__/assets/css/styles.css" />
79
  <link rel="stylesheet" href="/__public__/assets/css/webLoader.css" />
@@ -82,13 +87,17 @@
82
  <script src="https://cdnjs.cloudflare.com/ajax/libs/validator/13.15.15/validator.js"></script>
83
  <script src="https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.js"></script>
84
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.js"></script>
 
 
 
 
85
  </head>
86
  <body data-is-generating="<%= !!isGenerating %>"
87
- data-request-id="<%= requestId || '' %>">
88
  <div class="gradient-bg"></div>
89
  <div class="gradient-mesh"></div>
90
 
91
- <div id="welcomeModal" class="modal-overlay" style="display: none;">
92
  <div class="modal-content">
93
  <div class="modal-inner">
94
  <h2 class="header-gradient animate__animated
@@ -188,18 +197,15 @@
188
  fill="currentColor"/>
189
  </svg>
190
  </a>
191
- <form method="POST" action="/">
192
- <input type="hidden" name="action" value="delete">
193
- <input type="hidden" name="imageIndex"
194
- value="<%= index %>">
195
- <button type="submit" class="action-btn">
196
- <svg class="action-icon" viewBox="0 0 24 24"
197
- fill="none">
198
- <path d="M18.3 5.71C17.91 5.32 17.28 5.32 16.89 5.71L12 10.59L7.11 5.7C6.72 5.31 6.09 5.31 5.7 5.7C5.31 6.09 5.31 6.72 5.7 7.11L10.59 12L5.7 16.89C5.31 17.28 5.31 17.91 5.7 18.3C6.09 18.69 6.72 18.69 7.11 18.3L12 13.41L16.89 18.3C17.28 18.69 17.91 18.69 18.3 18.3C18.69 17.91 18.69 17.28 18.3 16.89L13.41 12L18.3 7.11C18.68 6.73 18.68 6.09 18.3 5.71Z"
199
- fill="currentColor"/>
200
- </svg>
201
- </button>
202
- </form>
203
  </div>
204
  <div class="image-info">
205
  <p class="image-prompt"><%= image.prompt %></p>
@@ -234,6 +240,8 @@
234
  <form method="POST" action="/" id="generateForm">
235
  <input type="hidden" name="action" value="generate"
236
  id="formAction">
 
 
237
 
238
  <div class="grid md:grid-cols-2 gap-6 form-grid">
239
  <div>
@@ -242,10 +250,6 @@
242
  class="input-field select-field"
243
  <%= isGenerating ? 'disabled' : '' %>>
244
  <option value="">Select a model</option>
245
- <option value="flux">Flux</option>
246
- <option value="kontext">Kontext</option>
247
- <option value="turbo">Turbo</option>
248
- <option value="nanobanana">Nano Banana</option>
249
  </select>
250
  </div>
251
 
@@ -255,30 +259,6 @@
255
  class="input-field select-field"
256
  <%= isGenerating ? 'disabled' : '' %>>
257
  <option value="">Select size</option>
258
- <optgroup label="Square">
259
- <option value="256x256">256×256</option>
260
- <option value="512x512">512×512</option>
261
- <option value="768x768">768×768</option>
262
- <option value="1024x1024">1024×1024</option>
263
- <option value="1536x1536">1536×1536</option>
264
- <option value="2048x2048">2048×2048</option>
265
- </optgroup>
266
- <optgroup label="Portrait">
267
- <option value="256x384">256×384</option>
268
- <option value="384x512">384×512</option>
269
- <option value="512x768">512×768</option>
270
- <option value="768x1024">768×1024</option>
271
- <option value="1024x1536">1024×1536</option>
272
- <option value="1536x2048">1536×2048</option>
273
- </optgroup>
274
- <optgroup label="Landscape">
275
- <option value="384x256">384×256</option>
276
- <option value="512x384">512×384</option>
277
- <option value="768x512">768×512</option>
278
- <option value="1024x768">1024×768</option>
279
- <option value="1536x1024">1536×1024</option>
280
- <option value="2048x1536">2048×1536</option>
281
- </optgroup>
282
  </select>
283
  </div>
284
  </div>
@@ -321,54 +301,8 @@
321
  <section class="card animate__animated animate__fadeIn
322
  animate__delay-1s examples-section">
323
  <h3 class="examples-title">Example Prompts</h3>
324
- <div class="grid lg:grid-cols-3 md:grid-cols-2 gap-4">
325
- <div class="example-card"
326
- onclick="triggerExample('A majestic mountain landscape at golden hour with dramatic clouds and vibrant colors reflecting on a crystal clear alpine lake', 'flux', '1536x1024')">
327
- <p class="example-text">
328
- A majestic mountain landscape at golden hour with
329
- dramatic clouds and vibrant colors reflecting on a
330
- crystal clear alpine lake
331
- </p>
332
- <p class="example-meta">
333
- <span class="example-model">FLUX</span> | 1536×1024
334
- </p>
335
- </div>
336
-
337
- <div class="example-card"
338
- onclick="triggerExample('An enchanted forest with bioluminescent plants and magical floating particles, moonlight streaming through ancient mystical trees', 'kontext', '1024x1024')">
339
- <p class="example-text">
340
- An enchanted forest with bioluminescent plants and
341
- magical floating particles, moonlight streaming
342
- through ancient mystical trees
343
- </p>
344
- <p class="example-meta">
345
- <span class="example-model">KONTEXT</span> | 1024×1024 | Sometimes the server is busy
346
- </p>
347
- </div>
348
-
349
- <div class="example-card"
350
- onclick="triggerExample('Underwater coral reef teeming with colorful tropical fish and sea life, sunbeams penetrating crystal clear turquoise water', 'turbo', '1024x768')">
351
- <p class="example-text">
352
- Underwater coral reef teeming with colorful tropical
353
- fish and sea life, sunbeams penetrating crystal
354
- clear turquoise water
355
- </p>
356
- <p class="example-meta">
357
- <span class="example-model">TURBO</span> | 1024×768 | Sometimes the server is busy
358
- </p>
359
- </div>
360
-
361
- <div class="example-card"
362
- onclick="triggerExample('A woman riding a horse across an open field, cinematic and realistic style', 'nanobanana', '1024x768')">
363
- <p class="example-text">
364
- A woman riding a horse across an open field,
365
- cinematic and realistic style
366
- </p>
367
- <p class="example-meta">
368
- <span class="example-model">NANO BANANA</span> | 1024×768
369
- </p>
370
- </div>
371
- </div>
372
  </section>
373
  </main>
374
 
@@ -386,6 +320,7 @@
386
  </a>
387
  </div>
388
  </footer>
 
389
  <script defer src="/__public__/assets/plugins/imageGenerator.js"></script>
390
  <script defer src="/__public__/assets/plugins/webLoader.js"></script>
391
  </body>
 
68
  type="image/x-icon" />
69
 
70
  <title>Image Generation Playground</title>
71
+
72
  <script src="https://cdn.tailwindcss.com"></script>
73
 
74
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"
75
+ rel="stylesheet">
76
 
77
  <link rel="stylesheet"
78
+ href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.css"
79
+ integrity="sha512-phGxLIsvHFArdI7IyLjv14dchvbVkEDaH95efvAae/y2exeWBQCQDpNFbOTdV1p4/pIa/XtbuDCnfhDEIXhvGQ=="
80
+ crossorigin="anonymous"
81
+ referrerpolicy="no-referrer" />
82
 
83
  <link rel="stylesheet" href="/__public__/assets/css/styles.css" />
84
  <link rel="stylesheet" href="/__public__/assets/css/webLoader.css" />
 
87
  <script src="https://cdnjs.cloudflare.com/ajax/libs/validator/13.15.15/validator.js"></script>
88
  <script src="https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.js"></script>
89
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xss.js"></script>
90
+
91
+ <script src="/__public__/data/models.js"></script>
92
+ <script src="/__public__/data/resolution.js"></script>
93
+ <script src="/__public__/data/examples.js"></script>
94
  </head>
95
  <body data-is-generating="<%= !!isGenerating %>"
96
+ data-session-id="<%= sessionId || '' %>">
97
  <div class="gradient-bg"></div>
98
  <div class="gradient-mesh"></div>
99
 
100
+ <div id="welcomeModal" class="modal-overlay" style="display: none;">
101
  <div class="modal-content">
102
  <div class="modal-inner">
103
  <h2 class="header-gradient animate__animated
 
197
  fill="currentColor"/>
198
  </svg>
199
  </a>
200
+ <button type="button"
201
+ onclick="deleteImage(<%= index %>)"
202
+ class="action-btn">
203
+ <svg class="action-icon" viewBox="0 0 24 24"
204
+ fill="none">
205
+ <path d="M18.3 5.71C17.91 5.32 17.28 5.32 16.89 5.71L12 10.59L7.11 5.7C6.72 5.31 6.09 5.31 5.7 5.7C5.31 6.09 5.31 6.72 5.7 7.11L10.59 12L5.7 16.89C5.31 17.28 5.31 17.91 5.7 18.3C6.09 18.69 6.72 18.69 7.11 18.3L12 13.41L16.89 18.3C17.28 18.69 17.91 18.69 18.3 18.3C18.69 17.91 18.69 17.28 18.3 16.89L13.41 12L18.3 7.11C18.68 6.73 18.68 6.09 18.3 5.71Z"
206
+ fill="currentColor"/>
207
+ </svg>
208
+ </button>
 
 
 
209
  </div>
210
  <div class="image-info">
211
  <p class="image-prompt"><%= image.prompt %></p>
 
240
  <form method="POST" action="/" id="generateForm">
241
  <input type="hidden" name="action" value="generate"
242
  id="formAction">
243
+ <input type="hidden" name="sessionId"
244
+ value="<%= sessionId %>" />
245
 
246
  <div class="grid md:grid-cols-2 gap-6 form-grid">
247
  <div>
 
250
  class="input-field select-field"
251
  <%= isGenerating ? 'disabled' : '' %>>
252
  <option value="">Select a model</option>
 
 
 
 
253
  </select>
254
  </div>
255
 
 
259
  class="input-field select-field"
260
  <%= isGenerating ? 'disabled' : '' %>>
261
  <option value="">Select size</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  </select>
263
  </div>
264
  </div>
 
301
  <section class="card animate__animated animate__fadeIn
302
  animate__delay-1s examples-section">
303
  <h3 class="examples-title">Example Prompts</h3>
304
+ <div class="grid lg:grid-cols-3 md:grid-cols-2 gap-4"
305
+ id="examplesGrid"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  </section>
307
  </main>
308
 
 
320
  </a>
321
  </div>
322
  </footer>
323
+ <script src="/__public__/assets/plugins/loadParameter.js"></script>
324
  <script defer src="/__public__/assets/plugins/imageGenerator.js"></script>
325
  <script defer src="/__public__/assets/plugins/webLoader.js"></script>
326
  </body>
resolution.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ const sizes = {
7
+ square: [
8
+ { value: '256x256', label: '256×256' },
9
+ { value: '512x512', label: '512×512' },
10
+ { value: '768x768', label: '768×768' },
11
+ { value: '1024x1024', label: '1024×1024' },
12
+ { value: '1536x1536', label: '1536×1536' },
13
+ { value: '2048x2048', label: '2048×2048' }
14
+ ],
15
+ portrait: [
16
+ { value: '256x384', label: '256×384' },
17
+ { value: '384x512', label: '384×512' },
18
+ { value: '512x768', label: '512×768' },
19
+ { value: '768x1024', label: '768×1024' },
20
+ { value: '1024x1536', label: '1024×1536' },
21
+ { value: '1536x2048', label: '1536×2048' }
22
+ ],
23
+ landscape: [
24
+ { value: '384x256', label: '384×256' },
25
+ { value: '512x384', label: '512×384' },
26
+ { value: '768x512', label: '768×512' },
27
+ { value: '1024x768', label: '1024×768' },
28
+ { value: '1536x1024', label: '1536×1024' },
29
+ { value: '2048x1536', label: '2048×1536' }
30
+ ]
31
+ };
32
+
33
+ if (typeof module !== 'undefined' && module.exports) {
34
+ module.exports = { sizes };
35
+ }
server.js CHANGED
@@ -4,6 +4,8 @@
4
  //
5
 
6
  import express from 'express';
 
 
7
  import { fileURLToPath } from 'url';
8
  import path from 'path';
9
  import config from './config.js';
@@ -13,23 +15,43 @@ import { initCleanup } from
13
  './src/services/storageManager.js';
14
  import { setupViewEngine } from
15
  './src/middleware/viewEngine.js';
 
 
16
 
17
  const __filename = fileURLToPath(import.meta.url);
18
  const __dirname = path.dirname(__filename);
19
-
20
  const app = express();
 
 
 
 
 
 
 
 
21
 
22
  setupViewEngine(app, __dirname);
 
23
 
24
  app.use(express.urlencoded({
25
  extended: true,
26
  limit: config.limits.bodySize
27
  }));
28
 
29
- app.use(
30
- "/__public__/assets",
31
- express.static(path.resolve("assets"))
32
- );
 
 
 
 
 
 
 
 
 
 
33
 
34
  app.use(express.static(
35
  path.join(__dirname, 'public')
@@ -39,7 +61,7 @@ app.use('/', imageRoutes);
39
 
40
  initCleanup();
41
 
42
- app.listen(
43
  config.server.port,
44
  config.server.host
45
  );
 
4
  //
5
 
6
  import express from 'express';
7
+ import { createServer } from 'http';
8
+ import { WebSocketServer } from 'ws';
9
  import { fileURLToPath } from 'url';
10
  import path from 'path';
11
  import config from './config.js';
 
15
  './src/services/storageManager.js';
16
  import { setupViewEngine } from
17
  './src/middleware/viewEngine.js';
18
+ import { setupWebSocket } from
19
+ './src/services/websocketManager.js';
20
 
21
  const __filename = fileURLToPath(import.meta.url);
22
  const __dirname = path.dirname(__filename);
 
23
  const app = express();
24
+ const mapping = {
25
+ "/__public__/assets": "assets",
26
+ "/__public__/data/models.js": "model.js",
27
+ "/__public__/data/resolution.js": "resolution.js",
28
+ "/__public__/data/examples.js": "example.js"
29
+ };
30
+ const server = createServer(app);
31
+ const wss = new WebSocketServer({ server });
32
 
33
  setupViewEngine(app, __dirname);
34
+ setupWebSocket(wss);
35
 
36
  app.use(express.urlencoded({
37
  extended: true,
38
  limit: config.limits.bodySize
39
  }));
40
 
41
+ app.use(express.json({
42
+ limit: config.limits.bodySize
43
+ }));
44
+
45
+ for (const [route, target]
46
+ of Object.entries(mapping)
47
+ ) {
48
+ app.use(
49
+ route,
50
+ express.static(
51
+ path.resolve(target)
52
+ )
53
+ );
54
+ }
55
 
56
  app.use(express.static(
57
  path.join(__dirname, 'public')
 
61
 
62
  initCleanup();
63
 
64
+ server.listen(
65
  config.server.port,
66
  config.server.host
67
  );
src/controllers/imageController.js CHANGED
@@ -20,19 +20,22 @@ import {
20
  cancelGeneration
21
  } from '../services/imageGenerator.js';
22
  import { renderView } from '../utils/viewRenderer.js';
 
 
 
23
 
24
  export const renderHome = (req, res) => {
25
- const requestId = req.query.rid || generateId();
26
- const data = getStorage(requestId) || getDefaultData();
27
 
28
  renderView(res, {
29
  ...data,
30
- requestId
31
  });
32
  };
33
 
34
  export const handleAction = async (req, res) => {
35
- const requestId = req.body.requestId || generateId();
36
  const {
37
  action,
38
  prompt,
@@ -41,23 +44,23 @@ export const handleAction = async (req, res) => {
41
  imageIndex
42
  } = req.body;
43
 
44
- let data = getStorage(requestId) || getDefaultData();
45
 
46
  switch(action) {
47
  case 'delete':
48
  return handleDelete(
49
- requestId,
50
  imageIndex,
51
  data,
52
  res
53
  );
54
 
55
  case 'cancel':
56
- return handleCancel(requestId, res);
57
 
58
  case 'generate':
59
  return handleGenerate(
60
- requestId,
61
  prompt,
62
  model,
63
  size,
@@ -66,12 +69,15 @@ export const handleAction = async (req, res) => {
66
  );
67
 
68
  default:
69
- return res.redirect(`/?rid=${requestId}`);
 
 
 
70
  }
71
  };
72
 
73
  const handleDelete = (
74
- requestId,
75
  imageIndex,
76
  data,
77
  res
@@ -81,18 +87,34 @@ const handleDelete = (
81
  data.images.length
82
  )) {
83
  data.images.splice(parseInt(imageIndex), 1);
84
- setStorage(requestId, data);
 
 
 
 
 
85
  }
86
- return res.redirect(`/?rid=${requestId}`);
 
 
 
 
87
  };
88
 
89
- const handleCancel = (requestId, res) => {
90
- cancelGeneration(requestId);
91
- return res.redirect(`/?rid=${requestId}`);
 
 
 
 
 
 
 
92
  };
93
 
94
  const handleGenerate = async (
95
- requestId,
96
  prompt,
97
  model,
98
  size,
@@ -106,33 +128,37 @@ const handleGenerate = async (
106
  model,
107
  size
108
  )) {
109
- data.error = 'Please fill in all required fields';
110
- setStorage(requestId, data);
111
- return renderView(res, {
112
- ...data,
113
- requestId
114
  });
115
  }
116
 
117
  if (!validateApiConfig()) {
118
- data.error = 'The server is currently busy. ' +
119
- 'Please try again later.';
120
- setStorage(requestId, data);
121
- return renderView(res, {
122
- ...data,
123
- requestId
124
  });
125
  }
126
 
127
  data.isGenerating = true;
128
- data.progress = 50;
129
  data.error = null;
130
- setStorage(requestId, data);
131
 
132
- res.redirect(`/?rid=${requestId}`);
 
 
 
 
 
 
 
 
133
 
134
  await generateImage(
135
- requestId,
136
  trimmedPrompt,
137
  model,
138
  size
 
20
  cancelGeneration
21
  } from '../services/imageGenerator.js';
22
  import { renderView } from '../utils/viewRenderer.js';
23
+ import {
24
+ sendToSession
25
+ } from '../services/websocketManager.js';
26
 
27
  export const renderHome = (req, res) => {
28
+ const sessionId = generateId();
29
+ const data = getDefaultData();
30
 
31
  renderView(res, {
32
  ...data,
33
+ sessionId
34
  });
35
  };
36
 
37
  export const handleAction = async (req, res) => {
38
+ const sessionId = req.body.sessionId || generateId();
39
  const {
40
  action,
41
  prompt,
 
44
  imageIndex
45
  } = req.body;
46
 
47
+ let data = getStorage(sessionId) || getDefaultData();
48
 
49
  switch(action) {
50
  case 'delete':
51
  return handleDelete(
52
+ sessionId,
53
  imageIndex,
54
  data,
55
  res
56
  );
57
 
58
  case 'cancel':
59
+ return handleCancel(sessionId, res);
60
 
61
  case 'generate':
62
  return handleGenerate(
63
+ sessionId,
64
  prompt,
65
  model,
66
  size,
 
69
  );
70
 
71
  default:
72
+ return res.json({
73
+ success: false,
74
+ error: 'Invalid action'
75
+ });
76
  }
77
  };
78
 
79
  const handleDelete = (
80
+ sessionId,
81
  imageIndex,
82
  data,
83
  res
 
87
  data.images.length
88
  )) {
89
  data.images.splice(parseInt(imageIndex), 1);
90
+ setStorage(sessionId, data);
91
+
92
+ sendToSession(sessionId, {
93
+ type: 'imageDeleted',
94
+ images: data.images
95
+ });
96
  }
97
+
98
+ return res.json({
99
+ success: true,
100
+ images: data.images
101
+ });
102
  };
103
 
104
+ const handleCancel = (sessionId, res) => {
105
+ cancelGeneration(sessionId);
106
+
107
+ sendToSession(sessionId, {
108
+ type: 'generationCancelled'
109
+ });
110
+
111
+ return res.json({
112
+ success: true
113
+ });
114
  };
115
 
116
  const handleGenerate = async (
117
+ sessionId,
118
  prompt,
119
  model,
120
  size,
 
128
  model,
129
  size
130
  )) {
131
+ return res.json({
132
+ success: false,
133
+ error: 'Please fill in all required fields'
 
 
134
  });
135
  }
136
 
137
  if (!validateApiConfig()) {
138
+ return res.json({
139
+ success: false,
140
+ error: 'The server is currently busy. ' +
141
+ 'Please try again later.'
 
 
142
  });
143
  }
144
 
145
  data.isGenerating = true;
146
+ data.progress = 0;
147
  data.error = null;
148
+ setStorage(sessionId, data);
149
 
150
+ sendToSession(sessionId, {
151
+ type: 'generationStarted',
152
+ progress: 0
153
+ });
154
+
155
+ res.json({
156
+ success: true,
157
+ sessionId
158
+ });
159
 
160
  await generateImage(
161
+ sessionId,
162
  trimmedPrompt,
163
  model,
164
  size
src/services/imageGenerator.js CHANGED
@@ -13,18 +13,26 @@ import {
13
  setActiveGeneration,
14
  deleteActiveGeneration
15
  } from './storageManager.js';
 
 
 
16
 
17
- const updateProgress = (requestId, progress) => {
18
- const data = getStorage(requestId);
19
  if (data) {
20
  data.progress = progress;
21
- setStorage(requestId, data);
 
 
 
 
 
22
  }
23
  };
24
 
25
- const createProgressUpdater = (requestId) => {
26
  return setInterval(() => {
27
- const data = getStorage(requestId);
28
  if (data &&
29
  data.isGenerating &&
30
  data.progress < config.generation.maxProgress) {
@@ -33,7 +41,7 @@ const createProgressUpdater = (requestId) => {
33
  config.generation.maxProgress,
34
  data.progress + increment
35
  );
36
- updateProgress(requestId, newProgress);
37
  }
38
  }, config.generation.progressInterval);
39
  };
@@ -80,15 +88,15 @@ const callImageApi = async (
80
  };
81
 
82
  export const generateImage = async (
83
- requestId,
84
  prompt,
85
  model,
86
  size
87
  ) => {
88
  const controller = new AbortController();
89
- setActiveGeneration(requestId, controller);
90
 
91
- const progressInterval = createProgressUpdater(requestId);
92
 
93
  setTimeout(async () => {
94
  try {
@@ -99,10 +107,10 @@ export const generateImage = async (
99
  controller.signal
100
  );
101
 
102
- const data = getStorage(requestId);
103
  if (!data) return;
104
 
105
- updateProgress(requestId, 100);
106
 
107
  if (response.data?.data?.length > 0) {
108
  const base64 = response.data.data[0].b64_json;
@@ -118,40 +126,50 @@ export const generateImage = async (
118
 
119
  data.isGenerating = false;
120
  data.progress = 0;
121
- setStorage(requestId, data);
 
 
 
 
 
122
 
123
  } catch (error) {
124
- const data = getStorage(requestId);
125
  if (!data) return;
126
 
127
  if (error.name !== 'CanceledError' &&
128
  error.code !== 'ERR_CANCELED') {
129
  data.error = 'The server is currently busy. ' +
130
  'Please try again later.';
 
 
 
 
 
131
  }
132
 
133
  data.isGenerating = false;
134
  data.progress = 0;
135
- setStorage(requestId, data);
136
 
137
  } finally {
138
  clearInterval(progressInterval);
139
- deleteActiveGeneration(requestId);
140
  }
141
  }, config.generation.startDelay);
142
  };
143
 
144
- export const cancelGeneration = (requestId) => {
145
- const controller = getActiveGeneration(requestId);
146
  if (controller) {
147
  controller.abort();
148
- deleteActiveGeneration(requestId);
149
 
150
- const data = getStorage(requestId);
151
  if (data) {
152
  data.isGenerating = false;
153
  data.progress = 0;
154
- setStorage(requestId, data);
155
  }
156
  }
157
  };
 
13
  setActiveGeneration,
14
  deleteActiveGeneration
15
  } from './storageManager.js';
16
+ import {
17
+ sendToSession
18
+ } from './websocketManager.js';
19
 
20
+ const updateProgress = (sessionId, progress) => {
21
+ const data = getStorage(sessionId);
22
  if (data) {
23
  data.progress = progress;
24
+ setStorage(sessionId, data);
25
+
26
+ sendToSession(sessionId, {
27
+ type: 'progressUpdate',
28
+ progress
29
+ });
30
  }
31
  };
32
 
33
+ const createProgressUpdater = (sessionId) => {
34
  return setInterval(() => {
35
+ const data = getStorage(sessionId);
36
  if (data &&
37
  data.isGenerating &&
38
  data.progress < config.generation.maxProgress) {
 
41
  config.generation.maxProgress,
42
  data.progress + increment
43
  );
44
+ updateProgress(sessionId, newProgress);
45
  }
46
  }, config.generation.progressInterval);
47
  };
 
88
  };
89
 
90
  export const generateImage = async (
91
+ sessionId,
92
  prompt,
93
  model,
94
  size
95
  ) => {
96
  const controller = new AbortController();
97
+ setActiveGeneration(sessionId, controller);
98
 
99
+ const progressInterval = createProgressUpdater(sessionId);
100
 
101
  setTimeout(async () => {
102
  try {
 
107
  controller.signal
108
  );
109
 
110
+ const data = getStorage(sessionId);
111
  if (!data) return;
112
 
113
+ updateProgress(sessionId, 100);
114
 
115
  if (response.data?.data?.length > 0) {
116
  const base64 = response.data.data[0].b64_json;
 
126
 
127
  data.isGenerating = false;
128
  data.progress = 0;
129
+ setStorage(sessionId, data);
130
+
131
+ sendToSession(sessionId, {
132
+ type: 'generationComplete',
133
+ images: data.images
134
+ });
135
 
136
  } catch (error) {
137
+ const data = getStorage(sessionId);
138
  if (!data) return;
139
 
140
  if (error.name !== 'CanceledError' &&
141
  error.code !== 'ERR_CANCELED') {
142
  data.error = 'The server is currently busy. ' +
143
  'Please try again later.';
144
+
145
+ sendToSession(sessionId, {
146
+ type: 'generationError',
147
+ error: data.error
148
+ });
149
  }
150
 
151
  data.isGenerating = false;
152
  data.progress = 0;
153
+ setStorage(sessionId, data);
154
 
155
  } finally {
156
  clearInterval(progressInterval);
157
+ deleteActiveGeneration(sessionId);
158
  }
159
  }, config.generation.startDelay);
160
  };
161
 
162
+ export const cancelGeneration = (sessionId) => {
163
+ const controller = getActiveGeneration(sessionId);
164
  if (controller) {
165
  controller.abort();
166
+ deleteActiveGeneration(sessionId);
167
 
168
+ const data = getStorage(sessionId);
169
  if (data) {
170
  data.isGenerating = false;
171
  data.progress = 0;
172
+ setStorage(sessionId, data);
173
  }
174
  }
175
  };
src/services/storageManager.js CHANGED
@@ -8,37 +8,37 @@ import config from '../../config.js';
8
  const tempStorage = new Map();
9
  const activeGenerations = new Map();
10
 
11
- export const getStorage = (requestId) => {
12
- const data = tempStorage.get(requestId);
13
  if (data) {
14
  data.lastAccess = Date.now();
15
  }
16
  return data;
17
  };
18
 
19
- export const setStorage = (requestId, data) => {
20
  data.lastAccess = Date.now();
21
- tempStorage.set(requestId, data);
22
  };
23
 
24
- export const deleteStorage = (requestId) => {
25
- tempStorage.delete(requestId);
26
- activeGenerations.delete(requestId);
27
  };
28
 
29
- export const getActiveGeneration = (requestId) => {
30
- return activeGenerations.get(requestId);
31
  };
32
 
33
  export const setActiveGeneration = (
34
- requestId,
35
  controller
36
  ) => {
37
- activeGenerations.set(requestId, controller);
38
  };
39
 
40
- export const deleteActiveGeneration = (requestId) => {
41
- activeGenerations.delete(requestId);
42
  };
43
 
44
  export const getDefaultData = () => ({
 
8
  const tempStorage = new Map();
9
  const activeGenerations = new Map();
10
 
11
+ export const getStorage = (sessionId) => {
12
+ const data = tempStorage.get(sessionId);
13
  if (data) {
14
  data.lastAccess = Date.now();
15
  }
16
  return data;
17
  };
18
 
19
+ export const setStorage = (sessionId, data) => {
20
  data.lastAccess = Date.now();
21
+ tempStorage.set(sessionId, data);
22
  };
23
 
24
+ export const deleteStorage = (sessionId) => {
25
+ tempStorage.delete(sessionId);
26
+ activeGenerations.delete(sessionId);
27
  };
28
 
29
+ export const getActiveGeneration = (sessionId) => {
30
+ return activeGenerations.get(sessionId);
31
  };
32
 
33
  export const setActiveGeneration = (
34
+ sessionId,
35
  controller
36
  ) => {
37
+ activeGenerations.set(sessionId, controller);
38
  };
39
 
40
+ export const deleteActiveGeneration = (sessionId) => {
41
+ activeGenerations.delete(sessionId);
42
  };
43
 
44
  export const getDefaultData = () => ({
src/services/websocketManager.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //
2
+ // SPDX-FileCopyrightText: Hadad <[email protected]>
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import config from '../../config.js';
7
+ import { generateId } from '../utils/idGenerator.js';
8
+
9
+ const clients = new Map();
10
+
11
+ export const setupWebSocket = (wss) => {
12
+ wss.on('connection', (ws) => {
13
+ const clientId = generateId();
14
+ let clientSessionId = null;
15
+
16
+ ws.clientId = clientId;
17
+ ws.isAlive = true;
18
+
19
+ ws.on('pong', () => {
20
+ ws.isAlive = true;
21
+ });
22
+
23
+ ws.on('message', (data) => {
24
+ try {
25
+ const message = JSON.parse(data.toString());
26
+
27
+ if (message.type === 'register' && message.sessionId) {
28
+ clientSessionId = message.sessionId;
29
+
30
+ if (clients.has(clientSessionId)) {
31
+ const oldClient = clients.get(clientSessionId);
32
+ if (oldClient && oldClient.readyState === 1) {
33
+ oldClient.close();
34
+ }
35
+ }
36
+
37
+ clients.set(clientSessionId, ws);
38
+ ws.sessionId = clientSessionId;
39
+
40
+ ws.send(JSON.stringify({
41
+ type: 'registered',
42
+ sessionId: clientSessionId
43
+ }));
44
+ }
45
+
46
+ handleWebSocketMessage(ws, message);
47
+ } catch (error) {}
48
+ });
49
+
50
+ ws.on('close', () => {
51
+ if (clientSessionId) {
52
+ clients.delete(clientSessionId);
53
+ }
54
+ });
55
+
56
+ ws.on('error', () => {
57
+ if (clientSessionId) {
58
+ clients.delete(clientSessionId);
59
+ }
60
+ });
61
+
62
+ ws.send(JSON.stringify({
63
+ type: 'connected',
64
+ clientId
65
+ }));
66
+ });
67
+
68
+ const interval = setInterval(() => {
69
+ wss.clients.forEach((ws) => {
70
+ if (ws.isAlive === false) {
71
+ return ws.terminate();
72
+ }
73
+ ws.isAlive = false;
74
+ ws.ping();
75
+ });
76
+ }, config.websocket.heartbeat);
77
+
78
+ wss.on('close', () => {
79
+ clearInterval(interval);
80
+ });
81
+ };
82
+
83
+ const handleWebSocketMessage = (ws, message) => {
84
+ if (message.type === 'ping') {
85
+ ws.send(JSON.stringify({ type: 'pong' }));
86
+ }
87
+ };
88
+
89
+ export const sendToSession = (sessionId, data) => {
90
+ const client = clients.get(sessionId);
91
+ if (client && client.readyState === 1) {
92
+ client.send(JSON.stringify({
93
+ ...data,
94
+ sessionId
95
+ }));
96
+ }
97
+ };
98
+
99
+ export const broadcastToAll = (data) => {
100
+ clients.forEach((client, sessionId) => {
101
+ if (client.readyState === 1) {
102
+ client.send(JSON.stringify({
103
+ ...data,
104
+ sessionId
105
+ }));
106
+ }
107
+ });
108
+ };