feat: implement chat history deletion functionality
All checks were successful
Deploy / deploy (push) Successful in 8m4s

Added a new DELETE endpoint to allow users to clear their chat history. This functionality ensures that while the chat messages are removed, any credits spent remain intact. Additionally, updated the ListemGovde component to reflect changes in user preferences and integrated a modal for notifying users about preference changes. Enhanced the SohbetClient component to include a button for clearing chat history, improving user experience and interface clarity.
This commit is contained in:
bilalgursen
2026-08-09 04:44:19 +03:00
parent 8870845fa1
commit 15e2c4c961
8 changed files with 591 additions and 89 deletions

View File

@@ -4,7 +4,7 @@ import { startTransition, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { AlertTriangle, ArrowUp, Coins, Lock, Sparkles } from "lucide-react";
import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
@@ -13,6 +13,14 @@ import {
ChatContainerRoot,
ChatContainerScrollAnchor,
} from "@/components/ui/chat-container";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Message, MessageContent } from "@/components/ui/message";
import { preloadMarkdown } from "@/components/ui/markdown-lazy";
import { onbellekliJsonGetir } from "@/lib/istek-cache";
@@ -24,6 +32,12 @@ import {
} from "@/components/ui/prompt-input";
import { PromptSuggestion } from "@/components/ui/prompt-suggestion";
import { ScrollButton } from "@/components/ui/scroll-button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DotsLoader } from "@/components/ui/loader";
import { Skeleton } from "@/components/ui/skeleton";
import { olay } from "@/lib/analitik";
@@ -36,10 +50,7 @@ export interface Mesaj {
export interface DanismanOzeti {
metin: string;
uyarilar?: string[];
kilitli?: boolean;
/** Paketsizde açık uyarının ardından kilitli kalan uyarı sayısı. */
kilitliUyariSayisi?: number;
}
/** Danışman mesajlarının avatarı — KolayTercih markası. */
@@ -76,6 +87,7 @@ export function SohbetClient({
initialMesajlar,
className,
ozet,
bildikleri,
}: {
baslangicKredi: number;
/** Sunucu tarafında çekilmiş geçmiş; verilirse mount fetch'i atlanır */
@@ -84,12 +96,17 @@ export function SohbetClient({
className?: string;
/** Raporun genel değerlendirmesi; sohbetin açılış mesajı olarak gösterilir */
ozet?: DanismanOzeti;
/** Danışmanın prompt'unda gerçekten yer alan bağlam maddeleri; başlıktaki
* "Danışmanın bildikleri" ipucunda listelenir. Boşsa ipucu çizilmez. */
bildikleri?: string[];
}) {
const [mesajlar, setMesajlar] = useState<Mesaj[]>(initialMesajlar ?? []);
const [girdi, setGirdi] = useState("");
const [bekliyor, setBekliyor] = useState(false);
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
const [kredi, setKredi] = useState(baslangicKredi);
const [temizleAcik, setTemizleAcik] = useState(false);
const [temizleniyor, setTemizleniyor] = useState(false);
const router = useRouter();
// Markdown chunk'ını sohbet görünür olur olmaz ısıt: ilk danışman cevabı
@@ -206,6 +223,29 @@ export function SohbetClient({
}
}
/**
* Sohbet geçmişini hem sunucudan hem ekrandan siler. Krediler iade
* edilmez; danışman bir sonraki soruda sıfır geçmişle başlar (rapor özeti
* ve liste bağlamı prompt'tan geldiği için korunur).
*/
async function sohbetiTemizle() {
if (temizleniyor) return;
setTemizleniyor(true);
try {
const res = await fetch("/api/soru", { method: "DELETE" });
if (!res.ok) throw new Error("silinemedi");
setMesajlar([]);
setTemizleAcik(false);
// Sunucudan gelen geçmiş (listem sayfasının initialMesajlar'ı) da boşalsın
router.refresh();
toast.success("Sohbet temizlendi.");
} catch {
toast.error("Sohbet temizlenemedi, tekrar dener misin?");
} finally {
setTemizleniyor(false);
}
}
const krediYok = kredi < 1;
return (
@@ -213,14 +253,57 @@ export function SohbetClient({
aria-label="Danışman sohbeti"
className={cn(
"flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white",
className ?? "h-[calc(100dvh-16rem)] min-h-[400px]",
className ?? "h-[calc(100dvh-24rem)] min-h-[340px]",
)}
>
<div className="flex items-center justify-between border-b border-slate-100 px-4 py-3">
<h2 className="font-heading text-base font-bold">Danışmanla konuş</h2>
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3">
{bildikleri?.length ? (
<TooltipProvider delayDuration={120}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Danışmanın bildikleri"
className="inline-flex size-7 cursor-help items-center justify-center rounded-full border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-800"
>
<Info className="size-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="max-w-xs flex-col items-start gap-1.5 p-3 text-left leading-relaxed"
>
<p className="font-semibold">
Danışmanın bildikleri — cevaplar bu bağlamla üretilir:
</p>
<ul className="flex list-disc flex-col gap-1 pl-4">
{bildikleri.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
<p className="text-background/70">
Adın, e-postan ve netlerin danışmanla paylaşılmaz.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
{mesajlar.length > 0 ? (
<button
type="button"
onClick={() => setTemizleAcik(true)}
disabled={bekliyor || temizleniyor}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-semibold text-slate-600 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-50"
title="Sohbet geçmişini sil"
>
<Trash2 className="size-3.5" aria-hidden />
Sohbeti temizle
</button>
) : null}
<Link
href="/paket"
className="inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 transition-colors hover:bg-amber-100"
className="ml-auto inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 transition-colors hover:bg-amber-100"
title="Kredilerin — yüklemek için tıkla"
>
<Coins className="size-3.5" aria-hidden />
@@ -228,6 +311,36 @@ export function SohbetClient({
</Link>
</div>
<Dialog open={temizleAcik} onOpenChange={setTemizleAcik}>
<DialogContent>
<DialogHeader>
<DialogTitle>Sohbeti temizle</DialogTitle>
<DialogDescription>
Danışmanla olan tüm yazışman silinir ve geri getirilemez. Listen
ve kredilerin etkilenmez; yeni bir soruyla sıfırdan devam
edebilirsin.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
className="cursor-pointer"
onClick={() => setTemizleAcik(false)}
disabled={temizleniyor}
>
Vazgeç
</Button>
<Button
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
onClick={sohbetiTemizle}
disabled={temizleniyor}
>
{temizleniyor ? <DotsLoader size="sm" /> : "Sohbeti temizle"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="relative flex-1 overflow-hidden">
<ChatContainerRoot className="h-full">
<ChatContainerContent className="gap-4 p-4">
@@ -241,36 +354,10 @@ export function SohbetClient({
<MessageContent className="bg-slate-100 text-slate-800">
{ozet.metin}
</MessageContent>
{ozet.uyarilar?.length ? (
<ul className="flex flex-col gap-2">
{ozet.uyarilar.map((uyari) => (
<li
key={uyari}
className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900"
>
<AlertTriangle
className="mt-0.5 size-4 shrink-0"
aria-hidden
/>
{uyari}
</li>
))}
</ul>
) : null}
{ozet.kilitli ? (
<p className="flex items-center gap-1.5 text-xs text-slate-500">
<Lock className="size-3.5" aria-hidden />
{ozet.kilitliUyariSayisi ? (
<>
{ozet.kilitliUyariSayisi} uyarı daha kilitli —
sıralamana özel uyarıların pakette.
</>
) : (
<>
Değerlendirmenin tamamı ve sıralamana özel uyarılar
pakette.
</>
)}
Değerlendirmenin tamamı pakette.
</p>
) : null}
</div>