Commit e2539fd362d7681e1740c4ca181f97c43f388390
1 parent
85f08823
调试信息
Showing
3 changed files
with
14 additions
and
680 deletions
Show diff stats
frontend/base.html deleted
| @@ -1,89 +0,0 @@ | @@ -1,89 +0,0 @@ | ||
| 1 | -<!DOCTYPE html> | ||
| 2 | -<html lang="zh-CN"> | ||
| 3 | -<head> | ||
| 4 | - <meta charset="UTF-8"> | ||
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | - <title>店匠通用搜索 - Base Configuration</title> | ||
| 7 | - <link rel="stylesheet" href="/static/css/style.css"> | ||
| 8 | -</head> | ||
| 9 | -<body> | ||
| 10 | - <div class="page-container"> | ||
| 11 | - <!-- Header --> | ||
| 12 | - <header class="top-header"> | ||
| 13 | - <div class="header-left"> | ||
| 14 | - <span class="logo">Shoplazza Base Search</span> | ||
| 15 | - <span class="product-count" id="productCount">0 products found</span> | ||
| 16 | - </div> | ||
| 17 | - <div class="header-right"> | ||
| 18 | - <button class="fold-btn" onclick="toggleFilters()">Fold</button> | ||
| 19 | - </div> | ||
| 20 | - </header> | ||
| 21 | - | ||
| 22 | - <!-- Search Bar --> | ||
| 23 | - <div class="search-bar"> | ||
| 24 | - <input type="text" id="searchInput" placeholder="输入搜索关键词... (支持中文、英文)" | ||
| 25 | - onkeypress="handleKeyPress(event)"> | ||
| 26 | - <button onclick="performSearch()" class="search-btn">搜索</button> | ||
| 27 | - </div> | ||
| 28 | - | ||
| 29 | - <!-- Filter Section --> | ||
| 30 | - <div class="filter-section" id="filterSection"> | ||
| 31 | - <!-- Category Filter --> | ||
| 32 | - <div class="filter-row"> | ||
| 33 | - <div class="filter-label">Categories:</div> | ||
| 34 | - <div class="filter-tags" id="categoryTags"></div> | ||
| 35 | - </div> | ||
| 36 | - | ||
| 37 | - <!-- Vendor Filter --> | ||
| 38 | - <div class="filter-row"> | ||
| 39 | - <div class="filter-label">Vendor:</div> | ||
| 40 | - <div class="filter-tags" id="brandTags"></div> | ||
| 41 | - </div> | ||
| 42 | - | ||
| 43 | - <!-- Tags Filter --> | ||
| 44 | - <div class="filter-row"> | ||
| 45 | - <div class="filter-label">Tags:</div> | ||
| 46 | - <div class="filter-tags" id="supplierTags"></div> | ||
| 47 | - </div> | ||
| 48 | - | ||
| 49 | - <!-- Price Range Filter --> | ||
| 50 | - <div class="filter-row"> | ||
| 51 | - <div class="filter-label">Price Range:</div> | ||
| 52 | - <div class="filter-tags" id="priceTags"></div> | ||
| 53 | - </div> | ||
| 54 | - | ||
| 55 | - <!-- Clear Filters Button --> | ||
| 56 | - <div class="filter-row"> | ||
| 57 | - <button id="clearFiltersBtn" onclick="clearAllFilters()" class="clear-filters-btn" style="display: none;"> | ||
| 58 | - Clear All Filters | ||
| 59 | - </button> | ||
| 60 | - </div> | ||
| 61 | - </div> | ||
| 62 | - | ||
| 63 | - <!-- Results Section --> | ||
| 64 | - <div class="results-section"> | ||
| 65 | - <div class="product-grid" id="productGrid"> | ||
| 66 | - <div class="welcome-message"> | ||
| 67 | - <h2>Welcome to Shoplazza Base Search</h2> | ||
| 68 | - <p>Enter keywords to search for products</p> | ||
| 69 | - </div> | ||
| 70 | - </div> | ||
| 71 | - </div> | ||
| 72 | - | ||
| 73 | - <!-- Loading Indicator --> | ||
| 74 | - <div id="loading" style="display: none; text-align: center; padding: 20px;"> | ||
| 75 | - <div class="spinner"></div> | ||
| 76 | - <p>Searching...</p> | ||
| 77 | - </div> | ||
| 78 | - | ||
| 79 | - <!-- Debug Info (collapsible) --> | ||
| 80 | - <div class="debug-section" id="debugSection" style="display: none;"> | ||
| 81 | - <button onclick="toggleDebug()" class="debug-toggle">Toggle Debug Info</button> | ||
| 82 | - <div id="debugInfo" style="display: none;"></div> | ||
| 83 | - </div> | ||
| 84 | - </div> | ||
| 85 | - | ||
| 86 | - <script src="/static/js/app_base.js"></script> | ||
| 87 | -</body> | ||
| 88 | -</html> | ||
| 89 | - |
frontend/static/js/app.js
| @@ -608,7 +608,7 @@ function displayDebugInfo(data) { | @@ -608,7 +608,7 @@ function displayDebugInfo(data) { | ||
| 608 | // ES Query | 608 | // ES Query |
| 609 | if (debugInfo.es_query) { | 609 | if (debugInfo.es_query) { |
| 610 | html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Query DSL:</strong>'; | 610 | html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Query DSL:</strong>'; |
| 611 | - html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}</pre>`; | 611 | + html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(customStringify(debugInfo.es_query))}</pre>`; |
| 612 | html += '</div>'; | 612 | html += '</div>'; |
| 613 | } | 613 | } |
| 614 | 614 | ||
| @@ -616,6 +616,19 @@ function displayDebugInfo(data) { | @@ -616,6 +616,19 @@ function displayDebugInfo(data) { | ||
| 616 | debugInfoDiv.innerHTML = html; | 616 | debugInfoDiv.innerHTML = html; |
| 617 | } | 617 | } |
| 618 | 618 | ||
| 619 | +// Custom JSON stringify that compresses numeric arrays (like embeddings) to single line | ||
| 620 | +function customStringify(obj) { | ||
| 621 | + return JSON.stringify(obj, (key, value) => { | ||
| 622 | + if (Array.isArray(value)) { | ||
| 623 | + // Only collapse arrays that contain numbers (like embeddings) | ||
| 624 | + if (value.every(item => typeof item === 'number')) { | ||
| 625 | + return JSON.stringify(value); | ||
| 626 | + } | ||
| 627 | + } | ||
| 628 | + return value; | ||
| 629 | + }, 2).replace(/"\[/g, '[').replace(/\]"/g, ']'); | ||
| 630 | +} | ||
| 631 | + | ||
| 619 | // Helper functions | 632 | // Helper functions |
| 620 | function escapeHtml(text) { | 633 | function escapeHtml(text) { |
| 621 | if (!text) return ''; | 634 | if (!text) return ''; |
frontend/static/js/app_base.js deleted
| @@ -1,590 +0,0 @@ | @@ -1,590 +0,0 @@ | ||
| 1 | -// SearchEngine Frontend - Modern UI (Multi-Tenant) | ||
| 2 | - | ||
| 3 | -const API_BASE_URL = 'http://localhost:6002'; | ||
| 4 | -document.getElementById('apiUrl').textContent = API_BASE_URL; | ||
| 5 | - | ||
| 6 | -// Get tenant ID from input | ||
| 7 | -function getTenantId() { | ||
| 8 | - const tenantInput = document.getElementById('tenantInput'); | ||
| 9 | - if (tenantInput) { | ||
| 10 | - return tenantInput.value.trim(); | ||
| 11 | - } | ||
| 12 | - return '1'; // Default fallback | ||
| 13 | -} | ||
| 14 | - | ||
| 15 | -// State Management | ||
| 16 | -let state = { | ||
| 17 | - query: '', | ||
| 18 | - currentPage: 1, | ||
| 19 | - pageSize: 20, | ||
| 20 | - totalResults: 0, | ||
| 21 | - filters: {}, | ||
| 22 | - rangeFilters: {}, | ||
| 23 | - sortBy: '', | ||
| 24 | - sortOrder: 'desc', | ||
| 25 | - facets: null, | ||
| 26 | - lastSearchData: null, | ||
| 27 | - debug: true // Always enable debug mode for test frontend | ||
| 28 | -}; | ||
| 29 | - | ||
| 30 | -// Initialize | ||
| 31 | -document.addEventListener('DOMContentLoaded', function() { | ||
| 32 | - console.log('SearchEngine loaded'); | ||
| 33 | - console.log('Debug mode: always enabled (test frontend)'); | ||
| 34 | - | ||
| 35 | - document.getElementById('searchInput').focus(); | ||
| 36 | -}); | ||
| 37 | - | ||
| 38 | -// Keyboard handler | ||
| 39 | -function handleKeyPress(event) { | ||
| 40 | - if (event.key === 'Enter') { | ||
| 41 | - performSearch(); | ||
| 42 | - } | ||
| 43 | -} | ||
| 44 | - | ||
| 45 | -// Toggle filters visibility | ||
| 46 | -function toggleFilters() { | ||
| 47 | - const filterSection = document.getElementById('filterSection'); | ||
| 48 | - filterSection.classList.toggle('hidden'); | ||
| 49 | -} | ||
| 50 | - | ||
| 51 | -// Perform search | ||
| 52 | -async function performSearch(page = 1) { | ||
| 53 | - const query = document.getElementById('searchInput').value.trim(); | ||
| 54 | - const tenantId = getTenantId(); | ||
| 55 | - | ||
| 56 | - if (!query) { | ||
| 57 | - alert('Please enter search keywords'); | ||
| 58 | - return; | ||
| 59 | - } | ||
| 60 | - | ||
| 61 | - if (!tenantId) { | ||
| 62 | - alert('Please enter tenant ID'); | ||
| 63 | - return; | ||
| 64 | - } | ||
| 65 | - | ||
| 66 | - state.query = query; | ||
| 67 | - state.currentPage = page; | ||
| 68 | - state.pageSize = parseInt(document.getElementById('resultSize').value); | ||
| 69 | - | ||
| 70 | - const from = (page - 1) * state.pageSize; | ||
| 71 | - | ||
| 72 | - // Define facets (简化配置) | ||
| 73 | - const facets = [ | ||
| 74 | - { | ||
| 75 | - "field": "category.keyword", | ||
| 76 | - "size": 15, | ||
| 77 | - "type": "terms" | ||
| 78 | - }, | ||
| 79 | - { | ||
| 80 | - "field": "vendor.keyword", | ||
| 81 | - "size": 15, | ||
| 82 | - "type": "terms" | ||
| 83 | - }, | ||
| 84 | - { | ||
| 85 | - "field": "tags.keyword", | ||
| 86 | - "size": 10, | ||
| 87 | - "type": "terms" | ||
| 88 | - }, | ||
| 89 | - { | ||
| 90 | - "field": "min_price", | ||
| 91 | - "type": "range", | ||
| 92 | - "ranges": [ | ||
| 93 | - {"key": "0-50", "to": 50}, | ||
| 94 | - {"key": "50-100", "from": 50, "to": 100}, | ||
| 95 | - {"key": "100-200", "from": 100, "to": 200}, | ||
| 96 | - {"key": "200+", "from": 200} | ||
| 97 | - ] | ||
| 98 | - } | ||
| 99 | - ]; | ||
| 100 | - | ||
| 101 | - // Show loading | ||
| 102 | - document.getElementById('loading').style.display = 'block'; | ||
| 103 | - document.getElementById('productGrid').innerHTML = ''; | ||
| 104 | - | ||
| 105 | - try { | ||
| 106 | - const response = await fetch(`${API_BASE_URL}/search/`, { | ||
| 107 | - method: 'POST', | ||
| 108 | - headers: { | ||
| 109 | - 'Content-Type': 'application/json', | ||
| 110 | - 'X-Tenant-ID': tenantId, | ||
| 111 | - }, | ||
| 112 | - body: JSON.stringify({ | ||
| 113 | - query: query, | ||
| 114 | - size: state.pageSize, | ||
| 115 | - from: from, | ||
| 116 | - filters: Object.keys(state.filters).length > 0 ? state.filters : null, | ||
| 117 | - range_filters: Object.keys(state.rangeFilters).length > 0 ? state.rangeFilters : null, | ||
| 118 | - facets: facets, | ||
| 119 | - sort_by: state.sortBy || null, | ||
| 120 | - sort_order: state.sortOrder, | ||
| 121 | - debug: state.debug | ||
| 122 | - }) | ||
| 123 | - }); | ||
| 124 | - | ||
| 125 | - if (!response.ok) { | ||
| 126 | - throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
| 127 | - } | ||
| 128 | - | ||
| 129 | - const data = await response.json(); | ||
| 130 | - state.lastSearchData = data; | ||
| 131 | - state.totalResults = data.total; | ||
| 132 | - state.facets = data.facets; | ||
| 133 | - | ||
| 134 | - displayResults(data); | ||
| 135 | - displayFacets(data.facets); | ||
| 136 | - displayPagination(); | ||
| 137 | - displayDebugInfo(data); | ||
| 138 | - updateProductCount(data.total); | ||
| 139 | - updateClearFiltersButton(); | ||
| 140 | - | ||
| 141 | - } catch (error) { | ||
| 142 | - console.error('Search error:', error); | ||
| 143 | - document.getElementById('productGrid').innerHTML = ` | ||
| 144 | - <div class="error-message"> | ||
| 145 | - <strong>Search Error:</strong> ${error.message} | ||
| 146 | - <br><br> | ||
| 147 | - <small>Please ensure backend service is running (${API_BASE_URL})</small> | ||
| 148 | - </div> | ||
| 149 | - `; | ||
| 150 | - } finally { | ||
| 151 | - document.getElementById('loading').style.display = 'none'; | ||
| 152 | - } | ||
| 153 | -} | ||
| 154 | - | ||
| 155 | -// Display results in grid | ||
| 156 | -function displayResults(data) { | ||
| 157 | - const grid = document.getElementById('productGrid'); | ||
| 158 | - | ||
| 159 | - if (!data.results || data.results.length === 0) { | ||
| 160 | - grid.innerHTML = ` | ||
| 161 | - <div class="no-results" style="grid-column: 1 / -1;"> | ||
| 162 | - <h3>No Results Found</h3> | ||
| 163 | - <p>Try different keywords or filters</p> | ||
| 164 | - </div> | ||
| 165 | - `; | ||
| 166 | - return; | ||
| 167 | - } | ||
| 168 | - | ||
| 169 | - let html = ''; | ||
| 170 | - | ||
| 171 | - data.results.forEach((spu) => { | ||
| 172 | - const score = spu.relevance_score; | ||
| 173 | - html += ` | ||
| 174 | - <div class="product-card"> | ||
| 175 | - <div class="product-image-wrapper"> | ||
| 176 | - ${spu.image_url ? ` | ||
| 177 | - <img src="${escapeHtml(spu.image_url)}" | ||
| 178 | - alt="${escapeHtml(spu.title)}" | ||
| 179 | - class="product-image" | ||
| 180 | - onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect fill=%22%23f0f0f0%22 width=%22100%22 height=%22100%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 font-size=%2214%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'"> | ||
| 181 | - ` : ` | ||
| 182 | - <div style="color: #ccc; font-size: 14px;">No Image</div> | ||
| 183 | - `} | ||
| 184 | - </div> | ||
| 185 | - | ||
| 186 | - <div class="product-price"> | ||
| 187 | - ${spu.price ? `$${spu.price.toFixed(2)}` : 'N/A'}${spu.compare_at_price && spu.compare_at_price > spu.price ? `<span style="text-decoration: line-through; color: #999; font-size: 0.9em; margin-left: 8px;">$${spu.compare_at_price.toFixed(2)}</span>` : ''} | ||
| 188 | - </div> | ||
| 189 | - | ||
| 190 | - <div class="product-stock"> | ||
| 191 | - ${spu.in_stock ? '<span style="color: green;">In Stock</span>' : '<span style="color: red;">Out of Stock</span>'} | ||
| 192 | - ${spu.skus && spu.skus.length > 0 ? `<span style="color: #666; font-size: 0.9em;">(${spu.skus.length} skus)</span>` : ''} | ||
| 193 | - </div> | ||
| 194 | - | ||
| 195 | - <div class="product-title"> | ||
| 196 | - ${escapeHtml(spu.title || 'N/A')} | ||
| 197 | - </div> | ||
| 198 | - | ||
| 199 | - <div class="product-meta">${spu.vendor ? escapeHtml(spu.vendor) : ''}${spu.category ? ' | ' + escapeHtml(spu.category) : ''} ${spu.tags ? ` | ||
| 200 | - <div class="product-tags"> | ||
| 201 | - Tags: ${escapeHtml(spu.tags)} | ||
| 202 | - </div> | ||
| 203 | - ` : ''} | ||
| 204 | - </div> | ||
| 205 | - `; | ||
| 206 | - }); | ||
| 207 | - | ||
| 208 | - grid.innerHTML = html; | ||
| 209 | -} | ||
| 210 | - | ||
| 211 | -// Display facets as filter tags (重构版 - 标准化格式) | ||
| 212 | -function displayFacets(facets) { | ||
| 213 | - if (!facets) return; | ||
| 214 | - | ||
| 215 | - facets.forEach(facet => { | ||
| 216 | - // 根据字段名找到对应的容器 | ||
| 217 | - let containerId = null; | ||
| 218 | - let maxDisplay = 10; | ||
| 219 | - | ||
| 220 | - if (facet.field === 'category.keyword') { | ||
| 221 | - containerId = 'categoryTags'; | ||
| 222 | - maxDisplay = 10; | ||
| 223 | - } else if (facet.field === 'vendor.keyword') { | ||
| 224 | - containerId = 'brandTags'; | ||
| 225 | - maxDisplay = 10; | ||
| 226 | - } else if (facet.field === 'tags.keyword') { | ||
| 227 | - containerId = 'supplierTags'; | ||
| 228 | - maxDisplay = 8; | ||
| 229 | - } | ||
| 230 | - | ||
| 231 | - if (!containerId) return; | ||
| 232 | - | ||
| 233 | - const container = document.getElementById(containerId); | ||
| 234 | - if (!container) return; | ||
| 235 | - | ||
| 236 | - let html = ''; | ||
| 237 | - | ||
| 238 | - // 渲染分面值 | ||
| 239 | - facet.values.slice(0, maxDisplay).forEach(facetValue => { | ||
| 240 | - const value = facetValue.value; | ||
| 241 | - const count = facetValue.count; | ||
| 242 | - const selected = facetValue.selected; | ||
| 243 | - | ||
| 244 | - html += ` | ||
| 245 | - <span class="filter-tag ${selected ? 'active' : ''}" | ||
| 246 | - onclick="toggleFilter('${escapeAttr(facet.field)}', '${escapeAttr(value)}')"> | ||
| 247 | - ${escapeHtml(value)} (${count}) | ||
| 248 | - </span> | ||
| 249 | - `; | ||
| 250 | - }); | ||
| 251 | - | ||
| 252 | - container.innerHTML = html; | ||
| 253 | - }); | ||
| 254 | -} | ||
| 255 | - | ||
| 256 | -// Toggle filter | ||
| 257 | -function toggleFilter(field, value) { | ||
| 258 | - if (!state.filters[field]) { | ||
| 259 | - state.filters[field] = []; | ||
| 260 | - } | ||
| 261 | - | ||
| 262 | - const index = state.filters[field].indexOf(value); | ||
| 263 | - if (index > -1) { | ||
| 264 | - state.filters[field].splice(index, 1); | ||
| 265 | - if (state.filters[field].length === 0) { | ||
| 266 | - delete state.filters[field]; | ||
| 267 | - } | ||
| 268 | - } else { | ||
| 269 | - state.filters[field].push(value); | ||
| 270 | - } | ||
| 271 | - | ||
| 272 | - performSearch(1); // Reset to page 1 | ||
| 273 | -} | ||
| 274 | - | ||
| 275 | -// Handle price filter (重构版 - 使用 rangeFilters) | ||
| 276 | -function handlePriceFilter(value) { | ||
| 277 | - if (!value) { | ||
| 278 | - delete state.rangeFilters.price; | ||
| 279 | - } else { | ||
| 280 | - const priceRanges = { | ||
| 281 | - '0-50': { lt: 50 }, | ||
| 282 | - '50-100': { gte: 50, lt: 100 }, | ||
| 283 | - '100-200': { gte: 100, lt: 200 }, | ||
| 284 | - '200+': { gte: 200 } | ||
| 285 | - }; | ||
| 286 | - | ||
| 287 | - if (priceRanges[value]) { | ||
| 288 | - state.rangeFilters.price = priceRanges[value]; | ||
| 289 | - } | ||
| 290 | - } | ||
| 291 | - | ||
| 292 | - performSearch(1); | ||
| 293 | -} | ||
| 294 | - | ||
| 295 | -// Handle time filter (重构版 - 使用 rangeFilters) | ||
| 296 | -function handleTimeFilter(value) { | ||
| 297 | - if (!value) { | ||
| 298 | - delete state.rangeFilters.create_time; | ||
| 299 | - } else { | ||
| 300 | - const now = new Date(); | ||
| 301 | - let fromDate; | ||
| 302 | - | ||
| 303 | - switch(value) { | ||
| 304 | - case 'today': | ||
| 305 | - fromDate = new Date(now.setHours(0, 0, 0, 0)); | ||
| 306 | - break; | ||
| 307 | - case 'week': | ||
| 308 | - fromDate = new Date(now.setDate(now.getDate() - 7)); | ||
| 309 | - break; | ||
| 310 | - case 'month': | ||
| 311 | - fromDate = new Date(now.setMonth(now.getMonth() - 1)); | ||
| 312 | - break; | ||
| 313 | - case '3months': | ||
| 314 | - fromDate = new Date(now.setMonth(now.getMonth() - 3)); | ||
| 315 | - break; | ||
| 316 | - case '6months': | ||
| 317 | - fromDate = new Date(now.setMonth(now.getMonth() - 6)); | ||
| 318 | - break; | ||
| 319 | - } | ||
| 320 | - | ||
| 321 | - if (fromDate) { | ||
| 322 | - state.rangeFilters.create_time = { | ||
| 323 | - gte: fromDate.toISOString() | ||
| 324 | - }; | ||
| 325 | - } | ||
| 326 | - } | ||
| 327 | - | ||
| 328 | - performSearch(1); | ||
| 329 | -} | ||
| 330 | - | ||
| 331 | -// Clear all filters | ||
| 332 | -function clearAllFilters() { | ||
| 333 | - state.filters = {}; | ||
| 334 | - state.rangeFilters = {}; | ||
| 335 | - document.getElementById('priceFilter').value = ''; | ||
| 336 | - document.getElementById('timeFilter').value = ''; | ||
| 337 | - performSearch(1); | ||
| 338 | -} | ||
| 339 | - | ||
| 340 | -// Update clear filters button visibility | ||
| 341 | -function updateClearFiltersButton() { | ||
| 342 | - const btn = document.getElementById('clearFiltersBtn'); | ||
| 343 | - if (Object.keys(state.filters).length > 0 || Object.keys(state.rangeFilters).length > 0) { | ||
| 344 | - btn.style.display = 'inline-block'; | ||
| 345 | - } else { | ||
| 346 | - btn.style.display = 'none'; | ||
| 347 | - } | ||
| 348 | -} | ||
| 349 | - | ||
| 350 | -// Update product count | ||
| 351 | -function updateProductCount(total) { | ||
| 352 | - document.getElementById('productCount').textContent = `${total.toLocaleString()} SPUs found`; | ||
| 353 | -} | ||
| 354 | - | ||
| 355 | -// Sort functions | ||
| 356 | -function setSortByDefault() { | ||
| 357 | - // Remove active from all buttons and arrows | ||
| 358 | - document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); | ||
| 359 | - document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active')); | ||
| 360 | - | ||
| 361 | - // Set default button active | ||
| 362 | - const defaultBtn = document.querySelector('.sort-btn[data-sort=""]'); | ||
| 363 | - if (defaultBtn) defaultBtn.classList.add('active'); | ||
| 364 | - | ||
| 365 | - state.sortBy = ''; | ||
| 366 | - state.sortOrder = 'desc'; | ||
| 367 | - | ||
| 368 | - performSearch(1); | ||
| 369 | -} | ||
| 370 | - | ||
| 371 | -function sortByField(field, order) { | ||
| 372 | - state.sortBy = field; | ||
| 373 | - state.sortOrder = order; | ||
| 374 | - | ||
| 375 | - // Remove active from all buttons (but keep "By default" if no sort) | ||
| 376 | - document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active')); | ||
| 377 | - | ||
| 378 | - // Remove active from all arrows | ||
| 379 | - document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active')); | ||
| 380 | - | ||
| 381 | - // Add active to clicked arrow | ||
| 382 | - const activeArrow = document.querySelector(`.arrow-up[data-field="${field}"][data-order="${order}"], .arrow-down[data-field="${field}"][data-order="${order}"]`); | ||
| 383 | - if (activeArrow) { | ||
| 384 | - activeArrow.classList.add('active'); | ||
| 385 | - } | ||
| 386 | - | ||
| 387 | - performSearch(state.currentPage); | ||
| 388 | -} | ||
| 389 | - | ||
| 390 | -// Pagination | ||
| 391 | -function displayPagination() { | ||
| 392 | - const paginationDiv = document.getElementById('pagination'); | ||
| 393 | - | ||
| 394 | - if (state.totalResults <= state.pageSize) { | ||
| 395 | - paginationDiv.style.display = 'none'; | ||
| 396 | - return; | ||
| 397 | - } | ||
| 398 | - | ||
| 399 | - paginationDiv.style.display = 'flex'; | ||
| 400 | - | ||
| 401 | - const totalPages = Math.ceil(state.totalResults / state.pageSize); | ||
| 402 | - const currentPage = state.currentPage; | ||
| 403 | - | ||
| 404 | - let html = ` | ||
| 405 | - <button class="page-btn" onclick="goToPage(${currentPage - 1})" | ||
| 406 | - ${currentPage === 1 ? 'disabled' : ''}> | ||
| 407 | - ← Previous | ||
| 408 | - </button> | ||
| 409 | - `; | ||
| 410 | - | ||
| 411 | - // Page numbers | ||
| 412 | - const maxVisible = 5; | ||
| 413 | - let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); | ||
| 414 | - let endPage = Math.min(totalPages, startPage + maxVisible - 1); | ||
| 415 | - | ||
| 416 | - if (endPage - startPage < maxVisible - 1) { | ||
| 417 | - startPage = Math.max(1, endPage - maxVisible + 1); | ||
| 418 | - } | ||
| 419 | - | ||
| 420 | - if (startPage > 1) { | ||
| 421 | - html += `<button class="page-btn" onclick="goToPage(1)">1</button>`; | ||
| 422 | - if (startPage > 2) { | ||
| 423 | - html += `<span class="page-info">...</span>`; | ||
| 424 | - } | ||
| 425 | - } | ||
| 426 | - | ||
| 427 | - for (let i = startPage; i <= endPage; i++) { | ||
| 428 | - html += ` | ||
| 429 | - <button class="page-btn ${i === currentPage ? 'active' : ''}" | ||
| 430 | - onclick="goToPage(${i})"> | ||
| 431 | - ${i} | ||
| 432 | - </button> | ||
| 433 | - `; | ||
| 434 | - } | ||
| 435 | - | ||
| 436 | - if (endPage < totalPages) { | ||
| 437 | - if (endPage < totalPages - 1) { | ||
| 438 | - html += `<span class="page-info">...</span>`; | ||
| 439 | - } | ||
| 440 | - html += `<button class="page-btn" onclick="goToPage(${totalPages})">${totalPages}</button>`; | ||
| 441 | - } | ||
| 442 | - | ||
| 443 | - html += ` | ||
| 444 | - <button class="page-btn" onclick="goToPage(${currentPage + 1})" | ||
| 445 | - ${currentPage === totalPages ? 'disabled' : ''}> | ||
| 446 | - Next → | ||
| 447 | - </button> | ||
| 448 | - `; | ||
| 449 | - | ||
| 450 | - html += ` | ||
| 451 | - <span class="page-info"> | ||
| 452 | - Page ${currentPage} of ${totalPages} (${state.totalResults.toLocaleString()} results) | ||
| 453 | - </span> | ||
| 454 | - `; | ||
| 455 | - | ||
| 456 | - paginationDiv.innerHTML = html; | ||
| 457 | -} | ||
| 458 | - | ||
| 459 | -function goToPage(page) { | ||
| 460 | - const totalPages = Math.ceil(state.totalResults / state.pageSize); | ||
| 461 | - if (page < 1 || page > totalPages) return; | ||
| 462 | - | ||
| 463 | - performSearch(page); | ||
| 464 | - | ||
| 465 | - // Scroll to top | ||
| 466 | - window.scrollTo({ top: 0, behavior: 'smooth' }); | ||
| 467 | -} | ||
| 468 | - | ||
| 469 | -// Display debug info | ||
| 470 | -function displayDebugInfo(data) { | ||
| 471 | - const debugInfoDiv = document.getElementById('debugInfo'); | ||
| 472 | - | ||
| 473 | - if (!state.debug || !data.debug_info) { | ||
| 474 | - // If debug mode is off or no debug info, show basic query info | ||
| 475 | - if (data.query_info) { | ||
| 476 | - let html = '<div style="padding: 10px;">'; | ||
| 477 | - html += `<div><strong>original_query:</strong> ${escapeHtml(data.query_info.original_query || 'N/A')}</div>`; | ||
| 478 | - html += `<div><strong>detected_language:</strong> ${getLanguageName(data.query_info.detected_language)}</div>`; | ||
| 479 | - html += '</div>'; | ||
| 480 | - debugInfoDiv.innerHTML = html; | ||
| 481 | - } else { | ||
| 482 | - debugInfoDiv.innerHTML = ''; | ||
| 483 | - } | ||
| 484 | - return; | ||
| 485 | - } | ||
| 486 | - | ||
| 487 | - // Display comprehensive debug info when debug mode is on | ||
| 488 | - const debugInfo = data.debug_info; | ||
| 489 | - let html = '<div style="padding: 10px; font-family: monospace; font-size: 12px;">'; | ||
| 490 | - | ||
| 491 | - // Query Analysis | ||
| 492 | - if (debugInfo.query_analysis) { | ||
| 493 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Query Analysis:</strong>'; | ||
| 494 | - html += `<div>original_query: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}</div>`; | ||
| 495 | - html += `<div>normalized_query: ${escapeHtml(debugInfo.query_analysis.normalized_query || 'N/A')}</div>`; | ||
| 496 | - html += `<div>rewritten_query: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}</div>`; | ||
| 497 | - html += `<div>detected_language: ${getLanguageName(debugInfo.query_analysis.detected_language)}</div>`; | ||
| 498 | - html += `<div>domain: ${escapeHtml(debugInfo.query_analysis.domain || 'default')}</div>`; | ||
| 499 | - html += `<div>is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}</div>`; | ||
| 500 | - | ||
| 501 | - if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { | ||
| 502 | - html += '<div>translations: '; | ||
| 503 | - for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) { | ||
| 504 | - if (translation) { | ||
| 505 | - html += `${getLanguageName(lang)}: ${escapeHtml(translation)}; `; | ||
| 506 | - } | ||
| 507 | - } | ||
| 508 | - html += '</div>'; | ||
| 509 | - } | ||
| 510 | - | ||
| 511 | - if (debugInfo.query_analysis.boolean_ast) { | ||
| 512 | - html += `<div>boolean_ast: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}</div>`; | ||
| 513 | - } | ||
| 514 | - | ||
| 515 | - html += `<div>has_vector: ${debugInfo.query_analysis.has_vector ? 'enabled' : 'disabled'}</div>`; | ||
| 516 | - html += '</div>'; | ||
| 517 | - } | ||
| 518 | - | ||
| 519 | - // Feature Flags | ||
| 520 | - if (debugInfo.feature_flags) { | ||
| 521 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Feature Flags:</strong>'; | ||
| 522 | - html += `<div>translation_enabled: ${debugInfo.feature_flags.translation_enabled ? 'enabled' : 'disabled'}</div>`; | ||
| 523 | - html += `<div>embedding_enabled: ${debugInfo.feature_flags.embedding_enabled ? 'enabled' : 'disabled'}</div>`; | ||
| 524 | - html += `<div>rerank_enabled: ${debugInfo.feature_flags.rerank_enabled ? 'enabled' : 'disabled'}</div>`; | ||
| 525 | - html += '</div>'; | ||
| 526 | - } | ||
| 527 | - | ||
| 528 | - // ES Response | ||
| 529 | - if (debugInfo.es_response) { | ||
| 530 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Response:</strong>'; | ||
| 531 | - html += `<div>took_ms: ${debugInfo.es_response.took_ms}ms</div>`; | ||
| 532 | - html += `<div>total_hits: ${debugInfo.es_response.total_hits}</div>`; | ||
| 533 | - html += `<div>max_score: ${debugInfo.es_response.max_score?.toFixed(3) || 0}</div>`; | ||
| 534 | - html += '</div>'; | ||
| 535 | - } | ||
| 536 | - | ||
| 537 | - // Stage Timings | ||
| 538 | - if (debugInfo.stage_timings) { | ||
| 539 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Stage Timings:</strong>'; | ||
| 540 | - for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) { | ||
| 541 | - html += `<div>${stage}: ${duration.toFixed(2)}ms</div>`; | ||
| 542 | - } | ||
| 543 | - html += '</div>'; | ||
| 544 | - } | ||
| 545 | - | ||
| 546 | - // ES Query | ||
| 547 | - if (debugInfo.es_query) { | ||
| 548 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Query DSL:</strong>'; | ||
| 549 | - html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}</pre>`; | ||
| 550 | - html += '</div>'; | ||
| 551 | - } | ||
| 552 | - | ||
| 553 | - html += '</div>'; | ||
| 554 | - debugInfoDiv.innerHTML = html; | ||
| 555 | -} | ||
| 556 | - | ||
| 557 | -// Helper functions | ||
| 558 | -function escapeHtml(text) { | ||
| 559 | - if (!text) return ''; | ||
| 560 | - const div = document.createElement('div'); | ||
| 561 | - div.textContent = text; | ||
| 562 | - return div.innerHTML; | ||
| 563 | -} | ||
| 564 | - | ||
| 565 | -function escapeAttr(text) { | ||
| 566 | - if (!text) return ''; | ||
| 567 | - return text.replace(/'/g, "\\'").replace(/"/g, '"'); | ||
| 568 | -} | ||
| 569 | - | ||
| 570 | -function formatDate(dateStr) { | ||
| 571 | - if (!dateStr) return ''; | ||
| 572 | - try { | ||
| 573 | - const date = new Date(dateStr); | ||
| 574 | - return date.toLocaleDateString('zh-CN'); | ||
| 575 | - } catch { | ||
| 576 | - return dateStr; | ||
| 577 | - } | ||
| 578 | -} | ||
| 579 | - | ||
| 580 | -function getLanguageName(code) { | ||
| 581 | - const names = { | ||
| 582 | - 'zh': '中文', | ||
| 583 | - 'en': 'English', | ||
| 584 | - 'ru': 'Русский', | ||
| 585 | - 'ar': 'العربية', | ||
| 586 | - 'ja': '日本語', | ||
| 587 | - 'unknown': 'Unknown' | ||
| 588 | - }; | ||
| 589 | - return names[code] || code; | ||
| 590 | -} |