scira-chat / lib /chat-store.ts
mukaddamzaid's picture
init commit
5012205
raw
history blame
7.97 kB
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') {
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<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)
)
);
}