🐛 fix: normalize AI-generated dates to valid ISO offset datetimes
- add normalizeAiDateString() to coerce bare ISO datetimes, date-only strings, and fractional-second variants into offset-aware format - apply via z.preprocess in AiEventResponseItemSchema so Zod validation no longer rejects AI responses missing a timezone offset - fix system prompt to use toISOString() (unambiguous UTC) and clarify expected datetime format for the AI model - install bun-types and add to tsconfig so bun:test resolves cleanly - add 8 behaviour-driven tests covering all normalizer edge cases
This commit is contained in:
@@ -17,7 +17,7 @@ TypeScript type:
|
||||
description?: string,
|
||||
location?: string,
|
||||
url?: string,
|
||||
start: string, // ISO datetime
|
||||
start: string, // ISO 8601 datetime with timezone, e.g. "2025-04-10T10:00:00-04:00"
|
||||
end?: string,
|
||||
allDay?: boolean,
|
||||
recurrenceRule?: string // valid iCal RRULE string like FREQ=WEEKLY;BYDAY=MO;INTERVAL=1
|
||||
@@ -26,7 +26,7 @@ TypeScript type:
|
||||
Rules:
|
||||
- If the user describes multiple events in one prompt, return multiple objects (one per event).
|
||||
- Always return a valid JSON array of objects, even if there's only one event.
|
||||
- Today is ${new Date().toLocaleString()}.
|
||||
- Today is ${new Date().toISOString()}.
|
||||
- If no time is given, assume allDay event.
|
||||
- If no end time is given (and event is not allDay), default to 1 hour after start.
|
||||
- If multiple events are described, return multiple.
|
||||
|
||||
10
src/lib/date-normalizer.ts
Normal file
10
src/lib/date-normalizer.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { formatISO, parseISO } from "date-fns";
|
||||
|
||||
const ISO_DATETIME_WITH_OFFSET =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/;
|
||||
|
||||
export const normalizeAiDateString = (input: string): string => {
|
||||
if (ISO_DATETIME_WITH_OFFSET.test(input)) return input;
|
||||
const parsed = parseISO(input);
|
||||
return formatISO(parsed);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
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 | undefined): boolean => {
|
||||
@@ -29,14 +30,19 @@ export const AiEventRequestSchema = z
|
||||
|
||||
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: z.string().datetime({ offset: true }),
|
||||
end: z.string().datetime({ offset: true }).optional(),
|
||||
start: aiDatetime,
|
||||
end: aiDatetime.optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
recurrenceRule: z.string().optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user