refactor: improve AI event handling and conflict display in chat

- AI fetches events on-demand via callbacks for better efficiency
- Add conflict detection with warning display when proposing overlapping events
- Improve event search and display in chat interface
- Load full chat history for display while limiting AI context
This commit is contained in:
2026-02-02 22:44:08 +01:00
parent 387bb2d1ee
commit 1092ff2648
19 changed files with 367 additions and 119 deletions

View File

@@ -5,6 +5,7 @@ import {
CreateEventDTO,
GetMessagesOptions,
UpdateMessageDTO,
ConflictingEvent,
} from "@calchat/shared";
import { ChatRepository } from "../../services/interfaces";
import { Logged } from "../../logging";
@@ -25,12 +26,20 @@ export class MongoChatRepository implements ChatRepository {
return conversation.toJSON() as unknown as Conversation;
}
async getConversationById(
conversationId: string,
): Promise<Conversation | null> {
const conversation = await ConversationModel.findById(conversationId);
return conversation
? (conversation.toJSON() as unknown as Conversation)
: null;
}
// Messages (cursor-based pagination)
async getMessages(
conversationId: string,
options?: GetMessagesOptions,
): Promise<ChatMessage[]> {
const limit = options?.limit ?? 20;
const query: Record<string, unknown> = { conversationId };
// Cursor: load messages before this ID (for "load more" scrolling up)
@@ -39,9 +48,12 @@ export class MongoChatRepository implements ChatRepository {
}
// Fetch newest first, then reverse for chronological order
const docs = await ChatMessageModel.find(query)
.sort({ _id: -1 })
.limit(limit);
// Only apply limit if explicitly specified (no default - load all messages)
let queryBuilder = ChatMessageModel.find(query).sort({ _id: -1 });
if (options?.limit) {
queryBuilder = queryBuilder.limit(options.limit);
}
const docs = await queryBuilder;
return docs.reverse().map((doc) => doc.toJSON() as unknown as ChatMessage);
}
@@ -88,12 +100,28 @@ export class MongoChatRepository implements ChatRepository {
messageId: string,
proposalId: string,
event: CreateEventDTO,
conflictingEvents?: ConflictingEvent[],
): Promise<ChatMessage | null> {
// Always set both fields - use empty array when no conflicts
// (MongoDB has issues combining $set and $unset on positional operator)
const setFields: Record<string, unknown> = {
"proposedChanges.$.event": event,
"proposedChanges.$.conflictingEvents":
conflictingEvents && conflictingEvents.length > 0
? conflictingEvents
: [],
};
const doc = await ChatMessageModel.findOneAndUpdate(
{ _id: messageId, "proposedChanges.id": proposalId },
{ $set: { "proposedChanges.$.event": event } },
{ $set: setFields },
{ new: true },
);
return doc ? (doc.toJSON() as unknown as ChatMessage) : null;
}
async getMessageById(messageId: string): Promise<ChatMessage | null> {
const doc = await ChatMessageModel.findById(messageId);
return doc ? (doc.toJSON() as unknown as ChatMessage) : null;
}
}

View File

@@ -28,6 +28,14 @@ export class MongoEventRepository implements EventRepository {
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
}
async searchByTitle(userId: string, query: string): Promise<CalendarEvent[]> {
const events = await EventModel.find({
userId,
title: { $regex: query, $options: "i" },
}).sort({ startTime: 1 });
return events.map((e) => e.toJSON() as unknown as CalendarEvent);
}
async create(userId: string, data: CreateEventDTO): Promise<CalendarEvent> {
const event = await EventModel.create({ userId, ...data });
// NOTE: Casting required because Mongoose's toJSON() type doesn't reflect our virtual 'id' field

View File

@@ -5,6 +5,7 @@ import {
CreateEventDTO,
UpdateEventDTO,
ProposedEventChange,
ConflictingEvent,
} from "@calchat/shared";
import { IdVirtual } from "./types";
@@ -41,6 +42,15 @@ const UpdatesSchema = new Schema<UpdateEventDTO>(
{ _id: false },
);
const ConflictingEventSchema = new Schema<ConflictingEvent>(
{
title: { type: String, required: true },
startTime: { type: Date, required: true },
endTime: { type: Date, required: true },
},
{ _id: false },
);
const ProposedChangeSchema = new Schema<ProposedEventChange>(
{
id: { type: String, required: true },
@@ -61,6 +71,7 @@ const ProposedChangeSchema = new Schema<ProposedEventChange>(
enum: ["single", "future", "all"],
},
occurrenceDate: { type: String },
conflictingEvents: { type: [ConflictingEventSchema] },
},
{ _id: false },
);