| import { NextRequest, NextResponse } from 'next/server'; |
| import { db } from '@/lib/db'; |
| import { getCookieToken, getSessionUser } from '@/lib/auth'; |
| import { chatSchema } from '@/lib/validation'; |
| import { chatRateLimit } from '@/lib/rate-limit'; |
|
|
| function buildInvoiceContext(userId: string): string { |
| |
| return ''; |
| } |
|
|
| export async function POST(request: NextRequest) { |
| try { |
| |
| 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 }); |
| } |
|
|
| |
| 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 } |
| ); |
| } |
|
|
| |
| 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)) }, |
| } |
| ); |
| } |
|
|
| |
| 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, history = [] } = parsed.data; |
|
|
| |
| 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.`; |
|
|
| |
| let aiResponse: string; |
|
|
| try { |
| |
| 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<string, unknown>; |
| 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 { |
| |
| 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 } |
| ); |
| } |
| } |
|
|
| |
|
|
| 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(); |
|
|
| |
| 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?`; |
| } |
|
|
| |
| 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.`; |
| } |
|
|
| |
| if (msgLower.includes('vendor') || msgLower.includes('supplier') || msgLower.includes('company')) { |
| const vendorMap = new Map<string, { count: number; total: number }>(); |
| 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.'}`; |
| } |
|
|
| |
| 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?`; |
| } |
|
|
| |
| 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?`; |
| } |
|
|
| |
| void buildInvoiceContext; |