vjeai's picture
Deploy: all fixes β€” yfinance candles, ml_signal 2y history, no handoff schemas, sequential report phase
3be03dd
Raw
History Blame Contribute Delete
10.3 kB
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)} />
)
}