| 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 |
|