mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
feat: complete Step 4 - Visual Highlighting with improved UX
This commit is contained in:
parent
f50514046d
commit
b411560787
@ -19,6 +19,7 @@
|
|||||||
let matchingMessageIds: string[] = [];
|
let matchingMessageIds: string[] = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let isNavigating = false; // Visual feedback for navigation
|
let isNavigating = false; // Visual feedback for navigation
|
||||||
|
let currentSearchTerm = ''; // Track current highlighted term
|
||||||
|
|
||||||
// Computed values
|
// Computed values
|
||||||
$: totalResults = matchingMessageIds.length;
|
$: totalResults = matchingMessageIds.length;
|
||||||
@ -53,17 +54,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
|
clearHighlights();
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
matchingMessageIds = [];
|
matchingMessageIds = [];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
|
currentSearchTerm = '';
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
const performSearch = (query: string) => {
|
const performSearch = (query: string) => {
|
||||||
|
// Clear previous highlights
|
||||||
|
clearHighlights();
|
||||||
|
|
||||||
if (!query.trim() || !history?.messages) {
|
if (!query.trim() || !history?.messages) {
|
||||||
matchingMessageIds = [];
|
matchingMessageIds = [];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
|
currentSearchTerm = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,13 +88,101 @@
|
|||||||
|
|
||||||
matchingMessageIds = messageIds;
|
matchingMessageIds = messageIds;
|
||||||
currentIndex = messageIds.length > 0 ? 0 : 0;
|
currentIndex = messageIds.length > 0 ? 0 : 0;
|
||||||
|
currentSearchTerm = searchTerm;
|
||||||
|
|
||||||
// Auto-navigate to first result when search finds matches
|
// Apply highlights and auto-navigate to first result
|
||||||
if (messageIds.length > 0) {
|
if (messageIds.length > 0) {
|
||||||
|
highlightMatches(searchTerm);
|
||||||
scrollToCurrentResult();
|
scrollToCurrentResult();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const highlightMatches = (searchTerm: string) => {
|
||||||
|
if (!searchTerm.trim()) return;
|
||||||
|
|
||||||
|
matchingMessageIds.forEach(messageId => {
|
||||||
|
const messageElement = document.getElementById(`message-${messageId}`);
|
||||||
|
if (messageElement) {
|
||||||
|
highlightInElement(messageElement, searchTerm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightInElement = (element: Element, searchTerm: string) => {
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
element,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
{
|
||||||
|
acceptNode: (node) => {
|
||||||
|
// Skip if parent already has highlight class or is a script/style tag
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (!parent || parent.classList.contains('search-highlight') ||
|
||||||
|
parent.tagName === 'SCRIPT' || parent.tagName === 'STYLE') {
|
||||||
|
return NodeFilter.FILTER_REJECT;
|
||||||
|
}
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const textNodes: Text[] = [];
|
||||||
|
let node;
|
||||||
|
while (node = walker.nextNode()) {
|
||||||
|
textNodes.push(node as Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
textNodes.forEach(textNode => {
|
||||||
|
const text = textNode.textContent || '';
|
||||||
|
const lowerText = text.toLowerCase();
|
||||||
|
const lowerSearchTerm = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerText.includes(lowerSearchTerm)) {
|
||||||
|
const parent = textNode.parentNode;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
// Create document fragment with highlighted content
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = lowerText.indexOf(lowerSearchTerm, lastIndex)) !== -1) {
|
||||||
|
// Add text before match
|
||||||
|
if (match > lastIndex) {
|
||||||
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex, match)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add highlighted match
|
||||||
|
const highlight = document.createElement('span');
|
||||||
|
highlight.className = 'search-highlight bg-yellow-200 dark:bg-yellow-600 px-0.5 rounded';
|
||||||
|
highlight.textContent = text.slice(match, match + searchTerm.length);
|
||||||
|
fragment.appendChild(highlight);
|
||||||
|
|
||||||
|
lastIndex = match + searchTerm.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining text
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the text node with highlighted content
|
||||||
|
parent.replaceChild(fragment, textNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearHighlights = () => {
|
||||||
|
// Remove all existing highlights
|
||||||
|
const highlights = document.querySelectorAll('.search-highlight');
|
||||||
|
highlights.forEach(highlight => {
|
||||||
|
const parent = highlight.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
parent.replaceChild(document.createTextNode(highlight.textContent || ''), highlight);
|
||||||
|
parent.normalize(); // Merge adjacent text nodes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleInput = () => {
|
const handleInput = () => {
|
||||||
performSearch(searchQuery);
|
performSearch(searchQuery);
|
||||||
};
|
};
|
||||||
@ -131,9 +226,12 @@
|
|||||||
inline: 'nearest'
|
inline: 'nearest'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtle visual feedback to the message
|
// Turn all highlights blue during navigation
|
||||||
|
setHighlightColor('blue');
|
||||||
|
|
||||||
|
// Add message background flash
|
||||||
messageElement.style.transition = 'background-color 0.3s ease';
|
messageElement.style.transition = 'background-color 0.3s ease';
|
||||||
messageElement.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; // Light blue highlight
|
messageElement.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; // More visible blue
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
messageElement.style.backgroundColor = '';
|
messageElement.style.backgroundColor = '';
|
||||||
@ -142,6 +240,17 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setHighlightColor = (color: 'yellow' | 'blue') => {
|
||||||
|
const allHighlights = document.querySelectorAll('.search-highlight');
|
||||||
|
const colorClass = color === 'blue'
|
||||||
|
? 'search-highlight bg-blue-200 dark:bg-blue-600 px-0.5 rounded'
|
||||||
|
: 'search-highlight bg-yellow-200 dark:bg-yellow-600 px-0.5 rounded';
|
||||||
|
|
||||||
|
allHighlights.forEach(highlight => {
|
||||||
|
highlight.className = colorClass;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Click outside handler
|
// Click outside handler
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (show && searchContainer && !searchContainer.contains(e.target as Node)) {
|
if (show && searchContainer && !searchContainer.contains(e.target as Node)) {
|
||||||
@ -154,6 +263,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
clearHighlights(); // Clean up highlights when component is destroyed
|
||||||
document.removeEventListener('click', handleClickOutside);
|
document.removeEventListener('click', handleClickOutside);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -183,7 +293,7 @@
|
|||||||
<!-- Results Counter with enhanced styling -->
|
<!-- Results Counter with enhanced styling -->
|
||||||
{#if totalResults > 0}
|
{#if totalResults > 0}
|
||||||
<div class="text-xs font-medium text-blue-600 dark:text-blue-400 whitespace-nowrap">
|
<div class="text-xs font-medium text-blue-600 dark:text-blue-400 whitespace-nowrap">
|
||||||
{currentResult} of {totalResults}
|
{currentResult} of {totalResults} {totalResults === 1 ? 'message' : 'messages'}
|
||||||
</div>
|
</div>
|
||||||
{:else if searchQuery.trim()}
|
{:else if searchQuery.trim()}
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
@ -234,9 +344,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if totalResults > 1}
|
{:else if totalResults > 1}
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
Navigate with <kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> /
|
Navigate between messages with <kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> /
|
||||||
<kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Shift+Enter</kbd>
|
<kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Shift+Enter</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if totalResults === 1}
|
||||||
|
<div class="mt-2 text-xs text-green-600 dark:text-green-400">
|
||||||
|
Found in 1 message with highlights
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
Loading…
Reference in New Issue
Block a user