Update package.json scripts, enhance layout with new components, and improve API error handling
Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
- Added new scripts for database operations and temporary production in package.json. - Integrated KaydetBannerLazy component into the layout for improved user notifications. - Enhanced API error handling in the soru route to mark credit exhaustion. - Updated the IletisimPage for better button styling and user experience. - Refactored ListemPage to streamline user flow and improve session handling. - Removed unused ListePaneli component to clean up the codebase.
This commit is contained in:
263
scripts/tadimlik-uret.ts
Normal file
263
scripts/tadimlik-uret.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel")
|
||||
* kombinasyonları için 1'er örnek tercih satırı (gerekce/riskNotu/trendOzeti)
|
||||
* üretip app.db'deki tadimlik_havuzu tablosuna yazar.
|
||||
*
|
||||
* pnpm tadimlik # boş anahtarları doldurur (idempotent)
|
||||
* pnpm tadimlik --force # mevcut satırları da yeniden üretir
|
||||
* pnpm tadimlik --kova=10-50k --tur=say --kategori=muhendislik-teknoloji
|
||||
* pnpm tadimlik --mock # anahtar olsa da mock metin (dev/test)
|
||||
* pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms)
|
||||
*
|
||||
* AI anahtarı yoksa (dev) mock metin üretir — akış uçtan uca test edilebilir.
|
||||
* Gemini ücretsiz katmanı dakikada ~5 istek kabul eder; gerçek üretimde
|
||||
* --bekleme=13000 gibi bir tempo kullan.
|
||||
* Anonim trafik LLM'e asla istek atmaz; tek maliyet bu script'in koşusudur.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { z } from "zod";
|
||||
import { appDb, schema } from "../src/lib/appdb";
|
||||
import { dilimAra, type DilimKey, type Program, type PuanTuruKey, PUAN_TURLERI } from "../src/lib/db";
|
||||
import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler";
|
||||
import { havuzuKompaktJson, turLabel } from "../src/lib/rapor-havuzu";
|
||||
import { GENEL_KATEGORI, KOVALAR, kategoriSlugBul } from "../src/lib/tadimlik-havuzu";
|
||||
import { anahtarVar } from "../src/lib/ai/client";
|
||||
import { yapilandirilmisUret } from "../src/lib/ai/cagri";
|
||||
|
||||
// Next dışında koşuyoruz; .env dosyalarını elle yükle (mevcut env kazanır).
|
||||
function envYukle(dosya: string): void {
|
||||
if (!existsSync(dosya)) return;
|
||||
for (const satir of readFileSync(dosya, "utf8").split("\n")) {
|
||||
const m = satir.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||
if (m && process.env[m[1]] === undefined) {
|
||||
process.env[m[1]] = m[2].replace(/^"(.*)"$/, "$1").trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
envYukle(".env.local");
|
||||
envYukle(".env");
|
||||
|
||||
const TadimlikSchema = z.object({
|
||||
gerekce: z.string(),
|
||||
riskNotu: z.string(),
|
||||
trendOzeti: z.string(),
|
||||
});
|
||||
|
||||
// rapor.ts'teki SISTEM kurallarının tek satırlık versiyonu.
|
||||
const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Sana GERÇEK
|
||||
YÖK Atlas verisiyle TEK bir program ve temsili bir aday tanımı verilecek; bu
|
||||
program için tercih listesi satırı metinleri üreteceksin.
|
||||
|
||||
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
|
||||
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ş.`;
|
||||
|
||||
function mockAktif(): boolean {
|
||||
if (process.argv.includes("--mock")) return true;
|
||||
return process.env.NODE_ENV !== "production" && !anahtarVar();
|
||||
}
|
||||
|
||||
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.",
|
||||
trendOzeti:
|
||||
"(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.",
|
||||
};
|
||||
}
|
||||
|
||||
/** rapor.ts programBilgisi ile aynı snapshot şekli. */
|
||||
function programBilgisi(p: Program) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden
|
||||
* (boşsa "garanti") kategoriye uyan, 2025'te en çok yerleşen alan program.
|
||||
* Tanınır/dolu programlar tadımlıkta daha ikna edici.
|
||||
*/
|
||||
function programSec(
|
||||
temsiliSira: number,
|
||||
tur: PuanTuruKey,
|
||||
kategori: string | null,
|
||||
): { program: Program; dilim: DilimKey } | null {
|
||||
for (const dilim of ["dengeli", "garanti"] as const) {
|
||||
const { programlar } = dilimAra(temsiliSira, tur, dilim, { limit: 60 });
|
||||
const uygunlar = kategori
|
||||
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori]))
|
||||
: programlar;
|
||||
if (uygunlar.length === 0) continue;
|
||||
const secilen = [...uygunlar].sort(
|
||||
(a, b) => (b.yerlesen2025 ?? -1) - (a.yerlesen2025 ?? -1),
|
||||
)[0];
|
||||
return { program: secilen, dilim };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function metinUret(
|
||||
p: Program,
|
||||
dilim: DilimKey,
|
||||
temsiliSira: number,
|
||||
tur: PuanTuruKey,
|
||||
kategori: string | null,
|
||||
): Promise<z.infer<typeof TadimlikSchema> | null> {
|
||||
if (mockAktif()) return mockMetin(p, temsiliSira, kategori);
|
||||
|
||||
const kullanici = [
|
||||
`Temsili aday: başarı sıralaması ~${temsiliSira.toLocaleString("tr-TR")} bandında, puan türü ${turLabel(tur)}.`,
|
||||
kategori ? `Adayın ilgi alanı: ${kategori}.` : null,
|
||||
`PROGRAM (yalnızca bunun için metin üret): ${havuzuKompaktJson([{ ...p, dilim }])}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
// Not: Gemini'de thinking bütçesi de bu sınırdan düşer; dar tutulursa
|
||||
// çıktı kırpılıp şema doğrulaması boşa düşüyor.
|
||||
return yapilandirilmisUret({
|
||||
system: SISTEM,
|
||||
kullanici,
|
||||
sema: TadimlikSchema,
|
||||
maxTokens: 4000,
|
||||
});
|
||||
}
|
||||
|
||||
function bayrak(ad: string): string | undefined {
|
||||
const arg = process.argv.find((a) => a.startsWith(`--${ad}=`));
|
||||
return arg?.slice(ad.length + 3);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const force = process.argv.includes("--force");
|
||||
const kovaFiltre = bayrak("kova");
|
||||
const turFiltre = bayrak("tur");
|
||||
const kategoriFiltre = bayrak("kategori");
|
||||
|
||||
const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter(
|
||||
(t) => !turFiltre || t === turFiltre,
|
||||
);
|
||||
const kategoriler: { ad: string | null; slug: string }[] = [
|
||||
{ ad: null, slug: GENEL_KATEGORI },
|
||||
...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })),
|
||||
].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre);
|
||||
const kovalar = KOVALAR.filter((k) => !kovaFiltre || k.slug === kovaFiltre);
|
||||
|
||||
const mevcutlar = new Set(
|
||||
(
|
||||
await appDb
|
||||
.select({
|
||||
kovaSlug: schema.tadimlikHavuzu.kovaSlug,
|
||||
tur: schema.tadimlikHavuzu.tur,
|
||||
kategoriSlug: schema.tadimlikHavuzu.kategoriSlug,
|
||||
})
|
||||
.from(schema.tadimlikHavuzu)
|
||||
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`),
|
||||
);
|
||||
|
||||
if (mockAktif()) {
|
||||
console.log("AI anahtarı yok — mock metinlerle üretiliyor (dev).");
|
||||
}
|
||||
|
||||
let uretilen = 0;
|
||||
let atlananMevcut = 0;
|
||||
let atlananBos = 0;
|
||||
|
||||
for (const kova of kovalar) {
|
||||
for (const tur of turlar) {
|
||||
for (const kategori of kategoriler) {
|
||||
const anahtar = `${kova.slug}|${tur}|${kategori.slug}`;
|
||||
if (!force && mevcutlar.has(anahtar)) {
|
||||
atlananMevcut++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const secim = programSec(kova.temsiliSira, tur, kategori.ad);
|
||||
if (!secim) {
|
||||
atlananBos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const metin = await metinUret(
|
||||
secim.program,
|
||||
secim.dilim,
|
||||
kova.temsiliSira,
|
||||
tur,
|
||||
kategori.ad,
|
||||
);
|
||||
if (!metin) {
|
||||
console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`);
|
||||
atlananBos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
await appDb
|
||||
.insert(schema.tadimlikHavuzu)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
kovaSlug: kova.slug,
|
||||
tur,
|
||||
kategoriSlug: kategori.slug,
|
||||
programId: secim.program.id,
|
||||
dilim: secim.dilim,
|
||||
gerekce: metin.gerekce,
|
||||
riskNotu: metin.riskNotu,
|
||||
trendOzeti: metin.trendOzeti,
|
||||
program: programBilgisi(secim.program),
|
||||
ornekSira: kova.temsiliSira,
|
||||
uretimAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.tadimlikHavuzu.kovaSlug,
|
||||
schema.tadimlikHavuzu.tur,
|
||||
schema.tadimlikHavuzu.kategoriSlug,
|
||||
],
|
||||
set: {
|
||||
programId: secim.program.id,
|
||||
dilim: secim.dilim,
|
||||
gerekce: metin.gerekce,
|
||||
riskNotu: metin.riskNotu,
|
||||
trendOzeti: metin.trendOzeti,
|
||||
program: programBilgisi(secim.program),
|
||||
ornekSira: kova.temsiliSira,
|
||||
uretimAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
uretilen++;
|
||||
console.log(`✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`);
|
||||
// Gerçek LLM'de rate limit dostu ardışık tempo
|
||||
if (!mockAktif()) {
|
||||
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
|
||||
await new Promise((r) => setTimeout(r, bekleme));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Bitti: ${uretilen} üretildi, ${atlananMevcut} mevcut atlandı, ${atlananBos} içeriksiz/şemasız atlandı.`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((hata) => {
|
||||
console.error(hata);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user