File size: 10,013 Bytes
eaab0a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
216
217
218
219
220
221
222
223
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<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;

export const POST = withDb(handler);