first try
This commit is contained in:
parent
b1005d7df5
commit
c4151f16a0
4 changed files with 135 additions and 7 deletions
|
|
@ -1,11 +1,12 @@
|
||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { api } from './_generated/api';
|
import { fuzzyMatchString } from '../../utils/fuzzy-search';
|
||||||
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 { getFirstSentence } from '../../utils/strings';
|
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({
|
export const get = query({
|
||||||
args: {
|
args: {
|
||||||
|
|
@ -278,3 +279,59 @@ export const remove = mutation({
|
||||||
await ctx.db.delete(args.conversation_id);
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export default function fuzzysearch<T>(options: {
|
||||||
/**
|
/**
|
||||||
* Internal helper function that performs the actual fuzzy string matching
|
* 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 hlen = haystack.length;
|
||||||
const nlen = needle.length;
|
const nlen = needle.length;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
import ModelPicker from './model-picker.svelte';
|
import ModelPicker from './model-picker.svelte';
|
||||||
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||||
import { cn } from '$lib/utils/utils.js';
|
import { cn } from '$lib/utils/utils.js';
|
||||||
|
import SearchModal from './search-modal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
|
@ -370,6 +371,7 @@
|
||||||
|
|
||||||
<!-- header -->
|
<!-- 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">
|
<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>
|
<Tooltip>
|
||||||
{#snippet trigger(tooltip)}
|
{#snippet trigger(tooltip)}
|
||||||
<Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}>
|
<Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}>
|
||||||
|
|
|
||||||
69
src/routes/chat/search-modal.svelte
Normal file
69
src/routes/chat/search-modal.svelte
Normal 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue