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 ( !open && closeModal()}> @@ -771,7 +825,8 @@ export function UnifiedEventModal() { Describe your event in natural language and our AI will create it for you. Include details like title, date, time, duration, location, and any other - relevant information. + relevant information. You can also describe multiple + events at once.

@@ -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() {