'use client'; import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Plane, MapPin, Calendar, Sparkles, Loader2, X, ArrowUp, Bookmark, Map as MapIcon, Plus, } from 'lucide-react'; import { useTravelChat, type TravelChatMessage } from '@/lib/hooks/useTravelChat'; import { TravelWidgetRenderer } from '@/components/TravelWidgets'; import { TravelMap } from '@/components/TravelMap'; import { fetchTrips, createTrip, deleteTrip } from '@/lib/api'; import type { Trip, RoutePoint, GeoLocation, EventCard, POICard, HotelCard, TransportOption } from '@/lib/types'; import ReactMarkdown from 'react-markdown'; const quickPrompts = [ { icon: '🏖️', text: 'Пляжный отдых на неделю', query: 'Хочу пляжный отдых на неделю, тёплое море, красивые закаты' }, { icon: '🏔️', text: 'Горы и природа', query: 'Поездка в горы, активный отдых, пешие маршруты, красивые виды' }, { icon: '🏛️', text: 'Культурное путешествие', query: 'Культурное путешествие, музеи, архитектура, история' }, { icon: '🎢', text: 'С детьми', query: 'Семейный отдых с детьми, развлечения, парки, безопасно' }, ]; interface TripCardProps { trip: Trip; onClick: () => void; onDelete: () => void; } function TripCard({ trip, onClick, onDelete }: TripCardProps) { const formatDate = (date: string) => { return new Date(date).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); }; return (

{trip.title || trip.destination}

{trip.destination}

{formatDate(trip.startDate)} - {formatDate(trip.endDate)}
{trip.route && trip.route.length > 0 && (
{trip.route.length} точек
)}
); } interface AssistantMessageProps { message: TravelChatMessage; onAddEventToMap: (event: EventCard) => void; onAddPOIToMap: (poi: POICard) => void; onSelectHotel: (hotel: HotelCard) => void; onSelectTransport: (option: TransportOption) => void; onClarifyingAnswer: (field: string, value: string) => void; onAction: (kind: string) => void; selectedEventIds: Set; selectedPOIIds: Set; selectedHotelId?: string; selectedTransportId?: string; } function AssistantMessage({ message, onAddEventToMap, onAddPOIToMap, onSelectHotel, onSelectTransport, onClarifyingAnswer, onAction, selectedEventIds, selectedPOIIds, selectedHotelId, selectedTransportId, }: AssistantMessageProps) { const travelWidgets = useMemo( () => message.widgets.filter((w) => w.type.startsWith('travel_')), [message.widgets] ); return (
{message.content && (
{message.content}
)} {message.isStreaming && !message.content && (
Планирую маршрут...
)}
{travelWidgets.length > 0 && (
{travelWidgets.map((widget) => ( ))}
)} {message.isStreaming && message.content && (
Собираю данные...
)}
); } export default function TravelPage() { const [inputValue, setInputValue] = useState(''); const [showMap, setShowMap] = useState(true); const [trips, setTrips] = useState([]); const [planOptions, setPlanOptions] = useState({ startDate: '', endDate: '', travelers: 2, budget: 0, }); const [showOptions, setShowOptions] = useState(false); const [selectedEventIds, setSelectedEventIds] = useState>(new Set()); const [selectedPOIIds, setSelectedPOIIds] = useState>(new Set()); const [selectedHotelId, setSelectedHotelId] = useState(); const [selectedTransportId, setSelectedTransportId] = useState(); const [userLocation, setUserLocation] = useState(null); const messagesEndRef = useRef(null); const textareaRef = useRef(null); useEffect(() => { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition( (position) => { setUserLocation({ lat: position.coords.latitude, lng: position.coords.longitude, name: 'Моё местоположение', }); }, () => {}, { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }, ); }, []); const { messages, isLoading, isResearching, currentRoute, routeDirection, routeSegments, sendMessage, stopGeneration, clearChat, addRoutePoint, answerClarifying, handleAction, addEventToRoute, addPOIToRoute, selectHotelOnRoute, } = useTravelChat({ onRouteUpdate: (route) => { if (route.length > 0) setShowMap(true); }, }); useEffect(() => { loadTrips(); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const loadTrips = async () => { try { const { trips: data } = await fetchTrips(10, 0); setTrips(data || []); } catch { // silently fail } }; const handleSend = useCallback(() => { if (!inputValue.trim() || isLoading) return; sendMessage(inputValue, { startDate: planOptions.startDate || undefined, endDate: planOptions.endDate || undefined, travelers: planOptions.travelers || undefined, budget: planOptions.budget || undefined, userLocation: userLocation || undefined, }); setInputValue(''); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; } }, [inputValue, isLoading, sendMessage, planOptions, userLocation]); const handleQuickPrompt = useCallback((query: string) => { sendMessage(query, { startDate: planOptions.startDate || undefined, endDate: planOptions.endDate || undefined, travelers: planOptions.travelers || undefined, budget: planOptions.budget || undefined, userLocation: userLocation || undefined, }); }, [sendMessage, planOptions, userLocation]); const handleAddEventToMap = useCallback((event: EventCard) => { setSelectedEventIds((prev) => { const next = new Set(prev); if (next.has(event.id)) { next.delete(event.id); } else { next.add(event.id); addEventToRoute(event); } return next; }); }, [addEventToRoute]); const handleAddPOIToMap = useCallback((poi: POICard) => { setSelectedPOIIds((prev) => { const next = new Set(prev); if (next.has(poi.id)) { next.delete(poi.id); } else { next.add(poi.id); addPOIToRoute(poi); } return next; }); }, [addPOIToRoute]); const handleSelectHotel = useCallback((hotel: HotelCard) => { setSelectedHotelId((prev) => { const newId = prev === hotel.id ? undefined : hotel.id; if (newId) selectHotelOnRoute(hotel); return newId; }); }, [selectHotelOnRoute]); const handleSelectTransport = useCallback((option: TransportOption) => { setSelectedTransportId((prev) => (prev === option.id ? undefined : option.id)); }, []); const handleMapClick = useCallback((location: GeoLocation) => { const point: RoutePoint = { id: `${Date.now()}`, lat: location.lat, lng: location.lng, name: location.name || 'Новая точка', type: 'custom', order: currentRoute.length, }; addRoutePoint(point); }, [addRoutePoint, currentRoute.length]); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const handleSaveTrip = async () => { if (currentRoute.length === 0) return; setSaveStatus('saving'); const lastMessage = messages.find((m) => m.role === 'assistant' && !m.isStreaming); const trip: Partial = { title: 'Новое путешествие', destination: currentRoute[0]?.name || 'Неизвестно', startDate: planOptions.startDate || new Date().toISOString(), endDate: planOptions.endDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), route: currentRoute, currency: 'RUB', status: 'planned', aiSummary: lastMessage?.content, }; try { const created = await createTrip(trip); setTrips((prev) => [created, ...prev]); setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2000); } catch { setSaveStatus('error'); setTimeout(() => setSaveStatus('idle'), 3000); } }; const handleDeleteTrip = async (id: string) => { try { await deleteTrip(id); setTrips((prev) => prev.filter((t) => t.id !== id)); } catch { // silently fail } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleInput = () => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`; } }; const hasMessages = messages.length > 0; return (
{!hasMessages ? (

Куда отправимся?

AI спланирует идеальное путешествие с маршрутом, отелями, мероприятиями и достопримечательностями