Files
karibeo_backend_admin/src/components/AIFloatingAssistant.tsx
gpt-engineer-app[bot] 5ddc52658d Initial commit from remix
2025-09-25 16:01:00 +00:00

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>
)}
</>
);
}