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') { parts = [{ type: 'text', text: msg.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) ) ); }