All checks were successful
Deploy / deploy (push) Successful in 9m29s
- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem". - Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom. - Updated pnpm-lock.yaml to reflect the new package versions and dependencies. - Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
288 lines
10 KiB
TypeScript
288 lines
10 KiB
TypeScript
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";
|
||
import { RISK_ETIKET, dilimdenRisk, riskHesapla } from "../risk";
|
||
|
||
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(),
|
||
uyarilar: z.array(z.string()).min(2).max(3),
|
||
});
|
||
|
||
// uyarilar şemada zorunlu ama eski kayıtlı raporlarda yok → tipte opsiyonel
|
||
export type RaporSonuc = Omit<z.infer<typeof RaporSchema>, "uyarilar"> & {
|
||
uyarilar?: string[];
|
||
// Render için havuzdan zenginleştirilmiş program bilgisi
|
||
// efektifSira: COALESCE(sira2025, sira2024) — eski kayıtlı raporlarda yok
|
||
// siraGecmisi: [2021..2025] taban sıralamaları — deterministik trend oku
|
||
programlar: Record<
|
||
string,
|
||
{
|
||
isim: string;
|
||
universite: string;
|
||
il: string | null;
|
||
unitur: string | null;
|
||
efektifSira?: number | null;
|
||
siraGecmisi?: (number | 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).
|
||
- METİN DİYETİ: aday 24 satır okuyacak; 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
|
||
beri taban 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, EN FAZLA 2 cümle. Uyarıları
|
||
buraya yazma.
|
||
- uyarilar: adaya 2-3 pratik, eyleme dönük uyarı; her biri tek kısa cümle.`;
|
||
|
||
export class RaporUretimHatasi extends Error {}
|
||
|
||
/** Havuz programını render için gereken bilgiye indirger. */
|
||
function programBilgisi(p: AdayProgram): RaporSonuc["programlar"][string] {
|
||
const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025];
|
||
return {
|
||
isim: p.isim,
|
||
universite: p.universite,
|
||
il: p.il,
|
||
unitur: p.unitur,
|
||
efektifSira: p.sira2025 ?? p.sira2024,
|
||
siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined,
|
||
};
|
||
}
|
||
|
||
// --- Dev mock: API key yokken (yalnızca development) gerçek havuzdan sahte
|
||
// ama tutarlı bir rapor üret. Akış (kredi, blur, risk renkleri, revizyon)
|
||
// gerçek AI olmadan uçtan uca test edilebilsin diye. Prod'da asla çalışmaz.
|
||
|
||
function mockAktif(): boolean {
|
||
return (
|
||
process.env.NODE_ENV !== "production" &&
|
||
!process.env.ANTHROPIC_API_KEY &&
|
||
!process.env.ANTHROPIC_AUTH_TOKEN
|
||
);
|
||
}
|
||
|
||
function mockRapor(
|
||
havuz: AdayProgram[],
|
||
params: RaporParams,
|
||
revizyonNotu?: string,
|
||
): RaporSonuc {
|
||
const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0;
|
||
const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
|
||
havuz
|
||
.filter((p) => p.dilim === dilim)
|
||
.sort((a, b) => efektif(a) - efektif(b))
|
||
.slice(0, adet);
|
||
|
||
// 5 hayal + 13 dengeli + 6 garanti; eksik dilim varsa havuzdan tamamla
|
||
const secilen = [
|
||
...dilimden("hayal", 5),
|
||
...dilimden("dengeli", 13),
|
||
...dilimden("garanti", 6),
|
||
];
|
||
for (const p of havuz) {
|
||
if (secilen.length >= 24) break;
|
||
if (!secilen.includes(p)) secilen.push(p);
|
||
}
|
||
|
||
const tercihler = secilen.slice(0, 24).map((p, i) => ({
|
||
programId: p.id,
|
||
sira: i + 1,
|
||
dilim: p.dilim,
|
||
gerekce: `(Dev mock) ${p.isim}, seçtiğin ilgi alanlarıyla ve ${params.sira.toLocaleString("tr-TR")}. sıralamanla uyumlu bir aday.`,
|
||
riskNotu:
|
||
p.dilim === "hayal"
|
||
? "(Dev mock) Taban sıralaması senin üstünde kapandı; düşük ihtimal ama listenin başında yeri var."
|
||
: p.dilim === "dengeli"
|
||
? "(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir."
|
||
: "(Dev mock) Taban sıralaması senin belirgin altında; yüksek ihtimalle yerleşirsin.",
|
||
trendOzeti:
|
||
"(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.",
|
||
}));
|
||
|
||
const programlar: RaporSonuc["programlar"] = {};
|
||
for (const p of secilen.slice(0, 24)) {
|
||
programlar[p.id] = programBilgisi(p);
|
||
}
|
||
|
||
return {
|
||
tercihler,
|
||
genelDegerlendirme: `(Dev mock — ANTHROPIC_API_KEY tanımlı değil, bu rapor gerçek AI çıktısı değildir.) ${params.sira.toLocaleString(
|
||
"tr-TR",
|
||
)}. sıra için hayal/dengeli/garanti dağılımlı 24 tercihlik örnek liste kuruldu.${
|
||
revizyonNotu ? ` Revizyon isteğin not alındı: "${revizyonNotu}".` : ""
|
||
}`,
|
||
uyarilar: [
|
||
"(Dev mock) Tercih dönemi kapanmadan listeyi ÖSYM sistemine kendin gir.",
|
||
"(Dev mock) Garanti dilimindeki son 3 tercihi asla silme.",
|
||
],
|
||
programlar,
|
||
};
|
||
}
|
||
|
||
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.",
|
||
);
|
||
}
|
||
|
||
// Dev'de API key yoksa gerçek AI yerine mock rapor (üretim gecikmesi taklidi)
|
||
if (mockAktif()) {
|
||
await new Promise((r) => setTimeout(r, 1500));
|
||
return mockRapor(havuz, params, revizyonNotu);
|
||
}
|
||
|
||
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] = programBilgisi(p);
|
||
}
|
||
return { ...sonuc, programlar };
|
||
}
|
||
|
||
export function listeOzetiCikar(rapor: RaporSonuc, adaySira?: number): string {
|
||
return rapor.tercihler
|
||
.map((t) => {
|
||
const p = rapor.programlar[t.programId];
|
||
const risk =
|
||
(adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ??
|
||
dilimdenRisk(t.dilim);
|
||
const taban =
|
||
p?.efektifSira != null
|
||
? ` (taban sırası ~${p.efektifSira.toLocaleString("tr-TR")})`
|
||
: "";
|
||
return `${t.sira}. [${t.dilim} | ${RISK_ETIKET[risk]}] ${p?.isim ?? "?"} — ${p?.universite ?? "?"}${taban}`;
|
||
})
|
||
.join("\n");
|
||
}
|
||
|
||
export type { AdayProgram };
|