Update package.json scripts, enhance layout with new components, and improve API error handling
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:
bilalgursen
2026-08-06 02:27:42 +03:00
parent 488845ae1f
commit e62c6f07df
77 changed files with 4132 additions and 1659 deletions

263
scripts/tadimlik-uret.ts Normal file
View 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);
});