refactor: update Dockerfile and authentication flow for improved user experience
All checks were successful
Deploy / deploy (push) Successful in 15m6s
All checks were successful
Deploy / deploy (push) Successful in 15m6s
- Updated Dockerfile to create a cache directory for Next.js runtime, ensuring proper permissions and ownership. - Enhanced the authentication flow by updating the magic link expiration time from 5 to 15 minutes, improving user experience and reducing login issues. - Added user-friendly error handling for invalid or expired magic links, providing clear feedback on the login page. - Adjusted email templates to reflect the new 15-minute validity period for magic links. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,14 @@ export async function yapilandirilmisUret<T extends z.ZodType>(opts: {
|
||||
system: string;
|
||||
kullanici: string;
|
||||
sema: T;
|
||||
// Sağlayıcıya giden json_schema bu şemadan üretilir (verilmezse sema).
|
||||
// Lokal doğrulama HER ZAMAN sema ile yapılır: tel şeması sıkı uzunluk
|
||||
// kısıtları taşıyabilir (grammar zorlaması), sağlayıcı bu kısıtları yok
|
||||
// sayarsa çıktı yine gevşek şemayla kurtarılır. Not: strict json_schema'da
|
||||
// minItems/maxItems bazı sağlayıcılarda 400 döndürebilir; mevcut sağlayıcı
|
||||
// (deepseek) kabul ediyor — sağlayıcı değişiminde geri çekilme yolu
|
||||
// wireSema'dan uzunluk kısıtlarını kaldırmaktır.
|
||||
wireSema?: z.ZodType;
|
||||
maxTokens: number;
|
||||
}): Promise<z.infer<T> | null> {
|
||||
// Tek sinyal tüm iç denemeleri kapsar: süre dolunca bekleyen retry'lar da
|
||||
@@ -98,13 +106,16 @@ export async function yapilandirilmisUret<T extends z.ZodType>(opts: {
|
||||
json_schema: {
|
||||
name: "yanit",
|
||||
strict: true,
|
||||
schema: jsonSemasi(opts.sema),
|
||||
schema: jsonSemasi(opts.wireSema ?? opts.sema),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ signal: sinyal },
|
||||
),
|
||||
);
|
||||
// OpenRouter üst sağlayıcı arızasında HTTP 200 gövdesinde {"error": ...}
|
||||
// dönebiliyor; choices o durumda yok — TypeError yerine null (retry yolu).
|
||||
if (!Array.isArray(yanit.choices) || yanit.choices.length === 0) return null;
|
||||
const metin = yanit.choices[0]?.message?.content;
|
||||
if (!metin) return null;
|
||||
try {
|
||||
@@ -147,7 +158,7 @@ export async function* sohbetAkisi(opts: {
|
||||
);
|
||||
for await (const parca of akis) {
|
||||
// Yalnızca delta.content akar; varsa delta.reasoning kullanıcıya sızmaz
|
||||
const metin = parca.choices[0]?.delta?.content;
|
||||
const metin = parca.choices?.[0]?.delta?.content;
|
||||
if (metin) yield metin;
|
||||
}
|
||||
// openai SDK akış ortasındaki abort'u yutup iteratörü NORMAL bitiriyor
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
havuzOlustur,
|
||||
havuzuKompaktJson,
|
||||
turLabel,
|
||||
dilimHedefiHesapla,
|
||||
IDEAL_HEDEF,
|
||||
type DilimHedef,
|
||||
type Dilim,
|
||||
type RaporParams,
|
||||
type AdayProgram,
|
||||
} from "../rapor-havuzu";
|
||||
@@ -27,26 +31,53 @@ const RaporSchema = z.object({
|
||||
});
|
||||
|
||||
// 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; 5/13/6'lık
|
||||
// dilim iskeleti ve tercih sırası da koddan gelir. Dilim başına SABİT uzunluklu
|
||||
// ayrı listeler istenir ki dağılım şema düzeyinde (grammar/JSON-schema ile)
|
||||
// zorlansın — reasoning kapalıyken model serbest dağılımı tutturamıyor
|
||||
// (ölçüldü: 13 hayal / 0 garanti dönmüştü). AI yalnızca seçer + gerekce yazar.
|
||||
// ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti
|
||||
// ve tercih sırası da koddan gelir. Dilim başına SABİT uzunluklu ayrı listeler
|
||||
// istenir ki dağılım şema düzeyinde (grammar/JSON-schema ile) zorlansın —
|
||||
// reasoning kapalıyken model serbest dağılımı tutturamıyor (ölçüldü: 13 hayal
|
||||
// / 0 garanti dönmüştü). Uzunluklar havuza uyarlanmış hedeften gelir (havuzda
|
||||
// dilim başına yeterli program olmayabiliyor; sabit 5/13/6 o durumda aritmetik
|
||||
// olarak imkânsızdı ve üretim her seferinde patlıyordu). AI yalnızca seçer +
|
||||
// gerekce yazar.
|
||||
const AiTercihSchema = z.object({
|
||||
programId: z.string(),
|
||||
gerekce: z.string(),
|
||||
});
|
||||
const AiRaporSchema = z.object({
|
||||
hayal: z.array(AiTercihSchema).length(5),
|
||||
dengeli: z.array(AiTercihSchema).length(13),
|
||||
garanti: z.array(AiTercihSchema).length(6),
|
||||
/** Sağlayıcıya giden tel şeması: dilim uzunlukları hedefe sabitlenir. */
|
||||
function aiSemaWire(hedef: DilimHedef) {
|
||||
return z.object({
|
||||
hayal: z.array(AiTercihSchema).length(hedef.hayal),
|
||||
dengeli: z.array(AiTercihSchema).length(hedef.dengeli),
|
||||
garanti: z.array(AiTercihSchema).length(hedef.garanti),
|
||||
genelDegerlendirme: z.string(),
|
||||
uyarilar: z.array(z.string()).min(2).max(3),
|
||||
});
|
||||
}
|
||||
// Lokal parse şeması: uzunluk kısıtsız. Sağlayıcı tel şemasındaki kısıtları
|
||||
// yok sayarsa çıktı reddedilmez; normalize + kurtarma katmanı düzeltir.
|
||||
const AiRaporGevsek = z.object({
|
||||
hayal: z.array(AiTercihSchema),
|
||||
dengeli: z.array(AiTercihSchema),
|
||||
garanti: z.array(AiTercihSchema),
|
||||
genelDegerlendirme: z.string(),
|
||||
uyarilar: z.array(z.string()).min(2).max(3),
|
||||
uyarilar: z.array(z.string()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Listenin havuza uyarlanma bilgisi. uyarlandi=true ise dağılım ideal
|
||||
* 5/13/6'dan sapmıştır (havuz yetersizdi) ve UI dürüst bir not gösterir.
|
||||
* Eski kayıtlı raporlarda yok → opsiyonel.
|
||||
*/
|
||||
export type RaporKapsam = {
|
||||
toplam: number;
|
||||
dagilim: Record<Dilim, number>;
|
||||
uyarlandi: boolean;
|
||||
};
|
||||
|
||||
// uyarilar şemada zorunlu ama eski kayıtlı raporlarda yok → tipte opsiyonel
|
||||
export type RaporSonuc = Omit<z.infer<typeof RaporSchema>, "uyarilar"> & {
|
||||
uyarilar?: string[];
|
||||
kapsam?: RaporKapsam;
|
||||
// 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
|
||||
@@ -63,9 +94,13 @@ export type RaporSonuc = Omit<z.infer<typeof RaporSchema>, "uyarilar"> & {
|
||||
>;
|
||||
};
|
||||
|
||||
const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Görevin: sana verilen
|
||||
function sistemMetni(hedef: DilimHedef): string {
|
||||
const adet = (d: Dilim) =>
|
||||
hedef[d] === 0 ? "BOŞ liste [] döndür" : `TAM ${hedef[d]} program`;
|
||||
const toplam = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
return `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Görevin: sana verilen
|
||||
GERÇEK YÖK Atlas aday havuzundan adaya en uygun programları seçip üç listeyi
|
||||
doldurmak: hayal (TAM 5 program), dengeli (TAM 13), garanti (TAM 6).
|
||||
doldurmak: hayal (${adet("hayal")}), dengeli (${adet("dengeli")}), garanti (${adet("garanti")}).
|
||||
|
||||
KURALLAR (kesin):
|
||||
- SADECE havuzdaki programId'leri kullan; havuzda olmayan program ASLA önerme.
|
||||
@@ -74,7 +109,7 @@ KURALLAR (kesin):
|
||||
değeri "hayal" olan programları koy; dengeli ve garanti için de aynısı.
|
||||
- Seçerken adayın ilgi alanlarını ve illerini, kontenjan/yerleşen hareketini
|
||||
ve bölüm çeşitliliğini gözet.
|
||||
- METİN DİYETİ: aday 24 satır okuyacak; gerekce TEK kısa cümle (en fazla ~15
|
||||
- METİN DİYETİ: aday ${toplam} satır okuyacak; gerekce TEK kısa cümle (en fazla ~15
|
||||
kelime, Türkçe): bu programın bu aday için neden mantıklı olduğu. Dolgu söz
|
||||
yok; ASLA "garanti", "kesin", "yüzde yüz" deme.
|
||||
- Adayın notlarındaki şehir/bölüm tercihlerine saygı göster ama dengeyi bozma.
|
||||
@@ -82,9 +117,53 @@ KURALLAR (kesin):
|
||||
buraya yazma.
|
||||
- uyarilar: adaya 2-3 pratik, eyleme dönük uyarı; her biri tek kısa cümle.
|
||||
(Tercih sırası, risk notu ve trend özeti koddan hesaplanır; sen yazmazsın.)`;
|
||||
}
|
||||
|
||||
export class RaporUretimHatasi extends Error {}
|
||||
|
||||
// Liste kurmak için gereken mutlak taban. Havuz bunun altındaysa hata ekranı
|
||||
// meşrudur; üstündeyse "ne olursa olsun liste kurulur" (kullanıcı kararı,
|
||||
// 7 Ağu 2026) — eksik dilimler uyarlanır ve UI'da dürüstçe belirtilir.
|
||||
// Not: ACIK_SATIR=3 maskesiyle tam 3 satırlık liste fiilen açık iner;
|
||||
// kabul edilen kenar durumu.
|
||||
const MIN_LISTE = 3;
|
||||
|
||||
const DILIMLER: Dilim[] = ["hayal", "dengeli", "garanti"];
|
||||
|
||||
function kapsamUret(hedef: DilimHedef): RaporKapsam {
|
||||
return {
|
||||
toplam: hedef.hayal + hedef.dengeli + hedef.garanti,
|
||||
dagilim: { ...hedef },
|
||||
uyarlandi: DILIMLER.some((d) => hedef[d] !== IDEAL_HEDEF[d]),
|
||||
};
|
||||
}
|
||||
|
||||
type AiSecim = z.infer<typeof AiTercihSchema>;
|
||||
|
||||
/**
|
||||
* AI çıktısını havuza göre normalize eder: havuzda olmayan id'ler ve
|
||||
* tekrarlar atılır, her program havuzdaki GERÇEK dilimine yeniden atanır
|
||||
* (modelin yanlış listeye koyması artık ret sebebi değil, düzeltilir).
|
||||
*/
|
||||
function normalizeEt(
|
||||
parsed: z.infer<typeof AiRaporGevsek>,
|
||||
havuzMap: Map<string, AdayProgram>,
|
||||
): Record<Dilim, AiSecim[]> {
|
||||
const gruplar: Record<Dilim, AiSecim[]> = {
|
||||
hayal: [],
|
||||
dengeli: [],
|
||||
garanti: [],
|
||||
};
|
||||
const gorulen = new Set<string>();
|
||||
for (const t of [...parsed.hayal, ...parsed.dengeli, ...parsed.garanti]) {
|
||||
const p = havuzMap.get(t.programId);
|
||||
if (!p || gorulen.has(t.programId)) continue;
|
||||
gorulen.add(t.programId);
|
||||
gruplar[p.dilim].push(t);
|
||||
}
|
||||
return gruplar;
|
||||
}
|
||||
|
||||
// --- Deterministik satır metinleri: risk ve trend gerçek YÖK Atlas sıra
|
||||
// geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod
|
||||
// kesin rakam verir ("45B düzeyinden 38B düzeyine indi").
|
||||
@@ -161,6 +240,9 @@ function mockRapor(
|
||||
params: RaporParams,
|
||||
revizyonNotu?: string,
|
||||
): RaporSonuc {
|
||||
// Prod ile aynı hedef hesabı: mock, uyarlanmış dağılımı da birebir taklit eder
|
||||
const hedef = dilimHedefiHesapla(havuz);
|
||||
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
const efektif = (p: AdayProgram) => p.sira2025 ?? p.sira2024 ?? 0;
|
||||
const dilimden = (dilim: AdayProgram["dilim"], adet: number) =>
|
||||
havuz
|
||||
@@ -168,18 +250,13 @@ function mockRapor(
|
||||
.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),
|
||||
...dilimden("hayal", hedef.hayal),
|
||||
...dilimden("dengeli", hedef.dengeli),
|
||||
...dilimden("garanti", hedef.garanti),
|
||||
];
|
||||
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) => ({
|
||||
const tercihler = secilen.map((p, i) => ({
|
||||
programId: p.id,
|
||||
sira: i + 1,
|
||||
dilim: p.dilim,
|
||||
@@ -190,7 +267,7 @@ function mockRapor(
|
||||
}));
|
||||
|
||||
const programlar: RaporSonuc["programlar"] = {};
|
||||
for (const p of secilen.slice(0, 24)) {
|
||||
for (const p of secilen) {
|
||||
programlar[p.id] = programBilgisi(p);
|
||||
}
|
||||
|
||||
@@ -198,14 +275,15 @@ function mockRapor(
|
||||
tercihler,
|
||||
genelDegerlendirme: `(Dev mock — Yapay Zeka anahtarı tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString(
|
||||
"tr-TR",
|
||||
)}. sıra için hayal/dengeli/garanti dağılımlı 24 tercihlik örnek liste kuruldu.${
|
||||
)}. sıra için hayal/dengeli/garanti dağılımlı ${toplamHedef} 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.",
|
||||
"(Dev mock) Garanti dilimindeki son tercihleri asla silme.",
|
||||
],
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,12 +293,18 @@ export async function raporUret(
|
||||
oncekiListeOzeti?: string,
|
||||
): Promise<RaporSonuc> {
|
||||
const { havuz, gevsetildi } = havuzOlustur(params);
|
||||
if (havuz.length < 24) {
|
||||
if (havuz.length < MIN_LISTE) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Bu sıralama ve filtrelerle yeterli program bulunamadı. Daha geniş kategori veya il seçmeyi dene.",
|
||||
);
|
||||
}
|
||||
|
||||
// Dilim hedefleri havuzun gerçek dağılımına uyarlanır: sabit 5/13/6 bazı
|
||||
// sıralamalarda (ör. çok iyi derece → dengeli dilim boş) imkânsızdı ve
|
||||
// üretim her seferinde doğrulamada patlıyordu.
|
||||
const hedef = dilimHedefiHesapla(havuz);
|
||||
const toplamHedef = hedef.hayal + hedef.dengeli + hedef.garanti;
|
||||
|
||||
const secimVar =
|
||||
(params.kategoriler?.length ?? 0) > 0 ||
|
||||
(params.iller?.length ?? 0) > 0 ||
|
||||
@@ -230,7 +314,7 @@ export async function raporUret(
|
||||
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.",
|
||||
`Adayın seçtiği illerde yeterli program yoktu; havuza başka illerden de program eklendi. Seçili illere öncelik ver ama listeyi ${toplamHedef}'e tamamlamak için diğerlerini de kullan.`,
|
||||
);
|
||||
}
|
||||
if (gevsetildi.includes("kategori")) {
|
||||
@@ -266,60 +350,98 @@ export async function raporUret(
|
||||
.join("\n\n");
|
||||
|
||||
const havuzMap = new Map(havuz.map((p) => [p.id, p]));
|
||||
const efektif = (id: string) => {
|
||||
const p = havuzMap.get(id)!;
|
||||
return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
let sonuc: z.infer<typeof AiRaporSchema> | null = null;
|
||||
let secim: Record<Dilim, AiSecim[]> | null = null;
|
||||
let sonSecim: Record<Dilim, AiSecim[]> | null = null;
|
||||
let genel = "";
|
||||
let uyarilar: string[] = [];
|
||||
let sonHata = "";
|
||||
for (let deneme = 0; deneme < 2; deneme++) {
|
||||
const parsed = await yapilandirilmisUret({
|
||||
system: SISTEM,
|
||||
system: sistemMetni(hedef),
|
||||
kullanici:
|
||||
deneme === 0
|
||||
? kullaniciMesaji
|
||||
: `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`,
|
||||
sema: AiRaporSchema,
|
||||
sema: AiRaporGevsek,
|
||||
wireSema: aiSemaWire(hedef),
|
||||
// Küçük şema (24 satır × gerekce + genel + uyarılar) ~1-1.5k token;
|
||||
// reasoning kapalıyken 4000 rahat bir tavan, kaçak uzun üretimi de keser.
|
||||
maxTokens: 4000,
|
||||
});
|
||||
|
||||
if (!parsed) {
|
||||
// Dilim uzunlukları (5/13/6) şemada sabit: sayı tutmazsa da buraya düşer
|
||||
sonHata = "Çıktı şemaya uymadı (hayal 5, dengeli 13, garanti 6 olmalı).";
|
||||
sonHata = `Çıktı şemaya uymadı (hayal ${hedef.hayal}, dengeli ${hedef.dengeli}, garanti ${hedef.garanti} olmalı).`;
|
||||
continue;
|
||||
}
|
||||
// Halüsinasyon koruması: id'ler havuzda, benzersiz ve doğru dilimde olmalı
|
||||
const gruplar = [
|
||||
["hayal", parsed.hayal],
|
||||
["dengeli", parsed.dengeli],
|
||||
["garanti", parsed.garanti],
|
||||
] as const;
|
||||
const idler = gruplar.flatMap(([, liste]) => liste.map((t) => t.programId));
|
||||
const bilinmeyen = idler.filter((id) => !havuzMap.has(id));
|
||||
if (bilinmeyen.length > 0) {
|
||||
sonHata = `Havuzda olmayan programId kullanıldı: ${bilinmeyen.slice(0, 3).join(", ")}.`;
|
||||
continue;
|
||||
// Halüsinasyon koruması normalize içinde: havuz-dışı id ve tekrarlar
|
||||
// atılır, dilimler havuzdan yeniden atanır. Ret yalnızca sayı tutmayınca.
|
||||
const norm = normalizeEt(parsed, havuzMap);
|
||||
sonSecim = norm;
|
||||
genel = parsed.genelDegerlendirme;
|
||||
uyarilar = parsed.uyarilar.slice(0, 3);
|
||||
if (DILIMLER.every((d) => norm[d].length === hedef[d])) {
|
||||
secim = norm;
|
||||
break;
|
||||
}
|
||||
if (new Set(idler).size !== idler.length) {
|
||||
sonHata = "Aynı programId birden fazla kez kullanıldı; her program tek kez seçilmeli.";
|
||||
continue;
|
||||
}
|
||||
const yanlisDilim = gruplar.flatMap(([dilim, liste]) =>
|
||||
liste
|
||||
.filter((t) => havuzMap.get(t.programId)!.dilim !== dilim)
|
||||
.map((t) => `${t.programId} (${dilim} listesinde ama havuzdaki dilimi farklı)`),
|
||||
);
|
||||
if (yanlisDilim.length > 0) {
|
||||
sonHata = `Programlar yanlış dilim listesine konmuş: ${yanlisDilim.slice(0, 3).join(", ")}. Havuzdaki "dilim" alanına birebir uy.`;
|
||||
continue;
|
||||
}
|
||||
sonuc = parsed;
|
||||
break;
|
||||
sonHata = `Dilim sayıları tutmadı (beklenen: hayal ${hedef.hayal}, dengeli ${hedef.dengeli}, garanti ${hedef.garanti}; havuz-dışı ve tekrar eden id'ler sayılmaz). Havuzdaki "dilim" alanına ve adetlere birebir uy.`;
|
||||
}
|
||||
|
||||
if (!sonuc) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Rapor üretilemedi (doğrulama iki denemede de başarısız). Tekrar dener misin? Revizyon hakkın kullanılmadı.",
|
||||
if (!secim) {
|
||||
// AI hiç kullanılabilir program seçemedi (parse iki kez null ya da tüm
|
||||
// id'ler havuz-dışı): gerçek üretim hatası, hata ekranı meşru.
|
||||
if (!sonSecim) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Rapor üretilemedi (doğrulama iki denemede de başarısız). Tekrar dener misin? Revizyon hakkın kullanılmadı.",
|
||||
);
|
||||
}
|
||||
const son = sonSecim;
|
||||
const eldeki = son.hayal.length + son.dengeli.length + son.garanti.length;
|
||||
if (eldeki === 0) {
|
||||
throw new RaporUretimHatasi(
|
||||
"Rapor üretilemedi (doğrulama iki denemede de başarısız). Tekrar dener misin? Revizyon hakkın kullanılmadı.",
|
||||
);
|
||||
}
|
||||
// Kurtarma ("ne olursa olsun liste kurulur"): fazla dilim en iyi taban
|
||||
// sıralamalılar kalacak şekilde kırpılır, eksik dilim havuzun seçilmemiş
|
||||
// programlarından deterministik tamamlanır. hedef[d] ≤ havuzdaki sayı
|
||||
// olduğu için tamamlama her zaman yeterli aday bulur.
|
||||
console.warn(
|
||||
`[rapor] kurtarma: AI ${eldeki} kullanılabilir program döndü, hedef ${toplamHedef} (hayal ${son.hayal.length}/${hedef.hayal}, dengeli ${son.dengeli.length}/${hedef.dengeli}, garanti ${son.garanti.length}/${hedef.garanti})`,
|
||||
);
|
||||
const kullanilan = new Set(
|
||||
DILIMLER.flatMap((d) => son[d].map((t) => t.programId)),
|
||||
);
|
||||
const kurtarilan: Record<Dilim, AiSecim[]> = {
|
||||
hayal: [],
|
||||
dengeli: [],
|
||||
garanti: [],
|
||||
};
|
||||
for (const d of DILIMLER) {
|
||||
const liste = [...son[d]]
|
||||
.sort((a, b) => efektif(a.programId) - efektif(b.programId))
|
||||
.slice(0, hedef[d]);
|
||||
if (liste.length < hedef[d]) {
|
||||
const adaylar = havuz
|
||||
.filter((p) => p.dilim === d && !kullanilan.has(p.id))
|
||||
.sort((a, b) => efektif(a.id) - efektif(b.id));
|
||||
for (const p of adaylar) {
|
||||
if (liste.length >= hedef[d]) break;
|
||||
kullanilan.add(p.id);
|
||||
liste.push({
|
||||
programId: p.id,
|
||||
gerekce:
|
||||
"Sıralamana ve seçimlerine uygun olduğu için listene eklendi.",
|
||||
});
|
||||
}
|
||||
}
|
||||
kurtarilan[d] = liste;
|
||||
}
|
||||
secim = kurtarilan;
|
||||
}
|
||||
|
||||
const programlar: RaporSonuc["programlar"] = {};
|
||||
@@ -328,19 +450,11 @@ export async function raporUret(
|
||||
}
|
||||
// Sıra ve satır metinleri deterministik: dilim içi iyi taban sıralamasından
|
||||
// kötüye dizilir (hayal -> dengeli -> garanti zaten bu yönde akar), sira
|
||||
// 1-24 koddan atanır; riskNotu + trendOzeti gerçek veriden üretilir.
|
||||
const efektif = (id: string) => {
|
||||
const p = havuzMap.get(id)!;
|
||||
return p.sira2025 ?? p.sira2024 ?? Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
const tercihler = (
|
||||
[
|
||||
["hayal", sonuc.hayal],
|
||||
["dengeli", sonuc.dengeli],
|
||||
["garanti", sonuc.garanti],
|
||||
] as const
|
||||
).flatMap(([dilim, liste]) =>
|
||||
[...liste].sort((a, b) => efektif(a.programId) - efektif(b.programId))
|
||||
// koddan atanır; riskNotu + trendOzeti gerçek veriden üretilir.
|
||||
const nihai = secim;
|
||||
const tercihler = DILIMLER.flatMap((dilim) =>
|
||||
[...nihai[dilim]]
|
||||
.sort((a, b) => efektif(a.programId) - efektif(b.programId))
|
||||
.map((t) => ({ dilim, ...t })),
|
||||
).map((t, i) => {
|
||||
const p = havuzMap.get(t.programId)!;
|
||||
@@ -355,9 +469,12 @@ export async function raporUret(
|
||||
});
|
||||
return {
|
||||
tercihler,
|
||||
genelDegerlendirme: sonuc.genelDegerlendirme,
|
||||
uyarilar: sonuc.uyarilar,
|
||||
genelDegerlendirme:
|
||||
genel.trim() ||
|
||||
`Listen ${toplamHedef} tercihten oluşuyor; hayal, dengeli ve garanti dilimleri sıralamana göre dengelendi.`,
|
||||
uyarilar: uyarilar.length > 0 ? uyarilar : undefined,
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user