From 12fcaa4afc466702ec6330c0e0460b2d9efbadf5 Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:40:49 +0100 Subject: [PATCH] search shortcut --- src/routes/+layout.svelte | 3 +- src/routes/chat/search-modal.svelte | 92 +++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e4df54b..03f7bcb 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,8 +5,9 @@ import { PUBLIC_CONVEX_URL } from '$env/static/public'; import { models } from '$lib/state/models.svelte'; import GlobalModal from '$lib/components/ui/modal/global-modal.svelte'; - import { shortcut } from '$lib/actions/shortcut.svelte'; + import { attachShortcut, shortcut } from '$lib/actions/shortcut.svelte'; import { goto } from '$app/navigation'; + import { openSearchModal } from './chat/search-modal.svelte'; let { children } = $props(); diff --git a/src/routes/chat/search-modal.svelte b/src/routes/chat/search-modal.svelte index 6016e81..8fc98c3 100644 --- a/src/routes/chat/search-modal.svelte +++ b/src/routes/chat/search-modal.svelte @@ -7,12 +7,16 @@ import { useQuery } from 'convex-svelte'; import { Debounced } from 'runed'; import { tick } from 'svelte'; + import { goto } from '$app/navigation'; import SearchIcon from '~icons/lucide/search'; + import { shortcut } from '$lib/actions/shortcut.svelte'; + import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte'; let open = $state(false); let input = $state(''); let searchMode = $state<'exact' | 'words' | 'fuzzy'>('words'); let inputEl = $state(); + let selectedIndex = $state(-1); const debouncedInput = new Debounced(() => input, 500); @@ -21,8 +25,63 @@ search_mode: searchMode, session_token: session.current?.session.token ?? '', })); + + // Reset selected index when search results change + $effect(() => { + if (search.data) { + selectedIndex = -1; + } + }); + + // Reset selected index when input changes + $effect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + input; // Track input changes + selectedIndex = -1; + }); + + function handleKeydown(event: KeyboardEvent) { + if (!search.data?.length) return; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, search.data.length - 1); + scrollToSelected(); + break; + case 'ArrowUp': + event.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, -1); + scrollToSelected(); + break; + case 'Enter': + event.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < search.data.length) { + const result = search.data[selectedIndex]; + if (result) { + goto(`/chat/${result.conversation._id}`); + open = false; + } + } + break; + case 'Escape': + event.preventDefault(); + open = false; + break; + } + } + + async function scrollToSelected() { + await tick(); + const selectedElement = document.querySelector(`[data-result-index="${selectedIndex}"]`); + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + } + (open = true) }} /> + {#snippet trigger(tooltip)} {/snippet} - Search + Search ({cmdOrCtrl} + K) @@ -47,6 +106,7 @@ { @@ -82,9 +142,27 @@ {:else if search.data?.length}
- {#each search.data as { conversation, messages, score, titleMatch }} + {#each search.data as { conversation, messages, score, titleMatch }, index}
{ + goto(`/chat/${conversation._id}`); + open = false; + }} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + goto(`/chat/${conversation._id}`); + open = false; + } + }} + onmouseenter={() => (selectedIndex = index)} >
@@ -106,8 +184,11 @@ variant="secondary" size="sm" class="shrink-0 text-xs" - href="/chat/{conversation._id}" - onclick={() => (open = false)} + onclick={(e: MouseEvent) => { + e.stopPropagation(); + goto(`/chat/${conversation._id}`); + open = false; + }} > View @@ -122,6 +203,7 @@ {:else}

Start typing to search your conversations

+

Use ↑↓ to navigate, Enter to select, Esc to close

{/if}