Lookii125/image_editing_agent
0
1// Generate a unique session key securely
2const sessionKey = "session_" + (window.crypto && crypto.randomUUID ? crypto.randomUUID().replace(/-/g, "") : Date.now().toString());
3let showDetections = false;
4let zoomActive = false;
5let rulersActive = true;
6let detectionsEverRun = false; // Tracks if detections were ever used in this workflow
7
8// --- DOM Elements ---
9const DOM = {
10 btnHelp: document.getElementById('btn-help'),
11 helpModal: document.getElementById('help-modal'),
12 closeHelpModal: document.querySelector('.close-help'),
13 btnUndo: document.getElementById('btn-undo'),
14 btnBack: document.getElementById('btn-back'),
15 btnForward: document.getElementById('btn-forward'),
16 btnClear: document.getElementById('btn-clear'),
17 uploadInput: document.getElementById('upload-input'),
18 btnExport: document.getElementById('btn-export'),
19 btnCode: document.getElementById('btn-code'),
20 btnLens: document.getElementById('btn-lens'),
21 btnRuler: document.getElementById('btn-ruler'),
22
23 noImageMsg: document.getElementById('no-image-msg'),
24 canvasContainer: document.getElementById('canvas-container'),
25 imgOriginal: document.getElementById('img-original'),
26 imgLive: document.getElementById('img-live'),
27 liveCanvasTitle: document.getElementById('live-canvas-title'),
28 btnToggleDetections: document.getElementById('btn-toggle-detections'),
29 staleMsg: document.getElementById('stale-msg'),
30
31 chatHistory: document.getElementById('chat-history'),
32 chatInput: document.getElementById('chat-input'),
33 btnSend: document.getElementById('btn-send'),
34
35 toast: document.getElementById('toast'),
36 modal: document.getElementById('code-modal'),
37 codeBlock: document.getElementById('code-block'),
38 closeModal: document.querySelector('.close')
39};
40
41// --- Utilities ---
42function showToast(message) {
43 DOM.toast.textContent = message;
44 DOM.toast.className = "toast show";
45 setTimeout(() => { DOM.toast.className = DOM.toast.className.replace("show", ""); }, 3000);
46}
47
48function appendChat(role, content) {
49 const div = document.createElement('div');
50 div.className = `message msg-${role}`;
51 div.innerHTML = `<strong>${role === 'user' ? 'You' : 'Assistant'}:</strong><br>${content}`;
52 DOM.chatHistory.appendChild(div);
53 DOM.chatHistory.scrollTop = DOM.chatHistory.scrollHeight;
54}
55
56// --- API Calls ---
57async function fetchState() {
58 const res = await fetch(`/api/state?session_key=${sessionKey}`);
59 const state = await res.json();
60 updateUI(state);
61}
62
63async function performAction(action) {
64 const res = await fetch(`/api/action`, {
65 method: 'POST',
66 headers: { 'Content-Type': 'application/json' },
67 body: JSON.stringify({ session_key: sessionKey, action: action })
68 });
69 const data = await res.json();
70 showToast(data.message);
71 fetchState();
72}
73
74// --- Ruler Calculation Logic ---
75function buildRulers(img, rulerXId, rulerYId) {
76 const rulerX = document.getElementById(rulerXId);
77 const rulerY = document.getElementById(rulerYId);
78 if (!rulerX || !rulerY || !img.naturalWidth) return;
79
80 rulerX.innerHTML = '';
81 rulerY.innerHTML = '';
82
83 const nw = img.naturalWidth;
84 const nh = img.naturalHeight;
85
86 let step = 100;
87 if (nw > 2500) step = 500;
88 else if (nw > 1500) step = 250;
89 else if (nw < 500) step = 50;
90
91 for (let x = 0; x <= nw; x += step) {
92 const perc = (x / nw) * 100;
93 const tick = document.createElement('div');
94 tick.className = 'tick-x';
95 tick.style.left = `${perc}%`;
96 tick.innerText = x;
97 rulerX.appendChild(tick);
98 }
99
100 for (let y = 0; y <= nh; y += step) {
101 const perc = (y / nh) * 100;
102 const tick = document.createElement('div');
103 tick.className = 'tick-y';
104 tick.style.top = `${perc}%`;
105 tick.innerText = y;
106 rulerY.appendChild(tick);
107 }
108}
109
110// --- Ruler Toggle Logic ---
111if (DOM.btnRuler) {
112 DOM.btnRuler.onclick = () => {
113 rulersActive = !rulersActive;
114 DOM.btnRuler.classList.toggle('active', rulersActive);
115 document.querySelectorAll('.ruler-wrapper').forEach(wrapper => {
116 if (rulersActive) {
117 wrapper.classList.remove('hide-rulers');
118 } else {
119 wrapper.classList.add('hide-rulers');
120 }
121 });
122 };
123}
124
125// --- Whole-Image Hover Zoom Logic ---
126if (DOM.btnLens) {
127 DOM.btnLens.onclick = () => {
128 zoomActive = !zoomActive;
129 DOM.btnLens.classList.toggle('active', zoomActive);
130 if (!zoomActive) {
131 DOM.imgLive.style.transform = "scale(1)";
132 DOM.imgOriginal.style.transform = "scale(1)";
133 }
134 };
135}
136
137function setupZoom(imgElement) {
138 imgElement.onmousemove = (e) => {
139 if (!zoomActive) return;
140 const rect = imgElement.getBoundingClientRect();
141
142 const x = e.clientX - rect.left;
143 const y = e.clientY - rect.top;
144 const xPercent = (x / rect.width) * 100;
145 const yPercent = (y / rect.height) * 100;
146
147 imgElement.style.transformOrigin = `${xPercent}% ${yPercent}%`;
148 imgElement.style.transform = "scale(2.5)";
149 imgElement.style.cursor = "crosshair";
150 };
151
152 imgElement.onmouseleave = () => {
153 imgElement.style.transform = "scale(1)";
154 setTimeout(() => { imgElement.style.transformOrigin = "center center"; }, 150);
155 };
156}
157
158if (DOM.imgOriginal) {
159 DOM.imgOriginal.onload = () => {
160 buildRulers(DOM.imgOriginal, 'ruler-x-original', 'ruler-y-original');
161 setupZoom(DOM.imgOriginal);
162 };
163}
164if (DOM.imgLive) {
165 DOM.imgLive.onload = () => {
166 buildRulers(DOM.imgLive, 'ruler-x-live', 'ruler-y-live');
167 setupZoom(DOM.imgLive);
168 };
169}
170
171// --- Update UI based on Backend State ---
172function updateUI(state) {
173 if (!state.has_image) {
174 DOM.noImageMsg.style.display = 'block';
175 DOM.canvasContainer.style.display = 'none';
176 DOM.btnExport.disabled = true;
177 DOM.chatInput.disabled = true;
178 DOM.btnSend.disabled = true;
179 detectionsEverRun = false;
180 return;
181 }
182
183 DOM.noImageMsg.style.display = 'none';
184 DOM.canvasContainer.style.display = 'grid';
185 DOM.btnExport.disabled = false;
186 DOM.chatInput.disabled = false;
187 DOM.btnSend.disabled = false;
188
189 const ts = new Date().getTime();
190 DOM.imgOriginal.src = `/api/image/original?session_key=${sessionKey}&t=${ts}`;
191
192 DOM.liveCanvasTitle.textContent = `Live Modifiable Canvas (Step Point: ${state.history_index})`;
193
194 if (state.has_detections) {
195 detectionsEverRun = true;
196 }
197
198 // Only show stale message if detections were actually run before and are now out of sync
199 if (state.detections_stale && detectionsEverRun) {
200 DOM.staleMsg.style.display = 'block';
201 if(DOM.btnToggleDetections) DOM.btnToggleDetections.style.display = 'none';
202 showDetections = false;
203 } else {
204 DOM.staleMsg.style.display = 'none';
205 if (state.has_detections) {
206 if(DOM.btnToggleDetections) {
207 DOM.btnToggleDetections.style.display = 'block';
208 DOM.btnToggleDetections.textContent = showDetections ? "Hide Detections" : "Show Detected Elements";
209 }
210 } else {
211 if(DOM.btnToggleDetections) DOM.btnToggleDetections.style.display = 'none';
212 }
213 }
214
215 DOM.imgLive.src = `/api/image/live?session_key=${sessionKey}&show_detections=${showDetections}&t=${ts}`;
216}
217
218// --- Event Listeners ---
219DOM.btnUndo.onclick = () => performAction('undo');
220DOM.btnBack.onclick = () => performAction('back');
221DOM.btnForward.onclick = () => performAction('forward');
222DOM.btnClear.onclick = () => {
223 showDetections = false;
224 detectionsEverRun = false;
225 DOM.chatHistory.innerHTML = '';
226 performAction('clear');
227};
228
229if (DOM.btnToggleDetections) {
230 DOM.btnToggleDetections.onclick = () => {
231 showDetections = !showDetections;
232 fetchState();
233 };
234}
235
236DOM.uploadInput.onchange = async (e) => {
237 if (!e.target.files[0]) return;
238 const formData = new FormData();
239 formData.append("file", e.target.files[0]);
240 formData.append("session_key", sessionKey);
241
242 await fetch('/api/upload', { method: 'POST', body: formData });
243 DOM.chatHistory.innerHTML = '';
244 showDetections = false;
245 detectionsEverRun = false;
246 fetchState();
247};
248
249DOM.btnExport.onclick = async () => {
250 const a = document.createElement('a');
251 a.href = `/api/image/live?session_key=${sessionKey}&show_detections=false`;
252 a.download = "canvas_export.png";
253 a.click();
254};
255
256// --- Help Modal Logic ---
257if (DOM.btnHelp) {
258 DOM.btnHelp.onclick = () => DOM.helpModal.style.display = "block";
259}
260
261if (DOM.closeHelpModal) {
262 DOM.closeHelpModal.onclick = () => DOM.helpModal.style.display = "none";
263}
264
265// Close modals if the user clicks anywhere outside of the modal content window
266window.onclick = (e) => {
267 if (e.target === DOM.helpModal) DOM.helpModal.style.display = "none";
268 if (e.target === DOM.modal) DOM.modal.style.display = "none"; // This covers your existing code modal too
269};
270
271DOM.btnCode.onclick = async () => {
272 const res = await fetch(`/api/code?session_key=${sessionKey}`);
273 const data = await res.json();
274
275 let pythonCode = `import cv2\nimport numpy as np\n\ndef process_image(img):\n `;
276 pythonCode += data.code_log.join('\n ') + `\n return img`;
277
278 DOM.codeBlock.textContent = pythonCode;
279 DOM.modal.style.display = "block";
280};
281
282DOM.closeModal.onclick = () => DOM.modal.style.display = "none";
283
284// --- Chat Logic ---
285async function sendQuery() {
286 const query = DOM.chatInput.value.trim();
287 if (!query) return;
288
289 appendChat('user', query);
290 DOM.chatInput.value = '';
291
292 const spinnerId = 'spinner-' + Date.now();
293 appendChat('assistant', `<span id="${spinnerId}">Thinking... </span>`);
294
295 const res = await fetch('/api/chat', {
296 method: 'POST',
297 headers: { 'Content-Type': 'application/json' },
298 body: JSON.stringify({ session_key: sessionKey, query: query })
299 });
300
301 const data = await res.json();
302 document.getElementById(spinnerId).parentElement.innerHTML = `<strong>Assistant:</strong><br>${data.response}`;
303
304 fetchState();
305}
306
307DOM.btnSend.onclick = sendQuery;
308DOM.chatInput.onkeypress = (e) => { if (e.key === 'Enter') sendQuery(); };
309
310// Initialize App
311fetchState();
312
313// Show Help Modal immediately on first load
314window.addEventListener('DOMContentLoaded', () => {
315 if (DOM.helpModal) {
316 DOM.helpModal.style.display = "block";
317 }
318});