diff --git a/scripts/tadimlik-uret.ts b/scripts/tadimlik-uret.ts index a869324..4e2c351 100644 --- a/scripts/tadimlik-uret.ts +++ b/scripts/tadimlik-uret.ts @@ -67,9 +67,10 @@ KURALLAR (kesin): - METİN DİYETİ: her alan TEK kısa cümle, dolgu söz yok. - gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle, en fazla ~15 kelime, Türkçe). -- riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti", - "kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan. -- trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den +- riskNotu: geçmiş taban verisine dayalı risk değerlendirmesi (TEK kısa cümle). + ASLA garanti/kesin/yüzde/olasılık/ihtimal deme; "güvenli / az riskli / + riskli" etiketleriyle konuş. +- trendOzeti: son 5 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor"). - Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`; @@ -82,9 +83,9 @@ function mockMetin(p: Program, temsiliSira: number, kategori: string | null) { return { gerekce: `(Dev mock) ${p.isim}, ${kategori ? `${kategori} ilgisiyle ve ` : ""}~${temsiliSira.toLocaleString("tr-TR")} bandındaki sıralamanla uyumlu bir aday.`, riskNotu: - "(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir.", + "(Dev mock) Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.", trendOzeti: - "(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.", + "(Dev mock) Son 5 yılda taban sıralaması görece stabil seyretti.", }; } diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts index 305f284..01c0cfc 100644 --- a/src/app/api/soru/route.ts +++ b/src/app/api/soru/route.ts @@ -45,7 +45,8 @@ KURALLAR: sıralaması adayın belirgin altında, Az riskli (sarı) = adayın sıralaması civarında, Riskli (kırmızı) = geçen yıl adayın sıralamasının üzerinde kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan. -- ASLA yerleşme garantisi verme; "yüksek/orta/düşük ihtimal" dili kullan. +- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma — + güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş. - Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle. - Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat. - Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır. diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6303e82..f9e2ed5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -52,7 +52,7 @@ export const metadata: Metadata = { template: "%s — KolayTercih", }, description: - "YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmanın onda bir fiyatına, yapay zekâ destekli tercih danışmanlığı.", + "YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmandan çok daha düşük fiyata, yapay zekâ destekli tercih danışmanlığı.", applicationName: "KolayTercih", openGraph: { type: "website", diff --git a/src/app/page.tsx b/src/app/page.tsx index 0a06a1e..6c15842 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -30,6 +30,7 @@ import { SectionEyebrow, } from "@/components/pixel-decor"; import { MAX_REVIZYON, URUNLER } from "@/lib/credits"; +import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami"; import { Accordion, AccordionContent, @@ -87,7 +88,7 @@ const problems = [ icon: Wallet, title: "Danışman lüks haline geldi", description: - "İyi bir tercih danışmanı 2.000–10.000 TL istiyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.", + "İyi bir tercih danışmanının ücreti birkaç bin lirayı bulabiliyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.", }, ]; @@ -103,8 +104,7 @@ const steps = [ icon: BarChart3, step: "2", title: "Yapay zekâ veriyi tarasın", - description: - "Son 4 yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.", + description: `Son ${VERI_YIL_SAYISI} yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.`, }, { icon: ShieldCheck, @@ -118,8 +118,7 @@ const steps = [ const faqs = [ { question: "KolayTercih'in önerileri neye dayanıyor?", - answer: - "Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son 4 yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.", + answer: `Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son ${VERI_YIL_SAYISI} yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.`, }, { question: "Ücretsiz tercih robotlarından farkı ne?", @@ -145,7 +144,7 @@ const comparison = [ tercihai: true, }, { - feature: "Son 4 yılın trend analizi", + feature: `Son ${VERI_YIL_SAYISI} yılın trend analizi`, robot: false, danisman: true, tercihai: true, @@ -165,7 +164,7 @@ const comparison = [ { feature: "Fiyat", robot: "Ücretsiz", - danisman: "2.000–10.000 TL", + danisman: "Danışmana göre değişir", tercihai: `${PAKET_FIYATI} TL · tek seferlik`, }, ]; @@ -177,7 +176,7 @@ const paketOzellikleri = [ }, { icon: ShieldCheck, - text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 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: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` }, @@ -362,7 +361,7 @@ export default function Home() { {[ { value: "23.000+", label: "Taranan lisans programı" }, { - value: "4 yıl", + value: `${VERI_YIL_SAYISI} yıl`, label: "Taban sıralaması trend verisi", }, { value: "%100", label: "Resmî YÖK Atlas kaynağı" }, @@ -481,7 +480,7 @@ export default function Home() { Ne kadar? Ne dahil?
- İnsan danışmanın 2.000–10.000 TL aldığı işi tek seferlik{" "} + İnsan danışmanın birkaç bin liraya yaptığı işi tek seferlik{" "} {PAKET_FIYATI} TL'ye alırsın. Abonelik yok, gizli ücret yok.
diff --git a/src/app/paket/page.tsx b/src/app/paket/page.tsx index 9e1eab4..e65bf11 100644 --- a/src/app/paket/page.tsx +++ b/src/app/paket/page.tsx @@ -46,11 +46,12 @@ export default function PaketPage({- Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih - aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar. + Tercih danışmanlarının ücreti bu dönemde birkaç bin lirayı + bulabiliyor. KolayTercih aynı işi gerçek YÖK Atlas verisiyle, + dakikalar içinde yapar.
diff --git a/src/components/arama-funnel-karti.tsx b/src/components/arama-funnel-karti.tsx index 29b8c5a..6e59359 100644 --- a/src/components/arama-funnel-karti.tsx +++ b/src/components/arama-funnel-karti.tsx @@ -35,7 +35,7 @@ const FUNNEL_METINLERI = [ "Sıralaman ne söylüyor? Birlikte bakalım.", "Her satırı senin için hesaplıyoruz.", "Akıllı tercih, içi rahat bir bekleyiş.", - "Yerleşme ihtimalini görerek seç.", + "Yerleşme riskini görerek seç.", "Sana özel liste birkaç dakikada hazır.", "İlgi alanların ve şehrin, tek listede.", "Boş kalan tercih, kaçan fırsattır.", diff --git a/src/features/odeme/components/paket-satinal.tsx b/src/features/odeme/components/paket-satinal.tsx index 6693d6f..5ebf077 100644 --- a/src/features/odeme/components/paket-satinal.tsx +++ b/src/features/odeme/components/paket-satinal.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import { getCurrentUser } from "@/lib/session"; import { URUNLER, DENEME_KREDISI } from "@/lib/credits"; +import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami"; import { Button } from "@/components/ui/button"; import { Parallax } from "@/components/parallax"; import { Skeleton } from "@/components/ui/skeleton"; @@ -28,7 +29,7 @@ const OZELLIKLER = [ }, { icon: ShieldCheck, - text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 yıl trend)", + 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ı" }, diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts index 7d6e305..416db7a 100644 --- a/src/lib/ai/rapor.ts +++ b/src/lib/ai/rapor.ts @@ -18,7 +18,13 @@ import { type AdayProgram, } from "../rapor-havuzu"; import { secimOzeti } from "@/features/sihirbaz/sihirbaz-sabitler"; -import { RISK_ETIKET, dilimdenRisk, riskHesapla } from "../risk"; +import { + RISK_ETIKET, + dilimdenRisk, + riskHesapla, + riskNotuMetni, +} from "../risk"; +import { trendOzetiMetni } from "../trend"; // Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu // ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti @@ -127,50 +133,19 @@ function normalizeEt( // geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod // kesin rakam verir ("45B düzeyinden 38B düzeyine indi"). -/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ -function siraKisa(n: number): string { - if (n >= 1_000_000) { - return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`; - } - if (n >= 1000) return `${Math.round(n / 1000)}B`; - return String(n); -} - function trendOzetiUret(p: AdayProgram): string { - const ham: Array<[number, number | null | undefined]> = [ - [2021, p.sira2021], - [2022, p.sira2022], - [2023, p.sira2023], - [2024, p.sira2024], - [2025, p.sira2025], - ]; - const seri = ham.filter((cift): cift is [number, number] => cift[1] != null); - if (seri.length < 2) { - return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; - } - const [ilkYil, ilkSira] = seri[0]; - const [sonYil, sonSira] = seri[seri.length - 1]; - const oran = (sonSira - ilkSira) / ilkSira; - if (oran <= -0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; bölüm zorlaşıyor.`; - } - if (oran >= 0.1) { - return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; yerleşmek görece kolaylaşıyor.`; - } - return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; + // Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts) + return trendOzetiMetni([ + p.sira2021, + p.sira2022, + p.sira2023, + p.sira2024, + p.sira2025, + ]); } function riskNotuUret(p: AdayProgram, adaySira: number): string { - switch (riskHesapla(p.sira2025 ?? p.sira2024, adaySira)) { - case "guvenli": - return "Taban sıralaması senin sıranın belirgin altında; yüksek ihtimalle yerleşirsin."; - case "az-riskli": - return "Taban sıralaması sırana yakın; orta ihtimalle yerleşilebilir."; - case "riskli": - return "Taban sıralaması senin sıranın üstünde kapandı; yerleşme ihtimali düşük."; - default: - return "Güncel taban sıralaması verisi yok; ihtimal öngörülemez, temkinli yaklaş."; - } + return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira)); } /** Havuz programını render için gereken bilgiye indirger. */ diff --git a/src/lib/risk.ts b/src/lib/risk.ts index 4b7c151..8d7898d 100644 --- a/src/lib/risk.ts +++ b/src/lib/risk.ts @@ -42,3 +42,22 @@ export function dilimdenRisk(dilim: Dilim | string): RiskSeviyesi { if (dilim === "dengeli") return "az-riskli"; return "riskli"; } + +/** + * Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık). + * Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın + * sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez. + * "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor. + */ +export function riskNotuMetni(risk: RiskSeviyesi | null): string { + switch (risk) { + case "guvenli": + return "Son taban sıralaması senin sıranın belirgin altında; geçmiş veriye göre güvenli aralıkta."; + case "az-riskli": + return "Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir."; + case "riskli": + return "Son taban sıralaması senin sıranın üstünde kapandı; hayal tercihi olarak düşün."; + default: + return "Güncel taban verisi yok; bu satırı temkinli değerlendir."; + } +} diff --git a/src/lib/tadimlik-havuzu.ts b/src/lib/tadimlik-havuzu.ts index 62dd85b..7b01b7b 100644 --- a/src/lib/tadimlik-havuzu.ts +++ b/src/lib/tadimlik-havuzu.ts @@ -8,6 +8,8 @@ import { turkishSlugify } from "./slug"; import type { PuanTuruKey } from "@/types/yokatlas"; import type { Dilim } from "./rapor-havuzu"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; +import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk"; +import { trendOzetiMetni } from "./trend"; /** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */ export const GENEL_KATEGORI = "genel"; @@ -48,6 +50,22 @@ export interface TadimlikSatir { export type TadimlikTip = "genel" | "devlet" | "vakif"; +/** + * gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya + * göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık / + * ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe + * servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı + * kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe + * kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı + * eşlemez). + */ +const YASAK_GEREKCE_DILI = + /olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/; + +export function gerekceServisEdilebilir(gerekce: string): boolean { + return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR")); +} + /** * Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye * özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf @@ -55,6 +73,9 @@ export type TadimlikTip = "genel" | "devlet" | "vakif"; * * Tip fallback'i YOKTUR: kullanıcı devlet/vakıf seçtiyse yanlış tipte örnek * asla gösterilmez — uygun satır yoksa tadımlık gizlenir. + * + * Gerekçesi dil filtresine takılan satır YOK sayılır: aynı sırayla "genel" + * kategoriye düşülür, o da takılırsa null (tadımlık gizlenir). */ export async function tadimlikSec( sira: number, @@ -75,14 +96,23 @@ export async function tadimlikSec( eq(schema.tadimlikHavuzu.tip, tip), ), }); - if (satir) { + if (satir && gerekceServisEdilebilir(satir.gerekce)) { + const program = satir.program as RaporSonuc["programlar"][string]; return { programId: satir.programId, dilim: satir.dilim, + // Servis edilen tek LLM metni; yukarıdaki dil filtresinden geçti. gerekce: satir.gerekce, - riskNotu: satir.riskNotu, - trendOzeti: satir.trendOzeti, - program: satir.program as RaporSonuc["programlar"][string], + // DB'deki riskNotu ve trendOzeti batch'te LLM'in yazdığı cümlelerdir; + // ikisi de servis EDİLMEZ. Risk notu, rozetle (tadimlik-satiri.tsx) + // aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı + // saf fonksiyondan programın sıra geçmişinden okuma anında üretilir — + // metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz. + riskNotu: riskNotuMetni( + riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim), + ), + trendOzeti: trendOzetiMetni(program.siraGecmisi), + program, }; } } diff --git a/src/lib/trend.ts b/src/lib/trend.ts new file mode 100644 index 0000000..4e3c8bd --- /dev/null +++ b/src/lib/trend.ts @@ -0,0 +1,51 @@ +// Deterministik trend cümlesi: rapor satırları (lib/ai/rapor.ts) ve girişsiz +// tadımlık (lib/tadimlik-havuzu.ts) AYNI saf fonksiyondan okur. Cümle gerçek +// YÖK Atlas sıra geçmişinden kesin rakamla kurulur ("45B düzeyinden 38B +// düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil, +// serideki dolu yıllardan gelir. + +/** + * Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar + * (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu + * çapa değişmez. + */ +const ILK_VERI_YILI = 2021; + +/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */ +function siraKisa(n: number): string { + if (n >= 1_000_000) { + return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`; + } + if (n >= 1000) return `${Math.round(n / 1000)}B`; + return String(n); +} + +/** + * [2021..] taban sıralaması dizisinden tek cümlelik trend özeti. + * + * Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar + * (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı + * kurmaz — iki uç yılın kıyası aradaki seyri ve gelecek yılı söylemez. + * Küçülen sıra = taban sıkılaştı, büyüyen sıra = taban gevşedi. + */ +export function trendOzetiMetni( + gecmis: readonly (number | null | undefined)[] | undefined, +): string { + const seri: Array<[number, number]> = []; + (gecmis ?? []).forEach((sira, i) => { + if (sira != null) seri.push([ILK_VERI_YILI + i, sira]); + }); + if (seri.length < 2) { + return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku."; + } + const [ilkYil, ilkSira] = seri[0]; + const [sonYil, sonSira] = seri[seri.length - 1]; + const oran = (sonSira - ilkSira) / ilkSira; + if (oran <= -0.1) { + return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`; + } + if (oran >= 0.1) { + return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`; + } + return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`; +} diff --git a/src/lib/veri-kapsami.ts b/src/lib/veri-kapsami.ts new file mode 100644 index 0000000..937f77a --- /dev/null +++ b/src/lib/veri-kapsami.ts @@ -0,0 +1,6 @@ +/** + * Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında + * sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu + * sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle. + */ +export const VERI_YIL_SAYISI = 5;