1/** 2 * FAQ Widget Loader Script 3 * This script can be embedded on any website to display FAQ content 4 * Usage: 5 * <div id="faq-widget-container"></div> 6 * <script src="https://www.trybased.com/faq-widget.js" data-workspace-id="WORKSPACE_ID" data-container="#faq-widget-container"></script> 7 */ 8 9(function () { 10 'use strict'; 11 12 // Get configuration from script tag 13 // Try multiple methods to find the script tag (for compatibility with different embed environments) 14 let scriptTag = document.currentScript; 15 16 // Fallback 1: Find by data attributes 17 if (!scriptTag) { 18 scriptTag = document.querySelector('script[data-workspace-id], script[data-link-id]'); 19 } 20 21 // Fallback 2: Find by src containing faq-widget.js 22 if (!scriptTag) { 23 const scripts = document.querySelectorAll('script[src*="faq-widget.js"]'); 24 if (scripts.length > 0) { 25 // Find the one with data attributes, or use the last one (most likely the one that just loaded) 26 scriptTag = Array.from(scripts).find(s => 27 s.hasAttribute('data-workspace-id') || s.hasAttribute('data-link-id') 28 ) || scripts[scripts.length - 1]; 29 } 30 } 31 32 // Fallback 3: Find any script with faq-widget in src and check all scripts 33 if (!scriptTag) { 34 const allScripts = document.querySelectorAll('script[src]'); 35 for (let i = allScripts.length - 1; i >= 0; i--) { 36 const script = allScripts[i]; 37 if (script.src && script.src.includes('faq-widget.js')) { 38 if (script.hasAttribute('data-workspace-id') || script.hasAttribute('data-link-id')) { 39 scriptTag = script; 40 break; 41 } 42 } 43 } 44 } 45 46 if (!scriptTag) { 47 console.error('FAQ Widget: Script tag not found. Make sure the script tag includes data-link-id or data-workspace-id attributes.'); 48 return; 49 } 50 51 // Get container selector first (needed for fallback) 52 const containerSelector = (scriptTag.getAttribute('data-container') || '#faq-widget-container').startsWith('#') || (scriptTag.getAttribute('data-container') || '#faq-widget-container').startsWith('.') 53 ? (scriptTag.getAttribute('data-container') || '#faq-widget-container') 54 : '#' + (scriptTag.getAttribute('data-container') || 'faq-widget-container'); 55 56 // Find container element early (needed for fallback config reading) 57 let container = document.querySelector(containerSelector); 58 59 // If container not found, try default 60 if (!container) { 61 container = document.querySelector('#faq-widget-container'); 62 } 63 64 // Build config from script tag 65 let config = { 66 workspaceId: scriptTag.getAttribute('data-workspace-id'), 67 linkId: scriptTag.getAttribute('data-link-id'), 68 container: scriptTag.getAttribute('data-container') || '#faq-widget-container', 69 theme: scriptTag.getAttribute('data-theme') || 'auto', 70 displayMode: (scriptTag.getAttribute('data-display-mode') || 'full').toLowerCase(), 71 baseUrl: scriptTag.getAttribute('data-base-url') || 'https://aiknowledgebase-7427e.web.app' 72 }; 73 74 // Debug logging for display mode 75 console.log('FAQ Widget: Display mode:', config.displayMode); 76 77 // Fallback: If script tag doesn't have attributes, try reading from container element's data attributes 78 // This is especially important for Bubble.io which may strip script tag attributes 79 if (container) { 80 if (!config.workspaceId) { 81 config.workspaceId = container.getAttribute('data-workspace-id'); 82 } 83 if (!config.linkId) { 84 config.linkId = container.getAttribute('data-link-id'); 85 } 86 if (container.getAttribute('data-container')) { 87 config.container = container.getAttribute('data-container'); 88 } 89 if (container.getAttribute('data-theme')) { 90 config.theme = container.getAttribute('data-theme'); 91 } 92 // Always check container for display-mode (Bubble.io may strip script attributes) 93 if (container.getAttribute('data-display-mode')) { 94 config.displayMode = container.getAttribute('data-display-mode').toLowerCase(); 95 } 96 } 97 98 // Fallback: Try reading from a separate config script tag 99 if (!config.workspaceId && !config.linkId) { 100 const configScript = document.querySelector('script[type="application/json"][data-faq-widget-config]'); 101 if (configScript) { 102 try { 103 const configData = JSON.parse(configScript.textContent); 104 config.workspaceId = configData.workspaceId || config.workspaceId; 105 config.linkId = configData.linkId || config.linkId; 106 config.container = configData.container || config.container; 107 config.theme = configData.theme || config.theme; 108 config.displayMode = config.displayMode || config.displayMode; 109 config.baseUrl = configData.baseUrl || config.baseUrl; 110 } catch (e) { 111 console.warn('FAQ Widget: Failed to parse config script', e); 112 } 113 } 114 } 115 116 // Fallback: Try reading from global variable 117 if (!config.workspaceId && !config.linkId && window.FAQWidgetConfig) { 118 config.workspaceId = window.FAQWidgetConfig.workspaceId || config.workspaceId; 119 config.linkId = window.FAQWidgetConfig.linkId || config.linkId; 120 config.container = window.FAQWidgetConfig.container || config.container; 121 config.theme = window.FAQWidgetConfig.theme || config.theme; 122 config.displayMode = (window.FAQWidgetConfig?.displayMode || config.displayMode).toLowerCase(); 123 config.baseUrl = window.FAQWidgetConfig.baseUrl || config.baseUrl; 124 } 125 126 // Debug logging 127 if (!config.workspaceId && !config.linkId) { 128 console.error('FAQ Widget: workspaceId or linkId is required'); 129 console.error('FAQ Widget: Found script tag:', scriptTag); 130 console.error('FAQ Widget: Script tag attributes:', { 131 'data-workspace-id': scriptTag.getAttribute('data-workspace-id'), 132 'data-link-id': scriptTag.getAttribute('data-link-id'), 133 'data-container': scriptTag.getAttribute('data-container'), 134 src: scriptTag.src 135 }); 136 if (container) { 137 console.error('FAQ Widget: Container element:', container); 138 console.error('FAQ Widget: Container attributes:', { 139 'data-workspace-id': container.getAttribute('data-workspace-id'), 140 'data-link-id': container.getAttribute('data-link-id'), 141 'data-container': container.getAttribute('data-container') 142 }); 143 } 144 console.error('FAQ Widget: All script tags with faq-widget:', 145 Array.from(document.querySelectorAll('script[src*="faq-widget"]')).map(s => ({ 146 src: s.src, 147 attributes: Array.from(s.attributes).map(a => `${a.name}="${a.value}"`).join(' ') 148 })) 149 ); 150 console.error('FAQ Widget: Please add data-link-id or data-workspace-id to either:'); 151 console.error(' 1. The script tag: <script ... data-link-id="YOUR_ID"></script>'); 152 console.error(' 2. The container div: <div id="faq-widget-container" data-link-id="YOUR_ID"></div>'); 153 console.error(' 3. A config script: <script type="application/json" data-faq-widget-config>{"linkId":"YOUR_ID"}</script>'); 154 return; 155 } 156 157 // Ensure container exists 158 if (!container) { 159 console.error(`FAQ Widget: Container not found: ${containerSelector}`); 160 return; 161 } 162 163 // Inject CSS if not already present 164 if (!document.getElementById('faq-widget-styles')) { 165 const style = document.createElement('style'); 166 style.id = 'faq-widget-styles'; 167 style.textContent = ` 168 .faq-widget-loading, .faq-widget-error { 169 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 170 } 171 @keyframes spin { 172 from { transform: rotate(0deg); } 173 to { transform: rotate(360deg); } 174 } 175 .faq-widget-full, .faq-widget-compact { 176 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 177 } 178 @keyframes slideDown { 179 from { 180 opacity: 0; 181 max-height: 0; 182 } 183 to { 184 opacity: 1; 185 max-height: 500px; 186 } 187 } 188 `; 189 document.head.appendChild(style); 190 } 191 192 // Show loading state 193 container.innerHTML = ` 194 <div class="faq-widget-loading" style="padding: 2rem; text-align: center;"> 195 <div style="width: 2rem; height: 2rem; border: 3px solid #e5e7eb; border-top-color: #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div> 196 <p style="margin-top: 1rem; color: #6b7280;">Loading FAQs...</p> 197 </div> 198 `; 199 200 // Load React and ReactDOM from CDN 201 const loadReact = () => { 202 return new Promise((resolve, reject) => { 203 if (window.React && window.ReactDOM) { 204 resolve(); 205 return; 206 } 207 208 const reactScript = document.createElement('script'); 209 reactScript.src = 'https://unpkg.com/react@18/umd/react.production.min.js'; 210 reactScript.crossOrigin = 'anonymous'; 211 reactScript.onload = () => { 212 const reactDOMScript = document.createElement('script'); 213 reactDOMScript.src = 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js'; 214 reactDOMScript.crossOrigin = 'anonymous'; 215 reactDOMScript.onload = resolve; 216 reactDOMScript.onerror = reject; 217 document.head.appendChild(reactDOMScript); 218 }; 219 reactScript.onerror = reject; 220 document.head.appendChild(reactScript); 221 }); 222 }; 223 224 // Load the widget component 225 const loadWidget = async () => { 226 try { 227 // Load React 228 await loadReact(); 229 230 // Load the widget bundle (we'll need to build this separately) 231 // For now, we'll use a simple fetch-based approach 232 const widgetModule = await import(`${config.baseUrl}/faq-widget-bundle.js`); 233 234 // Render widget 235 const { render } = window.ReactDOM; 236 const { default: FAQWidgetEmbed } = widgetModule; 237 238 render( 239 window.React.createElement(FAQWidgetEmbed, { 240 workspaceId: config.workspaceId, 241 linkId: config.linkId, 242 containerId: config.container.replace('#', ''), 243 theme: config.theme, 244 displayMode: config.displayMode 245 }), 246 container 247 ); 248 249 } catch (error) { 250 console.error('FAQ Widget: Error loading widget', error); 251 container.innerHTML = ` 252 <div class="faq-widget-error" style="padding: 2rem; text-align: center; color: #dc2626;"> 253 <p>Error loading FAQ widget. Please check your configuration.</p> 254 </div> 255 `; 256 } 257 }; 258 259 // Alternative: Simple fetch-based widget (no React dependency) 260 const loadSimpleWidget = async () => { 261 try { 262 const params = new URLSearchParams(); 263 if (config.workspaceId) params.append('workspaceId', config.workspaceId); 264 if (config.linkId) params.append('linkId', config.linkId); 265 266 const apiUrl = `https://us-central1-aiknowledgebase-7427e.cloudfunctions.net/getFAQData?${params.toString()}`; 267 console.log('FAQ Widget: Fetching FAQ data from:', apiUrl); 268 269 const response = await fetch(apiUrl, { 270 method: 'GET', 271 headers: { 272 'Accept': 'application/json' 273 } 274 }); 275 276 if (!response.ok) { 277 const errorText = await response.text(); 278 console.error('FAQ Widget: API error:', response.status, errorText); 279 throw new Error(`HTTP ${response.status}: ${errorText}`); 280 } 281 282 const data = await response.json(); 283 console.log('FAQ Widget: Received data:', { 284 faqsCount: data.faqs?.length || 0, 285 hasSchema: !!data.schema, 286 organization: data.organization?.name 287 }); 288 289 let faqs = data.faqs || []; 290 const organization = data.organization || { name: 'Knowledge Base', logoUrl: null }; 291 292 // Filter out FAQs with unhelpful answers or no answers 293 const unhelpfulAnswerPatterns = [ 294 /does not provide information/i,
295 /not available/i, 296 /couldn't find/i, 297 /no information/i, 298 /unable to/i, 299 /cannot find/i, 300 /no relevant/i, 301 /context does not/i 302 ]; 303 304 faqs = faqs.filter(faq => { 305 // Must have an answer 306 if (!faq.answer || faq.answer.trim() === '') { 307 return false; 308 } 309 310 // Check if answer is unhelpful 311 const answerLower = faq.answer.toLowerCase(); 312 const isUnhelpful = unhelpfulAnswerPatterns.some(pattern => 313 pattern.test(faq.answer) 314 ); 315 316 // Also check for generic "not available" messages 317 const isGenericUnavailable = answerLower.includes('answer not available') || 318 answerLower.includes('not available in the knowledge base'); 319 320 return !isUnhelpful && !isGenericUnavailable; 321 }); 322 323 console.log(`FAQ Widget: Filtered ${data.faqs?.length || 0} FAQs down to ${faqs.length} with helpful answers`); 324 325 if (faqs.length === 0) { 326 console.warn('FAQ Widget: No FAQs with helpful answers found.'); 327 } 328 329 // Store FAQs with answers for accordion functionality 330 window.faqWidgetData = { 331 faqs: faqs, 332 organization: organization, 333 config: config 334 }; 335 336 // Inject schema 337 if (data.schema) { 338 const schemaScript = document.createElement('script'); 339 schemaScript.type = 'application/ld+json'; 340 schemaScript.id = 'faq-widget-schema'; 341 schemaScript.textContent = JSON.stringify(data.schema); 342 document.head.appendChild(schemaScript); 343 } 344 345 // Render FAQs 346 if (config.displayMode === 'compact') { 347 if (faqs.length === 0) { 348 container.innerHTML = ` 349 <div class="faq-widget-compact"> 350 <p style="color: #6b7280; text-align: center; padding: 2rem;">No FAQs available yet. Please add documents with question headings to your knowledge base.</p> 351 </div> 352 `; 353 } else { 354 container.innerHTML = ` 355 <div class="faq-widget-compact"> 356 <div style="display: grid; gap: 0.75rem;"> 357 ${faqs.map((faq, index) => { 358 const faqId = `faq-compact-${index}`; 359 // All FAQs at this point have valid answers (already filtered) 360 const hasAnswer = true; 361 return ` 362 <div 363 id="${faqId}-card" 364 data-default-border="#e5e7eb" 365 data-expanded-border="#bfdbfe" 366 data-is-expanded="false" 367 style="background: white; border: 1px solid #e5e7eb; border-radius: 0.5rem; overflow: hidden; transition: all 0.2s;" 368 onmouseenter="(function(card){ card.setAttribute('data-is-hovered', 'true'); card.style.borderColor = '#3b82f6'; card.style.boxShadow='0 1px 3px rgba(0,0,0,0.1)'; })(this)" 369 onmouseleave="(function(card){ card.setAttribute('data-is-hovered', 'false'); card.style.borderColor = '#e5e7eb'; card.style.boxShadow='none'; })(this)" 370 > 371 <button 372 onclick="event.stopPropagation(); window.faqWidgetToggle && window.faqWidgetToggle('${faqId}', '${faq.question.replace(/'/g, "\\'")}', event)" 373 style="width: 100%; display: flex; align-items: start; gap: 0.75rem; padding: 0.75rem; background: none; border: none; text-align: left; cursor: pointer;" 374 >
375 <span style="font-size: 0.875rem; font-weight: 500; color: #111827; flex: 1;">${faq.question}</span> 376 ${hasAnswer ? ` 377 <svg id="${faqId}-icon" style="width: 1rem; height: 1rem; color: #6b7280; flex-shrink: 0; transition: transform 0.2s;" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 378 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> 379 </svg> 380 ` : ''} 381 </button> 382 ${hasAnswer ? ` 383 <div id="${faqId}-answer" style="display: none; padding: 0 0.75rem 0.75rem; color: #374151; line-height: 1.6; font-size: 0.8125rem; border-top: 1px solid #e5e7eb; margin-top: 0;"> 384 <p style="margin: 0.75rem 0 0 0;">${faq.answer}</p> 385 </div> 386 ` : ''} 387 </div> 388 `; 389 }).join('')} 390 </div> 391 </div> 392 `; 393 } 394 } else { 395 container.innerHTML = ` 396 <div class="faq-widget-full" style="min-height: 100vh; background: linear-gradient(to bottom right, #eff6ff, #ffffff, #faf5ff);"> 397 <header style="background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(4px); border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; z-index: 40; padding: 1rem 1.5rem;"> 398 <div style="display: flex; align-items: center; gap: 0.75rem;"> 399 ${organization.logoUrl ? ` 400 <img src="${organization.logoUrl}" alt="${organization.name}" style="height: 2.5rem; max-width: 200px; object-fit: contain;" /> 401 ` : ` 402 <div style="height: 2.5rem; width: 2.5rem; background: #3b82f6; color: white; border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; font-weight: bold;"> 403 ${organization.name.charAt(0).toUpperCase()} 404 </div> 405 `} 406 <span style="font-size: 1.125rem; font-weight: 600; color: #111827;">${organization.name}</span> 407 </div> 408 </header> 409 <div style="max-width: 64rem; margin: 0 auto; padding: 4rem 1.5rem 3rem;"> 410 <div style="text-align: center; margin-bottom: 3rem;"> 411 <h1 style="font-size: 2.25rem; font-weight: bold; color: #111827; margin-bottom: 1rem;">How can we help you today?</h1> 412 <p style="font-size: 1.25rem; color: #4b5563; max-width: 32rem; margin: 0 auto;">Search our knowledge base or browse frequently asked questions</p> 413 </div> 414 <div> 415 <h2 style="font-size: 1.5rem; font-weight: bold; color: #111827; margin-bottom: 1.5rem; text-align: center;">Frequently Asked Questions</h2> 416 ${faqs.length === 0 ? ` 417 <div style="text-align: center; padding: 3rem; background: white; border-radius: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);"> 418 <p style="font-size: 1.125rem; color: #6b7280; margin-bottom: 0.5rem;">No FAQs available yet</p> 419 <p style="font-size: 0.875rem; color: #9ca3af;">Add documents with question headings (containing "?" or starting with question words) to your knowledge base to generate FAQs.<
419/p> 420 </div> 421 ` : ` 422 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;"> 423 ${faqs.map((faq, index) => { 424 const faqId = `faq-${index}`; 425 // All FAQs at this point have valid answers (already filtered) 426 const hasAnswer = true; 427 return ` 428 <div 429 id="${faqId}-card" 430 style="background: white; border-radius: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); border: 2px solid transparent; transition: all 0.2s; overflow: hidden;" 431 onmouseenter="this.style.borderColor='#bfdbfe'; this.style.boxShadow='0 20px 25px -5px rgba(0,0,0,0.1)'; this.style.transform='translateY(-4px)'" 432 onmouseleave="this.style.borderColor='transparent'; this.style.boxShadow='0 4px 6px -1px rgba(0,0,0,0.1)'; this.style.transform='translateY(0)'" 433 > 434 <button 435 onclick="event.stopPropagation(); window.faqWidgetToggle && window.faqWidgetToggle('${faqId}', '${faq.question.replace(/'/g, "\\'")}', event)" 436 style="width: 100%; padding: 1.5rem; text-align: left; cursor: pointer; background: none; border: none; display: flex; justify-content: space-between; align-items: center;" 437 > 438 <h3 style="font-size: 0.875rem; font-weight: 600; color: #111827; margin: 0; flex: 1;">${faq.question}</h3> 439 ${hasAnswer ? ` 440 <svg id="${faqId}-icon" style="width: 1.25rem; height: 1.25rem; color: #6b7280; flex-shrink: 0; margin-left: 0.75rem; transition: transform 0.2s;" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 441 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> 442 </svg> 443 ` : ''} 444 </button> 445 ${hasAnswer ? ` 446 <div id="${faqId}-answer" style="display: none; padding: 0 1.5rem 1.5rem; color: #374151; line-height: 1.75; font-size: 0.875rem; border-top: 1px solid #e5e7eb; margin-top: 0; animation: slideDown 0.3s ease-out;"> 447 <p style="margin: 1rem 0 0 0;">${faq.answer}</p> 448 </div> 449 ` : ''} 450 </div> 451 `; 452 }).join('')} 453 </div> 454 `} 455 </div> 456 </div> 457 </div> 458 `; 459 } 460 461 // Toggle FAQ answer display (accordion functionality) 462 window.faqWidgetToggle = (faqId, question, event) => { 463 // Prevent event bubbling 464 if (event) { 465 event.stopPropagation(); 466 event.preventDefault(); 467 } 468 469 const answerDiv = document.getElementById(`${faqId}-answer`); 470 const icon = document.getElementById(`${faqId}-icon`); 471 const card = document.getElementById(`${faqId}-card`); 472 473 if (!answerDiv) { 474 console.warn(`FAQ Widget: Could not find answer div for ${faqId}`); 475 return; 476 } 477 478 // Find the parent container (either faq-widget-compact or faq-widget-full) 479 const widgetContainer = answerDiv.closest('.faq-widget-compact') || answerDiv.closest('.faq-widget-full') || container; 480 481 const isExpanded = answerDiv.style.display !== 'none' && answerDiv.style.display !== ''; 482 483 // Close all other FAQs first (accordion behavior - only one open at a time) 484 // Only close FAQs within the same widget container 485 if (!isExpanded) { 486 // Find all FAQ answer divs within the same container and close them 487 const allAnswerDivs = widgetContainer.querySelectorAll('[id$="-answer"]'); 488 const allIcons = widgetContainer.querySelectorAll('[id$="-icon"]'); 489 const allCards = widgetContainer.querySelectorAll('[id$="-card"]'); 490
491 allAnswerDivs.forEach(div => { 492 if (div.id !== `${faqId}-answer` && div.id) { 493 div.style.display = 'none'; 494 } 495 }); 496 497 allIcons.forEach(iconEl => { 498 if (iconEl.id !== `${faqId}-icon` && iconEl.id) { 499 iconEl.style.transform = 'rotate(0deg)'; 500 } 501 }); 502 503 allCards.forEach(cardEl => { 504 if (cardEl.id !== `${faqId}-card` && cardEl.id) { 505 cardEl.setAttribute('data-is-expanded', 'false'); 506 // Only reset border if not currently being hovered 507 const isHovered = cardEl.getAttribute('data-is-hovered') === 'true'; 508 if (!isHovered) { 509 cardEl.style.borderColor = '#e5e7eb'; 510 cardEl.style.boxShadow = 'none'; 511 } 512 } 513 }); 514 } 515 516 if (isExpanded) { 517 // Collapse 518 answerDiv.style.display = 'none'; 519 if (icon) icon.style.transform = 'rotate(0deg)'; 520 if (card) { 521 card.setAttribute('data-is-expanded', 'false'); 522 // Only reset border if not currently being hovered 523 const isHovered = card.getAttribute('data-is-hovered') === 'true'; 524 if (!isHovered) { 525 card.style.borderColor = '#e5e7eb'; 526 card.style.boxShadow = 'none'; 527 } 528 } 529 } else { 530 // Expand 531 answerDiv.style.display = 'block'; 532 if (icon) icon.style.transform = 'rotate(180deg)'; 533 if (card) { 534 card.setAttribute('data-is-expanded', 'true'); 535 // Only reset border if not currently being hovered 536 const isHovered = card.getAttribute('data-is-hovered') === 'true'; 537 if (!isHovered) { 538 card.style.borderColor = '#e5e7eb'; 539 card.style.boxShadow = 'none'; 540 } 541 } 542 543 // Update URL for SEO 544 const newUrl = new URL(window.location); 545 newUrl.searchParams.set('faq_q', encodeURIComponent(question)); 546 window.history.pushState({ question, type: 'faq', faqId }, '', newUrl); 547 548 // Update page title for SEO 549 const originalTitle = document.title; 550 if (!originalTitle.includes(question)) { 551 document.title = `${question} - ${originalTitle}`; 552 } 553 554 // Scroll the expanded FAQ into view 555 setTimeout(() => { 556 if (card) { 557 card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 558 } 559 }, 100); 560 } 561 }; 562 563 // Set up search function (for URL-based navigation) 564 window.faqWidgetSearch = (question) => { 565 // Find the FAQ by question and toggle it 566 if (window.faqWidgetData && window.faqWidgetData.faqs) { 567 const faqIndex = window.faqWidgetData.faqs.findIndex(f => f.question === question); 568 if (faqIndex >= 0) { 569 const faqId = `faq-${faqIndex}`; 570 window.faqWidgetToggle(faqId, question); 571 } 572 } 573 }; 574 575 // Handle initial URL parameter 576 const urlParams = new URLSearchParams(window.location.search); 577 const initialQuestion = urlParams.get('faq_q'); 578 if (initialQuestion) { 579 setTimeout(() => { 580 window.faqWidgetSearch(decodeURIComponent(initialQuestion)); 581 }, 500); 582 } 583 584 } catch (error) { 585 console.error('FAQ Widget: Error loading widget', error); 586 console.error('FAQ Widget: Error stack:', error.stack); 587 container.innerHTML = ` 588 <div class="faq-widget-error" style="padding: 2rem; text-align: center; color: #dc2626;"> 589 <p style="font-weight: 600; margin-bottom: 0.5rem;">Error loading FAQ widget</p> 590 <p style="font-size: 0.875rem; color: #9ca3af;">${error.message}</p> 591 <p style="font-size: 0.75rem; color: #9ca3af; margin-top: 1rem;">Check the browser console for more details.</p> 592 </div> 593 `; 594 } 595 }; 596 597 // Try to load React-based widget first, fallback to simple widget 598 if (config.displayMode === 'full' && window.React) { 599 loadWidget().catch(() => loadSimpleWidget()); 600 } else { 601 loadSimpleWidget(); 602 } 603 604 // Handle URL updates for SEO 605 if (config.displayMode === 'full') { 606 window.addEventListener('popstate', (event) => { 607 if (event.state && event.state.question) { 608 // Handle back/forward navigation 609 const question = event.state.question; 610 if (window.faqWidgetSearch) { 611 window.faqWidgetSearch(question); 612 } 613 } 614 }); 615 } 616})();
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.