From 8f0b8676fc9c60d7dc1f9346e4c8ddf5042b242d Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 06:15:17 -0500 Subject: [PATCH 01/18] guide users to add api key before enabling models --- eslint.config.js | 11 +++++- src/lib/cache/cached-query.svelte.ts | 2 +- .../account/api-keys/provider-card.svelte | 2 +- src/routes/account/models/+page.svelte | 38 +++++++++++++++++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 3e0ca46..4bd124f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,7 +20,16 @@ export default ts.config( languageOptions: { globals: { ...globals.browser, ...globals.node }, }, - rules: { 'no-undef': 'off' }, + rules: { + 'no-undef': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + }, + ], + }, }, { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], diff --git a/src/lib/cache/cached-query.svelte.ts b/src/lib/cache/cached-query.svelte.ts index 4f2756f..bd51bc0 100644 --- a/src/lib/cache/cached-query.svelte.ts +++ b/src/lib/cache/cached-query.svelte.ts @@ -80,7 +80,7 @@ export function invalidateQuery(query: FunctionReference<'query'>, queryArgs?: u globalCache.delete(key); } -export function invalidateQueriesMatching(pattern: string | RegExp): void { +export function invalidateQueriesMatching(_pattern: string | RegExp): void { // Note: This is a simplified implementation // In a real implementation, you'd need to track all cache keys console.warn( diff --git a/src/routes/account/api-keys/provider-card.svelte b/src/routes/account/api-keys/provider-card.svelte index 0d32781..5f14659 100644 --- a/src/routes/account/api-keys/provider-card.svelte +++ b/src/routes/account/api-keys/provider-card.svelte @@ -61,7 +61,7 @@ - + {meta.title} diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index 0526eed..d87043c 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -1,8 +1,10 @@ @@ -22,8 +33,27 @@
- {#each data.openRouterModels as model (model.id)} - {@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined} - - {/each} +
+

OpenRouter

+

Easy access to over 400 models.

+
+
+
+ {#each data.openRouterModels as model (model.id)} + {@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined} + + {/each} +
+ {#if !hasOpenRouterKey} +
+ +
+ {/if} +
From d5dbab44faa9e00ed458f7363516d441e40f6efc Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 06:18:40 -0500 Subject: [PATCH 02/18] prevent interaction when elements are masked out --- src/routes/account/models/+page.svelte | 2 +- src/routes/account/models/model-card.svelte | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index d87043c..081365a 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -45,7 +45,7 @@ > {#each data.openRouterModels as model (model.id)} {@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined} - + {/each} {#if !hasOpenRouterKey} diff --git a/src/routes/account/models/model-card.svelte b/src/routes/account/models/model-card.svelte index b88232b..f8b7638 100644 --- a/src/routes/account/models/model-card.svelte +++ b/src/routes/account/models/model-card.svelte @@ -17,9 +17,10 @@ provider: Provider; model: Model; enabled?: boolean; + disabled?: boolean; }; - let { provider, model, enabled = false }: Props = $props(); + let { provider, model, enabled = false, disabled = false }: Props = $props(); const client = useConvexClient(); @@ -58,8 +59,7 @@
{model.name} - - enabled, toggleEnabled} /> + enabled, toggleEnabled} {disabled} />
{showMore ? fullDescription : (shortDescription ?? fullDescription)} (showMore = !showMore)} + {disabled} > {showMore ? 'Show less' : 'Show more'} From 1aaae9b8f8bc69effcb1efd54d3a451d0dfe4159 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 06:42:56 -0500 Subject: [PATCH 03/18] 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 From 34b64420bf9b9ec208c049a02093d869c1225486 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 07:01:15 -0500 Subject: [PATCH 04/18] togglable sidebar --- convex.json | 2 +- package.json | 1 - pnpm-lock.yaml | 54 ----- src/lib/actions/shortcut.svelte.ts | 211 +++++++++++++++++++ src/lib/components/ui/button/button.svelte | 11 +- src/lib/components/ui/kbd/index.ts | 7 + src/lib/components/ui/kbd/kbd.svelte | 53 +++++ src/lib/components/ui/sidebar/sidebar.svelte | 3 + src/lib/hooks/is-mac.svelte.ts | 10 + src/routes/account/+layout.svelte | 19 +- 10 files changed, 309 insertions(+), 62 deletions(-) create mode 100644 src/lib/actions/shortcut.svelte.ts create mode 100644 src/lib/components/ui/kbd/index.ts create mode 100644 src/lib/components/ui/kbd/kbd.svelte create mode 100644 src/lib/hooks/is-mac.svelte.ts diff --git a/convex.json b/convex.json index d13b702..14af2a8 100644 --- a/convex.json +++ b/convex.json @@ -1,3 +1,3 @@ { - "functions": "src/lib/backend/convex" + "functions": "src/lib/backend/convex" } diff --git a/package.json b/package.json index 12b1975..dc7acaa 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "@tailwindcss/vite": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/svelte": "^5.2.4", - "bits-ui": "^2.6.2", "clsx": "^2.1.1", "concurrently": "^9.1.2", "convex": "^1.24.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34f5499..b6fb3f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,6 @@ importers: '@testing-library/svelte': specifier: ^5.2.4 version: 5.2.8(svelte@5.34.1)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))(vitest@3.2.3(@types/node@24.0.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)) - bits-ui: - specifier: ^2.6.2 - version: 2.6.2(@internationalized/date@3.8.2)(svelte@5.34.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -596,9 +593,6 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} - '@internationalized/date@3.8.2': - resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -814,9 +808,6 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@tailwindcss/node@4.1.10': resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==} @@ -1104,13 +1095,6 @@ packages: better-call@1.0.9: resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} - bits-ui@2.6.2: - resolution: {integrity: sha512-OlPSUAT+ENhtRarPjABljca1cCljyoAqOZKfgjCB8PxQii2fL0AKnzObhnEdhZKwYdpXczEtNOYqUUNYwliaWA==} - engines: {node: '>=20', pnpm: '>=8.7.0'} - peerDependencies: - '@internationalized/date': ^3.8.1 - svelte: ^5.33.0 - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2144,12 +2128,6 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte-toolbelt@0.9.1: - resolution: {integrity: sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==} - engines: {node: '>=18', pnpm: '>=8.7.0'} - peerDependencies: - svelte: ^5.30.2 - svelte@5.34.1: resolution: {integrity: sha512-jWNnN2hZFNtnzKPptCcJHBWrD9CtbHPDwIRIODufOYaWkR0kLmAIlM384lMt4ucwuIRX4hCJwD2D8ZtEcGJQ0Q==} engines: {node: '>=18'} @@ -2157,9 +2135,6 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tailwind-merge@3.0.2: resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} @@ -2789,10 +2764,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@internationalized/date@3.8.2': - dependencies: - '@swc/helpers': 0.5.17 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -2998,10 +2969,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@swc/helpers@0.5.17': - dependencies: - tslib: 2.8.1 - '@tailwindcss/node@4.1.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -3324,18 +3291,6 @@ snapshots: set-cookie-parser: 2.7.1 uncrypto: 0.1.3 - bits-ui@2.6.2(@internationalized/date@3.8.2)(svelte@5.34.1): - dependencies: - '@floating-ui/core': 1.7.1 - '@floating-ui/dom': 1.7.1 - '@internationalized/date': 3.8.2 - css.escape: 1.5.1 - esm-env: 1.2.2 - runed: 0.28.0(svelte@5.34.1) - svelte: 5.34.1 - svelte-toolbelt: 0.9.1(svelte@5.34.1) - tabbable: 6.2.0 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4294,13 +4249,6 @@ snapshots: style-to-object: 1.0.9 svelte: 5.34.1 - svelte-toolbelt@0.9.1(svelte@5.34.1): - dependencies: - clsx: 2.1.1 - runed: 0.28.0(svelte@5.34.1) - style-to-object: 1.0.9 - svelte: 5.34.1 - svelte@5.34.1: dependencies: '@ampproject/remapping': 2.3.0 @@ -4320,8 +4268,6 @@ snapshots: symbol-tree@3.2.4: {} - tabbable@6.2.0: {} - tailwind-merge@3.0.2: {} tailwind-merge@3.3.1: {} diff --git a/src/lib/actions/shortcut.svelte.ts b/src/lib/actions/shortcut.svelte.ts new file mode 100644 index 0000000..50c06a5 --- /dev/null +++ b/src/lib/actions/shortcut.svelte.ts @@ -0,0 +1,211 @@ +/* + Installed from @ieedan/shadcn-svelte-extras +*/ + +import { createAttachmentKey } from 'svelte/attachments'; + +export type Options = { + /** Event to use to detect the shortcut @default 'keydown' */ + event?: 'keydown' | 'keyup' | 'keypress'; + /** Function to be called when the shortcut is pressed */ + callback: (e: KeyboardEvent) => void; + /** Should the `Shift` key be pressed */ + shift?: boolean; + /** Should the `Ctrl` / `Command` key be pressed */ + ctrl?: boolean; + /** Should the `Alt` key be pressed */ + alt?: boolean; + /** Which key should be pressed */ + key: Key; + /** Control whether or not the shortcut prevents default behavior @default true */ + preventDefault?: boolean; + /** Control whether or not the shortcut stops propagation @default false */ + stopPropagation?: boolean; +}; + +/** Allows you to configure one or more shortcuts based on the key events of an element. + * + * ## Usage + * ```svelte + * + * + * ``` + */ +export const shortcut = (node: HTMLElement, options: Options[] | Options) => { + const handleKeydown = (e: KeyboardEvent, options: Options) => { + if (options.ctrl && !e.ctrlKey && !e.metaKey) return; + + if (options.alt && !e.altKey) return; + + if (options.shift && !e.shiftKey) return; + + if (e.key.toLocaleLowerCase() !== options.key.toLocaleLowerCase()) return; + + if (options.preventDefault === undefined || options.preventDefault) { + e.preventDefault(); + } + + if (options.stopPropagation) { + e.stopPropagation(); + } + + options.callback(e); + }; + + $effect(() => { + let optionsArr: Options[] = []; + if (Array.isArray(options)) { + optionsArr = options; + } else { + optionsArr = [options]; + } + + for (const opt of optionsArr) { + node.addEventListener(opt.event ?? 'keydown', (e) => handleKeydown(e, opt)); + } + + return () => { + for (const opt of optionsArr) { + node.removeEventListener(opt.event ?? 'keydown', (e) => handleKeydown(e, opt)); + } + }; + }); +}; + +/** Allows you to configure one or more shortcuts based on the key events of an element. + * + * ## Usage + * ```svelte + * + * + * ``` + */ +export function attachShortcut(opts: Options[] | Options) { + return { + [createAttachmentKey()]: (node: HTMLElement) => shortcut(node, opts), + }; +} + +export type Key = + | 'backspace' + | 'tab' + | 'enter' + | 'shift(left)' + | 'shift(right)' + | 'ctrl(left)' + | 'ctrl(right)' + | 'alt(left)' + | 'alt(right)' + | 'pause/break' + | 'caps lock' + | 'escape' + | 'space' + | 'page up' + | 'page down' + | 'end' + | 'home' + | 'left arrow' + | 'up arrow' + | 'right arrow' + | 'down arrow' + | 'print screen' + | 'insert' + | 'delete' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' + | 'left window key' + | 'right window key' + | 'select key (Context Menu)' + | 'numpad 0' + | 'numpad 1' + | 'numpad 2' + | 'numpad 3' + | 'numpad 4' + | 'numpad 5' + | 'numpad 6' + | 'numpad 7' + | 'numpad 8' + | 'numpad 9' + | 'multiply' + | 'add' + | 'subtract' + | 'decimal point' + | 'divide' + | 'f1' + | 'f2' + | 'f3' + | 'f4' + | 'f5' + | 'f6' + | 'f7' + | 'f8' + | 'f9' + | 'f10' + | 'f11' + | 'f12' + | 'num lock' + | 'scroll lock' + | 'audio volume mute' + | 'audio volume down' + | 'audio volume up' + | 'media player' + | 'launch application 1' + | 'launch application 2' + | 'semi-colon' + | 'equal sign' + | 'comma' + | 'dash' + | 'period' + | 'forward slash' + | 'Backquote/Grave accent' + | 'open bracket' + | 'back slash' + | 'close bracket' + | 'single quote'; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index a8db535..a9b71fe 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -3,7 +3,6 @@ --> + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte index fd845a7..7cbd77c 100644 --- a/src/lib/components/ui/sidebar/sidebar.svelte +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -2,12 +2,15 @@ import { cn } from '$lib/utils/utils'; import type { HTMLAttributes } from 'svelte/elements'; import { useSidebar } from './sidebar.svelte.js'; + import { shortcut } from '$lib/actions/shortcut.svelte.js'; let { children, ...rest }: HTMLAttributes = $props(); const sidebar = useSidebar(); + +
@@ -46,7 +50,7 @@
-
+
From 593257a3e9dae10a9c69842fd254fc93020f37c6 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 07:07:37 -0500 Subject: [PATCH 05/18] mobile stuff --- .prettierignore | 3 +++ src/app.css | 10 ++++++++++ src/routes/account/+layout.svelte | 8 ++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.prettierignore b/.prettierignore index 6562bcb..53da76a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,6 @@ pnpm-lock.yaml yarn.lock bun.lock bun.lockb + +# Convex formats this +convex.json \ No newline at end of file diff --git a/src/app.css b/src/app.css index 1687c6e..c3380bd 100644 --- a/src/app.css +++ b/src/app.css @@ -156,3 +156,13 @@ @apply bg-background text-foreground; } } + +/* For components that need horizontal scrolling */ +.scrollbar-hide { + -ms-overflow-style: none; /* Internet Explorer and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari, and Opera */ +} diff --git a/src/routes/account/+layout.svelte b/src/routes/account/+layout.svelte index d00d09c..88b1ab8 100644 --- a/src/routes/account/+layout.svelte +++ b/src/routes/account/+layout.svelte @@ -72,7 +72,7 @@ Keyboard Shortcuts
- Toggle Sidebar + Toggle Sidebar
{isMac.current ? '⌘' : 'Ctrl'} @@ -83,15 +83,15 @@
-
+
{#each navigation as tab (tab)} {tab.title} From 4ae694c9b8344ff16d73ed9384226d8352fdc488 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 08:04:14 -0500 Subject: [PATCH 06/18] better isMac --- src/lib/hooks/is-mac.svelte.ts | 15 +++++++-------- src/routes/account/+layout.svelte | 6 ++---- src/routes/account/models/+page.svelte | 12 ++++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/lib/hooks/is-mac.svelte.ts b/src/lib/hooks/is-mac.svelte.ts index 9d23676..09c9148 100644 --- a/src/lib/hooks/is-mac.svelte.ts +++ b/src/lib/hooks/is-mac.svelte.ts @@ -1,10 +1,9 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -import { browser } from '$app/environment'; - /** Attempts to determine if a user is on a Mac using `navigator.userAgent`. */ -export class IsMac { - readonly current = $derived(browser ? navigator.userAgent.includes('Mac') : false); +export function isMac() { + return navigator.userAgent.includes('Mac'); } + +/** `⌘` for mac or `Ctrl` for windows */ +export const cmdOrCtrl = isMac() ? '⌘' : 'Ctrl'; +/** `⌥` for mac or `Alt` for windows */ +export const optionOrAlt = isMac() ? '⌥' : 'Alt'; diff --git a/src/routes/account/+layout.svelte b/src/routes/account/+layout.svelte index 88b1ab8..368f9cc 100644 --- a/src/routes/account/+layout.svelte +++ b/src/routes/account/+layout.svelte @@ -7,7 +7,7 @@ import ArrowLeftIcon from '~icons/lucide/arrow-left'; import { Avatar } from 'melt/components'; import { Kbd } from '$lib/components/ui/kbd/index.js'; - import { IsMac } from '$lib/hooks/is-mac.svelte.js'; + import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js'; let { data, children } = $props(); @@ -35,8 +35,6 @@ await goto('/login'); } - - const isMac = new IsMac();
@@ -75,7 +73,7 @@ Toggle Sidebar
- {isMac.current ? '⌘' : 'Ctrl'} + {cmdOrCtrl} B
diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index feb4012..b3c2caf 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -28,6 +28,12 @@ let search = $state(''); + const openRouterToggle = new Toggle({ + value: true, + // TODO: enable this if and when when we use multiple providers + disabled: true, + }); + const openRouterModels = $derived( data.openRouterModels.filter((model) => { if (search !== '' && !hasOpenRouterKey) return false; @@ -36,12 +42,6 @@ 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, - }); From f4d7ec143aee385b100cedab5ee4230946651853 Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 08:30:34 -0500 Subject: [PATCH 07/18] even better --- src/lib/hooks/is-mac.svelte.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib/hooks/is-mac.svelte.ts b/src/lib/hooks/is-mac.svelte.ts index 09c9148..512a5ed 100644 --- a/src/lib/hooks/is-mac.svelte.ts +++ b/src/lib/hooks/is-mac.svelte.ts @@ -1,9 +1,7 @@ /** Attempts to determine if a user is on a Mac using `navigator.userAgent`. */ -export function isMac() { - return navigator.userAgent.includes('Mac'); -} +export const isMac = navigator.userAgent.includes('Mac'); /** `⌘` for mac or `Ctrl` for windows */ -export const cmdOrCtrl = isMac() ? '⌘' : 'Ctrl'; +export const cmdOrCtrl = isMac ? '⌘' : 'Ctrl'; /** `⌥` for mac or `Alt` for windows */ -export const optionOrAlt = isMac() ? '⌥' : 'Alt'; +export const optionOrAlt = isMac ? '⌥' : 'Alt'; From ff54b6b641cedbe93231b66b4a6af0435f5de83b Mon Sep 17 00:00:00 2001 From: Aidan Bleser Date: Mon, 16 Jun 2025 13:41:27 -0500 Subject: [PATCH 08/18] more merge --- src/routes/account/models/+page.svelte | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/routes/account/models/+page.svelte b/src/routes/account/models/+page.svelte index 01aebef..8329efe 100644 --- a/src/routes/account/models/+page.svelte +++ b/src/routes/account/models/+page.svelte @@ -10,12 +10,7 @@ import { Toggle } from 'melt/builders'; import XIcon from '~icons/lucide/x'; import PlusIcon from '~icons/lucide/plus'; - - let { data } = $props(); - - const enabledModels = useCachedQuery(api.user_enabled_models.get_enabled, { - user_id: session.current?.user.id ?? '', - }); + import { models } from '$lib/state/models.svelte'; const openRouterKeyQuery = useCachedQuery(api.user_keys.get, { provider: Provider.OpenRouter, @@ -35,7 +30,7 @@ }); const openRouterModels = $derived( - data.openRouterModels.filter((model) => { + models.from(Provider.OpenRouter).filter((model) => { if (search !== '' && !hasOpenRouterKey) return false; if (!openRouterToggle.value) return false; @@ -81,12 +76,10 @@ })} > {#each openRouterModels as model (model.id)} - {@const enabled = - enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined} {/each} From 82d3d4f9338da18712783d0ce0f5b8b5afc65dc1 Mon Sep 17 00:00:00 2001 From: Aidan Bleser <117548273+ieedan@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:43:53 -0500 Subject: [PATCH 09/18] rules (#9) --- src/lib/auth.ts | 16 +-- src/lib/backend/convex/schema.ts | 11 ++ src/lib/backend/convex/user_rules.ts | 55 ++++++++ src/lib/cache/cached-query.svelte.ts | 4 +- src/lib/components/ui/label/index.ts | 3 + src/lib/components/ui/label/label.svelte | 16 +++ src/lib/components/ui/textarea/index.ts | 3 + .../components/ui/textarea/textarea.svelte | 15 +++ src/routes/account/customization/+page.svelte | 120 ++++++++++++++++++ 9 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 src/lib/backend/convex/user_rules.ts create mode 100644 src/lib/components/ui/label/index.ts create mode 100644 src/lib/components/ui/label/label.svelte create mode 100644 src/lib/components/ui/textarea/index.ts create mode 100644 src/lib/components/ui/textarea/textarea.svelte create mode 100644 src/routes/account/customization/+page.svelte diff --git a/src/lib/auth.ts b/src/lib/auth.ts index af34f1e..581cbc0 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -14,14 +14,12 @@ export const auth = betterAuth({ clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, }, - // databaseHooks: { - // user: { - // create: { - // after: async ({ user }) => { - // // TODO: automatically enable default models for the user - // }, - // }, - // }, - // }, + databaseHooks: { + user: { + create: { + after: async (_user) => {}, + }, + }, + }, plugins: [], }); diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 77cb093..29d5bec 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -11,6 +11,8 @@ export const messageRoleValidator = v.union( export type MessageRole = Infer; +export const ruleAttachValidator = v.union(v.literal('always'), v.literal('manual')); + export default defineSchema({ user_keys: defineTable({ user_id: v.string(), @@ -30,6 +32,15 @@ export default defineSchema({ .index('by_model_provider', ['model_id', 'provider']) .index('by_provider_user', ['provider', 'user_id']) .index('by_model_provider_user', ['model_id', 'provider', 'user_id']), + user_rules: defineTable({ + user_id: v.string(), + name: v.string(), + attach: ruleAttachValidator, + rule: v.string(), + }) + .index('by_user', ['user_id']) + .index('by_user_attach', ['user_id', 'attach']) + .index('by_user_name', ['user_id', 'name']), conversations: defineTable({ user_id: v.string(), title: v.string(), diff --git a/src/lib/backend/convex/user_rules.ts b/src/lib/backend/convex/user_rules.ts new file mode 100644 index 0000000..32e73b1 --- /dev/null +++ b/src/lib/backend/convex/user_rules.ts @@ -0,0 +1,55 @@ +import { v } from 'convex/values'; +import { mutation, query } from './_generated/server'; +import { internal } from './_generated/api'; +import { ruleAttachValidator } from './schema'; +import { Doc } from './_generated/dataModel'; + +export const create = mutation({ + args: { + name: v.string(), + attach: ruleAttachValidator, + rule: v.string(), + sessionToken: v.string(), + }, + handler: async (ctx, args) => { + const session = await ctx.runQuery(internal.betterAuth.getSession, { + sessionToken: args.sessionToken, + }); + + if (!session) throw new Error('Invalid session token'); + + const existing = await ctx.db + .query('user_rules') + .withIndex('by_user_name', (q) => q.eq('user_id', session.userId).eq('name', args.name)) + .first(); + + if (existing) throw new Error('Rule with this name already exists'); + + await ctx.db.insert('user_rules', { + user_id: session.userId, + name: args.name, + attach: args.attach, + rule: args.rule, + }); + }, +}); + +export const all = query({ + args: { + sessionToken: v.string(), + }, + handler: async (ctx, args): Promise[]> => { + const session = await ctx.runQuery(internal.betterAuth.getSession, { + sessionToken: args.sessionToken, + }); + + if (!session) throw new Error('Invalid session token'); + + const allRules = await ctx.db + .query('user_rules') + .withIndex('by_user', (q) => q.eq('user_id', session.userId)) + .collect(); + + return allRules; + }, +}); diff --git a/src/lib/cache/cached-query.svelte.ts b/src/lib/cache/cached-query.svelte.ts index 959fb8d..7647129 100644 --- a/src/lib/cache/cached-query.svelte.ts +++ b/src/lib/cache/cached-query.svelte.ts @@ -3,14 +3,14 @@ import { SessionStorageCache } from './session-cache.js'; import { getFunctionName, type FunctionArgs, type FunctionReference } from 'convex/server'; import { extract, watch } from 'runed'; -interface CachedQueryOptions { +export interface CachedQueryOptions { cacheKey?: string; ttl?: number; staleWhileRevalidate?: boolean; enabled?: boolean; } -interface QueryResult { +export interface QueryResult { data: T | undefined; error: Error | undefined; isLoading: boolean; diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..d16fed0 --- /dev/null +++ b/src/lib/components/ui/label/index.ts @@ -0,0 +1,3 @@ +import Label from './label.svelte'; + +export { Label }; \ No newline at end of file diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..e58197f --- /dev/null +++ b/src/lib/components/ui/label/label.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..209df6e --- /dev/null +++ b/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,3 @@ +import Textarea from './textarea.svelte'; + +export { Textarea }; \ No newline at end of file diff --git a/src/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..b0185ed --- /dev/null +++ b/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/routes/account/customization/+page.svelte b/src/routes/account/customization/+page.svelte new file mode 100644 index 0000000..5d581b9 --- /dev/null +++ b/src/routes/account/customization/+page.svelte @@ -0,0 +1,120 @@ + + + + Customization | Thom.chat + + +

Customization

+

Customize your experience with Thom.chat.

+ +
+
+

Rules

+ +
+ {#if newRuleCollapsible.open} +
+
+

New Rule

+

+ Create a new rule to customize the behavior of your AI. +

+
+
+
+ + +
+
+ + +
+
+ +