aibanking.dev / views /Advisor.tsx
admin08077's picture
Upload 26 files
24c58d8 verified
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;