From 3abd8b5502b24e57304d2d6a6eec184672a3613c Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:49:33 +0100 Subject: [PATCH] trigger & grouping --- src/app.css | 3 + src/lib/backend/convex/chat.ts | 3 +- src/lib/backend/convex/conversations.ts | 12 +- src/lib/backend/convex/functions.ts | 16 ++- src/lib/backend/convex/messages.ts | 5 +- src/lib/backend/convex/schema.ts | 1 + src/lib/backend/convex/user_enabled_models.ts | 3 +- src/lib/backend/convex/user_keys.ts | 3 +- src/lib/backend/convex/user_rules.ts | 3 +- src/routes/chat/+layout.svelte | 126 +++++++++++++----- 10 files changed, 135 insertions(+), 40 deletions(-) diff --git a/src/app.css b/src/app.css index aa87a5b..c0af892 100644 --- a/src/app.css +++ b/src/app.css @@ -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); diff --git a/src/lib/backend/convex/chat.ts b/src/lib/backend/convex/chat.ts index 92cb5dd..1a361a5 100644 --- a/src/lib/backend/convex/chat.ts +++ b/src/lib/backend/convex/chat.ts @@ -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: { diff --git a/src/lib/backend/convex/conversations.ts b/src/lib/backend/convex/conversations.ts index 68f65ad..526ab7c 100644 --- a/src/lib/backend/convex/conversations.ts +++ b/src/lib/backend/convex/conversations.ts @@ -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, { diff --git a/src/lib/backend/convex/functions.ts b/src/lib/backend/convex/functions.ts index 989b667..d9d3c86 100644 --- a/src/lib/backend/convex/functions.ts +++ b/src/lib/backend/convex/functions.ts @@ -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(); @@ -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` diff --git a/src/lib/backend/convex/messages.ts b/src/lib/backend/convex/messages.ts index 102b362..68e7938 100644 --- a/src/lib/backend/convex/messages.ts +++ b/src/lib/backend/convex/messages.ts @@ -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: { diff --git a/src/lib/backend/convex/schema.ts b/src/lib/backend/convex/schema.ts index 29d5bec..936bb47 100644 --- a/src/lib/backend/convex/schema.ts +++ b/src/lib/backend/convex/schema.ts @@ -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(), diff --git a/src/lib/backend/convex/user_enabled_models.ts b/src/lib/backend/convex/user_enabled_models.ts index d5ca9f9..44298c3 100644 --- a/src/lib/backend/convex/user_enabled_models.ts +++ b/src/lib/backend/convex/user_enabled_models.ts @@ -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'; diff --git a/src/lib/backend/convex/user_keys.ts b/src/lib/backend/convex/user_keys.ts index 4b443a4..ebcdada 100644 --- a/src/lib/backend/convex/user_keys.ts +++ b/src/lib/backend/convex/user_keys.ts @@ -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'; diff --git a/src/lib/backend/convex/user_rules.ts b/src/lib/backend/convex/user_rules.ts index 07778eb..41974c0 100644 --- a/src/lib/backend/convex/user_rules.ts +++ b/src/lib/backend/convex/user_rules.ts @@ -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'; diff --git a/src/routes/chat/+layout.svelte b/src/routes/chat/+layout.svelte index 9940173..e77cb2c 100644 --- a/src/routes/chat/+layout.svelte +++ b/src/routes/chat/+layout.svelte @@ -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 }, + ]); @@ -71,37 +115,57 @@ New Chat -
- {#each conversationsQuery.data ?? [] as conversation (conversation._id)} - -

- {conversation.title} -

-
- - {#snippet trigger(tooltip)} - - {/snippet} - Pin thread - - - {#snippet trigger(tooltip)} - - {/snippet} - Delete thread - -
-
- {/each} +
+
+
+ {#each templateConversations as group, index (group.key)} + {#if group.conversations.length > 0} +
0}> +

{group.label}

+
+ {#each group.conversations as conversation (conversation._id)} + {@const isActive = page.params.id === conversation._id} + +
+

+ {conversation.title} +

+
+ + {#snippet trigger(tooltip)} + + {/snippet} + Pin thread + + + {#snippet trigger(tooltip)} + + {/snippet} + Delete thread + +
+
+
+ {/each} + {/if} + {/each} +
+
{#if data.session !== null}