quanthedge / frontend /src /pages /Sentiment.tsx
jashdoshi77's picture
added dark mode
e85ce30
import { useState, useEffect } from 'react';
import { sentimentAPI } from '../api/client';
import TickerSearch from '../components/TickerSearch';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
ResponsiveContainer, Cell, PieChart, Pie, RadialBarChart, RadialBar
} from 'recharts';
import {
TrendingUp, TrendingDown, Minus, Search, Activity, BarChart3,
Newspaper, ArrowUpRight, ArrowDownRight, Eye
} from 'lucide-react';
const MOOD_COLORS: Record<string, string> = { bullish: '#0a8f5c', bearish: '#c23030', neutral: '#b8860b' };
export default function Sentiment() {
const [tab, setTab] = useState<'market' | 'search'>('market');
const [marketMood, setMarketMood] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [searchTicker, setSearchTicker] = useState('');
const [tickerResult, setTickerResult] = useState<any>(null);
const [searchLoading, setSearchLoading] = useState(false);
useEffect(() => { loadMarketMood(); }, []);
const loadMarketMood = async () => {
setLoading(true);
try { const res = await sentimentAPI.marketMood(); setMarketMood(res.data); } catch { /* empty */ }
setLoading(false);
};
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!searchTicker.trim()) return;
setSearchLoading(true);
try { const res = await sentimentAPI.ticker(searchTicker.trim().toUpperCase()); setTickerResult(res.data); } catch { /* empty */ }
setSearchLoading(false);
};
if (loading) {
return <div className="page animate-fade-in"><div className="loading-overlay"><div className="spinner" /><span>Analyzing market sentiment...</span></div></div>;
}
const moodColor = MOOD_COLORS[(marketMood?.mood as string) || 'neutral'];
const MoodIcon = marketMood?.mood === 'bullish' ? TrendingUp : marketMood?.mood === 'bearish' ? TrendingDown : Minus;
const score = marketMood?.score || 0;
// Gauge data: normalize score from [-1,1] to [0,100]
const gaugeValue = ((score + 1) / 2) * 100;
const gaugeData = [{ name: 'Sentiment', value: gaugeValue, fill: moodColor }];
return (
<div className="page animate-fade-in">
<div className="page-header">
<h1 style={{fontSize:'1.75rem',fontFamily:'var(--font-serif)'}}>Sentiment Analysis</h1>
<p style={{color:'var(--text-muted)',fontSize:'0.85rem'}}>Real-time market sentiment derived from financial news and headlines</p>
</div>
{/* Market Mood Header */}
{marketMood && (
<div className="grid-2" style={{marginBottom:'1.5rem'}}>
{/* Mood Summary */}
<div className="card" style={{display:'flex',alignItems:'center',gap:'1.5rem',padding:'1.5rem'}}>
<div style={{width:56,height:56,borderRadius:'50%',background:`${moodColor}10`,display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
<MoodIcon size={28} color={moodColor} />
</div>
<div>
<div style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:'0.2rem'}}>Market Sentiment</div>
<h2 style={{color:moodColor,textTransform:'capitalize',fontSize:'1.5rem',fontFamily:'var(--font-serif)',marginBottom:'0.15rem'}}>
{marketMood.mood}
</h2>
<div style={{fontSize:'0.8rem',color:'var(--text-muted)'}}>
Score: <span style={{fontFamily:'var(--font-mono)',fontWeight:600}}>{score.toFixed(3)}</span> | {marketMood.tickers_analyzed} tickers analyzed
</div>
</div>
</div>
{/* Sentiment Gauge */}
<div className="card" style={{padding:'1rem'}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:'0.5rem'}}>
<span style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Sentiment Gauge</span>
<div style={{display:'flex',gap:'1rem',fontSize:'0.7rem',color:'var(--text-muted)'}}>
<span style={{display:'flex',alignItems:'center',gap:'0.25rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#c23030',display:'inline-block'}}/>Bearish</span>
<span style={{display:'flex',alignItems:'center',gap:'0.25rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#b8860b',display:'inline-block'}}/>Neutral</span>
<span style={{display:'flex',alignItems:'center',gap:'0.25rem'}}><span style={{width:8,height:8,borderRadius:'50%',background:'#0a8f5c',display:'inline-block'}}/>Bullish</span>
</div>
</div>
<div style={{height:130,display:'flex',alignItems:'center',justifyContent:'center'}}>
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart cx="50%" cy="85%" innerRadius="65%" outerRadius="100%" startAngle={180} endAngle={0} data={gaugeData} barSize={12}>
<RadialBar background={{ fill: 'var(--bg-tertiary)' }} dataKey="value" cornerRadius={6} />
</RadialBarChart>
</ResponsiveContainer>
</div>
<div style={{textAlign:'center',marginTop:'-0.25rem'}}>
<span style={{fontFamily:'var(--font-mono)',fontSize:'1.1rem',fontWeight:700,color:moodColor}}>{score.toFixed(3)}</span>
<span style={{color:'var(--text-muted)',fontSize:'0.75rem',marginLeft:'0.375rem'}}>/ 1.000</span>
</div>
</div>
</div>
)}
{/* Breakdown Chart */}
{marketMood?.breakdown?.length > 0 && (
<div className="card" style={{ marginBottom: '1.5rem' }}>
<div className="card-header">
<h3 style={{display:'flex',alignItems:'center',gap:'0.5rem'}}><BarChart3 size={15}/> Ticker Sentiment Breakdown</h3>
</div>
<div style={{ width: '100%', height: 320 }}>
<ResponsiveContainer>
<BarChart data={marketMood.breakdown}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--chart-grid)" />
<XAxis dataKey="ticker" tick={{ fontSize: 11, fill: 'var(--chart-axis)', fontWeight: 500 }} />
<YAxis tick={{ fontSize: 10, fill: 'var(--chart-axis)' }} domain={[-1, 1]} />
<Tooltip
formatter={(v: any) => (Number(v)).toFixed(3)}
contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem', boxShadow:'var(--shadow-md)' }}
/>
<Bar dataKey="score" radius={[4, 4, 0, 0]}>
{marketMood.breakdown.map((d: any, i: number) => (
<Cell key={i} fill={d.score > 0.15 ? '#0a8f5c' : d.score < -0.15 ? '#c23030' : '#b8860b'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Tabs */}
<div className="tabs">
<button className={`tab ${tab === 'market' ? 'active' : ''}`} onClick={() => setTab('market')}>
<span style={{display:'flex',alignItems:'center',gap:'0.375rem'}}><Activity size={13}/> Market Overview</span>
</button>
<button className={`tab ${tab === 'search' ? 'active' : ''}`} onClick={() => setTab('search')}>
<span style={{display:'flex',alignItems:'center',gap:'0.375rem'}}><Search size={13}/> Ticker Search</span>
</button>
</div>
{/* Market Overview */}
{tab === 'market' && marketMood?.breakdown && (
<div className="grid-3">
{marketMood.breakdown.map((t: any) => (
<div key={t.ticker} className="card" style={{borderLeft:`3px solid ${MOOD_COLORS[t.sentiment] || 'var(--border-color)'}`}}>
<div className="flex-between" style={{ marginBottom: '0.5rem' }}>
<h3 style={{ fontFamily:'var(--font-mono)', fontSize:'0.95rem' }}>{t.ticker}</h3>
<span style={{
display:'inline-flex',alignItems:'center',gap:'0.25rem',
padding:'0.15rem 0.5rem',borderRadius:'var(--radius-sm)',fontSize:'0.7rem',fontWeight:600,
background: `${MOOD_COLORS[t.sentiment] || '#6b7280'}10`,
color: MOOD_COLORS[t.sentiment] || 'var(--text-muted)',textTransform:'capitalize'
}}>
{t.sentiment === 'bullish' ? <ArrowUpRight size={11}/> : t.sentiment === 'bearish' ? <ArrowDownRight size={11}/> : <Minus size={11}/>}
{t.sentiment}
</span>
</div>
<div style={{display:'flex',alignItems:'baseline',gap:'0.5rem'}}>
<span style={{fontSize:'1.25rem',fontWeight:700,fontFamily:'var(--font-mono)',color: MOOD_COLORS[t.sentiment] || 'var(--text-primary)'}}>
{(t.score >= 0 ? '+' : '')}{t.score?.toFixed(3)}
</span>
<span style={{fontSize:'0.7rem',color:'var(--text-muted)'}}>sentiment score</span>
</div>
</div>
))}
</div>
)}
{/* Ticker Search */}
{tab === 'search' && (
<div>
<form onSubmit={handleSearch} style={{ display: 'flex', gap: '0.75rem', marginBottom: '1.5rem' }}>
<TickerSearch value={searchTicker} onChange={v => setSearchTicker(v)} onSelect={t => setSearchTicker(t.symbol)}
placeholder="Search stocks, ETFs, futures, crypto..." style={{ flex: 1 }} />
<button className="btn btn-primary" type="submit" disabled={searchLoading} style={{display:'flex',alignItems:'center',gap:'0.375rem'}}>
<Search size={14}/>
{searchLoading ? 'Analyzing...' : 'Analyze'}
</button>
</form>
{tickerResult && (
<>
{/* Summary Cards */}
<div className="grid-4" style={{ marginBottom: '1.5rem' }}>
<div className="card" style={{padding:'1rem 1.25rem',borderLeft:`3px solid ${MOOD_COLORS[tickerResult.sentiment] || '#6b7280'}`}}>
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'0.25rem'}}>
<Eye size={14} color={MOOD_COLORS[tickerResult.sentiment] || 'var(--text-muted)'}/>
<span style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Sentiment</span>
</div>
<div style={{fontSize:'1.25rem',fontWeight:700,textTransform:'capitalize',color: MOOD_COLORS[tickerResult.sentiment]}}>{tickerResult.sentiment}</div>
</div>
<div className="card" style={{padding:'1rem 1.25rem'}}>
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'0.25rem'}}>
<Activity size={14} color="var(--accent)"/>
<span style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Score</span>
</div>
<div style={{fontSize:'1.25rem',fontWeight:700,fontFamily:'var(--font-mono)'}}>{tickerResult.avg_score?.toFixed(3)}</div>
</div>
<div className="card" style={{padding:'1rem 1.25rem'}}>
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'0.25rem'}}>
<Newspaper size={14} color="var(--blue-info)"/>
<span style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Headlines</span>
</div>
<div style={{fontSize:'1.25rem',fontWeight:700,fontFamily:'var(--font-mono)'}}>{tickerResult.headline_count}</div>
</div>
<div className="card" style={{padding:'1rem 1.25rem'}}>
<div style={{display:'flex',alignItems:'center',gap:'0.5rem',marginBottom:'0.25rem'}}>
<BarChart3 size={14} color="var(--text-secondary)"/>
<span style={{fontSize:'0.68rem',fontWeight:600,color:'var(--text-muted)',textTransform:'uppercase',letterSpacing:'0.05em'}}>Distribution</span>
</div>
<div style={{display:'flex',gap:'0.5rem',fontSize:'0.85rem',fontWeight:600,fontFamily:'var(--font-mono)'}}>
<span style={{ color:'#0a8f5c' }}>{tickerResult.bullish}</span>
<span style={{ color:'var(--text-muted)' }}>/</span>
<span style={{ color:'#b8860b' }}>{tickerResult.neutral}</span>
<span style={{ color:'var(--text-muted)' }}>/</span>
<span style={{ color:'#c23030' }}>{tickerResult.bearish}</span>
</div>
</div>
</div>
<div className="grid-2">
{/* Pie Chart */}
<div className="card">
<div className="card-header"><h3 style={{display:'flex',alignItems:'center',gap:'0.5rem'}}><BarChart3 size={15}/> Sentiment Distribution</h3></div>
<div style={{ width: '100%', height: 260 }}>
<ResponsiveContainer>
<PieChart>
<Pie
data={[
{ name: 'Bullish', value: tickerResult.bullish, fill: '#0a8f5c' },
{ name: 'Neutral', value: tickerResult.neutral, fill: '#b8860b' },
{ name: 'Bearish', value: tickerResult.bearish, fill: '#c23030' },
]}
cx="50%" cy="50%" innerRadius={50} outerRadius={85} dataKey="value"
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
labelLine={{ stroke: 'var(--chart-axis)', strokeWidth: 1 }}
/>
<Tooltip contentStyle={{ background:'var(--bg-card)', border:'1px solid var(--border-color)', borderRadius:8, fontSize:'0.8rem' }} />
</PieChart>
</ResponsiveContainer>
</div>
</div>
{/* Headlines */}
<div className="card">
<div className="card-header"><h3 style={{display:'flex',alignItems:'center',gap:'0.5rem'}}><Newspaper size={15}/> Recent Headlines</h3></div>
<div style={{ maxHeight: 320, overflowY: 'auto' }}>
{tickerResult.headlines?.map((h: any, i: number) => (
<div key={i} style={{ padding:'0.625rem 0', borderBottom:'1px solid var(--border-subtle)', display:'flex', gap:'0.75rem', alignItems:'flex-start' }}>
<div style={{width:3,height:'100%',minHeight:36,borderRadius:2,background: MOOD_COLORS[h.sentiment] || '#6b7280',flexShrink:0,marginTop:'0.15rem'}} />
<div style={{ flex: 1 }}>
{h.url ? (
<a href={h.url} target="_blank" rel="noopener noreferrer" style={{ fontSize:'0.8rem', color:'var(--text-primary)', textDecoration:'none', fontWeight:500, lineHeight:1.4 }}>
{h.title}
</a>
) : (
<span style={{ fontSize:'0.8rem', fontWeight:500 }}>{h.title}</span>
)}
<div style={{ fontSize:'0.68rem', color:'var(--text-muted)', marginTop:'0.2rem', display:'flex', gap:'0.5rem', alignItems:'center' }}>
<span>{h.source}</span>
<span style={{width:3,height:3,borderRadius:'50%',background:'var(--text-muted)',display:'inline-block'}}/>
<span style={{fontFamily:'var(--font-mono)'}}>{h.score}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</>
)}
</div>
)}
</div>
);
}