// SearchEngine Frontend - Modern UI (Multi-Tenant)
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:6002';
if (document.getElementById('apiUrl')) {
document.getElementById('apiUrl').textContent = API_BASE_URL;
}
// Get tenant ID from select
function getTenantId() {
const tenantSelect = document.getElementById('tenantSelect');
if (tenantSelect) {
return tenantSelect.value.trim();
}
return '170'; // Default fallback
}
// Get sku_filter_dimension (as list) from input
function getSkuFilterDimension() {
const skuFilterInput = document.getElementById('skuFilterDimension');
if (skuFilterInput) {
const value = skuFilterInput.value.trim();
if (!value.length) {
return null;
}
// 支持用逗号分隔多个维度,例如:color,size 或 option1,color
const parts = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
return parts.length > 0 ? parts : null;
}
return null;
}
// State Management
let state = {
query: '',
currentPage: 1,
pageSize: 20,
totalResults: 0,
filters: {},
rangeFilters: {},
sortBy: '',
sortOrder: 'desc',
facets: null,
lastSearchData: null,
debug: true // Always enable debug mode for test frontend
};
// Initialize
function initializeApp() {
// 初始化租户下拉框和分面面板
console.log('Initializing app...');
initTenantSelect();
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
}
// 在 DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
// DOM 已经加载完成,直接执行
initializeApp();
}
// 备用初始化:如果上面的初始化失败,在 window.onload 时再试一次
window.addEventListener('load', function() {
const tenantSelect = document.getElementById('tenantSelect');
if (tenantSelect && tenantSelect.options.length === 0) {
console.log('Retrying tenant select initialization on window.load...');
initTenantSelect();
}
});
// 最后尝试:延迟执行,确保所有脚本都已加载
setTimeout(function() {
const tenantSelect = document.getElementById('tenantSelect');
if (tenantSelect && tenantSelect.options.length === 0) {
console.log('Final retry: Initializing tenant select after delay...');
if (typeof getAvailableTenantIds === 'function') {
initTenantSelect();
} else {
console.error('getAvailableTenantIds still not available after delay');
}
}
}, 100);
// Keyboard handler
function handleKeyPress(event) {
if (event.key === 'Enter') {
performSearch();
}
}
// 初始化租户下拉框
function initTenantSelect() {
const tenantSelect = document.getElementById('tenantSelect');
if (!tenantSelect) {
console.error('tenantSelect element not found');
return;
}
// 检查函数是否可用
if (typeof getAvailableTenantIds !== 'function') {
console.error('getAvailableTenantIds function not found. Make sure tenant_facets_config.js is loaded before app.js');
return;
}
const availableTenants = getAvailableTenantIds();
console.log('Available tenants:', availableTenants);
if (!availableTenants || availableTenants.length === 0) {
console.warn('No tenant IDs found in configuration');
return;
}
// 清空现有选项
tenantSelect.innerHTML = '';
// 添加选项
availableTenants.forEach(tenantId => {
const option = document.createElement('option');
option.value = tenantId;
option.textContent = tenantId;
tenantSelect.appendChild(option);
});
// 设置默认值
if (availableTenants.length > 0) {
tenantSelect.value = availableTenants.includes('170') ? '170' : availableTenants[0];
}
// 初始化分面面板
renderFacetsPanel();
}
// 租户ID改变时的处理
function onTenantIdChange() {
renderFacetsPanel();
// 清空当前的分面数据
clearFacetsData();
}
// 根据当前 tenant_id 渲染分面面板结构
function renderFacetsPanel() {
const tenantId = getTenantId();
const config = getTenantFacetsConfig(tenantId);
const container = document.getElementById('specificationFacetsContainer');
if (!container) return;
// 清空现有规格分面
container.innerHTML = '';
// 为每个规格字段创建分面行
config.specificationFields.forEach(specField => {
const row = document.createElement('div');
row.className = 'filter-row';
row.setAttribute('data-facet-field', specField.field);
row.innerHTML = `
${imageUrl ? `
})
` : `
No Image
`}
${price !== 'N/A' ? `¥${price}` : 'N/A'}
${escapeHtml(title)}
${category ? escapeHtml(category) : ''}
${vendor ? ' | ' + escapeHtml(vendor) : ''}
`;
});
grid.innerHTML = html;
}
// Display facets as filter tags (一级分类 + 三个属性分面)
function displayFacets(facets) {
if (!facets || !Array.isArray(facets)) {
return;
}
const tenantId = getTenantId();
facets.forEach((facet) => {
// 根据配置获取分面显示信息
const displayConfig = getFacetDisplayConfig(tenantId, facet.field);
if (!displayConfig) {
// 如果没有配置,跳过该分面
return;
}
const containerId = displayConfig.containerId;
const maxDisplay = displayConfig.maxDisplay;
const container = document.getElementById(containerId);
if (!container) {
return;
}
// 检查values是否存在且是数组
if (!facet.values || !Array.isArray(facet.values) || facet.values.length === 0) {
container.innerHTML = '';
return;
}
let html = '';
// 渲染分面值
facet.values.slice(0, maxDisplay).forEach((facetValue) => {
if (!facetValue || typeof facetValue !== 'object') {
return;
}
const value = facetValue.value;
const count = facetValue.count;
// 允许value为0或空字符串,但不允许undefined/null
if (value === undefined || value === null) {
return;
}
// 检查是否已选中
let selected = false;
if (facet.field.startsWith('specifications.')) {
// 检查specifications过滤
const specName = facet.field.split('.')[1];
if (state.filters.specifications) {
const specs = Array.isArray(state.filters.specifications)
? state.filters.specifications
: [state.filters.specifications];
selected = specs.some(spec => spec && spec.name === specName && spec.value === value);
}
} else {
// 检查普通字段过滤
if (state.filters[facet.field]) {
selected = state.filters[facet.field].includes(value);
}
}
html += `
';
html += `
original_query: ${escapeHtml(data.query_info.original_query || 'N/A')}
`;
html += `
detected_language: ${data.query_info.detected_languag}
`;
html += '
';
debugInfoDiv.innerHTML = html;
} else {
debugInfoDiv.innerHTML = '';
}
return;
}
// Display comprehensive debug info when debug mode is on
const debugInfo = data.debug_info;
let html = '';
// Query Analysis
if (debugInfo.query_analysis) {
html += '
Query Analysis:';
html += `
original_query: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}
`;
html += `
normalized_query: ${escapeHtml(debugInfo.query_analysis.normalized_query || 'N/A')}
`;
html += `
rewritten_query: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}
`;
html += `
detected_language: ${debugInfo.query_analysis.detected_language}
`;
html += `
domain: ${escapeHtml(debugInfo.query_analysis.domain || 'default')}
`;
html += `
is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}
`;
if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) {
html += '
translations: ';
for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) {
if (translation) {
html += `${lang}: ${escapeHtml(translation)}; `;
}
}
html += '
';
}
if (debugInfo.query_analysis.boolean_ast) {
html += `
boolean_ast: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}
`;
}
html += `
has_vector: ${debugInfo.query_analysis.has_vector ? 'enabled' : 'disabled'}
`;
html += '
';
}
// Feature Flags
if (debugInfo.feature_flags) {
html += '
Feature Flags:';
html += `
translation_enabled: ${debugInfo.feature_flags.translation_enabled ? 'enabled' : 'disabled'}
`;
html += `
embedding_enabled: ${debugInfo.feature_flags.embedding_enabled ? 'enabled' : 'disabled'}
`;
html += `
rerank_enabled: ${debugInfo.feature_flags.rerank_enabled ? 'enabled' : 'disabled'}
`;
html += '
';
}
// ES Response
if (debugInfo.es_response) {
html += '
ES Response:';
html += `
took_ms: ${debugInfo.es_response.took_ms}ms
`;
html += `
total_hits: ${debugInfo.es_response.total_hits}
`;
html += `
max_score: ${debugInfo.es_response.max_score?.toFixed(3) || 0}
`;
html += '
';
}
// Stage Timings
if (debugInfo.stage_timings) {
html += '
Stage Timings:';
for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) {
html += `
${stage}: ${duration.toFixed(2)}ms
`;
}
html += '
';
}
// ES Query
if (debugInfo.es_query) {
html += '
ES Query DSL:';
html += `
${escapeHtml(customStringify(debugInfo.es_query))}`;
html += '
';
}
html += '
';
debugInfoDiv.innerHTML = html;
}
// Custom JSON stringify that compresses numeric arrays (like embeddings) to single line
function customStringify(obj) {
return JSON.stringify(obj, (key, value) => {
if (Array.isArray(value)) {
// Only collapse arrays that contain numbers (like embeddings)
if (value.every(item => typeof item === 'number')) {
return JSON.stringify(value);
}
}
return value;
}, 2).replace(/"\[/g, '[').replace(/\]"/g, ']');
}
// Helper functions
function escapeAttr(text) {
if (!text) return '';
return text.replace(/'/g, "\\'").replace(/"/g, '"');
}
function formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN');
} catch {
return dateStr;
}
}