feat: review single AI-generated events before saving

This commit is contained in:
2026-04-09 11:59:32 -04:00
parent aef22f704f
commit ecff8bebb1
2 changed files with 86 additions and 41 deletions

View File

@@ -39,6 +39,7 @@ export default function HomePage() {
const [events, setEvents] = useState<CalendarEvent[]>([]); const [events, setEvents] = useState<CalendarEvent[]>([]);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [dialogSource, setDialogSource] = useState<"manual" | "ai">("manual");
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
// Form fields // Form fields
@@ -85,6 +86,7 @@ export default function HomePage() {
setEnd(""); setEnd("");
setAllDay(false); setAllDay(false);
setEditingId(null); setEditingId(null);
setDialogSource("manual");
setRecurrenceRule(undefined); setRecurrenceRule(undefined);
}; };
@@ -257,10 +259,11 @@ export default function HomePage() {
if (data.length === 1) { if (data.length === 1) {
populateEventForm(data[0]); populateEventForm(data[0]);
setDialogSource("ai");
setAiPrompt(""); setAiPrompt("");
setDialogOpen(true); setDialogOpen(true);
handleImagesClear(); handleImagesClear();
return { message: "Event has been created!" }; return { message: "Draft event is ready for review." };
} }
await persistAiEvents(data); await persistAiEvents(data);
@@ -318,6 +321,7 @@ export default function HomePage() {
setEnd(eventData.end || ""); setEnd(eventData.end || "");
setAllDay(eventData.allDay || false); setAllDay(eventData.allDay || false);
setEditingId(eventData.id); setEditingId(eventData.id);
setDialogSource("manual");
setRecurrenceRule(eventData.recurrenceRule); setRecurrenceRule(eventData.recurrenceRule);
setDialogOpen(true); setDialogOpen(true);
}; };
@@ -344,7 +348,11 @@ export default function HomePage() {
summary={summary} summary={summary}
summaryUpdated={summaryUpdated} summaryUpdated={summaryUpdated}
events={events} events={events}
onAddEvent={() => setDialogOpen(true)} onAddEvent={() => {
resetForm();
setDialogSource("manual");
setDialogOpen(true);
}}
onImport={handleImport} onImport={handleImport}
onExport={handleExport} onExport={handleExport}
onClearAll={handleClearAll} onClearAll={handleClearAll}
@@ -356,6 +364,7 @@ export default function HomePage() {
open={dialogOpen} open={dialogOpen}
onOpenChange={setDialogOpen} onOpenChange={setDialogOpen}
editingId={editingId} editingId={editingId}
dialogSource={dialogSource}
title={title} title={title}
setTitle={setTitle} setTitle={setTitle}
description={description} description={description}

View File

@@ -22,6 +22,7 @@ interface EventDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
editingId: string | null; editingId: string | null;
dialogSource: "manual" | "ai";
title: string; title: string;
setTitle: (title: string) => void; setTitle: (title: string) => void;
description: string; description: string;
@@ -46,6 +47,7 @@ export const EventDialog = ({
open, open,
onOpenChange, onOpenChange,
editingId, editingId,
dialogSource,
title, title,
setTitle, setTitle,
description, description,
@@ -65,6 +67,19 @@ export const EventDialog = ({
onSave, onSave,
onReset, onReset,
}: EventDialogProps) => { }: EventDialogProps) => {
const isAiDraft = dialogSource === "ai" && !editingId;
const titleText = editingId
? "Edit Event"
: isAiDraft
? "Review AI Draft"
: "New Event";
const descriptionText = editingId
? "Update the event details below. Title and start date are required."
: isAiDraft
? "AI filled in this event from your prompt. Review each field, then save when it looks right."
: "Create an event manually. Title and start date are required.";
const saveLabel = editingId ? "Update Event" : "Save Event";
const handleOpenChange = (val: boolean) => { const handleOpenChange = (val: boolean) => {
if (!val) onReset(); if (!val) onReset();
onOpenChange(val); onOpenChange(val);
@@ -94,52 +109,67 @@ export const EventDialog = ({
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="glass-strong max-w-md"> <DialogContent className="glass-strong max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-base"> <DialogTitle className="text-base">{titleText}</DialogTitle>
{editingId ? "Edit Event" : "New Event"} <DialogDescription>{descriptionText}</DialogDescription>
</DialogTitle>
<DialogDescription className="sr-only">
Fill in the event details below. Title and start date are required.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="space-y-3">
<Input {isAiDraft && (
id="event-title" <div className="rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-xs leading-relaxed text-primary">
name="title" This draft was generated from natural language. Double-check
placeholder="Event title" dates, times, location, recurrence, and links before saving.
value={title} </div>
onChange={(e) => setTitle(e.target.value)} )}
className="font-medium"
/>
<Textarea <div className="space-y-1.5">
id="event-description" <Label htmlFor="event-title">Title</Label>
name="description" <Input
className="field-sizing-content min-h-[60px] max-h-40 resize-none placeholder:text-muted-foreground/50" id="event-title"
placeholder="Add a description..." name="title"
value={description} placeholder="Event title"
onChange={(e) => setDescription(e.target.value)} value={title}
/> onChange={(e) => setTitle(e.target.value)}
className="font-medium"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="event-description">Description / notes</Label>
<Textarea
id="event-description"
name="description"
className="field-sizing-content min-h-[60px] max-h-40 resize-none placeholder:text-muted-foreground/50"
placeholder="Add a description..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="relative"> <div className="space-y-1.5">
<LucideMapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50" /> <Label htmlFor="event-location">Location</Label>
<div className="relative">
<LucideMapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50" />
<Input
id="event-location"
name="location"
placeholder="Location"
value={location}
onChange={(e) => setLocation(e.target.value)}
className="pl-8"
/>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="event-url">URL</Label>
<Input <Input
id="event-location" id="event-url"
name="location" name="url"
placeholder="Location" placeholder="URL"
value={location} value={url}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setUrl(e.target.value)}
className="pl-8"
/> />
</div> </div>
<Input
id="event-url"
name="url"
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div> </div>
<RecurrencePicker <RecurrencePicker
@@ -195,10 +225,16 @@ export const EventDialog = ({
</div> </div>
<DialogFooter className="gap-2 sm:gap-0"> <DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => handleOpenChange(false)}> <Button
type="button"
variant="ghost"
onClick={() => handleOpenChange(false)}
>
Cancel Cancel
</Button> </Button>
<Button onClick={onSave}>{editingId ? "Update" : "Create"}</Button> <Button type="button" onClick={onSave}>
{saveLabel}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>