PageSourceSearch

https://studio.wavel.ai/js/vidgen2-widget.js?v=1787548204

js wavel.ai collected 2026-09-25 14:36:52 UTC 32,403 bytes, 773 lines download raw bytes

1/**
2 * Standalone VidGen2 composer — completely separate from TrainingVideoMaker /
3 * tv-maker-tools.js state. Own fields, own IDs, own submit. Only wiring it
4 * shares with the rest of the page is being shown/hidden by tv-maker-tools.js
5 * when the "AI Video" tab is active (#vg2Composer visibility only).
6 */
7(function ($) {
8    'use strict';
9
10    var uploadedMedia = []; // [{file, url, type, uploading, previewSrc}]
11    var selectedCharacters = []; // [{avatar_id, name, image_url, voice_id, uploading, previewSrc}]
12    var isPickingCharacter = false;
13    var characterPickerSource = null;
14    var aspectRatio = '9:16';
15    var activeTemplate = null;
16    var AI_VOICE_FALLBACK_ID = 'ai_voice';
17    var CHARACTER_FALLBACK_IMAGE = '/images/v-thumbnail.webp';
18    var inputSelectorByType = {
19        image: '#vg2ImageInput'
20    };
21
22    function csrfToken() {
23        return $('meta[name="csrf-token"]').attr('content') || '';
24    }
25
26    function showError(message) {
27        var $el = $('#vg2ComposerError');
28        var $modalEl = $('#vg2CtaComposerError');
29        if (message) {
30            $el.text(message).show();
31            $modalEl.text(message).show();
32        } else {
33            $el.hide();
34            $modalEl.hide();
35        }
36    }
37
38    /**
39     * INSUFFICIENT_CREDITS carries required/available/is_premium/has_topped_up/
40     * has_payment_method/auto_topup_enabled (see VidGen2Controller::
41     * insufficientCreditsResponse()) so the CTA can point at top-up for
42     * anyone who's already paid for credits before, offer auto-topup when a
43     * card is already on file, or send a free user to upgrade — same
44     * distinction the result page's in-chat credit gate makes.
45     */
46    function showInsufficientCreditsError(resp) {
47        var required = (resp && resp.needed) || 0;
48        var available = (resp && resp.balance) || 0;
49        var hasPaymentMethod = !!(resp && resp.has_payment_method);
50        var autoTopupEnabled = !!(resp && resp.auto_topup_enabled);
51        var offerAutoTopup = hasPaymentMethod && !autoTopupEnabled;
52
53        var html = 'You need ' + required + ' credits for this (you have ' + available + '). ';
54        if (offerAutoTopup) {
55            html += '<button type="button" class="vg2-error-cta" data-vg2-enable-auto-topup="' + required + '">Set up auto top-up</button> ' +
56                '<button type="button" class="vg2-error-cta" data-vg2-buy-credits="' + required + '">Buy credits once</button>';
57        } else if (autoTopupEnabled) {
58            html += 'The auto top-up charge on your card didn\'t go through — check your payment method, or ' +
59                '<button type="button" class="vg2-error-cta" data-vg2-buy-credits="' + required + '">buy credits manually</button>.';
60        } else {
61            html += '<button type="button" class="vg2-error-cta" data-vg2-buy-credits="' + required + '">Buy credits</button>';
62        }
63
64        $('#vg2ComposerError').html(html).show();
65        $('#vg2CtaComposerError').html(html).show();
66    }
67
68    function enableAutoTopup($btn) {
69        if (typeof window.openCreditTopupModal !== 'function') {
70            window.location.href = '/billing/details';
71            return;
72        }
73
74        var requiredCredits = parseInt($btn.attr('data-vg2-enable-auto-topup') || '0', 10) || undefined;
75        window.openCreditTopupModal({
76            defaultCredits: requiredCredits,
77            onAutoTopupSuccess: function () {
78                var successHtml = 'Auto top-up settings saved — try creating your video again.';
79                $('#vg2ComposerError').html(successHtml).show();
80                $('#vg2CtaComposerError').html(successHtml).show();
81            }
82        });
83    }
84
85    function renderMediaPreviews() {
86        var $list = $('#vg2MediaPreviewList');
87        $list.empty();
88        var $modalList = $('#vg2CtaMediaPreviewList');
89        $modalList.empty();
90
91        uploadedMedia.forEach(function (item, idx) {
92            var $item = $('<div class="vg2-media-preview-item"></div>').attr('data-media-type', item.type || 'image');
93            if (item.uploading) $item.addClass('is-uploading');
94
95            $item.append($('<img>').attr('src', item.previewSrc || item.url || ''));
96
97            $item.append($('<span class="vg2-media-preview-type"></span>').text(item.type || 'image'));
98
99            var $remove = $('<button type="button" class="vg2-media-preview-remove" aria-label="Remove media">&times;</button>');
100            $remove.on('click', function () {
101                uploadedMedia.splice(idx, 1);
102                renderMediaPreviews();
103            });
104
105            $item.append($remove);
106            $list.append($item);
107            $modalList.append($item.clone(true, true));
108        });
109
110        $('#vg2MediaPreviewContainer').toggle(uploadedMedia.length > 0);
111        $('#vg2CtaMediaPreviewList').toggle(uploadedMedia.length > 0);
112    }
113
114    function detectMediaType(file) {
115        if (file && file.type && file.type.indexOf('image/') === 0) return 'image';
116        return 'image';
117    }
118
119    function uploadMedia(file) {
120        if (file && file.type && file.type.indexOf('image/') !== 0) {
121            showError('Only reference images are supported for now.');
122            return;
123        }
124
125        var item = {
126            file: file,
127            url: null,
128            type: detectMediaType(file),
129            uploading: true,
130            previewSrc: URL.createObjectURL(file)
131        };
132        uploadedMedia.push(item);
133        renderMediaPreviews();
134
135        var formData = new FormData();
136        formData.append('media_file', file);
137
138        $.ajax({
139            url: '/ai-ads-generator/presentations/upload-image',
140            type: 'POST',
141            data: formData,
142            processData: false,
143            contentType: false,
144            headers: { 'X-CSRF-TOKEN': csrfToken() }
145        }).done(function (resp) {
146            item.uploading = false;
147            if (resp && resp.status === 'success' && resp.media_url) {
148                item.type = resp.media_type || item.type;
149                item.url = resp.media_url;
150            } else {
151                var idx = uploadedMedia.indexOf(item);
152                if (idx !== -1) uploadedMedia.splice(idx, 1);
153                showError('Failed to upload one of the reference files.');
154            }
155            renderMediaPreviews();
156        }).fail(function () {
157            var idx = uploadedMedia.indexOf(item);
158            if (idx !== -1) uploadedMedia.splice(idx, 1);
159            showError('Failed to upload one of the reference files.');
160            renderMediaPreviews();
161        });
162    }
163
164    function characterImage(selection) {
165        return String((selection && (selection.image_url || selection.image || selection.gif || selection.avatar_url || selection.video)) || '').trim();
166    }
167
168    function characterVoiceId(selection) {
169        return String((selection && (selection.voice_id || selection.cloned_voice_id)) || AI_VOICE_FALLBACK_ID).trim();
170    }
171
172    function characterId(selection) {
173        return String((selection && (selection.avatar_id || selection.id || selection.look_id || selection.image_url || selection.image || selection.gif)) || '').trim();
174    }
175
176    /**
177     * Selected characters render as the same small square thumbnail +
178     * corner remove button as uploaded reference images (vg2-media-preview-
179     * item), in their own preview strip above the option buttons — not as
180     * wide pill chips sitting inline in the button row, which read like an
181     * extra tab appearing next to "Character".
182     */
183    function renderCharacters() {
184        var $list = $('#vg2CharacterList');
185        var $container = $('#vg2CharacterPreviewContainer');
186        var $modalList = $('#vg2CtaCharacterList');
187        var $label = $('#vg2CharacterSelectLabel');
188        var $modalLabel = $('#vg2CtaCharacterSelectLabel');
189        $list.empty();
190        $modalList.empty();
191
192        selectedCharacters.forEach(function (character, idx) {
193            var image = character.previewSrc || character.image_url || CHARACTER_FALLBACK_IMAGE;
194            var name = character.name || 'Character';
195            var $item = $('<div class="vg2-media-preview-item vg2-character-preview-item"></div>');
196            if (character.uploading) $item.addClass('is-uploading');
197            $item.append($('<img>').attr({ src: image, alt: name }).on('error', function () {
198                $(this).off('error').attr('src', CHARACTER_FALLBACK_IMAGE);
199            }));
200            $item.append($('<span class="vg2-media-preview-type"></span>').text(name).attr('title', name));
201            $item.append($('<button type="button" class="vg2-media-preview-remove" aria-label="Remove character">&times;</button>').on('click', function () {
202                selectedCharacters.splice(idx, 1);
203                renderCharacters();
204            }));
205            $list.append($item);
206            $modalList.append($item.clone(true, true));
207        });
208
209        if ($container.length) {
210            $container.toggle(selectedCharacters.length > 0);
211        } else {
212            $list.toggle(selectedCharacters.length > 0);
213        }
214        $modalList.toggle(selectedCharacters.length > 0);
215        $label.text(selectedCharacters.length ? 'Add Character' : 'Character');
216        $modalLabel.text(selectedCharacters.length ? 'Add Character' : (String(activeTemplate || '').trim() === 'ugc_ads' ? 'Character Required' : 'Character'));
217        markSelectedAvatarCards();
218    }
219
220    function selectedCharacterIds() {
221        return selectedCharacters.map(function (character) {
222            return String(character.avatar_id || '').trim();
223        }).filter(Boolean);
224    }
225
226    function isCharacterRequiredForTemplateCreate(options) {
227        options = options || {};
228        return String(options.buttonSelector || '').trim() === '#vg2CtaCreateBtn'
229            && String(activeTemplate || '').trim() === 'ugc_ads';
230    }
231
232    function markSelectedAvatarCards() {
233        var ids = selectedCharacterIds();
234        $('#wavelAvatarTooltipGrid .tv-avatar-flat-card, #wavelAvatarTooltipGrid .tv-avatar-group-card, #wavelAvatarTooltipGrid .tv-avatar-look-card').each(function () {
235            var $card = $(this);
236            var avatarId = String($card.attr('data-avatar-id') || $card.attr('data-vg2-character-id') || '').trim();
237            $card.toggleClass('selected', avatarId !== '' && ids.indexOf(avatarId) !== -1);
238        });
239    }
240
241    window.__vg2IsCharacterSelected = function (avatarId) {
242        avatarId = String(avatarId || '').trim();
243        return avatarId !== '' && selectedCharacterIds().indexOf(avatarId) !== -1;
244    };
245
246    function addCharacter(selection) {
247        var id = characterId(selection);
248        var imageUrl = characterImage(selection);
249        var voiceId = characterVoiceId(selection);
250        var name = String((selection && selection.name) || 'Character').trim() || 'Character';
251
252        if (!id || !imageUrl) {
253            showError('This character is missing an avatar image.');
254            return;
255        }
256
257        var exists = selectedCharacters.some(function (item) {
258            return item.avatar_id === id;
259        });
260        if (!exists) {
261            selectedCharacters.push({
262                avatar_id: id,
263                name: name,
264                image_url: imageUrl,
265                voice_id: voiceId
266            });
267        }
268
269        renderCharacters();
270        markSelectedAvatarCards();
271        showError(null);
272        closeCharacterPickerAfterSelection();
273    }
274
275    function closeCharacterPickerAfterSelection() {
276        var source = characterPickerSource;
277
278        isPickingCharacter = false;
279        characterPickerSource = null;
280        window.__vg2CharacterPicking = false;
281        window.__vg2KeepCharacterPickerOpen = false;
282        window.__vg2ForceCloseCharacterPicker = true;
283
284        if (window.TrainingVideoMaker && typeof window.TrainingVideoMaker.hideWavelAvatarTooltip === 'function') {
285            window.TrainingVideoMaker.hideWavelAvatarTooltip();
286        }
287
288        window.__vg2ForceCloseCharacterPicker = false;
289
290        window.setTimeout(function () {
291            var selector = source === 'cta' ? '#vg2CtaCharacterSelectBtn' : '#vg2CharacterSelectBtn';
292            var $target = $(selector);
293            if ($target.length) {
294                $target.trigger('focus');
295            }
296        }, 0);
297    }
298
299    function uploadCharacterImage(file) {
300        if (file && file.type && file.type.indexOf('image/') !== 0) {
301            showError('Only character images are supported.');
302            return;
303        }
304
305        var item = {
306            avatar_id: 'upload_' + Date.now() + '_' + Math.random().toString(36).slice(2),
307            name: file && file.name ? file.name.replace(/\.[^.]+$/, '') : 'Uploaded character',
308            image_url: '',
309            voice_id: AI_VOICE_FALLBACK_ID,
310            uploading: true,
311            previewSrc: URL.createObjectURL(file)
312        };
313        selectedCharacters.push(item);
314        renderCharacters();
315
316        var formData = new FormData();
317        formData.append('media_file', file);
318
319        $.ajax({
320            url: '/ai-ads-generator/presentations/upload-image',
321            type: 'POST',
322            data: formData,
323            processData: false,
324            contentType: false,
325            headers: { 'X-CSRF-TOKEN': csrfToken() }
326        }).done(function (resp) {
327            item.uploading = false;
328            if (resp && resp.status === 'success' && resp.media_url) {
329                item.image_url = resp.media_url;
330            } else {
331                var idx = selectedCharacters.indexOf(item);
332                if (idx !== -1) selectedCharacters.splice(idx, 1);
333                showError('Failed to upload one of the character images.');
334            }
335            renderCharacters();
336        }).fail(function () {
337            var idx = selectedCharacters.indexOf(item);
338            if (idx !== -1) selectedCharacters.splice(idx, 1);
339            showError('Failed to upload one of the character images.');
340            renderCharacters();
341        });
342    }
343
344    function patchAvatarSelectionForCharacters() {
345        if (!window.TrainingVideoMaker || typeof window.TrainingVideoMaker.selectWavelAvatar !== 'function') {
346            window.setTimeout(patchAvatarSelectionForCharacters, 100);
347            return;
348        }
349
350        if (window.TrainingVideoMaker.__vg2CharacterPatchApplied) {
351            return;
352        }
353
354        var originalSelect = window.TrainingVideoMaker.selectWavelAvatar;
355        window.TrainingVideoMaker.selectWavelAvatar = function (selection) {
356            if (isPickingCharacter) {
357                addCharacter(selection);
358                return;
359            }
360
361            return originalSelect.apply(this, arguments);
362        };
363        window.TrainingVideoMaker.__vg2CharacterPatchApplied = true;
364    }
365
366    function setCreateButtonState($btn, loading) {
367        $btn.prop('disabled', loading);
368        if (loading) {
369            $btn.text('Creating video…');
370            return;
371        }
372        $btn.text($btn.data('default-label') || 'Create Video');
373    }
374
375    function canCreateWithoutAuthPrompt() {
376        return window.wavelIsGuest === false ||
377            window.trainingVideoMakerCanCreateScenes === true ||
378            String($('#generateScenesBtn').data('can-create-scenes')) === '1';
379    }
380
381    function promptLoginThenResumeCreate(options) {
382        options = options || {};
383        var resumeOptions = {
384            promptSelector: options.promptSelector || '#vg2PromptInput',
385            buttonSelector: options.buttonSelector || '#vg2CreateBtn'
386        };
387
388        window.wavelPendingPostAuthAction = function () {
389            if (resumeOptions.buttonSelector === '#vg2CtaCreateBtn') {
390                $('#vg2PromptInput').val($('#vg2CtaPromptInput').val()).trigger('input');
391            }
392            handleCreate(resumeOptions);
393        };
394
395        if (typeof window.openWavelAuthModal === 'function') {
396            window.openWavelAuthModal('login');
397        } else {
398            window.location.href = '/ai-video?auth=login';
399        }
400    }
401
402    function handleCreate(options) {
403        options = options || {};
404        var $btn = $(options.buttonSelector || '#vg2CreateBtn');
405        var $promptInput = $(options.promptSelector || '#vg2PromptInput');
406        var prompt = String($promptInput.val() || '').trim();
407
408        if (!prompt) {
409            showError('Please describe the video you want to create.');
410            $promptInput.trigger('focus');
411            return;
412        }
413
414        if (uploadedMedia.some(function (i) { return i.uploading; })) {
415            showError('Please wait for uploads to finish.');
416            return;
417        }
418
419        if (selectedCharacters.some(function (i) { return i.uploading; })) {
420            showError('Please wait for character uploads to finish.');
421            return;
422        }
423
424        if (isCharacterRequiredForTemplateCreate(options) && selectedCharacters.length === 0) {
425            showError('Please select or upload a character before creating this UGC ad.');
426            $('#vg2CtaCharacterSelectBtn').trigger('focus');
427            return;
428        }
429
430        showError(null);
431
432        if (!canCreateWithoutAuthPrompt()) {
433            promptLoginThenResumeCreate(options);
434            return;
435        }
436
437        var inputImages = uploadedMedia.filter(function (i) { return i.type === 'image'; }).map(function (i) { return i.url; }
437).filter(Boolean);
438        var avatarPairings = selectedCharacters.map(function (character) {
439            return {
440                image_url: character.image_url,
441                voice_id: character.voice_id || AI_VOICE_FALLBACK_ID
442            };
443        }).filter(function (pairing) {
444            return pairing.image_url;
445        });
446
447        var payload = {
448            user_prompt: prompt,
449            aspect_ratio: aspectRatio
450        };
451        if (String(options.buttonSelector || '').trim() === '#vg2CtaCreateBtn' && activeTemplate) {
452            payload.template = String(activeTemplate).trim();
453        }
454        if (inputImages.length) payload.input_images = inputImages;
455        if (avatarPairings.length) payload.avatar_pairings = avatarPairings;
456
457        setCreateButtonState($btn, true);
458
459        $.ajax({
460            url: '/ai-ads-generator/vidgen2/create',
461            method: 'POST',
462            contentType: 'application/json',
463            dataType: 'json',
464            headers: { 'X-CSRF-TOKEN': csrfToken() },
465            data: JSON.stringify(payload)
466        }).done(function (resp) {
467            if (resp && resp.status === 'SUCCESS' && resp.clip_id) {
468                window.location.href = '/ai-ads-generator/vidgen2/result/' + encodeURIComponent(resp.clip_id);
469                return;
470            }
471            if (resp && resp.status === 'INSUFFICIENT_CREDITS') {
472                showInsufficientCreditsError(resp);
473                setCreateButtonState($btn, false);
474                return;
475            }
476            showError((resp && resp.message) || 'Failed to create video.');
477            setCreateButtonState($btn, false);
478        }).fail(function (xhr) {
479            var body = xhr && xhr.responseJSON;
480            if (xhr && xhr.status === 401) {
481                setCreateButtonState($btn, false);
482                promptLoginThenResumeCreate(options);
483                return;
484            }
485            if (body && body.status === 'INSUFFICIENT_CREDITS') {
486                showInsufficientCreditsError(body);
487                setCreateButtonState($btn, false);
488                return;
489            }
490            showError((body && body.message) || 'Failed to create video.');
491            setCreateButtonState($btn, false);
492        });
493    }
494
495    function syncAspectControls() {
496        $('#vg2AspectRatioGroup .vg2-aspect-toggle').each(function () {
497            $(this).toggleClass('is-active', String($(this).data('value') || '') === aspectRatio);
498        });
499        $('#vg2CtaAspectDropdownLabel').text(aspectRatio);
500        $('#vg2CtaAspectDropdownMenu .vg2-template-dropdown-item').each(function () {
501            var active = String($(this).data('value') || '') === aspectRatio;
502            $(this).toggleClass('is-active', active).attr('aria-selected', active ? 'true' : 'false');
503        });
504        var isHorizontal = aspectRatio === '16:9';
505        $('#vg2CtaAspectDropdownBtn .vg2-template-dropdown-icon').html(
506            isHorizontal
507                ? '<svg viewBox="0 0 24 24" fill="none"><rect x="3.5" y="6.5" width="17" height="11" rx="2" /></svg>'
508                : '<svg viewBox="0 0 24 24" fill="none"><rect x="7.5" y="3.5" width="9" height="17" rx="2" /></svg>'
509        );
510    }
511
512    function openTemplateModal($card) {
513        var title = String($card.attr('data-vg2-title') || $card.find('span').first().text() || 'AI Video').trim();
514        var accent = String($card.attr('data-vg2-accent') || '').trim();
515        var subtitle = String($card.attr('data-vg2-subtitle') || '').trim();
516        var prompt = String($card.attr('data-vg2-prompt') || '').trim();
517        var preview = String($card.attr('data-vg2-visual') || $card.attr('data-vg2-preview') || $card.find('img').attr('src') || '').trim();
518        var attachmentLabel = String($card.attr('data-vg2-attachment-label') || 'Add image(s)').trim();
519        var detailsLabel = String($card.attr('data-vg2-details-label') || 'What should this video include?').trim();
520        var detailsPlaceholder = String($card.attr('data-vg2-details-placeholder') || 'Describe the video you want to create.').trim();
521        var buttonLabel = String($card.attr('data-vg2-button-label') || 'Create Video').trim();
522
523        activeTemplate = String($card.attr('data-vg2-template') || '').trim();
524        if (accent && title.indexOf(accent) !== -1) {
525            $('#vg2TemplateModalTitle').html(title.replace(accent, '<span>' + accent + '</span>'));
526        } else {
527            $('#vg2TemplateModalTitle').text(title);
528        }
529        $('#vg2TemplateSubtitle').text(subtitle);
530        $('#vg2TemplatePreviewImage').attr({ src: preview, alt: title });
531        $('#vg2TemplateAttachmentLabel').text(attachmentLabel);
532        $('#vg2TemplateUploadTitle').text(attachmentLabel);
533        $('#vg2TemplateDetailsLabel').text(detailsLabel);
534        $('#vg2CtaPromptInput').val(prompt).trigger('input');
535        $('#vg2CtaPromptInput').attr('placeholder', detailsPlaceholder);
536        $('#vg2CtaCreateBtn').data('default-label', buttonLabel).text(buttonLabel);
537        $('.vg2-template-optional').text(activeTemplate === 'ugc_ads' ? 'Required' : 'Optional');
538        $('.tv-vidgen2-template-card').removeClass('is-selected');
539        $card.addClass('is-selected');
540        syncAspectControls();
541        renderMediaPreviews();
542        renderCharacters();
543        showError(null);
544        $('#vg2TemplateModal').attr('aria-hidden', 'false').css('display', 'flex').hide().fadeIn(120);
545        $('body').addClass('vg2-template-modal-open');
546        window.setTimeout(function () {
547            $('#vg2CtaPromptInput').trigger('focus');
548        }, 130);
549    }
550
551    function closeTemplateModal() {
552        $('#vg2TemplateModal').attr('aria-hidden', 'true').fadeOut(100);
553        $('body').removeClass('vg2-template-modal-open');
554        isPickingCharacter = false;
555        characterPickerSource = null;
556        window.__vg2ForceCloseCharacterPicker = true;
557        window.__vg2CharacterPicking = false;
558        window.__vg2KeepCharacterPickerOpen = false;
559        if (window.TrainingVideoMaker && typeof window.TrainingVideoMaker.hideWavelAvatarTooltip === 'function') {
560            window.TrainingVideoMaker.hideWavelAvatarTooltip();
561        }
562    }
563
564    function init() {
565        // Relocate the panel to be a guaranteed sibling of the tab bar itself,
566        // regardless of whatever conditional wrapper it was statically nested
567        // under in the blade template — same pattern this codebase already uses
568        // for floating menus (see tv-maker-tools.js appendTo('body')). Without
569        // this, the panel can end up inside an ancestor that's hidden/toggled
570        // independently of tv-maker-tools.js's #vg2Composer visibility toggle,
571        // which is why the tab appeared to do nothing.
572        var $panel = $('#vg2Composer');
573        var $tabs = $('.tvai-tool-tabs');
574        if ($panel.length && $tabs.length) {
575            $panel.insertAfter($tabs);
576        }
577
578        patchAvatarSelectionForCharacters();
579
580        $(document).on('wavel-avatar-tooltip:closed', function () {
581            isPickingCharacter = false;
582            characterPickerSource = null;
583            window.__vg2CharacterPicking = false;
584            window.__vg2KeepCharacterPickerOpen = false;
585        });
586
587        $(document).on('click', '.vg2-media-upload-control', function (e) {
588            if ($(e.target).is('input[type="file"]')) {
589                return;
590            }
591            e.preventDefault();
592            e.stopPropagation();
593            var type = $(this).data('vg2-upload') || 'image';
594            if ($(this).closest('#vg2TemplateModal').length) {
595                $('#vg2TemplateImageInput').trigger('click');
596                return;
597            }
598            $(inputSelectorByType[type] || '#vg2ImageInput').trigger('click');
599        });
600
601        $(document).on('change', '#vg2ImageInput, #vg2TemplateImageInput', function () {
602            var files = Array.prototype.slice.call(this.files || []);
603            files.forEach(function (file) {
604                uploadMedia(file);
605            });
606            this.value = '';
607        });
608
609        $(document).on('click', '#vg2CtaCharacterUploadBtn', function (e) {
610            e.preventDefault();
611            e.stopPropagation();
612            $('#vg2CtaCharacterImageInput').trigger('click');
613        });
614
615        $(document).on('change', '#vg2CtaCharacterImageInput', function () {
616            var files = Array.prototype.slice.call(this.files || []);
617            files.forEach(function (file) {
618                uploadCharacterImage(file);
619            });
620            this.value = '';
621        });
622
623        $(document).on('dragenter dragover', '.vg2-template-upload-card', function (e) {
624            e.preventDefault();
625            e.stopPropagation();
626            $(this).addClass('is-dragover');
627        });
628
629        $(document).on('dragleave dragend drop', '.vg2-template-upload-card', function (e) {
630            e.preventDefault();
631            e.stopPropagation();
632            $(this).removeClass('is-dragover');
633        });
634
635        $(document).on('drop', '.vg2-template-upload-card', function (e) {
636            var files = Array.prototype.slice.call((e.originalEvent && e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files) || []);
637            files.forEach(function (file) {
638                uploadMedia(file);
639            });
640        });
641
642        $(document).on('click', '#vg2CharacterSelectBtn, #vg2CtaCharacterSelectBtn', function (e) {
643            e.preventDefault();
644            e.stopPropagation();
645            if (!window.TrainingVideoMaker || typeof window.TrainingVideoMaker.showWavelAvatarTooltip !== 'function') {
646                showError('Character picker is not ready yet.');
647                return;
648            }
649            isPickingCharacter = true;
650            characterPickerSource = this.id === 'vg2CtaCharacterSelectBtn' ? 'cta' : 'composer';
651            window.__vg2CharacterPicking = true;
652            window.__vg2KeepCharacterPickerOpen = true;
653            $('#wavel-avatar-tooltip .tip-header-label').text('Select Character');
654            window.TrainingVideoMaker.showWavelAvatarTooltip('#' + this.id);
655        });
656
657        $(document).on('click', '#tvAvatarSelectBtn, #upload-avatar-btn', function () {
658            isPickingCharacter = false;
659            characterPickerSource = null;
660            window.__vg2CharacterPicking = false;
661            window.__vg2KeepCharacterPickerOpen = false;
662        });
663
664        $(document).on('click', '#vg2AspectRatioGroup .vg2-aspect-toggle', function () {
665            aspectRatio = $(this).data('value') || '9:16';
666            syncAspectControls();
667        });
668
669        $(document).on('click', '#vg2CtaAspectDropdownBtn', function (e) {
670            e.preventDefault();
671            e.stopPropagation();
672            var $menu = $('#vg2CtaAspectDropdownMenu');
673            var willOpen = !$menu.is(':visible');
674            $('.vg2-template-dropdown-menu').hide();
675            $menu.toggle(willOpen);
676            $(this).attr('aria-expanded', willOpen ? 'true' : 'false');
677        });
678
679        $(document).on('click', '#vg2CtaAspectDropdownMenu .vg2-template-dropdown-item', function (e) {
680            e.preventDefault();
681            e.stopPropagation();
682            aspectRatio = $(this).data('value') || '9:16';
683            $('#vg2CtaAspectDropdownMenu').hide();
684            $('#vg2CtaAspectDropdownBtn').attr('aria-expanded', 'false');
685            syncAspectControls();
686        });
687
688        $(document).on('click', function (e) {
689            if (!$(e.target).closest('#vg2CtaAspectDropdown').length) {
690                $('#vg2CtaAspectDropdownMenu').hide();
691                $('#vg2CtaAspectDropdownBtn').attr('aria-expanded', 'false');
692            }
693        });
694
695        $(document).on('input', '#vg2PromptInput', function () {
696            $('#vg2CharCount').text(String(this.value || '').length);
697        });
698
699        $(document).on('input', '#vg2CtaPromptInput', function () {
700            $('#vg2CtaCharCount').text(String(this.value || '').length);
701        });
702
703        $(document).on('click', '.tv-vidgen2-template-card', function () {
704            openTemplateModal($(this));
705        });
706
707        // Poster image by default, hover swaps to the matching preview clip
708        // (muted/looping, no controls) — mouseenter/mouseleave are already
709        // non-bubbling in jQuery so this is safe to delegate directly.
710        $(document).on('mouseenter', '.tv-vidgen2-template-card', function () {
711            var video = this.querySelector('.tv-vidgen2-template-card-video');
712            if (!video) return;
713            try { video.currentTime = 0; } catch (e) {}
714            var playPromise = video.play();
715            if (playPromise && typeof playPromise.catch === 'function') {
716                playPromise.catch(function () {});
717            }
718        });
719
720        $(document).on('mouseleave', '.tv-vidgen2-template-card', function () {
721            var video = this.querySelector('.tv-vidgen2-template-card-video');
722            if (video) video.pause();
723        });
724
725        $(document).on('click', '#vg2TemplateModalClose', closeTemplateModal);
726        $(document).on('click', '#vg2TemplateModal', function (e) {
727            if (e.target === this) closeTemplateModal();
728        });
729        $(document).on('keydown', function (e) {
730            if (e.key === 'Escape' && $('#vg2TemplateModal').is(':visible')) {
731                closeTemplateModal();
732            }
733        });
734
735        $(document).on('click', '[data-vg2-enable-auto-topup]', function () {
736            enableAutoTopup($(this));
737        });
738
739        $(document).on('click', '[data-vg2-buy-credits]', function () {
740            var requiredCredits = parseInt($(this).attr('data-vg2-buy-credits'), 10) || undefined;
741
742            if (typeof window.openCreditTopupModal !== 'function') {
743                window.location.href = '/billing/recharge';
744                return;
745            }
746
747            window.openCreditTopupModal({
748                defaultCredits: requiredCredits,
749                onSuccess: function () {
750                    var successHtml = 'Credits added — try creating your video again.';
751                    $('#vg2ComposerError').html(successHtml).show();
752                    $('#vg2CtaComposerError').html(successHtml).show();
753                }
754            });
755        });
756
757        $(document).on('click', '#vg2CreateBtn', function () {
758            handleCreate();
759        });
760        $(document).on('click', '#vg2CtaCreateBtn', function () {
761            $('#vg2PromptInput').val($('#vg2CtaPromptInput').val()).trigger('input');
762            handleCreate({ promptSelector: '#vg2CtaPromptInput', buttonSelector: '#vg2CtaCreateBtn' });
763        });
764
765        syncAspectControls();
766    }
767
768    if (document.readyState === 'loading') {
769        $(document).ready(init);
770    } else {
771        init();
772    }
773})(jQuery);

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.