PluginProbe
wpForo Forum / 3.1.6
wpForo Forum v3.1.6
3.1.6 3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 All 138 releases
wpforo / assets / js / ai-features.js

ai-features.js in wpForo Forum 3.1.6, at assets/js/ai-features.js

1,438 lines 52.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global wpforo, $wpf, wpforo_phrase, wpforo_load_hide */
2 /**
3 * wpForo AI Features JavaScript
4 *
5 * AI-powered search and helper functionality
6 * Requires: frontend.js to be loaded first for $wpf, wpforo_phrase, wpforo_load_hide
7 */
8
9 $wpf(document).ready(function ($) {
10 var wpforo_wrap = $('#wpforo-wrap');
11
12 // =========================================================================
13 // AI HELPER TOGGLE & TABS
14 // =========================================================================
15
16 // AI Helper Toggle
17 wpforo_wrap.on('click', '.wpf-ai-helper-toggle', function () {
18 var helper = $(this).closest('.wpforo-ai-helper');
19 helper.toggleClass('wpf-ai-open');
20 $('.wpf-ai-helper-content', helper).slideToggle(350);
21 });
22
23 // AI Helper Close Button
24 wpforo_wrap.on('click', '.wpf-ai-tab-close', function () {
25 var helper = $(this).closest('.wpforo-ai-helper');
26 helper.removeClass('wpf-ai-open');
27 $('.wpf-ai-helper-content', helper).slideUp(350);
28 });
29
30 // AI Helper Tab Switching
31 wpforo_wrap.on('click', '.wpf-ai-tab', function () {
32 var tab = $(this);
33 var tabId = tab.data('tab');
34 var helper = tab.closest('.wpf-ai-helper-inner');
35 // Switch active tab
36 $('.wpf-ai-tab', helper).removeClass('wpf-ai-tab-active');
37 tab.addClass('wpf-ai-tab-active');
38 // Switch active content
39 $('.wpf-ai-tab-content', helper).removeClass('wpf-ai-tab-content-active');
40 $('.wpf-ai-tab-content[data-tab-content="' + tabId + '"]', helper).addClass('wpf-ai-tab-content-active');
41 });
42
43 // AI Search Mode Toggle (switch between AI Search and Classic Search forms)
44 wpforo_wrap.on('click', '.wpf-ai-search-mode', function () {
45 var mode = $(this);
46 var modeId = mode.data('mode');
47 var tabContent = mode.closest('.wpf-ai-tab-content');
48 // Switch active mode
49 $('.wpf-ai-search-mode', tabContent).removeClass('wpf-ai-search-mode-active');
50 mode.addClass('wpf-ai-search-mode-active');
51 // Switch active form
52 $('.wpf-ai-search-form', tabContent).removeClass('wpf-ai-search-form-active');
53 $('.wpf-ai-search-form[data-search-form="' + modeId + '"]', tabContent).addClass('wpf-ai-search-form-active');
54 });
55
56 // =========================================================================
57 // AI PREFERENCES
58 // =========================================================================
59
60 // Save AI Preferences
61 wpforo_wrap.on('submit', '.wpf-ai-preferences-form', function (e) {
62 e.preventDefault();
63 wpforo_load_hide();
64
65 var form = $(this);
66 var saveBtn = form.find('.wpf-ai-pref-save');
67 var statusEl = form.find('.wpf-ai-pref-status');
68
69 // Get form values
70 var language = form.find('#wpf-ai-pref-language').val();
71 var maxResults = parseInt(form.find('#wpf-ai-pref-max-results').val(), 10);
72
73 // Client-side validation (max 10 results)
74 if (maxResults < 1) maxResults = 1;
75 if (maxResults > 10) maxResults = 10;
76 form.find('#wpf-ai-pref-max-results').val(maxResults);
77
78 // Disable button during save
79 saveBtn.prop('disabled', true).text(wpforo_phrase('Saving...'));
80 statusEl.removeClass('success error').text('');
81
82 // AJAX save
83 $.ajax({
84 url: wpforo.ajax_url,
85 type: 'POST',
86 data: {
87 action: 'wpforo_save_ai_preferences',
88 nonce: form.find('#wpf_ai_pref_nonce').val(),
89 language: language,
90 max_results: maxResults
91 },
92 success: function (response) {
93 saveBtn.prop('disabled', false).text(wpforo_phrase('Save Preferences'));
94 if (response.success) {
95 statusEl.addClass('success').text(wpforo_phrase('Preferences saved!'));
96 // Update the search preferences for current session
97 if (typeof aiSearchLimit !== 'undefined') {
98 aiSearchLimit = maxResults;
99 }
100 if (typeof aiSearchLanguage !== 'undefined') {
101 aiSearchLanguage = language;
102 }
103 } else {
104 statusEl.addClass('error').text(response.data || wpforo_phrase('Error saving preferences'));
105 }
106 // Clear status after 3 seconds
107 setTimeout(function () {
108 statusEl.removeClass('success error').text('');
109 }, 3000);
110 },
111 error: function (xhr) {
112 saveBtn.prop('disabled', false).text(wpforo_phrase('Save Preferences'));
113 // Try to get error message from response
114 var errorMsg = wpforo_phrase('Network error. Please try again.');
115 try {
116 var response = JSON.parse(xhr.responseText);
117 if (response.data && response.data.message) {
118 errorMsg = response.data.message;
119 }
120 } catch (e) {
121 // Keep default error message
122 }
123 statusEl.addClass('error').text(errorMsg);
124 }
125 });
126 });
127
128 // =========================================================================
129 // AI SEMANTIC SEARCH
130 // =========================================================================
131
132 var aiSearchOffset = 0;
133 var aiSearchQuery = '';
134 var aiSearchLimit = 5;
135 var aiSearchLanguage = 'en_US';
136
137 // Load preferences from data attribute (set by PHP with settings hierarchy)
138 var aiHelper = $('.wpforo-ai-helper');
139 if (aiHelper.length && aiHelper.data('ai-preferences')) {
140 var prefs = aiHelper.data('ai-preferences');
141 if (prefs.max_results) {
142 aiSearchLimit = parseInt(prefs.max_results, 10) || 5;
143 }
144 if (prefs.language) {
145 aiSearchLanguage = prefs.language;
146 }
147 }
148
149 // AI Loading animation messages
150 var aiLoadingMessages = [
151 'Searching forum content...',
152 'Analyzing with AI...',
153 'Finding relevant discussions...',
154 'Processing semantic matches...',
155 'Generating AI summary...',
156 'Ranking results by relevance...',
157 'Almost ready...'
158 ];
159 var aiLoadingInterval = null;
160
161 function wpforoShowAiLoading(container) {
162 var loadingHtml = '<div class="wpf-ai-loading">' +
163 '<div class="wpf-ai-loading-animation">' +
164 '<div class="wpf-ai-loading-stars">' +
165 '<svg class="wpf-ai-star wpf-ai-star-1" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
166 '<svg class="wpf-ai-star wpf-ai-star-2" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
167 '<svg class="wpf-ai-star wpf-ai-star-3" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
168 '</div>' +
169 '</div>' +
170 '<div class="wpf-ai-loading-text">' + wpforo_phrase(aiLoadingMessages[0]) + '</div>' +
171 '</div>';
172 container.html(loadingHtml);
173 container.closest('.wpf-ai-results').show();
174
175 // Rotate through loading messages
176 var msgIndex = 0;
177 aiLoadingInterval = setInterval(function() {
178 msgIndex = (msgIndex + 1) % aiLoadingMessages.length;
179 container.find('.wpf-ai-loading-text').text(wpforo_phrase(aiLoadingMessages[msgIndex]));
180 }, 3000);
181 }
182
183 function wpforoHideAiLoading() {
184 if (aiLoadingInterval) {
185 clearInterval(aiLoadingInterval);
186 aiLoadingInterval = null;
187 }
188 }
189
190 // AI Search Form Submit Handler
191 wpforo_wrap.on('submit', '.wpf-ai-form', function (e) {
192 e.preventDefault();
193 wpforo_load_hide();
194 var form = $(this);
195 var input = form.find('.wpf-ai-input');
196 var query = input.val().trim();
197 var submitBtn = form.find('.wpf-ai-submit');
198 var resultsWrap = form.closest('.wpf-ai-search-form').find('.wpf-ai-results');
199 var resultsList = resultsWrap.find('.wpf-ai-results-list');
200 var moreBtn = resultsWrap.find('.wpf-ai-results-more');
201
202 if (!query) return;
203
204 // Reset for new search
205 aiSearchOffset = 0;
206 aiSearchQuery = query;
207
208 // Show loading state on button
209 submitBtn.find('i').removeClass('fa-search').addClass('fa-spinner fa-spin');
210 submitBtn.prop('disabled', true);
211
212 // Show AI loading animation in results area
213 wpforoShowAiLoading(resultsList);
214 moreBtn.hide();
215
216 // Perform search
217 wpforoAiSearch(query, aiSearchLimit, aiSearchOffset, function (response) {
218 // Stop loading animation
219 wpforoHideAiLoading();
220 // Restore button
221 submitBtn.find('i').removeClass('fa-spinner fa-spin').addClass('fa-search');
222 submitBtn.prop('disabled', false);
223
224 if (response.success && response.data.results.length > 0) {
225 // Render AI enhancement (Summary + Recommendations) first, then results
226 // Pass results to enhancement renderer for resolving [[#N]] link markers
227 var enhancementHtml = wpforoRenderAiEnhancement(response.data.ai_enhancement, response.data.results);
228 var resultsHtml = wpforoRenderAiResults(response.data.results);
229 resultsList.html(enhancementHtml + resultsHtml);
230 resultsWrap.show();
231
232 // Hide recommendations and results until summary typewriter completes
233 var recsSection = resultsList.find('.wpf-ai-recommendations-section');
234 var resultsSection = resultsList.find('.wpf-ai-result-wrapper');
235 recsSection.hide();
236 resultsSection.hide();
237
238 // Trigger typewriter effect for AI summary
239 // When complete, reveal the other sections with animation
240 var typewriterEl = resultsList.find('.wpf-ai-typewriter')[0];
241 if (typewriterEl) {
242 var content = typewriterEl.getAttribute('data-typewriter-content');
243 if (content) {
244 wpforoTypewriterEffect(typewriterEl, content, 10, function() {
245 // Summary complete - now show recommendations and results
246 recsSection.slideDown(300);
247 setTimeout(function() {
248 resultsSection.slideDown(400);
249 }, 150);
250 });
251 } else {
252 // No summary content - show sections immediately
253 recsSection.show();
254 resultsSection.show();
255 }
256 } else {
257 // No typewriter element - show sections immediately
258 recsSection.show();
259 resultsSection.show();
260 }
261
262 // Show/hide more button
263 if (response.data.has_more) {
264 moreBtn.show();
265 } else {
266 moreBtn.hide();
267 }
268
269 aiSearchOffset = aiSearchLimit;
270 } else if (response.success && response.data.no_indexed_content) {
271 resultsList.html('<div class="wpf-ai-no-results"><i class="fas fa-info-circle"></i><p>' + response.data.message + '</p></div>');
272 resultsWrap.show();
273 moreBtn.hide();
274 } else if (response.success && response.data.results.length === 0) {
275 resultsList.html('<div class="wpf-ai-no-results"><i class="fas fa-search"></i><p>' + wpforo_phrase('No results found') + '</p></div>');
276 resultsWrap.show();
277 moreBtn.hide();
278 } else {
279 resultsList.html('<div class="wpf-ai-error"><i class="fas fa-exclamation-circle"></i><p>' + (response.data ? response.data.message : wpforo_phrase('Search failed')) + '</p></div>');
280 resultsWrap.show();
281 moreBtn.hide();
282 }
283 });
284 });
285
286 // More Results button
287 wpforo_wrap.on('click', '.wpf-ai-more-btn', function () {
288 var btn = $(this);
289 var resultsWrap = btn.closest('.wpf-ai-results');
290 var resultsList = resultsWrap.find('.wpf-ai-results-list');
291
292 // Show loading
293 btn.prop('disabled', true).text(wpforo_phrase('Loading...'));
294
295 wpforoAiSearch(aiSearchQuery, aiSearchLimit, aiSearchOffset, function (response) {
296 btn.prop('disabled', false).text(wpforo_phrase('More Results'));
297
298 if (response.success && response.data.results.length > 0) {
299 resultsList.append(wpforoRenderAiResults(response.data.results));
300 aiSearchOffset += aiSearchLimit;
301
302 if (!response.data.has_more) {
303 btn.parent().hide();
304 }
305 }
306 });
307 });
308
309 // AI Search AJAX function
310 function wpforoAiSearch(query, limit, offset, callback) {
311 $.ajax({
312 url: wpforo.ajax_url,
313 type: 'POST',
314 data: {
315 action: 'wpforo_ai_public_search',
316 query: query,
317 limit: limit,
318 offset: offset,
319 language: aiSearchLanguage,
320 _wpnonce: wpforo.nonces.wpforo_ai_public_search
321 },
322 success: callback,
323 error: function (xhr) {
324 // Try to get error message from response (handles 429 rate limit errors)
325 var errorMsg = wpforo_phrase('Request failed');
326 try {
327 var response = JSON.parse(xhr.responseText);
328 if (response.data && response.data.message) {
329 errorMsg = response.data.message;
330 }
331 } catch (e) {
332 // Keep default error message
333 }
334 callback({ success: false, data: { message: errorMsg } });
335 }
336 });
337 }
338
339 // =========================================================================
340 // TYPEWRITER EFFECT
341 // =========================================================================
342
343 // Typewriter effect for AI summary text
344 // onComplete callback fires when typing is finished
345 function wpforoTypewriterEffect(element, html, speed, onComplete) {
346 if (!element || !html) {
347 if (onComplete) onComplete();
348 return;
349 }
350 speed = speed || 15; // milliseconds per character
351
352 // Parse HTML to extract text nodes and tags
353 var tempDiv = document.createElement('div');
354 tempDiv.innerHTML = html;
355
356 element.innerHTML = '';
357 element.style.visibility = 'visible';
358
359 // Recursive function to type through DOM nodes
360 function typeNode(node, callback) {
361 if (node.nodeType === Node.TEXT_NODE) {
362 // Text node - type character by character
363 var text = node.textContent;
364 var textNode = document.createTextNode('');
365 element.appendChild(textNode);
366 var charIndex = 0;
367
368 function typeChar() {
369 if (charIndex < text.length) {
370 textNode.textContent += text[charIndex];
371 charIndex++;
372 setTimeout(typeChar, speed);
373 } else {
374 callback();
375 }
376 }
377 typeChar();
378 } else if (node.nodeType === Node.ELEMENT_NODE) {
379 // Element node - clone and append, then process children
380 var clone = node.cloneNode(false);
381 element.appendChild(clone);
382
383 var children = Array.from(node.childNodes);
384 var childIndex = 0;
385
386 function processNextChild() {
387 if (childIndex < children.length) {
388 // Temporarily change element to append to clone
389 var originalElement = element;
390 element = clone;
391 typeNode(children[childIndex], function() {
392 element = originalElement;
393 childIndex++;
394 processNextChild();
395 });
396 } else {
397 callback();
398 }
399 }
400 processNextChild();
401 } else {
402 callback();
403 }
404 }
405
406 // Process all top-level nodes
407 var topNodes = Array.from(tempDiv.childNodes);
408 var nodeIndex = 0;
409
410 function processNextTopNode() {
411 if (nodeIndex < topNodes.length) {
412 typeNode(topNodes[nodeIndex], function() {
413 nodeIndex++;
414 processNextTopNode();
415 });
416 } else {
417 // All nodes processed - call onComplete callback
418 if (onComplete) onComplete();
419 }
420 }
421 processNextTopNode();
422 }
423
424 // =========================================================================
425 // RENDER FUNCTIONS
426 // =========================================================================
427
428 // Render AI Enhancement sections (Summary and Recommendations)
429 // All HTML is pre-rendered by PHP - JavaScript only inserts it
430 function wpforoRenderAiEnhancement(enhancement, results) {
431 if (!enhancement) return '';
432
433 var html = '';
434
435 // AI Search Summary Section
436 // PHP already converts [[#N]] and [[#N:Title]] to HTML links
437 if (enhancement.summary || enhancement.quick_answer) {
438 html += '<div class="wpf-ai-summary-section notranslate" translate="no">';
439 html += '<div class="wpf-ai-section-header"><i class="fas fa-brain"></i> ' + wpforo_phrase('AI Search Summary') + '</div>';
440 if (enhancement.quick_answer) {
441 // Output directly - PHP has already processed link markers
442 html += '<div class="wpf-ai-quick-answer notranslate" translate="no">' + enhancement.quick_answer + '</div>';
443 }
444 if (enhancement.summary) {
445 // Store summary in data attribute for typewriter effect
446 var encodedSummary = enhancement.summary.replace(/"/g, '&quot;');
447 html += '<div class="wpf-ai-summary-text wpf-ai-typewriter notranslate" translate="no" data-typewriter-content="' + encodedSummary + '" style="visibility:hidden;min-height:50px;"></div>';
448 }
449 html += '</div>';
450 }
451
452 // AI Recommendations Section - use pre-rendered HTML from PHP
453 if (enhancement.recommendations_html) {
454 html += enhancement.recommendations_html;
455 }
456
457 return html;
458 }
459
460 // Render AI search results HTML
461 function wpforoRenderAiResults(results) {
462 var html = '';
463 // Add "AI Search Results" header before real results
464 html += '<div class="wpf-ai-result-wrapper">';
465 html += '<div class="wpf-ai-section-header"><i class="fas fa-search"></i> ' + wpforo_phrase('AI Search Results') + '</div>';
466 for (var i = 0; i < results.length; i++) {
467 var r = results[i];
468 html += '<div class="wpf-ai-result-card">';
469 var postIdBadge = r.post_id ? ' <span class="wpf-ai-result-postid">[ <i class="fa-regular fa-message"></i> ' + r.post_id + ' ]</span>' : '';
470 // Render title as link only if URL exists, otherwise plain text
471 if (r.url) {
472 html += '<div class="wpf-ai-result-title"><a href="' + r.url + '" target="_blank" rel="noopener">' + wpforoEscapeHtml(r.title) + postIdBadge + '</a></div>';
473 } else {
474 html += '<div class="wpf-ai-result-title"><span class="wpf-ai-result-title-text">' + wpforoEscapeHtml(r.title) + '</span></div>';
475 }
476 html += '<div class="wpf-ai-result-meta">';
477 if (r.content_source === 'custom_knowledge') {
478 var kbLabel = r.post_type_label || 'Knowledge Base';
479 html += '<span class="wpf-ai-result-post-type wpf-ai-knowledge-badge"><i class="fas fa-book"></i> ' + wpforoEscapeHtml(kbLabel) + '</span>';
480 } else if (r.content_source === 'wordpress') {
481 var typeLabel = r.post_type_label || 'Post';
482 html += '<span class="wpf-ai-result-post-type"><i class="fas fa-file-alt"></i> ' + wpforoEscapeHtml(typeLabel) + '</span>';
483 } else if (r.forum_title) {
484 html += '<span class="wpf-ai-result-forum"><i class="fas fa-folder-open"></i> ' + wpforoEscapeHtml(r.forum_title) + '</span>';
485 }
486 // Only show author if exists (custom_knowledge has no author)
487 if (r.author_name) {
488 html += '<span class="wpf-ai-result-author"><i class="fas fa-user"></i> ' + wpforoEscapeHtml(r.author_name) + '</span>';
489 }
490 // Only show date if exists (custom_knowledge has no date)
491 if (r.created_ago) {
492 html += '<span class="wpf-ai-result-date"><i class="far fa-clock"></i> ' + r.created_ago + '</span>';
493 }
494 html += '<span class="wpf-ai-result-score"><i class="fas fa-bullseye"></i> ' + r.score + '%</span>';
495 html += '</div>';
496 if (r.content) {
497 var formattedContent = wpforoFormatAiContent(r.content);
498 var lineCount = (r.content.match(/[\r\n]+/g) || []).length + 1;
499 var isLong = r.content.length > 500 || lineCount > 5;
500
501 html += '<div class="wpf-ai-result-content-wrap' + (isLong ? ' wpf-ai-collapsed' : '') + '">';
502 html += '<div class="wpf-ai-result-content">' + formattedContent + '</div>';
503 if (isLong) {
504 html += '<div class="wpf-ai-content-toggle"><span class="wpf-ai-toggle-btn" data-expanded="false"><i class="fas fa-chevron-down"></i> <span class="wpf-ai-toggle-text">' + wpforo_phrase('Show more') + '</span></span></div>';
505 }
506 html += '</div>';
507 }
508 html += '</div>';
509 }
510 html += '</div>';
511 return html;
512 }
513
514 // Format AI content: escape HTML and convert line breaks to <br>
515 function wpforoFormatAiContent(text) {
516 if (!text) return '';
517 // First escape HTML
518 var escaped = wpforoEscapeHtml(text);
519 // Convert \r\n, \r, \n to <br> for proper line breaks
520 escaped = escaped.replace(/\r\n/g, '<br>');
521 escaped = escaped.replace(/\r/g, '<br>');
522 escaped = escaped.replace(/\n/g, '<br>');
523 // Convert multiple <br> to paragraph breaks
524 escaped = escaped.replace(/(<br>){3,}/g, '<br><br>');
525 return escaped;
526 }
527
528 // Toggle AI search result content expand/collapse
529 wpforo_wrap.on('click', '.wpf-ai-toggle-btn', function() {
530 var $btn = $(this);
531 var $wrap = $btn.closest('.wpf-ai-result-content-wrap');
532 var isExpanded = $btn.data('expanded');
533
534 if (isExpanded) {
535 $wrap.addClass('wpf-ai-collapsed');
536 $btn.data('expanded', false);
537 $btn.find('.wpf-ai-toggle-text').text(wpforo_phrase('Show more'));
538 $btn.find('i').removeClass('fa-chevron-up').addClass('fa-chevron-down');
539 } else {
540 $wrap.removeClass('wpf-ai-collapsed');
541 $btn.data('expanded', true);
542 $btn.find('.wpf-ai-toggle-text').text(wpforo_phrase('Show less'));
543 $btn.find('i').removeClass('fa-chevron-down').addClass('fa-chevron-up');
544 }
545 });
546
547 // =========================================================================
548 // HELPER FUNCTIONS
549 // =========================================================================
550
551 // Escape HTML helper
552 function wpforoEscapeHtml(text) {
553 if (!text) return '';
554 var div = document.createElement('div');
555 div.textContent = text;
556 return div.innerHTML;
557 }
558
559 // =========================================================================
560 // AI TRANSLATION
561 // =========================================================================
562
563 // Toggle translation dropdown
564 wpforo_wrap.on('click', '.wpf-ai-translate-btn', function (e) {
565 e.stopPropagation();
566 var wrapper = $(this).closest('.wpf-ai-translate-wrapper');
567 var dropdown = wrapper.find('.wpf-ai-translate-dropdown');
568
569 // Close other dropdowns
570 $('.wpf-ai-translate-dropdown').not(dropdown).removeClass('wpf-ai-translate-dropdown-open');
571
572 // Toggle this dropdown
573 dropdown.toggleClass('wpf-ai-translate-dropdown-open');
574 });
575
576 // Close dropdown when clicking outside
577 $(document).on('click', function () {
578 $('.wpf-ai-translate-dropdown').removeClass('wpf-ai-translate-dropdown-open');
579 });
580
581 // Prevent dropdown from closing when clicking inside it
582 wpforo_wrap.on('click', '.wpf-ai-translate-dropdown', function (e) {
583 e.stopPropagation();
584 });
585
586 // Handle language selection for translation
587 wpforo_wrap.on('click', '.wpf-ai-translate-option', function () {
588 var option = $(this);
589 var wrapper = option.closest('.wpf-ai-translate-wrapper');
590 var postId = wrapper.data('postid');
591 var language = option.data('lang');
592 var dropdown = wrapper.find('.wpf-ai-translate-dropdown');
593 var translateBtn = wrapper.find('.wpf-ai-translate-btn');
594 var originalBtn = wrapper.find('.wpf-ai-translate-original');
595 var loadingEl = wrapper.find('.wpf-ai-translate-loading');
596
597 // Close dropdown
598 dropdown.removeClass('wpf-ai-translate-dropdown-open');
599
600 // Find the post/comment content element
601 // Structure for Post: .post-wrap > .wpforo-post > .wpforo-post-content
602 // Structure for Q&A Layout Comment: .comment-wrap > .wpforo-comment-content > .wpforo-comment-text
603 var postElement = wrapper.closest('.wpforo-post').find('.wpforo-post-content');
604 if (!postElement.length) {
605 postElement = wrapper.closest('.post-wrap').find('.wpforo-post-content');
606 }
607 if (!postElement.length) {
608 postElement = wrapper.closest('.wpforo-comment').find('.wpforo-comment-text');
609 }
610 if (!postElement.length) {
611 postElement = wrapper.closest('.comment-wrap').find('.wpforo-comment-text');
612 }
613 if (!postElement.length) {
614 console.error('wpForo AI: Could not find post content element');
615 return;
616 }
617
618 // Store original content if not already stored
619 if (!postElement.data('original-content')) {
620 postElement.data('original-content', postElement.html());
621 }
622
623 // Show loading state
624 translateBtn.hide();
625 loadingEl.show();
626
627 // Make AJAX request
628 $.ajax({
629 url: wpforo.ajax_url,
630 type: 'POST',
631 data: {
632 action: 'wpforo_ai_translate',
633 post_id: postId,
634 language: language,
635 nonce: wpforo.nonces.wpforo_ai_translate
636 },
637 success: function (response) {
638 loadingEl.hide();
639
640 if (response.success && response.data.translated_content) {
641 // Replace content with translated version
642 postElement.html(response.data.translated_content);
643 postElement.addClass('wpf-ai-translated');
644
645 // Add RTL class for right-to-left languages (Arabic, Hebrew)
646 var rtlLanguages = ['Arabic', 'Hebrew', 'ar', 'he'];
647 if (rtlLanguages.indexOf(language) !== -1) {
648 postElement.addClass('wpf-ai-translated-rtl');
649 }
650
651 // Show "Show Original" button
652 originalBtn.show();
653 } else {
654 // Show error
655 translateBtn.show();
656 var errorMsg = response.data && response.data.message ? response.data.message : wpforo_phrase('Translation failed');
657 alert(errorMsg);
658 }
659 },
660 error: function (xhr) {
661 loadingEl.hide();
662 translateBtn.show();
663 // Try to get error message from response (handles 429 rate limit errors)
664 var errorMsg = wpforo_phrase('Network error. Please try again.');
665 try {
666 var response = JSON.parse(xhr.responseText);
667 if (response.data && response.data.message) {
668 errorMsg = response.data.message;
669 }
670 } catch (e) {
671 // Keep default error message
672 }
673 alert(errorMsg);
674 }
675 });
676 });
677
678 // Handle "Show Original" button click
679 wpforo_wrap.on('click', '.wpf-ai-translate-original', function () {
680 var wrapper = $(this).closest('.wpf-ai-translate-wrapper');
681 var originalBtn = wrapper.find('.wpf-ai-translate-original');
682 var translateBtn = wrapper.find('.wpf-ai-translate-btn');
683
684 // Find the post/comment content element
685 var postElement = wrapper.closest('.wpforo-post').find('.wpforo-post-content');
686 if (!postElement.length) {
687 postElement = wrapper.closest('.post-wrap').find('.wpforo-post-content');
688 }
689 if (!postElement.length) {
690 postElement = wrapper.closest('.wpforo-comment').find('.wpforo-comment-text');
691 }
692 if (!postElement.length) {
693 postElement = wrapper.closest('.comment-wrap').find('.wpforo-comment-text');
694 }
695
696 // Restore original content
697 var originalContent = postElement.data('original-content');
698 if (originalContent) {
699 postElement.html(originalContent);
700 postElement.removeClass('wpf-ai-translated wpf-ai-translated-rtl');
701 }
702
703 // Show translate button, hide original button
704 originalBtn.hide();
705 translateBtn.show();
706 });
707
708 // =========================================================================
709 // AI TOPIC SUMMARIZATION
710 // =========================================================================
711
712 var summaryLoadingInterval = null;
713 var summaryLoadingMessages = [
714 'Reading topic posts...',
715 'Analyzing discussion...',
716 'Generating summary...',
717 'Almost ready...'
718 ];
719
720 // Show loading animation for topic summary
721 function wpforoShowSummaryLoading(container) {
722 var loadingHtml = '<div class="wpf-ai-loading">' +
723 '<div class="wpf-ai-loading-animation">' +
724 '<div class="wpf-ai-loading-stars">' +
725 '<svg class="wpf-ai-star wpf-ai-star-1" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
726 '<svg class="wpf-ai-star wpf-ai-star-2" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
727 '<svg class="wpf-ai-star wpf-ai-star-3" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
728 '</div>' +
729 '</div>' +
730 '<div class="wpf-ai-loading-text">' + wpforo_phrase(summaryLoadingMessages[0]) + '</div>' +
731 '</div>';
732 container.html(loadingHtml);
733
734 // Rotate through loading messages
735 var msgIndex = 0;
736 summaryLoadingInterval = setInterval(function() {
737 msgIndex = (msgIndex + 1) % summaryLoadingMessages.length;
738 container.find('.wpf-ai-loading-text').text(wpforo_phrase(summaryLoadingMessages[msgIndex]));
739 }, 2500);
740 }
741
742 function wpforoHideSummaryLoading() {
743 if (summaryLoadingInterval) {
744 clearInterval(summaryLoadingInterval);
745 summaryLoadingInterval = null;
746 }
747 }
748
749 // Topic Summary Button Click Handler
750 wpforo_wrap.on('click', '.wpf-ai-summarize-btn', function (e) {
751 e.preventDefault();
752 wpforo_load_hide();
753
754 var btn = $(this);
755 var topicId = btn.data('topicid');
756 var nonce = btn.data('nonce');
757 var container = $('#wpf-ai-summary-' + topicId);
758 var contentArea = container.find('.wpf-ai-summary-content');
759
760 // If container is already visible and has content, just toggle it
761 if (container.is(':visible') && contentArea.find('.wpf-ai-summary-result').length > 0) {
762 container.slideUp(350);
763 return;
764 }
765
766 // Show container with loading
767 container.slideDown(350);
768 wpforoShowSummaryLoading(contentArea);
769
770 // Disable button during request
771 btn.addClass('wpf-ai-loading-btn');
772
773 // Make AJAX request
774 $.ajax({
775 url: wpforo.ajax_url,
776 type: 'POST',
777 data: {
778 action: 'wpforo_ai_summarize_topic',
779 topicid: topicId,
780 nonce: nonce
781 },
782 success: function (response) {
783 wpforoHideSummaryLoading();
784 btn.removeClass('wpf-ai-loading-btn');
785
786 if (response.success && response.data.summary) {
787 // Build posts info (show notice when posts were limited)
788 var postsInfo = '';
789 if (response.data.posts_limited && response.data.total_posts_count) {
790 postsInfo = '<span class="wpf-ai-summary-meta wpf-ai-posts-limited">' +
791 response.data.reply_count + ' ' + wpforo_phrase('of') + ' ' + response.data.total_posts_count + ' ' + wpforo_phrase('posts') +
792 '</span>';
793 }
794
795 // Render summary with close button in header (like AI Assistant)
796 var summaryHtml = '<div class="wpf-ai-summary-result">' +
797 '<div class="wpf-ai-summary-header">' +
798 '<span class="wpf-ai-summary-title"><i class="fa-solid fa-wand-magic-sparkles"></i> ' + wpforo_phrase('AI Summary') + '</span>' +
799 '<span class="wpf-ai-summary-meta">' + wpforo_phrase('Style') + ': ' + wpforoEscapeHtml(response.data.style || 'detailed') + '</span>' +
800 postsInfo +
801 '<div class="wpf-ai-summary-close-btn">' +
802 '<i class="fas fa-times"></i>' +
803 '<span>' + wpforo_phrase('Close') + '</span>' +
804 '</div>' +
805 '</div>' +
806 '<div class="wpf-ai-summary-body">' + response.data.summary + '</div>' +
807 '</div>';
808 contentArea.html(summaryHtml);
809 } else {
810 // Show error with close button
811 var errorMsg = response.data && response.data.message ? response.data.message : wpforo_phrase('Failed to generate summary');
812 contentArea.html('<div class="wpf-ai-summary-error">' +
813 '<i class="fas fa-exclamation-circle"></i> ' + wpforoEscapeHtml(errorMsg) +
814 '<div class="wpf-ai-summary-close-btn" style="margin-left: 10px;">' +
815 '<i class="fas fa-times"></i>' +
816 '<span>' + wpforo_phrase('Close') + '</span>' +
817 '</div>' +
818 '</div>');
819 }
820 },
821 error: function (xhr) {
822 wpforoHideSummaryLoading();
823 btn.removeClass('wpf-ai-loading-btn');
824 // Try to get error message from response (handles 429 rate limit errors)
825 var errorMsg = wpforo_phrase('Network error. Please try again.');
826 try {
827 var response = JSON.parse(xhr.responseText);
828 if (response.data && response.data.message) {
829 errorMsg = response.data.message;
830 }
831 } catch (e) {
832 // Keep default error message
833 }
834 contentArea.html('<div class="wpf-ai-summary-error">' +
835 '<i class="fas fa-exclamation-circle"></i> ' + wpforoEscapeHtml(errorMsg) +
836 '<div class="wpf-ai-summary-close-btn" style="margin-left: 10px;">' +
837 '<i class="fas fa-times"></i>' +
838 '<span>' + wpforo_phrase('Close') + '</span>' +
839 '</div>' +
840 '</div>');
841 }
842 });
843 });
844
845 // Topic Summary Close Button Handler (handles both footer close and header close button)
846 wpforo_wrap.on('click', '.wpf-ai-summary-close, .wpf-ai-summary-close-btn', function () {
847 var container = $(this).closest('.wpf-ai-summary-container');
848 container.slideUp(350);
849 });
850
851 // =========================================================================
852 // AI TOPIC SUGGESTIONS (Smart Topic Suggestions)
853 // =========================================================================
854
855 var suggestionCallCount = 0;
856 var suggestionConfig = null;
857 var suggestionLastQuery = '';
858
859 // Initialize suggestion config from data attributes
860 function wpforoInitSuggestionConfig() {
861 var panel = $('.wpf-ai-suggestions-panel');
862 if (panel.length && panel.data('suggestion-config')) {
863 suggestionConfig = panel.data('suggestion-config');
864 } else {
865 // Default config - disabled if panel not found
866 suggestionConfig = {
867 enabled: false,
868 min_words: 3,
869 max_calls: 2,
870 show_related: true,
871 show_answer: true,
872 quality: 'balanced'
873 };
874 }
875 }
876
877 // Count words in a string
878 function wpforoCountWords(str) {
879 if (!str) return 0;
880 return str.trim().split(/\s+/).filter(function(w) { return w.length > 0; }).length;
881 }
882
883 // Topic title input handler - triggers on blur (when user leaves the title field)
884 // Topic title field has name="thread[title]" and id="thread_title" in wpForo
885 wpforo_wrap.on('blur', '#thread_title, input[name="thread[title]"]', function (e) {
886 // Initialize config if not done
887 if (!suggestionConfig) {
888 wpforoInitSuggestionConfig();
889 }
890
891 // Check if suggestions are enabled
892 if (!suggestionConfig || !suggestionConfig.enabled) {
893 return;
894 }
895
896 var input = $(this);
897 var title = input.val().trim();
898 var wordCount = wpforoCountWords(title);
899
900 // Check minimum words (from config)
901 if (wordCount < suggestionConfig.min_words) {
902 return;
903 }
904
905 // Skip if same query as last time
906 if (title === suggestionLastQuery) {
907 return;
908 }
909
910 // Check max API calls per topic creation session (from config)
911 if (suggestionCallCount >= suggestionConfig.max_calls) {
912 return;
913 }
914
915 // Fetch suggestions immediately on blur (no debounce needed)
916 wpforoFetchSuggestions(title, input);
917 });
918
919 // Fetch suggestions from API
920 function wpforoFetchSuggestions(title, inputElement) {
921 if (!suggestionConfig || !suggestionConfig.enabled) return;
922
923 suggestionLastQuery = title;
924 suggestionCallCount++;
925
926 var form = inputElement.closest('form');
927 var panel = form.find('.wpf-ai-suggestions-panel');
928 var contentArea = panel.find('.wpf-ai-suggestions-content');
929
930 // Show panel with loading immediately on blur
931 panel.slideDown(300);
932 wpforoShowSuggestionLoading(contentArea);
933
934 // Make AJAX request
935 $.ajax({
936 url: wpforo.ajax_url,
937 type: 'POST',
938 data: {
939 action: 'wpforo_ai_get_topic_suggestions',
940 title: title,
941 quality: suggestionConfig.quality || 'balanced',
942 include_similar: 1, // Always include similar topics - required for the feature
943 include_related: suggestionConfig.show_related ? 1 : 0,
944 include_answer: suggestionConfig.show_answer ? 1 : 0,
945 nonce: wpforo.nonces.wpforo_ai_get_topic_suggestions
946 },
947 success: function (response) {
948 wpforoHideSuggestionLoading();
949
950 if (response.success && response.data.has_suggestions) {
951 wpforoRenderSuggestions(contentArea, response.data);
952 } else if (response.success && !response.data.has_suggestions) {
953 // No similar topics found - show message briefly then hide
954 contentArea.html('<div class="wpf-ai-suggestions-no-results">' +
955 '<i class="fas fa-info-circle"></i> ' + wpforo_phrase('No similar topics have been found.') +
956 '</div>');
957 // Hide panel after 3 seconds
958 setTimeout(function() {
959 panel.slideUp(300);
960 }, 3000);
961 } else {
962 // Error
963 var errorMsg = response.data && response.data.message ? response.data.message : wpforo_phrase('Could not fetch suggestions');
964 contentArea.html('<div class="wpf-ai-suggestions-error">' +
965 '<i class="fas fa-exclamation-circle"></i> ' + wpforoEscapeHtml(errorMsg) +
966 '</div>');
967 }
968 },
969 error: function (xhr) {
970 wpforoHideSuggestionLoading();
971 // Try to get error message from response (handles 429 rate limit errors)
972 var errorMsg = wpforo_phrase('Network error. Please try again.');
973 try {
974 var response = JSON.parse(xhr.responseText);
975 if (response.data && response.data.message) {
976 errorMsg = response.data.message;
977 }
978 } catch (e) {
979 // Keep default error message
980 }
981 contentArea.html('<div class="wpf-ai-suggestions-error">' +
982 '<i class="fas fa-exclamation-circle"></i> ' + wpforoEscapeHtml(errorMsg) +
983 '</div>');
984 }
985 });
986 }
987
988 // Loading animation for suggestions - compact single line
989 var suggestionLoadingInterval = null;
990 var suggestionLoadingMessages = [
991 'Searching similar topics...',
992 'Analyzing your question...',
993 'Finding relevant discussions...',
994 'Almost ready...'
995 ];
996
997 function wpforoShowSuggestionLoading(container) {
998 var loadingHtml = '<div class="wpf-ai-suggestion-loading-inline">' +
999 '<svg class="wpf-ai-star-inline" viewBox="0 0 24 24"><path d="M12 0L14.59 8.41L23 11L14.59 13.59L12 22L9.41 13.59L1 11L9.41 8.41L12 0Z"/></svg>' +
1000 '<span class="wpf-ai-loading-text-inline">' + wpforo_phrase(suggestionLoadingMessages[0]) + '</span>' +
1001 '</div>';
1002 container.html(loadingHtml);
1003
1004 var msgIndex = 0;
1005 suggestionLoadingInterval = setInterval(function() {
1006 msgIndex = (msgIndex + 1) % suggestionLoadingMessages.length;
1007 container.find('.wpf-ai-loading-text-inline').text(wpforo_phrase(suggestionLoadingMessages[msgIndex]));
1008 }, 2000);
1009 }
1010
1011 function wpforoHideSuggestionLoading() {
1012 if (suggestionLoadingInterval) {
1013 clearInterval(suggestionLoadingInterval);
1014 suggestionLoadingInterval = null;
1015 }
1016 }
1017
1018 // Render suggestions UI
1019 function wpforoRenderSuggestions(container, data) {
1020 // No duplicate header - PHP already renders the main header
1021 var html = '';
1022
1023 // Similar Topics Section
1024 if (data.similar_topics && data.similar_topics.length > 0) {
1025 html += '<div class="wpf-ai-suggestions-section wpf-ai-similar-topics">' +
1026 '<div class="wpf-ai-suggestions-section-header">' +
1027 '<i class="fas fa-copy"></i> ' + wpforo_phrase('Similar Topics Already Exist') +
1028 '</div>' +
1029 '<div class="wpf-ai-suggestions-section-content">' +
1030 '<ul class="wpf-ai-similar-list">';
1031
1032 for (var i = 0; i < data.similar_topics.length; i++) {
1033 var topic = data.similar_topics[i];
1034 html += '<li class="wpf-ai-similar-item">' +
1035 '<i class="fas fa-angle-double-right wpf-ai-item-icon"></i>' +
1036 '<span class="wpf-ai-similar-score">' + topic.score + '% ' + wpforo_phrase('match') + '</span>' +
1037 '<a href="' + topic.url + '" target="_blank" rel="noopener" class="wpf-ai-similar-link">' +
1038 '<span class="wpf-ai-similar-title">' + wpforoEscapeHtml(topic.title) + '</span>' +
1039 '</a>' +
1040 '</li>';
1041 }
1042
1043 html += '</ul>' +
1044 '<div class="wpf-ai-similar-hint">' +
1045 '<i class="fas fa-info-circle"></i> ' + wpforo_phrase('Check these topics - your question might already be answered!') +
1046 '</div>' +
1047 '</div>' +
1048 '</div>';
1049 }
1050
1051 // Related Topics Section (AI suggestions)
1052 if (data.related_topics && data.related_topics.length > 0) {
1053 html += '<div class="wpf-ai-suggestions-section wpf-ai-related-topics">' +
1054 '<div class="wpf-ai-suggestions-section-header">' +
1055 '<svg class="wpf-ai-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="width:22px;height:22px;vertical-align:middle;margin-right:6px;fill:currentColor;margin-bottom: 3px;"><path d="M9,13 L19,13 C19.5522847,13 20,13.4477153 20,14 C20,14.5522847 19.5522847,15 19,15 L9,15 C8.44771525,15 8,14.5522847 8,14 C8,13.4477153 8.44771525,13 9,13 Z M9,17 L19,17 C19.5522847,17 20,17.4477153 20,18 C20,18.5522847 19.5522847,19 19,19 L9,19 C8.44771525,19 8,18.5522847 8,18 C8,17.4477153 8.44771525,17 9,17 Z M15,9 L19,9 C19.5522847,9 20,9.44771525 20,10 C20,10.5522847 19.5522847,11 19,11 L15,11 C14.4477153,11 14,10.5522847 14,10 C14,9.44771525 14.4477153,9 15,9 Z M7.74264069,10.9142136 L4,7.17157288 L5.41421356,5.75735931 L7.74264069,8.08578644 L12.8284271,3 L14.2426407,4.41421356 L7.74264069,10.9142136 Z"/></svg>' + wpforo_phrase('Related Topics You Might Explore') +
1056 '</div>' +
1057 '<div class="wpf-ai-suggestions-section-content">' +
1058 '<ul class="wpf-ai-related-list">';
1059
1060 for (var j = 0; j < data.related_topics.length; j++) {
1061 var related = data.related_topics[j];
1062 // Use the URL from API if available, fallback to search
1063 var topicUrl = related.url || (wpforo_url + '?foro=search&wpfkeyword=' + encodeURIComponent(related.title));
1064 html += '<li class="wpf-ai-related-item">' +
1065 '<svg class="wpf-ai-icon wpf-ai-item-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="width:14px;height:14px;vertical-align:middle;margin-right:5px;fill:currentColor;flex-shrink:0;"><polygon points="6 10 4 12 10 18 20 8 18 6 10 14"/></svg>' +
1066 '<a href="' + topicUrl + '" target="_blank" rel="noopener" class="wpf-ai-related-link">' +
1067 '<span class="wpf-ai-related-title">' + wpforoEscapeHtml(related.title) + '</span>' +
1068 '</a>' +
1069 (related.reason ? '<span class="wpf-ai-related-reason">' + wpforoEscapeHtml(related.reason) + '</span>' : '') +
1070 '</li>';
1071 }
1072
1073 html += '</ul>' +
1074 '</div>' +
1075 '</div>';
1076 }
1077
1078 // Quick AI Answer Section
1079 if (data.quick_answer && data.quick_answer.text) {
1080 html += '<div class="wpf-ai-suggestions-section wpf-ai-quick-answer-section">' +
1081 '<div class="wpf-ai-suggestions-section-header">' +
1082 '<svg class="wpf-ai-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:22px;height:22px;vertical-align:middle;margin-right:6px;margin-bottom: 3px;"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.33569 3.38268C7.93132 1.87244 10.0687 1.87244 10.6643 3.38268L11.7363 6.10082C11.7657 6.17532 11.8247 6.23429 11.8992 6.26367L14.6173 7.33569C16.1276 7.93132 16.1276 10.0687 14.6173 10.6643L11.8992 11.7363C11.8247 11.7657 11.7657 11.8247 11.7363 11.8992L10.6643 14.6173C10.0687 16.1276 7.93132 16.1276 7.33569 14.6173L6.26367 11.8992C6.23429 11.8247 6.17532 11.7657 6.10082 11.7363L3.38268 10.6643C1.87244 10.0687 1.87244 7.93132 3.38268 7.33569L6.10082 6.26367C6.17532 6.23429 6.23429 6.17532 6.26367 6.10082L7.33569 3.38268ZM9.26891 3.93301C9.17267 3.68899 8.82733 3.689 8.73109 3.93301L7.65907 6.65115C7.47722 7.11224 7.11224 7.47722 6.65116 7.65907L3.93301 8.73109C3.68899 8.82733 3.689 9.17267 3.93301 9.26891L6.65115 10.3409C7.11224 10.5228 7.47722 10.8878 7.65907 11.3488L8.73109 14.067C8.82733 14.311 9.17267 14.311 9.26891 14.067L10.3409 11.3488C10.5228 10.8878 10.8878 10.5228 11.3488 10.3409L14.067 9.26891C14.311 9.17267 14.311 8.82733 14.067 8.73109L11.3488 7.65907C10.8878 7.47722 10.5228 7.11224 10.3409 6.65116L9.26891 3.93301ZM15.7908 13.073C16.2235 11.9757 17.7765 11.9757 18.2092 13.073L18.9779 15.0221L20.927 15.7908C22.0243 16.2235 22.0243 17.7765 20.927 18.2092L18.9779 18.9779L18.2092 20.927C17.7765 22.0243 16.2235 22.0243 15.7908 20.927L15.0221 18.9779L13.073 18.2092C11.9757 17.7765 11.9757 16.2235 13.073 15.7908L15.0221 15.0221L15.7908 13.073ZM17 14.0953L16.3856 15.6533C16.2534 15.9883 15.9883 16.2534 15.6533 16.3856L14.0953 17L15.6533 17.6144C15.9883 17.7466 16.2534 18.0117 16.3856 18.3467L17 19.9047L17.6144 18.3467C17.7466 18.0117 18.0117 17.7466 18.3467 17.6144L19.9047 17L18.3467 16.3856C18.0117 16.2534 17.7466 15.9883 17.6144 15.6533L17 14.0953Z" fill="currentColor"/></svg>' + wpforo_phrase('Quick AI Answer') +
1083 '</div>' +
1084 '<div class="wpf-ai-suggestions-section-content">' +
1085 '<div class="wpf-ai-quick-answer-text">' + wpforoFormatAiContent(data.quick_answer.text) + '</div>';
1086
1087 html += '<div class="wpf-ai-answer-hint">' +
1088 wpforo_phrase('This is an AI-generated answer based on existing forum content. Post your topic for more accurate human responses.') +
1089 '</div>' +
1090 '</div>' +
1091 '</div>';
1092 }
1093
1094 container.html(html);
1095 }
1096
1097 // Close suggestions panel
1098 wpforo_wrap.on('click', '.wpf-ai-suggestions-close', function () {
1099 var panel = $(this).closest('.wpf-ai-suggestions-panel');
1100 panel.slideUp(300);
1101 });
1102
1103 // Toggle show more/less for quick answer
1104 wpforo_wrap.on('click', '.wpf-ai-show-more-answer', function () {
1105 var btn = $(this);
1106 var answerText = btn.closest('.wpf-ai-suggestions-section-content').find('.wpf-ai-quick-answer-text');
1107 var fullAnswer = btn.data('full');
1108 var isExpanded = btn.data('expanded');
1109
1110 if (isExpanded) {
1111 // Collapse - would need original stored, for now just hide
1112 btn.html('<i class="fas fa-chevron-down"></i> ' + wpforo_phrase('Show more'));
1113 btn.data('expanded', false);
1114 } else {
1115 answerText.html(wpforoFormatAiContent(fullAnswer));
1116 btn.html('<i class="fas fa-chevron-up"></i> ' + wpforo_phrase('Show less'));
1117 btn.data('expanded', true);
1118 }
1119 });
1120
1121 // Initialize suggestion config on page load
1122 wpforoInitSuggestionConfig();
1123
1124 // Re-initialize suggestion config when topic form is loaded via AJAX
1125 // wpForo triggers 'wpforo_topic_portable_form' event after AJAX form load
1126 $(document).on('wpforo_topic_portable_form', function(event, formElement) {
1127 // Reset suggestion state for new form
1128 suggestionCallCount = 0;
1129 suggestionLastQuery = '';
1130 suggestionConfig = null;
1131
1132 // Re-initialize config from the new form's panel
1133 if (formElement && formElement.length) {
1134 var panel = formElement.find('.wpf-ai-suggestions-panel');
1135 if (panel.length && panel.data('suggestion-config')) {
1136 suggestionConfig = panel.data('suggestion-config');
1137 }
1138 }
1139
1140 // Fallback to global search if not found in form element
1141 if (!suggestionConfig) {
1142 wpforoInitSuggestionConfig();
1143 }
1144 });
1145
1146 // =========================================================================
1147 // AI BOT REPLY
1148 // =========================================================================
1149
1150 /**
1151 * AI Bot Reply button click handler
1152 * Creates a bot-generated reply to the post
1153 */
1154 wpforo_wrap.on('click', '.wpf-ai-bot-reply', function (e) {
1155 e.preventDefault();
1156 e.stopPropagation();
1157
1158 var btn = $(this);
1159 var postId = btn.data('postid');
1160 var topicId = btn.data('topicid');
1161
1162 if (!postId || !topicId) {
1163 console.error('AI Bot Reply: Missing post or topic ID');
1164 return;
1165 }
1166
1167 // Prevent double-clicks (check for wpf-processing spinning class)
1168 if (btn.hasClass('wpf-processing')) {
1169 return;
1170 }
1171
1172 // Check if nonce is available
1173 var nonce = wpforo.nonces && wpforo.nonces.wpforo_ai_bot_reply;
1174 if (!nonce) {
1175 alert(wpforo_phrase('AI Bot Reply is not properly configured. Please refresh the page and try again.'));
1176 return;
1177 }
1178
1179 // Show loading state - spinning icon (like wpforo-aibot plugin)
1180 btn.addClass('wpf-processing');
1181 if (typeof wpforo_load_show === 'function') {
1182 wpforo_load_show();
1183 }
1184
1185 // Make AJAX request
1186 $.ajax({
1187 url: wpforo.ajax_url,
1188 type: 'POST',
1189 data: {
1190 action: 'wpforo_ai_bot_reply',
1191 _wpnonce: nonce,
1192 post_id: postId,
1193 topic_id: topicId
1194 }
1195 }).done(function (response) {
1196 if (response.success) {
1197 // Reload page to show new reply (with anchor to new post)
1198 var newPostId = response.data.post_id;
1199 setTimeout(function() {
1200 if (newPostId) {
1201 window.location.href = window.location.pathname + window.location.search + '#post-' + newPostId;
1202 window.location.reload();
1203 } else {
1204 window.location.reload();
1205 }
1206 }, 500);
1207 } else {
1208 btn.removeClass('wpf-processing');
1209 if (typeof wpforo_load_hide === 'function') {
1210 wpforo_load_hide();
1211 }
1212
1213 var errorMsg = response.data && response.data.message
1214 ? response.data.message
1215 : wpforo_phrase('Failed to generate bot reply');
1216
1217 // Provide helpful message for common configuration issues
1218 if (errorMsg.indexOf('Bot user not configured') !== -1) {
1219 errorMsg = wpforo_phrase('Bot user not configured') + '.\n\n' +
1220 wpforo_phrase('Please go to') + ' wpForo > Settings > AI Features > AI Bot Reply ' +
1221 wpforo_phrase('and select a WordPress user for the bot.');
1222 }
1223
1224 alert(errorMsg);
1225 }
1226 }).fail(function (xhr, status, error) {
1227 btn.removeClass('wpf-processing');
1228 if (typeof wpforo_load_hide === 'function') {
1229 wpforo_load_hide();
1230 }
1231
1232 console.error('AI Bot Reply error:', status, error, xhr.responseText);
1233
1234 // Try to parse error message from response
1235 var errorMsg = wpforo_phrase('Network error. Please try again.');
1236 try {
1237 var jsonResponse = JSON.parse(xhr.responseText);
1238 if (jsonResponse.data && jsonResponse.data.message) {
1239 errorMsg = jsonResponse.data.message;
1240 // Add helpful message for bot user not configured
1241 if (errorMsg.indexOf('Bot user not configured') !== -1) {
1242 errorMsg = wpforo_phrase('Bot user not configured') + '.\n\n' +
1243 wpforo_phrase('Please go to') + ' wpForo > Settings > AI Features > AI Bot Reply ' +
1244 wpforo_phrase('and select a WordPress user for the bot.');
1245 }
1246 }
1247 } catch (e) {
1248 // Keep default error message
1249 }
1250 alert(errorMsg);
1251 });
1252 });
1253
1254 /**
1255 * AI Suggest Reply button click handler
1256 * Generates AI reply suggestion and inserts into TinyMCE editor
1257 */
1258 wpforo_wrap.on('click', '.wpf-ai-suggest-reply', function (e) {
1259 e.preventDefault();
1260
1261 var btn = $(this);
1262 var topicId = btn.data('topicid');
1263
1264 if (!topicId) {
1265 console.error('AI Suggest Reply: Missing topic ID');
1266 return;
1267 }
1268
1269 // Prevent double-clicks
1270 if (btn.hasClass('wpf-ai-loading')) {
1271 return;
1272 }
1273
1274 // Check if nonce is available
1275 var nonce = wpforo.nonces && wpforo.nonces.wpforo_ai_suggest_reply;
1276 if (!nonce) {
1277 alert(wpforo_phrase('AI Suggest Reply is not properly configured. Please refresh the page and try again.'));
1278 return;
1279 }
1280
1281 // Find the form and get parent post ID if available (for threaded replies)
1282 var form = btn.closest('form');
1283 var parentId = 0;
1284 if (form.length) {
1285 var parentInput = form.find('input[name="parentid"]');
1286 if (parentInput.length) {
1287 parentId = parseInt(parentInput.val(), 10) || 0;
1288 }
1289 }
1290
1291 // Show loading state (CSS handles icon visibility via .wpf-ai-suggest-loading class)
1292 btn.addClass('wpf-ai-suggest-loading');
1293 btn.find('span').text(wpforo_phrase('Processing...'));
1294
1295 // Make AJAX request
1296 $.ajax({
1297 url: wpforo.ajax_url,
1298 type: 'POST',
1299 data: {
1300 action: 'wpforo_ai_suggest_reply',
1301 _wpnonce: nonce,
1302 topic_id: topicId,
1303 parent_id: parentId
1304 },
1305 success: function (response) {
1306 // Reset button state (CSS handles icon visibility)
1307 btn.removeClass('wpf-ai-suggest-loading');
1308 btn.find('span').text(wpforo_phrase('Suggest Reply'));
1309
1310 if (response.success) {
1311 var content = response.data.content || '';
1312 if (!content) {
1313 alert(wpforo_phrase('AI generated an empty reply'));
1314 return;
1315 }
1316
1317 // Append content to TinyMCE editor
1318 var inserted = wpforoInsertIntoEditor(content);
1319 if (!inserted) {
1320 // Fallback: try to append to textarea
1321 var textarea = form.find('textarea[name="postbody"]');
1322 if (textarea.length) {
1323 var existingVal = textarea.val().trim();
1324 if (existingVal) {
1325 textarea.val(existingVal + '\n\n' + content);
1326 } else {
1327 textarea.val(content);
1328 }
1329 } else {
1330 alert(wpforo_phrase('Could not insert content into editor'));
1331 }
1332 }
1333
1334 // Show credits used info
1335 var credits = response.data.credits_used || 0;
1336 if (credits > 0) {
1337 console.log('AI Suggest Reply: ' + credits + ' credits used');
1338 }
1339 } else {
1340 var errorMsg = response.data && response.data.message
1341 ? response.data.message
1342 : wpforo_phrase('Failed to generate reply suggestion');
1343 alert(errorMsg);
1344 }
1345 },
1346 error: function (xhr, status, error) {
1347 // Reset button state (CSS handles icon visibility)
1348 btn.removeClass('wpf-ai-suggest-loading');
1349 btn.find('span').text(wpforo_phrase('Suggest Reply'));
1350 console.error('AI Suggest Reply error:', error);
1351 // Try to get error message from response (handles 429 rate limit errors)
1352 var errorMsg = wpforo_phrase('Network error. Please try again.');
1353 try {
1354 var response = JSON.parse(xhr.responseText);
1355 if (response.data && response.data.message) {
1356 errorMsg = response.data.message;
1357 }
1358 } catch (e) {
1359 // Keep default error message
1360 }
1361 alert(errorMsg);
1362 }
1363 });
1364 });
1365
1366 /**
1367 * Append content to TinyMCE editor
1368 * @param {string} content HTML content to append
1369 * @returns {boolean} True if successful
1370 */
1371 function wpforoInsertIntoEditor(content) {
1372 // Try to find the active TinyMCE editor
1373 if (typeof tinyMCE !== 'undefined' && tinyMCE.activeEditor) {
1374 var editor = tinyMCE.activeEditor;
1375 // Append content to existing content with line break
1376 var existingContent = editor.getContent().trim();
1377 if (existingContent) {
1378 editor.setContent(existingContent + '<p>&nbsp;</p>' + content);
1379 } else {
1380 editor.setContent(content);
1381 }
1382 // Focus the editor
1383 editor.focus();
1384 return true;
1385 }
1386
1387 // Try by ID (wpForo's default editor ID)
1388 if (typeof tinyMCE !== 'undefined') {
1389 var editorIds = ['postbody', 'wpf_editor_postbody'];
1390 for (var i = 0; i < editorIds.length; i++) {
1391 var ed = tinyMCE.get(editorIds[i]);
1392 if (ed) {
1393 var existingContent = ed.getContent().trim();
1394 if (existingContent) {
1395 ed.setContent(existingContent + '<p>&nbsp;</p>' + content);
1396 } else {
1397 ed.setContent(content);
1398 }
1399 ed.focus();
1400 return true;
1401 }
1402 }
1403 }
1404
1405 return false;
1406 }
1407
1408 // =========================================================================
1409 // AI BUTTON VISIBILITY ANIMATIONS
1410 // =========================================================================
1411
1412 // Trigger animations when AI buttons become visible on screen
1413 if ('IntersectionObserver' in window) {
1414 var aiButtonObserver = new IntersectionObserver(function(entries) {
1415 entries.forEach(function(entry) {
1416 if (entry.isIntersecting) {
1417 // Add visible class to trigger animation
1418 entry.target.classList.add('wpf-ai-visible');
1419 // Stop observing after animation triggered
1420 aiButtonObserver.unobserve(entry.target);
1421 }
1422 });
1423 }, {
1424 threshold: 0.5 // Trigger when 50% visible
1425 });
1426
1427 // Observe AI Helper Toggle buttons
1428 document.querySelectorAll('.wpf-ai-helper-toggle').forEach(function(el) {
1429 aiButtonObserver.observe(el);
1430 });
1431
1432 // Observe AI Summarize buttons
1433 document.querySelectorAll('.wpf-ai-summarize-btn').forEach(function(el) {
1434 aiButtonObserver.observe(el);
1435 });
1436 }
1437 });
1438