375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { MessageCircle, X, Send, MapPin, Search, Sparkles, Star, Clock, DollarSign } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
|
|
interface Message {
|
|
id: string;
|
|
type: 'user' | 'ai';
|
|
content: string;
|
|
timestamp: Date;
|
|
suggestions?: string[];
|
|
results?: SearchResult[];
|
|
}
|
|
|
|
interface SearchResult {
|
|
id: string;
|
|
title: string;
|
|
type: 'restaurant' | 'hotel' | 'attraction' | 'event' | 'shop';
|
|
description: string;
|
|
rating: number;
|
|
price: string;
|
|
distance: string;
|
|
image: string;
|
|
location: string;
|
|
available: boolean;
|
|
}
|
|
|
|
const QUICK_ACTIONS = [
|
|
{ id: 'restaurants', label: '🍽️ Find Restaurants', query: 'Find the best restaurants near me' },
|
|
{ id: 'hotels', label: '🏨 Hotels & Stays', query: 'Show me hotels and accommodations nearby' },
|
|
{ id: 'attractions', label: '🎭 Attractions', query: 'What are the top attractions to visit here?' },
|
|
{ id: 'events', label: '🎪 Events Today', query: 'What events are happening today in my area?' },
|
|
{ id: 'shopping', label: '🛍️ Shopping', query: 'Find shopping centers and stores near me' },
|
|
{ id: 'deals', label: '💰 Best Deals', query: 'Show me the best deals and offers available' }
|
|
];
|
|
|
|
const SAMPLE_RESULTS: SearchResult[] = [
|
|
{
|
|
id: '1',
|
|
title: 'La Terrazza Restaurant',
|
|
type: 'restaurant',
|
|
description: 'Authentic Italian cuisine with ocean views',
|
|
rating: 4.8,
|
|
price: '$$$',
|
|
distance: '0.3 km',
|
|
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=300&h=200&fit=crop',
|
|
location: 'Downtown',
|
|
available: true
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Ocean View Hotel',
|
|
type: 'hotel',
|
|
description: 'Luxury beachfront hotel with spa services',
|
|
rating: 4.6,
|
|
price: '$150/night',
|
|
distance: '0.8 km',
|
|
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=300&h=200&fit=crop',
|
|
location: 'Beachfront',
|
|
available: true
|
|
},
|
|
{
|
|
id: '3',
|
|
title: 'Art Museum Moderna',
|
|
type: 'attraction',
|
|
description: 'Contemporary art collection and exhibitions',
|
|
rating: 4.4,
|
|
price: '$12',
|
|
distance: '1.2 km',
|
|
image: 'https://images.unsplash.com/photo-1554907984-15263bfd63bd?w=300&h=200&fit=crop',
|
|
location: 'Cultural District',
|
|
available: true
|
|
}
|
|
];
|
|
|
|
export function AIFloatingAssistant() {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [inputValue, setInputValue] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const [userLocation, setUserLocation] = useState<string>('');
|
|
|
|
useEffect(() => {
|
|
// Get user location
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const { latitude, longitude } = position.coords;
|
|
// In a real app, you'd reverse geocode these coordinates
|
|
setUserLocation(`${latitude.toFixed(4)}, ${longitude.toFixed(4)}`);
|
|
},
|
|
(error) => {
|
|
console.error('Error getting location:', error);
|
|
setUserLocation('Location unavailable');
|
|
}
|
|
);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isOpen && messages.length === 0) {
|
|
// Welcome message
|
|
const welcomeMessage: Message = {
|
|
id: '1',
|
|
type: 'ai',
|
|
content: `¡Hola! 👋 Soy tu asistente de IA de Karibeo. Estoy aquí para ayudarte a encontrar los mejores lugares, ofertas y experiencias según tu ubicación.
|
|
|
|
¿En qué puedo ayudarte hoy?`,
|
|
timestamp: new Date(),
|
|
suggestions: [
|
|
'Encuentra restaurantes cerca',
|
|
'Hoteles disponibles',
|
|
'Mejores ofertas del día',
|
|
'Eventos y actividades'
|
|
]
|
|
};
|
|
setMessages([welcomeMessage]);
|
|
}
|
|
}, [isOpen, messages.length]);
|
|
|
|
const handleSendMessage = async (query: string) => {
|
|
if (!query.trim()) return;
|
|
|
|
const userMessage: Message = {
|
|
id: Date.now().toString(),
|
|
type: 'user',
|
|
content: query,
|
|
timestamp: new Date()
|
|
};
|
|
|
|
setMessages(prev => [...prev, userMessage]);
|
|
setInputValue('');
|
|
setIsTyping(true);
|
|
|
|
// Simulate AI processing
|
|
setTimeout(() => {
|
|
const aiResponse = generateAIResponse(query);
|
|
setMessages(prev => [...prev, aiResponse]);
|
|
setIsTyping(false);
|
|
}, 1500);
|
|
};
|
|
|
|
const generateAIResponse = (query: string): Message => {
|
|
const lowerQuery = query.toLowerCase();
|
|
let response = '';
|
|
let results: SearchResult[] = [];
|
|
let suggestions: string[] = [];
|
|
|
|
if (lowerQuery.includes('restaurant') || lowerQuery.includes('comida') || lowerQuery.includes('comer')) {
|
|
response = '🍽️ He encontrado algunos restaurantes excelentes cerca de ti:';
|
|
results = SAMPLE_RESULTS.filter(r => r.type === 'restaurant');
|
|
suggestions = ['Reservar mesa', 'Ver menú', 'Más restaurantes', 'Ofertas especiales'];
|
|
} else if (lowerQuery.includes('hotel') || lowerQuery.includes('hospedaje') || lowerQuery.includes('alojamiento')) {
|
|
response = '🏨 Aquí tienes las mejores opciones de hospedaje:';
|
|
results = SAMPLE_RESULTS.filter(r => r.type === 'hotel');
|
|
suggestions = ['Verificar disponibilidad', 'Comparar precios', 'Ver fotos', 'Reservar ahora'];
|
|
} else if (lowerQuery.includes('atracciones') || lowerQuery.includes('lugares') || lowerQuery.includes('visitar')) {
|
|
response = '🎭 Estas son las atracciones más populares:';
|
|
results = SAMPLE_RESULTS.filter(r => r.type === 'attraction');
|
|
suggestions = ['Comprar entradas', 'Ver horarios', 'Más atracciones', 'Tours guiados'];
|
|
} else if (lowerQuery.includes('ofertas') || lowerQuery.includes('descuentos') || lowerQuery.includes('deals')) {
|
|
response = '💰 ¡Encontré estas ofertas especiales para ti!';
|
|
results = SAMPLE_RESULTS.map(r => ({ ...r, price: `${r.price} - 20% OFF` }));
|
|
suggestions = ['Ver más ofertas', 'Códigos de descuento', 'Ofertas flash', 'Membresías'];
|
|
} else {
|
|
response = `He buscando información sobre "${query}" en tu área. Aquí tienes algunas opciones que podrían interesarte:`;
|
|
results = SAMPLE_RESULTS.slice(0, 2);
|
|
suggestions = ['Búsqueda específica', 'Filtrar por precio', 'Ordenar por distancia', 'Ver mapa'];
|
|
}
|
|
|
|
return {
|
|
id: Date.now().toString(),
|
|
type: 'ai',
|
|
content: response,
|
|
timestamp: new Date(),
|
|
results,
|
|
suggestions
|
|
};
|
|
};
|
|
|
|
const handleQuickAction = (action: typeof QUICK_ACTIONS[0]) => {
|
|
handleSendMessage(action.query);
|
|
};
|
|
|
|
const renderSearchResults = (results: SearchResult[]) => {
|
|
return (
|
|
<div className="space-y-3 mt-4">
|
|
{results.map((result) => (
|
|
<Card key={result.id} className="cursor-pointer hover:shadow-lg transition-shadow">
|
|
<CardContent className="p-4">
|
|
<div className="flex gap-3">
|
|
<img
|
|
src={result.image}
|
|
alt={result.title}
|
|
className="w-16 h-16 rounded-lg object-cover flex-shrink-0"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between mb-1">
|
|
<h4 className="font-semibold text-sm truncate">{result.title}</h4>
|
|
<Badge variant={result.available ? "default" : "secondary"} className="ml-2 text-xs">
|
|
{result.available ? 'Disponible' : 'Ocupado'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
|
{result.description}
|
|
</p>
|
|
<div className="flex items-center justify-between text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center">
|
|
<Star className="h-3 w-3 text-yellow-400 fill-current" />
|
|
<span className="ml-1 font-medium">{result.rating}</span>
|
|
</div>
|
|
<div className="flex items-center text-muted-foreground">
|
|
<MapPin className="h-3 w-3" />
|
|
<span className="ml-1">{result.distance}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center font-semibold text-primary">
|
|
<DollarSign className="h-3 w-3" />
|
|
<span>{result.price}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Floating Button */}
|
|
<div className="fixed bottom-6 right-6 z-50">
|
|
<Button
|
|
onClick={() => setIsOpen(true)}
|
|
className={`w-14 h-14 rounded-full shadow-lg transition-all duration-300 ${
|
|
isOpen ? 'scale-0' : 'scale-100 hover:scale-110'
|
|
} bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600`}
|
|
>
|
|
<div className="relative">
|
|
<Sparkles className="h-6 w-6 text-white" />
|
|
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-pulse" />
|
|
</div>
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Chat Modal */}
|
|
{isOpen && (
|
|
<div className="fixed bottom-6 right-6 z-50 w-96 h-[600px] bg-background border rounded-2xl shadow-2xl flex flex-col animate-scale-in">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-t-2xl">
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src="" />
|
|
<AvatarFallback className="bg-white text-purple-500 text-xs font-bold">
|
|
AI
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<h3 className="font-semibold text-sm">Asistente Karibeo</h3>
|
|
<p className="text-xs opacity-90">
|
|
{userLocation ? `📍 ${userLocation}` : '🤖 Conectado'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setIsOpen(false)}
|
|
className="text-white hover:bg-white/20 h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
{messages.length <= 1 && (
|
|
<div className="p-4 border-b">
|
|
<p className="text-sm font-medium mb-3">Acciones rápidas:</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{QUICK_ACTIONS.slice(0, 4).map((action) => (
|
|
<Button
|
|
key={action.id}
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleQuickAction(action)}
|
|
className="text-xs h-8 justify-start"
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{messages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-[80%] rounded-2xl p-3 ${
|
|
message.type === 'user'
|
|
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white'
|
|
: 'bg-muted'
|
|
}`}
|
|
>
|
|
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
|
|
|
{message.results && renderSearchResults(message.results)}
|
|
|
|
{message.suggestions && (
|
|
<div className="mt-3 space-y-1">
|
|
{message.suggestions.map((suggestion, index) => (
|
|
<Button
|
|
key={index}
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleSendMessage(suggestion)}
|
|
className="text-xs h-6 mr-1 mb-1"
|
|
>
|
|
{suggestion}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isTyping && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-muted rounded-2xl p-3">
|
|
<div className="flex items-center space-x-1">
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" />
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<div className="p-4 border-t">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={inputValue}
|
|
onChange={(e) => setInputValue(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage(inputValue)}
|
|
placeholder="Escribe tu pregunta..."
|
|
className="flex-1"
|
|
disabled={isTyping}
|
|
/>
|
|
<Button
|
|
onClick={() => handleSendMessage(inputValue)}
|
|
disabled={!inputValue.trim() || isTyping}
|
|
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
} |