PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
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 | includes/admin-settings-page.php +234 -163 3.2.213.2.8 View file →
@@ -86,9 +86,8 @@
86 86 <button class="mxch-mobile-nav-sub-link" data-target="integrations-loops"><?php esc_html_e('Loops', 'mxchat'); ?></button>
87 87 <button class="mxch-mobile-nav-sub-link" data-target="integrations-brave"><?php esc_html_e('Brave Search', 'mxchat'); ?></button>
88 88 <button class="mxch-mobile-nav-sub-link" data-target="integrations-slack"><?php esc_html_e('Slack', 'mxchat'); ?></button>
89 89 <button class="mxch-mobile-nav-sub-link" data-target="integrations-telegram"><?php esc_html_e('Telegram', 'mxchat'); ?></button>
90 - <button class="mxch-mobile-nav-sub-link" data-target="integrations-webhook"><?php esc_html_e('Webhook', 'mxchat'); ?></button>
91 90 </div>
92 91 </div>
93 92 <!-- Resources Section -->
94 93 <div class="mxch-mobile-nav-section">
@@ -189,9 +188,8 @@
189 188 <button class="mxch-nav-sub-link" data-target="integrations-loops"><?php esc_html_e('Loops', 'mxchat'); ?></button>
190 189 <button class="mxch-nav-sub-link" data-target="integrations-brave"><?php esc_html_e('Brave Search', 'mxchat'); ?></button>
191 190 <button class="mxch-nav-sub-link" data-target="integrations-slack"><?php esc_html_e('Slack', 'mxchat'); ?></button>
192 191 <button class="mxch-nav-sub-link" data-target="integrations-telegram"><?php esc_html_e('Telegram', 'mxchat'); ?></button>
193 - <button class="mxch-nav-sub-link" data-target="integrations-webhook"><?php esc_html_e('Webhook', 'mxchat'); ?></button>
194 192 </div>
195 193 </div>
196 194 </div>
197 195
@@ -246,18 +244,8 @@
246 244 <h1 class="mxch-content-title"><?php esc_html_e('AI Models', 'mxchat'); ?></h1>
247 245 <p class="mxch-content-subtitle"><?php esc_html_e('Configure the AI models your chatbot will use for conversations and embeddings.', 'mxchat'); ?></p>
248 246 </div>
249 247
250 - <?php
251 - // Model-retirement warning (b65e8d). Rendered HERE rather than via
252 - // admin_notices because the branded shell paints over WP's notice
253 - // region — a core notice on this screen is invisible. No-ops unless
254 - // the daily liveness check has flagged the configured model.
255 - if (class_exists('MxChat_Model_Liveness')) {
256 - MxChat_Model_Liveness::render_inline_notice();
257 - }
258 - ?>
259 -
260 248 <div class="mxch-card">
261 249 <div class="mxch-card-body mxchat-autosave-section">
262 250 <?php
263 251 // Chat Model
@@ -274,12 +262,12 @@
274 262 mxchat_render_field_wrapper('enable_streaming', __('Enable Streaming', 'mxchat'), function() use ($admin_instance) {
275 263 $admin_instance->enable_streaming_toggle_callback();
276 264 }, __('Enable real-time streaming responses for supported models (OpenAI, Claude, DeepSeek, Grok).', 'mxchat'));
277 265
278 - // Enable Web Search (OpenAI & Gemini)
266 + // Enable Web Search (OpenAI only)
279 267 mxchat_render_field_wrapper('enable_web_search', __('Enable Web Search', 'mxchat'), function() use ($admin_instance) {
280 268 $admin_instance->enable_web_search_toggle_callback();
281 - }, __('Allow the chatbot to search the web for current information (OpenAI and Gemini models).', 'mxchat'));
269 + }, __('Allow the chatbot to search the web for current information (OpenAI models only).', 'mxchat'));
282 270 ?>
283 271 </div>
284 272 </div>
285 273 </div>
@@ -315,21 +303,8 @@
315 303 mxchat_render_field_wrapper('rag_chunks_limit', __('RAG Chunks Limit', 'mxchat'), function() use ($admin_instance) {
316 304 $admin_instance->mxchat_rag_chunks_limit_callback();
317 305 }, __('Maximum total content chunks sent to the AI across all sources. Higher values provide more context but increase token usage and cost.', 'mxchat'));
318 306
319 - // Hybrid keyword boost MOVED to Knowledge → Chunking &
320 - // Retrieval (plan 11720c) — it is KB retrieval tuning, not
321 - // conversation behavior. Same option key over there. The
322 - // breadcrumb below is a one-release courtesy: drop it in
323 - // the release after 3.2.17.
324 - echo '<p class="mxch-field-description">';
325 - printf(
326 - /* translators: %s: link to the Chunking & Retrieval settings tab. */
327 - esc_html__('Hybrid keyword boost has moved to %s.', 'mxchat'),
328 - '<a href="' . esc_url(admin_url('admin.php?page=mxchat-prompts#chunking')) . '">' . esc_html__('Knowledge, Chunking & Retrieval', 'mxchat') . '</a>'
329 - );
330 - echo '</p>';
331 -
332 307 // Contextual Awareness
333 308 mxchat_render_field_wrapper('contextual_awareness', __('Contextual Awareness', 'mxchat'), function() use ($admin_instance) {
334 309 $admin_instance->mxchat_contextual_awareness_callback();
335 310 }, __('Allow the chatbot to understand and reference the current page content for more relevant responses.', 'mxchat'));
@@ -338,14 +313,8 @@
338 313 mxchat_render_field_wrapper('citation_links', __('Citation Links', 'mxchat'), function() use ($admin_instance) {
339 314 $admin_instance->mxchat_citation_links_toggle_callback();
340 315 }, __('Allow the AI to include citation links from your knowledge database in responses. If disabled, ensure your AI Behavior settings do not mention links or the AI may fabricate URLs.', 'mxchat'));
341 316
342 - // Strip Unapproved Links (plan 58f8b4) — enforcement is now
343 - // its own control instead of riding Citation Links.
344 - mxchat_render_field_wrapper('strip_unapproved_links', __('Strip Unapproved Links', 'mxchat'), function() use ($admin_instance) {
345 - $admin_instance->mxchat_strip_unapproved_links_toggle_callback();
346 - }, __('Remove links the AI invents from its answers, even when Citation Links is off. Approved citations, real pages on this site, and links returned by your integrations (e.g. WooCommerce products) are always kept. On by default for new installs; existing sites keep their current behavior until enabled.', 'mxchat'));
347 -
348 317 // Satisfaction Rating Prompt
349 318 // The toggle callback inline-renders the 5 customization
350 319 // fields inside a single sub-options wrapper (plan-29caac).
351 320 // No separate customization group needed — the wrapper handles
@@ -352,12 +321,8 @@
352 321 // its own server-side display state based on the toggle value.
353 322 mxchat_render_field_wrapper('satisfaction_rating', __('Satisfaction Rating Prompt', 'mxchat'), function() use ($admin_instance) {
354 323 $admin_instance->mxchat_satisfaction_rating_toggle_callback();
355 324 }, __('Show a 👍 / 👎 prompt after a conversation has had a couple of bot replies and the visitor has been idle for a moment. Disable to hide the prompt site-wide.', 'mxchat'));
356 -
357 - // Editor Assistant toggle moved to Content → Settings (plan-f7df40):
358 - // it's a content feature, so it lives on the screen that owns it.
359 - // Rendering is in admin-content-page.php; option + save path unchanged.
360 325 ?>
361 326 </div>
362 327 </div>
363 328 </div>
@@ -393,23 +358,8 @@
393 358 // Chat Persistence
394 359 mxchat_render_field_wrapper('chat_persistence', __('Chat Persistence', 'mxchat'), function() use ($admin_instance) {
395 360 $admin_instance->mxchat_chat_persistence_toggle_callback();
396 361 }, __('Keep chat history when users navigate or return within 24 hours.', 'mxchat'));
397 -
398 - // Download Transcript button
399 - mxchat_render_field_wrapper('print_button', __('Show Download Transcript Button', 'mxchat'), function() use ($admin_instance) {
400 - $admin_instance->mxchat_print_button_toggle_callback();
401 - }, __('Show the "Download Transcript" item in the chat window menu.', 'mxchat'));
402 -
403 - // Start-New-Chat button (plan ac2e81) — lets a visitor reset the
404 - // conversation without the owner disabling chat persistence globally.
405 - mxchat_render_field_wrapper('reset_chat_enabled', __('Show Start-New-Chat Button', 'mxchat'), function() use ($admin_instance) {
406 - $admin_instance->mxchat_reset_chat_toggle_callback();
407 - }, __('Show a "Start new chat" item in the chat window menu that clears the current conversation and begins a fresh session.', 'mxchat'));
408 -
409 - mxchat_render_field_wrapper('reset_chat_label', __('Start-New-Chat Button Label', 'mxchat'), function() use ($admin_instance) {
410 - $admin_instance->mxchat_reset_chat_label_callback();
411 - }, __('Label for the Start-New-Chat menu item. Leave blank to use "Start new chat".', 'mxchat'));
412 362 ?>
413 363 </div>
414 364 </div>
415 365
@@ -441,13 +391,8 @@
441 391 mxchat_render_field_wrapper('input_copy', __('Input Placeholder', 'mxchat'), function() use ($admin_instance) {
442 392 $admin_instance->mxchat_input_copy_callback();
443 393 }, __('Placeholder text in the chat input field.', 'mxchat'));
444 394
445 - // Max Input Length (characters) — plan a3fae2 part C
446 - mxchat_render_field_wrapper('max_input_length', __('Max Input Length (characters)', 'mxchat'), function() use ($admin_instance) {
447 - $admin_instance->mxchat_max_input_length_callback();
448 - }, __('Cap how many characters a visitor can type or paste into a single message. Set to 0 for unlimited (default).', 'mxchat'));
449 -
450 395 // Chat Teaser
451 396 mxchat_render_field_wrapper('pre_chat_message', __('Chat Teaser Pop-up', 'mxchat'), function() use ($admin_instance) {
452 397 $admin_instance->mxchat_pre_chat_message_callback();
453 398 }, __('Message shown before users start chatting.', 'mxchat'));
@@ -497,23 +442,8 @@
497 442 // Name Placeholder
498 443 mxchat_render_field_wrapper('name_placeholder', __('Name Field Placeholder', 'mxchat'), function() use ($admin_instance) {
499 444 $admin_instance->name_field_placeholder_callback();
500 445 }, __('Placeholder text for the name input field.', 'mxchat'));
501 -
502 - // Consent Checkbox (b062c4)
503 - mxchat_render_field_wrapper('enable_consent_checkbox', __('Consent Checkbox', 'mxchat'), function() use ($admin_instance) {
504 - $admin_instance->enable_consent_checkbox_callback();
505 - }, __('Show a consent checkbox on the email form. The decision, time, and exact label shown are stored with the lead.', 'mxchat'));
506 -
507 - // Consent Label
508 - mxchat_render_field_wrapper('consent_checkbox_label', __('Consent Label', 'mxchat'), function() use ($admin_instance) {
509 - $admin_instance->consent_checkbox_label_callback();
510 - }, __('Text shown beside the checkbox. Anchor tags are allowed so you can link your Privacy Policy; other HTML is stripped.', 'mxchat'));
511 -
512 - // Consent Required
513 - mxchat_render_field_wrapper('consent_checkbox_required', __('Require Consent to Submit', 'mxchat'), function() use ($admin_instance) {
514 - $admin_instance->consent_checkbox_required_callback();
515 - }, __('Block form submission until the box is ticked. Off means the checkbox is optional (a soft opt-in).', 'mxchat'));
516 446 ?>
517 447 </div>
518 448 </div>
519 449
@@ -665,18 +595,8 @@
665 595 </div>
666 596 <p class="mxch-field-description"><?php esc_html_e('Control when the chatbot script loads to improve page performance.', 'mxchat'); ?></p>
667 597 </div>
668 598
669 - <?php
670 - // Smart asset loading (plan-915355; rebuilt on the house toggle
671 - // pattern in plan-64d34f). Standalone option — never part of
672 - // mxchat_options, so mxchat_sanitize() can't strip it.
673 - mxchat_render_field_wrapper('mxchat_smart_asset_loading', __('Only load chatbot files on pages where the chatbot appears', 'mxchat'), function() use ($admin_instance) {
674 - $admin_instance->mxchat_smart_asset_loading_toggle_callback();
675 - }, __('Skips the chatbot\'s CSS and JavaScript on pages where the widget is hidden (per-page visibility, post type include/exclude, or auto-display disabled). Pages using the chatbot shortcode still load everything automatically. Helps with "reduce unused CSS/JS" audit findings.', 'mxchat'),
676 - __('If your theme or page builder inserts the chatbot in an unusual way and it stops appearing, turn this off — or force-load assets with the mxchat_should_load_assets filter.', 'mxchat'));
677 - ?>
678 -
679 599 <div class="mxch-notice mxch-notice-info" style="margin-top: 16px;">
680 600 <svg class="mxch-notice-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
681 601 <div>
682 602 <strong><?php esc_html_e('Loading Strategies:', 'mxchat'); ?></strong>
@@ -794,15 +714,8 @@
794 714 <?php esc_html_e('Debug Mode', 'mxchat'); ?>
795 715 </h3>
796 716 </div>
797 717 <div class="mxch-card-body">
798 - <?php // Deliberate exception to the house .toggle-switch pattern
799 - // (audited in plan-64d34f): this card-body is NOT an
800 - // autosave section — the toggle saves through its own
801 - // dedicated handler (mxchat_toggle_debug_mode, wired at
802 - // mxchat-admin.js #mxchat_debug_mode) which appends its own
803 - // save indicator inside this label. Converting the markup
804 - // would orphan that indicator without touching the JS. ?>
805 718 <div class="mxch-field">
806 719 <label class="mxch-toggle">
807 720 <input type="checkbox" class="mxch-toggle-input" id="mxchat_debug_mode" name="mxchat_options[debug_mode]" value="on" <?php checked($debug_active); ?>>
808 721 <span class="mxch-toggle-switch"></span>
@@ -981,9 +894,9 @@
981 894 <svg class="mxch-notice-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
982 895 <div>
983 896 <?php printf(
984 897 esc_html__('Visit our %s for setup instructions.', 'mxchat'),
985 - '<a href="https://mxchat.ai/documentation/mxchat-core#slack-live-agent-handoff" target="_blank">' . esc_html__('documentation page', 'mxchat') . '</a>'
898 + '<a href="https://mxchat.ai/documentation/#slack_integration" target="_blank">' . esc_html__('documentation page', 'mxchat') . '</a>'
986 899 ); ?>
987 900 </div>
988 901 </div>
989 902
@@ -1120,69 +1033,8 @@
1120 1033 </div>
1121 1034 </div>
1122 1035
1123 1036 <!-- ========================================
1124 - INTEGRATIONS - WEBHOOK HANDOFF (plan d88e22)
1125 - ======================================== -->
1126 - <div id="integrations-webhook" class="mxch-section">
1127 - <div class="mxch-content-header">
1128 - <h1 class="mxch-content-title"><?php esc_html_e('Webhook', 'mxchat'); ?></h1>
1129 - <p class="mxch-content-subtitle"><?php esc_html_e('Route live-agent handoffs to any URL you choose — your helpdesk, an automation flow, a CRM.', 'mxchat'); ?></p>
1130 - </div>
1131 -
1132 - <div class="mxch-notice mxch-notice-info" style="margin-bottom: 24px;">
1133 - <svg class="mxch-notice-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
1134 - <div>
1135 - <?php esc_html_e('This destination is outbound-only: MxChat delivers the handoff to your URL and the chatbot keeps assisting the visitor. Your team follows up separately — by email, phone, or your own support tool.', 'mxchat'); ?>
1136 - </div>
1137 - </div>
1138 -
1139 - <div class="mxch-card">
1140 - <div class="mxch-card-body mxchat-autosave-section">
1141 - <table class="form-table">
1142 - <?php do_settings_fields('mxchat-embed', 'mxchat_webhook_handoff_section'); ?>
1143 - </table>
1144 - </div>
1145 - </div>
1146 -
1147 - <div class="mxch-card" style="margin-top: 24px;">
1148 - <div class="mxch-card-header">
1149 - <h3 class="mxch-card-title">
1150 - <svg class="mxch-card-title-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
1151 - <?php esc_html_e('Payload Reference', 'mxchat'); ?>
1152 - </h3>
1153 - </div>
1154 - <div class="mxch-card-body">
1155 - <p class="mxch-field-description" style="margin-bottom: 12px;">
1156 - <?php esc_html_e('Each handoff arrives at your URL as an HTTPS POST with Content-Type application/json:', 'mxchat'); ?>
1157 - </p>
1158 - <pre style="background: #f8f9fa; padding: 16px; border-radius: 8px; font-size: 12px; overflow-x: auto; margin: 0 0 16px;"><?php echo esc_html('{
1159 - "event": "live_agent_handoff",
1160 - "site": { "name": "Your Site", "url": "https://your-site.com" },
1161 - "session_id": "mxchat_chat_…",
1162 - "user_id": 0,
1163 - "visitor": { "name": "Ada", "email": "ada@example.com" },
1164 - "current_message": "I need to talk to a human",
1165 - "recent_messages": [
1166 - { "role": "user", "content": "…", "timestamp": 1755900000 },
1167 - { "role": "assistant", "content": "…", "timestamp": 1755900012 }
1168 - ],
1169 - "requested_at": "2026-08-22T14:30:00+00:00"
1170 -}'); ?></pre>
1171 - <p class="mxch-field-description" style="margin-bottom: 12px;">
1172 - <?php esc_html_e('When a Signing Secret is set, each request also carries this header so your endpoint can verify the sender — compute the HMAC of the raw request body with your secret and compare:', 'mxchat'); ?>
1173 - </p>
1174 - <div class="mxch-webhook-url-display" style="margin-bottom: 16px;">
1175 - <code>X-MxChat-Signature: sha256=&lt;hex hmac-sha256 of the raw body&gt;</code>
1176 - </div>
1177 - <p class="mxch-field-description">
1178 - <?php esc_html_e('Answer with any 2xx status. Delivery uses a 5-second timeout with one retry on transport errors and 5xx responses; redirects are not followed. The last failed delivery is shown under the Webhook URL field above.', 'mxchat'); ?>
1179 - </p>
1180 - </div>
1181 - </div>
1182 - </div>
1183 -
1184 - <!-- ========================================
1185 1037 TUTORIALS
1186 1038 ======================================== -->
1187 1039 <div id="tutorials" class="mxch-section">
1188 1040 <div class="mxch-content-header">
@@ -1439,13 +1291,145 @@
1439 1291 </div>
1440 1292
1441 1293 <script>
1442 1294 document.addEventListener('DOMContentLoaded', function() {
1443 - // Sidebar/tab navigation, accordion groups, ?tab=/#hash deep-linking
1444 - // and the mobile menu all live in the shared shell script
1445 - // (js/admin-sidebar.js) — consolidated by plan c192a4. Only the
1446 - // page-local widgets below remain inline.
1295 + // Sidebar navigation
1296 + const navLinks = document.querySelectorAll('.mxch-nav-link, .mxch-nav-sub-link');
1297 + const sections = document.querySelectorAll('.mxch-section');
1298 + const navItems = document.querySelectorAll('.mxch-nav-item');
1447 1299
1300 + function showSection(target) {
1301 + // Show target section
1302 + sections.forEach(section => {
1303 + section.classList.remove('active');
1304 + if (section.id === target) {
1305 + section.classList.add('active');
1306 + }
1307 + });
1308 +
1309 + // Scroll content area to top
1310 + const contentArea = document.querySelector('.mxch-content');
1311 + if (contentArea) {
1312 + contentArea.scrollTop = 0;
1313 + }
1314 + }
1315 +
1316 + function setActiveNav(clickedLink) {
1317 + // Remove active from all links
1318 + navLinks.forEach(l => l.classList.remove('active'));
1319 +
1320 + // Add active to clicked link
1321 + clickedLink.classList.add('active');
1322 +
1323 + // If clicking a sub-link, also highlight parent
1324 + if (clickedLink.classList.contains('mxch-nav-sub-link')) {
1325 + const parent = clickedLink.closest('.mxch-nav-item');
1326 + if (parent) {
1327 + parent.querySelector('.mxch-nav-link').classList.add('active');
1328 + }
1329 + }
1330 + }
1331 +
1332 + navLinks.forEach(link => {
1333 + link.addEventListener('click', function(e) {
1334 + e.preventDefault();
1335 + e.stopPropagation();
1336 + const target = this.dataset.target;
1337 + const parentItem = this.closest('.mxch-nav-item');
1338 + const hasSubmenu = parentItem && parentItem.querySelector('.mxch-nav-sub');
1339 +
1340 + // If this is a parent nav link WITH children (expandable menu)
1341 + if (this.classList.contains('mxch-nav-link') && hasSubmenu) {
1342 + const wasExpanded = parentItem.classList.contains('expanded');
1343 +
1344 + // Collapse all other expandable items
1345 + navItems.forEach(item => {
1346 + if (item !== parentItem && item.querySelector('.mxch-nav-sub')) {
1347 + item.classList.remove('expanded');
1348 + }
1349 + });
1350 +
1351 + // Toggle this item
1352 + parentItem.classList.toggle('expanded');
1353 +
1354 + // If expanding, show the first sub-item's content and mark it active
1355 + if (!wasExpanded) {
1356 + const firstSubLink = parentItem.querySelector('.mxch-nav-sub-link');
1357 + if (firstSubLink) {
1358 + const firstTarget = firstSubLink.dataset.target;
1359 + showSection(firstTarget);
1360 + setActiveNav(firstSubLink);
1361 + // Also mark parent as active
1362 + this.classList.add('active');
1363 + history.replaceState(null, null, '#' + firstTarget);
1364 + }
1365 + }
1366 + }
1367 + // If this is a nav link WITHOUT children (like API Keys, Tutorials)
1368 + else if (this.classList.contains('mxch-nav-link') && !hasSubmenu && target) {
1369 + // Collapse all expandable items
1370 + navItems.forEach(item => {
1371 + if (item.querySelector('.mxch-nav-sub')) {
1372 + item.classList.remove('expanded');
1373 + }
1374 + });
1375 +
1376 + // Show the section
1377 + showSection(target);
1378 + setActiveNav(this);
1379 + history.replaceState(null, null, '#' + target);
1380 + }
1381 + // If this is a sub-link
1382 + else if (this.classList.contains('mxch-nav-sub-link') && target) {
1383 + // Ensure parent stays expanded
1384 + if (parentItem) {
1385 + parentItem.classList.add('expanded');
1386 + }
1387 + showSection(target);
1388 + setActiveNav(this);
1389 + history.replaceState(null, null, '#' + target);
1390 + }
1391 + });
1392 + });
1393 +
1394 + // Handle initial deep-link (?tab=<slug> takes precedence over #hash so the
1395 + // Onboarding setup-step CTAs land on the right sub-tab). Whitelist of valid
1396 + // section ids must stay in sync with the .mxch-section[id] values above.
1397 + const validTabs = [
1398 + 'chatbot-ai-models', 'chatbot-behavior', 'chatbot-display',
1399 + 'chatbot-lead-capture', 'chatbot-quick-questions', 'chatbot-rate-limits',
1400 + 'api-keys', 'optimization', 'testing',
1401 + 'integrations-toolbar', 'integrations-loops', 'integrations-brave',
1402 + 'integrations-slack', 'integrations-telegram',
1403 + 'tutorials'
1404 + ];
1405 + function activateTab(target) {
1406 + if (!target) return false;
1407 + if (validTabs.indexOf(target) === -1) return false;
1408 + const targetLink = document.querySelector('[data-target="' + target + '"]');
1409 + if (!targetLink) return false;
1410 + const parentItem = targetLink.closest('.mxch-nav-item');
1411 + if (parentItem && targetLink.classList.contains('mxch-nav-sub-link')) {
1412 + parentItem.classList.add('expanded');
1413 + }
1414 + showSection(target);
1415 + setActiveNav(targetLink);
1416 + return true;
1417 + }
1418 + const urlParams = new URLSearchParams(window.location.search);
1419 + const tabParam = urlParams.get('tab');
1420 + let tabActivated = false;
1421 + if (tabParam) {
1422 + tabActivated = activateTab(tabParam);
1423 + // Invalid ?tab=… silently falls through to default (#hash or AI Models).
1424 + }
1425 + if (!tabActivated) {
1426 + const hash = window.location.hash.slice(1);
1427 + if (hash) {
1428 + activateTab(hash);
1429 + }
1430 + }
1431 +
1448 1432 // Rate limit accordion
1449 1433 const rateLimitHeaders = document.querySelectorAll('.mxch-rate-limit-header');
1450 1434 rateLimitHeaders.forEach(header => {
1451 1435 header.addEventListener('click', function(e) {
@@ -1472,8 +1456,102 @@
1472 1456 };
1473 1457 sel.addEventListener('change', sync);
1474 1458 sync();
1475 1459 });
1460 +
1461 + // =====================================================
1462 + // Mobile Menu Functionality
1463 + // =====================================================
1464 + const mobileMenuBtn = document.querySelector('.mxch-mobile-menu-btn');
1465 + const mobileMenuClose = document.querySelector('.mxch-mobile-menu-close');
1466 + const mobileMenu = document.querySelector('.mxch-mobile-menu');
1467 + const mobileOverlay = document.querySelector('.mxch-mobile-overlay');
1468 + const mobileNavLinks = document.querySelectorAll('.mxch-mobile-nav-link, .mxch-mobile-nav-sub-link');
1469 +
1470 + function openMobileMenu() {
1471 + mobileMenu.classList.add('open');
1472 + mobileOverlay.classList.add('open');
1473 + document.body.style.overflow = 'hidden';
1474 + }
1475 +
1476 + function closeMobileMenu() {
1477 + mobileMenu.classList.remove('open');
1478 + mobileOverlay.classList.remove('open');
1479 + document.body.style.overflow = '';
1480 + }
1481 +
1482 + if (mobileMenuBtn) {
1483 + mobileMenuBtn.addEventListener('click', openMobileMenu);
1484 + }
1485 +
1486 + if (mobileMenuClose) {
1487 + mobileMenuClose.addEventListener('click', closeMobileMenu);
1488 + }
1489 +
1490 + if (mobileOverlay) {
1491 + mobileOverlay.addEventListener('click', closeMobileMenu);
1492 + }
1493 +
1494 + // Mobile navigation links
1495 + mobileNavLinks.forEach(link => {
1496 + link.addEventListener('click', function(e) {
1497 + e.preventDefault();
1498 + e.stopPropagation();
1499 +
1500 + const target = this.dataset.target;
1501 + const parentId = this.dataset.parent;
1502 +
1503 + // If this is an expandable parent link
1504 + if (parentId && !target) {
1505 + const subNav = document.querySelector('.mxch-mobile-nav-sub[data-parent="' + parentId + '"]');
1506 + if (subNav) {
1507 + const isExpanded = subNav.classList.contains('expanded');
1508 + // Collapse all sub navs
1509 + document.querySelectorAll('.mxch-mobile-nav-sub').forEach(nav => {
1510 + nav.classList.remove('expanded');
1511 + });
1512 + document.querySelectorAll('.mxch-mobile-nav-link').forEach(l => {
1513 + l.classList.remove('expanded');
1514 + });
1515 + // Toggle this one
1516 + if (!isExpanded) {
1517 + subNav.classList.add('expanded');
1518 + this.classList.add('expanded');
1519 + }
1520 + }
1521 + }
1522 + // If this is a direct link or sub-link with a target
1523 + else if (target) {
1524 + // Update mobile nav active state
1525 + document.querySelectorAll('.mxch-mobile-nav-link, .mxch-mobile-nav-sub-link').forEach(l => {
1526 + l.classList.remove('active');
1527 + });
1528 + this.classList.add('active');
1529 +
1530 + // Also update desktop sidebar nav
1531 + const desktopLink = document.querySelector('.mxch-sidebar [data-target="' + target + '"]');
1532 + if (desktopLink) {
1533 + setActiveNav(desktopLink);
1534 + const parentItem = desktopLink.closest('.mxch-nav-item');
1535 + if (parentItem && desktopLink.classList.contains('mxch-nav-sub-link')) {
1536 + parentItem.classList.add('expanded');
1537 + }
1538 + }
1539 +
1540 + // Show section and close menu
1541 + showSection(target);
1542 + history.replaceState(null, null, '#' + target);
1543 + closeMobileMenu();
1544 + }
1545 + });
1546 + });
1547 +
1548 + // Close mobile menu on escape key
1549 + document.addEventListener('keydown', function(e) {
1550 + if (e.key === 'Escape' && mobileMenu && mobileMenu.classList.contains('open')) {
1551 + closeMobileMenu();
1552 + }
1553 + });
1476 1554 });
1477 1555 </script>
1478 1556 <?php
1479 1557 }
@@ -1479,14 +1557,10 @@
1479 1557 }
1480 1558
1481 1559 /**
1482 1560 * Helper function to render a field with consistent wrapper
1483 - *
1484 - * $hint renders as a .mxch-field-hint line under the description — the
1485 - * softer "caveats / fine print" slot (added for plan-64d34f so hint-bearing
1486 - * fields don't have to hand-roll the wrapper markup).
1487 1561 */
1488 -function mxchat_render_field_wrapper($id, $label, $callback, $description = '', $hint = '') {
1562 +function mxchat_render_field_wrapper($id, $label, $callback, $description = '') {
1489 1563 ?>
1490 1564 <div class="mxch-field">
1491 1565 <label class="mxch-field-label"><?php echo esc_html($label); ?></label>
1492 1566 <div class="mxch-field-control">
@@ -1493,11 +1567,8 @@
1493 1567 <?php $callback(); ?>
1494 1568 </div>
1495 1569 <?php if ($description): ?>
1496 1570 <p class="mxch-field-description"><?php echo esc_html($description); ?></p>
1497 - <?php endif; ?>
1498 - <?php if ($hint): ?>
1499 - <p class="mxch-field-hint"><?php echo esc_html($hint); ?></p>
1500 1571 <?php endif; ?>
1501 1572 </div>
1502 1573 <?php
1503 1574 }