Download src/app/api/chat/route.ts from simikkk/test7: direct link, hf CLI and curl.
- Browser
- Download file 9.78 kB
-
https://huggingface.co/spaces/simikkk/test7/resolve/main/src/app/api/chat/route.ts
- Command line
-
hf download hf://spaces/simikkk/test7/src/app/api/chat/route.ts
-
curl -L -o route.ts https://huggingface.co/spaces/simikkk/test7/resolve/main/src/app/api/chat/route.ts
9.78 kB
| 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 { | |
| // Intentionally synchronous placeholder — actual query runs async | |
| return ''; | |
| } | |
| export async function POST(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, history = [] } = parsed.data; | |
| // ── 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<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 { | |
| // 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<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.'}`; | |
| } | |
| // 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; |