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 (
{/* Header: icon + title + badge */}
{cfg.icon}

{cfg.title}

{cfg.desc}

{formatSignal(signal)}
{/* Progress bar */}
Weight {weight}%
{/* Plain English */}

{plainText}

{/* ML extras */} {sourceKey === 'ml' && (
{sourceData.cv_accuracy != null && ( Model accuracy: {(sourceData.cv_accuracy * 100).toFixed(0)}% )} {sourceData.margin != null && ( Decision margin: {(sourceData.margin * 100).toFixed(0)}% )} {sourceData.strength && ( Signal strength: {sourceData.strength} )}
)} {/* Technical extras */} {sourceKey === 'technical' && sourceData.votes && (
Bullish votes: {sourceData.votes[0]} / Bearish: {sourceData.votes[1]}
)}
) } // ── 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 (

🔬 What's driving the signal?

{entries.map(([key, val], i) => ( ))}
) }