From faaa5caea9f4bec39ef627f54193908bec63420b Mon Sep 17 00:00:00 2001 From: Nethmal Gunawardhana Date: Sat, 16 Aug 2025 21:31:11 +0530 Subject: [PATCH] feat: Implement booking conversation flow in RAGBot - Added BookingChatManager component to manage booking-related conversations. - Integrated booking intent detection and user response processing. - Enhanced TypingIndicator to display booking-specific messages. - Updated ChatInput and ChatContent to support booking mode. - Created BookingChatButton for initiating booking form navigation. - Introduced AuthPrompt for handling authentication requirements. - Implemented useBookingChat hook for managing booking data and state. - Updated NewBookingPage to pre-fill form data from chat. - Added translations for booking-related messages in multiple languages. --- .../booking-conversation-status/route.ts | 73 +++++ .../api/ragbot/booking-conversation/route.ts | 279 ++++++++++++++++ src/app/api/user/booking-data/route.ts | 212 ++++++++++++ src/app/ragbot/page.tsx | 212 ++++++++++-- src/app/user/booking/new/page.tsx | 34 ++ src/components/user/chat/AuthPrompt.tsx | 101 ++++++ .../user/chat/BookingChatButton.tsx | 143 ++++++++ .../user/chat/BookingChatManager.tsx | 308 ++++++++++++++++++ src/lib/hooks/useBookingChat.ts | 227 +++++++++++++ 9 files changed, 1566 insertions(+), 23 deletions(-) create mode 100644 src/app/api/ragbot/booking-conversation-status/route.ts create mode 100644 src/app/api/ragbot/booking-conversation/route.ts create mode 100644 src/app/api/user/booking-data/route.ts create mode 100644 src/components/user/chat/AuthPrompt.tsx create mode 100644 src/components/user/chat/BookingChatButton.tsx create mode 100644 src/components/user/chat/BookingChatManager.tsx create mode 100644 src/lib/hooks/useBookingChat.ts diff --git a/src/app/api/ragbot/booking-conversation-status/route.ts b/src/app/api/ragbot/booking-conversation-status/route.ts new file mode 100644 index 0000000..11b9d6f --- /dev/null +++ b/src/app/api/ragbot/booking-conversation-status/route.ts @@ -0,0 +1,73 @@ +// src/app/api/ragbot/booking-conversation-status/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +// Use the same schema as the booking conversation +const BookingConversationSchema = new mongoose.Schema({ + sessionId: { type: String, required: true }, + userId: { type: String, default: null }, + currentStep: { type: String, default: 'start' }, + collectedData: { + department: { type: String, default: '' }, + service: { type: String, default: '' }, + agentType: { type: String, default: '' }, + preferredDate: { type: String, default: '' }, + preferredTime: { type: String, default: '' }, + additionalNotes: { type: String, default: '' } + }, + conversationHistory: [{ + message: String, + response: String, + timestamp: { type: Date, default: Date.now } + }], + createdAt: { type: Date, default: Date.now } +}); + +// Add TTL index to auto-delete after 24 hours +BookingConversationSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingConversation = mongoose.models.BookingConversation || + mongoose.model('BookingConversation', BookingConversationSchema); + +export async function GET(request: NextRequest) { + try { + await connectDB(); + + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + + if (!sessionId) { + return NextResponse.json( + { error: 'Session ID is required' }, + { status: 400 } + ); + } + + // Find the booking conversation for this session + const conversation = await BookingConversation.findOne({ sessionId }); + + if (!conversation) { + return NextResponse.json({ + isComplete: false, + collectedData: null + }); + } + + // Check if the conversation is complete + const isComplete = conversation.currentStep === 'complete'; + + return NextResponse.json({ + isComplete, + collectedData: isComplete ? conversation.collectedData : null, + currentStep: conversation.currentStep + }); + + } catch (error) { + console.error('Error checking booking conversation status:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ragbot/booking-conversation/route.ts b/src/app/api/ragbot/booking-conversation/route.ts new file mode 100644 index 0000000..0b77f0c --- /dev/null +++ b/src/app/api/ragbot/booking-conversation/route.ts @@ -0,0 +1,279 @@ +// src/app/api/ragbot/booking-conversation/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +// Booking conversation state schema +const BookingConversationSchema = new mongoose.Schema({ + sessionId: { type: String, required: true }, + userId: { type: String, default: null }, + currentStep: { type: String, default: 'start' }, + collectedData: { + department: { type: String, default: '' }, + service: { type: String, default: '' }, + agentType: { type: String, default: '' }, + preferredDate: { type: String, default: '' }, + preferredTime: { type: String, default: '' }, + additionalNotes: { type: String, default: '' } + }, + conversationHistory: [{ + message: String, + response: String, + timestamp: { type: Date, default: Date.now } + }], + createdAt: { type: Date, default: Date.now } +}); + +// Add TTL index to auto-delete after 24 hours +BookingConversationSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingConversation = mongoose.models.BookingConversation || + mongoose.model('BookingConversation', BookingConversationSchema); + +interface BookingStep { + step: string; + question: string; + nextStep: string; + dataField: keyof BookingConversationState['collectedData']; +} + +interface BookingConversationState { + sessionId: string; + userId: string | null; + currentStep: string; + collectedData: { + department: string; + service: string; + agentType: string; + preferredDate: string; + preferredTime: string; + additionalNotes: string; + }; + conversationHistory: Array<{ + message: string; + response: string; + timestamp: Date; + }>; +} + +const bookingSteps: Record = { + start: { + step: 'start', + question: "I'd be happy to help you book an appointment! First, which government department do you need assistance with? (e.g., Immigration, Business Registration, Health Services, Education, etc.)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "Great! Now, what specific service do you need from this department? (e.g., passport renewal, license application, certificate request, etc.)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "Perfect! What type of government official would you like to meet with? (e.g., Senior Officer, Customer Service Agent, Technical Specialist, etc.)", + nextStep: 'agentType', + dataField: 'agentType' + }, + agentType: { + step: 'agentType', + question: "Excellent! What's your preferred date for the appointment? (Please provide in YYYY-MM-DD format or describe like 'next Monday', 'this Friday', etc.)", + nextStep: 'preferredDate', + dataField: 'preferredDate' + }, + preferredDate: { + step: 'preferredDate', + question: "Got it! What time would work best for you? (e.g., 9:00 AM, 2:00 PM, morning, afternoon, etc.)", + nextStep: 'preferredTime', + dataField: 'preferredTime' + }, + preferredTime: { + step: 'preferredTime', + question: "Almost done! Is there anything specific you'd like to mention or any additional requirements for your appointment? (Optional - you can say 'none' or provide any special notes)", + nextStep: 'additionalNotes', + dataField: 'additionalNotes' + }, + additionalNotes: { + step: 'additionalNotes', + question: "Perfect! I've collected all your information. You can now proceed to complete your booking by clicking the 'Open Booking Form' button that will appear below. Your information has been saved and will be automatically filled in the form.", + nextStep: 'complete', + dataField: 'additionalNotes' + } +}; + +const translations = { + en: { + steps: bookingSteps, + complete: "Perfect! I've collected all your booking information:\n\n📋 **Booking Summary:**\n• Department: {department}\n• Service: {service}\n• Agent Type: {agentType}\n• Preferred Date: {preferredDate}\n• Preferred Time: {preferredTime}\n• Additional Notes: {additionalNotes}\n\n✅ You can now click the **'Open Booking Form'** button below to complete your appointment booking. All your information will be automatically filled in!", + authRequired: "To save your booking information and proceed, you'll need to log in to your account. Please click the 'Login to Continue' button below.", + error: "I encountered an error processing your booking request. Please try again." + }, + si: { + steps: { + start: { + step: 'start', + question: "ඔබගේ හමුවීම වෙන්කරවීමට මම සතුටුයි! මුලින්ම, ඔබට කුමන රජයේ දෙපාර්තමේන්තුවේ සහාය අවශ්‍යද? (උදා: ගිණුම්කරණ, ව්‍යාපාර ලියාපදිංචිය, සෞඛ්‍ය සේවා, අධ්‍යාපනය)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "ගොඩක් හොඳයි! දැන්, ඔබට මෙම දෙපාර්තමේන්තුවෙන් කුමන නිශ්චිත සේවාව අවශ්‍යද? (උදා: විදේශ ගමන් බලපත්‍ර අලුත් කිරීම, බලපත්‍ර අයදුම්, සහතික ඉල්ලීම)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "පරිපූර්ණයි! ඔබට කුමන වර්ගයේ රජයේ නිලධාරියෙකු හමුවීමට අවශ්‍යද? (උදා: ජ්‍යෙෂ්ඨ නිලධාරී, පාරිභෝගික සේවා නියෝජිතයා, තාක්ෂණික විශේෂඥයා)", + nextStep: 'agentType', + dataField: 'agentType' + } + }, + complete: "පරිපූර්ණයි! මම ඔබගේ සියලුම වෙන්කරවීම් තොරතුරු එකතු කර ගත්තෙමි:\n\n📋 **වෙන්කරවීම් සාරාංශය:**\n• දෙපාර්තමේන්තුව: {department}\n• සේවාව: {service}\n• නියෝජිත වර්ගය: {agentType}\n• කැමති දිනය: {preferredDate}\n• කැමති වේලාව: {preferredTime}\n• අමතර සටහන්: {additionalNotes}\n\n✅ ඔබගේ හමුවීම් වෙන්කරවීම සම්පූර්ණ කිරීමට දැන් **'වෙන්කරවීම් ෆෝරමය විවෘත කරන්න'** බොත්තම ක්ලික් කරන්න. ඔබගේ සියලුම තොරතුරු ස්වයංක්‍රීයව පුරවනු ඇත!", + authRequired: "ඔබගේ වෙන්කරවීම් තොරතුරු සුරැකීමට සහ ඉදිරියට යාමට, ඔබගේ ගිණුමට ලොග් වීම අවශ්‍ය වේ.", + error: "ඔබගේ වෙන්කරවීම් ඉල්ලීම සැකසීමේදී දෝෂයක් ඇති විය." + }, + ta: { + steps: { + start: { + step: 'start', + question: "உங்கள் சந்திப்பை முன்பதிவு செய்ய நான் மகிழ்ச்சியடைகிறேன்! முதலில், எந்த அரசாங்க துறையின் உதவி உங்களுக்கு தேவை? (எ.கா: குடியேற்றம், வணிக பதிவு, சுகாதார சேவைகள், கல்வி)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "அருமை! இப்போது, இந்த துறையிலிருந்து எந்த குறிப்பிட்ட சேவை உங்களுக்கு தேவை? (எ.கா: கடவுச்சீட்டு புதுப்பித்தல், உரிமம் விண்ணப்பம், சான்றிதழ் கோரிக்கை)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "சரியானது! எந்த வகையான அரசாங்க அதிகாரியை சந்திக்க விரும்புகிறீர்கள்? (எ.கா: மூத்த அதிகாரி, வாடிக்கையாளர் சேவை பிரதிநிதி, தொழில்நுட்ப நிபுணர்)", + nextStep: 'agentType', + dataField: 'agentType' + } + }, + complete: "சரியானது! உங்கள் அனைத்து முன்பதிவு தகவலையும் நான் சேகரித்துவிட்டேன்:\n\n📋 **முன்பதிவு சுருக்கம்:**\n• துறை: {department}\n• சேவை: {service}\n• முகவர் வகை: {agentType}\n• விருப்பமான தேதி: {preferredDate}\n• விருப்பமான நேரம்: {preferredTime}\n• கூடுதல் குறிப்புகள்: {additionalNotes}\n\n✅ உங்கள் சந்திப்பு முன்பதிவை முடிக்க கீழே உள்ள **'முன்பதிவு படிவத்தை திற'** பொத்தானை கிளிக் செய்யலாம். உங்கள் அனைத்து தகவல்களும் தானாகவே நிரப்பப்படும்!", + authRequired: "உங்கள் முன்பதிவு தகவலை சேமிக்க மற்றும் தொடர, உங்கள் கணக்கில் உள்நுழைய வேண்டும்.", + error: "உங்கள் முன்பதிவு கோரிக்கையை செயலாக்குவதில் பிழை ஏற்பட்டது." + } +}; + +export async function POST(request: NextRequest) { + try { + await connectDB(); + + const { message, sessionId, language = 'en' } = await request.json(); + + if (!message || !sessionId) { + return NextResponse.json( + { error: 'Message and sessionId are required' }, + { status: 400 } + ); + } + + // Get or create conversation state + let conversation = await BookingConversation.findOne({ sessionId }); + + if (!conversation) { + conversation = new BookingConversation({ + sessionId, + currentStep: 'start', + collectedData: { + department: '', + service: '', + agentType: '', + preferredDate: '', + preferredTime: '', + additionalNotes: '' + }, + conversationHistory: [] + }); + } + + const t = translations[language as keyof typeof translations] || translations.en; + let response = ''; + + // If this is the first message and it's a booking intent, start the flow + if (conversation.currentStep === 'start') { + response = t.steps.start.question; + conversation.currentStep = 'department'; + } else { + // Process the user's response based on current step + const currentStepData = t.steps[conversation.currentStep as keyof typeof t.steps]; + + if (currentStepData && conversation.currentStep !== 'complete') { + // Save the user's response to the appropriate field + const dataField = currentStepData.dataField; + if (dataField && conversation.collectedData) { + conversation.collectedData[dataField] = message.trim(); + } + + // Move to next step + if (currentStepData.nextStep === 'complete') { + // All data collected, show summary + const summary = t.complete + .replace('{department}', conversation.collectedData.department || 'Not specified') + .replace('{service}', conversation.collectedData.service || 'Not specified') + .replace('{agentType}', conversation.collectedData.agentType || 'Not specified') + .replace('{preferredDate}', conversation.collectedData.preferredDate || 'Not specified') + .replace('{preferredTime}', conversation.collectedData.preferredTime || 'Not specified') + .replace('{additionalNotes}', conversation.collectedData.additionalNotes || 'None'); + + response = summary; + conversation.currentStep = 'complete'; + } else { + // Ask next question + const nextStep = t.steps[currentStepData.nextStep as keyof typeof t.steps]; + if (nextStep) { + response = nextStep.question; + conversation.currentStep = currentStepData.nextStep; + } + } + } else if (conversation.currentStep === 'complete') { + response = "Your booking information is ready! Please use the 'Open Booking Form' button to complete your appointment."; + } + } + + // Add to conversation history + conversation.conversationHistory.push({ + message, + response, + timestamp: new Date() + }); + + // Save conversation state + await conversation.save(); + + // Also save to the booking data collection for form pre-fill + if (conversation.currentStep === 'complete') { + try { + await fetch(`${request.nextUrl.origin}/api/user/booking-data`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + bookingData: conversation.collectedData + }) + }); + } catch (error) { + console.error('Error saving booking data:', error); + } + } + + return NextResponse.json({ + response, + currentStep: conversation.currentStep, + collectedData: conversation.collectedData, + isComplete: conversation.currentStep === 'complete' + }); + + } catch (error) { + console.error('Error in booking conversation:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/booking-data/route.ts b/src/app/api/user/booking-data/route.ts new file mode 100644 index 0000000..2d5d916 --- /dev/null +++ b/src/app/api/user/booking-data/route.ts @@ -0,0 +1,212 @@ +// src/app/api/user/booking-data/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +interface BookingData { + userId: string; + sessionId: string; + step: string; + department?: string; + departmentName?: string; + service?: string; + serviceName?: string; + agent?: string; + agentName?: string; + date?: string; + time?: string; + notes?: string; + completed: boolean; + createdAt: Date; + updatedAt: Date; +} + +const BookingDataSchema = new mongoose.Schema({ + userId: { type: String, required: true, index: true }, + sessionId: { type: String, required: true, unique: true }, + step: { type: String, required: true }, + department: String, + departmentName: String, + service: String, + serviceName: String, + agent: String, + agentName: String, + date: String, + time: String, + notes: String, + completed: { type: Boolean, default: false }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}, { + collection: 'booking_data' +}); + +// TTL index - automatically delete documents after 24 hours +BookingDataSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingDataModel = mongoose.models.BookingData || mongoose.model('BookingData', BookingDataSchema); + +// GET - Retrieve booking data by session ID +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + const userId = searchParams.get('userId'); + + if (!sessionId || !userId) { + return NextResponse.json( + { success: false, message: 'Session ID and User ID are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const bookingData = await BookingDataModel.findOne({ + sessionId, + userId + }).lean(); + + if (!bookingData) { + return NextResponse.json( + { success: false, message: 'Booking data not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + data: bookingData + }); + + } catch (error) { + console.error('Error retrieving booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to retrieve booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// POST - Save or update booking data +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + userId, + sessionId, + step, + department, + departmentName, + service, + serviceName, + agent, + agentName, + date, + time, + notes, + completed = false + } = body; + + if (!userId || !sessionId || !step) { + return NextResponse.json( + { success: false, message: 'User ID, Session ID, and step are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const bookingData = await BookingDataModel.findOneAndUpdate( + { sessionId, userId }, + { + $set: { + step, + department, + departmentName, + service, + serviceName, + agent, + agentName, + date, + time, + notes, + completed, + updatedAt: new Date() + } + }, + { + upsert: true, + new: true, + setDefaultsOnInsert: true + } + ); + + return NextResponse.json({ + success: true, + message: 'Booking data saved successfully', + data: bookingData + }); + + } catch (error) { + console.error('Error saving booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to save booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// DELETE - Clear booking data +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + const userId = searchParams.get('userId'); + + if (!sessionId || !userId) { + return NextResponse.json( + { success: false, message: 'Session ID and User ID are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const result = await BookingDataModel.deleteOne({ + sessionId, + userId + }); + + if (result.deletedCount === 0) { + return NextResponse.json( + { success: false, message: 'Booking data not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: 'Booking data cleared successfully' + }); + + } catch (error) { + console.error('Error clearing booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to clear booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} diff --git a/src/app/ragbot/page.tsx b/src/app/ragbot/page.tsx index 26cda3b..842f606 100644 --- a/src/app/ragbot/page.tsx +++ b/src/app/ragbot/page.tsx @@ -2,6 +2,8 @@ "use client"; import React, { Suspense, useState, useEffect, useRef } from 'react'; import UserDashboardLayout from '@/components/user/dashboard/UserDashboardLayout'; +import BookingChatManager from '@/components/user/chat/BookingChatManager'; +import { useRouter } from 'next/navigation'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; @@ -191,8 +193,17 @@ const UserMessage = ({ text, timestamp = new Date() }: { text: string; timestamp ); -const TypingIndicator = ({ language = 'en' }: { language?: Language }) => { +const TypingIndicator = ({ language = 'en', isBookingMode = false }: { language?: Language, isBookingMode?: boolean }) => { const t = chatTranslations[language]; + + const bookingMessages = { + en: { analyzing: 'Processing your booking details', preparing: 'Preparing your booking questions' }, + si: { analyzing: 'ඔබගේ වෙන්කරවීම් විස්තර සකසමින්', preparing: 'වෙන්කරවීම් ප්‍රශ්න සූදානම් කරමින්' }, + ta: { analyzing: 'உங்கள் முன்பதிவு விவரங்களை செயலாக்குகிறது', preparing: 'முன்பதிவு கேள்விகளை தயாரிക்கிறது' } + }; + + const messages = isBookingMode ? bookingMessages[language] : t; + return (
@@ -205,11 +216,11 @@ const TypingIndicator = ({ language = 'en' }: { language?: Language }) => { - {t.analyzing} + {messages.analyzing}
- {t.preparing} + {messages.preparing}
@@ -449,44 +460,179 @@ const ChatInput = ({ onSendMessage, language = 'en', disabled = false }: { onSen // --- MAIN CHAT PAGE COMPONENT --- export default function RAGBotPage() { + const router = useRouter(); const [messages, setMessages] = useState([]); const [isTyping, setIsTyping] = useState(false); const [currentLanguage, setCurrentLanguage] = useState('en'); const [sessionId] = useState(`rag_session_${Date.now()}_${Math.random().toString(36).substring(7)}`); + const [lastUserMessage, setLastUserMessage] = useState(''); + const [isInBookingConversation, setIsInBookingConversation] = useState(false); const t = chatTranslations[currentLanguage]; + // Store session ID in sessionStorage for BookingChatManager to access + useEffect(() => { + if (typeof window !== 'undefined') { + sessionStorage.setItem('ragSessionId', sessionId); + } + }, [sessionId]); + const handleLanguageChange = (newLanguage: Language) => { setCurrentLanguage(newLanguage); }; - const handleSendMessage = async (messageText: string) => { - if (!messageText.trim()) return; + const handleLoginRequired = () => { + router.push('/user/auth/login?redirect=/ragbot'); + }; - const userMessage: Message = { type: 'user', text: messageText, timestamp: new Date() }; - setMessages(prev => [...prev, userMessage]); - setIsTyping(true); + const handleBookingQuestionGenerated = (question: string) => { + const botMessage: Message = { + type: 'bot', + text: question, + timestamp: new Date(), + }; + setMessages(prev => [...prev, botMessage]); + }; + + // Check if user message contains booking intent vs information search intent + const checkBookingIntent = (message: string): boolean => { + // If we're already in a booking conversation, continue with booking flow + // unless user explicitly asks for information search + if (isInBookingConversation) { + const searchKeywords = [ + 'search', 'find', 'tell me about', 'information about', 'details about', + 'what is', 'how does', 'explain', 'describe', 'show me', 'lookup', + 'සොයන්න', 'විස්තර', 'කියන්න', 'පෙන්වන්න', + 'தேடு', 'விவரங்கள்', 'சொல்லு', 'காட்டு' + ]; + const lowerMessage = message.toLowerCase(); + const isExplicitSearch = searchKeywords.some(keyword => lowerMessage.includes(keyword)); + return !isExplicitSearch; // Continue booking unless explicit search request + } + + const lowerMessage = message.toLowerCase(); + + // Information search keywords - if these are present, it's NOT a booking intent + const searchKeywords = [ + 'search', 'find', 'tell me about', 'information about', 'details about', + 'what is', 'how does', 'explain', 'describe', 'show me', 'lookup', + 'සොයන්න', 'විස්තර', 'කියන්න', 'පෙන්වන්න', + 'தேடு', 'விவரங்கள்', 'சொல்லு', 'காட்டு' + ]; + + // If user is explicitly asking for information, return false (not booking) + if (searchKeywords.some(keyword => lowerMessage.includes(keyword))) { + return false; + } + + // Booking action keywords - these indicate intent to book/schedule + const bookingActionKeywords = [ + 'book', 'schedule', 'appointment', 'meeting', 'visit', 'apply for', + 'register for', 'submit application', 'request appointment', 'need appointment', + 'want to book', 'would like to schedule', 'can i book', 'වෙන්කරවීම', + 'හමුවීම ගන්න', 'ලියාපදිංචි වීම', 'முன்பதิवு', 'சந்திப்பு பதிவু' + ]; + + // Response patterns that indicate they're providing booking details + const responsePatterns = [ + // These patterns suggest user is responding to booking questions + 'immigration', 'business registration', 'health services', 'education', + 'passport', 'license', 'certificate', 'permit', 'renewal', + 'senior officer', 'customer service', 'agent', 'monday', 'tuesday', + 'morning', 'afternoon', 'am', 'pm', 'tomorrow', 'next week' + ]; + + // Check if it's a booking action or response to booking questions + const hasBookingAction = bookingActionKeywords.some(keyword => + lowerMessage.includes(keyword) + ); + + const hasResponsePattern = responsePatterns.some(pattern => + lowerMessage.includes(pattern) + ); + + return hasBookingAction || hasResponsePattern; + }; + // Handle booking conversation flow + const handleBookingConversation = async (message: string): Promise => { try { - const response = await fetch('/api/ragbot/chat', { + // Send message to booking conversation handler API + const response = await fetch('/api/ragbot/booking-conversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: messageText, sessionId }), + body: JSON.stringify({ + message, + sessionId, + language: currentLanguage + }), }); if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.details || 'Failed to get response from server'); + throw new Error('Failed to process booking conversation'); } const data = await response.json(); - const botMessage: Message = { - type: 'bot', - text: data.response, - timestamp: new Date(), - departmentContacts: data.departmentContacts, - sources: data.sources, - }; - setMessages(prev => [...prev, botMessage]); + return data.response; + } catch (error) { + console.error('Error in booking conversation:', error); + return "I'd like to help you with booking, but I encountered an error. Please try again or contact support."; + } + }; + + const handleSendMessage = async (messageText: string) => { + if (!messageText.trim()) return; + + const userMessage: Message = { type: 'user', text: messageText, timestamp: new Date() }; + setMessages(prev => [...prev, userMessage]); + setLastUserMessage(messageText); + setIsTyping(true); + + try { + // First check if this is a booking-related message + const isBookingIntent = checkBookingIntent(messageText); + + if (isBookingIntent) { + // Set booking conversation state + setIsInBookingConversation(true); + + // Handle booking conversation flow instead of RAG search + const bookingResponse = await handleBookingConversation(messageText); + const botMessage: Message = { + type: 'bot', + text: bookingResponse, + timestamp: new Date(), + }; + setMessages(prev => [...prev, botMessage]); + + // Check if booking is complete + if (bookingResponse.includes('Open Booking Form') || bookingResponse.includes('booking information is ready')) { + setIsInBookingConversation(false); + } + } else { + // Reset booking conversation state when switching to information mode + setIsInBookingConversation(false); + // Original RAG chat flow for general government information + const response = await fetch('/api/ragbot/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: messageText, sessionId }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.details || 'Failed to get response from server'); + } + + const data = await response.json(); + const botMessage: Message = { + type: 'bot', + text: data.response, + timestamp: new Date(), + departmentContacts: data.departmentContacts, + sources: data.sources, + }; + setMessages(prev => [...prev, botMessage]); + } } catch (error) { console.error('Error sending message:', error); @@ -532,18 +678,38 @@ export default function RAGBotPage() { }> - +
+
); }; -function ChatContent({ messages, isTyping, language = 'en', onSendMessage }: { messages: Message[]; isTyping: boolean; language: Language, onSendMessage: (message: string) => void; }) { +function ChatContent({ messages, isTyping, language = 'en', onSendMessage, isInBookingMode }: { + messages: Message[]; + isTyping: boolean; + language: Language; + onSendMessage: (message: string) => void; + isInBookingMode?: boolean; +}) { const bottomRef = useRef(null); const t = chatTranslations[language]; @@ -596,7 +762,7 @@ function ChatContent({ messages, isTyping, language = 'en', onSendMessage }: { m ))} - {isTyping && } + {isTyping && }
); diff --git a/src/app/user/booking/new/page.tsx b/src/app/user/booking/new/page.tsx index 5d0a789..71d587b 100644 --- a/src/app/user/booking/new/page.tsx +++ b/src/app/user/booking/new/page.tsx @@ -521,6 +521,7 @@ function mockAvailableSlots(agentId: string, date: string, baseSlots: string[]): export default function NewBookingPage() { const router = useRouter(); const { user, isAuthenticated, isLoading } = useAuth(); + const [searchParams, setSearchParams] = useState(null); const [form, setForm] = useState({ department: "", @@ -537,6 +538,7 @@ export default function NewBookingPage() { const [reference, setReference] = useState(''); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); + const [isFromChat, setIsFromChat] = useState(false); const [appointmentData, setAppointmentData] = useState<{ id: string; bookingReference: string; @@ -672,6 +674,38 @@ export default function NewBookingPage() { // Load departments on mount useEffect(() => { loadDepartments(); + + // Handle pre-filled data from chat + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + const fromChat = urlParams.get('fromChat') === 'true'; + + if (fromChat) { + setIsFromChat(true); + + // Pre-fill form with chat data + const chatData = { + department: urlParams.get('department') || '', + service: urlParams.get('service') || '', + position: urlParams.get('agent') || '', // Map agent to position + day: urlParams.get('date') || '', + slot: urlParams.get('time') || '', + notes: urlParams.get('notes') || '' + }; + + setForm(prev => ({ + ...prev, + ...chatData + })); + + // Clear booking data from localStorage since we're now in the form + if (typeof window !== 'undefined') { + localStorage.removeItem('govlink_booking_data'); + } + + console.log('Pre-filled form with chat data:', chatData); + } + } }, [loadDepartments]); // Load services when department changes diff --git a/src/components/user/chat/AuthPrompt.tsx b/src/components/user/chat/AuthPrompt.tsx new file mode 100644 index 0000000..f4e03c5 --- /dev/null +++ b/src/components/user/chat/AuthPrompt.tsx @@ -0,0 +1,101 @@ +// src/components/user/chat/AuthPrompt.tsx +"use client"; +import React from 'react'; +import { User, ArrowRight, Shield } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +interface AuthPromptProps { + message: string; + language?: 'en' | 'si' | 'ta'; + redirectPath?: string; +} + +const translations = { + en: { + loginRequired: 'Login Required', + loginPrompt: 'Please login to continue with booking appointments and accessing personalized services.', + loginButton: 'Login to Continue', + registerPrompt: "Don't have an account?", + registerButton: 'Register Now', + secureMessage: 'Your data is secure and encrypted' + }, + si: { + loginRequired: 'ප්‍රවේශය අවශ්‍යයි', + loginPrompt: 'හමුවීම් වෙන්කරවීම සහ පුද්ගලිකත සේවා වලට ප්‍රවේශ වීමට කරුණාකර ප්‍රවේශ වන්න.', + loginButton: 'ඉදිරියට යාමට ප්‍රවේශ වන්න', + registerPrompt: 'ගිණුමක් නැත?', + registerButton: 'දැන් ලියාපදිංචි වන්න', + secureMessage: 'ඔබගේ දත්ත ආරක්ෂිත සහ සංකේතනය කර ඇත' + }, + ta: { + loginRequired: 'உள்நுழைவு தேவை', + loginPrompt: 'சந்திப்பு முன்பதிவு மற்றும் தனிப்பயன் சேவைகளைப் பெற உள்நுழையவும்.', + loginButton: 'தொடர உள்நுழையவும்', + registerPrompt: 'கணக்கு இல்லையா?', + registerButton: 'இப்போது பதிவு செய்யவும்', + secureMessage: 'உங்கள் தரவு பாதுகாப்பானது மற்றும் மறைகுறியாக்கப்பட்டுள்ளது' + } +}; + +export const AuthPrompt: React.FC = ({ + message, + language = 'en', + redirectPath = '/Ragbot' +}) => { + const router = useRouter(); + const t = translations[language]; + + const handleLogin = () => { + router.push(`/user/auth/login?redirect=${encodeURIComponent(redirectPath)}`); + }; + + const handleRegister = () => { + router.push(`/user/auth/register?redirect=${encodeURIComponent(redirectPath)}`); + }; + + return ( +
+
+
+
+ +
+
+ +
+
+

+ {t.loginRequired} +

+

+ {message || t.loginPrompt} +

+
+ +
+ + + +
+ +

+ + {t.secureMessage} +

+
+
+
+ ); +}; diff --git a/src/components/user/chat/BookingChatButton.tsx b/src/components/user/chat/BookingChatButton.tsx new file mode 100644 index 0000000..0d6bb47 --- /dev/null +++ b/src/components/user/chat/BookingChatButton.tsx @@ -0,0 +1,143 @@ +// src/components/user/chat/BookingChatButton.tsx +"use client"; +import React from 'react'; +import { Calendar, User, CheckCircle, AlertCircle } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useBookingChat } from '@/lib/hooks/useBookingChat'; +import { useAuth } from '@/lib/auth/AuthContext'; + +interface BookingChatButtonProps { + onLoginRequest?: () => void; + language?: 'en' | 'si' | 'ta'; +} + +const translations = { + en: { + openBookingForm: 'Open Booking Form', + loginRequired: 'Login Required', + loginToBook: 'Login to Book Appointment', + bookingReady: 'Booking Form Ready', + collectingInfo: 'Collecting Information...', + complete: 'Information Complete', + loginFirst: 'Please login to proceed with booking' + }, + si: { + openBookingForm: 'වෙන්කරවීමේ ආකෘතිය විවෘත කරන්න', + loginRequired: 'ප්‍රවේශය අවශ්‍යයි', + loginToBook: 'හමුවීමක් වෙන්කරවීමට ප්‍රවේශ වන්න', + bookingReady: 'වෙන්කරවීමේ ආකෘතිය සූදානම්', + collectingInfo: 'තොරතුරු එකතු කරමින්...', + complete: 'තොරතුරු සම්පූර්ණයි', + loginFirst: 'වෙන්කරවීම සමඟ ඉදිරියට යාමට කරුණාකර ප්‍රවේශ වන්න' + }, + ta: { + openBookingForm: 'முன்பதிவு படிவத்தைத் திறக்கவும்', + loginRequired: 'உள்நுழைவு தேவை', + loginToBook: 'சந்திப்பு முன்பதிவு செய்ய உள்நுழையவும்', + bookingReady: 'முன்பதிவு படிவம் தயார்', + collectingInfo: 'தகவல் சேகரிக்கப்படுகிறது...', + complete: 'தகவல் முடிந்தது', + loginFirst: 'முன்பதிவுடன் தொடர உள்நுழையவும்' + } +}; + +export const BookingChatButton: React.FC = ({ + onLoginRequest, + language = 'en' +}) => { + const router = useRouter(); + const { isAuthenticated, user } = useAuth(); + const { bookingData, canProceedToForm, isReadyForBooking } = useBookingChat(); + const t = translations[language]; + + const handleBookingAction = () => { + if (!isAuthenticated || !user) { + if (onLoginRequest) { + onLoginRequest(); + } else { + router.push('/user/auth/login?redirect=/user/booking/new'); + } + return; + } + + if (canProceedToForm()) { + // Navigate to booking form with pre-filled data + const queryParams = new URLSearchParams(); + if (bookingData?.department) queryParams.set('department', bookingData.department); + if (bookingData?.service) queryParams.set('service', bookingData.service); + if (bookingData?.agent) queryParams.set('agent', bookingData.agent); + if (bookingData?.date) queryParams.set('date', bookingData.date); + if (bookingData?.time) queryParams.set('time', bookingData.time); + if (bookingData?.notes) queryParams.set('notes', bookingData.notes); + + router.push(`/user/booking/new?fromChat=true&${queryParams.toString()}`); + } + }; + + const getButtonContent = () => { + if (!isAuthenticated) { + return { + icon: , + text: t.loginToBook, + subtext: t.loginFirst, + className: 'from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700', + disabled: false + }; + } + + if (canProceedToForm()) { + return { + icon: , + text: t.openBookingForm, + subtext: t.bookingReady, + className: 'from-green-500 to-green-600 hover:from-green-600 hover:to-green-700', + disabled: false + }; + } + + // When the conversation is still gathering details, don't display a "Collecting Information" label + // Show a neutral disabled state with no subtext to avoid the collecting message appearing under the chat + if (isReadyForBooking()) { + return { + icon: , + text: t.bookingReady, + subtext: '', + className: 'from-yellow-500 to-yellow-600 hover:from-yellow-600 hover:to-yellow-700', + disabled: true + }; + } + + return { + icon: , + text: t.bookingReady, + subtext: '', + className: 'from-gray-500 to-gray-600', + disabled: true + }; + }; + + const { icon, text, subtext, className, disabled } = getButtonContent(); + + return ( +
+ +
+ ); +}; diff --git a/src/components/user/chat/BookingChatManager.tsx b/src/components/user/chat/BookingChatManager.tsx new file mode 100644 index 0000000..1652ed2 --- /dev/null +++ b/src/components/user/chat/BookingChatManager.tsx @@ -0,0 +1,308 @@ +// src/components/user/chat/BookingChatManager.tsx +"use client"; +import React, { useCallback, useEffect, useState } from 'react'; +import { useBookingChat, BookingChatContext } from '@/lib/hooks/useBookingChat'; +import { AuthPrompt } from './AuthPrompt'; +import { useAuth } from '@/lib/auth/AuthContext'; +import { useRouter } from 'next/navigation'; + +interface BookingConversationData { + department?: string; + service?: string; + agentType?: string; + preferredDate?: string; + preferredTime?: string; + additionalNotes?: string; +} + +interface BookingChatManagerProps { + onBookingQuestionGenerated?: (question: string) => void; + onLoginRequired?: () => void; + language?: 'en' | 'si' | 'ta'; + lastUserMessage?: string; + sessionId?: string; + messages?: Array<{ type: 'user' | 'bot'; text: string; timestamp: Date }>; +} + +interface BookingIntent { + isBookingIntent: boolean; + extractedData?: BookingChatContext; + confidence: number; +} + +const bookingKeywords = [ + 'book', 'appointment', 'schedule', 'meeting', 'visit', 'apply for', + 'register', 'submit', 'request', 'service', 'office', 'agent', + 'වෙන්කරවීම', 'හමුවීම', 'ලියාපදිංචිය', 'සේවාව', 'කාර්යාලය', + 'முன்பதிவு', 'சந்திப்பு', 'பதிவு', 'சேவை', 'அலுவலகம்' +]; + +const departmentKeywords = { + immigration: ['passport', 'visa', 'immigration', 'emigration', 'travel document'], + business: ['business', 'company', 'registration', 'trade license', 'commercial'], + health: ['health', 'medical', 'certificate', 'clinic', 'hospital'], + education: ['education', 'school', 'university', 'certificate', 'scholarship'], + tax: ['tax', 'revenue', 'VAT', 'income tax', 'registration'] +}; + +export const BookingChatManager: React.FC = ({ + onBookingQuestionGenerated, + onLoginRequired, + language = 'en', + lastUserMessage, + sessionId, + messages = [] +}) => { + const router = useRouter(); + const { isAuthenticated } = useAuth(); + const [showAuthPrompt, setShowAuthPrompt] = useState(false); + const [completedBookingData, setCompletedBookingData] = useState(null); + const [hasNavigated, setHasNavigated] = useState(false); + const { + bookingData, + getNextQuestion, + processUserResponse, + saveBookingData + } = useBookingChat(); + + // Check for completed booking conversation from the API + useEffect(() => { + const checkCompletedBooking = async () => { + if (!isAuthenticated) return; + + try { + // Use prop sessionId or fallback to sessionStorage + const currentSessionId = sessionId || sessionStorage.getItem('ragSessionId') || `rag_session_${Date.now()}`; + + const response = await fetch(`/api/ragbot/booking-conversation-status?sessionId=${currentSessionId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + if (data.isComplete && data.collectedData) { + setCompletedBookingData(data.collectedData); + // Also sync with the booking chat hook + await saveBookingData({ + department: data.collectedData.department, + service: data.collectedData.service, + agent: data.collectedData.agentType, + date: data.collectedData.preferredDate, + time: data.collectedData.preferredTime, + notes: data.collectedData.additionalNotes, + completed: true + }); + } + } + } catch (error) { + console.error('Error checking booking status:', error); + } + }; + + checkCompletedBooking(); + + // Check periodically for updates + const interval = setInterval(checkCompletedBooking, 2000); + return () => clearInterval(interval); + }, [isAuthenticated, saveBookingData, sessionId]); + + const detectBookingIntent = useCallback((message: string): BookingIntent => { + const lowerMessage = message.toLowerCase(); + const hasBookingKeyword = bookingKeywords.some(keyword => + lowerMessage.includes(keyword.toLowerCase()) + ); + + if (!hasBookingKeyword) { + return { isBookingIntent: false, confidence: 0 }; + } + + // Extract department information + const extractedData: BookingChatContext = {}; + let confidence = 0.5; + + for (const [dept, keywords] of Object.entries(departmentKeywords)) { + if (keywords.some(keyword => lowerMessage.includes(keyword))) { + extractedData.departmentName = dept; + confidence = 0.8; + break; + } + } + + return { + isBookingIntent: true, + extractedData: Object.keys(extractedData).length > 0 ? extractedData : undefined, + confidence + }; + }, []); + + const handleBookingFlow = useCallback(async (message: string) => { + if (!isAuthenticated) { + setShowAuthPrompt(true); + if (onLoginRequired) { + onLoginRequired(); + } + return; + } + + try { + const intent = detectBookingIntent(message); + + if (intent.isBookingIntent && !bookingData) { + // Start new booking process + await processUserResponse(message, intent.extractedData); + const nextQuestion = getNextQuestion(); + if (nextQuestion && onBookingQuestionGenerated) { + onBookingQuestionGenerated(nextQuestion); + } + } else if (bookingData && !bookingData.completed) { + // Continue existing booking process + await processUserResponse(message); + const nextQuestion = getNextQuestion(); + if (nextQuestion && onBookingQuestionGenerated) { + onBookingQuestionGenerated(nextQuestion); + } + } + } catch (error) { + console.error('Error in booking flow:', error); + if (error instanceof Error && error.message.includes('Authentication required')) { + setShowAuthPrompt(true); + if (onLoginRequired) { + onLoginRequired(); + } + } + } + }, [ + isAuthenticated, + bookingData, + detectBookingIntent, + processUserResponse, + getNextQuestion, + onBookingQuestionGenerated, + onLoginRequired, + setShowAuthPrompt + ]); + + // Process last user message for booking intent + useEffect(() => { + if (lastUserMessage) { + const intent = detectBookingIntent(lastUserMessage); + if (intent.isBookingIntent) { + handleBookingFlow(lastUserMessage); + } + } + }, [lastUserMessage, detectBookingIntent, handleBookingFlow]); + + // Check if the last few bot messages contain completion indicators + const recentBotMessages = messages + .filter(msg => msg.type === 'bot') + .slice(-3) // Check last 3 bot messages + .map(msg => msg.text); + + const hasCompletionMessage = recentBotMessages.some(text => + text.includes("Perfect! I've collected all your information") || + text.includes("Open Booking Form") || + text.includes("booking information is ready") || + text.includes("You can now proceed to complete your booking") + ); + + // Check if booking is completed (either through API or completion message) + const isBookingCompleted = completedBookingData || hasCompletionMessage; + + // Auto-navigate to booking form when conversation completes + useEffect(() => { + if (isBookingCompleted && isAuthenticated && !hasNavigated) { + console.log('Auto-navigating to booking form...'); + + // Build query params from either completed data or bookingData + const queryParams = new URLSearchParams(); + queryParams.set('fromChat', 'true'); + + const dataSource = completedBookingData || bookingData; + if (dataSource) { + // Handle both BookingChatContext and BookingData types + if ('departmentName' in dataSource && dataSource.departmentName) { + queryParams.set('department', dataSource.departmentName); + } else if ('department' in dataSource && dataSource.department) { + queryParams.set('department', dataSource.department); + } + + if ('serviceName' in dataSource && dataSource.serviceName) { + queryParams.set('service', dataSource.serviceName); + } else if ('service' in dataSource && dataSource.service) { + queryParams.set('service', dataSource.service); + } + + // Handle agentType from API data + if ('agentType' in dataSource && (dataSource as BookingConversationData).agentType) { + queryParams.set('agent', (dataSource as BookingConversationData).agentType!); + } else if ('agentName' in dataSource && dataSource.agentName) { + queryParams.set('agent', dataSource.agentName); + } else if ('agent' in dataSource && dataSource.agent) { + queryParams.set('agent', dataSource.agent); + } + + // Handle preferredDate from API data + if ('preferredDate' in dataSource && (dataSource as BookingConversationData).preferredDate) { + queryParams.set('date', (dataSource as BookingConversationData).preferredDate!); + } else if ('date' in dataSource && dataSource.date) { + queryParams.set('date', dataSource.date); + } + + // Handle preferredTime from API data + if ('preferredTime' in dataSource && (dataSource as BookingConversationData).preferredTime) { + queryParams.set('time', (dataSource as BookingConversationData).preferredTime!); + } else if ('time' in dataSource && dataSource.time) { + queryParams.set('time', dataSource.time); + } + + // Handle additionalNotes from API data + if ('additionalNotes' in dataSource && (dataSource as BookingConversationData).additionalNotes) { + queryParams.set('notes', (dataSource as BookingConversationData).additionalNotes!); + } else if ('notes' in dataSource && dataSource.notes) { + queryParams.set('notes', dataSource.notes); + } + } + + setHasNavigated(true); + router.push(`/user/booking/new?${queryParams.toString()}`); + } + }, [isBookingCompleted, isAuthenticated, hasNavigated, completedBookingData, bookingData, router]); + + // Enhanced detection - check if there's any booking-related conversation happening + const hasAnyBookingActivity = + bookingData || + completedBookingData || + hasCompletionMessage || + messages.some(msg => + msg.text.includes('department') || + msg.text.includes('appointment') || + msg.text.includes('book') || + msg.text.includes('schedule') || + msg.text.includes('service') || + msg.text.includes('agent') || + msg.text.includes('Perfect! I') || + msg.text.includes('booking') + ); + + // Don't render anything if no booking activity exists + if (!hasAnyBookingActivity && !lastUserMessage) { + return null; + } + + return ( +
+ {showAuthPrompt && ( + + )} +
+ {isBookingCompleted ? 'Redirecting to booking form...' : 'Processing your booking request...'} +
+
+ ); +}; + +export default BookingChatManager; diff --git a/src/lib/hooks/useBookingChat.ts b/src/lib/hooks/useBookingChat.ts new file mode 100644 index 0000000..3a07459 --- /dev/null +++ b/src/lib/hooks/useBookingChat.ts @@ -0,0 +1,227 @@ +// src/lib/hooks/useBookingChat.ts +import { useState, useCallback } from 'react'; +import { useAuth } from '@/lib/auth/AuthContext'; + +export interface BookingData { + step: 'department' | 'service' | 'agent' | 'schedule' | 'details' | 'complete'; + department?: string; + departmentName?: string; + service?: string; + serviceName?: string; + agent?: string; + agentName?: string; + date?: string; + time?: string; + notes?: string; + sessionId: string; + userId?: string; + completed: boolean; +} + +export interface BookingChatContext { + departmentId?: string; + departmentName?: string; + serviceId?: string; + serviceName?: string; + agentId?: string; + agentName?: string; + date?: string; + time?: string; +} + +export interface BookingChatHook { + bookingData: BookingData | null; + saveBookingData: (data: Partial) => void; + clearBookingData: () => void; + isReadyForBooking: () => boolean; + getNextQuestion: () => string | null; + processUserResponse: (response: string, context?: BookingChatContext) => Promise; + canProceedToForm: () => boolean; +} + +const BOOKING_STORAGE_KEY = 'govlink_booking_data'; + +const bookingQuestions = { + department: "I'd be happy to help you book an appointment! First, which department do you need assistance with? For example: Immigration, Business Registration, Education, Health, etc.", + service: "Great! Now, what specific service do you need from {department}? Please describe the service you're looking for.", + agent: "Perfect! What type of official would you like to meet with? For example: Senior Officer, Specialist, Manager, etc.", + schedule: "Excellent! When would you prefer to schedule your appointment? Please provide your preferred date and time.", + details: "Almost done! Do you have any specific notes or requirements for your appointment? If not, just say 'no' or 'none'.", + complete: "Thank you! I have collected all the necessary information. You can now open the booking form to complete your appointment request." +}; + +export const useBookingChat = (): BookingChatHook => { + const { user, isAuthenticated } = useAuth(); + const [bookingData, setBookingData] = useState(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(BOOKING_STORAGE_KEY); + if (stored) { + try { + return JSON.parse(stored); + } catch { + return null; + } + } + } + return null; + }); + + const saveBookingData = useCallback(async (data: Partial) => { + if (!isAuthenticated || !user) { + console.warn('User must be authenticated to save booking data'); + return; + } + + const newData: BookingData = { + step: 'department', + sessionId: `booking_${Date.now()}_${Math.random().toString(36).substring(7)}`, + completed: false, + userId: user.id, + ...bookingData, + ...data, + }; + + setBookingData(newData); + + // Save to localStorage + if (typeof window !== 'undefined') { + localStorage.setItem(BOOKING_STORAGE_KEY, JSON.stringify(newData)); + } + + // Also save to MongoDB for persistence across devices + try { + const response = await fetch('/api/user/booking-data', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newData), + }); + + if (!response.ok) { + console.warn('Failed to save booking data to server:', await response.text()); + } + } catch (error) { + console.warn('Error saving booking data to server:', error); + // Continue with localStorage only + } + }, [bookingData, isAuthenticated, user]); + + const clearBookingData = useCallback(async () => { + setBookingData(null); + + // Clear from localStorage + if (typeof window !== 'undefined') { + localStorage.removeItem(BOOKING_STORAGE_KEY); + } + + // Clear from MongoDB if user is authenticated + if (isAuthenticated && user && bookingData?.sessionId) { + try { + await fetch(`/api/user/booking-data?sessionId=${bookingData.sessionId}&userId=${user.id}`, { + method: 'DELETE', + }); + } catch (error) { + console.warn('Error clearing booking data from server:', error); + } + } + }, [isAuthenticated, user, bookingData]); + + const isReadyForBooking = useCallback(() => { + return !!(bookingData?.department && bookingData?.service); + }, [bookingData]); + + const canProceedToForm = useCallback(() => { + return !!( + bookingData?.department && + bookingData?.service && + bookingData?.agent && + bookingData?.completed && + isAuthenticated && + user + ); + }, [bookingData, isAuthenticated, user]); + + const getNextQuestion = useCallback(() => { + if (!bookingData) return bookingQuestions.department; + + switch (bookingData.step) { + case 'department': + return bookingQuestions.service.replace('{department}', bookingData.departmentName || 'the selected department'); + case 'service': + return bookingQuestions.agent; + case 'agent': + return bookingQuestions.schedule; + case 'schedule': + return bookingQuestions.details; + case 'details': + return bookingQuestions.complete; + default: + return null; + } + }, [bookingData]); + + const processUserResponse = useCallback(async (response: string, context?: BookingChatContext) => { + if (!isAuthenticated || !user) { + throw new Error('Authentication required to process booking'); + } + + if (!bookingData) { + // Start new booking process + saveBookingData({ + step: 'department', + department: context?.departmentId, + departmentName: context?.departmentName || response, + }); + return; + } + + switch (bookingData.step) { + case 'department': + saveBookingData({ + step: 'service', + service: context?.serviceId, + serviceName: context?.serviceName || response, + }); + break; + case 'service': + saveBookingData({ + step: 'agent', + agent: context?.agentId, + agentName: context?.agentName || response, + }); + break; + case 'agent': + saveBookingData({ + step: 'schedule', + date: context?.date, + time: context?.time, + }); + break; + case 'schedule': + saveBookingData({ + step: 'details', + notes: response, + }); + break; + case 'details': + saveBookingData({ + step: 'complete', + completed: true, + }); + break; + default: + break; + } + }, [bookingData, saveBookingData, isAuthenticated, user]); + + return { + bookingData, + saveBookingData, + clearBookingData, + isReadyForBooking, + getNextQuestion, + processUserResponse, + canProceedToForm, + }; +};