test7 / src /app /api /chat /route.ts
simikkk's picture
Upload 86 files
e218aaf verified
Raw
History Blame Contribute Delete
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;