search shortcut
This commit is contained in:
parent
9d36105fbd
commit
12fcaa4afc
2 changed files with 89 additions and 6 deletions
|
|
@ -5,8 +5,9 @@
|
||||||
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
||||||
import { models } from '$lib/state/models.svelte';
|
import { models } from '$lib/state/models.svelte';
|
||||||
import GlobalModal from '$lib/components/ui/modal/global-modal.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 { goto } from '$app/navigation';
|
||||||
|
import { openSearchModal } from './chat/search-modal.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,16 @@
|
||||||
import { useQuery } from 'convex-svelte';
|
import { useQuery } from 'convex-svelte';
|
||||||
import { Debounced } from 'runed';
|
import { Debounced } from 'runed';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import SearchIcon from '~icons/lucide/search';
|
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 open = $state(false);
|
||||||
let input = $state('');
|
let input = $state('');
|
||||||
let searchMode = $state<'exact' | 'words' | 'fuzzy'>('words');
|
let searchMode = $state<'exact' | 'words' | 'fuzzy'>('words');
|
||||||
let inputEl = $state<HTMLInputElement>();
|
let inputEl = $state<HTMLInputElement>();
|
||||||
|
let selectedIndex = $state(-1);
|
||||||
|
|
||||||
const debouncedInput = new Debounced(() => input, 500);
|
const debouncedInput = new Debounced(() => input, 500);
|
||||||
|
|
||||||
|
|
@ -21,8 +25,63 @@
|
||||||
search_mode: searchMode,
|
search_mode: searchMode,
|
||||||
session_token: session.current?.session.token ?? '',
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window use:shortcut={{ ctrl: true, key: 'k', callback: () => (open = true) }} />
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
{#snippet trigger(tooltip)}
|
{#snippet trigger(tooltip)}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -36,7 +95,7 @@
|
||||||
<span class="sr-only">Search</span>
|
<span class="sr-only">Search</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
Search
|
Search ({cmdOrCtrl} + K)
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Modal bind:open>
|
<Modal bind:open>
|
||||||
|
|
@ -47,6 +106,7 @@
|
||||||
<input
|
<input
|
||||||
bind:this={inputEl}
|
bind:this={inputEl}
|
||||||
bind:value={input}
|
bind:value={input}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
placeholder="Search conversations and messages..."
|
placeholder="Search conversations and messages..."
|
||||||
{@attach (node) => {
|
{@attach (node) => {
|
||||||
|
|
@ -82,9 +142,27 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if search.data?.length}
|
{:else if search.data?.length}
|
||||||
<div class="max-h-96 space-y-2 overflow-y-auto">
|
<div class="max-h-96 space-y-2 overflow-y-auto">
|
||||||
{#each search.data as { conversation, messages, score, titleMatch }}
|
{#each search.data as { conversation, messages, score, titleMatch }, index}
|
||||||
<div
|
<div
|
||||||
class="border-border flex items-center justify-between gap-2 rounded-lg border px-3 py-2"
|
data-result-index={index}
|
||||||
|
class="border-border flex cursor-pointer items-center justify-between gap-2 rounded-lg border px-3 py-2 transition-colors {index ===
|
||||||
|
selectedIndex
|
||||||
|
? 'bg-accent'
|
||||||
|
: 'hover:bg-muted/50'}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={() => {
|
||||||
|
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)}
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="mb-1 flex items-center gap-2">
|
<div class="mb-1 flex items-center gap-2">
|
||||||
|
|
@ -106,8 +184,11 @@
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="shrink-0 text-xs"
|
class="shrink-0 text-xs"
|
||||||
href="/chat/{conversation._id}"
|
onclick={(e: MouseEvent) => {
|
||||||
onclick={() => (open = false)}
|
e.stopPropagation();
|
||||||
|
goto(`/chat/${conversation._id}`);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -122,6 +203,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-muted-foreground py-8 text-center">
|
<div class="text-muted-foreground py-8 text-center">
|
||||||
<p>Start typing to search your conversations</p>
|
<p>Start typing to search your conversations</p>
|
||||||
|
<p class="mt-1 text-xs">Use ↑↓ to navigate, Enter to select, Esc to close</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue