Spaces:
Sleeping
Sleeping
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- 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 |
-
{/*
|
| 254 |
-
<
|
| 255 |
-
<div className="
|
| 256 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
<Sparkles className="w-4 h-4 text-white" />
|
| 258 |
</div>
|
| 259 |
-
<
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
</div>
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
</div>
|
| 271 |
)}
|
| 272 |
-
</
|
| 273 |
-
|
| 274 |
-
{/*
|
| 275 |
-
{
|
| 276 |
-
<div>
|
| 277 |
-
<
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
</div>
|
| 283 |
</div>
|
| 284 |
)}
|
|
@@ -372,7 +482,30 @@ function SearchContent() {
|
|
| 372 |
)}
|
| 373 |
</div>
|
| 374 |
)}
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|