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:
PVBLIC Foundation 2025-06-20 08:41:25 -07:00
parent b5f4c85bb1
commit ecb3000c32
4 changed files with 173 additions and 2 deletions

View File

@ -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>

View 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>

View File

@ -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);

View File

@ -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();