This is page 4 of 5. Use http://codebase.md/Xyborg/ChatGPT-Product-Info?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── assets │ ├── chatgpt-product-info-reviews.png │ └── chatgpt-product-info.png ├── chatgpt-product-info.js ├── chrome-extension │ ├── assets │ │ ├── flags │ │ │ ├── all.svg │ │ │ ├── ar.svg │ │ │ ├── at.svg │ │ │ ├── be.svg │ │ │ ├── br.svg │ │ │ ├── ch.svg │ │ │ ├── de.svg │ │ │ ├── es.svg │ │ │ ├── fr.svg │ │ │ ├── gb.svg │ │ │ ├── it.svg │ │ │ ├── mx.svg │ │ │ ├── nl.svg │ │ │ ├── pt.svg │ │ │ ├── se.svg │ │ │ └── us.svg │ │ └── icons-ui │ │ ├── analysis.svg │ │ ├── check.svg │ │ ├── database.svg │ │ ├── down-arrow.svg │ │ ├── edit.svg │ │ ├── error.svg │ │ ├── github.svg │ │ ├── history.svg │ │ ├── linkedin.svg │ │ ├── negative.svg │ │ ├── neutral.svg │ │ ├── positive.svg │ │ ├── project.svg │ │ ├── search.svg │ │ ├── settings.svg │ │ ├── tag.svg │ │ ├── up-arrow.svg │ │ ├── warning.svg │ │ └── x.svg │ ├── content-script.js │ ├── icons │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon19.png │ │ ├── icon32.png │ │ ├── icon48.png │ │ └── logobubble.svg │ ├── manifest.json │ ├── popup.html │ ├── popup.js │ ├── README.md │ └── styles.css ├── LICENSE └── README.md ``` # Files -------------------------------------------------------------------------------- /chatgpt-product-info.js: -------------------------------------------------------------------------------- ```javascript 1 | // ChatGPT Product Info Search 2 | // Paste this entire script into ChatGPT's browser console and it will create a floating button 3 | 4 | (function() { 5 | // Remove existing modal and button if present 6 | const existingModal = document.getElementById('chatgpt-product-search-modal'); 7 | const existingButton = document.getElementById('openProductSearchModalBtn'); 8 | if (existingModal) { 9 | existingModal.remove(); 10 | } 11 | if (existingButton) { 12 | existingButton.remove(); 13 | } 14 | 15 | // Add floating button styles 16 | const floatingButtonCSS = ` 17 | #openProductSearchModalBtn { 18 | position: fixed; 19 | right: 20px; 20 | top: 40%; 21 | transform: translateY(-50%); 22 | background-color: #007bff; 23 | color: white; 24 | border: none; 25 | border-radius: 50%; 26 | width: 60px; 27 | height: 60px; 28 | font-size: 24px; 29 | box-shadow: 0 3px 10px rgba(0,0,0,0.2); 30 | cursor: pointer; 31 | z-index: 9990; 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | transition: all 0.3s ease; 36 | } 37 | #openProductSearchModalBtn:hover { 38 | background-color: #0056b3; 39 | box-shadow: 0 5px 15px rgba(0,0,0,0.3); 40 | } 41 | `; 42 | 43 | // Create modal HTML 44 | const modalHTML = ` 45 | <div id="chatgpt-product-search-modal" style=" 46 | position: fixed; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | height: 100%; 51 | background: rgb(230 237 248 / 80%); 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | z-index: 10000; 56 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 57 | "> 58 | <div style=" 59 | background: white; 60 | width: 90%; 61 | height:85%; 62 | border-radius: 8px; 63 | display: flex; 64 | flex-direction: column; 65 | overflow: hidden; 66 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 67 | "> 68 | <div style=" 69 | background: #f8f9fa; 70 | padding: 12px 20px; 71 | display: flex; 72 | justify-content: space-between; 73 | align-items: center; 74 | border-bottom: 1px solid #e9ecef; 75 | "> 76 | <h1 style=" 77 | font-size: 18px; 78 | font-weight: 600; 79 | margin: 0; 80 | color: #495057; 81 | ">🔍 ChatGPT Product Info Search</h1> 82 | <button id="close-modal-btn" style=" 83 | background: none; 84 | border: none; 85 | color: #6c757d; 86 | font-size: 20px; 87 | width: 30px; 88 | height: 30px; 89 | border-radius: 4px; 90 | cursor: pointer; 91 | display: flex; 92 | align-items: center; 93 | justify-content: center; 94 | ">×</button> 95 | </div> 96 | 97 | <div style=" 98 | flex: 1; 99 | display: flex; 100 | flex-direction: column; 101 | overflow: hidden; 102 | "> 103 | <div id="search-area" style=" 104 | position: relative; 105 | padding: 20px; 106 | border-bottom: 1px solid #e9ecef; 107 | background: white; 108 | transition: all 0.3s ease; 109 | "> 110 | <!-- Collapse/Expand Button - positioned absolutely --> 111 | <div id="collapse-toggle" style=" 112 | display: none; 113 | position: absolute; 114 | top: 8px; 115 | right: 20px; 116 | cursor: pointer; 117 | color: #007bff; 118 | font-size: 12px; 119 | font-weight: 500; 120 | transition: all 0.2s ease; 121 | border-radius: 4px; 122 | padding: 4px 8px; 123 | background: rgba(0, 123, 255, 0.1); 124 | border: 1px solid rgba(0, 123, 255, 0.2); 125 | z-index: 10; 126 | "> 127 | <span id="collapse-text">▲ Hide</span> 128 | </div> 129 | 130 | <div id="search-controls"> 131 | <!-- Multi-product toggle --> 132 | <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding: 8px 0;"> 133 | <label style=" 134 | display: flex; 135 | align-items: center; 136 | gap: 8px; 137 | font-size: 14px; 138 | color: #495057; 139 | font-weight: 500; 140 | cursor: pointer; 141 | "> 142 | <div style=" 143 | position: relative; 144 | width: 44px; 145 | height: 24px; 146 | background: #dee2e6; 147 | border-radius: 12px; 148 | transition: background 0.3s ease; 149 | cursor: pointer; 150 | " id="toggle-background"> 151 | <input type="checkbox" id="multi-product-toggle" style=" 152 | position: absolute; 153 | opacity: 0; 154 | width: 100%; 155 | height: 100%; 156 | margin: 0; 157 | cursor: pointer; 158 | " /> 159 | <div style=" 160 | position: absolute; 161 | top: 2px; 162 | left: 2px; 163 | width: 20px; 164 | height: 20px; 165 | background: white; 166 | border-radius: 50%; 167 | transition: transform 0.3s ease; 168 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 169 | " id="toggle-slider"></div> 170 | </div> 171 | Multi-product search 172 | </label> 173 | <div style=" 174 | font-size: 12px; 175 | color: #6c757d; 176 | font-style: italic; 177 | ">Search multiple products at once</div> 178 | </div> 179 | 180 | <!-- Single product input --> 181 | <div id="single-product-input" style="display: flex; gap: 12px; margin-bottom: 12px;"> 182 | <input type="text" id="search-query" placeholder="Search query (e.g., iPhone 17, Nike shoes, Pets Deli Hundefutter)" style=" 183 | flex: 1; 184 | padding: 8px 12px; 185 | border: 1px solid #dee2e6; 186 | border-radius: 4px; 187 | font-size: 14px; 188 | box-sizing: border-box; 189 | " /> 190 | <button id="search-btn" style=" 191 | background: #007bff; 192 | color: white; 193 | border: none; 194 | padding: 8px 16px; 195 | border-radius: 4px; 196 | font-size: 14px; 197 | font-weight: 500; 198 | cursor: pointer; 199 | white-space: nowrap; 200 | ">Search</button> 201 | </div> 202 | 203 | <!-- Multi product input --> 204 | <div id="multi-product-input" style="display: none; margin-bottom: 12px;"> 205 | <textarea id="multi-search-query" placeholder="Enter product names, one per line: iPhone 17 Pro Samsung Galaxy S25 Google Pixel 9" style=" 206 | width: 100%; 207 | min-height: 100px; 208 | padding: 8px 12px; 209 | border: 1px solid #dee2e6; 210 | border-radius: 4px; 211 | font-size: 14px; 212 | box-sizing: border-box; 213 | resize: vertical; 214 | font-family: inherit; 215 | margin-bottom: 8px; 216 | "></textarea> 217 | <div style="display: flex; gap: 12px;"> 218 | <button id="multi-search-btn" style=" 219 | background: #007bff; 220 | color: white; 221 | border: none; 222 | padding: 8px 16px; 223 | border-radius: 4px; 224 | font-size: 14px; 225 | font-weight: 500; 226 | cursor: pointer; 227 | white-space: nowrap; 228 | ">Search All Products</button> 229 | <div style=" 230 | font-size: 12px; 231 | color: #6c757d; 232 | align-self: center; 233 | font-style: italic; 234 | ">Results will be shown in a table format</div> 235 | </div> 236 | </div> 237 | </div> <!-- End search-controls --> 238 | 239 | <!-- Hidden token field for status display --> 240 | <input type="password" id="auth-token" placeholder="Token will be fetched automatically" readonly style=" 241 | display: none; 242 | padding: 8px 12px; 243 | border: 1px solid #dee2e6; 244 | border-radius: 4px; 245 | font-size: 14px; 246 | box-sizing: border-box; 247 | background-color: #f9f9f9; 248 | cursor: not-allowed; 249 | " /> 250 | </div> 251 | 252 | <div id="results-container" style=" 253 | flex: 1; 254 | overflow-y: auto; 255 | padding: 20px; 256 | "> 257 | <div id="welcome-state" style=" 258 | text-align: center; 259 | padding: 60px 40px; 260 | color: #6c757d; 261 | display: flex; 262 | flex-direction: column; 263 | align-items: center; 264 | justify-content: center; 265 | height: 100%; 266 | min-height: 300px; 267 | "> 268 | <div style=" 269 | font-size: 48px; 270 | margin-bottom: 20px; 271 | opacity: 0.7; 272 | ">🔍</div> 273 | <h3 style=" 274 | margin: 0 0 12px 0; 275 | font-size: 20px; 276 | font-weight: 600; 277 | color: #495057; 278 | ">Product Search</h3> 279 | <p style=" 280 | margin: 0 0 24px 0; 281 | font-size: 16px; 282 | line-height: 1.5; 283 | max-width: 400px; 284 | ">Search for product reviews, comparisons, and detailed information from across the web</p> 285 | <div style=" 286 | padding: 16px 20px; 287 | border-left: 4px solid #007bff; 288 | max-width: 500px; 289 | text-align: left; 290 | "> 291 | <div style="font-weight: 600; margin-bottom: 8px; color: #495057;">Try searching for:</div> 292 | <div style="color: #6c757d; font-size: 14px; line-height: 1.4;"> 293 | • "iPhone 17 Pro camera quality"<br> 294 | • "Nike Air Max running shoes"<br> 295 | • "MacBook Air M3 performance"<br> 296 | • "Tesla Model 3 reviews" 297 | </div> 298 | </div> 299 | <div id="auth-status" style=" 300 | margin-top: 20px; 301 | padding: 8px 16px; 302 | border-radius: 20px; 303 | font-size: 13px; 304 | font-weight: 500; 305 | background: #e3f2fd; 306 | color: #1565c0; 307 | border: 1px solid #bbdefb; 308 | ">🔐 Checking authentication...</div> 309 | </div> 310 | </div> 311 | </div> 312 | 313 | <!-- Fixed Footer --> 314 | <div style=" 315 | position: fixed; 316 | bottom: 0; 317 | left: 0; 318 | right: 0; 319 | background: #f8f9fa; 320 | border-top: 1px solid #e9ecef; 321 | padding: 8px 0; 322 | text-align: center; 323 | font-size: 14px; 324 | z-index: 10001; 325 | "> 326 | Created by <a href="https://www.martinaberastegue.com/" target="_blank" rel="noopener noreferrer">Martin Aberastegue (@Xyborg)</a> 327 | </div> 328 | </div> 329 | </div> 330 | `; 331 | 332 | // Function to create and show modal 333 | function createModal() { 334 | // Remove existing modal if present 335 | const existingModal = document.getElementById('chatgpt-product-search-modal'); 336 | if (existingModal) { 337 | existingModal.remove(); 338 | } 339 | 340 | // Inject modal into page 341 | document.body.insertAdjacentHTML('beforeend', modalHTML); 342 | 343 | // Get modal elements 344 | const modal = document.getElementById('chatgpt-product-search-modal'); 345 | const closeBtn = document.getElementById('close-modal-btn'); 346 | const searchBtn = document.getElementById('search-btn'); 347 | const multiSearchBtn = document.getElementById('multi-search-btn'); 348 | const searchQuery = document.getElementById('search-query'); 349 | const multiSearchQuery = document.getElementById('multi-search-query'); 350 | const authToken = document.getElementById('auth-token'); 351 | const resultsContainer = document.getElementById('results-container'); 352 | const multiProductToggle = document.getElementById('multi-product-toggle'); 353 | const toggleBackground = document.getElementById('toggle-background'); 354 | const toggleSlider = document.getElementById('toggle-slider'); 355 | const singleProductInput = document.getElementById('single-product-input'); 356 | const multiProductInput = document.getElementById('multi-product-input'); 357 | const collapseToggle = document.getElementById('collapse-toggle'); 358 | const collapseText = document.getElementById('collapse-text'); 359 | const searchControls = document.getElementById('search-controls'); 360 | 361 | // Close modal functionality 362 | closeBtn.addEventListener('click', () => { 363 | modal.remove(); 364 | }); 365 | 366 | // Close on outside click 367 | modal.addEventListener('click', (e) => { 368 | if (e.target === modal) { 369 | modal.remove(); 370 | } 371 | }); 372 | 373 | // Close on Escape key 374 | document.addEventListener('keydown', (e) => { 375 | if (e.key === 'Escape') { 376 | modal.remove(); 377 | } 378 | }); 379 | 380 | // Toggle functionality 381 | multiProductToggle.addEventListener('change', () => { 382 | const isMultiMode = multiProductToggle.checked; 383 | 384 | if (isMultiMode) { 385 | // Switch to multi-product mode 386 | singleProductInput.style.display = 'none'; 387 | multiProductInput.style.display = 'block'; 388 | toggleBackground.style.background = '#007bff'; 389 | toggleSlider.style.transform = 'translateX(20px)'; 390 | } else { 391 | // Switch to single-product mode 392 | singleProductInput.style.display = 'flex'; 393 | multiProductInput.style.display = 'none'; 394 | toggleBackground.style.background = '#dee2e6'; 395 | toggleSlider.style.transform = 'translateX(0px)'; 396 | } 397 | }); 398 | 399 | // Collapse/Expand functionality 400 | collapseToggle.addEventListener('click', () => { 401 | const isCollapsed = searchControls.style.display === 'none'; 402 | 403 | if (isCollapsed) { 404 | // Expand 405 | searchControls.style.display = 'block'; 406 | collapseText.textContent = '▲ Hide'; 407 | collapseToggle.style.background = 'rgba(0, 123, 255, 0.1)'; 408 | collapseToggle.style.border = '1px solid rgba(0, 123, 255, 0.2)'; 409 | collapseToggle.style.color = '#007bff'; 410 | } else { 411 | // Collapse 412 | searchControls.style.display = 'none'; 413 | collapseText.textContent = '▼ Show'; 414 | collapseToggle.style.background = 'rgba(40, 167, 69, 0.1)'; 415 | collapseToggle.style.border = '1px solid rgba(40, 167, 69, 0.2)'; 416 | collapseToggle.style.color = '#28a745'; 417 | } 418 | }); 419 | 420 | // Add hover effects to collapse toggle 421 | collapseToggle.addEventListener('mouseenter', () => { 422 | const isCollapsed = searchControls.style.display === 'none'; 423 | if (isCollapsed) { 424 | collapseToggle.style.background = 'rgba(40, 167, 69, 0.2)'; 425 | collapseToggle.style.transform = 'scale(1.05)'; 426 | } else { 427 | collapseToggle.style.background = 'rgba(0, 123, 255, 0.2)'; 428 | collapseToggle.style.transform = 'scale(1.05)'; 429 | } 430 | }); 431 | 432 | collapseToggle.addEventListener('mouseleave', () => { 433 | const isCollapsed = searchControls.style.display === 'none'; 434 | if (isCollapsed) { 435 | collapseToggle.style.background = 'rgba(40, 167, 69, 0.1)'; 436 | } else { 437 | collapseToggle.style.background = 'rgba(0, 123, 255, 0.1)'; 438 | } 439 | collapseToggle.style.transform = 'scale(1)'; 440 | }); 441 | 442 | // Search functionality 443 | searchBtn.addEventListener('click', performSearch); 444 | multiSearchBtn.addEventListener('click', performMultiSearch); 445 | 446 | // Enter key support 447 | searchQuery.addEventListener('keydown', (e) => { 448 | if (e.key === 'Enter') { 449 | performSearch(); 450 | } 451 | }); 452 | 453 | // Initialize token status 454 | initializeTokenStatus(); 455 | } 456 | 457 | // Function to show the collapse toggle after results are displayed 458 | function showCollapseToggle() { 459 | const collapseToggle = document.getElementById('collapse-toggle'); 460 | if (collapseToggle) { 461 | collapseToggle.style.display = 'block'; 462 | } 463 | } 464 | 465 | async function performSearch() { 466 | const searchQuery = document.getElementById('search-query'); 467 | const searchBtn = document.getElementById('search-btn'); 468 | const resultsContainer = document.getElementById('results-container'); 469 | 470 | if (!searchQuery || !searchBtn || !resultsContainer) { 471 | alert('Modal elements not found. Please try again.'); 472 | return; 473 | } 474 | 475 | const query = searchQuery.value.trim(); 476 | 477 | if (!query) { 478 | alert('Please enter a search query'); 479 | return; 480 | } 481 | 482 | // Get token automatically 483 | let token; 484 | try { 485 | token = await getAutomaticToken(); 486 | } catch (error) { 487 | alert('Failed to get authentication token. Please make sure you\'re logged in to ChatGPT.'); 488 | return; 489 | } 490 | 491 | // Show loading state 492 | searchBtn.disabled = true; 493 | searchBtn.textContent = 'Searching...'; 494 | resultsContainer.innerHTML = ` 495 | <div style="text-align: center; padding: 40px; color: #666;"> 496 | <div style=" 497 | display: inline-block; 498 | width: 32px; 499 | height: 32px; 500 | border: 3px solid #f3f3f3; 501 | border-top: 3px solid #667eea; 502 | border-radius: 50%; 503 | animation: spin 1s linear infinite; 504 | margin-bottom: 10px; 505 | "></div> 506 | <p>Searching for "${query}"...</p> 507 | </div> 508 | <style> 509 | @keyframes spin { 510 | 0% { transform: rotate(0deg); } 511 | 100% { transform: rotate(360deg); } 512 | } 513 | </style> 514 | `; 515 | 516 | try { 517 | const result = await searchProduct(query, token); 518 | displayResults(result, query); 519 | } catch (error) { 520 | displayError(error.message); 521 | } finally { 522 | searchBtn.disabled = false; 523 | searchBtn.textContent = 'Search'; 524 | // Show collapse toggle after results are displayed 525 | showCollapseToggle(); 526 | } 527 | } 528 | 529 | async function performMultiSearch() { 530 | const multiSearchQuery = document.getElementById('multi-search-query'); 531 | const multiSearchBtn = document.getElementById('multi-search-btn'); 532 | const resultsContainer = document.getElementById('results-container'); 533 | 534 | if (!multiSearchQuery || !multiSearchBtn || !resultsContainer) { 535 | alert('Modal elements not found. Please try again.'); 536 | return; 537 | } 538 | 539 | const queries = multiSearchQuery.value.trim().split('\n').filter(q => q.trim()); 540 | 541 | if (queries.length === 0) { 542 | alert('Please enter at least one product name'); 543 | return; 544 | } 545 | 546 | if (queries.length > 10) { 547 | alert('Maximum 10 products allowed at once to avoid rate limiting'); 548 | return; 549 | } 550 | 551 | // Get token automatically 552 | let token; 553 | try { 554 | token = await getAutomaticToken(); 555 | } catch (error) { 556 | alert('Failed to get authentication token. Please make sure you\'re logged in to ChatGPT.'); 557 | return; 558 | } 559 | 560 | // Show loading state 561 | multiSearchBtn.disabled = true; 562 | multiSearchBtn.textContent = 'Searching...'; 563 | resultsContainer.innerHTML = ` 564 | <div style="text-align: center; padding: 40px; color: #666;"> 565 | <div style=" 566 | display: inline-block; 567 | width: 32px; 568 | height: 32px; 569 | border: 3px solid #f3f3f3; 570 | border-top: 3px solid #667eea; 571 | border-radius: 50%; 572 | animation: spin 1s linear infinite; 573 | margin-bottom: 10px; 574 | "></div> 575 | <p>Searching ${queries.length} products...</p> 576 | <div id="progress-status" style="font-size: 14px; color: #999; margin-top: 10px;"> 577 | Starting searches... 578 | </div> 579 | </div> 580 | <style> 581 | @keyframes spin { 582 | 0% { transform: rotate(0deg); } 583 | 100% { transform: rotate(360deg); } 584 | } 585 | </style> 586 | `; 587 | 588 | const results = []; 589 | const progressStatus = document.getElementById('progress-status'); 590 | 591 | try { 592 | // Search products one by one 593 | for (let i = 0; i < queries.length; i++) { 594 | const query = queries[i].trim(); 595 | if (progressStatus) { 596 | progressStatus.textContent = `Searching "${query}" (${i + 1}/${queries.length})...`; 597 | } 598 | 599 | try { 600 | const result = await searchProduct(query, token); 601 | results.push({ 602 | query: query, 603 | success: true, 604 | data: result 605 | }); 606 | } catch (error) { 607 | results.push({ 608 | query: query, 609 | success: false, 610 | error: error.message 611 | }); 612 | } 613 | 614 | // Add a small delay between requests to be respectful 615 | if (i < queries.length - 1) { 616 | await new Promise(resolve => setTimeout(resolve, 1000)); 617 | } 618 | } 619 | 620 | displayMultiResults(results); 621 | } catch (error) { 622 | displayError(error.message); 623 | } finally { 624 | multiSearchBtn.disabled = false; 625 | multiSearchBtn.textContent = 'Search All Products'; 626 | // Show collapse toggle after results are displayed 627 | showCollapseToggle(); 628 | } 629 | } 630 | 631 | async function getAutomaticToken() { 632 | try { 633 | const response = await fetch("/api/auth/session"); 634 | if (!response.ok) { 635 | throw new Error(`Session API responded with: ${response.status} ${response.statusText}`); 636 | } 637 | const sessionData = await response.json(); 638 | 639 | if (!sessionData.accessToken) { 640 | throw new Error("No access token found in session. Please make sure you're logged in to ChatGPT."); 641 | } 642 | 643 | // Update the token input field to show it's been fetched 644 | const tokenInput = document.getElementById('auth-token'); 645 | if (tokenInput) { 646 | tokenInput.placeholder = "✅ Token fetched from session"; 647 | tokenInput.style.backgroundColor = "#f0f8ff"; 648 | tokenInput.style.borderColor = "#007bff"; 649 | } 650 | 651 | return sessionData.accessToken; 652 | } catch (error) { 653 | // Update the token input field to show error 654 | const tokenInput = document.getElementById('auth-token'); 655 | if (tokenInput) { 656 | tokenInput.placeholder = "❌ Failed to fetch token automatically"; 657 | tokenInput.style.backgroundColor = "#fff5f5"; 658 | tokenInput.style.borderColor = "#ef4444"; 659 | tokenInput.readOnly = false; 660 | tokenInput.style.cursor = "text"; 661 | } 662 | 663 | throw error; 664 | } 665 | } 666 | 667 | async function searchProduct(query, token) { 668 | const requestBody = { 669 | "conversation_id": "", 670 | "is_client_thread": true, 671 | "message_id": "", 672 | "product_query": query, 673 | "supported_encodings": ["v1"], 674 | "product_lookup_key": { 675 | "data": JSON.stringify({ 676 | "request_query": query, 677 | "all_ids": {"p2": [""]}, 678 | "known_ids": {}, 679 | "metadata_sources": ["p1", "p3"], 680 | "variant_sources": null, 681 | "last_variant_group_types": null, 682 | "merchant_hints": [], 683 | "provider_title": query 684 | }), 685 | "version": "1", 686 | "variant_options_query": null 687 | } 688 | }; 689 | 690 | const response = await fetch("https://chatgpt.com/backend-api/search/product_info", { 691 | "headers": { 692 | "accept": "text/event-stream", 693 | "accept-language": "en-GB,en-US;q=0.9,en;q=0.8,es-AR;q=0.7,es;q=0.6,de;q=0.5,it;q=0.4,zh-CN;q=0.3,zh;q=0.2,id;q=0.1,pt-BR;q=0.1,pt;q=0.1,fr;q=0.1,tr;q=0.1,pl;q=0.1,sv;q=0.1,ru;q=0.1,ar;q=0.1,el;q=0.1", 694 | "authorization": "Bearer " + token, 695 | "content-type": "application/json", 696 | "oai-client-version": "prod-43c98f917bf2c3e3a36183e9548cd048e4e40615", 697 | "oai-device-id": generateDeviceId(), 698 | "oai-language": "en-US", 699 | "priority": "u=1, i", 700 | "sec-ch-ua": '"Opera";v="120", "Not-A.Brand";v="8", "Chromium";v="135"', 701 | "sec-ch-ua-arch": '"arm"', 702 | "sec-ch-ua-bitness": '"64"', 703 | "sec-ch-ua-full-version": '"120.0.5543.161"', 704 | "sec-ch-ua-full-version-list": '"Opera";v="120.0.5543.161", "Not-A.Brand";v="8.0.0.0", "Chromium";v="135.0.7049.115"', 705 | "sec-ch-ua-mobile": "?0", 706 | "sec-ch-ua-model": '""', 707 | "sec-ch-ua-platform": '"macOS"', 708 | "sec-ch-ua-platform-version": '"15.5.0"', 709 | "sec-fetch-dest": "empty", 710 | "sec-fetch-mode": "cors", 711 | "sec-fetch-site": "same-origin" 712 | }, 713 | "referrer": window.location.href, 714 | "referrerPolicy": "strict-origin-when-cross-origin", 715 | "body": JSON.stringify(requestBody), 716 | "method": "POST", 717 | "mode": "cors", 718 | "credentials": "include" 719 | }); 720 | 721 | if (!response.ok) { 722 | throw new Error(`HTTP error! status: ${response.status}`); 723 | } 724 | 725 | const responseText = await response.text(); 726 | return parseProductInfo(responseText); 727 | } 728 | 729 | function parseProductInfo(content) { 730 | const products = []; 731 | const reviews = []; 732 | const productLinks = []; // Store product links from grouped_citation 733 | const rationaleObj = { text: '' }; // Track the current rationale being built (using object for reference) 734 | const citations = new Map(); // Store citations by cite key 735 | const summaryObj = { text: '' }; // Track the current summary being built (using object for reference) 736 | 737 | const lines = content.split('\n'); 738 | let currentEvent = null; 739 | let currentData = []; 740 | let eventCount = 0; 741 | 742 | for (let i = 0; i < lines.length; i++) { 743 | const line = lines[i]; 744 | 745 | if (line.startsWith('event: ')) { 746 | if (currentEvent && currentData.length > 0) { 747 | processEvent(currentEvent, currentData.join('\n'), products, reviews, productLinks, rationaleObj, eventCount, citations, summaryObj); 748 | eventCount++; 749 | } 750 | 751 | currentEvent = line.replace('event: ', '').trim(); 752 | currentData = []; 753 | } else if (line.startsWith('data: ')) { 754 | currentData.push(line.replace('data: ', '')); 755 | } else if (line.trim() === '') { 756 | continue; 757 | } else { 758 | currentData.push(line); 759 | } 760 | } 761 | 762 | if (currentEvent && currentData.length > 0) { 763 | processEvent(currentEvent, currentData.join('\n'), products, reviews, productLinks, rationaleObj, eventCount, citations, summaryObj); 764 | eventCount++; 765 | } 766 | 767 | // Map citations to reviews 768 | for (const review of reviews) { 769 | if (review.cite && citations.has(review.cite)) { 770 | review.url = citations.get(review.cite).url; 771 | } 772 | } 773 | 774 | const uniqueMerchants = new Set(); 775 | for (const product of products) { 776 | for (const offer of product.offers || []) { 777 | if (offer.merchant_name) { 778 | uniqueMerchants.add(offer.merchant_name); 779 | } 780 | } 781 | } 782 | 783 | const reviewThemes = [...new Set(reviews.map(review => review.theme))]; 784 | 785 | return { 786 | products: products, 787 | productLinks: productLinks, // Add detected product links 788 | reviews: reviews, 789 | rationale: rationaleObj.text || null, 790 | reviewSummary: summaryObj.text || null, // Add the built summary 791 | summary: { 792 | total_products: products.length, 793 | total_product_links: productLinks.length, 794 | total_reviews: reviews.length, 795 | unique_merchants: uniqueMerchants.size, 796 | review_themes: reviewThemes 797 | } 798 | }; 799 | } 800 | 801 | function processEvent(eventType, dataStr, products, reviews, productLinks, rationaleObj, eventIndex, citations, summaryObj) { 802 | if (eventType !== 'delta' || !dataStr || dataStr === '""') { 803 | return; 804 | } 805 | 806 | try { 807 | const data = JSON.parse(dataStr); 808 | 809 | if (typeof data !== 'object' || data === null) { 810 | return; 811 | } 812 | 813 | // Track processed patches to avoid duplicates 814 | const processedPatches = new Set(); 815 | 816 | // Handle direct patch objects (like your examples) 817 | if (data.p === '/grouped_citation' && data.o === 'replace' && data.v && data.v.url) { 818 | eventIndex++; 819 | const citeKey = `turn0search${eventIndex}`; 820 | citations.set(citeKey, { 821 | url: data.v.url, 822 | title: data.v.title || '' 823 | }); 824 | 825 | // Remove any existing product link with the same URL to avoid duplicates 826 | const existingLinkIndex = productLinks.findIndex(link => link.url === data.v.url); 827 | 828 | // Capture ALL grouped_citation objects as product links 829 | const productLink = { 830 | title: data.v.title || '', 831 | url: data.v.url, 832 | snippet: data.v.snippet || '', 833 | source: extractDomainFromUrl(data.v.url) 834 | }; 835 | 836 | if (existingLinkIndex >= 0) { 837 | // Update existing link with potentially better title 838 | productLinks[existingLinkIndex] = productLink; 839 | } else { 840 | // Add new link 841 | productLinks.push(productLink); 842 | } 843 | } 844 | 845 | // Handle direct rationale patches 846 | if (data.p === '/rationale' && data.o === 'append' && data.v) { 847 | rationaleObj.text += data.v; 848 | } 849 | 850 | // Handle direct summary patches 851 | if (data.p === '/summary' && data.o === 'append' && data.v) { 852 | summaryObj.text += data.v; 853 | } 854 | 855 | // Capture citations from cite_map and rationale patches 856 | if (data.v && Array.isArray(data.v)) { 857 | for (const patch of data.v) { 858 | if (patch.p && patch.p.startsWith('/cite_map/') && patch.o === 'add' && patch.v && patch.v.url) { 859 | const citeKey = patch.p.replace('/cite_map/', ''); 860 | citations.set(citeKey, { 861 | url: patch.v.url, 862 | title: patch.v.title || '' 863 | }); 864 | } 865 | 866 | // Capture grouped_citation from data.v array (like your example!) 867 | if (patch.p === '/grouped_citation' && patch.o === 'replace' && patch.v && patch.v.url) { 868 | eventIndex++; 869 | const citeKey = `turn0search${eventIndex}`; 870 | citations.set(citeKey, { 871 | url: patch.v.url, 872 | title: patch.v.title || '' 873 | }); 874 | 875 | // Remove any existing product link with the same URL to avoid duplicates 876 | const existingLinkIndex = productLinks.findIndex(link => link.url === patch.v.url); 877 | 878 | // Capture ALL grouped_citation objects as product links 879 | const productLink = { 880 | title: patch.v.title || '', 881 | url: patch.v.url, 882 | snippet: patch.v.snippet || '', 883 | source: extractDomainFromUrl(patch.v.url) 884 | }; 885 | 886 | if (existingLinkIndex >= 0) { 887 | // Update existing link with potentially better title 888 | productLinks[existingLinkIndex] = productLink; 889 | } else { 890 | // Add new link 891 | productLinks.push(productLink); 892 | } 893 | } 894 | 895 | // Handle individual grouped_citation property updates (like URL and title changes) 896 | if (patch.p && patch.p.startsWith('/grouped_citation/') && patch.o === 'replace' && patch.v) { 897 | 898 | // Find or create the latest citation entry 899 | let latestCiteKey = null; 900 | let maxCiteNum = -1; 901 | 902 | // Find the highest numbered citation 903 | for (const [key, value] of citations.entries()) { 904 | if (key.startsWith('turn0search')) { 905 | const num = parseInt(key.replace('turn0search', '')); 906 | if (num > maxCiteNum) { 907 | maxCiteNum = num; 908 | latestCiteKey = key; 909 | } 910 | } 911 | } 912 | 913 | // If no citation exists yet, create one 914 | if (!latestCiteKey) { 915 | latestCiteKey = `turn0search${eventIndex}`; 916 | citations.set(latestCiteKey, {}); 917 | } 918 | 919 | // Update the citation with the new property 920 | const existingCitation = citations.get(latestCiteKey) || {}; 921 | 922 | if (patch.p === '/grouped_citation/url') { 923 | citations.set(latestCiteKey, { 924 | ...existingCitation, 925 | url: patch.v 926 | }); 927 | } else if (patch.p === '/grouped_citation/title') { 928 | citations.set(latestCiteKey, { 929 | ...existingCitation, 930 | title: patch.v 931 | }); 932 | } 933 | 934 | // Update or add product link if we have both URL and title 935 | const updatedCitation = citations.get(latestCiteKey); 936 | if (updatedCitation.url) { 937 | // Remove any existing product link with the same URL to avoid duplicates 938 | const existingLinkIndex = productLinks.findIndex(link => link.url === updatedCitation.url); 939 | 940 | const productLink = { 941 | title: updatedCitation.title || 'Product Link', 942 | url: updatedCitation.url, 943 | snippet: '', 944 | source: extractDomainFromUrl(updatedCitation.url) 945 | }; 946 | 947 | if (existingLinkIndex >= 0) { 948 | // Update existing link 949 | productLinks[existingLinkIndex] = productLink; 950 | } else { 951 | // Add new link 952 | productLinks.push(productLink); 953 | } 954 | } 955 | } 956 | 957 | // Capture rationale patches from data.v array 958 | if (patch.p === '/rationale' && patch.o === 'append' && patch.v) { 959 | const patchKey = `/rationale-${patch.v}`; 960 | if (!processedPatches.has(patchKey)) { 961 | processedPatches.add(patchKey); 962 | rationaleObj.text += patch.v; 963 | } 964 | } 965 | 966 | // Capture summary patches from data.v array 967 | if (patch.p === '/summary' && patch.o === 'append' && patch.v) { 968 | summaryObj.text += patch.v; 969 | } 970 | } 971 | } 972 | 973 | // Also check patch operations for citations and rationale updates 974 | if (data.o === 'patch' && data.v && Array.isArray(data.v)) { 975 | for (const patch of data.v) { 976 | if (patch.p && patch.p.startsWith('/cite_map/') && patch.o === 'add' && patch.v && patch.v.url) { 977 | const citeKey = patch.p.replace('/cite_map/', ''); 978 | citations.set(citeKey, { 979 | url: patch.v.url, 980 | title: patch.v.title || '' 981 | }); 982 | } 983 | 984 | if (patch.p === '/grouped_citation' && patch.o === 'replace' && patch.v && patch.v.url) { 985 | // This is a citation being added/updated 986 | citations.set(`turn0search${eventIndex}`, { 987 | url: patch.v.url, 988 | title: patch.v.title || '' 989 | }); 990 | 991 | // Remove any existing product link with the same URL to avoid duplicates 992 | const existingLinkIndex = productLinks.findIndex(link => link.url === patch.v.url); 993 | 994 | // Capture ALL grouped_citation objects as product links 995 | const productLink = { 996 | title: patch.v.title || '', 997 | url: patch.v.url, 998 | snippet: patch.v.snippet || '', 999 | source: extractDomainFromUrl(patch.v.url) 1000 | }; 1001 | 1002 | if (existingLinkIndex >= 0) { 1003 | // Update existing link with potentially better title 1004 | productLinks[existingLinkIndex] = productLink; 1005 | } else { 1006 | // Add new link 1007 | productLinks.push(productLink); 1008 | } 1009 | } 1010 | 1011 | // Capture rationale patches 1012 | if (patch.p === '/rationale' && patch.o === 'append' && patch.v) { 1013 | const patchKey = `/rationale-${patch.v}`; 1014 | if (!processedPatches.has(patchKey)) { 1015 | processedPatches.add(patchKey); 1016 | rationaleObj.text += patch.v; 1017 | } 1018 | } 1019 | 1020 | // Capture summary patches 1021 | if (patch.p === '/summary' && patch.o === 'append' && patch.v) { 1022 | summaryObj.text += patch.v; 1023 | } 1024 | } 1025 | } 1026 | 1027 | if (data.v && typeof data.v === 'object' && !Array.isArray(data.v)) { 1028 | const vData = data.v; 1029 | 1030 | if (vData.type === 'product_entity' && vData.product) { 1031 | const product = vData.product; 1032 | 1033 | const productInfo = { 1034 | title: product.title || '', 1035 | price: product.price || '', 1036 | url: product.url || '', 1037 | merchants: product.merchants || '', 1038 | description: product.description || null, 1039 | rating: product.rating || null, 1040 | num_reviews: product.num_reviews || null, 1041 | featured_tag: product.featured_tag || null, 1042 | image_urls: product.image_urls || [], 1043 | offers: [] 1044 | }; 1045 | 1046 | if (product.offers) { 1047 | for (const offerData of product.offers) { 1048 | const offer = { 1049 | merchant_name: offerData.merchant_name || '', 1050 | product_name: offerData.product_name || '', 1051 | url: offerData.url || '', 1052 | price: offerData.price || '', 1053 | details: offerData.details || null, 1054 | available: offerData.available !== false, 1055 | tag: offerData.tag?.text || null 1056 | }; 1057 | productInfo.offers.push(offer); 1058 | } 1059 | } 1060 | 1061 | productInfo.variants = []; 1062 | if (product.variants) { 1063 | for (const variant of product.variants) { 1064 | const selectedOption = variant.options?.find(opt => opt.selected)?.label || null; 1065 | productInfo.variants.push({ 1066 | type: variant.label || '', 1067 | selected: selectedOption 1068 | }); 1069 | } 1070 | } 1071 | 1072 | products.push(productInfo); 1073 | } 1074 | 1075 | else if (vData.type === 'product_reviews') { 1076 | // Initialize the current summary from the product_reviews object 1077 | if (vData.summary) { 1078 | summaryObj.text = vData.summary; 1079 | } 1080 | 1081 | const reviewList = vData.reviews || []; 1082 | for (const reviewData of reviewList) { 1083 | const review = { 1084 | source: reviewData.source || '', 1085 | theme: reviewData.theme || '', 1086 | summary: reviewData.summary || '', 1087 | sentiment: reviewData.sentiment || '', 1088 | rating: reviewData.rating || null, 1089 | num_reviews: reviewData.num_reviews || null, 1090 | cite: reviewData.cite || `turn0search${eventIndex}`, 1091 | url: null // Will be populated later from citations 1092 | }; 1093 | reviews.push(review); 1094 | } 1095 | } 1096 | 1097 | else if (vData.type === 'product_rationale') { 1098 | const rationale = vData.rationale || ''; 1099 | if (rationale) { 1100 | // Initialize rationale - set the initial text 1101 | rationaleObj.text = rationale; 1102 | } 1103 | } 1104 | } 1105 | 1106 | else if (data.o === 'add' && data.v) { 1107 | const vData = data.v; 1108 | if (typeof vData === 'object' && vData.type === 'product_reviews') { 1109 | const reviewList = vData.reviews || []; 1110 | for (const reviewData of reviewList) { 1111 | if (typeof reviewData === 'object') { 1112 | const review = { 1113 | source: reviewData.source || '', 1114 | theme: reviewData.theme || '', 1115 | summary: reviewData.summary || '', 1116 | sentiment: reviewData.sentiment || '', 1117 | rating: reviewData.rating || null, 1118 | num_reviews: reviewData.num_reviews || null, 1119 | cite: reviewData.cite || `turn0search${eventIndex}`, 1120 | url: null // Will be populated later from citations 1121 | }; 1122 | reviews.push(review); 1123 | } 1124 | } 1125 | } 1126 | } 1127 | 1128 | else if (data.o === 'patch' && data.v) { 1129 | const vData = data.v; 1130 | if (Array.isArray(vData)) { 1131 | for (const item of vData) { 1132 | if (typeof item === 'object' && item.p === '/reviews' && item.o === 'append') { 1133 | const reviewList = item.v || []; 1134 | if (Array.isArray(reviewList)) { 1135 | for (const reviewData of reviewList) { 1136 | if (typeof reviewData === 'object') { 1137 | const review = { 1138 | source: reviewData.source || '', 1139 | theme: reviewData.theme || '', 1140 | summary: reviewData.summary || '', 1141 | sentiment: reviewData.sentiment || '', 1142 | rating: reviewData.rating || null, 1143 | num_reviews: reviewData.num_reviews || null, 1144 | cite: reviewData.cite || `turn0search${eventIndex}`, 1145 | url: null // Will be populated later from citations 1146 | }; 1147 | reviews.push(review); 1148 | } 1149 | } 1150 | } 1151 | } 1152 | } 1153 | } 1154 | } 1155 | 1156 | if (data.v && Array.isArray(data.v)) { 1157 | for (const item of data.v) { 1158 | if (typeof item === 'object' && item.p === '/reviews' && item.o === 'append') { 1159 | const reviewList = item.v || []; 1160 | if (Array.isArray(reviewList)) { 1161 | for (const reviewData of reviewList) { 1162 | if (typeof reviewData === 'object') { 1163 | const review = { 1164 | source: reviewData.source || '', 1165 | theme: reviewData.theme || '', 1166 | summary: reviewData.summary || '', 1167 | sentiment: reviewData.sentiment || '', 1168 | rating: reviewData.rating || null, 1169 | num_reviews: reviewData.num_reviews || null, 1170 | cite: reviewData.cite || `turn0search${eventIndex}`, 1171 | url: null // Will be populated later from citations 1172 | }; 1173 | reviews.push(review); 1174 | } 1175 | } 1176 | } 1177 | } 1178 | } 1179 | } 1180 | 1181 | } catch (jsonError) { 1182 | return; 1183 | } 1184 | } 1185 | 1186 | function generateDeviceId() { 1187 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 1188 | const r = Math.random() * 16 | 0; 1189 | const v = c == 'x' ? r : (r & 0x3 | 0x8); 1190 | return v.toString(16); 1191 | }); 1192 | } 1193 | 1194 | function extractDomainFromUrl(url) { 1195 | try { 1196 | const urlObj = new URL(url); 1197 | return urlObj.hostname.replace('www.', ''); 1198 | } catch (e) { 1199 | return url.split('/')[2] || url; 1200 | } 1201 | } 1202 | 1203 | function displayResults(data, query) { 1204 | const resultsContainer = document.getElementById('results-container'); 1205 | if (!resultsContainer) { 1206 | console.error('Results container not found'); 1207 | return; 1208 | } 1209 | 1210 | if (!data || (!data.reviews.length && !data.products.length && !data.productLinks.length)) { 1211 | resultsContainer.innerHTML = ` 1212 | <div style=" 1213 | background: #fef2f2; 1214 | color: #991b1b; 1215 | padding: 15px; 1216 | border-radius: 8px; 1217 | border-left: 4px solid #ef4444; 1218 | margin: 20px 0; 1219 | "> 1220 | <h3>No results found</h3> 1221 | <p>No products or reviews were found for "${query}". Try a different search term.</p> 1222 | </div> 1223 | `; 1224 | return; 1225 | } 1226 | 1227 | let html = ` 1228 | <div style=" 1229 | padding: 12px; 1230 | margin-bottom: 16px; 1231 | border-bottom: 1px solid #e9ecef; 1232 | "> 1233 | <div style=" 1234 | font-size: 16px; 1235 | font-weight: 600; 1236 | color: #495057; 1237 | margin-bottom: 8px; 1238 | ">Results for "${query}"</div> 1239 | <div style="display: flex; gap: 16px; font-size: 14px; color: #6c757d;"> 1240 | <span>${data.summary.total_reviews} reviews</span> 1241 | <span>${data.summary.total_products} products</span> 1242 | <span>${data.summary.total_product_links} citation links</span> 1243 | <span>${data.summary.review_themes.length} themes</span> 1244 | </div> 1245 | </div> 1246 | `; 1247 | 1248 | // Show message when we have links but no reviews or products 1249 | if (data.productLinks.length > 0 && data.reviews.length === 0 && data.products.length === 0) { 1250 | html += ` 1251 | <div style="margin-bottom: 20px;"> 1252 | <div style=" 1253 | background: #fff3cd; 1254 | border: 1px solid #ffeaa7; 1255 | border-radius: 6px; 1256 | padding: 12px; 1257 | margin: 0 12px; 1258 | color: #856404; 1259 | font-size: 13px; 1260 | "> 1261 | <div style="font-weight: 600; margin-bottom: 4px;">⚠️ Limited Information Available</div> 1262 | <div>ChatGPT found ${data.productLinks.length} relevant link(s) for this product, but no detailed reviews or product information were retrieved. You can check the citation links below for more information.</div> 1263 | </div> 1264 | </div> 1265 | `; 1266 | } 1267 | 1268 | if (data.rationale && data.rationale.trim()) { 1269 | html += ` 1270 | <div style="margin-bottom: 20px;"> 1271 | <div style=" 1272 | font-size: 14px; 1273 | font-weight: 600; 1274 | color: #495057; 1275 | margin-bottom: 8px; 1276 | padding: 0 12px; 1277 | ">Product Overview</div> 1278 | <div style=" 1279 | background: #e2f1ff; 1280 | padding: 12px; 1281 | margin: 0 12px; 1282 | color: #000; 1283 | line-height: 1.4; 1284 | font-size: 13px; 1285 | border-left: 3px solid #3a6797; 1286 | ">${data.rationale}</div> 1287 | </div> 1288 | `; 1289 | } 1290 | 1291 | if (data.reviewSummary && data.reviewSummary.trim()) { 1292 | html += ` 1293 | <div style="margin-bottom: 20px;"> 1294 | <div style=" 1295 | font-size: 14px; 1296 | font-weight: 600; 1297 | color: #495057; 1298 | margin-bottom: 8px; 1299 | padding: 0 12px; 1300 | ">Review Summary</div> 1301 | <div style=" 1302 | background: #f5fee8; 1303 | padding: 12px; 1304 | margin: 0 12px; 1305 | color: #000; 1306 | line-height: 1.4; 1307 | font-size: 13px; 1308 | border-left: 3px solid #93ac71; 1309 | ">${data.reviewSummary}</div> 1310 | </div> 1311 | `; 1312 | } 1313 | 1314 | if (data.productLinks && data.productLinks.length > 0) { 1315 | html += ` 1316 | <div style="margin-bottom: 20px;"> 1317 | <div style=" 1318 | font-size: 14px; 1319 | font-weight: 600; 1320 | color: #495057; 1321 | margin-bottom: 8px; 1322 | padding: 0 12px; 1323 | ">Citation Links</div> 1324 | ${data.productLinks.map(link => ` 1325 | <div style=" 1326 | border: 1px solid #e9ecef; 1327 | border-radius: 6px; 1328 | padding: 12px; 1329 | margin: 0 12px 8px 12px; 1330 | background: #f8f9fa; 1331 | "> 1332 | <div style=" 1333 | display: flex; 1334 | align-items: center; 1335 | margin-bottom: 6px; 1336 | gap: 8px; 1337 | "> 1338 | <img src="https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(link.url)}&size=128" alt="${link.source}" style=" 1339 | width: 16px; 1340 | height: 16px; 1341 | border-radius: 2px; 1342 | flex-shrink: 0; 1343 | " onerror="this.style.display='none'"> 1344 | <span style=" 1345 | font-weight: 600; 1346 | color: #007bff; 1347 | font-size: 14px; 1348 | ">${link.title}</span> 1349 | <a href="${link.url}" target="_blank" style=" 1350 | color: #28a745; 1351 | text-decoration: none; 1352 | font-size: 14px; 1353 | margin-left: auto; 1354 | background: #d4edda; 1355 | padding: 4px 8px; 1356 | border-radius: 4px; 1357 | border: 1px solid #c3e6cb; 1358 | " title="Visit citation page">↗</a> 1359 | </div> 1360 | ${link.snippet ? `<div style=" 1361 | color: #6c757d; 1362 | font-size: 12px; 1363 | line-height: 1.3; 1364 | margin-top: 4px; 1365 | ">${link.snippet}</div>` : ''} 1366 | </div> 1367 | `).join('')} 1368 | </div> 1369 | `; 1370 | } 1371 | 1372 | if (data.reviews.length > 0) { 1373 | // Calculate sentiment distribution 1374 | const sentimentCounts = data.reviews.reduce((acc, review) => { 1375 | acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; 1376 | return acc; 1377 | }, {}); 1378 | 1379 | const totalReviews = data.reviews.length; 1380 | const positiveCount = sentimentCounts.positive || 0; 1381 | const neutralCount = sentimentCounts.neutral || 0; 1382 | const negativeCount = sentimentCounts.negative || 0; 1383 | 1384 | const positivePercent = Math.round((positiveCount / totalReviews) * 100); 1385 | const neutralPercent = Math.round((neutralCount / totalReviews) * 100); 1386 | const negativePercent = Math.round((negativeCount / totalReviews) * 100); 1387 | 1388 | html += ` 1389 | <div> 1390 | <div style=" 1391 | display: flex; 1392 | align-items: center; 1393 | justify-content: space-between; 1394 | margin-bottom: 12px; 1395 | padding: 0 12px; 1396 | "> 1397 | <div style=" 1398 | font-size: 14px; 1399 | font-weight: 600; 1400 | color: #495057; 1401 | ">Reviews</div> 1402 | <div style="display: flex; align-items: center; gap: 12px;"> 1403 | <div style=" 1404 | display: flex; 1405 | background: #f8f9fa; 1406 | border-radius: 8px; 1407 | overflow: hidden; 1408 | width: 100px; 1409 | height: 6px; 1410 | border: 1px solid #e9ecef; 1411 | "> 1412 | ${positiveCount > 0 ? `<div style=" 1413 | background: #28a745; 1414 | width: ${positivePercent}%; 1415 | height: 100%; 1416 | " title="${positiveCount} positive (${positivePercent}%)"></div>` : ''} 1417 | ${neutralCount > 0 ? `<div style=" 1418 | background: #ffc107; 1419 | width: ${neutralPercent}%; 1420 | height: 100%; 1421 | " title="${neutralCount} neutral (${neutralPercent}%)"></div>` : ''} 1422 | ${negativeCount > 0 ? `<div style=" 1423 | background: #dc3545; 1424 | width: ${negativePercent}%; 1425 | height: 100%; 1426 | " title="${negativeCount} negative (${negativePercent}%)"></div>` : ''} 1427 | </div> 1428 | <div style=" 1429 | font-size: 12px; 1430 | color: #6c757d; 1431 | white-space: nowrap; 1432 | "> 1433 | <span style="color: #28a745;">●</span> ${positivePercent}% 1434 | ${neutralCount > 0 ? `<span style="color: #ffc107; margin-left: 6px;">●</span> ${neutralPercent}%` : ''} 1435 | ${negativeCount > 0 ? `<span style="color: #dc3545; margin-left: 6px;">●</span> ${negativePercent}%` : ''} 1436 | </div> 1437 | </div> 1438 | </div> 1439 | ${data.reviews.map(review => { 1440 | const sentimentColor = review.sentiment === 'positive' ? '#28a745' : 1441 | review.sentiment === 'negative' ? '#dc3545' : '#ffc107'; 1442 | 1443 | 1444 | return ` 1445 | <div style=" 1446 | border-bottom: 1px solid #f8f9fa; 1447 | padding: 12px; 1448 | margin-bottom: 1px; 1449 | "> 1450 | <div style=" 1451 | display: flex; 1452 | align-items: center; 1453 | margin-bottom: 8px; 1454 | gap: 8px; 1455 | "> 1456 | <img src="https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(review.url || `https://${review.source.toLowerCase().replace(/\s+/g, '')}.com`)}&size=128" alt="${review.source}" style=" 1457 | width: 16px; 1458 | height: 16px; 1459 | border-radius: 2px; 1460 | flex-shrink: 0; 1461 | " onerror="this.style.display='none'"> 1462 | <span style="font-weight: 500; color: #495057; font-size: 14px;">${review.source}</span> 1463 | ${review.url ? `<a href="${review.url}" target="_blank" style=" 1464 | color: #6c757d; 1465 | text-decoration: none; 1466 | font-size: 12px; 1467 | margin-left: auto; 1468 | " title="Open source">↗</a>` : ''} 1469 | <span style=" 1470 | width: 8px; 1471 | height: 8px; 1472 | border-radius: 50%; 1473 | background: ${sentimentColor}; 1474 | display: inline-block; 1475 | margin-left: ${review.url ? '4px' : 'auto'}; 1476 | "></span> 1477 | </div> 1478 | <div style=" 1479 | font-size: 13px; 1480 | font-weight: 600; 1481 | color: #007bff; 1482 | margin-bottom: 6px; 1483 | ">${review.theme}</div> 1484 | <div style="color: #6c757d; line-height: 1.4; font-size: 13px;">${review.summary}</div> 1485 | </div> 1486 | `; 1487 | }).join('')} 1488 | </div> 1489 | `; 1490 | } 1491 | 1492 | if (data.summary.review_themes.length > 0) { 1493 | html += ` 1494 | <div style="margin-bottom: 20px;"> 1495 | <div style=" 1496 | font-size: 14px; 1497 | font-weight: 600; 1498 | color: #495057; 1499 | margin-bottom: 8px; 1500 | padding: 0 12px; 1501 | ">Themes</div> 1502 | <div style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px;"> 1503 | ${data.summary.review_themes.map(theme => 1504 | `<span style=" 1505 | background: #e9ecef; 1506 | color: #495057; 1507 | padding: 4px 8px; 1508 | border-radius: 12px; 1509 | font-size: 12px; 1510 | ">${theme}</span>` 1511 | ).join('')} 1512 | </div> 1513 | </div> 1514 | `; 1515 | } 1516 | 1517 | resultsContainer.innerHTML = html; 1518 | } 1519 | 1520 | function displayMultiResults(results) { 1521 | const resultsContainer = document.getElementById('results-container'); 1522 | if (!resultsContainer) { 1523 | console.error('Results container not found'); 1524 | return; 1525 | } 1526 | 1527 | const successfulResults = results.filter(r => r.success); 1528 | const failedResults = results.filter(r => !r.success); 1529 | 1530 | let html = ` 1531 | <div style=" 1532 | padding: 12px; 1533 | margin-bottom: 16px; 1534 | border-bottom: 1px solid #e9ecef; 1535 | "> 1536 | <div style=" 1537 | font-size: 16px; 1538 | font-weight: 600; 1539 | color: #495057; 1540 | margin-bottom: 8px; 1541 | ">Multi-Product Search Results</div> 1542 | <div style="display: flex; gap: 16px; font-size: 14px; color: #6c757d;"> 1543 | <span>${successfulResults.length}/${results.length} products found</span> 1544 | ${failedResults.length > 0 ? `<span style="color: #dc3545;">${failedResults.length} failed</span>` : ''} 1545 | </div> 1546 | </div> 1547 | `; 1548 | 1549 | if (failedResults.length > 0) { 1550 | html += ` 1551 | <div style="margin-bottom: 20px;"> 1552 | <div style=" 1553 | font-size: 14px; 1554 | font-weight: 600; 1555 | color: #dc3545; 1556 | margin-bottom: 8px; 1557 | padding: 0 12px; 1558 | ">Search Errors</div> 1559 | <div style=" 1560 | background: #f8d7da; 1561 | border: 1px solid #f5c6cb; 1562 | border-radius: 4px; 1563 | padding: 12px; 1564 | margin: 0 12px; 1565 | color: #721c24; 1566 | font-size: 13px; 1567 | "> 1568 | ${failedResults.map(r => `<div><strong>${r.query}:</strong> ${r.error}</div>`).join('<br>')} 1569 | </div> 1570 | </div> 1571 | `; 1572 | } 1573 | 1574 | if (successfulResults.length === 0) { 1575 | html += ` 1576 | <div style=" 1577 | text-align: center; 1578 | padding: 60px 40px; 1579 | color: #6c757d; 1580 | "> 1581 | <div style="font-size: 48px; margin-bottom: 20px; opacity: 0.7;">❌</div> 1582 | <h3 style="margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #495057;">No Results Found</h3> 1583 | <p style="margin: 0; font-size: 16px; line-height: 1.5;">None of the products could be found. Please try different search terms.</p> 1584 | </div> 1585 | `; 1586 | resultsContainer.innerHTML = html; 1587 | return; 1588 | } 1589 | 1590 | // Create comparison table 1591 | html += ` 1592 | <div style="margin-bottom: 20px;"> 1593 | <div style=" 1594 | font-size: 14px; 1595 | font-weight: 600; 1596 | color: #495057; 1597 | margin-bottom: 12px; 1598 | padding: 0 12px; 1599 | ">Product Comparison Table</div> 1600 | 1601 | <div style="overflow-x: auto; margin: 0 12px;"> 1602 | <table style=" 1603 | width: 100%; 1604 | border-collapse: collapse; 1605 | background: white; 1606 | border: 1px solid #e9ecef; 1607 | border-radius: 6px; 1608 | overflow: hidden; 1609 | font-size: 13px; 1610 | "> 1611 | <thead> 1612 | <tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;"> 1613 | <th style=" 1614 | padding: 12px 8px; 1615 | text-align: left; 1616 | font-weight: 600; 1617 | color: #495057; 1618 | border-right: 1px solid #e9ecef; 1619 | min-width: 150px; 1620 | ">Product</th> 1621 | <th style=" 1622 | padding: 12px 8px; 1623 | text-align: center; 1624 | font-weight: 600; 1625 | color: #495057; 1626 | border-right: 1px solid #e9ecef; 1627 | min-width: 80px; 1628 | ">Reviews</th> 1629 | <th style=" 1630 | padding: 12px 8px; 1631 | text-align: center; 1632 | font-weight: 600; 1633 | color: #495057; 1634 | border-right: 1px solid #e9ecef; 1635 | min-width: 120px; 1636 | ">Sentiment</th> 1637 | <th style=" 1638 | padding: 12px 8px; 1639 | text-align: left; 1640 | font-weight: 600; 1641 | color: #495057; 1642 | border-right: 1px solid #e9ecef; 1643 | min-width: 200px; 1644 | ">Top Themes</th> 1645 | <th style=" 1646 | padding: 12px 8px; 1647 | text-align: center; 1648 | font-weight: 600; 1649 | color: #495057; 1650 | min-width: 80px; 1651 | ">Links</th> 1652 | </tr> 1653 | </thead> 1654 | <tbody> 1655 | `; 1656 | 1657 | successfulResults.forEach((result, index) => { 1658 | const data = result.data; 1659 | 1660 | // Calculate sentiment distribution 1661 | const sentimentCounts = data.reviews.reduce((acc, review) => { 1662 | acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; 1663 | return acc; 1664 | }, {}); 1665 | 1666 | const totalReviews = data.reviews.length; 1667 | const positiveCount = sentimentCounts.positive || 0; 1668 | const neutralCount = sentimentCounts.neutral || 0; 1669 | const negativeCount = sentimentCounts.negative || 0; 1670 | 1671 | const positivePercent = totalReviews > 0 ? Math.round((positiveCount / totalReviews) * 100) : 0; 1672 | const neutralPercent = totalReviews > 0 ? Math.round((neutralCount / totalReviews) * 100) : 0; 1673 | const negativePercent = totalReviews > 0 ? Math.round((negativeCount / totalReviews) * 100) : 0; 1674 | 1675 | // Get top 3 themes 1676 | const topThemes = data.summary.review_themes.slice(0, 3); 1677 | 1678 | html += ` 1679 | <tr style=" 1680 | border-bottom: 1px solid #f8f9fa; 1681 | ${index % 2 === 0 ? 'background: #fdfdfd;' : 'background: white;'} 1682 | "> 1683 | <td style=" 1684 | padding: 12px 8px; 1685 | border-right: 1px solid #e9ecef; 1686 | vertical-align: top; 1687 | "> 1688 | <div style=" 1689 | font-weight: 600; 1690 | color: #007bff; 1691 | margin-bottom: 4px; 1692 | cursor: pointer; 1693 | " data-product-index="${index}" class="product-name-link">${result.query}</div> 1694 | ${data.rationale ? `<div style=" 1695 | color: #6c757d; 1696 | font-size: 11px; 1697 | line-height: 1.3; 1698 | max-height: 60px; 1699 | overflow: hidden; 1700 | ">${data.rationale.substring(0, 120)}${data.rationale.length > 120 ? '...' : ''}</div>` : ''} 1701 | </td> 1702 | <td style=" 1703 | padding: 12px 8px; 1704 | text-align: center; 1705 | border-right: 1px solid #e9ecef; 1706 | vertical-align: top; 1707 | "> 1708 | <div style="font-weight: 600; color: #495057;">${totalReviews}</div> 1709 | <div style="font-size: 11px; color: #6c757d;">reviews</div> 1710 | </td> 1711 | <td style=" 1712 | padding: 12px 8px; 1713 | text-align: center; 1714 | border-right: 1px solid #e9ecef; 1715 | vertical-align: top; 1716 | "> 1717 | ${totalReviews > 0 ? ` 1718 | <div style=" 1719 | display: flex; 1720 | background: #f8f9fa; 1721 | border-radius: 4px; 1722 | overflow: hidden; 1723 | width: 60px; 1724 | height: 6px; 1725 | margin: 0 auto 4px auto; 1726 | border: 1px solid #e9ecef; 1727 | "> 1728 | ${positiveCount > 0 ? `<div style=" 1729 | background: #28a745; 1730 | width: ${positivePercent}%; 1731 | height: 100%; 1732 | " title="${positiveCount} positive"></div>` : ''} 1733 | ${neutralCount > 0 ? `<div style=" 1734 | background: #ffc107; 1735 | width: ${neutralPercent}%; 1736 | height: 100%; 1737 | " title="${neutralCount} neutral"></div>` : ''} 1738 | ${negativeCount > 0 ? `<div style=" 1739 | background: #dc3545; 1740 | width: ${negativePercent}%; 1741 | height: 100%; 1742 | " title="${negativeCount} negative"></div>` : ''} 1743 | </div> 1744 | <div style=" 1745 | font-size: 10px; 1746 | color: #6c757d; 1747 | line-height: 1.2; 1748 | "> 1749 | <span style="color: #28a745;">●</span>${positivePercent}% 1750 | ${neutralCount > 0 ? `<br><span style="color: #ffc107;">●</span>${neutralPercent}%` : ''} 1751 | ${negativeCount > 0 ? `<br><span style="color: #dc3545;">●</span>${negativePercent}%` : ''} 1752 | </div> 1753 | ` : '<span style="color: #6c757d; font-size: 11px;">No reviews</span>'} 1754 | </td> 1755 | <td style=" 1756 | padding: 12px 8px; 1757 | border-right: 1px solid #e9ecef; 1758 | vertical-align: top; 1759 | "> 1760 | ${topThemes.length > 0 ? topThemes.map(theme => ` 1761 | <span style=" 1762 | display: inline-block; 1763 | background: #e9ecef; 1764 | color: #495057; 1765 | padding: 2px 6px; 1766 | border-radius: 8px; 1767 | font-size: 10px; 1768 | margin: 1px 2px 1px 0; 1769 | ">${theme}</span> 1770 | `).join('') : '<span style="color: #6c757d; font-size: 11px;">No themes</span>'} 1771 | ${data.summary.review_themes.length > 3 ? `<span style="color: #6c757d; font-size: 10px;">+${data.summary.review_themes.length - 3} more</span>` : ''} 1772 | </td> 1773 | <td style=" 1774 | padding: 12px 8px; 1775 | text-align: center; 1776 | vertical-align: top; 1777 | "> 1778 | <div style="font-weight: 600; color: #495057;">${data.productLinks.length}</div> 1779 | <div style="font-size: 11px; color: #6c757d;">links</div> 1780 | ${(data.reviews.length > 0 || data.products.length > 0 || data.rationale || data.reviewSummary) ? ` 1781 | <button data-product-index="${index}" class="view-details-btn" style=" 1782 | background: #007bff; 1783 | color: white; 1784 | border: none; 1785 | padding: 2px 6px; 1786 | border-radius: 3px; 1787 | font-size: 10px; 1788 | cursor: pointer; 1789 | margin-top: 4px; 1790 | ">View</button> 1791 | ` : data.productLinks.length > 0 ? ` 1792 | <button data-product-index="${index}" class="view-details-btn" style=" 1793 | background: #ffc107; 1794 | color: #212529; 1795 | border: none; 1796 | padding: 2px 6px; 1797 | border-radius: 3px; 1798 | font-size: 10px; 1799 | cursor: pointer; 1800 | margin-top: 4px; 1801 | ">Links</button> 1802 | ` : ''} 1803 | </td> 1804 | </tr> 1805 | `; 1806 | }); 1807 | 1808 | html += ` 1809 | </tbody> 1810 | </table> 1811 | </div> 1812 | </div> 1813 | `; 1814 | 1815 | // Add detailed results section (initially hidden) 1816 | html += ` 1817 | <div id="detailed-results" style="display: none;"> 1818 | <div style=" 1819 | font-size: 14px; 1820 | font-weight: 600; 1821 | color: #495057; 1822 | margin-bottom: 12px; 1823 | padding: 0 12px; 1824 | ">Detailed Product Information</div> 1825 | <div id="detailed-content"></div> 1826 | </div> 1827 | `; 1828 | 1829 | resultsContainer.innerHTML = html; 1830 | 1831 | // Store results for detailed view 1832 | window.multiSearchResults = successfulResults; 1833 | 1834 | // Add event listeners for product details links 1835 | const productNameLinks = document.querySelectorAll('.product-name-link'); 1836 | productNameLinks.forEach(link => { 1837 | link.addEventListener('click', function() { 1838 | const index = parseInt(this.getAttribute('data-product-index')); 1839 | showProductDetails(index); 1840 | }); 1841 | }); 1842 | 1843 | const viewDetailsBtns = document.querySelectorAll('.view-details-btn'); 1844 | viewDetailsBtns.forEach(btn => { 1845 | btn.addEventListener('click', function() { 1846 | const index = parseInt(this.getAttribute('data-product-index')); 1847 | showProductDetails(index); 1848 | }); 1849 | }); 1850 | } 1851 | 1852 | // Function to show detailed product information 1853 | window.showProductDetails = function(index) { 1854 | const detailedResults = document.getElementById('detailed-results'); 1855 | const detailedContent = document.getElementById('detailed-content'); 1856 | 1857 | if (!window.multiSearchResults || !detailedResults || !detailedContent) { 1858 | return; 1859 | } 1860 | 1861 | const result = window.multiSearchResults[index]; 1862 | if (!result) { 1863 | return; 1864 | } 1865 | 1866 | const productName = result.query; 1867 | 1868 | // Display the single product result using existing function 1869 | const tempContainer = document.createElement('div'); 1870 | tempContainer.innerHTML = ''; 1871 | 1872 | // Use the existing displayResults function but capture its output 1873 | const originalContainer = document.getElementById('results-container'); 1874 | const tempId = 'temp-results-container'; 1875 | tempContainer.id = tempId; 1876 | document.body.appendChild(tempContainer); 1877 | 1878 | // Temporarily replace the results container 1879 | const originalGetElementById = document.getElementById; 1880 | document.getElementById = function(id) { 1881 | if (id === 'results-container') { 1882 | return tempContainer; 1883 | } 1884 | return originalGetElementById.call(document, id); 1885 | }; 1886 | 1887 | displayResults(result.data, result.query); 1888 | 1889 | // Restore original function 1890 | document.getElementById = originalGetElementById; 1891 | 1892 | // Move the content to detailed view 1893 | detailedContent.innerHTML = ` 1894 | <div style=" 1895 | background: #f8f9fa; 1896 | padding: 12px; 1897 | margin-bottom: 16px; 1898 | border-radius: 6px; 1899 | border-left: 4px solid #007bff; 1900 | "> 1901 | <div style=" 1902 | display: flex; 1903 | justify-content: space-between; 1904 | align-items: center; 1905 | "> 1906 | <h3 style="margin: 0; color: #495057;">Detailed view: ${productName}</h3> 1907 | <button class="close-details-btn" style=" 1908 | background: #6c757d; 1909 | color: white; 1910 | border: none; 1911 | padding: 4px 8px; 1912 | border-radius: 3px; 1913 | font-size: 12px; 1914 | cursor: pointer; 1915 | ">Close</button> 1916 | </div> 1917 | </div> 1918 | ${tempContainer.innerHTML} 1919 | `; 1920 | 1921 | // Clean up 1922 | document.body.removeChild(tempContainer); 1923 | 1924 | // Add event listener for close button 1925 | const closeBtn = detailedContent.querySelector('.close-details-btn'); 1926 | if (closeBtn) { 1927 | closeBtn.addEventListener('click', function() { 1928 | detailedResults.style.display = 'none'; 1929 | }); 1930 | } 1931 | 1932 | // Show detailed results 1933 | detailedResults.style.display = 'block'; 1934 | detailedResults.scrollIntoView({ behavior: 'smooth' }); 1935 | }; 1936 | 1937 | function displayError(message) { 1938 | const resultsContainer = document.getElementById('results-container'); 1939 | if (!resultsContainer) { 1940 | console.error('Results container not found'); 1941 | alert(`Search Error: ${message}`); 1942 | return; 1943 | } 1944 | 1945 | resultsContainer.innerHTML = ` 1946 | <div style=" 1947 | background: #fef2f2; 1948 | color: #991b1b; 1949 | padding: 15px; 1950 | border-radius: 8px; 1951 | border-left: 4px solid #ef4444; 1952 | margin: 20px 0; 1953 | "> 1954 | <h3>Search Error</h3> 1955 | <p>${message}</p> 1956 | <p>Please check your token and try again.</p> 1957 | </div> 1958 | `; 1959 | } 1960 | 1961 | // Initialize token status check 1962 | async function initializeTokenStatus() { 1963 | const tokenInput = document.getElementById('auth-token'); 1964 | const authStatus = document.getElementById('auth-status'); 1965 | 1966 | if (!tokenInput || !authStatus) { 1967 | console.error('Token status elements not found'); 1968 | return; 1969 | } 1970 | 1971 | try { 1972 | const response = await fetch("/api/auth/session"); 1973 | if (response.ok) { 1974 | const sessionData = await response.json(); 1975 | if (sessionData.accessToken) { 1976 | // Update hidden token field 1977 | if (tokenInput) { 1978 | tokenInput.placeholder = "✅ Token ready - session active"; 1979 | tokenInput.style.backgroundColor = "#f0f8ff"; 1980 | tokenInput.style.borderColor = "#007bff"; 1981 | } 1982 | 1983 | // Update visible auth status 1984 | if (authStatus) { 1985 | authStatus.innerHTML = "✅ Ready to search"; 1986 | authStatus.style.background = "#d4edda"; 1987 | authStatus.style.color = "#155724"; 1988 | authStatus.style.borderColor = "#c3e6cb"; 1989 | } 1990 | 1991 | } else { 1992 | throw new Error("No access token in session"); 1993 | } 1994 | } else { 1995 | throw new Error(`Session check failed: ${response.status}`); 1996 | } 1997 | } catch (error) { 1998 | // Update hidden token field 1999 | if (tokenInput) { 2000 | tokenInput.placeholder = "❌ Please log in to ChatGPT first"; 2001 | tokenInput.style.backgroundColor = "#fff5f5"; 2002 | tokenInput.style.borderColor = "#ef4444"; 2003 | } 2004 | 2005 | // Update visible auth status 2006 | if (authStatus) { 2007 | authStatus.innerHTML = "❌ Please log in to ChatGPT first"; 2008 | authStatus.style.background = "#f8d7da"; 2009 | authStatus.style.color = "#721c24"; 2010 | authStatus.style.borderColor = "#f5c6cb"; 2011 | } 2012 | 2013 | } 2014 | } 2015 | 2016 | // Function to create floating button 2017 | function createFloatingButton() { 2018 | const button = document.createElement('button'); 2019 | button.id = 'openProductSearchModalBtn'; 2020 | button.innerHTML = '🛍️'; 2021 | button.title = 'Open ChatGPT Product Info Search'; 2022 | document.body.appendChild(button); 2023 | return button; 2024 | } 2025 | 2026 | // Add styles to the page 2027 | const styleSheet = document.createElement("style"); 2028 | styleSheet.type = "text/css"; 2029 | styleSheet.innerText = floatingButtonCSS; 2030 | document.head.appendChild(styleSheet); 2031 | 2032 | // Create floating button and add click handler 2033 | const floatingButton = createFloatingButton(); 2034 | floatingButton.addEventListener('click', createModal); 2035 | 2036 | })(); ```