"use client"; import { useState, useRef } from "react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { Sparkles, Loader2, Camera, Type, Upload, X, Shuffle } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { FakeProgressBar } from "@/components/ui/fake-progress-bar"; const LANGUAGES = [ { code: "en", label: "English" }, { code: "fr", label: "French" }, { code: "es", label: "Spanish" }, { code: "de", label: "German" }, { code: "it", label: "Italian" }, { code: "pt", label: "Portuguese" }, { code: "ja", label: "Japanese" }, { code: "zh", label: "Chinese" }, { code: "ar", label: "Arabic" }, ]; const SURPRISE_PROMPTS = [ "A cozy one-pot dish with whatever is in my pantry on a rainy evening", "A vibrant street food recipe from Southeast Asia", "A classic French bistro dish reimagined as a weeknight dinner", "A hearty plant-based bowl with bold spices", "A 5-ingredient pasta with pantry staples", "An unexpected fusion of Japanese and Mexican flavors", "A light summer salad with fresh herbs and a citrus dressing", "A slow-cooked braise that fills the house with aroma", "A festive appetizer that looks impressive but is easy to make", "A warming spiced soup from the Middle East", "A crowd-pleasing comfort food with a twist", "A 20-minute weeknight dinner using chicken thighs", "A rich chocolate dessert with a molten center", "A crispy baked dish that tastes deep-fried", "A refreshing no-cook meal for hot summer days", ]; type Tab = "describe" | "photo"; type GeneratedRecipe = { title: string; description?: string; baseServings?: number; prepMins?: number; cookMins?: number; difficulty?: "easy" | "medium" | "hard"; dietaryTags?: Record; ingredients: Array<{ rawName: string; quantity?: string; unit?: string; note?: string }>; steps: Array<{ instruction: string; timerSeconds?: number }>; }; export function AiGenerateDialog({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { const t = useTranslations("ai.generate"); const router = useRouter(); const [tab, setTab] = useState("describe"); // describe tab const [prompt, setPrompt] = useState(""); const [language, setLanguage] = useState("en"); const [difficulty, setDifficulty] = useState<"" | "easy" | "medium" | "hard">(""); // photo tab const [photoPreview, setPhotoPreview] = useState(null); const [photoBase64, setPhotoBase64] = useState(null); const [photoMime, setPhotoMime] = useState("image/jpeg"); const fileRef = useRef(null); const [busy, setBusy] = useState(false); function handleClose() { if (busy) return; onOpenChange(false); setTimeout(() => { setPrompt(""); setPhotoPreview(null); setPhotoBase64(null); setTab("describe"); }, 200); } function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setPhotoMime(file.type); const reader = new FileReader(); reader.onload = (ev) => { const dataUrl = ev.target?.result as string; setPhotoPreview(dataUrl); // strip the data:mime;base64, prefix setPhotoBase64(dataUrl.split(",")[1] ?? null); }; reader.readAsDataURL(file); e.target.value = ""; } async function handleDescribeGenerate() { if (!prompt.trim()) return; setBusy(true); try { const res = await fetch("/api/v1/ai/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: prompt.trim(), language, difficulty: difficulty || undefined }), }); if (!res.ok) { const err = await res.json() as { error?: string }; toast.error(err.error ?? t("error")); return; } const generated = await res.json() as GeneratedRecipe; const saveRes = await fetch("/api/v1/recipes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...generated, visibility: "private", aiGenerated: true, language }), }); if (!saveRes.ok) { toast.error(t("saveError")); return; } const saved = await saveRes.json() as { id: string }; toast.success(t("success")); handleClose(); router.push(`/recipes/${saved.id}/edit`); } finally { setBusy(false); } } async function handlePhotoGenerate() { if (!photoBase64) return; setBusy(true); try { const res = await fetch("/api/v1/ai/import-photo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageBase64: photoBase64, mimeType: photoMime }), }); if (!res.ok) { let msg = t("photoError"); try { msg = ((await res.json()) as { error?: string }).error ?? msg; } catch { /* non-JSON body */ } toast.error(msg); return; } const { id } = await res.json() as { id: string }; toast.success(t("photoSuccess")); handleClose(); router.push(`/recipes/${id}/edit`); } finally { setBusy(false); } } const canSubmit = tab === "describe" ? !!prompt.trim() : !!photoBase64; return ( Generate recipe with AI Describe a dish in words or snap a photo — AI builds the full recipe. {/* Tab switcher */}
{([ { key: "describe", label: "Describe", icon: Type }, { key: "photo", label: "From photo", icon: Camera }, ] as { key: Tab; label: string; icon: React.ElementType }[]).map(({ key, label, icon: Icon }) => ( ))}
{/* Describe tab */} {tab === "describe" && (