1/** 2 * UDX Docs Concierge -- Chat & Markdown 3 * 4 * Markdown renderer (XSS-safe), chat mode UI, message persistence, 5 * usage tracking, delete messages, cross-bot memory, and explore. 6 * Depends on window.UDXDocs namespace (initialised by concierge.js). 7 */ 8window.UDXDocs = window.UDXDocs || {}; 9(function (ns) { 10 'use strict'; 11 12 var USAGE_KEY = 'udx_docs_usage'; 13 var SESSION_KEY = 'udx_docs_session'; 14 var FREE_USES = 50; 15 16 var loaderSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>'; 17 18 /* ---- HTML escaping (exported for search-result XSS protection) ---- */ 19 function escHtml(t) { 20 return (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 21 } 22 function escAttr(t) { 23 return escHtml(t || '').replace(/"/g, '"').replace(/'/g, '''); 24 } 25 ns.escHtml = escHtml; 26 ns.escAttr = escAttr; 27 28 /* ---- Inline formatting ---- */ 29 function inlineFmt(t) { 30 var tokens = []; 31 t = t.replace(/`([^`]+)`/g, function (_, c) { 32 tokens.push('<code>' + escHtml(c) + '</code>'); 33 return '%%TK_' + (tokens.length - 1) + '%%'; 34 }); 35 t = t.replace(/\[([^\]]+)\]\(((?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)[\w-]{11}[^)]*)\)/g, function (_, text, href) { 36 tokens.push('<a href="' + escAttr(href) + '" target="_blank" rel="noopener" class="udx-docs-yt-link">\u25B6 ' + escHtml(text) + '</a>'); 37 return '%%TK_' + (tokens.length - 1) + '%%'; 38 }); 39 t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_, text, href) { 40 if (/^https?:\/\//i.test(href) || href.charAt(0) === '#' || href.charAt(0) === '/') { 41 tokens.push('<a href="' + escAttr(href) + '" target="_blank" rel="noopener">' + escHtml(text) + '</a>'); 42 return '%%TK_' + (tokens.length - 1) + '%%'; 43 } 44 return _; 45 }); 46 t = t.replace(/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]{11})(?:[&?][^\s]*)?/g, function (url) { 47 tokens.push('<a href="' + escAttr(url) + '" target="_blank" rel="noopener" class="udx-docs-yt-link">\u25B6 ' + escHtml(url.replace(/^https?:\/\/(www\.)?/, '').substring(0, 50)) + '</a>'); 48 return '%%TK_' + (tokens.length - 1) + '%%'; 49 }); 50 t = escHtml(t); 51 t = t.replace(/%%TK_(\d+)%%/g, function (_, i) { return tokens[parseInt(i, 10)]; }); 52 return t 53 .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') 54 .replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>') 55 .replace(/~~(.+?)~~/g, '<del>$1</del>'); 56 } 57 58 /* ---- YouTube URL helpers ---- */ 59 var ytRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]{11})(?:[&?][^\s]*)?$/; 60 61 function extractYouTubeId(url) { 62 var m = url.trim().match(ytRegex); 63 return m ? m[1] : null; 64 } 65 66 /* ---- SVG sanitisation ---- */ 67 var svgAllowedTags = ['svg','g','path','circle','rect','line','polyline','polygon','text','tspan','defs','use','clippath','mask','pattern','lineargradient','radialgradient','stop','marker','title','desc','ellipse']; 68 var svgAllowedAttrs = ['viewbox','width','height','fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','stroke-dasharray','stroke-dashoffset','d','cx','cy','r','rx','ry','x','y','x1','y1','x2','y2','points','transform','opacity','font-size','font-family','font-weight','text-anchor','dominant-baseline','id','class','offset','stop-color','stop-opacity','gradientunits','gradienttransform','patternunits','patterntransform','markerwidth','markerheight','refx','refy','orient','markerunits','xmlns','preserveaspectratio','dx','dy','rotate','textlength','lengthadjust','clip-path','clip-rule','fill-opacity','fill-rule','color','display','overflow','visibility']; 69 70 function sanitizeSvg(raw) { 71 var tmp = document.createElement('div'); 72 tmp.innerHTML = raw; 73 function walk(node) { 74 var children = Array.prototype.slice.call(node.childNodes); 75 children.forEach(function (child) { 76 if (child.nodeType === 3) return; // text node 77 if (child.nodeType !== 1) { child.remove(); return; } 78 var tag = child.tagName.toLowerCase(); 79 if (svgAllowedTags.indexOf(tag) === -1) { child.remove(); return; } 80 var attrs = Array.prototype.slice.call(child.attributes);
81 attrs.forEach(function (a) { 82 var name = a.name.toLowerCase(); 83 if (name.indexOf('on') === 0 || svgAllowedAttrs.indexOf(name) === -1) { 84 child.removeAttribute(a.name); 85 } 86 }); 87 walk(child); 88 }); 89 } 90 walk(tmp); 91 return tmp.innerHTML; 92 } 93 94 /* ---- Mermaid lazy loader ---- */ 95 var mermaidLoaded = false; 96 var mermaidReady = null; 97 98 function ensureMermaid() { 99 if (mermaidReady) return mermaidReady; 100 mermaidReady = new Promise(function (resolve) { 101 if (mermaidLoaded && window.mermaid) { resolve(); return; } 102 var sc = document.createElement('script'); 103 sc.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js'; 104 sc.onload = function () { 105 mermaidLoaded = true; 106 window.mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'strict' }); 107 resolve(); 108 }; 109 document.head.appendChild(sc); 110 }); 111 return mermaidReady; 112 } 113 114 ns.renderMermaid = function (container) { 115 var nodes = container.querySelectorAll('.udx-docs-mermaid[data-mermaid]'); 116 if (!nodes.length) return; 117 ensureMermaid().then(function () { 118 nodes.forEach(function (el) { 119 if (el.getAttribute('data-rendered')) return; 120 el.setAttribute('data-rendered', '1'); 121 var code = el.getAttribute('data-mermaid'); 122 var id = 'mmd-' + Math.random().toString(36).substr(2, 8); 123 try { 124 window.mermaid.render(id, code).then(function (result) { 125 el.innerHTML = result.svg; 126 }).catch(function () { 127 el.textContent = code; 128 }); 129 } catch (e) { 130 el.textContent = code; 131 } 132 }); 133 }); 134 }; 135 136 /* ---- Markdown renderer ---- */ 137 ns.md = function (s) { 138 if (!s) return ''; 139 140 var codeBlocks = []; 141 s = s.replace(/```([\w-]*)\n([\s\S]*?)```/g, function (_, lang, code) { 142 var idx = codeBlocks.length; 143 var langLower = (lang || '').toLowerCase(); 144 145 // Mermaid code blocks â render as diagram 146 if (langLower === 'mermaid') { 147 codeBlocks.push('<div class="udx-docs-mermaid" data-mermaid="' + escAttr(code.trim()) + '"></div>'); 148 return '\n%%CODEBLOCK_' + idx + '%%\n'; 149 } 150 151 // SVG code blocks â sanitize and render inline 152 if (langLower === 'svg') { 153 codeBlocks.push('<div class="udx-docs-svg-render">' + sanitizeSvg(code) + '</div>'); 154 return '\n%%CODEBLOCK_' + idx + '%%\n'; 155 } 156 157 var cls = lang ? ' class="language-' + lang + '"' : ''; 158 var label = lang ? '<span class="code-lang">' + lang + '</span>' : ''; 159 codeBlocks.push(label + '<pre data-lang="' + escAttr(lang || '') + '"><code' + cls + '>' + code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</code></pre>'); 160 return '\n%%CODEBLOCK_' + idx + '%%\n'; 161 }); 162 163 // YouTube standalone URLs â embed placeholders 164 var ytEmbeds = []; 165 s = s.replace(/^((?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)[\w-]{11}(?:[&?][^\s]*)?)$/gm, function (match) { 166 var vid = extractYouTubeId(match); 167 if (!vid) return match; 168 var idx = ytEmbeds.length; 169 ytEmbeds.push('<div class="udx-docs-yt-embed"><iframe src="https://www.youtube-nocookie.com/embed/' + vid + '" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe></div>'); 170 return '%%YTEMBED_' + idx + '%%'; 171 }); 172 173 var lines = s.split('\n'); 174 var out = []; 175 var i = 0; 176 177 while (i < lines.length) { 178 var line = lines[i]; 179 180 var cbMatch = line.match(/^%%CODEBLOCK_(\d+)%%$/); 181 if (cbMatch) { 182 out.push(codeBlocks[parseInt(cbMatch[1], 10)]); 183 i++; 184 continue; 185 } 186 187 var ytMatch = line.match(/^%%YTEMBED_(\d+)%%$/); 188 if (ytMatch) { 189 out.push(ytEmbeds[parseInt(ytMatch[1], 10)]); 190 i++; 191 continue; 192 } 193 194 if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) { 195 out.push('<hr>'); 196 i++; 197 continue; 198 } 199 200 var headMatch = line.match(/^(#{1,6})\s+(.+)$/); 201 if (headMatch) { 202 var lvl = headMatch[1].length; 203 out.push('<h' + lvl + '>' + inlineFmt(headMatch[2]) + '</h' + lvl + '>'); 204 i++; 205 continue; 206 } 207 208 if (/^>\s?/.test(line)) { 209 var bqLines = []; 210 while (i < lines.length && /^>\s?/.test(lines[i])) { 211 bqLines.push(lines[i].replace(/^>\s?/, '')); 212 i++; 213 } 214 out.push('<blockquote>' + inlineFmt(bqLines.join('<br>')) + '</blockquote>'); 215 continue; 216 } 217 218 if (line.indexOf('|') !== -1 && i + 1 < lines.length && /^\|?\s*[-:]+[-| :]*$/.test(lines[i + 1])
218) { 219 var headers = line.split('|').map(function (c) { return c.trim(); }).filter(Boolean); 220 var alignRow = lines[i + 1].split('|').map(function (c) { return c.trim(); }).filter(Boolean); 221 var aligns = alignRow.map(function (c) { 222 if (/^:-+:$/.test(c)) return 'center'; 223 if (/^-+:$/.test(c)) return 'right'; 224 return 'left'; 225 }); 226 var thtml = '<div class="md-table-wrap"><table><thead><tr>'; 227 for (var h = 0; h < headers.length; h++) { 228 thtml += '<th style="text-align:' + (aligns[h] || 'left') + '">' + inlineFmt(headers[h]) + '</th>'; 229 } 230 thtml += '</tr></thead><tbody>'; 231 i += 2; 232 while (i < lines.length && lines[i].indexOf('|') !== -1) { 233 var cells = lines[i].split('|').map(function (c) { return c.trim(); }).filter(Boolean); 234 thtml += '<tr>'; 235 for (var c = 0; c < headers.length; c++) { 236 thtml += '<td style="text-align:' + (aligns[c] || 'left') + '">' + inlineFmt(cells[c] || '') + '</td>'; 237 } 238 thtml += '</tr>'; 239 i++; 240 } 241 thtml += '</tbody></table></div>'; 242 out.push(thtml); 243 continue; 244 } 245 246 if (/^[\s]*[-*+]\s+/.test(line)) { 247 var items = []; 248 while (i < lines.length && /^[\s]*[-*+]\s+/.test(lines[i])) { 249 items.push(inlineFmt(lines[i].replace(/^[\s]*[-*+]\s+/, ''))); 250 i++; 251 } 252 out.push('<ul>' + items.map(function (t) { return '<li>' + t + '</li>'; }).join('') + '</ul>'); 253 continue; 254 } 255 256 if (/^[\s]*\d+[.)]\s+/.test(line)) { 257 var oItems = []; 258 while (i < lines.length && /^[\s]*\d+[.)]\s+/.test(lines[i])) { 259 oItems.push(inlineFmt(lines[i].replace(/^[\s]*\d+[.)]\s+/, ''))); 260 i++; 261 } 262 out.push('<ol>' + oItems.map(function (t) { return '<li>' + t + '</li>'; }).join('') + '</ol>'); 263 continue; 264 } 265 266 if (line.trim() === '') { 267 i++; 268 continue; 269 } 270 271 var pLines = []; 272 while (i < lines.length && lines[i].trim() !== '' && !/^#{1,6}\s/.test(lines[i]) && !/^[-*+]\s+/.test(lines[i]) && !/^\d+[.)]\s+/.test(lines[i]) && !/^>\s?/.test(lines[i]) && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i].trim()) && !/^%%CODEBLOCK_/.test(lines[i]) && !/^%%YTEMBED_/.test(lines[i]) && !(lines[i].indexOf('|') !== -1 && i + 1 < lines.length && /^\|?\s*[-:]+[-| :]*$/.test(lines[i + 1] || ''))) { 273 pLines.push(lines[i]); 274 i++; 275 } 276 if (pLines.length) { 277 out.push('<p>' + inlineFmt(pLines.join('<br>')) + '</p>'); 278 } 279 } 280 281 return out.join(''); 282 }; 283 284 /* ---- Copy buttons on code blocks ---- */ 285 ns.addCopyButtons = function (container) { 286 var blocks = container.querySelectorAll('pre'); 287 blocks.forEach(function (pre) { 288 if (pre.querySelector('.udx-docs-copy-btn')) return; 289 var btn = document.createElement('button'); 290 btn.className = 'udx-docs-copy-btn'; 291 btn.textContent = 'Copy'; 292 btn.addEventListener('click', function () { 293 var code = pre.querySelector('code'); 294 var text = code ? code.textContent : pre.textContent; 295 navigator.clipboard.writeText(text).then(function () { 296 btn.textContent = 'Copied'; 297 btn.classList.add('copied'); 298 setTimeout(function () { 299 btn.textContent = 'Copy'; 300 btn.classList.remove('copied'); 301 }, 2000); 302 }); 303 }); 304 pre.appendChild(btn); 305 }); 306 }; 307 308 /* ---- Usage tracking ---- */ 309 ns.getUsageCount = function () { 310 try { return parseInt(localStorage.getItem(USAGE_KEY) || '0', 10); } catch (e) { return 0; } 311 }; 312 ns.incrementUsage = function () { 313 try { localStorage.setItem(USAGE_KEY, String(ns.getUsageCount() + 1)); } catch (e) { /* noop */ } 314 }; 315 ns.isOverLimit = function () { 316 return ns.getUsageCount() >= FREE_USES; 317 }; 318 ns.renderUsageGate = function (container) { 319 var remaining = FREE_USES - ns.getUsageCount(); 320 if (remaining > 10) return; 321 var wrap = container.querySelector('.udx-docs-usage-note'); 322 if (wrap) wrap.remove(); 323 var el = document.createElement('div'); 324 el.className = 'udx-docs-usage-note'; 325 if (remaining <= 0) { 326 el.innerHTML = '<div style="text-align:center;padding:24px 16px;color:#19234c;font-size:0.85rem;line-height:1.5;">' + 327 '<p style="font-weight:500;margin:0 0 8px 0;">You\'ve used your free sessions</p>' + 328 '<p style="color:#5d5f68;margin:0 0 16px 0;font-size:0.78rem;">Get unlimited access and direct engineering support.</p>' + 329 '<a href="/contact" style="display:inline-block;padding:10px 24px;background:#204aff;color:#fff;border-radius:8px;text-decoration:none;font-size:0.85rem;font-weight:500;">Talk to us</a>' + 330 '</div>'; 331 } else if (remaining <= 10) { 332 el.innerHTML = '<div style="text-align:center;padding:8px;color:#5d5f68;font-size:0.72rem;">
332' + remaining + ' free questions remaining</div>'; 333 } 334 container.appendChild(el); 335 }; 336 337 /* ---- Session persistence (no expiry -- lives as long as browser allows) ---- */ 338 ns.saveSession = function () { 339 try { 340 var data = { 341 messages: ns.state.messages, 342 activeTopic: ns.state.activeTopic, 343 exploredContent: ns.state.exploredContent, 344 mode: ns.state.mode, 345 ts: Date.now(), 346 }; 347 localStorage.setItem(SESSION_KEY, JSON.stringify(data)); 348 } catch (e) { /* noop */ } 349 }; 350 ns.loadSession = function () { 351 try { 352 var raw = localStorage.getItem(SESSION_KEY); 353 if (!raw) return null; 354 return JSON.parse(raw); 355 } catch (e) { return null; } 356 }; 357 ns.clearSession = function () { 358 try { localStorage.removeItem(SESSION_KEY); } catch (e) { /* noop */ } 359 }; 360 361 /* ---- Cross-bot memory: read homepage chatbot history (PII-stripped) ---- */ 362 function stripPii(text) { 363 return text 364 .replace(/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g, '[email]') 365 .replace(/(\+?1?[\s.-]?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})/g, '[phone]') 366 .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[redacted]'); 367 } 368 ns.getCrossBotContext = function () { 369 try { 370 var keys = ['udx_concierge_v2', 'udx_chat_history', 'udx_chat_messages']; 371 for (var i = 0; i < keys.length; i++) { 372 var raw = localStorage.getItem(keys[i]); 373 if (!raw) continue; 374 var data = JSON.parse(raw); 375 var msgs = Array.isArray(data) ? data : (data.messages || data.history || []); 376 if (!msgs.length) continue; 377 var recent = msgs.slice(-10); 378 var summary = recent.map(function (m) { 379 var role = m.role || (m.isBot ? 'assistant' : 'user'); 380 var text = stripPii((m.content || m.text || m.message || '').substring(0, 150)); 381 return role + ': ' + text; 382 }).join('\n'); 383 return summary; 384 } 385 } catch (e) { /* noop */ } 386 return ''; 387 }; 388 389 /* ---- Chat mode ---- */ 390 ns.switchToChat = function (initialQuery, topic, restoring) { 391 ns.state.mode = 'chat'; 392 if (topic) ns.state.activeTopic = topic; 393 394 var header = ns.panel.querySelector('.udx-docs-header'); 395 if (header) header.style.display = 'none'; 396 397 var body = ns.panel.querySelector('.udx-docs-body'); 398 body.innerHTML = ''; 399 400 var chat = document.createElement('div'); 401 chat.className = 'udx-docs-chat'; 402 403 var topicBadge = ''; 404 var at = ns.state.activeTopic; 405 if (at && at.label) { 406 topicBadge = '<div class="udx-docs-topic-badge"><span class="badge-icon">' + 407 ns.getTopicIcon(at.id) + '</span>' + escHtml(at.label) + '</div>'; 408 } 409 410 chat.innerHTML = [ 411 topicBadge, 412 '<div class="udx-docs-chat-msgs"></div>', 413 '<div class="udx-docs-chat-input-wrap">', 414 ' <input class="udx-docs-chat-input" type="text" placeholder="Ask about ' + 415 escAttr(at && at.label ? at.label.toLowerCase() : 'UDX tools, automation, architecture') + 416 '..." autocomplete="off" />', 417 ' <button class="udx-docs-chat-send">Send</button>', 418 '</div>', 419 ].join('\n'); 420 body.appendChild(chat); 421 422 var input = chat.querySelector('.udx-docs-chat-input'); 423 var sendBtn = chat.querySelector('.udx-docs-chat-send'); 424 425 function send() { 426 var text = input.value.trim(); 427 if (!text || ns.state.asking) return; 428 429 if (text.toLowerCase() === 'clear') { 430 input.value = ''; 431 ns.state.messages = []; 432 ns.state.activeTopic = null; 433 ns.state.exploredContent = []; 434 ns.state.mode = 'search'; 435 ns.clearSession(); 436 if (header) header.style.display = ''; 437 ns.renderHome(); 438 return; 439 } 440 441 input.value = ''; 442 ns.sendMessage(text); 443 } 444 445 sendBtn.addEventListener('click', send); 446 input.addEventListener('keydown', function (e) { 447 if (e.key === 'Enter') { e.preventDefault(); send(); } 448 }); 449 450 if (restoring) { 451 ns.renderMessages(); 452 } else if (initialQuery) { 453 ns.sendMessage(initialQuery); 454 } else if (topic) { 455 var greeting = "I can help you with **" + topic.label + "**"; 456 if (topic.description) greeting += " â " + topic.description; 457 if (topic.explore && topic.explore.length > 0) { 458 greeting += "\n\nExplore the resources below for deeper context, or just ask me a question."; 459 } else { 460 greeting += "\n\nWhat would you like to know?"; 461 } 462 ns.addMessage(greeting, 'assistant'); 463 if (topic.explore && topic.explore.length > 0) { 464 ns.renderExploreSuggestions(topic.explore); 465 } 466 input.focus(); 467 } else { 468 input.focus(); 469 } 470 471 ns.saveSession(); 472 }; 473 474 /* ---- Send a message to the AI ---- */ 475 ns.sendMessage = function (text) { 476 if (ns.isOverLimit()) { 477 var msgs = ns.panel.querySelector('.udx-docs-chat-msgs'); 478 if (msgs) ns.renderUsageGate(msgs); 479 return; 480 } 481 ns.incrementUsage(); 482 483 ns.state.messages.push({ role: 'user', content: text }); 484 ns.renderMessages(); 485 ns.state.asking = true; 486 487 var msgs = ns.panel.querySelector('.udx-docs-chat-msgs'); 488 var typing = document.createElement('div'); 489 typing.className = 'udx-docs-retrieval'; 490 typing.innerHTML = '<span class="spin">' + loaderSvg + '</span><span>Thinking...</span>'; 491 var typingPhases = ['Thinking...', 'Retrieving documentation...', 'Analyzing results...']; 492 var phase = 0; 493 var phaseTimer = setInterval(function () { 494 phase++; 495 if (phase < typingPhases.length) { 496 var span = typing.querySelector('span:last-child'); 497 if (span) span.textContent = typingPhases[phase]; 498 } 499 }, 3000); 500 msgs.appendChild(typing); 501 msgs.scrollTop = msgs.scrollHeight; 502 503 var sendBtn = ns.panel.querySelector('.udx-docs-chat-send'); 504 if (sendBtn) sendBtn.disabled = true; 505 506 var crossBot = (ns.state.messages.length <= 1) ? ns.getCrossBotContext() : ''; 507 var prompt = crossBot 508 ? text + '\n\n[Context from previous conversation on this site:]\n' + crossBot 509 : text; 510 511 var payload = { 512 prompt: prompt, 513 search_query: text, 514 messages: ns.state.messages.map(function (m) { return { role: m.role, content: m.content }; }), 515 }; 516 517 if (ns.state.activeTopic) { 518 payload.topic = ns.state.activeTopic.id; 519 } 520 if (ns.state.exploredContent.length) { 521 payload.explored_content = ns.state.exploredContent; 522 } 523 524 var attempt = 0; 525 var maxRetries = 1; 526 527 function doFetch() { 528 return fetch(ns.askUrl, { 529 method: 'POST', 530 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': ns.nonce }, 531 body: JSON.stringify(payload), 532 }) 533 .then(function (r) { 534 if (r.status >= 500 && attempt < maxRetries) { 535 attempt++; 536 var span = typing.querySelector('span:last-child'); 537 if (span) span.textContent = 'Retrying...'; 538 return new Promise(function (resolve) { 539 setTimeout(resolve, 2000 * attempt); 540 }).then(doFetch); 541 } 542 if (r.status === 429) { 543 return { ok: false, message: 'Too many requests. Please wait a moment and try again.', rate_limited: true }; 544 } 545 return r.json(); 546 }) 547 .then(function (data) { 548 if (data && data.rate_limited) return data; 549 return data; 550 }); 551 } 552 553 doFetch() 554 .then(function (data) { 555 ns.state.asking = false;
556 clearInterval(phaseTimer); 557 if (sendBtn) sendBtn.disabled = false; 558 if (typing.parentNode) typing.remove(); 559 560 var reply = (data.ok ? data.response : (data.message || 'Something went wrong.')) || ''; 561 562 if (data.topic && !ns.state.activeTopic) { 563 var found = (ns.state.topics || []).find(function (t) { return t.id === data.topic; }); 564 ns.state.activeTopic = found || { id: data.topic, label: data.topic }; 565 } 566 567 if (data.fetched_urls && data.fetched_urls.length) { 568 reply += '\n\n*Sources consulted: ' + data.fetched_urls.map(function (u) { 569 return '[' + u.replace(/^https?:\/\/(www\.)?/, '').substring(0, 50) + '](' + u + ')'; 570 }).join(', ') + '*'; 571 } 572 ns.state.messages.push({ role: 'assistant', content: reply }); 573 ns.renderMessages(); 574 575 if (data.suggest_explore && data.suggest_explore.length) { 576 ns.renderExploreSuggestions(data.suggest_explore); 577 } 578 579 ns.saveSession(); 580 ns.renderUsageGate(msgs); 581 582 var chatInput = ns.panel.querySelector('.udx-docs-chat-input'); 583 if (chatInput) chatInput.focus(); 584 }) 585 .catch(function (err) { 586 if (attempt < maxRetries) { 587 attempt++; 588 var span = typing.querySelector('span:last-child'); 589 if (span) span.textContent = 'Retrying...'; 590 setTimeout(function () { 591 doFetch().then(function (data) { 592 ns.state.asking = false; 593 clearInterval(phaseTimer); 594 if (sendBtn) sendBtn.disabled = false; 595 if (typing.parentNode) typing.remove(); 596 var reply = (data && data.ok ? data.response : (data && data.message || 'Something went wrong.')) || ''; 597 if (data && data.topic && !ns.state.activeTopic) { 598 var found = (ns.state.topics || []).find(function (t) { return t.id === data.topic; }); 599 ns.state.activeTopic = found || { id: data.topic, label: data.topic }; 600 } 601 if (data && data.fetched_urls && data.fetched_urls.length) { 602 reply += '\n\n*Sources consulted: ' + data.fetched_urls.map(function (u) { 603 return '[' + u.replace(/^https?:\/\/(www\.)?/, '').substring(0, 50) + '](' + u + ')'; 604 }).join(', ') + '*'; 605 } 606 ns.state.messages.push({ role: 'assistant', content: reply }); 607 ns.renderMessages(); 608 ns.saveSession(); 609 }).catch(function () { 610 ns.state.asking = false; 611 clearInterval(phaseTimer); 612 if (sendBtn) sendBtn.disabled = false; 613 if (typing.parentNode) typing.remove(); 614 ns.state.messages.push({ role: 'assistant', content: 'Service temporarily unavailable. Please try again in a moment.' }); 615 ns.renderMessages(); 616 }); 617 }, 3000); 618 return; 619 } 620 ns.state.asking = false; 621 clearInterval(phaseTimer); 622 if (sendBtn) sendBtn.disabled = false; 623 if (typing.parentNode) typing.remove(); 624 var errMsg = err && err.message && err.message.indexOf('timeout') !== -1 625 ? 'Request timed out. The AI is processing a complex query. Please try again.' 626 : 'Service temporarily unavailable. Please try again in a moment.'; 627 ns.state.messages.push({ role: 'assistant', content: errMsg }); 628 ns.renderMessages(); 629 }); 630 }; 631 632 /* ---- Revert to topic picker when chat is empty ---- */ 633 ns.revertToTopics = function () { 634 ns.state.activeTopic = null; 635 ns.state.exploredContent = []; 636 ns.state.mode = 'search'; 637 ns.clearSession(); 638 var header = ns.panel.querySelector('.udx-docs-header'); 639 if (header) header.style.display = ''; 640 ns.renderHome(); 641 }; 642 643 /* ---- Delete individual message (with 7s undo) ---- */ 644 ns.deleteMsg = function (msg, msgEl) { 645 var idx = ns.state.messages.indexOf(msg); 646 if (idx < 0) return; 647 648 ns.state.messages.splice(idx, 1); 649 ns.saveSession(); 650 651 if (!msgEl) { 652 if (!ns.state.messages.length) { ns.revertToTopics(); return; } 653 ns.renderMessages(); 654 return; 655 } 656 657 var undo = document.createElement('div'); 658 undo.className = 'udx-docs-undo'; 659 undo.innerHTML = 'Message deleted. <button class="udx-docs-undo-btn">Undo</button>'; 660 msgEl.parentNode.replaceChild(undo, msgEl); 661 662 var timer = setTimeout(function () { 663 if (undo.parentNode) { 664 undo.style.opacity = '0'; 665 setTimeout(function () { 666 if (undo.parentNode) undo.parentNode.removeChild(undo); 667 if (!ns.state.messages.length) ns.revertToTopics(); 668 }, 300); 669 } 670 }, 7000); 671 672 undo.querySelector('.udx-docs-undo-btn').addEventListener('click', function () { 673 clearTimeout(timer); 674 ns.state.messages.splice(idx, 0, msg); 675 ns.saveSession(); 676 ns.renderMessages(); 677 }); 678 }; 679 680 /* ---- Render all messages ---- */ 681 ns.renderMessages = function () { 682 var msgs = ns.panel.querySelector('.udx-docs-chat-msgs'); 683 if (!msgs) return; 684 685 msgs.innerHTML = ''; 686
687 ns.state.messages.forEach(function (m) { 688 var div = document.createElement('div'); 689 div.className = 'udx-docs-msg udx-docs-msg-' + (m.role === 'user' ? 'user' : 'bot'); 690 var del = document.createElement('button'); 691 del.className = 'udx-docs-msg-del'; 692 del.title = 'Delete'; 693 del.innerHTML = '×'; 694 del.addEventListener('click', function (e) { e.stopPropagation(); ns.deleteMsg(m, div); }); 695 div.appendChild(del); 696 var bubble = document.createElement('div'); 697 bubble.className = 'msg-bubble'; 698 if (m.role === 'user') { 699 bubble.textContent = m.content; 700 } else { 701 bubble.innerHTML = ns.md(m.content); 702 } 703 div.appendChild(bubble); 704 msgs.appendChild(div); 705 }); 706 707 msgs.scrollTop = msgs.scrollHeight; 708 ns.addCopyButtons(msgs); 709 ns.renderMermaid(msgs); 710 }; 711 712 /* ---- Add a single message to the chat ---- */ 713 ns.addMessage = function (text, role) { 714 ns.state.messages.push({ role: role || 'assistant', content: text }); 715 ns.renderMessages(); 716 ns.saveSession(); 717 }; 718 719 /* ---- Explore: fetch page content for deeper context ---- */ 720 ns.renderExploreSuggestions = function (urls) { 721 var msgs = ns.panel.querySelector('.udx-docs-chat-msgs'); 722 if (!msgs) return; 723 724 var wrap = document.createElement('div'); 725 wrap.className = 'udx-docs-explore-wrap'; 726 wrap.innerHTML = '<p>Explore related docs for deeper context:</p>'; 727 728 urls.forEach(function (url) { 729 var alreadyExplored = ns.state.exploredContent.some(function (ec) { return ec.url === url; }); 730 if (alreadyExplored) return; 731 732 var btn = document.createElement('button'); 733 btn.className = 'udx-docs-explore-btn'; 734 btn.textContent = url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, ''); 735 btn.addEventListener('click', function () { 736 ns.exploreUrl(url, btn); 737 }); 738 wrap.appendChild(btn); 739 }); 740 741 if (wrap.querySelectorAll('.udx-docs-explore-btn').length > 0) { 742 msgs.appendChild(wrap); 743 msgs.scrollTop = msgs.scrollHeight; 744 } 745 }; 746 747 ns.exploreUrl = function (url, btn) { 748 if (btn) { 749 btn.classList.add('loading'); 750 btn.textContent = 'Loading...'; 751 } 752 753 fetch(ns.exploreEndpoint, { 754 method: 'POST', 755 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': ns.nonce }, 756 body: JSON.stringify({ url: url }), 757 }) 758 .then(function (r) { return r.json(); }) 759 .then(function (data) { 760 if (data.ok && data.content) { 761 ns.state.exploredContent.push({ url: data.url, content: data.content }); 762 if (btn) { 763 btn.textContent = '\u2713 Loaded'; 764 btn.style.color = '#2a7d4f'; 765 btn.style.cursor = 'default'; 766 btn.classList.remove('loading'); 767 btn.disabled = true; 768 } 769 ns.saveSession(); 770 771 var shortUrl = url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, ''); 772 if (shortUrl.length > 40) shortUrl = shortUrl.substring(0, 40) + '...'; 773 var input = ns.panel.querySelector('.udx-docs-chat-input'); 774 if (input && !ns.state.asking) { 775 input.placeholder = 'Ask about ' + shortUrl; 776 input.focus(); 777 } 778 ns.addMessage("Loaded content from **" + shortUrl + "**. You can now ask questions about it.", 'assistant'); 779 if (ns.state.activeTopic && ns.state.activeTopic.explore) { 780 ns.renderExploreSuggestions(ns.state.activeTopic.explore); 781 } 782 } else { 783 if (btn) { 784 btn.textContent = 'Failed'; 785 btn.classList.remove('loading'); 786 } 787 } 788 }) 789 .catch(function () { 790 if (btn) { 791 btn.textContent = 'Error'; 792 btn.classList.remove('loading'); 793 } 794 }); 795 }; 796 797})(window.UDXDocs);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.