diff --git a/data/app.db b/data/app.db
index 6fd4f28..e6f6f63 100644
Binary files a/data/app.db and b/data/app.db differ
diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts
index d77d610..305f284 100644
--- a/src/app/api/soru/route.ts
+++ b/src/app/api/soru/route.ts
@@ -105,6 +105,20 @@ export async function GET() {
});
}
+// Kullanıcının sohbet geçmişini siler. Kredi iadesi YOK: harcanan krediler
+// ledger'da kalır, yalnızca mesajlar temizlenir (bir sonraki soruda danışman
+// sıfırdan başlar; liste/özet bağlamı prompt'tan geldiği için korunur).
+export async function DELETE() {
+ const session = await getSession();
+ if (!session) {
+ return Response.json({ error: "Oturum gerekli." }, { status: 401 });
+ }
+ await appDb
+ .delete(schema.chatMessages)
+ .where(eq(schema.chatMessages.userId, session.user.id));
+ return Response.json({ ok: true });
+}
+
export async function POST(request: NextRequest) {
// Oturum ve gövde ayrıştırma bağımsız — paralel başlar, hata önceliği aynı
// kalır (önce 401, sonra 400).
diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx
index bf7c2ea..63050be 100644
--- a/src/features/rapor/components/listem-govde.tsx
+++ b/src/features/rapor/components/listem-govde.tsx
@@ -13,27 +13,37 @@ import {
FileText,
Lock,
PencilLine,
+ RefreshCcw,
ShieldCheck,
SlidersHorizontal,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { RaporListesi } from "./rapor-listesi";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
+import { tercihFarki } from "@/features/rapor/tercih-farki";
+import { TercihDegistiModali } from "./tercih-degisti-modali";
import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy";
import { YurtdisiHaritalarLazy } from "@/features/liste/components/yurtdisi-haritalar-lazy";
import { pinleriTuret } from "@/lib/harita-pinler";
-import type { RaporSonuc } from "@/features/rapor/types/rapor";
+import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor";
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
import { RevizyonKutusu } from "./revizyon-kutusu";
-import { profilDuzenlemeyiAc } from "@/features/liste/hooks/use-tercih-profili";
+import {
+ profilDuzenlemeyiAc,
+ useTercihProfili,
+} from "@/features/liste/hooks/use-tercih-profili";
+import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
+import { PUAN_TURLERI } from "@/types/yokatlas";
export function ListemGovde({
rapor,
+ params,
adaySira,
hasPaket,
kilitAcilisi,
kilitliBaslangic,
kredi,
+ raporKredi,
sohbetGecmisi,
ozet,
kalanHak,
@@ -41,6 +51,9 @@ export function ListemGovde({
paketKredi,
}: {
rapor: RaporSonuc;
+ /** Kayıtlı listenin üretildiği tercihler — başlıktaki butonun "düzenle" mi
+ * "güncelle" mi olacağı bunun güncel profille farkından çıkar. */
+ params: RaporParams;
adaySira: number;
hasPaket: boolean;
/** Ödeme dönüşü: blur çözülme animasyonuyla aç */
@@ -48,6 +61,9 @@ export function ListemGovde({
/** Paketsizde ACIK_SATIR; paketlide undefined (tamamı açık) */
kilitliBaslangic?: number;
kredi: number;
+ /** Bir liste üretiminin kredi bedeli — fark modalındaki güncelleme aynı
+ * kapıdan geçtiği için aynı bedeli gösterir. */
+ raporKredi: number;
sohbetGecmisi: Mesaj[];
ozet?: DanismanOzeti;
kalanHak: number;
@@ -71,6 +87,44 @@ export function ListemGovde({
const acikSatir = kilitliBaslangic ?? rapor.tercihler.length;
+ // Profil client'ta (localStorage) çözülür: ilk render'da null döner, yani
+ // SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı
+ // olmaz, fark varsa buton sonraki render'da "güncelle"ye döner.
+ const profil = useTercihProfili();
+ const farkVar = useMemo(
+ () => (profil ? tercihFarki(params, profil).length > 0 : false),
+ [params, profil],
+ );
+ const [farkModali, setFarkModali] = useState(false);
+
+ // Danışmanın sistem prompt'una gerçekten giren bağlam (bkz. app/api/soru).
+ // Sohbet başlığındaki "Danışmanın bildikleri" ipucunda gösterilir; prompt
+ // değişirse bu liste de güncellenmeli.
+ const danismaninBildikleri = useMemo(() => {
+ const satirlar = [
+ `Başarı sıralaman ${adaySira.toLocaleString("tr-TR")} ve puan türün ${PUAN_TURLERI[params.tur]}`,
+ hasPaket
+ ? "24'lük listenin tamamı: program, üniversite, şehir, taban sıralaması ve risk etiketi"
+ : `Listenin açık ilk ${acikSatir} satırı — kilitli tercihleri danışman da görmüyor`,
+ ];
+ if (params.kategoriler?.length) {
+ satirlar.push(`İlgi alanların: ${params.kategoriler.join(", ")}`);
+ }
+ if (params.iller?.length) {
+ satirlar.push(`Seçtiğin iller: ${params.iller.join(", ")}`);
+ }
+ if (params.universiteTipi && params.universiteTipi !== "farketmez") {
+ satirlar.push(
+ `Üniversite tipi tercihin: ${UNIVERSITE_TIPI_ETIKET[params.universiteTipi]}`,
+ );
+ }
+ if (params.oncelikler?.length) {
+ satirlar.push(`Önceliklerin: ${params.oncelikler.join(", ")}`);
+ }
+ satirlar.push("Bu sohbetteki son mesajların");
+ return satirlar;
+ }, [adaySira, params, hasPaket, acikSatir]);
+
// Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez).
// Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları
// için observer hiç kurulmaz.
@@ -126,18 +180,37 @@ export function ListemGovde({
{/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte
- TercihProfiliKapisi) profili günceller, /listem'e döner ve
- değişiklik varsa "tercihlerin değişti" uyarısı karşılar. */}
-
+ TercihProfiliKapisi) profili günceller, /listem'e döner.
+ Profil kayıtlı listeden ayrıldığı anda bu buton "Tercihlerin
+ değişmiş"e döner ve TercihDegistiModali'nı açar — fark
+ dökümü ve kredi kapısı orada tek yerde yaşar.
+
+ Renk: varsayılan (primary) buton. Kardeşleri outline olduğu
+ için tek dolu buton olması dikkati zaten çekiyor; turuncu
+ bu sayfada paywall CTA'sının rengi, amber ise risk dilinin
+ "az riskli" tonu — ikisi de burada yanlış sinyal olurdu. */}
+ {farkVar ? (
+
+ ) : (
+
+ )}
{hasPaket ? (
<>
) : (
+ {hasPaket
+ ? `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Kredi yüklediğinde listen ve sohbet geçmişin duruyor, kaldığın yerden devam edersin.`
+ : `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Tercih Dönemi Paketi'yle kredilerin, tam liste, PDF ve revizyon hakları birlikte gelir.`}
+
+
+
+ );
+}
diff --git a/src/features/rapor/components/uretim-mesaji.ts b/src/features/rapor/components/uretim-mesaji.ts
index 11d8715..cba71a0 100644
--- a/src/features/rapor/components/uretim-mesaji.ts
+++ b/src/features/rapor/components/uretim-mesaji.ts
@@ -1,7 +1,7 @@
"use client";
// Liste üretiminin bekleme metinleri tek kaynakta: ilk kurulum (ListeUretici)
-// ve tercih değişikliği sonrası yeniden kurulum (TercihDegistiUyarisi) aynı
+// ve tercih değişikliği sonrası yeniden kurulum (TercihDegistiModali) aynı
// dili ve ritmi paylaşsın diye.
import { useEffect, useState } from "react";
diff --git a/src/features/rapor/tercih-farki.ts b/src/features/rapor/tercih-farki.ts
index 12897ef..0ca77c2 100644
--- a/src/features/rapor/tercih-farki.ts
+++ b/src/features/rapor/tercih-farki.ts
@@ -1,6 +1,6 @@
// Kayıtlı listenin üretildiği tercihler (rapor params) ile kullanıcının şu
-// anki tercih profilini karşılaştırır. /listem'deki "tercihlerin değişti"
-// uyarısı ve güncelleme CTA'sı bu farktan doğar.
+// anki tercih profilini karşılaştırır. /listem başlığındaki "Tercihlerin
+// değişmiş" butonu ve açtığı güncelleme modalı bu farktan doğar.
//
// İzomorfik: yalnızca tip ve etiket sabitlerine bağlı — hem client bileşeni
// hem sunucu tarafı tüketebilir.
@@ -35,20 +35,6 @@ export const FARK_ETIKET: Record = {
oncelikler: "önceliklerin",
};
-/** Sıra/tür/seçim üçlüsünün kararlı imzası — uyarının "bu değişikliği
- * gizle" kaydı bu imzaya bağlanır (tercih yine değişirse uyarı geri gelir). */
-export function profilImzasi(profil: TercihProfili): string {
- const s = profil.secimler;
- return JSON.stringify([
- Math.round(profil.sira),
- profil.tur,
- [...s.kategoriler].sort(),
- [...s.iller].sort(),
- s.universiteTipi,
- [...s.oncelikler].sort(),
- ]);
-}
-
// Sıra bağımsız küme kıyası: sihirbaz seçimleri tekrarsız olarak doğrulanır
// (bkz. sihirbazDogrula), aynı seçimlerin farklı sırayla kaydedilmesi
// "değişiklik" değildir.
@@ -88,13 +74,6 @@ export function tercihFarki(
return farklar;
}
-/** "başarı sıralaman ve ilgi alanların" — uyarı cümlesinin ortası. */
-export function farkCumlesi(farklar: FarkAlani[]): string {
- const etiketler = farklar.map((f) => FARK_ETIKET[f]);
- if (etiketler.length <= 1) return etiketler[0] ?? "";
- return `${etiketler.slice(0, -1).join(", ")} ve ${etiketler[etiketler.length - 1]}`;
-}
-
export type FarkSatiri = { alan: FarkAlani; etiket: string; eski: string; yeni: string };
/** Değişen her alanın eski → yeni değeri; uyarıda tek tek gösterilir. */