import { generateObject } from "ai"; import { z } from "zod"; import { resolveModel, type AiConfig } from "../factory"; import { safeFetch } from "@/lib/validate-webhook-url"; import { dietaryTagsSchema, ingredientSchema, stepSchema } from "./recipe-schema"; const ImportedRecipeSchema = z.object({ recipeType: z.enum(["dish", "drink"]).describe("\"drink\" only for a beverage/cocktail (no cooking step) — \"dish\" for anything else, including if unsure."), language: z.string().min(2).max(10).describe("ISO 639-1 code of the language the recipe content (title/ingredients/steps) is actually written in, e.g. \"en\", \"fr\" — the source page's language, not a language to translate into."), title: z.string(), description: z.string().optional(), baseServings: z.number().int().min(1).optional(), prepMins: z.number().int().min(0).optional(), cookMins: z.number().int().min(0).optional(), difficulty: z.enum(["easy", "medium", "hard"]).optional(), dietaryTags: dietaryTagsSchema.optional(), ingredients: z.array(ingredientSchema(z.string())), steps: z.array(stepSchema), }); export type ImportedRecipe = z.infer; export async function importFromUrl(url: string, config?: AiConfig): Promise { const res = await safeFetch(url, { headers: { "User-Agent": "Mozilla/5.0 (compatible; Epicure/1.0; recipe importer)" }, signal: AbortSignal.timeout(10000), }); if (!res.ok) throw new Error(`Failed to fetch URL: ${res.status}`); const html = await res.text(); // strip scripts/styles, take first 30k chars to stay within context const cleaned = html .replace(//gi, "") .replace(//gi, "") .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .slice(0, 30000); const model = resolveModel(config); const { object } = await generateObject({ model, schema: ImportedRecipeSchema, system: "You are an expert at extracting structured recipe data from web page text. Extract all recipe information accurately, in the same language as the source page — do not translate it. For ingredients, separate quantity, unit, and name. For steps, extract timer durations in seconds when mentioned. Set recipeType to \"drink\" only for a beverage/cocktail with no cooking involved — never set cookMins for a drink. Set language to the ISO 639-1 code of the page's actual language.", prompt: `Extract the recipe from this web page:\n\nURL: ${url}\n\nContent:\n${cleaned}`, }); if (object.recipeType === "drink") { return { ...object, cookMins: undefined }; } return object; }