Files
Epicure/apps/web/lib/ai/features/generate-meal-plan.ts
T
Arnaud d9d58fd01a feat(ai): Vercel AI SDK provider factory with BYOK and per-use-case model prefs
Provider factory (OpenAI/Anthropic/OpenRouter/Ollama). generateObject for all outputs.
Features: recipe generation, photo-to-recipe vision, variations, drink/meal pairings,
ingredient substitution, recipe adaptation, nutrition analysis, URL import, meal plan gen.
User BYOK keys (AES-256-GCM). Per-use-case model preferences (text/vision/mealPlan).
Site-level key override via admin settings.
2026-07-01 08:10:19 +02:00

79 lines
3.1 KiB
TypeScript

import { generateObject } from "ai";
import { z } from "zod";
import { resolveModel, type AiConfig } from "../factory";
const MealPlanSchema = z.object({
entries: z.array(z.object({
day: z.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]),
mealType: z.enum(["breakfast", "lunch", "dinner"]),
recipe: z.object({
title: z.string().max(150),
description: z.string().max(300),
ingredients: z.array(z.object({
rawName: z.string(),
quantity: z.string().optional(),
unit: z.string().optional(),
})).max(20),
steps: z.array(z.object({
instruction: z.string(),
})).max(12),
prepMins: z.number().int().min(0).max(180).optional(),
cookMins: z.number().int().min(0).max(360).optional(),
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
}),
servings: z.number().int().min(1).max(20),
})),
});
export type GeneratedMealPlan = z.infer<typeof MealPlanSchema>;
const LANG: Record<string, string> = { en: "English", fr: "French" };
export async function generateMealPlan(
options: {
dietaryPrefs?: string;
servings?: number;
pantryItems?: string[];
days?: Array<"mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun">;
pantryMode?: boolean;
},
config?: AiConfig,
locale?: string
): Promise<GeneratedMealPlan> {
const model = resolveModel(config);
const lang = LANG[locale ?? "en"] ?? "English";
const servings = options.servings ?? 2;
const days = options.days ?? ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
const pantryMode = options.pantryMode ?? false;
const dietaryClause = options.dietaryPrefs?.trim()
? `Dietary requirements: ${options.dietaryPrefs.trim()}.`
: "";
const pantryClause =
options.pantryItems && options.pantryItems.length > 0
? `Available pantry ingredients to use: ${options.pantryItems.slice(0, 20).join(", ")}.`
: "";
const systemPrompt = pantryMode
? `You are a professional nutritionist and chef. Generate a meal plan that maximizes use of the provided pantry items. Prefer meals using multiple pantry ingredients. Minimize additional shopping required. Vary cuisines and cooking methods where possible. Make meals achievable for home cooks. Respond in ${lang}.`
: `You are a professional nutritionist and chef. Generate balanced, practical, and delicious weekly meal plans. Vary cuisines and cooking methods throughout the week. Make meals achievable for home cooks. Respond in ${lang}.`;
const { object } = await generateObject({
model,
schema: MealPlanSchema,
system: systemPrompt,
prompt: [
`Generate a meal plan for ${days.length} day(s): ${days.join(", ")}.`,
`Include breakfast, lunch, and dinner for each day.`,
`Plan for ${servings} servings per meal.`,
dietaryClause,
pantryClause,
pantryMode
? "Prioritize using the listed pantry ingredients across as many meals as possible. Minimize ingredients that need to be purchased."
: "Ensure nutritional balance across the week. Vary ingredients and cooking styles.",
].filter(Boolean).join(" "),
});
return object;
}