File size: 9,778 Bytes
35420f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 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; |