diff --git a/BOOKING_INTEGRATION.md b/BOOKING_INTEGRATION.md new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/ragbot/booking-suggestions/route.ts b/src/app/api/ragbot/booking-suggestions/route.ts new file mode 100644 index 0000000..c106f8b --- /dev/null +++ b/src/app/api/ragbot/booking-suggestions/route.ts @@ -0,0 +1,290 @@ +// src/app/api/ragbot/booking-suggestions/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import Department from '@/lib/models/departmentSchema'; + +interface ServiceSchema { + id: string; + name: string; + description?: string; + category?: string; + isActive?: boolean; + processingTime?: string; + fee?: number; + requirements?: string[]; +} +interface DepartmentData { + id: string; + departmentId: string; + name: string; + shortName: string; + description: string; + services?: ServiceData[]; +} + +interface ServiceData { + id: string; + name: string; + description?: string; + category?: string; + processingTime?: string; + fee?: number; + requirements?: string[]; + departmentId?: string; + departmentName?: string; +} + +interface BookingSuggestion { + type: 'department' | 'service'; + id: string; + name: string; + description?: string; + departmentId?: string; + departmentName?: string; + category?: string; + fee?: number; + relevanceScore: number; +} + +/** + * Server-side booking suggestions for the RAG bot + */ +export async function POST(request: NextRequest) { + try { + // Add proper error handling for JSON parsing + let body; + try { + body = await request.json(); + } catch (jsonError) { + console.error('❌ Invalid JSON in request body:', jsonError); + return NextResponse.json( + { + error: 'Invalid JSON in request body', + suggestions: [], + details: jsonError instanceof Error ? jsonError.message : 'Unknown JSON parsing error' + }, + { status: 400 } + ); + } + + const { query } = body; + + if (!query) { + return NextResponse.json( + { + error: 'Query is required', + suggestions: [] + }, + { status: 400 } + ); + } + + console.log('🔍 Booking suggestions API called with query:', query); + + await connectDB(); + + // Fetch departments with services + const departments = await Department.find( + { + $or: [ + { status: 'ACTIVE' }, + { status: 'active' } + ] + }, + { + _id: 1, + departmentId: 1, + name: 1, + shortName: 1, + description: 1, + services: 1 + } + ).lean(); + + console.log('🏛️ Raw departments from DB:', departments.length); + console.log('📊 First department services:', departments[0]?.services?.length || 0); + + // Transform data to match booking helper format + const departmentData: DepartmentData[] = departments.map(dept => ({ + id: dept._id ? String(dept._id) : '', + departmentId: dept.departmentId || '', + name: dept.name || '', + shortName: dept.shortName || '', + description: dept.description || '', + services: (dept.services || []) + .filter((service: ServiceSchema) => service.isActive) + .map((service: ServiceSchema) => ({ + id: service.id || '', + name: service.name || '', + description: service.description || '', + category: service.category || '', + processingTime: service.processingTime || '', + fee: service.fee || 0, + requirements: service.requirements || [], + departmentId: dept.departmentId || '', + departmentName: dept.name || '' + })) + })); + + // Flatten all services + const services: ServiceData[] = departmentData.flatMap(dept => dept.services || []); + + // Generate suggestions using server-side logic + const suggestions = generateBookingSuggestions(query, departmentData, services); + + console.log('📊 Generated suggestions:', suggestions.length); + console.log('🎯 Suggestions:', suggestions); + console.log('🏛️ Departments found:', departmentData.length); + console.log('📋 Services found:', services.length); + + return NextResponse.json({ + success: true, + suggestions, + departments: departmentData, + services + }); + + } catch (error) { + console.error('Error generating booking suggestions:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + + return NextResponse.json( + { + error: 'Internal server error', + details: errorMessage + }, + { status: 500 } + ); + } +} + +/** + * Server-side suggestion logic (similar to client-side but optimized for server) + */ +function generateBookingSuggestions( + query: string, + departments: DepartmentData[], + services: ServiceData[] +): BookingSuggestion[] { + const normalizedQuery = query.toLowerCase(); + const suggestions: BookingSuggestion[] = []; + + // Keywords mapping for better matching + const serviceKeywords: Record = { + passport: ['passport', 'travel', 'immigration', 'visa', 'abroad', 'international'], + license: ['license', 'permit', 'driving', 'vehicle', 'motorcycle', 'car'], + certificate: ['certificate', 'birth', 'death', 'marriage', 'divorce', 'citizenship'], + registration: ['register', 'registration', 'business', 'company', 'organization'], + tax: ['tax', 'income', 'vat', 'customs', 'duty', 'revenue'], + education: ['education', 'school', 'university', 'degree', 'scholarship', 'student'], + health: ['health', 'medical', 'hospital', 'medicine', 'doctor', 'treatment'], + property: ['property', 'land', 'house', 'building', 'real estate', 'title'], + insurance: ['insurance', 'social security', 'pension', 'retirement', 'benefits'], + employment: ['employment', 'job', 'work', 'labor', 'salary', 'employee'] + }; + + // Department keywords mapping + const departmentKeywords: Record = { + immigration: ['immigration', 'passport', 'visa', 'travel', 'foreign'], + transport: ['transport', 'vehicle', 'driving', 'license', 'road'], + registrar: ['registrar', 'birth', 'death', 'marriage', 'certificate'], + revenue: ['revenue', 'tax', 'customs', 'duty', 'vat'], + education: ['education', 'ministry of education', 'school', 'university'], + health: ['health', 'ministry of health', 'medical', 'hospital'], + lands: ['lands', 'property', 'title', 'survey', 'real estate'] + }; + + // Score services based on keyword matching + services.forEach(service => { + let relevanceScore = 0; + + // Direct name matching + if (service.name.toLowerCase().includes(normalizedQuery)) { + relevanceScore += 10; + } + + // Description matching + if (service.description && service.description.toLowerCase().includes(normalizedQuery)) { + relevanceScore += 7; + } + + // Category matching + if (service.category && service.category.toLowerCase().includes(normalizedQuery)) { + relevanceScore += 5; + } + + // Keyword matching + Object.entries(serviceKeywords).forEach(([category, keywords]) => { + keywords.forEach(keyword => { + if (normalizedQuery.includes(keyword)) { + if (service.name.toLowerCase().includes(category)) { + relevanceScore += 8; + } else if (service.description?.toLowerCase().includes(category)) { + relevanceScore += 6; + } else if (service.category?.toLowerCase().includes(category)) { + relevanceScore += 4; + } + } + }); + }); + + if (relevanceScore > 0) { + suggestions.push({ + type: 'service', + id: service.id, + name: service.name, + description: service.description, + departmentId: service.departmentId, + departmentName: service.departmentName, + category: service.category, + fee: service.fee, + relevanceScore + }); + } + }); + + // Score departments based on keyword matching + departments.forEach(dept => { + let relevanceScore = 0; + + // Direct name matching + if (dept.name.toLowerCase().includes(normalizedQuery) || + dept.shortName.toLowerCase().includes(normalizedQuery)) { + relevanceScore += 8; + } + + // Description matching + if (dept.description && dept.description.toLowerCase().includes(normalizedQuery)) { + relevanceScore += 6; + } + + // Keyword matching for departments + Object.entries(departmentKeywords).forEach(([category, keywords]) => { + keywords.forEach(keyword => { + if (normalizedQuery.includes(keyword)) { + if (dept.name.toLowerCase().includes(category) || + dept.shortName.toLowerCase().includes(category)) { + relevanceScore += 7; + } else if (dept.description?.toLowerCase().includes(category)) { + relevanceScore += 5; + } + } + }); + }); + + if (relevanceScore > 0) { + suggestions.push({ + type: 'department', + id: dept.departmentId, + name: dept.name, + description: dept.description, + relevanceScore + }); + } + }); + + // Sort by relevance score and return top 5 + return suggestions + .sort((a, b) => b.relevanceScore - a.relevanceScore) + .slice(0, 5); +} diff --git a/src/app/globals.css b/src/app/globals.css index 83d8719..70c8651 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -621,4 +621,51 @@ border: 2px solid #00ff00 !important; border-radius: 4px; box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); +} + +.markdown-content a { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin: 0 0.125rem; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + background: linear-gradient(135deg, + rgba(255, 199, 44, 0.1) 0%, + rgba(255, 87, 34, 0.1) 100%); + border: 1px solid rgba(255, 199, 44, 0.3); + color: #FFC72C; + font-weight: 500; + font-size: 0.875rem; + text-decoration: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(4px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.markdown-content a:hover { + color: #FF5722; + border-color: rgba(255, 87, 34, 0.5); + background: linear-gradient(135deg, + rgba(255, 199, 44, 0.15) 0%, + rgba(255, 87, 34, 0.15) 100%); + box-shadow: 0 4px 12px rgba(255, 199, 44, 0.2); + transform: translateY(-1px); +} + +/* Dark mode adjustments */ +:root.dark .markdown-content a { + background: linear-gradient(135deg, + rgba(255, 199, 44, 0.08) 0%, + rgba(255, 87, 34, 0.08) 100%); + border-color: rgba(255, 199, 44, 0.25); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +:root.dark .markdown-content a:hover { + background: linear-gradient(135deg, + rgba(255, 199, 44, 0.12) 0%, + rgba(255, 87, 34, 0.12) 100%); + box-shadow: 0 4px 12px rgba(255, 199, 44, 0.15); } \ No newline at end of file diff --git a/src/app/ragbot/page.tsx b/src/app/ragbot/page.tsx index 842f606..0830262 100644 --- a/src/app/ragbot/page.tsx +++ b/src/app/ragbot/page.tsx @@ -1,16 +1,17 @@ // src/app/ragbot/page.tsx "use client"; -import React, { Suspense, useState, useEffect, useRef } from 'react'; +import React, { Suspense, useState, useEffect, useRef, useCallback } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; import UserDashboardLayout from '@/components/user/dashboard/UserDashboardLayout'; -import BookingChatManager from '@/components/user/chat/BookingChatManager'; -import { useRouter } from 'next/navigation'; + +import { useAuth } from '@/lib/auth/AuthContext'; +import { Header } from '@/components/Header'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; import rehypeRaw from 'rehype-raw'; import 'highlight.js/styles/github-dark.css'; -// --- TYPES --- type Language = 'en' | 'si' | 'ta'; interface DepartmentContact { @@ -46,7 +47,7 @@ const chatTranslations: Record) => ( @@ -115,6 +116,80 @@ const SendIcon = (props: React.SVGProps) => ( ); +const HomeIcon = (props: React.SVGProps) => ( + + + + +); + +const DashboardIcon = (props: React.SVGProps) => ( + + + + + + +); + +const LoginIcon = (props: React.SVGProps) => ( + + + + + +); + +// --- NAVIGATION COMPONENT --- +const ChatbotNavigation = ({ isAuthenticated }: { isAuthenticated: boolean }) => { + const router = useRouter(); + + const handleHomeClick = () => { + router.push('/'); + }; + + const handleDashboardClick = () => { + if (isAuthenticated) { + router.push('/user/dashboard'); + } else { + router.push('/user/auth/login'); + } + }; + + return ( +
+ {/* Home Button */} + + + {/* Dashboard/Login Button */} + +
+ ); +}; + const SparklesIcon = (props: React.SVGProps) => ( @@ -148,7 +223,7 @@ const RAGBotIcon = () => ( ); -// --- PREMIUM MESSAGE COMPONENTS --- +// --- MESSAGE COMPONENTS --- const TimeAgo = ({ timestamp }: { timestamp: Date }) => { const [timeAgo, setTimeAgo] = useState(''); @@ -180,6 +255,13 @@ const TimeAgo = ({ timestamp }: { timestamp: Date }) => { return {timeAgo}; }; +const TopicTag = ({ text }: { text: string }) => ( +
+
+ {text} +
+); + const UserMessage = ({ text, timestamp = new Date() }: { text: string; timestamp?: Date }) => (
@@ -212,60 +294,59 @@ const TypingIndicator = ({ language = 'en', isBookingMode = false }: { language?
-
- - - - {messages.analyzing} +
+
+
+
+
+
+ {t.analyzing}
-
- {messages.preparing} -
); }; -const ContactCard = ({ contact }: { contact: DepartmentContact }) => { - return ( -
-

{contact.name}

-
- {contact.phone &&
📞 {contact.phone}
} - {contact.email &&
✉️ {contact.email}
} - {contact.website && } - {contact.address &&
📍 {contact.address}
} -
-
- ); - }; +const DepartmentContactCard = ({ contact }: { contact: DepartmentContact }) => ( +
+

{contact.name}

+
+ {contact.phone &&

📞 {contact.phone}

} + {contact.email &&

✉️ {contact.email}

} + {contact.website && ( +

🌐 {contact.website}

+ )} +
+
+); + +const SourcesSection = ({ sources, language = 'en' }: { sources: string[]; language?: Language }) => { + const t = chatTranslations[language]; -const SourcesSection = ({ sources, language }: { sources: string[]; language: Language }) => { - const t = chatTranslations[language]; - - if (!sources || sources.length === 0) return null; - - return ( -
-

{t.sources}

-
- {sources.map((source, index) => ( - - ))} -
+ if (!sources || sources.length === 0) return null; + + return ( +
+

+ 📚 {t.sources} +

+
+ {sources.map((source, index) => ( + + 🔗 {new URL(source).hostname} + + ))}
- ); +
+ ); }; const BotMessage = ({ message, language = 'en' }: { message: Message; language?: Language }) => { @@ -287,94 +368,46 @@ const BotMessage = ({ message, language = 'en' }: { message: Message; language?: h1: ({ children }) =>

{children}

, h2: ({ children }) =>

{children}

, h3: ({ children }) =>

{children}

, - h4: ({ children }) =>

{children}

, p: ({ children }) =>

{children}

, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , strong: ({ children }) => {children}, - em: ({ children }) => {children}, - blockquote: ({ children }) => ( -
    - {children} -
    - ), - code: ({ children, className, ...props }: React.ComponentProps<'code'>) => { - const isInline = !className; - if (isInline) { - return ( - - {children} - - ); - } - return ( - - {children} - - ); - }, - pre: ({ children }) => ( -
    -                      {children}
    -                    
    - ), - table: ({ children }) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }) => ( - - {children} - - ), - th: ({ children }) => ( - - {children} - - ), - td: ({ children }) => ( - - {children} - - ), - tr: ({ children, ...props }) => ( - - {children} - - ), - a: ({ children, href }) => ( - ( + + 🔗 {children} + ), }} > {text} - {departmentContacts && departmentContacts.length > 0 && ( -
    -

    {chatTranslations[language].contactDetails}

    -
    - {departmentContacts.map((contact, index) => ( - - ))} -
    -
    - )} -
    + + {departmentContacts && departmentContacts.length > 0 && ( +
    +

    + 🏛️ {chatTranslations[language].contactDetails} +

    +
    + {departmentContacts.map((contact) => ( + + ))} +
    +
    + )} + + {sources && } -
    - GovLink Assistant • +
    +
    @@ -382,65 +415,60 @@ const BotMessage = ({ message, language = 'en' }: { message: Message; language?: ); }; -const TopicTag = ({ text }: { text: string }) => ( -
    - {text} -
    -); - -const ChatInput = ({ onSendMessage, language = 'en', disabled = false }: { onSendMessage: (message: string) => void; language?: Language, disabled?: boolean }) => { +// --- CHAT INPUT COMPONENT --- +const ChatInput = ({ onSendMessage, language = 'en', disabled = false }: { onSendMessage: (message: string) => void; language?: Language; disabled?: boolean }) => { const [message, setMessage] = useState(''); - const [isFocused, setIsFocused] = useState(false); + const textareaRef = useRef(null); const t = chatTranslations[language]; - const handleSend = () => { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); if (message.trim() && !disabled) { onSendMessage(message.trim()); setMessage(''); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + } } }; - const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - handleSend(); + handleSubmit(e); } }; return ( -
    -
    -
    -
    +
    +
    +
    +