File size: 10,791 Bytes
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
import React, { useState, useEffect } from 'react';
import { Globe, Ship, AlertTriangle, ShieldCheck, Loader2, X } from 'lucide-react';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage } from '@langchain/core/messages';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

const newsHeadlines = [
  { source: 'Right-Leaning', headline: 'Supply Chain Bottlenecks Expose Dependence on Foreign Imports', sentiment: 'negative' },
  { source: 'Left-Leaning', headline: 'Global Trade Disruptions Threaten Consumer Price Stability', sentiment: 'negative' },
  { source: 'Financial Times', headline: 'Port Congestion Peaks as Holiday Inventory Arrives Early', sentiment: 'neutral' }
];

// Fallback data in case the ArcGIS API fails due to CORS or downtime
const mockPortData = Array.from({ length: 30 }, (_, i) => ({
  date: new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
  calls: Math.floor(12000 + Math.random() * 3000 + (i > 20 ? -2000 : 0)) // Simulating a recent dip
}));

export default function MacroTracker() {
  const [isOpen, setIsOpen] = useState(false);
  const [analysis, setAnalysis] = useState(null);
  const [loading, setLoading] = useState(false);
  const [chartData, setChartData] = useState([]);
  const [isLive, setIsLive] = useState(false);

  useEffect(() => {
    const fetchPortWatchData = async () => {
      try {
        const apiUrl = "https://services9.arcgis.com/weJ1QsnbMYJlCHdG/arcgis/rest/services/Daily_Trade_Data/FeatureServer/0/query?where=1=1&outFields=date,calls&orderByFields=date DESC&resultRecordCount=30&f=json";
        const response = await fetch(apiUrl);
        if (!response.ok) throw new Error('Network response was not ok');
        const data = await response.json();
        
        if (data && data.features && data.features.length > 0) {
          const parsedData = data.features.map(f => ({
            date: new Date(f.attributes.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
            calls: f.attributes.calls || 0
          })).reverse();
          setChartData(parsedData);
          setIsLive(true);
        } else {
          throw new Error('No features returned');
        }
      } catch (error) {
        console.warn("Failed to fetch live ArcGIS data. Falling back to mock dataset.", error);
        setChartData(mockPortData);
        setIsLive(false);
      }
    };
    fetchPortWatchData();
  }, []);

  const synthesizeNews = async () => {
    setLoading(true);
    setAnalysis(null);
    
    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,
        });

        const recentDataSummary = chartData.slice(-5).map(d => `${d.date}: ${d.calls} calls`).join(", ");

        const promptText = `
          You are an expert, calming financial advisor speaking to a novice investor. 
          The user is looking at a custom dashboard of global maritime trade data (port calls). 
          Here is the raw data for the last 5 days indicating recent activity levels: [${recentDataSummary}].
          
          Additionally, read these recent news headlines causing panic:
          1. ${newsHeadlines[0].headline}
          2. ${newsHeadlines[1].headline}
          3. ${newsHeadlines[2].headline}
          
          Provide a grounded, jargon-free explanation (max 3-4 sentences) that synthesizes this numerical data and the news. 
          Explain why this data represents normal market noise and why they should not panic sell their portfolio.
        `;

        const message = new HumanMessage({
          content: [{ type: "text", text: promptText }]
        });

        const response = await llm.invoke([message]);
        setAnalysis(response.content);
        setLoading(false);
      } catch (error) {
        console.error("Gemini Error:", error);
        runMockAnalysis();
      }
    } else {
      runMockAnalysis();
    }
  };

  const runMockAnalysis = () => {
    setTimeout(() => {
      setAnalysis("While the news highlights supply chain bottlenecks, the port data shows that daily ship calls remain within normal seasonal ranges, despite a slight recent dip. This indicates that global trade is still flowing actively. Temporary disruptions and the resulting inflation spikes rarely derail a well-diversified, long-term portfolio. Stay the course and avoid making emotional decisions based on short-term headlines.");
      setLoading(false);
    }, 2000);
  };

  const handleOpen = () => {
    setIsOpen(true);
    if (!analysis) {
      synthesizeNews();
    }
  };

  return (
    <>
      <button 
        onClick={handleOpen}
        className="w-full mt-8 py-4 bg-gs-navy text-white rounded-xl hover:bg-gs-navy/90 transition-all flex justify-center items-center font-medium shadow-md group"
      >
        <Globe className="mr-3 text-gs-gold group-hover:rotate-12 transition-transform" size={24} />
        Generate Ground Truth (Global Uncertainty Tracker)
      </button>

      {isOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
          <div className="w-full max-w-5xl bg-white rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
            <div className="bg-gs-navy p-6 text-white flex justify-between items-center sticky top-0 z-10">
              <div>
                <h2 className="text-xl font-medium flex items-center">
                  <Globe className="mr-2 text-gs-gold" size={20} /> Global Uncertainty Tracker
                </h2>
                <p className="text-sm text-gs-light/70 font-light mt-1">
                  Contextualizing geopolitical noise with real-world data to keep you grounded.
                </p>
              </div>
              <button onClick={() => setIsOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors">
                <X size={24} />
              </button>
            </div>

            <div className="p-6 overflow-y-auto">
              <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
                {/* Left: Custom Port Data Chart */}
                <div className="flex flex-col">
                  <h3 className="text-sm text-gs-slate uppercase tracking-wider font-medium mb-3 flex items-center justify-between">
                    <div className="flex items-center">
                      <Ship size={16} className="mr-2 text-blue-500" /> Global Port Activity
                    </div>
                    <span className={`text-xs font-semibold px-2 py-1 rounded ${isLive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
                      {isLive ? 'LIVE IMF DATA' : 'MOCK DATA FALLBACK'}
                    </span>
                  </h3>
                  <div className="rounded-xl border border-gray-200 p-4 h-64 w-full bg-gray-50/50">
                    <ResponsiveContainer width="100%" height="100%">
                      <AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
                        <defs>
                          <linearGradient id="colorCalls" x1="0" y1="0" x2="0" y2="1">
                            <stop offset="5%" stopColor="#1E293B" stopOpacity={0.8}/>
                            <stop offset="95%" stopColor="#1E293B" stopOpacity={0}/>
                          </linearGradient>
                        </defs>
                        <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E7EB" />
                        <XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#64748B' }} minTickGap={30} />
                        <YAxis axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: '#64748B' }} />
                        <Tooltip 
                          contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
                          labelStyle={{ fontWeight: 'bold', color: '#1E293B' }}
                        />
                        <Area type="monotone" dataKey="calls" stroke="#1E293B" strokeWidth={2} fillOpacity={1} fill="url(#colorCalls)" />
                      </AreaChart>
                    </ResponsiveContainer>
                  </div>
                  <p className="text-xs text-gs-slate mt-2 italic">Daily maritime trade volume index based on global port calls.</p>
                </div>

                {/* Right: News */}
                <div className="flex flex-col">
                  <h3 className="text-sm text-gs-slate uppercase tracking-wider font-medium mb-3 flex items-center">
                    <AlertTriangle size={16} className="mr-2 text-red-500" /> The News Cycle
                  </h3>
                  <div className="space-y-3 mb-6 flex-grow">
                    {newsHeadlines.map((news, idx) => (
                      <div key={idx} className="bg-gray-50 border-l-2 border-gs-gold p-3 rounded-r-lg text-sm">
                        <span className="text-xs font-semibold text-gs-slate mb-1 block uppercase tracking-wide">{news.source}</span>
                        <span className="text-gs-navy font-medium italic">"{news.headline}"</span>
                      </div>
                    ))}
                  </div>
                </div>
              </div>

              {/* Analysis Output */}
              <div className="bg-gs-light p-6 rounded-xl border border-gray-200">
                <div className="flex">
                  <div className="flex-shrink-0 mr-4">
                    <div className="w-10 h-10 rounded-full bg-gs-navy flex items-center justify-center text-gs-gold">
                      {loading ? <Loader2 size={20} className="animate-spin" /> : <ShieldCheck size={20} />}
                    </div>
                  </div>
                  <div>
                    <h4 className="text-sm font-semibold uppercase tracking-wider text-gs-navy mb-1">
                      {loading ? 'Synthesizing Ground Truth...' : 'Grounded Analysis'}
                    </h4>
                    {analysis ? (
                       <p className="text-gs-slate leading-relaxed font-light">{analysis}</p>
                    ) : (
                       <div className="h-4 bg-gray-200 rounded w-3/4 animate-pulse mt-2"></div>
                    )}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  );
}