feat: regenerate recipe with modifications from the editor
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
This commit is contained in:
@@ -24,6 +24,8 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { DietaryTagPicker } from "./dietary-tag-picker";
|
||||
import { PhotoUploader, type PhotoEntry } from "./photo-uploader";
|
||||
import { RegenerateRecipeButton } from "./regenerate-recipe-button";
|
||||
import type { RegeneratedRecipe } from "@/lib/ai/features/regenerate-recipe";
|
||||
|
||||
type DietaryTags = {
|
||||
vegan?: boolean;
|
||||
@@ -217,6 +219,31 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
setIngredients((prev) => prev.filter((_, idx) => idx !== i));
|
||||
}
|
||||
|
||||
// Merges an AI-regenerated draft into the in-progress form state — it
|
||||
// never touches the DB directly, so the user still has to hit Save.
|
||||
function applyRegenerated(recipe: RegeneratedRecipe) {
|
||||
setTitle(recipe.title);
|
||||
if (recipe.description !== undefined) setDescription(recipe.description);
|
||||
setBaseServings(String(recipe.baseServings));
|
||||
if (recipe.difficulty) setDifficulty(recipe.difficulty);
|
||||
setPrepMins(recipe.prepMins !== undefined ? String(recipe.prepMins) : "");
|
||||
setCookMins(recipe.cookMins !== undefined ? String(recipe.cookMins) : "");
|
||||
if (recipe.dietaryTags) setDietaryTags(recipe.dietaryTags);
|
||||
setIngredients(recipe.ingredients.map((ing) => ({
|
||||
id: crypto.randomUUID(),
|
||||
rawName: ing.rawName,
|
||||
quantity: ing.quantity !== undefined ? String(ing.quantity) : "",
|
||||
unit: ing.unit ?? "",
|
||||
note: ing.note ?? "",
|
||||
})));
|
||||
setSteps(recipe.steps.map((step) => ({
|
||||
id: crypto.randomUUID(),
|
||||
instruction: step.instruction,
|
||||
timerSeconds: step.timerSeconds !== undefined ? String(step.timerSeconds) : "",
|
||||
appliesTo: [],
|
||||
})));
|
||||
}
|
||||
|
||||
function addTag(raw: string) {
|
||||
const tag = raw.trim().toLowerCase().slice(0, 50);
|
||||
if (!tag || tags.includes(tag) || tags.length >= 20) return;
|
||||
@@ -392,6 +419,21 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
|
||||
<form onSubmit={handleSubmit} className="space-y-8 max-w-3xl pb-20">
|
||||
{/* Basic info */}
|
||||
<div className="space-y-4">
|
||||
{isEdit && (
|
||||
<div className="flex justify-end">
|
||||
<RegenerateRecipeButton
|
||||
current={{
|
||||
title,
|
||||
description,
|
||||
baseServings: Number(baseServings) || 1,
|
||||
difficulty,
|
||||
ingredients: ingredients.map((ing) => ({ rawName: ing.rawName, quantity: ing.quantity, unit: ing.unit })),
|
||||
steps: steps.map((step) => ({ instruction: step.instruction })),
|
||||
}}
|
||||
onRegenerated={applyRegenerated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">{t("titleLabel")}</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user