Spaces:
Sleeping
Sleeping
| import { useEffect, useRef } from 'react' | |
| import anime from 'animejs' | |
| // Per-source display config | |
| const SOURCE = { | |
| ml: { | |
| icon: 'π€', | |
| title: 'AI Model', | |
| desc: 'Machine learning trained on 5 years of price history', | |
| plain: { | |
| BUY: 'Our AI sees patterns similar to when prices rose in the past', | |
| 'STRONG BUY': 'Our AI is strongly predicting upward movement', | |
| SELL: 'Our AI sees patterns similar to when prices dropped in the past', | |
| 'STRONG SELL':'Our AI is strongly predicting downward movement', | |
| HOLD: 'Our AI is not confident enough to call a direction yet', | |
| }, | |
| }, | |
| technical: { | |
| icon: 'π', | |
| title: 'Chart Analysis', | |
| desc: 'Price trends, momentum, moving averages & volume', | |
| plain: { | |
| BUY: 'Price is trending up with positive momentum', | |
| 'STRONG BUY': 'Strong uptrend β multiple chart indicators agree', | |
| SELL: 'Price is trending down β momentum looks negative', | |
| 'STRONG SELL':'Strong downtrend β charts are flashing warning signs', | |
| HOLD: 'Chart signals are mixed β no clear trend right now', | |
| }, | |
| }, | |
| fundamental: { | |
| icon: 'π’', | |
| title: 'Company Health', | |
| desc: 'P/E ratio, revenue growth, profit margins & debt', | |
| plain: { | |
| undervalued: 'The company looks like a potential bargain vs similar companies', | |
| overvalued: 'The stock may be expensive compared to what the company actually earns', | |
| 'fair value': 'The company is priced fairly for its size and earnings', | |
| unknown: 'Not enough financial data to form a clear view', | |
| }, | |
| }, | |
| sentiment: { | |
| icon: 'π°', | |
| title: 'News & Mood', | |
| desc: 'Recent headlines and investor sentiment from the web', | |
| plain: { | |
| BULLISH: 'Recent news and investor mood are mostly positive', | |
| BEARISH: 'Recent news and investor mood are mostly negative', | |
| NEUTRAL: 'News is balanced β no strong positive or negative lean', | |
| }, | |
| }, | |
| } | |
| // Signal β colour | |
| function signalColor(score) { | |
| if (score > 0.2) return '#00e87a' | |
| if (score < -0.2) return '#ff4466' | |
| return '#ffcc44' | |
| } | |
| // Normalise signal text for display | |
| function formatSignal(signal) { | |
| return (signal || 'HOLD').toString().toUpperCase() | |
| } | |
| // ββ Individual card βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function SignalCard({ sourceKey, sourceData, index }) { | |
| const barRef = useRef(null) | |
| const cfg = SOURCE[sourceKey] || SOURCE.technical | |
| const score = sourceData.score ?? 0 | |
| const signal = sourceData.signal ?? 'HOLD' | |
| const weight = Math.round((sourceData.weight ?? 0) * 100) | |
| const color = signalColor(score) | |
| const barPct = Math.round((score + 1) * 50) // -1..+1 β 0..100% | |
| // Pick plain-English text | |
| const plainMap = cfg.plain | |
| const signalKey = signal.toString() | |
| const plainText = | |
| plainMap[signalKey] || | |
| plainMap[signalKey.toUpperCase()] || | |
| Object.values(plainMap)[1] || | |
| '' | |
| useEffect(() => { | |
| anime({ | |
| targets: barRef.current, | |
| width: ['0%', `${barPct}%`], | |
| delay: 700 + index * 120, | |
| duration: 1000, | |
| easing: 'easeOutExpo', | |
| }) | |
| }, []) | |
| return ( | |
| <div | |
| className="signal-card card" | |
| style={{ '--signal-color': color }} | |
| > | |
| {/* Header: icon + title + badge */} | |
| <div className="signal-card-header"> | |
| <span className="signal-icon">{cfg.icon}</span> | |
| <div className="signal-header-text"> | |
| <h4 className="signal-title">{cfg.title}</h4> | |
| <p className="signal-desc">{cfg.desc}</p> | |
| </div> | |
| <div | |
| className="signal-badge" | |
| style={{ | |
| color, | |
| borderColor: color + '44', | |
| background: color + '11', | |
| }} | |
| > | |
| {formatSignal(signal)} | |
| </div> | |
| </div> | |
| {/* Progress bar */} | |
| <div className="signal-bar-row"> | |
| <div className="signal-bar-track"> | |
| <div | |
| ref={barRef} | |
| className="signal-bar-fill" | |
| style={{ width: '0%', background: color }} | |
| /> | |
| <div className="signal-bar-center" /> | |
| </div> | |
| <span className="signal-weight">Weight {weight}%</span> | |
| </div> | |
| {/* Plain English */} | |
| <p className="signal-plain">{plainText}</p> | |
| {/* ML extras */} | |
| {sourceKey === 'ml' && ( | |
| <div className="signal-meta"> | |
| {sourceData.cv_accuracy != null && ( | |
| <span> | |
| Model accuracy: {(sourceData.cv_accuracy * 100).toFixed(0)}% | |
| </span> | |
| )} | |
| {sourceData.margin != null && ( | |
| <span> | |
| Decision margin: {(sourceData.margin * 100).toFixed(0)}% | |
| </span> | |
| )} | |
| {sourceData.strength && ( | |
| <span>Signal strength: {sourceData.strength}</span> | |
| )} | |
| </div> | |
| )} | |
| {/* Technical extras */} | |
| {sourceKey === 'technical' && sourceData.votes && ( | |
| <div className="signal-meta"> | |
| <span> | |
| Bullish votes: {sourceData.votes[0]} / Bearish: {sourceData.votes[1]} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| // ββ Container βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export default function SignalCards({ breakdown }) { | |
| const gridRef = useRef(null) | |
| useEffect(() => { | |
| if (!gridRef.current) return | |
| anime({ | |
| targets: gridRef.current.querySelectorAll('.signal-card'), | |
| translateY: [40, 0], | |
| opacity: [0, 1], | |
| delay: anime.stagger(110, { start: 350 }), | |
| easing: 'easeOutExpo', | |
| duration: 700, | |
| }) | |
| }, [breakdown]) | |
| if (!breakdown) return null | |
| const order = ['ml', 'technical', 'fundamental', 'sentiment'] | |
| const entries = order | |
| .filter((k) => breakdown[k]) | |
| .map((k) => [k, breakdown[k]]) | |
| return ( | |
| <div className="signal-cards-section"> | |
| <h2 className="section-title">π¬ What's driving the signal?</h2> | |
| <div ref={gridRef} className="signal-cards-grid"> | |
| {entries.map(([key, val], i) => ( | |
| <SignalCard | |
| key={key} | |
| sourceKey={key} | |
| sourceData={val} | |
| index={i} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |