b330583dd3
URL import extracts content in the source page's own language (no translation), but never recorded what that language was, so the Translate button's "recipe.language !== my locale" check always saw null and showed the button unconditionally. The extractor now returns a detected ISO 639-1 language code alongside the recipe. Photo import always writes in the caller's locale already (see its langInstruction) but never recorded that either — now sets it directly to locale since it's known, not detected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
2.6 KiB
TypeScript
55 lines
2.6 KiB
TypeScript
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<typeof ImportedRecipeSchema>;
|
|
|
|
export async function importFromUrl(url: string, config?: AiConfig): Promise<ImportedRecipe> {
|
|
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(/<script[\s\S]*?<\/script>/gi, "")
|
|
.replace(/<style[\s\S]*?<\/style>/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;
|
|
}
|