eeshaAI commited on
Commit
1c6db69
·
1 Parent(s): c3927bd

feat: conversational AI mode search (Alibaba-style) with thought process toggle, embedded product grid, follow-up chat

Browse files
Files changed (1) hide show
  1. src/app/search/page.tsx +161 -28
src/app/search/page.tsx CHANGED
@@ -7,7 +7,7 @@ import { Card } from '@/components/ui/card';
7
  import { Button } from '@/components/ui/button';
8
  import {
9
  Search, Camera, ChevronLeft, Store, Sparkles, Video as VideoIcon,
10
- Star, ShoppingBag, Play, Heart, MessageCircle
11
  } from 'lucide-react';
12
  import Link from 'next/link';
13
 
@@ -29,6 +29,11 @@ function SearchContent() {
29
 
30
  // Filter pills state
31
  const [activeFilters, setActiveFilters] = useState<string[]>([]);
 
 
 
 
 
32
 
33
  const FILTER_PILLS = [
34
  'Free shipping',
@@ -131,6 +136,35 @@ function SearchContent() {
131
  );
132
  };
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  // Apply client-side filters
135
  const filteredProducts = products.filter(p => {
136
  if (activeFilters.includes('Under ₦10,000') && p.price >= 10000) return false;
@@ -247,38 +281,114 @@ function SearchContent() {
247
  </div>
248
  )}
249
 
250
- {/* === AI MODE TAB === */}
251
  {tab === 'ai' && (
252
- <div className="space-y-4">
253
- {/* AI Overview card */}
254
- <Card className="p-4 border-slate-100 bg-gradient-to-br from-cyan-50 to-white">
255
- <div className="flex items-center gap-2 mb-3">
256
- <div className="w-7 h-7 rounded-lg brand-gradient flex items-center justify-center">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  <Sparkles className="w-4 h-4 text-white" />
258
  </div>
259
- <span className="font-bold text-sm text-black">AI Overview</span>
260
- </div>
261
- {aiLoading ? (
262
- <div className="space-y-2">
263
- <div className="h-4 bg-slate-100 rounded animate-pulse" />
264
- <div className="h-4 bg-slate-100 rounded animate-pulse w-5/6" />
265
- <div className="h-4 bg-slate-100 rounded animate-pulse w-4/6" />
 
 
 
 
 
266
  </div>
267
- ) : (
268
- <div className="text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
269
- {aiAnswer || 'Searching...'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  </div>
271
  )}
272
- </Card>
273
-
274
- {/* AI-recommended products */}
275
- {aiProducts.length > 0 && (
276
- <div>
277
- <h3 className="font-bold text-sm text-black mb-2">Top products for "{query}"</h3>
278
- <div className="space-y-3">
279
- {aiProducts.map(p => (
280
- <GoogleStyleResult key={p.id} product={p} />
281
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
  </div>
284
  )}
@@ -372,7 +482,30 @@ function SearchContent() {
372
  )}
373
  </div>
374
  )}
375
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  </div>
377
  );
378
  }
 
7
  import { Button } from '@/components/ui/button';
8
  import {
9
  Search, Camera, ChevronLeft, Store, Sparkles, Video as VideoIcon,
10
+ Star, ShoppingBag, Play, Heart, MessageCircle, Paperclip, Send, ChevronDown
11
  } from 'lucide-react';
12
  import Link from 'next/link';
13
 
 
29
 
30
  // Filter pills state
31
  const [activeFilters, setActiveFilters] = useState<string[]>([]);
32
+ // AI chat state
33
+ const [showThoughtProcess, setShowThoughtProcess] = useState(false);
34
+ const [chatHistory, setChatHistory] = useState<{user: string; ai: string}[]>([]);
35
+ const [followUpInput, setFollowUpInput] = useState('');
36
+ const [followUpLoading, setFollowUpLoading] = useState(false);
37
 
38
  const FILTER_PILLS = [
39
  'Free shipping',
 
136
  );
137
  };
138
 
139
+ const sendFollowUp = async () => {
140
+ if (!followUpInput.trim() || followUpLoading) return;
141
+ const userMsg = followUpInput.trim();
142
+ setFollowUpInput('');
143
+ setFollowUpLoading(true);
144
+ try {
145
+ const aiResp = await fetch('/api/ai-chat', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({
149
+ message: userMsg,
150
+ context: `User is searching for "${query}" on Cellex. Previous AI answer: ${aiAnswer}. Available products: ${products.slice(0, 5).map(p => `${p.name} (${formatPrice(p.price)})`).join(', ')}`,
151
+ history: chatHistory.map(m => [{ role: 'user', content: m.user }, { role: 'assistant', content: m.ai }]).flat(),
152
+ }),
153
+ });
154
+ let aiMsg = '';
155
+ if (aiResp.ok) {
156
+ const data = await aiResp.json();
157
+ aiMsg = data.reply || data.message || data.content || 'I can help with that. Could you be more specific?';
158
+ } else {
159
+ aiMsg = 'I can help with that. Try browsing the products below or refining your search.';
160
+ }
161
+ setChatHistory(prev => [...prev, { user: userMsg, ai: aiMsg }]);
162
+ } catch {
163
+ setChatHistory(prev => [...prev, { user: userMsg, ai: 'Sorry, I had trouble processing that. Please try again.' }]);
164
+ }
165
+ setFollowUpLoading(false);
166
+ };
167
+
168
  // Apply client-side filters
169
  const filteredProducts = products.filter(p => {
170
  if (activeFilters.includes('Under ₦10,000') && p.price >= 10000) return false;
 
281
  </div>
282
  )}
283
 
284
+ {/* === AI MODE TAB — Conversational chat (Alibaba AI mode style) === */}
285
  {tab === 'ai' && (
286
+ <div className="space-y-4 pb-32">
287
+ {/* User query bubble */}
288
+ <div className="flex justify-end">
289
+ <div className="bg-primary text-white rounded-2xl rounded-br-md px-4 py-2 max-w-[80%]">
290
+ <p className="text-sm">{query}</p>
291
+ </div>
292
+ </div>
293
+
294
+ {/* AI response */}
295
+ <div className="space-y-3">
296
+ {/* "Show thought process" toggle */}
297
+ <button
298
+ onClick={() => setShowThoughtProcess(!showThoughtProcess)}
299
+ className="flex items-center gap-1 text-xs text-slate-500 hover:text-primary"
300
+ >
301
+ <ChevronDown className={`w-3 h-3 transition-transform ${showThoughtProcess ? 'rotate-180' : ''}`} />
302
+ Show thought process
303
+ </button>
304
+
305
+ {/* Thought process (collapsible) */}
306
+ {showThoughtProcess && (
307
+ <div className="text-xs text-slate-400 bg-slate-50 rounded-lg p-3 space-y-1">
308
+ <p>1. Searched for "{query}" across {products.length} products on Cellex</p>
309
+ <p>2. Found {aiProducts.length} top matches based on relevance and popularity</p>
310
+ <p>3. Generated overview using DeepSeek-V4 AI model via NVIDIA NIM</p>
311
+ </div>
312
+ )}
313
+
314
+ {/* AI response text */}
315
+ <div className="flex items-start gap-2">
316
+ <div className="w-7 h-7 rounded-lg brand-gradient flex items-center justify-center shrink-0">
317
  <Sparkles className="w-4 h-4 text-white" />
318
  </div>
319
+ <div className="flex-1">
320
+ {aiLoading ? (
321
+ <div className="space-y-2">
322
+ <div className="h-4 bg-slate-100 rounded animate-pulse" />
323
+ <div className="h-4 bg-slate-100 rounded animate-pulse w-5/6" />
324
+ <div className="h-4 bg-slate-100 rounded animate-pulse w-4/6" />
325
+ </div>
326
+ ) : (
327
+ <div className="text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
328
+ {aiAnswer || 'Searching...'}
329
+ </div>
330
+ )}
331
  </div>
332
+ </div>
333
+
334
+ {/* Embedded product grid (within the AI response) */}
335
+ {aiProducts.length > 0 && !aiLoading && (
336
+ <div className="ml-9">
337
+ <div className="inline-block bg-primary/10 text-primary text-xs font-bold px-2 py-1 rounded mb-2">
338
+ {query}
339
+ </div>
340
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
341
+ {aiProducts.map(p => (
342
+ <Link key={p.id} href={`/product?id=${p.id}`} className="block group">
343
+ <Card className="overflow-hidden border-slate-100 hover:shadow-md transition-shadow">
344
+ <div className="aspect-square bg-slate-50">
345
+ {p.image_url ? (
346
+ <img src={p.image_url} alt={p.name} className="w-full h-full object-cover" loading="lazy" />
347
+ ) : (
348
+ <div className="w-full h-full flex items-center justify-center text-slate-300">
349
+ <Store className="w-8 h-8" />
350
+ </div>
351
+ )}
352
+ </div>
353
+ <div className="p-1.5">
354
+ <div className="text-xs font-medium text-black line-clamp-1">{p.name}</div>
355
+ <div className="text-sm font-bold price">{formatPrice(p.price)}</div>
356
+ </div>
357
+ </Card>
358
+ </Link>
359
+ ))}
360
+ </div>
361
  </div>
362
  )}
363
+ </div>
364
+
365
+ {/* Follow-up chat history */}
366
+ {chatHistory.map((msg, i) => (
367
+ <div key={i} className="space-y-2">
368
+ <div className="flex justify-end">
369
+ <div className="bg-primary text-white rounded-2xl rounded-br-md px-4 py-2 max-w-[80%]">
370
+ <p className="text-sm">{msg.user}</p>
371
+ </div>
372
+ </div>
373
+ <div className="flex items-start gap-2">
374
+ <div className="w-7 h-7 rounded-lg brand-gradient flex items-center justify-center shrink-0">
375
+ <Sparkles className="w-4 h-4 text-white" />
376
+ </div>
377
+ <div className="text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
378
+ {msg.ai}
379
+ </div>
380
+ </div>
381
+ </div>
382
+ ))}
383
+ {followUpLoading && (
384
+ <div className="flex items-start gap-2">
385
+ <div className="w-7 h-7 rounded-lg brand-gradient flex items-center justify-center shrink-0">
386
+ <Sparkles className="w-4 h-4 text-white" />
387
+ </div>
388
+ <div className="flex gap-1">
389
+ <span className="w-2 h-2 bg-slate-300 rounded-full animate-bounce" />
390
+ <span className="w-2 h-2 bg-slate-300 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
391
+ <span className="w-2 h-2 bg-slate-300 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
392
  </div>
393
  </div>
394
  )}
 
482
  )}
483
  </div>
484
  )}
485
+
486
+ {/* Follow-up question input (fixed at bottom, AI mode only) */}
487
+ {tab === 'ai' && (
488
+ <div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-slate-200 px-4 py-3">
489
+ <div className="max-w-4xl mx-auto flex items-center gap-2 border-2 border-slate-200 rounded-full px-4 py-2 focus-within:border-primary">
490
+ <Paperclip className="w-4 h-4 text-slate-400 shrink-0" />
491
+ <input
492
+ type="text"
493
+ value={followUpInput}
494
+ onChange={(e) => setFollowUpInput(e.target.value)}
495
+ onKeyDown={(e) => e.key === 'Enter' && sendFollowUp()}
496
+ placeholder="ask follow-up..."
497
+ className="flex-1 bg-transparent outline-none text-sm text-black placeholder:text-slate-400"
498
+ />
499
+ <button
500
+ onClick={sendFollowUp}
501
+ disabled={!followUpInput.trim() || followUpLoading}
502
+ className="w-8 h-8 rounded-full brand-gradient flex items-center justify-center disabled:opacity-30 shrink-0"
503
+ >
504
+ <Send className="w-4 h-4 text-white" />
505
+ </button>
506
+ </div>
507
+ </div>
508
+ )}
509
  </div>
510
  );
511
  }