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,
|
SectionEyebrow,
|
||||||
} from "@/components/pixel-decor";
|
} from "@/components/pixel-decor";
|
||||||
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||||
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
import { URUNLER } from "@/lib/credits";
|
||||||
import {
|
import {
|
||||||
PROGRAM_SAYISI_CUMLE,
|
PROGRAM_SAYISI_CUMLE,
|
||||||
PROGRAM_SAYISI_METNI,
|
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)`,
|
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: 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,
|
icon: MessageCircleQuestion,
|
||||||
text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`,
|
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)`,
|
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: 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,
|
icon: MessageCircleQuestion,
|
||||||
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
|
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export function ListemGovde({
|
|||||||
danismanProfili,
|
danismanProfili,
|
||||||
ozet,
|
ozet,
|
||||||
ilkYorum,
|
ilkYorum,
|
||||||
kalanHak,
|
|
||||||
paketFiyati,
|
paketFiyati,
|
||||||
paketKredi,
|
paketKredi,
|
||||||
}: {
|
}: {
|
||||||
@@ -99,7 +98,6 @@ export function ListemGovde({
|
|||||||
ozet?: DanismanOzeti;
|
ozet?: DanismanOzeti;
|
||||||
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
||||||
ilkYorum?: IlkTercihYorumu;
|
ilkYorum?: IlkTercihYorumu;
|
||||||
kalanHak: number;
|
|
||||||
paketFiyati: string;
|
paketFiyati: string;
|
||||||
paketKredi: number;
|
paketKredi: number;
|
||||||
}) {
|
}) {
|
||||||
@@ -216,12 +214,12 @@ export function ListemGovde({
|
|||||||
}
|
}
|
||||||
if (hasPaket) {
|
if (hasPaket) {
|
||||||
satirlar.push(
|
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");
|
satirlar.push("Bu sohbetteki son mesajların");
|
||||||
return satirlar;
|
return satirlar;
|
||||||
}, [adaySira, params, hasPaket, acikSatir, kalanHak]);
|
}, [adaySira, params, hasPaket, acikSatir]);
|
||||||
|
|
||||||
// Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez).
|
// 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ı
|
// Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları
|
||||||
@@ -331,8 +329,7 @@ export function ListemGovde({
|
|||||||
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
|
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
|
||||||
kutusunu odaklar, aday ne değişsin istediğini yazar;
|
kutusunu odaklar, aday ne değişsin istediğini yazar;
|
||||||
danışman gerekirse soru formuyla netleştirip listeyi
|
danışman gerekirse soru formuyla netleştirip listeyi
|
||||||
yeniden kurar. Hak bitince düğme kalkar. */}
|
yeniden kurar. Sayı sınırı yok (7 Eki 2026). */}
|
||||||
{kalanHak > 0 ? (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -342,7 +339,6 @@ export function ListemGovde({
|
|||||||
<PencilLine className="size-4" aria-hidden />
|
<PencilLine className="size-4" aria-hidden />
|
||||||
Revize et
|
Revize et
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -447,7 +443,8 @@ export function ListemGovde({
|
|||||||
bildikleri={danismaninBildikleri}
|
bildikleri={danismaninBildikleri}
|
||||||
genis={sohbetGenis}
|
genis={sohbetGenis}
|
||||||
onGenisDegistir={sohbetGenisligiAyarla}
|
onGenisDegistir={sohbetGenisligiAyarla}
|
||||||
revizyonHakki={kalanHak}
|
// 0/1 bayrak; prop adı entegrasyonda revizyonAcik olur
|
||||||
|
revizyonHakki={hasPaket ? 1 : 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
import { profilSatirlari } from "@/lib/danisman-profil";
|
import { profilSatirlari } from "@/lib/danisman-profil";
|
||||||
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
import { URUNLER, RAPOR_KREDI } from "@/lib/credits";
|
||||||
import type {
|
import type {
|
||||||
MaskeliRapor,
|
MaskeliRapor,
|
||||||
RaporParams,
|
RaporParams,
|
||||||
@@ -177,7 +177,6 @@ export async function ListemIcerik({
|
|||||||
danismanProfili={profilSatirlari(danismanProfili)}
|
danismanProfili={profilSatirlari(danismanProfili)}
|
||||||
ozet={danismanOzeti}
|
ozet={danismanOzeti}
|
||||||
ilkYorum={ilkYorum}
|
ilkYorum={ilkYorum}
|
||||||
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
|
||||||
paketFiyati={PAKET_FIYATI}
|
paketFiyati={PAKET_FIYATI}
|
||||||
paketKredi={URUNLER.paket.credits}
|
paketKredi={URUNLER.paket.credits}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const SENARYOLAR: Senaryo[] = [
|
|||||||
ok: true,
|
ok: true,
|
||||||
toplam: 24,
|
toplam: 24,
|
||||||
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
|
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
|
||||||
kalanHak: 1,
|
|
||||||
ilkTercihler: [
|
ilkTercihler: [
|
||||||
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
|
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
|
||||||
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
|
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
|
||||||
@@ -86,8 +85,7 @@ export const SENARYOLAR: Senaryo[] = [
|
|||||||
});
|
});
|
||||||
writer.text(
|
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" +
|
"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" +
|
"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",
|
||||||
"Kalan revizyon hakkın: 1.",
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.transport({ delayMs: 20 }),
|
.transport({ delayMs: 20 }),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { appDb, schema } from "@/lib/appdb";
|
|||||||
import {
|
import {
|
||||||
grantCredits,
|
grantCredits,
|
||||||
krediBittiIsaretle,
|
krediBittiIsaretle,
|
||||||
MAX_REVIZYON,
|
|
||||||
RAPOR_KREDI,
|
RAPOR_KREDI,
|
||||||
spendCredits,
|
spendCredits,
|
||||||
} from "@/lib/credits";
|
} 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
|
* (lib/ai/araclar.ts) burayı çağırır; eski /listem revizyon kutusu ve
|
||||||
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
|
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
|
||||||
*
|
*
|
||||||
* Koruma kapıları eski aksiyonla aynı: paket şartı, MAX_REVIZYON sayacı,
|
* Koruma kapıları: paket şartı (sayı sınırı yok — 7 Eki 2026; `revision_count`
|
||||||
* kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
|
* 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
|
* 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ı
|
* paketliye 0 (liste ve revizyon pakete dâhil); satır yine yazılır ki aynı
|
||||||
* mesaj ikinci kez revizyon tetiklemesin.
|
* mesaj ikinci kez revizyon tetiklemesin.
|
||||||
*/
|
*/
|
||||||
export type RevizyonSonucu =
|
export type RevizyonSonucu =
|
||||||
| { ok: true; ozet: Extract<RevizyonOzeti, { ok: true }>; rapor: RaporSonuc }
|
| { 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 {
|
function hataMesaji(err: unknown): string {
|
||||||
if (err instanceof RaporUretimHatasi) return err.message;
|
if (err instanceof RaporUretimHatasi) return err.message;
|
||||||
@@ -37,19 +36,18 @@ function hataMesaji(err: unknown): string {
|
|||||||
err instanceof Error &&
|
err instanceof Error &&
|
||||||
/abort|timeout|timed out/i.test(`${err.name} ${err.constructor.name} ${err.message}`)
|
/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));
|
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;
|
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
toplam: rapor.tercihler.length,
|
toplam: rapor.tercihler.length,
|
||||||
dagilim: { hayal: say("hayal"), dengeli: say("dengeli"), garanti: say("garanti") },
|
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) => {
|
ilkTercihler: rapor.tercihler.slice(0, 5).map((t) => {
|
||||||
const p = rapor.programlar[t.programId];
|
const p = rapor.programlar[t.programId];
|
||||||
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
|
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
|
||||||
@@ -79,14 +77,6 @@ export async function listeRevizeCekirdek(input: {
|
|||||||
if (!rapor?.result) {
|
if (!rapor?.result) {
|
||||||
return { ok: false, kod: "RAPOR", neden: "Adayın kayıtlı listesi yok; önce liste oluşturulmalı." };
|
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);
|
const kilit = await uretimKilidiAl(input.userId, input.requestId);
|
||||||
if (!kilit.ok) {
|
if (!kilit.ok) {
|
||||||
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
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);
|
const guncel = await kullanicininRaporu(input.userId);
|
||||||
if (guncel?.result) {
|
if (guncel?.result) {
|
||||||
const sonuc = guncel.result as RaporSonuc;
|
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." };
|
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));
|
.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 {
|
} finally {
|
||||||
await uretimKilidiBirak(input.userId, input.requestId);
|
await uretimKilidiBirak(input.userId, input.requestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export type RevizyonOzeti =
|
|||||||
ok: true;
|
ok: true;
|
||||||
toplam: number;
|
toplam: number;
|
||||||
dagilim: { hayal: number; dengeli: number; garanti: number };
|
dagilim: { hayal: number; dengeli: number; garanti: number };
|
||||||
kalanHak: number;
|
|
||||||
ilkTercihler: string[];
|
ilkTercihler: string[];
|
||||||
}
|
}
|
||||||
| { ok: false; neden: string };
|
| { ok: false; neden: string };
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ export const DENEME_KREDISI = 5;
|
|||||||
// Rapor üretimi/revizyonu başına düşen kredi
|
// Rapor üretimi/revizyonu başına düşen kredi
|
||||||
export const RAPOR_KREDI = 3;
|
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
|
* 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
|
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız
|
||||||
|
|||||||
Reference in New Issue
Block a user