arshvir commited on
Commit
c129a67
·
verified ·
1 Parent(s): f81eb55

Update static/js/main.js

Browse files
Files changed (1) hide show
  1. static/js/main.js +168 -219
static/js/main.js CHANGED
@@ -1,22 +1,20 @@
1
  /**
2
- * Radiant AI - Main Logic v4.0 (Production Stable)
3
- * Handles: Auto-Apply, Independent Tab Chaining, History, and Session Persistence.
4
  */
5
 
6
  const AppState = {
7
  // Session State
8
- uploadFilename: localStorage.getItem('radiant_upload_filename') || null, // The Clean Original
9
- currentFilename: localStorage.getItem('radiant_filename') || null, // The Latest Result
10
  currentUrl: localStorage.getItem('radiant_current_url') || null,
11
 
12
  // AI Lab specific state
13
  selectedAIModel: null,
14
 
15
- // History Stack
16
  history: [],
17
  historyIndex: -1,
18
 
19
- // Studio Params (Active State)
20
  serverParams: {
21
  brightness: 0, contrast: 0, saturation: 0, sharpness: 0,
22
  rotation: 0, flip_h: false, flip_v: false, filter: 'none'
@@ -27,57 +25,32 @@ const AppState = {
27
  };
28
 
29
  document.addEventListener('DOMContentLoaded', () => {
30
- console.log("🚀 Radiant AI Client v4.0 Loaded");
31
  initSession();
32
  setupGlobalListeners();
33
  setupKeyboardShortcuts();
34
  });
35
 
36
- // ==========================================================================
37
- // 1. CHAINING & SOURCE LOGIC (The Bug Fixer)
38
- // ==========================================================================
39
 
40
  /**
41
- * Determines which file to process based on the Tab's Checkbox.
42
- * @param {string} checkboxId - ID of the checkbox in the active tab
43
- * @returns {string} - The filename to send to the backend
44
  */
45
  function getSourceFilename(checkboxId) {
46
  const cb = document.getElementById(checkboxId);
47
 
48
- // If Checkbox is CHECKED and we have a previous result -> Use that result (Chain)
49
  if (cb && cb.checked && AppState.currentFilename) {
50
- // console.log("🔗 Chaining ON: Using previous result");
51
  return AppState.currentFilename;
52
  }
53
 
54
- // Default: Use the original Clean Upload
55
- // console.log("Chaining OFF: Using original file");
56
  return AppState.uploadFilename;
57
  }
58
 
59
- // Special Logic for Studio "Bake" Checkbox
60
- function toggleStudioChaining(checkbox) {
61
- if(checkbox.checked) {
62
- if(confirm("Bake current edits? This sets the current image as the new Original and resets sliders to 0.")) {
63
- // Make current result the new "Original"
64
- AppState.uploadFilename = AppState.currentFilename;
65
- localStorage.setItem('radiant_upload_filename', AppState.currentFilename);
66
-
67
- // Visual Reset
68
- resetSlidersUI();
69
-
70
- // Uncheck box to indicate action complete
71
- checkbox.checked = false;
72
- } else {
73
- checkbox.checked = false;
74
- }
75
- }
76
- }
77
-
78
- // ==========================================================================
79
- // 2. SESSION & HISTORY
80
- // ==========================================================================
81
 
82
  function initSession() {
83
  const savedParams = localStorage.getItem('radiant_params');
@@ -86,24 +59,20 @@ function initSession() {
86
  if (AppState.currentUrl && AppState.uploadFilename) {
87
  restoreImageToUI(AppState.currentUrl);
88
  updateSlidersUI();
89
- // If history empty, init it
90
  if(AppState.history.length === 0) pushHistory(AppState.currentUrl, AppState.serverParams);
91
  }
92
  }
93
 
94
  function pushHistory(url, params) {
95
- // Remove future history if we are in middle of stack
96
  if (AppState.historyIndex < AppState.history.length - 1) {
97
  AppState.history = AppState.history.slice(0, AppState.historyIndex + 1);
98
  }
99
-
100
- const paramsCopy = params ? JSON.parse(JSON.stringify(params)) : {};
101
-
102
  AppState.history.push({ url: url, params: paramsCopy });
103
  AppState.historyIndex++;
104
 
105
  localStorage.setItem('radiant_current_url', url);
106
- if(params) localStorage.setItem('radiant_params', JSON.stringify(params));
107
  }
108
 
109
  function undoEdit() {
@@ -122,70 +91,69 @@ function redoEdit() {
122
 
123
  function restoreState(state) {
124
  restoreImageToUI(state.url);
125
- // Restore Studio Params if they exist in this history state
126
- if(state.params && Object.keys(state.params).length > 0) {
127
- AppState.serverParams = JSON.parse(JSON.stringify(state.params));
128
- updateSlidersUI();
129
- }
130
-
131
- // We also update currentUrl so subsequent edits start from here (if chaining)
132
- // But we need to extract filename from URL for backend processing
133
- // URL format: /static/uploads/filename.ext?t=...
134
- const parts = state.url.split('?')[0].split('/');
135
- AppState.currentFilename = parts[parts.length - 1];
136
  }
137
 
138
- // ==========================================================================
139
- // 3. STUDIO LOGIC (Auto-Apply)
140
- // ==========================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  function handleSliderChange(param, value) {
143
  if(!AppState.uploadFilename) return;
144
-
145
- // Update Text
146
- const label = document.getElementById(`val-${param}`);
147
- if (label) label.innerText = value;
148
-
149
- // Update State
150
  AppState.serverParams[param] = parseInt(value);
151
-
152
- // Debounce
153
  if (AppState.debounceTimer) clearTimeout(AppState.debounceTimer);
154
- AppState.debounceTimer = setTimeout(() => processStudioEdit(), 500);
155
  }
156
 
157
  function handleFilterClick(filterName) {
158
  if(!AppState.uploadFilename) return alert("Upload image first");
159
-
160
- // UI Update
161
  document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
162
  if(event && event.target) event.target.classList.add('active');
163
-
164
  AppState.serverParams.filter = filterName;
165
- processStudioEdit();
166
  }
167
 
168
  function triggerAutoApply(action) {
169
  if(!AppState.uploadFilename) return alert("Upload image first");
170
-
171
  if (action === 'rotate_right') AppState.serverParams.rotation = (AppState.serverParams.rotation + 90) % 360;
172
  else if (action === 'rotate_left') AppState.serverParams.rotation = (AppState.serverParams.rotation - 90) % 360;
173
  else if (action === 'flip_h') AppState.serverParams.flip_h = !AppState.serverParams.flip_h;
174
  else if (action === 'flip_v') AppState.serverParams.flip_v = !AppState.serverParams.flip_v;
175
-
176
- processStudioEdit();
177
  }
178
 
179
- async function processStudioEdit() {
180
  if (AppState.isProcessing) return;
181
- setLoading(true);
 
 
182
 
183
  try {
184
- // Studio always uses uploadFilename (Clean Source) unless "Bake" was used
185
- const payload = {
186
- filename: AppState.uploadFilename,
187
- options: AppState.serverParams
188
- };
189
 
190
  const res = await fetch('/api/process-studio', {
191
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
@@ -193,33 +161,39 @@ async function processStudioEdit() {
193
  const data = await res.json();
194
 
195
  if (data.success) {
196
- handleSuccess(data);
 
 
197
  pushHistory(data.url, AppState.serverParams);
198
- } else {
199
- console.error(data.error);
200
  }
201
  } catch (err) { console.error(err); }
202
- finally { setLoading(false); }
 
 
 
203
  }
204
 
205
- // ==========================================================================
206
- // 4. VISION LOGIC
207
- // ==========================================================================
208
 
209
  async function runVisionTask(taskType) {
210
  if(!AppState.uploadFilename) return alert("Please upload an image first.");
211
 
212
- // Check Checkbox for Chaining
213
  const sourceFile = getSourceFilename('visionChainingCheckbox');
214
-
215
- // UI Setup
216
- document.getElementById('visionOverlay').style.display = 'none'; // Hide old overlays
217
- setLoading(true);
 
218
 
219
  const payload = { filename: sourceFile, task: taskType };
220
  if (taskType === 'replace_bg') payload.color = document.getElementById('bgColorPicker').value;
221
  if (taskType === 'face_blur') payload.value = document.getElementById('blurSlider').value;
222
 
 
 
 
 
223
  try {
224
  const res = await fetch('/api/process-vision', {
225
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
@@ -227,132 +201,98 @@ async function runVisionTask(taskType) {
227
  const data = await res.json();
228
 
229
  if(data.success) {
230
- handleSuccess(data);
231
- pushHistory(data.url, null); // Vision tasks don't have slider params to save
232
-
233
- // Handle Detections Overlay
234
- if(data.detections && data.detections.length > 0) {
235
- const overlay = document.getElementById('visionOverlay');
236
- if(overlay) {
237
- overlay.style.display = 'block';
238
- document.getElementById('detectionList').innerHTML = data.detections.map(d =>
239
- `<li>• <b>${d.label}</b> <small>${(d.conf*100).toFixed(0)}%</small></li>`
240
- ).join('');
241
- }
 
 
 
 
242
  }
243
  } else { alert("Vision Error: " + data.error); }
244
  } catch(err) { alert("Server Error"); }
245
- finally { setLoading(false); }
 
 
 
246
  }
247
 
248
- // ==========================================================================
249
- // 5. AI LABORATORY LOGIC
250
- // ==========================================================================
251
 
252
  function selectAIModel(task, card) {
253
  AppState.selectedAIModel = task;
254
  document.querySelectorAll('.model-card').forEach(c => c.classList.remove('active'));
255
  card.classList.add('active');
256
-
257
- if (AppState.uploadFilename) document.getElementById('btnRunAI').disabled = false;
258
  }
259
 
260
  async function runAITask() {
261
- if(!AppState.uploadFilename || !AppState.selectedAIModel) return alert("Select an image and a model.");
262
 
263
- // Check Checkbox for Chaining
264
  const sourceFile = getSourceFilename('aiChainingCheckbox');
265
-
266
- // Prepare UI
267
  const outImg = document.getElementById('aiOutputImg');
 
268
  const actions = document.getElementById('aiActions');
269
- const inputPreview = document.getElementById('aiInputImg');
270
- const aiLoader = document.getElementById('aiLoader');
271
 
272
- // Show what is being processed
273
- if(sourceFile === AppState.currentFilename) {
274
- inputPreview.src = `${AppState.currentUrl}?t=${new Date().getTime()}`;
 
275
  } else {
276
- inputPreview.src = `${AppState.originalUrl}?t=${new Date().getTime()}`;
 
277
  }
278
 
279
  outImg.style.display = 'none';
280
  actions.style.display = 'none';
281
- if(aiLoader) aiLoader.style.display = 'flex'; // Use AI specific loader
282
 
283
  try {
284
  const res = await fetch('/api/process-ai', {
285
- method: 'POST', headers: {'Content-Type': 'application/json'},
286
- body: JSON.stringify({ filename: sourceFile, task: AppState.selectedAIModel })
 
 
 
 
287
  });
 
288
  const data = await res.json();
289
 
290
  if(data.success) {
291
- // Manually handle AI UI because it has a split view
 
 
 
 
292
  outImg.src = `${data.url}?t=${new Date().getTime()}`;
293
  outImg.style.display = 'block';
294
  document.getElementById('aiDownload').href = data.url;
295
  actions.style.display = 'flex';
296
-
297
- // Also update global state so other tabs can use this result
298
- handleSuccess(data, false); // false = don't auto-update global main image tags yet to keep split view valid?
299
- // Actually, we SHOULD update global state so utilities can see it
300
- restoreImageToUI(data.url);
301
- pushHistory(data.url, null);
302
- } else { alert("AI Error: " + data.error); }
303
- } catch(err) { alert("Connection Failed"); }
304
- finally { if(aiLoader) aiLoader.style.display = 'none'; }
305
- }
306
-
307
- // ==========================================================================
308
- // 6. SHARED HELPERS
309
- // ==========================================================================
310
-
311
- function handleSuccess(data, updateUI=true) {
312
- AppState.currentFilename = data.filename;
313
- localStorage.setItem('radiant_filename', data.filename);
314
-
315
- if(updateUI) restoreImageToUI(data.url);
316
- }
317
-
318
- function setLoading(isLoading) {
319
- AppState.isProcessing = isLoading;
320
- // Generic overlays
321
- const overlays = document.querySelectorAll('#processingOverlay, #visionLoading');
322
- overlays.forEach(el => el.style.display = isLoading ? 'flex' : 'none');
323
-
324
- // Add simple blur effect to images while loading
325
- const imgs = document.querySelectorAll('img[id$="Preview"], img[id$="InputImg"]');
326
- imgs.forEach(img => img.style.opacity = isLoading ? '0.5' : '1');
327
  }
328
 
329
- function restoreImageToUI(url) {
330
- const fresh = `${url}?t=${new Date().getTime()}`;
331
-
332
- // Update all Preview Images
333
- ['imagePreview', 'visionPreview', 'aiInputImg', 'utilPreview'].forEach(id => {
334
- const el = document.getElementById(id);
335
- if(el) { el.src = fresh; el.style.display = 'block'; el.style.filter = 'none'; el.style.transform = 'none'; }
336
- });
337
-
338
- // Hide Placeholders
339
- ['placeholderState', 'visionPlaceholder', 'aiPlaceholder', 'utilPlaceholder'].forEach(id => {
340
- const el = document.getElementById(id);
341
- if(el) el.style.display = 'none';
342
- });
343
-
344
- // Update Downloads
345
- const dls = document.querySelectorAll('#downloadLink, #visionDownload, #utilDownload');
346
- dls.forEach(el => el.href = fresh);
347
-
348
- // Show Action Bars
349
- const bars = document.querySelectorAll('#actionBar, #visionActionBar');
350
- bars.forEach(el => el.style.display = 'flex');
351
-
352
- // Special: AI Tab Split View
353
- const aiWork = document.getElementById('aiWorkArea');
354
- if(aiWork) aiWork.style.display = 'grid';
355
- }
356
 
357
  function setupGlobalListeners() {
358
  ['fileInput', 'visionFileInput', 'aiFileInput', 'utilFileInput'].forEach(id => {
@@ -368,7 +308,7 @@ async function handleUpload(e) {
368
  const formData = new FormData();
369
  formData.append('file', file);
370
 
371
- // Loading State
372
  const btn = e.target.nextElementSibling;
373
  const oldText = btn ? btn.innerHTML : '';
374
  if(btn) btn.innerHTML = 'Uploading...';
@@ -378,55 +318,70 @@ async function handleUpload(e) {
378
  const data = await res.json();
379
 
380
  if(data.success) {
381
- // New Session Reset
382
- AppState.uploadFilename = data.filename;
383
- AppState.currentFilename = data.filename;
384
  AppState.originalUrl = data.url;
385
  AppState.currentUrl = data.url;
 
386
  AppState.history = [];
387
  AppState.historyIndex = -1;
388
-
389
- // Reset Studio Params
390
  AppState.serverParams = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0, rotation: 0, flip_h: false, flip_v: false, filter: 'none' };
391
- resetSlidersUI();
392
-
393
- // Persist
394
  localStorage.setItem('radiant_upload_filename', data.filename);
395
  localStorage.setItem('radiant_filename', data.filename);
396
- localStorage.setItem('radiant_original_url', data.url);
397
  localStorage.setItem('radiant_current_url', data.url);
 
398
 
399
- // Init UI
400
  pushHistory(data.url, AppState.serverParams);
401
  restoreImageToUI(data.url);
 
402
 
403
- // Enable AI Button if model selected
404
  if(AppState.selectedAIModel) document.getElementById('btnRunAI').disabled = false;
 
 
 
 
 
 
405
  }
406
  } catch(err) { alert("Upload error"); }
407
- finally {
408
- if(btn) btn.innerHTML = oldText;
409
- e.target.value = ''; // Allow re-uploading same file
410
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  }
412
 
413
  function updateSlidersUI() {
414
  const p = AppState.serverParams;
415
- // Set Slider Values
416
  setVal('brightness', p.brightness);
417
  setVal('contrast', p.contrast);
418
  setVal('saturation', p.saturation);
419
  setVal('sharpness', p.sharpness);
420
-
421
- // Set Filter Buttons
422
  document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
423
  const btns = document.querySelectorAll('.filter-btn');
424
  btns.forEach(b => {
425
- // Simple text matching for active state
426
- const txt = b.innerText.toLowerCase();
427
- if(txt === p.filter || (p.filter === 'none' && txt === 'normal')) {
428
- b.classList.add('active');
429
- }
430
  });
431
  }
432
 
@@ -438,19 +393,17 @@ function setVal(id, val) {
438
  }
439
 
440
  function resetOriginal() {
441
- if(AppState.uploadFilename && confirm("Reset to Original Image?")) {
442
- // Reset Logic
443
  AppState.serverParams = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0, rotation: 0, flip_h: false, flip_v: false, filter: 'none' };
444
 
445
- // Reset File Pointer
446
  AppState.currentFilename = AppState.uploadFilename;
447
  localStorage.setItem('radiant_filename', AppState.uploadFilename);
448
 
449
- // Update
450
- processStudioEdit(); // Will re-process original with 0 params
451
- resetSlidersUI();
452
 
453
- // Reset AI Output View
454
  const aiOut = document.getElementById('aiOutputImg');
455
  const aiAct = document.getElementById('aiActions');
456
  if(aiOut) aiOut.style.display = 'none';
@@ -458,10 +411,6 @@ function resetOriginal() {
458
  }
459
  }
460
 
461
- function resetSlidersUI() {
462
- updateSlidersUI(); // Will use default 0 params in state
463
- }
464
-
465
  function setupKeyboardShortcuts() {
466
  document.addEventListener('keydown', (e) => {
467
  if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); undoEdit(); }
 
1
  /**
2
+ * Radiant AI - Main Logic v3.3
3
+ * Features: Auto-Apply, Independent Tab Chaining, History, Robust Session
4
  */
5
 
6
  const AppState = {
7
  // Session State
8
+ uploadFilename: localStorage.getItem('radiant_upload_filename') || null, // Clean Original
9
+ currentFilename: localStorage.getItem('radiant_filename') || null, // Latest Edited
10
  currentUrl: localStorage.getItem('radiant_current_url') || null,
11
 
12
  // AI Lab specific state
13
  selectedAIModel: null,
14
 
 
15
  history: [],
16
  historyIndex: -1,
17
 
 
18
  serverParams: {
19
  brightness: 0, contrast: 0, saturation: 0, sharpness: 0,
20
  rotation: 0, flip_h: false, flip_v: false, filter: 'none'
 
25
  };
26
 
27
  document.addEventListener('DOMContentLoaded', () => {
 
28
  initSession();
29
  setupGlobalListeners();
30
  setupKeyboardShortcuts();
31
  });
32
 
33
+ // --- 1. Helper: Determine Source File ---
 
 
34
 
35
  /**
36
+ * Returns the correct filename to process based on the tab's checkbox state.
37
+ * @param {string} checkboxId - The ID of the checkbox in the active tab
 
38
  */
39
  function getSourceFilename(checkboxId) {
40
  const cb = document.getElementById(checkboxId);
41
 
42
+ // If checkbox exists AND is checked AND we have a previous result -> Chain it
43
  if (cb && cb.checked && AppState.currentFilename) {
44
+ console.log(`🔗 Chaining Enabled: Using ${AppState.currentFilename}`);
45
  return AppState.currentFilename;
46
  }
47
 
48
+ // Otherwise use the clean original
49
+ console.log(`Using Original: ${AppState.uploadFilename}`);
50
  return AppState.uploadFilename;
51
  }
52
 
53
+ // --- 2. Session & History ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  function initSession() {
56
  const savedParams = localStorage.getItem('radiant_params');
 
59
  if (AppState.currentUrl && AppState.uploadFilename) {
60
  restoreImageToUI(AppState.currentUrl);
61
  updateSlidersUI();
 
62
  if(AppState.history.length === 0) pushHistory(AppState.currentUrl, AppState.serverParams);
63
  }
64
  }
65
 
66
  function pushHistory(url, params) {
 
67
  if (AppState.historyIndex < AppState.history.length - 1) {
68
  AppState.history = AppState.history.slice(0, AppState.historyIndex + 1);
69
  }
70
+ const paramsCopy = JSON.parse(JSON.stringify(params));
 
 
71
  AppState.history.push({ url: url, params: paramsCopy });
72
  AppState.historyIndex++;
73
 
74
  localStorage.setItem('radiant_current_url', url);
75
+ localStorage.setItem('radiant_params', JSON.stringify(params));
76
  }
77
 
78
  function undoEdit() {
 
91
 
92
  function restoreState(state) {
93
  restoreImageToUI(state.url);
94
+ AppState.serverParams = JSON.parse(JSON.stringify(state.params));
95
+ updateSlidersUI();
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
+ // --- 3. Studio Logic (Auto-Apply + Chaining) ---
99
+
100
+ // Special Chaining Logic for Studio:
101
+ // If "Bake" is enabled, we commit the current state as the new "Original"
102
+ // and reset sliders to 0 so you can edit on top.
103
+ function toggleStudioChaining(checkbox) {
104
+ if(checkbox.checked) {
105
+ if(confirm("Bake current edits? This will reset sliders to 0 but keep the look.")) {
106
+ // "Bake" logic: Set current edited file as the new "Original"
107
+ AppState.uploadFilename = AppState.currentFilename;
108
+ localStorage.setItem('radiant_upload_filename', AppState.currentFilename);
109
+
110
+ // Reset params to 0 (visual reset)
111
+ AppState.serverParams = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0, rotation: 0, flip_h: false, flip_v: false, filter: 'none' };
112
+ updateSlidersUI();
113
+
114
+ // Uncheck box automatically after baking (optional UX choice)
115
+ checkbox.checked = false;
116
+ } else {
117
+ checkbox.checked = false;
118
+ }
119
+ }
120
+ }
121
 
122
  function handleSliderChange(param, value) {
123
  if(!AppState.uploadFilename) return;
124
+ document.getElementById(`val-${param}`).innerText = value;
 
 
 
 
 
125
  AppState.serverParams[param] = parseInt(value);
 
 
126
  if (AppState.debounceTimer) clearTimeout(AppState.debounceTimer);
127
+ AppState.debounceTimer = setTimeout(() => processAutoEdit(), 500);
128
  }
129
 
130
  function handleFilterClick(filterName) {
131
  if(!AppState.uploadFilename) return alert("Upload image first");
 
 
132
  document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
133
  if(event && event.target) event.target.classList.add('active');
 
134
  AppState.serverParams.filter = filterName;
135
+ processAutoEdit();
136
  }
137
 
138
  function triggerAutoApply(action) {
139
  if(!AppState.uploadFilename) return alert("Upload image first");
 
140
  if (action === 'rotate_right') AppState.serverParams.rotation = (AppState.serverParams.rotation + 90) % 360;
141
  else if (action === 'rotate_left') AppState.serverParams.rotation = (AppState.serverParams.rotation - 90) % 360;
142
  else if (action === 'flip_h') AppState.serverParams.flip_h = !AppState.serverParams.flip_h;
143
  else if (action === 'flip_v') AppState.serverParams.flip_v = !AppState.serverParams.flip_v;
144
+ processAutoEdit();
 
145
  }
146
 
147
+ async function processAutoEdit() {
148
  if (AppState.isProcessing) return;
149
+ const overlay = document.getElementById('processingOverlay');
150
+ if(overlay) overlay.style.display = 'flex';
151
+ AppState.isProcessing = true;
152
 
153
  try {
154
+ // Studio always uses uploadFilename (the clean source)
155
+ // unless "Bake" was triggered (which updates uploadFilename)
156
+ const payload = { filename: AppState.uploadFilename, options: AppState.serverParams };
 
 
157
 
158
  const res = await fetch('/api/process-studio', {
159
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
 
161
  const data = await res.json();
162
 
163
  if (data.success) {
164
+ AppState.currentFilename = data.filename;
165
+ localStorage.setItem('radiant_filename', data.filename);
166
+ restoreImageToUI(data.url);
167
  pushHistory(data.url, AppState.serverParams);
 
 
168
  }
169
  } catch (err) { console.error(err); }
170
+ finally {
171
+ if(overlay) overlay.style.display = 'none';
172
+ AppState.isProcessing = false;
173
+ }
174
  }
175
 
176
+ // --- 4. VISION LOGIC ---
 
 
177
 
178
  async function runVisionTask(taskType) {
179
  if(!AppState.uploadFilename) return alert("Please upload an image first.");
180
 
181
+ // Determine Source File (Chain or Original)
182
  const sourceFile = getSourceFilename('visionChainingCheckbox');
183
+
184
+ const loader = document.getElementById('visionLoading');
185
+ const img = document.getElementById('visionPreview');
186
+ const overlay = document.getElementById('visionOverlay');
187
+ const ph = document.getElementById('visionPlaceholder');
188
 
189
  const payload = { filename: sourceFile, task: taskType };
190
  if (taskType === 'replace_bg') payload.color = document.getElementById('bgColorPicker').value;
191
  if (taskType === 'face_blur') payload.value = document.getElementById('blurSlider').value;
192
 
193
+ if(loader) loader.style.display = 'flex';
194
+ if(img) img.style.filter = 'blur(5px) grayscale(80%)';
195
+ if(overlay) overlay.style.display = 'none';
196
+
197
  try {
198
  const res = await fetch('/api/process-vision', {
199
  method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
 
201
  const data = await res.json();
202
 
203
  if(data.success) {
204
+ // Update current filename so subsequent chains use this result
205
+ AppState.currentFilename = data.filename;
206
+ localStorage.setItem('radiant_filename', data.filename);
207
+
208
+ const freshUrl = `${data.url}?t=${new Date().getTime()}`;
209
+ if(img) { img.src = freshUrl; img.style.display = 'block'; img.style.filter = 'none'; }
210
+ if(ph) ph.style.display = 'none';
211
+
212
+ document.getElementById('visionDownload').href = data.url;
213
+ document.getElementById('visionActionBar').style.display = 'flex';
214
+
215
+ if(data.detections && data.detections.length > 0 && overlay) {
216
+ overlay.style.display = 'block';
217
+ document.getElementById('detectionList').innerHTML = data.detections.map(d =>
218
+ `<li>• <b>${d.label}</b> <small>${(d.conf*100).toFixed(0)}%</small></li>`
219
+ ).join('');
220
  }
221
  } else { alert("Vision Error: " + data.error); }
222
  } catch(err) { alert("Server Error"); }
223
+ finally {
224
+ if(loader) loader.style.display = 'none';
225
+ if(img) img.style.filter = 'none';
226
+ }
227
  }
228
 
229
+ // --- 5. AI LABORATORY LOGIC ---
 
 
230
 
231
  function selectAIModel(task, card) {
232
  AppState.selectedAIModel = task;
233
  document.querySelectorAll('.model-card').forEach(c => c.classList.remove('active'));
234
  card.classList.add('active');
235
+ const btn = document.getElementById('btnRunAI');
236
+ if (AppState.uploadFilename) btn.disabled = false;
237
  }
238
 
239
  async function runAITask() {
240
+ if(!AppState.uploadFilename || !AppState.selectedAIModel) return alert("Select an image and a model first.");
241
 
242
+ // Determine Source File (Chain or Original)
243
  const sourceFile = getSourceFilename('aiChainingCheckbox');
244
+
245
+ const loader = document.getElementById('aiLoader');
246
  const outImg = document.getElementById('aiOutputImg');
247
+ const inImg = document.getElementById('aiInputImg');
248
  const actions = document.getElementById('aiActions');
 
 
249
 
250
+ // Update Input Preview to match what we are processing
251
+ if(sourceFile !== AppState.uploadFilename) {
252
+ // If chaining, show the previous result as the input
253
+ inImg.src = `${AppState.currentUrl}?t=${new Date().getTime()}`;
254
  } else {
255
+ // Else show original
256
+ inImg.src = `${AppState.originalUrl}?t=${new Date().getTime()}`;
257
  }
258
 
259
  outImg.style.display = 'none';
260
  actions.style.display = 'none';
261
+ if(loader) loader.style.display = 'flex';
262
 
263
  try {
264
  const res = await fetch('/api/process-ai', {
265
+ method: 'POST',
266
+ headers: {'Content-Type': 'application/json'},
267
+ body: JSON.stringify({
268
+ filename: sourceFile,
269
+ task: AppState.selectedAIModel
270
+ })
271
  });
272
+
273
  const data = await res.json();
274
 
275
  if(data.success) {
276
+ // Update current filename so next chain uses this
277
+ AppState.currentFilename = data.filename;
278
+ localStorage.setItem('radiant_filename', data.filename);
279
+ localStorage.setItem('radiant_current_url', data.url);
280
+
281
  outImg.src = `${data.url}?t=${new Date().getTime()}`;
282
  outImg.style.display = 'block';
283
  document.getElementById('aiDownload').href = data.url;
284
  actions.style.display = 'flex';
285
+ } else {
286
+ alert("AI Error: " + data.error);
287
+ }
288
+ } catch(err) {
289
+ alert("Server connection failed.");
290
+ } finally {
291
+ if(loader) loader.style.display = 'none';
292
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  }
294
 
295
+ // --- 6. Upload & Helpers ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  function setupGlobalListeners() {
298
  ['fileInput', 'visionFileInput', 'aiFileInput', 'utilFileInput'].forEach(id => {
 
308
  const formData = new FormData();
309
  formData.append('file', file);
310
 
311
+ // Show loading
312
  const btn = e.target.nextElementSibling;
313
  const oldText = btn ? btn.innerHTML : '';
314
  if(btn) btn.innerHTML = 'Uploading...';
 
318
  const data = await res.json();
319
 
320
  if(data.success) {
321
+ // New Session
322
+ AppState.uploadFilename = data.filename;
323
+ AppState.currentFilename = data.filename; // Init current as original
324
  AppState.originalUrl = data.url;
325
  AppState.currentUrl = data.url;
326
+
327
  AppState.history = [];
328
  AppState.historyIndex = -1;
 
 
329
  AppState.serverParams = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0, rotation: 0, flip_h: false, flip_v: false, filter: 'none' };
330
+
 
 
331
  localStorage.setItem('radiant_upload_filename', data.filename);
332
  localStorage.setItem('radiant_filename', data.filename);
 
333
  localStorage.setItem('radiant_current_url', data.url);
334
+ localStorage.setItem('radiant_original_url', data.url);
335
 
 
336
  pushHistory(data.url, AppState.serverParams);
337
  restoreImageToUI(data.url);
338
+ updateSlidersUI();
339
 
340
+ // Enable AI button if model selected
341
  if(AppState.selectedAIModel) document.getElementById('btnRunAI').disabled = false;
342
+
343
+ // Show comparison view in AI tab
344
+ const aiWork = document.getElementById('aiWorkArea');
345
+ const aiPlace = document.getElementById('aiPlaceholder');
346
+ if(aiWork) aiWork.style.display = 'grid';
347
+ if(aiPlace) aiPlace.style.display = 'none';
348
  }
349
  } catch(err) { alert("Upload error"); }
350
+ finally { if(btn) btn.innerHTML = oldText; e.target.value = ''; }
351
+ }
352
+
353
+ function restoreImageToUI(url) {
354
+ const fresh = `${url}?t=${new Date().getTime()}`;
355
+ ['imagePreview', 'visionPreview', 'aiInputImg', 'utilPreview'].forEach(id => {
356
+ const el = document.getElementById(id);
357
+ if(el) { el.src = fresh; el.style.display = 'block'; }
358
+ });
359
+
360
+ ['placeholderState', 'visionPlaceholder', 'aiPlaceholder', 'utilPlaceholder'].forEach(id => {
361
+ const el = document.getElementById(id);
362
+ if(el) el.style.display = 'none';
363
+ });
364
+
365
+ const aiWork = document.getElementById('aiWorkArea');
366
+ if(aiWork && url) aiWork.style.display = 'grid';
367
+
368
+ const dl = document.getElementById('downloadLink');
369
+ if(dl) dl.href = fresh;
370
+ const bar = document.getElementById('actionBar');
371
+ if(bar) bar.style.display = 'flex';
372
  }
373
 
374
  function updateSlidersUI() {
375
  const p = AppState.serverParams;
 
376
  setVal('brightness', p.brightness);
377
  setVal('contrast', p.contrast);
378
  setVal('saturation', p.saturation);
379
  setVal('sharpness', p.sharpness);
 
 
380
  document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
381
  const btns = document.querySelectorAll('.filter-btn');
382
  btns.forEach(b => {
383
+ if(b.innerText.toLowerCase() === p.filter) b.classList.add('active');
384
+ else if (p.filter === 'none' && b.innerText === 'Normal') b.classList.add('active');
 
 
 
385
  });
386
  }
387
 
 
393
  }
394
 
395
  function resetOriginal() {
396
+ if(AppState.uploadFilename && confirm("Reset all edits?")) {
 
397
  AppState.serverParams = { brightness: 0, contrast: 0, saturation: 0, sharpness: 0, rotation: 0, flip_h: false, flip_v: false, filter: 'none' };
398
 
399
+ // Reset state to original
400
  AppState.currentFilename = AppState.uploadFilename;
401
  localStorage.setItem('radiant_filename', AppState.uploadFilename);
402
 
403
+ processAutoEdit();
404
+ updateSlidersUI();
 
405
 
406
+ // Reset AI Output
407
  const aiOut = document.getElementById('aiOutputImg');
408
  const aiAct = document.getElementById('aiActions');
409
  if(aiOut) aiOut.style.display = 'none';
 
411
  }
412
  }
413
 
 
 
 
 
414
  function setupKeyboardShortcuts() {
415
  document.addEventListener('keydown', (e) => {
416
  if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); undoEdit(); }