Enhance error handling and user experience in AI-related features
Some checks failed
Deploy / deploy (push) Failing after 38m47s

- Introduced a mechanism to save partially generated responses in case of errors, ensuring users do not lose their input.
- Updated the RevizyonKutusu component to utilize a persistent request ID for retrying failed requests without double charging credits.
- Improved the Sayfalama component to support client-side pagination without changing the URL, enhancing user navigation.
- Added a timeout for AI generation requests to ensure credits are refunded in case of prolonged processing.
- Refactored various components for better clarity and responsiveness, including updates to the BolumProgramListesi and ListePaneli components.
This commit is contained in:
bilalgursen
2026-08-03 14:12:06 +03:00
parent c2aba25814
commit a5c2bcbc4d
13 changed files with 384 additions and 180 deletions

View File

@@ -1,6 +1,6 @@
"use server";
import { eq, sql } from "drizzle-orm";
import { and, eq, sql } from "drizzle-orm";
import { getSession, getCurrentUser } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { PUAN_TURLERI, type PuanTuruKey } from "@/lib/db";
@@ -41,6 +41,11 @@ function siraTurGecerli(sira: number, tur: string): tur is PuanTuruKey {
function aiHataMesaji(err: unknown): string {
if (err instanceof RaporUretimHatasi) return err.message;
// AbortSignal.timeout: DOMException "TimeoutError"; SDK'lar abort'u kendi
// hata sınıflarıyla sarabiliyor (ör. APIUserAbortError) — ada göre yakala.
if (err instanceof Error && /abort|timeout/i.test(err.name)) {
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; harcanan kredin otomatik iade edildi. Tekrar dener misin?";
}
if (err instanceof Error && err.message === "AI_KEY_MISSING") {
// Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj
console.error("[liste] Yapay Zeka anahtarı eksik — üretim başlatılamadı");
@@ -114,7 +119,23 @@ export async function listeOlustur(input: {
error: `Liste oluşturmak için ${RAPOR_KREDI} kredi gerekli.`,
};
}
// DUPLICATE: bu requestId zaten işlendi → mevcut raporu döndür
// DUPLICATE: bu requestId zaten işlendi. Önceki deneme başarısız olup
// iade edildiyse mevcut (eski) raporu yeniymiş gibi döndürme; taze bir
// requestId ile yeniden denemesini iste.
const iade = await appDb.query.creditLedger.findFirst({
where: and(
eq(schema.creditLedger.reason, "refund"),
eq(schema.creditLedger.refId, input.requestId),
),
});
if (iade) {
return {
ok: false,
code: "HATA",
error:
"Önceki denemen başarısız olduğu için kredin iade edilmişti. Tekrar dener misin?",
};
}
const mevcut = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
@@ -226,6 +247,36 @@ export async function listeRevize(input: {
error: `Revizyon için ${RAPOR_KREDI} kredi gerekli.`,
};
}
// DUPLICATE: bu requestId zaten işlendi. İade varsa önceki deneme
// başarısızdı → taze bir denemeye yönlendir. Yoksa revizyon tamamlanmış
// ama cevap istemciye ulaşmamıştır → güncel raporu döndür.
const iade = await appDb.query.creditLedger.findFirst({
where: and(
eq(schema.creditLedger.reason, "refund"),
eq(schema.creditLedger.refId, input.requestId),
),
});
if (iade) {
return {
ok: false,
code: "HATA",
error:
"Önceki denemen başarısız olduğu için kredin iade edilmişti. Tekrar dener misin?",
};
}
const guncel = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
if (guncel?.result) {
return {
ok: true,
rapor: guncel.result as RaporSonuc,
params: guncel.params as RaporParams,
revisionCount: guncel.revisionCount,
kredi: user.creditBalance,
hasPaket: true,
};
}
return { ok: false, code: "HATA", error: "İstek tekrarlandı." };
}