From 6b1d400dd26f401d44b155ff3f791c28d8dc5cf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=B5=20Ho=C3=A0ng=20Ph=C3=BAc?=
Date: Fri, 14 Mar 2025 22:57:44 +0700
Subject: [PATCH] feat(calendar): introduce calendarEventsSchema for handling
multiple events and update related components
---
packages/ai/src/calendar/events.ts | 2 +
packages/ai/src/calendar/route.ts | 4 +-
.../ui/legacy/calendar/UnifiedEventModal.tsx | 187 ++++++++++++++----
3 files changed, 148 insertions(+), 45 deletions(-)
diff --git a/packages/ai/src/calendar/events.ts b/packages/ai/src/calendar/events.ts
index efeaf5a6b1..e0c61f940f 100644
--- a/packages/ai/src/calendar/events.ts
+++ b/packages/ai/src/calendar/events.ts
@@ -78,6 +78,8 @@ export const calendarEventSchema = z.object({
// created_by: z.string().optional().describe('User ID of the creator'),
});
+export const calendarEventsSchema = z.array(calendarEventSchema);
+
export type CalendarEvent = z.infer;
// This schema is for notifications related to calendar events
diff --git a/packages/ai/src/calendar/route.ts b/packages/ai/src/calendar/route.ts
index 0e160f65d6..fac8cf8b80 100644
--- a/packages/ai/src/calendar/route.ts
+++ b/packages/ai/src/calendar/route.ts
@@ -1,4 +1,4 @@
-import { calendarEventSchema } from './events';
+import { calendarEventsSchema } from './events';
import { google } from '@ai-sdk/google';
import { createClient } from '@tuturuuu/supabase/next/server';
import { streamObject } from 'ai';
@@ -81,7 +81,7 @@ export async function POST(req: Request) {
const result = streamObject({
model: google('gemini-2.0-flash'),
- schema: calendarEventSchema,
+ schema: calendarEventsSchema,
prompt: promptText,
});
diff --git a/packages/ui/src/components/ui/legacy/calendar/UnifiedEventModal.tsx b/packages/ui/src/components/ui/legacy/calendar/UnifiedEventModal.tsx
index 96626d7b61..479330daad 100644
--- a/packages/ui/src/components/ui/legacy/calendar/UnifiedEventModal.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/UnifiedEventModal.tsx
@@ -14,7 +14,7 @@ import {
OverlapWarning,
} from './EventFormComponents';
import { zodResolver } from '@hookform/resolvers/zod';
-import { calendarEventSchema } from '@tuturuuu/ai/calendar/events';
+import { calendarEventsSchema } from '@tuturuuu/ai/calendar/events';
import { useObject } from '@tuturuuu/ai/object/core';
import { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
import {
@@ -58,6 +58,7 @@ import {
Calendar as CalendarIcon,
Check,
ChevronLeft,
+ ChevronRight,
Clock,
Cog,
FileText,
@@ -113,11 +114,15 @@ export function UnifiedEventModal() {
});
// State for AI event generation
- const [generatedEvent, setGeneratedEvent] = useState(null);
+ const [generatedEvents, setGeneratedEvents] = useState([]);
+ const [currentEventIndex, setCurrentEventIndex] = useState(0);
const [userTimezone] = useState(
Intl.DateTimeFormat().resolvedOptions().timeZone
);
+ // Get the current event being previewed
+ const generatedEvent = generatedEvents[currentEventIndex];
+
// Determine if we're editing an existing event
const isEditing = activeEvent?.id && activeEvent.id !== 'new';
@@ -150,25 +155,37 @@ export function UnifiedEventModal() {
// AI generation
const { object, submit, error, isLoading } = useObject({
api: '/api/v1/calendar/events/generate',
- schema: calendarEventSchema,
+ schema: calendarEventsSchema,
});
- // Handle AI-generated event
+ // Handle AI-generated events
useEffect(() => {
if (object && !isLoading) {
- // Ensure color is uppercase to match SupportedColor type
- const processedEvent = {
- ...object,
- color: object.color
- ? (object.color.toString().toUpperCase() as SupportedColor)
- : 'BLUE',
- };
-
- setGeneratedEvent(processedEvent);
-
- // Find overlapping events when a new event is generated
- if (aiForm.getValues().smart_scheduling) {
- checkForOverlaps(processedEvent);
+ // Process the generated events
+ const processedEvents = Array.isArray(object) ? object : [object];
+
+ const formattedEvents = processedEvents
+ .map((event) => {
+ if (!event) return null;
+ return {
+ ...event,
+ color:
+ event.color && typeof event.color === 'string'
+ ? (event.color.toString().toUpperCase() as SupportedColor)
+ : 'BLUE',
+ };
+ })
+ .filter((event): event is NonNullable => event !== null);
+
+ setGeneratedEvents(formattedEvents);
+ setCurrentEventIndex(0);
+
+ // Find overlapping events for the first event
+ if (formattedEvents.length > 0 && aiForm.getValues().smart_scheduling) {
+ const firstEvent = formattedEvents[0];
+ if (firstEvent && firstEvent.start_at && firstEvent.end_at) {
+ checkForOverlaps(firstEvent as Partial);
+ }
}
setActiveTab('preview');
@@ -241,7 +258,7 @@ export function UnifiedEventModal() {
// Reset AI form
aiForm.reset();
- setGeneratedEvent(null);
+ setGeneratedEvents([]);
}
// Clear any error messages
@@ -326,30 +343,40 @@ export function UnifiedEventModal() {
// Handle AI event save
const handleAISave = async () => {
- if (!generatedEvent) return;
+ if (generatedEvents.length === 0) return;
setIsSaving(true);
try {
- const calendarEvent: Omit = {
- title: generatedEvent.title || 'New Event',
- description: generatedEvent.description || '',
- start_at: generatedEvent.start_at,
- end_at: generatedEvent.end_at,
- color: generatedEvent.color || 'BLUE',
- location: generatedEvent.location || '',
- is_all_day: Boolean(generatedEvent.is_all_day),
- scheduling_note: generatedEvent.scheduling_note || '',
- priority: generatedEvent.priority || aiForm.getValues().priority,
- };
+ // Save all events or just the current one based on user selection
+ const eventsToSave = generatedEvents;
+
+ for (const eventData of eventsToSave) {
+ const calendarEvent: Omit = {
+ title: eventData.title || 'New Event',
+ description: eventData.description || '',
+ start_at: eventData.start_at,
+ end_at: eventData.end_at,
+ color: eventData.color || 'BLUE',
+ location: eventData.location || '',
+ is_all_day: Boolean(eventData.is_all_day),
+ scheduling_note: eventData.scheduling_note || '',
+ priority: eventData.priority || aiForm.getValues().priority,
+ };
+
+ await addEvent(calendarEvent);
+ }
- await addEvent(calendarEvent);
+ toast({
+ title: 'Success',
+ description: `${eventsToSave.length} event${eventsToSave.length > 1 ? 's' : ''} added to your calendar`,
+ });
closeModal();
} catch (error) {
- console.error('Error saving AI event to calendar:', error);
+ console.error('Error saving AI events to calendar:', error);
toast({
title: 'Error',
- description: 'Failed to save AI-generated event. Please try again.',
+ description: 'Failed to save AI-generated events. Please try again.',
variant: 'destructive',
});
} finally {
@@ -495,8 +522,8 @@ export function UnifiedEventModal() {
// Generate helpful suggestions based on the prompt
const suggestions = [
- 'Consider adding a buffer time before/after this event',
- 'This event might benefit from a reminder notification',
+ 'Consider adding a buffer time before/after these events',
+ 'These events might benefit from reminder notifications',
'Based on your schedule, early morning might be better for focus',
'Consider adding meeting agenda or preparation notes',
];
@@ -509,15 +536,42 @@ export function UnifiedEventModal() {
existing_events: values.smart_scheduling ? existingEvents : undefined,
});
} catch (error) {
- console.error('Error generating event:', error);
+ console.error('Error generating events:', error);
toast({
- title: 'Error generating event',
+ title: 'Error generating events',
description: 'Please try again with a different prompt',
variant: 'destructive',
});
}
};
+ // Handle navigation between multiple events in preview
+ const goToNextEvent = () => {
+ if (currentEventIndex < generatedEvents.length - 1) {
+ const nextIndex = currentEventIndex + 1;
+ setCurrentEventIndex(nextIndex);
+ if (aiForm.getValues().smart_scheduling) {
+ const nextEvent = generatedEvents[nextIndex];
+ if (nextEvent && nextEvent.start_at && nextEvent.end_at) {
+ checkForOverlaps(nextEvent as Partial);
+ }
+ }
+ }
+ };
+
+ const goToPreviousEvent = () => {
+ if (currentEventIndex > 0) {
+ const prevIndex = currentEventIndex - 1;
+ setCurrentEventIndex(prevIndex);
+ if (aiForm.getValues().smart_scheduling) {
+ const prevEvent = generatedEvents[prevIndex];
+ if (prevEvent && prevEvent.start_at && prevEvent.end_at) {
+ checkForOverlaps(prevEvent as Partial);
+ }
+ }
+ }
+ };
+
return (
@@ -790,6 +845,11 @@ export function UnifiedEventModal() {
"Block 3 hours for focused work on the
presentation every morning this week"
+
+ "Create a series of 1-hour workout sessions at
+ the gym on Monday, Wednesday, and Friday at 7am
+ next week"
+
@@ -953,10 +1013,20 @@ export function UnifiedEventModal() {
AI Generated Event
+ {generatedEvents.length > 1 ? 's' : ''}
-
- Preview
-
+
+ {generatedEvents.length > 1 && (
+
+ {currentEventIndex + 1}
+ /
+ {generatedEvents.length}
+
+ )}
+
+ Preview
+
+
{generatedEvent && (
@@ -1026,6 +1096,32 @@ export function UnifiedEventModal() {
)}
+
+ {/* Navigation buttons for multiple events */}
+ {generatedEvents.length > 1 && (
+
+
+
+
+ )}
{/* AI Insights and Suggestions */}
@@ -1088,7 +1184,7 @@ export function UnifiedEventModal() {