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 | -<!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 | 608 | // ES Query |
| 609 | 609 | if (debugInfo.es_query) { |
| 610 | 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 | 612 | html += '</div>'; |
| 613 | 613 | } |
| 614 | 614 | |
| ... | ... | @@ -616,6 +616,19 @@ function displayDebugInfo(data) { |
| 616 | 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 | 632 | // Helper functions |
| 620 | 633 | function escapeHtml(text) { |
| 621 | 634 | if (!text) return ''; | ... | ... |
frontend/static/js/app_base.js deleted
| ... | ... | @@ -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 | -} |