This is page 5 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 -------------------------------------------------------------------------------- /chrome-extension/content-script.js: -------------------------------------------------------------------------------- ```javascript // ChatGPT E-commerce Product Research - Chrome Extension Content Script // Automatically injects the product search functionality into ChatGPT (function() { 'use strict'; // Only run on ChatGPT pages if (!window.location.hostname.includes('chatgpt.com')) { return; } // Wait for page to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeExtension); } else { initializeExtension(); } function initializeExtension() { // 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(); } // Migrate existing search history data to new format (Phase 1) migrateSearchHistoryData(); // Resolve extension assets const settingsIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/settings.svg') : 'assets/icons-ui/settings.svg'; const searchIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/search.svg') : 'assets/icons-ui/search.svg'; const historyIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/history.svg') : 'assets/icons-ui/history.svg'; const analysisIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/analysis.svg') : 'assets/icons-ui/analysis.svg'; const projectIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/project.svg') : 'assets/icons-ui/project.svg'; const tagIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/tag.svg') : 'assets/icons-ui/tag.svg'; const databaseIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/database.svg') : 'assets/icons-ui/database.svg'; const editIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/edit.svg') : 'assets/icons-ui/edit.svg'; const positiveIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/positive.svg') : 'assets/icons-ui/positive.svg'; const neutralIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/neutral.svg') : 'assets/icons-ui/neutral.svg'; const negativeIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/negative.svg') : 'assets/icons-ui/negative.svg'; const checkIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/check.svg') : 'assets/icons-ui/check.svg'; const warningIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/warning.svg') : 'assets/icons-ui/warning.svg'; const errorIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/error.svg') : 'assets/icons-ui/error.svg'; const xIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/x.svg') : 'assets/icons-ui/x.svg'; const clockIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="128 72 128 128 184 128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>'; const linkedinIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/linkedin.svg') : 'assets/icons-ui/linkedin.svg'; const githubIconUrl = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('assets/icons-ui/github.svg') : 'assets/icons-ui/github.svg'; function formatStatusMessage(iconType, message, size = 'medium') { const sizeClass = size === 'large' ? 'status-icon--large' : 'status-icon--medium'; return `<span class="status-icon ${sizeClass} status-icon--${iconType}" aria-hidden="true"></span><span>${message}</span>`; } function applyInputStatusStyles(input, { text, iconUrl, color, backgroundColor, borderColor }) { if (!input) { return; } input.placeholder = text; input.style.backgroundColor = backgroundColor; input.style.borderColor = borderColor; input.style.color = color; input.style.backgroundImage = iconUrl ? `url('${iconUrl}')` : ''; input.style.backgroundRepeat = 'no-repeat'; input.style.backgroundPosition = '12px center'; input.style.backgroundSize = '16px'; input.style.paddingLeft = iconUrl ? '36px' : '12px'; } function applyStatusBanner(element, { iconType, text, color, backgroundColor, borderColor }) { if (!element) { return; } element.innerHTML = formatStatusMessage(iconType, text); element.style.display = 'inline-flex'; element.style.alignItems = 'center'; element.style.gap = '8px'; element.style.background = backgroundColor; element.style.color = color; element.style.borderColor = borderColor; } // Create modal HTML const modalHTML = ` <div id="chatgpt-product-search-modal" style=" position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 244, 214, 0.55); backdrop-filter: blur(1.5px); -webkit-backdrop-filter: blur(1.5px); display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 24px; box-sizing: border-box; "> <style> .table-row-hover:hover { background-color: #edf2ff !important; } .sidebar-project { padding: 6px 8px; margin: 1px 0; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-size: 13px; color: #35426b; border: 1px solid transparent; } .sidebar-project:hover { background-color: rgba(91, 141, 239, 0.16) !important; } .sidebar-tag { padding: 4px 8px; margin: 1px 0; border-radius: 12px; cursor: pointer; transition: all 0.2s; font-size: 12px; color: #1f2a52; background-color: rgba(91, 141, 239, 0.22); border: 1px solid rgba(91, 141, 239, 0.35); display: flex; justify-content: space-between; align-items: center; } .sidebar-tag:hover { filter: brightness(1); transform: translateY(-1px); background-color: rgba(91, 141, 239, 0.32); border-color: rgba(91, 141, 239, 0.55); } #toggle-filters:hover { color: #3f6fe0 !important; text-decoration: underline; } .analysis-tag-label:hover { background-color: rgba(91, 141, 239, 0.12) !important; } #help-btn:hover { background: rgba(91, 141, 239, 0.22) !important; color: #1f2a52 !important; transform: translateY(-1px); } #help-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.35); } #settings-btn:hover { background: rgba(91, 141, 239, 0.22) !important; color: #1f2a52 !important; transform: translateY(-1px); } #settings-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.35); } #close-modal-btn:hover { background: rgba(91, 141, 239, 0.28) !important; color: #1f2a52 !important; } #close-modal-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.35); } .status-icon { display: inline-flex; background-color: currentColor; -webkit-mask-size: contain; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; mask-size: contain; mask-repeat: no-repeat; mask-position: center; flex-shrink: 0; } .status-icon--medium { width: 18px; height: 18px; } .status-icon--large { width: 48px; height: 48px; } .status-icon--success { -webkit-mask-image: url('${checkIconUrl}'); mask-image: url('${checkIconUrl}'); } .status-icon--warning { -webkit-mask-image: url('${warningIconUrl}'); mask-image: url('${warningIconUrl}'); } .status-icon--error { -webkit-mask-image: url('${errorIconUrl}'); mask-image: url('${errorIconUrl}'); } .history-list { display: flex; flex-direction: column; gap: 18px; } .history-day-group { display: flex; flex-direction: column; gap: 8px; } .history-day-header { position: sticky; top: 0; z-index: 2; align-self: flex-start; padding: 6px 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: #ffffff; background: rgb(164 155 189); border-radius: 8px; box-shadow: 0 1px 2px rgba(79, 130, 223, 0.08); } .history-day-body { border: 1px solid #e4e9f6; border-radius: 12px; overflow: hidden; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(2px); } .history-row { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) auto; gap: 14px; align-items: center; cursor: pointer; transition: background 0.18s ease, box-shadow 0.18s ease; } .history-row { padding: 10px 14px; } .history-row + .history-row { border-top: 1px solid #e4e8f6; } .history-row:hover { background: rgba(91, 141, 239, 0.04); } .history-row:focus-visible { outline: 2px solid rgba(91, 141, 239, 0.4); outline-offset: -2px; background: rgba(91, 141, 239, 0.08); } .history-row-left { display: flex; flex-direction: column; gap: 8px; min-width: 0; } .history-query-group { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 0; } .history-query-chip { display: inline-flex; align-items: center; gap: 4px; padding: 5px 12px; border-radius: 999px; background: #eef2ff; border: 1px solid #d5ddf5; color: #30426d; font-size: 13px; line-height: 1.25; white-space: nowrap; } .history-query-chip[data-extra="true"] { display: none; } .history-query-group[data-expanded="true"] .history-query-chip[data-extra="true"] { display: inline-flex; } .history-query-toggle { border: 1px dashed #c2cef3; background: #f7f9ff; color: #2f4db5; padding: 5px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; } .history-query-toggle:hover { background: #e8edff; } .history-row-info { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; } .history-row-actions { display: flex; align-items: center; gap: 6px; } .history-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 999px; font-size: 12px; line-height: 1.2; white-space: nowrap; } .history-chip img { width: 12px; height: 12px; } .history-chip--project { background: #f6f7fb; border: 1px solid #e3e7f3; color: #46536d; font-size: 11px; } .history-chip--tag { font-size: 11px; border: 1px solid #e1e6f5; background: #f7f9fd; color: var(--chip-tag-color, #4c63a6); } .history-chip-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--chip-dot-color, currentColor); } .history-chip--tag .history-chip-dot { background: var(--chip-tag-color, #5b8def); } .history-row-labels { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } .history-meta-group { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; font-size: 12px; color: #4a5874; } .history-meta { font-weight: 600; color: #243356; } .history-time { display: inline-flex; align-items: center; gap: 4px; color: #97a1b8; font-weight: 500; letter-spacing: 0.01em; } .history-time svg { width: 14px; height: 14px; } .history-market { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: #4a5874; } .history-market img { width: 16px; height: 12px; border-radius: 2px; box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.08); object-fit: cover; } .history-market-text { display: flex; flex-direction: column; gap: 2px; line-height: 1.1; } .history-market-label { font-weight: 600; color: #243356; } .history-market-language { font-size: 11px; color: #6c7893; } .history-stats { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: #4b5976; } .history-stats span { display: inline-flex; align-items: baseline; gap: 3px; } .history-stats span span { color: #5a6886; font-weight: 500; text-transform: lowercase; } .history-stats strong { color: #1f2d4a; font-weight: 600; } .history-empty-row { padding: 32px 16px; text-align: center; color: #6c7a9b; border: 1px dashed #d4dcf6; border-radius: 12px; background: rgba(255, 255, 255, 0.6); } .history-icon-btn { border: 1px solid transparent; border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 500; background: transparent; color: #4b5972; cursor: pointer; transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease; } .history-icon-btn:hover { background: rgba(91, 141, 239, 0.1); color: #2f4db5; } .history-icon-btn--primary { color: #2f4db5; } .history-icon-btn--danger { color: #c94b59; } .history-icon-btn--danger:hover { background: rgba(201, 75, 89, 0.12); } .history-icon-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(91, 141, 239, 0.35); } .search-input-group { display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: rgba(238, 243, 255, 0.9); border: 1px solid rgba(91, 141, 239, 0.35); border-radius: 20px; flex: 1; transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; } .search-input-group:focus-within { border-color: #5b8def; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.18); background: rgba(255, 255, 255, 0.98); } .search-input-divider { width: 1px; height: 24px; background: rgba(91, 141, 239, 0.3); } .search-input-field { flex: 1; background: transparent; border: none; font-size: 14px; color: #111827; height: 32px; padding: 0; min-width: 0; } .search-input-field:focus { outline: none; } .search-btn { width: 44px; height: 44px; border-radius: 50%; border: none; background: #000000; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; box-shadow: 0 10px 24px rgba(87, 125, 238, 0.25); } .search-btn:hover { transform: translateY(-2px); box-shadow: 0 14px 30px rgba(87, 125, 238, 0.35); background: #000000; } .search-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(121, 161, 255, 0.45); } .search-btn:disabled, .search-btn[data-state="loading"] { cursor: not-allowed; opacity: 0.6; transform: none; box-shadow: none; background: #000000; } .search-btn-icon { width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; } .search-btn-spinner { display: none; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.35); border-top-color: #ffffff; border-radius: 50%; animation: spin 1s linear infinite; } .search-btn[data-state="loading"] .search-btn-icon { display: none; } .search-btn[data-state="loading"] .search-btn-spinner { display: inline-flex; } .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @keyframes spin { to { transform: rotate(360deg); } } .market-select-trigger { display: flex; align-items: center; gap: 8px; border: none; background: white; padding: 6px 12px; border-radius: 14px; cursor: pointer; text-align: left; box-shadow: inset 0 0 0 1px #e5e7eb; transition: box-shadow 0.2s ease, transform 0.2s ease; min-width: 180px; min-height: 32px; white-space: nowrap; } .market-select-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 2px #10a37f; } .market-select-trigger:hover { box-shadow: inset 0 0 0 1px #10a37f, 0 2px 8px rgba(16, 163, 127, 0.12); } .market-select-text { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; flex: 0 1 auto; } .market-select-country { font-size: 13px; font-weight: 600; color: #343a40; line-height: 1.1; } .market-select-language { font-size: 12px; color: #6c757d; line-height: 1.1; } .market-select-caret { margin-left: auto; font-size: 12px; color: #6c757d; } .market-select-dropdown { position: absolute; top: calc(100% + 4px); left: 0; width: 100%; background: white; border: 1px solid #dee2e6; border-radius: 8px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); padding: 4px 0; max-height: 260px; overflow-y: auto; z-index: 25; display: none; } .market-select-dropdown.open { display: block; } .market-select-option { display: flex; align-items: center; gap: 10px; width: 100%; border: none; background: transparent; padding: 8px 12px; font-size: 13px; text-align: left; cursor: pointer; transition: background 0.15s ease; } .market-select-option:focus-visible { outline: none; background: #e7f1ff; } .market-select-option:hover { background: #f1f3f5; } .market-select-option--selected { background: #e9f5ff; } .market-select-option-flag { width: 20px; height: 14px; object-fit: cover; border-radius: 2px; flex-shrink: 0; } .market-select-option-text { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; } .market-select-option-country { font-weight: 600; color: #343a40; line-height: 1.1; } .market-select-option-language { font-size: 12px; color: #6c757d; line-height: 1.1; } </style> <div style=" background: linear-gradient(140deg, #f5f8ff 0%, #ffffff 60%); width: min(1600px, 90vw); height: min(1000px, 90vh); border-radius: 20px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 28px 68px rgba(32, 42, 92, 0.28); border: 1px solid rgba(74, 105, 183, 0.32); position: relative; "> <div style=" background: linear-gradient(90deg, rgba(91, 141, 239, 0.16), rgba(232, 238, 255, 0.9)); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(91, 141, 239, 0.18); "> <h1 style=" font-size: 19px; font-weight: 600; margin: 0; color: #27325f; display: flex; align-items: center; gap: 10px; letter-spacing: 0.15px; "><img src="${searchIconUrl}" alt="Search" style="width: 20px; height: 20px; filter: hue-rotate(18deg) saturate(1.1);" />ChatGPT E-commerce Product Research</h1> <button id="close-modal-btn" style=" border: none; color: #3c4b7c; font-size: 20px; width: 34px; height: 34px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; ">×</button> </div> <div style=" flex: 1; display: flex; flex-direction: row; overflow: hidden; "> <!-- Sidebar Navigation --> <div id="sidebar" style=" width: 200px; min-width: 200px; background: linear-gradient(180deg, rgba(91, 141, 239, 0.14), rgba(232, 238, 255, 0.35)); border-right: 1px solid rgba(91, 141, 239, 0.18); display: flex; flex-direction: column; overflow: hidden; "> <div style=" padding: 14px 18px; background: rgba(255, 255, 255, 0.96); border-bottom: 1px solid rgba(91, 141, 239, 0.18); display: flex; justify-content: space-between; align-items: center; "> <h3 style=" margin: 0; font-size: 14px; font-weight: 600; color: #27325f; ">Organization</h3> <div style="display: flex; gap: 8px;"> <button id="help-btn" style=" background: rgba(91, 141, 239, 0.12); border: none; color: #3c4b7c; font-size: 18px; width: 24px; height: 24px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; " title="Help & Tutorial">?</button> <button id="settings-btn" style=" background: rgba(91, 141, 239, 0.12); border: none; color: #3c4b7c; font-size: 16px; width: 24px; height: 24px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; " title="Settings"><img src="${settingsIconUrl}" alt="Settings" style="width: 18px; height: 18px;" /></button> </div> </div> <div style=" flex: 1; overflow-y: auto; padding: 8px; "> <!-- Projects Section --> <div style="margin-bottom: 16px;"> <div style=" display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; margin-bottom: 4px; "> <span style=" font-size: 12px; font-weight: 600; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; ">Projects</span> <button id="add-project-btn" style=" background: none; border: none; color: #5b8def; font-size: 12px; cursor: pointer; padding: 2px 4px; border-radius: 2px; " title="Add Project">+</button> </div> <div id="projects-list" style=" display: flex; flex-direction: column; gap: 2px; "> <!-- Projects will be dynamically loaded here --> </div> </div> <!-- Tags Section --> <div> <div style=" display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; margin-bottom: 4px; "> <span style=" font-size: 12px; font-weight: 600; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; ">Tags</span> <button id="add-tag-btn" style=" background: none; border: none; color: #5b8def; font-size: 12px; cursor: pointer; padding: 2px 4px; border-radius: 2px; " title="Add Tag">+</button> </div> <div id="tags-list" style=" display: flex; flex-direction: column; gap: 2px; "> <!-- Tags will be dynamically loaded here --> </div> </div> </div> </div> <!-- Main Content Area --> <div style=" flex: 1; display: flex; flex-direction: column; overflow: hidden; "> <!-- Tab Navigation --> <div id="tab-navigation" style=" display: flex; background: rgba(91, 141, 239, 0.12); border-bottom: 1px solid rgba(91, 141, 239, 0.18); backdrop-filter: blur(6px); "> <button id="search-tab" class="tab-button active-tab" style=" flex: 1; padding: 14px 24px; border: none; background: rgba(255, 255, 255, 0.96); color: #27325f; font-size: 14px; font-weight: 600; cursor: pointer; border-bottom: 3px solid #5b8def; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${searchIconUrl}" alt="Search" style="width: 20px; height: 20px;" />Search</button> <button id="history-tab" class="tab-button" style=" flex: 1; padding: 14px 24px; border: none; background: transparent; color: #5e6f9b; font-size: 14px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${historyIconUrl}" alt="History" style="width: 20px; height: 20px;" />History</button> <button id="reports-tab" class="tab-button" style=" flex: 1; padding: 14px 24px; border: none; background: transparent; color: #5e6f9b; font-size: 14px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${analysisIconUrl}" alt="Analysis" style="width: 20px; height: 20px;" />Analysis</button> </div> <div id="search-area" style=" position: relative; padding: 24px; border-bottom: 1px solid rgba(91, 141, 239, 0.18); background: rgba(255, 255, 255, 0.9); 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: #5b8def; font-size: 12px; font-weight: 500; transition: all 0.2s ease; border-radius: 4px; padding: 4px 8px; background: rgba(91, 141, 239, 0.1); border: 1px solid rgba(91, 141, 239, 0.18); 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; align-items: center;"> <div id="single-input-group" class="search-input-group" style="flex: 1;"> <input type="text" id="search-query" class="search-input-field" placeholder="Search query (e.g., iPhone 17, Nike shoes, Pets Deli Hundefutter)" autocomplete="off" /> <div id="market-input-divider" class="search-input-divider" aria-hidden="true"></div> <div id="market-select-container" style="position: relative; flex-shrink: 0;"> <button id="market-select-trigger" type="button" class="market-select-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="market-select-dropdown"> <img id="market-select-flag" src="" alt="Selected market" style=" width: 20px; height: 14px; object-fit: cover; border-radius: 2px; flex-shrink: 0; " /> <div class="market-select-text"> <span id="market-select-country" class="market-select-country">Deutschland</span> <span id="market-select-language" class="market-select-language">Deutsch</span> </div> <span class="market-select-caret" aria-hidden="true"><svg aria-hidden="true" tabindex="-1" disabled="" data-ui-name="Globe" width="16" height="16" viewBox="0 0 16 16" data-name="Globe" data-group="m" color="#A9ABB6" data-at="db-flag" aria-label="" style="--color_yxgog: #A9ABB6;"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0ZM3.201 4.398A5.99 5.99 0 0 1 8 2c1.801 0 3.417.794 4.517 2.05a4.578 4.578 0 0 0-.967.429v.02a2 2 0 0 0-1.27.16 3.87 3.87 0 0 0-1.55 1.63 1.51 1.51 0 0 0-.17 1.13c.108.308.364.465.619.623.286.176.572.352.651.738.03.157.034.317.039.478.006.217.012.435.081.642a.67.67 0 0 0 .65.56c.35 0 .57-.39.69-.69a6.38 6.38 0 0 1 .82-1.63c.176-.227 1.076-.705 1.59-.979.088-.046.164-.087.224-.12a6 6 0 1 1-11.893.35c.288.09.62.178.999.259a2.88 2.88 0 0 1 2 1.16 6.75 6.75 0 0 1 .89 2c.17.52.41 1.25 1.11 1.16.7-.09 1-1 1-1.63a1.64 1.64 0 0 0-.74-1.63 11.524 11.524 0 0 0-.26-.154C6.305 8.13 5.17 7.47 5.78 6.55a3.63 3.63 0 0 1 1.17-1l.093-.065c.288-.2.6-.417.637-.805a.62.62 0 0 0-.55-.67 4.46 4.46 0 0 0-1.21 0c-.323.04-.64.107-.955.173l-.225.047a7.6 7.6 0 0 1-1.539.168Z" shape-rendering="geometricPrecision"></path></svg></span> </button> <div id="market-select-dropdown" class="market-select-dropdown" role="listbox" aria-labelledby="market-select-trigger"></div> <select id="market-select" aria-label="Select language and market" style=" position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; border: none; background: transparent; "></select> </div> </div> <button id="search-btn" class="search-btn" type="button" aria-label="Run product search" data-state="ready" data-ready-aria-label="Run product search" data-loading-aria-label="Searching" data-ready-status="Search" data-loading-status="Searching"> <span class="search-btn-icon" aria-hidden="true"> <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="icon"> <path d="M8.99992 16V6.41407L5.70696 9.70704C5.31643 10.0976 4.68342 10.0976 4.29289 9.70704C3.90237 9.31652 3.90237 8.6835 4.29289 8.29298L9.29289 3.29298L9.36907 3.22462C9.76184 2.90427 10.3408 2.92686 10.707 3.29298L15.707 8.29298L15.7753 8.36915C16.0957 8.76192 16.0731 9.34092 15.707 9.70704C15.3408 10.0732 14.7618 10.0958 14.3691 9.7754L14.2929 9.70704L10.9999 6.41407V16C10.9999 16.5523 10.5522 17 9.99992 17C9.44764 17 8.99992 16.5523 8.99992 16Z"></path> </svg> </span> <span class="search-btn-spinner" aria-hidden="true"></span> <span class="visually-hidden search-btn-status">Search</span> </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; "></textarea> <div id="multi-product-actions" style=" display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 12px; "> <div style=" font-size: 12px; color: #6c757d; font-style: italic; ">Results will be shown in a table format</div> <div id="multi-product-controls" style="display: flex; gap: 12px; align-items: center;"> <div id="multi-market-select-mount" style="display: none; align-items: center; gap: 8px;"></div> <button id="multi-search-btn" class="search-btn" type="button" aria-label="Run multi-product search" data-state="ready" data-ready-aria-label="Run multi-product search" data-loading-aria-label="Searching all products" data-ready-status="Search All Products" data-loading-status="Searching all products"> <span class="search-btn-icon" aria-hidden="true"> <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="icon"> <path d="M8.99992 16V6.41407L5.70696 9.70704C5.31643 10.0976 4.68342 10.0976 4.29289 9.70704C3.90237 9.31652 3.90237 8.6835 4.29289 8.29298L9.29289 3.29298L9.36907 3.22462C9.76184 2.90427 10.3408 2.92686 10.707 3.29298L15.707 8.29298L15.7753 8.36915C16.0957 8.76192 16.0731 9.34092 15.707 9.70704C15.3408 10.0732 14.7618 10.0958 14.3691 9.7754L14.2929 9.70704L10.9999 6.41407V16C10.9999 16.5523 10.5522 17 9.99992 17C9.44764 17 8.99992 16.5523 8.99992 16Z"></path> </svg> </span> <span class="search-btn-spinner" aria-hidden="true"></span> <span class="visually-hidden search-btn-status">Search All Products</span> </button> </div> </div> </div> </div> <!-- End search-controls --> <!-- Hidden token field for status display --> <input type="text" 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: 24px; "> ${createWelcomeState()} </div> <!-- History Container --> <div id="history-container" style=" flex: 1; overflow-y: auto; padding: 24px; display: none; "> <div id="history-welcome-state" style=" text-align: center; padding: 60px 40px; color: #5e6f9b; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; min-height: 300px; "> <img src="${historyIconUrl}" alt="History" style="width: 52px; height: 52px; margin-bottom: 22px; opacity: 0.9; filter: hue-rotate(18deg) saturate(1.05);" /> <h3 style=" margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #27325f; ">Search History</h3> <p style=" margin: 0 0 24px 0; font-size: 16px; line-height: 1.5; max-width: 400px; color: #465584; ">Your search history will appear here. Start searching to build your history!</p> <button id="clear-history-btn" style=" background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; display: none; ">Clear All History</button> </div> <div id="history-content" style="display: none;"> <div style=" margin-bottom: 20px; border-bottom: 1px solid #e9ecef; "> <div style=" display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; "> <h3 style="margin: 0; font-size: 18px; color: #495057;">Search History</h3> <div style="display: flex; gap: 10px; align-items: center;"> <button id="toggle-filters" style=" background: none; color: #5b8def; border: none; padding: 6px 8px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 6px; text-decoration: none; "> <svg width="16" height="16" viewBox="0 0 90 90" style="fill: currentColor;"> <path d="M 85.813 59.576 H 55.575 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 30.237 c 1.657 0 3 1.343 3 3 S 87.47 59.576 85.813 59.576 z"/> <path d="M 48.302 66.849 c -5.664 0 -10.272 -4.608 -10.272 -10.272 c 0 -5.665 4.608 -10.273 10.272 -10.273 c 5.665 0 10.273 4.608 10.273 10.273 C 58.575 62.24 53.967 66.849 48.302 66.849 z M 48.302 52.303 c -2.356 0 -4.272 1.917 -4.272 4.273 c 0 2.355 1.917 4.272 4.272 4.272 c 2.356 0 4.273 -1.917 4.273 -4.272 C 52.575 54.22 50.658 52.303 48.302 52.303 z"/> <path d="M 41.029 59.576 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 36.842 c 1.657 0 3 1.343 3 3 S 42.686 59.576 41.029 59.576 z"/> <path d="M 85.813 36.424 h -57.79 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 57.79 c 1.657 0 3 1.343 3 3 S 87.47 36.424 85.813 36.424 z"/> <path d="M 20.75 43.697 c -5.665 0 -10.273 -4.608 -10.273 -10.273 s 4.608 -10.273 10.273 -10.273 s 10.273 4.608 10.273 10.273 S 26.414 43.697 20.75 43.697 z M 20.75 29.151 c -2.356 0 -4.273 1.917 -4.273 4.273 s 1.917 4.273 4.273 4.273 s 4.273 -1.917 4.273 -4.273 S 23.105 29.151 20.75 29.151 z"/> <path d="M 13.477 36.424 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 9.289 c 1.657 0 3 1.343 3 3 S 15.133 36.424 13.477 36.424 z"/> <path d="M 57.637 13.273 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 53.449 c 1.657 0 3 1.343 3 3 S 59.294 13.273 57.637 13.273 z"/> <path d="M 64.909 20.546 c -5.664 0 -10.272 -4.608 -10.272 -10.273 S 59.245 0 64.909 0 c 5.665 0 10.273 4.608 10.273 10.273 S 70.574 20.546 64.909 20.546 z M 64.909 6 c -2.355 0 -4.272 1.917 -4.272 4.273 s 1.917 4.273 4.272 4.273 c 2.356 0 4.273 -1.917 4.273 -4.273 S 67.266 6 64.909 6 z"/> <path d="M 85.813 13.273 h -13.63 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 13.63 c 1.657 0 3 1.343 3 3 S 87.47 13.273 85.813 13.273 z"/> <path d="M 85.813 82.728 h -57.79 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 57.79 c 1.657 0 3 1.343 3 3 S 87.47 82.728 85.813 82.728 z"/> <path d="M 20.75 90 c -5.665 0 -10.273 -4.608 -10.273 -10.272 c 0 -5.665 4.608 -10.273 10.273 -10.273 s 10.273 4.608 10.273 10.273 C 31.022 85.392 26.414 90 20.75 90 z M 20.75 75.454 c -2.356 0 -4.273 1.917 -4.273 4.273 c 0 2.355 1.917 4.272 4.273 4.272 s 4.273 -1.917 4.273 -4.272 C 25.022 77.371 23.105 75.454 20.75 75.454 z"/> <path d="M 13.477 82.728 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 9.289 c 1.657 0 3 1.343 3 3 S 15.133 82.728 13.477 82.728 z"/> </svg> <span id="filter-toggle-text">Filters</span> </button> <button id="clear-history-btn-header" style=" background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 13px; font-weight: 500; cursor: pointer; ">Clear History</button> </div> </div> <!-- Advanced Filter Panel --> <div id="filter-panel" style=" display: none; background: rgba(238, 243, 255, 0.85); border: 1px solid rgba(91, 141, 239, 0.18); border-radius: 12px; padding: 18px; margin-bottom: 16px; box-shadow: 0 12px 28px rgba(79, 130, 223, 0.08); "> <div style=" display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 12px; "> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #4f6091; margin-bottom: 6px; ">Search Text</label> <input type="text" id="filter-text" placeholder="Search in queries and results..." style=" width: 100%; padding: 8px 12px; border: 1px solid rgba(91, 141, 239, 0.3); border-radius: 8px; font-size: 13px; box-sizing: border-box; " /> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 6px; ">Project</label> <select id="filter-project" style=" width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 13px; background: white; box-sizing: border-box; "> <option value="">All Projects</option> </select> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 6px; ">Market</label> <select id="filter-market" style=" width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 13px; background: white; box-sizing: border-box; "> <option value="all">All Markets</option> </select> </div> </div> <div style="margin-bottom: 12px;"> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 6px; ">Tags</label> <div id="filter-tags" style=" min-height: 32px; max-height: 80px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px; background: white; display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; "> <!-- Tag checkboxes will be populated here --> </div> </div> <div style=" display: flex; justify-content: space-between; align-items: center; padding-top: 8px; border-top: 1px solid #e9ecef; "> <div id="filter-summary" style=" font-size: 12px; color: #6c757d; "> No filters applied </div> <div style="display: flex; gap: 8px;"> <button id="clear-filters" style=" background: #6c757d; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Clear Filters</button> <button id="apply-filters" style=" background: #28a745; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Apply</button> </div> </div> </div> <!-- Active Filters Display --> <div id="active-filters" style=" display: none; margin-bottom: 12px; padding: 8px 0; "> <div style=" font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 6px; ">Active Filters:</div> <div id="filter-chips" style=" display: flex; flex-wrap: wrap; gap: 6px; "> <!-- Filter chips will be populated here --> </div> </div> </div> <div id="history-list" class="history-list"></div> </div> <div id="history-detail-container" style=" display: none; height: 100%; flex-direction: column; gap: 16px; "> <div style=" display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; "> <div style=" display: flex; align-items: center; gap: 12px; flex-wrap: wrap; "> <button id="history-detail-back" style=" background: none; border: 1px solid #e9ecef; color: #495057; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; ">← Back to history</button> <div id="history-detail-title" style=" display: flex; flex-wrap: wrap; gap: 8px; align-items: center; "></div> </div> <div style="display: flex; gap: 10px; align-items: center;"> <span id="history-detail-meta" style=" font-size: 13px; color: #6c757d; "></span> <button id="history-detail-open-search" style=" background: #5b8def; color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: not-allowed; opacity: 0.6; " disabled>Open in Search</button> </div> </div> <div id="history-detail-results" style=" flex: 1; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 10px; padding: 16px; background: white; box-shadow: inset 0 1px 3px rgba(0,0,0,0.04); "></div> </div> </div> <!-- Reports Container --> <div id="reports-container" style=" flex: 1; overflow-y: auto; padding: 20px; display: none; "> <div id="reports-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; "> <img src="${analysisIconUrl}" alt="Analysis" style="width: 20px; height: 20px; margin-bottom: 20px; opacity: 0.7;" /> <h3 style=" margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #495057; ">Cross-Search Analysis</h3> <p style=" margin: 0 0 24px 0; font-size: 16px; line-height: 1.5; max-width: 400px; ">No search history available. Start searching to see analysis results here.</p> </div> <div id="analysis-content" style="display: none;"> <div style=" margin-bottom: 20px; border-bottom: 1px solid #e9ecef; "> <div style=" display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; "> <h4 style="margin: 0; font-size: 16px; color: #495057;">Analysis Results</h4> <div style="display: flex; gap: 8px;"> <button id="toggle-analysis-filters" style=" background: none; color: #5b8def; border: none; padding: 6px 12px; border-radius: 4px; font-size: 13px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; display: flex; align-items: center; gap: 6px; "> <svg width="16" height="16" viewBox="0 0 90 90" style="fill: currentColor;"> <path d="M 85.813 59.576 H 55.575 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 30.237 c 1.657 0 3 1.343 3 3 S 87.47 59.576 85.813 59.576 z"/> <path d="M 48.302 66.849 c -5.664 0 -10.272 -4.608 -10.272 -10.272 c 0 -5.665 4.608 -10.273 10.272 -10.273 c 5.665 0 10.273 4.608 10.273 10.273 C 58.575 62.24 53.967 66.849 48.302 66.849 z M 48.302 52.303 c -2.356 0 -4.272 1.917 -4.272 4.273 c 0 2.355 1.917 4.272 4.272 4.272 c 2.356 0 4.273 -1.917 4.273 -4.272 C 52.575 54.22 50.658 52.303 48.302 52.303 z"/> <path d="M 41.029 59.576 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 36.842 c 1.657 0 3 1.343 3 3 S 42.686 59.576 41.029 59.576 z"/> <path d="M 85.813 36.424 h -57.79 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 57.79 c 1.657 0 3 1.343 3 3 S 87.47 36.424 85.813 36.424 z"/> <path d="M 20.75 43.697 c -5.665 0 -10.273 -4.608 -10.273 -10.273 s 4.608 -10.273 10.273 -10.273 s 10.273 4.608 10.273 10.273 S 26.414 43.697 20.75 43.697 z M 20.75 29.151 c -2.356 0 -4.273 1.917 -4.273 4.273 s 1.917 4.273 4.273 4.273 s 4.273 -1.917 4.273 -4.273 S 23.105 29.151 20.75 29.151 z"/> <path d="M 13.477 36.424 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 9.289 c 1.657 0 3 1.343 3 3 S 15.133 36.424 13.477 36.424 z"/> <path d="M 57.637 13.273 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 53.449 c 1.657 0 3 1.343 3 3 S 59.294 13.273 57.637 13.273 z"/> <path d="M 64.909 20.546 c -5.664 0 -10.272 -4.608 -10.272 -10.273 S 59.245 0 64.909 0 c 5.665 0 10.273 4.608 10.273 10.273 S 70.574 20.546 64.909 20.546 z M 64.909 6 c -2.355 0 -4.272 1.917 -4.272 4.273 s 1.917 4.273 4.272 4.273 c 2.356 0 4.273 -1.917 4.273 -4.273 S 67.266 6 64.909 6 z"/> <path d="M 85.813 13.273 h -13.63 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 13.63 c 1.657 0 3 1.343 3 3 S 87.47 13.273 85.813 13.273 z"/> <path d="M 85.813 82.728 h -57.79 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 57.79 c 1.657 0 3 1.343 3 3 S 87.47 82.728 85.813 82.728 z"/> <path d="M 20.75 90 c -5.665 0 -10.273 -4.608 -10.273 -10.272 c 0 -5.665 4.608 -10.273 10.273 -10.273 s 10.273 4.608 10.273 10.273 C 31.022 85.392 26.414 90 20.75 90 z M 20.75 75.454 c -2.356 0 -4.273 1.917 -4.273 4.273 c 0 2.355 1.917 4.272 4.273 4.272 s 4.273 -1.917 4.273 -4.272 C 25.022 77.371 23.105 75.454 20.75 75.454 z"/> <path d="M 13.477 82.728 H 4.188 c -1.657 0 -3 -1.343 -3 -3 s 1.343 -3 3 -3 h 9.289 c 1.657 0 3 1.343 3 3 S 15.133 82.728 13.477 82.728 z"/> </svg> <span id="analysis-filter-toggle-text">Filters</span> </button> </div> </div> <!-- Advanced Filter Panel --> <div id="analysis-filter-panel" style=" display: none; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 16px; "> <div style=" display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 12px; "> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 4px; ">Filter by Project</label> <select id="analysis-project-filter" style=" width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px; background: white; "> <option value="">All Projects</option> </select> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 4px; ">Filter by Market</label> <select id="analysis-market-filter" style=" width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px; background: white; "> <option value="all">All Markets</option> </select> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 4px; ">Filter by Tags</label> <div id="analysis-tags-filter" style=" min-height: 32px; max-height: 80px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px; background: white; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; "> <!-- Tags populated dynamically --> </div> </div> </div> <div style=" display: flex; justify-content: space-between; align-items: center; "> <div id="analysis-filter-summary" style=" font-size: 13px; color: #6c757d; ">No filters applied</div> <div style="display: flex; gap: 8px;"> <button id="clear-analysis-filters" style=" background: #6c757d; color: white; border: none; border-radius: 4px; padding: 6px 12px; font-size: 12px; cursor: pointer; ">Clear</button> <button id="apply-analysis-filters" style=" background: #28a745; color: white; border: none; border-radius: 4px; padding: 6px 12px; font-size: 12px; cursor: pointer; ">Apply</button> </div> </div> </div> <!-- Active Filters Display --> <div id="analysis-active-filters" style=" display: none; margin-bottom: 12px; padding: 8px 0; "> <div style=" font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 6px; ">Active Filters:</div> <div id="analysis-filter-chips" style=" display: flex; flex-wrap: wrap; gap: 6px; "> <!-- Filter chips will be populated here --> </div> </div> </div> <!-- Analysis Results --> <div id="analysis-results"> <div style=" display: grid; grid-template-columns: 1fr 1fr; gap: 32px; margin-bottom: 32px; max-width: 1200px; margin: 0 auto 32px auto; justify-content: center; "> <div id="citation-sources-table"></div> <div id="review-sources-table"></div> </div> <!-- Sentiment Analysis Section --> <div id="sentiment-analysis-section" style=" max-width: 600px; margin: 0 auto; background: white; border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; "> <h3 style=" margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: #495057; ">Sentiment Analysis</h3> <div id="sentiment-content"> <!-- Populated by analysis --> </div> </div> </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; font-size: 14px; z-index: 10001; display: flex; justify-content: center; "> <div style="display: inline-flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap;"> <span>Created by <a href="https://www.martinaberastegue.com/" target="_blank" rel="noopener noreferrer"><strong>Martin Aberastegue</strong></a></span> <span style="display: inline-flex; align-items: center; gap: 10px;"> <a href="https://www.linkedin.com/in/aberastegue/" target="_blank" rel="noopener noreferrer" aria-label="Martin Aberastegue on LinkedIn"> <img src="${linkedinIconUrl}" alt="LinkedIn logo" style="width: 18px; height: 18px; display: block;" /> </a> <a href="https://github.com/Xyborg" target="_blank" rel="noopener noreferrer" aria-label="Martin Aberastegue on GitHub"> <img src="${githubIconUrl}" alt="GitHub logo" style="width: 18px; height: 18px; display: block;" /> </a> <a href="https://x.com/Xyborg" target="_blank" rel="noopener noreferrer" aria-label="Martin Aberastegue on X"> <img src="${xIconUrl}" alt="X logo" style="width: 18px; height: 18px; display: block;" /> </a> </span> </div> </div> </div> <!-- End Main Content Area --> </div> <!-- End Sidebar + Content Container --> </div> </div> `; // Tutorial Overlay HTML const tutorialHTML = ` <div id="tutorial-overlay" style=" position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 10001; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); "> <div style=" background: white; max-width: 600px; width: 90%; border-radius: 12px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); overflow: hidden; "> <!-- Tutorial content screens --> <div id="tutorial-screens"> <!-- Screen 1: Welcome --> <div class="tutorial-screen" data-screen="1" style="display: block;"> <div style=" background: linear-gradient(135deg, #5b8def 0%, #4a7de8 100%); padding: 48px 32px; text-align: center; color: white; "> <div style=" width: 80px; height: 80px; margin: 0 auto 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px; "><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M40.49,205.52,93,61.14a7.79,7.79,0,0,1,12.84-2.85l91.88,91.88A7.79,7.79,0,0,1,194.86,163L50.48,215.51A7.79,7.79,0,0,1,40.49,205.52Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M168,72s0-24,24-24,24-24,24-24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="144" y1="16" x2="144" y2="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="216" y1="112" x2="232" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="216" y1="80" x2="240" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="78.09" y1="102.09" x2="153.91" y2="177.91" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="101.11" y1="197.11" x2="58.89" y2="154.89" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg></div> <h2 style="margin: 0 0 12px 0; font-size: 28px; font-weight: 700;">Welcome!</h2> <p style="margin: 0; font-size: 16px; opacity: 0.95; line-height: 1.5;"> Let's take a quick tour of <strong>ChatGPT E-commerce Product Research</strong> </p> </div> <div style="padding: 32px;"> <p style=" font-size: 15px; line-height: 1.6; color: #495057; margin: 0 0 24px 0; "> This extension lets you search for product reviews, comparisons, and detailed information directly within ChatGPT. It's powerful yet simple to use! </p> <div style=" background: #f8f9fa; padding: 16px; border-radius: 4px; "> <p style="margin: 0; font-size: 14px; color: #6c757d;"> This tutorial takes about 30 seconds. You can skip it, but I recommend going through it once. </p> </div> </div> </div> <!-- Screen 2: The Bubble --> <div class="tutorial-screen" data-screen="2" style="display: none;"> <div style=" background: linear-gradient(135deg, #5b8def 0%, #4a7de8 100%); padding: 40px 32px; text-align: center; color: white; "> <h2 style="margin: 0 0 12px 0; font-size: 24px; font-weight: 700;">The Yellow Bubble</h2> <p style="margin: 0; font-size: 15px; opacity: 0.95;">Your quick access button</p> </div> <div style="padding: 32px;"> <div style=" background: #fffaed; border: 2px solid #ffd43b; border-radius: 8px; padding: 24px; margin-bottom: 20px; text-align: center; "> <div style=" width: 56px; height: 56px; background: #ffd43b; border-radius: 50%; margin: 0 auto 12px; display: flex; align-items: center; justify-content: center; font-size: 28px; box-shadow: 0 4px 12px rgba(255, 212, 59, 0.4); "><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="#000000" viewBox="0 0 256 256"><path d="M232,48V88a8,8,0,0,1-16,0V56H184a8,8,0,0,1,0-16h40A8,8,0,0,1,232,48ZM72,200H40V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H72a8,8,0,0,0,0-16Zm152-40a8,8,0,0,0-8,8v32H184a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,224,160ZM32,96a8,8,0,0,0,8-8V56H72a8,8,0,0,0,0-16H32a8,8,0,0,0-8,8V88A8,8,0,0,0,32,96ZM80,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,80,80Zm104,88V88a8,8,0,0,0-16,0v80a8,8,0,0,0,16,0ZM144,80a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,144,80Zm-32,0a8,8,0,0,0-8,8v80a8,8,0,0,0,16,0V88A8,8,0,0,0,112,80Z"></path></svg></div> <p style="margin: 0; font-size: 14px; color: #856404; font-weight: 500;"> Look for this button in the bottom-right corner! </p> </div> <p style=" font-size: 15px; line-height: 1.6; color: #495057; margin: 0; "> Click the <strong>yellow floating bubble</strong> anytime on ChatGPT to open this search modal. You can also click the extension icon in your browser toolbar. </p> </div> </div> <!-- Screen 3: Key Features --> <div class="tutorial-screen" data-screen="3" style="display: none;"> <div style=" background: linear-gradient(135deg, #5b8def 0%, #4a7de8 100%); padding: 40px 32px; text-align: center; color: white; "> <h2 style="margin: 0 0 12px 0; font-size: 24px; font-weight: 700;">Key Features</h2> <p style="margin: 0; font-size: 15px; opacity: 0.95;">Everything you need to know</p> </div> <div style="padding: 32px;"> <div style="display: flex; flex-direction: column; gap: 16px;"> <div style="display: flex; gap: 12px; align-items: start;"> <div style=" width: 32px; height: 32px; background: #5b8def; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; ">1</div> <div style="flex: 1;"> <h4 style="margin: 0 0 4px 0; color: #495057; font-size: 15px;">Search Tab</h4> <p style="margin: 0; color: #6c757d; font-size: 14px; line-height: 1.4;"> Search single products or compare multiple products side-by-side </p> </div> </div> <div style="display: flex; gap: 12px; align-items: start;"> <div style=" width: 32px; height: 32px; background: #5b8def; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; ">2</div> <div style="flex: 1;"> <h4 style="margin: 0 0 4px 0; color: #495057; font-size: 15px;">History Tab</h4> <p style="margin: 0; color: #6c757d; font-size: 14px; line-height: 1.4;"> All searches are auto-saved. Filter by projects & tags to organize your work </p> </div> </div> <div style="display: flex; gap: 12px; align-items: start;"> <div style=" width: 32px; height: 32px; background: #5b8def; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; ">3</div> <div style="flex: 1;"> <h4 style="margin: 0 0 4px 0; color: #495057; font-size: 15px;">Analysis Tab</h4> <p style="margin: 0; color: #6c757d; font-size: 14px; line-height: 1.4;"> See which sources provide reviews and citations across your searches </p> </div> </div> <div style="display: flex; gap: 12px; align-items: start;"> <div style=" width: 32px; height: 32px; background: #5b8def; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; ">4</div> <div style="flex: 1;"> <h4 style="margin: 0 0 4px 0; color: #495057; font-size: 15px;">Left Sidebar</h4> <p style="margin: 0; color: #6c757d; font-size: 14px; line-height: 1.4;"> Create projects & tags to organize searches. Use filters to narrow down results </p> </div> </div> </div> </div> </div> <!-- Screen 4: Ready to Start --> <div class="tutorial-screen" data-screen="4" style="display: none;"> <div style=" background: linear-gradient(135deg, #28a745 0%, #20914a 100%); padding: 48px 32px; text-align: center; color: white; "> <div style=" width: 80px; height: 80px; margin: 0 auto 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px; "><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M191.11,112.89c24-24,25.5-52.55,24.75-65.28a8,8,0,0,0-7.47-7.47c-12.73-.75-41.26.73-65.28,24.75L80,128l48,48Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M136,72H74.35a8,8,0,0,0-5.65,2.34L34.35,108.69a8,8,0,0,0,4.53,13.57L80,128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M184,120v61.65a8,8,0,0,1-2.34,5.65l-34.35,34.35a8,8,0,0,1-13.57-4.53L128,176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M94.56,187.82C90.69,196.31,77.65,216,40,216c0-37.65,19.69-50.69,28.18-54.56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg></div> <h2 style="margin: 0 0 12px 0; font-size: 28px; font-weight: 700;">You're All Set!</h2> <p style="margin: 0; font-size: 16px; opacity: 0.95; line-height: 1.5;"> Start searching for products right away </p> </div> <div style="padding: 32px;"> <div style=" background: #f8f9fa; border-radius: 8px; padding: 20px; margin-bottom: 20px; "> <h4 style="margin: 0 0 12px 0; color: #495057; font-size: 16px;">Try searching for:</h4> <ul style="margin: 0; padding-left: 20px; color: #6c757d; font-size: 14px; line-height: 1.8;"> <li>"iPhone 17 Pro camera quality"</li> <li>"best wireless headphones 2025"</li> <li>"Pets Deli Hundefutter"</li> </ul> </div> <p style=" font-size: 13px; color: #6c757d; text-align: center; margin: 0; "> 💡 You can re-watch this tutorial anytime by clicking the <strong>?</strong> icon in the sidebar </p> </div> </div> </div> <!-- Tutorial Navigation --> <div style=" border-top: 1px solid #e9ecef; padding: 20px 32px; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; "> <button id="tutorial-skip" style=" background: none; border: none; color: #6c757d; font-size: 14px; cursor: pointer; padding: 8px 12px; border-radius: 4px; transition: all 0.2s; ">Skip Tutorial</button> <div style="display: flex; gap: 8px;"> <div class="tutorial-dot" data-dot="1" style=" width: 8px; height: 8px; border-radius: 50%; background: #5b8def; cursor: pointer; transition: all 0.2s; "></div> <div class="tutorial-dot" data-dot="2" style=" width: 8px; height: 8px; border-radius: 50%; background: #dee2e6; cursor: pointer; transition: all 0.2s; "></div> <div class="tutorial-dot" data-dot="3" style=" width: 8px; height: 8px; border-radius: 50%; background: #dee2e6; cursor: pointer; transition: all 0.2s; "></div> <div class="tutorial-dot" data-dot="4" style=" width: 8px; height: 8px; border-radius: 50%; background: #dee2e6; cursor: pointer; transition: all 0.2s; "></div> </div> <div style="display: flex; gap: 12px;"> <button id="tutorial-prev" style=" background: white; border: 1px solid #dee2e6; color: #495057; font-size: 14px; font-weight: 500; cursor: pointer; padding: 8px 16px; border-radius: 6px; transition: all 0.2s; display: none; ">Previous</button> <button id="tutorial-next" style=" background: #5b8def; border: none; color: white; font-size: 14px; font-weight: 500; cursor: pointer; padding: 8px 20px; border-radius: 6px; transition: all 0.2s; ">Next</button> </div> </div> </div> </div> `; const ACCEPT_LANGUAGE_FALLBACK = 'en;q=0.8, es-AR;q=0.7, es;q=0.6, 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'; const MARKET_OPTIONS = [ { value: 'de-DE', label: 'Deutschland (Deutsch)', country: 'Deutschland', language: 'Deutsch', code: 'DE', acceptLanguagePrefix: 'de-DE, de;q=0.9', oaiLanguage: 'de-DE', icon: 'assets/flags/de.svg' }, { value: 'en-DE', label: 'Germany (English)', country: 'Germany', language: 'English', code: 'DE', acceptLanguagePrefix: 'en-DE, en;q=0.9', oaiLanguage: 'en-DE', icon: 'assets/flags/de.svg' }, { value: 'tr-DE', label: 'Almanya (Türkçe)', country: 'Almanya', language: 'Türkçe', code: 'DE', acceptLanguagePrefix: 'tr-DE, tr;q=0.9', oaiLanguage: 'tr-DE', icon: 'assets/flags/de.svg' }, { value: 'de-AT', label: 'Österreich (Deutsch)', country: 'Österreich', language: 'Deutsch', code: 'AT', acceptLanguagePrefix: 'de-AT, de;q=0.9', oaiLanguage: 'de-AT', icon: 'assets/flags/at.svg' }, { value: 'de-CH', label: 'Schweiz (Deutsch)', country: 'Schweiz', language: 'Deutsch', code: 'CH', acceptLanguagePrefix: 'de-CH, de;q=0.9', oaiLanguage: 'de-CH', icon: 'assets/flags/ch.svg' }, { value: 'fr-CH', label: 'Suisse (Français)', country: 'Suisse', language: 'Français', code: 'CH', acceptLanguagePrefix: 'fr-CH, fr;q=0.9', oaiLanguage: 'fr-CH', icon: 'assets/flags/ch.svg' }, { value: 'de-BE', label: 'Belgien (Deutsch)', country: 'Belgien', language: 'Deutsch', code: 'BE', acceptLanguagePrefix: 'de-BE, de;q=0.9', oaiLanguage: 'de-BE', icon: 'assets/flags/be.svg' }, { value: 'fr-BE', label: 'Belgique (Français)', country: 'Belgique', language: 'Français', code: 'BE', acceptLanguagePrefix: 'fr-BE, fr;q=0.9', oaiLanguage: 'fr-BE', icon: 'assets/flags/be.svg' }, { value: 'nl-BE', label: 'België (Nederlands)', country: 'België', language: 'Nederlands', code: 'BE', acceptLanguagePrefix: 'nl-BE, nl;q=0.9', oaiLanguage: 'nl-BE', icon: 'assets/flags/be.svg' }, { value: 'fr-FR', label: 'France (Français)', country: 'France', language: 'Français', code: 'FR', acceptLanguagePrefix: 'fr-FR, fr;q=0.9', oaiLanguage: 'fr-FR', icon: 'assets/flags/fr.svg' }, { value: 'es-ES', label: 'España (Español)', country: 'España', language: 'Español', code: 'ES', acceptLanguagePrefix: 'es-ES, es;q=0.9', oaiLanguage: 'es-ES', icon: 'assets/flags/es.svg' }, { value: 'es-AR', label: 'Argentina (Español)', country: 'Argentina', language: 'Español', code: 'AR', acceptLanguagePrefix: 'es-AR, es;q=0.9', oaiLanguage: 'es-AR', icon: 'assets/flags/ar.svg' }, { value: 'es-MX', label: 'México (Español)', country: 'México', language: 'Español', code: 'MX', acceptLanguagePrefix: 'es-MX, es;q=0.9', oaiLanguage: 'es-MX', icon: 'assets/flags/mx.svg' }, { value: 'nl-NL', label: 'Nederland (Nederlands)', country: 'Nederland', language: 'Nederlands', code: 'NL', acceptLanguagePrefix: 'nl-NL, nl;q=0.9', oaiLanguage: 'nl-NL', icon: 'assets/flags/nl.svg' }, { value: 'sv-SE', label: 'Sverige (Svenska)', country: 'Sverige', language: 'Svenska', code: 'SE', acceptLanguagePrefix: 'sv-SE, sv;q=0.9', oaiLanguage: 'sv-SE', icon: 'assets/flags/se.svg' }, { value: 'it-IT', label: 'Italia (Italiano)', country: 'Italia', language: 'Italiano', code: 'IT', acceptLanguagePrefix: 'it-IT, it;q=0.9', oaiLanguage: 'it-IT', icon: 'assets/flags/it.svg' }, { value: 'pt-PT', label: 'Portugal (Português)', country: 'Portugal', language: 'Português', code: 'PT', acceptLanguagePrefix: 'pt-PT, pt;q=0.9', oaiLanguage: 'pt-PT', icon: 'assets/flags/pt.svg' }, { value: 'pt-BR', label: 'Brasil (Português)', country: 'Brasil', language: 'Português', code: 'BR', acceptLanguagePrefix: 'pt-BR, pt;q=0.9', oaiLanguage: 'pt-BR', icon: 'assets/flags/br.svg' }, { value: 'en-GB', label: 'United Kingdom (English)', country: 'United Kingdom', language: 'English', code: 'GB', acceptLanguagePrefix: 'en-GB, en;q=0.9', oaiLanguage: 'en-GB', icon: 'assets/flags/gb.svg' }, { value: 'en-US', label: 'United States (English)', country: 'United States', language: 'English', code: 'US', acceptLanguagePrefix: 'en-US, en;q=0.9', oaiLanguage: 'en-US', icon: 'assets/flags/us.svg' } ]; function getMarketOption(value) { const match = MARKET_OPTIONS.find(option => option.value === value); return match || MARKET_OPTIONS[0]; } function buildAcceptLanguage(option) { const prefixParts = option.acceptLanguagePrefix.split(',').map(part => part.trim().toLowerCase()); const fallbackParts = ACCEPT_LANGUAGE_FALLBACK.split(',').map(part => part.trim()); const filteredFallback = fallbackParts.filter(part => !prefixParts.includes(part.toLowerCase())); return [option.acceptLanguagePrefix, filteredFallback.join(', ')].filter(Boolean).join(', '); } function updateMarketSelectorDisplay(value) { const option = getMarketOption(value); const flagEl = document.getElementById('market-select-flag'); const countryEl = document.getElementById('market-select-country'); const languageEl = document.getElementById('market-select-language'); const container = document.getElementById('market-select-container'); const trigger = document.getElementById('market-select-trigger'); if (flagEl) { flagEl.src = chrome.runtime.getURL(option.icon); flagEl.alt = `${option.country} flag`; } if (countryEl) { countryEl.textContent = option.country; } if (languageEl) { languageEl.textContent = option.language; } if (container) { container.title = `${option.country} • ${option.language}`; } if (trigger) { trigger.setAttribute('aria-label', `${option.country}, ${option.language}`); } updateMarketDropdownSelection(option.value); updateAnalysisFilterSummary(); updateAnalysisFilterChips(); } function updateMarketDropdownSelection(value) { const dropdown = document.getElementById('market-select-dropdown'); if (!dropdown) { return; } dropdown.querySelectorAll('.market-select-option').forEach(optionEl => { const isSelected = optionEl.getAttribute('data-value') === value; optionEl.setAttribute('aria-selected', String(isSelected)); optionEl.classList.toggle('market-select-option--selected', isSelected); }); } function initializeMarketDropdownControls() { const container = document.getElementById('market-select-container'); const dropdown = document.getElementById('market-select-dropdown'); const trigger = document.getElementById('market-select-trigger'); const marketSelect = document.getElementById('market-select'); if (!container || !dropdown || !trigger || !marketSelect) { return; } dropdown.innerHTML = ''; MARKET_OPTIONS.forEach((option, index) => { const optionButton = document.createElement('button'); optionButton.type = 'button'; optionButton.className = 'market-select-option'; optionButton.setAttribute('role', 'option'); optionButton.setAttribute('aria-selected', 'false'); optionButton.setAttribute('data-value', option.value); optionButton.setAttribute('data-index', String(index)); const iconUrl = chrome.runtime.getURL(option.icon); optionButton.innerHTML = ` <img src="${iconUrl}" alt="${option.country} flag" class="market-select-option-flag" /> <span class="market-select-option-text"> <span class="market-select-option-country">${option.country}</span> <span class="market-select-option-language">${option.language}</span> </span> `; optionButton.addEventListener('click', () => { selectOption(option.value); }); dropdown.appendChild(optionButton); }); const optionButtons = Array.from(dropdown.querySelectorAll('.market-select-option')); let isOpen = false; let activeIndex = -1; const outsideClickOptions = { capture: true }; function setActiveIndex(index) { if (index < 0 || index >= optionButtons.length) { return; } activeIndex = index; optionButtons[index].focus(); } function openDropdown() { if (isOpen) { return; } dropdown.classList.add('open'); trigger.setAttribute('aria-expanded', 'true'); isOpen = true; const currentValue = marketSelect.value || MARKET_OPTIONS[0].value; const currentIndex = optionButtons.findIndex(btn => btn.getAttribute('data-value') === currentValue); setActiveIndex(currentIndex >= 0 ? currentIndex : 0); document.addEventListener('mousedown', handleOutsideClick, outsideClickOptions); } function closeDropdown() { if (!isOpen) { return; } dropdown.classList.remove('open'); trigger.setAttribute('aria-expanded', 'false'); isOpen = false; activeIndex = -1; document.removeEventListener('mousedown', handleOutsideClick, outsideClickOptions); } function selectOption(value) { setMarketSelection(value); closeDropdown(); trigger.focus(); } function handleOutsideClick(event) { if (!container.contains(event.target)) { closeDropdown(); } } function focusNext(offset) { if (!isOpen) { return; } if (activeIndex === -1) { setActiveIndex(offset >= 0 ? 0 : optionButtons.length - 1); return; } const nextIndex = activeIndex + offset; if (nextIndex < 0) { setActiveIndex(0); } else if (nextIndex >= optionButtons.length) { setActiveIndex(optionButtons.length - 1); } else { setActiveIndex(nextIndex); } } function handleTriggerKeydown(event) { if (event.key === 'ArrowDown') { event.preventDefault(); if (!isOpen) { openDropdown(); } else { focusNext(1); } } else if (event.key === 'ArrowUp') { event.preventDefault(); if (!isOpen) { openDropdown(); } else { focusNext(-1); } } else if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (isOpen) { closeDropdown(); } else { openDropdown(); } } else if (event.key === 'Escape' && isOpen) { event.preventDefault(); closeDropdown(); } } function handleOptionKeydown(event, index) { switch (event.key) { case 'ArrowDown': event.preventDefault(); focusNext(1); break; case 'ArrowUp': event.preventDefault(); focusNext(-1); break; case 'Home': event.preventDefault(); setActiveIndex(0); break; case 'End': event.preventDefault(); setActiveIndex(optionButtons.length - 1); break; case 'Enter': case ' ': event.preventDefault(); selectOption(optionButtons[index].getAttribute('data-value')); break; case 'Escape': event.preventDefault(); closeDropdown(); trigger.focus(); break; default: break; } } trigger.addEventListener('click', (event) => { event.preventDefault(); if (isOpen) { closeDropdown(); } else { openDropdown(); } }); trigger.addEventListener('keydown', handleTriggerKeydown); optionButtons.forEach((button, index) => { button.addEventListener('keydown', (event) => handleOptionKeydown(event, index)); }); container.addEventListener('focusout', (event) => { if (!isOpen) { return; } const nextFocus = event.relatedTarget; if (!nextFocus || !container.contains(nextFocus)) { closeDropdown(); } }); updateMarketDropdownSelection(marketSelect.value || MARKET_OPTIONS[0].value); } function setMarketSelection(value) { const option = getMarketOption(value); const marketSelect = document.getElementById('market-select'); if (marketSelect) { marketSelect.value = option.value; } localStorage.setItem('chatgpt-product-search-market', option.value); updateMarketSelectorDisplay(option.value); return option; } function getSelectedMarketSettings() { const marketSelect = document.getElementById('market-select'); const storedValue = localStorage.getItem('chatgpt-product-search-market'); const selectedValue = marketSelect ? marketSelect.value : storedValue; const option = getMarketOption(selectedValue); return { ...option, acceptLanguage: buildAcceptLanguage(option) }; } function moveMarketSelector(isMultiMode) { const marketSelectContainer = document.getElementById('market-select-container'); const singleInputGroup = document.getElementById('single-input-group'); const divider = document.getElementById('market-input-divider'); const multiMarketMount = document.getElementById('multi-market-select-mount'); if (!marketSelectContainer) { return; } if (isMultiMode) { if (divider) { divider.style.display = 'none'; } if (multiMarketMount) { multiMarketMount.style.display = 'flex'; multiMarketMount.appendChild(marketSelectContainer); } } else { if (singleInputGroup) { if (divider) { divider.style.display = 'inline-flex'; singleInputGroup.insertBefore(marketSelectContainer, divider.nextSibling); } else { singleInputGroup.appendChild(marketSelectContainer); } } if (multiMarketMount) { multiMarketMount.style.display = 'none'; } } } function renderMarketBadge(marketValue, marketCode, marketLabel) { if (!marketValue) { return ''; } const option = getMarketOption(marketValue); if (!option) { return ''; } let countryText = option.country; if (typeof marketLabel === 'string' && marketLabel.length) { if (marketLabel.includes('(')) { const parsed = marketLabel.split('(')[0].trim(); if (parsed) { countryText = parsed; } } else if (!marketLabel.includes('/')) { countryText = marketLabel; } } const codeText = marketCode || option.code; const languageText = codeText ? `${option.language}` : option.language; const flagUrl = chrome.runtime.getURL(option.icon); return `<span style="display:inline-flex;align-items:center;gap:8px;"><img src="${flagUrl}" alt="${countryText} flag" style="width:16px;height:12px;object-fit:cover;border-radius:2px;flex-shrink:0;" /><span style="display:flex;flex-direction:column;gap:2px;min-width:0;"><span style="font-weight:600;color:#343a40;line-height:1.1;">${countryText}</span><span style="font-size:12px;color:#6c757d;line-height:1.1;">${languageText}</span></span></span>`; } // 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 marketSelect = document.getElementById('market-select'); const marketSelectContainer = document.getElementById('market-select-container'); const multiMarketMount = document.getElementById('multi-market-select-mount'); const singleInputGroup = document.getElementById('single-input-group'); const collapseToggle = document.getElementById('collapse-toggle'); const collapseText = document.getElementById('collapse-text'); const searchControls = document.getElementById('search-controls'); const historyDetailBack = document.getElementById('history-detail-back'); const historyDetailOpenSearch = document.getElementById('history-detail-open-search'); if (marketSelect) { marketSelect.innerHTML = ''; MARKET_OPTIONS.forEach(option => { const optionEl = document.createElement('option'); optionEl.value = option.value; optionEl.textContent = `${option.label}`; marketSelect.appendChild(optionEl); }); const savedValue = localStorage.getItem('chatgpt-product-search-market') || MARKET_OPTIONS[0].value; marketSelect.value = savedValue; initializeMarketDropdownControls(); setMarketSelection(savedValue); marketSelect.addEventListener('change', () => { setMarketSelection(marketSelect.value); }); } else { const savedValue = localStorage.getItem('chatgpt-product-search-market') || MARKET_OPTIONS[0].value; setMarketSelection(savedValue); } if (marketSelectContainer) { marketSelectContainer.style.height = '100%'; } moveMarketSelector(multiProductToggle ? multiProductToggle.checked : false); // 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 = '#5b8def'; 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)'; } moveMarketSelector(isMultiMode); }); // 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 = '#5b8def'; } 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)'; }); if (historyDetailBack) { historyDetailBack.addEventListener('click', () => { showHistoryListView(); }); } if (historyDetailOpenSearch) { historyDetailOpenSearch.addEventListener('click', () => { const historyId = historyDetailOpenSearch.dataset?.historyId; if (historyId) { openHistoryItemInSearchById(historyId); } }); } // Search functionality searchBtn.addEventListener('click', performSearch); multiSearchBtn.addEventListener('click', performMultiSearch); // Enter key support searchQuery.addEventListener('keydown', (e) => { if (e.key === 'Enter') { performSearch(); } }); // Tab switching functionality const searchTab = document.getElementById('search-tab'); const historyTab = document.getElementById('history-tab'); const reportsTab = document.getElementById('reports-tab'); const searchArea = document.getElementById('search-area'); const resultsContainerTab = document.getElementById('results-container'); const historyContainer = document.getElementById('history-container'); const reportsContainer = document.getElementById('reports-container'); searchTab.addEventListener('click', () => { switchTab('search'); // Hide reports container and reset reports tab const reportsContainer = document.getElementById('reports-container'); if (reportsContainer) reportsContainer.style.display = 'none'; if (reportsTab) { reportsTab.style.background = 'rgba(91, 141, 239, 0.08)'; reportsTab.style.color = '#5e6f9b'; reportsTab.style.borderBottom = '3px solid transparent'; reportsTab.classList.remove('active-tab'); } }); historyTab.addEventListener('click', () => { switchTab('history'); syncHistoryFiltersWithAnalysisFilters(); loadHistory(); // Hide reports container and reset reports tab const reportsContainer = document.getElementById('reports-container'); if (reportsContainer) reportsContainer.style.display = 'none'; if (reportsTab) { reportsTab.style.background = 'rgba(91, 141, 239, 0.08)'; reportsTab.style.color = '#5e6f9b'; reportsTab.style.borderBottom = '3px solid transparent'; reportsTab.classList.remove('active-tab'); } }); reportsTab.addEventListener('click', () => { // Reset all tabs first [searchTab, historyTab, reportsTab].forEach(t => { if (t) { t.style.background = 'rgba(91, 141, 239, 0.08)'; t.style.color = '#5e6f9b'; t.style.borderBottom = '3px solid transparent'; t.classList.remove('active-tab'); } }); // Set reports tab as active reportsTab.style.background = 'rgba(255, 255, 255, 0.96)'; reportsTab.style.color = '#27325f'; reportsTab.style.borderBottom = '3px solid #5b8def'; reportsTab.classList.add('active-tab'); // Hide all containers const searchArea = document.getElementById('search-area'); const resultsContainer = document.getElementById('results-container'); const historyContainer = document.getElementById('history-container'); const reportsContainer = document.getElementById('reports-container'); if (searchArea) searchArea.style.display = 'none'; if (resultsContainer) resultsContainer.style.display = 'none'; if (historyContainer) historyContainer.style.display = 'none'; // Show reports container if (reportsContainer) { reportsContainer.style.display = 'block'; } else { } // Initialize the Analysis Dashboard initializeAnalysisDashboard(); }); // History functionality const clearHistoryBtn = document.getElementById('clear-history-btn'); const clearHistoryBtnHeader = document.getElementById('clear-history-btn-header'); const historySearch = document.getElementById('history-search'); if (clearHistoryBtn) { clearHistoryBtn.addEventListener('click', clearAllHistory); } if (clearHistoryBtnHeader) { clearHistoryBtnHeader.addEventListener('click', clearAllHistory); } if (historySearch) { historySearch.addEventListener('input', filterHistory); } // Initialize token status initializeTokenStatus(); // Initialize sidebar (Phase 2) initializeSidebar(); // Recalculate counts to ensure consistency recalculateAllCounts(); // Check and show first-time tutorial checkAndShowTutorial(); } // ===== TUTORIAL FUNCTIONS ===== function checkAndShowTutorial() { const tutorialCompleted = localStorage.getItem('chatgpt-product-info-tutorial-completed'); if (!tutorialCompleted) { showTutorial(); } } function showTutorial() { const modal = document.getElementById('chatgpt-product-search-modal'); if (!modal) return; // Check if tutorial is already shown if (document.getElementById('tutorial-overlay')) return; // Inject tutorial HTML into modal const modalContent = modal.querySelector('div[style*="border-radius"]'); if (modalContent) { modalContent.style.position = 'relative'; modalContent.insertAdjacentHTML('beforeend', tutorialHTML); initializeTutorialControls(); } } function initializeTutorialControls() { let currentScreen = 1; const totalScreens = 4; const overlay = document.getElementById('tutorial-overlay'); const prevBtn = document.getElementById('tutorial-prev'); const nextBtn = document.getElementById('tutorial-next'); const skipBtn = document.getElementById('tutorial-skip'); const dots = document.querySelectorAll('.tutorial-dot'); if (!overlay || !prevBtn || !nextBtn || !skipBtn) return; function updateScreen(screenNum) { // Hide all screens document.querySelectorAll('.tutorial-screen').forEach(screen => { screen.style.display = 'none'; }); // Show current screen const currentScreenEl = document.querySelector(`.tutorial-screen[data-screen="${screenNum}"]`); if (currentScreenEl) { currentScreenEl.style.display = 'block'; } // Update dots dots.forEach(dot => { const dotNum = parseInt(dot.dataset.dot); if (dotNum === screenNum) { dot.style.background = '#5b8def'; dot.style.transform = 'scale(1.2)'; } else { dot.style.background = '#dee2e6'; dot.style.transform = 'scale(1)'; } }); // Update buttons if (screenNum === 1) { prevBtn.style.display = 'none'; } else { prevBtn.style.display = 'block'; } if (screenNum === totalScreens) { nextBtn.textContent = 'Get Started'; nextBtn.style.background = '#28a745'; } else { nextBtn.textContent = 'Next'; nextBtn.style.background = '#5b8def'; } currentScreen = screenNum; } // Next button nextBtn.addEventListener('click', () => { if (currentScreen < totalScreens) { updateScreen(currentScreen + 1); } else { closeTutorial(); } }); // Previous button prevBtn.addEventListener('click', () => { if (currentScreen > 1) { updateScreen(currentScreen - 1); } }); // Skip button skipBtn.addEventListener('click', () => { closeTutorial(); }); // Dot navigation dots.forEach(dot => { dot.addEventListener('click', () => { const targetScreen = parseInt(dot.dataset.dot); updateScreen(targetScreen); }); }); // Add hover effects to buttons skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#e9ecef'; }); skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = 'none'; }); prevBtn.addEventListener('mouseenter', () => { prevBtn.style.background = '#f8f9fa'; }); prevBtn.addEventListener('mouseleave', () => { prevBtn.style.background = 'white'; }); nextBtn.addEventListener('mouseenter', () => { if (currentScreen === totalScreens) { nextBtn.style.background = '#218838'; } else { nextBtn.style.background = '#4a7de8'; } }); nextBtn.addEventListener('mouseleave', () => { if (currentScreen === totalScreens) { nextBtn.style.background = '#28a745'; } else { nextBtn.style.background = '#5b8def'; } }); // Initialize first screen updateScreen(1); } function closeTutorial() { const overlay = document.getElementById('tutorial-overlay'); if (overlay) { overlay.remove(); } // Mark tutorial as completed localStorage.setItem('chatgpt-product-info-tutorial-completed', 'true'); } function resetTutorial() { localStorage.removeItem('chatgpt-product-info-tutorial-completed'); showTutorial(); } // ===== END TUTORIAL FUNCTIONS ===== // Helper to read active tab id: 'search' | 'history' | 'reports' function getActiveTab() { const active = document.querySelector('#tab-navigation .active-tab'); return active?.id?.replace('-tab', '') || 'search'; } function switchTab(tab) { const searchTab = document.getElementById('search-tab'); const historyTab = document.getElementById('history-tab'); const reportsTab = document.getElementById('reports-tab'); const searchArea = document.getElementById('search-area'); const resultsContainer = document.getElementById('results-container'); const historyContainer = document.getElementById('history-container'); const reportsContainer = document.getElementById('reports-container'); // Reset all tabs [searchTab, historyTab, reportsTab].forEach(t => { if (t) { t.style.background = 'rgba(91, 141, 239, 0.08)'; t.style.color = '#5e6f9b'; t.style.borderBottom = '3px solid transparent'; t.classList.remove('active-tab'); } }); // Hide all containers explicitly and aggressively // Hide all containers cleanly if (searchArea) { searchArea.style.display = 'none'; } if (resultsContainer) { resultsContainer.style.display = 'none'; } if (historyContainer) { historyContainer.style.display = 'none'; } if (reportsContainer) { reportsContainer.style.display = 'none'; } // Reset all internal content states const analysisContent = document.getElementById('analysis-content'); const reportsWelcomeState = document.getElementById('reports-welcome-state'); const historyContent = document.getElementById('history-content'); const historyWelcomeState = document.getElementById('history-welcome-state'); if (analysisContent) { analysisContent.style.display = 'none'; } if (reportsWelcomeState) { reportsWelcomeState.style.display = 'none'; } if (historyContent) { historyContent.style.display = 'none'; } if (historyWelcomeState) { historyWelcomeState.style.display = 'none'; } // Clean up any organization interfaces when switching tabs const postSearchInterface = document.getElementById('post-search-tagging'); const editInterface = document.getElementById('edit-organization-interface'); if (postSearchInterface) { postSearchInterface.remove(); } if (editInterface) { editInterface.remove(); } if (tab === 'search') { searchTab.style.background = 'rgba(255, 255, 255, 0.96)'; searchTab.style.color = '#27325f'; searchTab.style.borderBottom = '3px solid #5b8def'; searchTab.classList.add('active-tab'); if (searchArea) searchArea.style.display = 'block'; if (resultsContainer) { resultsContainer.style.display = 'block'; resultsContainer.innerHTML = ''; } if (typeof resetToCleanSearchState === 'function') { resetToCleanSearchState(); } } else if (tab === 'history') { historyTab.style.background = 'rgba(255, 255, 255, 0.96)'; historyTab.style.color = '#27325f'; historyTab.style.borderBottom = '3px solid #5b8def'; historyTab.classList.add('active-tab'); showHistoryListView(); // CRITICAL: Completely hide and clear analysis content when switching to history if (reportsContainer) { reportsContainer.style.display = 'none'; // Clear all analysis content from the DOM const analysisContent = reportsContainer.querySelector('#analysis-content'); if (analysisContent) { analysisContent.style.display = 'none'; } } // Also ensure no analysis content exists anywhere else const globalAnalysisContent = document.getElementById('analysis-content'); const globalAnalysisResults = document.getElementById('analysis-results'); const globalCitationTable = document.getElementById('citation-sources-table'); const globalReviewTable = document.getElementById('review-sources-table'); if (globalAnalysisContent) { globalAnalysisContent.style.display = 'none'; } if (globalAnalysisResults) { globalAnalysisResults.style.display = 'none'; } if (globalCitationTable) { globalCitationTable.style.display = 'none'; } if (globalReviewTable) { globalReviewTable.style.display = 'none'; } if (historyContainer) { historyContainer.style.display = 'block'; historyContainer.style.visibility = 'visible'; // Restore visibility that reports tab may have hidden ['history-content', 'history-welcome-state', 'history-list'].forEach(id => { const el = document.getElementById(id); if (el) { el.style.visibility = 'visible'; } }); } } else if (tab === 'reports') { reportsTab.style.background = 'rgba(255, 255, 255, 0.96)'; reportsTab.style.color = '#27325f'; reportsTab.style.borderBottom = '3px solid #5b8def'; reportsTab.classList.add('active-tab'); // CRITICAL: Completely hide and clear history content when switching to reports if (historyContainer) { historyContainer.style.display = 'none'; // Clear all history content from the DOM const historyContent = historyContainer.querySelector('#history-content'); const historyList = historyContainer.querySelector('#history-list'); if (historyContent) { historyContent.style.display = 'none'; } if (historyList) { historyList.style.display = 'none'; } } if (reportsContainer) { reportsContainer.style.display = 'block'; reportsContainer.style.visibility = 'visible'; reportsContainer.style.removeProperty('visibility'); // Restore visibility for analysis sections that may have been hidden ['analysis-content', 'analysis-results', 'citation-sources-table', 'review-sources-table'] .forEach(id => { const el = document.getElementById(id); if (el) { if (el.style.display === 'none') { el.style.display = 'block'; } el.style.visibility = 'visible'; el.style.removeProperty('visibility'); } }); // Ensure scrollable analysis containers remain scrollable after tab switches const scrollTargets = [ reportsContainer, document.getElementById('analysis-results'), document.getElementById('analysis-content'), document.querySelector('#analysis-content .analysis-scroll-area') ]; scrollTargets.forEach(target => { if (target) { target.style.overflowY = 'auto'; } }); // Initialize analysis dashboard when switching to reports initializeAnalysisDashboard(); } else { } } } // ===== ANALYSIS DASHBOARD FUNCTIONALITY - Phase 6 ===== function initializeAnalysisDashboard() { // Allow analysis dashboard to initialize when called const history = loadSearchHistory(); const reportsWelcomeState = document.getElementById('reports-welcome-state'); const analysisContent = document.getElementById('analysis-content'); if (history.length === 0) { if (reportsWelcomeState) reportsWelcomeState.style.display = 'flex'; if (analysisContent) analysisContent.style.display = 'none'; return; } // Show analysis content immediately and setup interface if (reportsWelcomeState) { reportsWelcomeState.style.display = 'none'; reportsWelcomeState.style.visibility = 'hidden'; } if (analysisContent) { analysisContent.style.display = 'block'; analysisContent.style.visibility = 'visible'; analysisContent.style.removeProperty('visibility'); // Also ensure analysis results are visible const analysisResults = document.getElementById('analysis-results'); const citationTable = document.getElementById('citation-sources-table'); const reviewTable = document.getElementById('review-sources-table'); if (analysisResults) { analysisResults.style.display = 'block'; analysisResults.style.visibility = 'visible'; analysisResults.style.removeProperty('visibility'); } if (citationTable) { citationTable.style.display = 'block'; citationTable.style.visibility = 'visible'; citationTable.style.removeProperty('visibility'); } if (reviewTable) { reviewTable.style.display = 'block'; reviewTable.style.visibility = 'visible'; reviewTable.style.removeProperty('visibility'); } } // Clean up any existing event listeners and reset state cleanupAnalysisInterface(); resetAnalysisFilterPanelState(); // Setup analysis interface and generate initial analysis setupAnalysisInterface(); initializeAnalysisFilters(); syncAnalysisFiltersWithHistoryFilters(); if (typeof applyAnalysisFilters === 'function') { applyAnalysisFilters(); } else { generateAnalysisReports(); } } function cleanupAnalysisInterface() { // Remove existing event listeners to prevent duplicates const toggleFiltersBtn = document.getElementById('toggle-analysis-filters'); const clearFiltersBtn = document.getElementById('clear-analysis-filters'); const applyFiltersBtn = document.getElementById('apply-analysis-filters'); if (toggleFiltersBtn) { // Clone the element to remove all event listeners const newToggleBtn = toggleFiltersBtn.cloneNode(true); toggleFiltersBtn.parentNode.replaceChild(newToggleBtn, toggleFiltersBtn); } if (clearFiltersBtn) { const newClearBtn = clearFiltersBtn.cloneNode(true); clearFiltersBtn.parentNode.replaceChild(newClearBtn, clearFiltersBtn); } if (applyFiltersBtn) { const newApplyBtn = applyFiltersBtn.cloneNode(true); applyFiltersBtn.parentNode.replaceChild(newApplyBtn, applyFiltersBtn); } // Clear any dynamic content that might have old event listeners const tagsFilter = document.getElementById('analysis-tags-filter'); if (tagsFilter) { tagsFilter.innerHTML = ''; } } function resetAnalysisFilterPanelState() { // Reset filter panel to hidden state const filterPanel = document.getElementById('analysis-filter-panel'); const toggleText = document.getElementById('analysis-filter-toggle-text'); const activeFiltersDiv = document.getElementById('analysis-active-filters'); if (filterPanel) { filterPanel.style.display = 'none'; } if (toggleText) { toggleText.textContent = 'Filters'; } // Hide active filters section if (activeFiltersDiv) { activeFiltersDiv.style.display = 'none'; } } function setupAnalysisInterface() { const toggleFiltersBtn = document.getElementById('toggle-analysis-filters'); const clearFiltersBtn = document.getElementById('clear-analysis-filters'); const applyFiltersBtn = document.getElementById('apply-analysis-filters'); // Toggle filters panel with hover effects if (toggleFiltersBtn) { toggleFiltersBtn.addEventListener('click', () => { const filterPanel = document.getElementById('analysis-filter-panel'); const toggleText = document.getElementById('analysis-filter-toggle-text'); if (filterPanel && toggleText) { const isHidden = filterPanel.style.display === 'none'; filterPanel.style.display = isHidden ? 'block' : 'none'; toggleText.textContent = isHidden ? 'Hide Filters' : 'Filters'; } }); // Add hover effects toggleFiltersBtn.addEventListener('mouseenter', () => { toggleFiltersBtn.style.backgroundColor = '#f8f9fa'; }); toggleFiltersBtn.addEventListener('mouseleave', () => { toggleFiltersBtn.style.backgroundColor = 'transparent'; }); } // Clear filters if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', clearAnalysisFilters); } // Apply filters if (applyFiltersBtn) { applyFiltersBtn.addEventListener('click', applyAnalysisFilters); } } function initializeAnalysisFilters() { const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const tagsFilter = document.getElementById('analysis-tags-filter'); const projects = loadProjects(); const tags = loadTags(); // Populate project filter if (projectFilter) { projectFilter.innerHTML = '<option value="">All Projects</option>' + projects.map(project => `<option value="${project.id}">${project.name}</option>`).join(''); projectFilter.addEventListener('change', updateAnalysisFilterSummary); } if (marketFilter) { marketFilter.innerHTML = '<option value="all">All Markets</option>' + MARKET_OPTIONS.map(option => `<option value="${option.value}">${option.label}</option>`).join(''); marketFilter.addEventListener('change', updateAnalysisFilterSummary); } // Populate tags filter if (tagsFilter) { tagsFilter.innerHTML = ''; if (tags.length === 0) { tagsFilter.innerHTML = ` <div style=" color: #6c757d; font-size: 12px; font-style: italic; padding: 8px; ">No tags available</div> `; } else { tags.forEach(tag => { const tagCheckbox = document.createElement('label'); tagCheckbox.className = 'analysis-tag-label'; tagCheckbox.style.cssText = ` display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 12px; background: ${tag.color}15; border: 1px solid ${tag.color}30; cursor: pointer; font-size: 12px; color: ${tag.color}; margin: 0; `; tagCheckbox.innerHTML = ` <input type="checkbox" class="analysis-tag-checkbox" value="${tag.id}" style="margin: 0; width: 12px; height: 12px;"> <img src="${tagIconUrl}" alt="Tag" style="width: 14px; height: 14px;" /> <span>${tag.name}</span> `; const checkbox = tagCheckbox.querySelector('input'); checkbox.addEventListener('change', updateAnalysisFilterSummary); tagsFilter.appendChild(tagCheckbox); }); } } } function syncAnalysisFiltersWithHistoryFilters() { if (!currentFilters) { return; } const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); if (projectFilter) { projectFilter.value = currentFilters.project || ''; } if (marketFilter) { const targetMarket = currentFilters.market || 'all'; marketFilter.value = marketFilter.querySelector(`option[value="${targetMarket}"]`) ? targetMarket : 'all'; } const tagCheckboxes = document.querySelectorAll('.analysis-tag-checkbox'); if (tagCheckboxes.length > 0) { const activeTags = new Set(currentFilters.tags || []); tagCheckboxes.forEach(cb => { cb.checked = activeTags.has(cb.value); }); } if (typeof updateAnalysisFilterSummary === 'function') { updateAnalysisFilterSummary(); } if (typeof updateAnalysisFilterChips === 'function') { updateAnalysisFilterChips(); } } function syncHistoryFiltersWithAnalysisFilters() { if (!currentFilters) { return; } // Sync filters from Analysis to History const filterTextInput = document.getElementById('filter-text'); const projectFilterSelect = document.getElementById('filter-project'); const marketFilterSelect = document.getElementById('filter-market'); if (filterTextInput) { filterTextInput.value = currentFilters.rawText || ''; } if (projectFilterSelect) { projectFilterSelect.value = currentFilters.project || ''; } if (marketFilterSelect) { const targetMarket = currentFilters.market || 'all'; marketFilterSelect.value = marketFilterSelect.querySelector(`option[value="${targetMarket}"]`) ? targetMarket : 'all'; } // Sync tag checkboxes const tagCheckboxes = document.querySelectorAll('.tag-checkbox'); if (tagCheckboxes.length > 0) { const activeTags = new Set(currentFilters.tags || []); tagCheckboxes.forEach(cb => { cb.checked = activeTags.has(cb.value); }); } // Update filter display elements if (typeof updateFilterSummary === 'function') { updateFilterSummary(); } if (typeof updateFilterChips === 'function') { updateFilterChips(); } } function syncAllFilterDisplays() { // Update both History and Analysis filter displays to match global state if (typeof syncAnalysisFiltersWithHistoryFilters === 'function') { syncAnalysisFiltersWithHistoryFilters(); } if (typeof syncHistoryFiltersWithAnalysisFilters === 'function') { syncHistoryFiltersWithAnalysisFilters(); } } function updateAnalysisFilterSummary() { const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const tagCheckboxes = document.querySelectorAll('.analysis-tag-checkbox:checked'); const summary = document.getElementById('analysis-filter-summary'); if (!summary) return; let filterCount = 0; if (projectFilter && projectFilter.value) filterCount++; if (marketFilter && marketFilter.value && marketFilter.value !== 'all') filterCount++; if (tagCheckboxes.length > 0) filterCount++; if (filterCount === 0) { summary.textContent = 'No filters applied'; } else if (filterCount === 1) { if (projectFilter && projectFilter.value) { const projects = loadProjects(); const project = projects.find(p => p.id === projectFilter.value); summary.textContent = `Filtered by: ${project?.name || 'Unknown Project'}`; } else if (marketFilter && marketFilter.value && marketFilter.value !== 'all') { const marketOption = getMarketOption(marketFilter.value); summary.textContent = `Filtered by: ${marketOption.label}`; } else { summary.textContent = `Filtered by: ${tagCheckboxes.length} tag${tagCheckboxes.length > 1 ? 's' : ''}`; } } else { summary.textContent = `${filterCount} filters applied`; } } function applyAnalysisFilters() { const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const checkedTagCheckboxes = document.querySelectorAll('.analysis-tag-checkbox:checked'); const selectedProject = projectFilter ? projectFilter.value : ''; const selectedMarket = marketFilter ? marketFilter.value : 'all'; const selectedTags = Array.from(checkedTagCheckboxes).map(cb => cb.value); // Keep shared filter state in sync so History reflects analysis selections const previousFilters = currentFilters || { text: '', rawText: '', project: '', tags: [], market: 'all', isActive: false }; currentFilters = { text: previousFilters.text || '', rawText: previousFilters.rawText || '', project: selectedProject, tags: selectedTags, market: selectedMarket, isActive: Boolean( (previousFilters.text && previousFilters.text.length) || selectedProject || selectedTags.length || (selectedMarket && selectedMarket !== 'all') ) }; _applyToHistory({ projectId: selectedProject, tags: selectedTags, market: selectedMarket, shouldSwitch: false }); // Hide filter panel const panel = document.getElementById('analysis-filter-panel'); const toggleText = document.getElementById('analysis-filter-toggle-text'); if (panel) panel.style.display = 'none'; if (toggleText) toggleText.textContent = 'Filters'; // Update filter chips display updateAnalysisFilterChips(); updateAnalysisFilterSummary(); // Generate filtered analysis generateAnalysisReports(); } function clearAnalysisFilters() { const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const tagCheckboxes = document.querySelectorAll('.analysis-tag-checkbox'); if (projectFilter) projectFilter.value = ''; if (marketFilter) marketFilter.value = 'all'; tagCheckboxes.forEach(checkbox => checkbox.checked = false); updateAnalysisFilterSummary(); updateAnalysisFilterChips(); const previousFilters = currentFilters || { text: '', rawText: '', project: '', tags: [], market: 'all', isActive: false }; currentFilters = { text: previousFilters.text || '', rawText: previousFilters.rawText || '', project: '', tags: [], market: 'all', isActive: Boolean(previousFilters.text && previousFilters.text.length) }; _applyToHistory({ projectId: '', tags: [], market: 'all', shouldSwitch: false }); } function updateAnalysisFilterChips() { const activeFiltersDiv = document.getElementById('analysis-active-filters'); const filterChips = document.getElementById('analysis-filter-chips'); if (!activeFiltersDiv || !filterChips) return; // Clear existing chips filterChips.innerHTML = ''; let hasActiveFilters = false; const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const tagCheckboxes = document.querySelectorAll('.analysis-tag-checkbox:checked'); // Project filter chip if (projectFilter && projectFilter.value) { hasActiveFilters = true; const projects = loadProjects(); const project = projects.find(p => p.id === projectFilter.value); const chip = createFilterChip('project', `<span style="display:flex; align-items:center; gap:4px;"><img src="${projectIconUrl}" alt="Project" style="width: 14px; height: 14px;" />${project?.name || 'Unknown Project'}</span>`, () => { projectFilter.value = ''; applyAnalysisFilters(); }); filterChips.appendChild(chip); } if (marketFilter && marketFilter.value && marketFilter.value !== 'all') { hasActiveFilters = true; const option = getMarketOption(marketFilter.value); const chip = createFilterChip('market', `🌍 ${option.label}`, () => { marketFilter.value = 'all'; applyAnalysisFilters(); }); filterChips.appendChild(chip); } // Tag filter chips if (tagCheckboxes.length > 0) { hasActiveFilters = true; const tags = loadTags(); Array.from(tagCheckboxes).forEach(checkbox => { const tag = tags.find(t => t.id === checkbox.value); if (tag) { const chip = createFilterChip('tag', tag.name, () => { checkbox.checked = false; applyAnalysisFilters(); }); filterChips.appendChild(chip); } }); } // Show/hide active filters section activeFiltersDiv.style.display = hasActiveFilters ? 'block' : 'none'; } function getFilteredSearchHistory() { const history = loadSearchHistory(); const projectFilter = document.getElementById('analysis-project-filter'); const marketFilter = document.getElementById('analysis-market-filter'); const selectedTags = Array.from(document.querySelectorAll('.analysis-tag-checkbox:checked')) .map(cb => cb.value); return history.filter(item => { // Filter by project if (projectFilter && projectFilter.value && item.projectId !== projectFilter.value) { return false; } if (marketFilter && marketFilter.value && marketFilter.value !== 'all') { if ((item.market || null) !== marketFilter.value) { return false; } } // Filter by tags (AND logic) if (selectedTags.length > 0) { const itemTags = item.tags || []; const hasAllTags = selectedTags.every(tagId => itemTags.includes(tagId)); if (!hasAllTags) return false; } return true; }); } function generateAnalysisReports() { const filteredHistory = getFilteredSearchHistory(); if (filteredHistory.length === 0) { // Show empty state const citationTable = document.getElementById('citation-sources-table'); const reviewTable = document.getElementById('review-sources-table'); const sentimentContent = document.getElementById('sentiment-content'); if (citationTable) citationTable.innerHTML = '<div style="text-align: center; padding: 40px; color: #6c757d;">No citation sources found</div>'; if (reviewTable) reviewTable.innerHTML = '<div style="text-align: center; padding: 40px; color: #6c757d;">No review sources found</div>'; if (sentimentContent) sentimentContent.innerHTML = '<div style="text-align: center; padding: 20px; color: #6c757d;">No sentiment data available</div>'; return; } // Generate simplified analysis reports const citationSources = generateSimpleCitationSources(filteredHistory); const reviewSources = generateSimpleReviewSources(filteredHistory); // Display tables const citationTable = document.getElementById('citation-sources-table'); const reviewTable = document.getElementById('review-sources-table'); if (citationTable) { citationTable.innerHTML = generateSimpleSourcesHTML(citationSources.slice(0, 10), 'citations'); } if (reviewTable) { reviewTable.innerHTML = generateSimpleSourcesHTML(reviewSources.slice(0, 10), 'reviews'); } // Generate sentiment analysis generateSimpleSentimentAnalysis(filteredHistory); } // Simplified helper functions for analysis dashboard function generateSimpleCitationSources(history) { const domainCounts = new Map(); history.forEach(item => { if (item.results?.citations) { item.results.citations.forEach(citation => { if (citation.url) { const domain = extractDomainFromUrl(citation.url); if (domain && domain !== 'unknown') { domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1); } } }); } if (item.results?.productLinks) { item.results.productLinks.forEach(link => { if (link.url) { const domain = extractDomainFromUrl(link.url); if (domain && domain !== 'unknown') { domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1); } } }); } }); return Array.from(domainCounts.entries()) .map(([domain, count]) => ({ domain, count })) .sort((a, b) => b.count - a.count); } function generateSimpleReviewSources(history) { const domainCounts = new Map(); history.forEach(item => { if (item.results?.reviews) { item.results.reviews.forEach(review => { if (review.url) { const domain = extractDomainFromUrl(review.url); if (domain && domain !== 'unknown') { domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1); } } }); } }); return Array.from(domainCounts.entries()) .map(([domain, count]) => ({ domain, count })) .sort((a, b) => b.count - a.count); } function generateSimpleSourcesHTML(sources, type) { const headerLabel = type === 'reviews' ? 'Review Sources' : 'Citation Sources'; const emptyMessage = type === 'reviews' ? 'No review sources found' : 'No citation sources found'; if (sources.length === 0) { return `<div style="text-align: center; padding: 40px; color: #6c757d;">${emptyMessage}</div>`; } return ` <table style="width: 100%; border-collapse: collapse;"> <thead> <tr style="background: #f8f9fa;"> <th style="padding: 8px; text-align: left; border-bottom: 1px solid #e9ecef;">${headerLabel}</th> <th style="padding: 8px; text-align: right; border-bottom: 1px solid #e9ecef;">Count</th> </tr> </thead> <tbody> ${sources.map(source => ` <tr style="border-bottom: 1px solid #f8f9fa;"> <td style="padding: 8px;"> <div style="display: flex; align-items: center; gap: 8px;"> <img src="${getFaviconUrl(`https://${source.domain}`)}" alt="${source.domain} favicon" style="width: 16px; height: 16px;" onerror="this.style.display='none'"> ${source.domain} </div> </td> <td style="padding: 8px; text-align: right; font-weight: bold;">${source.count}</td> </tr> `).join('')} </tbody> </table> `; } function generateSimpleSentimentAnalysis(history) { const sentimentContent = document.getElementById('sentiment-content'); if (!sentimentContent) return; let totalReviews = 0; let positiveCount = 0; let negativeCount = 0; let neutralCount = 0; history.forEach(search => { if (search.results && search.results.reviews) { search.results.reviews.forEach(review => { totalReviews++; // Use the existing sentiment property from the review data const sentiment = review.sentiment ? review.sentiment.toLowerCase() : 'neutral'; if (sentiment === 'positive') { positiveCount++; } else if (sentiment === 'negative') { negativeCount++; } else { neutralCount++; } }); } }); const positivePercent = totalReviews > 0 ? Math.round((positiveCount / totalReviews) * 100) : 0; const negativePercent = totalReviews > 0 ? Math.round((negativeCount / totalReviews) * 100) : 0; const neutralPercent = totalReviews > 0 ? Math.round((neutralCount / totalReviews) * 100) : 0; sentimentContent.innerHTML = ` <div style="margin-bottom: 16px; font-size: 12px; color: #6c757d;"> Based on ${totalReviews} review${totalReviews !== 1 ? 's' : ''} across ${history.length} search${history.length !== 1 ? 'es' : ''} </div> <table style=" width: 100%; border-collapse: collapse; border: 1px solid #e9ecef; "> <thead> <tr style="background: #f8f9fa;"> <th style="padding: 8px 12px; text-align: left; border-bottom: 1px solid #e9ecef;">Sentiment</th> <th style="padding: 8px 12px; text-align: center; border-bottom: 1px solid #e9ecef;">Count</th> <th style="padding: 8px 12px; text-align: center; border-bottom: 1px solid #e9ecef;">Percentage</th> <th style="padding: 8px 12px; text-align: center; border-bottom: 1px solid #e9ecef;">Visual</th> </tr> </thead> <tbody> <tr style="border-bottom: 1px solid #f8f9fa;"> <td style="padding: 8px 12px; color: #28a745; font-weight: 600;"> <span style="display: inline-flex; align-items: center; gap: 8px;"> <span style=" width: 20px; height: 20px; display: inline-block; background-color: currentColor; mask: url(${positiveIconUrl}) no-repeat center / contain; -webkit-mask: url(${positiveIconUrl}) no-repeat center / contain; " aria-hidden="true"></span> Positive </span> </td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${positiveCount}</td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${positivePercent}%</td> <td style="padding: 8px 12px;"> <div style="background: #e9ecef; border-radius: 4px; height: 8px; width: 100%; position: relative;"> <div style="background: #28a745; height: 100%; border-radius: 4px; width: ${positivePercent}%; transition: width 0.3s ease;"></div> </div> </td> </tr> <tr style="border-bottom: 1px solid #f8f9fa;"> <td style="padding: 8px 12px; color: #d39e00; font-weight: 600;"> <span style="display: inline-flex; align-items: center; gap: 8px;"> <span style=" width: 20px; height: 20px; display: inline-block; background-color: currentColor; mask: url(${neutralIconUrl}) no-repeat center / contain; -webkit-mask: url(${neutralIconUrl}) no-repeat center / contain; " aria-hidden="true"></span> Neutral </span> </td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${neutralCount}</td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${neutralPercent}%</td> <td style="padding: 8px 12px;"> <div style="background: #e9ecef; border-radius: 4px; height: 8px; width: 100%; position: relative;"> <div style="background: #ffc107; height: 100%; border-radius: 4px; width: ${neutralPercent}%; transition: width 0.3s ease;"></div> </div> </td> </tr> <tr> <td style="padding: 8px 12px; color: #dc3545; font-weight: 600;"> <span style="display: inline-flex; align-items: center; gap: 8px;"> <span style=" width: 20px; height: 20px; display: inline-block; background-color: currentColor; mask: url(${negativeIconUrl}) no-repeat center / contain; -webkit-mask: url(${negativeIconUrl}) no-repeat center / contain; " aria-hidden="true"></span> Negative </span> </td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${negativeCount}</td> <td style="padding: 8px 12px; text-align: center; font-weight: bold;">${negativePercent}%</td> <td style="padding: 8px 12px;"> <div style="background: #e9ecef; border-radius: 4px; height: 8px; width: 100%; position: relative;"> <div style="background: #dc3545; height: 100%; border-radius: 4px; width: ${negativePercent}%; transition: width 0.3s ease;"></div> </div> </td> </tr> </tbody> </table> `; } // Legacy function for backward compatibility // Function to show the collapse toggle after results are displayed function showCollapseToggle() { const collapseToggle = document.getElementById('collapse-toggle'); if (collapseToggle) { collapseToggle.style.display = 'block'; } } // Search functionality functions // History Management Functions function sanitizeForStorage(data) { try { const seen = new WeakSet(); const json = JSON.stringify(data, (_, value) => { if (typeof value === 'bigint') { return value.toString(); } if (typeof value === 'function' || typeof value === 'symbol') { return undefined; } if (value && typeof value === 'object') { if (seen.has(value)) { return undefined; } seen.add(value); } return value; }); return JSON.parse(json); } catch (error) { console.error('ChatGPT Product Info: Failed to sanitize search results for history storage.', error); return { summary: data?.summary || null, rationale: data?.rationale || null, reviewSummary: data?.reviewSummary || null, products: data?.products || [], productLinks: data?.productLinks || [], reviews: data?.reviews || [], multiResults: data?.multiResults || null, fallback: true }; } } function saveSearchToHistory(query, results, searchType = 'single', tags = [], projectId = null, marketValue = null) { try { const history = JSON.parse(localStorage.getItem('chatgpt-product-search-history') || '[]'); const sanitizedResults = sanitizeForStorage(results); const marketOption = marketValue ? getMarketOption(marketValue) : getSelectedMarketSettings(); const historyItem = { id: Date.now() + Math.random().toString(36).substr(2, 9), query: query, results: sanitizedResults, searchType: searchType, timestamp: Date.now(), date: new Date().toLocaleString(), tags: Array.isArray(tags) ? tags : [], projectId: projectId, version: 2, market: marketOption.value, marketLabel: marketOption.label, marketCode: marketOption.code }; if (tags && Array.isArray(tags)) { tags.forEach(tagId => { if (tagId) updateTagUsage(tagId); }); } if (projectId) { updateProjectSearchCount(projectId); } history.unshift(historyItem); if (history.length > 50) { history.splice(50); } localStorage.setItem('chatgpt-product-search-history', JSON.stringify(history)); return historyItem.id; } catch (error) { console.error('ChatGPT Product Info: Failed to save search history.', error); alert('Unable to save this search to history. Check the console for details.'); return null; } } function updateHistoryEntry(historyId, updates = {}) { try { const history = JSON.parse(localStorage.getItem('chatgpt-product-search-history') || '[]'); const index = history.findIndex(item => item.id === historyId); if (index === -1) { return false; } const existingItem = history[index]; const updatedItem = { ...existingItem }; if (Object.prototype.hasOwnProperty.call(updates, 'results')) { updatedItem.results = sanitizeForStorage(updates.results); } if (Object.prototype.hasOwnProperty.call(updates, 'query')) { updatedItem.query = updates.query; } if (Object.prototype.hasOwnProperty.call(updates, 'searchType')) { updatedItem.searchType = updates.searchType; } if (Object.prototype.hasOwnProperty.call(updates, 'tags')) { updatedItem.tags = Array.isArray(updates.tags) ? updates.tags : []; } if (Object.prototype.hasOwnProperty.call(updates, 'projectId')) { updatedItem.projectId = updates.projectId; } if (Object.prototype.hasOwnProperty.call(updates, 'market')) { updatedItem.market = updates.market; } if (Object.prototype.hasOwnProperty.call(updates, 'marketLabel')) { updatedItem.marketLabel = updates.marketLabel; } if (Object.prototype.hasOwnProperty.call(updates, 'marketCode')) { updatedItem.marketCode = updates.marketCode; } updatedItem.updatedAt = Date.now(); history[index] = updatedItem; localStorage.setItem('chatgpt-product-search-history', JSON.stringify(history)); return true; } catch (error) { console.error('ChatGPT Product Info: Failed to update search history.', error); alert('Unable to update this search in history. Check the console for details.'); return false; } } function loadSearchHistory() { try { return JSON.parse(localStorage.getItem('chatgpt-product-search-history') || '[]'); } catch (error) { return []; } } function clearAllHistory() { if (confirm('Are you sure you want to clear all search history? This action cannot be undone.')) { localStorage.removeItem('chatgpt-product-search-history'); // Reset all project search counts to 0 const projects = loadProjects(); projects.forEach(project => { project.searchCount = 0; }); saveProjects(projects); // Reset all tag usage counts to 0 const tags = loadTags(); tags.forEach(tag => { tag.usageCount = 0; }); saveTags(tags); loadHistory(); // Update sidebar to reflect reset counts populateProjectsList(); populateTagsList(); } } // ===== ENHANCED DATA MANAGEMENT FUNCTIONS ===== // Tags and Projects Management - Phase 1 Implementation function loadTags() { try { return JSON.parse(localStorage.getItem('chatgpt-product-search-tags') || '[]'); } catch (error) { return []; } } function saveTags(tags) { try { localStorage.setItem('chatgpt-product-search-tags', JSON.stringify(tags)); return true; } catch (error) { return false; } } function loadProjects() { try { return JSON.parse(localStorage.getItem('chatgpt-product-search-projects') || '[]'); } catch (error) { return []; } } function saveProjects(projects) { try { localStorage.setItem('chatgpt-product-search-projects', JSON.stringify(projects)); return true; } catch (error) { return false; } } function createTag(name, color = '#5b8def') { const tags = loadTags(); // Check for duplicate names if (tags.find(tag => tag.name.toLowerCase() === name.toLowerCase())) { throw new Error('A tag with this name already exists'); } const newTag = { id: Date.now() + Math.random().toString(36).substr(2, 9), name: name.trim(), color: color, created: Date.now(), usageCount: 0 }; tags.push(newTag); saveTags(tags); return newTag; } function createProject(name, description = '') { const projects = loadProjects(); // Check for duplicate names if (projects.find(project => project.name.toLowerCase() === name.toLowerCase())) { throw new Error('A project with this name already exists'); } const newProject = { id: Date.now() + Math.random().toString(36).substr(2, 9), name: name.trim(), description: description.trim(), created: Date.now(), searchCount: 0 }; projects.push(newProject); saveProjects(projects); return newProject; } function deleteTag(tagId) { const tags = loadTags(); const filteredTags = tags.filter(tag => tag.id !== tagId); if (filteredTags.length === tags.length) { throw new Error('Tag not found'); } // Remove tag from all searches const history = loadSearchHistory(); const updatedHistory = history.map(search => ({ ...search, tags: (search.tags || []).filter(id => id !== tagId) })); localStorage.setItem('chatgpt-product-search-history', JSON.stringify(updatedHistory)); saveTags(filteredTags); return true; } function deleteProject(projectId) { const projects = loadProjects(); const filteredProjects = projects.filter(project => project.id !== projectId); if (filteredProjects.length === projects.length) { throw new Error('Project not found'); } // Remove project from all searches const history = loadSearchHistory(); const updatedHistory = history.map(search => ({ ...search, projectId: search.projectId === projectId ? null : search.projectId })); localStorage.setItem('chatgpt-product-search-history', JSON.stringify(updatedHistory)); saveProjects(filteredProjects); return true; } function updateTagUsage(tagId) { const tags = loadTags(); const tag = tags.find(t => t.id === tagId); if (tag) { tag.usageCount = (tag.usageCount || 0) + 1; saveTags(tags); } } function updateProjectSearchCount(projectId) { const projects = loadProjects(); const project = projects.find(p => p.id === projectId); if (project) { project.searchCount = (project.searchCount || 0) + 1; saveProjects(projects); } } function decrementTagUsage(tagId) { const tags = loadTags(); const tag = tags.find(t => t.id === tagId); if (tag) { tag.usageCount = Math.max((tag.usageCount || 0) - 1, 0); saveTags(tags); } } function decrementProjectSearchCount(projectId) { const projects = loadProjects(); const project = projects.find(p => p.id === projectId); if (project) { project.searchCount = Math.max((project.searchCount || 0) - 1, 0); saveProjects(projects); } } function recalculateAllCounts() { // Recalculate project and tag counts from actual search history const history = loadSearchHistory(); const projects = loadProjects(); const tags = loadTags(); // Reset all counts projects.forEach(project => project.searchCount = 0); tags.forEach(tag => tag.usageCount = 0); // Count actual usage from history history.forEach(item => { if (item.projectId) { const project = projects.find(p => p.id === item.projectId); if (project) { project.searchCount = (project.searchCount || 0) + 1; } } if (item.tags && Array.isArray(item.tags)) { item.tags.forEach(tagId => { const tag = tags.find(t => t.id === tagId); if (tag) { tag.usageCount = (tag.usageCount || 0) + 1; } }); } }); // Save updated counts saveProjects(projects); saveTags(tags); // Update sidebar display populateProjectsList(); populateTagsList(); } // Migration function for existing data function migrateSearchHistoryData() { try { const history = loadSearchHistory(); let migrationNeeded = false; const migratedHistory = history.map(search => { // Check if this search needs migration (version 2 format) if (!search.version || search.version < 2) { migrationNeeded = true; return { ...search, tags: search.tags || [], projectId: search.projectId || null, version: 2 }; } return search; }); if (migrationNeeded) { localStorage.setItem('chatgpt-product-search-history', JSON.stringify(migratedHistory)); } return true; } catch (error) { return false; } } // ===== END ENHANCED DATA MANAGEMENT FUNCTIONS ===== // ===== WELCOME STATE HELPER ===== function createWelcomeState() { return ` <div id="welcome-state" style=" text-align: center; padding: 60px 40px; color: #5e6f9b; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; min-height: 300px; "> <img src="${searchIconUrl}" alt="Search" style="width: 52px; height: 52px; margin-bottom: 22px; opacity: 0.9; filter: hue-rotate(18deg) saturate(1.1);" /> <h3 style=" margin: 0 0 12px 0; font-size: 20px; font-weight: 600; color: #27325f; ">Product Search</h3> <p style=" margin: 0 0 24px 0; font-size: 16px; line-height: 1.5; max-width: 400px; color: #465584; ">Search for product reviews, comparisons, and detailed information from across the web</p> <div style=" padding: 4px 0 4px 18px; border-left: 4px solid #5b8def; max-width: 520px; text-align: left; "> <div style="font-weight: 600; margin-bottom: 8px; color: #27325f;">Try searching for:</div> <div style="color: #556694; font-size: 14px; line-height: 1.6;"> • "iPhone 17 Pro camera quality"<br> • "Nike Air Max running shoes"<br> • "MacBook Air M3 performance"<br> • "Pets Deli Hunde Nassfutter reviews" </div> </div> <div id="auth-status" style=" margin-top: 20px; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; background: #fff3cd; color: #856404; border: 1px solid #ffeeba; display: inline-flex; align-items: center; gap: 8px; "><span class="status-icon status-icon--medium status-icon--warning" aria-hidden="true"></span><span>Checking authentication...</span></div> </div> `; } // ===== END WELCOME STATE HELPER ===== // ===== EXPORT/IMPORT FUNCTIONALITY ===== function initializeExportImportTab() { // Update data counts updateDataCounts(); // Populate project selector populateProjectSelector(); // Set up event listeners const exportBtn = document.getElementById('export-data-btn'); const importBtn = document.getElementById('import-data-btn'); const selectFileBtn = document.getElementById('select-import-file-btn'); const fileInput = document.getElementById('import-file-input'); const exportScopeAll = document.getElementById('export-scope-all'); const exportScopeProject = document.getElementById('export-scope-project'); if (exportBtn) { exportBtn.addEventListener('click', exportData); } if (importBtn) { importBtn.addEventListener('click', importData); } if (selectFileBtn && fileInput) { selectFileBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileSelect); } // Export scope change listeners if (exportScopeAll) { exportScopeAll.addEventListener('change', handleExportScopeChange); } if (exportScopeProject) { exportScopeProject.addEventListener('change', handleExportScopeChange); } // Project selection change listener const projectSelector = document.getElementById('export-project-selector'); if (projectSelector) { projectSelector.addEventListener('change', handleProjectSelectionChange); } } function updateDataCounts() { const exportScope = document.querySelector('input[name="export-scope"]:checked')?.value; const selectedProjectId = document.getElementById('export-project-selector')?.value; const tagsCountEl = document.getElementById('tags-count'); const projectsCountEl = document.getElementById('projects-count'); const historyCountEl = document.getElementById('history-count'); if (exportScope === 'project' && selectedProjectId) { // Calculate counts for selected project only const projectCounts = calculateProjectCounts(selectedProjectId); if (tagsCountEl) tagsCountEl.textContent = projectCounts.tags; if (projectsCountEl) projectsCountEl.textContent = 1; // Always 1 for single project if (historyCountEl) historyCountEl.textContent = projectCounts.history; } else { // Show total counts for all data const tags = loadTags(); const projects = loadProjects(); const history = loadSearchHistory(); if (tagsCountEl) tagsCountEl.textContent = tags.length; if (projectsCountEl) projectsCountEl.textContent = projects.length; if (historyCountEl) historyCountEl.textContent = history.length; } } function calculateProjectCounts(projectId) { // Get searches for this project const projectHistory = loadSearchHistory().filter(item => item.projectId === projectId); // Count unique tags used in this project const usedTagIds = new Set(); projectHistory.forEach(item => { if (item.tags && Array.isArray(item.tags)) { item.tags.forEach(tagId => usedTagIds.add(tagId)); } }); return { tags: usedTagIds.size, history: projectHistory.length }; } function populateProjectSelector() { const selector = document.getElementById('export-project-selector'); if (!selector) return; const projects = loadProjects(); // Clear existing options except the first one selector.innerHTML = '<option value="">Choose a project...</option>'; // Add project options projects.forEach(project => { const option = document.createElement('option'); option.value = project.id; option.textContent = project.name; selector.appendChild(option); }); // If no projects, disable the selector if (projects.length === 0) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No projects available'; option.disabled = true; selector.appendChild(option); selector.disabled = true; } else { selector.disabled = false; } // Re-add event listener after repopulating selector.removeEventListener('change', handleProjectSelectionChange); selector.addEventListener('change', handleProjectSelectionChange); // Update counts when project list changes updateDataCounts(); } function handleExportScopeChange() { const projectScope = document.getElementById('export-scope-project'); const projectContainer = document.getElementById('project-selection-container'); if (projectScope && projectContainer) { if (projectScope.checked) { projectContainer.style.display = 'block'; // Refresh project list in case it changed populateProjectSelector(); } else { projectContainer.style.display = 'none'; } } // Update counts when scope changes updateDataCounts(); } function handleProjectSelectionChange() { // Update counts when project selection changes updateDataCounts(); } function exportData() { try { const exportTagsChecked = document.getElementById('export-tags')?.checked; const exportProjectsChecked = document.getElementById('export-projects')?.checked; const exportHistoryChecked = document.getElementById('export-history')?.checked; const exportScope = document.querySelector('input[name="export-scope"]:checked')?.value; const selectedProjectId = document.getElementById('export-project-selector')?.value; if (!exportTagsChecked && !exportProjectsChecked && !exportHistoryChecked) { showExportImportStatus('Please select at least one data type to export.', 'error'); return; } // Validate project selection if single project scope is chosen if (exportScope === 'project' && !selectedProjectId) { showExportImportStatus('Please select a project to export.', 'error'); return; } const exportData = { version: '1.0', exportDate: new Date().toISOString(), exportScope: exportScope || 'all', data: {} }; // Add project info if exporting single project if (exportScope === 'project' && selectedProjectId) { const allProjects = loadProjects(); const selectedProject = allProjects.find(p => p.id === selectedProjectId); if (selectedProject) { exportData.projectInfo = { id: selectedProject.id, name: selectedProject.name, created: selectedProject.created }; } } if (exportTagsChecked) { if (exportScope === 'project' && selectedProjectId) { // Get only tags used in this project's searches const projectHistory = loadSearchHistory().filter(item => item.projectId === selectedProjectId); const usedTagIds = new Set(); projectHistory.forEach(item => { if (item.tags && Array.isArray(item.tags)) { item.tags.forEach(tagId => usedTagIds.add(tagId)); } }); const allTags = loadTags(); exportData.data.tags = allTags.filter(tag => usedTagIds.has(tag.id)); } else { exportData.data.tags = loadTags(); } } if (exportProjectsChecked) { if (exportScope === 'project' && selectedProjectId) { // Export only the selected project const allProjects = loadProjects(); exportData.data.projects = allProjects.filter(project => project.id === selectedProjectId); } else { exportData.data.projects = loadProjects(); } } if (exportHistoryChecked) { if (exportScope === 'project' && selectedProjectId) { // Export only searches from this project exportData.data.history = loadSearchHistory().filter(item => item.projectId === selectedProjectId); } else { exportData.data.history = loadSearchHistory(); } } // Create and download file const dataStr = JSON.stringify(exportData, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); const filenameSuffix = exportScope === 'project' && selectedProjectId ? `-${exportData.projectInfo?.name?.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'project'}` : ''; link.href = url; link.download = `chatgpt-product-search-backup${filenameSuffix}-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); const message = exportScope === 'project' ? `Project "${exportData.projectInfo?.name || 'Unknown'}" exported successfully!` : 'Data exported successfully!'; showExportImportStatus(message, 'success'); } catch (error) { console.error('Export failed:', error); showExportImportStatus('Export failed. Please try again.', 'error'); } } let pendingImportData = null; function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; const fileInfo = document.getElementById('selected-file-info'); if (fileInfo) { fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`; fileInfo.style.display = 'block'; } const reader = new FileReader(); reader.onload = function(e) { try { const importedData = JSON.parse(e.target.result); if (validateImportData(importedData)) { pendingImportData = importedData; displayImportPreview(importedData); enableImportButton(); } else { showExportImportStatus('Invalid file format. Please select a valid export file.', 'error'); disableImportButton(); } } catch (error) { showExportImportStatus('Error reading file. Please ensure it\'s a valid JSON file.', 'error'); disableImportButton(); } }; reader.readAsText(file); } function validateImportData(data) { if (!data || typeof data !== 'object') return false; if (!data.version || !data.data) return false; const { tags, projects, history } = data.data; // Validate tags structure if (tags && (!Array.isArray(tags) || !tags.every(tag => tag.id && tag.name && typeof tag.created === 'number'))) { return false; } // Validate projects structure if (projects && (!Array.isArray(projects) || !projects.every(project => project.id && project.name && typeof project.created === 'number'))) { return false; } // Validate history structure if (history && (!Array.isArray(history) || !history.every(item => item.id && item.query && item.timestamp))) { return false; } return true; } function displayImportPreview(importedData) { const preview = document.getElementById('import-preview'); const previewContent = document.getElementById('import-preview-content'); if (!preview || !previewContent) return; const { tags, projects, history } = importedData.data; let previewHTML = ''; if (tags && tags.length > 0) { previewHTML += `<div><strong>Tags:</strong> ${tags.length} items</div>`; } if (projects && projects.length > 0) { previewHTML += `<div><strong>Projects:</strong> ${projects.length} items</div>`; } if (history && history.length > 0) { previewHTML += `<div><strong>History:</strong> ${history.length} items</div>`; } previewHTML += `<div style="margin-top: 8px; font-size: 12px; color: #6c757d;">Export Date: ${new Date(importedData.exportDate).toLocaleString()}</div>`; previewContent.innerHTML = previewHTML; preview.style.display = 'block'; } function enableImportButton() { const importBtn = document.getElementById('import-data-btn'); if (importBtn) { importBtn.style.background = '#28a745'; importBtn.style.cursor = 'pointer'; importBtn.disabled = false; } } function disableImportButton() { const importBtn = document.getElementById('import-data-btn'); if (importBtn) { importBtn.style.background = '#6c757d'; importBtn.style.cursor = 'not-allowed'; importBtn.disabled = true; } const preview = document.getElementById('import-preview'); if (preview) { preview.style.display = 'none'; } } function importData() { if (!pendingImportData) { showExportImportStatus('No file selected for import.', 'error'); return; } try { const { tags, projects, history } = pendingImportData.data; let importResults = { added: 0, skipped: 0 }; // ID mappings to maintain relationships const tagIdMapping = {}; // oldId -> newId const projectIdMapping = {}; // oldId -> newId // Import tags if (tags && tags.length > 0) { const existingTags = loadTags(); const existingTagNames = existingTags.map(t => t.name.toLowerCase()); tags.forEach(tag => { if (!existingTagNames.includes(tag.name.toLowerCase())) { const newId = Date.now() + Math.random().toString(36).substr(2, 9); tagIdMapping[tag.id] = newId; // Map old ID to new ID existingTags.push({ ...tag, id: newId }); importResults.added++; } else { // Find existing tag ID for mapping const existingTag = existingTags.find(t => t.name.toLowerCase() === tag.name.toLowerCase()); if (existingTag) { tagIdMapping[tag.id] = existingTag.id; // Map old ID to existing ID } importResults.skipped++; } }); saveTags(existingTags); } // Import projects if (projects && projects.length > 0) { const existingProjects = loadProjects(); const existingProjectNames = existingProjects.map(p => p.name.toLowerCase()); projects.forEach(project => { if (!existingProjectNames.includes(project.name.toLowerCase())) { const newId = Date.now() + Math.random().toString(36).substr(2, 9); projectIdMapping[project.id] = newId; // Map old ID to new ID existingProjects.push({ ...project, id: newId }); importResults.added++; } else { // Find existing project ID for mapping const existingProject = existingProjects.find(p => p.name.toLowerCase() === project.name.toLowerCase()); if (existingProject) { projectIdMapping[project.id] = existingProject.id; // Map old ID to existing ID } importResults.skipped++; } }); saveProjects(existingProjects); } // Import history with updated ID references if (history && history.length > 0) { const existingHistory = loadSearchHistory(); // Create a set of unique signatures from existing history for better duplicate detection const existingSignatures = new Set( existingHistory.map(h => `${h.query}|${h.timestamp}|${h.searchType || 'single'}`) ); history.forEach(item => { // Create unique signature for this item const itemSignature = `${item.query}|${item.timestamp}|${item.searchType || 'single'}`; if (!existingSignatures.has(itemSignature)) { const newHistoryItem = { ...item, id: Date.now() + Math.random().toString(36).substr(2, 9) // Generate new ID to avoid conflicts }; // Update project reference if it exists and we have a mapping if (item.projectId && projectIdMapping[item.projectId]) { newHistoryItem.projectId = projectIdMapping[item.projectId]; } else if (item.projectId && !projectIdMapping[item.projectId]) { // Project reference exists but no mapping found, remove reference newHistoryItem.projectId = null; } // Update tag references if they exist and we have mappings if (item.tags && Array.isArray(item.tags)) { newHistoryItem.tags = item.tags .map(tagId => tagIdMapping[tagId]) // Map to new IDs .filter(tagId => tagId !== undefined); // Remove unmapped tags } existingHistory.push(newHistoryItem); existingSignatures.add(itemSignature); // Add to signatures to prevent duplicates in this import batch importResults.added++; } else { importResults.skipped++; } }); localStorage.setItem('chatgpt-product-search-history', JSON.stringify(existingHistory)); } // Update UI updateDataCounts(); populateTagsManagement(); populateProjectsManagement(); populateProjectsList(); populateTagsList(); // Recalculate counts to reflect the imported history associations recalculateAllCounts(); // Clear pending data pendingImportData = null; disableImportButton(); // Clear file input const fileInput = document.getElementById('import-file-input'); if (fileInput) fileInput.value = ''; const fileInfo = document.getElementById('selected-file-info'); if (fileInfo) fileInfo.style.display = 'none'; showExportImportStatus( `Import completed! Added ${importResults.added} items, skipped ${importResults.skipped} duplicates.`, 'success' ); } catch (error) { console.error('Import failed:', error); showExportImportStatus('Import failed. Please try again.', 'error'); } } function showExportImportStatus(message, type) { const statusEl = document.getElementById('export-import-status'); if (!statusEl) return; statusEl.textContent = message; statusEl.style.display = 'block'; if (type === 'success') { statusEl.style.background = '#d4edda'; statusEl.style.color = '#155724'; statusEl.style.border = '1px solid #c3e6cb'; } else if (type === 'error') { statusEl.style.background = '#f8d7da'; statusEl.style.color = '#721c24'; statusEl.style.border = '1px solid #f5c6cb'; } setTimeout(() => { statusEl.style.display = 'none'; }, 5000); } // ===== END EXPORT/IMPORT FUNCTIONALITY ===== // ===== SIDEBAR FUNCTIONALITY - Phase 2 ===== function initializeSidebar() { // Populate sidebar with existing data populateProjectsList(); populateTagsList(); // Add event listeners for sidebar buttons const settingsBtn = document.getElementById('settings-btn'); const helpBtn = document.getElementById('help-btn'); const addProjectBtn = document.getElementById('add-project-btn'); const addTagBtn = document.getElementById('add-tag-btn'); if (settingsBtn) { settingsBtn.addEventListener('click', openSettingsModal); } if (helpBtn) { helpBtn.addEventListener('click', resetTutorial); } if (addProjectBtn) { addProjectBtn.addEventListener('click', quickAddProject); } if (addTagBtn) { addTagBtn.addEventListener('click', quickAddTag); } } function escapeAttributeValue(value) { if (typeof value !== 'string') { return ''; } return value .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"'); } function escapeHTML(value) { if (typeof value !== 'string') { return ''; } return value .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); } function populateProjectsList() { const projectsList = document.getElementById('projects-list'); if (!projectsList) return; const projects = loadProjects(); if (projects.length === 0) { projectsList.innerHTML = ` <div style=" padding: 8px; text-align: center; color: #6c757d; font-size: 12px; font-style: italic; ">No projects yet</div> `; return; } projectsList.innerHTML = projects.map(project => { const descriptionAttr = project.description ? ` title="${escapeAttributeValue(project.description)}"` : ''; return ` <div class="sidebar-project" data-project-id="${project.id}"> <div style=" display: flex; justify-content: space-between; align-items: center; "> <span${descriptionAttr} style="font-weight: 500; display: flex; align-items: center; gap: 6px;"><img src="${projectIconUrl}" alt="Project" style="width: 16px; height: 16px;" />${project.name}</span> <span style=" font-size: 11px; color: #6c757d; background: #f8f9fa; padding: 1px 4px; border-radius: 8px; ">${project.searchCount || 0}</span> </div> </div> `; }).join(''); // Add click handlers for project filtering document.querySelectorAll('.sidebar-project').forEach(element => { element.addEventListener('click', () => { const projectId = element.getAttribute('data-project-id'); filterByProject(projectId); }); }); } function populateTagsList() { const tagsList = document.getElementById('tags-list'); if (!tagsList) return; const tags = loadTags(); if (tags.length === 0) { tagsList.innerHTML = ` <div style=" padding: 8px; text-align: center; color: #6c757d; font-size: 12px; font-style: italic; ">No tags yet</div> `; return; } tagsList.innerHTML = tags.map(tag => ` <div class="sidebar-tag" data-tag-id="${tag.id}" style=" border: 1px solid ${tag.color}55; background: ${tag.color}26; color: #142049; "> <span style=" display: flex; align-items: center; gap: 4px; "> <img src="${tagIconUrl}" alt="Tag" style="width: 14px; height: 14px;" />${tag.name} </span> <span style=" font-size: 10px; color: #2d3d6e; background: rgba(255, 255, 255, 0.8); padding: 1px 4px; border-radius: 6px; ">${tag.usageCount || 0}</span> </div> `).join(''); // Add click handlers for tag filtering document.querySelectorAll('.sidebar-tag').forEach(element => { element.addEventListener('click', () => { const tagId = element.getAttribute('data-tag-id'); filterByTag(tagId); }); }); } function quickAddProject() { const name = prompt('Enter project name:'); if (!name || !name.trim()) return; try { const project = createProject(name.trim()); populateProjectsList(); // Refresh project selector in export section if it exists if (document.getElementById('export-project-selector')) { populateProjectSelector(); } alert(`Project "${project.name}" created successfully!`); } catch (error) { alert(`Error: ${error.message}`); } } function quickAddTag() { const name = prompt('Enter tag name:'); if (!name || !name.trim()) return; try { const tag = createTag(name.trim()); populateTagsList(); alert(`Tag "${tag.name}" created successfully!`); } catch (error) { alert(`Error: ${error.message}`); } } // filterByProject function removed - duplicate implementation exists below // filterByTag function removed - duplicate implementation exists below function openSettingsModal() { // Remove existing settings modal if present const existingModal = document.getElementById('settings-modal'); if (existingModal) { existingModal.remove(); } const settingsModalHTML = ` <div id="settings-modal" style=" position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10002; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; "> <div style=" background: white; width: 700px; max-width: 90vw; height: 500px; max-height: 90vh; border-radius: 8px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); "> <!-- Modal Header --> <div style=" background: #f8f9fa; padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e9ecef; "> <h2 style=" font-size: 18px; font-weight: 600; margin: 0; color: #495057; display: flex; align-items: center;"><img src="${settingsIconUrl}" alt="Settings" style="width: 20px;height: 20px;margin-right: 5px;" />Settings</h2> <button id="close-settings-modal" 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> <!-- Settings Tabs --> <div style=" display: flex; background: #f8f9fa; border-bottom: 1px solid #e9ecef; "> <button id="tags-settings-tab" class="settings-tab-button active-settings-tab" style=" flex: 1; padding: 12px 20px; border: none; background: white; color: #495057; font-size: 14px; font-weight: 500; cursor: pointer; border-bottom: 2px solid #5b8def; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${tagIconUrl}" alt="Tags" style="width: 20px; height: 20px;" />Tags</button> <button id="projects-settings-tab" class="settings-tab-button" style=" flex: 1; padding: 12px 20px; border: none; background: #f8f9fa; color: #6c757d; font-size: 14px; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${projectIconUrl}" alt="Projects" style="width: 20px; height: 20px;" />Projects</button> <button id="export-import-settings-tab" class="settings-tab-button" style=" flex: 1; padding: 12px 20px; border: none; background: #f8f9fa; color: #6c757d; font-size: 14px; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 8px; "><img src="${databaseIconUrl}" alt="Export/Import" style="width: 20px; height: 20px;" />Export/Import</button> </div> <!-- Settings Content --> <div style=" flex: 1; overflow: hidden; display: flex; flex-direction: column; "> <!-- Tags Settings --> <div id="tags-settings-content" style=" flex: 1; padding: 20px; overflow-y: auto; display: block; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; "> <h3 style="margin: 0; color: #495057;">Manage Tags</h3> <button id="create-tag-btn" style=" background: #5b8def; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; cursor: pointer; ">+ New Tag</button> </div> <div id="tags-management-list"> <!-- Tags list will be populated here --> </div> </div> <!-- Projects Settings --> <div id="projects-settings-content" style=" flex: 1; padding: 20px; overflow-y: auto; display: none; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; "> <h3 style="margin: 0; color: #495057;">Manage Projects</h3> <button id="create-project-btn" style=" background: #5b8def; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; cursor: pointer; ">+ New Project</button> </div> <div id="projects-management-list"> <!-- Projects list will be populated here --> </div> </div> <!-- Export/Import Settings --> <div id="export-import-settings-content" style=" flex: 1; padding: 20px; overflow-y: auto; display: none; "> <h3 style="margin: 0 0 20px 0; color: #495057;">Export/Import Data</h3> <!-- Export Section --> <div style=" background: #f8f9fa; border-radius: 6px; padding: 16px; margin-bottom: 24px; border: 1px solid #e9ecef; "> <h4 style=" margin: 0 0 12px 0; color: #495057; font-size: 16px; display: flex; align-items: center; gap: 8px; ">📤 Export Data</h4> <p style=" margin: 0 0 16px 0; color: #6c757d; font-size: 14px; line-height: 1.4; ">Export your tags, projects, and search history to a JSON file for backup or transfer to another browser.</p> <!-- Export Scope Selection --> <div style="margin-bottom: 16px; padding: 12px; background: white; border: 1px solid #dee2e6; border-radius: 4px;"> <h5 style="margin: 0 0 8px 0; color: #495057; font-size: 14px; font-weight: 600;">Export Scope</h5> <div style="display: flex; gap: 16px; align-items: center;"> <label style=" display: flex; align-items: center; gap: 6px; font-size: 14px; color: #495057; cursor: pointer; "> <input type="radio" name="export-scope" id="export-scope-all" value="all" checked style="margin: 0;"> All Data </label> <label style=" display: flex; align-items: center; gap: 6px; font-size: 14px; color: #495057; cursor: pointer; "> <input type="radio" name="export-scope" id="export-scope-project" value="project" style="margin: 0;"> Single Project </label> </div> <!-- Project Selection (hidden by default) --> <div id="project-selection-container" style=" margin-top: 12px; display: none; "> <label style=" display: block; font-size: 13px; color: #6c757d; margin-bottom: 4px; ">Select Project:</label> <select id="export-project-selector" style=" width: 100%; padding: 6px 8px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; background: white; "> <option value="">Choose a project...</option> </select> </div> </div> <!-- Data Type Selection --> <div style="margin-bottom: 16px;"> <h5 style="margin: 0 0 8px 0; color: #495057; font-size: 14px; font-weight: 600;">Data Types</h5> <div style="display: flex; flex-wrap: wrap; gap: 8px;"> <label style=" display: flex; align-items: center; gap: 6px; font-size: 14px; color: #495057; cursor: pointer; "> <input type="checkbox" id="export-tags" checked style="margin: 0;"> Tags (<span id="tags-count">0</span>) </label> <label style=" display: flex; align-items: center; gap: 6px; font-size: 14px; color: #495057; cursor: pointer; "> <input type="checkbox" id="export-projects" checked style="margin: 0;"> Projects (<span id="projects-count">0</span>) </label> <label style=" display: flex; align-items: center; gap: 6px; font-size: 14px; color: #495057; cursor: pointer; "> <input type="checkbox" id="export-history" checked style="margin: 0;"> History (<span id="history-count">0</span>) </label> </div> </div> <button id="export-data-btn" style=" background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; ">Export Selected Data</button> </div> <!-- Import Section --> <div style=" background: #f8f9fa; border-radius: 6px; padding: 16px; border: 1px solid #e9ecef; "> <h4 style=" margin: 0 0 12px 0; color: #495057; font-size: 16px; display: flex; align-items: center; gap: 8px; ">📥 Import Data</h4> <p style=" margin: 0 0 16px 0; color: #6c757d; font-size: 14px; line-height: 1.4; ">Import data from a previously exported JSON file. Duplicate items will be skipped automatically.</p> <div style="margin-bottom: 16px;"> <input type="file" id="import-file-input" accept=".json" style=" display: none; "> <button id="select-import-file-btn" style=" background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; ">Select JSON File</button> <div id="selected-file-info" style=" margin-top: 8px; font-size: 13px; color: #6c757d; display: none; "></div> </div> <div id="import-preview" style=" display: none; background: white; border: 1px solid #dee2e6; border-radius: 4px; padding: 12px; margin-bottom: 16px; max-height: 200px; overflow-y: auto; "> <h5 style="margin: 0 0 8px 0; color: #495057; font-size: 14px;">Import Preview:</h5> <div id="import-preview-content"></div> </div> <button id="import-data-btn" style=" background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 4px; font-size: 14px; cursor: not-allowed; display: flex; align-items: center; gap: 8px; " disabled>Import Data</button> </div> <div id="export-import-status" style=" margin-top: 16px; padding: 8px 12px; border-radius: 4px; font-size: 14px; display: none; "></div> </div> </div> <!-- Modal Footer --> <div style=" background: #f8f9fa; padding: 16px 20px; border-top: 1px solid #e9ecef; display: flex; justify-content: flex-end; gap: 12px; "> <button id="cancel-settings" style=" background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; cursor: pointer; ">Cancel</button> <button id="save-settings" style=" background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-size: 14px; cursor: pointer; ">Save Changes</button> </div> </div> </div> `; // Create and append modal const modalDiv = document.createElement('div'); modalDiv.innerHTML = settingsModalHTML; const modal = modalDiv.firstElementChild; document.body.appendChild(modal); // Initialize settings modal initializeSettingsModal(); } function initializeSettingsModal() { // Add event listeners for modal controls const closeBtn = document.getElementById('close-settings-modal'); const cancelBtn = document.getElementById('cancel-settings'); const saveBtn = document.getElementById('save-settings'); const tagsTab = document.getElementById('tags-settings-tab'); const projectsTab = document.getElementById('projects-settings-tab'); const exportImportTab = document.getElementById('export-import-settings-tab'); const createTagBtn = document.getElementById('create-tag-btn'); const createProjectBtn = document.getElementById('create-project-btn'); // Close modal handlers if (closeBtn) { closeBtn.addEventListener('click', closeSettingsModal); } if (cancelBtn) { cancelBtn.addEventListener('click', closeSettingsModal); } if (saveBtn) { saveBtn.addEventListener('click', saveSettingsAndClose); } // Tab switching if (tagsTab) { tagsTab.addEventListener('click', () => switchSettingsTab('tags')); } if (projectsTab) { projectsTab.addEventListener('click', () => switchSettingsTab('projects')); } if (exportImportTab) { exportImportTab.addEventListener('click', () => switchSettingsTab('export-import')); } // Create buttons if (createTagBtn) { createTagBtn.addEventListener('click', showCreateTagForm); } if (createProjectBtn) { createProjectBtn.addEventListener('click', showCreateProjectForm); } // Close on outside click const modal = document.getElementById('settings-modal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { closeSettingsModal(); } }); } // Populate initial content populateTagsManagement(); populateProjectsManagement(); initializeExportImportTab(); } function switchSettingsTab(tab) { const tagsTab = document.getElementById('tags-settings-tab'); const projectsTab = document.getElementById('projects-settings-tab'); const exportImportTab = document.getElementById('export-import-settings-tab'); const tagsContent = document.getElementById('tags-settings-content'); const projectsContent = document.getElementById('projects-settings-content'); const exportImportContent = document.getElementById('export-import-settings-content'); // Reset all tabs to inactive state const tabs = [tagsTab, projectsTab, exportImportTab]; const contents = [tagsContent, projectsContent, exportImportContent]; tabs.forEach(tabEl => { if (tabEl) { tabEl.style.background = '#f8f9fa'; tabEl.style.color = '#6c757d'; tabEl.style.borderBottom = '2px solid transparent'; } }); contents.forEach(contentEl => { if (contentEl) { contentEl.style.display = 'none'; } }); // Activate selected tab if (tab === 'tags' && tagsTab && tagsContent) { tagsTab.style.background = 'white'; tagsTab.style.color = '#495057'; tagsTab.style.borderBottom = '2px solid #5b8def'; tagsContent.style.display = 'block'; } else if (tab === 'projects' && projectsTab && projectsContent) { projectsTab.style.background = 'white'; projectsTab.style.color = '#495057'; projectsTab.style.borderBottom = '2px solid #5b8def'; projectsContent.style.display = 'block'; } else if (tab === 'export-import' && exportImportTab && exportImportContent) { exportImportTab.style.background = 'white'; exportImportTab.style.color = '#495057'; exportImportTab.style.borderBottom = '2px solid #5b8def'; exportImportContent.style.display = 'block'; } } function populateTagsManagement() { const tagsManagementList = document.getElementById('tags-management-list'); if (!tagsManagementList) return; const tags = loadTags(); if (tags.length === 0) { tagsManagementList.innerHTML = ` <div style=" text-align: center; padding: 40px; color: #6c757d; font-style: italic; "> No tags created yet. Click "New Tag" to create your first tag. </div> `; return; } tagsManagementList.innerHTML = tags.map(tag => ` <div class="tag-management-item" data-tag-id="${tag.id}" style=" border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; "> <div style=" display: flex; align-items: center; gap: 12px; "> <input type="color" value="${tag.color}" class="tag-color-input" style=" width: 30px; height: 30px; border: none; border-radius: 4px; cursor: pointer; "> <input type="text" value="${tag.name}" class="tag-name-input" style=" border: 1px solid #e9ecef; border-radius: 4px; padding: 6px 8px; font-size: 14px; flex: 1; min-width: 200px; "> </div> <div style=" display: flex; gap: 8px; align-items: center; "> <span style=" background: #f8f9fa; padding: 4px 8px; border-radius: 12px; font-size: 12px; color: #6c757d; ">${tag.usageCount || 0} uses</span> <button class="delete-tag-btn" data-tag-id="${tag.id}" style=" background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Delete</button> </div> </div> <div style=" font-size: 12px; color: #6c757d; ">Created: ${new Date(tag.created).toLocaleDateString()}</div> </div> `).join(''); // Add event listeners for delete buttons document.querySelectorAll('.delete-tag-btn').forEach(btn => { btn.addEventListener('click', (e) => { const tagId = btn.getAttribute('data-tag-id'); deleteTagFromSettings(tagId); }); }); } function populateProjectsManagement() { const projectsManagementList = document.getElementById('projects-management-list'); if (!projectsManagementList) return; const projects = loadProjects(); if (projects.length === 0) { projectsManagementList.innerHTML = ` <div style=" text-align: center; padding: 40px; color: #6c757d; font-style: italic; "> No projects created yet. Click "New Project" to create your first project. </div> `; return; } projectsManagementList.innerHTML = projects.map(project => ` <div class="project-management-item" data-project-id="${project.id}" style=" border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: white; "> <div style=" display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; "> <div style="flex: 1; margin-right: 16px;"> <input type="text" value="${project.name}" class="project-name-input" style=" border: 1px solid #e9ecef; border-radius: 4px; padding: 6px 8px; font-size: 14px; width: 100%; margin-bottom: 8px; font-weight: 500; "> <textarea class="project-description-input" placeholder="Project description (optional)" style=" border: 1px solid #e9ecef; border-radius: 4px; padding: 6px 8px; font-size: 13px; width: 100%; min-height: 60px; resize: vertical; font-family: inherit; ">${project.description || ''}</textarea> </div> <div style=" display: flex; flex-direction: column; gap: 8px; align-items: flex-end; "> <span style=" background: #f8f9fa; padding: 4px 8px; border-radius: 12px; font-size: 12px; color: #6c757d; ">${project.searchCount || 0} searches</span> <button class="delete-project-btn" data-project-id="${project.id}" style=" background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Delete</button> </div> </div> <div style=" font-size: 12px; color: #6c757d; ">Created: ${new Date(project.created).toLocaleDateString()}</div> </div> `).join(''); // Add event listeners for delete buttons document.querySelectorAll('.delete-project-btn').forEach(btn => { btn.addEventListener('click', (e) => { const projectId = btn.getAttribute('data-project-id'); deleteProjectFromSettings(projectId); }); }); } function showCreateTagForm() { const name = prompt('Enter tag name:'); if (!name || !name.trim()) return; const color = prompt('Enter tag color (hex):', '#5b8def'); if (!color) return; try { createTag(name.trim(), color); populateTagsManagement(); alert(`Tag "${name}" created successfully!`); } catch (error) { alert(`Error: ${error.message}`); } } function showCreateProjectForm() { const name = prompt('Enter project name:'); if (!name || !name.trim()) return; const description = prompt('Enter project description (optional):') || ''; try { createProject(name.trim(), description.trim()); populateProjectsManagement(); // Refresh project selector in export section if it exists if (document.getElementById('export-project-selector')) { populateProjectSelector(); } alert(`Project "${name}" created successfully!`); } catch (error) { alert(`Error: ${error.message}`); } } function deleteTagFromSettings(tagId) { const tag = loadTags().find(t => t.id === tagId); if (!tag) return; if (confirm(`Are you sure you want to delete the tag "${tag.name}"? This will remove it from all searches.`)) { try { deleteTag(tagId); populateTagsManagement(); alert('Tag deleted successfully!'); } catch (error) { alert(`Error: ${error.message}`); } } } function deleteProjectFromSettings(projectId) { const project = loadProjects().find(p => p.id === projectId); if (!project) return; if (confirm(`Are you sure you want to delete the project "${project.name}"? This will remove it from all searches.`)) { try { deleteProject(projectId); populateProjectsManagement(); alert('Project deleted successfully!'); } catch (error) { alert(`Error: ${error.message}`); } } } function saveSettingsAndClose() { // Save all changes from the forms try { // Save tag changes const tagItems = document.querySelectorAll('.tag-management-item'); tagItems.forEach(item => { const tagId = item.getAttribute('data-tag-id'); const nameInput = item.querySelector('.tag-name-input'); const colorInput = item.querySelector('.tag-color-input'); if (nameInput && colorInput) { const tags = loadTags(); const tag = tags.find(t => t.id === tagId); if (tag) { tag.name = nameInput.value.trim(); tag.color = colorInput.value; } saveTags(tags); } }); // Save project changes const projectItems = document.querySelectorAll('.project-management-item'); projectItems.forEach(item => { const projectId = item.getAttribute('data-project-id'); const nameInput = item.querySelector('.project-name-input'); const descInput = item.querySelector('.project-description-input'); if (nameInput && descInput) { const projects = loadProjects(); const project = projects.find(p => p.id === projectId); if (project) { project.name = nameInput.value.trim(); project.description = descInput.value.trim(); } saveProjects(projects); } }); // Refresh sidebar populateProjectsList(); populateTagsList(); // Close modal closeSettingsModal(); alert('Settings saved successfully!'); } catch (error) { alert(`Error saving settings: ${error.message}`); } } function closeSettingsModal() { const modal = document.getElementById('settings-modal'); if (modal) { modal.remove(); } } // ===== POST-SEARCH TAGGING FUNCTIONALITY - Phase 4 ===== function showPostSearchTagging(query, results, searchType, historyItemId = null) { // Remove any existing tagging or edit interfaces const existingInterface = document.getElementById('post-search-tagging'); const existingToggle = document.getElementById('post-search-toggle'); const editInterface = document.getElementById('edit-organization-interface'); const editToggle = document.getElementById('edit-organization-toggle'); if (existingInterface) { existingInterface.remove(); } if (existingToggle) { existingToggle.remove(); } if (editInterface) { editInterface.remove(); } if (editToggle) { editToggle.remove(); } // Add toggle button for post-search tagging addPostSearchTaggingToggle(query, results, searchType, historyItemId); } function addPostSearchTaggingToggle(query, results, searchType, historyItemId) { const toggleHTML = ` <div id="post-search-toggle" style=" background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 12px 16px; margin: 16px 0; border-left: 4px solid #28a745; cursor: pointer; display: flex; justify-content: space-between; align-items: center; "> <div style=" display: flex; align-items: center; gap: 8px; "> <img src="${tagIconUrl}" alt="Tags" style="width: 16px; height: 16px;" /> <span style=" font-weight: 600; color: #155724; font-size: 14px; ">Organize Search</span> <span style=" font-size: 12px; color: #6c757d; ">Add tags and assign to project</span> </div> <span id="post-search-arrow" style=" font-size: 14px; color: #155724; transition: transform 0.2s ease; ">▼</span> </div> <div id="post-search-content" style="display: none;"> </div> `; // Insert the toggle at the top of results container const resultsContainer = document.getElementById('results-container'); if (resultsContainer) { resultsContainer.insertAdjacentHTML('afterbegin', toggleHTML); // Add click handler for toggle const toggle = document.getElementById('post-search-toggle'); const content = document.getElementById('post-search-content'); const arrow = document.getElementById('post-search-arrow'); if (toggle) { if (historyItemId) { toggle.dataset.historyId = historyItemId; } else { delete toggle.dataset.historyId; } } toggle.addEventListener('click', () => { const isHidden = content.style.display === 'none'; content.style.display = isHidden ? 'block' : 'none'; arrow.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'; arrow.textContent = isHidden ? '▲' : '▼'; if (isHidden) { // Show the tagging interface const currentHistoryId = toggle?.dataset?.historyId || historyItemId; showActualPostSearchTagging(query, results, searchType, currentHistoryId || null); } else { // Clear the content content.innerHTML = ''; } }); } } function showActualPostSearchTagging(query, results, searchType, historyItemId) { const tags = loadTags(); const projects = loadProjects(); // Create tagging interface const taggingHTML = ` <div id="post-search-tagging" style=" background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; margin: 16px 0; border-left: 4px solid #28a745; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; "> <h4 style=" margin: 0; color: #495057; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; "><img src="${tagIconUrl}" alt="Tags" style="width: 16px; height: 16px;" />Organize This Search</h4> <div style=" font-size: 12px; color: #6c757d; ">Optional - help organize your research</div> </div> <div style=" display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; "> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 4px; ">Project</label> <select id="tagging-project" style=" width: 100%; padding: 6px 8px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 13px; background: white; "> <option value="">No project</option> ${projects.map(project => ` <option value="${project.id}">${project.name}</option> `).join('')} </select> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #6c757d; margin-bottom: 4px; ">Tags</label> <div id="tagging-tags" style=" min-height: 32px; border: 1px solid #dee2e6; border-radius: 4px; padding: 4px; background: white; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; "> <button id="add-tag-to-search" style=" background: none; border: 1px dashed #5b8def; color: #5b8def; padding: 2px 6px; border-radius: 12px; font-size: 11px; cursor: pointer; ">+ Add Tag</button> </div> </div> </div> <div style=" display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; border-top: 1px solid #e9ecef; "> <button id="skip-tagging" style=" background: #6c757d; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Skip</button> <button id="save-with-tags" style=" background: #28a745; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Save to History</button> </div> </div> `; // Insert the interface in the content area const content = document.getElementById('post-search-content'); if (content) { content.innerHTML = taggingHTML; // Initialize the interface initializePostSearchTagging(query, results, searchType, historyItemId); // No auto-hide since it's now manually controlled } } function initializePostSearchTagging(query, results, searchType, historyItemId) { const selectedTags = new Set(); let currentHistoryId = historyItemId || null; const toggleElement = document.getElementById('post-search-toggle'); const setToggleHistoryId = (id) => { if (!toggleElement) { return; } if (id) { toggleElement.dataset.historyId = id; } else { delete toggleElement.dataset.historyId; } }; setToggleHistoryId(currentHistoryId); // Add tag button functionality const addTagBtn = document.getElementById('add-tag-to-search'); if (addTagBtn) { addTagBtn.addEventListener('click', () => { showTagSelector(selectedTags); }); } // Skip button const skipBtn = document.getElementById('skip-tagging'); if (skipBtn) { skipBtn.addEventListener('click', () => { if (!currentHistoryId) { currentHistoryId = saveSearchWithSelectedTags(null, query, results, searchType, selectedTags) || currentHistoryId; setToggleHistoryId(currentHistoryId); } const taggingInterface = document.getElementById('post-search-tagging'); if (taggingInterface) { taggingInterface.remove(); } }); } // Save button const saveBtn = document.getElementById('save-with-tags'); if (saveBtn) { saveBtn.addEventListener('click', () => { currentHistoryId = saveSearchWithSelectedTags(currentHistoryId, query, results, searchType, selectedTags) || currentHistoryId; setToggleHistoryId(currentHistoryId); const taggingInterface = document.getElementById('post-search-tagging'); if (taggingInterface) { taggingInterface.remove(); } }); } } function showTagSelector(selectedTags) { const tags = loadTags(); const tagsContainer = document.getElementById('tagging-tags'); if (!tagsContainer) return; // Create dropdown for tag selection const dropdown = document.createElement('select'); dropdown.style.cssText = ` width: 100%; padding: 6px 8px; border: 1px solid #dee2e6; border-radius: 4px; background: white; font-size: 12px; margin: 2px 0; `; dropdown.innerHTML = ` <option value="">Select a tag...</option> ${tags.filter(tag => !selectedTags.has(tag.id)).map(tag => ` <option value="${tag.id}">${tag.name}</option> `).join('')} `; dropdown.addEventListener('change', (e) => { if (e.target.value) { selectedTags.add(e.target.value); updateTagsDisplay(selectedTags); dropdown.remove(); } }); dropdown.addEventListener('blur', () => { setTimeout(() => dropdown.remove(), 100); }); // Remove any existing dropdowns first const existingDropdown = tagsContainer.querySelector('select'); if (existingDropdown) { existingDropdown.remove(); } // Insert dropdown in a clean way tagsContainer.insertBefore(dropdown, tagsContainer.firstChild); dropdown.focus(); } function updateTagsDisplay(selectedTags) { const tagsContainer = document.getElementById('tagging-tags'); if (!tagsContainer) return; const tags = loadTags(); // Clear existing tags display and any dropdowns tagsContainer.innerHTML = ''; // Add selected tags selectedTags.forEach(tagId => { const tag = tags.find(t => t.id === tagId); if (tag) { const tagElement = document.createElement('span'); tagElement.style.cssText = ` background: ${tag.color}20; color: ${tag.color}; border: 1px solid ${tag.color}40; padding: 2px 6px; border-radius: 12px; font-size: 11px; display: flex; align-items: center; gap: 4px; `; tagElement.innerHTML = ` ${tag.name} <button class="remove-tag-btn" data-tag-id="${tagId}" style=" background: none; border: none; color: ${tag.color}; cursor: pointer; padding: 0; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; font-size: 10px; ">×</button> `; // Add event listener for remove button const removeBtn = tagElement.querySelector('.remove-tag-btn'); removeBtn.addEventListener('click', () => { selectedTags.delete(tagId); updateTagsDisplay(selectedTags); }); tagsContainer.appendChild(tagElement); } }); // Re-add the add button const addButton = document.createElement('button'); addButton.id = 'add-tag-to-search'; addButton.style.cssText = ` background: none; border: 1px dashed #5b8def; color: #5b8def; padding: 2px 6px; border-radius: 12px; font-size: 11px; cursor: pointer; `; addButton.textContent = '+ Add Tag'; addButton.addEventListener('click', () => showTagSelector(selectedTags)); tagsContainer.appendChild(addButton); } function saveSearchWithSelectedTags(historyId, query, results, searchType, selectedTags = new Set()) { const projectSelect = document.getElementById('tagging-project'); const selectedProject = projectSelect ? projectSelect.value || null : null; const normalizedTags = selectedTags instanceof Set ? Array.from(selectedTags) : Array.from(new Set(selectedTags || [])); let effectiveHistoryId = historyId || null; if (effectiveHistoryId) { const history = loadSearchHistory(); const existingItem = history.find(item => item.id === effectiveHistoryId); const previousTags = existingItem ? existingItem.tags || [] : []; const previousProject = existingItem ? existingItem.projectId || null : null; const newTags = normalizedTags.filter(tag => !previousTags.includes(tag)); if (newTags.length > 0) { newTags.forEach(tagId => { if (tagId) updateTagUsage(tagId); }); } if (selectedProject && selectedProject !== previousProject) { updateProjectSearchCount(selectedProject); } const updated = updateHistoryEntry(effectiveHistoryId, { results, query, searchType, tags: normalizedTags, projectId: selectedProject }); if (!updated) { effectiveHistoryId = saveSearchToHistory(query, results, searchType, normalizedTags, selectedProject); } } else { effectiveHistoryId = saveSearchToHistory(query, results, searchType, normalizedTags, selectedProject); } // Update sidebar to reflect new usage populateProjectsList(); populateTagsList(); return effectiveHistoryId; } // ===== EDIT EXISTING SEARCH ORGANIZATION - Phase 4.6 ===== function showEditOrganizationInterface(historyItem, mountTarget) { // Remove any existing edit interface or post-search tagging const existingInterface = document.getElementById('edit-organization-interface'); const existingToggle = document.getElementById('edit-organization-toggle'); const postSearchInterface = document.getElementById('post-search-tagging'); const postSearchToggle = document.getElementById('post-search-toggle'); if (existingInterface) { existingInterface.remove(); } if (existingToggle) { existingToggle.remove(); } if (postSearchInterface) { postSearchInterface.remove(); } if (postSearchToggle) { postSearchToggle.remove(); } // Add toggle button for edit organization addEditOrganizationToggle(historyItem, mountTarget); } function addEditOrganizationToggle(historyItem, mountTarget) { const toggleHTML = ` <div id="edit-organization-toggle" style=" background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px 16px; margin: 16px 0; border-left: 4px solid #f39c12; cursor: pointer; display: flex; justify-content: space-between; align-items: center; "> <div style=" display: flex; align-items: center; gap: 8px; "> <img src="${tagIconUrl}" alt="Tags" style="width: 16px; height: 16px;" /> <span style=" font-weight: 600; color: #856404; font-size: 14px; ">Edit Organization</span> <span style=" font-size: 12px; color: #6c757d; ">Modify tags and project assignment</span> </div> <span id="edit-organization-arrow" style=" font-size: 14px; color: #856404; transition: transform 0.2s ease; ">▼</span> </div> <div id="edit-organization-content" style="display: none;"> </div> `; // Insert the toggle at the top of results container let targetContainer = null; if (mountTarget instanceof HTMLElement) { targetContainer = mountTarget; } else if (typeof mountTarget === 'string') { targetContainer = document.getElementById(mountTarget); } if (!targetContainer) { targetContainer = document.getElementById('results-container'); } if (targetContainer) { targetContainer.insertAdjacentHTML('afterbegin', toggleHTML); // Add click handler for toggle const toggle = targetContainer.querySelector('#edit-organization-toggle'); const content = targetContainer.querySelector('#edit-organization-content'); const arrow = targetContainer.querySelector('#edit-organization-arrow'); if (!toggle || !content || !arrow) { return; } toggle.addEventListener('click', () => { const isHidden = content.style.display === 'none'; content.style.display = isHidden ? 'block' : 'none'; arrow.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'; arrow.textContent = isHidden ? '▲' : '▼'; if (isHidden) { // Show the edit interface showActualEditOrganizationInterface(historyItem); } else { // Clear the content content.innerHTML = ''; } }); } } function showActualEditOrganizationInterface(historyItem) { const tags = loadTags(); const projects = loadProjects(); const currentTags = historyItem.tags || []; const currentProject = historyItem.projectId || ''; // Create edit interface const editHTML = ` <div id="edit-organization-interface" style=" background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 16px; margin: 16px 0; border-left: 4px solid #f39c12; "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; "> <h4 style=" margin: 0; color: #856404; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; "><img src="${editIconUrl}" alt="Edit" style="width: 16px; height: 16px;" />Edit Organization</h4> <div style=" font-size: 12px; color: #856404; ">Reopened from history • ${historyItem.date}</div> </div> <div style=" display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; "> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #856404; margin-bottom: 4px; ">Project</label> <select id="edit-project" style=" width: 100%; padding: 6px 8px; border: 1px solid #f1c40f; border-radius: 4px; font-size: 13px; background: white; "> <option value="">No project</option> ${projects.map(project => ` <option value="${project.id}" ${project.id === currentProject ? 'selected' : ''}>${project.name}</option> `).join('')} </select> </div> <div> <label style=" display: block; font-size: 12px; font-weight: 600; color: #856404; margin-bottom: 4px; ">Tags</label> <div id="edit-tags" style=" min-height: 32px; border: 1px solid #f1c40f; border-radius: 4px; padding: 4px; background: white; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; "> <button id="add-tag-to-edit" style=" background: none; border: 1px dashed #f39c12; color: #f39c12; padding: 2px 6px; border-radius: 12px; font-size: 11px; cursor: pointer; ">+ Add Tag</button> </div> </div> </div> <div style=" display: flex; justify-content: space-between; align-items: center; padding-top: 8px; border-top: 1px solid #f1c40f; "> <div style=" font-size: 12px; color: #856404; ">Changes will update this search in your history</div> <div style="display: flex; gap: 8px;"> <button id="cancel-edit" style=" background: #6c757d; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Cancel</button> <button id="save-edit" style=" background: #f39c12; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; ">Save Changes</button> </div> </div> </div> `; // Insert the interface in the content area const content = document.getElementById('edit-organization-content'); if (content) { content.innerHTML = editHTML; // Initialize the interface with current tags/project initializeEditOrganizationInterface(historyItem, currentTags); } } function initializeEditOrganizationInterface(historyItem, selectedTags) { const selectedTagsSet = new Set(selectedTags); // Display current tags updateEditTagsDisplay(selectedTagsSet); // Add tag button functionality const addTagBtn = document.getElementById('add-tag-to-edit'); if (addTagBtn) { addTagBtn.addEventListener('click', () => { showEditTagSelector(selectedTagsSet); }); } // Cancel button const cancelBtn = document.getElementById('cancel-edit'); if (cancelBtn) { cancelBtn.addEventListener('click', () => { const editInterface = document.getElementById('edit-organization-interface'); if (editInterface) { editInterface.remove(); } }); } // Save button const saveBtn = document.getElementById('save-edit'); if (saveBtn) { saveBtn.addEventListener('click', () => { saveEditedOrganization(historyItem, selectedTagsSet); }); } } function showEditTagSelector(selectedTags) { const tags = loadTags(); const tagsContainer = document.getElementById('edit-tags'); if (!tagsContainer) return; // Create dropdown for tag selection const dropdown = document.createElement('select'); dropdown.style.cssText = ` width: 100%; padding: 6px 8px; border: 1px solid #f1c40f; border-radius: 4px; background: white; font-size: 12px; margin: 2px 0; `; dropdown.innerHTML = ` <option value="">Select a tag...</option> ${tags.filter(tag => !selectedTags.has(tag.id)).map(tag => ` <option value="${tag.id}">${tag.name}</option> `).join('')} `; dropdown.addEventListener('change', (e) => { if (e.target.value) { selectedTags.add(e.target.value); updateEditTagsDisplay(selectedTags); dropdown.remove(); } }); dropdown.addEventListener('blur', () => { setTimeout(() => dropdown.remove(), 100); }); // Remove any existing dropdowns first const existingDropdown = tagsContainer.querySelector('select'); if (existingDropdown) { existingDropdown.remove(); } // Insert dropdown in a clean way tagsContainer.insertBefore(dropdown, tagsContainer.firstChild); dropdown.focus(); } function updateEditTagsDisplay(selectedTags) { const tagsContainer = document.getElementById('edit-tags'); if (!tagsContainer) return; const tags = loadTags(); // Clear existing tags display and any dropdowns tagsContainer.innerHTML = ''; // Add selected tags selectedTags.forEach(tagId => { const tag = tags.find(t => t.id === tagId); if (tag) { const tagElement = document.createElement('span'); tagElement.style.cssText = ` background: ${tag.color}20; color: ${tag.color}; border: 1px solid ${tag.color}40; padding: 2px 6px; border-radius: 12px; font-size: 11px; display: flex; align-items: center; gap: 4px; `; tagElement.innerHTML = ` <img src="${tagIconUrl}" alt="Tag" style="width: 14px; height: 14px;" /> <span>${tag.name}</span> <button class="remove-edit-tag-btn" data-tag-id="${tagId}" style=" background: none; border: none; color: ${tag.color}; cursor: pointer; padding: 0; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; font-size: 10px; ">×</button> `; // Add event listener for remove button const removeBtn = tagElement.querySelector('.remove-edit-tag-btn'); removeBtn.addEventListener('click', () => { selectedTags.delete(tagId); updateEditTagsDisplay(selectedTags); }); tagsContainer.appendChild(tagElement); } }); // Re-add the add button const addButton = document.createElement('button'); addButton.id = 'add-tag-to-edit'; addButton.style.cssText = ` background: none; border: 1px dashed #f39c12; color: #f39c12; padding: 2px 6px; border-radius: 12px; font-size: 11px; cursor: pointer; `; addButton.textContent = '+ Add Tag'; addButton.addEventListener('click', () => showEditTagSelector(selectedTags)); tagsContainer.appendChild(addButton); } function saveEditedOrganization(historyItem, selectedTags) { const projectSelect = document.getElementById('edit-project'); const newProject = projectSelect ? projectSelect.value || null : null; const newTags = Array.from(selectedTags); // Update the history item const history = loadSearchHistory(); const itemIndex = history.findIndex(h => h.id === historyItem.id); if (itemIndex !== -1) { // Remove old tag usage counts if (historyItem.tags) { historyItem.tags.forEach(tagId => { const tags = loadTags(); const tag = tags.find(t => t.id === tagId); if (tag && tag.usageCount > 0) { tag.usageCount = Math.max(0, tag.usageCount - 1); } saveTags(tags); }); } // Remove old project count if (historyItem.projectId) { const projects = loadProjects(); const project = projects.find(p => p.id === historyItem.projectId); if (project && project.searchCount > 0) { project.searchCount = Math.max(0, project.searchCount - 1); } saveProjects(projects); } // Update history item history[itemIndex] = { ...historyItem, tags: newTags, projectId: newProject }; // Update new tag usage counts newTags.forEach(tagId => { updateTagUsage(tagId); }); // Update new project count if (newProject) { updateProjectSearchCount(newProject); } // Save updated history localStorage.setItem('chatgpt-product-search-history', JSON.stringify(history)); // Update sidebar to reflect changes populateProjectsList(); populateTagsList(); // Remove edit interface const editInterface = document.getElementById('edit-organization-interface'); if (editInterface) { editInterface.remove(); } // Show confirmation const resultsContainer = document.getElementById('results-container'); if (resultsContainer) { const confirmation = document.createElement('div'); confirmation.style.cssText = ` background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 8px 12px; margin: 16px 0; color: #155724; font-size: 13px; text-align: center; `; confirmation.innerHTML = formatStatusMessage('success', 'Organization updated successfully!'); confirmation.style.display = 'inline-flex'; confirmation.style.alignItems = 'center'; confirmation.style.justifyContent = 'center'; confirmation.style.gap = '8px'; resultsContainer.insertAdjacentElement('afterbegin', confirmation); // Remove confirmation after 3 seconds setTimeout(() => { confirmation.remove(); }, 3000); } } } // ===== END EDIT EXISTING SEARCH ORGANIZATION ===== // ===== END POST-SEARCH TAGGING FUNCTIONALITY ===== // ===== HISTORY ENHANCEMENT FUNCTIONS - Phase 4 ===== function generateHistoryTagsAndProject(item) { const tags = loadTags(); const projects = loadProjects(); const itemTags = Array.isArray(item.tags) ? item.tags : []; const itemProject = item.projectId; const chips = []; if (itemProject) { const project = projects.find(p => p.id === itemProject); if (project) { chips.push(` <span class="history-chip history-chip--project" ${project.description ? `title="${escapeAttributeValue(project.description)}"` : ''}> <img src="${projectIconUrl}" alt="" aria-hidden="true" /> <span>${escapeHTML(project.name)}</span> </span> `); } } if (itemTags.length > 0) { itemTags.forEach(tagId => { const tag = tags.find(t => t.id === tagId); if (tag) { const safeName = escapeHTML(tag.name); const dotColor = tag.color || '#5b8def'; chips.push(` <span class="history-chip history-chip--tag" style="--chip-tag-color: ${dotColor};"> <span class="history-chip-dot"></span> <span>${safeName}</span> </span> `); } }); } if (chips.length === 0) { return ''; } return `<div class="history-row-labels">${chips.join('')}</div>`; } // ===== END HISTORY ENHANCEMENT FUNCTIONS ===== // ===== ADVANCED FILTERING SYSTEM - Phase 5 ===== let historyDayFormatter; let historyTimeFormatter; try { historyDayFormatter = new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'long', year: 'numeric' }); } catch (error) { historyDayFormatter = { format: date => date.toLocaleDateString() }; } try { historyTimeFormatter = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }); } catch (error) { historyTimeFormatter = { format: date => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }; } // Global filter state let currentFilters = { text: '', rawText: '', project: '', tags: [], market: 'all', isActive: false }; function initializeAdvancedFiltering() { // Populate filter dropdowns and checkboxes populateFilterOptions(); // Add event listeners const toggleBtn = document.getElementById('toggle-filters'); const applyBtn = document.getElementById('apply-filters'); const clearBtn = document.getElementById('clear-filters'); const filterText = document.getElementById('filter-text'); const filterProject = document.getElementById('filter-project'); const filterMarket = document.getElementById('filter-market'); if (toggleBtn) { toggleBtn.addEventListener('click', toggleFilterPanel); } if (applyBtn) { applyBtn.addEventListener('click', applyFilters); } if (clearBtn) { clearBtn.addEventListener('click', clearAllFilters); } if (filterText) { filterText.addEventListener('input', updateFilterSummary); } if (filterProject) { filterProject.addEventListener('change', updateFilterSummary); } if (filterMarket) { filterMarket.addEventListener('change', updateFilterSummary); } } function populateFilterOptions() { const projects = loadProjects(); const tags = loadTags(); const marketSelect = document.getElementById('filter-market'); // Populate projects dropdown const projectSelect = document.getElementById('filter-project'); if (projectSelect) { projectSelect.innerHTML = '<option value="">All Projects</option>'; projects.forEach(project => { const option = document.createElement('option'); option.value = project.id; option.textContent = project.name; projectSelect.appendChild(option); }); } if (marketSelect) { const previousValue = currentFilters.market || 'all'; marketSelect.innerHTML = '<option value="all">All Markets</option>'; MARKET_OPTIONS.forEach(option => { const optionEl = document.createElement('option'); optionEl.value = option.value; optionEl.textContent = option.label; marketSelect.appendChild(optionEl); }); if (previousValue && marketSelect.querySelector(`option[value="${previousValue}"]`)) { marketSelect.value = previousValue; } else { marketSelect.value = 'all'; currentFilters.market = 'all'; } } // Populate tags checkboxes const tagsContainer = document.getElementById('filter-tags'); if (tagsContainer) { tagsContainer.innerHTML = ''; if (tags.length === 0) { tagsContainer.innerHTML = ` <div style=" color: #6c757d; font-size: 12px; font-style: italic; padding: 8px; ">No tags available</div> `; } else { tags.forEach(tag => { const tagCheckbox = document.createElement('label'); tagCheckbox.style.cssText = ` display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 12px; background: ${tag.color}15; border: 1px solid ${tag.color}30; cursor: pointer; font-size: 12px; color: ${tag.color}; white-space: nowrap; `; tagCheckbox.innerHTML = ` <input type="checkbox" value="${tag.id}" style=" margin: 0; width: 12px; height: 12px; " /> <img src="${tagIconUrl}" alt="Tag" style="width: 14px; height: 14px;" /> <span>${tag.name}</span> `; const checkbox = tagCheckbox.querySelector('input'); checkbox.addEventListener('change', updateFilterSummary); tagsContainer.appendChild(tagCheckbox); }); } } updateFilterSummary(); } function toggleFilterPanel() { const panel = document.getElementById('filter-panel'); const toggleText = document.getElementById('filter-toggle-text'); if (panel && toggleText) { const isVisible = panel.style.display !== 'none'; panel.style.display = isVisible ? 'none' : 'block'; toggleText.textContent = isVisible ? 'Filters' : 'Hide Filters'; } } function updateFilterSummary() { const filterText = document.getElementById('filter-text'); const filterProject = document.getElementById('filter-project'); const filterMarket = document.getElementById('filter-market'); const tagCheckboxes = document.querySelectorAll('#filter-tags input[type="checkbox"]:checked'); const summary = document.getElementById('filter-summary'); if (!summary) return; let activeCount = 0; let summaryParts = []; if (filterText && filterText.value.trim()) { activeCount++; summaryParts.push('text search'); } if (filterProject && filterProject.value) { activeCount++; const selectedProject = loadProjects().find(p => p.id === filterProject.value); summaryParts.push(`project: ${selectedProject?.name || 'Unknown'}`); } if (filterMarket && filterMarket.value && filterMarket.value !== 'all') { activeCount++; const marketOption = getMarketOption(filterMarket.value); summaryParts.push(`market: ${marketOption.label}`); } if (tagCheckboxes.length > 0) { activeCount++; summaryParts.push(`${tagCheckboxes.length} tag${tagCheckboxes.length > 1 ? 's' : ''}`); } if (activeCount === 0) { summary.textContent = 'No filters applied'; } else { summary.textContent = `${activeCount} filter${activeCount > 1 ? 's' : ''} ready: ${summaryParts.join(', ')}`; } } function applyFilters() { const filterText = document.getElementById('filter-text'); const filterProject = document.getElementById('filter-project'); const tagCheckboxes = document.querySelectorAll('#filter-tags input[type="checkbox"]:checked'); const rawText = filterText ? filterText.value.trim() : ''; const normalizedText = rawText.toLowerCase(); // Update global filter state const filterMarket = document.getElementById('filter-market'); const nextProject = filterProject ? filterProject.value : ''; const nextTags = Array.from(tagCheckboxes).map(cb => cb.value); const nextMarket = filterMarket ? filterMarket.value : 'all'; currentFilters = { text: normalizedText, rawText: rawText, project: nextProject, tags: nextTags, market: nextMarket, isActive: Boolean(normalizedText || nextProject || nextTags.length > 0 || (nextMarket && nextMarket !== 'all')) }; // Apply filters to history const history = loadSearchHistory(); const filteredHistory = applyAdvancedFilters(history); showHistoryListView(); renderHistoryList(filteredHistory); // Update filter chips display updateFilterChips(); updateFilterSummary(); // Sync filters to Analysis tab syncAnalysisFiltersWithHistoryFilters(); // Hide filter panel const panel = document.getElementById('filter-panel'); const toggleText = document.getElementById('filter-toggle-text'); if (panel) panel.style.display = 'none'; if (toggleText) toggleText.textContent = 'Filters'; } function applyAdvancedFilters(history) { if (!currentFilters.isActive && !currentFilters.text && !currentFilters.project && currentFilters.tags.length === 0 && (!currentFilters.market || currentFilters.market === 'all')) { return history; } return history.filter(item => { // Text filter - search in query, rationale, and review summary if (currentFilters.text) { const searchableText = [ item.query || '', item.results?.rationale || '', item.results?.reviewSummary || '', ...(item.results?.reviews || []).map(r => r.content || ''), ...(item.results?.citations || []).map(c => c.snippet || ''), ...(item.results?.productLinks || []).map(p => p.title || '') ].join(' ').toLowerCase(); if (!searchableText.includes(currentFilters.text)) { return false; } } // Project filter if (currentFilters.project) { if (item.projectId !== currentFilters.project) { return false; } } // Tags filter - item must have ALL selected tags (AND logic) if (currentFilters.tags.length > 0) { const itemTags = item.tags || []; if (!currentFilters.tags.every(tagId => itemTags.includes(tagId))) { return false; } } // Market filter - match stored market value (legacy entries may not have market) if (currentFilters.market && currentFilters.market !== 'all') { const itemMarket = item.market || null; if (itemMarket !== currentFilters.market) { return false; } } return true; }); } function updateFilterChips() { const activeFiltersDiv = document.getElementById('active-filters'); const filterChips = document.getElementById('filter-chips'); if (!activeFiltersDiv || !filterChips) return; // Clear existing chips filterChips.innerHTML = ''; let hasActiveFilters = false; // Text filter chip if (currentFilters.text) { hasActiveFilters = true; const chip = createFilterChip('text', `Text: "${currentFilters.text}"`, () => { const filterText = document.getElementById('filter-text'); if (filterText) filterText.value = ''; applyFilters(); }); filterChips.appendChild(chip); } // Project filter chip if (currentFilters.project) { hasActiveFilters = true; const project = loadProjects().find(p => p.id === currentFilters.project); const chip = createFilterChip('project', `<span style="display:flex; align-items:center; gap:4px;"><img src="${projectIconUrl}" alt="Project" style="width: 14px; height: 14px;" />${project?.name || 'Unknown Project'}</span>`, () => { const filterProject = document.getElementById('filter-project'); if (filterProject) filterProject.value = ''; applyFilters(); }); filterChips.appendChild(chip); } if (currentFilters.market && currentFilters.market !== 'all') { hasActiveFilters = true; const option = getMarketOption(currentFilters.market); const chip = createFilterChip('market', `🌍 ${option.label}`, () => { const filterMarket = document.getElementById('filter-market'); if (filterMarket) filterMarket.value = 'all'; applyFilters(); }); filterChips.appendChild(chip); } // Tag filter chips if (currentFilters.tags.length > 0) { hasActiveFilters = true; const tags = loadTags(); currentFilters.tags.forEach(tagId => { const tag = tags.find(t => t.id === tagId); if (tag) { const chip = createFilterChip('tag', tag.name, () => { const checkbox = document.querySelector(`#filter-tags input[value="${tagId}"]`); if (checkbox) checkbox.checked = false; applyFilters(); }, tag.color); filterChips.appendChild(chip); } }); } // Show/hide active filters section activeFiltersDiv.style.display = hasActiveFilters ? 'block' : 'none'; } function createFilterChip(type, text, onRemove, color = '#5b8def') { const chip = document.createElement('div'); chip.style.cssText = ` display: flex; align-items: center; gap: 6px; background: ${color}15; color: ${color}; border: 1px solid ${color}30; padding: 4px 8px; border-radius: 12px; font-size: 12px; white-space: nowrap; `; chip.innerHTML = ` <span>${text}</span> <button style=" background: none; border: none; color: ${color}; cursor: pointer; padding: 0; width: 14px; height: 14px; display: flex; align-items: center; justify-content: center; font-size: 10px; border-radius: 50%; ">×</button> `; const removeBtn = chip.querySelector('button'); removeBtn.addEventListener('click', onRemove); return chip; } function clearAllFilters() { // Reset form const filterText = document.getElementById('filter-text'); const filterProject = document.getElementById('filter-project'); const filterMarket = document.getElementById('filter-market'); const tagCheckboxes = document.querySelectorAll('#filter-tags input[type="checkbox"]'); if (filterText) filterText.value = ''; if (filterProject) filterProject.value = ''; if (filterMarket) filterMarket.value = 'all'; tagCheckboxes.forEach(cb => cb.checked = false); // Reset global state currentFilters = { text: '', rawText: '', project: '', tags: [], market: 'all', isActive: false }; // Show all history const history = loadSearchHistory(); renderHistoryList(history); // Update UI updateFilterSummary(); updateFilterChips(); } // ===== SIDEBAR-TO-FILTER INTEGRATION ===== function filterByProject(projectId) { const tab = typeof _activeTab === 'function' ? _activeTab() : (typeof getActiveTab === 'function' ? getActiveTab() : 'history'); if (tab === 'reports') { _applyToAnalysis({ projectId }); return; } _applyToHistory({ projectId }); } function filterByTag(tagId) { const tab = typeof _activeTab === 'function' ? _activeTab() : (typeof getActiveTab === 'function' ? getActiveTab() : 'history'); if (tab === 'reports') { _applyToAnalysis({ tagId }); return; } _applyToHistory({ tagId }); } // ===== END SIDEBAR-TO-FILTER INTEGRATION ===== // ===== END ADVANCED FILTERING SYSTEM ===== // ===== END SIDEBAR FUNCTIONALITY ===== function deleteHistoryItem(itemId) { try { const history = loadSearchHistory(); const itemToDelete = history.find(item => item.id === itemId); // Decrement counts before deleting if (itemToDelete) { // Decrement project count if (itemToDelete.projectId) { decrementProjectSearchCount(itemToDelete.projectId); } // Decrement tag usage counts if (itemToDelete.tags && Array.isArray(itemToDelete.tags)) { itemToDelete.tags.forEach(tagId => { if (tagId) { decrementTagUsage(tagId); } }); } } const filteredHistory = history.filter(item => item.id !== itemId); localStorage.setItem('chatgpt-product-search-history', JSON.stringify(filteredHistory)); loadHistory(); // Update sidebar to reflect new counts populateProjectsList(); populateTagsList(); } catch (error) { console.error('Error deleting history item:', error); } } function resetToCleanSearchState() { // Clear search input fields const searchQuery = document.getElementById('search-query'); const multiSearchQuery = document.getElementById('multi-search-query'); const multiProductToggle = document.getElementById('multi-product-toggle'); const searchControls = document.getElementById('search-controls'); const collapseToggle = document.getElementById('collapse-toggle'); const collapseText = document.getElementById('collapse-text'); if (searchQuery) { searchQuery.value = ''; } if (multiSearchQuery) { multiSearchQuery.value = ''; } if (multiProductToggle) { multiProductToggle.checked = false; // Trigger change event to update UI multiProductToggle.dispatchEvent(new Event('change')); } if (searchControls) { searchControls.style.display = 'block'; } if (collapseText) { collapseText.textContent = '▲ Hide'; } if (collapseToggle) { collapseToggle.style.display = 'none'; collapseToggle.style.background = 'rgba(0, 123, 255, 0.1)'; collapseToggle.style.border = '1px solid rgba(0, 123, 255, 0.2)'; collapseToggle.style.color = '#5b8def'; } // Remove any organization interfaces const editToggle = document.getElementById('edit-organization-toggle'); const editContent = document.getElementById('edit-organization-content'); const postSearchToggle = document.getElementById('post-search-toggle'); const postSearchContent = document.getElementById('post-search-content'); if (editToggle) editToggle.remove(); if (editContent) editContent.remove(); if (postSearchToggle) postSearchToggle.remove(); if (postSearchContent) postSearchContent.remove(); // Reset results container to welcome state const resultsContainer = document.getElementById('results-container'); if (resultsContainer) { resultsContainer.innerHTML = createWelcomeState(); // Re-initialize token status after creating new welcome state if (typeof initializeTokenStatus === 'function') { initializeTokenStatus(); } } } function loadHistory() { // Force-hide other containers so nothing leaks into History const searchArea = document.getElementById('search-area'); const resultsContainer = document.getElementById('results-container'); const reportsContainer = document.getElementById('reports-container'); const historyContainer = document.getElementById('history-container'); if (searchArea) searchArea.style.display = 'none'; if (resultsContainer) resultsContainer.style.display = 'none'; if (reportsContainer) reportsContainer.style.display = 'none'; if (historyContainer) historyContainer.style.display = 'block'; showHistoryListView(); const history = loadSearchHistory(); const historyWelcome = document.getElementById('history-welcome-state'); const historyContent = document.getElementById('history-content'); const historyList = document.getElementById('history-list'); const clearHistoryBtn = document.getElementById('clear-history-btn'); // Ensure history elements are visible again after visiting other tabs ['history-content', 'history-welcome-state', 'history-list'].forEach(id => { const el = document.getElementById(id); if (el) { el.style.visibility = 'visible'; } }); if (history.length === 0) { historyWelcome.style.display = 'flex'; historyContent.style.display = 'none'; if (clearHistoryBtn) { clearHistoryBtn.style.display = 'none'; } } else { historyWelcome.style.display = 'none'; historyContent.style.display = 'block'; if (clearHistoryBtn) { clearHistoryBtn.style.display = 'block'; } // Initialize advanced filtering system initializeAdvancedFiltering(); // Restore filter UI to match current filter state const filterTextField = document.getElementById('filter-text'); const filterProjectField = document.getElementById('filter-project'); const filterTagCheckboxes = document.querySelectorAll('#filter-tags input[type="checkbox"]'); if (filterTextField) { filterTextField.value = currentFilters.rawText || ''; } if (filterProjectField) { filterProjectField.value = currentFilters.project || ''; } if (filterTagCheckboxes.length > 0) { const activeTags = new Set(currentFilters.tags || []); filterTagCheckboxes.forEach(cb => { cb.checked = activeTags.has(cb.value); }); } currentFilters.isActive = Boolean( (currentFilters.rawText && currentFilters.rawText.trim().length) || (currentFilters.project && currentFilters.project.length) || (currentFilters.tags && currentFilters.tags.length) ); if (typeof updateFilterSummary === 'function') { updateFilterSummary(); } if (typeof updateFilterChips === 'function') { updateFilterChips(); } // Apply current filters (if any) or show all const filteredHistory = applyAdvancedFilters(history); renderHistoryList(filteredHistory); } } function renderHistoryList(history) { const historyList = document.getElementById('history-list'); if (!historyList) { return; } const listItems = Array.isArray(history) ? history.slice() : []; if (listItems.length === 0) { historyList.innerHTML = '<div class="history-empty-row">No searches match the current filters.</div>'; return; } const groups = []; const groupCache = new Map(); listItems.forEach(item => { const itemDate = resolveHistoryDate(item); const groupKey = buildHistoryGroupKey(itemDate); let group = groupCache.get(groupKey); if (!group) { group = { label: buildHistoryGroupLabel(itemDate), items: [] }; groupCache.set(groupKey, group); groups.push(group); } group.items.push({ item, itemDate }); }); historyList.innerHTML = groups.map(group => ` <section class="history-day-group"> <header class="history-day-header">${group.label}</header> <div class="history-day-body"> ${group.items.map(({ item, itemDate }) => renderHistoryRowMarkup(item, itemDate)).join('')} </div> </section> `).join(''); historyList.querySelectorAll('.reopen-search-btn').forEach(btn => { btn.addEventListener('click', event => { event.stopPropagation(); const itemId = btn.getAttribute('data-id'); if (itemId) { reopenSearch(itemId); } }); }); historyList.querySelectorAll('.delete-history-btn').forEach(btn => { btn.addEventListener('click', event => { event.stopPropagation(); const itemId = btn.getAttribute('data-id'); if (itemId) { deleteHistoryItem(itemId); } }); }); historyList.querySelectorAll('.history-row').forEach(row => { row.addEventListener('click', event => { if (event.target.closest('.history-icon-btn')) { return; } const itemId = row.getAttribute('data-id'); if (itemId) { reopenSearch(itemId); } }); row.addEventListener('keydown', event => { if (event.defaultPrevented) { return; } if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); const itemId = row.getAttribute('data-id'); if (itemId) { reopenSearch(itemId); } } }); }); historyList.querySelectorAll('.history-query-toggle').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); const container = btn.closest('.history-query-group'); if (!container) { return; } const isExpanded = container.dataset.expanded === 'true'; const total = btn.getAttribute('data-total') || container.querySelectorAll('.history-query-chip').length; container.dataset.expanded = isExpanded ? 'false' : 'true'; btn.textContent = isExpanded ? `Show all (${total})` : 'Show less'; }); }); const historyContainer = document.getElementById('history-container'); if (historyContainer) { const analysisResults = historyContainer.querySelector('#analysis-results'); const analysisContent = historyContainer.querySelector('#analysis-content'); const citationTable = historyContainer.querySelector('#citation-sources-table'); const reviewTable = historyContainer.querySelector('#review-sources-table'); if (analysisResults) { analysisResults.remove(); } if (analysisContent) { analysisContent.remove(); } if (citationTable) { citationTable.remove(); } if (reviewTable) { reviewTable.remove(); } } } function resolveHistoryDate(item) { if (item && typeof item.timestamp === 'number') { const fromTimestamp = new Date(item.timestamp); if (!Number.isNaN(fromTimestamp.getTime())) { return fromTimestamp; } } if (item && typeof item.date === 'string') { const parsed = new Date(item.date); if (!Number.isNaN(parsed.getTime())) { return parsed; } } return new Date(); } function buildHistoryGroupKey(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function buildHistoryGroupLabel(date) { if (historyDayFormatter && typeof historyDayFormatter.format === 'function') { try { return historyDayFormatter.format(date); } catch (error) { // Fall through to manual formatting } } return date.toLocaleDateString(); } function formatHistoryTime(date) { if (!date) { return ''; } if (historyTimeFormatter && typeof historyTimeFormatter.format === 'function') { try { return historyTimeFormatter.format(date); } catch (error) { // Fall through to manual formatting } } return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function extractHistoryQueries(item) { if (!item) { return []; } if (item.searchType === 'multi' && typeof item.query === 'string') { const parts = item.query.split('\n') .map(q => q.trim()) .filter(Boolean); if (parts.length) { return parts; } } if (typeof item.query === 'string' && item.query.trim()) { return [item.query.trim()]; } return []; } function renderHistoryQueryChips(queries, searchType) { const total = queries.length; if (total === 0) { return '<div class="history-query-group"><span class="history-query-chip">Untitled search</span></div>'; } const isMulti = searchType === 'multi'; const maxVisible = isMulti ? Math.min(2, total) : total; const chipsHtml = queries.map((text, index) => { const extraAttr = index >= maxVisible ? ' data-extra="true"' : ''; return `<span class="history-query-chip"${extraAttr}>${escapeHTML(text)}</span>`; }).join(''); const hasHidden = total > maxVisible; const toggleHtml = hasHidden ? `<button type="button" class="history-query-toggle" data-total="${total}">Show all (${total})</button>` : ''; const expandedAttr = hasHidden ? 'data-expanded="false"' : 'data-expanded="true"'; return `<div class="history-query-group" ${expandedAttr}>${chipsHtml}${toggleHtml}</div>`; } function renderHistoryMarketInfo(item) { if (!item) { return ''; } const marketValue = item.market || item.marketCode; if (!marketValue) { return ''; } const option = getMarketOption(marketValue); const fallbackLabel = escapeHTML(item.marketLabel || marketValue); if (!option) { if (!fallbackLabel) { return ''; } return ` <span class="history-market"> <span class="history-market-text"> <span class="history-market-label">${fallbackLabel}</span> </span> </span> `; } const countryLabel = escapeHTML(option.country || option.label || item.marketLabel || marketValue); const languageLabel = escapeHTML(option.language || ''); const flagSrc = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL(option.icon) : option.icon; const altText = escapeAttributeValue(`${countryLabel} flag`); return ` <span class="history-market"> <img src="${flagSrc}" alt="${altText}" /> <span class="history-market-text"> <span class="history-market-label">${countryLabel}</span> ${languageLabel ? `<span class="history-market-language">${languageLabel}</span>` : ''} </span> </span> `; } function renderHistoryRowMarkup(item, itemDate) { const queries = extractHistoryQueries(item); const queryChipsMarkup = renderHistoryQueryChips(queries, item.searchType); const tagsHtml = generateHistoryTagsAndProject(item); const timeLabel = formatHistoryTime(itemDate); const marketMarkup = renderHistoryMarketInfo(item); const metaPieces = []; if (marketMarkup) { metaPieces.push(marketMarkup); } if (timeLabel) { metaPieces.push(`<span class="history-time">${clockIconSvg}<span>${escapeHTML(timeLabel)}</span></span>`); } const metaHtml = metaPieces.length ? `<div class="history-meta-group">${metaPieces.join('')}</div>` : ''; const summary = item?.results?.summary || {}; const statsBadges = [ { label: 'reviews', value: summary.total_reviews || 0 }, { label: 'products', value: summary.total_products || 0 }, { label: 'links', value: summary.total_product_links || 0 } ].map(stat => `<span><strong>${escapeHTML(String(stat.value))}</strong><span>${escapeHTML(stat.label)}</span></span>`).join(''); const statsHtml = `<div class="history-stats">${statsBadges}</div>`; const accessibleLabel = escapeAttributeValue(queries[0] || 'history item'); return ` <div class="history-row" data-id="${item.id}" role="button" tabindex="0" aria-label="Open history item ${accessibleLabel}"> <div class="history-row-left"> ${queryChipsMarkup} ${tagsHtml || ''} </div> <div class="history-row-info"> ${metaHtml}${statsHtml} </div> <div class="history-row-actions"> <button class="history-icon-btn history-icon-btn--primary reopen-search-btn" data-id="${item.id}" aria-label="Open search ${accessibleLabel}">Open</button> <button class="history-icon-btn history-icon-btn--danger delete-history-btn" data-id="${item.id}" aria-label="Delete search ${accessibleLabel}">Delete</button> </div> </div> `; } function toggleMultiProductSearch() { 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'); if (!multiProductToggle) return; const isMultiMode = multiProductToggle.checked; if (isMultiMode) { // Switch to multi-product mode if (singleProductInput) singleProductInput.style.display = 'none'; if (multiProductInput) multiProductInput.style.display = 'block'; if (toggleBackground) toggleBackground.style.background = '#5b8def'; if (toggleSlider) toggleSlider.style.transform = 'translateX(20px)'; } else { // Switch to single-product mode if (singleProductInput) singleProductInput.style.display = 'flex'; if (multiProductInput) multiProductInput.style.display = 'none'; if (toggleBackground) toggleBackground.style.background = '#dee2e6'; if (toggleSlider) toggleSlider.style.transform = 'translateX(0px)'; } moveMarketSelector(isMultiMode); } function showHistoryListView() { const historyContent = document.getElementById('history-content'); const historyDetailContainer = document.getElementById('history-detail-container'); const historyDetailResults = document.getElementById('history-detail-results'); const historyDetailTitle = document.getElementById('history-detail-title'); const historyDetailMeta = document.getElementById('history-detail-meta'); const historyDetailOpenSearch = document.getElementById('history-detail-open-search'); if (historyContent) { historyContent.style.display = 'block'; } if (historyDetailContainer) { historyDetailContainer.style.display = 'none'; delete historyDetailContainer.dataset.historyId; } if (historyDetailResults) { historyDetailResults.innerHTML = ''; } if (historyDetailTitle) { historyDetailTitle.innerHTML = ''; } if (historyDetailMeta) { historyDetailMeta.textContent = ''; } if (historyDetailOpenSearch) { historyDetailOpenSearch.disabled = true; historyDetailOpenSearch.style.cursor = 'not-allowed'; historyDetailOpenSearch.style.opacity = '0.6'; delete historyDetailOpenSearch.dataset.historyId; } } function showHistoryDetailView(item) { const historyContent = document.getElementById('history-content'); const historyDetailContainer = document.getElementById('history-detail-container'); const historyDetailResults = document.getElementById('history-detail-results'); const historyDetailTitle = document.getElementById('history-detail-title'); const historyDetailMeta = document.getElementById('history-detail-meta'); const historyDetailOpenSearch = document.getElementById('history-detail-open-search'); if (!historyDetailContainer || !historyDetailResults) { openHistoryItemInSearch(item); return; } if (historyContent) { historyContent.style.display = 'none'; } historyDetailContainer.style.display = 'flex'; historyDetailContainer.dataset.historyId = String(item.id); if (historyDetailTitle) { historyDetailTitle.innerHTML = ''; const chipsWrapper = document.createElement('div'); chipsWrapper.style.display = 'flex'; chipsWrapper.style.flexWrap = 'wrap'; chipsWrapper.style.gap = '8px'; chipsWrapper.dataset.expanded = 'false'; const applyChipStyles = (chipEl) => { chipEl.style.display = 'inline-flex'; chipEl.style.alignItems = 'center'; chipEl.style.background = '#e3f2fd'; chipEl.style.color = '#1565c0'; chipEl.style.padding = '4px 8px'; chipEl.style.borderRadius = '12px'; chipEl.style.fontSize = '13px'; chipEl.style.border = '1px solid #bbdefb'; }; const queries = (() => { if (item.searchType === 'multi' && typeof item.query === 'string') { const parts = item.query.split('\n').map(q => q.trim()).filter(Boolean); if (parts.length === 0 && item.query) { return [item.query]; } return parts; } if (item.query) { return [item.query]; } return []; })(); if (queries.length === 0) { const chip = document.createElement('span'); chip.textContent = 'Untitled search'; applyChipStyles(chip); chipsWrapper.appendChild(chip); } else { queries.forEach((text, index) => { const chip = document.createElement('span'); chip.textContent = text; applyChipStyles(chip); if (index > 0) { chip.dataset.extra = 'true'; chip.style.display = 'none'; } chipsWrapper.appendChild(chip); }); } historyDetailTitle.appendChild(chipsWrapper); if (queries.length > 1) { const toggleButton = document.createElement('button'); toggleButton.id = 'history-detail-toggle'; toggleButton.style.background = 'none'; toggleButton.style.border = 'none'; toggleButton.style.color = '#5b8def'; toggleButton.style.fontSize = '13px'; toggleButton.style.cursor = 'pointer'; toggleButton.textContent = `Show all (${queries.length})`; toggleButton.addEventListener('click', () => { const isExpanded = chipsWrapper.dataset.expanded === 'true'; const nextState = !isExpanded; chipsWrapper.dataset.expanded = nextState ? 'true' : 'false'; chipsWrapper.querySelectorAll('span[data-extra="true"]').forEach(chip => { chip.style.display = nextState ? 'inline-flex' : 'none'; }); toggleButton.textContent = nextState ? 'Hide queries' : `Show all (${queries.length})`; }); historyDetailTitle.appendChild(toggleButton); } else { chipsWrapper.dataset.expanded = 'true'; } } if (historyDetailMeta) { const metaParts = []; if (item.date) metaParts.push(item.date); metaParts.push(item.searchType === 'multi' ? 'Multi-product search' : 'Single search'); if (item.marketLabel) { metaParts.push(item.marketLabel); } else if (item.marketCode) { metaParts.push(item.marketCode); } historyDetailMeta.textContent = metaParts.filter(Boolean).join(' • '); } if (historyDetailOpenSearch) { historyDetailOpenSearch.dataset.historyId = String(item.id); historyDetailOpenSearch.disabled = false; historyDetailOpenSearch.style.cursor = 'pointer'; historyDetailOpenSearch.style.opacity = ''; } historyDetailResults.innerHTML = ''; if (item.searchType === 'multi' && item.results?.multiResults) { displayMultiResults(item.results.multiResults, historyDetailResults, { suppressHeader: true }); } else { displayResults(item.results, item.query, historyDetailResults); } showEditOrganizationInterface(item, historyDetailResults); historyDetailContainer.scrollTop = 0; historyDetailResults.scrollTop = 0; } function openHistoryItemInSearch(item) { if (!item) { return; } showHistoryListView(); switchTab('search'); if (item.market) { setMarketSelection(item.market); } const searchQuery = document.getElementById('search-query'); const multiSearchQuery = document.getElementById('multi-search-query'); const multiProductToggle = document.getElementById('multi-product-toggle'); if (item.searchType === 'multi') { if (multiProductToggle) { multiProductToggle.checked = true; toggleMultiProductSearch(); } if (multiSearchQuery) { multiSearchQuery.value = item.query; } if (searchQuery) { searchQuery.value = ''; } } else { if (multiProductToggle) { multiProductToggle.checked = false; toggleMultiProductSearch(); } if (searchQuery) { searchQuery.value = item.query; } if (multiSearchQuery) { multiSearchQuery.value = ''; } } if (item.searchType === 'multi' && item.results?.multiResults) { displayMultiResults(item.results.multiResults); } else { displayResults(item.results, item.query); } showCollapseToggle(); showEditOrganizationInterface(item); } function openHistoryItemInSearchById(historyId) { const history = loadSearchHistory(); const item = history.find(h => h.id === historyId); if (!item) { return; } openHistoryItemInSearch(item); } function reopenSearch(itemId) { const history = loadSearchHistory(); const item = history.find(h => h.id === itemId); if (!item) { return; } showHistoryDetailView(item); } function filterHistory() { // Legacy function - now redirects to advanced filtering // This maintains compatibility with existing event listeners applyFilters(); } function setButtonLoadingState(button, isLoading, overrides = {}) { if (!button) { return; } const readyAriaLabel = overrides.readyAriaLabel ?? button.getAttribute('data-ready-aria-label') ?? button.getAttribute('aria-label') ?? 'Run search'; const loadingAriaLabel = overrides.loadingAriaLabel ?? button.getAttribute('data-loading-aria-label') ?? 'Searching'; const readyStatusText = overrides.readyStatusText ?? button.getAttribute('data-ready-status') ?? readyAriaLabel; const loadingStatusText = overrides.loadingStatusText ?? button.getAttribute('data-loading-status') ?? loadingAriaLabel; const statusText = button.querySelector('.search-btn-status'); button.dataset.state = isLoading ? 'loading' : 'ready'; button.disabled = isLoading; button.setAttribute('aria-busy', isLoading ? 'true' : 'false'); button.setAttribute('aria-label', isLoading ? loadingAriaLabel : readyAriaLabel); if (statusText) { statusText.textContent = isLoading ? loadingStatusText : readyStatusText; } } 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; } const marketSettings = getSelectedMarketSettings(); // 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 setButtonLoadingState(searchBtn, true); resultsContainer.style.display = 'block'; resultsContainer.innerHTML = ` <div style="text-align: center; padding: 40px; color: #666;"> <div class="cpr-loading-spinner"></div> <p>Searching for "${query}"...</p> </div> `; try { const result = await searchProduct(query, token, marketSettings); displayResults(result, query); const historyId = saveSearchToHistory(query, result, 'single', [], null, marketSettings.value); // Show post-search tagging interface showPostSearchTagging(query, result, 'single', historyId); } catch (error) { displayError(error.message); } finally { setButtonLoadingState(searchBtn, false); 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()); // Remove duplicates (case-insensitive) to avoid unnecessary requests const uniqueQueries = [...new Set(queries.map(q => q.toLowerCase()))].map(lowerQuery => { // Find the original case version of this query return queries.find(originalQuery => originalQuery.toLowerCase() === lowerQuery); }); if (uniqueQueries.length === 0) { alert('Please enter at least one product name'); return; } // Show info if duplicates were removed if (queries.length > uniqueQueries.length) { } // If only one unique query remains, treat as single product search if (uniqueQueries.length === 1) { const singleQuery = document.getElementById('search-query'); if (singleQuery) { singleQuery.value = uniqueQueries[0]; setButtonLoadingState(multiSearchBtn, true, { loadingAriaLabel: 'Searching product', loadingStatusText: 'Searching product' }); try { await performSearch(); } finally { setButtonLoadingState(multiSearchBtn, false); } return; } } if (uniqueQueries.length > 10) { alert('Maximum 10 products allowed at once to avoid rate limiting'); return; } const marketSettings = getSelectedMarketSettings(); // 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 setButtonLoadingState(multiSearchBtn, true, { loadingAriaLabel: `Searching ${uniqueQueries.length} products`, loadingStatusText: `Searching ${uniqueQueries.length} products` }); resultsContainer.style.display = 'block'; resultsContainer.innerHTML = ` <div style="text-align: center; padding: 40px; color: #666;"> <div class="cpr-loading-spinner"></div> <p>Searching ${uniqueQueries.length} products...</p> <div id="progress-status" style="font-size: 14px; color: #999; margin-top: 10px;"> Starting searches... </div> </div> `; const results = []; const progressStatus = document.getElementById('progress-status'); try { // Search products one by one for (let i = 0; i < uniqueQueries.length; i++) { const query = uniqueQueries[i].trim(); if (progressStatus) { progressStatus.textContent = `Searching "${query}" (${i + 1}/${uniqueQueries.length})...`; } try { const result = await searchProduct(query, token, marketSettings); 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 < uniqueQueries.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } displayMultiResults(results); // Show post-search tagging interface for multi-search const queriesText = uniqueQueries.join('\n'); const combinedResults = { summary: { total_reviews: results.reduce((acc, r) => acc + (r.success ? (r.data.summary?.total_reviews || 0) : 0), 0), total_products: results.reduce((acc, r) => acc + (r.success ? (r.data.summary?.total_products || 0) : 0), 0), total_product_links: results.reduce((acc, r) => acc + (r.success ? (r.data.summary?.total_product_links || 0) : 0), 0), review_themes: [] }, multiResults: results, rationale: `Multi-product search for ${queries.length} products`, reviewSummary: `Combined results from ${results.filter(r => r.success).length} successful searches` }; const historyId = saveSearchToHistory(queriesText, combinedResults, 'multi', [], null, marketSettings.value); showPostSearchTagging(queriesText, combinedResults, 'multi', historyId); } catch (error) { displayError(error.message); } finally { setButtonLoadingState(multiSearchBtn, false); 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) { applyInputStatusStyles(tokenInput, { text: 'Token fetched from session', iconUrl: checkIconUrl, color: '#155724', backgroundColor: '#d4edda', borderColor: '#c3e6cb' }); tokenInput.readOnly = true; tokenInput.style.cursor = 'not-allowed'; } return sessionData.accessToken; } catch (error) { // Update the token input field to show error const tokenInput = document.getElementById('auth-token'); if (tokenInput) { applyInputStatusStyles(tokenInput, { text: 'Failed to fetch token automatically', iconUrl: errorIconUrl, color: '#721c24', backgroundColor: '#f8d7da', borderColor: '#f5c6cb' }); tokenInput.readOnly = false; tokenInput.style.cursor = "text"; } throw error; } } async function searchProduct(query, token, marketSettings = getSelectedMarketSettings()) { const effectiveMarket = marketSettings || getSelectedMarketSettings(); 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": effectiveMarket.acceptLanguage, "authorization": "Bearer " + token, "content-type": "application/json", "oai-client-version": "prod-43c98f917bf2c3e3a36183e9548cd048e4e40615", "oai-device-id": generateDeviceId(), "oai-language": effectiveMarket.oaiLanguage || 'en-US' }, "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 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 getFaviconUrl(url) { try { const urlObj = new URL(url); const domain = urlObj.protocol + '//' + urlObj.hostname; return `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(domain)}&size=16`; } catch (e) { // Fallback for invalid URLs return `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${encodeURIComponent(url)}&size=16`; } } // Function to create floating button function createFloatingButton() { const button = document.createElement('button'); button.id = 'openProductSearchModalBtn'; button.title = 'Open ChatGPT E-commerce Product Research'; const iconBase = (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getURL === 'function') ? chrome.runtime.getURL('icons/logobubble.svg') : null; if (iconBase) { const iconImg = document.createElement('img'); iconImg.src = iconBase; iconImg.alt = 'ChatGPT Product Info'; iconImg.style.pointerEvents = 'none'; button.appendChild(iconImg); } else { button.textContent = '🛍️'; button.classList.add('icon-fallback'); } document.body.appendChild(button); return button; } // Create floating button and add click handler const floatingButton = createFloatingButton(); floatingButton.addEventListener('click', createModal); // Initialize token status check async function initializeTokenStatus() { const tokenInput = document.getElementById('auth-token'); const authStatus = document.getElementById('auth-status'); if (!tokenInput || !authStatus) { 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) { applyInputStatusStyles(tokenInput, { text: 'Token ready - session active', iconUrl: checkIconUrl, color: '#155724', backgroundColor: '#d4edda', borderColor: '#c3e6cb' }); tokenInput.readOnly = true; tokenInput.style.cursor = 'not-allowed'; } // Update visible auth status if (authStatus) { applyStatusBanner(authStatus, { iconType: 'success', text: 'Ready to search', color: '#155724', backgroundColor: '#d4edda', 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) { applyInputStatusStyles(tokenInput, { text: 'Please log in to ChatGPT first', iconUrl: errorIconUrl, color: '#721c24', backgroundColor: '#f8d7da', borderColor: '#f5c6cb' }); tokenInput.readOnly = false; tokenInput.style.cursor = 'text'; } // Update visible auth status if (authStatus) { applyStatusBanner(authStatus, { iconType: 'error', text: 'Please log in to ChatGPT first', color: '#721c24', backgroundColor: '#f8d7da', borderColor: '#f5c6cb' }); } } } // Parse product info from API response 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.url && 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))]; productLinks.forEach((link, index) => { }); // Remove exact duplicates (same title AND url) while preserving different sources const deduplicatedLinks = []; const seenCombinations = new Set(); productLinks.forEach(link => { const key = `${link.title}|||${link.url}`; if (!seenCombinations.has(key)) { seenCombinations.add(key); deduplicatedLinks.push(link); } else { } }); return { products: products, productLinks: deduplicatedLinks, // Use deduplicated product links reviews: reviews, rationale: rationaleObj.text || null, reviewSummary: summaryObj.text || null, // Add the built summary summary: { total_products: products.length, total_product_links: deduplicatedLinks.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 || '' }); // 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) }; 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 || '' }); // 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) }; productLinks.push(productLink); } // Handle supporting_websites array if (patch.p === '/grouped_citation/supporting_websites' && patch.o === 'append' && patch.v && Array.isArray(patch.v)) { for (const supportingSite of patch.v) { if (supportingSite.url) { const productLink = { title: supportingSite.title || 'Supporting Link', url: supportingSite.url, snippet: supportingSite.snippet || '', source: extractDomainFromUrl(supportingSite.url) }; 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 || '' }); // 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) }; productLinks.push(productLink); } // Handle supporting_websites array in patch operations too if (patch.p === '/grouped_citation/supporting_websites' && patch.o === 'append' && patch.v && Array.isArray(patch.v)) { for (const supportingSite of patch.v) { if (supportingSite.url) { const productLink = { title: supportingSite.title || 'Supporting Link', url: supportingSite.url, snippet: supportingSite.snippet || '', source: extractDomainFromUrl(supportingSite.url) }; 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 || null, url: reviewData.url || null }; 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 || null, url: reviewData.url || null }; 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 || null, url: reviewData.url || null }; 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 || null, url: reviewData.url || null }; reviews.push(review); } } } } } } } catch (jsonError) { return; } } // Determine theme sentiment color styling for review themes function getThemeColor(theme, reviews) { if (!reviews || reviews.length === 0) { return { background: '#f8f9fa', color: '#6c757d' }; } const themeReviews = reviews.filter(review => review.theme && review.theme.toLowerCase() === theme.toLowerCase() ); if (themeReviews.length === 0) { const sentimentCounts = reviews.reduce((acc, review) => { acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; return acc; }, {}); const totalReviews = reviews.length; const positivePercent = (sentimentCounts.positive || 0) / totalReviews; const negativePercent = (sentimentCounts.negative || 0) / totalReviews; if (positivePercent > 0.6) { return { background: '#d1f2d1', color: '#2d5a2d' }; } if (negativePercent > 0.6) { return { background: '#f8d7da', color: '#721c24' }; } return { background: '#fff3cd', color: '#856404' }; } const themeSentimentCounts = themeReviews.reduce((acc, review) => { acc[review.sentiment] = (acc[review.sentiment] || 0) + 1; return acc; }, {}); const themeTotal = themeReviews.length; const positivePercent = (themeSentimentCounts.positive || 0) / themeTotal; const negativePercent = (themeSentimentCounts.negative || 0) / themeTotal; if (positivePercent > negativePercent && positivePercent > 0.5) { return { background: '#d1f2d1', color: '#2d5a2d' }; } if (negativePercent > positivePercent && negativePercent > 0.5) { return { background: '#f8d7da', color: '#721c24' }; } return { background: '#fff3cd', color: '#856404' }; } function copyMultiResultsToClipboard(results) { const successfulResults = results.filter(r => r.success); // Format as Markdown let markdown = `# Multi-Product Search Results\n\n`; markdown += `**${successfulResults.length}** products found\n\n`; markdown += `---\n\n`; // Add each product's results successfulResults.forEach((result, index) => { const data = result.data; const query = result.query; markdown += `# ${index + 1}. ${query}\n\n`; // Stats markdown += `**${data.summary.total_reviews}** reviews • **${data.summary.total_products}** products • **${data.summary.total_product_links}** citation links • **${data.summary.review_themes.length}** themes\n\n`; // Product Overview if (data.rationale && data.rationale.trim()) { markdown += `## Product Overview\n\n${data.rationale}\n\n`; } // Review Summary if (data.reviewSummary && data.reviewSummary.trim()) { markdown += `## Review Summary\n\n${data.reviewSummary}\n\n`; } // Citation Links if (data.productLinks && data.productLinks.length > 0) { markdown += `## Citation Links\n\n`; data.productLinks.forEach(link => { markdown += `### ${link.title}\n`; markdown += `🔗 ${link.url}\n`; if (link.snippet) { markdown += `${link.snippet}\n`; } markdown += `\n`; }); } // Reviews with sentiment analysis if (data.reviews.length > 0) { markdown += `## Reviews\n\n`; // 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 positivePercent = Math.round(((sentimentCounts.positive || 0) / totalReviews) * 100); const neutralPercent = Math.round(((sentimentCounts.neutral || 0) / totalReviews) * 100); const negativePercent = Math.round(((sentimentCounts.negative || 0) / totalReviews) * 100); markdown += `**Sentiment Distribution:** `; markdown += `✅ ${positivePercent}% Positive`; if (neutralPercent > 0) markdown += ` • ⚠️ ${neutralPercent}% Neutral`; if (negativePercent > 0) markdown += ` • ❌ ${negativePercent}% Negative`; markdown += `\n\n`; data.reviews.forEach(review => { const sentimentEmoji = review.sentiment === 'positive' ? '✅' : review.sentiment === 'negative' ? '❌' : '⚠️'; markdown += `### ${sentimentEmoji} ${review.theme}\n`; markdown += `**Source:** ${review.source}`; if (review.url) { markdown += ` (${review.url})`; } markdown += `\n\n${review.summary}\n\n`; }); } // Themes if (data.summary.review_themes.length > 0) { markdown += `## Key Themes\n\n`; markdown += data.summary.review_themes.map(theme => `- ${theme}`).join('\n'); markdown += `\n\n`; } // Separator between products if (index < successfulResults.length - 1) { markdown += `---\n\n`; } }); // Copy to clipboard navigator.clipboard.writeText(markdown).then(() => { const copyBtn = document.getElementById('copy-multi-results-btn'); if (copyBtn) { const originalHTML = copyBtn.innerHTML; copyBtn.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor"> <path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"/> </svg> Copied! `; copyBtn.style.background = '#d4edda'; copyBtn.style.color = '#155724'; copyBtn.style.borderColor = '#c3e6cb'; setTimeout(() => { copyBtn.innerHTML = originalHTML; copyBtn.style.background = '#fff'; copyBtn.style.color = '#495057'; copyBtn.style.borderColor = '#dee2e6'; }, 2000); } }).catch(err => { console.error('Failed to copy:', err); alert('Failed to copy to clipboard'); }); } function copyResultsToClipboard(data, query) { // Format data as Markdown let markdown = `# Search Results: ${query}\n\n`; // Summary stats markdown += `**${data.summary.total_reviews}** reviews • **${data.summary.total_products}** products • **${data.summary.total_product_links}** citation links • **${data.summary.review_themes.length}** themes\n\n`; // Product Overview if (data.rationale && data.rationale.trim()) { markdown += `## Product Overview\n\n${data.rationale}\n\n`; } // Review Summary if (data.reviewSummary && data.reviewSummary.trim()) { markdown += `## Review Summary\n\n${data.reviewSummary}\n\n`; } // Citation Links if (data.productLinks && data.productLinks.length > 0) { markdown += `## Citation Links\n\n`; data.productLinks.forEach(link => { markdown += `### ${link.title}\n`; markdown += `🔗 ${link.url}\n`; if (link.snippet) { markdown += `${link.snippet}\n`; } markdown += `\n`; }); } // Reviews with sentiment analysis if (data.reviews.length > 0) { markdown += `## Reviews\n\n`; // 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 positivePercent = Math.round(((sentimentCounts.positive || 0) / totalReviews) * 100); const neutralPercent = Math.round(((sentimentCounts.neutral || 0) / totalReviews) * 100); const negativePercent = Math.round(((sentimentCounts.negative || 0) / totalReviews) * 100); markdown += `**Sentiment Distribution:** `; markdown += `✅ ${positivePercent}% Positive`; if (neutralPercent > 0) markdown += ` • ⚠️ ${neutralPercent}% Neutral`; if (negativePercent > 0) markdown += ` • ❌ ${negativePercent}% Negative`; markdown += `\n\n`; data.reviews.forEach(review => { const sentimentEmoji = review.sentiment === 'positive' ? '✅' : review.sentiment === 'negative' ? '❌' : '⚠️'; markdown += `### ${sentimentEmoji} ${review.theme}\n`; markdown += `**Source:** ${review.source}`; if (review.url) { markdown += ` (${review.url})`; } markdown += `\n\n${review.summary}\n\n`; }); } // Themes if (data.summary.review_themes.length > 0) { markdown += `## Key Themes\n\n`; markdown += data.summary.review_themes.map(theme => `- ${theme}`).join('\n'); markdown += `\n`; } // Copy to clipboard navigator.clipboard.writeText(markdown).then(() => { const copyBtn = document.getElementById('copy-results-btn'); if (copyBtn) { const originalHTML = copyBtn.innerHTML; copyBtn.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor"> <path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"/> </svg> Copied! `; copyBtn.style.background = '#d4edda'; copyBtn.style.color = '#155724'; copyBtn.style.borderColor = '#c3e6cb'; setTimeout(() => { copyBtn.innerHTML = originalHTML; copyBtn.style.background = '#fff'; copyBtn.style.color = '#495057'; copyBtn.style.borderColor = '#dee2e6'; }, 2000); } }).catch(err => { console.error('Failed to copy:', err); alert('Failed to copy to clipboard'); }); } function displayResults(data, query, targetContainer, options = {}) { const defaultContainer = document.getElementById('results-container'); let resultsContainer = null; if (targetContainer instanceof HTMLElement) { resultsContainer = targetContainer; } else if (typeof targetContainer === 'string') { resultsContainer = document.getElementById(targetContainer); } if (!resultsContainer) { resultsContainer = defaultContainer; } if (!resultsContainer) { return; } if (resultsContainer === defaultContainer) { resultsContainer.style.display = 'block'; } 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; } const suppressHeader = Boolean(options.suppressHeader); let html = ''; if (!suppressHeader) { html += ` <div style=" background: #f8f9fa; padding: 10px 14px; margin-bottom: 16px; border-left: 4px solid #5b8def; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; "> <div style="display: flex; flex-direction: column; gap: 4px;"> <h3 style="margin: 0; color: #495057;">Results for "${query}"</h3> <div style="font-size: 12px; color: #6c757d;"> ${data.summary.total_reviews} reviews • ${data.summary.total_products} products • ${data.summary.total_product_links} citation links • ${data.summary.review_themes.length} themes </div> </div> <button id="copy-results-btn" title="Copy results to clipboard in Markdown format" style=" display: flex; align-items: center; gap: 6px; padding: 8px 14px; background: #fff; border: 1px solid #dee2e6; border-radius: 6px; color: #495057; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; "> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor"> <rect width="256" height="256" fill="none"/> <polyline points="168 168 216 168 216 40 88 40 88 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> <rect x="40" y="88" width="128" height="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> </svg> Copy Results </button> </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: 6px 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: 6px 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: 6px 12px; margin: 0 12px 8px 12px; background: #f8f9fa; "> <div style=" display: flex; align-items: center; margin-bottom: 6px; gap: 8px; "> <img src="${getFaviconUrl(link.url)}" alt="Site favicon" style=" width: 16px; height: 16px; flex-shrink: 0; " onerror="this.style.display='none'" /> <span style=" font-weight: 600; color: #5b8def; 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: 6px 12px; margin-bottom: 1px; "> <div style=" display: flex; align-items: center; margin-bottom: 8px; gap: 8px; "> ${review.url ? `<img src="${getFaviconUrl(review.url)}" alt="Site favicon" style=" width: 16px; height: 16px; 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: #5b8def; 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 => { const colors = getThemeColor(theme, data.reviews); return `<span style=" background: ${colors.background}; color: ${colors.color}; padding: 4px 8px; border-radius: 12px; font-size: 12px; border: 1px solid ${colors.background === '#d1f2d1' ? '#c3e6cb' : colors.background === '#f8d7da' ? '#f5c6cb' : '#ffeaa7'}; ">${theme}</span>`; }).join('')} </div> </div> `; } resultsContainer.innerHTML = html; // Attach copy button event listener if (!suppressHeader) { const copyBtn = document.getElementById('copy-results-btn'); if (copyBtn) { copyBtn.addEventListener('click', () => copyResultsToClipboard(data, query)); // Add hover effects copyBtn.addEventListener('mouseenter', () => { copyBtn.style.background = '#f8f9fa'; copyBtn.style.borderColor = '#5b8def'; }); copyBtn.addEventListener('mouseleave', () => { copyBtn.style.background = '#fff'; copyBtn.style.borderColor = '#dee2e6'; }); } } } function displayMultiResults(results, targetContainer, options = {}) { const defaultContainer = document.getElementById('results-container'); let resultsContainer = null; if (targetContainer instanceof HTMLElement) { resultsContainer = targetContainer; } else if (typeof targetContainer === 'string') { resultsContainer = document.getElementById(targetContainer); } if (!resultsContainer) { resultsContainer = defaultContainer; } if (!resultsContainer) { return; } if (resultsContainer === defaultContainer) { resultsContainer.style.display = 'block'; } const successfulResults = results.filter(r => r.success); const failedResults = results.filter(r => !r.success); let html = ` <div style=" padding: 6px 12px; margin-bottom: 16px; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; "> <div> <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> ${successfulResults.length > 0 ? ` <button id="copy-multi-results-btn" title="Copy all product results to clipboard in Markdown format" style=" display: flex; align-items: center; gap: 6px; padding: 8px 14px; background: #fff; border: 1px solid #dee2e6; border-radius: 6px; color: #495057; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; "> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor"> <rect width="256" height="256" fill="none"/> <polyline points="168 168 216 168 216 40 88 40 88 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> <rect x="40" y="88" width="128" height="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> </svg> Copy All Results </button> ` : ''} </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: 6px 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; "> <span class="status-icon status-icon--large status-icon--error" aria-hidden="true" style="margin-bottom: 20px; color: #dc3545; opacity: 0.8;"></span> <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; ">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 all themes instead of just top 3 const topThemes = data.summary.review_themes; 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: #5b8def; 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 => { const colors = getThemeColor(theme, data.reviews); return ` <span style=" display: inline-block; background: ${colors.background}; color: ${colors.color}; padding: 2px 6px; border-radius: 8px; font-size: 10px; margin: 1px 2px 1px 0; border: 1px solid ${colors.background === '#d1f2d1' ? '#c3e6cb' : colors.background === '#f8d7da' ? '#f5c6cb' : '#ffeaa7'}; ">${theme}</span> `; }).join('') : '<span style="color: #6c757d; font-size: 11px;">No themes</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: #5b8def; 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 id="detailed-content"></div> </div> `; resultsContainer.innerHTML = html; // Store results for detailed view window.multiSearchResults = successfulResults; // Attach copy button event listener for multi-results if (successfulResults.length > 0) { const copyMultiBtn = document.getElementById('copy-multi-results-btn'); if (copyMultiBtn) { copyMultiBtn.addEventListener('click', () => copyMultiResultsToClipboard(results)); // Add hover effects copyMultiBtn.addEventListener('mouseenter', () => { copyMultiBtn.style.background = '#f8f9fa'; copyMultiBtn.style.borderColor = '#5b8def'; }); copyMultiBtn.addEventListener('mouseleave', () => { copyMultiBtn.style.background = '#fff'; copyMultiBtn.style.borderColor = '#dee2e6'; }); } } // 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, null, { suppressHeader: true }); // Restore original function document.getElementById = originalGetElementById; // Move the content to detailed view detailedContent.innerHTML = ` <div style=" background: #f8f9fa; padding: 10px 14px; margin-bottom: 16px; border-left: 4px solid #5b8def; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; "> <div style="display: flex; flex-direction: column; gap: 4px;"> <h3 style="margin: 0; color: #495057;">Results for "${productName}"</h3> <div style="font-size: 12px; color: #6c757d;"> ${result.data?.reviews?.length || 0} reviews • ${result.data?.products?.length || 0} products • ${(result.data?.productLinks?.length || 0)} citation links • ${result.data?.summary?.review_themes?.length || 0} themes </div> </div> <button class="close-details-btn" style=" background: #6c757d; color: white; border: none; padding: 4px 10px; border-radius: 3px; font-size: 12px; cursor: pointer; ">Close</button> </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) { 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> `; } // --- Patched, context-aware sidebar filters (last definition wins) --- function _activeTab() { const reportsContainer = document.getElementById('reports-container'); if (reportsContainer && reportsContainer.style.display !== 'none') { return 'reports'; } const historyContainer = document.getElementById('history-container'); if (historyContainer && historyContainer.style.display !== 'none') { return 'history'; } return 'search'; } function _applyToAnalysis({ projectId = '', tagId = '' }) { const projectSelects = document.querySelectorAll('select#analysis-project-filter'); if (projectSelects.length > 0) { projectSelects.forEach(select => { select.value = projectId || ''; }); } if (tagId) { const tagChecks = document.querySelectorAll('.analysis-tag-checkbox'); const targetCheckbox = Array.from(tagChecks).find(cb => cb.value === tagId); if (targetCheckbox) { // Toggle the tag - if it's checked, uncheck it; if unchecked, check it targetCheckbox.checked = !targetCheckbox.checked; } } // Apply the filters to actually trigger the filtering and update global state if (typeof applyAnalysisFilters === 'function') { applyAnalysisFilters(); } else { if (typeof updateAnalysisFilterSummary === 'function') { updateAnalysisFilterSummary(); } if (typeof updateAnalysisFilterChips === 'function') { updateAnalysisFilterChips(); } } if (projectId || tagId) { const syncOptions = { shouldSwitch: false }; if (projectId) syncOptions.projectId = projectId; if (tagId) { syncOptions.tagId = tagId; syncOptions.tags = [tagId]; } _applyToHistory(syncOptions); } } function _applyToHistory({ projectId, tagId, tags, market, shouldSwitch = true } = {}) { const previousFilters = currentFilters || { text: '', rawText: '', project: '', tags: [], market: 'all', isActive: false }; const filterTextInput = document.getElementById('filter-text'); const rawFromDom = filterTextInput ? filterTextInput.value.trim() : ''; const effectiveRawText = rawFromDom || previousFilters.rawText || ''; let nextProject = previousFilters.project || ''; if (typeof projectId === 'string') { nextProject = projectId; } let nextTags = Array.isArray(previousFilters.tags) ? [...previousFilters.tags] : []; if (Array.isArray(tags)) { nextTags = Array.from(new Set( tags.filter(tag => typeof tag === 'string' && tag.length > 0) )); } if (typeof tagId === 'string' && tagId.length > 0) { // For sidebar tag selection, preserve existing tags and add/toggle the new one if (nextTags.includes(tagId)) { // If tag is already selected, remove it (toggle off) nextTags = nextTags.filter(tag => tag !== tagId); } else { // Add the tag to existing selection nextTags = [...nextTags, tagId]; } // Don't clear project when selecting a tag from sidebar } // Only clear tags when explicitly setting a project with empty tags array if (typeof projectId === 'string' && Array.isArray(tags) && tags.length === 0) { nextTags = []; } const filterMarketSelect = document.getElementById('filter-market'); let nextMarket = previousFilters.market || 'all'; const domMarket = filterMarketSelect ? filterMarketSelect.value : ''; if (typeof market === 'string') { nextMarket = market; } else if (domMarket) { nextMarket = domMarket; } const normalizedText = effectiveRawText.toLowerCase(); currentFilters = { text: normalizedText, rawText: effectiveRawText, project: nextProject, tags: nextTags, market: nextMarket, isActive: Boolean(normalizedText || nextProject || nextTags.length || (nextMarket && nextMarket !== 'all')) }; if (filterMarketSelect) { filterMarketSelect.value = nextMarket || 'all'; } const historyIsActive = _activeTab() === 'history'; const shouldRender = shouldSwitch || historyIsActive; if (!shouldRender) { return; } if (shouldSwitch && !historyIsActive) { switchTab('history'); } if (typeof loadHistory === 'function') { loadHistory(); } // Sync filters to Analysis tab if (typeof syncAnalysisFiltersWithHistoryFilters === 'function') { syncAnalysisFiltersWithHistoryFilters(); } ['history-content', 'history-welcome-state', 'history-list'].forEach(id => { const el = document.getElementById(id); if (el) { el.style.visibility = 'visible'; } }); const projectSel = document.getElementById('filter-project'); if (projectSel) { projectSel.value = currentFilters.project || ''; } const tagChecks = document.querySelectorAll('#filter-tags input[type="checkbox"]'); if (tagChecks.length > 0) { const activeTags = new Set(currentFilters.tags || []); tagChecks.forEach(cb => { cb.checked = activeTags.has(cb.value); }); } const updatedFilterText = document.getElementById('filter-text'); if (updatedFilterText) { updatedFilterText.value = currentFilters.rawText || ''; } if (typeof updateFilterSummary === 'function') { updateFilterSummary(); } if (typeof updateFilterChips === 'function') { updateFilterChips(); } if (typeof loadSearchHistory === 'function' && typeof applyAdvancedFilters === 'function') { const history = loadSearchHistory(); const filtered = applyAdvancedFilters(history); if (typeof renderHistoryList === 'function') { renderHistoryList(filtered); } } } window.filterByProject = filterByProject; window.filterByTag = filterByTag; // Listen for messages from popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'ping') { sendResponse({ status: 'ready' }); return true; } if (message.action === 'openSearch') { createModal(); sendResponse({ status: 'opened' }); return true; } }); } })(); ```