Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { | |
| Send, User, Bot, Sparkles, Loader2, Volume2, VolumeX, ChevronRight | |
| } from 'lucide-react'; | |
| import { speakText, getFinancialAdviceStream } from '../services/geminiService'; | |
| import { apiClient } from '../services/api'; | |
| interface Message { | |
| id: string; | |
| role: 'user' | 'assistant'; | |
| content: string; | |
| timestamp: string; | |
| isStreaming?: boolean; | |
| } | |
| const Advisor: React.FC = () => { | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [input, setInput] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [voiceEnabled, setVoiceEnabled] = useState(true); | |
| const scrollRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| const initMemory = async () => { | |
| try { | |
| const history = await apiClient.chat.getHistory(); | |
| if (history && history.length > 0) { | |
| setMessages(history.map((h: any) => ({ ...h, id: h.id.toString() }))); | |
| } else { | |
| setMessages([{ id: '1', role: 'assistant', content: "Neural Core online. Standing by for institutional instructions.", timestamp: new Date().toISOString() }]); | |
| } | |
| } catch (err) { | |
| setMessages([{ id: '1', role: 'assistant', content: "Neural Core online. Safe mode registry active.", timestamp: new Date().toISOString() }]); | |
| } | |
| }; | |
| initMemory(); | |
| }, []); | |
| useEffect(() => { | |
| if (scrollRef.current) { | |
| scrollRef.current.scrollTop = scrollRef.current.scrollHeight; | |
| } | |
| }, [messages]); | |
| const handleSend = async (forcedInput?: string) => { | |
| const query = (forcedInput || input).trim(); | |
| if (!query || loading) return; | |
| const userMsgId = Date.now().toString(); | |
| setMessages(prev => [...prev, { | |
| id: userMsgId, | |
| role: 'user', | |
| content: query, | |
| timestamp: new Date().toISOString() | |
| }]); | |
| setInput(''); | |
| setLoading(true); | |
| const assistantMsgId = (Date.now() + 1).toString(); | |
| setMessages(prev => [...prev, { | |
| id: assistantMsgId, | |
| role: 'assistant', | |
| content: '', | |
| timestamp: new Date().toISOString(), | |
| isStreaming: true | |
| }]); | |
| try { | |
| const context = { system: "LUMINA_ADVISORY_NODE", mode: "Institutional_Treasury" }; | |
| const response = await getFinancialAdviceStream(query, context); | |
| const finalContent = response?.[0]?.text || "Signal interrupted. No data received from Gemini Node."; | |
| setMessages(prev => prev.map(m => | |
| m.id === assistantMsgId ? { ...m, content: finalContent, isStreaming: false } : m | |
| )); | |
| await apiClient.chat.saveMessage('user', query); | |
| await apiClient.chat.saveMessage('assistant', finalContent); | |
| if (voiceEnabled) speakText(finalContent); | |
| } catch (error) { | |
| console.error("Advisor Link Failure:", error); | |
| setMessages(prev => prev.map(m => | |
| m.id === assistantMsgId ? { | |
| ...m, | |
| content: "Neural link lost. Ensure process.env.API_KEY is properly configured and model quota is available.", | |
| isStreaming: false | |
| } : m | |
| )); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="flex h-[calc(100vh-140px)] gap-10 animate-in fade-in duration-700"> | |
| <div className="hidden lg:flex flex-col w-96 space-y-8"> | |
| <div className="bg-zinc-950 border border-zinc-800 rounded-[3rem] p-10 flex flex-col h-full relative overflow-hidden shadow-2xl"> | |
| <div className="flex items-center space-x-3 mb-10 relative z-10"> | |
| <div className="p-3 bg-blue-600/10 text-blue-500 rounded-2xl border border-blue-500/20"> | |
| <Sparkles size={24} /> | |
| </div> | |
| <div> | |
| <h3 className="text-white font-black italic tracking-tighter uppercase text-xl">Nexus_Core</h3> | |
| <p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest">Protocol: Advisory</p> | |
| </div> | |
| </div> | |
| <div className="space-y-4 flex-1 overflow-y-auto pr-3 custom-scrollbar relative z-10"> | |
| <p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-2 mb-4">Command Presets</p> | |
| {["Analyze Liquidity Drift", "Stress-test Commercial Portfolio", "Model Inflation Spike impact"].map((text, i) => ( | |
| <button | |
| key={i} | |
| onClick={() => handleSend(text)} | |
| disabled={loading} | |
| className="w-full text-left p-6 rounded-2xl bg-black border border-zinc-900 hover:border-blue-500/50 hover:bg-blue-500/5 transition-all group flex items-start justify-between disabled:opacity-30" | |
| > | |
| <span className="text-[10px] font-black uppercase tracking-widest text-zinc-500 group-hover:text-blue-400 leading-relaxed">{text}</span> | |
| <ChevronRight size={16} className="text-zinc-800 group-hover:text-blue-500 transition-colors" /> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-1 flex flex-col bg-zinc-950 border border-zinc-900 rounded-[3rem] overflow-hidden shadow-2xl relative"> | |
| <div className="h-20 border-b border-zinc-900 px-10 flex items-center justify-between bg-black/40 backdrop-blur-xl z-20"> | |
| <div className="flex items-center space-x-6"> | |
| <div className={`w-3 h-3 rounded-full shadow-[0_0_15px_#10b981] ${loading ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500'}`}></div> | |
| <span className="font-black text-white text-sm uppercase tracking-[0.4em] italic">Quantum_Advisory_Node</span> | |
| </div> | |
| <button | |
| onClick={() => setVoiceEnabled(!voiceEnabled)} | |
| className={`p-4 rounded-2xl border transition-all ${voiceEnabled ? 'bg-blue-600/10 border-blue-500/20 text-blue-500' : 'bg-zinc-900 border-zinc-800 text-zinc-600'}`} | |
| > | |
| {voiceEnabled ? <Volume2 size={20} /> : <VolumeX size={20} />} | |
| </button> | |
| </div> | |
| <div ref={scrollRef} className="flex-1 overflow-y-auto p-12 space-y-12 custom-scrollbar"> | |
| {messages.map((m) => ( | |
| <div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4`}> | |
| <div className={`flex max-w-[85%] ${m.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-8`}> | |
| <div className={`w-14 h-14 rounded-2xl flex-shrink-0 flex items-center justify-center border transition-all ${m.role === 'user' ? 'bg-zinc-900 border-zinc-800 text-zinc-500' : 'bg-blue-600 border-blue-400 text-white shadow-2xl shadow-blue-900/40'}`}> | |
| {m.role === 'user' ? <User size={24} /> : <Bot size={28} />} | |
| </div> | |
| <div className={`p-8 rounded-[2.5rem] ${m.role === 'user' ? 'bg-zinc-900 text-zinc-100 rounded-tr-none border border-zinc-800' : 'bg-black border border-zinc-800 text-zinc-300 rounded-tl-none shadow-2xl'}`}> | |
| <div className="text-base font-medium leading-relaxed tracking-tight whitespace-pre-wrap"> | |
| {m.content || (m.isStreaming ? ( | |
| <div className="flex items-center gap-3"> | |
| <Loader2 className="animate-spin text-blue-500" size={18} /> | |
| <span className="text-[10px] font-black uppercase tracking-[0.3em] animate-pulse">Computing Alpha...</span> | |
| </div> | |
| ) : "Handshaking...")} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="p-10 bg-black/20 border-t border-zinc-900 backdrop-blur-2xl z-20"> | |
| <form | |
| onSubmit={(e) => { e.preventDefault(); handleSend(); }} | |
| className="relative group max-w-5xl mx-auto flex gap-4" | |
| > | |
| <input | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder="Input treasury instructions for the Quantum Core..." | |
| className="flex-1 bg-black border border-zinc-800 focus:border-blue-500 rounded-[2rem] py-7 px-10 text-white text-base outline-none transition-all placeholder:text-zinc-800 font-bold" | |
| /> | |
| <button | |
| type="submit" | |
| disabled={loading || !input.trim()} | |
| className="px-8 bg-blue-600 hover:bg-blue-500 text-white rounded-[1.5rem] font-black uppercase flex items-center gap-3 disabled:opacity-50 shadow-xl transition-all" | |
| > | |
| {loading ? <Loader2 className="animate-spin" size={20} /> : <Send size={20} />} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Advisor; |