PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
← All changes | js/mxchat-admin.js +128 -15 3.2.19 → 3.2.25 View file →
@@ -541,9 +541,11 @@
541 541 // each editor folds them into its own hidden live_agent_schedule_<channel>
542 542 // input and fires ONE change, which this same handler then saves. Without
543 543 // the exclusion each keystroke would POST a nameless field the server can
544 544 // only reject.
545 - $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model, .mxchat-la-field').on('change', function() {
545 + // .mxchat-acf-group-toggle is excluded: group toggles are nameless and
546 + // save through their own batch action (bf57e0), never this per-field path.
547 + $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model, .mxchat-la-field, .mxchat-acf-group-toggle').on('change', function() {
546 548 const $field = $(this);
547 549 const name = $field.attr('name');
548 550
549 551 // Debounce rate limit fields to prevent UI freezing from rapid changes
@@ -1086,12 +1088,12 @@
1086 1088 openrouter: [
1087 1089 { value: 'openrouter', label: 'OpenRouter', description: 'Access 100+ models from multiple providers (add API key to browse)' }
1088 1090 ],
1089 1091 gemini: [
1090 - { value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', description: 'Stable — newest Flash generation, recommended default' },
1092 + { value: 'gemini-3.8-flash', label: 'Gemini 3.8 Flash', description: 'Stable — newest Flash generation, recommended default' },
1091 1093 ],
1092 1094 openai: [
1093 - { value: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', description: 'Recommended — newest OpenAI flagship for reasoning, coding and chat' },
1095 + { value: 'gpt-6-astra', label: 'GPT-6 Astra', description: 'Recommended — newest OpenAI flagship; strongest reasoning, coding and agentic work' },
1094 1096 ],
1095 1097 claude: [
1096 1098 { value: 'claude-fable-5', label: 'Claude Fable 5', description: 'Latest Flagship — newest and most capable Anthropic model' },
1097 1099 { value: 'claude-opus-5', label: 'Claude Opus 5', description: 'Latest Opus — best for complex agentic and coding work' },
@@ -3889,29 +3891,50 @@
3889 3891
3890 3892 // A successful autosave round-trip re-baselines the field it saved. The
3891 3893 // baseline takes the value the request actually SENT, so edits made while
3892 3894 // the save was in flight keep the field dirty and the guard armed.
3895 + function rebaselineField(name, saved) {
3896 + saved = saved == null ? '' : String(saved);
3897 + // Checkboxes go over the wire as on/off (or 1/0); normalize to 1/0.
3898 + if (saved === 'on') saved = '1';
3899 + if (saved === 'off') saved = '0';
3900 + baseline.set(name, saved);
3901 + const $fs = $sections.find('[name="' + name.replace(/"/g, '\\"') + '"]');
3902 + if ($fs.length > 1) {
3903 + // The baseline Map is per-NAME, so duplicate names share one entry
3904 + // and dirty-compare against each other's state — the exact shape
3905 + // that let the exit beacon revert saves through same-named ACF
3906 + // twins before 30e81f moved those toggles onto unique field keys.
3907 + console.warn('MxChat: duplicate field name "' + name + '" in autosave sections — per-name dirty tracking may misreport these fields.');
3908 + }
3909 + $fs.each(function() { syncBadge($(this)); });
3910 + }
3911 +
3893 3912 $(document).ajaxSuccess(function(event, xhr, settings) {
3894 3913 if (!settings || typeof settings.data !== 'string') return;
3895 - if (settings.data.indexOf('action=mxchat_save_setting') === -1 &&
3914 + const isGroupBatch = settings.data.indexOf('action=mxchat_acf_toggle_group') !== -1;
3915 + if (!isGroupBatch &&
3916 + settings.data.indexOf('action=mxchat_save_setting') === -1 &&
3896 3917 settings.data.indexOf('action=mxchat_save_prompts_setting') === -1) {
3897 3918 return;
3898 3919 }
3920 + if (!xhr || !xhr.responseJSON || xhr.responseJSON.success !== true) return;
3921 + if (isGroupBatch) {
3922 + // A group batch (bf57e0) flips many toggles in one response —
3923 + // re-baseline every field it touched, or the pagehide beacon
3924 + // would post them all individually on the way out (and silently
3925 + // drop everything past BEACON_MAX_FIELDS).
3926 + const fields = (xhr.responseJSON.data && xhr.responseJSON.data.fields) || [];
3927 + fields.forEach(function(f) {
3928 + if (f && f.name) rebaselineField(f.name, f.value);
3929 + });
3930 + return;
3931 + }
3899 3932 let params;
3900 3933 try { params = new URLSearchParams(settings.data); } catch (err) { return; }
3901 3934 const name = params.get('name');
3902 3935 if (!name || !baseline.has(name)) return;
3903 - if (!xhr || !xhr.responseJSON || xhr.responseJSON.success !== true) return;
3904 - let saved = params.get('value');
3905 - saved = saved == null ? '' : String(saved);
3906 - // Checkboxes go over the wire as on/off (or 1/0); normalize to 1/0.
3907 - if (saved === 'on') saved = '1';
3908 - if (saved === 'off') saved = '0';
3909 - baseline.set(name, saved);
3910 - const $f = $sections.find('[name="' + name.replace(/"/g, '\\"') + '"]').first();
3911 - if ($f.length) {
3912 - syncBadge($f);
3913 - }
3936 + rebaselineField(name, params.get('value'));
3914 3937 });
3915 3938
3916 3939 // Navigation guard — recomputed per-field at the moment of leaving, so it
3917 3940 // arms only when a tracked value genuinely differs from last-saved.
@@ -4007,7 +4030,97 @@
4007 4030 if (navigator.sendBeacon(route.url, fd)) {
4008 4031 sent++;
4009 4032 }
4010 4033 if (sent >= BEACON_MAX_FIELDS) return false;
4034 + });
4035 + });
4036 +});
4037 +
4038 +// ─── ACF group-level toggles (plan bf57e0) ──────────────────────────────
4039 +// One click includes/excludes every field in an ACF field group via a
4040 +// DEDICATED batch action — one option write server-side. Looping the
4041 +// per-field autosave endpoint from here would be a lost-update race: each
4042 +// request reads the exclusion option before the others have written it
4043 +// back, and the last write wins.
4044 +jQuery(function($) {
4045 + const $groups = $('[data-mxchat-acf-group]');
4046 + if (!$groups.length || typeof mxchatPromptsAdmin === 'undefined') return;
4047 +
4048 + function groupInputs($group) {
4049 + return $group.find('input.mxchat-autosave-field[name^="mxchat_acf_field_"]');
4050 + }
4051 +
4052 + // Derived state, never stored: ON when nothing in the group is excluded,
4053 + // OFF when everything is, indeterminate when mixed. Recomputed from the
4054 + // field toggles' DOM so it can never drift from the real per-field state.
4055 + function refreshGroupToggle($group) {
4056 + const $toggle = $group.find('.mxchat-acf-group-toggle').first();
4057 + if (!$toggle.length) return;
4058 + const $fields = groupInputs($group);
4059 + const on = $fields.filter(':checked').length;
4060 + $toggle.prop('indeterminate', on > 0 && on < $fields.length);
4061 + $toggle.prop('checked', $fields.length > 0 && on === $fields.length);
4062 + }
4063 +
4064 + // indeterminate is a JS-only property; the server carries the mixed
4065 + // state via data-indeterminate on render.
4066 + $groups.find('.mxchat-acf-group-toggle[data-indeterminate="1"]').prop('indeterminate', true);
4067 +
4068 + // A hand-flipped field toggle updates its group's header state right away.
4069 + $groups.on('change', 'input.mxchat-autosave-field[name^="mxchat_acf_field_"]', function() {
4070 + refreshGroupToggle($(this).closest('[data-mxchat-acf-group]'));
4071 + });
4072 +
4073 + $groups.on('change', '.mxchat-acf-group-toggle', function() {
4074 + const $toggle = $(this);
4075 + const $group = $toggle.closest('[data-mxchat-acf-group]');
4076 + // A click on an indeterminate box lands on checked — i.e. include
4077 + // everything, the less destructive direction. Documented choice.
4078 + const include = $toggle.is(':checked');
4079 +
4080 + const feedbackContainer = $('<div class="feedback-container"></div>');
4081 + const spinner = $('<div class="saving-spinner"></div>');
4082 + const successIcon = $('<div class="success-icon">✔</div>');
4083 + $toggle.closest('.mxchat-toggle-switch').after(feedbackContainer);
4084 + feedbackContainer.append(spinner);
4085 + $toggle.prop('disabled', true);
4086 +
4087 + $.ajax({
4088 + url: mxchatPromptsAdmin.ajax_url,
4089 + type: 'POST',
4090 + data: {
4091 + action: 'mxchat_acf_toggle_group',
4092 + group_key: $group.attr('data-mxchat-acf-group'),
4093 + state: include ? 'on' : 'off',
4094 + _ajax_nonce: $toggle.data('nonce')
4095 + },
4096 + success: function(response) {
4097 + if (response && response.success) {
4098 + const fields = (response.data && response.data.fields) || [];
4099 + fields.forEach(function(f) {
4100 + if (!f || !f.name) return;
4101 + $group.find('[name="' + f.name.replace(/"/g, '\\"') + '"]').prop('checked', f.value === 'on');
4102 + });
4103 + refreshGroupToggle($group);
4104 + spinner.fadeOut(200, function() {
4105 + feedbackContainer.append(successIcon);
4106 + successIcon.fadeIn(200).delay(1000).fadeOut(200, function() {
4107 + feedbackContainer.remove();
4108 + });
4109 + });
4110 + } else {
4111 + feedbackContainer.remove();
4112 + refreshGroupToggle($group); // fall back to the fields' real state
4113 + alert('Error saving: ' + ((response && response.data && response.data.message) || 'Unknown error'));
4114 + }
4115 + },
4116 + error: function() {
4117 + feedbackContainer.remove();
4118 + refreshGroupToggle($group);
4119 + alert('Error saving: request failed');
4120 + },
4121 + complete: function() {
4122 + $toggle.prop('disabled', false);
4123 + }
4011 4124 });
4012 4125 });
4013 4126 });