diff --git a/src/frontend/web/von_interface/static/js/vontology.js b/src/frontend/web/von_interface/static/js/vontology.js index bf0473de..5f9f1c81 100644 --- a/src/frontend/web/von_interface/static/js/vontology.js +++ b/src/frontend/web/von_interface/static/js/vontology.js @@ -3237,6 +3237,7 @@ let __vontologySearchState = { // JVNAUTOSCI-550 additions: queue selections before tree ready & guard init let __vontologyTreeReady = false; const __pendingTreeSelections = []; +const __searchContentTypes = new Set(['conversations', 'concepts', 'tasks']); let __searchUiInitialised = false; let __conversationRecoveryInitialised = false; let __conversationRecoveryCountAbortController = null; @@ -3384,6 +3385,27 @@ export function setupVontologySearchUI() { await performVontologySearch(q); }, 180); + const contentTypes = document.getElementById('searchContentTypes'); + contentTypes?.addEventListener('change', (event) => { + const checkbox = event.target; + if (!checkbox.matches('input[type="checkbox"]')) return; + if (checkbox.checked) __searchContentTypes.add(checkbox.value); + else __searchContentTypes.delete(checkbox.value); + const count = __searchContentTypes.size; + contentTypes.classList.toggle('is-filtered', count !== 3); + contentTypes.querySelector('#searchContentTypesCount').textContent = count === 3 ? 'All' : `${count}/3`; + contentTypes.querySelector('summary').setAttribute('aria-label', + `Search content types: ${count === 3 ? 'all selected' : `${count} of 3 selected`}`); + if (input.value.trim()) void performVontologySearch(input.value); + else clearSearchResults({ invalidate: true }); + }); + contentTypes?.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') return; + event.preventDefault(); + contentTypes.open = false; + contentTypes.querySelector('summary').focus(); + }); + // Input handlers const handleInput = () => { if (!input.value.trim()) { @@ -3450,6 +3472,8 @@ export function setupVontologySearchUI() { // Click outside to close document.addEventListener('click', (e) => { const recoveryButton = document.getElementById('conversationRecoveryButton'); + if (contentTypes?.contains(e.target)) return; + if (contentTypes) contentTypes.open = false; if (!results.contains(e.target) && e.target !== input && !recoveryButton?.contains(e.target)) { clearSearchResults({ invalidate: true }); setExpanded(false); @@ -3622,7 +3646,7 @@ export async function performVontologySearch(q) { setRecoveryButtonPressed(false); renderSearchResults(); - const conceptTask = fetchConceptSearchItems(trimmedQuery, abortController.signal) + const conceptTask = !__searchContentTypes.has('concepts') ? Promise.resolve() : fetchConceptSearchItems(trimmedQuery, abortController.signal) .then(conceptItems => { if (!isCurrentUnifiedSearchRequest(requestGeneration)) return; __vontologySearchState.concepts = { @@ -3645,7 +3669,7 @@ export async function performVontologySearch(q) { }; renderSearchResults(); }); - const conversationTask = fetchConversationSearchPage(trimmedQuery, { signal: abortController.signal }) + const conversationTask = !__searchContentTypes.has('conversations') ? Promise.resolve() : fetchConversationSearchPage(trimmedQuery, { signal: abortController.signal }) .then(data => { if (!isCurrentUnifiedSearchRequest(requestGeneration)) return; const conversationItems = Array.isArray(data?.results) ? data.results : []; @@ -3670,7 +3694,8 @@ export async function performVontologySearch(q) { }; renderSearchResults(); }); - const taskSearch = loadTaskSearchResults({ requestGeneration, signal: abortController.signal }); + const taskSearch = __searchContentTypes.has('tasks') + ? loadTaskSearchResults({ requestGeneration, signal: abortController.signal }) : Promise.resolve(); await Promise.all([conceptTask, conversationTask, taskSearch]); } @@ -3811,9 +3836,9 @@ function normaliseConversationSearchItem(item) { function getOrderedSearchGroups() { if (__vontologySearchState.mode === 'recovery') return ['conversations']; - return isConversationWorkspaceActive() + return (isConversationWorkspaceActive() ? ['conversations', 'concepts', 'tasks'] - : ['concepts', 'tasks', 'conversations']; + : ['concepts', 'tasks', 'conversations']).filter(key => __searchContentTypes.has(key)); } function rebuildSearchKeyboardItems() { @@ -3989,6 +4014,9 @@ function renderSearchResults() { return; } + if (__vontologySearchState.mode === 'search' && !__searchContentTypes.size) { + results.appendChild(createSearchProviderNotice('Select at least one content type to search.', 'is-empty')); + } const keyboardIndexByKey = new Map( __vontologySearchState.items.map((item, index) => [`${item.searchType}:${item.id}`, index]) ); diff --git a/src/frontend/web/von_interface/static/styles.css b/src/frontend/web/von_interface/static/styles.css index df7b1432..38260757 100644 --- a/src/frontend/web/von_interface/static/styles.css +++ b/src/frontend/web/von_interface/static/styles.css @@ -19080,3 +19080,39 @@ html[data-footer-uptime="false"][data-footer-runtimecost="false"][data-footer-bu } .chat-composer > #clearTurnModelButton { grid-column: 1 / -1; } #turnModelStatus[hidden], #clearTurnModelButton[hidden] { display: none !important; } + +/* Content filters alongside the global search field. */ +.search-content-types { position: relative; flex: 0 0 auto; } +.search-content-types summary { + cursor: pointer; + padding: 8px; + border: 1px solid #cbd5e1; + border-radius: 6px; + background: #fff; + color: #475569; + font-size: 14px; + white-space: nowrap; +} +.search-content-types.is-filtered summary { + background: #fef3c7; + border-color: #b45309; + color: #78350f; +} +.search-content-types summary:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; } +.search-content-types-menu { + position: absolute; + top: calc(100% + 6px); + right: 0; + z-index: 1400; + width: 210px; + margin: 0; + padding: 12px; + border: 1px solid #cbd5e1; + border-radius: 6px; + background: #fff; + color: #1e293b; + box-shadow: 0 4px 12px #0002; +} +.search-content-types-menu legend { font-size: 13px; background: #fff; } +.search-content-types-menu label { display: flex; align-items: center; gap: 8px; min-height: 36px; cursor: pointer; } +.search-content-types-menu input { margin: 0; width: auto; } diff --git a/src/frontend/web/von_interface/templates/von_interface.html b/src/frontend/web/von_interface/templates/von_interface.html index 1d326902..90d995d6 100644 --- a/src/frontend/web/von_interface/templates/von_interface.html +++ b/src/frontend/web/von_interface/templates/von_interface.html @@ -73,6 +73,15 @@

aria-label="Search results"> +
+ Types All +
+ Search content types + + + +
+