PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.13
MxChat – AI Chatbot & Content Generation for WordPress v3.2.13
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
mxchat-basic / js / admin-onboarding-wizard.js

admin-onboarding-wizard.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.13, at js/admin-onboarding-wizard.js

670 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Onboarding Wizard — client-side step machine.
3 *
4 * Plan: plan-mxchat-20260527-905439 (initial 5-step wizard).
5 * Plan: plan-mxchat-20260528-a2e4d6 (v2 polish — 6-step machine,
6 * clickable pill indicator, AI Behavior step, defensive
7 * confirmation-row fix on embedding step).
8 *
9 * Reads window.MxChatOnboardingWizard (populated by admin-onboarding-page.php)
10 * for catalog/progress/nonce/urls/stepsMeta and drives the 6-step linear wizard.
11 *
12 * Step machine:
13 * 1. chat provider + model + key → POST save_step which=chat
14 * 2. AI Behavior textarea → POST save_step which=behavior (NEW — a2e4d6)
15 * 3. embedding provider + model + key → POST save_step which=embedding
16 * 4. KB seed (polls kb_status every 5s) → POST mark_step
17 * 5. Actions (optional skip) → POST mark_step
18 * 6. Congrats; POST auto_graduate on land. Has a "Review earlier steps" Back.
19 *
20 * One step visible at a time. Continue is gated until the step's completion
21 * criteria are met. Back is available on every step except Step 1 (nothing
22 * to go back to). The pill indicator at the top is a separate navigation
23 * surface — completed pills are clickable and jump directly to that step.
24 */
25 (function () {
26 'use strict';
27
28 var W = window.MxChatOnboardingWizard;
29 if (!W || typeof W !== 'object') return;
30
31 var root = document.getElementById('mxch-onboarding-wizard');
32 if (!root) return;
33
34 // --- Shorthand selectors (scoped to the wizard card) ----------------
35 function $(sel) { return root.querySelector(sel); }
36 function $$(sel) { return Array.prototype.slice.call(root.querySelectorAll(sel)); }
37 function pickStep(n) { return root.querySelector('.mxch-wizard-step[data-step="' + n + '"]'); }
38 function whichSel(which, sel) {
39 return root.querySelector(sel + '[data-mxch-which="' + which + '"]');
40 }
41
42 var TOTAL_STEPS = 6;
43
44 // Map each step number to the progress flag that gates it (null = no flag,
45 // i.e. Congrats which is "all flags true").
46 var STEP_FLAG = {
47 1: 'chat_model',
48 2: 'behavior',
49 3: 'embedding_model',
50 4: 'knowledge_base',
51 5: 'actions',
52 6: null
53 };
54
55 var STEP_NAMES = {
56 1: 'Choose your chat model',
57 2: 'Tell your chatbot how to behave',
58 3: 'Choose your embedding model',
59 4: 'Add to your knowledge base',
60 5: 'Try Actions (optional)',
61 6: 'You\'re set up'
62 };
63
64 // --- Internal state -------------------------------------------------
65 var state = {
66 current: W.initialStep || 1,
67 progress: Object.assign({}, W.progress || {}),
68 chat: {
69 provider: W.currentChatProvider || '',
70 model: W.currentChatModel || '',
71 keyKnown: !!(W.currentChatProvider && W.catalog[W.currentChatProvider] && W.catalog[W.currentChatProvider].hasKey),
72 keyFresh: ''
73 },
74 embedding: {
75 provider: W.currentEmbedProvider || '',
76 model: W.currentEmbedModel || '',
77 keyKnown: !!(W.currentEmbedProvider && W.catalog[W.currentEmbedProvider] && W.catalog[W.currentEmbedProvider].hasKey),
78 keyFresh: ''
79 },
80 behavior: {
81 value: (typeof W.currentInstructions === 'string') ? W.currentInstructions : ''
82 },
83 kbCount: W.kbCount || 0,
84 kbPollTimer: null,
85 graduated: false
86 };
87
88 // --- Generic AJAX helper -------------------------------------------
89 function ajax(action, payload, cb) {
90 var body = new URLSearchParams();
91 body.append('action', action);
92 body.append('nonce', W.nonce);
93 Object.keys(payload || {}).forEach(function (k) {
94 if (payload[k] !== undefined && payload[k] !== null) {
95 body.append(k, payload[k]);
96 }
97 });
98 fetch(W.ajaxUrl, {
99 method: 'POST',
100 credentials: 'same-origin',
101 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
102 body: body.toString()
103 }).then(function (r) { return r.json().catch(function () { return null; }); })
104 .then(function (j) { cb(null, j); })
105 .catch(function (e) { cb(e); });
106 }
107
108 // --- Pill indicator render (plan-a2e4d6) ----------------------------
109 // Determines, per pill, which of three visual states it's in:
110 // - is-complete : the flag for that step is true (and it's not the current step)
111 // - is-current : the wizard is on that step right now
112 // - is-future : flag false AND not current → non-clickable, faded
113 function renderPillNav() {
114 var pills = $$('.mxch-wizard-pill');
115 pills.forEach(function (pill) {
116 var n = parseInt(pill.getAttribute('data-mxch-pill-step'), 10);
117 var flag = STEP_FLAG[n];
118 var done = (flag === null)
119 ? Object.keys(STEP_FLAG).every(function (k) {
120 var f = STEP_FLAG[k]; return f === null ? true : !!state.progress[f];
121 })
122 : !!state.progress[flag];
123 var isCurrent = (n === state.current);
124
125 pill.classList.toggle('is-complete', done && !isCurrent);
126 pill.classList.toggle('is-current', isCurrent);
127 pill.classList.toggle('is-future', !done && !isCurrent);
128
129 if (isCurrent) {
130 pill.setAttribute('aria-current', 'step');
131 } else {
132 pill.removeAttribute('aria-current');
133 }
134 // Clickable iff complete (jump back) OR current (no-op). Future is disabled.
135 if (!done && !isCurrent) {
136 pill.setAttribute('disabled', '');
137 pill.setAttribute('aria-disabled', 'true');
138 } else {
139 pill.removeAttribute('disabled');
140 pill.removeAttribute('aria-disabled');
141 }
142 });
143 }
144
145 // Pill click → jump to that step IFF complete or current.
146 $$('.mxch-wizard-pill').forEach(function (pill) {
147 pill.addEventListener('click', function () {
148 if (pill.hasAttribute('disabled')) return;
149 var n = parseInt(pill.getAttribute('data-mxch-pill-step'), 10);
150 if (!isNaN(n) && n >= 1 && n <= TOTAL_STEPS) showStep(n);
151 });
152 });
153
154 // --- Step show/hide + progress chrome ------------------------------
155 function showStep(n) {
156 if (state.kbPollTimer) {
157 clearInterval(state.kbPollTimer);
158 state.kbPollTimer = null;
159 }
160 state.current = n;
161 $$('.mxch-wizard-step').forEach(function (el) {
162 el.hidden = (parseInt(el.getAttribute('data-step'), 10) !== n);
163 });
164 var lbl = $('[data-mxch-current-step]');
165 if (lbl) lbl.textContent = String(n);
166 var name = $('[data-mxch-step-name]');
167 if (name) name.textContent = STEP_NAMES[n] || '';
168 var fill = $('[data-mxch-progress-fill]');
169 if (fill) fill.style.width = ((n - 1) / (TOTAL_STEPS - 1) * 100) + '%';
170 var bar = $('.mxch-wizard-progress-bar');
171 if (bar) bar.setAttribute('aria-valuenow', String((n - 1) / (TOTAL_STEPS - 1) * 100 | 0));
172
173 renderPillNav();
174
175 if (n === 1) hydrateStep1();
176 if (n === 2) hydrateStep2_behavior();
177 if (n === 3) hydrateStep3_embedding();
178 // Step 4 (Knowledge) is optional — no hydration / no KB-status poll.
179 if (n === 5) hydrateStep5_actions();
180 if (n === 6) hydrateStep6_congrats();
181 }
182
183 // ====================================================================
184 // STEP 1 (chat)
185 // ====================================================================
186 function hydrateStep1() {
187 var providerSel = whichSel('chat', '.mxch-wiz-select[data-mxch-role="provider"]');
188 if (state.chat.provider && providerSel.value !== state.chat.provider) {
189 providerSel.value = state.chat.provider;
190 }
191 applyProviderUI('chat');
192 refreshContinue(1);
193 }
194
195 // ====================================================================
196 // STEP 2 (behavior — NEW)
197 // ====================================================================
198 function hydrateStep2_behavior() {
199 var ta = document.getElementById('mxch-wiz-behavior-textarea');
200 if (ta && ta.value === '' && state.behavior.value !== '') {
201 ta.value = state.behavior.value;
202 }
203 refreshContinue(2);
204 }
205
206 // Wire the behavior textarea + example one-click-fill buttons.
207 var behaviorTa = document.getElementById('mxch-wiz-behavior-textarea');
208 if (behaviorTa) {
209 behaviorTa.addEventListener('input', function () {
210 state.behavior.value = behaviorTa.value;
211 });
212 }
213
214 var BEHAVIOR_EXAMPLES = {
215 helpful: "You are a helpful website assistant. Your goal is to answer visitor questions using the information available about this website. Keep replies under 3 sentences and direct. Don’t invent facts — if something isn’t in your knowledge base, say so honestly and suggest the visitor contact us. Stay focused on this site’s content; light conversation is fine but redirect toward useful answers. Respond in the visitor’s language. Hyperlink any URLs you reference.",
216 sales: "You are a friendly product expert for this store. Your goal is to help visitors find the right product, answer questions about features and pricing, and gently guide them toward making a purchase or adding items to their cart. Highlight benefits, mention popular choices, and suggest related items when relevant. Don’t pressure — be informative. If a visitor asks about something not in your knowledge base, say so and offer to connect them with a human. Keep replies concise (2–4 sentences) and warm.",
217 support: "You are a patient and empathetic customer support agent. Your job is to help visitors troubleshoot issues, answer how-to questions, and resolve concerns based on our documentation and knowledge base. Ask clarifying questions when needed. Acknowledge the visitor’s frustration when appropriate. If you can’t resolve an issue or the information isn’t in your knowledge base, recommend they open a support ticket. Keep replies clear and step-by-step when troubleshooting. Always be respectful, never dismissive."
218 };
219
220 $$('.mxch-wiz-behavior-example').forEach(function (btn) {
221 btn.addEventListener('click', function () {
222 var key = btn.getAttribute('data-mxch-example');
223 var text = BEHAVIOR_EXAMPLES[key];
224 if (!text || !behaviorTa) return;
225 behaviorTa.value = text;
226 state.behavior.value = text;
227 behaviorTa.focus();
228 // Visual feedback: brief highlight on the picked example.
229 $$('.mxch-wiz-behavior-example').forEach(function (b) { b.classList.remove('is-picked'); });
230 btn.classList.add('is-picked');
231 });
232 });
233
234 // --- Sample Instructions modal (mirrors mxchat-admin.js behavior) ---
235 (function wireSampleModal() {
236 var viewBtn = document.getElementById('mxchatViewSampleBtn');
237 var modal = document.getElementById('mxchatSampleModal');
238 if (!viewBtn || !modal) return;
239 var modalClose = document.getElementById('mxchatModalClose');
240 var copyBtn = document.getElementById('mxchatCopyBtn');
241 var modalContent = modal.querySelector('.mxchat-instructions-modal-content');
242 var instructionsContent = modal.querySelector('.mxchat-instructions-content');
243
244 viewBtn.addEventListener('click', function (e) {
245 e.preventDefault();
246 e.stopPropagation();
247 modal.classList.add('mxchat-instructions-show');
248 });
249 function closeModal(e) {
250 if (e) { e.preventDefault(); e.stopPropagation(); }
251 modal.classList.remove('mxchat-instructions-show');
252 }
253 if (modalClose) modalClose.addEventListener('click', closeModal);
254 modal.addEventListener('click', function (e) {
255 if (e.target === modal) closeModal(e);
256 });
257 if (modalContent) {
258 modalContent.addEventListener('click', function (e) { e.stopPropagation(); });
259 }
260 document.addEventListener('keydown', function (e) {
261 if (e.key === 'Escape' && modal.classList.contains('mxchat-instructions-show')) closeModal();
262 });
263 if (copyBtn && instructionsContent) {
264 copyBtn.addEventListener('click', function (e) {
265 e.preventDefault();
266 e.stopPropagation();
267 var text = instructionsContent.textContent;
268 if (navigator.clipboard) {
269 navigator.clipboard.writeText(text).catch(function () { fallbackCopy(text); });
270 } else {
271 fallbackCopy(text);
272 }
273 });
274 }
275 function fallbackCopy(text) {
276 var ta = document.createElement('textarea');
277 ta.value = text;
278 ta.style.position = 'fixed';
279 ta.style.left = '-999999px';
280 document.body.appendChild(ta);
281 ta.select();
282 try { document.execCommand('copy'); } catch (_e) {}
283 document.body.removeChild(ta);
284 }
285 })();
286
287 // ====================================================================
288 // STEP 3 (embedding)
289 // ====================================================================
290 function hydrateStep3_embedding() {
291 var providerSel = whichSel('embedding', '.mxch-wiz-select[data-mxch-role="provider"]');
292 if (state.embedding.provider && providerSel.value !== state.embedding.provider) {
293 providerSel.value = state.embedding.provider;
294 }
295 applyProviderUI('embedding');
296 refreshContinue(3);
297 }
298
299 /**
300 * Render the model dropdown + key UI for the currently-selected provider
301 * on the given step ('chat' or 'embedding').
302 *
303 * Plan-a2e4d6 fix for Issue 5 (double-confirmation): DEFENSIVELY hide
304 * EVERY conditional UI row at the top of this function before deciding
305 * which one to show. The previous attempt (d14e89) hid the dedup row
306 * but conditionally hid keySavedRow only inside an else branch — a stale
307 * keyKnown state plus a no-op early return could leave both visible.
308 * Belt-and-suspenders: hide all four first, then show only the right one.
309 */
310 function applyProviderUI(which) {
311 var slot = state[which];
312 var providerSlug = slot.provider;
313 var modelField = whichSel(which, '.mxch-wiz-model-field');
314 var keyField = whichSel(which, '.mxch-wiz-key-field');
315 var keySavedRow = whichSel(which, '.mxch-wiz-key-saved');
316 var dedupRow = whichSel(which, '.mxch-wiz-key-dedup');
317 var errBox = whichSel(which, '.mxch-wiz-error');
318
319 // DEFENSIVE: hide ALL conditional rows before deciding which to show.
320 // This is the fix Maxwell explicitly approved for plan-a2e4d6 Issue 5.
321 if (errBox) { errBox.hidden = true; errBox.textContent = ''; }
322 if (dedupRow) dedupRow.hidden = true;
323 if (keySavedRow) keySavedRow.hidden = true;
324 if (keyField) keyField.hidden = true;
325 if (modelField) modelField.hidden = true;
326
327 if (!providerSlug || !W.catalog[providerSlug]) {
328 return;
329 }
330
331 var entry = W.catalog[providerSlug];
332 var modelList = which === 'chat' ? entry.chatModels : entry.embeddingModels;
333
334 // Populate the model dropdown.
335 var modelSel = whichSel(which, '.mxch-wiz-select[data-mxch-role="model"]');
336 modelSel.innerHTML = '';
337 var placeholderOpt = document.createElement('option');
338 placeholderOpt.value = '';
339 placeholderOpt.textContent = W.strings.selectModel;
340 modelSel.appendChild(placeholderOpt);
341 Object.keys(modelList).forEach(function (val) {
342 var opt = document.createElement('option');
343 opt.value = val;
344 opt.textContent = modelList[val];
345 modelSel.appendChild(opt);
346 });
347 if (slot.model && modelList[slot.model]) {
348 modelSel.value = slot.model;
349 } else {
350 slot.model = '';
351 }
352 if (modelField) modelField.hidden = false;
353
354 // Dedup case (embedding only): same provider as chat AND key known.
355 var isDedup = (which === 'embedding'
356 && state.chat.provider === providerSlug
357 && (state.chat.keyKnown || state.chat.keyFresh !== ''));
358 if (isDedup) {
359 if (dedupRow) {
360 var label = entry.label;
361 dedupRow.querySelector('[data-mxch-dedup-text]').textContent =
362 W.strings.embedKeyReused.replace('%s', label);
363 dedupRow.hidden = false;
364 }
365 slot.keyKnown = true;
366 slot.keyFresh = '';
367 return;
368 }
369
370 // Non-dedup: show either the already-saved checkmark, or the input.
371 // hasKey is read fresh from W.catalog[providerSlug] each call so we
372 // never display a stale provider's confirmation row.
373 var hasSavedKey = !!entry.hasKey || (slot.keyFresh !== '' && slot.provider === providerSlug);
374 if (hasSavedKey) {
375 if (keySavedRow) {
376 keySavedRow.querySelector('[data-mxch-key-saved-text]').textContent =
377 W.strings.keyAlreadySaved.replace('%s', entry.label);
378 keySavedRow.hidden = false;
379 }
380 } else {
381 if (keyField) {
382 keyField.hidden = false;
383 var lbl = keyField.querySelector('[data-mxch-key-label]');
384 if (lbl) lbl.textContent = entry.label + ' ' + 'API key';
385 var input = keyField.querySelector('.mxch-wiz-key-input');
386 if (input && slot.keyFresh === '') input.value = '';
387 }
388 }
389 }
390
391 // Provider/model dropdown change handlers.
392 $$('.mxch-wiz-select[data-mxch-role="provider"]').forEach(function (sel) {
393 sel.addEventListener('change', function () {
394 var which = sel.getAttribute('data-mxch-which');
395 state[which].provider = sel.value;
396 state[which].model = '';
397 state[which].keyFresh = '';
398 state[which].keyKnown = !!(sel.value && W.catalog[sel.value] && W.catalog[sel.value].hasKey);
399 applyProviderUI(which);
400 refreshContinue(which === 'chat' ? 1 : 3);
401 });
402 });
403 $$('.mxch-wiz-select[data-mxch-role="model"]').forEach(function (sel) {
404 sel.addEventListener('change', function () {
405 var which = sel.getAttribute('data-mxch-which');
406 state[which].model = sel.value;
407 refreshContinue(which === 'chat' ? 1 : 3);
408 });
409 });
410
411 // Save-key handlers (Step 1 & Step 3 non-dedup).
412 $$('.mxch-wiz-key-save').forEach(function (btn) {
413 btn.addEventListener('click', function () {
414 var which = btn.getAttribute('data-mxch-which');
415 var input = whichSel(which, '.mxch-wiz-key-field').querySelector('.mxch-wiz-key-input');
416 var val = (input && input.value || '').trim();
417 var errBox = whichSel(which, '.mxch-wiz-error');
418 if (!val) {
419 if (errBox) {
420 errBox.textContent = 'Enter a key, then click Save.';
421 errBox.hidden = false;
422 }
423 return;
424 }
425 var slot = state[which];
426 slot.keyFresh = val;
427 slot.keyKnown = true;
428 if (W.catalog[slot.provider]) W.catalog[slot.provider].hasKey = true;
429 applyProviderUI(which);
430 refreshContinue(which === 'chat' ? 1 : 3);
431 });
432 });
433
434 // Replace-key links.
435 $$('.mxch-wiz-replace-key-link').forEach(function (link) {
436 link.addEventListener('click', function () {
437 var which = link.getAttribute('data-mxch-which');
438 state[which].keyKnown = false;
439 state[which].keyFresh = '';
440 var keyField = whichSel(which, '.mxch-wiz-key-field');
441 var keySavedRow = whichSel(which, '.mxch-wiz-key-saved');
442 if (keySavedRow) keySavedRow.hidden = true;
443 if (keyField) {
444 keyField.hidden = false;
445 var lbl = keyField.querySelector('[data-mxch-key-label]');
446 var entry = W.catalog[state[which].provider];
447 if (lbl && entry) lbl.textContent = entry.label + ' ' + 'API key';
448 var input = keyField.querySelector('.mxch-wiz-key-input');
449 if (input) { input.value = ''; input.focus(); }
450 }
451 refreshContinue(which === 'chat' ? 1 : 3);
452 });
453 });
454
455 // --- Continue-gating ----------------------------------------------
456 function isStepReady(n) {
457 if (n === 1) {
458 var c = state.chat;
459 return !!(c.provider && c.model && (c.keyKnown || c.keyFresh !== ''));
460 }
461 if (n === 2) return true; // behavior is optional — empty is valid
462 if (n === 3) {
463 var e = state.embedding;
464 return !!(e.provider && e.model && (e.keyKnown || e.keyFresh !== ''));
465 }
466 if (n === 4) return true; // knowledge base is optional
467 if (n === 5) return true; // actions are optional
468 return false;
469 }
470
471 function refreshContinue(n) {
472 var btn = root.querySelector('.mxch-wiz-continue[data-mxch-from-step="' + n + '"]');
473 if (!btn) return;
474 btn.disabled = !isStepReady(n);
475 }
476
477 // --- Continue / Back handlers --------------------------------------
478 $$('.mxch-wiz-continue').forEach(function (btn) {
479 btn.addEventListener('click', function () {
480 var n = parseInt(btn.getAttribute('data-mxch-from-step'), 10);
481 if (!isStepReady(n)) return;
482
483 // Steps 1 (chat) and 3 (embedding) — save provider/model/key.
484 if (n === 1 || n === 3) {
485 var which = (n === 1) ? 'chat' : 'embedding';
486 var slot = state[which];
487 btn.disabled = true;
488 btn.textContent = W.strings.saving;
489 ajax('mxchat_onboarding_save_step', {
490 which: which,
491 provider: slot.provider,
492 model: slot.model,
493 api_key: slot.keyFresh
494 }, function (err, j) {
495 btn.textContent = 'Continue';
496 if (err || !j || !j.success) {
497 var errBox = whichSel(which, '.mxch-wiz-error');
498 if (errBox) {
499 errBox.textContent = (j && j.data && j.data.message) ? j.data.message : W.strings.saveError;
500 errBox.hidden = false;
501 }
502 btn.disabled = false;
503 return;
504 }
505 if (j.data && j.data.progress) state.progress = j.data.progress;
506 slot.keyFresh = '';
507 slot.keyKnown = true;
508 showStep(n + 1);
509 });
510 } else if (n === 2) {
511 // Behavior step — save the textarea value (or empty).
512 btn.disabled = true;
513 btn.textContent = W.strings.saving;
514 ajax('mxchat_onboarding_save_step', {
515 which: 'behavior',
516 system_prompt_instructions: state.behavior.value
517 }, function (err, j) {
518 btn.textContent = 'Continue';
519 btn.disabled = false;
520 if (err || !j || !j.success) {
521 var errBox = whichSel('behavior', '.mxch-wiz-error');
522 if (errBox) {
523 errBox.textContent = (j && j.data && j.data.message) ? j.data.message : W.strings.saveError;
524 errBox.hidden = false;
525 }
526 return;
527 }
528 if (j.data && j.data.progress) state.progress = j.data.progress;
529 showStep(3);
530 });
531 } else if (n === 4) {
532 ajax('mxchat_onboarding_mark_step', { step: 'knowledge_base' }, function (err, j) {
533 if (j && j.data && j.data.progress) state.progress = j.data.progress;
534 showStep(5);
535 });
536 } else if (n === 5) {
537 ajax('mxchat_onboarding_mark_step', { step: 'actions' }, function (err, j) {
538 if (j && j.data && j.data.progress) state.progress = j.data.progress;
539 showStep(6);
540 });
541 }
542 });
543 });
544
545 $$('.mxch-wiz-back').forEach(function (btn) {
546 btn.addEventListener('click', function () {
547 var n = parseInt(btn.getAttribute('data-mxch-from-step'), 10);
548 if (n > 1) showStep(n - 1);
549 });
550 });
551
552 // ====================================================================
553 // STEP 4 (KB polling)
554 // ====================================================================
555 function hydrateStep4_kb() {
556 updateKbStatus(state.kbCount);
557 state.kbPollTimer = setInterval(function () {
558 ajax('mxchat_onboarding_kb_status', {}, function (err, j) {
559 if (j && j.success && j.data && typeof j.data.count === 'number') {
560 state.kbCount = j.data.count;
561 updateKbStatus(state.kbCount);
562 refreshContinue(4);
563 }
564 });
565 }, 5000);
566 }
567
568 function updateKbStatus(count) {
569 var dot = $('[data-mxch-kb-dot]');
570 var text = $('[data-mxch-kb-text]');
571 var wrap = $('[data-mxch-kb-status]');
572 if (!dot || !text || !wrap) return;
573 if (count > 0) {
574 wrap.classList.add('mxch-wiz-kb-ok');
575 wrap.classList.remove('mxch-wiz-kb-empty');
576 dot.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
577 text.textContent = count === 1
578 ? W.strings.kbFoundOne
579 : W.strings.kbFoundMany.replace('%d', String(count));
580 } else {
581 wrap.classList.remove('mxch-wiz-kb-ok');
582 wrap.classList.add('mxch-wiz-kb-empty');
583 dot.innerHTML = '';
584 text.textContent = W.strings.kbNone;
585 }
586 }
587
588 // ====================================================================
589 // STEP 5 (actions — optional)
590 // ====================================================================
591 function hydrateStep5_actions() {
592 refreshContinue(5);
593 }
594
595 // ====================================================================
596 // STEP 6 (congrats + auto-graduate)
597 // ====================================================================
598 function hydrateStep6_congrats() {
599 if (state.graduated) return;
600 state.graduated = true;
601 ajax('mxchat_onboarding_auto_graduate', {}, function () {
602 // Menu item disappears on next admin nav.
603 });
604 }
605
606 // --- Shortcode copy buttons (Step 6 — plan-23987f) ---
607 $$('.mxch-wiz-shortcode-copy').forEach(function (btn) {
608 btn.addEventListener('click', function (e) {
609 e.preventDefault();
610 var sc = btn.getAttribute('data-mxch-copy-shortcode') || '';
611 if (!sc) return;
612 var label = btn.querySelector('.mxch-wiz-shortcode-copy-text');
613 var origLabel = label ? label.textContent : '';
614 function flashCopied() {
615 btn.classList.add('is-copied');
616 if (label) {
617 label.textContent = (W.strings && W.strings.shortcodeCopied) || 'Copied!';
618 }
619 setTimeout(function () {
620 btn.classList.remove('is-copied');
621 if (label) label.textContent = origLabel;
622 }, 1500);
623 }
624 function fallback() {
625 var ta = document.createElement('textarea');
626 ta.value = sc;
627 ta.style.position = 'fixed';
628 ta.style.left = '-999999px';
629 document.body.appendChild(ta);
630 ta.select();
631 try { document.execCommand('copy'); flashCopied(); } catch (_e) {}
632 document.body.removeChild(ta);
633 }
634 if (navigator.clipboard && navigator.clipboard.writeText) {
635 navigator.clipboard.writeText(sc).then(flashCopied, fallback);
636 } else {
637 fallback();
638 }
639 });
640 });
641
642 // --- Dismiss button (preserved from f7c7d4) ---
643 var dismissBtn = root.querySelector('.mxch-onboarding-dismiss');
644 // Note: dismissBtn is rendered at PAGE level (outside the wizard card), so
645 // `root.querySelector` (scoped to the wizard) will not find it. Fall back
646 // to document-level so the existing behavior is preserved.
647 if (!dismissBtn) dismissBtn = document.querySelector('.mxch-onboarding-dismiss');
648 if (dismissBtn) {
649 dismissBtn.addEventListener('click', function (e) {
650 e.preventDefault();
651 if (!window.confirm('Hide MxChat Onboarding from the menu? You can bring it back from Settings → Display.')) return;
652 var nonce = dismissBtn.getAttribute('data-mxch-dismiss-nonce');
653 if (!nonce) return;
654 var body = new URLSearchParams();
655 body.append('action', 'mxchat_dismiss_onboarding');
656 body.append('nonce', nonce);
657 fetch(W.ajaxUrl, {
658 method: 'POST',
659 credentials: 'same-origin',
660 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
661 body: body.toString()
662 }).then(function () { window.location.reload(); })
663 .catch(function () { window.location.reload(); });
664 });
665 }
666
667 // --- Initial render -------------------------------------------------
668 showStep(state.current);
669 })();
670