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

@@ -9,8 +9,8 @@ import {
CreateEventDTO,
UpdateEventDTO,
EventAction,
CreateMessageDTO,
RecurringDeleteMode,
ConflictingEvent,
} from "@calchat/shared";
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
import { EventService } from "./EventService";
@@ -570,7 +570,6 @@ export class ChatService {
responseIndex++;
} else {
// Production mode: use real AI
const events = await this.eventRepo.findByUserId(userId);
const history = await this.chatRepo.getMessages(conversationId, {
limit: 20,
});
@@ -578,8 +577,16 @@ export class ChatService {
response = await this.aiProvider.processMessage(data.content, {
userId,
conversationHistory: history,
existingEvents: events,
currentDate: new Date(),
fetchEventsInRange: async (start, end) => {
return this.eventService.getByDateRange(userId, start, end);
},
searchEvents: async (query) => {
return this.eventRepo.searchByTitle(userId, query);
},
fetchEventById: async (eventId) => {
return this.eventService.getById(eventId, userId);
},
});
}
@@ -713,6 +720,56 @@ export class ChatService {
proposalId: string,
event: CreateEventDTO,
): Promise<ChatMessage | null> {
return this.chatRepo.updateProposalEvent(messageId, proposalId, event);
// Get the message to find the conversation
const message = await this.chatRepo.getMessageById(messageId);
if (!message) {
return null;
}
// Get the conversation to find the userId
const conversation = await this.chatRepo.getConversationById(
message.conversationId,
);
if (!conversation) {
return null;
}
const userId = conversation.userId;
// Get event times
const eventStart = new Date(event.startTime);
const eventEnd = new Date(event.endTime);
// Get day range for conflict checking
const dayStart = new Date(eventStart);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayStart.getDate() + 1);
// Fetch events for the day
const dayEvents = await this.eventService.getByDateRange(
userId,
dayStart,
dayEnd,
);
// Check for time overlaps (use occurrenceStart/End for expanded recurring events)
const conflicts: ConflictingEvent[] = dayEvents
.filter(
(e) =>
new Date(e.occurrenceStart) < eventEnd &&
new Date(e.occurrenceEnd) > eventStart,
)
.map((e) => ({
title: e.title,
startTime: new Date(e.occurrenceStart),
endTime: new Date(e.occurrenceEnd),
}));
return this.chatRepo.updateProposalEvent(
messageId,
proposalId,
event,
conflicts.length > 0 ? conflicts : undefined,
);
}
}

View File

@@ -1,14 +1,21 @@
import {
CalendarEvent,
ChatMessage,
ProposedEventChange,
ExpandedEvent,
CalendarEvent,
} from "@calchat/shared";
export interface AIContext {
userId: string;
conversationHistory: ChatMessage[];
existingEvents: CalendarEvent[];
currentDate: Date;
// Callback to load events from a specific date range
// Returns ExpandedEvent[] with occurrenceStart/occurrenceEnd for recurring events
fetchEventsInRange: (startDate: Date, endDate: Date) => Promise<ExpandedEvent[]>;
// Callback to search events by title
searchEvents: (query: string) => Promise<CalendarEvent[]>;
// Callback to fetch a single event by ID
fetchEventById: (eventId: string) => Promise<CalendarEvent | null>;
}
export interface AIResponse {

View File

@@ -5,11 +5,13 @@ import {
CreateEventDTO,
GetMessagesOptions,
UpdateMessageDTO,
ConflictingEvent,
} from "@calchat/shared";
export interface ChatRepository {
// Conversations
getConversationsByUser(userId: string): Promise<Conversation[]>;
getConversationById(conversationId: string): Promise<Conversation | null>;
createConversation(userId: string): Promise<Conversation>;
// Messages (cursor-based pagination)
@@ -38,5 +40,8 @@ export interface ChatRepository {
messageId: string,
proposalId: string,
event: CreateEventDTO,
conflictingEvents?: ConflictingEvent[],
): Promise<ChatMessage | null>;
getMessageById(messageId: string): Promise<ChatMessage | null>;
}

View File

@@ -8,6 +8,7 @@ export interface EventRepository {
startDate: Date,
endDate: Date,
): Promise<CalendarEvent[]>;
searchByTitle(userId: string, query: string): Promise<CalendarEvent[]>;
create(userId: string, data: CreateEventDTO): Promise<CalendarEvent>;
update(id: string, data: UpdateEventDTO): Promise<CalendarEvent | null>;
delete(id: string): Promise<boolean>;