import { db } from "./db"; import { chats, messages, type Chat, type Message, MessageRole, type MessagePart, type DBMessage, } from "./db/schema"; import { eq, desc, and } from "drizzle-orm"; import { nanoid } from "nanoid"; import { generateTitle } from "@/app/actions"; type AIMessage = { role: string; content: string | any[]; id?: string; parts?: MessagePart[]; }; type UIMessage = { id: string; role: string; content: string; parts: MessagePart[]; createdAt?: Date; }; type SaveChatParams = { id?: string; userId: string; messages?: any[]; title?: string; }; type ChatWithMessages = Chat & { messages: Message[]; }; export async function saveMessages({ messages: dbMessages, }: { messages: Array; }) { try { if (dbMessages.length > 0) { const chatId = dbMessages[0].chatId; // First delete any existing messages for this chat await db.delete(messages).where(eq(messages.chatId, chatId)); // Then insert the new messages return await db.insert(messages).values(dbMessages); } return null; } catch (error) { console.error("Failed to save messages in database", error); throw error; } } // Function to convert AI messages to DB format export function convertToDBMessages( aiMessages: AIMessage[], chatId: string ): DBMessage[] { return aiMessages.map((msg) => { // Use existing id or generate a new one const messageId = msg.id || nanoid(); // If msg has parts, use them directly if (msg.parts) { return { id: messageId, chatId, role: msg.role, parts: msg.parts, createdAt: new Date(), }; } // Otherwise, convert content to parts let parts: MessagePart[]; if (typeof msg.content === "string") { const thinkRegex = /([\s\S]*?)<\/think>/g; const content = msg.content; parts = []; let lastIndex = 0; let match; while ((match = thinkRegex.exec(content)) !== null) { // Add text part before the think tag if (match.index > lastIndex) { parts.push({ type: "text", text: content.substring(lastIndex, match.index), }); } // Add the thinking part parts.push({ type: "thinking", thinking: match[1], details: [{ type: "text", text: match[1] }], }); lastIndex = thinkRegex.lastIndex; } // Add any remaining text part if (lastIndex < content.length) { parts.push({ type: "text", text: content.substring(lastIndex) }); } // If no thinking tags were found, treat the whole content as a single text part if (parts.length === 0) { parts.push({ type: "text", text: content }); } } else if (Array.isArray(msg.content)) { if ( msg.content.every((item) => typeof item === "object" && item !== null) ) { // Content is already in parts-like format parts = msg.content as MessagePart[]; } else { // Content is an array but not in parts format parts = [{ type: "text", text: JSON.stringify(msg.content) }]; } } else { // Default case parts = [{ type: "text", text: String(msg.content) }]; } return { id: messageId, chatId, role: msg.role, parts, createdAt: new Date(), }; }); } // Convert DB messages to UI format export function convertToUIMessages( dbMessages: Array ): Array { return dbMessages.map((message) => ({ id: message.id, parts: message.parts as MessagePart[], role: message.role as string, content: getTextContent(message), // For backward compatibility createdAt: message.createdAt, })); } export async function saveChat({ id, userId, messages: aiMessages, title, }: SaveChatParams) { // Generate a new ID if one wasn't provided const chatId = id || nanoid(); // Check if title is provided, if not generate one let chatTitle = title; // Generate title if messages are provided and no title is specified if (aiMessages && aiMessages.length > 0) { const hasEnoughMessages = aiMessages.length >= 2 && aiMessages.some((m) => m.role === "user") && aiMessages.some((m) => m.role === "assistant"); if (!chatTitle || chatTitle === "New Chat" || chatTitle === undefined) { if (hasEnoughMessages) { try { // Use AI to generate a meaningful title based on conversation chatTitle = await generateTitle(aiMessages); } catch (error) { console.error("Error generating title:", error); // Fallback to basic title extraction if AI title generation fails const firstUserMessage = aiMessages.find((m) => m.role === "user"); if (firstUserMessage) { // Check for parts first (new format) if ( firstUserMessage.parts && Array.isArray(firstUserMessage.parts) ) { const textParts = firstUserMessage.parts.filter( (p: MessagePart) => p.type === "text" && p.text ); if (textParts.length > 0) { chatTitle = textParts[0].text?.slice(0, 50) || "New Chat"; if ((textParts[0].text?.length || 0) > 50) { chatTitle += "..."; } } else { chatTitle = "New Chat"; } } // Fallback to content (old format) else if (typeof firstUserMessage.content === "string") { chatTitle = firstUserMessage.content.slice(0, 50); if (firstUserMessage.content.length > 50) { chatTitle += "..."; } } else { chatTitle = "New Chat"; } } else { chatTitle = "New Chat"; } } } else { // Not enough messages for AI title, use first message const firstUserMessage = aiMessages.find((m) => m.role === "user"); if (firstUserMessage) { // Check for parts first (new format) if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) { const textParts = firstUserMessage.parts.filter( (p: MessagePart) => p.type === "text" && p.text ); if (textParts.length > 0) { chatTitle = textParts[0].text?.slice(0, 50) || "New Chat"; if ((textParts[0].text?.length || 0) > 50) { chatTitle += "..."; } } else { chatTitle = "New Chat"; } } // Fallback to content (old format) else if (typeof firstUserMessage.content === "string") { chatTitle = firstUserMessage.content.slice(0, 50); if (firstUserMessage.content.length > 50) { chatTitle += "..."; } } else { chatTitle = "New Chat"; } } else { chatTitle = "New Chat"; } } } } else { chatTitle = chatTitle || "New Chat"; } // Check if chat already exists const existingChat = await db.query.chats.findFirst({ where: and(eq(chats.id, chatId), eq(chats.userId, userId)), }); if (existingChat) { // Update existing chat await db .update(chats) .set({ title: chatTitle, updatedAt: new Date(), }) .where(and(eq(chats.id, chatId), eq(chats.userId, userId))); } else { // Create new chat await db.insert(chats).values({ id: chatId, userId, title: chatTitle, createdAt: new Date(), updatedAt: new Date(), }); } return { id: chatId }; } // Helper to get just the text content for display export function getTextContent(message: Message): string { try { const parts = message.parts as MessagePart[]; return parts .filter((part) => part.type === "text" && part.text) .map((part) => part.text) .join("\n"); } catch (e) { // If parsing fails, return empty string return ""; } } export async function getChats(userId: string) { return await db.query.chats.findMany({ where: eq(chats.userId, userId), orderBy: [desc(chats.updatedAt)], }); } export async function getChatById( id: string, userId: string ): Promise { const chat = await db.query.chats.findFirst({ where: and(eq(chats.id, id), eq(chats.userId, userId)), }); if (!chat) return null; const chatMessages = await db.query.messages.findMany({ where: eq(messages.chatId, id), orderBy: [messages.createdAt], }); return { ...chat, messages: chatMessages, }; } export async function deleteChat(id: string, userId: string) { await db.delete(chats).where(and(eq(chats.id, id), eq(chats.userId, userId))); }