"use client"; import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { List, Plus, Pencil, Trash2, Check, X } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { useLocale } from "@/lib/i18n/provider"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; export type Conversation = { id: string; title: string | null; createdAt: string; updatedAt: string }; /** * Lists, creates, renames, and deletes the general assistant's conversations * — the homepage chat can hold more than one thread, unlike the per-recipe * chat (which stays single-threaded, one conversation per recipe, by design). */ export function ConversationMenu({ activeId, onSelect, onCreated, onDeletedActive, }: { activeId: string | null; onSelect: (id: string) => void; onCreated: (conversation: Conversation) => void; onDeletedActive: () => void; }) { const t = useTranslations("conversations"); const { locale } = useLocale(); const [open, setOpen] = useState(false); const [conversations, setConversations] = useState([]); const [loading, setLoading] = useState(false); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(""); const [deleteTargetId, setDeleteTargetId] = useState(null); const containerRef = useRef(null); const loadedRef = useRef(false); useEffect(() => { function handleClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); setRenamingId(null); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); async function loadConversations() { setLoading(true); try { const res = await fetch("/api/v1/ai/conversations"); if (!res.ok) return; const json = await res.json() as { data: Conversation[] }; setConversations(json.data); } finally { setLoading(false); } } function handleToggle() { setOpen((v) => !v); if (!loadedRef.current) { loadedRef.current = true; void loadConversations(); } } async function handleCreate() { const res = await fetch("/api/v1/ai/conversations", { method: "POST" }); if (!res.ok) { toast.error(t("createFailed")); return; } const created = await res.json() as Conversation; setConversations((prev) => [created, ...prev]); onCreated(created); setOpen(false); } function startRename(conversation: Conversation) { setRenamingId(conversation.id); setRenameValue(conversation.title ?? ""); } async function submitRename() { if (!renamingId) return; const id = renamingId; const title = renameValue.trim() || null; setRenamingId(null); const res = await fetch(`/api/v1/ai/conversations/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); if (!res.ok) { toast.error(t("renameFailed")); return; } setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c))); } async function confirmDelete() { const id = deleteTargetId; setDeleteTargetId(null); if (!id) return; const res = await fetch(`/api/v1/ai/conversations/${id}`, { method: "DELETE" }); if (!res.ok) { toast.error(t("deleteFailed")); return; } setConversations((prev) => prev.filter((c) => c.id !== id)); if (id === activeId) onDeletedActive(); } return (
{open && (
{loading &&

{t("loading")}

} {!loading && conversations.length === 0 && (

{t("empty")}

)} {!loading && conversations.map((c) => (
{renamingId === c.id ? ( <> setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void submitRename(); if (e.key === "Escape") setRenamingId(null); }} maxLength={100} className="flex-1 min-w-0 bg-transparent border-b border-primary text-sm outline-none" /> ) : ( <> )}
))}
)} { if (!v) setDeleteTargetId(null); }}> {t("deleteConfirmTitle")} {t("deleteConfirmDescription")} {t("deleteCancel")} { void confirmDelete(); }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t("deleteConfirm")}
); }