ORA / frontend /components /ChatWidget.tsx
Abdalkaderdev's picture
Initial ORA deployment
5e0532d
'use client';
import { useState, useRef, useEffect } from 'react';
import { X, Bot, Sparkles, Send, Shield, BookOpen, Heart, Brain, ChevronDown, Eye, EyeOff } from 'lucide-react';
import ThinkingIndicator from './chat/ThinkingIndicator';
import MarkdownMessage from './chat/MarkdownMessage';
interface Message {
role: 'user' | 'assistant';
content: string;
agent?: string;
trace?: TraceStep[];
timestamp?: Date;
}
interface TraceStep {
agent: string;
action: string;
result?: string;
}
const agentIcons: Record<string, typeof Shield> = {
Gatekeeper: Shield,
Theologian: BookOpen,
Healer: Heart,
Orchestrator: Brain,
System: Sparkles,
};
const agentColors: Record<string, { bg: string; border: string; text: string }> = {
Gatekeeper: { bg: 'bg-purple-500/20', border: 'border-purple-500/30', text: 'text-purple-400' },
Theologian: { bg: 'bg-blue-500/20', border: 'border-blue-500/30', text: 'text-blue-400' },
Healer: { bg: 'bg-rose-500/20', border: 'border-rose-500/30', text: 'text-rose-400' },
Orchestrator: { bg: 'bg-amber-500/20', border: 'border-amber-500/30', text: 'text-amber-400' },
System: { bg: 'bg-neutral-500/20', border: 'border-neutral-500/30', text: 'text-neutral-400' },
};
export default function ChatWidget() {
const [chatOpen, setChatOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content: "Hello, I'm **ORA**, your AI spiritual companion. I'm here to help you explore Scripture, reflect on your faith, and grow in wisdom. How may I serve you today?",
agent: 'Gatekeeper',
timestamp: new Date(),
}
]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [activeAgent, setActiveAgent] = useState<string>('Gatekeeper');
const [showTrace, setShowTrace] = useState<Record<number, boolean>>({});
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
const handleSend = async () => {
if (!input.trim()) return;
const userMsg: Message = {
role: 'user',
content: input,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMsg]);
setInput('');
setIsTyping(true);
setActiveAgent('Gatekeeper'); // Start with Gatekeeper
try {
const response = await fetch('http://localhost:6000/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: input })
});
const data = await response.json();
if (data.success) {
// Simulate agent handoff animation
if (data.response.agent !== 'Gatekeeper') {
setActiveAgent(data.response.agent);
await new Promise(resolve => setTimeout(resolve, 500));
}
const assistantMsg: Message = {
role: 'assistant',
content: data.response.content,
agent: data.response.agent,
trace: data.response.trace || [],
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMsg]);
}
} catch (error) {
console.error('Chat failed:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: "I apologize, but I'm having trouble connecting to my reasoning core. Please ensure the ORA backend is running on port 6000.",
agent: 'System',
timestamp: new Date(),
}]);
} finally {
setIsTyping(false);
}
};
const toggleTrace = (index: number) => {
setShowTrace(prev => ({ ...prev, [index]: !prev[index] }));
};
const AgentIcon = ({ agent }: { agent: string }) => {
const Icon = agentIcons[agent] || Sparkles;
const colors = agentColors[agent] || agentColors.System;
return (
<div className={`w-7 h-7 rounded-full ${colors.bg} flex items-center justify-center border ${colors.border}`}>
<Icon className={`w-3.5 h-3.5 ${colors.text}`} />
</div>
);
};
return (
<div className="fixed bottom-6 right-6 z-50 hidden md:block">
<div className="relative">
{/* Chat Button */}
<button
onClick={() => setChatOpen(!chatOpen)}
className="relative w-14 h-14 bg-gradient-to-br from-purple-600 to-purple-800 hover:from-purple-500 hover:to-purple-700 text-white rounded-full flex items-center justify-center shadow-[0_4px_30px_rgba(168,85,247,0.4)] hover:shadow-[0_4px_40px_rgba(168,85,247,0.6)] hover:scale-105 active:scale-95 transition-all duration-300 z-50"
>
{chatOpen ? (
<X className="w-6 h-6" />
) : (
<div className="relative">
<Sparkles className="w-6 h-6 animate-pulse-fast" />
</div>
)}
{!chatOpen && (
<span className="absolute top-0 right-0 w-3.5 h-3.5 bg-emerald-500 border-2 border-[#0a0a0a] rounded-full">
<span className="absolute inset-0 rounded-full bg-emerald-400 animate-ping" />
</span>
)}
</button>
{/* Chat Box */}
<div
className={`absolute bottom-[calc(100%+16px)] right-0 w-[420px] bg-[#0a0a0a]/98 backdrop-blur-xl border border-purple-500/20 rounded-2xl shadow-2xl shadow-purple-500/10 transition-all duration-300 overflow-hidden flex flex-col ${
chatOpen
? 'visible opacity-100 translate-y-0 scale-100'
: 'invisible opacity-0 translate-y-4 scale-95'
}`}
style={{ maxHeight: 'calc(100vh - 150px)' }}
>
{/* Header */}
<div className="bg-gradient-to-r from-purple-900/60 to-purple-800/40 p-4 border-b border-white/5 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500/30 to-purple-700/30 flex items-center justify-center border border-purple-500/40 animate-orb-glow-purple">
<Sparkles className="w-5 h-5 text-purple-300" />
</div>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-500 border-2 border-[#0a0a0a] rounded-full shadow-[0_0_8px_rgba(16,185,129,0.6)]" />
</div>
<div>
<span className="block text-white text-sm font-semibold tracking-wide">ORA</span>
<span className="block text-[10px] text-emerald-400 font-medium">
Sovereign AI Companion
</span>
</div>
</div>
<button
onClick={() => setChatOpen(false)}
className="p-1.5 rounded-full hover:bg-white/10 text-neutral-400 hover:text-white transition-all"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Messages */}
<div className="flex-1 p-4 space-y-4 min-h-[300px] max-h-[450px] overflow-y-auto bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-purple-500/5 via-transparent to-transparent scrollbar-thin scrollbar-thumb-purple-500/20 scrollbar-track-transparent">
{messages.map((m, i) => (
<div
key={i}
className={`flex gap-3 items-start animate-message-in ${
m.role === 'user' ? 'flex-row-reverse' : ''
}`}
>
{/* Avatar */}
<div className="shrink-0">
{m.role === 'user' ? (
<div className="w-7 h-7 rounded-full bg-amber-500/20 flex items-center justify-center border border-amber-500/30">
<Bot className="w-3.5 h-3.5 text-amber-400" />
</div>
) : (
<AgentIcon agent={m.agent || 'System'} />
)}
</div>
{/* Message Content */}
<div className="flex flex-col gap-1 max-w-[85%]">
{/* Agent Badge */}
{m.agent && m.role === 'assistant' && (
<div className="flex items-center gap-2">
<span className={`text-[9px] font-mono uppercase tracking-wider ${agentColors[m.agent]?.text || 'text-neutral-400'}`}>
{m.agent}
</span>
{m.trace && m.trace.length > 0 && (
<button
onClick={() => toggleTrace(i)}
className="flex items-center gap-1 text-[9px] text-neutral-500 hover:text-purple-400 transition-colors"
>
{showTrace[i] ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
{showTrace[i] ? 'Hide' : 'Show'} Reasoning
</button>
)}
</div>
)}
{/* Trace Steps */}
{showTrace[i] && m.trace && m.trace.length > 0 && (
<div className="animate-trace-expand mb-2 p-3 rounded-xl bg-white/[0.02] border border-white/5">
<div className="text-[9px] text-neutral-500 uppercase tracking-wider mb-2">
Reasoning Trace
</div>
<div className="space-y-2">
{m.trace.map((step, j) => (
<div key={j} className="flex items-start gap-2 text-[10px]">
<div className={`w-4 h-4 rounded shrink-0 flex items-center justify-center ${agentColors[step.agent]?.bg || 'bg-neutral-500/20'}`}>
<span className={`text-[8px] font-bold ${agentColors[step.agent]?.text || 'text-neutral-400'}`}>
{j + 1}
</span>
</div>
<div>
<span className={`font-medium ${agentColors[step.agent]?.text || 'text-neutral-400'}`}>
{step.agent}:
</span>
<span className="text-neutral-400 ml-1">{step.action}</span>
{step.result && (
<div className="text-neutral-500 mt-0.5 italic">{step.result}</div>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Message Bubble */}
<div
className={`rounded-2xl p-3.5 text-sm leading-relaxed ${
m.role === 'user'
? 'bg-gradient-to-br from-amber-500/15 to-amber-600/10 border border-amber-500/20 rounded-tr-sm'
: `bg-gradient-to-br from-purple-500/10 to-purple-600/5 border ${agentColors[m.agent || 'System']?.border || 'border-purple-500/20'} rounded-tl-sm`
}`}
>
<MarkdownMessage
content={m.content}
variant={m.role}
/>
</div>
{/* Timestamp */}
{m.timestamp && (
<span className="text-[9px] text-neutral-600 px-1">
{m.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
)}
</div>
</div>
))}
{/* Thinking Indicator */}
{isTyping && (
<ThinkingIndicator activeAgent={activeAgent} stage="thinking" />
)}
<div ref={messagesEndRef} />
</div>
{/* Quick Actions */}
<div className="px-4 py-2 border-t border-white/5 bg-black/20">
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-none">
{['Pray with me', 'Explain a verse', 'Daily reflection'].map((action) => (
<button
key={action}
onClick={() => setInput(action)}
className="shrink-0 px-3 py-1.5 text-[10px] font-medium text-purple-300 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/20 rounded-full transition-all hover:scale-105"
>
{action}
</button>
))}
</div>
</div>
{/* Input */}
<div className="p-3 border-t border-white/5 bg-black/30">
<div className="flex items-center gap-2 bg-white/5 border border-white/10 rounded-full px-4 py-2.5 focus-within:border-purple-500/40 focus-within:bg-white/[0.07] transition-all">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
placeholder="Ask ORA anything..."
className="bg-transparent border-none focus:outline-none text-sm text-white w-full placeholder-neutral-500"
disabled={isTyping}
/>
<button
onClick={handleSend}
disabled={isTyping || !input.trim()}
className={`p-2 rounded-full transition-all ${
isTyping || !input.trim()
? 'text-neutral-600 cursor-not-allowed'
: 'text-purple-400 hover:text-purple-300 hover:bg-purple-500/20'
}`}
>
<Send className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
);
}