feat(revizyon): revizyon sayısı sınırı kaldırıldı — tek koşul paket + sohbet kredisi
MAX_REVIZYON silindi; liste-revizyon.ts'teki HAK kapısı ve RevizyonSonucu'nun "HAK" kodu kalktı, ozetCikar kalanHak döndürmüyor (sohbet-meta tipi de). reports.revision_count sayaç olarak kalır (göç yok). /listem: "Revize et" düğmesi paketlide hep görünür, "kalan hak" ifadeleri kalktı; sohbet-client prop'u 0/1 bayrak olarak besleniyor (entegrasyonda revizyonAcik olacak). Pazarlama metni: "Sınırsız liste revizyonu (danışmanla sohbetten)" (ana sayfa ve paket kartı). Paketsiz-kredili adayda paket şartı korunur. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
SectionEyebrow,
|
||||
} from "@/components/pixel-decor";
|
||||
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
||||
import { URUNLER } from "@/lib/credits";
|
||||
import {
|
||||
PROGRAM_SAYISI_CUMLE,
|
||||
PROGRAM_SAYISI_METNI,
|
||||
@@ -244,7 +244,7 @@ const paketOzellikleri = [
|
||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||
},
|
||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||
{ icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` },
|
||||
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
|
||||
{
|
||||
icon: MessageCircleQuestion,
|
||||
text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`,
|
||||
|
||||
@@ -34,7 +34,7 @@ const OZELLIKLER = [
|
||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||
},
|
||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },
|
||||
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
|
||||
{
|
||||
icon: MessageCircleQuestion,
|
||||
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
|
||||
|
||||
@@ -75,7 +75,6 @@ export function ListemGovde({
|
||||
danismanProfili,
|
||||
ozet,
|
||||
ilkYorum,
|
||||
kalanHak,
|
||||
paketFiyati,
|
||||
paketKredi,
|
||||
}: {
|
||||
@@ -99,7 +98,6 @@ export function ListemGovde({
|
||||
ozet?: DanismanOzeti;
|
||||
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
||||
ilkYorum?: IlkTercihYorumu;
|
||||
kalanHak: number;
|
||||
paketFiyati: string;
|
||||
paketKredi: number;
|
||||
}) {
|
||||
@@ -216,12 +214,12 @@ export function ListemGovde({
|
||||
}
|
||||
if (hasPaket) {
|
||||
satirlar.push(
|
||||
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar (kalan hak: ${kalanHak})`,
|
||||
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar`,
|
||||
);
|
||||
}
|
||||
satirlar.push("Bu sohbetteki son mesajların");
|
||||
return satirlar;
|
||||
}, [adaySira, params, hasPaket, acikSatir, kalanHak]);
|
||||
}, [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ı
|
||||
@@ -331,18 +329,16 @@ export function ListemGovde({
|
||||
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
|
||||
kutusunu odaklar, aday ne değişsin istediğini yazar;
|
||||
danışman gerekirse soru formuyla netleştirip listeyi
|
||||
yeniden kurar. Hak bitince düğme kalkar. */}
|
||||
{kalanHak > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => sohbetiOdakla("Listemde şunu değiştir: ")}
|
||||
className={IKINCIL_BUTON}
|
||||
>
|
||||
<PencilLine className="size-4" aria-hidden />
|
||||
Revize et
|
||||
</Button>
|
||||
) : null}
|
||||
yeniden kurar. Sayı sınırı yok (7 Eki 2026). */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => sohbetiOdakla("Listemde şunu değiştir: ")}
|
||||
className={IKINCIL_BUTON}
|
||||
>
|
||||
<PencilLine className="size-4" aria-hidden />
|
||||
Revize et
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
@@ -447,7 +443,8 @@ export function ListemGovde({
|
||||
bildikleri={danismaninBildikleri}
|
||||
genis={sohbetGenis}
|
||||
onGenisDegistir={sohbetGenisligiAyarla}
|
||||
revizyonHakki={kalanHak}
|
||||
// 0/1 bayrak; prop adı entegrasyonda revizyonAcik olur
|
||||
revizyonHakki={hasPaket ? 1 : 0}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||
import { profilSatirlari } from "@/lib/danisman-profil";
|
||||
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
||||
import { URUNLER, RAPOR_KREDI } from "@/lib/credits";
|
||||
import type {
|
||||
MaskeliRapor,
|
||||
RaporParams,
|
||||
@@ -177,7 +177,6 @@ export async function ListemIcerik({
|
||||
danismanProfili={profilSatirlari(danismanProfili)}
|
||||
ozet={danismanOzeti}
|
||||
ilkYorum={ilkYorum}
|
||||
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
||||
paketFiyati={PAKET_FIYATI}
|
||||
paketKredi={URUNLER.paket.credits}
|
||||
/>
|
||||
|
||||
@@ -75,7 +75,6 @@ export const SENARYOLAR: Senaryo[] = [
|
||||
ok: true,
|
||||
toplam: 24,
|
||||
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
|
||||
kalanHak: 1,
|
||||
ilkTercihler: [
|
||||
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
|
||||
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
|
||||
@@ -86,8 +85,7 @@ export const SENARYOLAR: Senaryo[] = [
|
||||
});
|
||||
writer.text(
|
||||
"Listeni yeniden kurdum: ilk 5 tercihin artık İstanbul'dan, kalan 19 tercih Ankara ve İzmir'le devam ediyor. Dağılım yine 5 hayal / 13 dengeli / 6 güvenli.\n\n" +
|
||||
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa\n\n" +
|
||||
"Kalan revizyon hakkın: 1.",
|
||||
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa",
|
||||
);
|
||||
})
|
||||
.transport({ delayMs: 20 }),
|
||||
|
||||
@@ -4,7 +4,6 @@ import { appDb, schema } from "@/lib/appdb";
|
||||
import {
|
||||
grantCredits,
|
||||
krediBittiIsaretle,
|
||||
MAX_REVIZYON,
|
||||
RAPOR_KREDI,
|
||||
spendCredits,
|
||||
} from "@/lib/credits";
|
||||
@@ -21,15 +20,15 @@ import type { RaporParams, RaporSonuc, RevizyonKurali } from "./types/rapor";
|
||||
* (lib/ai/araclar.ts) burayı çağırır; eski /listem revizyon kutusu ve
|
||||
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
|
||||
*
|
||||
* Koruma kapıları eski aksiyonla aynı: paket şartı, MAX_REVIZYON sayacı,
|
||||
* kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
|
||||
* Koruma kapıları: paket şartı (sayı sınırı yok — 7 Eki 2026; `revision_count`
|
||||
* yalnız sayaç/istatistik), kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
|
||||
* kimliği) ile idempotent defter kaydı, başarısız üretimde iade. Bedel
|
||||
* paketliye 0 (liste ve revizyon pakete dâhil); satır yine yazılır ki aynı
|
||||
* mesaj ikinci kez revizyon tetiklemesin.
|
||||
*/
|
||||
export type RevizyonSonucu =
|
||||
| { ok: true; ozet: Extract<RevizyonOzeti, { ok: true }>; rapor: RaporSonuc }
|
||||
| { ok: false; neden: string; kod: "PAKET" | "HAK" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
|
||||
| { ok: false; neden: string; kod: "PAKET" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
|
||||
|
||||
function hataMesaji(err: unknown): string {
|
||||
if (err instanceof RaporUretimHatasi) return err.message;
|
||||
@@ -37,19 +36,18 @@ function hataMesaji(err: unknown): string {
|
||||
err instanceof Error &&
|
||||
/abort|timeout|timed out/i.test(`${err.name} ${err.constructor.name} ${err.message}`)
|
||||
) {
|
||||
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; revizyon hakkın kullanılmadı.";
|
||||
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; kredin/paketin etkilenmedi.";
|
||||
}
|
||||
console.error("[revizyon] üretim hatası:", err instanceof Error ? err.message : String(err));
|
||||
return "Liste yeniden kurulurken bir sorun oluştu; revizyon hakkın kullanılmadı.";
|
||||
return "Liste yeniden kurulurken bir sorun oluştu; kredin/paketin etkilenmedi.";
|
||||
}
|
||||
|
||||
function ozetCikar(rapor: RaporSonuc, revisionCount: number): Extract<RevizyonOzeti, { ok: true }> {
|
||||
function ozetCikar(rapor: RaporSonuc): Extract<RevizyonOzeti, { ok: true }> {
|
||||
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
|
||||
return {
|
||||
ok: true,
|
||||
toplam: rapor.tercihler.length,
|
||||
dagilim: { hayal: say("hayal"), dengeli: say("dengeli"), garanti: say("garanti") },
|
||||
kalanHak: Math.max(0, MAX_REVIZYON - revisionCount),
|
||||
ilkTercihler: rapor.tercihler.slice(0, 5).map((t) => {
|
||||
const p = rapor.programlar[t.programId];
|
||||
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
|
||||
@@ -79,14 +77,6 @@ export async function listeRevizeCekirdek(input: {
|
||||
if (!rapor?.result) {
|
||||
return { ok: false, kod: "RAPOR", neden: "Adayın kayıtlı listesi yok; önce liste oluşturulmalı." };
|
||||
}
|
||||
if (rapor.revisionCount >= MAX_REVIZYON) {
|
||||
return {
|
||||
ok: false,
|
||||
kod: "HAK",
|
||||
neden: `Revizyon hakkı doldu (en fazla ${MAX_REVIZYON}).`,
|
||||
};
|
||||
}
|
||||
|
||||
const kilit = await uretimKilidiAl(input.userId, input.requestId);
|
||||
if (!kilit.ok) {
|
||||
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
||||
@@ -123,7 +113,7 @@ export async function listeRevizeCekirdek(input: {
|
||||
const guncel = await kullanicininRaporu(input.userId);
|
||||
if (guncel?.result) {
|
||||
const sonuc = guncel.result as RaporSonuc;
|
||||
return { ok: true, ozet: ozetCikar(sonuc, guncel.revisionCount), rapor: sonuc };
|
||||
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
|
||||
}
|
||||
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
||||
}
|
||||
@@ -157,7 +147,7 @@ export async function listeRevizeCekirdek(input: {
|
||||
})
|
||||
.where(eq(schema.reports.id, rapor.id));
|
||||
|
||||
return { ok: true, ozet: ozetCikar(sonuc, rapor.revisionCount + 1), rapor: sonuc };
|
||||
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
|
||||
} finally {
|
||||
await uretimKilidiBirak(input.userId, input.requestId);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export type RevizyonOzeti =
|
||||
ok: true;
|
||||
toplam: number;
|
||||
dagilim: { hayal: number; dengeli: number; garanti: number };
|
||||
kalanHak: number;
|
||||
ilkTercihler: string[];
|
||||
}
|
||||
| { ok: false; neden: string };
|
||||
|
||||
@@ -14,9 +14,6 @@ export const DENEME_KREDISI = 5;
|
||||
// Rapor üretimi/revizyonu başına düşen kredi
|
||||
export const RAPOR_KREDI = 3;
|
||||
|
||||
/** Paketle gelen geri bildirimli liste revizyonu hakkı */
|
||||
export const MAX_REVIZYON = 2;
|
||||
|
||||
/**
|
||||
* UNIQUE ihlali mi? drizzle sürücü hatasını DrizzleQueryError'a sarar: üst
|
||||
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız
|
||||
|
||||
Reference in New Issue
Block a user