semanticsearch
This commit is contained in:
@@ -25,33 +25,55 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
|
||||
<div class="filters-container">
|
||||
<!-- Search Section -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-card-compact">
|
||||
<div class="filter-header-compact">
|
||||
<h3>🔍 Suche</h3>
|
||||
</div>
|
||||
<div class="search-wrapper">
|
||||
<div class="search-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<div class="filter-section">
|
||||
<div class="filter-card-compact">
|
||||
<div class="filter-header-compact">
|
||||
<h3>🔍 Suche</h3>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<div class="search-wrapper">
|
||||
<div class="search-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Software, Beschreibung oder Tags durchsuchen..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Semantic Search Toggle - Inline -->
|
||||
<div id="semantic-search-container" class="semantic-search-inline hidden">
|
||||
<label class="semantic-toggle-wrapper" title="Semantische Suche verwendet KI-basierte Bedeutungserkennung statt nur Stichwortvergleich. Findet ähnliche Tools auch wenn andere Begriffe verwendet werden.">
|
||||
<input type="checkbox" id="semantic-search-enabled" />
|
||||
<div class="semantic-checkbox-custom"></div>
|
||||
<span class="semantic-toggle-label">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
|
||||
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
|
||||
</svg>
|
||||
Semantisch
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Display -->
|
||||
<div id="semantic-status" class="semantic-status hidden">
|
||||
<span class="semantic-results-count"></span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Software, Beschreibung oder Tags durchsuchen..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button id="clear-search" class="search-clear hidden" title="Suche löschen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Filters Section - ONLY Domain and Phase -->
|
||||
<div class="filter-section">
|
||||
@@ -289,6 +311,10 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
const elements = {
|
||||
searchInput: document.getElementById('search-input'),
|
||||
clearSearch: document.getElementById('clear-search'),
|
||||
semanticContainer: document.getElementById('semantic-search-container'),
|
||||
semanticCheckbox: document.getElementById('semantic-search-enabled'),
|
||||
semanticStatus: document.getElementById('semantic-status'),
|
||||
semanticResultsCount: document.querySelector('.semantic-results-count'),
|
||||
domainSelect: document.getElementById('domain-select'),
|
||||
phaseSelect: document.getElementById('phase-select'),
|
||||
typeSelect: document.getElementById('type-select'),
|
||||
@@ -324,6 +350,54 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
let selectedTags = new Set();
|
||||
let selectedPhase = '';
|
||||
let isTagCloudExpanded = false;
|
||||
let semanticSearchEnabled = false;
|
||||
let semanticSearchAvailable = false;
|
||||
let lastSemanticResults = null;
|
||||
|
||||
// Check embeddings availability
|
||||
async function checkEmbeddingsAvailability() {
|
||||
try {
|
||||
const response = await fetch('/api/ai/embeddings-status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
semanticSearchAvailable = data.embeddings?.enabled && data.embeddings?.initialized;
|
||||
|
||||
if (semanticSearchAvailable && elements.semanticContainer) {
|
||||
elements.semanticContainer.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EMBEDDINGS] Status check failed:', error.message);
|
||||
semanticSearchAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Semantic search function
|
||||
async function performSemanticSearch(query) {
|
||||
if (!semanticSearchAvailable || !query.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/search/semantic', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: query.trim() })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || [];
|
||||
} catch (error) {
|
||||
console.error('[SEMANTIC] Search failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapsible(toggleBtn, content, storageKey) {
|
||||
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
|
||||
@@ -494,7 +568,19 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
: `${count} von ${total} Tools`;
|
||||
}
|
||||
|
||||
function filterTools() {
|
||||
function updateSemanticStatus(results) {
|
||||
if (!elements.semanticStatus || !elements.semanticResultsCount) return;
|
||||
|
||||
if (semanticSearchEnabled && results?.length > 0) {
|
||||
elements.semanticStatus.classList.remove('hidden');
|
||||
elements.semanticResultsCount.textContent = `${results.length} semantische Treffer`;
|
||||
} else {
|
||||
elements.semanticStatus.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Consolidated filtering logic with semantic search support
|
||||
async function filterTools() {
|
||||
const searchTerm = elements.searchInput.value.trim().toLowerCase();
|
||||
const selectedDomain = elements.domainSelect.value;
|
||||
const selectedPhaseFromSelect = elements.phaseSelect.value;
|
||||
@@ -508,15 +594,32 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
|
||||
const activePhase = selectedPhaseFromSelect || selectedPhase;
|
||||
|
||||
const filtered = window.toolsData.filter(tool => {
|
||||
if (searchTerm && !(
|
||||
tool.name.toLowerCase().includes(searchTerm) ||
|
||||
tool.description.toLowerCase().includes(searchTerm) ||
|
||||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
let filteredTools = window.toolsData;
|
||||
let semanticResults = null;
|
||||
|
||||
// CONSOLIDATED: Use semantic search if enabled and search term exists
|
||||
if (semanticSearchEnabled && semanticSearchAvailable && searchTerm) {
|
||||
semanticResults = await performSemanticSearch(searchTerm);
|
||||
lastSemanticResults = semanticResults;
|
||||
|
||||
if (semanticResults?.length > 0) {
|
||||
filteredTools = [...semanticResults];
|
||||
}
|
||||
} else {
|
||||
lastSemanticResults = null;
|
||||
|
||||
// Traditional text-based search
|
||||
if (searchTerm) {
|
||||
filteredTools = window.toolsData.filter(tool =>
|
||||
tool.name.toLowerCase().includes(searchTerm) ||
|
||||
tool.description.toLowerCase().includes(searchTerm) ||
|
||||
(tool.tags || []).some(tag => tag.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply additional filters to the results
|
||||
filteredTools = filteredTools.filter(tool => {
|
||||
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
|
||||
return false;
|
||||
}
|
||||
@@ -560,13 +663,20 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
return true;
|
||||
});
|
||||
|
||||
const finalResults = searchTerm && window.prioritizeSearchResults
|
||||
? window.prioritizeSearchResults(filtered, searchTerm)
|
||||
: filtered;
|
||||
// FIXED: Preserve semantic order when semantic search is used
|
||||
const finalResults = semanticSearchEnabled && lastSemanticResults
|
||||
? filteredTools // Already sorted by semantic similarity
|
||||
: (searchTerm && window.prioritizeSearchResults
|
||||
? window.prioritizeSearchResults(filteredTools, searchTerm)
|
||||
: filteredTools);
|
||||
|
||||
updateResultsCounter(finalResults.length);
|
||||
updateSemanticStatus(lastSemanticResults);
|
||||
|
||||
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: finalResults }));
|
||||
window.dispatchEvent(new CustomEvent('toolsFiltered', {
|
||||
detail: finalResults,
|
||||
semanticSearch: semanticSearchEnabled && !!lastSemanticResults
|
||||
}));
|
||||
}
|
||||
|
||||
function resetPrimaryFilters() {
|
||||
@@ -599,12 +709,17 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
function resetAllFilters() {
|
||||
elements.searchInput.value = '';
|
||||
elements.clearSearch.classList.add('hidden');
|
||||
elements.semanticCheckbox.checked = false;
|
||||
semanticSearchEnabled = false;
|
||||
lastSemanticResults = null;
|
||||
updateSemanticStatus(null);
|
||||
resetPrimaryFilters();
|
||||
resetAdvancedFilters();
|
||||
resetTags();
|
||||
filterTagCloud();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
elements.searchInput.addEventListener('input', (e) => {
|
||||
const hasValue = e.target.value.length > 0;
|
||||
elements.clearSearch.classList.toggle('hidden', !hasValue);
|
||||
@@ -619,16 +734,30 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
filterTools();
|
||||
});
|
||||
|
||||
// Semantic search checkbox handler
|
||||
if (elements.semanticCheckbox) {
|
||||
elements.semanticCheckbox.addEventListener('change', (e) => {
|
||||
semanticSearchEnabled = e.target.checked;
|
||||
filterTools();
|
||||
});
|
||||
}
|
||||
|
||||
[elements.domainSelect, elements.phaseSelect, elements.typeSelect, elements.skillSelect,
|
||||
elements.platformSelect, elements.licenseSelect, elements.accessSelect].forEach(select => {
|
||||
select.addEventListener('change', filterTools);
|
||||
if (select) {
|
||||
select.addEventListener('change', filterTools);
|
||||
}
|
||||
});
|
||||
|
||||
[elements.hostedOnly, elements.knowledgebaseOnly].forEach(checkbox => {
|
||||
checkbox.addEventListener('change', filterTools);
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', filterTools);
|
||||
}
|
||||
});
|
||||
|
||||
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
|
||||
if (elements.tagCloudToggle) {
|
||||
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
|
||||
}
|
||||
|
||||
elements.tagCloudItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
@@ -676,6 +805,8 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
window.clearTagFilters = resetTags;
|
||||
window.clearAllFilters = resetAllFilters;
|
||||
|
||||
// Initialize
|
||||
checkEmbeddingsAvailability();
|
||||
initializeCollapsible();
|
||||
initTagCloud();
|
||||
filterTagCloud();
|
||||
|
||||
Reference in New Issue
Block a user