25e624f618
Every existing AI entry point (generate, generate-from-idea, adapt, variations) either creates a new recipe or a saved variation — none let you revise the draft you're currently editing in place. Adds a stateless /api/v1/ai/regenerate endpoint that takes the editor's current in-progress fields (not a recipeId, so unsaved edits are included) plus a free-text instruction, and returns a full revised draft the editor merges into its own state. No DB write happens; the user still saves normally. v0.42.0
62 lines
2.8 KiB
TypeScript
62 lines
2.8 KiB
TypeScript
import { generateObject } from "ai";
|
|
import { z } from "zod";
|
|
import { resolveModel, type AiConfig } from "../factory";
|
|
import { dietaryTagsSchema, ingredientSchema, stepSchema } from "./recipe-schema";
|
|
|
|
const RegeneratedRecipeSchema = z.object({
|
|
title: z.string(),
|
|
description: z.string().optional(),
|
|
baseServings: z.number().int().min(1).max(100),
|
|
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.number())),
|
|
steps: z.array(stepSchema),
|
|
});
|
|
|
|
export type RegeneratedRecipe = z.infer<typeof RegeneratedRecipeSchema>;
|
|
|
|
const LANG: Record<string, string> = { en: "English", fr: "French" };
|
|
|
|
/** Revises a recipe draft already open in the editor per a free-text
|
|
* instruction — "add more spice", "make it vegetarian", "double the yield".
|
|
* Unlike adaptRecipe/suggestVariations, this never creates a new recipe or
|
|
* variation record; it returns a full replacement draft for the caller to
|
|
* merge into the in-progress form state, which the user still has to save. */
|
|
export async function regenerateRecipe(
|
|
current: {
|
|
title: string;
|
|
description?: string | null;
|
|
baseServings: number;
|
|
difficulty?: string | null;
|
|
ingredients: Array<{ rawName: string; quantity?: string | number | null; unit?: string | null }>;
|
|
steps: Array<{ instruction: string }>;
|
|
},
|
|
instruction: string,
|
|
config?: AiConfig & { language?: string }
|
|
): Promise<RegeneratedRecipe> {
|
|
const model = resolveModel(config);
|
|
const lang = LANG[config?.language ?? "en"] ?? "English";
|
|
const langInstruction = lang !== "English" ? ` Write the entire recipe (title, description, ingredient names, step instructions) in ${lang}.` : "";
|
|
|
|
const currentText = [
|
|
`Title: ${current.title}`,
|
|
current.description ? `Description: ${current.description}` : "",
|
|
`Servings: ${current.baseServings}`,
|
|
current.difficulty ? `Difficulty: ${current.difficulty}` : "",
|
|
`Ingredients:\n${current.ingredients.map((i) => ` - ${i.quantity ? `${i.quantity} ` : ""}${i.unit ? `${i.unit} ` : ""}${i.rawName}`).join("\n")}`,
|
|
`Steps:\n${current.steps.map((s, i) => ` ${i + 1}. ${s.instruction}`).join("\n")}`,
|
|
].filter(Boolean).join("\n");
|
|
|
|
const { object } = await generateObject({
|
|
model,
|
|
schema: RegeneratedRecipeSchema,
|
|
system:
|
|
`You are revising an existing recipe draft per the user's instruction. Keep everything unchanged except what the instruction requires you to change — this is an edit, not a fresh rewrite. Return the complete updated recipe, not just the changed parts. For ingredient quantities: use numbers only, units separately.${langInstruction}`,
|
|
prompt: `Current recipe draft:\n${currentText}\n\nInstruction: ${instruction}`,
|
|
});
|
|
|
|
return object;
|
|
}
|