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,
|
chatTitle,
|
||||||
showArtifacts,
|
showArtifacts,
|
||||||
tools,
|
tools,
|
||||||
toolServers
|
toolServers,
|
||||||
|
showChatSearch
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
convertMessagesToHistory,
|
convertMessagesToHistory,
|
||||||
@ -89,6 +90,7 @@
|
|||||||
import Placeholder from './Placeholder.svelte';
|
import Placeholder from './Placeholder.svelte';
|
||||||
import NotificationToast from '../NotificationToast.svelte';
|
import NotificationToast from '../NotificationToast.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
import ChatSearch from './ChatSearch.svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
@ -2231,4 +2233,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Chat Search Overlay -->
|
||||||
|
<ChatSearch
|
||||||
|
show={$showChatSearch}
|
||||||
|
on:close={() => {
|
||||||
|
showChatSearch.set(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</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 showSidebar = writable(false);
|
||||||
export const showSearch = writable(false);
|
export const showSearch = writable(false);
|
||||||
|
export const showChatSearch = writable(false);
|
||||||
export const showSettings = writable(false);
|
export const showSettings = writable(false);
|
||||||
export const showArchivedChats = writable(false);
|
export const showArchivedChats = writable(false);
|
||||||
export const showChangelog = writable(false);
|
export const showChangelog = writable(false);
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
showChangelog,
|
showChangelog,
|
||||||
temporaryChatEnabled,
|
temporaryChatEnabled,
|
||||||
toolServers,
|
toolServers,
|
||||||
showSearch
|
showSearch,
|
||||||
|
showChatSearch
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||||
@ -129,6 +130,16 @@
|
|||||||
showSearch.set(!$showSearch);
|
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
|
// Check if Ctrl + Shift + O is pressed
|
||||||
if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') {
|
if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
Loading…
Reference in New Issue
Block a user