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:
2026-01-09 16:21:01 +01:00
parent d86b18173f
commit c897b6d680
11 changed files with 245 additions and 45 deletions

View File

@@ -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" });
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -80,6 +80,10 @@ const ChatMessageSchema = new Schema<
proposedChange: {
type: ProposedChangeSchema,
},
respondedAction: {
type: String,
enum: ["confirm", "reject"],
},
},
{
timestamps: true,

View File

@@ -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);
}
}

View File

@@ -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>;
}