Enhance error handling and user experience in AI-related features
Some checks failed
Deploy / deploy (push) Failing after 38m47s
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:
@@ -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ı." };
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useMemo, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { FileText, Lock, RefreshCcw } from "lucide-react";
|
||||
import { ArrowRight, FileText, Lock, RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SohbetPopup } from "./sohbet-popup";
|
||||
@@ -156,6 +156,8 @@ export function ListePaneli({
|
||||
if (durum === "uretiliyor") return <UretimGovdesi />;
|
||||
|
||||
if (durum === "kredi-yetersiz") {
|
||||
// Paketli kullanıcıya paket satma: onun ürünü top-up. Liste ve sohbet
|
||||
// geçmişi kalıcı; kredi yükleyince kaldığı yerden devam eder.
|
||||
return (
|
||||
<section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
|
||||
<Lock className="mx-auto size-8 text-slate-400" aria-hidden />
|
||||
@@ -163,15 +165,19 @@ export function ListePaneli({
|
||||
Kredin bu liste için yetmiyor
|
||||
</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-slate-600">
|
||||
Liste üretimi 3 kredi. Tercih Dönemi Paketi'yle 60 kredi, tam
|
||||
liste, PDF ve revizyon hakları birlikte gelir.
|
||||
{hasPaket
|
||||
? "Liste üretimi 3 kredi. +30 kredi yüklediğinde listen ve sohbet geçmişin duruyor, kaldığın yerden devam edersin."
|
||||
: "Liste üretimi 3 kredi. Tercih Dönemi Paketi'yle 60 kredi, tam liste, PDF ve revizyon hakları birlikte gelir."}
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href="/paket">299 TL — Paketi aktive et</Link>
|
||||
<Link href="/paket">
|
||||
{hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,11 @@ import { ManuelHarita } from "@/components/manuel-liste/manuel-harita";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { SiraGerekli } from "./sira-gerekli";
|
||||
|
||||
// listeOlustur aksiyonu 2 AI denemesi yapabiliyor (deneme başına 50 sn abort
|
||||
// sınırı, bkz. lib/ai/cagri.ts); platform limiti AI zaman aşımından SONRA
|
||||
// dolmalı ki kredi iadesi her zaman çalışabilsin.
|
||||
export const maxDuration = 120;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Tercih Listeni Kur",
|
||||
// Param uzayı sonsuz + içerik sıralamaya göre kişisel: indexlenmez ama
|
||||
|
||||
@@ -105,6 +105,8 @@ export function SihirbazBolumu({
|
||||
|
||||
// Aynı üretim için sabit requestId (retry çifte harcama yapmaz)
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
// Revizyon için aynı desen (bkz. listem/revizyon-kutusu.tsx)
|
||||
const revizyonIdRef = useRef<string | null>(null);
|
||||
const otomatikBasladi = useRef(false);
|
||||
const bolumRef = useRef<HTMLDivElement>(null);
|
||||
// Funnel CTA'ları (banner olayı dahil) tıklama anındaki bekleyen seçimi
|
||||
@@ -228,9 +230,17 @@ export function SihirbazBolumu({
|
||||
// Yapay Zeka çıktısı bu sayfada gösterilmez: üretim animasyonu açık kalır,
|
||||
// liste (paketsizse paywall'lı hâliyle) /listem'de karşılar.
|
||||
router.push("/listem");
|
||||
// Header'daki kredi pill'i layout'ta server-render edilir; push layout'u
|
||||
// yeniden çizmediği için düşen bakiye refresh ile güncellenir.
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
// Hata: aynı requestId'yi koru ki tekrar denemede çifte harcama olmasın
|
||||
// Sunucudan cevap geldiyse harcama ya hiç olmadı ya iade edildi;
|
||||
// sonraki deneme taze requestId ile başlar (aynı id ledger'daki UNIQUE'e
|
||||
// takılıp DUPLICATE çıkmazına giriyordu). requestId yalnızca cevap
|
||||
// alınamayan network hatasında (catch) korunur — orada çifte harcama
|
||||
// riski gerçek.
|
||||
requestIdRef.current = null;
|
||||
if (sonuc.code === "KREDI" || sonuc.code === "PAKET") {
|
||||
setDurum("kredi-yetersiz");
|
||||
} else if (sonuc.code === "AUTH") {
|
||||
@@ -389,10 +399,14 @@ export function SihirbazBolumu({
|
||||
sira={sira}
|
||||
acilis={acilis}
|
||||
onRevize={async (feedback) => {
|
||||
if (!revizyonIdRef.current) {
|
||||
revizyonIdRef.current = crypto.randomUUID();
|
||||
}
|
||||
const sonuc = await listeRevize({
|
||||
feedback,
|
||||
requestId: crypto.randomUUID(),
|
||||
requestId: revizyonIdRef.current,
|
||||
});
|
||||
revizyonIdRef.current = null;
|
||||
if (sonuc.ok) {
|
||||
olay("liste_revize", { sayfa: "sonuc" });
|
||||
setRapor({
|
||||
|
||||
Reference in New Issue
Block a user