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:
@@ -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>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user