feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle
- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası) - iyzico ödeme entegrasyonu ve paket satın alma akışı - kredi sistemi ve uygulama veritabanı şeması (drizzle) - AI destekli rapor üretimi ve tercih sihirbazı - rapor yazdırma sayfası ve site header/kullanıcı menüsü Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
179
src/lib/ai/rapor.ts
Normal file
179
src/lib/ai/rapor.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { z } from "zod";
|
||||
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
||||
import { getAnthropic, RAPOR_MODEL } from "./client";
|
||||
import {
|
||||
havuzOlustur,
|
||||
havuzuKompaktJson,
|
||||
turLabel,
|
||||
type RaporParams,
|
||||
type AdayProgram,
|
||||
} from "../rapor-havuzu";
|
||||
import { secimOzeti } from "../sihirbaz";
|
||||
|
||||
const TercihSchema = z.object({
|
||||
programId: z.string(),
|
||||
sira: z.number().int(),
|
||||
dilim: z.enum(["hayal", "dengeli", "garanti"]),
|
||||
gerekce: z.string(),
|
||||
riskNotu: z.string(),
|
||||
trendOzeti: z.string(),
|
||||
});
|
||||
|
||||
const RaporSchema = z.object({
|
||||
tercihler: z.array(TercihSchema),
|
||||
genelDegerlendirme: z.string(),
|
||||
});
|
||||
|
||||
export type RaporSonuc = z.infer<typeof RaporSchema> & {
|
||||
// Render için havuzdan zenginleştirilmiş program bilgisi
|
||||
programlar: Record<
|
||||
string,
|
||||
{ isim: string; universite: string; il: string | null; unitur: string | null }
|
||||
>;
|
||||
};
|
||||
|
||||
const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Görevin: sana verilen
|
||||
GERÇEK YÖK Atlas aday havuzundan, adayın sıralamasına göre 24 tercihlik dengeli
|
||||
bir liste kurmak.
|
||||
|
||||
KURALLAR (kesin):
|
||||
- SADECE havuzdaki programId'leri kullan. Havuzda olmayan program ASLA önerme.
|
||||
- Tam 24 tercih üret; sira alanı 1'den 24'e sıralı olsun.
|
||||
- Liste yapısı: en üstte 4-6 hayal, ortada 12-14 dengeli, sonda 5-7 garanti.
|
||||
Tercih sırası MUTLAKA iyi taban sıralamasından kötüye doğru gitmeli
|
||||
(küçük taban sıralaması = daha zor bölüm üstte).
|
||||
- gerekce: bu programın bu aday için neden mantıklı olduğu (1-2 cümle, Türkçe).
|
||||
- riskNotu: yerleşme ihtimali değerlendirmesi. 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 (ör. "2022'den beri taban
|
||||
sıralaması 45B'den 38B'ye indi, bölüm zorlaşıyor").
|
||||
- Kontenjan artışı/azalışı ve yerleşen sayısını değerlendirmene kat.
|
||||
- Adayın notlarındaki şehir/bölüm tercihlerine saygı göster ama dengeyi bozma.
|
||||
- genelDegerlendirme: listenin genel stratejisi + adaya 2-3 pratik uyarı (Türkçe).`;
|
||||
|
||||
export class RaporUretimHatasi extends Error {}
|
||||
|
||||
export async function raporUret(
|
||||
params: RaporParams,
|
||||
revizyonNotu?: string,
|
||||
oncekiListeOzeti?: string,
|
||||
): Promise<RaporSonuc> {
|
||||
const { havuz, gevsetildi } = havuzOlustur(params);
|
||||
if (havuz.length < 24) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Bu sıralama ve filtrelerle yeterli program bulunamadı. Daha geniş kategori veya il seçmeyi dene.",
|
||||
);
|
||||
}
|
||||
|
||||
const secimVar =
|
||||
(params.kategoriler?.length ?? 0) > 0 ||
|
||||
(params.iller?.length ?? 0) > 0 ||
|
||||
(params.oncelikler?.length ?? 0) > 0 ||
|
||||
(params.universiteTipi && params.universiteTipi !== "farketmez");
|
||||
|
||||
const gevsetmeNotu: string[] = [];
|
||||
if (gevsetildi.includes("il")) {
|
||||
gevsetmeNotu.push(
|
||||
"Adayın seçtiği illerde yeterli program yoktu; havuza başka illerden de program eklendi. Seçili illere öncelik ver ama listeyi 24'e tamamlamak için diğerlerini de kullan.",
|
||||
);
|
||||
}
|
||||
if (gevsetildi.includes("kategori")) {
|
||||
gevsetmeNotu.push(
|
||||
"Adayın seçtiği ilgi alanlarında yeterli program yoktu; havuza yakın alanlardan program eklendi. Seçili alanlara öncelik ver.",
|
||||
);
|
||||
}
|
||||
|
||||
const client = getAnthropic();
|
||||
const havuzJson = havuzuKompaktJson(havuz);
|
||||
const kullaniciMesaji = [
|
||||
`Aday bilgisi: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${turLabel(params.tur)}.`,
|
||||
secimVar ? `ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({
|
||||
kategoriler: params.kategoriler ?? [],
|
||||
iller: params.iller ?? [],
|
||||
universiteTipi: params.universiteTipi ?? "farketmez",
|
||||
oncelikler: params.oncelikler ?? [],
|
||||
})}\nSeçili kategori ve illere öncelik ver; havuzun elverdiği ölçüde bu seçimlere sadık kal.`
|
||||
: null,
|
||||
params.notlar ? `Adayın ek notları: ${params.notlar}` : null,
|
||||
gevsetmeNotu.length > 0 ? gevsetmeNotu.join("\n") : null,
|
||||
revizyonNotu
|
||||
? `REVİZYON İSTEĞİ: Aday mevcut listede şu değişikliği istiyor: ${revizyonNotu}\nÖnceki listenin özeti: ${oncekiListeOzeti ?? "yok"}`
|
||||
: null,
|
||||
`ADAY HAVUZU (yalnızca buradan seç): ${havuzJson}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const gecerliIdler = new Set(havuz.map((p) => p.id));
|
||||
|
||||
let sonuc: z.infer<typeof RaporSchema> | null = null;
|
||||
let sonHata = "";
|
||||
for (let deneme = 0; deneme < 2; deneme++) {
|
||||
const response = await client.messages.parse({
|
||||
model: RAPOR_MODEL,
|
||||
max_tokens: 16000,
|
||||
thinking: { type: "adaptive" },
|
||||
system: SISTEM,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
deneme === 0
|
||||
? kullaniciMesaji
|
||||
: `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`,
|
||||
},
|
||||
],
|
||||
output_config: { format: zodOutputFormat(RaporSchema) },
|
||||
});
|
||||
|
||||
const parsed = response.parsed_output;
|
||||
if (!parsed) {
|
||||
sonHata = "Çıktı şemaya uymadı.";
|
||||
continue;
|
||||
}
|
||||
// Halüsinasyon koruması: her programId havuzda olmalı + tam 24 öğe
|
||||
const bilinmeyen = parsed.tercihler.filter(
|
||||
(t) => !gecerliIdler.has(t.programId),
|
||||
);
|
||||
if (parsed.tercihler.length !== 24 || bilinmeyen.length > 0) {
|
||||
sonHata =
|
||||
parsed.tercihler.length !== 24
|
||||
? `Liste ${parsed.tercihler.length} öğe içeriyor, tam 24 olmalı.`
|
||||
: `Havuzda olmayan programId kullanıldı: ${bilinmeyen
|
||||
.map((t) => t.programId)
|
||||
.slice(0, 3)
|
||||
.join(", ")}.`;
|
||||
continue;
|
||||
}
|
||||
sonuc = parsed;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!sonuc) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Rapor üretilemedi (doğrulama iki denemede de başarısız). Tekrar dener misin? Revizyon hakkın kullanılmadı.",
|
||||
);
|
||||
}
|
||||
|
||||
const programlar: RaporSonuc["programlar"] = {};
|
||||
for (const p of havuz) {
|
||||
programlar[p.id] = {
|
||||
isim: p.isim,
|
||||
universite: p.universite,
|
||||
il: p.il,
|
||||
unitur: p.unitur,
|
||||
};
|
||||
}
|
||||
return { ...sonuc, programlar };
|
||||
}
|
||||
|
||||
export function listeOzetiCikar(rapor: RaporSonuc): string {
|
||||
return rapor.tercihler
|
||||
.map(
|
||||
(t) =>
|
||||
`${t.sira}. [${t.dilim}] ${rapor.programlar[t.programId]?.isim ?? "?"} — ${rapor.programlar[t.programId]?.universite ?? "?"}`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export type { AdayProgram };
|
||||
Reference in New Issue
Block a user