Spaces:
Sleeping
Sleeping
File size: 12,173 Bytes
dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee 0c5252a dbc70ee | 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | import React, { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Users, User, BarChart2, AlertCircle, PlayCircle, Loader2 } from 'lucide-react';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
const agents = {
macro: { name: 'Macro Economist', role: 'Analyzes broader economic trends, rates, and inflation', icon: <BarChart2 size={16} />, color: 'text-blue-500', bg: 'bg-blue-500/10' },
tech: { name: 'Technical Analyst', role: 'Focuses on momentum, moving averages, and charts', icon: <User size={16} />, color: 'text-purple-500', bg: 'bg-purple-500/10' },
skeptic: { name: 'The Skeptic', role: 'Looks for flaws, overvaluation, and risks', icon: <AlertCircle size={16} />, color: 'text-red-500', bg: 'bg-red-500/10' },
system: { name: 'System', role: 'Moderator', icon: <Users size={16} />, color: 'text-gs-gold', bg: 'bg-gs-gold/10' }
};
const mockDebates = {
'AAPL': [
{ agent: 'macro', text: 'iPhone demand remains steady globally, but services growth is the real story for Apple.' },
{ agent: 'tech', text: 'AAPL is consolidating near its 200-day average. A breakout above current levels would be very bullish.' },
{ agent: 'skeptic', text: 'Regulatory pressure in the EU and US is a massive dark cloud that could hurt margins.' },
{ agent: 'system', text: 'Consensus: Strong ecosystem but legal risks. Conviction Score: 72/100 (Hold).' }
],
'TSLA': [
{ agent: 'macro', text: 'High interest rates are cooling the EV market, forcing aggressive price cuts.' },
{ agent: 'tech', text: 'Tesla is in a clear downtrend. We need to see a higher low before turning bullish.' },
{ agent: 'skeptic', text: 'Elon is distracted and competition from China is fierce. Valuation is still disconnected from reality.' },
{ agent: 'system', text: 'Consensus: High volatility and macro headwinds. Conviction Score: 35/100 (Underweight).' }
],
'NVDA': [
{ agent: 'macro', text: 'The AI infrastructure build-out is a once-in-a-generation shift that favors NVIDIA.' },
{ agent: 'tech', text: 'Parabolic move. It is overextended, but momentum like this can last longer than expected.' },
{ agent: 'skeptic', text: 'At some point, the hyperscalers will stop buying at this rate. The drop will be as fast as the rise.' },
{ agent: 'system', text: 'Consensus: Unrivaled leader in a booming sector. Conviction Score: 88/100 (Overweight).' }
],
'MSFT': [
{ agent: 'macro', text: 'Enterprise software and cloud (Azure) are the backbone of the modern economy.' },
{ agent: 'tech', text: 'Steady uptrend. Microsoft is a "safe haven" in the tech world right now.' },
{ agent: 'skeptic', text: 'The Activision deal is done, but integrating it perfectly won\'t be easy or cheap.' },
{ agent: 'system', text: 'Consensus: Solid growth and AI tailwinds. Conviction Score: 82/100 (Overweight).' }
],
'default': [
{ agent: 'macro', text: 'The macro outlook for [TICKER] is heavily dependent on current sector trends and inflationary pressures affecting production costs.' },
{ agent: 'tech', text: '[TICKER] is currently testing a key resistance level. We need to see sustained volume before confirming a new upward trajectory.' },
{ agent: 'skeptic', text: 'A primary concern for [TICKER] is the potential for margin compression if competitors maintain aggressive pricing strategies.' },
{ agent: 'system', text: 'Consensus: Position is stable for [TICKER], but suggests a cautious stance until quarterly data confirms growth. Conviction Score: 62/100 (Neutral).' }
]
};
export default function InvestmentCommittee({ ticker, isDebating: externalIsDebating, setIsDebating: externalSetIsDebating, inline = false }) {
const [messages, setMessages] = useState([]);
const [convictionScore, setConvictionScore] = useState(null);
const [loading, setLoading] = useState(false);
const [internalIsDebating, setInternalIsDebating] = useState(false);
const scrollRef = useRef(null);
const isDebating = externalIsDebating !== undefined ? externalIsDebating : internalIsDebating;
const setIsDebating = externalSetIsDebating !== undefined ? externalSetIsDebating : setInternalIsDebating;
useEffect(() => {
setMessages([]);
setConvictionScore(null);
setIsDebating(false);
}, [ticker]);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const runDebate = async () => {
if (!ticker) return;
setIsDebating(true);
setMessages([]);
setConvictionScore(null);
setLoading(true);
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
if (apiKey && apiKey.length > 10) {
try {
const llm = new ChatGoogleGenerativeAI({
apiKey: apiKey,
modelName: 'gemini-1.5-flash',
maxOutputTokens: 2048,
});
// Agent 1: Macro
setMessages([{ agent: 'macro', text: `Quantifying macro factors for ${ticker}...` }]);
const macroResponse = await llm.invoke([
new SystemMessage(`You are a top-tier Macro Economist. Analyze the stock ${ticker} with extreme specificity. Mention a specific economic factor like "global logistics", "energy costs", or "labor markets" as it relates to this EXACT company. Provide a 12-month price target. 1 short sentence.`),
new HumanMessage(`What is your unique macro view on ${ticker}?`)
]);
setMessages([{ agent: 'macro', text: macroResponse.content }]);
// Agent 2: Tech
setMessages(prev => [...prev, { agent: 'tech', text: `Evaluating technical metrics for ${ticker}...` }]);
const techResponse = await llm.invoke([
new SystemMessage(`You are a Technical Analyst. Analyze ${ticker}'s price chart. Mention a specific "support zone", "RSI divergence", or "moving average cross" observation for this company. Provide a specific Fair Value. 1 sentence.`),
new HumanMessage(`Provide a non-generic technical view on ${ticker}.`)
]);
setMessages(prev => [prev[0], { agent: 'tech', text: techResponse.content }]);
// Agent 3: Skeptic
setMessages(prev => [...prev, { agent: 'skeptic', text: `Quantifying downside risks for ${ticker}...` }]);
const skepticResponse = await llm.invoke([
new SystemMessage(`You are a Professional Skeptic. Find a "poison pill" for ${ticker}. What is the one specific, non-obvious risk (e.g. patent cliff, specific litigation, supply chain bottleneck) for this stock? 1 sentence.`),
new HumanMessage(`What is the hidden risk in ${ticker}?`)
]);
setMessages(prev => [prev[0], prev[1], { agent: 'skeptic', text: skepticResponse.content }]);
// System consensus
setMessages(prev => [...prev, { agent: 'system', text: 'Synthesizing quantitative consensus...' }]);
const consensusResponse = await llm.invoke([
new SystemMessage("Committee Moderator. Based on the previous data points, output a final Conviction Score (0-100). Format: [SCORE] followed by a 1-sentence quantitative justification."),
new HumanMessage(`Synthesize these quantitative views for ${ticker}: 1. ${macroResponse.content} 2. ${techResponse.content} 3. ${skepticResponse.content}`)
]);
let score = parseInt(consensusResponse.content.replace(/\D/g,''));
if (isNaN(score)) score = 50;
setMessages(prev => [prev[0], prev[1], prev[2], { agent: 'system', text: consensusResponse.content }]);
setConvictionScore(score);
} catch (error) {
console.error("LLM Error:", error);
runMockDebate();
}
} else {
runMockDebate();
}
setLoading(false);
};
const runMockDebate = () => {
const script = mockDebates[ticker] || mockDebates['default'];
const debateScript = script.map(msg => ({
...msg,
text: msg.text.replace(/\[TICKER\]/g, ticker)
}));
let step = 0;
const interval = setInterval(() => {
if (step < debateScript.length) {
const msg = debateScript[step];
if (msg.agent === 'system' && msg.text.includes('Conviction Score')) {
const match = msg.text.match(/(\d+)\/100/);
if (match) setConvictionScore(parseInt(match[1]));
}
setMessages(prev => [...prev, msg]);
step++;
} else {
clearInterval(interval);
setLoading(false);
setIsDebating(false);
}
}, 1200);
};
return (
<div className={`bg-white rounded-2xl shadow-sm border border-gray-100 flex flex-col ${inline ? 'h-[350px] p-4' : 'h-[500px] p-6'}`}>
<header className={`flex justify-between items-center border-b ${inline ? 'mb-2 pb-2' : 'mb-4 pb-4'}`}>
<div>
<h2 className={`${inline ? 'text-sm' : 'text-xl'} font-medium text-gs-navy flex items-center`}>
<Users className={`${inline ? 'mr-1.5' : 'mr-2'} text-gs-gold`} size={inline ? 16 : 20} /> AI Committee
</h2>
</div>
{ticker && !isDebating && !convictionScore && (
<button
onClick={runDebate}
disabled={loading}
className={`flex items-center bg-gs-navy text-white rounded-lg hover:bg-gs-navy/90 transition-colors font-bold ${inline ? 'px-2 py-1 text-[10px]' : 'px-4 py-2 text-sm'}`}
>
{loading ? <Loader2 size={inline ? 12 : 16} className="animate-spin mr-1.5" /> : <PlayCircle size={inline ? 12 : 16} className="mr-1.5" />}
Debate
</button>
)}
</header>
<div className="flex-1 overflow-y-auto space-y-3 pr-1 custom-scrollbar" ref={scrollRef}>
{!ticker && (
<div className="h-full flex items-center justify-center text-gray-400 text-xs italic">
Waiting for selection...
</div>
)}
<AnimatePresence>
{messages.map((msg, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex gap-2"
>
<div className={`mt-0.5 flex-shrink-0 w-6 h-6 rounded-lg ${agents[msg.agent].bg} ${agents[msg.agent].color} flex items-center justify-center`}>
{agents[msg.agent].icon}
</div>
<div className="bg-gray-50 rounded-xl p-2.5 text-[11px] text-gs-slate border border-gray-100 w-full leading-relaxed">
<span className={`text-[9px] font-bold uppercase tracking-wider block mb-0.5 ${agents[msg.agent].color}`}>
{agents[msg.agent].name}
</span>
{msg.text}
</div>
</motion.div>
))}
</AnimatePresence>
{loading && messages.length > 0 && messages.length < 4 && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex items-center text-gray-400 text-[10px] ml-8">
<Loader2 size={10} className="animate-spin mr-1.5" /> Typing...
</motion.div>
)}
</div>
{convictionScore !== null && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className={`mt-2 pt-2 border-t`}
>
<div className="bg-gs-light p-3 rounded-xl border border-gs-gold/10">
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-gs-navy uppercase">Conviction Score</span>
<div className="flex items-center">
<span className={`text-lg font-bold ${convictionScore >= 70 ? 'text-green-600' : convictionScore >= 40 ? 'text-gs-gold' : 'text-red-500'}`}>
{convictionScore}
</span>
<span className="text-[10px] text-gray-400 ml-0.5">/ 100</span>
</div>
</div>
</div>
</motion.div>
)}
</div>
);
}
|