first try

This commit is contained in:
Thomas G. Lopes 2025-06-18 17:17:40 +01:00
parent b1005d7df5
commit c4151f16a0
4 changed files with 135 additions and 7 deletions

View file

@ -1,11 +1,12 @@
import { v } from 'convex/values';
import { api } from './_generated/api';
import { query } from './_generated/server';
import { type Id } from './_generated/dataModel';
import { type SessionObj } from './betterAuth';
import { messageRoleValidator } from './schema';
import { mutation } from './functions';
import { fuzzyMatchString } from '../../utils/fuzzy-search';
import { getFirstSentence } from '../../utils/strings';
import { api } from './_generated/api';
import { Doc, type Id } from './_generated/dataModel';
import { query } from './_generated/server';
import { type SessionObj } from './betterAuth';
import { mutation } from './functions';
import { messageRoleValidator } from './schema';
export const get = query({
args: {
@ -278,3 +279,59 @@ export const remove = mutation({
await ctx.db.delete(args.conversation_id);
},
});
export const search = query({
args: {
session_token: v.string(),
search_term: v.string(),
},
handler: async (ctx, args) => {
const session = await ctx.runQuery(api.betterAuth.publicGetSession, {
session_token: args.session_token,
});
if (!session) {
throw new Error('Unauthorized');
}
type SearchResult = {
conversation: Doc<'conversations'>;
messages: Doc<'messages'>[];
};
const res: SearchResult[] = [];
if (!args.search_term.trim()) return res;
const convQuery = ctx.db
.query('conversations')
.withIndex('by_user', (q) => q.eq('user_id', session.userId));
for await (const conversation of convQuery) {
const searchResult: SearchResult = {
conversation,
messages: [],
};
const msgQuery = ctx.db
.query('messages')
.withIndex('by_conversation', (q) => q.eq('conversation_id', conversation._id))
.order('asc');
for await (const message of msgQuery) {
if (fuzzyMatchString(args.search_term, message.content)) {
console.log('Found message for search');
searchResult.messages.push(message);
}
}
if (
searchResult.messages.length > 0 ||
fuzzyMatchString(args.search_term, conversation.title)
) {
res.push(searchResult);
}
}
return res;
},
});

View file

@ -38,7 +38,7 @@ export default function fuzzysearch<T>(options: {
/**
* Internal helper function that performs the actual fuzzy string matching
*/
function fuzzyMatchString(needle: string, haystack: string): boolean {
export function fuzzyMatchString(needle: string, haystack: string): boolean {
const hlen = haystack.length;
const nlen = needle.length;

View file

@ -37,6 +37,7 @@
import ModelPicker from './model-picker.svelte';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import { cn } from '$lib/utils/utils.js';
import SearchModal from './search-modal.svelte';
const client = useConvexClient();
@ -370,6 +371,7 @@
<!-- header -->
<div class="md:bg-sidebar fixed top-2 right-2 z-50 flex rounded-bl-lg p-1 md:top-0 md:right-0">
<SearchModal />
<Tooltip>
{#snippet trigger(tooltip)}
<Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}>

View file

@ -0,0 +1,69 @@
<script lang="ts">
import { api } from '$lib/backend/convex/_generated/api';
import { Button } from '$lib/components/ui/button/index.js';
import Modal from '$lib/components/ui/modal/modal.svelte';
import Tooltip from '$lib/components/ui/tooltip.svelte';
import { session } from '$lib/state/session.svelte';
import { useQuery } from 'convex-svelte';
import { Debounced } from 'runed';
import SearchIcon from '~icons/lucide/search';
let open = $state(true);
let input = $state('');
let inputEl = $state<HTMLInputElement>();
const debouncedInput = new Debounced(() => input, 500);
const search = useQuery(api.conversations.search, () => ({
search_term: debouncedInput.current,
session_token: session.current?.session.token ?? '',
}));
</script>
<Tooltip>
{#snippet trigger(tooltip)}
<Button
onclick={() => (open = true)}
variant="ghost"
size="icon"
class="size-8"
{...tooltip.trigger}
>
<SearchIcon class="!size-4" />
<span class="sr-only">Search</span>
</Button>
{/snippet}
Search
</Tooltip>
<Modal bind:open>
<h2>Search</h2>
<input bind:this={inputEl} bind:value={input} class="w-full border" placeholder="Search" />
{#if search.isLoading}
<div class="text-center">
<div class="animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
{:else if search.data?.length}
<div class="space-y-2">
{#each search.data as { conversation, messages }}
<div
class="border-border flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm"
>
<div class="flex items-center gap-2">
<div class="text-muted-foreground text-xs">
{conversation.title}
</div>
<div class="text-muted-foreground text-xs">
{messages.length} message{messages.length > 1 ? 's' : ''}
</div>
</div>
<!-- TODO: Add message count to conversation -->
<Button variant="secondary" size="sm" class="text-xs">View</Button>
</div>
{/each}
</div>
{/if}
</Modal>