/* ===== DOM & Image Utilities ===== */ window.$ = function(sel, parent) { return (parent || document).querySelector(sel); }; window.$$ = function(sel, parent) { return [...(parent || document).querySelectorAll(sel)]; }; window.NAIUtils = { fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); }, dataUrlToBase64(dataUrl) { const idx = dataUrl.indexOf(','); return idx >= 0 ? dataUrl.substring(idx + 1) : dataUrl; }, loadImageFromDataUrl(dataUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = dataUrl; }); }, async imageToBase64Jpeg(imgElement, maxSize) { const canvas = document.createElement('canvas'); let w = imgElement.naturalWidth || imgElement.width; let h = imgElement.naturalHeight || imgElement.height; if (maxSize) { const scale = maxSize / Math.max(w, h); if (scale < 1) { w = Math.floor(w * scale); h = Math.floor(h * scale); } } canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(imgElement, 0, 0, w, h); return this.dataUrlToBase64(canvas.toDataURL('image/jpeg', 0.95)); }, async resizeImageToBase64(dataUrl, maxSize) { const img = await this.loadImageFromDataUrl(dataUrl); return this.imageToBase64Jpeg(img, maxSize); }, async sha256(str) { const buf = new TextEncoder().encode(str); const hash = await crypto.subtle.digest('SHA-256', buf); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); }, autoResize(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; }, setupAutoResize(textarea) { textarea.addEventListener('input', () => this.autoResize(textarea)); setTimeout(() => this.autoResize(textarea), 0); }, createDynSlider(label, id, min, max, step, value) { const decimals = step < 0.1 ? 2 : 1; const group = document.createElement('div'); group.className = 'form-group dyn-slider-group'; group.innerHTML = `
`; setTimeout(() => { const sl = $(`#${id}`); const valEl = $(`#${id}-val`); if (sl && valEl) { sl.addEventListener('input', () => { valEl.textContent = parseFloat(sl.value).toFixed(decimals); }); } }, 0); return group; }, bindSlider(sliderId, valueId, transform) { const slider = $(`#${sliderId}`); const valueEl = $(`#${valueId}`); if (!slider || !valueEl) return; const update = () => { valueEl.textContent = transform ? transform(parseFloat(slider.value)) : slider.value; }; slider.addEventListener('input', update); update(); }, };