mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
feat: implement Step 1 - Basic Chat Search Overlay
✨ Features: - Add ChatSearch.svelte component with clean, professional design - Implement Ctrl+F global keyboard shortcut for chat pages - Add showChatSearch store following existing patterns (showSearch, showSidebar) - Non-intrusive overlay that doesn't block page scrolling 🎨 UI/UX: - Floating search box in top-right corner with smooth animations - Auto-focus search input when opened - Visual feedback with placeholder result counter - Consistent styling with OpenWebUI design system ⚡ Technical Implementation: - Reuse existing icon components (Search, ChevronUp, ChevronDown, XMark) - Follow OpenWebUI patterns for global state management via stores - Proper click-outside detection without blocking page interaction - Clean event handling with proper cleanup (onMount/onDestroy) - Accessibility features (ARIA labels, keyboard navigation) 🔧 Keyboard Shortcuts: - Ctrl+F: Open/close search (chat pages only) - Escape: Close search - Enter/Shift+Enter: Navigate results (placeholder) 📁 Files Modified: - src/lib/components/chat/ChatSearch.svelte (new) - src/lib/stores/index.ts (add showChatSearch store) - src/lib/components/chat/Chat.svelte (integrate search component) - src/routes/(app)/+layout.svelte (add Ctrl+F handler) 🧪 Testing: - ✅ Ctrl+F opens search overlay - ✅ Page scrolling works while search is open - ✅ Click outside closes search - ✅ Escape key closes search - ✅ Clean, professional appearance - ✅ No duplication - reuses existing components and patterns Ready for Step 2: Basic text search functionality
This commit is contained in:
parent
b5f4c85bb1
commit
ecb3000c32
@ -36,7 +36,8 @@
|
||||
chatTitle,
|
||||
showArtifacts,
|
||||
tools,
|
||||
toolServers
|
||||
toolServers,
|
||||
showChatSearch
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@ -89,6 +90,7 @@
|
||||
import Placeholder from './Placeholder.svelte';
|
||||
import NotificationToast from '../NotificationToast.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import ChatSearch from './ChatSearch.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let chatIdProp = '';
|
||||
@ -2231,4 +2233,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Chat Search Overlay -->
|
||||
<ChatSearch
|
||||
show={$showChatSearch}
|
||||
on:close={() => {
|
||||
showChatSearch.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
149
src/lib/components/chat/ChatSearch.svelte
Normal file
149
src/lib/components/chat/ChatSearch.svelte
Normal file
@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
// Import existing icon components
|
||||
import Search from '../icons/Search.svelte';
|
||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show = false;
|
||||
|
||||
let searchInput: HTMLInputElement;
|
||||
let searchContainer: HTMLDivElement;
|
||||
let searchQuery = '';
|
||||
let totalResults = 0;
|
||||
let currentResult = 0;
|
||||
|
||||
// Auto-focus when search opens
|
||||
$: if (show && searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeSearch();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
// Previous result (Shift+Enter)
|
||||
console.log('Previous result');
|
||||
} else {
|
||||
// Next result (Enter)
|
||||
console.log('Next result');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeSearch = () => {
|
||||
searchQuery = '';
|
||||
dispatch('close');
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
// For now, just simulate some results
|
||||
if (searchQuery.trim()) {
|
||||
totalResults = 3; // Placeholder
|
||||
currentResult = 1; // Placeholder
|
||||
} else {
|
||||
totalResults = 0;
|
||||
currentResult = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Click outside handler
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (show && searchContainer && !searchContainer.contains(e.target as Node)) {
|
||||
closeSearch();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<!-- Search Overlay -->
|
||||
<div
|
||||
bind:this={searchContainer}
|
||||
class="fixed top-4 right-4 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-3 min-w-80"
|
||||
transition:fly={{ y: -20, duration: 200 }}
|
||||
on:keydown={handleKeydown}
|
||||
role="dialog"
|
||||
aria-label="Chat search"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Search Icon -->
|
||||
<Search className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
|
||||
<!-- Search Input -->
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
bind:value={searchQuery}
|
||||
on:input={handleInput}
|
||||
type="text"
|
||||
placeholder="Search in chat..."
|
||||
class="flex-1 bg-transparent border-none outline-none text-sm text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
||||
/>
|
||||
|
||||
<!-- Results Counter -->
|
||||
{#if totalResults > 0}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{currentResult} of {totalResults}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Navigation Buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
|
||||
disabled={totalResults === 0}
|
||||
title="Previous (Shift+Enter)"
|
||||
aria-label="Previous result"
|
||||
>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
|
||||
disabled={totalResults === 0}
|
||||
title="Next (Enter)"
|
||||
aria-label="Next result"
|
||||
>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
on:click={closeSearch}
|
||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="Close (Esc)"
|
||||
aria-label="Close search"
|
||||
>
|
||||
<XMark className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Tips -->
|
||||
{#if searchQuery === ''}
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Press <kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> to navigate results
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
kbd {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
</style>
|
@ -66,6 +66,7 @@ export const settings: Writable<Settings> = writable({});
|
||||
|
||||
export const showSidebar = writable(false);
|
||||
export const showSearch = writable(false);
|
||||
export const showChatSearch = writable(false);
|
||||
export const showSettings = writable(false);
|
||||
export const showArchivedChats = writable(false);
|
||||
export const showChangelog = writable(false);
|
||||
|
@ -37,7 +37,8 @@
|
||||
showChangelog,
|
||||
temporaryChatEnabled,
|
||||
toolServers,
|
||||
showSearch
|
||||
showSearch,
|
||||
showChatSearch
|
||||
} from '$lib/stores';
|
||||
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
@ -129,6 +130,16 @@
|
||||
showSearch.set(!$showSearch);
|
||||
}
|
||||
|
||||
// Check if Ctrl + F is pressed (Chat Search)
|
||||
if (isCtrlPressed && event.key.toLowerCase() === 'f') {
|
||||
// Only trigger in chat pages, not globally
|
||||
if (window.location.pathname.startsWith('/c/') || window.location.pathname === '/') {
|
||||
event.preventDefault();
|
||||
console.log('chatSearch');
|
||||
showChatSearch.set(!$showChatSearch);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Ctrl + Shift + O is pressed
|
||||
if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') {
|
||||
event.preventDefault();
|
||||
|
Loading…
Reference in New Issue
Block a user