Files
Epicure/apps/web/components/recipe/clear-chat-history-button.tsx
T
Arnaud adaa837564 feat: clear chat history manually, auto-expire old history after 90 days
Manual: DELETE /api/v1/ai/chat-history (scoped by recipeId or scope=general,
matching GET's scoping) plus a trash-icon button + confirm dialog in both
chat panels' headers.

Automatic: new internal cron endpoint (chat-cleanup), same shared-secret
pattern as the existing leftover-reminders/weekly-digest crons, deletes
any chat_messages older than 90 days. Wired into cron/crontab (daily,
03:00 UTC) and the Dockerfile's cron stage.

Verified locally: cleared a real conversation through the actual UI and
confirmed it didn't come back on reopen (not just cleared client-side);
inserted a 100-day-old and a 5-day-old message directly, called the cron
endpoint with the real shared-secret check, confirmed only the old one
was deleted and the recent one survived; confirmed the endpoint 401s with
no/wrong secret.
2026-07-12 23:27:05 +02:00

84 lines
2.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { Trash2 } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
export function ClearChatHistoryButton({
recipeId,
disabled,
onCleared,
}: {
recipeId?: string;
disabled?: boolean;
onCleared: () => void;
}) {
const t = useTranslations("chatHistory");
const [confirmOpen, setConfirmOpen] = useState(false);
const [clearing, setClearing] = useState(false);
async function handleClear() {
setClearing(true);
try {
const params = new URLSearchParams();
if (recipeId) params.set("recipeId", recipeId);
else params.set("scope", "general");
const res = await fetch(`/api/v1/ai/chat-history?${params.toString()}`, { method: "DELETE" });
if (!res.ok) {
toast.error(t("clearFailed"));
return;
}
onCleared();
toast.success(t("cleared"));
} catch {
toast.error(t("clearFailed"));
} finally {
setClearing(false);
setConfirmOpen(false);
}
}
return (
<>
<button
type="button"
onClick={() => setConfirmOpen(true)}
disabled={disabled || clearing}
className="p-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground disabled:opacity-40"
aria-label={t("clearAriaLabel")}
>
<Trash2 className="h-4 w-4" />
</button>
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("clearConfirmTitle")}</AlertDialogTitle>
<AlertDialogDescription>{t("clearConfirmDescription")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("clearCancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => { void handleClear(); }}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{t("clearConfirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}