import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { auth } from "@/auth"; import { buildMultimodalMessages } from "@/lib/ai-event-messages"; import { getAiDisabledMessage, isAdminAiEnabled } from "@/lib/ai-feature-flags"; import { extractJsonFromText } from "@/lib/json-utils"; import { openRouterClient } from "@/lib/openrouter-client"; import { AiEventRequestSchema, AiEventResponseSchema } from "@/lib/types"; const MODEL = process.env.AI_MODEL ?? "openai/gpt-5.4-mini"; const buildSystemPrompt = () => ` You are an assistant that converts natural language and images into an ARRAY of calendar events. TypeScript type: { id?: string, title: string, description?: string, location?: string, url?: string, 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 }[] 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().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. - If recurrence is implied (e.g. "every Monday", "daily for 10 days", "monthly on the 15th"), generate a recurrenceRule. - When analyzing images, extract ALL visible event details: titles, dates, times, locations, descriptions. - If multiple images are provided, treat them all as sources for events (e.g. multiple flyer pages). - Output ONLY valid JSON (no prose). `; const callTextOnly = async (systemPrompt: string, prompt: string) => { const result = openRouterClient.callModel({ model: MODEL, instructions: systemPrompt, input: prompt, }); const rawResponse = await result.getText(); return { rawResponse }; }; /** Extract the text content from an OpenRouter chat.send response. */ const extractContentFromChatResponse = (response: unknown): string => { if ( typeof response === "object" && response !== null && "choices" in response ) { const choices = ( response as { choices: Array<{ message: { content: string | unknown } }>; } ).choices; const content = choices?.[0]?.message?.content; if (typeof content === "string") return content; if (content) return JSON.stringify(content); } throw new Error("Unexpected response format from AI chat API"); }; const callMultimodal = async ( systemPrompt: string, prompt: string | undefined, images: string[], ) => { const messages = buildMultimodalMessages(systemPrompt, prompt, images); const response = await openRouterClient.chat.send({ chatRequest: { model: MODEL, messages, }, }); const rawResponse = extractContentFromChatResponse(response); return { rawResponse }; }; export async function POST(request: Request) { if (!isAdminAiEnabled()) { return NextResponse.json( { error: getAiDisabledMessage() }, { status: 403 }, ); } const session = await auth.api.getSession({ headers: await headers(), }); if (!session?.user) { return NextResponse.json( { error: "Authentication required" }, { status: 401 }, ); } const body = await request.json(); const parsedInput = AiEventRequestSchema.safeParse(body); if (!parsedInput.success) { return NextResponse.json( { error: "Invalid input", details: parsedInput.error.flatten().fieldErrors, }, { status: 400 }, ); } const { prompt, images } = parsedInput.data; const systemPrompt = buildSystemPrompt(); try { const result = images && images.length > 0 ? await callMultimodal(systemPrompt, prompt, images) : await callTextOnly(systemPrompt, prompt ?? ""); const rawJson = extractJsonFromText(result.rawResponse); const validated = AiEventResponseSchema.safeParse(rawJson); if (!validated.success) { console.error("AI response validation failed:", { issues: validated.error.flatten().fieldErrors, }); return NextResponse.json( { error: "AI returned invalid event data", details: validated.error.flatten().fieldErrors, }, { status: 422 }, ); } return NextResponse.json(validated.data); } catch (error) { console.error("AI Event Creation Error:", error); return NextResponse.json( { error: "Failed to process AI response. Please try again." }, { status: 500 }, ); } }