CoolFace
Apppublic

Lookii125/image_editing_agent

sourceHugging Faceupdated 1mo agoView on Hugging Face
0likes
app.js318 linesDownload Raw Back to static
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});