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>
  );
}