PageSourceSearch

https://picsart.com/blog/wp-content/themes/twentynineteen-child/assets/js/prompt-box.js?ver=1790157561

js picsart.com collected 2026-09-24 08:24:52 UTC 15,979 bytes, 361 lines download raw bytes

1/**
2 * Prompt Box widget — frontend behavior.
3 * Mirrors src/components/PromptBox/index.tsx in landings-ssr.
4 */
5(function () {
6    'use strict';
7
8    var ARROW_SVG = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg>';
9    var SPINNER_SVG = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="prompt_box_spinner"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>';
10    var X_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
11
12    var MAX_HEIGHT = 200;
13    var MAX_IMAGES = 4;
14    var MAX_FILE_SIZE = 10 * 1024 * 1024;
15
16    function uploadFileToCDN(file) {
17        var UPLOAD_URL = 'https://upload.picsart.com/files';
18        var UPLOAD_URL_STAGE2 = 'https://upload-stage.picsartstage2.com/files';
19        var hostname = window.location.hostname;
20        var isStage2 = hostname.indexOf('picsartstage2.com') !== -1 || hostname.indexOf('stage2') !== -1;
21        var url = isStage2 ? UPLOAD_URL_STAGE2 : UPLOAD_URL;
22        var formData = new FormData();
23        formData.append('type', 'editing-temp-landings');
24        formData.append('file', file);
25        formData.append('url', '');
26        formData.append('metainfo', '');
27        return fetch(url, { method: 'POST', body: formData, mode: 'cors', cache: 'no-store' })
28            .then(function (res) {
29                return res.json().then(function (data) {
30                    return { ok: res.ok, data: data };
31                });
32            })
33            .then(function (out) {
34                if (out.ok && out.data && out.data.result && out.data.result.url) return out.data.result.url;
35                throw new Error('Upload failed');
36            });
37    }
38
39    function buildUrl(rawDeeplink, params) {
40        try {
41            var url = new URL(rawDeeplink, window.location.origin);
42            Object.keys(params).forEach(function (k) {
43                url.searchParams.set(k, params[k]);
44            });
45            return url.href;
46        } catch (e) {
47            var pairs = Object.keys(params).map(function (k) {
48                return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
49            });
50            var separator = rawDeeplink.indexOf('?') !== -1 ? '&' : '?';
51            return rawDeeplink + separator + pairs.join('&');
52        }
53    }
54
55    function init(container) {
56        if (!container || container.dataset.promptBoxInit === '1') return;
57        container.dataset.promptBoxInit = '1';
58
59        var deeplink   = container.getAttribute('data-deeplink') || '';
60        var textarea   = container.querySelector('.prompt_box_textarea');
61        var submitBtn  = container.querySelector('[data-action="submit"]');
62        if (!textarea || !submitBtn) return;
63        var submitIcon = submitBtn.querySelector('[data-role="submit-icon"]');
64        var fileInput  = container.querySelector('.prompt_box_file_input');
65        var thumbsEl   = container.querySelector('[data-role="thumbs"]');
66        var openFileBtn = container.querySelector('[data-action="open-file"]');
67        var libraryBtn = container.querySelector('[data-action="toggle-library"]');
68        var libraryEl  = container.querySelector('[data-role="library"]');
69        var ratioBtn   = container.querySelector('[data-action="toggle-ratio"]');
70        var ratioEl    = container.querySelector('[data-role="ratio"]');
71        var ratioLabel = container.querySelector('[data-role="ratio-label"]');
72        var micBtn     = container.querySelector('[data-action="toggle-mic"]');
73
74        var images = [];
75        var selectedRatio = 'auto';
76        var isUploading = false;
77
78        function autoresize() {
79            textarea.style.height = 'auto';
80            var next = Math.min(textarea.scrollHeight, MAX_HEIGHT);
81            textarea.style.height = next + 'px';
82            textarea.style.overflowY = textarea.scrollHeight > MAX_HEIGHT ? 'auto' : 'hidden';
83        }
84
85        function canSubmitNow() {
86            var hasText = textarea.value.trim().length > 0;
87            var hasImages = images.length > 0;
88            return (hasText || hasImages) && !isUploading;
89        }
90
91        function syncSubmitState() {
92            var canSubmit = canSubmitNow();
93            submitBtn.classList.toggle('prompt_box_submit--active', canSubmit);
94            submitBtn.setAttribute('aria-disabled', canSubmit ? 'false' : 'true');
95        }
96
97        // --- Reference images ---
98        function renderThumbs() {
99            if (!thumbsEl) return;
100            thumbsEl.innerHTML = '';
101            if (!images.length) {
102                thumbsEl.classList.remove('prompt_box_thumbs--has-items');
103                return;
104            }
105            thumbsEl.classList.add('prompt_box_thumbs--has-items');
106            images.forEach(function (img, index) {
107                var wrap = document.createElement('div');
108                wrap.className = 'prompt_box_thumb';
109
110                var image = document.createElement('img');
111                image.src = img.preview;
112                image.alt = 'Reference ' + (index + 1);
113                wrap.appendChild(image);
114
115                var remove = document.createElement('button');
116                remove.type = 'button';
117                remove.className = 'prompt_box_thumb_remove';
118                remove.setAttribute('aria-label', 'Remove image ' + (index + 1));
119                remove.innerHTML = X_SVG;
120                remove.addEventListener('click', function () {
121                    images.splice(index, 1);
122                    renderThumbs();
123                    syncSubmitState();
124                });
125                wrap.appendChild(remove);
126
127                thumbsEl.appendChild(wrap);
128            });
129        }
130
131        function readFile(file) {
132            if (!file || !file.type || file.type.indexOf('image/') !== 0) return;
133            if (file.size > MAX_FILE_SIZE) return;
134            if (images.length >= MAX_IMAGES) return;
135            var reader = new FileReader();
136            reader.onload = function (e) {
137                if (images.length >= MAX_IMAGES) return;
138                images.push({ file: file, preview: e.target.result });
139                renderThumbs();
140                syncSubmitState();
141            };
142            reader.readAsDataURL(file);
143        }
144
145        if (openFileBtn && fileInput) {
146            openFileBtn.addEventListener('click', function () {
147                if (images.length >= MAX_IMAGES) return;
148                fileInput.click();
149            });
150            fileInput.addEventListener('change', function (e) {
151                if (e.target.files && e.target.files[0]) readFile(e.target.files[0]);
152                fileInput.value = '';
153            });
154        }
155
156        // --- Dropdowns ---
157        function closeAllDropdowns(except) {
158            if (libraryEl && libraryEl !== except) {
159                libraryEl.hidden = true;
160                if (libraryBtn) libraryBtn.setAttribute('aria-expanded', 'false');
161            }
162            if (ratioEl && ratioEl !== except) {
163                ratioEl.hidden = true;
164                if (ratioBtn) ratioBtn.setAttribute('aria-expanded', 'false');
165            }
166        }
167
168        if (libraryBtn && libraryEl) {
169            libraryBtn.addEventListener('click', function (e) {
170                e.stopPropagation();
171                e.preventDefault();
172                var willOpen = libraryEl.hidden;
173                closeAllDropdowns(willOpen ? libraryEl : null);
174                libraryEl.hidden = !willOpen;
175                libraryBtn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
176            });
177
178            // Stop clicks inside the popup body from bubbling to the doc-level
179            // mousedown listener, which would otherwise race with the item click.
180            libraryEl.addEventListener('mousedown', function (e) { e.stopPropagation(); });
181
182            // Mirrors handlePromptSelect in landings-ssr/src/components/PromptBox/index.tsx:
183            //   const selected = prompt.prompt || prompt.text;
184            //   setPromptText(prev => prev.trim() ? `${prev.trim()} ${selected}` : selected);
185            //   setSelectedPrompt(prompt.text);
186            //   setShowPromptLibrary(false);
187            var items = libraryEl.querySelectorAll('.prompt_box_library_item');
188            items.forEach(function (btn) {
189                btn.addEventListener('click', function (e) {
190                    e.preventDefault();
191                    e.stopPropagation();
192                    var fullPrompt = btn.getAttribute('data-prompt-full') || btn.textContent.trim();
193                    var current = textarea.value.trim();
194                    textarea.value = current ? (current + ' ' + fullPrompt) : fullPrompt;
195                    items.forEach(function (other) {
196                        other.classList.toggle('prompt_box_library_item--selected', other === btn);
197                    });
198                    autoresize();
199                    syncSubmitState();
200                    textarea.focus();
201                    closeAllDropdowns();
202                });
203            });
204        }
205
206        if (ratioBtn && ratioEl) {
207            ratioBtn.addEventListener('click', function (e) {
208                e.stopPropagation();
209                e.preventDefault();
210                var willOpen = ratioEl.hidden;
211                closeAllDropdowns(willOpen ? ratioEl : null);
212                ratioEl.hidden = !willOpen;
213                ratioBtn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
214            });
215
216            ratioEl.addEventListener('mousedown', function (e) { e.stopPropagation(); });
217
218            var ratioItems = ratioEl.querySelectorAll('.prompt_box_ratio_item');
219            ratioItems.forEach(function (btn) {
220                btn.addEventListener('click', function (e) {
221                    e.preventDefault();
222                    e.stopPropagation();
223                    selectedRatio = btn.getAttribute('data-ratio-value') || 'auto';
224                    if (ratioLabel) ratioLabel.textContent = btn.getAttribute('data-ratio-label') || 'Auto';
225                    ratioItems.forEach(function (b) {
226                        b.classList.toggle('prompt_box_ratio_item--selected', b === btn);
227                    });
228                    closeAllDropdowns();
229                });
230            });
231        }
232
233        document.addEventListener('mousedown', function (e) {
234            if (!container.contains(e.target)) closeAllDropdowns();
235        });
236
237        // --- Microphone (mirrors useSpeechToText in landings-ssr/src/hooks) ---
238        // The React version always renders the mic button; the hook bails out
239        // silently when the browser does not expose the SpeechRecognition API.
240        var recognition = null;
241        var isListening = false;
242        var finalTranscript = '';
243
244        if (micBtn) {
245            micBtn.addEventListener('click', function () {
246                var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
247                if (!SpeechRecognition) {
248                    if (window.console && console.warn) {
249                        console.warn('Speech Recognition is not supported in this browser.');
250                    }
251                    return;
252                }
253                if (isListening && recognition) {
254                    try { recognition.stop(); } catch (e) {}
255                    return;
256                }
257                recognition = new SpeechRecognition();
258                recognition.lang = (document.documentElement.lang || navigator.language || 'en-US');
259                recognition.interimResults = true;
260                recognition.continuous = true;
261                finalTranscript = textarea.value;
262
263                recognition.onresult = function (event) {
264                    var interim = '';
265                    for (var i = event.resultIndex; i < event.results.length; i++) {
266                        var transcript = event.results[i][0].transcript;
267                        if (event.results[i].isFinal) {
268                            finalTranscript += (finalTranscript ? ' ' : '') + transcript;
269                        } else {
270                            interim += transcript;
271                        }
272                    }
273                    textarea.value = finalTranscript + (interim ? ' ' + interim : '');
274                    autoresize();
275                    syncSubmitState();
276                };
277                recognition.onerror = function () {
278                    isListening = false;
279                    micBtn.classList.remove('prompt_box_mic_btn--active');
280                    recognition = null;
281                };
282                recognition.onend = function () {
283                    isListening = false;
284                    micBtn.classList.remove('prompt_box_mic_btn--active');
285                    recognition = null;
286                };
287
288                try {
289                    recognition.start();
290                    isListening = true;
291                    micBtn.classList.add('prompt_box_mic_btn--active');
292                } catch (e) {}
293            });
294        }
295
296        // --- Submit ---
297        function setUploading(state) {
298            isUploading = state;
299            if (submitIcon) submitIcon.innerHTML = state ? SPINNER_SVG : ARROW_SVG;
300            submitBtn.classList.toggle('prompt_box_submit--uploading', state);
301            syncSubmitState();
302        }
303
304        function submit() {
305            if (!canSubmitNow()) return;
306            var value = textarea.value.trim();
307            var params = {};
308            if (value) params['prompt'] = value;
309            if (selectedRatio) params['aspect_ratio'] = selectedRatio;
310
311            var go = function (cdnUrls) {
312                if (cdnUrls && cdnUrls.length) params['images'] = cdnUrls.join(',');
313                window.open(buildUrl(deeplink, params), '_blank', 'noopener,noreferrer');
314            };
315
316            if (images.length) {
317                setUploading(true);
318                Promise.all(images.map(function (img) { return uploadFileToCDN(img.file); }))
319                    .then(function (urls) { setUploading(false); go(urls); })
320                    .catch(function () { setUploading(false); alert('Upload failed. Please try again.'); });
321            } else {
322                go();
323            }
324        }
325
326        textarea.addEventListener('input', function () { autoresize(); syncSubmitState(); });
327        // Close any open dropdown when the user interacts with the textarea.
328        textarea.addEventListener('focus', function () { closeAllDropdowns(); });
329        textarea.addEventListener('mousedown', function () { closeAllDropdowns(); });
330        textarea.addEventListener('keydown', function (e) {
331            closeAllDropdowns();
332            if (e.key === 'Enter' && !e.shiftKey) {
333                e.preventDefault();
334                submit();
335            }
336        });
337        submitBtn.addEventListener('click', function (e) {
338            e.preventDefault();
339            submit();
340        });
341
342        autoresize();
343        syncSubmitState();
344    }
345
346    function bootAll() {
347        var nodes = document.querySelectorAll('.section_prompt_box');
348        for (var i = 0; i < nodes.length; i++) {
349            init(nodes[i]);
350        }
351    }
352
353    if (document.readyState === 'loading') {
354        document.addEventListener('DOMContentLoaded', bootAll);
355    } else {
356        bootAll();
357    }
358
359    // Expose for late-rendered widgets (e.g. Elementor editor preview).
360    window.PicsartPromptBox = { init: init, bootAll: bootAll };
361})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.