feat: support multiple image uploads for AI event generation 🖼️

- Updated OpenRouter integration to accept an array of image URLs
- Updated ImagePicker to use the `multiple` attribute natively
- Added `appendImagesDeduped` for handling client-side image deduplication
- Enhanced clipboard pasting to extract multiple images at once
- Rendered multiple images in a horizontal thumbnail strip in the AIToolbar
- Added tests to cover multi-image logic and AI request mapping
This commit is contained in:
2026-04-08 20:46:43 -04:00
parent cac201a4d2
commit 513aafcebc
18 changed files with 881 additions and 238 deletions

View File

@@ -5,23 +5,28 @@ import { ImageIcon } from "lucide-react";
import type React from "react";
import { useImperativeHandle, useRef } from "react";
import { Button, type buttonVariants } from "@/components/ui/button";
import { IMAGE_ACCEPT } from "@/lib/constants";
interface ImagePickerProps extends VariantProps<typeof buttonVariants> {
onFileSelect?: (file: File) => void;
/** Called with ALL selected files (array of 1..N). */
onFilesSelect?: (files: File[]) => void;
className?: string;
children?: React.ReactNode;
disabled?: boolean;
/** Allow selecting multiple images at once (native OS picker multi-select). */
multiple?: boolean;
/** Expose an imperative trigger so parents can open the file dialog via ref */
triggerRef?: React.Ref<{ open: () => void }>;
}
export function ImagePicker({
onFileSelect,
onFilesSelect,
className,
children,
variant = "ghost",
size = "icon",
disabled = false,
multiple = false,
triggerRef,
}: ImagePickerProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -38,10 +43,11 @@ export function ImagePicker({
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file && onFileSelect) {
onFileSelect(file);
const fileList = event.target.files;
if (fileList && fileList.length > 0 && onFilesSelect) {
onFilesSelect(Array.from(fileList));
}
// Reset so the same file(s) can be re-selected
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -53,7 +59,8 @@ export function ImagePicker({
ref={fileInputRef}
type="file"
name="image-upload"
accept="image/png,image/jpeg,image/webp"
accept={IMAGE_ACCEPT}
multiple={multiple}
onChange={handleFileChange}
className="hidden"
/>