From 1aaae9b8f8bc69effcb1efd54d3a451d0dfe4159 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 06:42:56 -0500 Subject: [PATCH] search functionality --- convex.json | 2 +- src/app.css | 2 +- src/lib/cache/cached-query.svelte.ts | 1 - src/lib/cache/lru-cache.ts | 220 ++++++++--------- src/lib/cache/session-cache.ts | 262 ++++++++++----------- src/lib/components/ui/search/index.ts | 1 + src/lib/components/ui/search/search.svelte | 13 + src/lib/components/ui/switch/index.ts | 2 +- src/routes/+page.server.ts | 6 +- src/routes/account/models/+page.svelte | 88 +++++-- tsconfig.json | 4 +- 11 files changed, 326 insertions(+), 275 deletions(-) create mode 100644 src/lib/components/ui/search/index.ts create mode 100644 src/lib/components/ui/search/search.svelte diff --git a/convex.json b/convex.json index 14af2a8..d13b702 100644 --- a/convex.json +++ b/convex.json @@ -1,3 +1,3 @@ { - "functions": "src/lib/backend/convex" + "functions": "src/lib/backend/convex" } diff --git a/src/app.css b/src/app.css index f7ce0eb..1687c6e 100644 --- a/src/app.css +++ b/src/app.css @@ -1,6 +1,6 @@ @import 'tailwindcss'; -@custom-variant dark (&:where(.dark, .dark *)); +@custom-variant dark (&:is(.dark *)); :root { --background: oklch(0.9754 0.0084 325.6414); diff --git a/src/lib/cache/cached-query.svelte.ts b/src/lib/cache/cached-query.svelte.ts index bd51bc0..eb4be3b 100644 --- a/src/lib/cache/cached-query.svelte.ts +++ b/src/lib/cache/cached-query.svelte.ts @@ -93,4 +93,3 @@ export function clearQueryCache(): void { } export { globalCache as queryCache }; - diff --git a/src/lib/cache/lru-cache.ts b/src/lib/cache/lru-cache.ts index 18334f7..39ac584 100644 --- a/src/lib/cache/lru-cache.ts +++ b/src/lib/cache/lru-cache.ts @@ -1,136 +1,136 @@ interface CacheNode { - key: K; - value: V; - size: number; - prev: CacheNode | null; - next: CacheNode | null; + key: K; + value: V; + size: number; + prev: CacheNode | null; + next: CacheNode | null; } export class LRUCache { - private capacity: number; - private currentSize = 0; - private cache = new Map>(); - private head: CacheNode | null = null; - private tail: CacheNode | null = null; + private capacity: number; + private currentSize = 0; + private cache = new Map>(); + private head: CacheNode | null = null; + private tail: CacheNode | null = null; - constructor(maxSizeBytes = 1024 * 1024) { - this.capacity = maxSizeBytes; - } + constructor(maxSizeBytes = 1024 * 1024) { + this.capacity = maxSizeBytes; + } - private calculateSize(value: V): number { - try { - return new Blob([JSON.stringify(value)]).size; - } catch { - return JSON.stringify(value).length * 2; - } - } + private calculateSize(value: V): number { + try { + return new Blob([JSON.stringify(value)]).size; + } catch { + return JSON.stringify(value).length * 2; + } + } - private removeNode(node: CacheNode): void { - if (node.prev) { - node.prev.next = node.next; - } else { - this.head = node.next; - } + private removeNode(node: CacheNode): void { + if (node.prev) { + node.prev.next = node.next; + } else { + this.head = node.next; + } - if (node.next) { - node.next.prev = node.prev; - } else { - this.tail = node.prev; - } - } + if (node.next) { + node.next.prev = node.prev; + } else { + this.tail = node.prev; + } + } - private addToHead(node: CacheNode): void { - node.prev = null; - node.next = this.head; + private addToHead(node: CacheNode): void { + node.prev = null; + node.next = this.head; - if (this.head) { - this.head.prev = node; - } + if (this.head) { + this.head.prev = node; + } - this.head = node; + this.head = node; - if (!this.tail) { - this.tail = node; - } - } + if (!this.tail) { + this.tail = node; + } + } - private evictLRU(): void { - while (this.tail && this.currentSize > this.capacity) { - const lastNode = this.tail; - this.removeNode(lastNode); - this.cache.delete(lastNode.key); - this.currentSize -= lastNode.size; - } - } + private evictLRU(): void { + while (this.tail && this.currentSize > this.capacity) { + const lastNode = this.tail; + this.removeNode(lastNode); + this.cache.delete(lastNode.key); + this.currentSize -= lastNode.size; + } + } - get(key: K): V | undefined { - const node = this.cache.get(key); - if (!node) return undefined; + get(key: K): V | undefined { + const node = this.cache.get(key); + if (!node) return undefined; - this.removeNode(node); - this.addToHead(node); + this.removeNode(node); + this.addToHead(node); - return node.value; - } + return node.value; + } - set(key: K, value: V): void { - const size = this.calculateSize(value); - - if (size > this.capacity) { - return; - } + set(key: K, value: V): void { + const size = this.calculateSize(value); - const existingNode = this.cache.get(key); - - if (existingNode) { - existingNode.value = value; - this.currentSize = this.currentSize - existingNode.size + size; - existingNode.size = size; - this.removeNode(existingNode); - this.addToHead(existingNode); - } else { - const newNode: CacheNode = { - key, - value, - size, - prev: null, - next: null, - }; + if (size > this.capacity) { + return; + } - this.currentSize += size; - this.cache.set(key, newNode); - this.addToHead(newNode); - } + const existingNode = this.cache.get(key); - this.evictLRU(); - } + if (existingNode) { + existingNode.value = value; + this.currentSize = this.currentSize - existingNode.size + size; + existingNode.size = size; + this.removeNode(existingNode); + this.addToHead(existingNode); + } else { + const newNode: CacheNode = { + key, + value, + size, + prev: null, + next: null, + }; - delete(key: K): boolean { - const node = this.cache.get(key); - if (!node) return false; + this.currentSize += size; + this.cache.set(key, newNode); + this.addToHead(newNode); + } - this.removeNode(node); - this.cache.delete(key); - this.currentSize -= node.size; - return true; - } + this.evictLRU(); + } - clear(): void { - this.cache.clear(); - this.head = null; - this.tail = null; - this.currentSize = 0; - } + delete(key: K): boolean { + const node = this.cache.get(key); + if (!node) return false; - get size(): number { - return this.cache.size; - } + this.removeNode(node); + this.cache.delete(key); + this.currentSize -= node.size; + return true; + } - get bytes(): number { - return this.currentSize; - } + clear(): void { + this.cache.clear(); + this.head = null; + this.tail = null; + this.currentSize = 0; + } - has(key: K): boolean { - return this.cache.has(key); - } -} \ No newline at end of file + get size(): number { + return this.cache.size; + } + + get bytes(): number { + return this.currentSize; + } + + has(key: K): boolean { + return this.cache.has(key); + } +} diff --git a/src/lib/cache/session-cache.ts b/src/lib/cache/session-cache.ts index 445924f..1abbeab 100644 --- a/src/lib/cache/session-cache.ts +++ b/src/lib/cache/session-cache.ts @@ -1,160 +1,156 @@ import { LRUCache } from './lru-cache.js'; interface CacheEntry { - data: T; - timestamp: number; - ttl?: number; + data: T; + timestamp: number; + ttl?: number; } export class SessionStorageCache { - private memoryCache: LRUCache>; - private storageKey: string; - private writeTimeout: ReturnType | null = null; - private debounceMs: number; - private pendingWrites = new Set(); + private memoryCache: LRUCache>; + private storageKey: string; + private writeTimeout: ReturnType | null = null; + private debounceMs: number; + private pendingWrites = new Set(); - constructor( - storageKey = 'query-cache', - maxSizeBytes = 1024 * 1024, - debounceMs = 300 - ) { - this.storageKey = storageKey; - this.debounceMs = debounceMs; - this.memoryCache = new LRUCache>(maxSizeBytes); - this.loadFromSessionStorage(); - } + constructor(storageKey = 'query-cache', maxSizeBytes = 1024 * 1024, debounceMs = 300) { + this.storageKey = storageKey; + this.debounceMs = debounceMs; + this.memoryCache = new LRUCache>(maxSizeBytes); + this.loadFromSessionStorage(); + } - private loadFromSessionStorage(): void { - try { - const stored = sessionStorage.getItem(this.storageKey); - if (!stored) return; + private loadFromSessionStorage(): void { + try { + const stored = sessionStorage.getItem(this.storageKey); + if (!stored) return; - const data = JSON.parse(stored) as Record>; - const now = Date.now(); + const data = JSON.parse(stored) as Record>; + const now = Date.now(); - for (const [key, entry] of Object.entries(data)) { - if (entry.ttl && now - entry.timestamp > entry.ttl) { - continue; - } - this.memoryCache.set(key, entry); - } - } catch (error) { - console.warn('Failed to load cache from sessionStorage:', error); - } - } + for (const [key, entry] of Object.entries(data)) { + if (entry.ttl && now - entry.timestamp > entry.ttl) { + continue; + } + this.memoryCache.set(key, entry); + } + } catch (error) { + console.warn('Failed to load cache from sessionStorage:', error); + } + } - private debouncedWrite(): void { - if (this.writeTimeout) { - clearTimeout(this.writeTimeout); - } + private debouncedWrite(): void { + if (this.writeTimeout) { + clearTimeout(this.writeTimeout); + } - this.writeTimeout = setTimeout(() => { - this.writeToSessionStorage(); - this.writeTimeout = null; - }, this.debounceMs); - } + this.writeTimeout = setTimeout(() => { + this.writeToSessionStorage(); + this.writeTimeout = null; + }, this.debounceMs); + } - private writeToSessionStorage(): void { - try { - const cacheData: Record> = {}; - const now = Date.now(); + private writeToSessionStorage(): void { + try { + const cacheData: Record> = {}; + const now = Date.now(); - for (const key of this.pendingWrites) { - const entry = this.memoryCache.get(key); - if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) { - cacheData[key] = entry; - } - } + for (const key of this.pendingWrites) { + const entry = this.memoryCache.get(key); + if (entry && (!entry.ttl || now - entry.timestamp < entry.ttl)) { + cacheData[key] = entry; + } + } - const existingData = sessionStorage.getItem(this.storageKey); - if (existingData) { - const existing = JSON.parse(existingData) as Record>; - for (const [key, entry] of Object.entries(existing)) { - if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) { - cacheData[key] = entry; - } - } - } + const existingData = sessionStorage.getItem(this.storageKey); + if (existingData) { + const existing = JSON.parse(existingData) as Record>; + for (const [key, entry] of Object.entries(existing)) { + if (!this.pendingWrites.has(key) && (!entry.ttl || now - entry.timestamp < entry.ttl)) { + cacheData[key] = entry; + } + } + } - sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData)); - this.pendingWrites.clear(); - } catch (error) { - console.warn('Failed to write cache to sessionStorage:', error); - } - } + sessionStorage.setItem(this.storageKey, JSON.stringify(cacheData)); + this.pendingWrites.clear(); + } catch (error) { + console.warn('Failed to write cache to sessionStorage:', error); + } + } - get(key: string): T | undefined { - const entry = this.memoryCache.get(key); - if (!entry) return undefined; + get(key: string): T | undefined { + const entry = this.memoryCache.get(key); + if (!entry) return undefined; - if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) { - this.delete(key); - return undefined; - } + if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) { + this.delete(key); + return undefined; + } - return entry.data; - } + return entry.data; + } - set(key: string, data: T, ttlMs?: number): void { - const entry: CacheEntry = { - data, - timestamp: Date.now(), - ttl: ttlMs, - }; + set(key: string, data: T, ttlMs?: number): void { + const entry: CacheEntry = { + data, + timestamp: Date.now(), + ttl: ttlMs, + }; - this.memoryCache.set(key, entry); - this.pendingWrites.add(key); - this.debouncedWrite(); - } + this.memoryCache.set(key, entry); + this.pendingWrites.add(key); + this.debouncedWrite(); + } - delete(key: string): boolean { - const deleted = this.memoryCache.delete(key); - if (deleted) { - this.pendingWrites.add(key); - this.debouncedWrite(); - } - return deleted; - } + delete(key: string): boolean { + const deleted = this.memoryCache.delete(key); + if (deleted) { + this.pendingWrites.add(key); + this.debouncedWrite(); + } + return deleted; + } - clear(): void { - this.memoryCache.clear(); - try { - sessionStorage.removeItem(this.storageKey); - } catch (error) { - console.warn('Failed to clear sessionStorage:', error); - } - if (this.writeTimeout) { - clearTimeout(this.writeTimeout); - this.writeTimeout = null; - } - this.pendingWrites.clear(); - } + clear(): void { + this.memoryCache.clear(); + try { + sessionStorage.removeItem(this.storageKey); + } catch (error) { + console.warn('Failed to clear sessionStorage:', error); + } + if (this.writeTimeout) { + clearTimeout(this.writeTimeout); + this.writeTimeout = null; + } + this.pendingWrites.clear(); + } - has(key: string): boolean { - const entry = this.memoryCache.get(key); - if (!entry) return false; - - if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) { - this.delete(key); - return false; - } - - return true; - } + has(key: string): boolean { + const entry = this.memoryCache.get(key); + if (!entry) return false; - get size(): number { - return this.memoryCache.size; - } + if (entry.ttl && Date.now() - entry.timestamp > entry.ttl) { + this.delete(key); + return false; + } - get bytes(): number { - return this.memoryCache.bytes; - } + return true; + } - forceWrite(): void { - if (this.writeTimeout) { - clearTimeout(this.writeTimeout); - this.writeTimeout = null; - } - this.writeToSessionStorage(); - } -} \ No newline at end of file + get size(): number { + return this.memoryCache.size; + } + + get bytes(): number { + return this.memoryCache.bytes; + } + + forceWrite(): void { + if (this.writeTimeout) { + clearTimeout(this.writeTimeout); + this.writeTimeout = null; + } + this.writeToSessionStorage(); + } +} diff --git a/src/lib/components/ui/search/index.ts b/src/lib/components/ui/search/index.ts new file mode 100644 index 0000000..a3762fd --- /dev/null +++ b/src/lib/components/ui/search/index.ts @@ -0,0 +1 @@ +export { default as Search } from './search.svelte'; diff --git a/src/lib/components/ui/search/search.svelte b/src/lib/components/ui/search/search.svelte new file mode 100644 index 0000000..5d0d351 --- /dev/null +++ b/src/lib/components/ui/search/search.svelte @@ -0,0 +1,13 @@ + + +
+ + +
diff --git a/src/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts index 42ac3b6..c96a6aa 100644 --- a/src/lib/components/ui/switch/index.ts +++ b/src/lib/components/ui/switch/index.ts @@ -1,3 +1,3 @@ import Switch from './switch.svelte'; -export { Switch }; \ No newline at end of file +export { Switch }; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 9048099..dc7b4fd 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,6 +1,6 @@ -import { redirect } from "@sveltejs/kit"; +import { redirect } from '@sveltejs/kit'; export async function load() { // temporary redirect to /chat - redirect(303, '/chat'); -} \ No newline at end of file + redirect(303, '/chat'); +} diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index 081365a..feb4012 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -2,10 +2,14 @@ import { api } from '$lib/backend/convex/_generated/api'; import { useCachedQuery } from '$lib/cache/cached-query.svelte'; import { Button } from '$lib/components/ui/button'; + import { Search } from '$lib/components/ui/search'; import { session } from '$lib/state/session.svelte'; import { Provider } from '$lib/types.js'; import { cn } from '$lib/utils/utils'; import ModelCard from './model-card.svelte'; + import { Toggle } from 'melt/builders'; + import XIcon from '~icons/lucide/x'; + import PlusIcon from '~icons/lucide/plus'; let { data } = $props(); @@ -21,6 +25,23 @@ const hasOpenRouterKey = $derived( openRouterKeyQuery.data !== undefined && openRouterKeyQuery.data !== '' ); + + let search = $state(''); + + const openRouterModels = $derived( + data.openRouterModels.filter((model) => { + if (search !== '' && !hasOpenRouterKey) return false; + if (!openRouterToggle.value) return false; + + return model.name.toLowerCase().includes(search.toLowerCase()); + }) + ); + + const openRouterToggle = new Toggle({ + value: true, + // TODO: enable this if and when when we use multiple providers + disabled: true, + }); @@ -32,28 +53,51 @@ Choose which models appear in your model selector. This won't affect existing conversations. -
-
-

OpenRouter

-

Easy access to over 400 models.

-
-
-
+ +
+
- {#if !hasOpenRouterKey} -
- -
- {/if} + OpenRouter + + +
+ +{#if openRouterModels.length > 0} +
+
+

OpenRouter

+

Easy access to over 400 models.

+
+
+
+ {#each openRouterModels as model (model.id)} + {@const enabled = + enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined} + + {/each} +
+ {#if !hasOpenRouterKey} +
+ +
+ {/if} +
+
+{/if} diff --git a/tsconfig.json b/tsconfig.json index 603cbc4..c29076e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,9 +10,7 @@ "sourceMap": true, "strict": true, "moduleResolution": "bundler", - "types": [ - "unplugin-icons/types/svelte" - ] + "types": ["unplugin-icons/types/svelte"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files