import { NextRequest, NextResponse } from 'next/server'; import { withDb } from '@/lib/api-handler'; import { db } from '@/lib/db'; import { getCookieToken, getSessionUser } from '@/lib/auth'; import { chatSchema } from '@/lib/validation'; import { chatRateLimit } from '@/lib/rate-limit'; function stripHtmlTags(input: string): string { return input.replace(/<[^>]*>/g, '').trim(); } function buildInvoiceContext(userId: string): string { // Intentionally synchronous placeholder — actual query runs async return ''; } async function handler(request: NextRequest) { try { // ── Auth ────────────────────────────────────────────────────────── const token = getCookieToken(request); if (!token) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } const user = await getSessionUser(token); if (!user) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } // ── Plan Check (Pro / Enterprise only) ─────────────────────────── if (user.plan !== 'pro' && user.plan !== 'enterprise') { return NextResponse.json( { error: 'AI Chat is available on Pro and Enterprise plans. Please upgrade your plan.' }, { status: 403 } ); } // ── Rate Limit ──────────────────────────────────────────────────── const { allowed, retryAfterMs } = await chatRateLimit(`chat:${user.id}`); if (!allowed) { return NextResponse.json( { error: 'Chat rate limit exceeded. Please try again later.' }, { status: 429, headers: { 'Retry-After': String(Math.ceil(retryAfterMs / 1000)) }, } ); } // ── Validate Body ───────────────────────────────────────────────── const body = await request.json(); const parsed = chatSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: parsed.error.issues[0].message }, { status: 400 } ); } const { message: rawMessage, history = [] } = parsed.data; const message = stripHtmlTags(rawMessage); // ── Fetch User's Invoices for Context ───────────────────────────── const recentInvoices = await db.invoice.findMany({ where: { userId: user.id }, orderBy: { createdAt: 'desc' }, take: 20, }); const invoiceSummary = recentInvoices.length > 0 ? recentInvoices .map( (inv) => `- Invoice ${inv.invNumber || 'N/A'} from ${inv.vendor || 'Unknown'}: ${inv.currency} ${inv.total ?? 0} (${inv.status}, ${inv.isDuplicate ? 'duplicate' : 'unique'}, confidence: ${inv.confidence ? (inv.confidence * 100).toFixed(0) + '%' : 'N/A'})` ) .join('\n') : 'No invoices found.'; const totalAmount = recentInvoices.reduce((sum, inv) => sum + (inv.total ?? 0), 0); const duplicateCount = recentInvoices.filter((inv) => inv.isDuplicate).length; const systemPrompt = `You are OmniParse AI, an intelligent invoice processing assistant. You help users understand, analyze, and manage their invoices. Be concise, helpful, and professional. Here is the user's recent invoice data: Total invoices (recent 20): ${recentInvoices.length} Total amount: USD ${totalAmount.toFixed(2)} Duplicates found: ${duplicateCount} ${invoiceSummary} The user is on the "${user.plan}" plan. Answer questions about their invoices, provide spending insights, flag potential duplicates, and help with invoice management. Keep responses concise and actionable.`; // ── Try AI SDK, Fall Back to Demo Response ──────────────────────── let aiResponse: string; try { // Attempt to use z-ai-web-dev-sdk for real AI responses const { default: ZAI } = await import('z-ai-web-dev-sdk'); const zai = await ZAI.create(); const messages: Array<{ role: string; content: string }> = [ { role: 'system', content: systemPrompt }, ...history.map((h) => ({ role: h.role, content: h.content })), { role: 'user', content: message }, ]; const result = await zai.chat.completions.create({ model: 'default', messages: messages as never, }); const completion = result as Record; const choices = completion.choices as Array<{ message: { content: string } }>; aiResponse = choices?.[0]?.message?.content ?? 'I was unable to generate a response. Please try again.'; } catch { // Fallback: Generate a helpful context-aware demo response aiResponse = generateDemoResponse(message, recentInvoices, totalAmount, duplicateCount, user.plan); } return NextResponse.json({ reply: aiResponse }); } catch (error) { console.error('[CHAT_ERROR]', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } } // ─── Demo Response Generator (fallback when AI SDK unavailable) ──────────────── function generateDemoResponse( message: string, invoices: Array<{ vendor: string | null; invNumber: string | null; total: number | null; currency: string; status: string; isDuplicate: boolean; confidence: number | null; invDate: string | null; dueDate: string | null; }>, totalAmount: number, duplicateCount: number, plan: string ): string { const msgLower = message.toLowerCase(); // Summary / overview queries if (msgLower.includes('summary') || msgLower.includes('overview') || msgLower.includes('how many')) { if (invoices.length === 0) { return "You don't have any invoices yet. Upload some invoices to get started, and I'll help you analyze them!"; } const vendors = [...new Set(invoices.map((i) => i.vendor).filter(Boolean))]; const avgAmount = totalAmount / invoices.length; return `Here's a quick overview of your invoices:\n\n` + `📊 **Total invoices:** ${invoices.length}\n` + `💰 **Total amount:** $${totalAmount.toFixed(2)}\n` + `📈 **Average invoice:** $${avgAmount.toFixed(2)}\n` + `🏢 **Unique vendors:** ${vendors.length} (${vendors.slice(0, 5).join(', ')}${vendors.length > 5 ? '...' : ''})\n` + `🔄 **Duplicates:** ${duplicateCount}\n\n` + `Would you like me to break this down by vendor or time period?`; } // Duplicate queries if (msgLower.includes('duplicate') || msgLower.includes('repeat')) { if (duplicateCount === 0) { return "Great news! No duplicates were detected in your invoices. The system automatically flags potential duplicates during upload based on vendor and amount matching."; } const dups = invoices.filter((i) => i.isDuplicate); const dupSummary = dups .map((i) => `• ${i.invNumber || 'N/A'} from ${i.vendor || 'Unknown'} — $${i.total ?? 0}`) .join('\n'); return `⚠️ **${duplicateCount} potential duplicate(s) found:**\n\n${dupSummary}\n\n` + `These were flagged because they share the same vendor and a similar amount with another invoice. You can review them in the "Duplicates" tab or delete any that are truly repeated.`; } // Vendor queries if (msgLower.includes('vendor') || msgLower.includes('supplier') || msgLower.includes('company')) { const vendorMap = new Map(); for (const inv of invoices) { const v = inv.vendor || 'Unknown'; const existing = vendorMap.get(v) ?? { count: 0, total: 0 }; vendorMap.set(v, { count: existing.count + 1, total: existing.total + (inv.total ?? 0), }); } const lines = [...vendorMap.entries()] .sort((a, b) => b[1].total - a[1].total) .map(([v, data]) => `• **${v}**: ${data.count} invoice(s), totaling $${data.total.toFixed(2)}`) .join('\n'); return `Here's a breakdown by vendor:\n\n${lines || 'No vendor data available.'}`; } // Spending / amount queries if (msgLower.includes('spend') || msgLower.includes('cost') || msgLower.includes('total') || msgLower.includes('amount')) { if (invoices.length === 0) return "No invoices to analyze yet."; const avgAmount = totalAmount / invoices.length; const maxInv = invoices.reduce((a, b) => (b.total ?? 0) > (a.total ?? 0) ? b : a); const minInv = invoices.reduce((a, b) => (b.total ?? 0) < (a.total ?? 0) ? b : a); return `💰 **Spending Analysis:**\n\n` + `• Total: $${totalAmount.toFixed(2)} across ${invoices.length} invoices\n` + `• Average: $${avgAmount.toFixed(2)} per invoice\n` + `• Largest: $${maxInv.total ?? 0} from ${maxInv.vendor || 'Unknown'} (${maxInv.invNumber || 'N/A'})\n` + `• Smallest: $${minInv.total ?? 0} from ${minInv.vendor || 'Unknown'} (${minInv.invNumber || 'N/A'})\n\n` + `Want me to analyze spending trends or identify cost-saving opportunities?`; } // Default helpful response return `I can help you with your invoices! Here are some things you can ask me:\n\n` + `• **"Give me a summary"** — Overview of all your invoices\n` + `• **"Show duplicates"** — Find potential duplicate invoices\n` + `• **"Breakdown by vendor"** — Spending per vendor\n` + `• **"Spending analysis"** — Cost insights and trends\n\n` + `You currently have **${invoices.length} invoice(s)** totaling **$${totalAmount.toFixed(2)}** on the **${plan}** plan. How can I help?`; } // Suppress unused import warning void buildInvoiceContext; export const POST = withDb(handler);