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; 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; export type CalendarEvent = AiEventResponseItem & { id: string; createdAt?: string; lastModified?: string; };