PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.79
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.79
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / admin / js / ai-page-translator.js

ai-page-translator.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.79, at includes/admin/js/ai-page-translator.js

3,591 lines 148.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, elementor) {
2 'use strict';
3
4 // Check if Elementor and our AI settings exist
5 if (!elementor || !window.KingAddonsAiField) {
6 return;
7 }
8
9 // Translation progress tracking
10 var translationState = {
11 isTranslating: false,
12 totalElements: 0,
13 translatedElements: 0,
14 failedElements: 0,
15 currentElement: null,
16 fromLang: '',
17 toLang: '',
18 isCancelled: false,
19 currentRequests: [], // Store active AJAX requests to cancel them
20 doneElementIds: [], // Elements finished in this run, for resuming later
21 failedElementIds: [],
22 consecutiveFailures: 0,
23 resumedCount: 0,
24 lastErrorMessage: ''
25 };
26
27 // Saved progress lets a run continue after the editor is reloaded.
28 var PROGRESS_STORAGE_PREFIX = 'king_addons_ai_translator_progress_';
29 var PROGRESS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // A week-old run is stale.
30
31 /**
32 * Id of the document currently open in the editor, or 0 when unknown.
33 */
34 function getCurrentDocumentId() {
35 try {
36 var doc = elementor.documents.getCurrent();
37 return doc && doc.id ? parseInt(doc.id, 10) : 0;
38 } catch (e) {
39 return 0;
40 }
41 }
42
43 function getProgressStorageKey(documentId) {
44 return PROGRESS_STORAGE_PREFIX + (documentId || getCurrentDocumentId());
45 }
46
47 /**
48 * Persist where the run got to. Storage can be unavailable (private mode,
49 * blocked site data), and losing resume support must never break a run.
50 */
51 function saveTranslationProgress() {
52 var documentId = getCurrentDocumentId();
53 if (!documentId || !translationState.totalElements) {
54 return;
55 }
56
57 try {
58 window.localStorage.setItem(getProgressStorageKey(documentId), JSON.stringify({
59 v: 1,
60 documentId: documentId,
61 fromLang: translationState.fromLang,
62 toLang: translationState.toLang,
63 total: translationState.totalElements,
64 done: translationState.doneElementIds,
65 failed: translationState.failedElementIds,
66 updatedAt: Date.now()
67 }));
68 } catch (e) {
69 // Ignore - resuming is a convenience, not a requirement.
70 }
71 }
72
73 function clearTranslationProgress() {
74 try {
75 window.localStorage.removeItem(getProgressStorageKey());
76 } catch (e) {
77 // Ignore.
78 }
79 }
80
81 /**
82 * Saved progress for the open document, or null when there is nothing
83 * usable to resume.
84 */
85 function loadTranslationProgress() {
86 var documentId = getCurrentDocumentId();
87 if (!documentId) {
88 return null;
89 }
90
91 var raw;
92 try {
93 raw = window.localStorage.getItem(getProgressStorageKey(documentId));
94 } catch (e) {
95 return null;
96 }
97
98 if (!raw) {
99 return null;
100 }
101
102 var saved;
103 try {
104 saved = JSON.parse(raw);
105 } catch (e) {
106 clearTranslationProgress();
107 return null;
108 }
109
110 var valid = saved
111 && saved.v === 1
112 && saved.documentId === documentId
113 && saved.toLang
114 && Array.isArray(saved.done)
115 && typeof saved.total === 'number';
116
117 if (!valid) {
118 clearTranslationProgress();
119 return null;
120 }
121
122 // Drop stale entries, and finished ones that were never cleaned up.
123 if ((Date.now() - (saved.updatedAt || 0)) > PROGRESS_MAX_AGE_MS || saved.done.length >= saved.total) {
124 clearTranslationProgress();
125 return null;
126 }
127
128 return saved;
129 }
130
131 // Language options
132 var languages = {
133 'en': 'English',
134 'es': 'Spanish (Español)',
135 'fr': 'French (Français)',
136 'de': 'German (Deutsch)',
137 'it': 'Italian (Italiano)',
138 'pt': 'Portuguese (Português)',
139 'ru': 'Russian (Русский)',
140 'ja': 'Japanese (日本語)',
141 'ko': 'Korean (한국어)',
142 'zh': 'Chinese (中文)',
143 'ar': 'Arabic (العربية)',
144 'hi': 'Hindi (हिन्दी)',
145 'nl': 'Dutch (Nederlands)',
146 'pl': 'Polish (Polski)',
147 'tr': 'Turkish (Türkçe)',
148 'uk': 'Ukrainian (Українська)',
149 'cs': 'Czech (Čeština)',
150 'sv': 'Swedish (Svenska)',
151 'no': 'Norwegian (Norsk)',
152 'da': 'Danish (Dansk)',
153 'fi': 'Finnish (Suomi)'
154 };
155
156 /**
157 * Check if premium version is active
158 */
159 function isPremiumActive() {
160 // Check for premium indicators
161 return !!(
162 window.KingAddonsPro ||
163 window.kingAddonsPro ||
164 (window.KingAddonsAiField && window.KingAddonsAiField.is_pro) ||
165 (window.KingAddonsAiField && window.KingAddonsAiField.premium_active) ||
166 document.querySelector('body.king-addons-pro') ||
167 (typeof jQuery !== 'undefined' && jQuery('body').hasClass('king-addons-pro'))
168 );
169 }
170
171 /**
172 * Inject CSS styles for the translator
173 */
174 function injectTranslatorStyles() {
175 if ($('#king-addons-ai-translator-styles').length === 0) {
176 const styles = `
177 <style id="king-addons-ai-translator-styles">
178 /* Design tokens - flat surfaces, one accent, no gradients. */
179 :root {
180 --ka-tr-accent: #5B03FF;
181 --ka-tr-accent-hover: #4A02D6;
182 --ka-tr-accent-soft: rgba(91, 3, 255, 0.08);
183 --ka-tr-ink: #16161a;
184 --ka-tr-ink-muted: #6b7280;
185 --ka-tr-surface: #ffffff;
186 --ka-tr-surface-sunken: #f6f7f9;
187 --ka-tr-border: #e4e6ea;
188 --ka-tr-border-strong: #d3d6db;
189 --ka-tr-success: #10794a;
190 --ka-tr-success-soft: #eefaf3;
191 --ka-tr-success-border: #c2e9d4;
192 --ka-tr-warning: #8a5a00;
193 --ka-tr-warning-soft: #fff8ec;
194 --ka-tr-warning-border: #f3ddb4;
195 --ka-tr-danger: #b3261e;
196 --ka-tr-radius: 12px;
197 --ka-tr-radius-sm: 8px;
198 --ka-tr-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
199 }
200
201 /* Translator Button Styles */
202 /* Desaturated violet at the toolbar's own 4px radius: the
203 saturated fill shimmered against the near-black bar and
204 its 8px corners did not match any neighbouring control. */
205 .king-addons-ai-translator-btn {
206 background: #6C5CE7 !important;
207 border: none !important;
208 color: #fff !important;
209 padding: 8px 14px !important;
210 border-radius: 4px !important;
211 font-size: 12px !important;
212 font-weight: 600 !important;
213 cursor: pointer !important;
214 display: inline-flex !important;
215 align-items: center !important;
216 gap: 6px !important;
217 transition: background-color 0.15s ease !important;
218 margin: 8px !important;
219 position: relative !important;
220 z-index: 10 !important;
221 text-decoration: none !important;
222 outline: none !important;
223 box-shadow: none !important;
224 }
225 .king-addons-ai-translator-btn:hover {
226 background: #5B4BD6 !important;
227 box-shadow: none !important;
228 }
229 .king-addons-ai-translator-btn:focus-visible {
230 outline: 2px solid #8C7DFF !important;
231 outline-offset: 2px !important;
232 }
233 .king-addons-ai-translator-btn img {
234 width: 16px !important;
235 height: 16px !important;
236 flex-shrink: 0 !important;
237 }
238 .king-addons-ai-translator-btn span {
239 white-space: nowrap !important;
240 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
241 }
242
243 /* Location-specific styles */
244
245 /* In panel header */
246 .king-addons-translator-location-panel-header {
247 position: absolute !important;
248 top: 50% !important;
249 right: 16px !important;
250 transform: translateY(-50%) !important;
251 margin: 0 !important;
252 z-index: 1000 !important;
253 }
254 .king-addons-translator-location-panel-header:hover {
255 transform: translateY(-50%) translateY(-1px) !important;
256 }
257
258 /* In header within panel */
259 .king-addons-translator-location-header-in-panel {
260 margin-left: auto !important;
261 margin-right: 8px !important;
262 }
263
264 /* At top of panel */
265 .king-addons-translator-location-panel-top {
266 width: calc(100% - 16px) !important;
267 margin: 8px !important;
268 justify-content: center !important;
269 }
270
271 /* In general elementor panel */
272 .king-addons-translator-location-elementor-panel {
273 margin: 8px !important;
274 align-self: flex-end !important;
275 }
276
277 /* Toolbar button group integration styles */
278 .king-addons-translator-location-left-group,
279 .king-addons-translator-location-toolbar-stack,
280 .king-addons-translator-location-toolbar,
281 .king-addons-translator-location-grid-stack {
282 /* Material UI button styling is handled in the HTML structure */
283 display: inline-flex !important;
284 }
285
286 /* Additional spacing for toolbar button */
287 .king-addons-translator-location-left-group .king-addons-ai-translator-btn,
288 .king-addons-translator-location-toolbar-stack .king-addons-ai-translator-btn,
289 .king-addons-translator-location-grid-stack .king-addons-ai-translator-btn {
290 margin-left: 8px !important;
291 }
292
293 /* Compact popup styles */
294 .king-addons-translator-popup.compact .king-addons-translator-progress-text {
295 font-size: 14px;
296 margin-bottom: 8px;
297 }
298
299 .king-addons-translator-popup.compact .king-addons-translator-current-element {
300 font-size: 12px;
301 margin-top: 8px;
302 color: #666;
303 overflow: hidden;
304 text-overflow: ellipsis;
305 white-space: nowrap;
306 }
307
308 .king-addons-translator-popup.compact .king-addons-translator-stats {
309 margin: 12px 0;
310 }
311
312 .king-addons-translator-popup.compact .king-addons-translator-stat {
313 margin: 0 8px;
314 }
315
316 .king-addons-translator-popup.compact .king-addons-translator-stat-number {
317 font-size: 18px;
318 }
319
320 .king-addons-translator-popup.compact .king-addons-translator-stat-label {
321 font-size: 11px;
322 }
323
324 /* Compact mode adjustments for custom fields */
325 .king-addons-translator-popup.compact .king-addons-prompt-examples {
326 padding: 6px;
327 margin-top: 4px;
328 }
329
330 .king-addons-translator-popup.compact .king-addons-prompt-examples small {
331 font-size: 10px;
332 }
333
334 .king-addons-translator-popup.compact .king-addons-pro-info {
335 font-size: 11px;
336 margin-top: 8px;
337 padding: 6px 8px;
338 background: #f8f9fa;
339 border-radius: 4px;
340 border-left: 3px solid #5B03FF;
341 }
342
343 /* Element highlighting styles moved to preview iframe */
344
345 /* Ensure button appears properly in all panel locations */
346 #elementor-panel .king-addons-ai-translator-btn,
347 .elementor-panel .king-addons-ai-translator-btn {
348 max-width: 200px !important;
349 overflow: hidden !important;
350 }
351
352 /* Responsive behavior */
353 @media (max-width: 600px) {
354 /* Hide text in panel buttons on small screens */
355 .king-addons-ai-translator-btn span {
356 display: none !important;
357 }
358 .king-addons-ai-translator-btn {
359 padding: 8px !important;
360 min-width: 32px !important;
361 }
362 }
363
364 /* Popup Overlay */
365 .king-addons-translator-overlay {
366 position: fixed;
367 top: 0;
368 left: 0;
369 right: 0;
370 bottom: 0;
371 background: rgba(16, 16, 20, 0.55);
372 z-index: 999999;
373 display: flex;
374 align-items: center;
375 justify-content: center;
376 transition: opacity 0.3s ease;
377 }
378
379 .king-addons-translator-overlay.hiding {
380 opacity: 0;
381 pointer-events: none;
382 }
383
384 /* Popup Container */
385 .king-addons-translator-popup {
386 --ka-tr-pad: 28px;
387 background: var(--ka-tr-surface);
388 padding: var(--ka-tr-pad);
389 border-radius: var(--ka-tr-radius);
390 box-shadow: 0 1px 2px rgba(16,16,20,0.06), 0 12px 32px rgba(16,16,20,0.16);
391 width: 90%;
392 max-width: 480px;
393 max-height: 82vh;
394 overflow-y: auto;
395 transition: all 0.3s ease;
396 transform: scale(1);
397 font-family: var(--ka-tr-font);
398 color: var(--ka-tr-ink);
399 line-height: 1.5;
400 }
401
402 /* Compact popup for top-right positioning */
403 .king-addons-translator-popup.compact {
404 --ka-tr-pad: 16px;
405 position: fixed;
406 top: 80px;
407 right: 20px;
408 width: 350px;
409 max-width: 350px;
410 padding: var(--ka-tr-pad);
411 z-index: 999999;
412 max-height: 400px;
413 transform: scale(1);
414 box-shadow: 0 8px 32px rgba(0,0,0,0.4);
415 }
416
417 /* Compact popup header */
418 .king-addons-translator-popup.compact h3 {
419 font-size: 16px;
420 margin: 0 0 12px 0;
421 display: flex;
422 justify-content: space-between;
423 align-items: center;
424 }
425
426 /* Close button for compact popup */
427 .king-addons-translator-close-btn {
428 background: none;
429 border: none;
430 font-size: 18px;
431 cursor: pointer;
432 color: #999;
433 width: 24px;
434 height: 24px;
435 display: flex;
436 align-items: center;
437 justify-content: center;
438 border-radius: 3px;
439 }
440
441 .king-addons-translator-close-btn:hover {
442 background: #f0f0f0;
443 color: #333;
444 }
445
446 /* Animation states */
447 .king-addons-translator-popup.moving {
448 transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
449 }
450
451 /* Notification banner animation */
452 @keyframes slideDown {
453 0% {
454 transform: translateY(-100%);
455 opacity: 0;
456 }
457 100% {
458 transform: translateY(0);
459 opacity: 1;
460 }
461 }
462
463 /* Pulse animation for success numbers */
464 @keyframes pulse {
465 0% {
466 transform: scale(1);
467 opacity: 1;
468 }
469 50% {
470 transform: scale(1.1);
471 opacity: 0.8;
472 }
473 100% {
474 transform: scale(1);
475 opacity: 1;
476 }
477 }
478
479 .king-addons-translator-popup h3 {
480 margin: 0 0 6px 0;
481 font-size: 18px;
482 font-weight: 650;
483 letter-spacing: -0.01em;
484 color: var(--ka-tr-ink);
485 display: flex;
486 align-items: center;
487 gap: 10px;
488 }
489
490 .king-addons-translator-form {
491 display: flex;
492 flex-direction: column;
493 gap: 16px;
494 }
495
496 .king-addons-translator-field {
497 display: flex;
498 flex-direction: column;
499 gap: 6px;
500 }
501
502 .king-addons-translator-field label {
503 font-weight: 600;
504 color: var(--ka-tr-ink);
505 font-size: 13px;
506 }
507
508 .king-addons-translator-field select {
509 padding: 10px 12px;
510 border: 1px solid var(--ka-tr-border-strong);
511 border-radius: var(--ka-tr-radius-sm);
512 font-size: 14px;
513 height: auto;
514 background: var(--ka-tr-surface);
515 color: var(--ka-tr-ink);
516 }
517
518 .king-addons-translator-field select:focus {
519 border-color: #5B03FF;
520 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
521 outline: none;
522 }
523
524 .king-addons-translator-field input[type="text"] {
525 padding: 10px 12px;
526 border: 1px solid var(--ka-tr-border-strong);
527 border-radius: var(--ka-tr-radius-sm);
528 font-size: 14px;
529 margin-top: 6px;
530 transition: border-color 0.3s ease, box-shadow 0.3s ease;
531 }
532
533 .king-addons-translator-field input[type="text"]:focus {
534 border-color: #5B03FF;
535 box-shadow: 0 0 0 1px rgba(91,3,255,0.3);
536 outline: none;
537 }
538
539 .king-addons-custom-language-field {
540 margin-top: 8px;
541 display: none;
542 animation: slideDown 0.3s ease-out;
543 }
544
545 .king-addons-custom-language-field.show {
546 display: block;
547 }
548
549 .king-addons-custom-language-field input {
550 width: 100%;
551 box-sizing: border-box;
552 }
553
554 .king-addons-custom-language-field label {
555 font-size: 13px;
556 color: #666;
557 margin-bottom: 4px;
558 display: block;
559 }
560
561 .king-addons-pro-badge {
562 background: #f5b301;
563 color: #3a2c00;
564 font-size: 10px;
565 font-weight: bold;
566 padding: 2px 6px;
567 border-radius: 3px;
568 margin-left: 6px;
569 vertical-align: middle;
570 }
571
572 /* Style for disabled custom option when not premium */
573 .king-addons-translator-field select option[value="custom"]:disabled {
574 color: #999;
575 background-color: #f5f5f5;
576 }
577
578 /* Enhanced styling for custom language fields */
579 .king-addons-custom-language-field.show input:focus {
580 border-color: #5B03FF;
581 box-shadow: 0 0 0 2px rgba(91,3,255,0.1);
582 }
583
584 /* Info text for premium features */
585 .king-addons-pro-info {
586 font-size: 12px;
587 color: var(--ka-tr-ink-muted);
588 margin-top: 4px;
589 line-height: 1.5;
590 background: var(--ka-tr-surface-sunken);
591 border: 1px solid var(--ka-tr-border);
592 border-radius: var(--ka-tr-radius-sm);
593 padding: 12px 14px;
594 }
595
596 .king-addons-pro-info a {
597 color: #5B03FF;
598 text-decoration: none;
599 font-weight: 500;
600 }
601
602 .king-addons-pro-info a:hover {
603 color: #4f00e6;
604 text-decoration: underline;
605 }
606
607 /* Prompt examples styling */
608 .king-addons-prompt-examples {
609 margin-top: 6px;
610 padding: 10px 12px;
611 background: var(--ka-tr-surface-sunken);
612 border: 1px solid var(--ka-tr-border);
613 border-radius: var(--ka-tr-radius-sm);
614 }
615
616 .king-addons-prompt-examples small {
617 color: #666;
618 font-size: 11px;
619 line-height: 1.4;
620 display: block;
621 }
622
623 @keyframes slideDown {
624 from {
625 opacity: 0;
626 max-height: 0;
627 transform: translateY(-10px);
628 }
629 to {
630 opacity: 1;
631 max-height: 100px;
632 transform: translateY(0);
633 }
634 }
635
636 /* Loading spinner animation */
637 @keyframes rotate {
638 from {
639 transform: rotate(0deg);
640 }
641 to {
642 transform: rotate(360deg);
643 }
644 }
645
646 /* Error popup specific styles */
647 .king-addons-translator-popup .king-addons-error-icon {
648 width: 60px;
649 height: 60px;
650 background: #f44336;
651 border-radius: 50%;
652 margin: 0 auto 16px;
653 display: flex;
654 align-items: center;
655 justify-content: center;
656 animation: errorPulse 2s ease-in-out infinite;
657 }
658
659 @keyframes errorPulse {
660 0%, 100% {
661 transform: scale(1);
662 box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4);
663 }
664 50% {
665 transform: scale(1.05);
666 box-shadow: 0 0 0 8px rgba(244, 67, 54, 0.1);
667 }
668 }
669
670 .king-addons-translator-actions {
671 display: flex;
672 gap: 12px;
673 position: sticky;
674 bottom: calc(var(--ka-tr-pad) * -1);
675 margin: 8px calc(var(--ka-tr-pad) * -1) calc(var(--ka-tr-pad) * -1);
676 padding: 14px var(--ka-tr-pad) var(--ka-tr-pad);
677 background: var(--ka-tr-surface);
678 border-top: 1px solid var(--ka-tr-border);
679 }
680
681 .king-addons-translator-btn-primary,
682 .king-addons-translator-btn-secondary {
683 padding: 11px 20px;
684 border-radius: var(--ka-tr-radius-sm);
685 font-size: 14px;
686 font-weight: 600;
687 font-family: inherit;
688 line-height: 1.2;
689 cursor: pointer;
690 flex: 1;
691 transition: background-color 0.15s ease, border-color 0.15s ease;
692 }
693
694 .king-addons-translator-btn-primary {
695 background: var(--ka-tr-accent);
696 border: 1px solid var(--ka-tr-accent);
697 color: #fff;
698 }
699
700 .king-addons-translator-btn-primary:hover {
701 background: var(--ka-tr-accent-hover);
702 border-color: var(--ka-tr-accent-hover);
703 color: #fff;
704 }
705
706 .king-addons-translator-btn-primary:disabled {
707 background: var(--ka-tr-border-strong);
708 border-color: var(--ka-tr-border-strong);
709 color: #fff;
710 cursor: not-allowed;
711 }
712
713 .king-addons-translator-btn-secondary {
714 background: var(--ka-tr-surface);
715 border: 1px solid var(--ka-tr-border-strong);
716 color: var(--ka-tr-ink);
717 }
718
719 .king-addons-translator-btn-secondary:hover {
720 background: var(--ka-tr-surface-sunken);
721 }
722
723 .king-addons-translator-btn-primary:focus-visible,
724 .king-addons-translator-btn-secondary:focus-visible {
725 outline: 2px solid var(--ka-tr-accent);
726 outline-offset: 2px;
727 }
728
729 /* Progress Styles */
730 .king-addons-translator-progress {
731 margin-top: 16px;
732 padding: 16px;
733 background: var(--ka-tr-surface-sunken);
734 border: 1px solid var(--ka-tr-border);
735 border-radius: var(--ka-tr-radius-sm);
736 }
737
738 .king-addons-translator-progress-text {
739 font-size: 14px;
740 color: #555;
741 margin-bottom: 8px;
742 }
743
744 .king-addons-translator-progress-bar {
745 width: 100%;
746 height: 6px;
747 background: var(--ka-tr-border);
748 border-radius: 999px;
749 overflow: hidden;
750 margin-bottom: 8px;
751 }
752
753 .king-addons-translator-progress-fill {
754 height: 100%;
755 background: var(--ka-tr-accent);
756 width: 0%;
757 transition: width 0.3s ease;
758 }
759
760 .king-addons-translator-current-element {
761 font-size: 12px;
762 color: var(--ka-tr-ink-muted);
763 }
764
765 .ka-tr-activity {
766 display: flex;
767 align-items: center;
768 gap: 8px;
769 min-height: 18px;
770 }
771
772 .ka-tr-spinner {
773 flex: 0 0 13px;
774 width: 13px;
775 height: 13px;
776 border: 2px solid var(--ka-tr-border);
777 border-top-color: var(--ka-tr-accent);
778 border-radius: 50%;
779 animation: rotate 0.7s linear infinite;
780 }
781
782 /* Respect a reduced-motion preference rather than spinning regardless. */
783 @media (prefers-reduced-motion: reduce) {
784 .ka-tr-spinner {
785 animation-duration: 2.4s;
786 }
787 }
788
789 .ka-tr-snippet {
790 margin-top: 8px;
791 padding: 8px 10px;
792 background: var(--ka-tr-surface);
793 border: 1px solid var(--ka-tr-border);
794 border-radius: var(--ka-tr-radius-sm);
795 font-size: 12px;
796 line-height: 1.45;
797 color: var(--ka-tr-ink-muted);
798 display: -webkit-box;
799 -webkit-line-clamp: 2;
800 -webkit-box-orient: vertical;
801 overflow: hidden;
802 }
803
804 .king-addons-translator-progress-note {
805 display: none;
806 margin-top: 10px;
807 padding: 10px 12px;
808 background: var(--ka-tr-warning-soft);
809 border: 1px solid var(--ka-tr-warning-border);
810 border-radius: var(--ka-tr-radius-sm);
811 color: var(--ka-tr-warning);
812 font-size: 12px;
813 line-height: 1.5;
814 }
815
816 /* Stats Styles */
817 .king-addons-translator-stats {
818 margin-top: 16px;
819 display: grid;
820 grid-template-columns: repeat(3, 1fr);
821 gap: 12px;
822 }
823
824 /* Shared dialog building blocks */
825 .ka-tr-dialog-head {
826 margin-bottom: 20px;
827 }
828
829 .ka-tr-dialog-head h3 {
830 margin: 0 0 6px 0;
831 }
832
833 .ka-tr-dialog-sub {
834 margin: 0;
835 font-size: 13px;
836 color: var(--ka-tr-ink-muted);
837 }
838
839 /* Says whose feature this is - inside Elementor's editor the
840 dialog otherwise reads as one of Elementor's own. */
841 .ka-tr-byline {
842 margin: -2px 0 12px;
843 font-size: 11px;
844 font-weight: 700;
845 letter-spacing: .08em;
846 text-transform: uppercase;
847 color: var(--ka-tr-accent);
848 }
849
850 .ka-tr-panel {
851 background: var(--ka-tr-surface-sunken);
852 border: 1px solid var(--ka-tr-border);
853 border-radius: var(--ka-tr-radius-sm);
854 padding: 16px;
855 margin-bottom: 12px;
856 }
857
858 .ka-tr-panel--accent {
859 background: var(--ka-tr-accent-soft);
860 border-color: rgba(91, 3, 255, 0.18);
861 }
862
863 .ka-tr-panel--warning {
864 background: var(--ka-tr-warning-soft);
865 border-color: var(--ka-tr-warning-border);
866 color: var(--ka-tr-warning);
867 }
868
869 .ka-tr-panel h4 {
870 margin: 0 0 10px 0;
871 font-size: 13px;
872 font-weight: 650;
873 color: var(--ka-tr-ink);
874 text-transform: uppercase;
875 letter-spacing: 0.04em;
876 }
877
878 .ka-tr-panel p {
879 margin: 0 0 12px 0;
880 font-size: 13px;
881 color: var(--ka-tr-ink-muted);
882 }
883
884 .ka-tr-panel p:last-child {
885 margin-bottom: 0;
886 }
887
888 .ka-tr-steps {
889 list-style: none;
890 counter-reset: ka-tr-step;
891 margin: 0;
892 padding: 0;
893 }
894
895 .ka-tr-steps li {
896 counter-increment: ka-tr-step;
897 position: relative;
898 padding-left: 28px;
899 margin: 0 0 10px 0;
900 font-size: 13px;
901 color: var(--ka-tr-ink);
902 line-height: 1.5;
903 }
904
905 .ka-tr-steps li:last-child {
906 margin-bottom: 0;
907 }
908
909 .ka-tr-steps li::before {
910 content: counter(ka-tr-step);
911 position: absolute;
912 left: 0;
913 top: 0;
914 width: 20px;
915 height: 20px;
916 border-radius: 50%;
917 background: var(--ka-tr-accent);
918 color: #fff;
919 font-size: 11px;
920 font-weight: 650;
921 display: flex;
922 align-items: center;
923 justify-content: center;
924 }
925
926 .ka-tr-steps a,
927 .ka-tr-panel a {
928 color: var(--ka-tr-accent);
929 font-weight: 600;
930 text-decoration: none;
931 }
932
933 .ka-tr-steps a:hover,
934 .ka-tr-panel a:hover {
935 text-decoration: underline;
936 }
937
938 .ka-tr-rows {
939 display: grid;
940 gap: 8px;
941 }
942
943 .ka-tr-row {
944 display: flex;
945 justify-content: space-between;
946 gap: 12px;
947 font-size: 13px;
948 }
949
950 .ka-tr-row span {
951 color: var(--ka-tr-ink-muted);
952 }
953
954 .ka-tr-row strong {
955 color: var(--ka-tr-ink);
956 font-weight: 600;
957 }
958
959 .ka-tr-detail {
960 font-size: 12px;
961 color: var(--ka-tr-ink-muted);
962 line-height: 1.5;
963 word-break: break-word;
964 max-height: 120px;
965 overflow-y: auto;
966 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
967 }
968
969 .king-addons-translator-stat {
970 text-align: center;
971 padding: 14px 12px;
972 background: var(--ka-tr-surface-sunken);
973 border: 1px solid var(--ka-tr-border);
974 border-radius: var(--ka-tr-radius-sm);
975 }
976
977 .king-addons-translator-stat-number {
978 font-size: 26px;
979 font-weight: 650;
980 line-height: 1.1;
981 letter-spacing: -0.02em;
982 color: var(--ka-tr-ink);
983 }
984
985 .king-addons-translator-stat-label {
986 margin-top: 4px;
987 font-size: 11px;
988 font-weight: 600;
989 text-transform: uppercase;
990 letter-spacing: 0.05em;
991 color: var(--ka-tr-ink-muted);
992 }
993
994 .king-addons-translator-stat-number {
995 font-size: 20px;
996 font-weight: bold;
997 color: #5B03FF;
998 }
999
1000 .king-addons-translator-stat-label {
1001 font-size: 12px;
1002 color: #777;
1003 margin-top: 4px;
1004 }
1005
1006 /* Element animations handled in preview iframe */
1007 </style>
1008 `;
1009 $('head').append(styles);
1010 }
1011 }
1012
1013 /**
1014 * Add translator button to Elementor panel
1015 */
1016 function addTranslatorButton() {
1017 // Check if button already exists
1018 if (document.querySelector('.king-addons-ai-translator-btn')) {
1019 return;
1020 }
1021
1022 // Strategy 1: Try to find the left button group in the top toolbar (after initial buttons)
1023 var $leftButtonGroup = $('#elementor-editor-wrapper-v2 .MuiStack-root.eui-1g5sxhh:first');
1024 if ($leftButtonGroup.length) {
1025 return addButtonToElement($leftButtonGroup, 'left-group');
1026 }
1027
1028 // Strategy 2: Try to find the first stack group in the toolbar
1029 var $toolbarStack = $('#elementor-editor-wrapper-v2 .MuiStack-root');
1030 if ($toolbarStack.length) {
1031 return addButtonToElement($toolbarStack.first(), 'toolbar-stack');
1032 }
1033
1034 // Strategy 2.5: Try to find grid container with stacks
1035 var $gridContainer = $('#elementor-editor-wrapper-v2 .MuiGrid-container:first');
1036 if ($gridContainer.length) {
1037 var $firstStack = $gridContainer.find('.MuiStack-root:first');
1038 if ($firstStack.length) {
1039 return addButtonToElement($firstStack, 'grid-stack');
1040 }
1041 }
1042
1043 // Strategy 3: Try to find the toolbar itself
1044 var $toolbar = $('#elementor-editor-wrapper-v2 .MuiToolbar-root');
1045 if ($toolbar.length) {
1046 return addButtonToElement($toolbar, 'toolbar');
1047 }
1048
1049 // Strategy 4: Try to find the Elementor panel header (fallback)
1050 var $panelHeader = $('#elementor-panel-header');
1051 if ($panelHeader.length) {
1052 return addButtonToElement($panelHeader, 'panel-header');
1053 }
1054
1055 // Strategy 5: Try to find the main panel (further fallback)
1056 var $panel = $('#elementor-panel');
1057 if ($panel.length) {
1058 return addButtonToElement($panel, 'panel-fallback');
1059 }
1060 return false;
1061 }
1062
1063 /**
1064 * Helper function to add button to specific element
1065 */
1066 function addButtonToElement($target, location) {
1067 // Try using the custom icon, fallback to the standard AI icon
1068 var iconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
1069 var fallbackIconUrl = KingAddonsAiField.plugin_url + 'includes/admin/img/ai.svg';
1070
1071 var $translatorBtn;
1072
1073 // Create button with appropriate styling based on location
1074 if (location === 'left-group' || location === 'toolbar-stack' || location === 'toolbar' || location === 'grid-stack') {
1075 // Material UI style button for toolbar with text and custom icon
1076 $translatorBtn = $('<span class="MuiBox-root eui-0">' +
1077 '<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textInherit MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorInherit king-addons-ai-translator-btn eui-17yw4pm" ' +
1078 'tabindex="0" type="button" aria-label="AI Page Translate & Transform" title="AI Page Translate & Transform">' +
1079 '<span class="MuiButton-startIcon MuiButton-iconSizeSmall" style="margin-right: 4px;">' +
1080 '<img src="' + iconUrl + '" alt="AI" onerror="this.src=\'' + fallbackIconUrl + '\'" style="width: 20px; height: 20px;" />' +
1081 '</span>' +
1082 '<span class="MuiStack-root" style="color: white;">AI Page Translate &amp; Transform</span>' +
1083 '</button>' +
1084 '</span>');
1085 } else {
1086 // Original button style for panel locations
1087 $translatorBtn = $('<button class="king-addons-ai-translator-btn" title="AI Page Translate & Transform">' +
1088 '<img src="' + iconUrl + '" alt="" onerror="this.src=\'' + fallbackIconUrl + '\'"/>' +
1089 '<span>AI Page Translate &amp; Transform</span>' +
1090 '</button>');
1091 }
1092
1093 // Add location-specific class for different styling if needed
1094 $translatorBtn.addClass('king-addons-translator-location-' + location);
1095
1096 // Add to the target element based on location
1097 if (location === 'panel-top' || location === 'panel-fallback') {
1098 $target.prepend($translatorBtn);
1099 } else if (location === 'left-group' || location === 'toolbar-stack' || location === 'grid-stack') {
1100 // Add after existing buttons in the left group
1101 $target.append($translatorBtn);
1102 } else {
1103 $target.append($translatorBtn);
1104 }
1105
1106 // Bind click event (works for both button structures)
1107 $translatorBtn.find('button').length ?
1108 $translatorBtn.find('button').on('click', handleButtonClick) :
1109 $translatorBtn.on('click', handleButtonClick);
1110
1111 function handleButtonClick(e) {
1112 e.preventDefault();
1113 // Check if button is disabled or translation is in progress
1114 if (translationState.isTranslating || $(e.currentTarget).prop('disabled')) {
1115 return;
1116 }
1117 showTranslatorPopup();
1118 }
1119
1120 // Add debug info to button
1121 var $btn = $translatorBtn.find('button').length ? $translatorBtn.find('button') : $translatorBtn;
1122 $btn.attr('data-location', location);
1123 $btn.attr('data-target', $target.prop('tagName') + ($target.attr('id') ? '#' + $target.attr('id') : '') + ($target.attr('class') ? '.' + $target.attr('class').split(' ').join('.') : ''));
1124
1125 return true;
1126 }
1127
1128 /**
1129 * Show the translator popup
1130 */
1131 function showTranslatorPopup() {
1132 // Check API key first
1133 checkApiKeyAndShowPopup();
1134 }
1135
1136 /**
1137 * Check API key before showing popup
1138 */
1139 function checkApiKeyAndShowPopup() {
1140 // Show loading state briefly
1141 var $loadingOverlay = showLoadingOverlay();
1142
1143 $.post(KingAddonsAiField.ajax_url, {
1144 action: 'king_addons_ai_check_tokens',
1145 nonce: KingAddonsAiField.generate_nonce
1146 }, function(response) {
1147 $loadingOverlay.remove();
1148
1149 if (!response.success) {
1150 if (response.data && response.data.message) {
1151 var errorMessage = response.data.message;
1152
1153 // Check for token limit errors first
1154 if (errorMessage.toLowerCase().includes('token limit') ||
1155 errorMessage.toLowerCase().includes('daily limit') ||
1156 errorMessage.toLowerCase().includes('limit reached') ||
1157 errorMessage.toLowerCase().includes('quota exceeded') ||
1158 errorMessage.toLowerCase().includes('rate limit')) {
1159
1160 showTokenLimitError(errorMessage);
1161 return;
1162 }
1163
1164 showApiKeyError('API Error', errorMessage);
1165 } else {
1166 showApiKeyError('Connection Issue', 'Unable to connect right now. Please check your internet connection and try again.');
1167 }
1168 return;
1169 }
1170
1171 if (!response.data.api_key_valid) {
1172 var errorMessage = response.data.error_message || 'API key is missing or invalid';
1173
1174 // Check for token limit errors first
1175 if (errorMessage.toLowerCase().includes('token limit') ||
1176 errorMessage.toLowerCase().includes('daily limit') ||
1177 errorMessage.toLowerCase().includes('limit reached') ||
1178 errorMessage.toLowerCase().includes('quota exceeded') ||
1179 errorMessage.toLowerCase().includes('rate limit') ||
1180 errorMessage.toLowerCase().includes('too many requests')) {
1181
1182 showTokenLimitError(errorMessage);
1183 return;
1184 }
1185
1186 showApiKeyError('API Key Required', errorMessage);
1187 return;
1188 }
1189
1190 var saved = loadTranslationProgress();
1191 if (saved) {
1192 showResumePopup(saved);
1193 return;
1194 }
1195
1196 createAndShowPopup();
1197 }).fail(function(xhr) {
1198 $loadingOverlay.remove();
1199
1200 if (xhr.status === 0) {
1201 showApiKeyError('Connection Issue', 'Network connection failed. Please check your internet connection and try again.');
1202 } else {
1203 showApiKeyError('Temporary Issue', 'Server is temporarily unavailable. Please try again in a few minutes.');
1204 }
1205 });
1206 }
1207
1208 /**
1209 * Show loading overlay
1210 */
1211 function showLoadingOverlay() {
1212 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1213 var $popup = $('<div class="king-addons-translator-popup" style="text-align: center; padding: 40px;"></div>');
1214
1215 var loadingHtml = `
1216 <div style="margin-bottom: 16px;">
1217 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:40px;height:40px;filter: invert(1); animation: rotate 1s linear infinite;"/>
1218 </div>
1219 <div style="font-size: 16px; color: #333; margin-bottom: 8px;">🔍 Verifying Setup...</div>
1220 <div style="font-size: 12px; color: #666;">Just checking that everything is ready for translation</div>
1221 `;
1222
1223 $popup.html(loadingHtml);
1224 $overlay.append($popup);
1225 $('body').append($overlay);
1226
1227 return $overlay;
1228 }
1229
1230 /**
1231 * Show API key error with detailed information
1232 */
1233 function showApiKeyError(title, message) {
1234 // Aggressively remove any existing popups/overlays
1235 $('.king-addons-translator-overlay').remove();
1236 $('.king-addons-translator-popup').remove();
1237
1238 // Wait a bit to ensure cleanup is complete
1239 setTimeout(function() {
1240 showApiKeyErrorDelayed(title, message);
1241 }, 100);
1242 }
1243
1244 function showApiKeyErrorDelayed(title, message) {
1245 var cfg = window.KingAddonsAiField || {};
1246 var settingsUrl = cfg.settings_url || '/wp-admin/admin.php?page=king-addons-ai-settings';
1247
1248 // The setup steps name whichever AI provider is configured.
1249 var keysUrl = cfg.api_keys_url || 'https://platform.openai.com/api-keys';
1250 var keysLabel = cfg.api_keys_label || 'OpenAI Platform';
1251 var billingNote = cfg.setup_billing_note || 'and top up your OpenAI account balance by at least $5';
1252 var costNote = cfg.setup_cost_note || 'Processing a page costs pennies (about $0.01 per full page).';
1253
1254 function esc(value) {
1255 return $('<div></div>').text(String(value == null ? '' : value)).html();
1256 }
1257
1258 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1259 var $popup = $('<div class="king-addons-translator-popup"></div>');
1260
1261 var errorHtml = `
1262 <div class="ka-tr-dialog-head">
1263 <h3>${esc(title || 'AI Page Translate & Transform')}</h3>
1264 <div class="ka-tr-byline">by King Addons</div>
1265 <p class="ka-tr-dialog-sub">Connect an AI provider once and the feature is ready to use.</p>
1266 </div>
1267
1268 <div class="ka-tr-panel">
1269 <h4>What you need to do</h4>
1270 <ol class="ka-tr-steps">
1271 <li>Get an API key from <a href="${esc(keysUrl)}" target="_blank" rel="noopener noreferrer">${esc(keysLabel)}</a> ${esc(billingNote)}</li>
1272 <li>Paste it into AI Settings</li>
1273 <li>Come back here and translate the page</li>
1274 </ol>
1275 </div>
1276
1277 ${message ? `<div class="ka-tr-panel"><h4>Details</h4><div class="ka-tr-detail">${esc(message)}</div></div>` : ''}
1278
1279 <div class="ka-tr-panel">
1280 <h4>What it costs</h4>
1281 <p>${esc(costNote)}</p>
1282 </div>
1283
1284 <div class="king-addons-translator-actions">
1285 <button class="king-addons-translator-btn-secondary" id="king-addons-error-close">Not now</button>
1286 <a href="${esc(settingsUrl)}" class="king-addons-translator-btn-primary" style="text-decoration: none; display: flex; align-items: center; justify-content: center;">Go to AI Settings</a>
1287 </div>
1288 `;
1289
1290 $popup.html(errorHtml);
1291 $overlay.append($popup);
1292 $('body').append($overlay);
1293
1294 // Bind close event
1295 $('#king-addons-error-close').on('click', function() {
1296 $overlay.remove();
1297 });
1298
1299 // Close on overlay click
1300 $overlay.on('click', function(e) {
1301 if (e.target === $overlay[0]) {
1302 $overlay.remove();
1303 }
1304 });
1305 }
1306
1307 /**
1308 * Show token limit error popup
1309 */
1310 function showTokenLimitError(message, errorCode) {
1311 // Remove any existing popups first
1312 $('.king-addons-translator-overlay').remove();
1313 $('.king-addons-translator-popup').remove();
1314
1315 // Wait a bit to ensure cleanup is complete
1316 setTimeout(function() {
1317 showTokenLimitErrorDelayed(message, errorCode);
1318 }, 100);
1319 }
1320
1321 /**
1322 * A run can be stopped by four different limits, and they need four
1323 * different answers: the plugin's own token cap, the provider's short-term
1324 * throttling, a per-model daily cap, and an empty account balance. Showing
1325 * "increase your Daily Token Limit" for all of them sends people to a
1326 * setting that has nothing to do with the failure.
1327 */
1328 function showTokenLimitErrorDelayed(message, errorCode) {
1329 var cfg = window.KingAddonsAiField || {};
1330 var settingsUrl = cfg.settings_url || '/wp-admin/admin.php?page=king-addons-ai-settings';
1331 var providerLabel = cfg.provider_label || 'the AI provider';
1332 var isOpenRouter = cfg.provider === 'openrouter';
1333
1334 function esc(value) {
1335 return $('<div></div>').text(String(value == null ? '' : value)).html();
1336 }
1337
1338 var variants = {
1339 local_limit: {
1340 title: 'Daily token limit reached',
1341 subtitle: 'Your own safety limit stopped the translation.',
1342 heading: 'What happened',
1343 body: 'King Addons has a <strong>"Daily Token Limit"</strong> setting that prevents accidental '
1344 + 'overspending, and this page hit it. Nothing is wrong with your ' + esc(providerLabel) + ' account.',
1345 steps: [
1346 '<strong>Increase the "Daily Token Limit"</strong> in AI Settings (recommended)',
1347 'Or wait until tomorrow &mdash; the limit resets automatically'
1348 ],
1349 tip: 'Go to <strong>AI Settings → Daily Token Limit</strong> and set a higher number. '
1350 + 'For regular use, try <strong>50,000 or 100,000 tokens</strong>.'
1351 },
1352 rate_limit: {
1353 title: 'Model rate limit reached',
1354 subtitle: esc(providerLabel) + ' is throttling requests for the selected model.',
1355 heading: 'What happened',
1356 body: 'The model was asked for translations faster than the provider allows, and it kept '
1357 + 'refusing after several retries. This is a temporary limit, not a problem with your account.',
1358 steps: [
1359 'Wait a minute and resume &mdash; the limit clears on its own',
1360 'Or pick a less busy model in AI Settings'
1361 ].concat(isOpenRouter ? ['Free models share a pool with other users; a paid model has far higher limits'] : []),
1362 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1363 + 'to continue from where it stopped.'
1364 },
1365 daily_limit: {
1366 title: 'Daily model limit reached',
1367 subtitle: esc(providerLabel) + ' has capped this model for today.',
1368 heading: 'What happened',
1369 body: 'The selected model has a daily request cap and it has been used up. Waiting a few '
1370 + 'seconds will not help &mdash; the cap resets on the provider\'s schedule.',
1371 steps: [
1372 'Switch to a different model in AI Settings',
1373 'Or come back after the cap resets'
1374 ].concat(isOpenRouter ? ['Free models have daily caps that credits do not lift; a paid model avoids them'] : []),
1375 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1376 + 'to continue from where it stopped.'
1377 },
1378 credits: {
1379 title: 'Out of credits',
1380 subtitle: 'Your ' + esc(providerLabel) + ' account has no balance left.',
1381 heading: 'What happened',
1382 body: esc(providerLabel) + ' rejected the request because the account balance is empty. '
1383 + 'The plugin and your API key are fine.',
1384 steps: isOpenRouter
1385 ? ['Add credit at <a href="https://openrouter.ai/settings/credits" target="_blank" rel="noopener noreferrer" style="color:#5B03FF;">openrouter.ai/settings/credits</a>',
1386 'Or switch to a free model in AI Settings']
1387 : ['Top up your account balance in the provider dashboard',
1388 'Then run the translation again'],
1389 tip: 'Your progress was saved. Reopen the AI Translator and choose <strong>Resume</strong> '
1390 + 'to continue from where it stopped.'
1391 }
1392 };
1393
1394 var variant = variants[errorCode] || variants.local_limit;
1395
1396 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1397 var $popup = $('<div class="king-addons-translator-popup"></div>');
1398
1399 var stepsHtml = variant.steps.map(function(step) {
1400 return '<li>' + step + '</li>';
1401 }).join('');
1402
1403 // The provider's own wording is the most precise explanation there is,
1404 // so it is shown verbatim rather than paraphrased away.
1405 var detailHtml = message ? `
1406 <div class="ka-tr-panel">
1407 <h4>Provider response</h4>
1408 <div class="ka-tr-detail">${esc(message)}</div>
1409 </div>` : '';
1410
1411 $popup.html(`
1412 <div class="ka-tr-dialog-head">
1413 <h3>${variant.title}</h3>
1414 <p class="ka-tr-dialog-sub">${variant.subtitle}</p>
1415 </div>
1416
1417 <div class="ka-tr-panel">
1418 <h4>${variant.heading}</h4>
1419 <p>${variant.body}</p>
1420 </div>
1421
1422 <div class="ka-tr-panel ka-tr-panel--accent">
1423 <h4>What to do</h4>
1424 <ol class="ka-tr-steps">${stepsHtml}</ol>
1425 </div>
1426
1427 ${detailHtml}
1428
1429 <div class="ka-tr-panel ka-tr-panel--warning">
1430 <p>${variant.tip}</p>
1431 </div>
1432
1433 <div class="king-addons-translator-actions">
1434 <button class="king-addons-translator-btn-secondary" id="king-addons-limit-close">I understand</button>
1435 <a href="${esc(settingsUrl)}" class="king-addons-translator-btn-primary" style="text-decoration: none; display: flex; align-items: center; justify-content: center;">Go to AI Settings</a>
1436 </div>
1437 `);
1438
1439 $overlay.append($popup);
1440 $('body').append($overlay);
1441
1442 $('#king-addons-limit-close').on('click', function() {
1443 $overlay.remove();
1444 });
1445
1446 $overlay.on('click', function(e) {
1447 if (e.target === $overlay[0]) {
1448 $overlay.remove();
1449 }
1450 });
1451 }
1452
1453 function toggleTranslatorButton(disabled) {
1454 var $button = $('.king-addons-ai-translator-btn');
1455
1456 if (disabled) {
1457 $button.prop('disabled', true);
1458 $button.css('opacity', '0.5');
1459 $button.css('cursor', 'not-allowed');
1460 } else {
1461 $button.prop('disabled', false);
1462 $button.css('opacity', '1');
1463 $button.css('cursor', 'pointer');
1464 }
1465 }
1466
1467 /**
1468 * Stop the translation process
1469 */
1470 function stopTranslationProcess() {
1471 // Prevent multiple calls
1472 if (translationState.isCancelled) {
1473 return;
1474 }
1475
1476 translationState.isCancelled = true;
1477 translationState.isTranslating = false;
1478
1479 // Cancel all active AJAX requests
1480 if (translationState.currentRequests.length > 0) {
1481 translationState.currentRequests.forEach(function(request) {
1482 if (request && request.abort) {
1483 request.abort();
1484 }
1485 });
1486 translationState.currentRequests = [];
1487 }
1488
1489 // Remove any highlighting from current element
1490 if (translationState.currentElement) {
1491 highlightElementInPreview(translationState.currentElement.elementId, false);
1492 }
1493
1494 // Remove any existing popups/overlays
1495 $('.king-addons-translator-overlay').remove();
1496
1497 // Re-enable the button
1498 toggleTranslatorButton(false);
1499 }
1500
1501 /**
1502 * Animate popup to top-right corner
1503 */
1504 function movePopupToCorner($popup, $overlay) {
1505 return new Promise(function(resolve) {
1506 // Add moving class for smooth animation
1507 $popup.addClass('moving');
1508
1509 // Hide overlay with fade
1510 $overlay.addClass('hiding');
1511
1512 // Calculate current position and target position
1513 var currentRect = $popup[0].getBoundingClientRect();
1514 var targetTop = 80;
1515 var targetRight = 20;
1516 var targetLeft = window.innerWidth - 350 - 20;
1517
1518 // Move popup from overlay to body with current position
1519 $popup.css({
1520 'position': 'fixed',
1521 'top': currentRect.top + 'px',
1522 'left': currentRect.left + 'px',
1523 'width': currentRect.width + 'px',
1524 'margin': '0',
1525 'transform': 'none',
1526 'z-index': 999999
1527 });
1528
1529 // Append popup to body (remove from overlay)
1530 $('body').append($popup);
1531
1532 // Wait for overlay to fade, then animate popup
1533 setTimeout(function() {
1534 // Force reflow
1535 $popup[0].offsetHeight;
1536
1537 // Animate to final position
1538 $popup.css({
1539 'top': targetTop + 'px',
1540 'left': targetLeft + 'px',
1541 'width': '350px',
1542 'padding': '16px'
1543 });
1544
1545 // Add compact class after animation and remove overlay
1546 setTimeout(function() {
1547 $popup.removeClass('moving').addClass('compact');
1548 $overlay.remove(); // Remove overlay completely
1549 resolve();
1550 }, 500);
1551
1552 }, 300);
1553 });
1554 }
1555
1556 /**
1557 * Create and show the main popup
1558 */
1559 /**
1560 * Human readable name for a language code or a custom prompt.
1561 */
1562 function describeLanguage(code) {
1563 return languages[code] || code || 'the target language';
1564 }
1565
1566 /**
1567 * Offer to continue an interrupted run instead of starting over.
1568 *
1569 * Elementor keeps translated content as unsaved changes, so a reload only
1570 * preserves it once the document has been saved or autosaved - the prompt
1571 * says so rather than pretending otherwise.
1572 */
1573 function showResumePopup(saved) {
1574 $('.king-addons-translator-overlay').remove();
1575
1576 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1577 var $popup = $('<div class="king-addons-translator-popup"></div>');
1578
1579 var remaining = Math.max(0, saved.total - saved.done.length);
1580
1581 function esc(value) {
1582 return $('<div></div>').text(String(value == null ? '' : value)).html();
1583 }
1584
1585 // Mirrors the main popup's skeleton (h3, a subtitle sibling, and a
1586 // .king-addons-translator-form body) so showProgressInPopup() can take
1587 // it over once the run starts.
1588 $popup.html(`
1589 <h3>
1590 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);" alt=""/>
1591 Resume this run?
1592 </h3>
1593 <div class="ka-tr-byline">by King Addons</div>
1594 <div class="ka-tr-dialog-sub" style="margin-bottom: 16px;">
1595 A run on this page was interrupted.
1596 </div>
1597 <div class="king-addons-translator-form">
1598 <div class="ka-tr-panel">
1599 <div class="ka-tr-rows">
1600 <div class="ka-tr-row"><span>Progress</span><strong>${esc(saved.done.length)} / ${esc(saved.total)} elements</strong></div>
1601 <div class="ka-tr-row"><span>Remaining</span><strong>${esc(remaining)} elements</strong></div>
1602 <div class="ka-tr-row"><span>Translating into</span><strong>${esc(describeLanguage(saved.toLang))}</strong></div>
1603 </div>
1604 </div>
1605
1606 <div class="ka-tr-panel ka-tr-panel--warning">
1607 <p>
1608 Resuming skips the elements that were already done. If the page was reloaded
1609 without saving, those elements kept their original text &mdash; choose
1610 <strong>Start over</strong> to translate the whole page again.
1611 </p>
1612 </div>
1613
1614 <div class="king-addons-translator-actions">
1615 <button class="king-addons-translator-btn-secondary" id="king-addons-resume-discard">Start over</button>
1616 <button class="king-addons-translator-btn-primary" id="king-addons-resume-continue">Resume</button>
1617 </div>
1618 </div>
1619 `);
1620
1621 $overlay.append($popup);
1622 $('body').append($overlay);
1623
1624 // Hand the same popup to the normal flow, which swaps its body for the
1625 // progress UI and animates it into the corner.
1626 $('#king-addons-resume-continue').on('click', function() {
1627 startTranslation(saved.fromLang || 'auto', saved.toLang, $popup, $overlay, saved);
1628 });
1629
1630 $('#king-addons-resume-discard').on('click', function() {
1631 clearTranslationProgress();
1632 $overlay.remove();
1633 createAndShowPopup();
1634 });
1635
1636 $overlay.on('click', function(e) {
1637 if (e.target === $overlay[0]) {
1638 $overlay.remove();
1639 }
1640 });
1641 }
1642
1643 /**
1644 * Small banner shown after the editor loads when a run can be continued.
1645 */
1646 function offerResumeOnLoad() {
1647 if (translationState.isTranslating || $('#king-addons-translator-resume-banner').length) {
1648 return;
1649 }
1650
1651 var saved = loadTranslationProgress();
1652 if (!saved) {
1653 return;
1654 }
1655
1656 var $banner = $(`
1657 <div id="king-addons-translator-resume-banner" style="position: fixed; bottom: 20px; right: 20px; z-index: 999998; max-width: 320px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); padding: 16px; font-size: 13px; color: #2d3748;">
1658 <div style="font-weight: 600; margin-bottom: 6px;">Unfinished run</div>
1659 <div style="color: #718096; line-height: 1.5; margin-bottom: 12px;">
1660 ${saved.done.length} of ${saved.total} elements were processed into ${$('<div></div>').text(describeLanguage(saved.toLang)).html()}.
1661 </div>
1662 <div style="display: flex; gap: 8px;">
1663 <button type="button" id="king-addons-resume-banner-dismiss" style="flex: 1; border: 1px solid #e2e8f0; background: #f7fafc; color: #4a5568; border-radius: 6px; padding: 7px 10px; cursor: pointer;">Later</button>
1664 <button type="button" id="king-addons-resume-banner-open" style="flex: 1; border: none; background: #5B03FF; color: #fff; border-radius: 6px; padding: 7px 10px; cursor: pointer;">Resume</button>
1665 </div>
1666 </div>
1667 `);
1668
1669 $('body').append($banner);
1670
1671 $('#king-addons-resume-banner-open').on('click', function() {
1672 $banner.remove();
1673 var current = loadTranslationProgress();
1674 if (current) {
1675 showResumePopup(current);
1676 }
1677 });
1678
1679 // "Later" only hides the banner; the saved progress stays available
1680 // from the AI Translator button.
1681 $('#king-addons-resume-banner-dismiss').on('click', function() {
1682 $banner.remove();
1683 });
1684 }
1685
1686 function createAndShowPopup() {
1687 var $overlay = $('<div class="king-addons-translator-overlay"></div>');
1688 var $popup = $('<div class="king-addons-translator-popup"></div>');
1689
1690 var isPro = isPremiumActive();
1691 var customOptionHtml = isPro ?
1692 '<option value="custom">Custom language or prompt (PRO)</option>' :
1693 '<option value="custom" disabled>Custom language or prompt (PRO)</option>';
1694
1695 var upgradeUrl = 'https://kingaddons.com/pricing/?utm_source=ai-translator&utm_medium=plugin&utm_campaign=custom-prompts';
1696 var proInfoHtml = isPro ?
1697 '<div class="king-addons-pro-info" style="color: #4CAF50; border-left-color: #4CAF50;">�
1698 PRO Active: Use custom languages and translation prompts!</div>' :
1699 '<div class="king-addons-pro-info">💎 <a href="' + upgradeUrl + '" target="_blank" style="color: #5B03FF; text-decoration: none;">Upgrade to King Addons PRO</a> to use custom languages, regional dialects and custom translation prompts (formal tone, technical style, etc.)!</div>';
1700
1701 var popupContent = `
1702 <h3>
1703 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);" alt=""/>
1704 AI Page Translate &amp; Transform
1705 </h3>
1706 <div class="ka-tr-byline">by King Addons</div>
1707 <div class="ka-tr-dialog-sub" style="margin-bottom: 20px;">
1708 Translate to any language, or transform the text style (formal, casual, technical).
1709 </div>
1710 <div class="king-addons-translator-form">
1711 <div class="king-addons-translator-field">
1712 <label>From Language</label>
1713 <select id="king-addons-from-lang">
1714 <option value="auto">Auto-detect</option>
1715 ${Object.keys(languages).map(code =>
1716 `<option value="${code}">${languages[code]}</option>`
1717 ).join('')}
1718 ${customOptionHtml}
1719 </select>
1720 <div class="king-addons-custom-language-field" id="king-addons-custom-from-field">
1721 <label>Custom language or translation prompt</label>
1722 <input type="text" id="king-addons-custom-from-lang" placeholder="e.g., Klingon, Old English, formal business tone, medical terminology..." />
1723 <div class="king-addons-prompt-examples">
1724 <small>Examples: "Klingon", "Shakespeare English", "formal business style", "casual conversational tone"</small>
1725 </div>
1726 </div>
1727 </div>
1728 <div class="king-addons-translator-field">
1729 <label>To Language</label>
1730 <select id="king-addons-to-lang">
1731 ${Object.keys(languages).map(code =>
1732 `<option value="${code}" ${code === 'en' ? 'selected' : ''}>${languages[code]}</option>`
1733 ).join('')}
1734 ${customOptionHtml}
1735 </select>
1736 <div class="king-addons-custom-language-field" id="king-addons-custom-to-field">
1737 <label>Custom language or translation style</label>
1738 <input type="text" id="king-addons-custom-to-lang" placeholder="e.g., Dothraki, Academic writing, pirate speak, baby talk..." />
1739 <div class="king-addons-prompt-examples">
1740 <small>Examples: "Dothraki", "academic paper style", "pirate language", "simplified for children"</small>
1741 </div>
1742 </div>
1743 </div>
1744 ${proInfoHtml}
1745 <div class="king-addons-translator-actions">
1746 <button class="king-addons-translator-btn-secondary" id="king-addons-cancel-translation">
1747 Cancel
1748 </button>
1749 <button class="king-addons-translator-btn-primary" id="king-addons-start-translation">
1750 Start Translation
1751 </button>
1752 </div>
1753 </div>
1754 `;
1755
1756 $popup.html(popupContent);
1757 $overlay.append($popup);
1758 $('body').append($overlay);
1759
1760 // Store references globally
1761 window.currentTranslatorPopup = $popup;
1762 window.currentTranslatorOverlay = $overlay;
1763
1764 // Bind events for language selection
1765 bindLanguageSelectionEvents();
1766
1767 // Bind events
1768 $('#king-addons-cancel-translation').on('click', function() {
1769 if (!translationState.isTranslating) {
1770 $overlay.remove();
1771 }
1772 });
1773
1774 $('#king-addons-start-translation').on('click', function() {
1775 var result = getSelectedLanguages();
1776
1777 if (!result.valid) {
1778 alert(result.error);
1779 return;
1780 }
1781
1782 // Double-check API key before starting translation
1783 var $button = $(this);
1784 $button.prop('disabled', true).text('🔍 Verifying...');
1785
1786 $.post(KingAddonsAiField.ajax_url, {
1787 action: 'king_addons_ai_check_tokens',
1788 nonce: KingAddonsAiField.generate_nonce
1789 }, function(response) {
1790 $button.prop('disabled', false).text('Start Translation');
1791
1792 if (!response.success || !response.data.api_key_valid) {
1793 var errorMessage = 'API key verification failed. Please check your API key in settings.';
1794 if (response.data && response.data.error_message) {
1795 errorMessage = response.data.error_message;
1796 }
1797
1798 // Check for token limit errors first
1799 if (errorMessage.toLowerCase().includes('token limit') ||
1800 errorMessage.toLowerCase().includes('daily limit') ||
1801 errorMessage.toLowerCase().includes('limit reached') ||
1802 errorMessage.toLowerCase().includes('quota exceeded') ||
1803 errorMessage.toLowerCase().includes('rate limit') ||
1804 errorMessage.toLowerCase().includes('too many requests')) {
1805
1806 // Show token limit error popup
1807 showTokenLimitError(errorMessage);
1808 return;
1809 }
1810
1811 // Show error popup (will handle cleanup automatically)
1812 showApiKeyError('Setup Required', errorMessage);
1813 return;
1814 }
1815
1816 // API key is valid, proceed with translation
1817 startTranslation(result.fromLang, result.toLang, $popup, $overlay);
1818
1819 }).fail(function() {
1820 $button.prop('disabled', false).text('Start Translation');
1821
1822 // Show error popup (will handle cleanup automatically)
1823 showApiKeyError('Connection Issue', 'Failed to connect right now. Please check your connection and try again.');
1824 });
1825 });
1826
1827 // Close on overlay click only if not translating
1828 $overlay.on('click', function(e) {
1829 if (e.target === $overlay[0] && !translationState.isTranslating) {
1830 $overlay.remove();
1831 }
1832 });
1833 }
1834
1835 /**
1836 * Bind events for language selection dropdowns
1837 */
1838 function bindLanguageSelectionEvents() {
1839 // Handle From Language selection
1840 $('#king-addons-from-lang').on('change', function() {
1841 var selectedValue = $(this).val();
1842 var $customField = $('#king-addons-custom-from-field');
1843
1844 if (selectedValue === 'custom') {
1845 if (!isPremiumActive()) {
1846 // Reset to previous value and show upgrade message
1847 $(this).val('auto');
1848 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1849 return;
1850 }
1851 $customField.addClass('show');
1852 $('#king-addons-custom-from-lang').focus();
1853 } else {
1854 $customField.removeClass('show');
1855 }
1856 });
1857
1858 // Handle To Language selection
1859 $('#king-addons-to-lang').on('change', function() {
1860 var selectedValue = $(this).val();
1861 var $customField = $('#king-addons-custom-to-field');
1862
1863 if (selectedValue === 'custom') {
1864 if (!isPremiumActive()) {
1865 // Reset to previous value and show upgrade message
1866 $(this).val('en');
1867 alert('Custom languages and translation prompts are a PRO feature. Please upgrade to King Addons PRO to use custom languages or translation styles.');
1868 return;
1869 }
1870 $customField.addClass('show');
1871 $('#king-addons-custom-to-lang').focus();
1872 } else {
1873 $customField.removeClass('show');
1874 }
1875 });
1876 }
1877
1878 /**
1879 * Get selected languages with validation
1880 */
1881 function getSelectedLanguages() {
1882 var fromLang = $('#king-addons-from-lang').val();
1883 var toLang = $('#king-addons-to-lang').val();
1884 var customFromLang = $('#king-addons-custom-from-lang').val().trim();
1885 var customToLang = $('#king-addons-custom-to-lang').val().trim();
1886
1887 // Handle custom from language
1888 if (fromLang === 'custom') {
1889 if (!customFromLang) {
1890 return {
1891 valid: false,
1892 error: 'Please enter a custom source language or translation prompt.'
1893 };
1894 }
1895 fromLang = customFromLang;
1896 }
1897
1898 // Handle custom to language
1899 if (toLang === 'custom') {
1900 if (!customToLang) {
1901 return {
1902 valid: false,
1903 error: 'Please enter a custom target language or translation style.'
1904 };
1905 }
1906 toLang = customToLang;
1907 }
1908
1909 // Validate languages are different (except auto-detect)
1910 if (fromLang === toLang && fromLang !== 'auto') {
1911 return {
1912 valid: false,
1913 error: 'Source and target languages cannot be the same.'
1914 };
1915 }
1916
1917 return {
1918 valid: true,
1919 fromLang: fromLang,
1920 toLang: toLang
1921 };
1922 }
1923
1924 /**
1925 * Start the translation process
1926 */
1927 function startTranslation(fromLang, toLang, $popup, $overlay, resumeFrom) {
1928 translationState.isTranslating = true;
1929 translationState.isCancelled = false; // Reset cancellation flag
1930 translationState.currentRequests = []; // Clear any previous requests
1931 translationState.fromLang = fromLang;
1932 translationState.toLang = toLang;
1933 translationState.translatedElements = 0;
1934 translationState.failedElements = 0;
1935 translationState.doneElementIds = [];
1936 translationState.failedElementIds = [];
1937 translationState.lastErrorMessage = '';
1938 translationState.consecutiveFailures = 0;
1939
1940 // Inject animation styles into preview iframe immediately
1941 injectPreviewStyles();
1942
1943 // Disable the AI Translator button
1944 toggleTranslatorButton(true);
1945
1946 // Get all translatable elements
1947 var elements = getTranslatableElements();
1948
1949 if (elements.length === 0) {
1950 alert('No translatable text elements found on this page.');
1951 translationState.isTranslating = false;
1952 toggleTranslatorButton(false);
1953 clearTranslationProgress();
1954 return;
1955 }
1956
1957 // Resuming: keep the elements already handled out of this run, but keep
1958 // counting them so the progress bar reflects the whole page.
1959 if (resumeFrom && Array.isArray(resumeFrom.done) && resumeFrom.done.length) {
1960 var alreadyDone = resumeFrom.done;
1961 var remaining = elements.filter(function(element) {
1962 return alreadyDone.indexOf(element.elementId) === -1;
1963 });
1964
1965 // Every element accounted for means there is nothing left to do.
1966 if (!remaining.length) {
1967 translationState.isTranslating = false;
1968 toggleTranslatorButton(false);
1969 clearTranslationProgress();
1970 alert('This page has already been translated.');
1971 return;
1972 }
1973
1974 translationState.doneElementIds = alreadyDone.slice();
1975 translationState.failedElementIds = Array.isArray(resumeFrom.failed) ? resumeFrom.failed.slice() : [];
1976 translationState.translatedElements = alreadyDone.length;
1977 translationState.failedElements = translationState.failedElementIds.length;
1978 translationState.resumedCount = alreadyDone.length;
1979 elements = remaining;
1980 } else {
1981 translationState.resumedCount = 0;
1982 }
1983
1984 translationState.totalElements = elements.length + translationState.doneElementIds.length;
1985 saveTranslationProgress();
1986
1987 // Update popup to show progress
1988 showProgressInPopup($popup);
1989
1990 // Animate popup to corner and start translation
1991 movePopupToCorner($popup, $overlay).then(function() {
1992 // Start translating elements one by one
1993 translateElementsSequentially(elements, 0, $popup);
1994 });
1995 }
1996
1997 /**
1998 * Get all translatable text elements
1999 */
2000 function getTranslatableElements() {
2001 var elements = [];
2002 // Get the main document container using Elementor 3.0+ API
2003 var documentContainer = elementor.documents.getCurrent().container;
2004 var elementorElements = [];
2005
2006 // Use the new API to get children - for Elementor 3.0+
2007 if (documentContainer.children && typeof documentContainer.children.models !== 'undefined') {
2008 // Backbone collection - extract models
2009 elementorElements = documentContainer.children.models || [];
2010 } else if (documentContainer.elements && typeof documentContainer.elements.models !== 'undefined') {
2011 // Alternative property name in some Elementor versions
2012 elementorElements = documentContainer.elements.models || [];
2013 } else if (Array.isArray(documentContainer.children)) {
2014 // Fallback for older API
2015 elementorElements = documentContainer.children;
2016 } else {
2017 // console.warn('🚨 Unable to find container children using any known API');
2018 elementorElements = [];
2019 }
2020
2021 function processContainer(container) {
2022 var model = container.model;
2023 var elementType = model.get('elType');
2024 var widgetType = model.get('widgetType');
2025
2026 // Process text-based widgets
2027 if (widgetType) {
2028 // First, check if this widget type should be skipped entirely
2029 var nonTextWidgets = [
2030 'spacer', 'divider', 'html', 'shortcode', 'sidebar',
2031 'menu-anchor', 'read-more', 'google_maps', 'paypal_button',
2032 'stripe_button', 'facebook_button', 'facebook_page',
2033 'video', 'audio', 'iframe', 'code', 'wp-widget',
2034 'map', 'rating', 'progress', 'counter', 'countdown',
2035 'social-icons', 'share-buttons', 'login', 'lottie',
2036 'image' // Image widget should be skipped
2037 ];
2038
2039 if (nonTextWidgets.indexOf(widgetType) !== -1) {
2040 return; // Exit early for blacklisted widgets
2041 }
2042
2043 var settings = model.get('settings').attributes;
2044
2045 // Now check for text fields in remaining widgets
2046 var textFields = getTextFieldsForWidget(widgetType, settings, container);
2047
2048 // If we found text fields, process the widget
2049 if (textFields.length > 0) {
2050 elements.push({
2051 container: container,
2052 widgetType: widgetType,
2053 textFields: textFields,
2054 elementId: model.get('id')
2055 });
2056 return;
2057 }
2058 }
2059
2060 // Process child containers recursively using Elementor 3.0+ API
2061 if (container.children && container.children.length > 0) {
2062 // Check if children is a Backbone collection
2063 if (typeof container.children.models !== 'undefined') {
2064 container.children.models.forEach(processContainer);
2065 } else if (Array.isArray(container.children)) {
2066 container.children.forEach(processContainer);
2067 }
2068 }
2069 }
2070
2071 elementorElements.forEach(processContainer);
2072 return elements;
2073 }
2074
2075 /**
2076 * Get text fields for a specific widget type using Elementor control types
2077 */
2078 function getTextFieldsForWidget(widgetType, settings, container) {
2079 var textFields = [];
2080
2081 // Try to get widget controls schema from Elementor
2082 var controls = getWidgetControls(widgetType, container);
2083
2084 if (controls && Object.keys(controls).length > 0) {
2085 // Look for text-based controls
2086 Object.keys(controls).forEach(function(controlName) {
2087 var control = controls[controlName];
2088 var controlType = control.type;
2089 var settingValue = settings[controlName];
2090
2091 // Check if this is a text-based control type
2092 var textControlTypes = [
2093 'text', 'textarea', 'wysiwyg', 'url', 'email',
2094 'password', 'search', 'tel', 'date', 'time',
2095 'datetime-local', 'month', 'week'
2096 ];
2097
2098 if (textControlTypes.includes(controlType)) {
2099 // Check if the field has a non-empty string value
2100 if (settingValue && typeof settingValue === 'string' && settingValue.trim()) {
2101 // Skip obviously non-translatable fields
2102 var skipFields = [
2103 '_element_id', '_css_classes', 'link', 'url', 'href',
2104 'custom_css', 'css_id', 'anchor', 'html_tag'
2105 ];
2106
2107 if (!skipFields.includes(controlName)) {
2108 textFields.push({
2109 field: controlName,
2110 value: settingValue,
2111 type: controlType === 'wysiwyg' ? 'wysiwyg' : 'text'
2112 });
2113 }
2114 }
2115 }
2116
2117 // Also check for repeater controls
2118 if (controlType === 'repeater' && settingValue) {
2119 checkRepeaterFieldsByType(controlName, control, settingValue, textFields);
2120 }
2121 });
2122 } else {
2123 // Fallback: Use the original method for widgets without accessible controls
2124 var commonTextFields = [
2125 'title', 'text', 'content', 'description', 'subtitle', 'button_text',
2126 'heading_title', 'heading_subtitle', 'testimonial_content', 'testimonial_name',
2127 'title_text', 'description_text', 'content_text', 'editor'
2128 ];
2129
2130 commonTextFields.forEach(function(field) {
2131 if (settings[field] && typeof settings[field] === 'string' && settings[field].trim()) {
2132 textFields.push({
2133 field: field,
2134 value: settings[field],
2135 type: field === 'editor' ? 'wysiwyg' : 'text'
2136 });
2137 }
2138 });
2139
2140 // Check for repeater fields using the old method
2141 checkRepeaterFields(settings, textFields);
2142 }
2143
2144 return textFields;
2145 }
2146
2147 /**
2148 * Get widget controls schema from Elementor
2149 */
2150 function getWidgetControls(widgetType, container) {
2151 try {
2152 // Method 1: Try to get controls from container model
2153 if (container && container.model && container.model.get) {
2154 var model = container.model;
2155
2156 // Try to get controls from the model's widget config
2157 if (model.config && model.config.controls) {
2158 return model.config.controls;
2159 }
2160
2161 // Try to get controls from the container settings
2162 if (container.settings && container.settings.controls) {
2163 return container.settings.controls;
2164 }
2165 }
2166
2167 // Method 2: Try to get controls from Elementor widgets registry
2168 if (window.elementor && elementor.widgets) {
2169 var widgetConfig = elementor.widgets.getWidgetType(widgetType);
2170 if (widgetConfig && widgetConfig.controls) {
2171 return widgetConfig.controls;
2172 }
2173 }
2174
2175 // Method 3: Try to get controls from elements manager
2176 if (window.elementor && elementor.elementsManager) {
2177 var elementView = elementor.elementsManager.getElementView(container.model.get('id'));
2178 if (elementView && elementView.model && elementView.model.controls) {
2179 return elementView.model.controls;
2180 }
2181 }
2182
2183 return null;
2184
2185 } catch (error) {
2186 // console.warn('⚠️ Error getting widget controls:', error);
2187 return null;
2188 }
2189 }
2190
2191 /**
2192 * Check repeater fields using control type information
2193 */
2194 function checkRepeaterFieldsByType(repeaterName, repeaterControl, repeaterData, textFields) {
2195 try {
2196 // Get the fields schema for this repeater
2197 var repeaterFields = repeaterControl.fields || repeaterControl.controls || {};
2198
2199 // Find text-based fields in the repeater schema
2200 var textFieldNames = [];
2201 Object.keys(repeaterFields).forEach(function(fieldName) {
2202 var fieldControl = repeaterFields[fieldName];
2203 var textControlTypes = ['text', 'textarea', 'wysiwyg', 'url', 'email'];
2204
2205 if (textControlTypes.includes(fieldControl.type)) {
2206 textFieldNames.push(fieldName);
2207 }
2208 });
2209
2210 if (textFieldNames.length === 0) {
2211 return;
2212 }
2213
2214 // Process repeater data (same as before)
2215 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
2216 // Backbone collection
2217 const models = repeaterData.models || [];
2218 for (let i = 0; i < models.length; i++) {
2219 const model = models[i];
2220 const modelData = model.attributes || model.toJSON();
2221
2222 for (const fieldName of textFieldNames) {
2223 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
2224 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
2225 const fieldValue = modelData[fieldName];
2226
2227 textFields.push({
2228 field: fieldKey,
2229 value: fieldValue,
2230 type: 'text',
2231 isRepeater: true,
2232 repeaterKey: repeaterName,
2233 repeaterIndex: i,
2234 repeaterField: fieldName
2235 });
2236 }
2237 }
2238 }
2239 } else if (Array.isArray(repeaterData)) {
2240 // Regular array
2241 for (let i = 0; i < repeaterData.length; i++) {
2242 const item = repeaterData[i];
2243 for (const fieldName of textFieldNames) {
2244 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2245 const fieldKey = `${repeaterName}[${i}][${fieldName}]`;
2246 const fieldValue = item[fieldName];
2247
2248 textFields.push({
2249 field: fieldKey,
2250 value: fieldValue,
2251 type: 'text',
2252 isRepeater: true,
2253 repeaterKey: repeaterName,
2254 repeaterIndex: i,
2255 repeaterField: fieldName
2256 });
2257 }
2258 }
2259 }
2260 }
2261
2262 } catch (error) {
2263 // console.warn('⚠️ Error processing repeater by type:', error);
2264 // Fallback to old method
2265 var repeaterConfig = {};
2266 repeaterConfig[repeaterName] = ['content', 'text', 'title', 'description'];
2267 checkRepeaterFields({[repeaterName]: repeaterData}, textFields);
2268 }
2269 }
2270
2271 /**
2272 * Check for repeater fields in settings (fallback method)
2273 */
2274 function checkRepeaterFields(settings, textFields) {
2275 // King Addons specific repeater configurations
2276 const repeaterConfigs = {
2277 'kng_styled_txt_content_items': ['kng_styled_txt_content'],
2278 'kng_tabs_items': ['kng_tabs_title', 'kng_tabs_content'],
2279 'kng_accordion_items': ['kng_accordion_title', 'kng_accordion_content'],
2280 'kng_testimonials_items': ['kng_testimonials_content', 'kng_testimonials_name'],
2281 'kng_team_members': ['kng_team_name', 'kng_team_position', 'kng_team_description'],
2282 'kng_price_list_items': ['kng_price_title', 'kng_price_description'],
2283 'kng_business_hours_items': ['kng_business_day', 'kng_business_hours'],
2284 // Standard Elementor repeaters
2285 'tabs': ['tab_title', 'tab_content'],
2286 'icon_list': ['text'],
2287 'slides': ['heading', 'description', 'button_text'],
2288 'list_items': ['text'],
2289 'testimonials': ['testimonial_content', 'testimonial_name'],
2290 'items': ['item_title', 'item_description', 'item_content'],
2291 'price_list': ['price_title', 'price_description']
2292 };
2293
2294 for (const [repeaterKey, fieldNames] of Object.entries(repeaterConfigs)) {
2295 if (settings[repeaterKey]) {
2296 let repeaterData = settings[repeaterKey];
2297
2298 // Handle Backbone Collections (common in King Addons and some Elementor widgets)
2299 if (repeaterData && typeof repeaterData === 'object' && repeaterData.models) {
2300 // Extract models from Backbone collection
2301 const models = repeaterData.models || [];
2302 for (let i = 0; i < models.length; i++) {
2303 const model = models[i];
2304 const modelData = model.attributes || model.toJSON();
2305
2306 for (const fieldName of fieldNames) {
2307 if (modelData[fieldName] && typeof modelData[fieldName] === 'string' && modelData[fieldName].trim()) {
2308 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
2309 const fieldValue = modelData[fieldName];
2310
2311 textFields.push({
2312 field: fieldKey,
2313 value: fieldValue,
2314 type: 'text',
2315 isRepeater: true,
2316 repeaterKey: repeaterKey,
2317 repeaterIndex: i,
2318 repeaterField: fieldName
2319 });
2320 }
2321 }
2322 }
2323 }
2324 // Handle regular arrays
2325 else if (Array.isArray(repeaterData)) {
2326 for (let i = 0; i < repeaterData.length; i++) {
2327 const item = repeaterData[i];
2328 for (const fieldName of fieldNames) {
2329 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2330 const fieldKey = `${repeaterKey}[${i}][${fieldName}]`;
2331 const fieldValue = item[fieldName];
2332
2333 textFields.push({
2334 field: fieldKey,
2335 value: fieldValue,
2336 type: 'text',
2337 isRepeater: true,
2338 repeaterKey: repeaterKey,
2339 repeaterIndex: i,
2340 repeaterField: fieldName
2341 });
2342 }
2343 }
2344 }
2345 }
2346 // Handle objects with numbered keys (alternative format)
2347 else if (repeaterData && typeof repeaterData === 'object') {
2348 const keys = Object.keys(repeaterData).filter(key => /^\d+$/.test(key));
2349
2350 for (const key of keys) {
2351 const item = repeaterData[key];
2352 for (const fieldName of fieldNames) {
2353 if (item[fieldName] && typeof item[fieldName] === 'string' && item[fieldName].trim()) {
2354 const fieldKey = `${repeaterKey}[${key}][${fieldName}]`;
2355 const fieldValue = item[fieldName];
2356
2357 textFields.push({
2358 field: fieldKey,
2359 value: fieldValue,
2360 type: 'text',
2361 isRepeater: true,
2362 repeaterKey: repeaterKey,
2363 repeaterIndex: key,
2364 repeaterField: fieldName
2365 });
2366 }
2367 }
2368 }
2369 }
2370 }
2371 }
2372 }
2373
2374 /**
2375 * Show progress UI in popup
2376 */
2377 function showProgressInPopup($popup) {
2378 // Update header for compact mode with close button
2379 var headerHtml = `
2380 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
2381 AI is working on your page
2382 <button class="king-addons-translator-close-btn" title="Close">×</button>
2383 `;
2384
2385 var progressHtml = `
2386 <div class="king-addons-translator-progress">
2387 <div class="king-addons-translator-progress-text">
2388 Processing page elements&hellip; <span id="king-addons-progress-count">0 / ${translationState.totalElements}</span>
2389 </div>
2390 <div class="king-addons-translator-progress-bar">
2391 <div class="king-addons-translator-progress-fill" id="king-addons-progress-fill"></div>
2392 </div>
2393 <div class="ka-tr-activity">
2394 <span class="ka-tr-spinner" aria-hidden="true"></span>
2395 <span class="king-addons-translator-current-element" id="king-addons-current-element">Preparing…</span>
2396 </div>
2397 <div class="ka-tr-snippet" id="king-addons-progress-snippet" hidden></div>
2398 <div class="king-addons-translator-progress-note" id="king-addons-progress-note"></div>
2399 </div>
2400 `;
2401
2402 // Update header
2403 $popup.find('h3').html(headerHtml);
2404
2405 // Hide the description and the byline while the run is in progress.
2406 $popup.find('.ka-tr-dialog-sub, .ka-tr-byline').hide();
2407
2408 $popup.find('.king-addons-translator-form').html(progressHtml);
2409
2410 // Note: Close button events are handled by global document handler to prevent duplicates
2411 }
2412
2413 /**
2414 * Translate elements sequentially
2415 */
2416 function translateElementsSequentially(elements, index, $popup) {
2417 // Check if translation was cancelled
2418 if (translationState.isCancelled) {
2419 return;
2420 }
2421
2422 if (index >= elements.length) {
2423 showTranslationComplete($popup);
2424 return;
2425 }
2426
2427 var element = elements[index];
2428 translationState.currentElement = element;
2429
2430 // Update progress UI
2431 updateProgressUI(translationState.doneElementIds.length + 1, element);
2432
2433 // Highlight current element in preview
2434 highlightElementInPreview(element.elementId, true);
2435
2436 // Translate all text fields for this element
2437 translateElementFields(element, function(success) {
2438 // Remove highlight
2439 highlightElementInPreview(element.elementId, false);
2440
2441 // A fatal error stops the run from inside the request handler; do
2442 // not record the element or schedule the next one.
2443 if (translationState.isCancelled) {
2444 return;
2445 }
2446
2447 if (success) {
2448 translationState.translatedElements++;
2449 translationState.consecutiveFailures = 0;
2450 showElementSuccess(element.elementId);
2451 } else {
2452 translationState.failedElements++;
2453 translationState.consecutiveFailures++;
2454 translationState.failedElementIds.push(element.elementId);
2455 }
2456
2457 // Either way the element is behind us, so a resume skips it.
2458 translationState.doneElementIds.push(element.elementId);
2459 saveTranslationProgress();
2460
2461 // A long run of failures means the provider or model is unusable;
2462 // stop rather than working through the rest of the page for nothing.
2463 if (translationState.consecutiveFailures >= MAX_CONSECUTIVE_ELEMENT_FAILURES) {
2464 handleFatalTranslationError({
2465 code: 'unknown',
2466 message: (translationState.lastErrorMessage || 'Several elements failed in a row.')
2467 + '\n\nTranslation stopped after '
2468 + MAX_CONSECUTIVE_ELEMENT_FAILURES
2469 + ' consecutive failures. You can resume it later from where it stopped.'
2470 });
2471 return;
2472 }
2473
2474 // Continue with next element after a short delay
2475 setTimeout(function() {
2476 // Check if translation was cancelled before proceeding
2477 if (!translationState.isCancelled) {
2478 translateElementsSequentially(elements, index + 1, $popup);
2479 }
2480 }, 500);
2481 });
2482 }
2483
2484 /**
2485 * Translate all text fields for an element
2486 */
2487 function translateElementFields(element, callback) {
2488 // Check if translation was cancelled before starting
2489 if (translationState.isCancelled) {
2490 callback(false);
2491 return;
2492 }
2493
2494 var fieldsToTranslate = element.textFields.slice();
2495 var translatedFields = {};
2496 var completedFields = 0;
2497 var hasErrors = false;
2498
2499 if (fieldsToTranslate.length === 0) {
2500 callback(true);
2501 return;
2502 }
2503
2504 function translateNextField() {
2505 // Check if translation was cancelled before processing next field
2506 if (translationState.isCancelled) {
2507 callback(false);
2508 return;
2509 }
2510
2511 if (completedFields >= fieldsToTranslate.length) {
2512 // All fields translated, update the element
2513 if (Object.keys(translatedFields).length > 0 && !translationState.isCancelled) {
2514 updateElementSettings(element.container, translatedFields);
2515 }
2516 callback(!hasErrors);
2517 return;
2518 }
2519
2520 var field = fieldsToTranslate[completedFields];
2521 setActivity(element, completedFields + 1, fieldsToTranslate.length, field.value);
2522 translateSingleField(field.value, function(translatedText, success) {
2523 // Check if translation was cancelled while waiting for response
2524 if (translationState.isCancelled) {
2525 callback(false);
2526 return;
2527 }
2528
2529 if (success && translatedText) {
2530 translatedFields[field.field] = translatedText;
2531 } else {
2532 hasErrors = true;
2533 }
2534
2535 completedFields++;
2536 setTimeout(translateNextField, 200); // Small delay between field translations
2537 });
2538 }
2539
2540 translateNextField();
2541 }
2542
2543 /**
2544 * Translate a single text field
2545 */
2546 // How many times a temporary failure is retried before giving up on a field,
2547 // and how long to wait before each retry.
2548 var RETRY_DELAYS = [2000, 5000, 12000];
2549
2550 // A model that is throttled or down fails every field, so the run stops
2551 // rather than grinding through the whole page collecting failures.
2552 var MAX_CONSECUTIVE_ELEMENT_FAILURES = 5;
2553
2554 /**
2555 * Normalise a failed translation request into { code, message, retryable }.
2556 *
2557 * The server classifies provider failures and answers with a meaningful
2558 * status, but requests can also fail before reaching it (offline, proxy,
2559 * PHP fatal), so the status code is used as a fallback.
2560 */
2561 function parseTranslationError(response, xhr) {
2562 var data = null;
2563
2564 if (response && typeof response === 'object' && response.data) {
2565 data = response.data;
2566 } else if (xhr && xhr.responseJSON && xhr.responseJSON.data) {
2567 data = xhr.responseJSON.data;
2568 } else if (xhr && xhr.responseText) {
2569 try {
2570 var parsed = JSON.parse(xhr.responseText);
2571 data = parsed && parsed.data;
2572 } catch (e) {
2573 // Not JSON - fall back to the status code below.
2574 }
2575 }
2576
2577 // No xhr means the HTTP call itself succeeded and the body carried the
2578 // failure, so it must not be mistaken for a lost connection.
2579 var status = xhr && typeof xhr.status === 'number' ? xhr.status : (response ? 200 : 0);
2580 var message = '';
2581 var code = '';
2582 var retryable = null;
2583
2584 if (data && typeof data === 'object') {
2585 message = data.message || '';
2586 code = data.code || '';
2587 if (typeof data.retryable === 'boolean') {
2588 retryable = data.retryable;
2589 }
2590 } else if (typeof data === 'string') {
2591 message = data;
2592 }
2593
2594 if (!code) {
2595 if (status === 200) {
2596 code = 'unknown';
2597 } else if (status === 0) {
2598 code = 'network';
2599 } else if (status === 401 || status === 403) {
2600 code = 'auth';
2601 } else if (status === 402) {
2602 code = 'credits';
2603 } else if (status === 400 || status === 404) {
2604 code = 'model';
2605 } else if (status === 429) {
2606 code = 'rate_limit';
2607 } else if (status >= 500) {
2608 code = 'upstream';
2609 } else {
2610 code = 'unknown';
2611 }
2612 }
2613
2614 if (retryable === null) {
2615 retryable = (code === 'rate_limit' || code === 'upstream' || code === 'network');
2616 }
2617
2618 if (!message) {
2619 var fallbacks = {
2620 network: 'Network connection failed. Please check your internet connection.',
2621 auth: 'The API key is invalid or expired. Please check it in AI Settings.',
2622 credits: 'The AI provider reports insufficient credits.',
2623 model: 'The selected model was rejected by the provider. Pick another model in AI Settings.',
2624 rate_limit: 'Rate limit reached. Please wait a moment and try again.',
2625 daily_limit: 'The daily limit for this model has been reached.',
2626 upstream: 'The AI provider is temporarily unavailable. Please try again shortly.'
2627 };
2628 message = fallbacks[code] || 'Translation failed.';
2629 }
2630
2631 return { code: code, message: message, retryable: retryable, status: status };
2632 }
2633
2634 /**
2635 * Stop the run and explain why, choosing the popup that fits the cause.
2636 */
2637 function handleFatalTranslationError(error) {
2638 // Keep whatever has been translated so far resumable.
2639 saveTranslationProgress();
2640 stopTranslationProcess();
2641
2642 var providerLabel = (window.KingAddonsAiField && KingAddonsAiField.provider_label) || 'AI provider';
2643
2644 setTimeout(function() {
2645 if (error.code === 'auth') {
2646 showApiKeyError('Setup Required', error.message + '\n\nPlease check your API key in AI Settings and try again.');
2647 } else if (error.code === 'credits' || error.code === 'daily_limit'
2648 || error.code === 'rate_limit' || error.code === 'local_limit') {
2649 showTokenLimitError(error.message, error.code);
2650 } else if (error.code === 'model') {
2651 showApiKeyError('Model Not Available', error.message);
2652 } else {
2653 showApiKeyError('Run Stopped', error.message);
2654 }
2655 }, 500);
2656 }
2657
2658 /**
2659 * Show a short-lived note in the progress popup (retry countdown, warnings).
2660 */
2661 function setProgressNote(text) {
2662 var $note = $('#king-addons-progress-note');
2663 if (!$note.length) {
2664 return;
2665 }
2666 if (text) {
2667 $note.text(text).show();
2668 } else {
2669 $note.text('').hide();
2670 }
2671 }
2672
2673 /**
2674 * Translate a single text field, retrying temporary provider failures.
2675 */
2676 function translateSingleField(text, callback, attempt) {
2677 attempt = attempt || 0;
2678
2679 // Check if translation was cancelled before making request
2680 if (translationState.isCancelled) {
2681 callback(text, false);
2682 return;
2683 }
2684
2685 var request = $.post(KingAddonsAiField.ajax_url, {
2686 action: 'king_addons_ai_translate_text',
2687 nonce: KingAddonsAiField.generate_nonce,
2688 text: text,
2689 from_lang: translationState.fromLang,
2690 to_lang: translationState.toLang
2691 });
2692
2693 // Store the request so we can cancel it if needed
2694 translationState.currentRequests.push(request);
2695
2696 function releaseRequest() {
2697 var index = translationState.currentRequests.indexOf(request);
2698 if (index > -1) {
2699 translationState.currentRequests.splice(index, 1);
2700 }
2701 }
2702
2703 function onFailure(error) {
2704 if (translationState.isCancelled) {
2705 callback(text, false);
2706 return;
2707 }
2708
2709 // Temporary problem: wait and try the same field again.
2710 if (error.retryable && attempt < RETRY_DELAYS.length) {
2711 var delay = RETRY_DELAYS[attempt];
2712 setProgressNote('⏳ ' + error.message + ' Retrying in ' + Math.round(delay / 1000) + 's…');
2713
2714 setTimeout(function() {
2715 if (translationState.isCancelled) {
2716 callback(text, false);
2717 return;
2718 }
2719 setProgressNote('');
2720 translateSingleField(text, callback, attempt + 1);
2721 }, delay);
2722 return;
2723 }
2724
2725 setProgressNote('');
2726
2727 // A dead end (bad key, no credit, unusable model, daily cap) will
2728 // fail every remaining field, so stop instead of burning the page.
2729 var fatalCodes = ['auth', 'credits', 'model', 'daily_limit', 'rate_limit', 'local_limit'];
2730 if (fatalCodes.indexOf(error.code) > -1) {
2731 handleFatalTranslationError(error);
2732 return;
2733 }
2734
2735 // Anything else: give up on this field and let the run continue.
2736 translationState.lastErrorMessage = error.message;
2737 callback(text, false);
2738 }
2739
2740 request.done(function(response) {
2741 releaseRequest();
2742
2743 if (translationState.isCancelled) {
2744 callback(text, false);
2745 return;
2746 }
2747
2748 if (response && response.success && response.data && response.data.translated_text) {
2749 setProgressNote('');
2750 callback(response.data.translated_text, true);
2751 return;
2752 }
2753
2754 onFailure(parseTranslationError(response, null));
2755 }).fail(function(xhr, textStatus) {
2756 releaseRequest();
2757
2758 // An aborted request is a cancellation, not a provider failure.
2759 if (translationState.isCancelled || textStatus === 'abort') {
2760 return;
2761 }
2762
2763 onFailure(parseTranslationError(null, xhr));
2764 });
2765 }
2766
2767 /**
2768 * Update element settings with translated text
2769 */
2770 function updateElementSettings(container, translatedFields) {
2771 try {
2772 // Separate regular fields from repeater fields
2773 var regularFields = {};
2774 var repeaterUpdates = {};
2775
2776 Object.keys(translatedFields).forEach(function(fieldKey) {
2777 var translatedValue = translatedFields[fieldKey];
2778
2779 // Check if this is a repeater field
2780 var repeaterMatch = fieldKey.match(/^(.+)\[(\d+)\]\[(.+)\]$/);
2781 if (repeaterMatch) {
2782 // This is a repeater field: repeaterKey[index][fieldName]
2783 var repeaterKey = repeaterMatch[1];
2784 var itemIndex = parseInt(repeaterMatch[2]);
2785 var itemField = repeaterMatch[3];
2786
2787 if (!repeaterUpdates[repeaterKey]) {
2788 repeaterUpdates[repeaterKey] = {};
2789 }
2790 if (!repeaterUpdates[repeaterKey][itemIndex]) {
2791 repeaterUpdates[repeaterKey][itemIndex] = {};
2792 }
2793 repeaterUpdates[repeaterKey][itemIndex][itemField] = translatedValue;
2794 } else {
2795 // Regular field
2796 regularFields[fieldKey] = translatedValue;
2797 }
2798 });
2799
2800 // Apply regular field updates
2801 if (Object.keys(regularFields).length > 0) {
2802 $e.run('document/elements/settings', {
2803 container: container,
2804 settings: regularFields
2805 });
2806 }
2807
2808 // Apply repeater field updates
2809 Object.keys(repeaterUpdates).forEach(function(repeaterKey) {
2810 var currentSettings = container.settings.get(repeaterKey);
2811
2812 // Handle Backbone Collections (King Addons and some Elementor widgets)
2813 if (currentSettings && typeof currentSettings.models !== 'undefined') {
2814 // Work with Backbone collection
2815 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2816 var index = parseInt(itemIndex);
2817 if (currentSettings.models[index]) {
2818 var model = currentSettings.models[index];
2819
2820 // Update the specific fields in this repeater item
2821 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2822 var oldValue = model.get(fieldName);
2823 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2824
2825 // Update the model attribute
2826 model.set(fieldName, newValue);
2827 });
2828 }
2829 });
2830
2831 // Use Elementor's proper API to notify of changes instead of direct trigger
2832 try {
2833 // Method 1: Use Elementor's run command to update the entire repeater
2834 var backboneData = currentSettings.toJSON ? currentSettings.toJSON() :
2835 currentSettings.models.map(function(model) {
2836 return model.toJSON ? model.toJSON() : model.attributes;
2837 });
2838
2839 var repeaterSettings = {};
2840 repeaterSettings[repeaterKey] = backboneData;
2841
2842 $e.run('document/elements/settings', {
2843 container: container,
2844 settings: repeaterSettings
2845 });
2846 } catch (e) {
2847 // console.warn('⚠️ Error updating via Elementor API, trying alternative method:', e);
2848
2849 // Fallback: Try to manually trigger save without change events
2850 try {
2851 if (typeof container.saveSettings === 'function') {
2852 container.saveSettings();
2853 }
2854 } catch (e2) {
2855 // console.warn('⚠️ Fallback method also failed:', e2);
2856 }
2857 }
2858 }
2859 // Handle regular arrays (standard Elementor repeaters)
2860 else if (Array.isArray(currentSettings)) {
2861 var updatedRepeater = currentSettings.slice(); // Clone array
2862
2863 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2864 var index = parseInt(itemIndex);
2865 if (updatedRepeater[index]) {
2866 // Update the specific fields in this repeater item
2867 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2868 var oldValue = updatedRepeater[index][fieldName];
2869 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2870 updatedRepeater[index][fieldName] = newValue;
2871 });
2872 }
2873 });
2874
2875 // Update the entire repeater field
2876 var repeaterSettings = {};
2877 repeaterSettings[repeaterKey] = updatedRepeater;
2878
2879 $e.run('document/elements/settings', {
2880 container: container,
2881 settings: repeaterSettings
2882 });
2883 }
2884 // Handle objects with numbered keys
2885 else if (currentSettings && typeof currentSettings === 'object') {
2886 var updatedObject = Object.assign({}, currentSettings); // Clone object
2887
2888 Object.keys(repeaterUpdates[repeaterKey]).forEach(function(itemIndex) {
2889 if (updatedObject[itemIndex]) {
2890 // Update the specific fields in this repeater item
2891 Object.keys(repeaterUpdates[repeaterKey][itemIndex]).forEach(function(fieldName) {
2892 var oldValue = updatedObject[itemIndex][fieldName];
2893 var newValue = repeaterUpdates[repeaterKey][itemIndex][fieldName];
2894 updatedObject[itemIndex][fieldName] = newValue;
2895 });
2896 }
2897 });
2898
2899 // Update the entire repeater field
2900 var repeaterSettings = {};
2901 repeaterSettings[repeaterKey] = updatedObject;
2902
2903 $e.run('document/elements/settings', {
2904 container: container,
2905 settings: repeaterSettings
2906 });
2907 } else {
2908 }
2909 });
2910
2911 } catch (error) {
2912 // console.error('❌ Error updating element settings:', error);
2913 // console.error('Error details:', {
2914 // message: error.message,
2915 // stack: error.stack,
2916 // translatedFields: translatedFields,
2917 // widgetType: container.model.get('widgetType')
2918 // });
2919 }
2920 }
2921
2922 /**
2923 * Update progress UI
2924 */
2925 function updateProgressUI(current, element) {
2926 var percentage = (current / translationState.totalElements) * 100;
2927
2928 $('#king-addons-progress-count').text(current + ' / ' + translationState.totalElements);
2929 $('#king-addons-progress-fill').css('width', percentage + '%');
2930 setActivity(element, 0, element.textFields.length, '');
2931 }
2932
2933 /**
2934 * Describe what the translator is working on right now.
2935 *
2936 * @param {Object} element Element being processed.
2937 * @param {number} fieldIndex 1-based field position, 0 while starting out.
2938 * @param {number} fieldTotal Number of fields on the element.
2939 * @param {string} text Source text of the current field.
2940 */
2941 function setActivity(element, fieldIndex, fieldTotal, text) {
2942 var label = element ? element.widgetType : '';
2943 if (fieldTotal > 1 && fieldIndex > 0) {
2944 label += ' — field ' + fieldIndex + ' of ' + fieldTotal;
2945 }
2946 if (translationState.toLang) {
2947 label += ' → ' + describeLanguage(translationState.toLang);
2948 }
2949
2950 $('#king-addons-current-element').text(label);
2951
2952 // Showing the actual string makes a long run legible: you can see it
2953 // move rather than watching a counter that only ticks per element.
2954 var $snippet = $('#king-addons-progress-snippet');
2955 var plain = $('<div></div>').html(String(text || '')).text().replace(/\s+/g, ' ').trim();
2956 if (plain) {
2957 $snippet.text(plain.length > 160 ? plain.slice(0, 160) + '…' : plain).prop('hidden', false);
2958 } else {
2959 $snippet.text('').prop('hidden', true);
2960 }
2961 }
2962
2963 /**
2964 * Inject animation styles into preview iframe
2965 */
2966 function injectPreviewStyles() {
2967 if (!elementor || !elementor.$preview) return;
2968
2969 var $previewDoc = elementor.$preview.contents();
2970 var $previewHead = $previewDoc.find('head');
2971
2972 if ($previewHead.length && !$previewDoc.find('#king-addons-preview-translator-styles').length) {
2973 var previewStyles = `
2974 <style id="king-addons-preview-translator-styles">
2975 /* Element highlighting animation for translation */
2976 .king-addons-translating-element {
2977 position: relative !important;
2978 border: 3px solid #2196F3 !important;
2979 box-shadow: 0 0 20px rgba(33, 150, 243, 0.4) !important;
2980 border-radius: 4px !important;
2981 animation: king-addons-translate-pulse 1.5s infinite ease-in-out !important;
2982 z-index: 999 !important;
2983 }
2984
2985 .king-addons-translating-element::before {
2986 content: "🔄 Translating..." !important;
2987 position: absolute !important;
2988 top: -35px !important;
2989 left: 50% !important;
2990 transform: translateX(-50%) !important;
2991 background: #2196F3 !important;
2992 color: white !important;
2993 padding: 6px 12px !important;
2994 border-radius: 20px !important;
2995 font-size: 12px !important;
2996 font-weight: 600 !important;
2997 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
2998 z-index: 10000 !important;
2999 animation: king-addons-translate-bounce 0.8s ease-out !important;
3000 box-shadow: 0 3px 10px rgba(33, 150, 243, 0.3) !important;
3001 white-space: nowrap !important;
3002 }
3003
3004 .king-addons-translated-element {
3005 position: relative !important;
3006 border: 3px solid #4CAF50 !important;
3007 box-shadow: 0 0 20px rgba(76, 175, 80, 0.4) !important;
3008 border-radius: 4px !important;
3009 animation: king-addons-translate-success 1.2s ease-out !important;
3010 z-index: 999 !important;
3011 }
3012
3013 .king-addons-translated-element::before {
3014 content: "�
3015 Translated!" !important;
3016 position: absolute !important;
3017 top: -35px !important;
3018 left: 50% !important;
3019 transform: translateX(-50%) !important;
3020 background: #4CAF50 !important;
3021 color: white !important;
3022 padding: 6px 12px !important;
3023 border-radius: 20px !important;
3024 font-size: 12px !important;
3025 font-weight: 600 !important;
3026 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
3027 z-index: 10000 !important;
3028 animation: king-addons-translate-bounce 0.8s ease-out !important;
3029 box-shadow: 0 3px 10px rgba(76, 175, 80, 0.3) !important;
3030 white-space: nowrap !important;
3031 }
3032
3033 /* Pulsing animation for translating elements */
3034 @keyframes king-addons-translate-pulse {
3035 0% {
3036 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.7),
3037 0 0 20px rgba(33, 150, 243, 0.4);
3038 transform: scale(1);
3039 }
3040 50% {
3041 box-shadow: 0 0 0 8px rgba(33, 150, 243, 0.2),
3042 0 0 30px rgba(33, 150, 243, 0.6);
3043 transform: scale(1.02);
3044 }
3045 100% {
3046 box-shadow: 0 0 0 0 rgba(33, 150, 243, 0),
3047 0 0 20px rgba(33, 150, 243, 0.4);
3048 transform: scale(1);
3049 }
3050 }
3051
3052 /* Success animation for completed elements */
3053 @keyframes king-addons-translate-success {
3054 0% {
3055 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7),
3056 0 0 20px rgba(76, 175, 80, 0.4);
3057 transform: scale(1);
3058 }
3059 20% {
3060 box-shadow: 0 0 0 12px rgba(76, 175, 80, 0.3),
3061 0 0 40px rgba(76, 175, 80, 0.6);
3062 transform: scale(1.05);
3063 }
3064 40% {
3065 transform: scale(0.98);
3066 }
3067 60% {
3068 transform: scale(1.02);
3069 }
3070 80% {
3071 transform: scale(0.99);
3072 }
3073 100% {
3074 box-shadow: 0 0 0 0 rgba(76, 175, 80, 0),
3075 0 0 20px rgba(76, 175, 80, 0.2);
3076 transform: scale(1);
3077 }
3078 }
3079
3080 /* Bounce animation for labels */
3081 @keyframes king-addons-translate-bounce {
3082 0% {
3083 transform: translateX(-50%) translateY(-10px) scale(0.8);
3084 opacity: 0;
3085 }
3086 50% {
3087 transform: translateX(-50%) translateY(-2px) scale(1.1);
3088 opacity: 1;
3089 }
3090 70% {
3091 transform: translateX(-50%) translateY(-1px) scale(0.95);
3092 }
3093 100% {
3094 transform: translateX(-50%) translateY(0) scale(1);
3095 opacity: 1;
3096 }
3097 }
3098 </style>
3099 `;
3100 $previewHead.append(previewStyles);
3101 }
3102 }
3103
3104 /**
3105 * Highlight element in preview
3106 */
3107 function highlightElementInPreview(elementId, highlight) {
3108 // Ensure preview styles are injected
3109 injectPreviewStyles();
3110
3111 // Find element in preview iframe
3112 if (!elementor || !elementor.$preview) {
3113 return;
3114 }
3115
3116 var $previewDoc = elementor.$preview.contents();
3117 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
3118
3119 if ($previewElement.length === 0) {
3120 return;
3121 }
3122
3123 if (highlight) {
3124 // Remove any existing classes first
3125 $previewElement.removeClass('king-addons-translated-element');
3126 $previewElement.addClass('king-addons-translating-element');
3127 scrollPreviewToElement($previewElement);
3128 } else {
3129 $previewElement.removeClass('king-addons-translating-element');
3130 }
3131 }
3132
3133 /**
3134 * Bring the element being translated into view inside the preview.
3135 *
3136 * A long page otherwise translates itself off screen, so the highlight and
3137 * the success animation are never actually seen.
3138 *
3139 * The preview iframe is not scrolled by plain window.scrollTo - Elementor
3140 * drives it itself - so its own helper is used, the same one the Navigator
3141 * uses to jump to a widget. It already skips elements that are in view and
3142 * animates the rest.
3143 *
3144 * @param {jQuery} $element Element inside the preview document.
3145 */
3146 function scrollPreviewToElement($element) {
3147 if (!$element || !$element.length) {
3148 return;
3149 }
3150
3151 try {
3152 if (elementor.helpers && typeof elementor.helpers.scrollToView === 'function') {
3153 // Second argument is the delay before scrolling; the default
3154 // half second would lag behind a fast run.
3155 elementor.helpers.scrollToView($element, 0);
3156 return;
3157 }
3158 } catch (e) {
3159 // Fall through to the native path below.
3160 }
3161
3162 try {
3163 $element[0].scrollIntoView({
3164 behavior: prefersReducedMotion() ? 'auto' : 'smooth',
3165 block: 'center'
3166 });
3167 } catch (e) {
3168 // A torn-down preview must not break the run.
3169 }
3170 }
3171
3172 /**
3173 * Whether the viewer asked for less animation.
3174 */
3175 function prefersReducedMotion() {
3176 try {
3177 return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
3178 } catch (e) {
3179 return false;
3180 }
3181 }
3182
3183 /**
3184 * Show element success animation
3185 */
3186 function showElementSuccess(elementId) {
3187 // Ensure preview styles are injected
3188 injectPreviewStyles();
3189
3190 // Find element in preview iframe
3191 if (!elementor || !elementor.$preview) {
3192 return;
3193 }
3194
3195 var $previewDoc = elementor.$preview.contents();
3196 var $previewElement = $previewDoc.find('[data-id="' + elementId + '"]');
3197
3198 if ($previewElement.length === 0) {
3199 return;
3200 }
3201
3202 // Remove translating class and add translated class
3203 $previewElement.removeClass('king-addons-translating-element');
3204 $previewElement.addClass('king-addons-translated-element');
3205
3206 // Remove the success animation after it completes
3207 setTimeout(function() {
3208 $previewElement.removeClass('king-addons-translated-element');
3209 }, 1200);
3210 }
3211
3212 /**
3213 * Show translation complete with stats (stays open until manually closed)
3214 */
3215 function showTranslationComplete($popup) {
3216 translationState.isTranslating = false;
3217
3218 var failedIds = translationState.failedElementIds.slice();
3219
3220 // Elements that failed are worth another attempt - a model can refuse
3221 // one string and handle it fine on a retry. Keeping a progress entry
3222 // that marks everything except the failures as done turns the normal
3223 // Resume path into "retry just the ones that failed".
3224 if (failedIds.length) {
3225 translationState.doneElementIds = translationState.doneElementIds.filter(function(id) {
3226 return failedIds.indexOf(id) === -1;
3227 });
3228 saveTranslationProgress();
3229 } else {
3230 // The page is done, so there is nothing left to resume.
3231 clearTranslationProgress();
3232 }
3233
3234 if (!$popup || $popup.length === 0) {
3235 // console.error('❌ Cannot show translation results: popup not found');
3236 return;
3237 }
3238
3239 var statsHtml = `
3240 <div class="king-addons-translator-progress">
3241 <div class="king-addons-translator-stats">
3242 <div class="king-addons-translator-stat">
3243 <div class="king-addons-translator-stat-number">${translationState.totalElements}</div>
3244 <div class="king-addons-translator-stat-label">Elements</div>
3245 </div>
3246 <div class="king-addons-translator-stat">
3247 <div class="king-addons-translator-stat-number" style="color: var(--ka-tr-success);">${translationState.translatedElements}</div>
3248 <div class="king-addons-translator-stat-label">Translated</div>
3249 </div>
3250 <div class="king-addons-translator-stat">
3251 <div class="king-addons-translator-stat-number" style="color: ${translationState.failedElements > 0 ? 'var(--ka-tr-danger)' : 'var(--ka-tr-ink-muted)'};">${translationState.failedElements}</div>
3252 <div class="king-addons-translator-stat-label">Failed</div>
3253 </div>
3254 </div>
3255 ${failedIds.length ? `
3256 <div class="ka-tr-panel ka-tr-panel--warning" style="margin-top: 16px;">
3257 <p>${failedIds.length} element${failedIds.length === 1 ? '' : 's'} could not be translated${
3258 translationState.lastErrorMessage
3259 ? ': ' + $('<div></div>').text(translationState.lastErrorMessage).html()
3260 : '.'
3261 }</p>
3262 </div>` : ''}
3263 <div class="king-addons-translator-actions" style="margin-top: 20px;">
3264 <button class="king-addons-translator-btn-secondary" id="king-addons-close-stats">Close</button>
3265 ${failedIds.length ? '<button class="king-addons-translator-btn-primary" id="king-addons-retry-failed">Retry failed</button>' : ''}
3266 </div>
3267 </div>
3268 `;
3269
3270 var $form = $popup.find('.king-addons-translator-form');
3271
3272 $form.html(statsHtml);
3273
3274 if (!failedIds.length) {
3275 $form.find('#king-addons-close-stats')
3276 .removeClass('king-addons-translator-btn-secondary')
3277 .addClass('king-addons-translator-btn-primary');
3278 }
3279
3280 $form.find('#king-addons-retry-failed').on('click', function() {
3281 var saved = loadTranslationProgress();
3282 $popup.closest('.king-addons-translator-overlay').remove();
3283 $popup.remove();
3284 if (saved) {
3285 showResumePopup(saved);
3286 }
3287 });
3288
3289 // Play success sound (Web Audio API)
3290 try {
3291 var audioContext = new (window.AudioContext || window.webkitAudioContext)();
3292 var oscillator = audioContext.createOscillator();
3293 var gainNode = audioContext.createGain();
3294
3295 oscillator.connect(gainNode);
3296 gainNode.connect(audioContext.destination);
3297
3298 oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
3299 oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1);
3300 oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.2);
3301
3302 gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
3303 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
3304
3305 oscillator.start(audioContext.currentTime);
3306 oscillator.stop(audioContext.currentTime + 0.3);
3307 } catch (e) {
3308 }
3309
3310 // Show temporary notification to attract attention
3311 var $notificationBanner = $('<div style="position: fixed; top: 0; left: 0; right: 0; background: #10794a; color: #fff; padding: 12px; text-align: center; font-size: 14px; font-weight: 600; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; z-index: 1000000; animation: slideDown 0.4s ease;">Your page is ready &mdash; see the results panel.</div>');
3312 $('body').append($notificationBanner);
3313
3314 // Remove notification after 5 seconds
3315 setTimeout(function() {
3316 $notificationBanner.fadeOut(300, function() {
3317 $notificationBanner.remove();
3318 });
3319 }, 5000);
3320
3321 // Update header to show completion
3322 var completionHeaderHtml = `
3323 <img src="${KingAddonsAiField.plugin_url}includes/admin/img/ai.svg" style="width:20px;height:20px;filter: invert(1);"/>
3324 Your page is ready
3325 `;
3326 $popup.find('h3').html(completionHeaderHtml);
3327
3328 // Restore the byline, and rewrite the description for the result - the
3329 // popup may have started life as the resume prompt, whose subtitle no
3330 // longer applies.
3331 $popup.find('.ka-tr-byline').show();
3332 $popup.find('.ka-tr-dialog-sub')
3333 .text('Into ' + describeLanguage(translationState.toLang) + '.')
3334 .show();
3335
3336 // If popup is in compact mode, move it back to center for better visibility
3337 if ($popup.hasClass('compact')) {
3338 // Remove compact class and positioning
3339 $popup.removeClass('compact moving');
3340 $popup.css({
3341 'position': 'fixed',
3342 'top': '50%',
3343 'left': '50%',
3344 'transform': 'translate(-50%, -50%)',
3345 'right': 'auto',
3346 'bottom': 'auto',
3347 'width': '500px',
3348 'max-width': '90vw',
3349 'z-index': '999999',
3350 'background': 'white',
3351 'border-radius': '8px',
3352 'box-shadow': '0 10px 25px rgba(0,0,0,0.2)',
3353 'opacity': '1',
3354 'visibility': 'visible'
3355 });
3356
3357 // Re-add overlay if it doesn't exist
3358 if (!$popup.closest('.king-addons-translator-overlay').length) {
3359 var $overlay = $('<div class="king-addons-translator-overlay"></div>').css({
3360 'position': 'fixed',
3361 'top': '0',
3362 'left': '0',
3363 'width': '100%',
3364 'height': '100%',
3365 'background': 'rgba(0, 0, 0, 0.5)',
3366 'z-index': '999998',
3367 'display': 'flex',
3368 'align-items': 'center',
3369 'justify-content': 'center'
3370 });
3371 $popup.wrap($overlay);
3372 } else {
3373 // Make sure existing overlay is visible
3374 $popup.closest('.king-addons-translator-overlay').css({
3375 'z-index': '999998',
3376 'display': 'flex'
3377 });
3378 }
3379
3380 // Add entrance animation
3381 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
3382 } else {
3383 // For non-compact popups, ensure they're also properly visible
3384 $popup.css({
3385 'z-index': '999999',
3386 'opacity': '1',
3387 'visibility': 'visible',
3388 'position': 'fixed'
3389 });
3390
3391 // Make sure overlay is visible
3392 var $overlay = $popup.closest('.king-addons-translator-overlay');
3393 if ($overlay.length) {
3394 $overlay.css({
3395 'z-index': '999998',
3396 'display': 'block',
3397 'opacity': '1',
3398 'visibility': 'visible'
3399 });
3400 }
3401
3402 // Add entrance animation
3403 $popup.css('opacity', '0').animate({'opacity': '1'}, 300);
3404 }
3405
3406 $('#king-addons-close-stats').on('click', function() {
3407 // For compact popup, just remove it directly since overlay is already gone
3408 if ($popup.hasClass('compact')) {
3409 $popup.remove();
3410 } else {
3411 $popup.closest('.king-addons-translator-overlay').remove();
3412 }
3413 });
3414
3415 // Reset translation state and re-enable button
3416 translationState.isTranslating = false;
3417 translationState.isCancelled = false;
3418 translationState.currentRequests = []; // Clear any remaining requests
3419 toggleTranslatorButton(false);
3420
3421 // Note: Auto-close removed by user request - popup stays open until manually closed
3422 }
3423
3424 /**
3425 * Handle Elementor initialization
3426 */
3427 function onElementorInit() {
3428 // Check if AI Page Translator is enabled
3429 if (typeof KingAddonsAiField !== 'undefined' && KingAddonsAiField.translator_enabled === false) {
3430 return;
3431 }
3432
3433 // Inject styles first
3434 injectTranslatorStyles();
3435
3436 // Try to inject preview styles (will work when preview is available)
3437 setTimeout(function() {
3438 injectPreviewStyles();
3439 }, 1000);
3440
3441 // Add button immediately
3442 addTranslatorButton();
3443
3444 // Also add button when panel opens
3445 if (typeof elementor !== 'undefined' && elementor.hooks) {
3446 elementor.hooks.addAction('panel/open_editor/widget', function() {
3447 setTimeout(addTranslatorButton, 100);
3448 });
3449
3450 // Add button when navigator opens
3451 elementor.hooks.addAction('navigator/init', function() {
3452 setTimeout(addTranslatorButton, 100);
3453 });
3454
3455 // Inject preview styles when preview loads
3456 elementor.hooks.addAction('preview/loaded', function() {
3457 injectPreviewStyles();
3458 });
3459 }
3460
3461 // Monitor for panel changes
3462 observePanelChanges();
3463
3464 // Surface an interrupted run once the document is available.
3465 setTimeout(offerResumeOnLoad, 1500);
3466 }
3467
3468 /**
3469 * Observe panel changes to re-add button if needed
3470 */
3471 function observePanelChanges() {
3472 function createObserver() {
3473 return new MutationObserver(function(mutations) {
3474 var shouldCheck = false;
3475
3476 mutations.forEach(function(mutation) {
3477 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
3478 shouldCheck = true;
3479 }
3480 });
3481
3482 if (shouldCheck) {
3483 setTimeout(addTranslatorButton, 300);
3484 }
3485 });
3486 }
3487
3488 // Observe changes in the top toolbar (priority)
3489 var topToolbar = document.querySelector('#elementor-editor-wrapper-v2 .MuiToolbar-root');
3490 if (topToolbar) {
3491 var topObserver = createObserver();
3492 topObserver.observe(topToolbar, {
3493 childList: true,
3494 subtree: true
3495 });
3496 }
3497
3498 // Also observe the main editor wrapper for structural changes
3499 var editorWrapper = document.querySelector('#elementor-editor-wrapper-v2');
3500 if (editorWrapper) {
3501 var wrapperObserver = createObserver();
3502 wrapperObserver.observe(editorWrapper, {
3503 childList: true,
3504 subtree: false
3505 });
3506 }
3507
3508 // Observe changes in the main panel (fallback)
3509 var panel = document.querySelector('#elementor-panel');
3510 if (panel) {
3511 var panelObserver = createObserver();
3512 panelObserver.observe(panel, {
3513 childList: true,
3514 subtree: true
3515 });
3516 }
3517
3518 // Observe the main Elementor editor area
3519 var editorArea = document.querySelector('#elementor-editor-wrapper, .elementor-editor-wrapper');
3520 if (editorArea) {
3521 var editorObserver = createObserver();
3522 editorObserver.observe(editorArea, {
3523 childList: true,
3524 subtree: true
3525 });
3526 }
3527 }
3528
3529 /**
3530 * Initialize the translator
3531 */
3532 function initTranslator() {
3533 // Wait for Elementor to be fully loaded
3534 $(window).on('elementor:init', function() {
3535 // Add small delay to ensure Material UI is rendered
3536 setTimeout(onElementorInit, 500);
3537 });
3538
3539 // Fallback if elementor:init doesn't fire
3540 setTimeout(function() {
3541 onElementorInit();
3542 }, 3000);
3543
3544 // Additional fallback for when Material UI components are ready
3545 setTimeout(function() {
3546 if (!document.querySelector('.king-addons-ai-translator-btn')) {
3547 onElementorInit();
3548 }
3549 }, 5000);
3550 }
3551
3552 // Initialize when DOM is ready
3553 $(document).ready(function() {
3554 initTranslator();
3555
3556 // Global event handler for close buttons (backup protection)
3557 $(document).off('click.aiTranslatorGlobal').on('click.aiTranslatorGlobal', '.king-addons-translator-close-btn', function(e) {
3558 if (translationState.isTranslating) {
3559 stopTranslationProcess();
3560
3561 // Show cancellation notice
3562 var $notice = $('<div style="position: fixed; top: 120px; right: 20px; background: #ff9800; color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; z-index: 1000000;">Run cancelled</div>');
3563 $('body').append($notice);
3564 setTimeout(function() {
3565 $notice.fadeOut(300, function() {
3566 $notice.remove();
3567 });
3568 }, 2000);
3569 }
3570
3571 // Close popup/overlay
3572 var $popup = $(this).closest('.king-addons-translator-popup');
3573 if ($popup.hasClass('compact')) {
3574 $popup.remove();
3575 } else {
3576 $popup.closest('.king-addons-translator-overlay').remove();
3577 }
3578
3579 // Reset state and re-enable button. Saved progress is deliberately
3580 // kept so a cancelled run can be resumed from the same place.
3581 translationState.isTranslating = false;
3582 translationState.isCancelled = false;
3583 translationState.currentRequests = [];
3584 toggleTranslatorButton(false);
3585
3586 e.preventDefault();
3587 e.stopPropagation();
3588 });
3589 });
3590
3591 })(jQuery, window.elementor);