import * as z from 'zod'; import { messages, messageParts, attachments, topics } from '~~/drizzle/schema'; import { db } from '~~/server/lib/db'; import { nanoid } from 'nanoid'; import { eq, and, inArray } from 'drizzle-orm'; export default defineEventHandler(async (event) => { await protectRoute(event); const userId = event.context.user!.id; const topicId = getRouterParam(event, 'topicId')!; const result = await readValidatedBody(event, (body) => z .object({ messageId: z.string(), }) .safeParse(body), ); if (!result.success) { throw createError({ statusCode: 400, message: result.error.issues[0]!.message, }); } const { messageId } = result.data; const topic = await db.select().from(topics).where(and( eq(topics.id, topicId), eq(topics.userId, userId), )).limit(1).then(rows => rows[0]); if (!topic) { throw createError({ statusCode: 404, statusMessage: 'Not Found', message: 'Topic not found', }); } const topicMessages = await db.select().from(messages) .where(eq(messages.topicId, topicId)) .orderBy(messages.createdAt); const forkMessage = topicMessages.find(m => m.id === messageId); if (!forkMessage) { throw createError({ statusCode: 404, statusMessage: 'Not Found', message: 'Message not found in topic', }); } const childrenMap = new Map(); for (const msg of topicMessages) { if (msg.parentMessageId) { const siblings = childrenMap.get(msg.parentMessageId) || []; siblings.push(msg); childrenMap.set(msg.parentMessageId, siblings); } } const messageIdsToCopy = new Set(); const messageMap = new Map(topicMessages.map(m => [m.id, m])); const addMessageAndSiblings = (msgId: string) => { const msg = messageMap.get(msgId); if (!msg) return; if (msg.parentMessageId) { const siblings = childrenMap.get(msg.parentMessageId) || []; for (const sibling of siblings) { messageIdsToCopy.add(sibling.id); } } else { messageIdsToCopy.add(msgId); } }; let current: typeof forkMessage | undefined = forkMessage; while (current) { addMessageAndSiblings(current.id); current = current.parentMessageId ? messageMap.get(current.parentMessageId) : undefined; } const forkIndex = topicMessages.findIndex(m => m.id === messageId); for (let i = 0; i <= forkIndex; i++) { const msg = topicMessages[i]!; if (!msg.parentMessageId) { messageIdsToCopy.add(msg.id); const children = childrenMap.get(msg.id) || []; for (const child of children) { messageIdsToCopy.add(child.id); } } } const addDescendants = (msgId: string) => { const children = childrenMap.get(msgId) || []; for (const child of children) { if (!messageIdsToCopy.has(child.id)) { messageIdsToCopy.add(child.id); addDescendants(child.id); } } }; const initialIds = Array.from(messageIdsToCopy); for (const id of initialIds) { addDescendants(id); } const messagesToCopy = topicMessages .filter(m => messageIdsToCopy.has(m.id)) .sort((a, b) => { if (!a.parentMessageId && b.parentMessageId) return -1; if (a.parentMessageId && !b.parentMessageId) return 1; return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); }); const newTopicId = nanoid(); const newTopicName = `${topic.name} (fork)`; await db.insert(topics).values({ id: newTopicId, userId, name: newTopicName, agentId: topic.agentId, createdAt: new Date(), }); const messageIdMap = new Map(); for (const msg of messagesToCopy) { const newMessageId = nanoid(); messageIdMap.set(msg.id, newMessageId); await db.insert(messages).values({ id: newMessageId, userId, topicId: newTopicId, parentMessageId: msg.parentMessageId ? (messageIdMap.get(msg.parentMessageId) ?? null) : null, generationId: null, role: msg.role, content: msg.content, activeChildId: null, deleted: msg.deleted, createdAt: msg.createdAt, updatedAt: new Date(), }); } for (const msg of messagesToCopy) { if (msg.activeChildId) { const newMessageId = messageIdMap.get(msg.id); const newActiveChildId = messageIdMap.get(msg.activeChildId); if (newMessageId && newActiveChildId) { await db.update(messages) .set({ activeChildId: newActiveChildId }) .where(eq(messages.id, newMessageId)); } } } const originalMessageIds = Array.from(messageIdMap.keys()); if (originalMessageIds.length > 0) { const parts = await db.select().from(messageParts) .where(inArray(messageParts.messageId, originalMessageIds)); for (const part of parts) { const newMessageId = messageIdMap.get(part.messageId); if (!newMessageId) continue; await db.insert(messageParts).values({ userId, topicId: newTopicId, messageId: newMessageId, type: part.type, content: part.content, providerOptions: part.providerOptions, finished: part.finished, createdAt: part.createdAt, lastUpdatedAt: new Date(), }); } const attachmentsResult = await db.select().from(attachments) .where(inArray(attachments.messageId, originalMessageIds)); for (const attachment of attachmentsResult) { const newMessageId = messageIdMap.get(attachment.messageId); if (!newMessageId) continue; await db.insert(attachments).values({ userId, topicId: newTopicId, messageId: newMessageId, fileId: attachment.fileId, createdAt: new Date(), }); } } return { ok: true, topicId: newTopicId, name: newTopicName, }; });