feat: support multiple image uploads for AI event generation 🖼️

- 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
This commit is contained in:
2026-04-08 20:46:43 -04:00
parent cac201a4d2
commit 513aafcebc
18 changed files with 881 additions and 238 deletions

View File

@@ -1,6 +1,7 @@
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import { buildMultimodalMessages } from "@/lib/ai-event-messages";
import { extractJsonFromText } from "@/lib/json-utils";
import { openRouterClient } from "@/lib/openrouter-client";
import { AiEventRequestSchema, AiEventResponseSchema } from "@/lib/types";
@@ -31,7 +32,8 @@ Rules:
- 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 an image, extract ALL visible event details: titles, dates, times, locations, descriptions.
- 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).
`;
@@ -68,27 +70,9 @@ const extractContentFromChatResponse = (response: unknown): string => {
const callMultimodal = async (
systemPrompt: string,
prompt: string | undefined,
imageBase64: string,
images: string[],
) => {
const messages = [
{
role: "system" as const,
content: systemPrompt,
},
{
role: "user" as const,
content: [
{
type: "text" as const,
text: prompt || "Extract all calendar events from this image.",
},
{
type: "image_url" as const,
imageUrl: { url: imageBase64 },
},
],
},
];
const messages = buildMultimodalMessages(systemPrompt, prompt, images);
const response = await openRouterClient.chat.send({
chatRequest: {
@@ -126,13 +110,14 @@ export async function POST(request: Request) {
);
}
const { prompt, imageBase64 } = parsedInput.data;
const { prompt, images } = parsedInput.data;
const systemPrompt = buildSystemPrompt();
try {
const result = imageBase64
? await callMultimodal(systemPrompt, prompt, imageBase64)
: await callTextOnly(systemPrompt, prompt ?? "");
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);