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

@@ -3,30 +3,35 @@ import { MAX_IMAGE_SIZE_BYTES } from "@/lib/constants";
import { normalizeAiDateString } from "@/lib/date-normalizer";
/** Validates that a base64 data URL string decodes to binary under the max size. */
const isValidImageSize = (val: string | undefined): boolean => {
if (!val) return true;
const isValidImageSize = (val: string): boolean => {
const base64Part = val.split(",")[1] ?? "";
const binarySize = Math.ceil(base64Part.length * 0.75);
return binarySize <= MAX_IMAGE_SIZE_BYTES;
};
/** Single image data-URL validator (reused inside the array schema). */
const imageDataUrl = z
.string()
.regex(
/^data:image\/(png|jpeg|webp);base64,/,
"Must be a valid image data URL (PNG, JPEG, or WebP)",
)
.refine(isValidImageSize, {
message: "Image must be less than 10MB",
});
export const AiEventRequestSchema = z
.object({
prompt: z.string().trim().max(2000).optional(),
imageBase64: z
.string()
.regex(
/^data:image\/(png|jpeg|webp);base64,/,
"Must be a valid image data URL (PNG, JPEG, or WebP)",
)
.refine(isValidImageSize, {
message: "Image must be less than 10MB",
})
.optional(),
/** Array of base64-encoded image data URLs (PNG, JPEG, WebP). */
images: z.array(imageDataUrl).optional(),
})
.refine((data) => data.prompt || data.imageBase64, {
message: "Either a prompt or an image is required",
});
.refine(
(data) =>
(data.prompt && data.prompt.trim().length > 0) ||
(data.images && data.images.length > 0),
{ message: "Either a prompt or at least one image is required" },
);
export type AiEventRequest = z.infer<typeof AiEventRequestSchema>;