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 / admin / assets / js / ai-features-tools.js

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

870 lines 25.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * wpForo AI Features - AI Tools (Custom Knowledge)
3 *
4 * Isolated JavaScript for AI Tools tab.
5 * Handles custom knowledge file management and priority settings.
6 *
7 * @package wpForo
8 * @subpackage Admin
9 * @since 3.0.0
10 */
11
12 (function($) {
13 'use strict';
14
15 const WpForoAITools = {
16 initialized: false,
17 deleteFileId: null,
18 deleteFileName: null,
19 mediaFrame: null,
20
21 /**
22 * Initialize AI Tools features
23 */
24 init: function() {
25 if (this.initialized) return;
26
27 // Only init if on AI Tools tab
28 if (!$('.wpforo-ai-tools-tab').length) return;
29
30 this.initialized = true;
31 this.bindEvents();
32 this.loadKnowledgeFiles();
33 this.loadBoardSettings();
34 },
35
36 /**
37 * Bind all event handlers
38 */
39 bindEvents: function() {
40 const self = this;
41
42 // Media Library button (TXT/MD/JSON)
43 $(document).on('click', '#knowledge-media-btn', function(e) {
44 e.preventDefault();
45 self.openMediaLibrary('text');
46 });
47
48 // Media Library button (PDF)
49 $(document).on('click', '#knowledge-pdf-media-btn', function(e) {
50 e.preventDefault();
51 self.openMediaLibrary('pdf');
52 });
53
54 // URL input change - auto-detect file info (TXT/MD/JSON)
55 $(document).on('input', '#knowledge-file-url', function() {
56 self.detectFileInfo($(this).val(), 'text');
57 });
58
59 // URL input change - auto-detect file info (PDF)
60 $(document).on('input', '#knowledge-pdf-file-url', function() {
61 self.detectFileInfo($(this).val(), 'pdf');
62 });
63
64 // Add knowledge form (TXT/MD/JSON)
65 $(document).on('submit', '#wpforo-ai-knowledge-upload-form', function(e) {
66 e.preventDefault();
67 self.handleAddKnowledge('text');
68 });
69
70 // Add knowledge form (PDF)
71 $(document).on('submit', '#wpforo-ai-knowledge-pdf-upload-form', function(e) {
72 e.preventDefault();
73 self.handleAddKnowledge('pdf');
74 });
75
76 // Delete knowledge button
77 $(document).on('click', '.wpforo-ai-delete-file', function(e) {
78 e.preventDefault();
79 self.handleDeleteClick($(this));
80 });
81
82 // Board selector change
83 $(document).on('change', '#knowledge-board-select', function() {
84 self.loadBoardSettings();
85 });
86
87 // Enable toggle change - show/hide priority section
88 $(document).on('change', '#knowledge-enabled', function() {
89 self.togglePrioritySection($(this).is(':checked'));
90 });
91
92 // Save settings form (combined enable + priorities)
93 $(document).on('submit', '#wpforo-ai-knowledge-settings-form', function(e) {
94 e.preventDefault();
95 self.handleSaveSettings();
96 });
97
98 // Refresh files button
99 $(document).on('click', '#knowledge-refresh-files', function(e) {
100 e.preventDefault();
101 self.handleRefreshFiles();
102 });
103
104 // Delete confirmation modal
105 $(document).on('click', '#knowledge-delete-confirm', function(e) {
106 e.preventDefault();
107 self.confirmDelete();
108 });
109 $(document).on('click', '#knowledge-delete-cancel, .wpforo-ai-modal-close', function(e) {
110 e.preventDefault();
111 self.cancelDelete();
112 });
113
114 // Priority select change - prevent duplicate selections
115 $(document).on('change', '.priority-select', function() {
116 self.handlePriorityChange($(this));
117 });
118 },
119
120 /**
121 * Open WordPress Media Library.
122 *
123 * kind: 'text' (default — TXT/MD/JSON form) or 'pdf' (PDF form).
124 */
125 openMediaLibrary: function(kind) {
126 const self = this;
127 kind = kind === 'pdf' ? 'pdf' : 'text';
128
129 const isPdf = kind === 'pdf';
130 const frameKey = isPdf ? 'mediaFramePdf' : 'mediaFrame';
131 const urlInputId = isPdf ? '#knowledge-pdf-file-url' : '#knowledge-file-url';
132 const mimeFilter = isPdf
133 ? ['application/pdf']
134 : ['application/json', 'text/plain', 'text/markdown', 'text/x-markdown'];
135 const title = isPdf ? 'Select PDF File' : 'Select Knowledge File';
136
137 // Reuse the per-kind frame if already created
138 if (this[frameKey]) {
139 this[frameKey].open();
140 return;
141 }
142
143 this[frameKey] = wp.media({
144 title: title,
145 button: { text: 'Use This File' },
146 multiple: false,
147 library: { type: mimeFilter }
148 });
149
150 this[frameKey].on('select', function() {
151 const attachment = self[frameKey].state().get('selection').first().toJSON();
152 const url = attachment.url;
153
154 $(urlInputId).val(url);
155 self.detectFileInfo(url, kind);
156 });
157
158 this[frameKey].open();
159 },
160
161 /**
162 * Detect file type and name from URL.
163 *
164 * kind: 'text' (TXT/MD/JSON form) or 'pdf' (PDF form). The PDF form
165 * keeps file_type=pdf regardless of detected extension, but still
166 * updates the display name and badge for the user.
167 */
168 detectFileInfo: function(url, kind) {
169 kind = kind === 'pdf' ? 'pdf' : 'text';
170 const isPdf = kind === 'pdf';
171
172 const $detected = $(isPdf ? '#knowledge-pdf-file-detected' : '#knowledge-file-detected');
173 const $typeInput = $(isPdf ? '#knowledge-pdf-file-type' : '#knowledge-file-type');
174 const $nameInput = $(isPdf ? '#knowledge-pdf-file-name' : '#knowledge-file-name');
175
176 if (!url || !url.trim()) {
177 $detected.hide();
178 $typeInput.val(isPdf ? 'pdf' : 'text');
179 $nameInput.val('');
180 return;
181 }
182
183 // Extract filename from URL
184 let filename = '';
185 try {
186 const urlObj = new URL(url);
187 const path = urlObj.pathname;
188 filename = path.split('/').pop() || '';
189 } catch (e) {
190 // Invalid URL, try simple extraction
191 filename = url.split('/').pop().split('?')[0] || '';
192 }
193
194 if (!filename) {
195 $detected.hide();
196 return;
197 }
198
199 // Detect file type from extension
200 const ext = filename.split('.').pop().toLowerCase();
201 let fileType = isPdf ? 'pdf' : 'text';
202 let typeLabel = isPdf ? 'PDF' : 'TEXT';
203 let icon = isPdf ? '📕' : '📄';
204
205 if (isPdf) {
206 // PDF form: force-set type=pdf, but keep label honest if a non-PDF was pasted
207 fileType = 'pdf';
208 if (ext !== 'pdf') {
209 typeLabel = ext.toUpperCase();
210 }
211 } else if (ext === 'json') {
212 fileType = 'json';
213 typeLabel = 'JSON';
214 icon = '📋';
215 } else if (ext === 'md' || ext === 'markdown') {
216 fileType = 'markdown';
217 typeLabel = 'MD';
218 icon = '📝';
219 } else if (ext === 'txt') {
220 fileType = 'text';
221 typeLabel = 'TXT';
222 icon = '📄';
223 }
224
225 // Get display name (filename without extension)
226 const displayName = filename.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
227
228 // Update hidden fields
229 $typeInput.val(fileType);
230 $nameInput.val(displayName);
231
232 // Show detection UI
233 $detected.find('.file-icon').text(icon);
234 $detected.find('.file-name').text(displayName);
235 $detected.find('.file-type-badge').text(typeLabel);
236 $detected.show();
237 },
238
239 /**
240 * Handle add-knowledge form submission.
241 *
242 * kind: 'text' (TXT/MD/JSON form) or 'pdf' (PDF form) — controls
243 * which form's inputs are read.
244 */
245 handleAddKnowledge: function(kind) {
246 const self = this;
247 kind = kind === 'pdf' ? 'pdf' : 'text';
248 const isPdf = kind === 'pdf';
249
250 const $form = $(isPdf ? '#wpforo-ai-knowledge-pdf-upload-form' : '#wpforo-ai-knowledge-upload-form');
251 const $button = $(isPdf ? '#knowledge-pdf-upload-btn' : '#knowledge-upload-btn');
252 const $progress = $(isPdf ? '#knowledge-pdf-upload-progress' : '#knowledge-upload-progress');
253 const $detected = $(isPdf ? '#knowledge-pdf-file-detected' : '#knowledge-file-detected');
254
255 const fileUrl = $(isPdf ? '#knowledge-pdf-file-url' : '#knowledge-file-url').val().trim();
256 const fileType = $(isPdf ? '#knowledge-pdf-file-type' : '#knowledge-file-type').val();
257 const fileName = $(isPdf ? '#knowledge-pdf-file-name' : '#knowledge-file-name').val().trim();
258
259 if (!fileUrl) {
260 this.showNotice('Please enter a file URL.', 'error');
261 return;
262 }
263
264 // Reject mismatched extensions on the client to avoid a confusing
265 // "type=pdf but file is .txt" round-trip to the backend.
266 const urlExt = (fileUrl.split('?')[0].split('#')[0].split('.').pop() || '').toLowerCase();
267 if (isPdf && urlExt !== 'pdf') {
268 this.showNotice(
269 'This form is for PDF files. Please use the form above for TXT, MD, or JSON files.',
270 'error'
271 );
272 return;
273 }
274 if (!isPdf && urlExt === 'pdf') {
275 this.showNotice(
276 'PDF files should be uploaded via the PDF form below.',
277 'error'
278 );
279 return;
280 }
281
282 $button.prop('disabled', true).addClass('updating-message');
283 $progress.show();
284 $progress.find('.wpforo-ai-progress-text').text('Submitting...');
285
286 $.ajax({
287 url: wpforoAIAdmin.ajaxUrl,
288 type: 'POST',
289 data: {
290 action: 'wpforo_ai_add_knowledge',
291 nonce: wpforoAIAdmin.nonce,
292 file_url: fileUrl,
293 file_type: fileType,
294 file_name: fileName
295 },
296 success: function(response) {
297 if (response.success) {
298 const data = response.data;
299 $form[0].reset();
300 $detected.hide();
301 // PDF form's hidden file_type defaults back to 'pdf' after reset
302 if (isPdf) {
303 $('#knowledge-pdf-file-type').val('pdf');
304 }
305
306 if (data.async && data.file_id) {
307 // Async processing - start polling for status
308 self.showNotice('File queued for processing. This may take a few minutes...', 'info');
309 self.loadKnowledgeFiles();
310 self.startPollingJobStatus(data.file_id, data.name || fileName);
311 } else {
312 // Sync processing complete
313 self.showNotice(data.message || 'Knowledge file added successfully.', 'success');
314 self.loadKnowledgeFiles();
315 }
316 } else {
317 self.showNotice(response.data.message || 'Failed to add knowledge file.', 'error');
318 }
319 },
320 error: function(xhr, status, error) {
321 self.showNotice('Network error: ' + error, 'error');
322 },
323 complete: function() {
324 $button.prop('disabled', false).removeClass('updating-message');
325 $progress.hide();
326 }
327 });
328 },
329
330 /**
331 * Poll for async job status
332 */
333 startPollingJobStatus: function(fileId, fileName) {
334 const self = this;
335 const pollInterval = 5000; // 5 seconds
336 // Backend Lambda timeout is 300s. Poll for 6 min so a job that
337 // finishes right at the deadline is still picked up cleanly.
338 const maxPolls = 72; // Max 6 minutes
339 let pollCount = 0;
340
341 const poll = function() {
342 pollCount++;
343 if (pollCount > maxPolls) {
344 self.showNotice('Processing is taking longer than expected. Check the file list for status.', 'warning');
345 return;
346 }
347
348 $.ajax({
349 url: wpforoAIAdmin.ajaxUrl,
350 type: 'POST',
351 data: {
352 action: 'wpforo_ai_get_job_status',
353 nonce: wpforoAIAdmin.nonce,
354 file_id: fileId
355 },
356 success: function(response) {
357 if (response.success) {
358 const status = response.data.status;
359
360 if (status === 'enabled' || status === 'completed') {
361 self.showNotice('"' + fileName + '" indexed successfully! (' + (response.data.chunk_count || 0) + ' chunks)', 'success');
362 self.loadKnowledgeFiles();
363 } else if (status === 'failed') {
364 self.showNotice('Indexing failed: ' + (response.data.error_message || 'Unknown error'), 'error');
365 self.loadKnowledgeFiles();
366 } else {
367 // Still processing - poll again
368 setTimeout(poll, pollInterval);
369 }
370 } else {
371 // Error getting status - poll again
372 setTimeout(poll, pollInterval);
373 }
374 },
375 error: function() {
376 // Network error - poll again
377 setTimeout(poll, pollInterval);
378 }
379 });
380 };
381
382 // Start polling after a short delay
383 setTimeout(poll, 2000);
384 },
385
386 /**
387 * Handle delete button click - show modal
388 */
389 handleDeleteClick: function($button) {
390 this.deleteFileId = $button.data('file-id');
391 this.deleteFileName = $button.data('file-name') || this.deleteFileId;
392
393 $('.wpforo-ai-delete-file-name').text(this.deleteFileName);
394 $('#knowledge-delete-modal').show();
395 },
396
397 /**
398 * Confirm delete action
399 */
400 confirmDelete: function() {
401 const self = this;
402 const $modal = $('#knowledge-delete-modal');
403 const $button = $('#knowledge-delete-confirm');
404 const fileId = this.deleteFileId;
405
406 if (!fileId) {
407 $modal.hide();
408 return;
409 }
410
411 // Close modal immediately and show "deleting" status on the file row
412 $modal.hide();
413 $button.prop('disabled', false).removeClass('updating-message');
414
415 // Update the file row to show "deleting" status with spinner
416 const $row = $('tr[data-file-id="' + fileId + '"]');
417 if ($row.length) {
418 $row.find('.column-status .wpforo-ai-status-badge')
419 .removeClass('status-enabled status-disabled status-processing status-queued status-error')
420 .addClass('status-deleting')
421 .html('<span class="wpforo-ai-mini-spinner"></span>deleting');
422 // Disable the delete button for this row
423 $row.find('.wpforo-ai-delete-file').prop('disabled', true).css('opacity', '0.5');
424 }
425
426 // Clear delete state
427 this.deleteFileId = null;
428 this.deleteFileName = null;
429
430 $.ajax({
431 url: wpforoAIAdmin.ajaxUrl,
432 type: 'POST',
433 timeout: 60000,
434 data: {
435 action: 'wpforo_ai_delete_knowledge',
436 nonce: wpforoAIAdmin.nonce,
437 file_id: fileId
438 },
439 success: function(response) {
440 if (response.success) {
441 self.showNotice(response.data.message || 'Knowledge file deleted successfully.', 'success');
442 // Remove the row directly - don't rely on reload which might get stale data
443 $row.fadeOut(300, function() {
444 $(this).remove();
445 // Update file count
446 const remaining = $('#knowledge-files-tbody tr').length;
447 $('#knowledge-file-count').text('(' + remaining + ')');
448 // Show empty state if no files left
449 if (remaining === 0) {
450 $('#knowledge-files-table').hide();
451 $('#knowledge-files-empty').show();
452 }
453 });
454 } else {
455 self.showNotice(response.data.message || 'Failed to delete knowledge file.', 'error');
456 // Restore the row on failure
457 self.loadKnowledgeFiles();
458 }
459 },
460 error: function(xhr, status, error) {
461 if (status === 'timeout') {
462 self.showNotice('Deletion is taking longer than expected. Please refresh the page in a moment.', 'warning');
463 } else {
464 self.showNotice('Network error: ' + error, 'error');
465 }
466 // Reload to get current state
467 self.loadKnowledgeFiles();
468 }
469 });
470 },
471
472 /**
473 * Cancel delete action
474 */
475 cancelDelete: function() {
476 this.deleteFileId = null;
477 this.deleteFileName = null;
478 $('#knowledge-delete-modal').hide();
479 },
480
481 /**
482 * Handle priority select change - prevent duplicates within same feature
483 */
484 handlePriorityChange: function($select) {
485 const feature = $select.data('feature');
486 const selectedValue = $select.val();
487
488 // Get all selects for this feature
489 const $featureSelects = $('.priority-select[data-feature="' + feature + '"]');
490 const selectIndex = $featureSelects.index($select);
491
492 // Find which other select has the same value and swap
493 $featureSelects.each(function(index) {
494 const $other = $(this);
495
496 if (index !== selectIndex && $other.val() === selectedValue) {
497 // Find an available value for the other select
498 const usedValues = [];
499 $featureSelects.each(function(i) {
500 if (i !== index) {
501 usedValues.push($(this).val());
502 }
503 });
504
505 const allValues = ['forum', 'wordpress', 'custom_knowledge'];
506 for (let i = 0; i < allValues.length; i++) {
507 if (usedValues.indexOf(allValues[i]) === -1) {
508 $other.val(allValues[i]);
509 break;
510 }
511 }
512 }
513 });
514 },
515
516 /**
517 * Load board settings from WordPress (per-board)
518 */
519 loadBoardSettings: function() {
520 const self = this;
521 const boardId = $('#knowledge-board-select').val() || '0';
522 const $loading = $('#knowledge-settings-loading');
523 const $content = $('#knowledge-settings-content');
524
525 $loading.show();
526 $content.hide();
527
528 // Update hidden field
529 $('#knowledge-settings-board-id').val(boardId);
530
531 $.ajax({
532 url: wpforoAIAdmin.ajaxUrl,
533 type: 'POST',
534 data: {
535 action: 'wpforo_ai_get_knowledge_settings',
536 nonce: wpforoAIAdmin.nonce,
537 board_id: boardId
538 },
539 success: function(response) {
540 $loading.hide();
541 $content.show();
542
543 if (response.success && response.data) {
544 const enabled = response.data.enabled || false;
545 const priorities = response.data.priorities || {};
546
547 // Set enabled toggle
548 $('#knowledge-enabled').prop('checked', enabled);
549 self.togglePrioritySection(enabled);
550
551 // Set priority selects
552 self.updatePrioritySelects(priorities);
553 } else {
554 // Use defaults
555 $('#knowledge-enabled').prop('checked', false);
556 self.togglePrioritySection(false);
557 self.updatePrioritySelects({});
558 }
559 },
560 error: function() {
561 $loading.hide();
562 $content.show();
563 // Use defaults on error
564 $('#knowledge-enabled').prop('checked', false);
565 self.togglePrioritySection(false);
566 self.updatePrioritySelects({});
567 }
568 });
569 },
570
571 /**
572 * Toggle visibility of priority section based on enabled state
573 */
574 togglePrioritySection: function(enabled) {
575 const $section = $('#knowledge-priority-section');
576 if (enabled) {
577 $section.slideDown(200);
578 } else {
579 $section.slideUp(200);
580 }
581 },
582
583 /**
584 * Handle save settings form (combined enable + priorities)
585 */
586 handleSaveSettings: function() {
587 const self = this;
588 const $form = $('#wpforo-ai-knowledge-settings-form');
589 const $button = $('#knowledge-save-settings');
590 const $status = $('#settings-save-status');
591
592 const boardId = $('#knowledge-settings-board-id').val();
593 const enabled = $('#knowledge-enabled').is(':checked');
594
595 // Collect priorities from selects - build arrays in order
596 const priorities = {
597 search_priority: [],
598 chat_priority: [],
599 bot_reply_priority: []
600 };
601
602 ['search', 'chat', 'bot_reply'].forEach(function(feature) {
603 $form.find('[name="' + feature + '_priority[]"]').each(function() {
604 priorities[feature + '_priority'].push($(this).val());
605 });
606 });
607
608 $button.prop('disabled', true).addClass('updating-message');
609 $status.text('Saving...');
610
611 $.ajax({
612 url: wpforoAIAdmin.ajaxUrl,
613 type: 'POST',
614 data: {
615 action: 'wpforo_ai_save_knowledge_settings',
616 nonce: wpforoAIAdmin.nonce,
617 board_id: boardId,
618 enabled: enabled ? 1 : 0,
619 search_priority: priorities.search_priority,
620 chat_priority: priorities.chat_priority,
621 bot_reply_priority: priorities.bot_reply_priority
622 },
623 success: function(response) {
624 if (response.success) {
625 $status.text('Saved!').addClass('success');
626 setTimeout(function() {
627 $status.text('').removeClass('success');
628 }, 2000);
629 } else {
630 self.showNotice(response.data.message || 'Failed to save settings.', 'error');
631 $status.text('');
632 }
633 },
634 error: function(xhr, status, error) {
635 self.showNotice('Network error: ' + error, 'error');
636 $status.text('');
637 },
638 complete: function() {
639 $button.prop('disabled', false).removeClass('updating-message');
640 }
641 });
642 },
643
644 /**
645 * Handle refresh files button
646 */
647 handleRefreshFiles: function() {
648 const self = this;
649 const $button = $('#knowledge-refresh-files');
650 const $icon = $button.find('.dashicons-update');
651
652 $icon.addClass('wpforo-spin');
653 $button.prop('disabled', true);
654
655 this.loadKnowledgeFiles(function() {
656 $icon.removeClass('wpforo-spin');
657 $button.prop('disabled', false);
658 });
659 },
660
661 /**
662 * Load knowledge files from API
663 */
664 loadKnowledgeFiles: function(callback) {
665 const self = this;
666 const $loading = $('#knowledge-files-loading');
667 const $table = $('#knowledge-files-table');
668 const $empty = $('#knowledge-files-empty');
669
670 $loading.show();
671 $table.hide();
672 $empty.hide();
673
674 $.ajax({
675 url: wpforoAIAdmin.ajaxUrl,
676 type: 'POST',
677 data: {
678 action: 'wpforo_ai_get_knowledge_files',
679 nonce: wpforoAIAdmin.nonce
680 },
681 success: function(response) {
682 $loading.hide();
683
684 if (response.success) {
685 const files = response.data.files || [];
686 const totals = response.data.totals || {};
687
688 if (files.length > 0) {
689 self.renderFilesTable(files, totals);
690 $table.show();
691 } else {
692 $empty.show();
693 }
694
695 // Update file count
696 $('#knowledge-file-count').text('(' + files.length + ')');
697 } else {
698 // API returned error - show empty state
699 $empty.show();
700 }
701 },
702 error: function(xhr, status, error) {
703 // Network/API error - show empty state silently
704 // Backend endpoints may not exist yet (Phase 6)
705 $loading.hide();
706 $empty.show();
707 },
708 complete: function() {
709 if (typeof callback === 'function') {
710 callback();
711 }
712 }
713 });
714 },
715
716 /**
717 * Render files table
718 */
719 renderFilesTable: function(files, totals) {
720 const self = this;
721 const $tbody = $('#knowledge-files-tbody');
722 $tbody.empty();
723
724 let totalChunks = 0;
725 let totalCredits = 0;
726 let totalSize = 0;
727
728 files.forEach(function(file) {
729 const statusLabel = file.status || 'unknown';
730 const statusClass = statusLabel === 'enabled' ? 'status-enabled' :
731 statusLabel === 'disabled' ? 'status-disabled' :
732 statusLabel === 'processing' ? 'status-processing' :
733 statusLabel === 'queued' ? 'status-queued' :
734 statusLabel === 'pending' ? 'status-pending' :
735 statusLabel === 'deleting' ? 'status-deleting' :
736 statusLabel === 'error' || statusLabel === 'failed' ? 'status-error' : '';
737
738 const sizeBytes = parseInt(file.size_bytes || 0, 10);
739 totalSize += sizeBytes;
740 totalChunks += parseInt(file.chunks || 0, 10);
741 totalCredits += parseInt(file.credits_used || 0, 10);
742
743 const row = '<tr data-file-id="' + self.escapeHtml(file.file_id) + '">' +
744 '<td class="column-name">' +
745 '<strong>' + self.escapeHtml(file.name || file.file_id) + '</strong>' +
746 '<div class="row-actions">' +
747 '<span class="view">' +
748 '<a href="' + self.escapeHtml(file.url) + '" target="_blank" rel="noopener">View</a>' +
749 '</span>' +
750 '</div>' +
751 '</td>' +
752 '<td class="column-type">' + self.escapeHtml(file.type || 'text') + '</td>' +
753 '<td class="column-size">' + self.formatFileSize(sizeBytes) + '</td>' +
754 '<td class="column-chunks">' + (file.chunks || 0) + '</td>' +
755 '<td class="column-credits">' + (file.credits_used || 0) + '</td>' +
756 '<td class="column-status">' +
757 '<span class="wpforo-ai-status-badge ' + statusClass + '">' +
758 (statusLabel === 'processing' || statusLabel === 'queued' || statusLabel === 'deleting' ? '<span class="wpforo-ai-mini-spinner"></span>' : '') +
759 self.escapeHtml(statusLabel) +
760 '</span>' +
761 '</td>' +
762 '<td class="column-actions">' +
763 '<button type="button" class="wpforo-ai-delete-file" title="Delete this file" data-file-id="' + self.escapeHtml(file.file_id) + '" data-file-name="' + self.escapeHtml(file.name || file.file_id) + '">' +
764 '<span class="dashicons dashicons-trash"></span>' +
765 '</button>' +
766 '</td>' +
767 '</tr>';
768
769 $tbody.append(row);
770 });
771
772 // Update totals - prefer server-provided totals if available
773 $('#knowledge-total-size').text(self.formatFileSize(totalSize));
774 $('#knowledge-total-chunks').text(totals.total_chunks || totalChunks);
775 $('#knowledge-total-credits').text(totals.total_credits || totalCredits);
776 },
777
778 /**
779 * Format file size in human readable format
780 */
781 formatFileSize: function(bytes) {
782 if (bytes === 0) return '-';
783 if (bytes < 1024) return bytes + ' B';
784 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
785 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
786 },
787
788 /**
789 * Update priority selects with current values from server
790 */
791 updatePrioritySelects: function(priorities) {
792 // Default priorities per feature
793 const defaults = {
794 search: ['forum', 'wordpress', 'custom_knowledge'],
795 chat: ['custom_knowledge', 'forum', 'wordpress'],
796 bot_reply: ['forum', 'custom_knowledge', 'wordpress']
797 };
798
799 const features = ['search', 'chat', 'bot_reply'];
800
801 features.forEach(function(feature) {
802 // PHP returns priorities.search, priorities.chat, etc. (no _priority suffix)
803 const priority = priorities[feature];
804 const order = (Array.isArray(priority) && priority.length === 3) ? priority : defaults[feature];
805
806 // Select array-based inputs: name="search_priority[]"
807 const $selects = $('[name="' + feature + '_priority[]"]');
808 $selects.each(function(index) {
809 if (order[index]) {
810 $(this).val(order[index]);
811 }
812 });
813 });
814 },
815
816 /**
817 * Show notice
818 */
819 showNotice: function(message, type) {
820 const $container = $('.wpforo-ai-tools-tab');
821 const noticeClass = type === 'error' ? 'notice-error' : type === 'success' ? 'notice-success' : 'notice-info';
822
823 // Remove existing notices
824 $container.find('.wpforo-ai-notice').remove();
825
826 const $notice = $('<div class="notice ' + noticeClass + ' wpforo-ai-notice is-dismissible" style="margin: 15px 0;">' +
827 '<p>' + message + '</p>' +
828 '<button type="button" class="notice-dismiss">' +
829 '<span class="screen-reader-text">Dismiss this notice.</span>' +
830 '</button>' +
831 '</div>');
832
833 $container.prepend($notice);
834
835 // Auto-dismiss after 5 seconds
836 setTimeout(function() {
837 $notice.fadeOut(function() {
838 $(this).remove();
839 });
840 }, 5000);
841
842 // Manual dismiss
843 $notice.on('click', '.notice-dismiss', function() {
844 $notice.fadeOut(function() {
845 $(this).remove();
846 });
847 });
848 },
849
850 /**
851 * Escape HTML to prevent XSS
852 */
853 escapeHtml: function(text) {
854 if (text === null || text === undefined) return '';
855 const div = document.createElement('div');
856 div.textContent = String(text);
857 return div.innerHTML;
858 }
859 };
860
861 // Initialize on document ready
862 $(document).ready(function() {
863 WpForoAITools.init();
864 });
865
866 // Expose globally for debugging
867 window.WpForoAITools = WpForoAITools;
868
869 })(jQuery);
870