"use client"; import { useState, useRef, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { MessageCircle, Send, Bot, User, Maximize2, Minimize2, ChefHat, Check, X, Loader2 } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { cn } from "@/lib/utils"; import { ChatHistorySearch } from "./chat-history-search"; import { ConversationMenu, type Conversation } from "./conversation-menu"; import { ConversationSidebar } from "./conversation-sidebar"; import { ShoppingListProposalCard, type ShoppingListProposal } from "./shopping-list-proposal-card"; type RecipeProposal = { title: string; description?: string; baseServings: number; recipeType: "dish" | "drink"; difficulty?: "easy" | "medium" | "hard"; prepMins?: number; cookMins?: number; ingredients: Array<{ rawName: string; quantity?: number; unit?: string }>; steps: Array<{ instruction: string }>; }; type ProposalStatus = "pending" | "creating" | "created" | "discarded"; type Message = { role: "user" | "assistant"; content: string; proposedRecipe?: RecipeProposal; proposalStatus?: ProposalStatus; proposedShoppingList?: ShoppingListProposal; shoppingProposalStatus?: ProposalStatus; }; type HistoryEntry = { role: "user" | "assistant"; content: string }; export function CookingAssistantPanel() { const t = useTranslations("cookingChat"); const tCommon = useTranslations("common"); const router = useRouter(); const [open, setOpen] = useState(false); const [fullscreen, setFullscreen] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [conversationId, setConversationId] = useState(null); const bottomRef = useRef(null); const historyLoadedRef = useRef(false); async function loadMessages(id: string) { try { const res = await fetch(`/api/v1/ai/chat-history?conversationId=${id}&limit=50`); const json = res.ok ? (await res.json()) as { data?: HistoryEntry[] } : null; setMessages(json?.data?.length ? json.data.map((m) => ({ role: m.role, content: m.content })) : []); } catch { setMessages([]); } } function handleSelectConversation(id: string) { setConversationId(id); void loadMessages(id); } function handleConversationCreated(conversation: Conversation) { setConversationId(conversation.id); setMessages([]); } function handleActiveConversationDeleted() { // Fall back to a fresh conversation rather than leaving the panel // pointed at an id that no longer exists. fetch("/api/v1/ai/conversations", { method: "POST" }) .then((res) => (res.ok ? res.json() : null)) .then((created: Conversation | null) => { if (created) { setConversationId(created.id); setMessages([]); } }) .catch(() => {}); } useEffect(() => { if (!open || historyLoadedRef.current) return; historyLoadedRef.current = true; (async () => { const listRes = await fetch("/api/v1/ai/conversations"); const list = listRes.ok ? ((await listRes.json()) as { data: Conversation[] }).data : []; const id = list.length > 0 ? list[0]!.id : await fetch("/api/v1/ai/conversations", { method: "POST" }) .then((res) => (res.ok ? res.json() : null)) .then((created: Conversation | null) => created?.id ?? null); if (!id) return; setConversationId(id); await loadMessages(id); })().catch(() => {}); }, [open]); useEffect(() => { if (open && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages, open]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const question = input.trim(); if (!question || loading) return; setInput(""); setMessages((prev) => [...prev, { role: "user", content: question }]); setLoading(true); try { const res = await fetch("/api/v1/ai/cooking-chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question, conversationId: conversationId ?? undefined }), }); const data = await res.json() as { answer?: string; error?: string; proposedRecipe?: RecipeProposal; proposedShoppingList?: ShoppingListProposal; }; setMessages((prev) => [ ...prev, { role: "assistant", content: data.answer ?? t("sorry"), proposedRecipe: data.proposedRecipe, proposalStatus: data.proposedRecipe ? "pending" : undefined, proposedShoppingList: data.proposedShoppingList, shoppingProposalStatus: data.proposedShoppingList ? "pending" : undefined, }, ]); } catch { setMessages((prev) => [ ...prev, { role: "assistant", content: t("error") }, ]); } finally { setLoading(false); } }; async function handleCreateProposal(index: number) { const proposal = messages[index]?.proposedRecipe; if (!proposal) return; setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "creating" } : m))); try { const res = await fetch("/api/v1/recipes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: proposal.title, description: proposal.description, baseServings: proposal.baseServings, recipeType: proposal.recipeType, difficulty: proposal.difficulty, prepMins: proposal.prepMins, cookMins: proposal.cookMins, visibility: "private", aiGenerated: true, ingredients: proposal.ingredients.map((ing, i) => ({ rawName: ing.rawName, quantity: ing.quantity, unit: ing.unit, order: i })), steps: proposal.steps.map((step, i) => ({ instruction: step.instruction, order: i })), }), }); if (!res.ok) { setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "pending" } : m))); toast.error(t("proposalCreateFailed")); return; } const { id } = await res.json() as { id: string }; setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "created" } : m))); toast.success(t("proposalCreated")); router.push(`/recipes/${id}/edit`); } catch { setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "pending" } : m))); toast.error(t("proposalCreateFailed")); } } function handleDiscardProposal(index: number) { setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, proposalStatus: "discarded" } : m))); } function handleShoppingProposalStatusChange(index: number, status: ProposalStatus) { setMessages((prev) => prev.map((m, i) => (i === index ? { ...m, shoppingProposalStatus: status } : m))); } const suggestions = [ t("suggestion1"), t("suggestion2"), t("suggestion3"), t("suggestion4"), ]; return ( <> { setOpen(v); if (!v) setFullscreen(false); }}> {fullscreen && ( )}
{t("title")}
{/* On desktop fullscreen, ConversationSidebar (left) replaces this dropdown — kept for mobile fullscreen and the normal drawer, where there's no room for a persistent sidebar. */}

{t("subtitle")}

{messages.length === 0 && (

{t("intro")}

{suggestions.map((s) => ( ))}
)} {messages.map((msg, i) => (
{msg.role === "user" ? : }
{msg.role === "assistant" ? ( {msg.content} ) : ( msg.content )}
{msg.proposedRecipe && (
{msg.proposedRecipe.title}

{t("proposalSummary", { ingredients: msg.proposedRecipe.ingredients.length, steps: msg.proposedRecipe.steps.length, })}

{msg.proposalStatus === "created" ? (

{t("proposalCreated")}

) : msg.proposalStatus === "discarded" ? (

{t("proposalDiscarded")}

) : (
)}
)} {msg.proposedShoppingList && msg.shoppingProposalStatus && ( handleShoppingProposalStatusChange(i, status)} /> )}
))} {loading && (
)}
setInput(e.target.value)} placeholder={t("inputPlaceholder")} disabled={loading} className="flex-1" autoComplete="off" />
); }