"use client"; import React, { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { MessageCircle, X, Send, Loader2, Sparkles, Trash2, } from "lucide-react"; interface Message { id: string; text: string; sender: "user" | "bot"; timestamp: Date; isTyping?: boolean; } export default function ChatWidget() { // Prevent hydration mismatch by only rendering after mount const [mounted, setMounted] = useState(false); const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const [isLoading, setIsLoading] = useState(false); const [conversationId, setConversationId] = useState("default"); useEffect(() => { setMounted(true); // Generate or retrieve conversation ID only on client try { const stored = localStorage.getItem("chatSessionId"); if (stored) { setConversationId(stored); return; } // Generate UUID with fallback for browsers without crypto.randomUUID let newId: string; if (typeof crypto !== "undefined" && crypto.randomUUID) { newId = crypto.randomUUID(); } else { // Fallback UUID generation newId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } localStorage.setItem("chatSessionId", newId); setConversationId(newId); } catch (error) { // localStorage might be disabled or full if (process.env.NODE_ENV === 'development') { console.warn('Failed to access localStorage for chat session:', error); } setConversationId(`session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`); } }, []); const messagesEndRef = useRef(null); const inputRef = useRef(null); // Auto-scroll to bottom when new messages arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Focus input when chat opens useEffect(() => { if (isOpen) { inputRef.current?.focus(); } }, [isOpen]); // Helper function to decode HTML entities const decodeHtmlEntities = (text: string): string => { if (!text || typeof text !== "string") return text; const textarea = document.createElement("textarea"); textarea.innerHTML = text; return textarea.value; }; // Load messages from localStorage useEffect(() => { if (typeof window !== "undefined") { try { const stored = localStorage.getItem("chatMessages"); if (stored) { try { const parsed = JSON.parse(stored); setMessages( parsed.map((m: Message) => ({ ...m, text: decodeHtmlEntities(m.text), // Decode HTML entities when loading timestamp: new Date(m.timestamp), })), ); } catch (e) { if (process.env.NODE_ENV === 'development') { console.error("Failed to parse chat history", e); } // Clear corrupted data try { localStorage.removeItem("chatMessages"); } catch { // Ignore cleanup errors } // Add welcome message setMessages([ { id: "welcome", text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀", sender: "bot", timestamp: new Date(), }, ]); } } else { // Add welcome message setMessages([ { id: "welcome", text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀", sender: "bot", timestamp: new Date(), }, ]); } } catch (error) { // localStorage might be disabled if (process.env.NODE_ENV === 'development') { console.warn("Failed to load chat history from localStorage:", error); } // Add welcome message anyway setMessages([ { id: "welcome", text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀", sender: "bot", timestamp: new Date(), }, ]); } } }, []); // Save messages to localStorage useEffect(() => { if (typeof window !== "undefined" && messages.length > 0) { try { localStorage.setItem("chatMessages", JSON.stringify(messages)); } catch (error) { // localStorage might be full or disabled if (process.env.NODE_ENV === 'development') { console.warn("Failed to save chat messages to localStorage:", error); } } } }, [messages]); const handleSend = async () => { if (!inputValue.trim() || isLoading) return; const userMessage: Message = { id: Date.now().toString(), text: inputValue.trim(), sender: "user", timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setInputValue(""); setIsLoading(true); // Get last 10 messages for context const history = messages.slice(-10).map((m) => ({ role: m.sender === "user" ? "user" : "assistant", content: m.text, })); try { const response = await fetch("/api/n8n/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: userMessage.text, conversationId, history, }), }); if (!response.ok) { const errorText = await response.text().catch(() => "Unknown error"); console.error("Chat API error:", { status: response.status, statusText: response.statusText, error: errorText, }); throw new Error( `Failed to get response: ${response.status} - ${errorText.substring(0, 100)}`, ); } const data = await response.json(); // Log response for debugging (only in development) if (process.env.NODE_ENV === "development") { console.log("Chat API response:", data); } // Decode HTML entities in the reply let replyText = data.reply || "Sorry, I couldn't process that. Please try again."; // Decode HTML entities client-side (double safety) replyText = decodeHtmlEntities(replyText); const botMessage: Message = { id: (Date.now() + 1).toString(), text: replyText, sender: "bot", timestamp: new Date(), }; setMessages((prev) => [...prev, botMessage]); } catch (error) { console.error("Chat error:", error); const errorMessage: Message = { id: (Date.now() + 1).toString(), text: "Sorry, I'm having trouble connecting right now. Please try again later or use the contact form below.", sender: "bot", timestamp: new Date(), }; setMessages((prev) => [...prev, errorMessage]); } finally { setIsLoading(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const clearChat = () => { // Reset session ID const newId = crypto.randomUUID(); setConversationId(newId); if (typeof window !== "undefined") { localStorage.setItem("chatSessionId", newId); localStorage.removeItem("chatMessages"); } setMessages([ { id: "welcome", text: "Conversation restarted! Ask me anything about Dennis! 🚀", sender: "bot", timestamp: new Date(), }, ]); }; // Don't render until mounted to prevent hydration mismatch if (!mounted) { return null; } return ( <> {/* Chat Button */} {!isOpen && ( setIsOpen(true)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { setIsOpen(true); } }} className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-[#292524] text-[#fdfcf8] p-3.5 rounded-full shadow-[0_8px_20px_rgba(41,37,36,0.25)] hover:bg-[#44403c] hover:scale-105 transition-all duration-300 group cursor-pointer border border-[#f3f1e7]/20 ring-1 ring-[#f3f1e7]/10" aria-label="Open chat" > {/* Tooltip */} Chat with AI )} {/* Chat Window */} {isOpen && ( {/* Header */}

Assistant

Powered by AI

{/* Messages */}
{messages.map((message) => (

{message.text}

{message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })}

))} {/* Typing Indicator */} {isLoading && (
)}
{/* Input */}
setInputValue(e.target.value)} onKeyPress={handleKeyPress} placeholder="Ask anything..." disabled={isLoading} className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-[#fdfcf8] disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-all shadow-inner" />
{/* Quick Actions */}
{[ "Skills 🛠️", "Projects 🚀", "Contact 📧", ].map((suggestion, index) => ( ))}
)} ); }