Ride-intelligent-assistant / project /src /ChatInterface.tsx
CHRISDANIEL145
Deploy Ride Intelligent Assistant (Clean Build)
c95a333
Raw
History Blame Contribute Delete
23.7 kB
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);
// Panel Visibility State
const [showDashboard, setShowDashboard] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const [showMap, setShowMap] = useState(false);
// Panel Position State (x, y)
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];
// Drag Sensors
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;