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 ( return (
<> <div className="glass-card p-4 mb-6 animate-pulse">
{isPending ? ( <div className="h-10 bg-muted/50 rounded-lg" />
<div className="mb-4 p-4 text-center animate-pulse bg-muted">
Loading...
</div> </div>
) : ( );
<div> }
return (
<div className="mb-6 space-y-3">
<div className="glass-card p-3">
{/* AI command — only shown when authenticated */}
{isAuthenticated ? ( {isAuthenticated ? (
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start"> <>
<div className="w-full"> <div className="flex items-start gap-3">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2 mb-1.5">
<Sparkles className="h-3.5 w-3.5 text-primary shrink-0" />
<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)}
/> />
<AnimatePresence>
{imagePreview && ( {imagePreview && (
<div className="relative mt-2 inline-block max-w-full overflow-hidden"> <motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="relative inline-block"
>
<Image <Image
src={imagePreview} src={imagePreview}
alt="Attached event flyer" alt="Attached event flyer"
className="h-20 rounded-md object-cover border" className="h-16 w-16 rounded-md object-cover ring-1 ring-border"
width={80} width={64}
height={80} height={64}
unoptimized unoptimized
/> />
<Button <Button
variant="destructive" variant="destructive"
size="icon" size="icon"
className="absolute -top-2 -right-2 h-5 w-5" className="absolute -top-1.5 -right-1.5 h-5 w-5 rounded-full"
onClick={onImageClear} onClick={onImageClear}
aria-label="Remove image" aria-label="Remove image"
> >
<X className="h-3 w-3" /> <X className="h-2.5 w-2.5" />
</Button> </Button>
</div> </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="flex items-center gap-2 mb-3 pb-3 border-b border-border/50">
<div className="text-sm text-muted-foreground"> <Bot className="h-3.5 w-3.5 text-muted-foreground/60 shrink-0" />
Sign in to unlock natural language event creation powered by AI <p className="text-xs text-muted-foreground/60">
</div> Sign in to create events with AI
</div> </p>
)}
</div> </div>
)} )}
{/* Summary Panel */} {/* Action buttons — always visible */}
{summary && ( <div className="flex items-center gap-2 flex-wrap">
<Card className="p-4 mb-4"> <Button size="sm" onClick={onAddEvent} className="h-8 text-xs">
<div className="text-sm mb-1">Summary updated {summaryUpdated}</div> <CalendarPlus className="h-3.5 w-3.5 mr-1.5" />
<div>{summary}</div> Add Event
</Card> </Button>
)}
{/* AI Actions Toolbar */} <IcsFilePicker onFileSelect={onImport} variant="outline" size="sm">
<p className="text-muted-foreground text-sm pb-2 pl-1">AI actions</p> <span className="flex items-center gap-1.5">
<div className="gap-2 mb-4"> <FileUp className="h-3.5 w-3.5" />
Import
</span>
</IcsFilePicker>
{events.length > 0 && (
<>
<Button <Button
variant="secondary" 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} onClick={onAiSummarize}
disabled={aiLoading} disabled={aiLoading}
className="text-xs h-7"
> >
{aiLoading ? "Summarizing..." : "AI Summarize"} <Bot className="h-3 w-3 mr-1.5" />
{aiLoading ? "Summarizing..." : "Summarize"}
</Button> </Button>
)}
</div>
</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> </div>
</>
); );
}; };