Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useRef } from 'react' | |
| import anime from 'animejs' | |
| import RecommendationHero from './components/RecommendationHero' | |
| import SignalCards from './components/SignalCards' | |
| import AgentStream from './components/AgentStream' | |
| import RiskFlags from './components/RiskFlags' | |
| import PortfolioView from './components/PortfolioView' | |
| import PriceChart from './components/PriceChart' | |
| const POPULAR = ['AAPL', 'NVDA', 'TSLA', 'MSFT', 'AMZN', 'GOOGL', 'META', 'SPY'] | |
| // ββ Portfolio page βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function PortfolioPage({ onAnalyse, onBack }) { | |
| return ( | |
| <div className="dashboard"> | |
| <div className="dashboard-header"> | |
| <button className="btn-back" onClick={onBack}>β Back</button> | |
| <div className="ticker-header"> | |
| <span className="ticker-name">My Portfolio</span> | |
| </div> | |
| </div> | |
| <div className="dashboard-content"> | |
| <PortfolioView onAnalyse={onAnalyse} /> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Landing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function Landing({ onSearch, onPortfolio }) { | |
| const [input, setInput] = useState('') | |
| const cardRef = useRef(null) | |
| useEffect(() => { | |
| // Animate each letter of the title | |
| anime({ | |
| targets: '.hero-letter', | |
| translateY: [40, 0], | |
| opacity: [0, 1], | |
| delay: anime.stagger(45), | |
| easing: 'easeOutExpo', | |
| duration: 750, | |
| }) | |
| // Animate the search card up | |
| anime({ | |
| targets: cardRef.current, | |
| translateY: [30, 0], | |
| opacity: [0, 1], | |
| delay: 500, | |
| easing: 'easeOutExpo', | |
| duration: 800, | |
| }) | |
| }, []) | |
| const go = (ticker) => { | |
| const t = ticker.trim().toUpperCase() | |
| if (t) onSearch(t) | |
| } | |
| return ( | |
| <div className="landing"> | |
| <div className="landing-hero"> | |
| <div className="landing-badge animate-fadeUp"> | |
| π€ Powered by AI Agents | |
| </div> | |
| <h1 className="landing-title"> | |
| {'Should you buy it?'.split('').map((ch, i) => ( | |
| <span key={i} className="hero-letter" style={{ opacity: 0 }}> | |
| {ch === ' ' ? '\u00a0' : ch} | |
| </span> | |
| ))} | |
| </h1> | |
| <p | |
| className="landing-subtitle animate-fadeUp" | |
| style={{ animationDelay: '0.25s', opacity: 0, animationFillMode: 'forwards' }} | |
| > | |
| Type any stock ticker and get a plain-English verdict powered by | |
| machine learning, technical analysis, and real-time news. | |
| </p> | |
| <div ref={cardRef} className="card search-card" style={{ opacity: 0 }}> | |
| <div className="search-row"> | |
| <input | |
| className="search-input" | |
| placeholder="Enter ticker β e.g. AAPL, NVDA, TSLA" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value.toUpperCase())} | |
| onKeyDown={(e) => e.key === 'Enter' && go(input)} | |
| maxLength={10} | |
| autoFocus | |
| /> | |
| <button className="btn-primary" onClick={() => go(input)}> | |
| Analyse β | |
| </button> | |
| </div> | |
| <div className="popular-tickers"> | |
| <span className="popular-label">Popular:</span> | |
| {POPULAR.map((t) => ( | |
| <button key={t} className="ticker-pill" onClick={() => go(t)}> | |
| {t} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div | |
| className="feature-pills animate-fadeUp" | |
| style={{ animationDelay: '0.7s', opacity: 0, animationFillMode: 'forwards' }} | |
| > | |
| <div className="feature-pill">π Technical Charts</div> | |
| <div className="feature-pill">π’ Company Financials</div> | |
| <div className="feature-pill">π€ ML Prediction</div> | |
| <div className="feature-pill">π° News Sentiment</div> | |
| </div> | |
| <button | |
| className="btn-portfolio animate-fadeUp" | |
| style={{ animationDelay: '0.9s', opacity: 0, animationFillMode: 'forwards' }} | |
| onClick={onPortfolio} | |
| > | |
| π View My Portfolio | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function LoadingScreen({ ticker }) { | |
| useEffect(() => { | |
| anime({ | |
| targets: '.loading-dot', | |
| translateY: [-10, 0], | |
| delay: anime.stagger(160), | |
| loop: true, | |
| direction: 'alternate', | |
| easing: 'easeInOutSine', | |
| duration: 450, | |
| }) | |
| }, []) | |
| return ( | |
| <div className="loading-screen"> | |
| <div className="loading-content"> | |
| <div className="loading-dots"> | |
| <div className="loading-dot" /> | |
| <div className="loading-dot" /> | |
| <div className="loading-dot" /> | |
| </div> | |
| <p> | |
| Analysing <strong>{ticker}</strong>β¦ | |
| </p> | |
| <span>Fetching price data, financials, ML signals & news</span> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Error βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function ErrorScreen({ error, onBack }) { | |
| return ( | |
| <div className="loading-screen"> | |
| <div className="loading-content"> | |
| <div style={{ fontSize: '2.5rem' }}>β οΈ</div> | |
| <p>Couldn't find that ticker</p> | |
| <span>{error}</span> | |
| <button className="btn-primary" onClick={onBack} style={{ marginTop: '1.5rem' }}> | |
| Try another ticker | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function Dashboard({ ticker, onBack }) { | |
| const [data, setData] = useState(null) | |
| const [loading, setLoading] = useState(true) | |
| const [error, setError] = useState(null) | |
| const [deepMode, setDeepMode] = useState(false) | |
| useEffect(() => { | |
| setLoading(true) | |
| setError(null) | |
| setData(null) | |
| setDeepMode(false) | |
| fetch(`/quick/${ticker}`) | |
| .then((r) => { | |
| if (!r.ok) throw new Error(`Ticker "${ticker}" not found or API error`) | |
| return r.json() | |
| }) | |
| .then((d) => { | |
| setData(d) | |
| setLoading(false) | |
| }) | |
| .catch((e) => { | |
| setError(e.message) | |
| setLoading(false) | |
| }) | |
| }, [ticker]) | |
| if (loading) return <LoadingScreen ticker={ticker} /> | |
| if (error) return <ErrorScreen error={error} onBack={onBack} /> | |
| return ( | |
| <div className="dashboard"> | |
| {/* Header */} | |
| <div className="dashboard-header"> | |
| <button className="btn-back" onClick={onBack}> | |
| β Back | |
| </button> | |
| <div className="ticker-header"> | |
| <span className="ticker-name">{ticker}</span> | |
| {data.current_price > 0 && ( | |
| <span className="ticker-price">${data.current_price.toFixed(2)}</span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="dashboard-content"> | |
| <RecommendationHero data={data} /> | |
| <PriceChart data={data} /> | |
| <SignalCards breakdown={data.breakdown} /> | |
| {/* Forecast */} | |
| {data.forecast_upside !== undefined && data.forecast_upside !== 0 && ( | |
| <div className="card forecast-card"> | |
| <h3>π 30-Day Price Forecast</h3> | |
| <div className="forecast-content"> | |
| <div | |
| className={`forecast-badge ${data.forecast_upside > 0 ? 'up' : 'down'}`} | |
| > | |
| {data.forecast_upside > 0 ? 'β' : 'β'}{' '} | |
| {Math.abs(data.forecast_upside).toFixed(1)}% | |
| </div> | |
| <p className="forecast-text"> | |
| {data.forecast_upside > 0 | |
| ? `Our AI forecasts the price could rise roughly ${data.forecast_upside.toFixed(1)}% over the next 30 days β though forecasts are never guaranteed.` | |
| : `Our AI forecasts the price could dip roughly ${Math.abs(data.forecast_upside).toFixed(1)}% over the next 30 days β though forecasts can be wrong.`} | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Risk flags */} | |
| {data.risk_flags?.length > 0 && <RiskFlags flags={data.risk_flags} />} | |
| {/* Deep analysis */} | |
| <div className="card deep-analysis-card"> | |
| {!deepMode ? ( | |
| <div className="deep-cta"> | |
| <div> | |
| <h3>π Want the full picture?</h3> | |
| <p> | |
| Run 6 specialised AI agents to get a detailed investment report | |
| with step-by-step reasoning. | |
| </p> | |
| <span className="time-badge">β± Takes about 2β3 minutes</span> | |
| </div> | |
| <button className="btn-primary" onClick={() => setDeepMode(true)}> | |
| Run Deep Analysis | |
| </button> | |
| </div> | |
| ) : ( | |
| <AgentStream ticker={ticker} /> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // ββ Root App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export default function App() { | |
| const [ticker, setTicker] = useState(null) | |
| const [showPortfolio, setPortfolio] = useState(false) | |
| if (showPortfolio) | |
| return <PortfolioPage | |
| onAnalyse={(t) => { setPortfolio(false); setTicker(t) }} | |
| onBack={() => setPortfolio(false)} | |
| /> | |
| return ticker ? ( | |
| <Dashboard ticker={ticker} onBack={() => setTicker(null)} /> | |
| ) : ( | |
| <Landing onSearch={setTicker} onPortfolio={() => setPortfolio(true)} /> | |
| ) | |
| } | |