This is page 4 of 5. Use http://codebase.md/Xyborg/ChatGPT-Product-Info?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 // ChatGPT Product Info Search // Paste this entire script into ChatGPT's browser console and it will create a floating button (function() { // Remove existing modal and button if present const existingModal = document.getElementById('chatgpt-product-search-modal'); const existingButton = document.getElementById('openProductSearchModalBtn'); if (existingModal) { existingModal.remove(); } if (existingButton) { existingButton.remove(); } // Add floating button styles const floatingButtonCSS = ` #openProductSearchModalBtn { position: fixed; right: 20px; top: 40%; transform: translateY(-50%); background-color: #007bff; color: white; border: none; border-radius: 50%; width: 60px; height: 60px; font-size: 24px; box-shadow: 0 3px 10px rgba(0,0,0,0.2); cursor: pointer; z-index: 9990; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; } #openProductSearchModalBtn:hover { background-color: #0056b3; box-shadow: 0 5px 15px rgba(0,0,0,0.3); } `; // Create modal HTML const modalHTML = ` <div id="chatgpt-product-search-modal" style=" position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgb(230 237 248 / 80%); display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; "> <div style=" background: white; width: 90%; height:85%; border-radius: 8px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); "> <div style=" background: #f8f9fa; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e9ecef; "> <h1 style=" font-size: 18px; font-weight: 600; margin: 0; color: #495057; ">🔍 ChatGPT Product Info Search</h1> <button id="close-modal-btn" style=" background: none; border: none; color: #6c757d; font-size: 20px; width: 30px; height: 30px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; ">×</button> </div> <div style=" flex: 1; display: flex; flex-direction: column; overflow: hidden; "> <div id="search-area" style=" position: relative; padding: 20px; border-bottom: 1px solid #e9ecef; background: white; transition: all 0.3s ease; "> <!-- Collapse/Expand Button - positioned absolutely --> <div id="collapse-toggle" style=" display: none; position: absolute; top: 8px; right: 20px; cursor: pointer; color: #007bff; font-size: 12px; font-weight: 500; transition: all 0.2s ease; border-radius: 4px; padding: 4px 8px; background: rgba(0, 123, 255, 0.1); border: 1px solid rgba(0, 123, 255, 0.2); z-index: 10; "> <span id="collapse-text">▲ Hide</span> </div> <div id="search-controls"> <!-- Multi-product toggle --> <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding: 8px 0;"> <label style=" display: flex; align-items: center; gap: 8px; font-size: 14px; color: #495057; font-weight: 500; cursor: pointer; "> <div style=" position: relative; width: 44px; height: 24px; background: #dee2e6; border-radius: 12px; transition: background 0.3s ease; cursor: pointer; " id="toggle-background"> <input type="checkbox" id="multi-product-toggle" style=" position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; " /> <div style=" position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.2); " id="toggle-slider"></div> </div> Multi-product search </label> <div style=" font-size: 12px; color: #6c757d; font-style: italic; ">Search multiple products at once</div> </div> <!-- Single product input --> <div id="single-product-input" style="display: flex; gap: 12px; margin-bottom: 12px;"> <input type="text" id="search-query" placeholder="Search query (e.g., iPhone 17, Nike shoes, Pets Deli Hundefutter)" style=" flex: 1; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px; box-sizing: border-box; " /> <button id="search-btn" style=" background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; white-space: nowrap; ">Search</button> </div> <!-- Multi product input --> <div id="multi-product-input" style="display: none; margin-bottom: 12px;"> <textarea id="multi-search-query" placeholder="Enter product names, one per line: iPhone 17 Pro Samsung Galaxy S25 Google Pixel 9" style=" width: 100%; min-height: 100px; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px; box-sizing: border-box; resize: vertical; font-family: inherit; margin-bottom: 8px; "></textarea> <div style="display: flex; gap: 12px;"> <button id="multi-search-btn" style=" background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; white-space: nowrap; ">Search All Products</button> <div style=" font-size: 12px; color: #6c757d; align-self: center; font-style: italic; ">Results will be shown in a table format</div> </div> </div> </div> <!-- End search-controls --> <!-- Hidden token field for status display --> <input type="password" id="auth-token" placeholder="Token will be fetched automatically" readonly style=" display: none; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px; box-sizing: border-box; background-color: #f9f9f9; cursor: not-allowed; " /> </div> <div id="results-container" style=" flex: 1; overflow-y: auto; padding: 20px; "> <div id="welcome-state" style=" text-align: center; padding: 60px 40px; color: #6c757d; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; min-height: 300px; "> <div style=" font-size: 48px; margin-bottom: 20px; opacity: 0.7; ">🔍</div> <h3 style=" margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #495057; ">Product Search</h3> <p style=" margin: 0 0 24px 0; font-size: 16px; line-height: 1.5; max-width: 400px; ">Search for product reviews, comparisons, and detailed information from across the web</p> <div style=" padding: 16px 20px; border-left: 4px solid #007bff; max-width: 500px; text-align: left; "> <div style="font-weight: 600; margin-bottom: 8px; color: #495057;">Try searching for:</div> <div style="color: #6c757d; font-size: 14px; line-height: 1.4;"> • "iPhone 17 Pro camera quality"<br> • "Nike Air Max running shoes"<br> • "MacBook Air M3 performance"<br> • "Tesla Model 3 reviews" </div> </div> <div id="auth-status" style=" margin-top: 20px; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb; ">🔐 Checking authentication...</div> </div> </div> </div> <!-- Fixed Footer --> <div style=" position: fixed; bottom: 0; left: 0; right: 0; background: #f8f9fa; border-top: 1px solid #e9ecef; padding: 8px 0; text-align: center; font-size: 14px; z-index: 10001; "> Created by <a href="https://www.martinaberastegue.com/" target="_blank" rel="noopener noreferrer">Martin Aberastegue (@Xyborg)</a> </div> </div> </div> `; // Function to create and show modal function createModal() { // Remove existing modal if present const existingModal = document.getElementById('chatgpt-product-search-modal'); if (existingModal) { existingModal.remove(); } // Inject modal into page document.body.insertAdjacentHTML('beforeend', modalHTML); // Get modal elements const modal = document.getElementById('chatgpt-product-search-modal'); const closeBtn = document.getElementById('close-modal-btn'); const searchBtn = document.getElementById('search-btn'); const multiSearchBtn = document.getElementById('multi-search-btn'); const searchQuery = document.getElementById('search-query'); const multiSearchQuery = document.getElementById('multi-search-query'); const authToken = document.getElementById('auth-token'); const resultsContainer = document.getElementById('results-container'); const multiProductToggle = document.getElementById('multi-product-toggle'); const toggleBackground = document.getElementById('toggle-background'); const toggleSlider = document.getElementById('toggle-slider'); const singleProductInput = document.getElementById('single-product-input'); const multiProductInput = document.getElementById('multi-product-input'); const collapseToggle = document.getElementById('collapse-toggle'); const collapseText = document.getElementById('collapse-text'); const searchControls = document.getElementById('search-controls'); // Close modal functionality closeBtn.addEventListener('click', () => { modal.remove(); }); // Close on outside click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); // Close on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { modal.remove(); } }); // Toggle functionality multiProductToggle.addEventListener('change', () => { const isMultiMode = multiProductToggle.checked; if (isMultiMode) { // Switch to multi-product mode singleProductInput.style.display = 'none'; multiProductInput.style.display = 'block'; toggleBackground.style.background = '#007bff'; toggleSlider.style.transform = 'translateX(20px)'; } else { // Switch to single-product mode singleProductInput.style.display = 'flex'; multiProductInput.style.display = 'none'; toggleBackground.style.background = '#dee2e6'; toggleSlider.style.transform = 'translateX(0px)'; } }); // Collapse/Expand functionality collapseToggle.addEventListener('click', () => { const isCollapsed = searchControls.style.display === 'none'; if (isCollapsed) { // Expand searchControls.style.display = 'block'; collapseText.textContent = '▲ Hide'; collapseToggle.style.background = 'rgba(0, 123, 255, 0.1)'; collapseToggle.style.border = '1px solid rgba(0, 123, 255, 0.2)'; collapseToggle.style.color = '#007bff'; } else { // Collapse searchControls.style.display = 'none'; collapseText.textContent = '▼ Show'; collapseToggle.style.background = 'rgba(40, 167, 69, 0.1)'; collapseToggle.style.border = '1px solid rgba(40, 167, 69, 0.2)'; collapseToggle.style.color = '#28a745'; } }); // Add hover effects to collapse toggle collapseToggle.addEventListener('mouseenter', () => { const isCollapsed = searchControls.style.display === 'none'; if (isCollapsed) { collapseToggle.style.background = 'rgba(40, 167, 69, 0.2)'; collapseToggle.style.transform = 'scale(1.05)'; } else { collapseToggle.style.background = 'rgba(0, 123, 255, 0.2)'; collapseToggle.style.transform = 'scale(1.05)'; } }); collapseToggle.addEventListener('mouseleave', () => { const isCollapsed = searchControls.style.display === 'none'; if (isCollapsed) { collapseToggle.style.background = 'rgba(40, 167, 69, 0.1)'; } else { collapseToggle.style.background = 'rgba(0, 123, 255, 0.1)'; } collapseToggle.style.transform = 'scale(1)'; }); // Search functionality searchBtn.addEventListener('click', performSearch); multiSearchBtn.addEventListener('click', performMultiSearch); // Enter key support searchQuery.addEventListener('keydown', (e) => { if (e.key === 'Enter') { performSearch(); } }); // Initialize token status initializeTokenStatus(); } // Function to show the collapse toggle after results are displayed function showCollapseToggle() { const collapseToggle = document.getElementById('collapse-toggle'); if (collapseToggle) { collapseToggle.style.display = 'block'; } } async function performSearch() { const searchQuery = document.getElementById('search-query'); const searchBtn = document.getElementById('search-btn'); const resultsContainer = document.getElementById('results-container'); if (!searchQuery || !searchBtn || !resultsContainer) { alert('Modal elements not found. Please try again.'); return; } const query = searchQuery.value.trim(); if (!query) { alert('Please enter a search query'); return; } // Get token automatically let token; try { token = await getAutomaticToken(); } catch (error) { alert('Failed to get authentication token. Please make sure you\'re logged in to ChatGPT.'); return; } // Show loading state searchBtn.disabled = true; searchBtn.textContent = 'Searching...'; resultsContainer.innerHTML = ` <div style="text-align: center; padding: 40px; color: #666;"> <div style=" display: inline-block; width: 32px; height: 32px; border: 3px solid #f3f3f3; border-top: 3px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 10px; "></div> <p>Searching for "${query}"...</p> </div> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> `; try { const result = await searchProduct(query, token); displayResults(result, query); } catch (error) { displayError(error.message); } finally { searchBtn.disabled = false; searchBtn.textContent = 'Search'; // Show collapse toggle after results are displayed showCollapseToggle(); } } async function performMultiSearch() { const multiSearchQuery = document.getElementById('multi-search-query'); const multiSearchBtn = document.getElementById('multi-search-btn'); const resultsContainer = document.getElementById('results-container'); if (!multiSearchQuery || !multiSearchBtn || !resultsContainer) { alert('Modal elements not found. Please try again.'); return; } const queries = multiSearchQuery.value.trim().split('\n').filter(q => q.trim()); if (queries.length === 0) { alert('Please enter at least one product name'); return; } if (queries.length > 10) { alert('Maximum 10 products allowed at once to avoid rate limiting'); return; } // Get token automatically let token; try { token = await getAutomaticToken(); } catch (error) { alert('Failed to get authentication token. Please make sure you\'re logged in to ChatGPT.'); return; } // Show loading state multiSearchBtn.disabled = true; multiSearchBtn.textContent = 'Searching...'; resultsContainer.innerHTML = ` <div style="text-align: center; padding: 40px; color: #666;"> <div style=" display: inline-block; width: 32px; height: 32px; border: 3px solid #f3f3f3; border-top: 3px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 10px; "></div> <p>Searching ${queries.length} products...</p> <div id="progress-status" style="font-size: 14px; color: #999; margin-top: 10px;"> Starting searches... </div> </div> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> `; const results = []; const progressStatus = document.getElementById('progress-status'); try { // Search products one by one for (let i = 0; i < queries.length; i++) { const query = queries[i].trim(); if (progressStatus) { progressStatus.textContent = `Searching "${query}" (${i + 1}/${queries.length})...`; } try { const result = await searchProduct(query, token); results.push({ query: query, success: true, data: result }); } catch (error) { results.push({ query: query, success: false, error: error.message }); } // Add a small delay between requests to be respectful if (i < queries.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } displayMultiResults(results); } catch (error) { displayError(error.message); } finally { multiSearchBtn.disabled = false; multiSearchBtn.textContent = 'Search All Products'; // Show collapse toggle after results are displayed showCollapseToggle(); } } async function getAutomaticToken() { try { const response = await fetch("/api/auth/session"); if (!response.ok) { throw new Error(`Session API responded with: ${response.status} ${response.statusText}`); } const sessionData = await response.json(); if (!sessionData.accessToken) { throw new Error("No access token found in session. Please make sure you're logged in to ChatGPT."); } // Update the token input field to show it's been fetched const tokenInput = document.getElementById('auth-token'); if (tokenInput) { tokenInput.placeholder = "✅ Token fetched from session"; tokenInput.style.backgroundColor = "#f0f8ff"; tokenInput.style.borderColor = "#007bff"; } return sessionData.accessToken; } catch (error) { // Update the token input field to show error const tokenInput = document.getElementById('auth-token'); if (tokenInput) { tokenInput.placeholder = "❌ Failed to fetch token automatically"; tokenInput.style.backgroundColor = "#fff5f5"; tokenInput.style.borderColor = "#ef4444"; tokenInput.readOnly = false; tokenInput.style.cursor = "text"; } throw error; } } async function searchProduct(query, token) { const requestBody = { "conversation_id": "", "is_client_thread": true, "message_id": "", "product_query": query, "supported_encodings": ["v1"], "product_lookup_key": { "data": JSON.stringify({ "request_query": query, "all_ids": {"p2": [""]}, "known_ids": {}, "metadata_sources": ["p1", "p3"], "variant_sources": null, "last_variant_group_types": null, "merchant_hints": [], "provider_title": query }), "version": "1", "variant_options_query": null } }; const response = await fetch("https://chatgpt.com/backend-api/search/product_info", { "headers": { "accept": "text/event-stream", "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", "authorization": "Bearer " + token, "content-type": "application/json", "oai-client-version": "prod-43c98f917bf2c3e3a36183e9548cd048e4e40615", "oai-device-id": generateDeviceId(), "oai-language": "en-US", "priority": "u=1, i", "sec-ch-ua": '"Opera";v="120", "Not-A.Brand";v="8", "Chromium";v="135"', "sec-ch-ua-arch": '"arm"', "sec-ch-ua-bitness": '"64"', "sec-ch-ua-full-version": '"120.0.5543.161"', "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"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-model": '""', "sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform-version": '"15.5.0"', "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, "referrer": window.location.href, "referrerPolicy": "strict-origin-when-cross-origin", "body": JSON.stringify(requestBody), "method": "POST", "mode": "cors", "credentials": "include" }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const responseText = await response.text(); return parseProductInfo(responseText); } function parseProductInfo(content) { const products = []; const reviews = []; const productLinks = []; // Store product links from grouped_citation const rationaleObj = { text: '' }; // Track the current rationale being built (using object for reference) const citations = new Map(); // Store citations by cite key const summaryObj = { text: '' }; // Track the current summary being built (using object for reference) const lines = content.split('\n'); let currentEvent = null; let currentData = []; let eventCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('event: ')) { if (currentEvent && currentData.length > 0) { processEvent(currentEvent, currentData.join('\n'), products, reviews, productLinks, rationaleObj, eventCount, citations, summaryObj); eventCount++; } currentEvent = line.replace('event: ', '').trim(); currentData = []; } else if (line.startsWith('data: ')) { currentData.push(line.replace('data: ', '')); } else if (line.trim() === '') { continue; } else { currentData.push(line); } } if (currentEvent && currentData.length > 0) { processEvent(currentEvent, currentData.join('\n'), products, reviews, productLinks, rationaleObj, eventCount, citations, summaryObj); eventCount++; } // Map citations to reviews for (const review of reviews) { if (review.cite && citations.has(review.cite)) { review.url = citations.get(review.cite).url; } } const uniqueMerchants = new Set(); for (const product of products) { for (const offer of product.offers || []) { if (offer.merchant_name) { uniqueMerchants.add(offer.merchant_name); } } } const reviewThemes = [...new Set(reviews.map(review => review.theme))]; return { products: products, productLinks: productLinks, // Add detected product links reviews: reviews, rationale: rationaleObj.text || null, reviewSummary: summaryObj.text || null, // Add the built summary summary: { total_products: products.length, total_product_links: productLinks.length, total_reviews: reviews.length, unique_merchants: uniqueMerchants.size, review_themes: reviewThemes } }; } function processEvent(eventType, dataStr, products, reviews, productLinks, rationaleObj, eventIndex, citations, summaryObj) { if (eventType !== 'delta' || !dataStr || dataStr === '""') { return; } try { const data = JSON.parse(dataStr); if (typeof data !== 'object' || data === null) { return; } // Track processed patches to avoid duplicates const processedPatches = new Set(); // Handle direct patch objects (like your examples) if (data.p === '/grouped_citation' && data.o === 'replace' && data.v && data.v.url) { eventIndex++; const citeKey = `turn0search${eventIndex}`; citations.set(citeKey, { url: data.v.url, title: data.v.title || '' }); // Remove any existing product link with the same URL to avoid duplicates const existingLinkIndex = productLinks.findIndex(link => link.url === data.v.url); // Capture ALL grouped_citation objects as product links const productLink = { title: data.v.title || '', url: data.v.url, snippet: data.v.snippet || '', source: extractDomainFromUrl(data.v.url) }; if (existingLinkIndex >= 0) { // Update existing link with potentially better title productLinks[existingLinkIndex] = productLink; } else { // Add new link productLinks.push(productLink); } } // Handle direct rationale patches if (data.p === '/rationale' && data.o === 'append' && data.v) { rationaleObj.text += data.v; } // Handle direct summary patches if (data.p === '/summary' && data.o === 'append' && data.v) { summaryObj.text += data.v; } // Capture citations from cite_map and rationale patches if (data.v && Array.isArray(data.v)) { for (const patch of data.v) { if (patch.p && patch.p.startsWith('/cite_map/') && patch.o === 'add' && patch.v && patch.v.url) { const citeKey = patch.p.replace('/cite_map/', ''); citations.set(citeKey, { url: patch.v.url, title: patch.v.title || '' }); } // Capture grouped_citation from data.v array (like your example!) if (patch.p === '/grouped_citation' && patch.o === 'replace' && patch.v && patch.v.url) { eventIndex++; const citeKey = `turn0search${eventIndex}`; citations.set(citeKey, { url: patch.v.url, title: patch.v.title || '' }); // Remove any existing product link with the same URL to avoid duplicates const existingLinkIndex = productLinks.findIndex(link => link.url === patch.v.url); // Capture ALL grouped_citation objects as product links const productLink = { title: patch.v.title || '', url: patch.v.url, snippet: patch.v.snippet || '', source: extractDomainFromUrl(patch.v.url) }; if (existingLinkIndex >= 0) { // Update existing link with potentially better title productLinks[existingLinkIndex] = productLink; } else { // Add new link productLinks.push(productLink); } } // Handle individual grouped_citation property updates (like URL and title changes) if (patch.p && patch.p.startsWith('/grouped_citation/') && patch.o === 'replace' && patch.v) { // Find or create the latest citation entry let latestCiteKey = null; let maxCiteNum = -1; // Find the highest numbered citation for (const [key, value] of citations.entries()) { if (key.startsWith('turn0search')) { const num = parseInt(key.replace('turn0search', '')); if (num > maxCiteNum) { maxCiteNum = num; latestCiteKey = key; } } } // If no citation exists yet, create one if (!latestCiteKey) { latestCiteKey = `turn0search${eventIndex}`; citations.set(latestCiteKey, {}); } // Update the citation with the new property const existingCitation = citations.get(latestCiteKey) || {}; if (patch.p === '/grouped_citation/url') { citations.set(latestCiteKey, { ...existingCitation, url: patch.v }); } else if (patch.p === '/grouped_citation/title') { citations.set(latestCiteKey, { ...existingCitation, title: patch.v }); } // Update or add product link if we have both URL and title const updatedCitation = citations.get(latestCiteKey); if (updatedCitation.url) { // Remove any existing product link with the same URL to avoid duplicates const existingLinkIndex = productLinks.findIndex(link => link.url === updatedCitation.url); const productLink = { title: updatedCitation.title || 'Product Link', url: updatedCitation.url, snippet: '', source: extractDomainFromUrl(updatedCitation.url) }; if (existingLinkIndex >= 0) { // Update existing link productLinks[existingLinkIndex] = productLink; } else { // Add new link productLinks.push(productLink); } } } // Capture rationale patches from data.v array if (patch.p === '/rationale' && patch.o === 'append' && patch.v) { const patchKey = `/rationale-${patch.v}`; if (!processedPatches.has(patchKey)) { processedPatches.add(patchKey); rationaleObj.text += patch.v; } } // Capture summary patches from data.v array if (patch.p === '/summary' && patch.o === 'append' && patch.v) { summaryObj.text += patch.v; } } } // Also check patch operations for citations and rationale updates if (data.o === 'patch' && data.v && Array.isArray(data.v)) { for (const patch of data.v) { if (patch.p && patch.p.startsWith('/cite_map/') && patch.o === 'add' && patch.v && patch.v.url) { const citeKey = patch.p.replace('/cite_map/', ''); citations.set(citeKey, { url: patch.v.url, title: patch.v.title || '' }); } if (patch.p === '/grouped_citation' && patch.o === 'replace' && patch.v && patch.v.url) { // This is a citation being added/updated citations.set(`turn0search${eventIndex}`, { url: patch.v.url, title: patch.v.title || '' }); // Remove any existing product link with the same URL to avoid duplicates const existingLinkIndex = productLinks.findIndex(link => link.url === patch.v.url); // Capture ALL grouped_citation objects as product links const productLink = { title: patch.v.title || '', url: patch.v.url, snippet: patch.v.snippet || '', source: extractDomainFromUrl(patch.v.url) }; if (existingLinkIndex >= 0) { // Update existing link with potentially better title productLinks[existingLinkIndex] = productLink; } else { // Add new link productLinks.push(productLink); } } // Capture rationale patches if (patch.p === '/rationale' && patch.o === 'append' && patch.v) { const patchKey = `/rationale-${patch.v}`; if (!processedPatches.has(patchKey)) { processedPatches.add(patchKey); rationaleObj.text += patch.v; } } // Capture summary patches if (patch.p === '/summary' && patch.o === 'append' && patch.v) { summaryObj.text += patch.v; } } } if (data.v && typeof data.v === 'object' && !Array.isArray(data.v)) { const vData = data.v; if (vData.type === 'product_entity' && vData.product) { const product = vData.product; const productInfo = { title: product.title || '', price: product.price || '', url: product.url || '', merchants: product.merchants || '', description: product.description || null, rating: product.rating || null, num_reviews: product.num_reviews || null, featured_tag: product.featured_tag || null, image_urls: product.image_urls || [], offers: [] }; if (product.offers) { for (const offerData of product.offers) { const offer = { merchant_name: offerData.merchant_name || '', product_name: offerData.product_name || '', url: offerData.url || '', price: offerData.price || '', details: offerData.details || null, available: offerData.available !== false, tag: offerData.tag?.text || null }; productInfo.offers.push(offer); } } productInfo.variants = []; if (product.variants) { for (const variant of product.variants) { const selectedOption = variant.options?.find(opt => opt.selected)?.label || null; productInfo.variants.push({ type: variant.label || '', selected: selectedOption }); } } products.push(productInfo); } else if (vData.type === 'product_reviews') { // Initialize the current summary from the product_reviews object if (vData.summary) { summaryObj.text = vData.summary; } const reviewList = vData.reviews || []; for (const reviewData of reviewList) { const review = { source: reviewData.source || '', theme: reviewData.theme || '', summary: reviewData.summary || '', sentiment: reviewData.sentiment || '', rating: reviewData.rating || null, num_reviews: reviewData.num_reviews || null, cite: reviewData.cite || `turn0search${eventIndex}`, url: null // Will be populated later from citations }; reviews.push(review); } } else if (vData.type === 'product_rationale') { const rationale = vData.rationale || ''; if (rationale) { // Initialize rationale - set the initial text rationaleObj.text = rationale; } } } else if (data.o === 'add' && data.v) { const vData = data.v; if (typeof vData === 'object' && vData.type === 'product_reviews') { const reviewList = vData.reviews || []; for (const reviewData of reviewList) { if (typeof reviewData === 'object') { const review = { source: reviewData.source || '', theme: reviewData.theme || '', summary: reviewData.summary || '', sentiment: reviewData.sentiment || '', rating: reviewData.rating || null, num_reviews: reviewData.num_reviews || null, cite: reviewData.cite || `turn0search${eventIndex}`, url: null // Will be populated later from citations }; reviews.push(review); } } } } else if (data.o === 'patch' && data.v) { const vData = data.v; if (Array.isArray(vData)) { for (const item of vData) { if (typeof item === 'object' && item.p === '/reviews' && item.o === 'append') { const reviewList = item.v || []; if (Array.isArray(reviewList)) { for (const reviewData of reviewList) { if (typeof reviewData === 'object') { const review = { source: reviewData.source || '', theme: reviewData.theme || '', summary: reviewData.summary || '', sentiment: reviewData.sentiment || '', rating: reviewData.rating || null, num_reviews: reviewData.num_reviews || null, cite: reviewData.cite || `turn0search${eventIndex}`, url: null // Will be populated later from citations }; reviews.push(review); } } } } } } } if (data.v && Array.isArray(data.v)) { for (const item of data.v) { if (typeof item === 'object' && item.p === '/reviews' && item.o === 'append') { const reviewList = item.v || []; if (Array.isArray(reviewList)) { for (const reviewData of reviewList) { if (typeof reviewData === 'object') { const review = { source: reviewData.source || '', theme: reviewData.theme || '', summary: reviewData.summary || '', sentiment: reviewData.sentiment || '', rating: reviewData.rating || null, num_reviews: reviewData.num_reviews || null, cite: reviewData.cite || `turn0search${eventIndex}`, url: null // Will be populated later from citations }; reviews.push(review); } } } } } } } catch (jsonError) { return; } } function generateDeviceId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function extractDomainFromUrl(url) { try { const urlObj = new URL(url); return urlObj.hostname.replace('www.', ''); } catch (e) { return url.split('/')[2] || url; } } function displayResults(data, query) { const resultsContainer = document.getElementById('results-container'); if (!resultsContainer) { console.error('Results container not found'); return; } if (!data || (!data.reviews.length && !data.products.length && !data.productLinks.length)) { resultsContainer.innerHTML = ` <div style=" background: #fef2f2; color: #991b1b; padding: 15px; border-radius: 8px; border-left: 4px solid #ef4444; margin: 20px 0; "> <h3>No results found</h3> <p>No products or reviews were found for "${query}". Try a different search term.</p> </div> `; return; } let html = ` <div style=" padding: 12px; margin-bottom: 16px; border-bottom: 1px solid #e9ecef; "> <div style=" font-size: 16px; font-weight: 600; color: #495057; margin-bottom: 8px; ">Results for "${query}"</div> <div style="display: flex; gap: 16px; font-size: 14px; color: #6c757d;"> <span>${data.summary.total_reviews} reviews</span> <span>${data.summary.total_products} products</span> <span>${data.summary.total_product_links} citation links</span> <span>${data.summary.review_themes.length} themes</span> </div> </div> `; // Show message when we have links but no reviews or products if (data.productLinks.length > 0 && data.reviews.length === 0 && data.products.length === 0) { html += ` <div style="margin-bottom: 20px;"> <div style=" background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 12px; margin: 0 12px; color: #856404; font-size: 13px; "> <div style="font-weight: 600; margin-bottom: 4px;">⚠️ Limited Information Available</div> <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> </div> </div> `; } if (data.rationale && data.rationale.trim()) { html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 8px; padding: 0 12px; ">Product Overview</div> <div style=" background: #e2f1ff; padding: 12px; margin: 0 12px; color: #000; line-height: 1.4; font-size: 13px; border-left: 3px solid #3a6797; ">${data.rationale}</div> </div> `; } if (data.reviewSummary && data.reviewSummary.trim()) { html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 8px; padding: 0 12px; ">Review Summary</div> <div style=" background: #f5fee8; padding: 12px; margin: 0 12px; color: #000; line-height: 1.4; font-size: 13px; border-left: 3px solid #93ac71; ">${data.reviewSummary}</div> </div> `; } if (data.productLinks && data.productLinks.length > 0) { html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 8px; padding: 0 12px; ">Citation Links</div> ${data.productLinks.map(link => ` <div style=" border: 1px solid #e9ecef; border-radius: 6px; padding: 12px; margin: 0 12px 8px 12px; background: #f8f9fa; "> <div style=" display: flex; align-items: center; margin-bottom: 6px; gap: 8px; "> <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=" width: 16px; height: 16px; border-radius: 2px; flex-shrink: 0; " onerror="this.style.display='none'"> <span style=" font-weight: 600; color: #007bff; font-size: 14px; ">${link.title}</span> <a href="${link.url}" target="_blank" style=" color: #28a745; text-decoration: none; font-size: 14px; margin-left: auto; background: #d4edda; padding: 4px 8px; border-radius: 4px; border: 1px solid #c3e6cb; " title="Visit citation page">↗</a> </div> ${link.snippet ? `<div style=" color: #6c757d; font-size: 12px; line-height: 1.3; margin-top: 4px; ">${link.snippet}</div>` : ''} </div> `).join('')} </div> `; } if (data.reviews.length > 0) { // Calculate sentiment distribution const sentimentCounts = data.reviews.reduce((acc, review) => { acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; return acc; }, {}); const totalReviews = data.reviews.length; const positiveCount = sentimentCounts.positive || 0; const neutralCount = sentimentCounts.neutral || 0; const negativeCount = sentimentCounts.negative || 0; const positivePercent = Math.round((positiveCount / totalReviews) * 100); const neutralPercent = Math.round((neutralCount / totalReviews) * 100); const negativePercent = Math.round((negativeCount / totalReviews) * 100); html += ` <div> <div style=" display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding: 0 12px; "> <div style=" font-size: 14px; font-weight: 600; color: #495057; ">Reviews</div> <div style="display: flex; align-items: center; gap: 12px;"> <div style=" display: flex; background: #f8f9fa; border-radius: 8px; overflow: hidden; width: 100px; height: 6px; border: 1px solid #e9ecef; "> ${positiveCount > 0 ? `<div style=" background: #28a745; width: ${positivePercent}%; height: 100%; " title="${positiveCount} positive (${positivePercent}%)"></div>` : ''} ${neutralCount > 0 ? `<div style=" background: #ffc107; width: ${neutralPercent}%; height: 100%; " title="${neutralCount} neutral (${neutralPercent}%)"></div>` : ''} ${negativeCount > 0 ? `<div style=" background: #dc3545; width: ${negativePercent}%; height: 100%; " title="${negativeCount} negative (${negativePercent}%)"></div>` : ''} </div> <div style=" font-size: 12px; color: #6c757d; white-space: nowrap; "> <span style="color: #28a745;">●</span> ${positivePercent}% ${neutralCount > 0 ? `<span style="color: #ffc107; margin-left: 6px;">●</span> ${neutralPercent}%` : ''} ${negativeCount > 0 ? `<span style="color: #dc3545; margin-left: 6px;">●</span> ${negativePercent}%` : ''} </div> </div> </div> ${data.reviews.map(review => { const sentimentColor = review.sentiment === 'positive' ? '#28a745' : review.sentiment === 'negative' ? '#dc3545' : '#ffc107'; return ` <div style=" border-bottom: 1px solid #f8f9fa; padding: 12px; margin-bottom: 1px; "> <div style=" display: flex; align-items: center; margin-bottom: 8px; gap: 8px; "> <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=" width: 16px; height: 16px; border-radius: 2px; flex-shrink: 0; " onerror="this.style.display='none'"> <span style="font-weight: 500; color: #495057; font-size: 14px;">${review.source}</span> ${review.url ? `<a href="${review.url}" target="_blank" style=" color: #6c757d; text-decoration: none; font-size: 12px; margin-left: auto; " title="Open source">↗</a>` : ''} <span style=" width: 8px; height: 8px; border-radius: 50%; background: ${sentimentColor}; display: inline-block; margin-left: ${review.url ? '4px' : 'auto'}; "></span> </div> <div style=" font-size: 13px; font-weight: 600; color: #007bff; margin-bottom: 6px; ">${review.theme}</div> <div style="color: #6c757d; line-height: 1.4; font-size: 13px;">${review.summary}</div> </div> `; }).join('')} </div> `; } if (data.summary.review_themes.length > 0) { html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 8px; padding: 0 12px; ">Themes</div> <div style="display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px;"> ${data.summary.review_themes.map(theme => `<span style=" background: #e9ecef; color: #495057; padding: 4px 8px; border-radius: 12px; font-size: 12px; ">${theme}</span>` ).join('')} </div> </div> `; } resultsContainer.innerHTML = html; } function displayMultiResults(results) { const resultsContainer = document.getElementById('results-container'); if (!resultsContainer) { console.error('Results container not found'); return; } const successfulResults = results.filter(r => r.success); const failedResults = results.filter(r => !r.success); let html = ` <div style=" padding: 12px; margin-bottom: 16px; border-bottom: 1px solid #e9ecef; "> <div style=" font-size: 16px; font-weight: 600; color: #495057; margin-bottom: 8px; ">Multi-Product Search Results</div> <div style="display: flex; gap: 16px; font-size: 14px; color: #6c757d;"> <span>${successfulResults.length}/${results.length} products found</span> ${failedResults.length > 0 ? `<span style="color: #dc3545;">${failedResults.length} failed</span>` : ''} </div> </div> `; if (failedResults.length > 0) { html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #dc3545; margin-bottom: 8px; padding: 0 12px; ">Search Errors</div> <div style=" background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; padding: 12px; margin: 0 12px; color: #721c24; font-size: 13px; "> ${failedResults.map(r => `<div><strong>${r.query}:</strong> ${r.error}</div>`).join('<br>')} </div> </div> `; } if (successfulResults.length === 0) { html += ` <div style=" text-align: center; padding: 60px 40px; color: #6c757d; "> <div style="font-size: 48px; margin-bottom: 20px; opacity: 0.7;">❌</div> <h3 style="margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #495057;">No Results Found</h3> <p style="margin: 0; font-size: 16px; line-height: 1.5;">None of the products could be found. Please try different search terms.</p> </div> `; resultsContainer.innerHTML = html; return; } // Create comparison table html += ` <div style="margin-bottom: 20px;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 12px; padding: 0 12px; ">Product Comparison Table</div> <div style="overflow-x: auto; margin: 0 12px;"> <table style=" width: 100%; border-collapse: collapse; background: white; border: 1px solid #e9ecef; border-radius: 6px; overflow: hidden; font-size: 13px; "> <thead> <tr style="background: #f8f9fa; border-bottom: 2px solid #e9ecef;"> <th style=" padding: 12px 8px; text-align: left; font-weight: 600; color: #495057; border-right: 1px solid #e9ecef; min-width: 150px; ">Product</th> <th style=" padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; border-right: 1px solid #e9ecef; min-width: 80px; ">Reviews</th> <th style=" padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; border-right: 1px solid #e9ecef; min-width: 120px; ">Sentiment</th> <th style=" padding: 12px 8px; text-align: left; font-weight: 600; color: #495057; border-right: 1px solid #e9ecef; min-width: 200px; ">Top Themes</th> <th style=" padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; min-width: 80px; ">Links</th> </tr> </thead> <tbody> `; successfulResults.forEach((result, index) => { const data = result.data; // Calculate sentiment distribution const sentimentCounts = data.reviews.reduce((acc, review) => { acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; return acc; }, {}); const totalReviews = data.reviews.length; const positiveCount = sentimentCounts.positive || 0; const neutralCount = sentimentCounts.neutral || 0; const negativeCount = sentimentCounts.negative || 0; const positivePercent = totalReviews > 0 ? Math.round((positiveCount / totalReviews) * 100) : 0; const neutralPercent = totalReviews > 0 ? Math.round((neutralCount / totalReviews) * 100) : 0; const negativePercent = totalReviews > 0 ? Math.round((negativeCount / totalReviews) * 100) : 0; // Get top 3 themes const topThemes = data.summary.review_themes.slice(0, 3); html += ` <tr style=" border-bottom: 1px solid #f8f9fa; ${index % 2 === 0 ? 'background: #fdfdfd;' : 'background: white;'} "> <td style=" padding: 12px 8px; border-right: 1px solid #e9ecef; vertical-align: top; "> <div style=" font-weight: 600; color: #007bff; margin-bottom: 4px; cursor: pointer; " data-product-index="${index}" class="product-name-link">${result.query}</div> ${data.rationale ? `<div style=" color: #6c757d; font-size: 11px; line-height: 1.3; max-height: 60px; overflow: hidden; ">${data.rationale.substring(0, 120)}${data.rationale.length > 120 ? '...' : ''}</div>` : ''} </td> <td style=" padding: 12px 8px; text-align: center; border-right: 1px solid #e9ecef; vertical-align: top; "> <div style="font-weight: 600; color: #495057;">${totalReviews}</div> <div style="font-size: 11px; color: #6c757d;">reviews</div> </td> <td style=" padding: 12px 8px; text-align: center; border-right: 1px solid #e9ecef; vertical-align: top; "> ${totalReviews > 0 ? ` <div style=" display: flex; background: #f8f9fa; border-radius: 4px; overflow: hidden; width: 60px; height: 6px; margin: 0 auto 4px auto; border: 1px solid #e9ecef; "> ${positiveCount > 0 ? `<div style=" background: #28a745; width: ${positivePercent}%; height: 100%; " title="${positiveCount} positive"></div>` : ''} ${neutralCount > 0 ? `<div style=" background: #ffc107; width: ${neutralPercent}%; height: 100%; " title="${neutralCount} neutral"></div>` : ''} ${negativeCount > 0 ? `<div style=" background: #dc3545; width: ${negativePercent}%; height: 100%; " title="${negativeCount} negative"></div>` : ''} </div> <div style=" font-size: 10px; color: #6c757d; line-height: 1.2; "> <span style="color: #28a745;">●</span>${positivePercent}% ${neutralCount > 0 ? `<br><span style="color: #ffc107;">●</span>${neutralPercent}%` : ''} ${negativeCount > 0 ? `<br><span style="color: #dc3545;">●</span>${negativePercent}%` : ''} </div> ` : '<span style="color: #6c757d; font-size: 11px;">No reviews</span>'} </td> <td style=" padding: 12px 8px; border-right: 1px solid #e9ecef; vertical-align: top; "> ${topThemes.length > 0 ? topThemes.map(theme => ` <span style=" display: inline-block; background: #e9ecef; color: #495057; padding: 2px 6px; border-radius: 8px; font-size: 10px; margin: 1px 2px 1px 0; ">${theme}</span> `).join('') : '<span style="color: #6c757d; font-size: 11px;">No themes</span>'} ${data.summary.review_themes.length > 3 ? `<span style="color: #6c757d; font-size: 10px;">+${data.summary.review_themes.length - 3} more</span>` : ''} </td> <td style=" padding: 12px 8px; text-align: center; vertical-align: top; "> <div style="font-weight: 600; color: #495057;">${data.productLinks.length}</div> <div style="font-size: 11px; color: #6c757d;">links</div> ${(data.reviews.length > 0 || data.products.length > 0 || data.rationale || data.reviewSummary) ? ` <button data-product-index="${index}" class="view-details-btn" style=" background: #007bff; color: white; border: none; padding: 2px 6px; border-radius: 3px; font-size: 10px; cursor: pointer; margin-top: 4px; ">View</button> ` : data.productLinks.length > 0 ? ` <button data-product-index="${index}" class="view-details-btn" style=" background: #ffc107; color: #212529; border: none; padding: 2px 6px; border-radius: 3px; font-size: 10px; cursor: pointer; margin-top: 4px; ">Links</button> ` : ''} </td> </tr> `; }); html += ` </tbody> </table> </div> </div> `; // Add detailed results section (initially hidden) html += ` <div id="detailed-results" style="display: none;"> <div style=" font-size: 14px; font-weight: 600; color: #495057; margin-bottom: 12px; padding: 0 12px; ">Detailed Product Information</div> <div id="detailed-content"></div> </div> `; resultsContainer.innerHTML = html; // Store results for detailed view window.multiSearchResults = successfulResults; // Add event listeners for product details links const productNameLinks = document.querySelectorAll('.product-name-link'); productNameLinks.forEach(link => { link.addEventListener('click', function() { const index = parseInt(this.getAttribute('data-product-index')); showProductDetails(index); }); }); const viewDetailsBtns = document.querySelectorAll('.view-details-btn'); viewDetailsBtns.forEach(btn => { btn.addEventListener('click', function() { const index = parseInt(this.getAttribute('data-product-index')); showProductDetails(index); }); }); } // Function to show detailed product information window.showProductDetails = function(index) { const detailedResults = document.getElementById('detailed-results'); const detailedContent = document.getElementById('detailed-content'); if (!window.multiSearchResults || !detailedResults || !detailedContent) { return; } const result = window.multiSearchResults[index]; if (!result) { return; } const productName = result.query; // Display the single product result using existing function const tempContainer = document.createElement('div'); tempContainer.innerHTML = ''; // Use the existing displayResults function but capture its output const originalContainer = document.getElementById('results-container'); const tempId = 'temp-results-container'; tempContainer.id = tempId; document.body.appendChild(tempContainer); // Temporarily replace the results container const originalGetElementById = document.getElementById; document.getElementById = function(id) { if (id === 'results-container') { return tempContainer; } return originalGetElementById.call(document, id); }; displayResults(result.data, result.query); // Restore original function document.getElementById = originalGetElementById; // Move the content to detailed view detailedContent.innerHTML = ` <div style=" background: #f8f9fa; padding: 12px; margin-bottom: 16px; border-radius: 6px; border-left: 4px solid #007bff; "> <div style=" display: flex; justify-content: space-between; align-items: center; "> <h3 style="margin: 0; color: #495057;">Detailed view: ${productName}</h3> <button class="close-details-btn" style=" background: #6c757d; color: white; border: none; padding: 4px 8px; border-radius: 3px; font-size: 12px; cursor: pointer; ">Close</button> </div> </div> ${tempContainer.innerHTML} `; // Clean up document.body.removeChild(tempContainer); // Add event listener for close button const closeBtn = detailedContent.querySelector('.close-details-btn'); if (closeBtn) { closeBtn.addEventListener('click', function() { detailedResults.style.display = 'none'; }); } // Show detailed results detailedResults.style.display = 'block'; detailedResults.scrollIntoView({ behavior: 'smooth' }); }; function displayError(message) { const resultsContainer = document.getElementById('results-container'); if (!resultsContainer) { console.error('Results container not found'); alert(`Search Error: ${message}`); return; } resultsContainer.innerHTML = ` <div style=" background: #fef2f2; color: #991b1b; padding: 15px; border-radius: 8px; border-left: 4px solid #ef4444; margin: 20px 0; "> <h3>Search Error</h3> <p>${message}</p> <p>Please check your token and try again.</p> </div> `; } // Initialize token status check async function initializeTokenStatus() { const tokenInput = document.getElementById('auth-token'); const authStatus = document.getElementById('auth-status'); if (!tokenInput || !authStatus) { console.error('Token status elements not found'); return; } try { const response = await fetch("/api/auth/session"); if (response.ok) { const sessionData = await response.json(); if (sessionData.accessToken) { // Update hidden token field if (tokenInput) { tokenInput.placeholder = "✅ Token ready - session active"; tokenInput.style.backgroundColor = "#f0f8ff"; tokenInput.style.borderColor = "#007bff"; } // Update visible auth status if (authStatus) { authStatus.innerHTML = "✅ Ready to search"; authStatus.style.background = "#d4edda"; authStatus.style.color = "#155724"; authStatus.style.borderColor = "#c3e6cb"; } } else { throw new Error("No access token in session"); } } else { throw new Error(`Session check failed: ${response.status}`); } } catch (error) { // Update hidden token field if (tokenInput) { tokenInput.placeholder = "❌ Please log in to ChatGPT first"; tokenInput.style.backgroundColor = "#fff5f5"; tokenInput.style.borderColor = "#ef4444"; } // Update visible auth status if (authStatus) { authStatus.innerHTML = "❌ Please log in to ChatGPT first"; authStatus.style.background = "#f8d7da"; authStatus.style.color = "#721c24"; authStatus.style.borderColor = "#f5c6cb"; } } } // Function to create floating button function createFloatingButton() { const button = document.createElement('button'); button.id = 'openProductSearchModalBtn'; button.innerHTML = '🛍️'; button.title = 'Open ChatGPT Product Info Search'; document.body.appendChild(button); return button; } // Add styles to the page const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = floatingButtonCSS; document.head.appendChild(styleSheet); // Create floating button and add click handler const floatingButton = createFloatingButton(); floatingButton.addEventListener('click', createModal); })(); ```