update rules

This commit is contained in:
Aidan Bleser 2025-06-16 14:01:58 -05:00
parent 82d3d4f933
commit 8a5c3da385
5 changed files with 153 additions and 11 deletions

View file

@ -34,6 +34,53 @@ export const create = mutation({
},
});
export const update = mutation({
args: {
ruleId: v.id('user_rules'),
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.get(args.ruleId);
if (!existing) throw new Error('Rule not found');
if (existing.user_id !== session.userId) throw new Error('You are not the owner of this rule');
await ctx.db.patch(args.ruleId, {
attach: args.attach,
rule: args.rule,
});
},
});
export const remove = mutation({
args: {
ruleId: v.id('user_rules'),
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.get(args.ruleId);
if (!existing) throw new Error('Rule not found');
if (existing.user_id !== session.userId) throw new Error('You are not the owner of this rule');
await ctx.db.delete(args.ruleId);
},
});
export const all = query({
args: {
sessionToken: v.string(),

View file

@ -1,3 +1,3 @@
import Label from './label.svelte';
export { Label };
export { Label };

View file

@ -1,3 +1,3 @@
import Textarea from './textarea.svelte';
export { Textarea };
export { Textarea };

View file

@ -12,6 +12,7 @@
import type { Doc } from '$lib/backend/convex/_generated/dataModel';
import { Input } from '$lib/components/ui/input';
import { api } from '$lib/backend/convex/_generated/api';
import Rule from './rule.svelte';
const client = useConvexClient();
@ -28,9 +29,9 @@
async function submitNewRule(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const name = formData.get('name');
const attach = formData.get('attach');
const rule = formData.get('rule');
const name = formData.get('name') as string;
const attach = formData.get('attach') as 'always' | 'manual';
const rule = formData.get('rule') as string;
if (rule === '' || !rule) return;
@ -59,7 +60,7 @@
<div class="mt-8 flex flex-col gap-4">
<div class="flex place-items-center justify-between">
<h3 class="text-lg font-bold">Rules</h3>
<h3 class="text-xl font-bold">Rules</h3>
<Button
{...newRuleCollapsible.trigger}
variant={newRuleCollapsible.open ? 'outline' : 'default'}
@ -76,6 +77,7 @@
<div
{...newRuleCollapsible.content}
in:slide={{ duration: 150, axis: 'y' }}
out:slide={{ duration: 150, axis: 'y' }}
class="bg-card flex flex-col gap-4 rounded-lg border p-4"
>
<div class="flex flex-col gap-1">
@ -111,10 +113,7 @@
</form>
</div>
{/if}
{#each userRulesQuery.data ?? [] as rule}
<div class="flex flex-col gap-2">
<h3 class="text-lg font-bold">{rule.name}</h3>
<p class="text-muted-foreground text-sm">{rule.rule}</p>
</div>
{#each userRulesQuery.data ?? [] as rule (rule._id)}
<Rule {rule} />
{/each}
</div>

View file

@ -0,0 +1,96 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import { Textarea } from '$lib/components/ui/textarea';
import { Button } from '$lib/components/ui/button';
import type { Doc } from '$lib/backend/convex/_generated/dataModel';
import { useConvexClient } from 'convex-svelte';
import { api } from '$lib/backend/convex/_generated/api';
import { session } from '$lib/state/session.svelte';
import { LocalToasts } from '$lib/builders/local-toasts.svelte';
import { ResultAsync } from 'neverthrow';
type Props = {
rule: Doc<'user_rules'>;
};
const id = $props.id();
let { rule }: Props = $props();
const client = useConvexClient();
let updating = $state(false);
const toasts = new LocalToasts({ id });
async function updateRule(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const attach = formData.get('attach') as 'always' | 'manual';
const ruleText = formData.get('rule') as string;
if (ruleText === '' || !ruleText) return;
updating = true;
const res = await ResultAsync.fromPromise(
client.mutation(api.user_rules.update, {
ruleId: rule._id,
attach,
rule: ruleText,
sessionToken: session.current?.session.token ?? '',
}),
(e) => e
);
toasts.addToast({
data: {
content: res.isOk() ? 'Saved' : 'Failed to save',
variant: res.isOk() ? 'info' : 'danger',
},
});
updating = false;
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>{rule.name}</Card.Title>
</Card.Header>
<Card.Content tag="form" onsubmit={updateRule}>
<div class="flex flex-col gap-2">
<Label for="attach">Attach</Label>
<select
id="attach"
name="attach"
value={rule.attach}
class="border-input bg-background h-9 w-fit rounded-md border px-2 pr-6 text-sm"
required
>
<option value="always">Always</option>
<option value="manual">Manual</option>
</select>
</div>
<div class="flex flex-col gap-2">
<Label for="rule">Instructions</Label>
<Textarea
id="rule"
value={rule.rule}
name="rule"
placeholder="How should the AI respond?"
required
/>
</div>
<div class="flex justify-end">
<Button loading={updating} {...toasts.trigger} type="submit">Save</Button>
</div>
</Card.Content>
</Card.Root>
{#each toasts.toasts as toast (toast)}
<div {...toast.attrs} class={toast.class}>
{toast.data.content}
</div>
{/each}