feat: review single AI-generated events before saving
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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,15 +109,20 @@ 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">
|
||||||
|
{isAiDraft && (
|
||||||
|
<div className="rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-xs leading-relaxed text-primary">
|
||||||
|
This draft was generated from natural language. Double-check
|
||||||
|
dates, times, location, recurrence, and links before saving.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="event-title">Title</Label>
|
||||||
<Input
|
<Input
|
||||||
id="event-title"
|
id="event-title"
|
||||||
name="title"
|
name="title"
|
||||||
@@ -111,7 +131,10 @@ export const EventDialog = ({
|
|||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="event-description">Description / notes</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="event-description"
|
id="event-description"
|
||||||
name="description"
|
name="description"
|
||||||
@@ -120,8 +143,11 @@ export const EventDialog = ({
|
|||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
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="space-y-1.5">
|
||||||
|
<Label htmlFor="event-location">Location</Label>
|
||||||
<div className="relative">
|
<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" />
|
<LucideMapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50" />
|
||||||
<Input
|
<Input
|
||||||
@@ -133,6 +159,9 @@ export const EventDialog = ({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor="event-url">URL</Label>
|
||||||
<Input
|
<Input
|
||||||
id="event-url"
|
id="event-url"
|
||||||
name="url"
|
name="url"
|
||||||
@@ -141,6 +170,7 @@ export const EventDialog = ({
|
|||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<RecurrencePicker
|
<RecurrencePicker
|
||||||
value={recurrenceRule}
|
value={recurrenceRule}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user