File size: 6,173 Bytes
94c9882
 
c92b696
 
 
 
 
f580c68
698bbca
f580c68
 
 
698bbca
 
94c9882
 
f580c68
 
c92b696
 
 
 
 
94c9882
f580c68
94c9882
f580c68
 
 
 
 
 
698bbca
f580c68
 
 
c92b696
 
 
 
 
f580c68
 
 
 
 
c92b696
 
 
f580c68
94c9882
f580c68
698bbca
94c9882
f580c68
94c9882
c92b696
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94c9882
f580c68
 
 
c92b696
 
 
f580c68
 
 
698bbca
f580c68
 
c92b696
 
 
 
 
 
 
 
 
 
 
f580c68
 
 
 
 
 
 
 
 
 
 
c92b696
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f580c68
 
c92b696
 
 
 
 
 
 
 
 
 
f580c68
 
c92b696
 
f580c68
 
 
 
 
c92b696
 
f580c68
c92b696
 
 
 
 
 
 
 
 
 
 
 
94c9882
f580c68
 
 
c92b696
 
 
 
 
 
 
 
94c9882
c92b696
 
 
 
 
 
94c9882
 
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
import { NextRequest, NextResponse } from 'next/server';

/**
 * AI Chat API route — calls NVIDIA NIM directly for fast responses.
 * All API keys are read from environment variables (never hardcoded).
 */

const NVIDIA_API_URL = 'https://integrate.api.nvidia.com/v1/chat/completions';
const NVIDIA_API_KEY = process.env.NVIDIA_API_KEY || '';
const BRAIN_MODEL = 'meta/llama-3.1-8b-instruct';
const VISION_MODEL = 'meta/llama-3.2-11b-vision-instruct';

const SUPABASE_URL = process.env.SUPABASE_URL || 'https://tcwdbokruvlizkxcpkzj.supabase.co';
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
const COOKIE_NAME = 'cellex_session_id';

const SYSTEM_PROMPT = `You are Cellex AI, a friendly shopping assistant for Cellex — Nigeria's #1 social commerce marketplace.
Help users find products, compare prices, and make shopping decisions.
Prices are in Nigerian Naira (₦). Be concise, friendly, and helpful (2-3 sentences max).
If users ask about specific products, mention you can search for them.
For return policy: 7-day returns on most items in original condition.
For delivery: 48-hour shipping within Nigeria, free shipping on orders over ₦50,000.
For payment: PalmPay bank transfer with auto-verification, pay on delivery available.`;

const SEARCH_KEYWORDS = ['search', 'find', 'looking for', 'need', 'want', 'buy', 'show me', 'do you have', 'available', 'price', 'how much', 'under', 'over', 'cheap', 'best', 'recommend'];

function needsProductSearch(message: string): boolean {
  const lower = message.toLowerCase();
  return SEARCH_KEYWORDS.some(kw => lower.includes(kw));
}

async function searchProducts(query: string): Promise<any[]> {
  if (!SUPABASE_ANON_KEY) return [];
  try {
    const resp = await fetch(`${SUPABASE_URL}/functions/v1/products`, {
      method: 'POST',
      headers: {
        'apikey': SUPABASE_ANON_KEY,
        'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ op: 'search', query }),
    });
    if (!resp.ok) return [];
    const data = await resp.json();
    return (data.results || data.products || []).slice(0, 6);
  } catch {
    return [];
  }
}

async function describeImage(imageUrl: string): Promise<string> {
  if (!NVIDIA_API_KEY) return '';
  try {
    const resp = await fetch(NVIDIA_API_URL, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${NVIDIA_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: VISION_MODEL,
        messages: [{
          role: 'user',
          content: [
            { type: 'text', text: 'Describe this product in detail (color, type, material, brand if visible) so I can search for it.' },
            { type: 'image_url', image_url: { url: imageUrl } },
          ],
        }],
        max_tokens: 150,
        stream: false,
      }),
    });
    if (!resp.ok) return '';
    const data = await resp.json();
    return data?.choices?.[0]?.message?.content || '';
  } catch {
    return '';
  }
}

async function callNVIDIA(messages: Array<{ role: string; content: string }>, maxTokens: number = 200): Promise<string> {
  if (!NVIDIA_API_KEY) throw new Error('NVIDIA_API_KEY not set');
  const resp = await fetch(NVIDIA_API_URL, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NVIDIA_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: BRAIN_MODEL,
      messages,
      max_tokens: maxTokens,
      temperature: 0.5,
      stream: false,
    }),
  });
  if (!resp.ok) throw new Error(`NVIDIA HTTP ${resp.status}`);
  const data = await resp.json();
  return data?.choices?.[0]?.message?.content || '';
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { message, context, history, image } = body;

    if (!message || typeof message !== 'string') {
      return NextResponse.json({ success: false, error: 'Missing "message"' }, { status: 400 });
    }

    const messages: Array<{ role: string; content: string }> = [
      { role: 'system', content: SYSTEM_PROMPT },
    ];

    if (Array.isArray(history)) {
      for (const msg of history.slice(-6)) {
        if (msg.role && msg.content) {
          messages.push({ role: msg.role, content: msg.content });
        }
      }
    }

    let userMessage = message;
    if (image) {
      const description = await describeImage(image);
      if (description) {
        userMessage = `User uploaded an image of: ${description}. User request: ${message}`;
      }
    }

    if (context) {
      messages.push({ role: 'system', content: `Context: ${context}` });
    }

    const shouldSearch = needsProductSearch(message) || (context && context.includes('Search overview'));

    let products: any[] = [];
    if (shouldSearch) {
      const [aiReply, searchResults] = await Promise.all([
        callNVIDIA([...messages, { role: 'user', content: userMessage }], 200),
        searchProducts(message.replace(/under.*naira|over.*naira|cheap|best/gi, '').trim() || message),
      ]);
      products = searchResults;

      let finalReply = aiReply;
      if (products.length > 0) {
        const productList = products.slice(0, 4).map(p => `${p.name} (₦${Number(p.price).toLocaleString('en-NG')})`).join(', ');
        finalReply = `${aiReply}\n\nI found ${products.length} products for you: ${productList}`;
      }

      return NextResponse.json({
        success: true,
        reply: finalReply,
        message: finalReply,
        content: finalReply,
        products: products.length > 0 ? products : null,
      });
    }

    messages.push({ role: 'user', content: userMessage });
    const reply = await callNVIDIA(messages, 200);

    return NextResponse.json({
      success: true,
      reply,
      message: reply,
      content: reply,
      products: null,
    });
  } catch (error) {
    console.error('AI chat error:', error);
    return NextResponse.json({
      success: false,
      error: String(error),
      reply: "I'm having trouble right now. Please try again.",
    }, { status: 200 });
  }
}