2ch commited on
Commit
6f880fd
·
verified ·
1 Parent(s): bf56852

Create Grok Imagine Pro UI v2.5-with_optimizer.user.js

Browse files
Grok Imagine Pro UI v2.5-with_optimizer.user.js ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==UserScript==
2
+ // @name Grok Imagine Pro UI v2.5
3
+ // @namespace http://tampermonkey.net/
4
+ // @version 2.5
5
+ // @description Advanced UI for x.ai with Prompt History and Gallery Management
6
+ // @author Gemini-3-flash
7
+ // @match https://console.x.ai/team/*/imagine
8
+ // @grant GM_xmlhttpRequest
9
+ // @connect filetrash-webp.hf.space
10
+ // ==/UserScript==
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ let historyImages = []; // Сгенерированные картинки
16
+ let selectedImages = []; // Хранилище Base64 выбранных файлов
17
+ let promptHistory = JSON.parse(localStorage.getItem('grok_prompt_history') || '[]');
18
+ let currentIndex = -1;
19
+
20
+ const style = document.createElement('style');
21
+ style.innerHTML = `
22
+ #custom-gen-ui {
23
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
24
+ background: #0a0a0a; color: #fff; z-index: 9998;
25
+ display: flex; flex-direction: row; font-family: sans-serif;
26
+ }
27
+
28
+ /* SIDEBAR */
29
+ #sidebar {
30
+ width: 300px; background: #111; border-right: 1px solid #333;
31
+ display: flex; flex-direction: column; transition: 0.3s; overflow: hidden;
32
+ }
33
+ #sidebar.collapsed { width: 0; border: none; }
34
+ .sidebar-header { padding: 15px; font-weight: bold; border-bottom: 1px solid #222; display: flex; justify-content: space-between; align-items: center; white-space: nowrap; }
35
+ #prompt-list { flex: 1; overflow-y: auto; padding: 10px; }
36
+ .prompt-item {
37
+ background: #1a1a1a; padding: 10px; border-radius: 6px; margin-bottom: 8px;
38
+ font-size: 13px; cursor: pointer; position: relative; border: 1px solid transparent; transition: 0.2s;
39
+ }
40
+ .prompt-item:hover { border-color: #555; background: #222; }
41
+ .prompt-item .delete-prompt { position: absolute; right: 5px; top: 5px; color: #666; padding: 2px 5px; }
42
+ .prompt-item .delete-prompt:hover { color: #ff4444; }
43
+
44
+ /* MAIN AREA */
45
+ #main-content { flex: 1; display: flex; flex-direction: column; padding: 20px; overflow-y: auto; position: relative; }
46
+ .controls { width: 100%; max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; background: #161616; padding: 20px; border-radius: 12px; border: 1px solid #333; }
47
+ textarea { width: 100%; height: 280px; background: #000; color: #eee; border: 1px solid #444; padding: 12px; border-radius: 8px; font-size: 15px; }
48
+ .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
49
+
50
+ select, input[type="number"] { background: #000; color: #fff; border: 1px solid #444; padding: 8px; border-radius: 6px; }
51
+ button { border: none; padding: 10px 18px; font-weight: bold; cursor: pointer; border-radius: 6px; transition: 0.2s; }
52
+ #gen-btn { background: #fff; color: #000; }
53
+ #gen-btn:hover { background: #ccc; }
54
+ #gen-btn:disabled { background: #444; color: #888; }
55
+ .btn-secondary { background: #333; color: #fff; }
56
+ .btn-secondary:hover { background: #444; }
57
+ .btn-danger { background: #422; color: #f66; border: 1px solid #633; }
58
+ .btn-danger:hover { background: #622; }
59
+
60
+ #results { margin-top: 25px; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 15px; width: 100%; }
61
+ .img-card { background: #161616; border: 1px solid #333; padding: 5px; border-radius: 8px; cursor: pointer; position: relative; }
62
+ .img-card img { width: 100%; height: 200px; object-fit: cover; border-radius: 4px; }
63
+
64
+ /* MODAL */
65
+ #img-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 10000; display: none; align-items: center; justify-content: center; }
66
+ #modal-img { max-width: 90%; max-height: 90vh; border-radius: 4px; }
67
+ .modal-btn { position: absolute; background: rgba(255,255,255,0.1); color: #fff; border: none; width: 50px; height: 50px; border-radius: 50%; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
68
+ #modal-close { top: 20px; right: 20px; }
69
+ #modal-prev { left: 20px; }
70
+ #modal-next { right: 20px; }
71
+ #modal-download { position:absolute;top:40px; left:auto;width:auto; padding:0 25px; background:var(--color-green-500); color:#000; text-decoration:none; line-height:44px; font-weight:bold; border-radius:6px; box-shadow:black 0px 0px 10px 2px }
72
+ .status { color: #888; font-size: 13px; font-family: monospace; }
73
+
74
+ /* Превью референсов */
75
+ #reference-previews {
76
+ display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap;
77
+ min-height: 20px; padding: 10px; border: 2px dashed transparent;
78
+ border-radius: 8px; transition: 0.2s;
79
+ }
80
+ #reference-previews.drag-over { border-color: #0f0; background: rgba(0,255,0,0.05); }
81
+
82
+ .ref-item {
83
+ width: 80px; height: 80px; position: relative; cursor: grab;
84
+ border: 2px solid #333; border-radius: 6px; overflow: hidden;
85
+ background: #000; transition: transform 0.2s;
86
+ }
87
+ .ref-item:active { cursor: grabbing; }
88
+ .ref-item img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
89
+ .ref-item .remove-ref {
90
+ position: absolute; top: 2px; right: 2px; background: rgba(0,0,0,0.7);
91
+ color: #ff4444; border: none; border-radius: 4px; padding: 0 4px;
92
+ font-size: 12px; cursor: pointer; z-index: 2;
93
+ }
94
+ .ref-item .ref-index {
95
+ position: absolute; bottom: 2px; left: 2px; background: rgba(0,0,0,0.7);
96
+ color: #fff; font-size: 10px; padding: 0 4px; border-radius: 4px;
97
+ }
98
+ .ref-item.dragging { opacity: 0.5; transform: scale(0.9); }
99
+ `;
100
+ document.head.appendChild(style);
101
+
102
+ const sleep = (ms) => new Promise(res => setTimeout(res, ms));
103
+
104
+ async function uploadToOptimizer(b64Data) {
105
+ const apiBase = "https://filetrash-webp.hf.space";
106
+ return new Promise((resolve) => {
107
+ GM_xmlhttpRequest({
108
+ method: "POST",
109
+ url: `${apiBase}/upload`,
110
+ headers: { "Content-Type": "application/json" },
111
+ data: JSON.stringify({ image: `data:image/png;base64,${b64Data}` }),
112
+ onload: async function(response) {
113
+ if (response.status === 200) {
114
+ try {
115
+ const data = JSON.parse(response.responseText);
116
+ // Сразу скачиваем превью как Blob, чтобы обойти CSP
117
+ const thumbBlob = await fetchAsBlob(apiBase + data.thumbnail);
118
+ resolve({
119
+ fullUrl: apiBase + data.image, // Пока просто ссылка
120
+ thumb: thumbBlob || `data:image/png;base64,${b64Data}`
121
+ });
122
+ } catch (e) { resolve(null); }
123
+ } else { resolve(null); }
124
+ },
125
+ onerror: () => resolve(null)
126
+ });
127
+ });
128
+ }
129
+
130
+ async function fetchAsBlob(url) {
131
+ return new Promise((resolve) => {
132
+ GM_xmlhttpRequest({
133
+ method: "GET",
134
+ url: url,
135
+ responseType: "blob",
136
+ onload: (res) => {
137
+ if (res.status === 200) {
138
+ resolve(URL.createObjectURL(res.response));
139
+ } else {
140
+ resolve(null);
141
+ }
142
+ },
143
+ onerror: () => resolve(null)
144
+ });
145
+ });
146
+ }
147
+
148
+ function createUI() {
149
+ document.querySelectorAll('aside, main').forEach(el => el.remove());
150
+ const ui = document.createElement('div');
151
+ ui.id = 'custom-gen-ui';
152
+ ui.innerHTML = `
153
+ <div id="sidebar">
154
+ <div class="sidebar-header">
155
+ <span>PROMPT HISTORY</span>
156
+ <button class="btn-secondary" id="close-sidebar" style="padding: 2px 8px;">✕</button>
157
+ </div>
158
+ <div id="prompt-list"></div>
159
+ </div>
160
+ <div id="main-content">
161
+ <div class="controls">
162
+ <textarea id="prompt-input" placeholder="What should I imagine?"></textarea>
163
+ <div class="row">
164
+ <button class="btn-secondary" id="toggle-sidebar">📜 History</button>
165
+ <input type="file" id="file-input" multiple accept="image/*" style="display:none">
166
+ <button class="btn-secondary" id="upload-btn">📎 Edit (0/10)</button>
167
+ <button class="btn-danger" id="clear-files" style="display:none; padding: 2px 8px;">✕</button>
168
+ <select id="ratio-select">
169
+ <option value="auto">Auto Ratio</option>
170
+ <option value="1:1">1:1</option>
171
+ <option value="16:9">16:9</option>
172
+ <option value="9:16">9:16</option>
173
+ <option value="19.5:9">19.5:9</option>
174
+ <option value="9:19.5">9:19.5</option>
175
+ <option value="20:9">20:9</option>
176
+ <option value="9:20">9:20</option>
177
+ <option value="4:3">4:3</option>
178
+ <option value="3:2">3:2</option>
179
+ <option value="2:3">2:3</option>
180
+ <option value="2:1">2:1</option>
181
+ <option value="1:2">1:2</option>
182
+ </select>
183
+ <input type="number" id="count-input" value="1" min="1" max="20" style="width: 45px;">
184
+ <select id="mode-select">
185
+ <option value="parallel">Parallel</option>
186
+ <option value="native">Native (n)</option>
187
+ <option value="stepwise">Stepwise</option>
188
+ </select>
189
+ <button id="gen-btn">GENERATE</button>
190
+ <button class="btn-danger" id="clear-gallery">Clear Gallery</button>
191
+ <div id="reference-previews"></div>
192
+ <span id="global-status" class="status"></span>
193
+ </div>
194
+ </div>
195
+ <div id="results"></div>
196
+ </div>
197
+
198
+ <div id="img-modal">
199
+ <button id="modal-close" class="modal-btn">✕</button>
200
+ <button id="modal-prev" class="modal-btn">←</button>
201
+ <img id="modal-img" src="">
202
+ <button id="modal-next" class="modal-btn">→</button>
203
+ <a id="modal-download" download="grok.png">Download</a>
204
+ </div>
205
+ `;
206
+ document.body.appendChild(ui);
207
+
208
+ // Bindings
209
+ document.getElementById('gen-btn').onclick = startGeneration;
210
+ document.getElementById('clear-gallery').onclick = clearGallery;
211
+ document.getElementById('toggle-sidebar').onclick = toggleSidebar;
212
+ document.getElementById('close-sidebar').onclick = toggleSidebar;
213
+
214
+ document.getElementById('modal-close').onclick = closeModal;
215
+ document.getElementById('modal-prev').onclick = () => navigate(-1);
216
+ document.getElementById('modal-next').onclick = () => navigate(1);
217
+ document.getElementById('img-modal').onclick = (e) => { if(e.target.id === 'img-modal') closeModal(); };
218
+
219
+ document.addEventListener('keydown', (e) => {
220
+ if (document.getElementById('img-modal').style.display === 'flex') {
221
+ if (e.key === 'Escape') closeModal();
222
+ if (e.key === 'ArrowLeft') navigate(-1);
223
+ if (e.key === 'ArrowRight') navigate(1);
224
+ }
225
+ });
226
+
227
+ const refContainer = document.getElementById('reference-previews');
228
+ const fileInput = document.getElementById('file-input');
229
+ const uploadBtn = document.getElementById('upload-btn');
230
+ const ratioSelect = document.getElementById('ratio-select');
231
+ const genBtn = document.getElementById('gen-btn');
232
+
233
+ // Функция обновления UI референсов
234
+ function renderRefPreviews() {
235
+ refContainer.innerHTML = '';
236
+ selectedImages.forEach((imgObj, index) => {
237
+ const item = document.createElement('div');
238
+ item.className = 'ref-item';
239
+ item.draggable = true;
240
+ item.dataset.index = index;
241
+ item.innerHTML = `
242
+ <img src="${imgObj.url}">
243
+ <button class="remove-ref">✕</button>
244
+ <span class="ref-index">${index + 1}</span>
245
+ `;
246
+
247
+ // Удаление одного референса
248
+ item.querySelector('.remove-ref').onclick = (e) => {
249
+ e.stopPropagation();
250
+ selectedImages.splice(index, 1);
251
+ updateEditState();
252
+ };
253
+
254
+ // Логика сортировки (Drag & Drop элементов)
255
+ item.ondragstart = (e) => {
256
+ e.dataTransfer.setData('text/plain', index);
257
+ item.classList.add('dragging');
258
+ };
259
+ item.ondragend = () => item.classList.remove('dragging');
260
+
261
+ item.ondragover = (e) => e.preventDefault();
262
+ item.ondrop = (e) => {
263
+ e.preventDefault();
264
+ const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
265
+ const toIdx = index;
266
+ if (fromIdx === toIdx) return;
267
+
268
+ const movedItem = selectedImages.splice(fromIdx, 1)[0];
269
+ selectedImages.splice(toIdx, 0, movedItem);
270
+ updateEditState();
271
+ };
272
+
273
+ refContainer.appendChild(item);
274
+ });
275
+ }
276
+
277
+ function updateEditState() {
278
+ const count = selectedImages.length;
279
+ uploadBtn.innerText = `📎 Edit (${count}/10)`;
280
+ uploadBtn.style.color = count > 0 ? "#0f0" : "#fff";
281
+ ratioSelect.disabled = count > 0;
282
+ if (count > 0) ratioSelect.value = "auto";
283
+ genBtn.innerText = count > 0 ? "EDIT IMAGES" : "GENERATE";
284
+ renderRefPreviews();
285
+ }
286
+
287
+ // Обработка добавления файлов
288
+ async function handleFiles(files) {
289
+ const remainingSlots = 10 - selectedImages.length;
290
+ const filesToAdd = Array.from(files).slice(0, remainingSlots);
291
+
292
+ for (const file of filesToAdd) {
293
+ const base64 = await new Promise(resolve => {
294
+ const reader = new FileReader();
295
+ reader.onload = () => resolve(reader.result);
296
+ reader.readAsDataURL(file);
297
+ });
298
+ selectedImages.push({ url: base64 });
299
+ }
300
+ updateEditState();
301
+ }
302
+
303
+ // События для кнопки и инпута
304
+ uploadBtn.onclick = () => fileInput.click();
305
+ fileInput.onchange = (e) => handleFiles(e.target.files);
306
+
307
+ // Drag-and-drop файлов на контейнер или кнопку
308
+ [uploadBtn, refContainer].forEach(el => {
309
+ el.ondragover = (e) => {
310
+ e.preventDefault();
311
+ refContainer.classList.add('drag-over');
312
+ };
313
+ el.ondragleave = () => refContainer.classList.remove('drag-over');
314
+ el.ondrop = (e) => {
315
+ e.preventDefault();
316
+ refContainer.classList.remove('drag-over');
317
+ if (e.dataTransfer.files.length > 0) {
318
+ handleFiles(e.dataTransfer.files);
319
+ }
320
+ };
321
+ });
322
+
323
+ renderHistory();
324
+ }
325
+
326
+ // --- HISTORY LOGIC ---
327
+ function savePrompt(text) {
328
+ if (!text || promptHistory[0] === text) return;
329
+ promptHistory = [text, ...promptHistory.filter(t => t !== text)].slice(0, 50);
330
+ localStorage.setItem('grok_prompt_history', JSON.stringify(promptHistory));
331
+ renderHistory();
332
+ }
333
+
334
+ function renderHistory() {
335
+ const list = document.getElementById('prompt-list');
336
+ list.innerHTML = '';
337
+ promptHistory.forEach((text, i) => {
338
+ const item = document.createElement('div');
339
+ item.className = 'prompt-item';
340
+ item.title = text;
341
+ item.innerHTML = `
342
+ <div class="text-truncate">${text.substring(0, 80)}${text.length > 80 ? '...' : ''}</div>
343
+ <span class="delete-prompt" data-idx="${i}">✕</span>
344
+ `;
345
+ item.onclick = (e) => {
346
+ if (e.target.className !== 'delete-prompt') {
347
+ document.getElementById('prompt-input').value = text;
348
+ }
349
+ };
350
+ item.querySelector('.delete-prompt').onclick = (e) => {
351
+ e.stopPropagation();
352
+ promptHistory.splice(i, 1);
353
+ localStorage.setItem('grok_prompt_history', JSON.stringify(promptHistory));
354
+ renderHistory();
355
+ };
356
+ list.appendChild(item);
357
+ });
358
+ }
359
+
360
+ function toggleSidebar() {
361
+ document.getElementById('sidebar').classList.toggle('collapsed');
362
+ }
363
+
364
+ function clearGallery() {
365
+ if (confirm("Clear all images from current gallery?")) {
366
+ historyImages = [];
367
+ document.getElementById('results').innerHTML = '';
368
+ currentIndex = -1;
369
+ }
370
+ }
371
+
372
+ // --- REQUEST LOGIC ---
373
+ async function sendRequest(prompt, ratio, n = 1) {
374
+ const isEdit = selectedImages.length > 0;
375
+ const url = isEdit ? 'https://console.x.ai/v1/images/edits' : 'https://console.x.ai/v1/images/generations';
376
+
377
+ const body = {
378
+ model: 'grok-imagine-image',
379
+ prompt, n,
380
+ aspect_ratio: isEdit ? 'auto' : ratio,
381
+ resolution: '2k',
382
+ response_format: 'b64_json'
383
+ };
384
+
385
+ if (isEdit) {
386
+ body.images = selectedImages; // Добавляем массив картинок
387
+ }
388
+
389
+ const response = await fetch(url, {
390
+ method: 'POST',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify(body)
393
+ });
394
+
395
+ if (!response.ok) {
396
+ const err = await response.json();
397
+ throw new Error(err.error || `HTTP ${response.status}`);
398
+ }
399
+ return await response.json();
400
+ }
401
+
402
+ async function addImageToGallery(b64Data) {
403
+ const status = document.getElementById('global-status');
404
+ const oldStatus = status.innerText;
405
+ status.innerText = "Optimizing...";
406
+
407
+ const optimized = await uploadToOptimizer(b64Data);
408
+ const idx = historyImages.length;
409
+
410
+ if (optimized) {
411
+ historyImages.push({
412
+ url: optimized.fullUrl,
413
+ thumb: optimized.thumb,
414
+ isLoaded: false, // Флаг: скачан ли оригинал
415
+ name: `grok_${Date.now()}.webp`
416
+ });
417
+ } else {
418
+ // Фолбек если API сбоит
419
+ const dataUrl = `data:image/png;base64,${b64Data}`;
420
+ historyImages.push({ url: dataUrl, thumb: dataUrl, isLoaded: true, name: `grok_${Date.now()}.png` });
421
+ }
422
+
423
+ const card = document.createElement('div');
424
+ card.className = 'img-card';
425
+ card.innerHTML = `<img src="${historyImages[idx].thumb}" loading="lazy">`;
426
+ card.onclick = () => openModal(idx);
427
+ document.getElementById('results').prepend(card);
428
+
429
+ status.innerText = oldStatus;
430
+ }
431
+
432
+ async function startGeneration() {
433
+ const prompt = document.getElementById('prompt-input').value;
434
+ const ratio = document.getElementById('ratio-select').value;
435
+ const count = parseInt(document.getElementById('count-input').value);
436
+ const mode = document.getElementById('mode-select').value;
437
+ const status = document.getElementById('global-status');
438
+ const btn = document.getElementById('gen-btn');
439
+
440
+ if (!prompt) return;
441
+ btn.disabled = true;
442
+ savePrompt(prompt);
443
+
444
+ try {
445
+ if (mode === 'native') {
446
+ status.innerText = "Native batching...";
447
+ const res = await sendRequest(prompt, ratio, count);
448
+ for (const item of res.data) {
449
+ await addImageToGallery(item.b64_json);
450
+ }
451
+ } else if (mode === 'parallel') {
452
+ status.innerText = `Parallel: 0/${count}`;
453
+ let done = 0;
454
+ const tasks = Array.from({ length: count }).map(() =>
455
+ sendRequest(prompt, ratio, 1).then(async r => {
456
+ await addImageToGallery(r.data[0].b64_json);
457
+ done++;
458
+ status.innerText = `Parallel: ${done}/${count}`;
459
+ })
460
+ );
461
+ await Promise.all(tasks);
462
+ } else if (mode === 'stepwise') {
463
+ for (let i = 0; i < count; i++) {
464
+ status.innerText = `Stepwise: ${i+1}/${count}`;
465
+ const res = await sendRequest(prompt, ratio, 1);
466
+ await addImageToGallery(res.data[0].b64_json);
467
+ if (i < count - 1) {
468
+ const wait = Math.floor(Math.random() * 8000) + 5000;
469
+ for(let s = wait/1000; s > 0; s--) {
470
+ status.innerText = `Wait ${Math.round(s)}s (${i+1}/${count})`;
471
+ await sleep(1000);
472
+ }
473
+ }
474
+ }
475
+ }
476
+ status.innerText = "Done.";
477
+ } catch (e) {
478
+ status.innerText = `Error: ${e.message}`;
479
+ } finally {
480
+ btn.disabled = false;
481
+ }
482
+ }
483
+
484
+ // --- MODAL & NAV ---
485
+ async function openModal(i) {
486
+ currentIndex = i;
487
+ const imgObj = historyImages[i];
488
+ const modalImg = document.getElementById('modal-img');
489
+ const downloadBtn = document.getElementById('modal-download');
490
+ const status = document.getElementById('global-status');
491
+
492
+ // Если оригинал еще не скачан (лежит на hf.space)
493
+ if (!imgObj.isLoaded && imgObj.url.startsWith('http')) {
494
+ status.innerText = "Loading HD...";
495
+ modalImg.style.opacity = "0.5"; // Визуальный фидбек
496
+
497
+ const fullBlob = await fetchAsBlob(imgObj.url);
498
+ if (fullBlob) {
499
+ imgObj.url = fullBlob;
500
+ imgObj.isLoaded = true;
501
+ }
502
+ modalImg.style.opacity = "1";
503
+ status.innerText = "";
504
+ }
505
+
506
+ modalImg.src = imgObj.url;
507
+ downloadBtn.href = imgObj.url;
508
+ downloadBtn.download = imgObj.name;
509
+ document.getElementById('img-modal').style.display = 'flex';
510
+ document.body.style.overflow = 'hidden';
511
+ }
512
+
513
+ function closeModal() {
514
+ document.getElementById('img-modal').style.display = 'none';
515
+ document.body.style.overflow = 'auto';
516
+ }
517
+ function navigate(dir) {
518
+ currentIndex = (currentIndex + dir + historyImages.length) % historyImages.length;
519
+ openModal(currentIndex);
520
+ }
521
+
522
+ const checkExist = setInterval(() => {
523
+ if (document.querySelector('textarea, [role="textbox"]')) {
524
+ clearInterval(checkExist);
525
+ createUI();
526
+ }
527
+ }, 1000);
528
+ })();