| import React, { useState, useRef, useEffect } from "react"; |
| import axios from "axios"; |
| import { MessageCircle, Send, Cpu, User, LogOut, Activity, Mic, MicOff, Database, Globe as GlobeIcon, Radio } from "lucide-react"; |
| import { motion, AnimatePresence } from "framer-motion"; |
| import { clsx, type ClassValue } from "clsx"; |
| import { twMerge } from "tailwind-merge"; |
| import { DndContext, useSensor, useSensors, PointerSensor, DragEndEvent } from '@dnd-kit/core'; |
|
|
| import SentimentDashboard from "./SentimentDashboard"; |
| import RideMemoryBank from "./RideMemoryBank"; |
| import GlobalNetworkMap from "./GlobalNetworkMap"; |
| import QuantumInsight from "./QuantumInsight"; |
| import ReputationSystem from "./ReputationSystem"; |
| import LiveFeed from "./LiveFeed"; |
| import AudioVisualizer from "./AudioVisualizer"; |
| import ThemeSelector from "./ThemeSelector"; |
| import DraggablePanel from "./DraggablePanel"; |
|
|
| function cn(...inputs: ClassValue[]) { |
| return twMerge(clsx(inputs)); |
| } |
|
|
| const NeonButton = ({ children, onClick, className, disabled }: any) => ( |
| <motion.button |
| whileHover={!disabled ? { scale: 1.02, boxShadow: "0 0 20px rgba(0, 243, 255, 0.4)" } : {}} |
| whileTap={!disabled ? { scale: 0.98 } : {}} |
| onClick={onClick} |
| disabled={disabled} |
| className={cn( |
| "relative group w-full bg-gradient-to-r from-neon-blue to-blue-600 text-black font-bold py-3 px-6 rounded-xl", |
| "hover:from-neon-blue hover:to-neon-purple transition-all duration-300", |
| "flex items-center justify-center gap-2 shadow-[0_0_10px_rgba(0,243,255,0.2)]", |
| "disabled:opacity-50 disabled:cursor-not-allowed", |
| className |
| )} |
| > |
| {children} |
| </motion.button> |
| ); |
|
|
| interface Message { |
| id: number; |
| text: string; |
| sender: "user" | "ai"; |
| timestamp: string; |
| } |
|
|
| interface ChatInterfaceProps { |
| onLogout: () => void; |
| userId: string; |
| } |
|
|
| const ChatInterface: React.FC<ChatInterfaceProps> = ({ onLogout, userId }) => { |
| const [messages, setMessages] = useState<Message[]>([ |
| { |
| id: 1, |
| text: "Hello! I'm your Ride Intelligence Assistant. How can I help verify your trip or analyze feedback today?", |
| sender: "ai", |
| timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), |
| }, |
| ]); |
| const [input, setInput] = useState(""); |
| const [isLoading, setIsLoading] = useState(false); |
|
|
| |
| const [showDashboard, setShowDashboard] = useState(false); |
| const [showHistory, setShowHistory] = useState(false); |
| const [showMap, setShowMap] = useState(false); |
|
|
| |
| const [dashboardPos, setDashboardPos] = useState({ x: 20, y: 100 }); |
| const [historyPos, setHistoryPos] = useState({ x: 50, y: 150 }); |
| const [mapPos, setMapPos] = useState({ x: 80, y: 200 }); |
|
|
| const [showFeed, setShowFeed] = useState(true); |
| const [isListening, setIsListening] = useState(false); |
| const [persona, setPersona] = useState<"Guardian" | "Crimson" | "Zen">("Guardian"); |
| const [actionCount, setActionCount] = useState(0); |
| const [customTheme, setCustomTheme] = useState<any>(null); |
|
|
| const messagesEndRef = useRef<HTMLDivElement>(null); |
| const recognitionRef = useRef<any>(null); |
|
|
| const themes = { |
| Guardian: { color: "text-neon-blue", border: "border-neon-blue", bg: "bg-neon-blue", grad: "from-neon-blue" }, |
| Crimson: { color: "text-red-500", border: "border-red-500", bg: "bg-red-500", grad: "from-red-600" }, |
| Zen: { color: "text-green-500", border: "border-green-500", bg: "bg-green-500", grad: "from-green-500" }, |
| }; |
|
|
| const activeTheme = customTheme || themes[persona]; |
|
|
| |
| const sensors = useSensors( |
| useSensor(PointerSensor, { |
| activationConstraint: { |
| distance: 8, |
| }, |
| }) |
| ); |
|
|
| const handleDragEnd = (event: DragEndEvent) => { |
| const { active, delta } = event; |
| const id = active.id as string; |
|
|
| if (id === 'dashboard-panel') { |
| setDashboardPos(prev => ({ x: prev.x + delta.x, y: prev.y + delta.y })); |
| } else if (id === 'history-panel') { |
| setHistoryPos(prev => ({ x: prev.x + delta.x, y: prev.y + delta.y })); |
| } else if (id === 'map-panel') { |
| setMapPos(prev => ({ x: prev.x + delta.x, y: prev.y + delta.y })); |
| } |
| }; |
|
|
| const handleThemeChange = (colorKey: string) => { |
| const themeMap: any = { |
| "neon-blue": themes.Guardian, |
| "neon-purple": { color: "text-purple-500", border: "border-purple-500", bg: "bg-purple-500", grad: "from-purple-600" }, |
| "neon-green": themes.Zen, |
| "amber-500": { color: "text-yellow-500", border: "border-yellow-500", bg: "bg-yellow-500", grad: "from-yellow-600" } |
| }; |
| if (themeMap[colorKey]) setCustomTheme(themeMap[colorKey]); |
| }; |
|
|
| const scrollToBottom = () => { |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }; |
|
|
| useEffect(() => { |
| scrollToBottom(); |
| }, [messages]); |
|
|
| useEffect(() => { |
| if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { |
| const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; |
| recognitionRef.current = new SpeechRecognition(); |
| recognitionRef.current.continuous = false; |
| recognitionRef.current.interimResults = false; |
|
|
| recognitionRef.current.onresult = (event: any) => { |
| const transcript = event.results[0][0].transcript; |
| setInput(transcript); |
| setIsListening(false); |
| }; |
|
|
| recognitionRef.current.onerror = (event: any) => { |
| console.error("Speech error", event); |
| setIsListening(false); |
| }; |
|
|
| recognitionRef.current.onend = () => { |
| setIsListening(false); |
| }; |
| } |
| }, []); |
|
|
| const toggleListening = () => { |
| if (isListening) { |
| recognitionRef.current?.stop(); |
| } else { |
| recognitionRef.current?.start(); |
| setIsListening(true); |
| } |
| }; |
|
|
| const speak = (text: string) => { |
| if ('speechSynthesis' in window) { |
| const utterance = new SpeechSynthesisUtterance(text); |
| utterance.pitch = persona === "Crimson" ? 0.8 : persona === "Zen" ? 0.9 : 1; |
| utterance.rate = persona === "Crimson" ? 1.2 : persona === "Zen" ? 0.8 : 1; |
| window.speechSynthesis.speak(utterance); |
| } |
| }; |
|
|
| const handleSendMessage = async (e?: React.FormEvent) => { |
| e?.preventDefault(); |
| if (!input.trim() || isLoading) return; |
|
|
| const userMessage: Message = { |
| id: Date.now(), |
| text: input, |
| sender: "user", |
| timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), |
| }; |
|
|
| setMessages((prev) => [...prev, userMessage]); |
| setInput(""); |
| setIsLoading(true); |
| setActionCount(prev => prev + 1); |
|
|
| try { |
| const response = await axios.post("/api/ai/chat", { |
| message: userMessage.text, |
| }); |
|
|
| const aiResponseText = response.data.response; |
| const aiMessage: Message = { |
| id: Date.now() + 1, |
| text: aiResponseText, |
| sender: "ai", |
| timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), |
| }; |
|
|
| setMessages((prev) => [...prev, aiMessage]); |
| speak(aiResponseText); |
|
|
| axios.post("/api/auth/history", { |
| userId, |
| feedback: userMessage.text, |
| aiResponse: aiResponseText, |
| sentiment: "Neutral" |
| }).catch(e => console.error("Failed to save history", e)); |
|
|
| } catch (error) { |
| console.error("Chat error:", error); |
| const errorMessage: Message = { |
| id: Date.now() + 1, |
| text: "Network link unstable. Please retry transmission.", |
| sender: "ai", |
| timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), |
| }; |
| setMessages((prev) => [...prev, errorMessage]); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| return ( |
| <DndContext sensors={sensors} onDragEnd={handleDragEnd}> |
| <div className="flex flex-col h-screen max-h-screen relative overflow-hidden bg-deep-bg text-white font-sans selection:bg-neon-blue/30"> |
| |
| {/* Live Feed Overlay */} |
| <AnimatePresence> |
| {showFeed && <LiveFeed />} |
| </AnimatePresence> |
| |
| {/* Background Ambience */} |
| <div className="absolute inset-0 pointer-events-none transition-colors duration-1000"> |
| <div className="absolute top-0 left-0 w-full h-32 bg-gradient-to-b from-deep-bg to-transparent z-10" /> |
| <div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-t from-deep-bg to-transparent z-10" /> |
| <div className={`absolute top-[-20%] right-[-10%] w-[600px] h-[600px] rounded-full blur-[120px] opacity-20 ${activeTheme.bg}`} /> |
| <div className={`absolute bottom-[-10%] left-[-10%] w-[500px] h-[500px] rounded-full blur-[100px] opacity-10 ${activeTheme.bg}`} /> |
| </div> |
| |
| {/* Header */} |
| <header className="relative z-20 flex items-center justify-between px-6 py-4 border-b border-deep-border bg-deep-bg/80 backdrop-blur-md"> |
| <div className="flex items-center gap-6"> |
| <div className="flex items-center gap-3"> |
| <div className="relative hidden md:block"> |
| <div className={`absolute inset-0 blur-sm opacity-50 animate-pulse ${activeTheme.bg}`} /> |
| <div className={`relative bg-deep-bg border ${activeTheme.border} p-2 rounded-lg`}> |
| <Cpu className={`w-6 h-6 ${activeTheme.color}`} /> |
| </div> |
| </div> |
| <div> |
| <h1 className="text-xl font-bold tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-400"> |
| RIDE<span className={activeTheme.color}>AI</span> |
| </h1> |
| <div className="flex items-center gap-2"> |
| <span className={`w-1.5 h-1.5 rounded-full animate-pulse ${activeTheme.bg}`} /> |
| <span className={`text-xs tracking-widest uppercase ${activeTheme.color}`}>System Online</span> |
| </div> |
| </div> |
| </div> |
| <div className="hidden lg:flex items-center gap-4"> |
| <ReputationSystem actionCount={actionCount} /> |
| <div className="h-6 w-px bg-gray-700" /> |
| <ThemeSelector currentTheme={activeTheme.bg} onThemeChange={handleThemeChange} /> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-2"> |
| <button |
| onClick={() => setShowMap(!showMap)} |
| className={cn("p-2 rounded-lg transition-colors border group mr-2", showMap ? `bg-white/10 ${activeTheme.border} ${activeTheme.color}` : "border-transparent hover:bg-white/5 text-gray-400 hover:text-white")} |
| title="Holographic Map" |
| > |
| <GlobeIcon className="w-5 h-5 group-hover:animate-spin-slow" /> |
| </button> |
| <button |
| onClick={() => setShowDashboard(!showDashboard)} |
| className={cn("p-2 rounded-lg transition-colors border group mr-2", showDashboard ? `bg-white/10 ${activeTheme.border} ${activeTheme.color}` : "border-transparent hover:bg-white/5 text-gray-400 hover:text-white")} |
| title="Toggle Analytics" |
| > |
| <Activity className="w-5 h-5 group-hover:animate-pulse" /> |
| </button> |
| <button |
| onClick={() => setShowHistory(!showHistory)} |
| className={cn("p-2 rounded-lg transition-colors border group mr-2", showHistory ? `bg-white/10 ${activeTheme.border} ${activeTheme.color}` : "border-transparent hover:bg-white/5 text-gray-400 hover:text-white")} |
| title="Neural Memory Bank" |
| > |
| <Database className="w-5 h-5 group-hover:animate-pulse" /> |
| </button> |
| <button |
| onClick={() => setShowFeed(!showFeed)} |
| className={cn("p-2 rounded-lg transition-colors border group mr-2", showFeed ? `bg-white/10 ${activeTheme.border} ${activeTheme.color}` : "border-transparent hover:bg-white/5 text-gray-400 hover:text-white")} |
| title="Toggle Network Feed" |
| > |
| <Radio className="w-5 h-5 group-hover:animate-pulse" /> |
| </button> |
| <button |
| onClick={onLogout} |
| className="p-2 rounded-lg hover:bg-white/5 transition-colors text-gray-400 hover:text-white border border-transparent hover:border-white/10 group" |
| title="Disconnect" |
| > |
| <LogOut className="w-5 h-5 group-hover:text-red-400 transition-colors" /> |
| </button> |
| </div> |
| </header> |
| |
| <div className="flex-1 flex overflow-hidden relative"> |
| {/* Draggable Panels Area */} |
| <div className="absolute inset-0 z-30 pointer-events-none"> |
| <AnimatePresence> |
| {showDashboard && ( |
| <DraggablePanel id="dashboard-panel" style={{ top: dashboardPos.y, left: dashboardPos.x }} className="pointer-events-auto"> |
| <SentimentDashboard /> |
| </DraggablePanel> |
| )} |
| {showHistory && ( |
| <DraggablePanel id="history-panel" style={{ top: historyPos.y, left: historyPos.x }} className="pointer-events-auto"> |
| <RideMemoryBank userId={userId} /> |
| </DraggablePanel> |
| )} |
| {showMap && ( |
| <DraggablePanel id="map-panel" style={{ top: mapPos.y, left: mapPos.x }} className="pointer-events-auto"> |
| <GlobalNetworkMap /> |
| </DraggablePanel> |
| )} |
| </AnimatePresence> |
| </div> |
| |
| {/* Messages Area */} |
| <div className="flex-1 flex flex-col h-full overflow-hidden relative z-10"> |
| <div className="flex-1 overflow-y-auto p-4 md:p-6 md:pl-72 space-y-6 scrollbar-thin scrollbar-thumb-deep-border scrollbar-track-transparent pb-32"> |
| <AnimatePresence initial={false}> |
| {messages.map((msg) => ( |
| <motion.div |
| key={msg.id} |
| initial={{ opacity: 0, y: 20, scale: 0.95 }} |
| animate={{ opacity: 1, y: 0, scale: 1 }} |
| transition={{ duration: 0.3 }} |
| className={cn( |
| "flex w-full", |
| msg.sender === "user" ? "justify-end" : "justify-start" |
| )} |
| > |
| <div |
| className={cn( |
| "max-w-[85%] md:max-w-[70%] rounded-2xl p-4 md:p-5 relative shadow-lg", |
| msg.sender === "user" |
| ? `bg-gradient-to-br ${activeTheme.grad}/20 to-blue-600/20 border ${activeTheme.border}/30 text-white rounded-br-sm` |
| : "bg-deep-card border border-deep-border text-gray-100 rounded-bl-sm" |
| )} |
| > |
| <div className={cn( |
| "absolute -top-3 w-8 h-8 rounded-full border flex items-center justify-center backdrop-blur-xl shadow-md", |
| msg.sender === "user" |
| ? `bg-deep-bg ${activeTheme.border}/50 -right-2 bg-gradient-to-br ${activeTheme.grad}/20 to-transparent` |
| : `bg-deep-bg ${activeTheme.border}/50 -left-2 bg-gradient-to-br ${activeTheme.grad}/20 to-transparent` |
| )}> |
| {msg.sender === "user" ? <User size={14} className={activeTheme.color} /> : <MessageCircle size={14} className={activeTheme.color} />} |
| </div> |
| |
| <p className="text-sm md:text-base leading-relaxed tracking-wide">{msg.text}</p> |
| <div className={cn( |
| "text-[10px] mt-2 opacity-50 uppercase tracking-widest flex items-center gap-1", |
| msg.sender === "user" ? `justify-end ${activeTheme.color}` : activeTheme.color |
| )}> |
| {msg.timestamp} |
| </div> |
| </div> |
| </motion.div> |
| ))} |
| </AnimatePresence> |
| |
| {isLoading && ( |
| <motion.div |
| initial={{ opacity: 0 }} |
| animate={{ opacity: 1 }} |
| className="flex justify-start" |
| > |
| <div className="bg-deep-card border border-deep-border rounded-2xl rounded-bl-sm p-4 flex items-center gap-1"> |
| <span className={`w-2 h-2 ${activeTheme.bg} rounded-full animate-bounce [animation-delay:-0.3s]`} /> |
| <span className={`w-2 h-2 ${activeTheme.bg} rounded-full animate-bounce [animation-delay:-0.15s]`} /> |
| <span className={`w-2 h-2 ${activeTheme.bg} rounded-full animate-bounce`} /> |
| </div> |
| </motion.div> |
| )} |
| <div ref={messagesEndRef} /> |
| </div> |
| |
| {/* Input Area */} |
| <div className="p-4 md:p-6 bg-deep-bg/80 backdrop-blur-xl border-t border-deep-border z-20"> |
| <form |
| onSubmit={handleSendMessage} |
| className="max-w-4xl mx-auto relative flex items-center gap-3" |
| > |
| <QuantumInsight text={input} /> |
| |
| <button |
| type="button" |
| onClick={toggleListening} |
| className={cn( |
| "p-0 rounded-xl transition-all duration-300 border flex items-center justify-center overflow-hidden", |
| isListening |
| ? "w-48 bg-black/50 border-red-500 shadow-[0_0_15px_rgba(239,68,68,0.4)]" |
| : `w-14 h-14 bg-deep-card border-deep-border text-gray-400 hover:${activeTheme.color} hover:${activeTheme.border}` |
| )} |
| > |
| {isListening ? ( |
| <AudioVisualizer isListening={isListening} color={activeTheme.color} /> |
| ) : ( |
| <Mic className="w-5 h-5" /> |
| )} |
| </button> |
| |
| <div className="flex-1 relative group"> |
| <input |
| type="text" |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| placeholder={isListening ? "Listening..." : "Enter ride details for analysis..."} |
| className={`w-full bg-deep-card border border-deep-border text-white placeholder-gray-500 rounded-xl py-4 pl-6 pr-12 focus:outline-none focus:${activeTheme.border}/50 focus:ring-1 focus:ring-neon-blue/30 transition-all duration-300 shadow-inner`} |
| disabled={isLoading} |
| /> |
| <div className={`absolute top-0 left-0 w-2 h-2 border-t border-l ${activeTheme.border}/50 rounded-tl-lg opacity-0 group-hover:opacity-100 transition-opacity`} /> |
| <div className={`absolute bottom-0 right-0 w-2 h-2 border-b border-r ${activeTheme.border}/50 rounded-br-lg opacity-0 group-hover:opacity-100 transition-opacity`} /> |
| </div> |
| |
| <NeonButton |
| type="submit" |
| className={`w-auto px-6 py-4 rounded-xl !m-0 aspect-square flex items-center justify-center bg-gradient-to-r ${activeTheme.grad} to-blue-600 hover:${activeTheme.grad} hover:brightness-110`} |
| disabled={!input.trim() || isLoading} |
| > |
| <Send className="w-5 h-5" /> |
| </NeonButton> |
| </form> |
| </div> |
| </div> |
| </div> |
| </div> |
| </DndContext> |
| ); |
| }; |
|
|
| export default ChatInterface; |
|
|