feat: multimodal AI event creation with image support #1

Merged
old4ever merged 20 commits from image-parse into main 2026-04-07 15:21:28 -04:00
Showing only changes of commit 7bb4f2be9d - Show all commits

View File

@@ -1,12 +1,26 @@
import { z } from "zod";
import { MAX_IMAGE_SIZE_BYTES } from "@/lib/constants";
/** 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 base64Part = val.split(",")[1] ?? "";
const binarySize = Math.ceil(base64Part.length * 0.75);
return binarySize <= MAX_IMAGE_SIZE_BYTES;
};
export const AiEventRequestSchema = z
.object({
prompt: z.string().trim().max(2000).optional(),
imageBase64: z
.string()
.startsWith("data:", "Must be a valid data URL")
.max(10 * 1024 * 1024 * 1.37, "Image must be less than 10MB")
.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(),
})
.refine((data) => data.prompt || data.imageBase64, {
@@ -21,25 +35,18 @@ export const AiEventResponseItemSchema = z.object({
description: z.string().optional(),
location: z.string().optional(),
url: z.string().optional(),
start: z.string(),
end: z.string().optional(),
start: z.string().datetime({ offset: true }),
end: z.string().datetime({ offset: true }).optional(),
allDay: z.boolean().optional(),
recurrenceRule: z.string().optional(),
});
export const AiEventResponseSchema = z.array(AiEventResponseItemSchema);
export type CalendarEvent = {
export type AiEventResponseItem = z.infer<typeof AiEventResponseItemSchema>;
export type CalendarEvent = AiEventResponseItem & {
id: string;
title: string;
description?: string;
location?: string;
url?: string;
start: string;
end?: string;
allDay?: boolean;
createdAt?: string;
lastModified?: string;
recurrenceRule?: string;
};