enable/disable mutation
This commit is contained in:
parent
ef5a698c67
commit
0763dc465d
11 changed files with 178 additions and 23 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
3
src/lib/components/ui/switch/index.ts
Normal file
3
src/lib/components/ui/switch/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Switch from './switch.svelte';
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
24
src/lib/components/ui/switch/switch.svelte
Normal file
24
src/lib/components/ui/switch/switch.svelte
Normal 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
80
src/lib/utils/array.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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}`,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue