Spaces:
Runtime error
Runtime error
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { X, Send, Loader2 } from 'lucide-react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import axios from '../api/axiosConfig'; | |
| export default function AgentTakeover({ sessionId, visitorName, onClose }) { | |
| const [message, setMessage] = useState(''); | |
| const [messages, setMessages] = useState([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isSending, setIsSending] = useState(false); | |
| const [socket, setSocket] = useState(null); | |
| const messagesEndRef = useRef(null); | |
| useEffect(() => { | |
| // Load session history | |
| loadSessionHistory(); | |
| // Connect WebSocket (simplified - you'll need socket.io-client) | |
| const ws = new WebSocket(`ws://localhost:8000/ws/session/${sessionId}`); | |
| ws.onopen = () => { | |
| // Send takeover event | |
| ws.send(JSON.stringify({ | |
| type: 'takeover', | |
| session_id: sessionId, | |
| agent_id: 'current_user_id' // Get from auth context | |
| })); | |
| }; | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'visitor_message') { | |
| setMessages(prev => [...prev, { | |
| id: Date.now(), | |
| message: data.message, | |
| from: 'visitor', | |
| timestamp: new Date() | |
| }]); | |
| } | |
| }; | |
| setSocket(ws); | |
| return () => { | |
| if (ws) { | |
| ws.close(); | |
| } | |
| }; | |
| }, [sessionId]); | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| const loadSessionHistory = async () => { | |
| try { | |
| const response = await axios.get(`/api/chat/sessions/${sessionId}/messages`); | |
| setMessages(response.data.map(msg => ({ | |
| id: msg.id, | |
| message: msg.message, | |
| from: msg.is_from_visitor ? 'visitor' : msg.is_from_ai ? 'ai' : 'agent', | |
| timestamp: new Date(msg.created_at) | |
| }))); | |
| setIsLoading(false); | |
| } catch (error) { | |
| console.error('Failed to load session history:', error); | |
| setIsLoading(false); | |
| } | |
| }; | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }; | |
| const sendMessage = async () => { | |
| if (!message.trim() || isSending || !socket) return; | |
| setIsSending(true); | |
| const newMessage = { | |
| id: Date.now(), | |
| message: message, | |
| from: 'agent', | |
| timestamp: new Date() | |
| }; | |
| // Optimistic update | |
| setMessages(prev => [...prev, newMessage]); | |
| // Send via WebSocket | |
| socket.send(JSON.stringify({ | |
| type: 'agent_message', | |
| session_id: sessionId, | |
| message: message, | |
| agent_id: 'current_user_id' | |
| })); | |
| setMessage(''); | |
| setIsSending(false); | |
| }; | |
| const handleKeyPress = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }; | |
| const saveAsKnowledge = async (messageId) => { | |
| try { | |
| await axios.post(`/api/knowledge/save`, { | |
| message_id: messageId, | |
| session_id: sessionId | |
| }); | |
| alert('Saved to knowledge base!'); | |
| } catch (error) { | |
| console.error('Failed to save:', error); | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.95 }} | |
| className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl h-[80vh] flex flex-col" | |
| > | |
| {/* Header */} | |
| <div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-6 rounded-t-2xl flex items-center justify-between"> | |
| <div> | |
| <h2 className="text-2xl font-bold">Live Chat Session</h2> | |
| <p className="text-blue-100 text-sm mt-1"> | |
| Chatting with {visitorName || 'Anonymous'} • Session #{sessionId} | |
| </p> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-2 hover:bg-white/10 rounded-lg transition-colors" | |
| > | |
| <X className="w-6 h-6" /> | |
| </button> | |
| </div> | |
| {/* Messages */} | |
| <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-gray-50"> | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center h-full"> | |
| <Loader2 className="w-8 h-8 animate-spin text-blue-600" /> | |
| </div> | |
| ) : ( | |
| <> | |
| <AnimatePresence> | |
| {messages.map((msg) => ( | |
| <motion.div | |
| key={msg.id} | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -10 }} | |
| className={`flex ${msg.from === 'visitor' ? 'justify-start' : 'justify-end'}`} | |
| > | |
| <div | |
| className={`max-w-[70%] rounded-2xl px-4 py-3 ${msg.from === 'visitor' | |
| ? 'bg-white border border-gray-200 text-gray-900' | |
| : msg.from === 'ai' | |
| ? 'bg-gradient-to-r from-purple-100 to-purple-50 border border-purple-200 text-purple-900' | |
| : 'bg-gradient-to-r from-blue-600 to-blue-700 text-white' | |
| }`} | |
| > | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className="text-xs font-semibold opacity-70"> | |
| {msg.from === 'visitor' ? 'Visitor' : msg.from === 'ai' ? '🤖 AI' : 'You'} | |
| </span> | |
| <span className="text-xs opacity-50"> | |
| {msg.timestamp.toLocaleTimeString()} | |
| </span> | |
| </div> | |
| <p className="text-sm leading-relaxed">{msg.message}</p> | |
| {/* Save as knowledge button for agent messages */} | |
| {msg.from === 'agent' && ( | |
| <button | |
| onClick={() => saveAsKnowledge(msg.id)} | |
| className="mt-2 text-xs opacity-70 hover:opacity-100 underline" | |
| > | |
| Save as Knowledge | |
| </button> | |
| )} | |
| </div> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| <div ref={messagesEndRef} /> | |
| </> | |
| )} | |
| </div> | |
| {/* Input */} | |
| <div className="p-6 bg-white border-t border-gray-200 rounded-b-2xl"> | |
| <div className="flex gap-3"> | |
| <textarea | |
| value={message} | |
| onChange={(e) => setMessage(e.target.value)} | |
| onKeyPress={handleKeyPress} | |
| placeholder="Type your message..." | |
| rows="2" | |
| className="flex-1 border border-gray-300 rounded-xl px-4 py-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none" | |
| disabled={isSending} | |
| /> | |
| <button | |
| onClick={sendMessage} | |
| disabled={!message.trim() || isSending} | |
| className="bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-3 rounded-xl font-semibold hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" | |
| > | |
| {isSending ? ( | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| ) : ( | |
| <> | |
| <Send className="w-5 h-5" /> | |
| Send | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| {/* Quick actions */} | |
| <div className="flex gap-2 mt-3"> | |
| <button className="text-xs bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-lg transition-colors"> | |
| Mark as Resolved | |
| </button> | |
| <button className="text-xs bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-lg transition-colors"> | |
| Request More Info | |
| </button> | |
| <button className="text-xs bg-gray-100 hover:bg-gray-200 px-3 py-1 rounded-lg transition-colors"> | |
| Schedule Follow-up | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |