feat: implement chat persistence with MongoDB
- Add full chat persistence to database (conversations and messages) - Implement MongoChatRepository with cursor-based pagination - Add getConversations/getConversation endpoints in ChatController - Save user and assistant messages in ChatService.processMessage() - Track respondedAction (confirm/reject) on proposed event messages - Load existing messages on chat screen mount - Add addMessages() bulk action and chatMessageToMessageData() helper to ChatStore - Add RespondedAction type and UpdateMessageDTO to shared types
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
EventAction,
|
||||
GetMessagesOptions,
|
||||
} from "@caldav/shared";
|
||||
import { ChatService } from "../services";
|
||||
import { AuthenticatedRequest } from "../middleware";
|
||||
@@ -66,13 +67,43 @@ export class ChatController {
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const conversations = await this.chatService.getConversations(userId);
|
||||
res.json(conversations);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to get conversations" });
|
||||
}
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const { before, limit } = req.query as {
|
||||
before?: string;
|
||||
limit?: string;
|
||||
};
|
||||
|
||||
const options: GetMessagesOptions = {};
|
||||
if (before) options.before = before;
|
||||
if (limit) options.limit = parseInt(limit, 10);
|
||||
|
||||
const messages = await this.chatService.getConversation(
|
||||
userId,
|
||||
id,
|
||||
options,
|
||||
);
|
||||
res.json(messages);
|
||||
} catch (error) {
|
||||
if ((error as Error).message === "Conversation not found") {
|
||||
res.status(404).json({ error: "Conversation not found" });
|
||||
} else {
|
||||
res.status(500).json({ error: "Failed to get conversation" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
UpdateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
import { ChatRepository } from "../../services/interfaces";
|
||||
import { ChatMessageModel, ConversationModel } from "./models";
|
||||
@@ -10,11 +11,15 @@ import { ChatMessageModel, ConversationModel } from "./models";
|
||||
export class MongoChatRepository implements ChatRepository {
|
||||
// Conversations
|
||||
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
||||
throw new Error("Not implemented");
|
||||
const conversations = await ConversationModel.find({ userId });
|
||||
return conversations.map((c) => c.toJSON() as unknown as Conversation);
|
||||
}
|
||||
|
||||
async createConversation(userId: string): Promise<Conversation> {
|
||||
throw new Error("Not implemented");
|
||||
const conversation = await ConversationModel.create({
|
||||
userId,
|
||||
});
|
||||
return conversation.toJSON() as unknown as Conversation;
|
||||
}
|
||||
|
||||
// Messages (cursor-based pagination)
|
||||
@@ -22,13 +27,44 @@ export class MongoChatRepository implements ChatRepository {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
const limit = options?.limit ?? 20;
|
||||
const query: Record<string, unknown> = { conversationId };
|
||||
|
||||
// Cursor: load messages before this ID (for "load more" scrolling up)
|
||||
if (options?.before) {
|
||||
query._id = { $lt: options.before };
|
||||
}
|
||||
|
||||
// Fetch newest first, then reverse for chronological order
|
||||
const docs = await ChatMessageModel.find(query)
|
||||
.sort({ _id: -1 })
|
||||
.limit(limit);
|
||||
|
||||
return docs.reverse().map((doc) => doc.toJSON() as unknown as ChatMessage);
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage> {
|
||||
throw new Error("Not implemented");
|
||||
const repoMessage = await ChatMessageModel.create({
|
||||
conversationId: conversationId,
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
proposedChange: message.proposedChange,
|
||||
});
|
||||
return repoMessage.toJSON() as unknown as ChatMessage;
|
||||
}
|
||||
|
||||
async updateMessage(
|
||||
messageId: string,
|
||||
updates: UpdateMessageDTO,
|
||||
): Promise<ChatMessage | null> {
|
||||
const doc = await ChatMessageModel.findByIdAndUpdate(
|
||||
messageId,
|
||||
{ $set: updates },
|
||||
{ new: true },
|
||||
);
|
||||
return doc ? (doc.toJSON() as unknown as ChatMessage) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ const ChatMessageSchema = new Schema<
|
||||
proposedChange: {
|
||||
type: ProposedChangeSchema,
|
||||
},
|
||||
respondedAction: {
|
||||
type: String,
|
||||
enum: ["confirm", "reject"],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
EventAction,
|
||||
CreateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
|
||||
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
||||
@@ -252,6 +253,18 @@ export class ChatService {
|
||||
userId: string,
|
||||
data: SendMessageDTO,
|
||||
): Promise<ChatResponse> {
|
||||
let conversationId = data.conversationId;
|
||||
if (!conversationId) {
|
||||
const conversation = await this.chatRepo.createConversation(userId);
|
||||
conversationId = conversation.id;
|
||||
}
|
||||
|
||||
// Save user message
|
||||
await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "user",
|
||||
content: data.content,
|
||||
});
|
||||
|
||||
const response = await getTestResponse(
|
||||
responseIndex,
|
||||
this.eventRepo,
|
||||
@@ -259,15 +272,14 @@ export class ChatService {
|
||||
);
|
||||
responseIndex++;
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId: data.conversationId || "temp-conv-id",
|
||||
// Save and then return assistant response
|
||||
const answerMessage = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content: response.content,
|
||||
proposedChange: response.proposedChange,
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId: message.conversationId };
|
||||
return { message: answerMessage, conversationId: conversationId };
|
||||
}
|
||||
|
||||
async confirmEvent(
|
||||
@@ -279,6 +291,10 @@ export class ChatService {
|
||||
eventId?: string,
|
||||
updates?: UpdateEventDTO,
|
||||
): Promise<ChatResponse> {
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, { respondedAction: "confirm" });
|
||||
|
||||
// Perform the actual event operation
|
||||
let content: string;
|
||||
|
||||
if (action === "create" && event) {
|
||||
@@ -298,12 +314,12 @@ export class ChatService {
|
||||
content = "Ungültige Aktion.";
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
// Save response message to DB
|
||||
const message = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId };
|
||||
}
|
||||
|
||||
@@ -312,17 +328,34 @@ export class ChatService {
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
): Promise<ChatResponse> {
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, { respondedAction: "reject" });
|
||||
|
||||
// Save response message to DB
|
||||
const message = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content: "Der Vorschlag wurde abgelehnt.",
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId };
|
||||
}
|
||||
|
||||
async getConversations(userId: string): Promise<ConversationSummary[]> {
|
||||
throw new Error("Not implemented");
|
||||
const conversations = await this.chatRepo.getConversationsByUser(userId);
|
||||
|
||||
// For each conversation, get the last message
|
||||
const summaries: ConversationSummary[] = await Promise.all(
|
||||
conversations.map(async (conv) => {
|
||||
const messages = await this.chatRepo.getMessages(conv.id, { limit: 1 });
|
||||
return {
|
||||
id: conv.id,
|
||||
lastMessage: messages[0],
|
||||
createdAt: conv.createdAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
@@ -330,6 +363,14 @@ export class ChatService {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
// Verify conversation belongs to user
|
||||
const conversations = await this.chatRepo.getConversationsByUser(userId);
|
||||
const conversation = conversations.find((c) => c.id === conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error("Conversation not found");
|
||||
}
|
||||
|
||||
return this.chatRepo.getMessages(conversationId, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
UpdateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
|
||||
export interface ChatRepository {
|
||||
@@ -15,8 +16,14 @@ export interface ChatRepository {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]>;
|
||||
|
||||
createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage>;
|
||||
|
||||
updateMessage(
|
||||
messageId: string,
|
||||
updates: UpdateMessageDTO,
|
||||
): Promise<ChatMessage | null>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user