305 lines
9.1 KiB
TypeScript
305 lines
9.1 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useRef, useEffect } from 'react'
|
|
import { MessageCircle, X, Send, User, Bot, Heart } from 'lucide-react'
|
|
import { useLanguage } from '@/components/providers/LanguageProvider'
|
|
import { useAuth } from '@/components/providers/MockAuthProvider'
|
|
|
|
type Message = {
|
|
id: string
|
|
text: string
|
|
sender: 'user' | 'bot'
|
|
timestamp: Date
|
|
type?: 'general' | 'mental_health'
|
|
}
|
|
|
|
type ChatMode = 'general' | 'mental_health'
|
|
|
|
export const ChatWidget: React.FC = () => {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [messages, setMessages] = useState<Message[]>([])
|
|
const [inputText, setInputText] = useState('')
|
|
const [isTyping, setIsTyping] = useState(false)
|
|
const [chatMode, setChatMode] = useState<ChatMode>('general')
|
|
const [showSurvey, setShowSurvey] = useState(false)
|
|
const [surveyRating, setSurveyRating] = useState(0)
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
const { t, dir } = useLanguage()
|
|
const { user, userProfile } = useAuth()
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [messages])
|
|
|
|
const addMessage = (text: string, sender: 'user' | 'bot', type?: 'general' | 'mental_health') => {
|
|
const newMessage: Message = {
|
|
id: Date.now().toString(),
|
|
text,
|
|
sender,
|
|
timestamp: new Date(),
|
|
type,
|
|
}
|
|
setMessages(prev => [...prev, newMessage])
|
|
}
|
|
|
|
const handleSendMessage = async () => {
|
|
if (!inputText.trim()) return
|
|
|
|
const userMessage = inputText.trim()
|
|
setInputText('')
|
|
addMessage(userMessage, 'user', chatMode)
|
|
|
|
setIsTyping(true)
|
|
|
|
try {
|
|
// Create userContext if user is logged in
|
|
const userContext = user ? {
|
|
role: userProfile?.role || 'student',
|
|
token: user.id,
|
|
profile: userProfile ? {
|
|
name: userProfile.name,
|
|
email: userProfile.email,
|
|
year: userProfile.year,
|
|
faculty: userProfile.faculty,
|
|
balance: userProfile.balance
|
|
} : undefined
|
|
} : undefined;
|
|
|
|
const response = await fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: userMessage,
|
|
mode: chatMode,
|
|
history: messages,
|
|
userContext
|
|
}),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
setIsTyping(false)
|
|
addMessage(data.message || data.response, 'bot', chatMode)
|
|
|
|
// Check for mental health escalation
|
|
if (data.shouldEscalate) {
|
|
setTimeout(() => {
|
|
addMessage(
|
|
`${t('mental_health_support')} - ${t('book_counselor')}: https://calendly.com/university-counseling`,
|
|
'bot',
|
|
'mental_health'
|
|
)
|
|
}, 1000)
|
|
}
|
|
|
|
// Show survey after interaction
|
|
if (Math.random() > 0.7) {
|
|
setTimeout(() => {
|
|
setShowSurvey(true)
|
|
}, 2000)
|
|
}
|
|
} catch (error) {
|
|
setIsTyping(false)
|
|
addMessage('Sorry, I encountered an error. Please try again.', 'bot')
|
|
console.error('Chat error:', error)
|
|
}
|
|
}
|
|
|
|
const handleSurveySubmit = async () => {
|
|
if (surveyRating > 0) {
|
|
try {
|
|
await fetch('/api/survey', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
rating: surveyRating,
|
|
type: 'chat_satisfaction',
|
|
sessionId: Date.now().toString(),
|
|
}),
|
|
})
|
|
} catch (error) {
|
|
console.error('Survey submission error:', error)
|
|
}
|
|
}
|
|
setShowSurvey(false)
|
|
setSurveyRating(0)
|
|
}
|
|
|
|
const switchToMentalHealth = () => {
|
|
setChatMode('mental_health')
|
|
addMessage(
|
|
'I\'m here to provide support. How are you feeling today?',
|
|
'bot',
|
|
'mental_health'
|
|
)
|
|
}
|
|
|
|
const switchToGeneral = () => {
|
|
setChatMode('general')
|
|
addMessage(
|
|
'I can help you with general university questions. What would you like to know?',
|
|
'bot',
|
|
'general'
|
|
)
|
|
}
|
|
|
|
if (!isOpen) {
|
|
return (
|
|
<button
|
|
onClick={() => setIsOpen(true)}
|
|
className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-full shadow-lg hover:bg-blue-700 transition-colors z-50"
|
|
aria-label="Open chat"
|
|
>
|
|
<MessageCircle size={24} />
|
|
</button>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 w-80 h-96 bg-white rounded-lg shadow-xl border border-gray-200 flex flex-col z-50">
|
|
{/* Header */}
|
|
<div className="bg-blue-600 text-white p-4 rounded-t-lg flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Bot size={20} />
|
|
<span className="font-medium">University Assistant</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsOpen(false)}
|
|
className="hover:bg-blue-700 rounded p-1"
|
|
aria-label="Close chat"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mode Toggle */}
|
|
<div className="p-2 bg-gray-50 border-b flex space-x-2">
|
|
<button
|
|
onClick={switchToGeneral}
|
|
className={`px-3 py-1 rounded text-sm ${
|
|
chatMode === 'general'
|
|
? 'bg-blue-600 text-white'
|
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
}`}
|
|
>
|
|
{t('chat')}
|
|
</button>
|
|
<button
|
|
onClick={switchToMentalHealth}
|
|
className={`px-3 py-1 rounded text-sm flex items-center space-x-1 ${
|
|
chatMode === 'mental_health'
|
|
? 'bg-pink-600 text-white'
|
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
}`}
|
|
>
|
|
<Heart size={12} />
|
|
<span>{t('wellbeing')}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{messages.length === 0 && (
|
|
<div className="text-center text-gray-500 text-sm">
|
|
<p>{t('ask_question')}</p>
|
|
</div>
|
|
)}
|
|
|
|
{messages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-xs px-3 py-2 rounded-lg text-sm ${
|
|
message.sender === 'user'
|
|
? 'bg-blue-600 text-white'
|
|
: message.type === 'mental_health'
|
|
? 'bg-pink-100 text-pink-900 border border-pink-200'
|
|
: 'bg-gray-100 text-gray-900'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-2 mb-1">
|
|
{message.sender === 'user' ? (
|
|
<User size={12} />
|
|
) : (
|
|
<Bot size={12} />
|
|
)}
|
|
<span className="text-xs opacity-75">
|
|
{message.timestamp.toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
<p>{message.text}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isTyping && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-gray-100 px-3 py-2 rounded-lg text-sm flex items-center space-x-2">
|
|
<Bot size={12} />
|
|
<span>Typing...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Survey */}
|
|
{showSurvey && (
|
|
<div className="p-4 bg-yellow-50 border-t border-yellow-200">
|
|
<p className="text-sm text-yellow-800 mb-2">{t('survey_rating')}</p>
|
|
<div className="flex space-x-2 mb-2">
|
|
{[1, 2, 3, 4, 5].map((rating) => (
|
|
<button
|
|
key={rating}
|
|
onClick={() => setSurveyRating(rating)}
|
|
className={`w-6 h-6 rounded-full text-xs ${
|
|
surveyRating >= rating
|
|
? 'bg-yellow-500 text-white'
|
|
: 'bg-gray-200 text-gray-600'
|
|
}`}
|
|
>
|
|
{rating}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={handleSurveySubmit}
|
|
className="bg-yellow-600 text-white px-3 py-1 rounded text-sm hover:bg-yellow-700"
|
|
>
|
|
{t('submit_feedback')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Input */}
|
|
<div className="p-4 border-t">
|
|
<div className="flex space-x-2">
|
|
<input
|
|
type="text"
|
|
value={inputText}
|
|
onChange={(e) => setInputText(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
|
placeholder={t('type_message')}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
dir={dir}
|
|
/>
|
|
<button
|
|
onClick={handleSendMessage}
|
|
disabled={!inputText.trim()}
|
|
className="bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Send size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|