- 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
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import { z } from "zod";
|
|
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): 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(),
|
|
/** Array of base64-encoded image data URLs (PNG, JPEG, WebP). */
|
|
images: z.array(imageDataUrl).optional(),
|
|
})
|
|
.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>;
|
|
|
|
const aiDatetime = z.preprocess(
|
|
(val) => (typeof val === "string" ? normalizeAiDateString(val) : val),
|
|
z.string().datetime({ offset: true }),
|
|
);
|
|
|
|
export const AiEventResponseItemSchema = z.object({
|
|
id: z.string().optional(),
|
|
title: z.string().min(1),
|
|
description: z.string().optional(),
|
|
location: z.string().optional(),
|
|
url: z.string().optional(),
|
|
start: aiDatetime,
|
|
end: aiDatetime.optional(),
|
|
allDay: z.boolean().optional(),
|
|
recurrenceRule: z.string().optional(),
|
|
});
|
|
|
|
export const AiEventResponseSchema = z.array(AiEventResponseItemSchema);
|
|
|
|
export type AiEventResponseItem = z.infer<typeof AiEventResponseItemSchema>;
|
|
|
|
export type CalendarEvent = AiEventResponseItem & {
|
|
id: string;
|
|
createdAt?: string;
|
|
lastModified?: string;
|
|
};
|