v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
8 changed files with 27 additions and 47 deletions
Showing only changes of commit e3d79f582e - Show all commits

View File

@@ -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`,

View File

@@ -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)`,

View File

@@ -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,18 +329,16 @@ 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" onClick={() => sohbetiOdakla("Listemde şunu değiştir: ")}
onClick={() => sohbetiOdakla("Listemde şunu değiştir: ")} className={IKINCIL_BUTON}
className={IKINCIL_BUTON} >
> <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>
) : ( ) : (

View File

@@ -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}
/> />

View File

@@ -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 }),

View File

@@ -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);
} }

View File

@@ -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 };

View File

@@ -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