Spaces:
Sleeping
Sleeping
File size: 8,809 Bytes
b8b3edf 5196f44 b8b3edf 5196f44 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 24c58d8 b8b3edf 5196f44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | 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; |