feat: support multiple event proposals in single AI response
- Change proposedChange to proposedChanges array in ChatMessage type - Add unique id and individual respondedAction to each ProposedEventChange - Implement arrow navigation UI for multiple proposals with "Event X von Y" counter - Add updateProposalResponse() method for per-proposal confirm/reject tracking - GPTAdapter now collects multiple tool call results into proposals array - Add RRULE documentation to system prompt (separate events for different times) - Fix RRULE parsing to strip RRULE: prefix if present - Add log summarization for large args (conversationHistory, existingEvents) - Keep proposedChanges logged in full for debugging AI issues
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
|
||||
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
||||
|
||||
type TestResponse = { content: string; proposedChange?: ProposedEventChange };
|
||||
type TestResponse = { content: string; proposedChanges?: ProposedEventChange[] };
|
||||
|
||||
// Test response index (cycles through responses)
|
||||
let responseIndex = 0;
|
||||
@@ -22,7 +22,134 @@ let responseIndex = 0;
|
||||
// Static test responses (event proposals)
|
||||
const staticResponses: TestResponse[] = [
|
||||
// {{{
|
||||
// Response 0: Help response (text only)
|
||||
// === 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" +
|
||||
@@ -35,30 +162,36 @@ const staticResponses: TestResponse[] = [
|
||||
{
|
||||
content:
|
||||
"Alles klar! Ich erstelle dir einen Termin für das Meeting mit Jens am nächsten Freitag um 14:00 Uhr:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Meeting mit Jens",
|
||||
startTime: getDay("Friday", 1, 14, 0),
|
||||
endTime: getDay("Friday", 1, 15, 0),
|
||||
description: "Arbeitstreffen",
|
||||
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:",
|
||||
proposedChange: {
|
||||
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",
|
||||
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: "" },
|
||||
@@ -68,45 +201,54 @@ const staticResponses: TestResponse[] = [
|
||||
{
|
||||
content:
|
||||
"Ich habe dir einen Arzttermin eingetragen. Denk daran, deine Versichertenkarte mitzunehmen!",
|
||||
proposedChange: {
|
||||
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",
|
||||
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:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Mamas Geburtstag",
|
||||
startTime: getDay("Thursday", 2, 0, 0),
|
||||
endTime: getDay("Thursday", 2, 23, 59),
|
||||
isRecurring: true,
|
||||
recurrenceRule: "FREQ=YEARLY",
|
||||
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:",
|
||||
proposedChange: {
|
||||
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",
|
||||
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: "" },
|
||||
@@ -114,44 +256,53 @@ const staticResponses: TestResponse[] = [
|
||||
{
|
||||
content:
|
||||
"Alles klar! Ich habe das Telefonat mit deiner Mutter eingetragen:",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Telefonat mit Mama",
|
||||
startTime: getDay("Wednesday", 1, 11, 0),
|
||||
endTime: getDay("Wednesday", 1, 11, 30),
|
||||
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ß!",
|
||||
proposedChange: {
|
||||
action: "create",
|
||||
event: {
|
||||
title: "Geburtstagsfeier Lisa",
|
||||
startTime: getDay("Saturday", 2, 19, 0),
|
||||
endTime: getDay("Saturday", 2, 23, 0),
|
||||
description: "Geschenk: Buch über Fotografie",
|
||||
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:",
|
||||
proposedChange: {
|
||||
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",
|
||||
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: "" },
|
||||
@@ -177,17 +328,20 @@ async function getTestResponse(
|
||||
if (jensEvent) {
|
||||
return {
|
||||
content: "Soll ich diesen Termin wirklich löschen?",
|
||||
proposedChange: {
|
||||
action: "delete",
|
||||
eventId: jensEvent.id,
|
||||
event: {
|
||||
title: jensEvent.title,
|
||||
startTime: jensEvent.startTime,
|
||||
endTime: jensEvent.endTime,
|
||||
description: jensEvent.description,
|
||||
isRecurring: jensEvent.isRecurring,
|
||||
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." };
|
||||
@@ -210,18 +364,21 @@ async function getTestResponse(
|
||||
return {
|
||||
content:
|
||||
"Alles klar, ich verschiebe das Telefonat mit Mama auf Samstag um 13:00 Uhr:",
|
||||
proposedChange: {
|
||||
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,
|
||||
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." };
|
||||
@@ -290,7 +447,7 @@ export class ChatService {
|
||||
const answerMessage = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content: response.content,
|
||||
proposedChange: response.proposedChange,
|
||||
proposedChanges: response.proposedChanges,
|
||||
});
|
||||
|
||||
return { message: answerMessage, conversationId: conversationId };
|
||||
@@ -300,15 +457,14 @@ export class ChatService {
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
proposalId: string,
|
||||
action: EventAction,
|
||||
event?: CreateEventDTO,
|
||||
eventId?: string,
|
||||
updates?: UpdateEventDTO,
|
||||
): Promise<ChatResponse> {
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, {
|
||||
respondedAction: "confirm",
|
||||
});
|
||||
// Update specific proposal with respondedAction
|
||||
await this.chatRepo.updateProposalResponse(messageId, proposalId, "confirm");
|
||||
|
||||
// Perform the actual event operation
|
||||
let content: string;
|
||||
@@ -343,9 +499,10 @@ export class ChatService {
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
proposalId: string,
|
||||
): Promise<ChatResponse> {
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, { respondedAction: "reject" });
|
||||
// Update specific proposal with respondedAction
|
||||
await this.chatRepo.updateProposalResponse(messageId, proposalId, "reject");
|
||||
|
||||
// Save response message to DB
|
||||
const message = await this.chatRepo.createMessage(conversationId, {
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface AIContext {
|
||||
|
||||
export interface AIResponse {
|
||||
content: string;
|
||||
proposedChange?: ProposedEventChange;
|
||||
proposedChanges?: ProposedEventChange[];
|
||||
}
|
||||
|
||||
export interface AIProvider {
|
||||
|
||||
@@ -26,4 +26,10 @@ export interface ChatRepository {
|
||||
messageId: string,
|
||||
updates: UpdateMessageDTO,
|
||||
): Promise<ChatMessage | null>;
|
||||
|
||||
updateProposalResponse(
|
||||
messageId: string,
|
||||
proposalId: string,
|
||||
respondedAction: "confirm" | "reject",
|
||||
): Promise<ChatMessage | null>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user