import { eq, desc, asc } from "drizzle-orm"; import type { NextRequest } from "next/server"; import { getSession } from "@/lib/session"; import { appDb, schema } from "@/lib/appdb"; import { spendCreditForMessage, grantCredits } from "@/lib/credits"; import { getAnthropic, SOHBET_MODEL, SORUMLULUK_REDDI } from "@/lib/ai/client"; import { listeOzetiCikar, type RaporSonuc } from "@/lib/ai/rapor"; import type { RaporParams } from "@/lib/rapor-havuzu"; import { secimOzeti } from "@/lib/sihirbaz"; import { PUAN_TURLERI } from "@/lib/db"; const MAX_GECMIS = 12; const MAX_MESAJ_UZUNLUK = 2000; function sistemPromptu(opts: { params: RaporParams; listeOzeti: string }): string { const { params } = opts; const secimVar = (params.kategoriler?.length ?? 0) > 0 || (params.iller?.length ?? 0) > 0 || (params.oncelikler?.length ?? 0) > 0 || (params.universiteTipi && params.universiteTipi !== "farketmez"); const parcalar = [ `Sen KolayTercih'in YKS tercih danışmanısın. Türkçe, samimi ama profesyonel konuş. Aday az önce sana verilen 24'lük tercih listesini oluşturdu. Görevin: adayın bu liste ve seçimleri BAĞLAMINDA sorularını (bölüm kıyaslama, şehir, risk, kontenjan, iş imkânları, tercih sırası) net ve dürüst yanıtlamak. KURALLAR: - Cevaplarını adayın 24'lük listesi ve seçimleri bağlamında ver; listedeki programlara referans verebilirsin. - ASLA yerleşme garantisi verme; "yüksek/orta/düşük ihtimal" dili kullan. - Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle. - Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat. - Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır. - Tercih dışı konularda (ödev, kod, genel sohbet) kibarca reddet ve tercih konusuna dön. - Kesin konuşmaktan kaçın: "${SORUMLULUK_REDDI}"`, `ADAY BİLGİSİ: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${PUAN_TURLERI[params.tur]}.`, `ADAYIN 24'LÜK LİSTESİ:\n${opts.listeOzeti}`, ]; if (secimVar) { parcalar.push( `ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({ kategoriler: params.kategoriler ?? [], iller: params.iller ?? [], universiteTipi: params.universiteTipi ?? "farketmez", oncelikler: params.oncelikler ?? [], })}`, ); } return parcalar.join("\n\n"); } // Widget açıldığında geçmişi ve kredi bakiyesini döner export async function GET() { const session = await getSession(); if (!session) { return Response.json({ error: "Oturum gerekli." }, { status: 401 }); } const [mesajlar, kullanici] = await Promise.all([ appDb .select({ id: schema.chatMessages.id, role: schema.chatMessages.role, content: schema.chatMessages.content, }) .from(schema.chatMessages) .where(eq(schema.chatMessages.userId, session.user.id)) .orderBy(asc(schema.chatMessages.createdAt)) .limit(100), appDb.query.user.findFirst({ where: eq(schema.user.id, session.user.id), columns: { creditBalance: true }, }), ]); return Response.json({ mesajlar, kredi: kullanici?.creditBalance ?? 0, }); } export async function POST(request: NextRequest) { const session = await getSession(); if (!session) { return Response.json({ error: "Oturum gerekli." }, { status: 401 }); } let body: { message?: unknown; clientMessageId?: unknown }; try { body = await request.json(); } catch { return Response.json({ error: "Geçersiz istek." }, { status: 400 }); } const mesaj = typeof body.message === "string" ? body.message.trim() : ""; const clientMessageId = typeof body.clientMessageId === "string" ? body.clientMessageId : ""; if (!mesaj || mesaj.length > MAX_MESAJ_UZUNLUK || !clientMessageId) { return Response.json({ error: "Geçersiz mesaj." }, { status: 400 }); } // Soru sormak için önce liste oluşturulmuş olmalı const rapor = await appDb.query.reports.findFirst({ where: eq(schema.reports.userId, session.user.id), }); if (!rapor?.result) { return Response.json( { error: "Önce listeni oluşturmalısın.", code: "NO_REPORT" }, { status: 409 }, ); } // Geçmişi krediyi düşmeden ÖNCE oku ki kullanıcı mesajı dahil olmasın const gecmis = ( await appDb .select() .from(schema.chatMessages) .where(eq(schema.chatMessages.userId, session.user.id)) .orderBy(desc(schema.chatMessages.createdAt)) .limit(MAX_GECMIS) ).reverse(); // Kredi düşümü stream'den önce — yetersizse hiç başlama const harcama = await spendCreditForMessage({ userId: session.user.id, content: mesaj, clientMessageId, }); if (!harcama.ok) { if (harcama.error === "INSUFFICIENT") { return Response.json( { error: "Kredin bitti.", code: "INSUFFICIENT" }, { status: 402 }, ); } return Response.json( { error: "Bu mesaj zaten işlendi.", code: "DUPLICATE" }, { status: 409 }, ); } const kullaniciMesajId = harcama.messageId!; const system = sistemPromptu({ params: rapor.params as RaporParams, listeOzeti: listeOzetiCikar(rapor.result as RaporSonuc), }); let client; try { client = getAnthropic(); } catch { await grantCredits({ userId: session.user.id, delta: 1, reason: "refund", refId: kullaniciMesajId, }); return Response.json( { error: "AI altyapısı yapılandırılmadı." }, { status: 503 }, ); } const mesajlar = [ ...gecmis.map((m) => ({ role: m.role as "user" | "assistant", content: m.content, })), { role: "user" as const, content: mesaj }, ]; const encoder = new TextEncoder(); const userId = session.user.id; let stream; try { stream = client.messages.stream({ model: SOHBET_MODEL, max_tokens: 1024, system, messages: mesajlar, }); } catch { await grantCredits({ userId, delta: 1, reason: "refund", refId: kullaniciMesajId }); return Response.json({ error: "AI'ya ulaşılamadı." }, { status: 502 }); } const readable = new ReadableStream({ async start(controller) { let tamamlandi = false; try { for await (const event of stream) { if ( event.type === "content_block_delta" && event.delta.type === "text_delta" ) { controller.enqueue(encoder.encode(event.delta.text)); } } const final = await stream.finalMessage(); const metin = final.content .filter((b) => b.type === "text") .map((b) => b.text) .join(""); await appDb.insert(schema.chatMessages).values({ id: crypto.randomUUID(), userId, role: "assistant", content: metin, createdAt: new Date(), }); tamamlandi = true; controller.close(); } catch (err) { if (!tamamlandi) { // Hiç çıktı üretilmeden hata olduysa krediyi iade et await grantCredits({ userId, delta: 1, reason: "refund", refId: kullaniciMesajId, }).catch(() => {}); } console.error("[soru] stream hatası:", err); controller.error(err); } }, }); return new Response(readable, { headers: { "Content-Type": "text/plain; charset=utf-8", "X-Content-Type-Options": "nosniff", "Cache-Control": "no-store", }, }); }