vn6295337 Claude Opus 4.5 commited on
Commit
2cf130c
Β·
1 Parent(s): c1e5f17

Fix MCPDataPanel to use metrics from activity log

Browse files

- Use MetricEntry[] from workflow metrics instead of parsing raw_data
- Group metrics by source (financials, valuation, volatility, macro)
- Show news articles from raw_data when available
- Fallback to metric display for news/sentiment if no URLs

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

frontend/src/App.tsx CHANGED
@@ -606,8 +606,8 @@ Generated by Instant SWOT Agent`
606
  </div>
607
 
608
  {/* MCP Source Data */}
609
- {analysisResult.raw_data && Object.keys(analysisResult.raw_data).length > 0 && (
610
- <MCPDataPanel rawData={analysisResult.raw_data} />
611
  )}
612
 
613
  {/* SWOT Cards */}
 
606
  </div>
607
 
608
  {/* MCP Source Data */}
609
+ {metrics.length > 0 && (
610
+ <MCPDataPanel metrics={metrics} rawData={analysisResult.raw_data} />
611
  )}
612
 
613
  {/* SWOT Cards */}
frontend/src/components/MCPDataPanel.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import React from "react"
 
2
  import type { MCPRawData } from "@/lib/types"
3
  import {
4
  DollarSign,
@@ -11,48 +12,44 @@ import {
11
  } from "lucide-react"
12
 
13
  interface MCPDataPanelProps {
14
- rawData: MCPRawData
 
15
  }
16
 
17
  // Format numbers for display
18
- function formatValue(value: unknown, type: 'currency' | 'percent' | 'ratio' | 'number' = 'number'): string {
19
  if (value === null || value === undefined) return 'β€”'
20
 
21
- const num = typeof value === 'number' ? value : parseFloat(String(value))
22
- if (isNaN(num)) return String(value)
23
-
24
- switch (type) {
25
- case 'currency':
26
- if (Math.abs(num) >= 1e12) return `$${(num / 1e12).toFixed(1)}T`
27
- if (Math.abs(num) >= 1e9) return `$${(num / 1e9).toFixed(1)}B`
28
- if (Math.abs(num) >= 1e6) return `$${(num / 1e6).toFixed(1)}M`
29
- return `$${num.toLocaleString()}`
30
- case 'percent':
31
- return `${(num * 100).toFixed(1)}%`
32
- case 'ratio':
33
- return num.toFixed(2)
34
- default:
35
- return num.toFixed(2)
36
- }
37
  }
38
 
39
  // MCP row component
40
  interface MCPRowProps {
41
  icon: React.ReactNode
42
  label: string
 
43
  children: React.ReactNode
44
- available: boolean
45
  }
46
 
47
- function MCPRow({ icon, label, children, available }: MCPRowProps) {
 
 
48
  return (
49
- <div className={`flex items-center gap-2 py-1.5 px-3 border-b border-border last:border-b-0 ${!available ? 'opacity-50' : ''}`}>
50
- <div className="flex items-center gap-2 w-24 shrink-0">
51
  {icon}
52
- <span className="text-xs font-medium text-muted-foreground">{label}</span>
53
  </div>
54
- <div className="flex-1 flex items-center gap-3 overflow-x-auto text-xs">
55
- {available ? children : <span className="text-muted-foreground italic">Unavailable</span>}
56
  </div>
57
  </div>
58
  )
@@ -73,7 +70,7 @@ function DataItem({ label, value }: DataItemProps) {
73
  )
74
  }
75
 
76
- // Link item component for news/sentiment
77
  interface LinkItemProps {
78
  title: string
79
  url: string
@@ -85,7 +82,7 @@ function LinkItem({ title, url }: LinkItemProps) {
85
  href={url}
86
  target="_blank"
87
  rel="noopener noreferrer"
88
- className="inline-flex items-center gap-1 text-blue-400 hover:text-blue-300 hover:underline whitespace-nowrap max-w-[200px] truncate"
89
  title={title}
90
  >
91
  <span className="truncate">{title}</span>
@@ -94,10 +91,52 @@ function LinkItem({ title, url }: LinkItemProps) {
94
  )
95
  }
96
 
97
- export function MCPDataPanel({ rawData }: MCPDataPanelProps) {
98
- const sourcesAvailable = new Set(rawData.sources_available || [])
99
-
100
- const { financials, valuation, volatility, macro, news, sentiment } = rawData
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  return (
103
  <div className="bg-card rounded-lg border border-border overflow-hidden">
@@ -108,109 +147,74 @@ export function MCPDataPanel({ rawData }: MCPDataPanelProps) {
108
  <div className="divide-y divide-border">
109
  {/* Financials */}
110
  <MCPRow
111
- icon={<DollarSign className="h-4 w-4 text-emerald-500" />}
112
  label="Financials"
113
- available={sourcesAvailable.has('financials')}
114
  >
115
- {financials && (
116
- <>
117
- {financials.revenue !== undefined && <DataItem label="Revenue" value={formatValue(financials.revenue, 'currency')} />}
118
- {financials.gross_margin !== undefined && <DataItem label="Gross Margin" value={formatValue(financials.gross_margin, 'percent')} />}
119
- {financials.operating_margin !== undefined && <DataItem label="Op Margin" value={formatValue(financials.operating_margin, 'percent')} />}
120
- {financials.net_margin !== undefined && <DataItem label="Net Margin" value={formatValue(financials.net_margin, 'percent')} />}
121
- {financials.debt_to_equity !== undefined && <DataItem label="D/E" value={formatValue(financials.debt_to_equity, 'ratio')} />}
122
- {financials.current_ratio !== undefined && <DataItem label="Current" value={formatValue(financials.current_ratio, 'ratio')} />}
123
- </>
124
- )}
125
  </MCPRow>
126
 
127
  {/* Valuation */}
128
  <MCPRow
129
- icon={<TrendingUp className="h-4 w-4 text-blue-500" />}
130
  label="Valuation"
131
- available={sourcesAvailable.has('valuation')}
132
  >
133
- {valuation && (
134
- <>
135
- {valuation.market_cap !== undefined && <DataItem label="Mkt Cap" value={formatValue(valuation.market_cap, 'currency')} />}
136
- {valuation.pe_ratio !== undefined && <DataItem label="P/E" value={formatValue(valuation.pe_ratio, 'ratio')} />}
137
- {valuation.ps_ratio !== undefined && <DataItem label="P/S" value={formatValue(valuation.ps_ratio, 'ratio')} />}
138
- {valuation.pb_ratio !== undefined && <DataItem label="P/B" value={formatValue(valuation.pb_ratio, 'ratio')} />}
139
- {valuation.ev_ebitda !== undefined && <DataItem label="EV/EBITDA" value={formatValue(valuation.ev_ebitda, 'ratio')} />}
140
- {valuation.peg_ratio !== undefined && <DataItem label="PEG" value={formatValue(valuation.peg_ratio, 'ratio')} />}
141
- </>
142
- )}
143
  </MCPRow>
144
 
145
  {/* Volatility */}
146
  <MCPRow
147
- icon={<Activity className="h-4 w-4 text-yellow-500" />}
148
  label="Volatility"
149
- available={sourcesAvailable.has('volatility')}
150
  >
151
- {volatility && (
152
- <>
153
- {volatility.beta !== undefined && <DataItem label="Beta" value={formatValue(volatility.beta, 'ratio')} />}
154
- {volatility.vix !== undefined && <DataItem label="VIX" value={formatValue(volatility.vix, 'ratio')} />}
155
- {volatility.historical_volatility !== undefined && <DataItem label="Hist Vol" value={formatValue(volatility.historical_volatility, 'percent')} />}
156
- {volatility.implied_volatility !== undefined && <DataItem label="Impl Vol" value={formatValue(volatility.implied_volatility, 'percent')} />}
157
- </>
158
- )}
159
  </MCPRow>
160
 
161
  {/* Macro */}
162
  <MCPRow
163
- icon={<Globe className="h-4 w-4 text-purple-500" />}
164
  label="Macro"
165
- available={sourcesAvailable.has('macro')}
166
  >
167
- {macro && (
168
- <>
169
- {macro.gdp_growth !== undefined && <DataItem label="GDP" value={formatValue(macro.gdp_growth, 'percent')} />}
170
- {macro.interest_rate !== undefined && <DataItem label="Fed Rate" value={formatValue(macro.interest_rate, 'percent')} />}
171
- {macro.inflation_rate !== undefined && <DataItem label="CPI" value={formatValue(macro.inflation_rate, 'percent')} />}
172
- {macro.unemployment_rate !== undefined && <DataItem label="Unemp" value={formatValue(macro.unemployment_rate, 'percent')} />}
173
- </>
174
- )}
175
  </MCPRow>
176
 
177
  {/* News */}
178
  <MCPRow
179
- icon={<Newspaper className="h-4 w-4 text-orange-500" />}
180
  label="News"
181
- available={sourcesAvailable.has('news')}
182
  >
183
- {news && news.length > 0 && (
184
- <>
185
- {news.slice(0, 4).map((article, i) => (
186
- <LinkItem
187
- key={i}
188
- title={article.title}
189
- url={article.url}
190
- />
191
- ))}
192
- {news.length > 4 && (
193
- <span className="text-muted-foreground">+{news.length - 4} more</span>
194
- )}
195
- </>
196
- )}
197
  </MCPRow>
198
 
199
  {/* Sentiment */}
200
  <MCPRow
201
- icon={<MessageSquare className="h-4 w-4 text-pink-500" />}
202
  label="Sentiment"
203
- available={sourcesAvailable.has('sentiment')}
204
  >
205
- {sentiment && (
206
- <>
207
- {sentiment.analyst_rating && <DataItem label="Rating" value={String(sentiment.analyst_rating)} />}
208
- {sentiment.analyst_target_price !== undefined && <DataItem label="Target" value={formatValue(sentiment.analyst_target_price, 'currency')} />}
209
- {sentiment.social_sentiment !== undefined && <DataItem label="Social" value={formatValue(sentiment.social_sentiment, 'percent')} />}
210
- {sentiment.news_sentiment !== undefined && <DataItem label="News Sent" value={formatValue(sentiment.news_sentiment, 'percent')} />}
211
- {sentiment.insider_sentiment && <DataItem label="Insider" value={String(sentiment.insider_sentiment)} />}
212
- </>
213
- )}
214
  </MCPRow>
215
  </div>
216
  </div>
 
1
  import React from "react"
2
+ import type { MetricEntry } from "@/lib/api"
3
  import type { MCPRawData } from "@/lib/types"
4
  import {
5
  DollarSign,
 
12
  } from "lucide-react"
13
 
14
  interface MCPDataPanelProps {
15
+ metrics: MetricEntry[]
16
+ rawData?: MCPRawData
17
  }
18
 
19
  // Format numbers for display
20
+ function formatValue(value: string | number): string {
21
  if (value === null || value === undefined) return 'β€”'
22
 
23
+ if (typeof value === 'string') return value
24
+
25
+ const num = value
26
+ if (Math.abs(num) >= 1e12) return `$${(num / 1e12).toFixed(1)}T`
27
+ if (Math.abs(num) >= 1e9) return `$${(num / 1e9).toFixed(1)}B`
28
+ if (Math.abs(num) >= 1e6) return `$${(num / 1e6).toFixed(1)}M`
29
+ if (Math.abs(num) < 0.01 && num !== 0) return num.toExponential(2)
30
+ if (Number.isInteger(num)) return num.toLocaleString()
31
+ return num.toFixed(2)
 
 
 
 
 
 
 
32
  }
33
 
34
  // MCP row component
35
  interface MCPRowProps {
36
  icon: React.ReactNode
37
  label: string
38
+ color: string
39
  children: React.ReactNode
 
40
  }
41
 
42
+ function MCPRow({ icon, label, color, children }: MCPRowProps) {
43
+ const hasContent = React.Children.toArray(children).length > 0
44
+
45
  return (
46
+ <div className={`flex items-center gap-2 py-1.5 px-3 border-b border-border last:border-b-0 ${!hasContent ? 'opacity-40' : ''}`}>
47
+ <div className={`flex items-center gap-2 w-24 shrink-0 ${color}`}>
48
  {icon}
49
+ <span className="text-xs font-medium">{label}</span>
50
  </div>
51
+ <div className="flex-1 flex items-center gap-4 overflow-x-auto text-xs scrollbar-thin">
52
+ {hasContent ? children : <span className="text-muted-foreground italic">No data</span>}
53
  </div>
54
  </div>
55
  )
 
70
  )
71
  }
72
 
73
+ // Link item component for news
74
  interface LinkItemProps {
75
  title: string
76
  url: string
 
82
  href={url}
83
  target="_blank"
84
  rel="noopener noreferrer"
85
+ className="inline-flex items-center gap-1 text-blue-400 hover:text-blue-300 hover:underline whitespace-nowrap max-w-[250px]"
86
  title={title}
87
  >
88
  <span className="truncate">{title}</span>
 
91
  )
92
  }
93
 
94
+ export function MCPDataPanel({ metrics, rawData }: MCPDataPanelProps) {
95
+ // Group metrics by source
96
+ const groupedMetrics = React.useMemo(() => {
97
+ const groups: Record<string, Array<{metric: string, value: string | number}>> = {
98
+ financials: [],
99
+ valuation: [],
100
+ volatility: [],
101
+ macro: [],
102
+ news: [],
103
+ sentiment: []
104
+ }
105
+
106
+ for (const m of metrics) {
107
+ const source = m.source.toLowerCase()
108
+ if (source in groups) {
109
+ groups[source].push({ metric: m.metric, value: m.value })
110
+ }
111
+ }
112
+
113
+ return groups
114
+ }, [metrics])
115
+
116
+ // Extract news articles from raw_data if available
117
+ const newsArticles = React.useMemo(() => {
118
+ if (!rawData) return []
119
+
120
+ // Try to get articles from metrics.news.articles
121
+ const newsData = rawData.metrics?.news || rawData.news
122
+ if (newsData && 'articles' in newsData) {
123
+ return (newsData.articles as Array<{title: string, url: string}>).slice(0, 4)
124
+ }
125
+
126
+ // Fallback: check if news is an array directly
127
+ if (Array.isArray(rawData.news)) {
128
+ return rawData.news.slice(0, 4)
129
+ }
130
+
131
+ return []
132
+ }, [rawData])
133
+
134
+ // Check if we have any data at all
135
+ const hasAnyData = metrics.length > 0 || newsArticles.length > 0
136
+
137
+ if (!hasAnyData) {
138
+ return null
139
+ }
140
 
141
  return (
142
  <div className="bg-card rounded-lg border border-border overflow-hidden">
 
147
  <div className="divide-y divide-border">
148
  {/* Financials */}
149
  <MCPRow
150
+ icon={<DollarSign className="h-4 w-4" />}
151
  label="Financials"
152
+ color="text-emerald-500"
153
  >
154
+ {groupedMetrics.financials.map((m, i) => (
155
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
156
+ ))}
 
 
 
 
 
 
 
157
  </MCPRow>
158
 
159
  {/* Valuation */}
160
  <MCPRow
161
+ icon={<TrendingUp className="h-4 w-4" />}
162
  label="Valuation"
163
+ color="text-blue-500"
164
  >
165
+ {groupedMetrics.valuation.map((m, i) => (
166
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
167
+ ))}
 
 
 
 
 
 
 
168
  </MCPRow>
169
 
170
  {/* Volatility */}
171
  <MCPRow
172
+ icon={<Activity className="h-4 w-4" />}
173
  label="Volatility"
174
+ color="text-yellow-500"
175
  >
176
+ {groupedMetrics.volatility.map((m, i) => (
177
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
178
+ ))}
 
 
 
 
 
179
  </MCPRow>
180
 
181
  {/* Macro */}
182
  <MCPRow
183
+ icon={<Globe className="h-4 w-4" />}
184
  label="Macro"
185
+ color="text-purple-500"
186
  >
187
+ {groupedMetrics.macro.map((m, i) => (
188
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
189
+ ))}
 
 
 
 
 
190
  </MCPRow>
191
 
192
  {/* News */}
193
  <MCPRow
194
+ icon={<Newspaper className="h-4 w-4" />}
195
  label="News"
196
+ color="text-orange-500"
197
  >
198
+ {newsArticles.length > 0 ? (
199
+ newsArticles.map((article, i) => (
200
+ <LinkItem key={i} title={article.title} url={article.url} />
201
+ ))
202
+ ) : groupedMetrics.news.length > 0 ? (
203
+ groupedMetrics.news.map((m, i) => (
204
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
205
+ ))
206
+ ) : null}
 
 
 
 
 
207
  </MCPRow>
208
 
209
  {/* Sentiment */}
210
  <MCPRow
211
+ icon={<MessageSquare className="h-4 w-4" />}
212
  label="Sentiment"
213
+ color="text-pink-500"
214
  >
215
+ {groupedMetrics.sentiment.map((m, i) => (
216
+ <DataItem key={i} label={m.metric} value={formatValue(m.value)} />
217
+ ))}
 
 
 
 
 
 
218
  </MCPRow>
219
  </div>
220
  </div>
frontend/src/lib/types.ts CHANGED
@@ -26,6 +26,7 @@ export interface SentimentData {
26
  export interface MCPRawData {
27
  sources_available?: string[]
28
  sources_failed?: string[]
 
29
  // Financials MCP
30
  financials?: {
31
  revenue?: number
 
26
  export interface MCPRawData {
27
  sources_available?: string[]
28
  sources_failed?: string[]
29
+ metrics?: Record<string, unknown>
30
  // Financials MCP
31
  financials?: {
32
  revenue?: number
static/assets/{index-D6SjG3SU.js β†’ index-DP1mfG7B.js} RENAMED
The diff for this file is too large to render. See raw diff
 
static/assets/{index-BjoTJiPq.css β†’ index-DRWwG9eL.css} RENAMED
The diff for this file is too large to render. See raw diff
 
static/index.html CHANGED
@@ -5,8 +5,8 @@
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>frontend</title>
8
- <script type="module" crossorigin src="/assets/index-D6SjG3SU.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-BjoTJiPq.css">
10
  </head>
11
  <body>
12
  <div id="root"></div>
 
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>frontend</title>
8
+ <script type="module" crossorigin src="/assets/index-DP1mfG7B.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DRWwG9eL.css">
10
  </head>
11
  <body>
12
  <div id="root"></div>