Commit e2539fd362d7681e1740c4ca181f97c43f388390

Authored by tangwang
1 parent 85f08823

调试信息

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, '&quot;');
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   -}