PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.7
MxChat – AI Chatbot & Content Generation for WordPress v3.0.7
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | js/mxchat_actions.js +23 -642 3.2.193.0.7 View file →
@@ -59,427 +59,13 @@
59 59 $('.mxch-mobile-menu').removeClass('open');
60 60 $('.mxch-mobile-overlay').removeClass('open');
61 61 });
62 62
63 - // ==========================================================================
64 - // AI Tools: autosave the function-calling toggle + per-tool checklist
65 - // (matches MxChat's autosave UX — no Save button). plan a41dee.
66 - // ==========================================================================
67 - var $fc = $('#mxch-fc-settings');
68 - if ($fc.length) {
69 - var fcNonce = $fc.data('fc-nonce');
70 - var $fcStatus = $('#mxch-fc-save-status');
71 - var fcTimer = null;
72 -
73 - function fcCollectAndSave() {
74 - // Presence = active (plan d450a7): the global on/off toggle is gone, so
75 - // the gate is derived from whether any tool is enabled. The server
76 - // derives this too; we send it for an immediate, correct nav badge.
77 - var enabled = $fc.find('input[name="mxchat_fc_tools[]"]:checked').length > 0 ? '1' : '0';
78 - var allTools = $fc.find('input[name="mxchat_fc_all_tools[]"]').map(function () { return $(this).val(); }).get();
79 - var checked = $fc.find('input[name="mxchat_fc_tools[]"]:checked').map(function () { return $(this).val(); }).get();
80 - // Per-tool "when to use" hints, keyed by callback.
81 - var hints = {};
82 - $fc.find('.mxch-fc-hint-input').each(function () {
83 - var cb = $(this).data('fc-callback');
84 - if (cb) { hints[cb] = $(this).val(); }
85 - });
86 -
87 - if ($fcStatus.length) { $fcStatus.text('Saving…').removeClass('is-saved is-error'); }
88 -
89 - $.post(mxchActionsData.ajaxUrl, {
90 - action: 'mxchat_fc_autosave',
91 - nonce: fcNonce,
92 - enabled: enabled,
93 - all_tools: allTools,
94 - tools: checked,
95 - hints: hints
96 - }).done(function () {
97 - if ($fcStatus.length) {
98 - $fcStatus.text('Saved').addClass('is-saved');
99 - setTimeout(function () { $fcStatus.text('').removeClass('is-saved'); }, 2000);
100 - }
101 - }).fail(function () {
102 - if ($fcStatus.length) { $fcStatus.text('Save failed — try again').addClass('is-error'); }
103 - });
104 - }
105 -
106 - // The hidden store's checkboxes are toggled programmatically by the
107 - // list/detail/modal flow (which calls fcCollectAndSave() directly). Keep
108 - // a change-bound autosave as a safety net for any direct mutation.
109 - $fc.on('change', 'input[name="mxchat_fc_tools[]"]', function () {
110 - clearTimeout(fcTimer);
111 - fcTimer = setTimeout(fcCollectAndSave, 200);
112 - });
113 -
114 - // ======================================================================
115 - // AI Tools MASTER-DETAIL list + detail panel (plan d450a7).
116 - // Clones the Trigger Phrases list/detail UX: the left list shows the
117 - // ACTIVE tools (a tool in the list = active — no global toggle, no
118 - // per-tool enable checkbox); selecting one shows its detail on the right
119 - // with Edit (usage note) + Remove. "Add" opens the same two-step
120 - // capability picker. Pure view layer over the hidden #mxch-fc-tool-store
121 - // inputs — the unchanged mxchat_fc_autosave contract (mxchat_fc_tools[] /
122 - // mxchat_fc_all_tools[] + hints) stays the source of truth. FC-specific
123 - // IDs/classes keep the Trigger Phrases JS off these elements.
124 - // ======================================================================
125 - var $fcStore = $('#mxch-fc-tool-store');
126 - if ($fcStore.length) {
127 - var $fcPanel = $('#mxch-fc-panel');
128 - var $fcList = $('#mxch-fc-list');
129 - var $fcCount = $('#mxch-fc-count');
130 - var $fcEmpty = $('#mxch-fc-empty');
131 - var $fcView = $('#mxch-fc-view');
132 - var $fcDetail = $('#mxch-fc-detail-panel');
133 - var $fcSearch = $('#mxch-fc-search');
134 - var $fcModal = $('#mxch-fc-tool-modal');
135 - var fcCurrentCb = null; // tool being added/edited in the modal
136 - var fcViewCb = null; // tool currently shown in the detail panel
137 - var fcGuardedClose = null; // dirty-checked close, re-armed on each open
138 - var fcSelected = new Set(); // callbacks ticked for bulk remove (plan 5f7409)
139 -
140 - var fcI18n = {
141 - tools: $fcPanel.data('i18n-tools') || 'tools',
142 - tool: $fcPanel.data('i18n-tool') || 'tool',
143 - active: $fcPanel.data('i18n-active') || 'Active',
144 - addon: $fcPanel.data('i18n-addon') || 'Add-on',
145 - sensitive: $fcPanel.data('i18n-sensitive') || 'Sensitive',
146 - noHint: $fcPanel.data('i18n-nohint') || 'No usage note — the AI decides on its own when to use this.'
147 - };
148 - // Localized Add/Edit titles + button labels read from the modal DOM.
149 - var $fcStep2Title = $('#mxch-fc-modal-step2-title');
150 - var $fcSaveBtn = $('#mxch-fc-modal-save');
151 - var FC_TITLE_ADD = $fcStep2Title.data('add-title') || 'Add Tool';
152 - var FC_TITLE_EDIT = $fcStep2Title.data('edit-title') || 'Edit Tool';
153 - var FC_BTN_ADD = $fcSaveBtn.data('add-label') || 'Add Tool';
154 - var FC_BTN_SAVE = $fcSaveBtn.data('save-label') || 'Save Changes';
155 -
156 - function fcEsc(s) { return $('<div>').text(s == null ? '' : String(s)).html(); }
157 - function fcEntry(cb) {
158 - return $fcStore.find('.mxch-fc-tool').filter(function () {
159 - return String($(this).data('fc-callback')) === String(cb);
160 - });
161 - }
162 - function fcIsOn($t) { return $t.find('input[name="mxchat_fc_tools[]"]').is(':checked'); }
163 - function fcIsMobile() { return window.innerWidth <= 782; }
164 -
165 - // Show the empty "Select a tool" state in the detail panel.
166 - function fcShowEmpty() {
167 - fcViewCb = null;
168 - if ($fcView.length) $fcView.hide();
169 - if ($fcEmpty.length) $fcEmpty.show();
170 - $fcList.find('.mxch-fc-list-item').removeClass('active');
171 - if (fcIsMobile()) { $fcDetail.removeClass('mobile-active'); $('body').removeClass('mxch-mobile-panel-open'); }
172 - }
173 -
174 - // Build the left list from the store (active tools only), applying the
175 - // current search filter. Updates the count + resets the view if the
176 - // shown tool was just removed.
177 - function fcRenderList() {
178 - if (!$fcList.length) return;
179 - var q = $.trim(($fcSearch.val() || '')).toLowerCase();
180 - $fcList.empty();
181 - var total = 0;
182 - $fcStore.find('.mxch-fc-tool').each(function () {
183 - var $t = $(this);
184 - if (!fcIsOn($t)) return;
185 - total++;
186 - var cb = $t.data('fc-callback');
187 - var label = $t.data('fc-label') || cb;
188 - var desc = $t.data('fc-desc') || '';
189 - var icon = $t.data('fc-icon') || 'admin-generic';
190 - var isAddon = String($t.data('fc-addon')) === '1';
191 - var isCautious = String($t.data('fc-cautious')) === '1';
192 - if (q && String(label).toLowerCase().indexOf(q) === -1 && String(desc).toLowerCase().indexOf(q) === -1) return;
193 - var typeBits = [];
194 - if (isAddon) typeBits.push(fcI18n.addon);
195 - if (isCautious) typeBits.push(fcI18n.sensitive);
196 - var typeLabel = typeBits.length ? typeBits.join(' · ') : fcI18n.active;
197 - var activeCls = (String(cb) === String(fcViewCb)) ? ' active' : '';
198 - var isSel = fcSelected.has(String(cb));
199 - var selCls = isSel ? ' selected' : '';
200 - $fcList.append(
201 - '<div class="mxch-fc-list-item' + activeCls + selCls + '" data-fc-callback="' + fcEsc(cb) + '">' +
202 - '<input type="checkbox" class="mxch-fc-checkbox"' + (isSel ? ' checked' : '') + '>' +
203 - '<div class="mxch-action-icon"><span class="dashicons dashicons-' + fcEsc(icon) + '"></span></div>' +
204 - '<div class="mxch-action-info">' +
205 - '<div class="mxch-action-name">' + fcEsc(label) + '</div>' +
206 - '<div class="mxch-action-type-label">' + fcEsc(typeLabel) + '</div>' +
207 - '</div>' +
208 - '<div class="mxch-action-meta">' +
209 - '<span class="mxch-action-status enabled">' + fcEsc(fcI18n.active) + '</span>' +
210 - '</div>' +
211 - '</div>'
212 - );
213 - });
214 - if ($fcCount.length) {
215 - $fcCount.text(total + ' ' + (total === 1 ? fcI18n.tool : fcI18n.tools));
216 - }
217 - // Drop any selected callbacks that are no longer in the list (e.g.
218 - // filtered out or removed) so the bulk count stays truthful.
219 - var fcPresent = {};
220 - $fcList.find('.mxch-fc-list-item').each(function () {
221 - fcPresent[String($(this).data('fc-callback'))] = true;
222 - });
223 - fcSelected.forEach(function (selCb) {
224 - if (!fcPresent[String(selCb)]) { fcSelected.delete(selCb); }
225 - });
226 - fcUpdateSelectionUI();
227 - // If the tool currently in the detail panel is no longer active, reset.
228 - if (fcViewCb && !fcIsOn(fcEntry(fcViewCb))) { fcShowEmpty(); }
229 - }
230 -
231 - // Populate + show the detail view for one tool.
232 - function fcSelectTool(cb) {
233 - var $t = fcEntry(cb);
234 - if (!$t.length || !fcIsOn($t)) { fcShowEmpty(); return; }
235 - fcViewCb = String(cb);
236 - var label = $t.data('fc-label') || cb;
237 - var desc = $t.data('fc-desc') || '';
238 - var icon = $t.data('fc-icon') || 'admin-generic';
239 - var isAddon = String($t.data('fc-addon')) === '1';
240 - var isCautious = String($t.data('fc-cautious')) === '1';
241 - var hint = $.trim($t.find('.mxch-fc-hint-input').val() || '');
242 - var typeBits = [];
243 - if (isAddon) typeBits.push(fcI18n.addon);
244 - if (isCautious) typeBits.push(fcI18n.sensitive);
245 - $('#mxch-fc-view-icon').html('<span class="dashicons dashicons-' + fcEsc(icon) + '"></span>');
246 - $('#mxch-fc-view-label').text(label);
247 - $('#mxch-fc-view-type').text(typeBits.join(' · '));
248 - $('#mxch-fc-view-desc').text(desc);
249 - var $hint = $('#mxch-fc-view-hint');
250 - if (hint) { $hint.text(hint).removeClass('is-muted'); }
251 - else { $hint.text(fcI18n.noHint).addClass('is-muted'); }
252 - // Setup requirement (plan 183856): show the tool's setup note (e.g. the
253 - // Brave-key requirement on Web/Image Search) when present, else hide it.
254 - var setup = $.trim(String($t.data('fc-setup') || ''));
255 - var $setupWrap = $('#mxch-fc-view-setup-wrap');
256 - if (setup) { $('#mxch-fc-view-setup').text(setup); $setupWrap.show(); }
257 - else { $setupWrap.hide(); }
258 - $fcEmpty.hide();
259 - $fcView.show();
260 - $fcList.find('.mxch-fc-list-item').removeClass('active')
261 - .filter(function () { return String($(this).data('fc-callback')) === String(cb); }).addClass('active');
262 - if (fcIsMobile()) { $fcDetail.addClass('mobile-active'); $('body').addClass('mxch-mobile-panel-open'); }
263 - }
264 -
265 - // ---- Add/Edit modal (two-step capability picker) ----
266 - function fcShowStep(n) {
267 - $fcModal.find('.mxch-fc-modal-step').hide();
268 - $fcModal.find('.mxch-fc-modal-step[data-step="' + n + '"]').show();
269 - }
270 - function fcCloseModal() { $fcModal.hide(); fcCurrentCb = null; fcGuardedClose = null; }
271 -
272 - // The modal holds an editable "when to use" hint, so it never closes on the
273 - // backdrop and confirms before discarding. fcArmGuard must run AFTER a step
274 - // populates its fields: mxchatGuardedClose snapshots at call time, so arming
275 - // it earlier would measure "dirty" against empty and confirm on every close.
276 - function fcArmGuard() {
277 - if (typeof mxchatGuardedClose !== 'function' || !$fcModal.length) return;
278 - fcGuardedClose = mxchatGuardedClose($fcModal[0], fcCloseModal);
279 - }
280 - function fcRequestClose(e) { (fcGuardedClose || fcCloseModal)(e); }
281 -
282 - // Step 1: grid of tools not yet added.
283 - function fcOpenAddModal() {
284 - // Back-to-step-1 keeps the step-2 baseline: re-arming here would treat a
285 - // hint typed before Back as clean and discard it silently on close.
286 - var fcWasOpen = $fcModal.is(':visible');
287 - var $grid = $('#mxch-fc-modal-grid').empty();
288 - var addable = 0;
289 - $fcStore.find('.mxch-fc-tool').each(function () {
290 - var $t = $(this);
291 - if (fcIsOn($t)) return; // already active
292 - addable++;
293 - var cb = $t.data('fc-callback');
294 - var label = $t.data('fc-label') || cb;
295 - var desc = $t.data('fc-desc') || '';
296 - var icon = $t.data('fc-icon') || 'admin-generic';
297 - var isAddon = String($t.data('fc-addon')) === '1';
298 - var isCautious = String($t.data('fc-cautious')) === '1';
299 - var badges = '';
300 - if (isAddon) { badges += ' <span class="mxch-fc-badge mxch-fc-badge-addon">' + fcEsc(fcI18n.addon) + '</span>'; }
301 - if (isCautious) { badges += ' <span class="mxch-fc-badge mxch-fc-badge-sensitive">' + fcEsc(fcI18n.sensitive) + '</span>'; }
302 - $grid.append(
303 - '<div class="mxch-type-card mxch-fc-type-card" data-fc-callback="' + fcEsc(cb) + '">' +
304 - '<div class="mxch-type-icon"><span class="dashicons dashicons-' + fcEsc(icon) + '"></span></div>' +
305 - '<div class="mxch-type-info">' +
306 - '<h4>' + fcEsc(label) + badges + '</h4>' +
307 - '<p>' + fcEsc(desc) + '</p>' +
308 - '</div>' +
309 - '</div>'
310 - );
311 - });
312 - $('#mxch-fc-modal-allset').toggle(addable === 0);
313 - $grid.toggle(addable > 0);
314 - fcShowStep(1);
315 - $fcModal.css('display', 'flex');
316 - if (!fcWasOpen) fcArmGuard();
317 - }
318 -
319 - // Step 2: confirm + when-to-use (shared by Add and Edit).
320 - function fcOpenConfigStep(cb) {
321 - var $t = fcEntry(cb);
322 - if (!$t.length) return;
323 - fcCurrentCb = cb;
324 - var editing = fcIsOn($t);
325 - $('#mxch-fc-modal-selected-icon').html('<span class="dashicons dashicons-' + fcEsc($t.data('fc-icon') || 'admin-generic') + '"></span>');
326 - $('#mxch-fc-modal-selected-label').text($t.data('fc-label') || cb);
327 - $('#mxch-fc-modal-selected-desc').text($t.data('fc-desc') || '');
328 - $('#mxch-fc-modal-hint').val($t.find('.mxch-fc-hint-input').val() || '');
329 - $('#mxch-fc-modal-sensitive').toggle(String($t.data('fc-cautious')) === '1');
330 - $fcStep2Title.text(editing ? FC_TITLE_EDIT : FC_TITLE_ADD);
331 - $fcSaveBtn.text(editing ? FC_BTN_SAVE : FC_BTN_ADD);
332 - fcShowStep(2);
333 - $fcModal.css('display', 'flex');
334 - fcArmGuard(); // after the hint is loaded: that value is the clean baseline
335 - }
336 -
337 - // ---- wiring ----
338 - // "Add" buttons (panel header + empty state) — delegated on $fc.
339 - $fc.on('click', '.js-mxch-fc-add', fcOpenAddModal);
340 - $fcModal.on('click', '.mxch-fc-type-card', function () { fcOpenConfigStep($(this).data('fc-callback')); });
341 - $('#mxch-fc-modal-back').on('click', fcOpenAddModal);
342 - $fcModal.on('click', '[data-fc-modal-close]', fcRequestClose); // X + Cancel
343 - // No backdrop close: a click dispatches on the common ancestor of its mousedown
344 - // and mouseup, so releasing a selection past the dialog edge targets the backdrop
345 - // and would discard the hint — and a plain backdrop click discarded it outright.
346 - $(document).on('keydown', function (e) { if (e.key === 'Escape' && $fcModal.is(':visible')) fcRequestClose(e); });
347 -
348 - // Modal Save (Add or Edit): write the store inputs, autosave, refresh
349 - // the list, and show the tool's detail on the right.
350 - $fcSaveBtn.on('click', function () {
351 - if (!fcCurrentCb) return;
352 - var $t = fcEntry(fcCurrentCb);
353 - if (!$t.length) return;
354 - var cb = fcCurrentCb;
355 - $t.find('.mxch-fc-hint-input').val($('#mxch-fc-modal-hint').val() || '');
356 - $t.find('input[name="mxchat_fc_tools[]"]').prop('checked', true);
357 - fcCloseModal();
358 - fcRenderList();
359 - fcSelectTool(cb);
360 - fcCollectAndSave();
361 - });
362 -
363 - // List item click → show that tool's detail.
364 - $fcList.on('click', '.mxch-fc-list-item', function () {
365 - fcSelectTool($(this).data('fc-callback'));
366 - });
367 -
368 - // Detail Edit → reopen the config step for the viewed tool.
369 - $('#mxch-fc-edit-btn').on('click', function () {
370 - if (fcViewCb) fcOpenConfigStep(fcViewCb);
371 - });
372 - // Detail Remove → disable the tool (keep its hint for a later re-add),
373 - // refresh the list, and return to the empty state.
374 - $('#mxch-fc-remove-btn').on('click', function () {
375 - if (!fcViewCb) return;
376 - var $t = fcEntry(fcViewCb);
377 - if (!$t.length) return;
378 - $t.find('input[name="mxchat_fc_tools[]"]').prop('checked', false);
379 - fcShowEmpty();
380 - fcRenderList();
381 - fcCollectAndSave();
382 - });
383 -
384 - // ---- Bulk selection + bulk remove ----
385 - // Clones the Trigger Phrases #all-actions bulk toolbar behavior, but
386 - // wires removal to the SAME path the single Remove button uses (uncheck
387 - // each tool's store checkbox, then one autosave). FC-specific IDs keep
388 - // this isolated from the Trigger Phrases bulk JS (plan 5f7409).
389 - var $fcSelectAll = $('#mxch-fc-select-all');
390 - var $fcBulkDelete = $('#mxch-fc-delete-selected');
391 - var $fcSelCount = $('#mxch-fc-selected-count');
392 -
393 - function fcUpdateSelectionUI() {
394 - var count = fcSelected.size;
395 - if (count > 0) {
396 - $fcSelCount.text(count + ' selected').addClass('has-selection');
397 - $fcBulkDelete.prop('disabled', false);
398 - $fcList.addClass('selection-mode');
399 - } else {
400 - $fcSelCount.removeClass('has-selection');
401 - $fcBulkDelete.prop('disabled', true);
402 - $fcList.removeClass('selection-mode');
403 - }
404 - var totalItems = $fcList.find('.mxch-fc-checkbox').length;
405 - var checkedItems = $fcList.find('.mxch-fc-checkbox:checked').length;
406 - $fcSelectAll.prop('checked', totalItems > 0 && checkedItems === totalItems);
407 - $fcSelectAll.prop('indeterminate', checkedItems > 0 && checkedItems < totalItems);
408 - }
409 -
410 - // Select-all toggles every visible tool row.
411 - $fcSelectAll.on('change', function () {
412 - var on = $(this).is(':checked');
413 - fcSelected.clear();
414 - $fcList.find('.mxch-fc-list-item').each(function () {
415 - var $row = $(this);
416 - var cb = String($row.data('fc-callback'));
417 - $row.find('.mxch-fc-checkbox').prop('checked', on);
418 - if (on) { fcSelected.add(cb); $row.addClass('selected'); }
419 - else { $row.removeClass('selected'); }
420 - });
421 - fcUpdateSelectionUI();
422 - });
423 -
424 - // Per-row checkbox: toggle selection without opening the detail view.
425 - $fcList.on('click', '.mxch-fc-checkbox', function (e) {
426 - e.stopPropagation();
427 - var $row = $(this).closest('.mxch-fc-list-item');
428 - var cb = String($row.data('fc-callback'));
429 - if ($(this).is(':checked')) { fcSelected.add(cb); $row.addClass('selected'); }
430 - else { fcSelected.delete(cb); $row.removeClass('selected'); }
431 - fcUpdateSelectionUI();
432 - });
433 -
434 - // Bulk remove: same removal path as single Remove — uncheck each tool's
435 - // store checkbox (presence = active), then refresh + autosave once.
436 - $fcBulkDelete.on('click', function () {
437 - if (!fcSelected.size) return;
438 - var msg = (window.mxchActionsData && mxchActionsData.i18n && mxchActionsData.i18n.confirmBulkDelete)
439 - || 'Remove the selected tools? They stay available to add back later.';
440 - if (!window.confirm(msg)) return;
441 - var removingViewed = false;
442 - fcSelected.forEach(function (cb) {
443 - var $t = fcEntry(cb);
444 - if ($t.length) { $t.find('input[name="mxchat_fc_tools[]"]').prop('checked', false); }
445 - if (String(cb) === String(fcViewCb)) { removingViewed = true; }
446 - });
447 - fcSelected.clear();
448 - if (removingViewed) { fcShowEmpty(); }
449 - fcRenderList();
450 - fcCollectAndSave();
451 - });
452 -
453 - // Search filter.
454 - $fcSearch.on('input', function () { fcRenderList(); });
455 -
456 - // Refresh — re-render the list from the store (parity with the Trigger
457 - // Phrases refresh affordance; the list is client-built so this re-syncs
458 - // the view rather than re-fetching).
459 - $('#mxch-fc-refresh').on('click', function () {
460 - var $btn = $(this);
461 - $btn.addClass('spinning');
462 - fcRenderList();
463 - setTimeout(function () { $btn.removeClass('spinning'); }, 500);
464 - });
465 -
466 - fcRenderList();
467 - }
468 - }
469 -
470 63 // Quick action buttons
471 64 $('.mxch-quick-action-btn[data-action="view-actions"]').on('click', function() {
472 65 $('.mxch-nav-link[data-target="all-actions"]').trigger('click');
473 66 });
474 67
475 - // Dashboard explainer cards — generic "jump to this section" (e.g. Set up AI Tools).
476 - $('.mxch-approach-btn[data-approach-target]').on('click', function() {
477 - var target = $(this).data('approach-target');
478 - $('.mxch-nav-link[data-target="' + target + '"]').trigger('click');
479 - });
480 -
481 - // "Add Trigger Phrase" on the dashboard → jump to Trigger Phrases + open the editor.
482 68 $('#mxch-add-action-dashboard-btn').on('click', function() {
483 69 $('.mxch-nav-link[data-target="all-actions"]').trigger('click');
484 70 setTimeout(function() {
485 71 openActionEditor();
@@ -527,171 +113,8 @@
527 113 let selectedActions = new Set();
528 114 let currentSortOrder = 'desc';
529 115 let currentFilter = '';
530 116
531 - // ==========================================================================
532 - // Phrase Tag Management
533 - // ==========================================================================
534 -
535 - let currentPhrases = []; // Array of {id, text, isLegacy}
536 -
537 - function renderPhraseTags() {
538 - const $container = $('#mxch-phrase-tags');
539 - let html = '';
540 - currentPhrases.forEach(function(phrase, index) {
541 - const legacyClass = phrase.isLegacy ? ' mxch-phrase-legacy' : '';
542 - const badge = phrase.isLegacy ? ' <span class="mxch-legacy-badge">legacy</span>' : '';
543 - html += '<span class="mxch-phrase-pill' + legacyClass + '" data-index="' + index + '" data-phrase-id="' + (phrase.id || '') + '">'
544 - + escapeHtml(phrase.text) + badge
545 - + ' <button type="button" class="mxch-phrase-remove" title="Remove">&times;</button>'
546 - + '</span>';
547 - });
548 - $container.html(html);
549 - }
550 -
551 - function addPhraseFromInput() {
552 - const $input = $('#mxch-phrase-input');
553 - const text = $input.val().trim();
554 - if (!text) return;
555 -
556 - const formMode = $('#mxch-form-mode').val();
557 - const intentId = $('#mxch-action-id').val();
558 -
559 - if (formMode === 'edit' && intentId) {
560 - // Edit mode: AJAX add immediately
561 - const $btn = $('#mxch-add-phrase-btn');
562 - $btn.prop('disabled', true).text('Adding...');
563 -
564 - $.ajax({
565 - url: mxchActionsData.ajaxUrl,
566 - type: 'POST',
567 - data: {
568 - action: 'mxchat_add_phrase',
569 - intent_id: intentId,
570 - phrase: text,
571 - security: mxchActionsData.addPhraseNonce
572 - },
573 - success: function(response) {
574 - $btn.prop('disabled', false).text('Add');
575 - if (response.success) {
576 - currentPhrases.push({id: response.data.id, text: text, isLegacy: false});
577 - renderPhraseTags();
578 - $input.val('').focus();
579 - } else {
580 - alert(response.data || 'Failed to add phrase.');
581 - }
582 - },
583 - error: function() {
584 - $btn.prop('disabled', false).text('Add');
585 - alert('Failed to add phrase. Please try again.');
586 - }
587 - });
588 - } else {
589 - // Add mode: just push locally
590 - currentPhrases.push({id: null, text: text, isLegacy: false});
591 - renderPhraseTags();
592 - $input.val('').focus();
593 - }
594 - }
595 -
596 - // Add phrase on Enter key
597 - $(document).on('keydown', '#mxch-phrase-input', function(e) {
598 - if (e.key === 'Enter') {
599 - e.preventDefault();
600 - addPhraseFromInput();
601 - }
602 - });
603 -
604 - // Add phrase on button click
605 - $(document).on('click', '#mxch-add-phrase-btn', function() {
606 - addPhraseFromInput();
607 - });
608 -
609 - // Remove phrase on X click
610 - $(document).on('click', '.mxch-phrase-remove', function(e) {
611 - e.stopPropagation();
612 - const $pill = $(this).closest('.mxch-phrase-pill');
613 - const index = parseInt($pill.data('index'));
614 - const phrase = currentPhrases[index];
615 -
616 - if (!phrase) return;
617 -
618 - if (phrase.isLegacy) {
619 - // Delete legacy phrases from main table
620 - const intentId = $('#mxch-action-id').val();
621 - if (!intentId) {
622 - currentPhrases.splice(index, 1);
623 - renderPhraseTags();
624 - return;
625 - }
626 -
627 - if (!confirm('Remove all legacy grouped phrases? You can re-add them individually for better matching.')) return;
628 -
629 - $.ajax({
630 - url: mxchActionsData.ajaxUrl,
631 - type: 'POST',
632 - data: {
633 - action: 'mxchat_delete_legacy_phrases',
634 - intent_id: intentId,
635 - security: mxchActionsData.deleteLegacyNonce
636 - },
637 - success: function(response) {
638 - if (response.success) {
639 - currentPhrases.splice(index, 1);
640 - renderPhraseTags();
641 - } else {
642 - alert(response.data || 'Failed to remove legacy phrases.');
643 - }
644 - }
645 - });
646 - } else if (phrase.id) {
647 - // Delete individual phrase via AJAX
648 - $.ajax({
649 - url: mxchActionsData.ajaxUrl,
650 - type: 'POST',
651 - data: {
652 - action: 'mxchat_delete_phrase',
653 - phrase_id: phrase.id,
654 - security: mxchActionsData.deletePhraseNonce
655 - },
656 - success: function(response) {
657 - if (response.success) {
658 - currentPhrases.splice(index, 1);
659 - renderPhraseTags();
660 - } else {
661 - alert(response.data || 'Failed to delete phrase.');
662 - }
663 - }
664 - });
665 - } else {
666 - // Local-only phrase (add mode, not yet saved)
667 - currentPhrases.splice(index, 1);
668 - renderPhraseTags();
669 - }
670 - });
671 -
672 - function fetchIndividualPhrases(intentId, callback) {
673 - $.ajax({
674 - url: mxchActionsData.ajaxUrl,
675 - type: 'POST',
676 - data: {
677 - action: 'mxchat_get_phrases',
678 - intent_id: intentId,
679 - security: mxchActionsData.getPhrasesNonce
680 - },
681 - success: function(response) {
682 - if (response.success) {
683 - callback(response.data.phrases || []);
684 - } else {
685 - callback([]);
686 - }
687 - },
688 - error: function() {
689 - callback([]);
690 - }
691 - });
692 - }
693 -
694 117 // Load action list on page load
695 118 loadActionList(1, '', '');
696 119
697 120 // Search functionality with debounce
@@ -846,15 +269,15 @@
846 269 totalPages = response.data.total_pages;
847 270 updateActionCount(response.data.showing_start, response.data.showing_end, response.data.total_actions);
848 271 renderPagination(response.data.page, response.data.total_pages, searchTerm, filter);
849 272 } else {
850 - $container.html('<div class="mxch-list-empty"><p>No trigger phrases found</p></div>');
273 + $container.html('<div class="mxch-list-empty"><p>No actions found</p></div>');
851 274 updateActionCount(0, 0, 0);
852 275 $('#mxch-actions-pagination').html('');
853 276 }
854 277 },
855 278 error: function() {
856 - $container.html('<div class="mxch-list-empty"><p>Error loading trigger phrases</p></div>');
279 + $container.html('<div class="mxch-list-empty"><p>Error loading actions</p></div>');
857 280 }
858 281 });
859 282 }
860 283
@@ -878,11 +301,9 @@
878 301 data-phrases="${escapeHtml(action.phrases)}"
879 302 data-threshold="${action.threshold}"
880 303 data-callback="${escapeHtml(action.callback_function)}"
881 304 data-enabled="${action.enabled ? '1' : '0'}"
882 - data-enabled-bots='${escapeHtml(JSON.stringify(action.enabled_bots))}'
883 - data-has-legacy="${action.has_legacy_vector ? '1' : '0'}"
884 - data-individual-count="${action.individual_phrase_count || 0}">
305 + data-enabled-bots='${escapeHtml(JSON.stringify(action.enabled_bots))}'>
885 306 <input type="checkbox" class="mxch-action-checkbox"${isChecked}>
886 307 <div class="mxch-action-icon">
887 308 <span class="dashicons dashicons-${escapeHtml(action.icon || 'admin-generic')}"></span>
888 309 </div>
@@ -934,11 +355,11 @@
934 355
935 356 // Update action count display
936 357 function updateActionCount(start, end, total) {
937 358 if (total === 0) {
938 - $('#mxch-action-count').text('0 trigger phrases');
359 + $('#mxch-action-count').text('0 actions');
939 360 } else {
940 - $('#mxch-action-count').text(`${start}-${end} / ${total} trigger phrases`);
361 + $('#mxch-action-count').text(`${start}-${end} / ${total} actions`);
941 362 }
942 363 }
943 364
944 365 // Render pagination
@@ -987,18 +408,12 @@
987 408 phrases: $item.data('phrases'),
988 409 threshold: $item.data('threshold'),
989 410 callback_function: $item.data('callback'),
990 411 enabled: $item.data('enabled') == '1',
991 - enabled_bots: $item.data('enabled-bots') || ['default'],
992 - has_legacy: $item.data('has-legacy') == '1',
993 - individualPhrases: []
412 + enabled_bots: $item.data('enabled-bots') || ['default']
994 413 };
995 414
996 - // Fetch individual phrases then show view
997 - fetchIndividualPhrases(actionId, function(phrases) {
998 - data.individualPhrases = phrases;
999 - showActionView(data);
1000 - });
415 + showActionView(data);
1001 416 }
1002 417
1003 418 // Show action view (details)
1004 419 function showActionView(action) {
@@ -1025,22 +440,14 @@
1025 440 const iconClass = $typeCard.find('.dashicons').attr('class') || 'dashicons dashicons-admin-generic';
1026 441 $('#mxch-view-icon').html('<span class="' + iconClass + '"></span>');
1027 442 $('#mxch-detail-icon').html('<span class="' + iconClass + '"></span>');
1028 443
1029 - // Phrases - show legacy grouped phrases and individual phrases
444 + // Phrases
445 + const phrases = action.phrases.split(',').map(p => p.trim()).filter(p => p);
1030 446 let phrasesHtml = '';
1031 - const legacyPhrases = action.phrases ? String(action.phrases).trim() : '';
1032 - if (legacyPhrases) {
1033 - phrasesHtml += '<span class="mxch-phrase-tag mxch-phrase-legacy">' + escapeHtml(legacyPhrases) + ' <span class="mxch-legacy-badge">legacy</span></span>';
1034 - }
1035 - if (action.individualPhrases && action.individualPhrases.length) {
1036 - action.individualPhrases.forEach(function(p) {
1037 - phrasesHtml += '<span class="mxch-phrase-tag">' + escapeHtml(p.phrase) + '</span>';
1038 - });
1039 - }
1040 - if (!phrasesHtml) {
1041 - phrasesHtml = '<span class="mxch-no-phrases">No trigger phrases configured</span>';
1042 - }
447 + phrases.forEach(function(phrase) {
448 + phrasesHtml += '<span class="mxch-phrase-tag">' + escapeHtml(phrase) + '</span>';
449 + });
1043 450 $('#mxch-view-phrases').html(phrasesHtml);
1044 451
1045 452 // Settings
1046 453 $('#mxch-view-threshold').text(action.threshold + '%');
@@ -1098,24 +505,12 @@
1098 505 $('#mxch-action-id').val(action.id);
1099 506 $('#mxch-callback-function').val(action.callback_function);
1100 507 $('#mxch-form-mode').val('edit');
1101 508 $('#mxch-action-label').val(action.label);
509 + $('#mxch-action-phrases').val(action.phrases);
1102 510 $('#mxch-action-threshold').val(action.threshold);
1103 511 $('#mxch-threshold-value').text(action.threshold + '%');
1104 512
1105 - // Populate phrase tags
1106 - currentPhrases = [];
1107 - const legacyPhrases = action.phrases ? String(action.phrases).trim() : '';
1108 - if (legacyPhrases) {
1109 - currentPhrases.push({id: 'legacy', text: legacyPhrases, isLegacy: true});
1110 - }
1111 - if (action.individualPhrases && action.individualPhrases.length) {
1112 - action.individualPhrases.forEach(function(p) {
1113 - currentPhrases.push({id: p.id, text: p.phrase, isLegacy: false});
1114 - });
1115 - }
1116 - renderPhraseTags();
1117 -
1118 513 // Set selected type display
1119 514 const $typeCard = $('.mxch-type-card[data-value="' + action.callback_function + '"]');
1120 515 const iconHtml = $typeCard.find('.mxch-type-icon').html();
1121 516 const typeLabel = $typeCard.data('label');
@@ -1123,9 +518,9 @@
1123 518
1124 519 $('#mxch-selected-type-icon').html(iconHtml);
1125 520 $('#mxch-selected-type-label').text(typeLabel);
1126 521 $('#mxch-selected-type-desc').text(typeDesc);
1127 - $('#mxch-config-title').text('Edit Trigger Phrase');
522 + $('#mxch-config-title').text('Edit Action');
1128 523
1129 524 // Set enabled bots
1130 525 const enabledBots = Array.isArray(action.enabled_bots) ? action.enabled_bots : ['default'];
1131 526 $('#mxch-bot-selector input[type="checkbox"]').each(function() {
@@ -1132,9 +527,9 @@
1132 527 $(this).prop('checked', enabledBots.includes($(this).val()));
1133 528 });
1134 529
1135 530 // Update save button text
1136 - $('#mxch-save-action').text('Update Trigger Phrase');
531 + $('#mxch-save-action').text('Update Action');
1137 532 }
1138 533
1139 534 // Reset form
1140 535 function resetForm() {
@@ -1141,16 +536,13 @@
1141 536 $('#mxch-action-id').val('');
1142 537 $('#mxch-callback-function').val('');
1143 538 $('#mxch-form-mode').val('add');
1144 539 $('#mxch-action-label').val('');
540 + $('#mxch-action-phrases').val('');
1145 541 $('#mxch-action-threshold').val(85);
1146 -
1147 - // Clear phrase tags
1148 - currentPhrases = [];
1149 - renderPhraseTags();
1150 542 $('#mxch-threshold-value').text('85%');
1151 - $('#mxch-config-title').text('Configure Trigger Phrase');
1152 - $('#mxch-save-action').text('Save Trigger Phrase');
543 + $('#mxch-config-title').text('Configure Action');
544 + $('#mxch-save-action').text('Save Action');
1153 545
1154 546 // Reset bot selection
1155 547 $('#mxch-bot-selector input[type="checkbox"]').prop('checked', false);
1156 548 $('#mxch-bot-selector input[value="default"]').prop('checked', true);
@@ -1327,8 +719,9 @@
1327 719 const formMode = $('#mxch-form-mode').val();
1328 720 const actionId = $('#mxch-action-id').val();
1329 721 const callbackFunction = $('#mxch-callback-function').val();
1330 722 const label = $('#mxch-action-label').val().trim();
723 + const phrases = $('#mxch-action-phrases').val().trim();
1331 724 const threshold = $('#mxch-action-threshold').val();
1332 725
1333 726 // Validation
1334 727 if (!callbackFunction) {
@@ -1341,12 +734,11 @@
1341 734 $('#mxch-action-label').focus();
1342 735 return;
1343 736 }
1344 737
1345 - // Check phrases exist (from tag input)
1346 - if (currentPhrases.length === 0) {
1347 - alert('Please add at least one trigger phrase.');
1348 - $('#mxch-phrase-input').focus();
738 + if (!phrases) {
739 + alert('Please enter at least one trigger phrase.');
740 + $('#mxch-action-phrases').focus();
1349 741 return;
1350 742 }
1351 743
1352 744 // Get enabled bots
@@ -1363,8 +755,9 @@
1363 755
1364 756 const data = {
1365 757 action: formMode === 'edit' ? 'mxchat_edit_intent_ajax' : 'mxchat_add_intent_ajax',
1366 758 intent_label: label,
759 + phrases: phrases,
1367 760 similarity_threshold: threshold,
1368 761 callback_function: callbackFunction,
1369 762 enabled_bots: enabledBots,
1370 763 security: formMode === 'edit' ? mxchActionsData.editNonce : mxchActionsData.addNonce
@@ -1371,20 +764,8 @@
1371 764 };
1372 765
1373 766 if (formMode === 'edit') {
1374 767 data.intent_id = actionId;
1375 - // In edit mode, phrases are managed via individual AJAX calls already
1376 - // Send empty phrases to skip legacy re-embedding
1377 - data.phrases = '';
1378 - } else {
1379 - // Add mode: send individual phrases for per-phrase embedding
1380 - const nonLegacyPhrases = currentPhrases.filter(p => !p.isLegacy).map(p => p.text);
1381 - if (nonLegacyPhrases.length > 0) {
1382 - data.individual_phrases = nonLegacyPhrases;
1383 - data.phrases = '';
1384 - } else {
1385 - data.phrases = '';
1386 - }
1387 768 }
1388 769
1389 770 $.ajax({
1390 771 url: mxchActionsData.ajaxUrl,