Spaces:
Running
Running
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<DBMessage>; | |
}) { | |
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 = /<think>([\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<Message> | |
): Array<UIMessage> { | |
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<ChatWithMessages | null> { | |
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))); | |
} | |