2154512e54
Recipe cards, the recipe detail page, and meal planner still rendered raw
"{n} servings"/"{n}m cook"/"{n} srv" text and untranslated difficulty labels
outside the earlier i18n pass. Also wires the user's locale into AI features
that previously always answered in English regardless of app language:
recipe chat, ingredient substitution, recipe ideas, and generate-from-idea.
The recipe-language picker in the AI generate dialog now defaults to the
user's locale instead of always defaulting to English.
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import { useRouter } from "next/navigation";
|
|
import { Sparkles, Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import { FakeProgressBar } from "@/components/ui/fake-progress-bar";
|
|
import { useLocale } from "@/lib/i18n/provider";
|
|
|
|
type GeneratedRecipe = {
|
|
ingredients: Array<{ rawName: string; quantity?: number; unit?: string; note?: string }>;
|
|
steps: Array<{ instruction: string; timerSeconds?: number }>;
|
|
};
|
|
|
|
export function GenerateContentButton({
|
|
recipeId,
|
|
title,
|
|
description,
|
|
}: {
|
|
recipeId: string;
|
|
title: string;
|
|
description?: string | null;
|
|
}) {
|
|
const t = useTranslations("ai.generate");
|
|
const router = useRouter();
|
|
const locale = useLocale();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function generate() {
|
|
setBusy(true);
|
|
try {
|
|
const prompt = description?.trim()
|
|
? `${title}: ${description.trim()}`
|
|
: title;
|
|
|
|
const genRes = await fetch("/api/v1/ai/generate", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ prompt, language: locale }),
|
|
});
|
|
|
|
if (!genRes.ok) {
|
|
toast.error(t("contentGenerateFailed"));
|
|
return;
|
|
}
|
|
|
|
const generated = await genRes.json() as GeneratedRecipe;
|
|
|
|
const saveRes = await fetch(`/api/v1/recipes/${recipeId}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
ingredients: generated.ingredients.map((ing, i) => ({
|
|
rawName: ing.rawName,
|
|
quantity: ing.quantity,
|
|
unit: ing.unit,
|
|
note: ing.note,
|
|
order: i,
|
|
})),
|
|
steps: generated.steps.map((s, i) => ({
|
|
instruction: s.instruction,
|
|
timerSeconds: s.timerSeconds,
|
|
order: i,
|
|
})),
|
|
}),
|
|
});
|
|
|
|
if (!saveRes.ok) {
|
|
toast.error(t("contentSaveFailed"));
|
|
return;
|
|
}
|
|
|
|
toast.success(t("contentGenerated"));
|
|
router.refresh();
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-3">
|
|
<FakeProgressBar active={busy} durationMs={10000} label={busy ? "Generating recipe content…" : undefined} />
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => { void generate(); }}
|
|
disabled={busy}
|
|
className="gap-1.5"
|
|
>
|
|
{busy ? (
|
|
<><Loader2 className="h-4 w-4 animate-spin" />Generating…</>
|
|
) : (
|
|
<><Sparkles className="h-4 w-4" />Generate with AI</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|