import { ChatMessage, ChatResponse, SendMessageDTO, ConversationSummary, GetMessagesOptions, ProposedEventChange, getDay, CreateEventDTO, UpdateEventDTO, EventAction, CreateMessageDTO, } from "@calchat/shared"; import { ChatRepository, EventRepository, AIProvider } from "./interfaces"; import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters"; type TestResponse = { content: string; proposedChanges?: ProposedEventChange[] }; // Test response index (cycles through responses) let responseIndex = 0; // Static test responses (event proposals) const staticResponses: TestResponse[] = [ // {{{ // === MULTI-EVENT TEST RESPONSES === // Response 0: 3 Meetings an verschiedenen Tagen { content: "Alles klar! Ich erstelle dir 3 Team-Meetings für diese Woche:", proposedChanges: [ { id: "multi-1-a", action: "create", event: { title: "Team-Meeting Montag", startTime: getDay("Monday", 1, 10, 0), endTime: getDay("Monday", 1, 11, 0), description: "Wöchentliches Standup", }, }, { id: "multi-1-b", action: "create", event: { title: "Team-Meeting Mittwoch", startTime: getDay("Wednesday", 1, 10, 0), endTime: getDay("Wednesday", 1, 11, 0), description: "Sprint Planning", }, }, { id: "multi-1-c", action: "create", event: { title: "Team-Meeting Freitag", startTime: getDay("Friday", 1, 10, 0), endTime: getDay("Friday", 1, 11, 0), description: "Retrospektive", }, }, ], }, // Response 1: 5 Termine für einen Projekttag { content: "Ich habe deinen kompletten Projekttag am Dienstag geplant:", proposedChanges: [ { id: "multi-2-a", action: "create", event: { title: "Kickoff-Meeting", startTime: getDay("Tuesday", 1, 9, 0), endTime: getDay("Tuesday", 1, 10, 0), description: "Projektstart mit dem Team", }, }, { id: "multi-2-b", action: "create", event: { title: "Design Review", startTime: getDay("Tuesday", 1, 10, 30), endTime: getDay("Tuesday", 1, 11, 30), description: "UI/UX Besprechung", }, }, { id: "multi-2-c", action: "create", event: { title: "Mittagspause", startTime: getDay("Tuesday", 1, 12, 0), endTime: getDay("Tuesday", 1, 13, 0), description: "Team-Lunch", }, }, { id: "multi-2-d", action: "create", event: { title: "Tech Review", startTime: getDay("Tuesday", 1, 14, 0), endTime: getDay("Tuesday", 1, 15, 30), description: "Architektur-Diskussion", }, }, { id: "multi-2-e", action: "create", event: { title: "Wrap-up", startTime: getDay("Tuesday", 1, 16, 0), endTime: getDay("Tuesday", 1, 16, 30), description: "Zusammenfassung und nächste Schritte", }, }, ], }, // Response 2: 2 wiederkehrende Termine { content: "Ich erstelle dir zwei wiederkehrende Fitness-Termine:", proposedChanges: [ { id: "multi-3-a", action: "create", event: { title: "Yoga", startTime: getDay("Monday", 1, 7, 0), endTime: getDay("Monday", 1, 8, 0), description: "Morgen-Yoga", isRecurring: true, recurrenceRule: "FREQ=WEEKLY;BYDAY=MO,WE,FR", }, }, { id: "multi-3-b", action: "create", event: { title: "Laufen", startTime: getDay("Tuesday", 1, 18, 0), endTime: getDay("Tuesday", 1, 19, 0), description: "Abendlauf im Park", isRecurring: true, recurrenceRule: "FREQ=WEEKLY;BYDAY=TU,TH", }, }, ], }, // === ORIGINAL RESPONSES === // Response 3: Help response (text only) { content: "Ich bin dein Kalender-Assistent! Du kannst mir einfach sagen, welche Termine du erstellen, ändern oder löschen möchtest. Zum Beispiel:\n\n" + '• "Erstelle einen Termin für morgen um 15 Uhr"\n' + '• "Was habe ich nächste Woche vor?"\n' + '• "Verschiebe das Meeting auf Donnerstag"\n\n' + "Wie kann ich dir helfen?", }, // Response 1: Meeting mit Jens - next Friday 14:00 { content: "Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:", proposedChanges: [ { id: "test-1", action: "create", event: { title: "Meeting mit Jens", startTime: getDay("Friday", 1, 14, 0), endTime: getDay("Friday", 1, 15, 0), description: "Arbeitstreffen", }, }, ], }, // Response 2: Recurring event - every Saturday 10:00 { content: "Verstanden! Ich erstelle einen wiederkehrenden Termin: Jeden Samstag um 10:00 Uhr Badezimmer putzen:", proposedChanges: [ { id: "test-2", action: "create", event: { title: "Badezimmer putzen", startTime: getDay("Saturday", 1, 10, 0), endTime: getDay("Saturday", 1, 11, 0), isRecurring: true, recurrenceRule: "FREQ=WEEKLY;BYDAY=SA", }, }, ], }, // Response 3: 2-week overview (DYNAMIC - placeholder) { content: "" }, // Response 4: Delete "Meeting mit Jens" (DYNAMIC - placeholder) { content: "" }, // Response 5: Doctor appointment with description { content: "Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!", proposedChanges: [ { id: "test-5", action: "create", event: { title: "Arzttermin Dr. Müller", startTime: getDay("Wednesday", 1, 9, 30), endTime: getDay("Wednesday", 1, 10, 30), description: "Routineuntersuchung - Versichertenkarte nicht vergessen", }, }, ], }, // Response 6: Birthday - yearly recurring { content: "Geburtstage vergisst man leicht - aber nicht mit mir! Ich habe Mamas Geburtstag eingetragen:", proposedChanges: [ { id: "test-6", action: "create", event: { title: "Mamas Geburtstag", startTime: getDay("Thursday", 2, 0, 0), endTime: getDay("Thursday", 2, 23, 59), isRecurring: true, recurrenceRule: "FREQ=YEARLY", }, }, ], }, // Response 7: Gym - recurring for 2 months (8 weeks) { content: "Perfekt! Ich habe dein Probetraining eingetragen - jeden Dienstag für die nächsten 2 Monate:", proposedChanges: [ { id: "test-7", action: "create", event: { title: "Fitnessstudio Probetraining", startTime: getDay("Tuesday", 1, 18, 0), endTime: getDay("Tuesday", 1, 19, 30), isRecurring: true, recurrenceRule: "FREQ=WEEKLY;BYDAY=TU;COUNT=8", }, }, ], }, // Response 8: 1-week overview (DYNAMIC - placeholder) { content: "" }, // Response 9: Phone call - short appointment (Wednesday, so +2 days = Friday) { content: "Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:", proposedChanges: [ { id: "test-9", action: "create", event: { title: "Telefonat mit Mama", startTime: getDay("Wednesday", 1, 11, 0), endTime: getDay("Wednesday", 1, 11, 30), }, }, ], }, // Response 10: Update "Telefonat mit Mama" +3 days (DYNAMIC - placeholder) { content: "" }, // Response 11: Birthday party - evening event { content: "Super! Die Geburtstagsfeier ist eingetragen. Viel Spaß!", proposedChanges: [ { id: "test-11", action: "create", event: { title: "Geburtstagsfeier Lisa", startTime: getDay("Saturday", 2, 19, 0), endTime: getDay("Saturday", 2, 23, 0), description: "Geschenk: Buch über Fotografie", }, }, ], }, // Response 12: Language course - limited to 8 weeks (Thu + Sat) { content: "Dein Spanischkurs ist eingetragen! Er läuft jeden Donnerstag und Samstag für die nächsten 8 Wochen:", proposedChanges: [ { id: "test-12", action: "create", event: { title: "Spanischkurs VHS", startTime: getDay("Thursday", 1, 19, 0), endTime: getDay("Thursday", 1, 20, 30), isRecurring: true, recurrenceRule: "FREQ=WEEKLY;BYDAY=TH,SA;COUNT=8", }, }, ], }, // Response 13: Monthly overview (DYNAMIC - placeholder) { content: "" }, // }}} ]; async function getTestResponse( index: number, eventRepo: EventRepository, userId: string, ): Promise { const responseIdx = index % staticResponses.length; // Dynamic responses: fetch events from DB and format if (responseIdx === 3) { return { content: await getWeeksOverview(eventRepo, userId, 2) }; } if (responseIdx === 4) { // Delete "Meeting mit Jens" const events = await eventRepo.findByUserId(userId); const jensEvent = events.find((e) => e.title === "Meeting mit Jens"); if (jensEvent) { return { content: "Soll ich diesen Termin wirklich löschen?", proposedChanges: [ { id: "test-4", action: "delete", eventId: jensEvent.id, event: { title: jensEvent.title, startTime: jensEvent.startTime, endTime: jensEvent.endTime, description: jensEvent.description, isRecurring: jensEvent.isRecurring, }, }, ], }; } return { content: "Ich konnte keinen Termin 'Meeting mit Jens' finden." }; } if (responseIdx === 8) { return { content: await getWeeksOverview(eventRepo, userId, 1) }; } if (responseIdx === 10) { // Update "Telefonat mit Mama" +3 days and change time to 13:00 const events = await eventRepo.findByUserId(userId); const mamaEvent = events.find((e) => e.title === "Telefonat mit Mama"); if (mamaEvent) { const newStart = new Date(mamaEvent.startTime); newStart.setDate(newStart.getDate() + 3); newStart.setHours(13, 0, 0, 0); const newEnd = new Date(newStart); newEnd.setMinutes(30); return { content: "Alles klar, ich verschiebe das Telefonat mit Mama auf Samstag um 13:00 Uhr:", proposedChanges: [ { id: "test-10", action: "update", eventId: mamaEvent.id, updates: { startTime: newStart, endTime: newEnd }, // Include event with new times for display event: { title: mamaEvent.title, startTime: newStart, endTime: newEnd, description: mamaEvent.description, }, }, ], }; } return { content: "Ich konnte keinen Termin 'Telefonat mit Mama' finden." }; } if (responseIdx === 13) { const now = new Date(); return { content: await getMonthOverview( eventRepo, userId, now.getFullYear(), now.getMonth(), ), }; } return staticResponses[responseIdx]; } export class ChatService { constructor( private chatRepo: ChatRepository, private eventRepo: EventRepository, private aiProvider: AIProvider, ) {} async processMessage( userId: string, data: SendMessageDTO, ): Promise { 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, }); let response: TestResponse; if (process.env.USE_TEST_RESPONSES === "true") { // Test mode: use static responses response = await getTestResponse(responseIndex, this.eventRepo, userId); responseIndex++; } else { // Production mode: use real AI const events = await this.eventRepo.findByUserId(userId); const history = await this.chatRepo.getMessages(conversationId, { limit: 20, }); response = await this.aiProvider.processMessage(data.content, { userId, conversationHistory: history, existingEvents: events, currentDate: new Date(), }); } // Save and then return assistant response const answerMessage = await this.chatRepo.createMessage(conversationId, { sender: "assistant", content: response.content, proposedChanges: response.proposedChanges, }); return { message: answerMessage, conversationId: conversationId }; } async confirmEvent( userId: string, conversationId: string, messageId: string, proposalId: string, action: EventAction, event?: CreateEventDTO, eventId?: string, updates?: UpdateEventDTO, ): Promise { // Update specific proposal with respondedAction await this.chatRepo.updateProposalResponse(messageId, proposalId, "confirm"); // Perform the actual event operation let content: string; if (action === "create" && event) { const createdEvent = await this.eventRepo.create(userId, event); content = `Der Termin "${createdEvent.title}" wurde erstellt.`; } else if (action === "update" && eventId && updates) { const updatedEvent = await this.eventRepo.update(eventId, updates); content = updatedEvent ? `Der Termin "${updatedEvent.title}" wurde aktualisiert.` : "Termin nicht gefunden."; } else if (action === "delete" && eventId) { await this.eventRepo.delete(eventId); content = event?.title ? `Der Termin "${event.title}" wurde gelöscht.` : "Der Termin wurde gelöscht."; } else { content = "Ungültige Aktion."; } // Save response message to DB const message = await this.chatRepo.createMessage(conversationId, { sender: "assistant", content, }); return { message, conversationId }; } async rejectEvent( userId: string, conversationId: string, messageId: string, proposalId: string, ): Promise { // Update specific proposal with respondedAction await this.chatRepo.updateProposalResponse(messageId, proposalId, "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 { 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( userId: string, conversationId: string, options?: GetMessagesOptions, ): Promise { // 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); } }