Spaces:
Sleeping
Sleeping
Add clickable links for news articles and sentiment sources
Browse files- Fix news extraction to use 'results' field (Tavily API format)
- Add sentiment source links (Finnhub, Reddit) with scores
- Display news as clickable article links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
frontend/src/components/MCPDataPanel.tsx
CHANGED
|
@@ -182,10 +182,17 @@ export function MCPDataPanel({ metrics, rawData, mcpStatus, companyName, ticker,
|
|
| 182 |
const newsArticles = React.useMemo(() => {
|
| 183 |
if (!rawData) return []
|
| 184 |
|
| 185 |
-
// Try to get articles from metrics.news
|
| 186 |
const newsData = rawData.metrics?.news || rawData.news
|
| 187 |
-
if (newsData &&
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
// Fallback: check if news is an array directly
|
|
@@ -196,6 +203,45 @@ export function MCPDataPanel({ metrics, rawData, mcpStatus, companyName, ticker,
|
|
| 196 |
return []
|
| 197 |
}, [rawData])
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
// Extract company profile info from raw_data if available
|
| 200 |
const companyProfile = React.useMemo(() => {
|
| 201 |
if (!rawData) return null
|
|
@@ -376,15 +422,39 @@ export function MCPDataPanel({ metrics, rawData, mcpStatus, companyName, ticker,
|
|
| 376 |
color="text-pink-500"
|
| 377 |
status={mcpStatus?.sentiment}
|
| 378 |
>
|
| 379 |
-
{
|
| 380 |
-
|
| 381 |
-
key={i}
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
</MCPRow>
|
| 389 |
</div>
|
| 390 |
</div>
|
|
|
|
| 182 |
const newsArticles = React.useMemo(() => {
|
| 183 |
if (!rawData) return []
|
| 184 |
|
| 185 |
+
// Try to get articles from metrics.news (field name is "results" from Tavily API)
|
| 186 |
const newsData = rawData.metrics?.news || rawData.news
|
| 187 |
+
if (newsData && typeof newsData === 'object') {
|
| 188 |
+
// Check "results" first (Tavily API format), then "articles" as fallback
|
| 189 |
+
const articles = (newsData as Record<string, unknown>).results || (newsData as Record<string, unknown>).articles
|
| 190 |
+
if (Array.isArray(articles)) {
|
| 191 |
+
return articles.slice(0, 4).map((a: Record<string, unknown>) => ({
|
| 192 |
+
title: a.title as string || a.content as string || 'News article',
|
| 193 |
+
url: a.url as string || a.link as string || '#'
|
| 194 |
+
}))
|
| 195 |
+
}
|
| 196 |
}
|
| 197 |
|
| 198 |
// Fallback: check if news is an array directly
|
|
|
|
| 203 |
return []
|
| 204 |
}, [rawData])
|
| 205 |
|
| 206 |
+
// Extract sentiment sources with links from raw_data
|
| 207 |
+
const sentimentSources = React.useMemo(() => {
|
| 208 |
+
if (!rawData) return []
|
| 209 |
+
|
| 210 |
+
const sentimentData = rawData.metrics?.sentiment || rawData.sentiment
|
| 211 |
+
if (!sentimentData || typeof sentimentData !== 'object') return []
|
| 212 |
+
|
| 213 |
+
const sources: Array<{name: string, score: number | null, url?: string}> = []
|
| 214 |
+
const metrics = (sentimentData as Record<string, unknown>).metrics as Record<string, unknown> | undefined
|
| 215 |
+
|
| 216 |
+
// Finnhub sentiment
|
| 217 |
+
if (metrics?.finnhub) {
|
| 218 |
+
const finnhub = metrics.finnhub as Record<string, unknown>
|
| 219 |
+
sources.push({
|
| 220 |
+
name: 'Finnhub',
|
| 221 |
+
score: finnhub.score as number || finnhub.sentiment_score as number || null,
|
| 222 |
+
url: 'https://finnhub.io'
|
| 223 |
+
})
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// Reddit sentiment
|
| 227 |
+
if (metrics?.reddit) {
|
| 228 |
+
const reddit = metrics.reddit as Record<string, unknown>
|
| 229 |
+
sources.push({
|
| 230 |
+
name: 'Reddit',
|
| 231 |
+
score: reddit.score as number || null,
|
| 232 |
+
url: 'https://reddit.com'
|
| 233 |
+
})
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// Composite score
|
| 237 |
+
const composite = (sentimentData as Record<string, unknown>).composite_score as number | undefined
|
| 238 |
+
if (composite !== undefined && sources.length === 0) {
|
| 239 |
+
sources.push({ name: 'Composite', score: composite })
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
return sources
|
| 243 |
+
}, [rawData])
|
| 244 |
+
|
| 245 |
// Extract company profile info from raw_data if available
|
| 246 |
const companyProfile = React.useMemo(() => {
|
| 247 |
if (!rawData) return null
|
|
|
|
| 422 |
color="text-pink-500"
|
| 423 |
status={mcpStatus?.sentiment}
|
| 424 |
>
|
| 425 |
+
{sentimentSources.length > 0 ? (
|
| 426 |
+
sentimentSources.map((s, i) => (
|
| 427 |
+
<span key={i} className="whitespace-nowrap">
|
| 428 |
+
{s.url ? (
|
| 429 |
+
<a
|
| 430 |
+
href={s.url}
|
| 431 |
+
target="_blank"
|
| 432 |
+
rel="noopener noreferrer"
|
| 433 |
+
className="text-pink-400 hover:text-pink-300 hover:underline"
|
| 434 |
+
>
|
| 435 |
+
{s.name}
|
| 436 |
+
</a>
|
| 437 |
+
) : (
|
| 438 |
+
<span className="text-muted-foreground">{s.name}</span>
|
| 439 |
+
)}
|
| 440 |
+
{s.score !== null && (
|
| 441 |
+
<span className="text-foreground font-medium ml-1">
|
| 442 |
+
{s.score.toFixed(1)}
|
| 443 |
+
</span>
|
| 444 |
+
)}
|
| 445 |
+
</span>
|
| 446 |
+
))
|
| 447 |
+
) : groupedMetrics.sentiment.length > 0 ? (
|
| 448 |
+
groupedMetrics.sentiment.map((m, i) => (
|
| 449 |
+
<DataItem
|
| 450 |
+
key={i}
|
| 451 |
+
label={m.metric}
|
| 452 |
+
value={formatValue(m.value)}
|
| 453 |
+
fiscalPeriod={m.fiscalPeriod}
|
| 454 |
+
endDate={m.endDate}
|
| 455 |
+
/>
|
| 456 |
+
))
|
| 457 |
+
) : null}
|
| 458 |
</MCPRow>
|
| 459 |
</div>
|
| 460 |
</div>
|