adaa837564
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.
84 lines
2.3 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|