feat(ai-toolbar): merge event actions into toolbar, add motion animations and AI summary panel

- Add event action props (events, onAddEvent, onImport, onExport, onClearAll)
- Show skeleton loading state while session is pending
- Render event action buttons (Add, Import, Export, Clear) in action bar
- Add AnimatePresence for image preview attach/remove
- Replace Card summary panel with animated glass-card panel
- Inline AI Summarize button into action bar when authenticated
- Add Sparkles icon to AI command section header
This commit is contained in:
2026-04-08 00:56:58 -04:00
parent 8d7cc5b2a5
commit 2fc21ee929

View File

@@ -1,9 +1,24 @@
import { X } from "lucide-react"; "use client";
import { AnimatePresence, motion } from "framer-motion";
import {
Bot,
CalendarPlus,
Download,
FileUp,
ImageIcon,
Loader2,
Sparkles,
Trash2,
X,
} from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { IcsFilePicker } from "@/components/ics-file-picker";
import { ImagePicker } from "@/components/image-picker"; import { ImagePicker } from "@/components/image-picker";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import type { CalendarEvent } from "@/lib/types";
interface AIToolbarProps { interface AIToolbarProps {
isAuthenticated: boolean; isAuthenticated: boolean;
@@ -18,6 +33,12 @@ interface AIToolbarProps {
onAiSummarize: () => void; onAiSummarize: () => void;
summary: string | null; summary: string | null;
summaryUpdated: string | null; summaryUpdated: string | null;
// event actions
events: CalendarEvent[];
onAddEvent: () => void;
onImport: (file: File) => void;
onExport: () => void;
onClearAll: () => void;
} }
export const AIToolbar = ({ export const AIToolbar = ({
@@ -33,86 +54,186 @@ export const AIToolbar = ({
onAiSummarize, onAiSummarize,
summary, summary,
summaryUpdated, summaryUpdated,
events,
onAddEvent,
onImport,
onExport,
onClearAll,
}: AIToolbarProps) => { }: AIToolbarProps) => {
if (isPending) {
return (
<div className="glass-card p-4 mb-6 animate-pulse">
<div className="h-10 bg-muted/50 rounded-lg" />
</div>
);
}
return ( return (
<> <div className="mb-6 space-y-3">
{isPending ? ( <div className="glass-card p-3">
<div className="mb-4 p-4 text-center animate-pulse bg-muted"> {/* AI command — only shown when authenticated */}
Loading... {isAuthenticated ? (
</div> <>
) : ( <div className="flex items-start gap-3">
<div> <div className="flex-1 space-y-2">
{isAuthenticated ? ( <div className="flex items-center gap-2 mb-1.5">
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start"> <Sparkles className="h-3.5 w-3.5 text-primary shrink-0" />
<div className="w-full"> <span className="text-xs font-medium text-muted-foreground">
AI Command
</span>
</div>
<Textarea <Textarea
className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto sm:overflow-y-visible px-3 py-2 scroll-p-8 placeholder:italic" className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto bg-transparent border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 px-0 py-0 text-sm placeholder:text-muted-foreground/60 placeholder:italic"
style={{ clipPath: "inset(0 round 1rem)" }} placeholder="Describe an event or paste details..."
placeholder="Describe event or attach an image for AI to create"
value={aiPrompt} value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)} onChange={(e) => setAiPrompt(e.target.value)}
/> />
{imagePreview && ( <AnimatePresence>
<div className="relative mt-2 inline-block max-w-full overflow-hidden"> {imagePreview && (
<Image <motion.div
src={imagePreview} initial={{ opacity: 0, scale: 0.9 }}
alt="Attached event flyer" animate={{ opacity: 1, scale: 1 }}
className="h-20 rounded-md object-cover border" exit={{ opacity: 0, scale: 0.9 }}
width={80} className="relative inline-block"
height={80}
unoptimized
/>
<Button
variant="destructive"
size="icon"
className="absolute -top-2 -right-2 h-5 w-5"
onClick={onImageClear}
aria-label="Remove image"
> >
<X className="h-3 w-3" /> <Image
</Button> src={imagePreview}
</div> alt="Attached event flyer"
)} className="h-16 w-16 rounded-md object-cover ring-1 ring-border"
width={64}
height={64}
unoptimized
/>
<Button
variant="destructive"
size="icon"
className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full"
onClick={onImageClear}
aria-label="Remove image"
>
<X className="h-2.5 w-2.5" />
</Button>
</motion.div>
)}
</AnimatePresence>
</div> </div>
<div className="flex flex-row gap-2 pt-1"> <div className="flex flex-col gap-1.5 pt-6">
<ImagePicker <ImagePicker
onFileSelect={onImageSelect} onFileSelect={onImageSelect}
disabled={aiLoading} disabled={aiLoading}
/> className="h-8 w-8"
<Button onClick={onAiCreate} disabled={aiLoading}> >
{aiLoading ? "Thinking..." : "AI Create"} <ImageIcon className="h-3.5 w-3.5" />
</ImagePicker>
<Button
size="icon"
className="h-8 w-8"
onClick={onAiCreate}
disabled={aiLoading || (!aiPrompt.trim() && !imagePreview)}
>
{aiLoading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Sparkles className="h-3.5 w-3.5" />
)}
<span className="sr-only">
{aiLoading ? "Creating..." : "Create event"}
</span>
</Button> </Button>
</div> </div>
</div> </div>
) : ( <Separator className="my-3 opacity-50" />
<div className="mb-4 p-4 border border-dashed rounded-lg text-center"> </>
<div className="text-sm text-muted-foreground"> ) : (
Sign in to unlock natural language event creation powered by AI <div className="flex items-center gap-2 mb-3 pb-3 border-b border-border/50">
</div> <Bot className="h-3.5 w-3.5 text-muted-foreground/60 shrink-0" />
</div> <p className="text-xs text-muted-foreground/60">
Sign in to create events with AI
</p>
</div>
)}
{/* Action buttons — always visible */}
<div className="flex items-center gap-2 flex-wrap">
<Button size="sm" onClick={onAddEvent} className="h-8 text-xs">
<CalendarPlus className="h-3.5 w-3.5 mr-1.5" />
Add Event
</Button>
<IcsFilePicker onFileSelect={onImport} variant="outline" size="sm">
<span className="flex items-center gap-1.5">
<FileUp className="h-3.5 w-3.5" />
Import
</span>
</IcsFilePicker>
{events.length > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={onExport}
className="h-8 text-xs"
>
<Download className="h-3.5 w-3.5 mr-1.5" />
Export
</Button>
<Button
variant="ghost"
size="sm"
onClick={onClearAll}
className="h-8 text-xs text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
Clear
</Button>
</>
)} )}
<div className="ml-auto flex items-center gap-3">
{events.length > 0 && (
<span className="text-xs text-muted-foreground">
{events.length} event{events.length !== 1 ? "s" : ""}
</span>
)}
{isAuthenticated && (
<Button
variant="ghost"
size="sm"
onClick={onAiSummarize}
disabled={aiLoading}
className="text-xs h-7"
>
<Bot className="h-3 w-3 mr-1.5" />
{aiLoading ? "Summarizing..." : "Summarize"}
</Button>
)}
</div>
</div> </div>
)}
{/* Summary Panel */}
{summary && (
<Card className="p-4 mb-4">
<div className="text-sm mb-1">Summary updated {summaryUpdated}</div>
<div>{summary}</div>
</Card>
)}
{/* AI Actions Toolbar */}
<p className="text-muted-foreground text-sm pb-2 pl-1">AI actions</p>
<div className="gap-2 mb-4">
<Button
variant="secondary"
onClick={onAiSummarize}
disabled={aiLoading}
>
{aiLoading ? "Summarizing..." : "AI Summarize"}
</Button>
</div> </div>
</>
<AnimatePresence>
{summary && (
<motion.div
initial={{ opacity: 0, y: -4, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -4, height: 0 }}
className="overflow-hidden"
>
<div className="glass-card p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-muted-foreground">
AI Summary
</span>
<span className="text-xs text-muted-foreground/60">
{summaryUpdated}
</span>
</div>
<p className="text-sm leading-relaxed">{summary}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
); );
}; };