trigger & grouping

This commit is contained in:
Thomas G. Lopes 2025-06-17 11:49:33 +01:00
parent c622ff25a7
commit 3abd8b5502
10 changed files with 135 additions and 40 deletions

View file

@ -15,6 +15,7 @@
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.3257 0.1161 325.0372);
--primary: oklch(0.5316 0.1409 355.1999);
--heading: oklch(0.5797 0.1194 237.7893);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.8696 0.0675 334.8991);
--secondary-foreground: oklch(0.4448 0.1341 324.7991);
@ -62,6 +63,7 @@
--popover: oklch(0.1548 0.0132 338.9015);
--popover-foreground: oklch(0.9647 0.0091 341.8035);
--primary: oklch(0.5797 0.1194 237.7893);
--heading: oklch(0.85 0.1194 237.7893);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.3137 0.0306 310.061);
--secondary-foreground: oklch(0.8483 0.0382 307.9613);
@ -108,6 +110,7 @@
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-heading: var(--heading);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);

View file

@ -1,8 +1,9 @@
import { v } from 'convex/values';
import { Provider } from '../../types';
import { internal } from './_generated/api';
import { mutation, query } from './_generated/server';
import { query } from './_generated/server';
import { providerValidator } from './schema';
import { mutation } from './functions';
export const all = query({
args: {

View file

@ -1,9 +1,10 @@
import { v } from 'convex/values';
import { api } from './_generated/api';
import { mutation, query } from './_generated/server';
import { query } from './_generated/server';
import { type Id } from './_generated/dataModel';
import { type SessionObj } from './betterAuth';
import { messageRoleValidator } from './schema';
import { mutation } from './functions';
export const get = query({
args: {
@ -24,7 +25,12 @@ export const get = query({
.withIndex('by_user', (q) => q.eq('user_id', s.userId))
.collect();
return conversations;
return conversations.sort((a, b) => {
const aTime = a.updated_at ?? 0;
const bTime = b.updated_at ?? 0;
return bTime - aTime;
});
},
});
@ -45,6 +51,7 @@ export const create = mutation({
title: 'Untitled (for now)',
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Id type is janking out
user_id: session.userId as any,
updated_at: Date.now(),
});
return res;
@ -73,6 +80,7 @@ export const createAndAddMessage = mutation({
title: 'Untitled (for now)',
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Id type is janking out
user_id: session.userId as any,
updated_at: Date.now(),
});
const messageId = await ctx.runMutation(api.messages.create, {

View file

@ -7,7 +7,7 @@ import {
import { customCtx, customMutation } from 'convex-helpers/server/customFunctions';
import { Triggers } from 'convex-helpers/server/triggers';
import { DataModel } from './_generated/dataModel';
import { type Id, type DataModel } from './_generated/dataModel';
const triggers = new Triggers<DataModel>();
@ -24,6 +24,20 @@ triggers.register('conversations', async (ctx, change) => {
}
});
// Update conversation updated_at when a message is created/updated
triggers.register('messages', async (ctx, change) => {
if (change.operation === 'insert' || change.operation === 'update') {
const conversationId = change.newDoc.conversation_id;
const conversation = await ctx.db.get(conversationId as Id<'conversations'>);
if (!conversation) return;
await ctx.db.patch(conversationId as Id<'conversations'>, {
updated_at: Date.now(),
});
}
});
// TODO: Cascade delete rules when a user is deleted
// create wrappers that replace the built-in `mutation` and `internalMutation`

View file

@ -1,8 +1,9 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { api } from './_generated/api';
import { messageRoleValidator, providerValidator } from './schema';
import { type Id } from './_generated/dataModel';
import { query } from './_generated/server';
import { messageRoleValidator, providerValidator } from './schema';
import { mutation } from './functions';
export const getAllFromConversation = query({
args: {

View file

@ -44,6 +44,7 @@ export default defineSchema({
conversations: defineTable({
user_id: v.string(),
title: v.string(),
updated_at: v.optional(v.number()),
}).index('by_user', ['user_id']),
messages: defineTable({
conversation_id: v.string(),

View file

@ -1,4 +1,5 @@
import { query, mutation } from './_generated/server';
import { query } from './_generated/server';
import { mutation } from './functions';
import { v } from 'convex/values';
import { providerValidator } from './schema';
import * as array from '../../utils/array';

View file

@ -1,7 +1,8 @@
import { v } from 'convex/values';
import { Provider } from '../../types';
import { api, internal } from './_generated/api';
import { mutation, query } from './_generated/server';
import { query } from './_generated/server';
import { mutation } from './functions';
import { providerValidator } from './schema';
import { type SessionObj } from './betterAuth';

View file

@ -1,5 +1,6 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { query } from './_generated/server';
import { mutation } from './functions';
import { internal } from './_generated/api';
import { ruleAttachValidator } from './schema';
import { type Doc } from './_generated/dataModel';

View file

@ -16,6 +16,7 @@
import { goto } from '$app/navigation';
import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
import { api } from '$lib/backend/convex/_generated/api.js';
import { type Doc } from '$lib/backend/convex/_generated/dataModel.js';
import { TextareaAutosize } from '$lib/spells/textarea-autosize.svelte.js';
import Tooltip from '$lib/components/ui/tooltip.svelte';
@ -51,6 +52,49 @@
});
const _autosize = new TextareaAutosize();
function groupConversationsByTime(conversations: Doc<'conversations'>[]) {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const groups = {
today: [] as Doc<'conversations'>[],
yesterday: [] as Doc<'conversations'>[],
lastWeek: [] as Doc<'conversations'>[],
lastMonth: [] as Doc<'conversations'>[],
older: [] as Doc<'conversations'>[],
};
conversations.forEach((conversation) => {
const updatedAt = conversation.updated_at ?? 0;
const timeDiff = now - updatedAt;
if (timeDiff < oneDay) {
groups.today.push(conversation);
} else if (timeDiff < 2 * oneDay) {
groups.yesterday.push(conversation);
} else if (timeDiff < sevenDays) {
groups.lastWeek.push(conversation);
} else if (timeDiff < thirtyDays) {
groups.lastMonth.push(conversation);
} else {
groups.older.push(conversation);
}
});
return groups;
}
const groupedConversations = $derived(groupConversationsByTime(conversationsQuery.data ?? []));
const templateConversations = $derived([
{ key: 'today', label: 'Today', conversations: groupedConversations.today },
{ key: 'yesterday', label: 'Yesterday', conversations: groupedConversations.yesterday },
{ key: 'lastWeek', label: 'Last 7 days', conversations: groupedConversations.lastWeek },
{ key: 'lastMonth', label: 'Last 30 days', conversations: groupedConversations.lastMonth },
{ key: 'older', label: 'Older', conversations: groupedConversations.older },
]);
</script>
<svelte:head>
@ -71,17 +115,30 @@
New Chat
</a>
</div>
<div class="relative flex flex-1 flex-col">
<div
class="from-sidebar pointer-events-none absolute top-0 right-0 left-0 z-10 h-4 bg-gradient-to-b to-transparent"
></div>
<div class="flex flex-1 flex-col overflow-y-auto py-2">
{#each conversationsQuery.data ?? [] as conversation (conversation._id)}
<a
href={`/chat/${conversation._id}`}
class="group relative overflow-clip py-0.5 pr-2.5 text-left text-sm"
{#each templateConversations as group, index (group.key)}
{#if group.conversations.length > 0}
<div class="px-2 py-1" class:mt-2={index > 0}>
<h3 class="text-heading text-xs font-medium">{group.label}</h3>
</div>
{#each group.conversations as conversation (conversation._id)}
{@const isActive = page.params.id === conversation._id}
<a href={`/chat/${conversation._id}`} class="group py-0.5 pr-2.5 text-left text-sm">
<div class="relative overflow-clip">
<p
class={[
' rounded-lg py-2 pl-3',
isActive ? 'bg-sidebar-accent' : 'group-hover:bg-sidebar-accent ',
]}
>
<p class="group-hover:bg-sidebar-accent rounded-md py-1.5 pl-3">
{conversation.title}
<span>{conversation.title}</span>
</p>
<div
class=" to-sidebar-accent pointer-events-none absolute inset-y-0 right-0 flex translate-x-full items-center gap-2 rounded-r-lg bg-gradient-to-r from-transparent pr-2 transition group-hover:pointer-events-auto group-hover:translate-0"
class=" to-sidebar-accent pointer-events-none absolute inset-y-0.5 right-0 flex translate-x-full items-center gap-2 rounded-r-lg bg-gradient-to-r from-transparent pr-2 transition group-hover:pointer-events-auto group-hover:translate-0"
>
<Tooltip>
{#snippet trigger(tooltip)}
@ -100,8 +157,15 @@
Delete thread
</Tooltip>
</div>
</div>
</a>
{/each}
{/if}
{/each}
</div>
<div
class="from-sidebar pointer-events-none absolute right-0 bottom-0 left-0 z-10 h-4 bg-gradient-to-t to-transparent"
></div>
</div>
<div class="py-2">
{#if data.session !== null}