enable/disable mutation

This commit is contained in:
Aidan Bleser 2025-06-14 09:28:12 -05:00
parent ef5a698c67
commit 0763dc465d
11 changed files with 178 additions and 23 deletions

View file

@ -8,6 +8,7 @@
"paths": { "paths": {
"*": "$lib/blocks", "*": "$lib/blocks",
"utils": "$lib/utils", "utils": "$lib/utils",
"ts": "$lib/utils",
"ui": "$lib/components/ui", "ui": "$lib/components/ui",
"actions": "$lib/actions", "actions": "$lib/actions",
"hooks": "$lib/hooks" "hooks": "$lib/hooks"

View file

@ -14,5 +14,14 @@ export const auth = betterAuth({
clientSecret: process.env.GITHUB_CLIENT_SECRET!, clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}, },
}, },
databaseHooks: {
user: {
create: {
after: async ({ user }) => {
// TODO: automatically enable default models for the user
},
},
},
},
plugins: [], plugins: [],
}); });

View file

@ -6,14 +6,14 @@ export const providerValidator = v.union(...Object.values(Provider).map((p) => v
export default defineSchema({ export default defineSchema({
user_keys: defineTable({ user_keys: defineTable({
user_id: v.id('users'), user_id: v.string(),
provider: providerValidator, provider: providerValidator,
key: v.string(), key: v.string(),
}) })
.index('by_user', ['user_id']) .index('by_user', ['user_id'])
.index('by_provider_user', ['provider', 'user_id']), .index('by_provider_user', ['provider', 'user_id']),
user_enabled_models: defineTable({ user_enabled_models: defineTable({
user_id: v.id('users'), user_id: v.string(),
provider: providerValidator, provider: providerValidator,
/** Different providers may use different ids for the same model */ /** Different providers may use different ids for the same model */
model_id: v.string(), model_id: v.string(),

View file

@ -1,16 +1,19 @@
import { mutation } from './_generated/server'; import { query, mutation } from './_generated/server';
import { v } from 'convex/values'; import { v } from 'convex/values';
import { providerValidator } from './schema'; import { providerValidator } from './schema';
import * as array from '../../utils/array';
export const get = mutation({ export const get_enabled = query({
args: { args: {
user_id: v.id('users'), user_id: v.string(),
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {
return await ctx.db const models = await ctx.db
.query('user_enabled_models') .query('user_enabled_models')
.withIndex('by_user', (q) => q.eq('user_id', args.user_id)) .withIndex('by_user', (q) => q.eq('user_id', args.user_id))
.collect(); .collect();
return array.toMap(models, (m) => [`${m.provider}:${m.model_id}`, m]);
}, },
}); });
@ -18,7 +21,8 @@ export const set = mutation({
args: { args: {
provider: providerValidator, provider: providerValidator,
model_id: v.string(), model_id: v.string(),
user_id: v.id('users'), user_id: v.string(),
enabled: v.boolean(),
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const existing = await ctx.db const existing = await ctx.db
@ -28,8 +32,15 @@ export const set = mutation({
) )
.first(); .first();
if (existing) return; if (args.enabled && existing) return; // nothing to do here
await ctx.db.insert('user_enabled_models', { ...args, pinned: null }); if (existing) {
await ctx.db.delete(existing._id);
} else {
await ctx.db.insert('user_enabled_models', {
...{ ...args, enabled: undefined },
pinned: null,
});
}
}, },
}); });

View file

@ -5,7 +5,7 @@ import { providerValidator } from './schema';
export const get = query({ export const get = query({
args: { args: {
user_id: v.id('users'), user_id: v.string(),
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {
const allKeys = await ctx.db const allKeys = await ctx.db
@ -26,7 +26,7 @@ export const get = query({
export const set = mutation({ export const set = mutation({
args: { args: {
provider: providerValidator, provider: providerValidator,
user_id: v.id('users'), user_id: v.string(),
key: v.string(), key: v.string(),
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {

View file

@ -0,0 +1,3 @@
import Switch from './switch.svelte';
export { Switch };

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from '$lib/utils/utils';
import { Toggle, type ToggleProps } from 'melt/builders';
let { class: className, ...rest }: ToggleProps & { class?: string } = $props();
const toggle = new Toggle(rest);
</script>
<button
{...toggle.trigger}
class={cn(
'bg-muted-foreground/20 relative h-5 w-10 rounded-full transition-all',
{ 'bg-primary': toggle.value },
className
)}
>
<span
class={cn('bg-background absolute top-0.5 left-0.5 h-4 w-4 rounded-full transition-all', {
'bg-primary-foreground': toggle.value,
})}
style="transform: translateX({toggle.value ? '20px' : '0px'})"
></span>
</button>

80
src/lib/utils/array.ts Normal file
View file

@ -0,0 +1,80 @@
/*
Installed from @ieedan/std
*/
/** Maps the provided map into an array using the provided mapping function.
*
* @param map Map to be entered into an array
* @param fn A mapping function to transform each pair into an item
* @returns
*
* ## Usage
* ```ts
* console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 }
*
* const arr = fromMap(map, (_, value) => value);
*
* console.log(arr); // [5, 4, 3, 2, 1]
* ```
*/
export function fromMap<K, V, T>(map: Map<K, V>, fn: (key: K, value: V) => T): T[] {
const items: T[] = [];
for (const [key, value] of map) {
items.push(fn(key, value));
}
return items;
}
/** Calculates the sum of all elements in the array based on the provided function.
*
* @param arr Array of items to be summed.
* @param fn Summing function
* @returns
*
* ## Usage
*
* ```ts
* const total = sum([1, 2, 3, 4, 5], (num) => num);
*
* console.log(total); // 15
* ```
*/
export function sum<T>(arr: T[], fn: (item: T) => number): number {
let total = 0;
for (const item of arr) {
total = total + fn(item);
}
return total;
}
/** Maps the provided array into a map
*
* @param arr Array of items to be entered into a map
* @param fn A mapping function to transform each item into a key value pair
* @returns
*
* ## Usage
* ```ts
* const map = toMap([5, 4, 3, 2, 1], (item, i) => [i, item]);
*
* console.log(map); // Map(5) { 0 => 5, 1 => 4, 2 => 3, 3 => 2, 4 => 1 }
* ```
*/
export function toMap<T, V>(
arr: T[],
fn: (item: T, index: number) => [key: string, value: V]
): Record<string, V> {
const map: Record<string, V> = {};
for (let i = 0; i < arr.length; i++) {
const [key, value] = fn(arr[i], i);
map[key] = value;
}
return map;
}

View file

@ -39,7 +39,7 @@
const key = formData.get('key'); const key = formData.get('key');
if (key === null || !session.current?.user.id) return; if (key === null || !session.current?.user.id) return;
const res = await client.mutation(api.user_keys.set, { await client.mutation(api.user_keys.set, {
provider, provider,
user_id: session.current?.user.id ?? '', user_id: session.current?.user.id ?? '',
key: `${key}`, key: `${key}`,

View file

@ -1,11 +1,15 @@
<script lang="ts"> <script lang="ts">
import { Provider } from '$lib/types.js'; import { Provider } from '$lib/types.js';
import { useConvexClient } from 'convex-svelte'; import { useQuery } from 'convex-svelte';
import Model from './model.svelte'; import Model from './model.svelte';
import { session } from '$lib/state/session.svelte';
import { api } from '$lib/backend/convex/_generated/api';
let { data } = $props(); let { data } = $props();
const client = useConvexClient(); const enabledModels = useQuery(api.user_enabled_models.get_enabled, {
user_id: session.current?.user.id ?? '',
});
</script> </script>
<svelte:head> <svelte:head>
@ -20,5 +24,6 @@
</div> </div>
{#each data.openRouterModels as model} {#each data.openRouterModels as model}
<Model provider={Provider.OpenRouter} model={model} /> {@const enabled = enabledModels.data?.[`${Provider.OpenRouter}:${model.id}`] !== undefined}
<Model provider={Provider.OpenRouter} {model} {enabled} />
{/each} {/each}

View file

@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Provider } from '$lib/types'; import type { Provider } from '$lib/types';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Switch } from '$lib/components/ui/switch';
import { useConvexClient } from 'convex-svelte';
import { api } from '$lib/backend/convex/_generated/api';
import { session } from '$lib/state/session.svelte.js';
type Model = { type Model = {
id: string; id: string;
@ -11,12 +15,15 @@
type Props = { type Props = {
provider: Provider; provider: Provider;
model: Model; model: Model;
enabled?: boolean;
}; };
let { provider, model }: Props = $props(); let { provider, model, enabled = false }: Props = $props();
const client = useConvexClient();
function getShortDescription(text: string) { function getShortDescription(text: string) {
// match any punctuation followed by a space or the end of the string // match any punctuation followed by a space or the end of the string
const index = text.match(/[.!?](\s|$)/)?.index; const index = text.match(/[.!?](\s|$)/)?.index;
if (index === undefined) return { shortDescription: null, fullDescription: text }; if (index === undefined) return { shortDescription: null, fullDescription: text };
@ -27,23 +34,38 @@
const { shortDescription, fullDescription } = $derived(getShortDescription(model.description)); const { shortDescription, fullDescription } = $derived(getShortDescription(model.description));
let showMore = $state(false); let showMore = $state(false);
async function toggleEnabled(enabled: boolean) {
if (!session.current?.user.id) return;
await client.mutation(api.user_enabled_models.set, {
provider,
user_id: session.current.user.id,
model_id: model.id,
enabled,
});
}
</script> </script>
<Card.Root> <Card.Root>
<Card.Header> <Card.Header>
<Card.Title>{model.name}</Card.Title> <div class="flex items-center justify-between">
<Card.Description>{showMore ? fullDescription : shortDescription ?? fullDescription}</Card.Description> <Card.Title>{model.name}</Card.Title>
<!-- TODO: make this actually work -->
<Switch value={enabled} onValueChange={toggleEnabled} />
</div>
<Card.Description
>{showMore ? fullDescription : (shortDescription ?? fullDescription)}</Card.Description
>
{#if shortDescription !== null} {#if shortDescription !== null}
<button <button
type="button" type="button"
class="text-muted-foreground text-start w-fit text-xs" class="text-muted-foreground w-fit text-start text-xs"
onclick={() => (showMore = !showMore)} onclick={() => (showMore = !showMore)}
> >
{showMore ? 'Show less' : 'Show more'} {showMore ? 'Show less' : 'Show more'}
</button> </button>
{/if} {/if}
</Card.Header> </Card.Header>
<Card.Content> <Card.Content></Card.Content>
</Card.Content>
</Card.Root> </Card.Root>