267 lines
7.0 KiB
TypeScript
267 lines
7.0 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { mutation, query } from './_generated/server';
|
|
import type { Id } from './_generated/dataModel';
|
|
|
|
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(),
|
|
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) => {
|
|
const messages = await ctx.db
|
|
.query('messages')
|
|
.withIndex('by_chat_id_and_created_at', (q) => q.eq('chatId', args.chatId))
|
|
.order('asc')
|
|
.collect();
|
|
|
|
return messages.map((m) => ({
|
|
_id: m._id,
|
|
_creationTime: m._creationTime,
|
|
chatId: m.chatId,
|
|
role: m.role,
|
|
content: m.content,
|
|
followUpOptions: m.followUpOptions,
|
|
source: m.source,
|
|
createdAt: m.createdAt,
|
|
isStreaming: m.isStreaming
|
|
}));
|
|
}
|
|
});
|
|
|
|
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')),
|
|
imageBase64: v.optional(v.string()),
|
|
imageMediaType: v.optional(v.string()),
|
|
imagesBase64: v.optional(v.array(v.string())),
|
|
imagesMediaTypes: v.optional(v.array(v.string())),
|
|
photoDraftIds: v.optional(v.array(v.id('photoDrafts'))),
|
|
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,
|
|
imageBase64: args.imageBase64,
|
|
imageMediaType: args.imageMediaType,
|
|
imagesBase64: args.imagesBase64,
|
|
imagesMediaTypes: args.imagesMediaTypes,
|
|
followUpOptions: args.followUpOptions,
|
|
createdAt: Date.now(),
|
|
isStreaming: args.isStreaming
|
|
});
|
|
|
|
const drafts: Array<{ base64: string; mediaType: string; id: Id<'photoDrafts'> }> = [];
|
|
if (args.photoDraftIds && args.photoDraftIds.length > 0) {
|
|
for (const draftId of args.photoDraftIds) {
|
|
const draft = await ctx.db.get(draftId);
|
|
if (draft) {
|
|
drafts.push({ base64: draft.base64, mediaType: draft.mediaType, id: draft._id });
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < drafts.length; i++) {
|
|
await ctx.db.insert('messageImages', {
|
|
messageId,
|
|
base64: drafts[i].base64,
|
|
mediaType: drafts[i].mediaType,
|
|
order: i
|
|
});
|
|
}
|
|
|
|
if (args.source === 'web' && args.role === 'user') {
|
|
const chat = await ctx.db.get(args.chatId);
|
|
if (chat) {
|
|
const pendingGenId = await ctx.db.insert('pendingGenerations', {
|
|
userId: chat.userId,
|
|
chatId: args.chatId,
|
|
userMessage: args.content,
|
|
createdAt: Date.now()
|
|
});
|
|
|
|
for (let i = 0; i < drafts.length; i++) {
|
|
await ctx.db.insert('pendingGenerationImages', {
|
|
pendingGenerationId: pendingGenId,
|
|
base64: drafts[i].base64,
|
|
mediaType: drafts[i].mediaType,
|
|
order: i
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const draft of drafts) {
|
|
await ctx.db.delete(draft.id);
|
|
}
|
|
|
|
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 setTelegramMessageId = mutation({
|
|
args: { messageId: v.id('messages'), telegramMessageId: v.number() },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
await ctx.db.patch(args.messageId, { telegramMessageId: args.telegramMessageId });
|
|
return null;
|
|
}
|
|
});
|
|
|
|
export const getTelegramMessageId = query({
|
|
args: { messageId: v.id('messages') },
|
|
returns: v.union(v.number(), v.null()),
|
|
handler: async (ctx, args) => {
|
|
const msg = await ctx.db.get(args.messageId);
|
|
return msg?.telegramMessageId ?? 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(),
|
|
imageBase64: v.optional(v.string()),
|
|
imageMediaType: v.optional(v.string()),
|
|
imagesBase64: v.optional(v.array(v.string())),
|
|
imagesMediaTypes: v.optional(v.array(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 getChatImages = query({
|
|
args: { chatId: v.id('chats') },
|
|
returns: v.array(
|
|
v.object({
|
|
base64: v.string(),
|
|
mediaType: v.string()
|
|
})
|
|
),
|
|
handler: async (ctx, args) => {
|
|
const messages = await ctx.db
|
|
.query('messages')
|
|
.withIndex('by_chat_id', (q) => q.eq('chatId', args.chatId))
|
|
.collect();
|
|
|
|
const images: Array<{ base64: string; mediaType: string }> = [];
|
|
|
|
for (const m of messages) {
|
|
const msgImages = await ctx.db
|
|
.query('messageImages')
|
|
.withIndex('by_message_id', (q) => q.eq('messageId', m._id))
|
|
.collect();
|
|
|
|
for (const img of msgImages.sort((a, b) => a.order - b.order)) {
|
|
images.push({ base64: img.base64, mediaType: img.mediaType });
|
|
}
|
|
|
|
if (m.imagesBase64 && m.imagesMediaTypes) {
|
|
for (let i = 0; i < m.imagesBase64.length; i++) {
|
|
images.push({
|
|
base64: m.imagesBase64[i],
|
|
mediaType: m.imagesMediaTypes[i]
|
|
});
|
|
}
|
|
} else if (m.imageBase64 && m.imageMediaType) {
|
|
images.push({
|
|
base64: m.imageBase64,
|
|
mediaType: m.imageMediaType
|
|
});
|
|
}
|
|
}
|
|
|
|
return images;
|
|
}
|
|
});
|