feat(*): first mvp
This commit is contained in:
215
frontend/src/lib/convex/messages.ts
Normal file
215
frontend/src/lib/convex/messages.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { v } from 'convex/values';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
|
||||
export const listByChat = query({
|
||||
args: { chatId: v.id('chats') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('messages'),
|
||||
_creationTime: v.number(),
|
||||
chatId: v.id('chats'),
|
||||
role: v.union(v.literal('user'), v.literal('assistant')),
|
||||
content: v.string(),
|
||||
imageStorageId: v.optional(v.id('_storage')),
|
||||
imageMediaType: v.optional(v.string()),
|
||||
followUpOptions: v.optional(v.array(v.string())),
|
||||
source: v.union(v.literal('telegram'), v.literal('web')),
|
||||
createdAt: v.number(),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db
|
||||
.query('messages')
|
||||
.withIndex('by_chat_id_and_created_at', (q) => q.eq('chatId', args.chatId))
|
||||
.order('asc')
|
||||
.collect();
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
chatId: v.id('chats'),
|
||||
role: v.union(v.literal('user'), v.literal('assistant')),
|
||||
content: v.string(),
|
||||
source: v.union(v.literal('telegram'), v.literal('web')),
|
||||
imageStorageId: v.optional(v.id('_storage')),
|
||||
imageMediaType: v.optional(v.string()),
|
||||
followUpOptions: v.optional(v.array(v.string())),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
},
|
||||
returns: v.id('messages'),
|
||||
handler: async (ctx, args) => {
|
||||
const messageId = await ctx.db.insert('messages', {
|
||||
chatId: args.chatId,
|
||||
role: args.role,
|
||||
content: args.content,
|
||||
source: args.source,
|
||||
imageStorageId: args.imageStorageId,
|
||||
imageMediaType: args.imageMediaType,
|
||||
followUpOptions: args.followUpOptions,
|
||||
createdAt: Date.now(),
|
||||
isStreaming: args.isStreaming
|
||||
});
|
||||
|
||||
if (args.source === 'web' && args.role === 'user') {
|
||||
const chat = await ctx.db.get(args.chatId);
|
||||
if (chat) {
|
||||
await ctx.db.insert('pendingGenerations', {
|
||||
userId: chat.userId,
|
||||
chatId: args.chatId,
|
||||
userMessage: args.content,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messageId;
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
messageId: v.id('messages'),
|
||||
content: v.optional(v.string()),
|
||||
followUpOptions: v.optional(v.array(v.string())),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const updates: {
|
||||
content?: string;
|
||||
followUpOptions?: string[];
|
||||
isStreaming?: boolean;
|
||||
} = {};
|
||||
|
||||
if (args.content !== undefined) {
|
||||
updates.content = args.content;
|
||||
}
|
||||
if (args.followUpOptions !== undefined) {
|
||||
updates.followUpOptions = args.followUpOptions;
|
||||
}
|
||||
if (args.isStreaming !== undefined) {
|
||||
updates.isStreaming = args.isStreaming;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.messageId, updates);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const getHistoryForAI = query({
|
||||
args: { chatId: v.id('chats'), limit: v.optional(v.number()) },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
role: v.union(v.literal('user'), v.literal('assistant')),
|
||||
content: v.string()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const messages = await ctx.db
|
||||
.query('messages')
|
||||
.withIndex('by_chat_id_and_created_at', (q) => q.eq('chatId', args.chatId))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
const limit = args.limit ?? 50;
|
||||
const limited = messages.slice(-limit);
|
||||
|
||||
return limited.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
export const getLastAssistantMessage = query({
|
||||
args: { chatId: v.id('chats') },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
_id: v.id('messages'),
|
||||
_creationTime: v.number(),
|
||||
chatId: v.id('chats'),
|
||||
role: v.union(v.literal('user'), v.literal('assistant')),
|
||||
content: v.string(),
|
||||
imageStorageId: v.optional(v.id('_storage')),
|
||||
imageMediaType: v.optional(v.string()),
|
||||
followUpOptions: v.optional(v.array(v.string())),
|
||||
source: v.union(v.literal('telegram'), v.literal('web')),
|
||||
createdAt: v.number(),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const messages = await ctx.db
|
||||
.query('messages')
|
||||
.withIndex('by_chat_id_and_created_at', (q) => q.eq('chatId', args.chatId))
|
||||
.order('desc')
|
||||
.collect();
|
||||
|
||||
return messages.find((m) => m.role === 'assistant') ?? null;
|
||||
}
|
||||
});
|
||||
|
||||
export const generateUploadUrl = mutation({
|
||||
args: {},
|
||||
returns: v.string(),
|
||||
handler: async (ctx) => {
|
||||
return await ctx.storage.generateUploadUrl();
|
||||
}
|
||||
});
|
||||
|
||||
export const getImageUrls = query({
|
||||
args: { chatId: v.id('chats') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
storageId: v.id('_storage'),
|
||||
mediaType: v.string(),
|
||||
url: v.union(v.string(), v.null())
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const messages = await ctx.db
|
||||
.query('messages')
|
||||
.withIndex('by_chat_id', (q) => q.eq('chatId', args.chatId))
|
||||
.collect();
|
||||
|
||||
const imageMessages = messages.filter((m) => m.imageStorageId && m.imageMediaType);
|
||||
const results = [];
|
||||
|
||||
for (const msg of imageMessages) {
|
||||
if (msg.imageStorageId && msg.imageMediaType) {
|
||||
const url = await ctx.storage.getUrl(msg.imageStorageId);
|
||||
results.push({
|
||||
storageId: msg.imageStorageId,
|
||||
mediaType: msg.imageMediaType,
|
||||
url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
});
|
||||
|
||||
export const createWithImage = internalMutation({
|
||||
args: {
|
||||
chatId: v.id('chats'),
|
||||
content: v.string(),
|
||||
imageStorageId: v.id('_storage'),
|
||||
imageMediaType: v.string()
|
||||
},
|
||||
returns: v.id('messages'),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.insert('messages', {
|
||||
chatId: args.chatId,
|
||||
role: 'user' as const,
|
||||
content: args.content,
|
||||
source: 'telegram' as const,
|
||||
imageStorageId: args.imageStorageId,
|
||||
imageMediaType: args.imageMediaType,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user